ElementUI 源码分析1 - 构建篇

ElementUI 是一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库。

0、前言

老规矩,带着问题看源码:

  • 组件全量引入和按需引入是如何做的?
  • 主题是如何实现定制的?
  • 国际化是如何实现的?
  • 怎样支持CDN引入和基于webpack的两种开发模式?
  • 开发组件时,组件MD文档是如何处理的?

1、目录结构

  • 基本结构
    build:存放构建相关的 shell 脚本和 js 脚本
    examples:Element 官方网站前端代码
    packages:组件库代码
    src:官方网站的入口文件和一些公用代码,如utils,mixins,directives,transitions等
    test:单元测试代码
    types:类型定义文件(typescript)
    注意这里没有最终编译生成的文件夹 lib,源码都这样,得运行脚本来构建lib
  • package.json
  // 待发布的npm包由哪些目录组成
"files": [
  "lib",
  "src",
  "packages",
  "types"
],
// npm 包的入口
"main": "lib/element-ui.common.js",
// 类型定义入口
"typings": "types/index.d.ts",
"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 build/webpack.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 build/webpack.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 build/webpack.demo.js",
    "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",
    "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"
  },

2、构建脚本分析

2.1、npm run dev

  • npm run build:file 并行执行以下四个js脚本
    1、node build/bin/iconInit.js
    通过 postcss 解析 icon.scss ,筛选出类名并最终导出到 icon.json 文件
// node build/bin/iconInit.js
'use strict';

var postcss = require('postcss');
var fs = require('fs');
var path = require('path');
var fontFile = fs.readFileSync(path.resolve(__dirname, '../../packages/theme-chalk/src/icon.scss'), 'utf8');
var nodes = postcss.parse(fontFile).nodes;
var classList = [];

nodes.forEach((node) => {
  var selector = node.selector || '';
  var reg = new RegExp(/\.el-icon-([^:]+):before/);
  var arr = selector.match(reg);

  if (arr && arr[1]) {
    classList.push(arr[1]);
  }
});

classList.reverse(); // 希望按 css 文件顺序倒序排列

fs.writeFile(path.resolve(__dirname, '../../examples/icon.json'), JSON.stringify(classList), () => {});

// 效果:
// icon.scss 部分
.el-icon-platform-eleme:before {
  content: "\e7ca";
}
// 生成的 icon.json
['platform-eleme']

至于生成的 icon.json 有啥用先不管。

2、node build/bin/build-entry.js
构建 src/index.js 这个文件,这个文件可能随着组件的增加删除会经常变动,故用脚本来产生

var Components = require('../../components.json'); // 所有可用组件的映射表(组件名=>组件定义)
var fs = require('fs');
var render = require('json-templater/string'); // 模板渲染工具
var uppercamelcase = require('uppercamelcase'); // 转驼峰 a-bc =>ABc
var path = require('path');
var endOfLine = require('os').EOL;

var OUTPUT_PATH = path.join(__dirname, '../../src/index.js');
var IMPORT_TEMPLATE = 'import {{name}} from \'../packages/{{package}}/index.js\';';
var INSTALL_COMPONENT_TEMPLATE = '  {{name}}';
var MAIN_TEMPLATE = `/* Automatically generated by './build/bin/build-entry.js' */

{{include}}
import locale from 'element-ui/src/locale';
import CollapseTransition from 'element-ui/src/transitions/collapse-transition';

const components = [
{{install}},
  CollapseTransition
];

const install = function(Vue, opts = {}) {
  locale.use(opts.locale);
  locale.i18n(opts.i18n);

  components.forEach(component => {
    Vue.component(component.name, component);
  });

  Vue.use(Loading.directive);

  Vue.prototype.$ELEMENT = {
    size: opts.size || '',
    zIndex: opts.zIndex || 2000
  };

  Vue.prototype.$loading = Loading.service;
  Vue.prototype.$msgbox = MessageBox;
  Vue.prototype.$alert = MessageBox.alert;
  Vue.prototype.$confirm = MessageBox.confirm;
  Vue.prototype.$prompt = MessageBox.prompt;
  Vue.prototype.$notify = Notification;
  Vue.prototype.$message = Message;

};

/* istanbul ignore if */
if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue);
}

export default {
  version: '{{version}}',
  locale: locale.use,
  i18n: locale.i18n,
  install,
  CollapseTransition,
  Loading,
{{list}}
};
`;

