ElementUI的结构与源码研究(未完待续)
说明:本文基于element-ui@2.13.0,源码详见element。
内容目录:
一、代码结构及工程化
1.1 package.json主要关注点1.1.1 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
1.1.2 pub
- npm run bootstrap
- sh build/git-release.sh
- sh build/release.sh
- node build/bin/gen-indices.js
- sh build/deploy-faas.sh
1.1.3 dev
1.1.4 test
二、src分析
2.1 directives:mousewheel & repeat-click
2.2 locale(国际化)
2.3 mixins
2.4 transitions
三、组件
四、主题
五、examples分析
一、代码结构及工程化
代码结构components.json
是一份组件清单,将在下面多处用到:
{
"pagination": "./packages/pagination/index.js",
"dialog": "./packages/dialog/index.js",
"autocomplete": "./packages/autocomplete/index.js",
"dropdown": "./packages/dropdown/index.js",
"dropdown-menu": "./packages/dropdown-menu/index.js",
"dropdown-item": "./packages/dropdown-item/index.js",
"menu": "./packages/menu/index.js",
"submenu": "./packages/submenu/index.js",
"menu-item": "./packages/menu-item/index.js",
"menu-item-group": "./packages/menu-item-group/index.js",
"input": "./packages/input/index.js",
.......
"drawer": "./packages/drawer/index.js",
"popconfirm": "./packages/popconfirm/index.js"
}
1.1 package.json主要关注点
- 对外发布的内容有["lib", "src", "packages", "types"];其中lib是运行打包命令后生成的目录
- scripts中主要关注
dist
、pub
、dev
和test
命令
1.1.1 dist
dist
命令主要有9个步骤,如下:
"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
"
-
npm run clean:
删除上次打包生成的目录及文件,主要有lib目录、test目录以及package/theme-chalk/lib(跟主题有关,后文详讲)目录 -
npm run build:file:
利用postcss
,根据package/theme-chalk/src/icon.scss,往example目录生成icon相关的信息;
利用json-templater/string
模板引擎,根据根目录下components.json
,往src目录下生成index.js
文件,index.js
主要是引入packages目录下的组件及install(vue插件)方法,并对外export;
利用正则
,根据examples/i18n/page.json
和examples/pages/template
,生成不同语言的文件,examples的内容相当于element UI官网,后面详讲 -
npm run lint:
利用eslint
,根据.eslintrc
和.eslintignore
文件,检测代码规范 -
webpack --config build/webpack.conf.js
入口文件:src/index.js
(npm run build:file生成)
输出:以umd
形式输出到lib/index.js
;
loader:babel-loader处理jsx等文件;vue-loader处理packages下面的vue组件 -
webpack --config build/webpack.common.js
入口文件:src/index.js
(npm run build:file生成)
输出:以commonjs2
形式输出到lib/element-ui.common.js
;
loader:babel-loader处理jsx、babel和es6等文件;vue-loader处理packages下面的vue组件;style-loader和css-loader处理css文件;以url-loader处理图片等; -
webpack --config build/webpack.component.js
按组件打包
入口文件:components.json
,包含packages下的组件;
输出:把packages下的组件,以commonjs2
形式分别输出到lib目录
;
loader:babel-loader处理jsx、babel和es6等文件;vue-loader处理packages下面的vue组件;style-loader和css-loader处理css文件;以url-loader处理图片等;
按需引入
:这里打包出来的内容如下图,可以安组件打包,方便按需引入:
过程如下:
a.import { Button } from 'element-ui'
b.借助babel插件babel-plugin-component(具体可参考babel-plugin-import的配置项、针对iview进行优化:babel-plugin-import-custom),可以把a步骤的代码转换成下面形式:
var button = require('element-ui/lib/button') // lib/button.js即按组件打包后的el-button组件
require('element-ui/lib/theme-chalk/button.css')
该插件对应的.babelrc相关配置:
{
"presets": [["es2015", { "modules": false }]],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
"libraryDirectory": "lib", // default: lib
}
]
]
}
但是
element-ui 这种按需引入的方式虽然方便,但背后却要解决几个问题,由于我们支持每个组件可以单独引入,那么如果产生了组件依赖并且同时按需引入的时候,代码冗余问题怎么解决。举个例子,在 element-ui 中,Table 组件依赖了 CheckBox 组件,那么当我同时引入了 Table 组件和 CheckBox 组件的时候,会不会产生代码冗余呢?
import { Table, CheckBox } from 'element-ui'
如果你不做任何处理的话,答案是会,你最终引入的包会有 2 份 CheckBox 的代码。那么 element-ui 是怎么解决这个问题的呢?实际上只是部分解决了,它的 webpack 配置文件中配置了 externals,在 build/config.js 中我们可以看到这些具体的配置:
var externals = {}; Object.keys(Components).forEach(function(key) { externals[`element-ui/packages/${key}`] = `element-ui/lib/${key}`; }); externals['element-ui/src/locale'] = 'element-ui/lib/locale'; utilsList.forEach(function(file) { file = path.basename(file, '.js'); externals[`element-ui/src/utils/${file}`] = `element-ui/lib/utils/${file}`; }); mixinsList.forEach(function(file) { file = path.basename(file, '.js'); externals[`element-ui/src/mixins/${file}`] = `element-ui/lib/mixins/${file}`; }); transitionList.forEach(function(file) { file = path.basename(file, '.js'); externals[`element-ui/src/transitions/${file}`] = `element->ui/lib/transitions/${file}`; }); externals = [Object.assign({ vue: 'vue' }, externals), nodeExternals()];
externals 可以防止将这些 import 的包打包到 bundle 中,并在运行时再去从外部获取这些扩展依赖。
举例:
packages/table/src/table.vue:import ElCheckbox from 'element-ui/packages/checkbox'; import { debounce, throttle } from 'throttle-debounce'; import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event'; import Mousewheel from 'element-ui/src/directives/mousewheel'; import Locale from 'element-ui/src/mixins/locale'; import Migrating from 'element-ui/src/mixins/migrating';
按组件打包后,对应的文件为lib/table.js
// EXTERNAL MODULE: ./packages/checkbox/index.js + 5 modules var packages_checkbox = __webpack_require__(31); // EXTERNAL MODULE: ./node_modules/_throttle-debounce@1.1.0@throttle-debounce/index.js var _throttle_debounce_1_1_0_throttle_debounce = __webpack_require__(86); // EXTERNAL MODULE: ./src/utils/resize-event.js var resize_event = __webpack_require__(18); ......
由于external配置,element-ui/packages/checkbox/index.js最后指向element-ui/lib/checkbox.js
我们来看一下打包后的 lib/table.js,我们可以看到编译后的 table.js 对 CheckBox 组件的依赖引入:/***/ (function(module, exports) { module.exports = require("throttle-debounce/debounce"); ...... module.exports = require("element-ui/lib/checkbox");
这么处理的话,就不会打包生成 2 份
CheckBox
JS 部分的代码了,但是对于 CSS 部分,element-ui
并未处理冗余情况,可以看到lib/theme-chalk/checkbox.css
和lib/theme-chalk/table.css
中都会有CheckBox
组件的 CSS 样式。其实,要解决按需引入的 JS 和 CSS 的冗余问题并非难事,可以用后编译的思想,即依赖包提供源码,而编译交给应用处理,这样不仅不会有组件冗余代码,甚至连编译的冗余代码都不会有,实际上我们基于
element-ui
fork 的组件库zoom-ui
就应用了后编译技术,之前在滴滴搞的开源组件库cube-ui
组件库也是这么玩的。更多后编译相关介绍可以参考这篇文章。
像iview UI组件,也可以使用babel-plugin-import
插件,可以使import { Circle } from 'iview';
,通过配置改成:
import _Table from "iview/src/components/table";
// tables.js中直接引入table.vue
Vue.component("iCircle", _Circle);
,
这种是相当于直接引入编译前的源码,省去了按组件编译的过程。
- npm run build:utils
设置BABEL_ENV=utils
(.babelrc文件中env 选项的值将从 process.env.BABEL_ENV 获取,如果没有的话,则获取 process.env.NODE_ENV 的值,它也无法获取时会设置为 "development" )
cross-env BABEL_ENV=utils babel src --out-dir lib --ignore src/index.js
:用Babel处理src下的directive、locale、mixins、transitions和utils,并输出到lib
目录 - npm run build:umd
利用babel-core
及插件add-module-exports
和transform-es2015-modules-umd
处理src/locale/lang下的文件,生成umd格式的文件;
利用file-save
进一步处理,如将define('zh-CN'
处理成define('element/locale/zh-CN'
,将global.zhCN = mod.exports
处理成global.ELEMENT.lang = global.ELEMENT.lang || {};global.ELEMENT.lang.zhCN = mod.exports;
(function (global, factory) {
if (typeof define === "function" && define.amd) {
define('element/locale/zh-CN', ['module', 'exports'], factory);
} else if (typeof exports !== "undefined") {
factory(module, exports);
} else {
var mod = {
exports: {}
};
factory(mod, mod.exports);
global.ELEMENT.lang = global.ELEMENT.lang || {};
global.ELEMENT.lang.zhCN = mod.exports;
}
})(......
- npm run build:theme,请见
四、主题
章节
1.1.2 pub
"pub": "
npm run bootstrap &&
sh build/git-release.sh &&
sh build/release.sh &&
node build/bin/gen-indices.js &&
sh build/deploy-faas.sh
"
-
npm run bootstrap:
安装依赖,注意的是vue
是以peerDependencies的形式配置的 -
sh build/git-release.sh:
git checkout dev -
sh build/release.sh:
a.checkout master分支,并合并dev分支;
b.通过npx临时安装select-version-cli,与开发者进行交互,更新版本信息;
c.执行npm run dist;
d.测试ssr;
e.进入packages/theme-chalk目录,利用npm version和npm publish,发布主题(packages/theme-chalk是个基于gulp的工程),由此可见elementUI的主题是可以独立发布的,不过会保证version跟elementUI保持一致;
f.退回到根目录,提交代码并通过npm version更新版本(更新package.json中的版本号);
g.在当前分支(a步骤切换到master)push代码,然后checkout dev分支,并rebase master分支,最后push代码;
h.如果version为beta,则通过npm publis --tag打上标签,否则直接publish -
node build/bin/gen-indices.js
element algolisearch
利用algoliasearch进行搜索,需要把examples/docs/下的.md文件内容以一定格式上传给algolia
-
sh build/deploy-faas.sh
a.在build目录下,新建temp_web目录;
b.执行npm run deploy:build;
b1. npm run build:file:见前文,主要处理icon、生成src/index和国际化相关;
b2. webpack --config build/webpack.demo.js:见下文,主要用于生成或更新example目录;
b3. echo element.eleme.io>>examples/element-ui/CNAME":examples/element-ui/CNAME文件中写入
element.eleme.io
:Managing a custom domain for your GitHub Pages site
c.克隆element的gh-pages分支(可以通过http://elemefe.github.io/element/访问,实际会根据CNAME文件的设置,路由到element.eleme.io
,在这里进行cname查询),并进入element目录;
d.根据版本号新建目录,如2.13,然后将第b步中输出目录(examples/element-ui)里的内容拷贝到新建目录(2.13)里;
e.部署:faas deploy alpha -P element
1.1.3 dev
npm run build:file &&
cross-env NODE_ENV=development webpack-dev-server
--config build/webpack.demo.js & node build/bin/template.js
"
这块主要功能是启动example,如下图:
example
主要看build/webpack.demo.js
,其除了通过设置入口文件entry.js(引入组件、搭建网站)外,比较重要的两点就是:
a.在examples/route.config.js中动态生成如上图展示的左侧菜单,点击不同组件名称,加载对应的examples/docs/*/下的对应的组件markdown文件;
b.而markdown文件就是用相关的npm工具,对markdown文件进行处理,生成上图右侧区域的内容:
{
test: /\.md$/,
use: [
{
loader: 'vue-loader',
options: {
compilerOptions: {
preserveWhitespace: false
}
}
},
{
loader: path.resolve(__dirname, './md-loader/index.js')
}
]
}
markdown文件额外定制了特殊的语法或者标记,用于解析,如:::demo、```html等。
markdown文件对应的loader:
md-loader的原理很简单,输入是文件的原始内容,返回的是经过 loader 处理后的内容。对于 md-loader,输入的是 .md 文档,输出的则是一个 Vue SFC 格式的字符串,这样它的输出就可以作为下一个 vue-loader 的输入做处理了。
我们来简单看一下 md-loader 中间处理过程。首先执行了 md.render(source) 对 md 文档解析,提取文档中 :::demo {content} ::: 内容,分别生成一些 Vue 的模板字符串,然后再从这个模板字符串中循环查找 包裹的内容,从中提取模板字符串到 output 中,提取 script 到 componenetsString 中,然后构造 pageScript,最后返回的内容就是:
return ` <template> <section class="content element-doc"> ${output.join('')} </section> </template> ${pageScript} ` ;
最终生成的字符串满足我们通常编写的 .vue SFC 格式,它会作为下一个 vue-loader 的输入,所以这样我们就相当于通过加载一个 .md 格式的文件的方式加载了 Vue 组件。
c.输出目录是examples/element-ui
关于examples详细分析,后面进行。
1.1.4 test
通过karma
测试工具和mocha
, sinon-chai
测试框架进行单元测试
二、src分析
2.1 directives:mousewheel & repeat-click
2.2 locale(国际化)
elementUI——locale,国际化方案
2.3 mixins
elementUI——mixins
2.4 transitions
elementU——transitions
2.5 utils:其他分析文章里穿插介绍
三、组件
组件都放在packages目录下,后面将陆续就写的不错的组件进行分析。
四、主题
五、examples分析
examples website上图各页面实际是对应着examples目录,提供了
指南
说明、组件
展示功能、主题
定制、资源
工具和语言切换
功能。