elementUI

ElementUI的结构与源码研究(未完待续)

2020-04-20  本文已影响0人  videring

说明:本文基于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主要关注点

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
"
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);,
这种是相当于直接引入编译前的源码,省去了按组件编译的过程。

(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;
  }
})(......

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
"

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 技术揭秘 - 组件库的整体设计

上一篇下一篇

猜你喜欢

热点阅读