组件库构建方案演进

阅读数:24 2020 年 1 月 17 日 18:09

组件库构建方案演进

组件库是前端开发过程中重要一环,一个足够灵活、功能强大的组件库方案将大幅度提升前端开发工作效率。在本篇文章中,笔者将从构建需求、项目结构、基于 webpack 的实现方案、基于 rollup 的实现方案、基于 rollup 的优化方案等五个部分来介绍组件库构建方案的演进过程,与读者共勉。

构建需求

  • 按需加载
    这里说的按需加载和浏览器前端资源的按需加载有点区别,组件库按需加载指的是项目只引入使用到的组件,没有使用到的组件不会被引入到项目中。通常这种引入方式都会依赖一些前端模块加载器来实现的,比如 webpack、rollup 等,这种引入方式带来的好处是减少项目代码体积,提高加载速度。
  • 更小的组件体积
    在开发组件的过程中,我们或多或少可能会使用到一些新的语法,像 es6,es7,ts 等等,最后都是需要经过编译才能保证在浏览器中正常运行,但是编译的过程中也总会加上一些额外的代码来实现一定的语法转换或者接口转换,甚至会带有一些脏代码等,这就不可避免地导致组件的源码大小会增加。因此我们这里说的更小的组件体积是指尽可能保持组件源码大小,把编译工具引入的额外代码量降到最低,并且去掉编译过程中的脏代码。

总的来说,第一个需求可以说是使用功能,第二个需求则更多的是偏向于代码量优化,下面我会通过一个例子来介绍组件库为实现这两个构建需求所使用的构建方案。

项目结构

在介绍构建方案之前,我们先来了解一下整个示例项目的项目结构,这个示例的项目结构和组件库的是保持一致的。

  • 源码目录结构
    组件源码都是放在 components 文件夹下,这个示例中有 a, b, c 三个组件以及 utils 公共方法集;每个组件都包含交互逻辑、样式、以及导出三个文件;其中 b 和 c 组件依赖 a 组件,a 组件依赖 utils 提供的工具函数。如下图:

组件库构建方案演进

  • 打包目录结构
    在构建需求中,我们说到组件按需加载是指只引入项目组件需要的组件,避免引入不需要的组件,实现这个需求,只要把每个组件单独打包成一个文件模块,就能实现项目引入需要的组件模块,而不引入其他组件模块。这跟 nodejs 的模块化思想是一样的,因此我们需要把组件打包成一个个 commonjs 模块。这里说明下为了保证打包后的脚本代码和样式代码是分离的,因此每个组件的样式也会单独打包成一个文件。最终我们定义的打包后的目录结构如下图:

组件库构建方案演进

从上图可以看出,我们可以通过以下的引入方法按需加载 A 组件:

复制代码
import A from ‘component-build/lib/a.js';
import ' component-build/lib/style/a.css';

基于 webpack 的实现方案

现在我们来介绍基于 webpack 实现的构建方案,主要会从 4 个方面说一下 webpack 方案的配置要点。

  • 每个组件都是一个入口
    我们前面说到需要把每个组件都单独打包成一个模块文件,在 webpack 里如果需要把一个组件打包成一个文件,那么只需要把这个组件配置成 webpack 的一个入口即可。因此 webpack 的入口配置会是下面这样:
复制代码
{
// 每个组件模块单独一个入口
entry: {
a: 'components/a/index',
b: 'components/b/index',
c: 'components/c/index',
utils: 'components/utils/index'
}
}

这个配置可以保证每个组件打包出来之后都是单独一个文件,满足按需加载的需求。

  • 模块的构建目标
    我们的目标是打包出来的文件应该是满足 commonjs 模块规范的文件,可以通过设置 webpack 的 libraryTarget 来指明要把模块打包成符合 commonjs 规范。配置如下:
复制代码
{
// 确保打包出来的模块使用 module.exports 导致组件
libraryTarget: ‘commonjs2’
}
  • 提取样式文件
    组件通常会有样式代码,需要把样式代码单独提取成为一个样式文件,为了能满足我们打包后的目录结构,需要通过设置 mini-css-extract-plugin 插件的参数,把样式代码打包到 style 文件夹里。配置如下:
