美文网首页elementUI
ElementUI的结构与源码研究(未完待续)

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

作者: videring | 来源:发表于2020-04-20 17:55 被阅读0次

    说明:本文基于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 技术揭秘 - 组件库的整体设计

    相关文章

      网友评论

        本文标题:ElementUI的结构与源码研究(未完待续)

        本文链接:https://www.haomeiwen.com/subject/doyrihtx.html