陪伴前端开发日常的一定是组件了吧,能提高开发效率,降低代码重复度等,在我司后端,ui,产品都经常说这不就是一个组件么拿来用就好了,但是堆砌的组件成了我们中后台打开性能的最大阻碍,gzip前体积高达4mb的js + 700kb的css,究其根本就是我们内部的组件库不支持按需导入,在入口就加载了这个巨大无比的组件库,但是在使用一些社区的组件库时并没有这些阻碍,我们先来看看他们是如何实现的
我司内部基于vue@2.7+antd@1.7.8开发,主要参考antd element的实现方案
element
在package.json可以看到,element提供了dist指令完成发布前的一些操作,我们拆解看一下
npm run clean // 清空发布目录
npm run build:file(
node build/bin/iconInit.js & // 大概是处理图标
node build/bin/build-entry.js & // 在根据根目录的components.json文件生成umd的入口文件, 包括umd入口注册组件的一些模版代码
node build/bin/i18n.js & // 多语言相关的逻辑
node build/bin/version.js)
npm run lint
webpack --config build/webpack.conf.js // 重点关注 !!!
webpack --config build/webpack.common.js // 生成commonjs版本
webpack --config build/webpack.component.js // 重点关注 !!!
npm run build:utils // 使用babel降级src/目录中的公共文件
npm run build:umd // 使用babel降级多语言的js源文件
npm run build:theme // 根据默认的主题处理sass文件
看到这里element内部组件的打包使用webpack完成,接下来我们详细看一下如何生成es,umd的组件库代码
上面多个脚本都依赖到了根目录的components.json, 我们看一下这个文件的缩略版
{
"pagination": "./packages/pagination/index.js",
"dialog": "./packages/dialog/index.js",
}
看到这里我们也能大概猜到是如何生成es版本的组件库了吧,在webpack中的entry不就是提供一个入口文件么,哪对于umd格式,刚才有个脚本生成了所有compents的模版文件(node build/bin/build-entry.js),我们继续看一下element的实现方式
webpack --config build/webpack.conf.js
指定了entry(上文中提到了根据components.json渲染的模版文件,包括导入组件,注册组件) eg
// 精简版
import Pagination from '../packages/pagination/index.js';
import Dialog from '../packages/dialog/index.js';
const components = [
Pagination,
Dialog,
]
const install = function(Vue, opts = {}) {
components.forEach(component => {
Vue.component(component.name, component);
});
};
export default {
version: '2.15.12',
install,
Pagination,
Dialog,
}
所以总结一下umd的生成
最后附上完整的webpack配置
const path = require('path');
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const TerserPlugin = require('terser-webpack-plugin');
const config = require('./config');
module.exports = {
mode: 'production',
entry: {
app: ['./src/index.js']
},
output: {
path: path.resolve(process.cwd(), './lib'),
publicPath: '/dist/',
filename: 'index.js',
chunkFilename: '[id].js',
libraryTarget: 'umd',
libraryExport: 'default',
library: 'ELEMENT',
umdNamedDefine: true,
globalObject: 'typeof self !== 'undefined' ? self : this'
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: config.alias
},
externals: {
vue: config.vue
},
optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
output: {
comments: false
}
}
})
]
},
performance: {
hints: false
},
stats: {
children: false
},
module: {
rules: [
{
test: /.(jsx?|babel|es6)$/,
include: process.cwd(),
exclude: config.jsexclude,
loader: 'babel-loader'
},
{
test: /.vue$/,
loader: 'vue-loader',
options: {
compilerOptions: {
preserveWhitespace: false
}
}
}
]
},
plugins: [
new ProgressBarPlugin(),
new VueLoaderPlugin()
]
};
我们继续看一下es版本的生成过程
有了每个组件的入口,我们是不是就能一一对应的生成出构建结果,但是事情还没那么简单,例如el-pagination导入了el-button,我们在项目中使用到了el-pagination,el-button那么构建的结果是不是就包含了两份el-button的代码,可能有同学说有treeshark,但是这个场景下很明显不可以,大家可以想一下为什么
如果这样做,哪我们的按需导入不就没有意义了么,继续看看element是如何解决的,答案就是webpack 的externals。只要将内部的组件,代码引用全部externals掉不就解决重复导入了么
我们看一下具体的webpack配置
const path = require('path');
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const Components = require('../components.json');
const config = require('./config');
const webpackConfig = {
mode: 'production',
entry: Components,
output: {
path: path.resolve(process.cwd(), './lib'),
publicPath: '/dist/',
filename: '[name].js',
chunkFilename: '[id].js',
libraryTarget: 'commonjs2'
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: config.alias,
modules: ['node_modules']
},
externals: config.externals,
performance: {
hints: false
},
stats: 'none',
optimization: {
minimize: false
},
module: {
rules: [
{
test: /.(jsx?|babel|es6)$/,
include: process.cwd(),
exclude: config.jsexclude,
loader: 'babel-loader'
},
{
test: /.vue$/,
loader: 'vue-loader',
options: {
compilerOptions: {
preserveWhitespace: false
}
}
},
{
test: /.css$/,
loaders: ['style-loader', 'css-loader']
},
{
test: /.(svg|otf|ttf|woff2?|eot|gif|png|jpe?g)(?\S*)?$/,
loader: 'url-loader',
query: {
limit: 10000,
name: path.posix.join('static', '[name].[hash:7].[ext]')
}
}
]
},
plugins: [
new ProgressBarPlugin(),
new VueLoaderPlugin()
]
};
module.exports = webpackConfig;
除了基础的配置外,我们看一下externals是如何做的
如我们想的一样,确实是枚举了element内部的组件+公共文件。
最后看一下es版本的整体流程
这个时候js的构建逻辑基本就完成了,css的构建在element中使用了gulp, 换个目录直出即可,就不展开说了
ant-design
在来看一下大名鼎鼎的ant-design,老规矩还是先看package.json, antd的发布脚本使用antd-tools compile,这里antd-tools应该是一个内部的库,我们转移视线来到antd-tools的仓库,看看他是如何提供构建的。
antd-tools run dist
antd-tools run compile // 主要编译的逻辑
antd-tools run clean
antd-tools run pub
antd-tools run guard
antd-tools run deps-lint
antd-tools run sort-api-table
antd-tools run api-collection
有了上面element的经验, 我们大概知道要实现按需,必须拆分打包结果,做到一个结果对应一个组件,在element中他手写json配置的方式给出了每个组件的路径,在antd中并没有找到类似的配置,哪他是咋样实现的呢,答案是目录结构,约定一种文件结构使用glob来读取所有关联的文件。了解了基本的思路我们在看看antd是如何做es版本的输出的。
看一下核心compile task,antd内部用了gulp来承载这些操作, 有了目录结构,我们是不是就可以只做transform不做bundle了, 看一下antd es的处理逻辑,他主要做了文件的transform,包括js降级,类型文件生成
compile逻辑
- 拿到目标文件夹下所有的文件
- 不同类型匹配对应的翻译器eg js使用babel ts使用tsc
- 处理完毕按照原文件路径输出到指定目录
这不就有了es的版本么,还顺便知道了为什么使用了gulp来做
我们思考一下,我们一个组件库发布到npm, 要做到开箱即用,必须要做哪些事情,拍脑袋一想可能有
-
提供不同格式 比如 es umd
-
尽可能降低对编译速度的影响,比如将一些原本不支持的文件做编译 .vue ==> js ts ==> js
-
type
对比上面的两种方式,在antd 肯定更加适合在2022年使用, 毕竟element的构建脚本已经停留在了5年前,哈哈