组件库需求背景
- 提高开发效率
- 统一整体
UI风格
- 快速上手,提高了代码的可维护性
组件库设计
在设计组件库时,需要转化自己的角色。如果你从来没有组件库的开发经验,可以先思考自己在使用组件库时有哪些好用的功能,并记录使用组件库的一些痛点。在开发组件库时,你的角色从原来的一个使用者变成设计者,此时就要逆向考虑如何实现上述的好功能并且完善或解决一些痛点。
- 自动化打包流程-区分模块
- 多语言
- 组件文档示例
- 版本修改记录
- 自动化创建组件
- 按需加载
- babel-plugin-component
- 单元测试
开发组件库和使用脚手架开发项目有所不同,其中最大的区别就是组件库有很多的自动化打包流程,这句话该如何理解呢?
当我们在开发项目时,使用 vue
、react
的脚手架就能满足日常的打包(npm run dev
、npm run build
),这些工具脚手架都帮我们内置好的,用户直接使用即可。但是组件库不同,这些功能都需要你作为开发者时一一实现的,其他方面亦是如此。接下来我们分析 element UI 的设计风格,看从中能学习到什么。
element UI 组件库
这里采用的 elementUI版本为v2.15.12
,下载源码打开后我们第一个切入点是先看看它的目录结构。
目录作用:
- element ui 使用
build
文件夹用来存放工程化的内容以及打包工具的配置文件; examples
存放 Element UI 组件示例;packages
存放组件源码,也就是我们后面重点要分析的目标;src
存放入口文件以及辅助文件;test
存放单元测试文件,合格的单元测试是一个成熟的开源项目必备的;types
存放声明文件,方便引入使用 typescript 的项目中;.travis.yml
持续集成(CI)的配置文件,提交代码时,可根据该文件执行对应的脚本;CHANFELOG
更新日志,Element UI 准备了 4 个不同的语言版本;components.json
配置文件,用于标注组件的文件路径,方便 webpack 打包;element_log.svg
Element UI 的图标,使用svg格式,大大减少图片大小;FAQ.md
Element UI 开发者对常见问题的解答LICENSE
开源许可证;Makefile
这个文件定义了一系列的规则来制定文件变异操作。可以使用工程化编译工具 --------- make 命令进行操作,例如make new
自动创建组件目录结构,这些目录包含测试代码、入口文件、文档,实现一套自动化流程。
重点目录分析
packages
从 packages 文件夹分析,这里面的每一个子包都有自己的源代码和一个入口文件,都没有 package.json 文件,所以每个子包不能单独发布,因此可知 element ui 不是一个使用多包管理的组件库。
src
重点关注 src/index.js ,它是项目的入口文件:
js
/* Automatically generated by './build/bin/build-entry.js' */
import Pagination from '../packages/pagination/index.js';
import Dialog from '../packages/dialog/index.js';
// ...
// 引入组件
const components = [
Pagination,
Dialog,
// ...
// 组件名称
];
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 原型上挂载方法
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.15.12',
locale: locale.use,
i18n: locale.i18n,
install,
CollapseTransition,
// ...
// 导出组件
};
使用 Vue.use 方法调用插件时,会自动调用 install 函数,所以只需要在 install 函数中批量全局注册各种指令、组件、挂载全局方法即可。
Element UI 的入口文件有以下两点值得我们学习:
- 初始化时,提供选项用于配置全局属性,大大方便了组件的使用
- 自动化生成入口文件
自动化生成入口文件
首先看入口文件的第一句话:
js
/* Automatically generated by './build/bin/build-entry.js' */
解释:该文件是由 ./build/bin/build-entry.js
生成的,那么我们找到该文件查看一番。
js
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}}';
// 模板
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);
var includeComponentTemplate = [];
var installTemplate = [];
var listTemplate = [];
// 根据 components.json 文件批量生成模板所需的参数
ComponentNames.forEach(name => {
var componentName = uppercamelcase(name);
includeComponentTemplate.push(render(IMPORT_TEMPLATE, {
name: componentName,
package: name
}));
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);
build-entry.js
使用了json-templater
生成入口文件,并且引入 components.json
批量生成了组件引入、注册的代码。没有自动化生成入口文件之前,当我们每添加或者删除一个组件时,就需要在入口文件中进行多处修改,使用自动化生成入口文件后,我们只修改一处就可以了。
另外,components.js 文件也是自动化生成的,这又该从哪里追源寻根呢? 这个问题我目前还有找到答案,先顺着下文的思路慢慢解析吧。
packages.json
接下来,我们把注意力集中到 packages.json
中,当你不了解一个项目时,从这里入手是能获取到信息量最多的最好途径!
脚本
js
// packages.json
"scripts": {
"bootstrap": "yarn || npm i",
"build:file": "node build/bin/iconInit.js & node build/bin/build-entry.js & node build/bin/i18n.js & node build/bin/version.js",
"build:theme": "node build/bin/gen-cssfile && gulp build --gulpfile packages/theme-chalk/gulpfile.js && cp-cli packages/theme-chalk/lib lib/theme-chalk",
"build:utils": "cross-env BABEL_ENV=utils babel src --out-dir lib --ignore src/index.js",
"build:umd": "node build/bin/build-locale.js",
"clean": "rimraf lib && rimraf packages/*/lib && rimraf test/**/coverage",
"deploy:build": "npm run build:file && cross-env NODE_ENV=production webpack --config build/webpack.demo.js && echo element.eleme.io>>examples/element-ui/CNAME",
"deploy:extension": "cross-env NODE_ENV=production webpack --config build/webpack.extension.js",
"dev:extension": "rimraf examples/extension/dist && cross-env NODE_ENV=development webpack --watch --config build/webpack.extension.js",
"dev": "npm run bootstrap && npm run build:file && cross-env NODE_ENV=development webpack-dev-server --config build/webpack.demo.js & node build/bin/template.js",
"dev:play": "npm run build:file && cross-env NODE_ENV=development PLAY_ENV=true webpack-dev-server --config build/webpack.demo.js",
"dist": "npm run clean && npm run build:file && npm run lint && webpack --config build/webpack.conf.js && webpack --config build/webpack.common.js && webpack --config build/webpack.component.js && npm run build:utils && npm run build:umd && npm run build:theme",
"i18n": "node build/bin/i18n.js",
"lint": "eslint src/**/* test/**/* packages/**/* build/**/* --quiet",
"pub": "npm run bootstrap && sh build/git-release.sh && sh build/release.sh && node build/bin/gen-indices.js",
"test": "npm run lint && npm run build:theme && cross-env CI_ENV=/dev/ BABEL_ENV=test karma start test/unit/karma.conf.js --single-run",
"test:watch": "npm run build:theme && cross-env BABEL_ENV=test karma start test/unit/karma.conf.js"
},
首先,来分析 scripts 中的 dev
命令,他由三个串行一个并行命令组成,在终端中执行npm run dev
会发生什么?
第一步它会执行 npm run bootstrap
,用来安装项目所需的依赖;
js
"bootstrap": "yarn || npm i"
第二步执行 npm run build:file
,在以上的脚本中可以看到,build:file
又对应着 4 个并行命令,使用node运行了4个文件,这几个文件都可以在 build / bin 目录下找到,看看具体做了什么操作。
js
"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 / iconInit.js
- 利用 node.js 批量处理 icont 图标,倒序写入
examples/icon.json
中;
- 利用 node.js 批量处理 icont 图标,倒序写入
- node build / bin / build-entry.js
- 从 components.json 中拿到每个组件的名字,组件名 push 进一个数组中;
- 遍历数组,通过模版引擎生成入口文件,也就是上文提到过的
src / index.js
;
- node build / bin / i18n.js
- 运行多语言库,生成不同的语言的.vue文件,供用户切换语言时使用
- node build / bin / version.js
- 形成版本列表,切换不同版本的组件库
第三步在编译环节将 NODE_ENV
配置为 development,webpack-dev-server --config build/webpack.demo.js
意味着将要启动一个 webpack 服务,config 配置文件指向 build/webpack.demo.js,那么我们来看看 webpack.demo.js
中做了什么事情:
js
// webpack.demo.js
const webpackConfig = {
// ...
// 使用 ./examples/entry.js 作为入口文件
entry: isProd ? {
docs: './examples/entry.js'
} : (isPlay ? './examples/play.js' : './examples/entry.js'),
// 使用 /examples/element-ui/ 作为出口文件
output: {
path: path.resolve(process.cwd(), './examples/element-ui/'),
publicPath: process.env.CI_ENV || '',
filename: '[name].[hash:7].js',
chunkFilename: isProd ? '[name].[hash:7].js' : '[name].js'
},
// ...
// 项目可以在 8085 端口进行预览
devServer: {
host: '0.0.0.0',
port: 8085,
publicPath: '/',
// ...
}
},
// 各种 loader 配置
module: {
rules: [
// ...
// 这个 md 的loader很重要
{
test: /\.md$/,
use: [
{
loader: 'vue-loader',
options: {
compilerOptions: {
preserveWhitespace: false
}
}
},
{
loader: path.resolve(__dirname, './md-loader/index.js')
}
]
},
{
test: /\.(svg|otf|ttf|woff2?|eot|gif|png|jpe?g)(\?\S*)?$/,
loader: 'url-loader',
// todo: 这种写法有待调整
query: {
limit: 10000,
name: path.posix.join('static', '[name].[hash:7].[ext]')
}
}
]
},
plugins: []
};
上面的代码中有两个重要的文件,分别是入口文件和出口文件,我们先来看一下 /examples/entry.js,点进去大致可以看出来这个文档的编码风格和一个普通的 vue 项目的入口文件很相似。由此不难分析,整个 examples
文件可以看做是个独立的 vue 项目。webpack对入口文件进行编译打包构建,就相当于将整个examples
的 vue项目启动了,并且可以在 8085 端口进行预览。
第四步执行 node build/bin/template.js
,主要功能是监测 examples/pages/template文件下的模版信息,如果该模板发生改变,则重新执行一遍 npm run i18n
,重新编译多语言组件。
js
const path = require('path');
const templates = path.resolve(process.cwd(), './examples/pages/template');
const chokidar = require('chokidar');
let watcher = chokidar.watch([templates]);
watcher.on('ready', function() {
watcher
.on('change', function() {
exec('npm run i18n');
});
});
function exec(cmd) {
return require('child_process').execSync(cmd).toString().trim();
}
分析到这里,我们可以总结一下npm run dev
做了什么,首先做一系列的准备工作,初始化项目包,处理图标以及多语言,准备好后使用 webpack 将项目启动在 8085 端口,最后再监听国际化模版信息,当模板发生改变时,及时更新多语言组件页面。
这一篇分析到这里就结束啦,本文仅作为学习笔记,有需要补充以及更正的地方,欢迎大家评论区指正!