delete Components.font;

var ComponentNames = Object.keys(Components);

var includeComponentTemplate = [];
var installTemplate = [];
var listTemplate = [];

ComponentNames.forEach(name => {
  var componentName = uppercamelcase(name);

  includeComponentTemplate.push(render(IMPORT_TEMPLATE, {
    name: componentName,
    package: name
  }));

  if (['Loading', 'MessageBox', 'Notification', 'Message'].indexOf(componentName) === -1) {
    installTemplate.push(render(INSTALL_COMPONENT_TEMPLATE, {
      name: componentName,
      component: name
    }));
  }

  if (componentName !== 'Loading') listTemplate.push(`  ${componentName}`);
});

var template = render(MAIN_TEMPLATE, {
  include: includeComponentTemplate.join(endOfLine),
  install: installTemplate.join(',' + endOfLine),
  version: process.env.VERSION || require('../../package.json').version,
  list: listTemplate.join(',' + endOfLine)
});

fs.writeFileSync(OUTPUT_PATH, template);
console.log('[build entry] DONE:', OUTPUT_PATH);

缺点:components.json需要自行维护,不够自动化

3、node build/bin/i18n.js
以 i18n/page.json 作为数据,以 pages/templates 作为模版来生成 pages 目录下的多语言版本。官方网站支持多语言版本就是这么来的

'use strict';

var fs = require('fs');
var path = require('path');
var langConfig = require('../../examples/i18n/page.json');

langConfig.forEach(lang => {
  try {
    fs.statSync(path.resolve(__dirname, `../../examples/pages/${ lang.lang }`));
  } catch (e) {
    fs.mkdirSync(path.resolve(__dirname, `../../examples/pages/${ lang.lang }`));
  }

  Object.keys(lang.pages).forEach(page => {
    var templatePath = path.resolve(__dirname, `../../examples/pages/template/${ page }.tpl`);
    var outputPath = path.resolve(__dirname, `../../examples/pages/${ lang.lang }/${ page }.vue`);
    var content = fs.readFileSync(templatePath, 'utf8');
    var pairs = lang.pages[page];

    Object.keys(pairs).forEach(key => {
      content = content.replace(new RegExp(`<%=\\s*${ key }\\s*>`, 'g'), pairs[key]);
    });

    fs.writeFileSync(outputPath, content);
  });
});

4、node build/bin/version.js
记录 Element 版本号到examples/version.json,这个需要再官方网站上切换展示

var fs = require('fs');
var path = require('path');
var version = process.env.VERSION || require('../../package.json').version;
var content = { '1.4.13': '1.4', '2.0.11': '2.0', '2.1.0': '2.1', '2.2.2': '2.2', '2.3.9': '2.3', '2.4.11': '2.4', '2.5.4': '2.5', '2.6.3': '2.6', '2.7': '2.7.2' };
if (!content[version]) content[version] = '2.8';
fs.writeFileSync(path.resolve(__dirname, '../../examples/versions.json'), JSON.stringify(content));
  • webpack-dev-server --config build/webpack.demo.js 与 node build/bin/template.js 并行执行
    1、node build/bin/template.js
    监听 examples/pages/template 下文件的变化并运行 npm run i18n 重新生成多语言版本的 pages
const path = require('path');
const templates = path.resolve(process.cwd(), './examples/pages/template');

const chokidar = require('chokidar'); // 专门用于文件监控的库
let watcher = chokidar.watch([templates]);

watcher.on('ready', function() {
  watcher
    .on('change', function() {
      exec('npm run i18n');
    });
});

function exec(cmd) {
  return require('child_process').execSync(cmd).toString().trim();
}

2、build/webpack.demo.js
这个就是正式启动本地开发模式了,内容就不说了

2.2、分析 npm run dist

  • npm run clean && npm run build:file && npm run lint
    同上,略过
  • webpack --config build/webpack.conf.js
    构建入口为src/index.js ; 出口为 lib/index.js 用于打出UMD格式的包,供CDN方式引入
<!-- 引入样式 -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<!-- 引入组件库 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>

