前言
本文主要给大家带来一些我在研究 ElementUI 源码过程中,自己的一些思考。
温馨提示:上个月写了一篇 Babel 相关的文章《从 Babel 到组件按需引入原理》,重点分析了 Babel 的底层原理。今天我们从 ElementUI 的实现方案出发,重点聊一聊组件库按需加载功能。希望正在研究自己创建组件库的同学可以看看,或许能对你产生一些帮助。
思考:
其实最简单的“按需加载组件”实现方式,就是在应用中直接引用所需组件的源文件,在应用的构建工具中跟应用一起构建。那么为什么不这样做呢?
在应用中构建这些组件源文件,就意味着应用的构建工具必须要具备构建这些组件的能力。比如需要有编译 Vue
模板、编译 ES6+
语法、编译 Sass/Less
语法。甚至有些场景需要处理定制主题、国际化等等。这里会给直接在用户的应用中编译组件源码带来困难。
所以到这里我们可能想到:能不能用 webpack
实现构建多个 bundle
呢?
大致思路就是我们新增一个 webpack
配置文件,基于组件库的组件配置文件生成一个对象,key
是组件名,value
是组件的入口 js
文件,将此对象作为该配置文件的 entry
选项值即可,其他配置与完整版的组件库 webpack
配置文件一致(输出目录可根据需要自行配置)。构建时执行这两个配置文件,即可构建出一个完整版的组件库包和每个组件独立的包。
脚本生成入口文件
由于 Github 近期服务不太稳定,这里源码链接采用 Gitee 中链接代替。
了解 Element 的构建流程,我们先查看 Element 2.13.1
版本 package.json
的 npm 脚本
之一: npm run build:file
/* 自动生成一些源码文件 */
"build:file": "node build/bin/iconInit.js & node build/bin/build-entry.js & node build/bin/i18n.js & node build/bin/version.js"
其中 node build/bin/build-entry.js
主要用于生成 Webpack 构建的入口文件 src/index.js
。
这个入口文件会对外暴露一个对象,里面会包含 install
方法。install
方法中会在传入的 vue
实例上挂载全部的组件、指令和全局方法。除了 install
方法外,为了支持单组件的使用, 还会在这个对外暴露对象上面添加所有的组件作为该对象的属性。
我们看源码:
/* 组件地址 */
var Components = require('../../components.json');
var fs = require('fs');
/* 胡子写法变量替换 */
var render = require('json-templater/string');
/* 将给定字符串转换成驼峰写法 */
var uppercamelcase = require('uppercamelcase');
var path = require('path');
/* 一个字符串常量, 定义操作系统相关的行末标志 */
var endOfLine = require('os').EOL;
/* 输出路径,也就是项目的入口文件的地址 */
var OUTPUT_PATH = path.join(__dirname, '../../src/index.js');
/* 组件引入模版 */
var IMPORT_TEMPLATE = 'import {{name}} from \'../packages/{{package}}/index.js\';';
/* 组件注册模版 */
var INSTALL_COMPONENT_TEMPLATE = ' {{name}}';
/* 主题字符串模版,里面有 include, install, version, list 四个变量 */
var MAIN_TEMPLATE = `/* Automatically generated by './build/bin/build-entry.js' */
{{include}}
import locale from 'element-ui/src/locale';
import CollapseTransition from 'element-ui/src/transitions/collapse-transition';
const components = [
{{install}},
CollapseTransition
];
const install = function(Vue, opts = {}) {
locale.use(opts.locale);
locale.i18n(opts.i18n);
components.forEach(component => {
Vue.component(component.name, component);
});
Vue.use(InfiniteScroll);
Vue.use(Loading.directive);
Vue.prototype.$ELEMENT = {
size: opts.size || '',
zIndex: opts.zIndex || 2000
};
Vue.prototype.$loading = Loading.service;
Vue.prototype.$msgbox = MessageBox;
Vue.prototype.$alert = MessageBox.alert;
Vue.prototype.$confirm = MessageBox.confirm;
Vue.prototype.$prompt = MessageBox.prompt;
Vue.prototype.$notify = Notification;
Vue.prototype.$message = Message;
};
/* istanbul ignore if */
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue);
}
export default {
version: '{{version}}',
locale: locale.use,
i18n: locale.i18n,
install,
CollapseTransition,
Loading,
{{list}}
};
`;
delete Components.font;
/* 获取所有组件名称 */
var ComponentNames = Object.keys(Components);
/* 替换 include, install, list 的数组 */
var includeComponentTemplate = [];
var installTemplate = [];
var listTemplate = [];
ComponentNames.forEach(name => {
/* 驼峰法命名组件,之前在 components.json 中是单词加 '-' 拼接 */
var componentName = uppercamelcase(name);
includeComponentTemplate.push(render(IMPORT_TEMPLATE, {
name: componentName,
package: name
}));
/* install 方法模版排除直接挂载到全局的组件 */
if (['Loading', 'MessageBox', 'Notification', 'Message', 'InfiniteScroll'].indexOf(componentName) === -1) {
installTemplate.push(render(INSTALL_COMPONENT_TEMPLATE, {
name: componentName,
component: name
}));
}
/* 生成导出对象的组件名称模版 */
if (componentName !== 'Loading') listTemplate.push(` ${componentName}`);
});
/* 替换模版中的变量 */
var template = render(MAIN_TEMPLATE, {
include: includeComponentTemplate.join(endOfLine),
install: installTemplate.join(',' + endOfLine),
version: process.env.VERSION || require('../../package.json').version,
list: listTemplate.join(',' + endOfLine)
});
/* 将字符串模板写入到框架入口文件 */
fs.writeFileSync(OUTPUT_PATH, template);
console.log('[build entry] DONE:', OUTPUT_PATH);
为什么要编写生成框架入口文件的脚本:
- 组件较多,减少了一些不必要的重复劳动
- 实现构建框架的流程配置自动化
仔细想一下,我们开发公共组件库的目的,就是让公司所有前端团队做项目更加便利。那么,在写脚本提升我们开发效率的方面,也是值得我们深入探索的。之前有看过一篇掘友文章,充分利用 NodeJS动态创建 vue 组件,配置路由的。感兴趣的小伙伴可以试一试。
脚本做了哪些事情:
- 引入插件
- 初始化字符串模版
- 组织数据,用于替换字符串模版中变量
- 替换模版中变量,并将模板写入到框架入口文件
入口文件内容
看下脚本执行的结果 element/src/index.js
:
/* Automatically generated by './build/bin/build-entry.js' */
/* 组件源码引入 */
import Pagination from '../packages/pagination/index.js';
import Dialog from '../packages/dialog/index.js';
import Autocomplete from '../packages/autocomplete/index.js';
import Dropdown from '../packages/dropdown/index.js';
import DropdownMenu from '../packages/dropdown-menu/index.js';
// ...
import Avatar from '../packages/avatar/index.js';
import Drawer from '../packages/drawer/index.js';
import Popconfirm from '../packages/popconfirm/index.js';
import locale from 'element-ui/src/locale';
import CollapseTransition from 'element-ui/src/transitions/collapse-transition';
const components = [
Pagination,
Dialog,
Autocomplete,
Dropdown,
DropdownMenu,
// ...
CascaderPanel,
Avatar,
Drawer,
Popconfirm,
CollapseTransition
];
const install = function(Vue, opts = {}) {
locale.use(opts.locale);
/* 国际化 */
locale.i18n(opts.i18n);
/* 注册组件 */
components.forEach(component => {
Vue.component(component.name, component);
});
Vue.use(InfiniteScroll);
Vue.use(Loading.directive);
Vue.prototype.$ELEMENT = {
size: opts.size || '',
zIndex: opts.zIndex || 2000
};
/* 添加全局功能 */
Vue.prototype.$loading = Loading.service;
Vue.prototype.$msgbox = MessageBox;
Vue.prototype.$alert = MessageBox.alert;
Vue.prototype.$confirm = MessageBox.confirm;
Vue.prototype.$prompt = MessageBox.prompt;
Vue.prototype.$notify = Notification;
Vue.prototype.$message = Message;
};
/* istanbul ignore if */
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue);
}
/* 全部导出 */
export default {
version: '2.13.1',
locale: locale.use,
i18n: locale.i18n,
install,
CollapseTransition,
Loading,
Pagination,
Dialog,
Autocomplete,
Dropdown,
DropdownMenu,
// ...
PageHeader,
CascaderPanel,
Avatar,
Drawer,
Popconfirm
};
入口文件干了什么:
- 导入所有组件
- 定义
install
方法 - 判断环境执行
install
方法 - 最后整体导出
组件打包
webpack --config build/webpack.component.js
脚本用于构建 commonjs2
的UI组件(提供按需引入功能),执行该脚本最终会在 lib
下生成所有 Element
支持的 UI 组件如图目录(同时这些文件也会被 element-ui.common.js
总入口文件引用)。
不废话,直接看源码 webpack.component.js
:
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']
},
/* 排除其他UI组件、支持国际化、utils的源码,这些源码会额外构建 */
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;
构建单个组件和构建总体入口文件 element-ui.common.js
的 Webpack
配置类似,需要将 utils
、locale
以及其他一些依赖排除。
文章写到这里就结束了,其实还有很多细节值得去研究,比如:样式的处理、国际化的实现等等。那么大家感兴趣的就直接去 ElementUI
源码寻找答案吧!😅
参考文章
感谢
如果本文对你有帮助,就点个赞支持下吧!感谢阅读。
网友评论