今天带来一篇不一样的组件库开发文章,强烈推荐先收藏后阅读。
当你看了很多的从0到1开发的组件库的文章后会发现跟你主流使用的组件库总是天差地别,但又不是不能用。这样的组件库其实就严重缺少了工程化的支撑,缺少了全局的考虑。如果你已经看过了那些文章,那么你将通过这次介绍的6件事让你的组件库提升到更高的高度~
1. 留一个好的印象给Contributor
1.1 背景描述
在 Workspace 模式的项目中各个子包都会有一定的相互依赖存在,当你未能构建某个特定子包时就会造成另外一个子包无法启动,为了避免这样的问题出现,也为了给Contributor留下一个好的印象,我决定在你拿到项目安装依赖之后就帮你把该做的事情都做好,达到开箱即可体验的目的。
1.2 回归案例
在这次的案例中按 Workspace 模式开发的组件库项目包含了有ui、docs和example三个子包,其中docs和example都依赖ui子包构建后的产物,那么我需要做的就是在你安装项目依赖后自动实现ui包构建。
1.3 技术调研
在查看NPM文档后得知在执行依赖安装前后其实都会触发特定的钩子,我将利用这一特性,在触发到postinstall
钩子后自动执行ui包构建。
1.4 实现过程
在 Workspace 下的 package 下添加构建ui包的脚本;
- 通过
--filter
来限制脚本执行的子包集;
{
"scripts": {
"postinstall": "pnpm -r --filter=@gfe/ui run build"
}
}
2. Build Tools API 更适合
2.1 背景描述
在一些常规的项目中通常只需要应用到webpack
或者vite
的配置文件就可以让项目正常的运行及构建,当你的项目变得复杂的时候就或不停的往配置文件中增加更多的loader
或plugins
来充实构建工具的功能,在组件库开发的时候如果你仅使用配置文件来实现的话这一切将变得很复杂,所以就需要应用到构建工具提供的API来在构建脚本的函数中动态调用,实现更加灵活的执行。
2.2 回归案例
在这次的案例中将利用Vite构建工具来实现每个组件的打包,在后期使用组件时既可以引入全量的组件包又可以选择性的使用某一个指定的组件包。在组件包中我们还将利用脚本来充实组件包的内容达到更好的使用体验,这也是纯配置文件不那么容易搞定的事情。
2.3 实现过程
2.3.1 组件全量编译
全量编译不需要配置过多的信息,因为它会按照你在vite配置执行编译,只需要调用vite提供的build
函数就可以了~
import { build } from "vite";
const buildAll = async () => {
// 全量打包
await build();
}
buildAll();
2.3.2 组件分包编译
分包编译就需要通过遍历组件目录得到符合组件包特征的组件列表,在遍历组件列表的时候实时配置组件编译选项再执行build
函数。
// 按组件分别打包
const srcDir = path.resolve(__dirname, "../src/");
// 提取包含index.ts入口的组件目录
const componentsDir = fs.readdirSync(srcDir).filter(filename => {
const componentDir = path.resolve(srcDir, filename);
const isDir = fs.lstatSync(componentDir).isDirectory();
return isDir && fs.readdirSync(componentDir).includes("index.ts");
})
// 遍历需要打包的组件分别打包
for (let name of componentsDir) {
const outDir = path.resolve(output, name)
const customConfig = {
lib: {
entry: path.resolve(srcDir, name),
name,
fileName: "index",
formats: ["esm", "umd"]
},
outDir,
}
await build({ build: customConfig, } as InlineConfig);
};
2.3.3 调整package信息
在全量构建完成和每个分包构建完成都应该给它们维护其专属的package信息,在使用组件时将很有用。
// 输出package信息函数
function outputPkgFile(filepath, pkg) {
fs.outputFile(path.resolve(filepath, `package.json`), JSON.stringify(pkg, null, 2), `utf-8`);
}
全量构建的package信息不需要全部重写,可以导入ui包下的package信息,在此基础上进行修改,补充main、module、types信息,分别指向umd输出产物路径、esm输出产物路径、.d.ts输出产物路径(在组件库的使用体验很重要一节会详细说明);
outputPkgFile(output, {
...require("../package.json"),
main: `gfe-ui.umd.js`,
module: `gfe-ui.esm.js`,
types: `gfe-ui.d.ts`,
});
每个分包构建后的package信息除了上面的三个属性外name属性也需要调整成每个组件自己的名称,因为它属于这个组件;
outputPkgFile(outDir, {
name: `@gfe-ui/${name.toLocaleLowerCase()}`,
main: `index.umd.js`,
module: `index.esm.js`,
types: `../types/${name}/index.d.ts`
});
2.3.4 输出自述文档
自述文档在没有项目的根目录下都会有一份,在ui包的根目录下的自述文档描述了组件库的安装、导入及使用的方式,在输出的产物中也需要包含这个文件;
fs.copyFileSync(path.resolve("./README.md"), path.resolve(output, `README.md`));
输出结构:
dist
├─ Button
│ ├─ styles
│ │ ├─ index.css
│ │ └─ style.css
│ ├─ index.esm.js
│ ├─ index.esm.js.map
│ ├─ index.iife.js
│ ├─ index.iife.js.map
│ ├─ index.umd.js
│ ├─ index.umd.js.map
│ └─ package.json
├─ gfe-ui.d.ts
├─ gfe-ui.esm.js
├─ gfe-ui.esm.js.map
├─ gfe-ui.iife.js
├─ gfe-ui.iife.js.map
├─ gfe-ui.umd.js
├─ gfe-ui.umd.js.map
├─ package.json
└─ README.md
3. 顺手的工具远胜与一切
3.1 背景描述
前端项目从不需要构建到使用webpack
、vite
构建,还有最近新出现的Turbopack
,无论选择哪种构建工具都会遇到某一些文件是没办法直接处理的,那么首先就会去寻找对应的插件来看是否可能满足要求,我想说的是其实没有那么必要拘泥于使用一种构建工具,有更顺手的构建工具配合将是一种不错的选择。
3.2 回归案例
在这次的案例中构建组件的主要是基于vite
来做的,但是在less模块的构建中我选择了相对熟悉的gulp
来编写构建脚本,通过遍历组件文件夹来提取到所有的less模块文件,在分别注册gulp
任务,最后交由gulp
来统一执行,定制化程度高,我想不到什么样的vite
插件可以这么灵活的实现;
3.3 实现过程
- 配置
gulp
构建less
模块的脚本,使用到了gulp-less
模块支持,如果需要对less构建完的结果做进一步处理,比如要压缩,就可以继续使用pipe
处理,因为gulp
基于 node 强大的流(stream)能力:
import gulp from "gulp";
import less from "gulp-less";
// gulp core code
const register = (name, src, dist) => {
gulp.task(name, function () {
return gulp.src(src)
.pipe(less())
.pipe(gulp.dest(dist));
});
}
// TODO register task
// 导出所有 gulp task
export default gulp.series(...tasks);
- 确定组件文件的和输出文件的路径,如果在
vite.config.ts
中指定了outDir
选项将会被优先使用:
const srcDir = path.resolve(__dirname, "../src/");
const output = path.resolve(require("../vite.config.ts").build?.outDir || "../dist");
- 通过组件包特征(包含入口
index.ts
文件)来确定所有组件的目录:
const componentsDir = fs.readdirSync(srcDir).filter(filename => {
const componentDir = path.resolve(srcDir, filename);
const isDir = fs.lstatSync(componentDir).isDirectory();
return isDir && fs.readdirSync(componentDir).includes("index.ts");
})
- 最后遍历组件注册任务:
register(`${name}Task`,
path.resolve(entryDir, 'style.less'),
path.resolve(outDir, 'styles')
);
总结:
无论是刚参与到组件库开发、正处在组件库开发期间还是在使用组件库过程中都有考虑,倾力打造一款优秀体验的组件库,在组件库的工程化方面和组件开发当中还有哪些可以提升体验的地方欢迎一起讨论~