ELEME组件库源码要点分析
最近有计划写一个自己的组件库,一是练习一些通用业务逻辑的写法,二是为自己以后的项目组件做个积累。于是大概阅读了下比较有名的两个组件库——element和Ant Design的源码。这篇文章就对element的一些值得学习的编码思路做个整理。
入口分析
看一个前端项目的基本结构,首先看package.json
,其中的scripts
字段又是重中之重,就先从这里开始。
"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 buildack.demo.js && echo element.eleme.io>>examples/element-ui/CNAME",
"dev": "npm run bootstrap && npm run build:file && cross-env NODE_ENV=development webpack-dev-server --config buildack.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 buildack.demo.js",
"dist": "npm run clean && npm run build:file && npm run lint && webpack --config buildack.conf.js && webpack --config buildack.common.js && webpack --config buildack.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 && sh build/deploy-faas.sh",
"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"
}
icon管理
通过build:file
命令,会首先执行build/bin/iconInit.js
,该脚本会获取packages/theme-chalk/src/icon.scss
下的icon相关scss代码,通过正则匹配获取class中icon名统一生成icon.json文件,记录了项目中的所有icon。
进入到theme-chalk
这个文件夹中也可以发现,这也是一个单独的小项目,由gulp和scss组成,目的在于提供给eleme单独管理样式管理能力。
入口文件生成自动化
build:file
命令中进行的第二个操作就是build-entry
,这个文件的作用就是根据组件库中的单个组件导出自动生成一个index.js
集成所有导出,方便在组件库更新时实现入口文件自动化更新。实现上主要是通过json-templater
这个库,将处理过后的字符串转化为JS代码。
国际化和版本管理
build:file
集成的最后两个操作就是bin
目录下i18n.js
和version.js
的执行,前者根据国际化JSON配置生成不同语言版本的首页,后者生成版本记录JSON。
样式管理
先前也提到了组件默认样式都写在theme-chalk
文件夹下,样式和组件的整合工作就靠build:theme
实现。首先执行bin/gen-cssfile.js
,根据components.json
获取所有组件名,遍历组件名写入导入样式语句将theme-chalk
内对应样式都整合到index.scss
中,接着利用gulp
对scss文件做一些css压缩等常规处理,最终样式文件生成到./lib
下。
Webpack模式
开发模式下,分为dev
和dev:play
两种情况,使用的Webpack
配置文件为webpack.demo.js
,两者通过环境变量区分,具体配置上也只是入口文件不同。play
模式下会启动一个空白页,我的理解是在开发时在这个空白页引入需要编辑的组件查看效果。非play
模式下会启动一个集成所有组件的预览页,适合查看已开发完成组件的效果。
生产环境下,会通过webpack.conf.js
和webpack.common.js
打包umd
和commonjs
规范的代码各一份,并通过webpack.component.js
对各个组件分开打包一份支持按需引用。
通用工具分析
项目组件主体部分位于packages
文件夹下,其中各个组件通用工具函数被抽出到src/utils
和src/mixins
下,这里选几个比较重要的谈谈。
popup
这个模块负责管理模态弹出框,在组件里通过mixin
的形式引用。核心思路的简单实现如下。
import PopupManager from 'element-ui/src/utils/popup/popup-manager';
{
props: {
visible: {
type: Boolean,
default: false
}
},
beforeMount() {
this._popupId = 'popup-' + idSeed++;
PopupManager.register(this._popupId, this);
},
beforeDestroy() {
PopupManager.deregister(this._popupId);
},
watch: {
visible(val) {
if (val) {
if (!this.rendered) {
Vue.nextTick(() => {
this.open();
});
} else {
this.open();
}
} else {
this.close();
}
}
},
methods: {
open() {
PopupManager.openModal(this._popupId);
}
close() {
PopupManager.closeModal(this._popupId);
}
}
}
简单来说,就是通过watch一个标志变量决定是打开还是关闭弹窗,并将细节处理代理到PopupManager
上,PopupManager
进一步负责真实模态窗dom
的生命周期,id
记录等。
同理的,除了popup
负责的模态窗管理外,还有vue-popper
负责的定位弹出框管理,原理也是一样的,只是将PopupManger
的实现替换为了三方库popper
。
emitter
这是eleme内部使用的跨组件通信机制,在由一系列小组件封装成的完整组件中很有用,避免了prop drilling
。原理就是利用了vue组件父子关系明确的特点递归的处理事件。
function broadcast(componentName, eventName, params) {
this.$children.forEach(child => {
var name = child.$options.componentName;
if (name === componentName) {
child.$emit.apply(child, [eventName].concat(params));
} else {
broadcast.apply(child, [componentName, eventName].concat([params]));
}
});
}
export default {
methods: {
dispatch(componentName, eventName, params) {
var parent = this.$parent || this.$root;
var name = parent.$options.componentName;
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.componentName;
}
}
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
},
broadcast(componentName, eventName, params) {
broadcast.call(this, componentName, eventName, params);
}
}
};
小结
以上是我认为该组件库从宏观上看值得注意的点,当然各个组件实现细节肯定也有很多值得学习的地方就不在这里赘述了。
-- EOF --