复制代码
{
module: {
rules: [
{
test: /\.(s)css$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
importLoaders: 1
}
},
'postcss-loader',
'sass-loader'
]
}
]
},
// 提取组件样式,并统一放置到 style 文件夹下
plugins: [
new MiniCssExtractPlugin('style/[name].css')
]
}
  • 组件的依赖处理
    通常在开发组件的时候不可避免地可能会依赖另外一些组件,如果我们不处理当前组件对其他组件的依赖,那么 webpack 打包出来的当前组件代码中会包含其他组件的源码,这在按需加载中会产生冗余代码。由于每个组件都是 commonjs 的一个模块,我们只需把每个组件都配置成外部引用模块,webpack 就能把打包到当前组件的其他组件源码替换成通过 require 外部模块的方式引入。具体 webpack 配置如下:
复制代码
{
// 所有组件在被其他组件所依赖时都替换成 commonjs2 的方式引入
externals: {
// name 变量为组件库名称
'components/a': { commonjs2: `${name}lib/a.js` },
'components/b': { commonjs2: `${name}lib/b.js` },
'components/c': { commonjs2: `${name}lib/c.js` },
'components/utils': { commonjs2: `${name}lib/utils.js` }
}
}
  • webpack 方案的构建结果

组件库构建方案演进
lib 目录下的文件就是构建输出目录,输出的文件结构和前面定义的输出目录结构一致。取其中一个组件看看打包后的结果是否跟我们预期的一致,以 A 组件为示例:

  • style 下包含 A 组件的样式文件 a.css
  • 组件 A 使用 module.exports 导出
  • 组件 a 依赖的 utils 工具是以模块化的方式引入,而不是 utils 的代码直接打包在组件 a 里面

从上图打包出来的结果看,现在的方案已经是能实现按需加载了。

  • 基于 webpack 方案的缺点
    组件库构建方案演进

我们来看看打包出来的 utils 模块,utils 模块中需要导出的是 add 和 remove 函数,但是从代码看却多了很多额外的代码,其中 webpack 的启动函数就占了大部分代码量,那么 webpack 的启动函数是否能删除掉?另外,我们组件只用到了 add 方法,remove 方法没有使用,是否能在项目打包的时候通过 tree shaking 去掉?基于这个分析我们可以看到目前方案有以下两个缺点:

  • webpack 自带的启动函数增加代码量
  • 打包的模块语法不支持 es6 的导入导出语法,没法通过 tree shaking 减少没有使用的代码

基于 rollup 的实现方案

为了解决基于 webpack 实现方案的这两个缺点,我们调研了其他的一些打包工具,发现使用 rollup 打包出来的代码不会带有工具的启动函数,而且 rollup 支持打包出 es6 模块,那么我们现在介绍一下基于 rollup 的实现方案,rollup 方案配置的内容其实和 webpack 是比较像的,rollup 方案的核心配置也是前面那 4 部分。

  • 一个配置对应一个入口组件
    每个组件都单独使用一份配置来进行打包(即调用多次 ruollup 打包),组件的配置只有入口文件以及输出文件不一样,其他配置内容都是一样,如下:
复制代码
{
// 每个组件模块是一个入口,并且单独一份配置;A 组件作为示例
input: 'components/a/index',
output: {
file: 'lib/a.js',
}
}

rollup 支持 es 模块语法的打包,即打包出来的模块使用的导入导出语法是 es6 的模块语法,所以在 rollup 的配置里设置 output.format: ‘es’ 属性值,表明打包出来的是 es6 模块,即使用 es6 的导入导出语法。如下:

复制代码
{
// 声明 rollup 打包出来的是 es 模块
output: {
format: 'es'
}
}

 提取样式文件
使用 rollup-plugin-postcss 插件提取组件的样式代码,避免样式代码打包到脚本代码中。配置如下:

// 提取组件样式,并统一放置到 style 文件夹下,A 组件的配置如下:

复制代码
{
plugins: [
postcss({ extract: `lib/style/a.css` })
]
}
  • 组件依赖处理
    rollup 使用 external 属性来标记外部模块,并通过 output.paths 来匹配引用路径并替换成对应的值;external 的配置指定了 components 开头引入的模块都会标记为外部模块,然后通过在 paths 里面找到对应的值进行替换。配置如下:
复制代码
{
output: {
paths: {
// name 变量为组件库名称
'components/a': { commonjs2: `${name} /lib/a.js` },
'components/b': { commonjs2: `${name} /lib/b.js` },
'components/c': { commonjs2: `${name}/lib/c.js` },
'components/utils': { commonjs2: `${name}/lib/utils.js` }
}
},
// 匹配出要被标记为外部引入的模块
externals: (id, parent) => {
return /^components/i.test(id) && !!parent;
}
}

经过上面的配置,我们可以实现以下组件依赖的替换:

复制代码
import 'component/a' => import 'component-build/lib/a.js'
  • 基于 rollup 方案的打包结果

组件库构建方案演进
改成 rollup 的方案后,打包出来的 utils 模块是这样的:

  • 只保留了 utils 模块的 add 和 remove 方案,没有启动函数
  • 导出语法是 es6 语法,支持 tree shaking

相比 webpack 的方案,看起来代码简洁了许多,而且基本只保留了我们必须的代码。但是这个方案依然不完美。

  • 基于 rollup 方案的缺点
    B 组件打包出来的结果图:
    组件库构建方案演进

C 组件打包出来的结果图:
组件库构建方案演进

我们来看看基于 rollup 方案打包出来的 B 和 C 组件的代码:

  • 组件的依赖模块
  • 组件的 helper 函数(babel 编译自动添加的)
  • 编译后的源码
  • VUE 的序列化函数
  • 组件的导出

从 B 和 C 组件的一些相同点我们可以看出:

  • 打包后的文件可能会包含相同的 helper 函数
  • 打包后的文件包含相同的 vue 序列化函数代码

基于 rollup 的优化方案
针对前面那两个缺点,我们需要对 rollup 的打包方案做一些调整,以便可以复用多个组件都使用到的相同的 helper 函数,以及去掉各个组件相同的 vue 序列化函数。

  • 模块化引入 helper 函数和 vue 的序列化函数
    标记 helper 函数为外部模块,转换成模块化引入,实现不同组件使用相同 helper 函数时引用的都是同一个模块,而不是把 helper 函数的源码直接打包到组件代码中,这样通过模块化引入的方式就能解决多个组件可能包含相同 helper 函数源码的问题。externals 属性的配置修改成以下:
复制代码
{
// 标记出 helper 函数的模块
externals: (id, parent) => {
return /^components/i.test(id) && !!parent ||
/^vue-runtime-helpers/.test(id) ||
/^@babel\/runtime/.test(id) ||
/^@vue\/babel-helper-vue-jsx-merge-props/.test(id);
}
}

另外一种去掉 vue 的序列化函数方法是:vue 组件格式化函数主要是为 vue 组件绑定 render 函数以及添加样式;目前我们样式是单独一个 scss 文件的,可以说是跟 vue 组件分离的,那么组件格式化函数也是可以去掉的。目前我们使用 rollup-plugin-vue v3.0.0 版本,不使用最新 v4.x.x 版本,3.0.0 版本不会生成序列化函数。使用旧版本的配置和新版本的配置是一样的。

 指明依赖 helper 函数的 npm 包
通过前面这一步的配置,打包出来的组件代码会依赖 helper 函数的类库,因此我们需要在 package.json 中指明 helper 函数对应的 npm 包为我们的依赖包。如下:

复制代码
{
"dependencies": {
"@babel/runtime": "^7.8.3",
"vue": "^2.6.11",
"vue-runtime-helpers": "^v1.1.2"
}
}
  • 基于 rollup 优化方案的打包结果
    B 组件打包后的结果:

    组件库构建方案演进
    C 组件打包后的结果:

组件库构建方案演进
从这两个图中,我们可以看出优化后的 rollup 方案,打包出来的 B 和 C 组件的代码:

  • Helper 函数变成依赖引入
  • Vue 序列化函数已变成依赖引入

最后,整个示例的代码仓库信息如下:

代码仓库地址: https://github.com/maiwenan/component-build

评论

发布