Monorepo 项目实践
思路
项目管理
- Monorepo : 是一种管理项目的方式,即将多个项目放在一个仓库中,这样可以方便地管理项目之间的依赖关系,提高代码复用性,减少重复代码,提高开发效率。
依赖管理
- Pnpm : 可以加速
npm
安装依赖的速度,并减少磁盘空间占用。
微前端架构
- qiankun : 一种基于微前端架构的前端应用解决方案,它将一个大型前端应用拆分成多个独立的子应用,每个子应用独立开发、测试、部署,互不干扰,最终组装成一个整体的应用。
UI 组件库、工具类等共用
- npm : 由于
npm
包管理工具的存在,使得多个项目可以共用同一个包,不重复安装,提高开发效率。
业务组件共用
- 模块联邦(Module Federation) : 它允许不同的应用或组件之间进行动态的模块共享。
样式共用
- Ant Design Token : 可以将多个项目的样式变量集中管理,并通过工具生成对应的样式文件,实现样式的共享。
仓库地址
搭建项目
创建 lerna
项目
新建一个空文件夹
在根目录下执行
npx lerna init
, 初始化项目
开启 pnpm workspaces
lerna.json
中添加下面配置1
2
3
4
5{
"npmClient": "pnpm",
"packages": ["packages/*"],
... // 其他配置
}在根目录下创建
pnpm-workspace.yaml
并添加下面配置1
2packages:
- 'packages/*'然后在根目录下创建
packages
目录 (注意:名称要与上面配置的packages/*
目录名称一致)
进入 packages
创建主应用 app
依次执行下面命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14# 进入 packages 目录
cd ./packages
# 创建主应用 app
npm create vite@latest
# Project name
app
# Select a framework
React
# Select a variant
TypeScript + SWC在
packages/app/vite.config.ts
文件中配置端口号1
2
3
4
5
6export default defineConfig({
server: {
port: 8000,
},
... // 其他配置
});
进入 packages
创建子应用 demo1
依次执行下面命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14# 进入 packages 目录
cd ./packages
# 创建子应用 demo1
npm create vite@latest
# Project name
demo1
# Select a framework
React
# Select a variant
TypeScript + SWC在
packages/app/vite.config.ts
文件中配置端口号1
2
3
4
5
6
7export default defineConfig({
server: {
port: 8001,
},
... // 其他配置
});
进入 packages
创建公共包 sdk
这里每个文件夹通常是封装成单独的 npm 包, 这里写在一起是为了方便演示
依次执行下面命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17# 进入 packages 目录
cd ./packages
# 创建公共包 sdk
npx create-father sdk
# Pick target platform(s)
Browser
# Pick NPM client
npm
# nput NPM package name
sdk
# Input NPM package description
sdk配置
packages/sdk/tsconfig.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21{
"compilerOptions": {
"resolveJsonModule": true, // 允许 import 命令导入 JSON 文件
"esModuleInterop": true, // 修复了一些 CommonJS 和 ES6 模块之间的兼容性问题
"moduleResolution": "node", // 确定模块路径的算法,即如何查找模块。采用 Node.js 的 CommonJS 模块算法
"jsx": "react-jsx", // 设置如何处理.tsx文件。它一般以下三个值
"module": "commonjs", // 指定编译产物的模块格式。它的默认值与target属性有关,如果target是ES3或ES5,它的默认值是commonjs,否则就是ES6/ES2015
"target": "es5", // 指定编译出来的 JavaScript 代码的 ECMAScript 版本,比如es2021,默认是es3
"allowJs": false, // 指定源目录的 JavaScript 文件是否原样拷贝到编译后的目录
"noUnusedLocals": false, // 设置是否允许未使用的局部变量
"preserveConstEnums": true, // 将const enum结构保留下来,不替换成常量值
"skipLibCheck": true, // 跳过所有的声明文件(.d.ts)的类型检查
"sourceMap": true, // 设置编译时是否生成 SourceMap 文件
"inlineSources": true, // 设置将原始的.ts代码嵌入编译后的 JS 中
"declaration": true, // 设置编译时是否为每个脚本生成类型声明文件.d.ts
"experimentalDecorators": true, // 启用实验性的ES装饰器
"downlevelIteration": true, // 编译器将迭代器转换为ECMAScript 3兼容代码
"baseUrl": "./", // 指定 TypeScript 项目的基准目录
"lib": ["ESNext"] // 描述项目需要加载的 TypeScript 内置类型描述文件,跟三斜线指令/// <reference lib="" />作用相同
}
}配置
packages/sdk/.fatherrc.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23import { defineConfig } from 'father';
// 文档:https://github.com/umijs/father/blob/master/docs/config.md
export default defineConfig({
esm: {
// 将源码转换为 CommonJS 产物
output: 'esm', // 输出目录
},
cjs: {
// 将源码转换为 CommonJS 产物
output: 'cjs', // 输出目录
},
umd: {
// 将源码打包为 UMD 产物
output: 'lib', // 输出目录
sourcemap: true, // 为 JavaScript 构建产物生成 sourcemap 文件
externals: {
// 源码打包过程中需要处理的外部依赖
react: 'React',
'react-dom': 'ReactDOM',
},
},
});配置
packages/sdk/package.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31{
"name": "@zxiaosi/sdk",
"version": "0.0.1",
"description": "",
"keywords": [],
"authors": [],
"main": "lib",
"module": "esm",
"types": "esm/index.d.ts",
"files": ["lib", "esm", "cjs"],
"exports": {
".": {
"import": "./esm/index.js",
"types": "./esm/index.d.ts"
}
},
"scripts": {
"dev": "father dev",
"build": "father build",
"build:deps": "father prebundle",
"prepublishOnly": "father doctor && npm run build"
},
"license": "MIT",
"publishConfig": {
"access": "public"
},
"devDependencies": {
"father": "^4.5.1"
},
"dependencies": {}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22"name": 包名 (必填)
"version": 版本号 (必填)
"description": 描述
"keywords": 关键字
"authors": 作者
"homepage": 主页
"repository": 仓库地址
"main": 入口文件
"module": ES Module 入口文件
"types": 类型文件
"files": 需要发布的文件
"exports": 导出的文件
"scripts": 脚本
"license": 许可证
"publishConfig": 发布配置
"devDependencies": 开发依赖
"dependencies": 依赖
在 packages/app/package.json
、packages/demo1/package.json
中引入 sdk
包
1 | { |
最后在根目录下执行 pnpm install
安装依赖
项目启动顺序
先启动
sdk
包, 进入packages/sdk
目录执行pnpm run dev
再启动
demo1
子应用, 进入packages/demo1
目录执行pnpm run dev
最后启动
app
主应用, 进入packages/app
目录执行pnpm run dev
必须先启动
sdk
包,因为demo1
子应用和app
主应用都依赖sdk
包。(通常情况下,sdk
包是在其他地方统一维护的npm
包)
在 sdk
中配置公用包
项目目录
1 | |--- cjs # CommonJS |
配置 components
包 (antd)
在
sdk
中安装antd
包1
2
3
4
5# 进入 sdk 目录
cd ./packages/sdk
# 安装 antd
pnpm add antd在
packages/sdk/src/components/index.ts
导出antd
组件1
export * from 'antd';
在 ``packages/sdk/package.json
导出
components` 组件1
2
3
4
5
6
7
8
9
10{
... // 其他配置
"exports": {
... // 其他配置
"./components": {
"import": "./esm/components/index.js",
"types": "./esm/components/index.d.ts"
}
}
}配置按需加载
antd
组件安装
babel-plugin-import
插件1
2
3
4
5# 进入 sdk 目录
cd ./packages/sdk
# 安装 babel-plugin-import
pnpm add babel-plugin-import在
packages/sdk/.fatherrc.ts
中配置babel-plugin-import
1
2
3
4
5
6
7
8
9import { defineConfig } from 'father';
// 文档:https://github.com/umijs/father/blob/master/docs/config.md
export default defineConfig({
// ... 其他配置
extraBabelPlugins: [
['babel-plugin-import', { libraryName: 'antd', style: true }, 'antd'],
],
});
最后在
app
或者demo1
中使用sdk
包的components
组件1
2
3
4
5import { Button } from '@zxiaosi/sdk/components';
export default function App() {
return <Button type="primary">Button</Button>;
}
配置 icons
包 (@ant-design/icons)
在
sdk
中安装@ant-design/icons
包1
2
3
4
5# 进入 sdk 目录
cd ./packages/sdk
# 安装 @ant-design/icons
pnpm add @ant-design/icons在
packages/sdk/src/icons/index.ts
导出@ant-design/icons
组件1
export * from '@ant-design/icons';
在 ``packages/sdk/package.json
导出
icons` 组件1
2
3
4
5
6
7
8
9
10{
... // 其他配置
"exports": {
... // 其他配置
"./icons": {
"import": "./esm/icons/index.js",
"types": "./esm/icons/index.d.ts"
}
}
}配置按需加载
@ant-design/icons
组件安装
babel-plugin-import
插件 (如果已经安装过,可以跳过)1
2
3
4
5# 进入 sdk 目录
cd ./packages/sdk
# 安装 babel-plugin-import
pnpm add babel-plugin-import在
packages/sdk/.fatherrc.ts
中配置babel-plugin-import
1
2
3
4
5
6
7
8
9
10
11
12
13
14import { defineConfig } from 'father';
// 文档:https://github.com/umijs/father/blob/master/docs/config.md
export default defineConfig({
// ... 其他配置
extraBabelPlugins: [
// ... 其他配置
[
'babel-plugin-import',
{ libraryName: '@ant-design/icons', style: true },
'@ant-design/icons',
],
],
});
最后在
app
或者demo1
中引入sdk
包的icons
组件1
2
3
4
5import { UserOutlined } from '@zxiaosi/sdk/icons';
export default function App() {
return <UserOutlined />;
}
配置 pro-components
包 (@ant-design/pro-components)
在
sdk
中安装@ant-design/pro-components
包1
2
3
4
5# 进入 sdk 目录
cd ./packages/sdk
# 安装 @ant-design/pro-components
pnpm add @ant-design/pro-components在
packages/sdk/src/pro-components/index.ts
导出@ant-design/pro-components
组件1
export * from '@ant-design/pro-components';
在 ``packages/sdk/package.json
导出
pro-components` 组件1
2
3
4
5
6
7
8
9
10{
... // 其他配置
"exports": {
... // 其他配置
"./pro-components": {
"import": "./esm/pro-components/index.js",
"types": "./esm/pro-components/index.d.ts"
}
}
}配置按需加载
@ant-design/pro-components
组件安装
babel-plugin-import
插件 (如果已经安装过,可以跳过)1
2
3
4
5# 进入 sdk 目录
cd ./packages/sdk
# 安装 babel-plugin-import
pnpm add babel-plugin-import在
packages/sdk/.fatherrc.ts
中配置babel-plugin-import
1
2
3
4
5
6
7
8
9
10
11
12
13
14import { defineConfig } from 'father';
// 文档:https://github.com/umijs/father/blob/master/docs/config.md
export default defineConfig({
// ... 其他配置
extraBabelPlugins: [
// ... 其他配置
[
'babel-plugin-import',
{ libraryName: '@ant-design/pro-components', style: true },
'@ant-design/pro-components',
],
],
});
最后在
app
或者demo1
中引入sdk
包的pro-components
组件1
2
3
4
5import { ProCard } from '@zxiaosi/sdk/pro-components';
export default function App() {
return <ProCard>123</ProCard>;
}