我们用 dumi 做演示,脚手架搭建跟着官网教程走就行了
https://d.umijs.org/guide
先提一嘴,这里用的 father4,不是 father build(father2),用 father2 打包没啥问题,如果是 father2 迁移 father4 的话有一些需要注意的地方,这个我后面会再写一篇文章来讲
配置pnpm
官网:
https://pnpm.io/zh/
如果全局没有 pnpm,需要先安装
npm install -g pnpm
如果刚刚执行过 npm install 了,或者用过 yarn 了,就把 node_modules 删了,然后执行
pnpm install
安装完成后可能会弹出类似于下方截图的内容
typescript 默认是没有装的可以装一下
在 package.json 中增加下面的内容
"pnpm": {
"peerDependencyRules": {
"ignoreMissing": [
"react",
"react-dom",
"antd",
"dva",
"postcss",
"webpack",
"eslint",
"stylelint",
"redux",
"@babel/core"
]
}
},
增加 .npmrc 文件,增加下面的内容
registry=https://registry.npmjs.org/
strict-peer-dependencies=false
我们使用 lerna 主要是用于管理包依赖,用了的话与 dumi 默认放组件的位置会有所不同,dumi 放在 src 文件夹中,但是 monorepo 模式下通常会放在 packages 中,这个文件夹默认没有,暂时不管。因为组件目录变了,pnpm 需要额外配置一些内容,在组件库根目录下创建 pnpm-workspace.yaml 文件,配置内容参考
packages:
# all packages in direct subdirs of packages/
- 'packages/*'
# all packages in subdirs of components/
- 'components/**'
# exclude packages that are inside test directories
- '!**/test/**'
主要是 packages,另外两个如果暂时没用到不配置也行
配置lerna
官网:
https://lerna.js.org/docs/getting-started
如果没有安装 lerna ,需要全局安装
npm install lerna@5.6.2 -g
为了避免一些问题,这里暂时安装5.6.2的版本,最新的已经是6.x的版本了,暂时不管,继续往下执行
安装完成之后执行
lerna init
这个命令会帮助我们创建 package.json 以及 lerna.json ,创建完成之后 package.json 中被添加了这样一段内容
"workspaces": [
"packages/*"
]
因为我们用 pnpm,用到了 pnpm-workspace.yaml,所以这一段用不到了,要删掉
我们继续用 lerna 帮我们创建几个组件包
lerna create @mino/tag packages/tag --yes
lerna create @mino/button packages/button --yes
lerna 会帮我们创建两个组件包,这里演示的话只留 package.json 就行了,其余的可以删掉
然后我们在两个组件目录下创建 src 目录,在下面创建 index.ts 文件,写一点内容
/src/index.tsx
import React from 'react';
const index: React.FC = () => {
return (
<div>this is button</div>
)
}
export default index;
会报错,显示没有安装 react
在根目录和组件目录下的 package.json 中都配一下 react 的依赖
"dependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
然后再执行一次 pnpm install
装完之后就不会报错了,然后同样的内容在 tag 的组件目录下也写一份
最后,在根目录的 package.json 下配置组件私有(避免误发布)
"private": true,
配置turbo
官网:
https://turbo.build/repo/docs
安装 npm install turbo --global
然后添加 turbo.json 到根目录
{
"$schema": "https://turborepo.org/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
}
}
}
然后到根目录下的 package.json ,配置下面的内容
"packageManager": "pnpm@7.27.1" // 这里写你安装的 pnpm 版本
配置father
地址:
https://github.com/umijs/father
执行命令 pnpm add father -w 或者 pnpm install father -w
装的是 father4,装2的话可能会遇到一些问题,后面单独出文章讲一下(迁移可能确实会遇到需要装2的情况,等 father4 支持 umd 打包文件名会好很多)
在每一个组件目录的 package.json 中配置内容
"scripts": {
"build": "father build"
},
然后每个组件目录下面都要配置 .father.ts ,可以参考 umi 的方法,在根目录写一个 .fatherrc.base.ts,然后组件目录下用 extends 加载配置(仅限father4)
//.fatherrc.base.ts
import { defineConfig } from 'father';
export default defineConfig({
cjs: {
output: 'lib',
},
esm: {
output: 'es',
},
umd: {
output: 'dist'
}
});
// packages/button/.fatherrc.ts
import { defineConfig } from 'father';
export default defineConfig({
extends: '../../.fatherrc.base.ts',
});
关于三种类型的打包输出文件,这里就是按需配置,笔者这边有遇到三种类型都要的场景。关于三种类型在什么情况下使用,可以参考
https://github.com/umijs/father/blob/master/docs/guide/build-mode.md
https://github.com/umijs/father/blob/master/docs/guide/umd.md#%E5%A6%82%E4%BD%95%E9%80%89%E6%8B%A9
umd 类型可能还需要配置 runtimeHelpers,但是 father4 改为约定式了,需要安装 @babel/runtime
pnpm add @babel/runtime -w
要注意在需要用到的组件的 packages.json 中配置上
开始打包
进行了上面的步骤之后就可以开始打包了,如果遇到问题欢迎在评论区留言
直接执行
pnpm turbo build
// 或者 npx turbo build
turbo 打包就是快,因为 turbo 是同时打包多个组件的。
可以看到,我们需要的打包类型全部都成功输出了
小总结
从头到尾没有太多的解释说明,只讲了步骤,主要这四个玩意内容量基本都是能独占一篇文章的,展开讲暂时没有那么多时间,这里简单说明一下。
pnpm 是包管理工具,用它的原因就是速度比 npm 和 yarn 快很多,pnpm 的 node_modules 格式也相比前两个有很大的不同,当然快的主要原因是 pnpm 用软硬连接解决重复依赖的问题,而前两者主要靠 copy
lerna 在这里主要是用来管理包依赖的问题,实际上处理 monorepo 工作的是 turbo ,并不是 lerna。而 turbo 强力的 cache 功能我们并没有使用,因此在这里 turbo 快主要在于它同时对多个组件同时进行打包。
father 是实际处理打包任务的那个,在 father 2 的时候其实内置了 monorepo 方案,在升级到v4之后不再内置,也因此我们要自己提供,这也是我们用到 turbo 的原因之一
更进一步提升打包速度
turbo 有着很强大的功能 remote caching,这个笔者暂时没有体验过,也暂时没有机会体验,后面有机会肯定会尝试一下。
那么在不使用 remote caching 的情况下我们要如何提升速度呢?我们这里没有挂载 git ,可能看不太出来。当我们挂载 git 仓库的时候就能发现,我们用 turbo 帮我们跑打包任务的时候,有些组件的代码并没有发生变更,但是 turbo 依旧帮我们用 father 重新打包了组件,这个其实就浪费了时间。
在 turbo 官网中,我们可以查询到一些相关的内容
简单来说就是 turbo 能够帮我们监听 git 变更,来帮我们只打包发生变更的文件。那么 turbo 既然有解决方案为什么我还要单独拉出来讲呢,因为我发现不太好用。不知道是不是环境配置原因,我在实际使用中发现这个功能有时候并没有生效,有时候会有延迟,有时候 turbo 甚至根本就拿不到最新的变更。各位可以直接使用 turbo 的提供的这个功能试试,在了解真正的原因之前我自己写了一个根据 git 变更打包的脚本,借助 turbo 的 filter来实现。
首先需要在根目录的 package.json 中添加执行脚本
"scripts": {
"start": "npm run dev",
"dev": "dumi dev",
"build": "dumi build && node scripts/build.js",
"prepare": "husky install && dumi setup"
},
主要看 build,我们增加执行我们自己的脚本,然后我们新增这个 build.js 脚本
// build.js
const { spawn, exec } = require('child_process');
const execg = spawn('git', ['log', '-2']);
const commitArray = [];
execg.stdout.on('data', (data) => {
const commitContent = Buffer.from(data).toString();
const commitStrList = commitContent.split('commit');
const commit = commitStrList[1].split('Author')[0].trim().split('\n')[0];
commitArray.push(commit)
if (commitArray.length === 2) {
const childExec = spawn('git', ['diff', commitArray[0], commitArray[1]]);
childExec.stdout.on('data', (data) => {
const diffContent = Buffer.from(data).toString();
getChangedFiles(diffContent);
});
childExec.stderr.on('data', (data) => {
process.stderr.write(`stderr: ${data}`);
});
childExec.on('close', () => {
buildChangeFiles();
});
}
});
execg.stderr.on('data', (data) => {
process.stderr.write(data);
});
const changeFiles = new Set();
// 获取变更的文件
const getChangedFiles = (content) => {
const catchReg = /--- a\/packages\/[a-z\-]*/g;
let res;
while (res = catchReg.exec(content)) {
changeFiles.add(res[0].split('a/packages/')[1]);
}
}
// 打包变更文件
const buildChangeFiles = () => {
const fileList = [...changeFiles];
console.log('变更组件列表: ', fileList);
if (fileList.length !== 0) {
const paramsList = fileList.map(item => `--filter=@--/${item}`);
paramsList.unshift('turbo', 'build');
console.log('paramsList: ', paramsList);
console.log("开始打包变更文件")
// spawn('pnpm', ['--version']);
const buildOut = exec(`pnpm ${paramsList.join(' ')}`);
buildOut.stdout.on('data', function (data) {
process.stdout.write(data);
});
buildOut.stderr.on('data', function (data) {
process.stderr.write(data);
});
}
}
概括一下上面的代码,我们通过脚本执行一些 git 命令,来获取最近两次的git 提交信息
const execg = spawn('git', ['log', '-2']);
然后解析这些信息,获取到两次 commit 的 hash,在用这两个 hash 执行 git 命令获取这两次提交的变更内容(主要考虑到方便后面调整),在解析变更内容获取到具体发生变更的组件
// 获取变更的文件
const getChangedFiles = (content) => {
const catchReg = /--- a\/packages\/[a-z\-]*/g;
let res;
while (res = catchReg.exec(content)) {
changeFiles.add(res[0].split('a/packages/')[1]);
}
}
最后执行命令,用 turbo 打包发生变更的文件,它的格式是这样的
turbo run build --filter=@组织名/组件名1 --filter=@组织名/组件名2
当然还有别的格式,详情参考官网。
这样,我通过脚本就做到了原来 turbo Head 描述的相关功能,它会只打包发生变更的组件,这样能节省很多时间
不过我上面这段代码有一个问题,就是如果你是改变的 package.json 中的内容,比如你升级了某个依赖包,这样上面的代码是不会有任何动作的,你需要自己添加相关内容,当 package.json 中的依赖发生变更时,最好能打包全部的组件,当然,如果你有手段确定具体依赖这个包的组件,那么你可以选择只打包对应的组件