likes
comments
collection
share

基于Nx从零搭建一个Vue3+Vite3的微前端项目

作者站长头像
站长
· 阅读数 37

背景简介

随着项目不断的功能迭代,项目越来越臃肿,表现为在本地开发的时候,改了代码之后,至少1分钟以上,页面才会刷新显示出最新的修改效果; 另外编译打包的时候,比较耗时,需要十来分钟,而且这两种场景下,还动不动内存溢出,编译器直接卡死,所以亟待寻找技术方案,提升开发效率。网上搜寻一番之后,最终选择了基于Monorepo的Nx框架微前端方案, 因为Nx可以很轻松的将代码拆分成单独的项目,每个项目可以独立开发运行,独立打包编译,独立部署。

代码托管策略Monorepo是所有的项目都使用一个代码库管理,相较于 Mutlirepo的优点:

  • 不用频繁地切换仓库,也不需要繁琐的配置
  • 代码复用变得容易,内部模块之间可以彼此相互引用
  • 每个模块可以独立开发、降低了构建时间
  • 避免重复安装依赖包

Nx介绍

前Google的Angular团队成员Jeff Cross和Victor Savkin创办了一家顾问公司Nrwl, Nx是Nrwl这家顾问公司推出的一款工具,致力于研发新一代智能化、快速,可扩展的Monorepo构建框架。

  • 支持前端三大框架:Vue、React、Angular
  • 支持后端框架:Express、Nest、Next
  • 支持各类型测试:Cypress、Jest
  • 支持Prettier,TypeScript等工具
  • 为Monorepo框架,可以构建全栈应用程序
  • 使用Google,Facebook和Microsoft开创的有效开发实践

Nx设计理念

Nx与VSCode有着相同的简约设计哲学,通过扩展插件增强功能,但扩展插件不包括在核心安装包中,在VSCode之中,即便不下载安装任何扩展插件,也有相当好的使用体验。VSCode的扩展生态系统, 则使开发更高效。Nx也是相同的,Nx核心提供了通用功能,通过外挂插件的方式(如下图所示),丰富和延伸自身功能, 适应多种使用场景。Nx插件对许多项目非常有用,但完全是可选的。

基于Nx从零搭建一个Vue3+Vite3的微前端项目

安装Nx

方式1. 用npm全局安装nx

npm install nx -g

方式2. 用yarn全局安装nx

yarn global add nx

创建一个Nx Monorepo初始项目

npx create-nx-workspace@latest nx-micro-fe

选择创建一个集成monorepo项目, Integrated monorepo相较于Package-based monorepo,提供可扩展的预配置设置, 减轻了配置负担,并提供脚手架支持和自动代码迁移功能。 基于Nx从零搭建一个Vue3+Vite3的微前端项目 选择创建apps, 因为需要支持在单一仓库下构建多个应用。 基于Nx从零搭建一个Vue3+Vite3的微前端项目 是否启用分布式缓存使您的 CI 更快,这一步选No, 项目不需要用到商用版的Nx Cloud服务。 基于Nx从零搭建一个Vue3+Vite3的微前端项目 Nx默认是使用npm包管理器,可以自己换成yarn/pnpm等,等待依赖安装完成,工作空间就创建好了,生成的工作空间目录如下:

基于Nx从零搭建一个Vue3+Vite3的微前端项目

项目有两种类型:应用程序和库。

├──/apps/ 应用程序项目,是可运行应用程序的入口点。
├──/libs/ 库项目,有许多不同类型的库,每个库实现自己的业务功能,库之间的边界保持清晰。
├──/tools/ 对代码库执行操作的脚本,比如数据库脚本、自定义执行器(或生成器)或工作区生成器。
├──/workspace.json 定义工作区中的每个项目以及可以在这些项目上运行的执行器。
├──/nx.json 添加有关项目的额外信息,包括手动定义的依赖项和可用于限制项目相互依赖的方式的标记。
├──/tsconfig.base.json 设置全局类型脚本设置,并为每个库创建别名,以帮助创建类型脚本导入。

有一个库要特别说明一下,shared库,各个应用之间可以共享的接口,静态资源,组件,配置,指令,路由,工具方法等都放在这个库下。

基于Nx从零搭建一个Vue3+Vite3的微前端项目

编写模板代码生成器脚本

目前项目中定制了四个模板代码生成器,分别是app应用程序,特性库,api接口,页面模板生成器。

基于Nx从零搭建一个Vue3+Vite3的微前端项目

这里以app应用模板为例,说明一下如何编写模板代码生成器。 基于Nx从零搭建一个Vue3+Vite3的微前端项目