这里 index.css 的生成请看 npm run build:theme 的分析

  • npm run build:theme
    1、node build/bin/gen-cssfile
    产生 index.scss / index.css 文件,这个文件引入了所有组件的 scss/css 文件
    2、gulp build --gulpfile packages/theme-chalk/gulpfile.js
    编译 scss 文件为 css 文件,包括各组件的 css 文件和一个总的 css 文件
    3、cp-cli packages/theme-chalk/lib lib/theme-chalk
    复制 packages/theme-chalk/lib 至 lib/theme-chalk
  • webpack --config build/webpack.component.js
    构建入口为 components.json ; 出口为 lib/[name].js 用于将 packages 中的所有组件单独打出一个 js 文件用于做按需加载
  • webpack --config build/webpack.common.js
    构建入口为src/index.js ; 出口为 lib/element-ui.common.js 用于打出commonjs格式的包,用以完全导入方式使用,产生的 element-ui.common.js 也是 package.json 的 main 入口
  • npm run build:utils
    将 src 目录下除 index.js 外的所有文件 Babel 编译到 lib 目录下。算是除了组件库以外,额外提供了一些小工具供开发者使用,如:
import { kebabCase } from 'element-ui/src/utils/util';
  • npm run build:umd
    将 src/locale/lang 下的ES6格式的文件转为UMD格式,放在 lib/umd/locale。用于CDN方式加载。

3、小结&收获

小结

回答下开头的问题:

  • 组件全量引入和按需引入是如何做的?
    如果是 cdn 方式来加载,则只能全量引入。如果是用 webpack 这种工程方式引入,则两种方式都可以,其中按需引入借助了 babel-plugin-component
// .babelrc
{
  "presets": [["es2015", { "modules": false }]],
  "plugins": [
    [
      "component",
      {
        "libraryName": "element-ui",
        "styleLibraryName": "theme-chalk"
      }
    ]
  ]
}
// 上述配置会转换以下代码
import { Button } from 'element-ui';
// 转为
import Button from 'element-ui/lib/button.js'
import Button from 'element-ui/lib/theme-chalk/button.css'
  • 主题是如何实现定制的?
    有两种主要方式:1、如果使用scss,则是通过修改 scss 变量来实现主题定制;2、如果使用css,则手动引入定制好的css文件来替换默认的css文件
  • 组件国际化是如何实现的?
    将组件中的使用的文本抽离出来,然后用各种不同的语言去填充即可实现。难点在于怎样提供多语言版本的文件
  • 怎样支持CDN引入和基于webpack的两种开发模式?
    一套源码打两套格式的包,一种umd格式,一种 commonjs2 格式。
  • 开发组件时,组件MD文档是如何处理的?
    ElementUI 开发了一个 md-loader 来把 .md 文档封装成 .vue 组件,实现了组件文档的渲染

收获

  • postcss.parse 可以将 scss 文件内容处理成 js 对象,再通过 postcss.stringify 转回 scss 文件。放便对scss文件做批处理
  • 可通过 require('child_process').execSync(cmd).toString().trim() 来获取 shell 脚本执行的结果
  • cross-env 设置环境变量可屏蔽 mac 和 window 系统的差异
  • commonjs , commonjs2 区别(一个用 exports导出,一个用module.exports,所以我们平时用的都是commonjs2)
commonjs: exports['MyLibrary'] = entry_return
commonjs2: module.exports = entry_return
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352

推荐阅读更多精彩内容

  • 前言 本文主要从webpack4.x入手,会对平时常用的Webpack配置一一讲解,各个功能点都有对应的详细例子,...
    BetterChen阅读 1,948评论 0 3
  • 目录第1章 webpack简介 11.1 webpack是什么? 11.2 官网地址 21.3 为什么使用 web...
    lemonzoey阅读 1,733评论 0 1
  • 在现在的前端开发中,前后端分离、模块化开发、版本控制、文件合并与压缩、mock数据等等一些原本后端的思想开始...
    Charlot阅读 5,439评论 1 32
  • 源码地址:https://github.com/h2huanghui/WEBPACK-BASE 一、概念 webp...
    smartHui阅读 1,747评论 0 1
  • 生活到底会去到何方?一直在我的脑海里。一直的寻寻觅觅,似乎也未曾找到答案。二宝的到来更是打破了生活的原本...
    石华月阅读 148评论 0 1