UI组件库重构实践——整合Lerna与pnpm
背景与目标
最近对原有的UI组件库进行了重构,以解决之前一次性打包所有组件导致无法单独引入的问题。通过引入Lerna管理和pnpm作为包管理工具,使得每个组件能够独立打包、独立引入,同时也保留了整体引入所有组件的功能。
使用工具与技术栈
- Lerna V8
- pnpm
- Rollup
初始化Lerna与pnpm集成
-
执行
lerna init
生成lerna.json
配置文件,并对其做了如下调整:{ "packages": ["packages/*"], "npmClient": "pnpm" }
-
创建并配置
pnpm-workspace.yaml
文件以指定包范围:packages: - "packages/*"
-
在每个子包(例如Form、Table等组件)目录下创建各自的
package.json
文件,并设置打包命令,使其指向Rollup配置文件:"scripts": { "build": "rollup --config ../../scripts/rollup.config.prod.js --environment PROJECT_NAME:autoComplete,MODE:development" }
这样便能在各子包目录下分别产出
dist
包,便于单独引入每个组件。
组件聚合与整体引入
为了同时支持一次性引入所有组件的功能,编写了一个名为 build.cjs
的脚本,用于将所有子包编译后生成的 dist
目录下的文件复制到 lib
目录,并创建一个聚合所有组件的 index.js
文件。
以下是 build.cjs
脚本内容:
/**
* @Author: yuxuan-ctrl
* @Date: 2024-03-12 16:46:09
* @LastEditors: yuxuan-ctrl
* @LastEditTime: 2024-03-13 17:21:44
* @FilePath: \scripts\build.cjs
* @Description: 聚合子包dist文件到lib目录
*/
const fs = require('fs-extra');
const path = require('path');
const targetDir = path.resolve(__dirname, '../lib'); // 目标目录
const packagesDir = path.resolve(__dirname, '../packages'); // 子包目录
const srcDir = path.resolve(__dirname, '../src'); // 源码目录(这里假设src也需要同步到lib)
// 遍历子包并将dist目录下的文件复制至lib对应目录
fs.readdirSync(packagesDir).forEach(packageName => {
const packageDir = path.join(packagesDir, packageName, 'dist');
const packageJsonFile = path.join(packagesDir, packageName, 'package.json');
if (fs.existsSync(packageDir)) {
fs.readdirSync(packageDir).forEach(file => {
const sourceFile = path.join(packageDir, file);
const targetFile = path.join(targetDir, packageName, file);
// 若目标路径的上级目录不存在,则递归创建
if (!fs.existsSync(path.dirname(targetFile))) {
fs.mkdirSync(path.dirname(targetFile), { recursive: true });
}
// 复制文件
fs.copyFileSync(sourceFile, targetFile);
});
}
});
// 同时将src目录下的源码结构完整复制到lib目录
fs.copy(srcDir, targetDir, (err) => {
if (err) {
console.error('An error occurred while copying:', err);
} else {
console.log('Successfully copied src directory to lib directory!');
}
});
通过运行该脚本,所有组件被打包后的产物会被复制到 lib
目录,并且保持原包结构。接着,在 lib
目录下创建一个 index.js
文件,用于一次性导入并注册所有组件。这样既实现了单个组件的精细管理,也兼顾了全量引入的需求,提高了组件库的灵活性和可维护性。
Lerna V8踩坑
自Lerna v7.0.0开始,Lerna默认移除了lerna bootstrap
、lerna add
以及lerna link
命令。这一改动促使用户采用更加现代化的方式管理项目依赖关系,即利用包管理器自带的工作空间功能来替代Lerna的这部分职责。
核心理念转变在于认识到Lerna不再负责仓库中的依赖安装和链接工作,而是由对应的包管理器更胜任此任务。为此,您应当利用包管理器提供的工作空间特性:
启用工作空间后,包管理器会在执行install
命令时自动完成之前lerna bootstrap
和lerna link
所实现的相同链接操作,无需额外执行其他命令(前提是已按照上述包管理器文档正确配置了工作空间)。
同样地,替换lerna add
命令也很直接。添加或移除依赖本来就是包管理器的基本功能,在启用工作空间后,您可以直接运行适当的安装命令来向特定包或工作空间添加依赖项,系统会自动处理相关的本地链接。
具体来说,当你迁移至Lerna v7及以上版本时,以下是比较明确的变化及前后使用方式对比:
之前(Lerna v6及更低版本):
- 安装并链接所有包依赖:
lerna bootstrap
- 添加依赖到某个包:
lerna add <dependency> --scope=<package-name>
- 创建本地链接:
lerna link
现在(Lerna v7及以上版本配合包管理器工作空间):
- 安装并链接所有包依赖:仅需运行
pnpm install
(若使用pnpm)、npm install
(若使用npm带有workspaces配置)或yarn install
(若使用yarn工作空间) - 添加依赖到某个包:直接在相应的包目录下运行如
pnpm add <dependency>
、npm install <dependency>
(npm会依据workspace配置自动定位到正确的包)或yarn add <dependency>
,包管理器会自动将其链接到适当的位置。
通过这样的调整,项目构建流程更加简洁,依赖管理更为透明,且充分利用了现代包管理器的功能。
如何执行Link操作呢?
以Pnpm举例:
pnpm link
pnpm link --global
pnpm link --global
Links package from <dir>
folder to node_modules of package from where you’re executing this command or specified via --dir
option.
例如,如果你在
~/projects/foo
里面,执行pnpm link --dir ../bar
,那么foo
就会链接到bar/node_modules/foo
。英:For example, if you are inside
~/projects/foo
and you executepnpm link --dir ../bar
, thenfoo
will be linked tobar/node_modules/foo
.