先看协议配置文件schema.json的内容, 生成器的名称是app-generator,从命令行接收两个参数,一个是应用名称,一个是应用启动端口号,默认是5000,其中应用名称是必填参数。

{
  "$schema": "http://json-schema.org/schema",
  "cli": "nx",
  "$id": "app-generator",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "Application name",
      "$default": {
        "$source": "argv",
        "index": 0
      },
      "x-prompt": "请输入 Application name:"
    },
    "port": {
      "type": "string",
      "description": "Application port",
      "default": "5000",
      "x-prompt": "请输入 Application port:"
    }
  },
  "required": ["name"]
}

接着看模板代码生成器脚本文件 index.ts,调用@nrwl/devkit工具包提供的方法,获取命令行输入的应用名称、端口和项目的根目录,判断应用是否已经创建,如果未创建,将tools\generators\app-generator\files目录下所有的模板文件,写入到项目根目录apps下命令行输入的应用名称下。接着创建应用对应的测试文件夹, 向工作空间配置写入应用配置信息。最后对项目中的所有文件进行代码格式化。

import {
  Tree,
  formatFiles,
  installPackagesTask,
  generateFiles,
  joinPathFragments,
  readProjectConfiguration,
  getWorkspaceLayout,
  getWorkspacePath,
  getProjects,
  readWorkspaceConfiguration,
  updateWorkspaceConfiguration,
  updateJson,
  names
} from '@nrwl/devkit';

import { clc } from '../../scripts/help';

export default async function (tree: Tree, schema: any) {
  const { name, port } = schema;
  const { appsDir } = getWorkspaceLayout(tree);
  const projects = getProjects(tree);

  if (projects.get(name)) {
    console.log(clc.red(`${appsDir} 下已经存在 ${name},请查明原因后再进行操作`));
    process.exit(1);
  }

  // 将 app 模板文件写入指定目录
  const projectRoot = joinPathFragments(appsDir, name);
  generateFiles(
    tree,
    joinPathFragments(__dirname, './files'),
    projectRoot,
    {
      tmpl: '',
      ...schema
    }
  );

  // 将 app-e2e 模板文件写入指定目录
  const projectE2eRoot = joinPathFragments(appsDir, name + '-e2e');
  generateFiles(
    tree,
    joinPathFragments(__dirname, './e2e-files'),
    projectE2eRoot,
    {
      tmpl: '',
      ...schema
    }
  );

  // 更新 workspace.json
  updateJson(tree, getWorkspacePath(tree), (jsonData) => {
    jsonData.projects = jsonData.projects ?? {};
    jsonData.projects[name] = `${appsDir}/${name}`;
    jsonData.projects[name + '-e2e'] = `${appsDir}/${name}-e2e`;
    return jsonData;
  });

  // 格式化代码
  await formatFiles(tree);

  return () => {
    console.log(clc.magentaBright(`

    app ${name} 已安装成功,接下来请安装一个 feature lib:

    nx workspace-generator feature-generator <feature-lib-name>

    `));
  };
}

从上面的代码逻辑可以看出,getWorkspacePath(tree)方法会寻找项目根目录下的workspace.json文件,我们手动创建一个空的工作空间配置文件。workspace.json的内容如下:

{
  "version": 1,
  "projects": {}
}

再说说模板文件,模板文件的内容很简单,就是固定的代码片段,有个细节需要说一下,模板文件传递动态参数的方法:原理就是字符串替换,定义一个常量标记字符串, 然后读取模板文件内容,查找常量标记所在位置,用实际变量进行替换。

  • 定义的地方: 基于Nx从零搭建一个Vue3+Vite3的微前端项目
  • 使用的地方 基于Nx从零搭建一个Vue3+Vite3的微前端项目

有一处无关紧要,说一下也无妨。模板代码生成器脚本文件引入了scripts/help.ts 文件中的cls方法,定义如下

// log颜色设置
export const clc = {
  green: (text) => `\x1B[32m${text}\x1B[39m`,
  yellow: (text) => `\x1B[33m${text}\x1B[39m`,
  red: (text) => `\x1B[31m${text}\x1B[39m`,
  magentaBright: (text) => `\x1B[95m${text}\x1B[39m`,
  cyanBright: (text) => `\x1B[96m${text}\x1B[39m`,
};

// ...

tools/tsconfig.tools.json用到了@types/node包,安装一下

yarn add @types/node -D

运行模板代码生成器

运行应用程序生成命令

nx workspace-generator app-generator app1

