基于Nx从零搭建一个Vue3+Vite3的微前端项目
背景简介
随着项目不断的功能迭代,项目越来越臃肿,表现为在本地开发的时候,改了代码之后,至少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
方式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,提供可扩展的预配置设置, 减轻了配置负担,并提供脚手架支持和自动代码迁移功能。
选择创建apps, 因为需要支持在单一仓库下构建多个应用。
是否启用分布式缓存使您的 CI 更快,这一步选No, 项目不需要用到商用版的Nx Cloud服务。
Nx默认是使用npm包管理器,可以自己换成yarn/pnpm等,等待依赖安装完成,工作空间就创建好了,生成的工作空间目录如下:
项目有两种类型:应用程序和库。
├──/apps/ 应用程序项目,是可运行应用程序的入口点。
├──/libs/ 库项目,有许多不同类型的库,每个库实现自己的业务功能,库之间的边界保持清晰。
├──/tools/ 对代码库执行操作的脚本,比如数据库脚本、自定义执行器(或生成器)或工作区生成器。
├──/workspace.json 定义工作区中的每个项目以及可以在这些项目上运行的执行器。
├──/nx.json 添加有关项目的额外信息,包括手动定义的依赖项和可用于限制项目相互依赖的方式的标记。
├──/tsconfig.base.json 设置全局类型脚本设置,并为每个库创建别名,以帮助创建类型脚本导入。
有一个库要特别说明一下,shared库,各个应用之间可以共享的接口,静态资源,组件,配置,指令,路由,工具方法等都放在这个库下。
编写模板代码生成器脚本
目前项目中定制了四个模板代码生成器,分别是app应用程序,特性库,api接口,页面模板生成器。
这里以app应用模板为例,说明一下如何编写模板代码生成器。
先看协议配置文件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": {}
}
再说说模板文件,模板文件的内容很简单,就是固定的代码片段,有个细节需要说一下,模板文件传递动态参数的方法:原理就是字符串替换,定义一个常量标记字符串, 然后读取模板文件内容,查找常量标记所在位置,用实际变量进行替换。
- 定义的地方:
- 使用的地方
有一处无关紧要,说一下也无妨。模板代码生成器脚本文件引入了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
在apps下生成了app1应用入口
运行应用库生成命令
nx workspace-generator feature-generator
有一点要注意一下,如果生成应用库的时候不小心输入错了名称, 再次生成的话,要手动删除 apps\app1\project.json的隐式依赖implicitDependencies字段中的值。
配置项目与安装依赖
添加脚本命令
包含创建本地环境变量,本地运行,打包构建,全量以及增量的项目测试,代码格式检查,代码质量检查,页面和接口模板代码生成器等命令。
{
"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
运行效果:
构建应用测试
pnpm build:dev --project=app1
打包效果:
启动和打包一个应用说明不了问题,因为微前端的核心,就是将一个大应用拆分成若干个子应用,所以我们再创建一个应用程序和对应的库,并测试一下运行和打包构建。
nx workspace-generator app-generator
记得修改一下端口号,不要与第一个应用重复
创建好之后,要手动创建应用2的dev环境变量文件,并在vite.config.ts中添加一下启动时默认打开的页面
接着生成应用2的应用库
nx workspace-generator feature-generator feature-app2
再看看运行与打包,如下图所示,皆无问题。说明对应用的拆分是OK的, 那么拆分之后的应用,两个应用之间的页面如何跳转呢,也很简单,直接用
location.href='绝对路径'
就可以。
pnpm start:dev --project app2
pnpm build:dev --project app2
最后
鉴于篇幅缘故,其它配置文件,如构建配置vite.base.config.ts, 模板代码生成文件夹tools下的文件模板,代码格式化.prettierrc,样式校验配置.stylelintrc.js,代码提交格式校验commitlint.config.js, 代码提交钩子配置.husky, .vscode下的项目配置,ts配置tsconfig.base.json等就不铺开贴在文章里了,这个Nx+Vite3+Vue3的模板项目已经上传至码云,感兴趣的同学可以下载体验,查看配置内容与学习。
转载自:https://juejin.cn/post/7196439418876477495