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中主要关注distpubdevtest命令

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.jsonexamples/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.csslib/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-exportstransform-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
    利用algoliasearch进行搜索,需要把examples/docs/下的.md文件内容以一定格式上传给algolia

    element algolisearch

  • 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.ioManaging a custom domain for your GitHub Pages site
c.克隆elementgh-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目录下,后面将陆续就写的不错的组件进行分析。

四、主题

elementUI——主题及自定义

五、examples分析

examples website

上图各页面实际是对应着examples目录,提供了指南说明、组件展示功能、主题定制、资源工具和语言切换功能。

推荐阅读:
ElementUI的构建流程
Element-UI 技术揭秘 - 组件库的整体设计

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345