基于Nx从零搭建一个Vue3+Vite3的微前端项目 在apps下生成了app1应用入口

基于Nx从零搭建一个Vue3+Vite3的微前端项目

运行应用库生成命令

nx workspace-generator feature-generator

基于Nx从零搭建一个Vue3+Vite3的微前端项目

基于Nx从零搭建一个Vue3+Vite3的微前端项目

有一点要注意一下,如果生成应用库的时候不小心输入错了名称, 再次生成的话,要手动删除 apps\app1\project.json的隐式依赖implicitDependencies字段中的值。

基于Nx从零搭建一个Vue3+Vite3的微前端项目

配置项目与安装依赖

添加脚本命令

包含创建本地环境变量,本地运行,打包构建,全量以及增量的项目测试,代码格式检查,代码质量检查,页面和接口模板代码生成器等命令。

{
  "name": "nx-micro-fe",
  "version": "0.0.0",
  "license": "MIT",
  "private": true,
  "scripts": {
    "gen:env": "node ./tools/scripts/generate-env-local-file.js",
    "start:dev": "nx serve --mode dev",
    "build:dev": "nx build --mode dev",
    "test": "nx --target=test run-many --all",
    "e2e": "nx --target=e2e run-many --all",
    "lint": "nx --target=lint run-many --all",
    "lint:fix": "nx --target=lint --fix run-many --all",
    "format:check": "nx format:check",
    "format:fix": "nx format:write",
    "affected:test": "nx affected:test",
    "affected:lint": "nx affected:lint",
    "affected:lint:fix": "nx affected:lint --fix",
    "affected:format:check": "nx affected --target=format:check",
    "affected:format:fix": "nx affected --target=format:write",
    "affected:e2e": "nx affected:e2e",
    "postinstall": "node node_modules/@nx-plus/vite/patch-nx-dep-graph.js",
    "add:api": "nx workspace-generator api-generator",
    "add:page": "nx workspace-generator page-generator"
  },
}

添加运行依赖包

主要包括UI库,vue工具集,富文本编辑器,网络请求工具,粘贴板复制工具,加密工具,日期工具,图表工具,状态管理工具,表情工具,js工具库,进度条工具,node进程管理工具,cookie操作工具,环境变量工具等。

  "dependencies": {
    "@ant-design/icons-vue": "6.1.0",
    "@tinymce/tinymce-vue": "5.0.0",
    "@vue/reactivity": "^3.2.45",
    "@vueuse/core": "8.9.2",
    "@vueuse/integrations": "8.9.2",
    "@wangeditor/editor-for-vue": "5.1.12",
    "ant-design-vue": "3.2.10",
    "axios": "0.26.1",
    "copy-to-clipboard": "3.3.1",
    "crypto-js": "4.1.1",
    "dayjs": "^1.10.5",
    "echarts": "5.4.0",
    "emoji-mart-vue-fast": "10.2.2",
    "fetch-jsonp": "1.2.1",
    "js-sha1": "0.6.0",
    "lodash-es": "4.17.21",
    "nprogress": "1.0.0-1",
    "pinia": "2.0.16",
    "process": "^0.11.10",
    "tinymce": "6.1.0",
    "universal-cookie": "4.0.4",
    "vite-plugin-nx-dotenv": "1.0.1",
    "vue": "3.2.37",
    "vue-request": "1.2.4",
    "vue-router": "4.1.2"
  },

添加开发依赖包

开发时依赖包主要包括nx工具集, 一系列工具库类型定义,构建工具vite系列工具,代码质量检查eslint系列工具,vue系列工具,样式校验stylelint系列工具,环境变量管理工具dotenv,测试工具cypress, git提交代码钩子husky,代码美化工具prettier, 日志工具consola, 样式及样式处理系列工具等。

   "devDependencies": {
    "@commitlint/cli": "17.0.2",
    "@commitlint/config-conventional": "17.0.2",
    "@nrwl/cli": "15.3.0",
    "@nrwl/cypress": "15.3.0",
    "@nrwl/devkit": "15.3.0",
    "@nrwl/eslint-plugin-nx": "15.3.0",
    "@nrwl/jest": "15.3.0",
    "@nrwl/linter": "15.3.0",
    "@nrwl/workspace": "15.3.0",
    "@nx-plus/vite": "14.1.0",
    "@nx-plus/vue": "14.1.0",
    "@phenomnomnominal/tsquery": "4.2.0",
    "@types/crypto-js": "4.1.1",
    "@types/jest": "27.0.2",
    "@types/jsdom": "16.2.14",
    "@types/lodash-es": "4.17.6",
    "@types/node": "^18.11.18",
    "@types/nprogress": "^0.2.0",
    "@typescript-eslint/eslint-plugin": "5.18.0",
    "@typescript-eslint/parser": "5.18.0",
    "@vitejs/plugin-legacy": "2.3.1",
    "@vitejs/plugin-vue": "3.2.0",
    "@vitejs/plugin-vue-jsx": "2.1.1",
    "@vue/cli-plugin-typescript": "4.5.0",
    "@vue/cli-service": "4.5.0",
    "@vue/eslint-config-prettier": "7.0.0",
    "@vue/eslint-config-typescript": "10.0.0",
    "@vue/test-utils": "2.0.0-rc.20",
    "consola": "2.15.3",
    "cypress": "9.1.0",
    "dotenv": "16.0.0",
    "eslint": "8.12.0",
    "eslint-config-prettier": "8.1.0",
    "eslint-plugin-cypress": "2.10.3",
    "eslint-plugin-prettier": "3.1.3",
    "eslint-plugin-vue": "7.20.0",
    "husky": "8.0.1",
    "inquirer": "8.0.0",
    "jest": "27.2.3",
    "jest-serializer-vue": "2.0.2",
    "jest-transform-stub": "2.0.0",
    "less": "4.1.2",
    "nx": "15.6.3",
    "postcss-html": "1.3.0",
    "postcss-less": "6.0.0",
    "prettier": "^2.6.2",
    "stylelint": "14.8.5",
    "stylelint-config-recess-order": "3.0.0",
    "stylelint-config-recommended-vue": "1.4.0",
    "stylelint-config-standard": "25.0.0",
    "stylelint-order": "5.0.0",
    "ts-jest": "27.0.5",
    "typescript": "~4.8.2",
    "vite": "3.2.4",
    "vite-plugin-compression": "0.5.1",
    "vite-plugin-html": "3.2.0",
    "vite-plugin-style-import": "2.0.0",
    "vite-plugin-svg-icons": "2.0.1",
    "vite-plugin-vue-setup-extend": "0.4.0",
    "vite-plugin-windicss": "1.8.4",
    "vue3-jest": "27.0.0-alpha.1",
    "windicss": "3.5.1"
  }

安装依赖

用yarn安装半天老是报网络错误, 果断弃暗投明,删除了项目中的yarn.lock文件,换成了pnpm,马到功成。这里顺便说一下pnpm install和pnpm add安装包指令的区别,pnpm install是地毯覆盖式的安装package.json中的依赖包,pnpm是精准安装单个依赖包。

pnpm install 

启动与构建测试

启动应用测试

pnpm start:dev --project app1

基于Nx从零搭建一个Vue3+Vite3的微前端项目 运行效果:

基于Nx从零搭建一个Vue3+Vite3的微前端项目

构建应用测试

pnpm build:dev --project=app1

打包效果: 基于Nx从零搭建一个Vue3+Vite3的微前端项目

启动和打包一个应用说明不了问题,因为微前端的核心,就是将一个大应用拆分成若干个子应用,所以我们再创建一个应用程序和对应的库,并测试一下运行和打包构建。

nx workspace-generator app-generator

记得修改一下端口号,不要与第一个应用重复 基于Nx从零搭建一个Vue3+Vite3的微前端项目 创建好之后,要手动创建应用2的dev环境变量文件,并在vite.config.ts中添加一下启动时默认打开的页面 基于Nx从零搭建一个Vue3+Vite3的微前端项目 接着生成应用2的应用库

nx workspace-generator feature-generator feature-app2

基于Nx从零搭建一个Vue3+Vite3的微前端项目 再看看运行与打包,如下图所示,皆无问题。说明对应用的拆分是OK的, 那么拆分之后的应用,两个应用之间的页面如何跳转呢,也很简单,直接用location.href='绝对路径'就可以。

pnpm start:dev --project app2

基于Nx从零搭建一个Vue3+Vite3的微前端项目

pnpm build:dev --project app2

基于Nx从零搭建一个Vue3+Vite3的微前端项目

最后

鉴于篇幅缘故,其它配置文件,如构建配置vite.base.config.ts, 模板代码生成文件夹tools下的文件模板,代码格式化.prettierrc,样式校验配置.stylelintrc.js,代码提交格式校验commitlint.config.js, 代码提交钩子配置.husky, .vscode下的项目配置,ts配置tsconfig.base.json等就不铺开贴在文章里了,这个Nx+Vite3+Vue3的模板项目已经上传至码云,感兴趣的同学可以下载体验,查看配置内容与学习。