ElementUI 源碼分析1 - 構(gòu)建篇

ElementUI 是一套為開(kāi)發(fā)者馏鹤、設(shè)計(jì)師和產(chǎn)品經(jīng)理準(zhǔn)備的基于 Vue 2.0 的桌面端組件庫(kù)米绕。

0垢村、前言

老規(guī)矩割疾,帶著問(wèn)題看源碼:

  • 組件全量引入和按需引入是如何做的?
  • 主題是如何實(shí)現(xiàn)定制的嘉栓?
  • 國(guó)際化是如何實(shí)現(xiàn)的宏榕?
  • 怎樣支持CDN引入和基于webpack的兩種開(kāi)發(fā)模式?
  • 開(kāi)發(fā)組件時(shí)侵佃,組件MD文檔是如何處理的麻昼?

1、目錄結(jié)構(gòu)

  • 基本結(jié)構(gòu)
    build:存放構(gòu)建相關(guān)的 shell 腳本和 js 腳本
    examples:Element 官方網(wǎng)站前端代碼
    packages:組件庫(kù)代碼
    src:官方網(wǎng)站的入口文件和一些公用代碼馋辈,如utils,mixins,directives,transitions等
    test:?jiǎn)卧獪y(cè)試代碼
    types:類(lèi)型定義文件(typescript)
    注意這里沒(méi)有最終編譯生成的文件夾 lib抚芦,源碼都這樣,得運(yùn)行腳本來(lái)構(gòu)建lib
  • package.json
  // 待發(fā)布的npm包由哪些目錄組成
"files": [
  "lib",
  "src",
  "packages",
  "types"
],
// npm 包的入口
"main": "lib/element-ui.common.js",
// 類(lèi)型定義入口
"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迈螟、構(gòu)建腳本分析

2.1叉抡、npm run dev

  • npm run build:file 并行執(zhí)行以下四個(gè)js腳本
    1、node build/bin/iconInit.js
    通過(guò) postcss 解析 icon.scss 答毫,篩選出類(lèi)名并最終導(dǎo)出到 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
構(gòu)建 src/index.js 這個(gè)文件洗搂,這個(gè)文件可能隨著組件的增加刪除會(huì)經(jīng)常變動(dòng)消返,故用腳本來(lái)產(chǎn)生

var Components = require('../../components.json'); // 所有可用組件的映射表(組件名=>組件定義)
var fs = require('fs');
var render = require('json-templater/string'); // 模板渲染工具
var uppercamelcase = require('uppercamelcase'); // 轉(zhuǎn)駝峰 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);

缺點(diǎn):components.json需要自行維護(hù),不夠自動(dòng)化

3蚕脏、node build/bin/i18n.js
以 i18n/page.json 作為數(shù)據(jù)侦副,以 pages/templates 作為模版來(lái)生成 pages 目錄下的多語(yǔ)言版本。官方網(wǎng)站支持多語(yǔ)言版本就是這么來(lái)的

'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 版本號(hào)到examples/version.json秦驯,這個(gè)需要再官方網(wǎng)站上切換展示

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 并行執(zhí)行
    1、node build/bin/template.js
    監(jiān)聽(tīng) examples/pages/template 下文件的變化并運(yùn)行 npm run i18n 重新生成多語(yǔ)言版本的 pages
const path = require('path');
const templates = path.resolve(process.cwd(), './examples/pages/template');

const chokidar = require('chokidar'); // 專(zhuān)門(mén)用于文件監(jiān)控的庫(kù)
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
這個(gè)就是正式啟動(dòng)本地開(kāi)發(fā)模式了译隘,內(nèi)容就不說(shuō)了

2.2亲桥、分析 npm run dist

  • npm run clean && npm run build:file && npm run lint
    同上,略過(guò)
  • webpack --config build/webpack.conf.js
    構(gòu)建入口為src/index.js ; 出口為 lib/index.js 用于打出UMD格式的包固耘,供CDN方式引入
<!-- 引入樣式 -->
<link rel="stylesheet" >
<!-- 引入組件庫(kù) -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>

這里 index.css 的生成請(qǐng)看 npm run build:theme 的分析

  • npm run build:theme
    1题篷、node build/bin/gen-cssfile
    產(chǎn)生 index.scss / index.css 文件,這個(gè)文件引入了所有組件的 scss/css 文件
    2厅目、gulp build --gulpfile packages/theme-chalk/gulpfile.js
    編譯 scss 文件為 css 文件番枚,包括各組件的 css 文件和一個(gè)總的 css 文件
    3、cp-cli packages/theme-chalk/lib lib/theme-chalk
    復(fù)制 packages/theme-chalk/lib 至 lib/theme-chalk
  • webpack --config build/webpack.component.js
    構(gòu)建入口為 components.json ; 出口為 lib/[name].js 用于將 packages 中的所有組件單獨(dú)打出一個(gè) js 文件用于做按需加載
  • webpack --config build/webpack.common.js
    構(gòu)建入口為src/index.js ; 出口為 lib/element-ui.common.js 用于打出commonjs格式的包损敷,用以完全導(dǎo)入方式使用葫笼,產(chǎn)生的 element-ui.common.js 也是 package.json 的 main 入口
  • npm run build:utils
    將 src 目錄下除 index.js 外的所有文件 Babel 編譯到 lib 目錄下。算是除了組件庫(kù)以外拗馒,額外提供了一些小工具供開(kāi)發(fā)者使用路星,如:
import { kebabCase } from 'element-ui/src/utils/util';
  • npm run build:umd
    將 src/locale/lang 下的ES6格式的文件轉(zhuǎn)為UMD格式,放在 lib/umd/locale诱桂。用于CDN方式加載洋丐。

3、小結(jié)&收獲

小結(jié)

回答下開(kāi)頭的問(wèn)題:

  • 組件全量引入和按需引入是如何做的挥等?
    如果是 cdn 方式來(lái)加載友绝,則只能全量引入。如果是用 webpack 這種工程方式引入肝劲,則兩種方式都可以九榔,其中按需引入借助了 babel-plugin-component
// .babelrc
{
  "presets": [["es2015", { "modules": false }]],
  "plugins": [
    [
      "component",
      {
        "libraryName": "element-ui",
        "styleLibraryName": "theme-chalk"
      }
    ]
  ]
}
// 上述配置會(huì)轉(zhuǎn)換以下代碼
import { Button } from 'element-ui';
// 轉(zhuǎn)為
import Button from 'element-ui/lib/button.js'
import Button from 'element-ui/lib/theme-chalk/button.css'
  • 主題是如何實(shí)現(xiàn)定制的?
    有兩種主要方式:1涡相、如果使用scss哲泊,則是通過(guò)修改 scss 變量來(lái)實(shí)現(xiàn)主題定制;2催蝗、如果使用css切威,則手動(dòng)引入定制好的css文件來(lái)替換默認(rèn)的css文件
  • 組件國(guó)際化是如何實(shí)現(xiàn)的?
    將組件中的使用的文本抽離出來(lái)丙号,然后用各種不同的語(yǔ)言去填充即可實(shí)現(xiàn)先朦。難點(diǎn)在于怎樣提供多語(yǔ)言版本的文件
  • 怎樣支持CDN引入和基于webpack的兩種開(kāi)發(fā)模式?
    一套源碼打兩套格式的包犬缨,一種umd格式喳魏,一種 commonjs2 格式。
  • 開(kāi)發(fā)組件時(shí)怀薛,組件MD文檔是如何處理的刺彩?
    ElementUI 開(kāi)發(fā)了一個(gè) md-loader 來(lái)把 .md 文檔封裝成 .vue 組件,實(shí)現(xiàn)了組件文檔的渲染

收獲

  • postcss.parse 可以將 scss 文件內(nèi)容處理成 js 對(duì)象,再通過(guò) postcss.stringify 轉(zhuǎn)回 scss 文件创倔。放便對(duì)scss文件做批處理
  • 可通過(guò) require('child_process').execSync(cmd).toString().trim() 來(lái)獲取 shell 腳本執(zhí)行的結(jié)果
  • cross-env 設(shè)置環(huán)境變量可屏蔽 mac 和 window 系統(tǒng)的差異
  • commonjs , commonjs2 區(qū)別(一個(gè)用 exports導(dǎo)出嗡害,一個(gè)用module.exports,所以我們平時(shí)用的都是commonjs2)
commonjs: exports['MyLibrary'] = entry_return
commonjs2: module.exports = entry_return
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末畦攘,一起剝皮案震驚了整個(gè)濱河市霸妹,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌知押,老刑警劉巖叹螟,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異台盯,居然都是意外死亡首妖,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)爷恳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人象踊,你說(shuō)我怎么就攤上這事温亲。” “怎么了杯矩?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵栈虚,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我史隆,道長(zhǎng)魂务,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任泌射,我火速辦了婚禮粘姜,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘熔酷。我一直安慰自己孤紧,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布拒秘。 她就那樣靜靜地躺著号显,像睡著了一般。 火紅的嫁衣襯著肌膚如雪躺酒。 梳的紋絲不亂的頭發(fā)上押蚤,一...
    開(kāi)封第一講書(shū)人閱讀 49,031評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音羹应,去河邊找鬼揽碘。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的钾菊。 我是一名探鬼主播帅矗,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼煞烫!你這毒婦竟也來(lái)了浑此?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤滞详,失蹤者是張志新(化名)和其女友劉穎凛俱,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體料饥,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蒲犬,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了岸啡。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片原叮。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖巡蘸,靈堂內(nèi)的尸體忽然破棺而出奋隶,到底是詐尸還是另有隱情,我是刑警寧澤悦荒,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布唯欣,位于F島的核電站,受9級(jí)特大地震影響搬味,放射性物質(zhì)發(fā)生泄漏境氢。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一碰纬、第九天 我趴在偏房一處隱蔽的房頂上張望萍聊。 院中可真熱鬧,春花似錦悦析、人聲如沸脐区。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)牛隅。三九已至,卻和暖如春酌泰,著一層夾襖步出監(jiān)牢的瞬間媒佣,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工陵刹, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留默伍,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像也糊,于是被迫代替她去往敵國(guó)和親炼蹦。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 前言 本文主要從webpack4.x入手狸剃,會(huì)對(duì)平時(shí)常用的Webpack配置一一講解掐隐,各個(gè)功能點(diǎn)都有對(duì)應(yīng)的詳細(xì)例子,...
    BetterChen閱讀 1,943評(píng)論 0 3
  • 目錄第1章 webpack簡(jiǎn)介 11.1 webpack是什么钞馁? 11.2 官網(wǎng)地址 21.3 為什么使用 web...
    lemonzoey閱讀 1,731評(píng)論 0 1
  • 在現(xiàn)在的前端開(kāi)發(fā)中虑省,前后端分離、模塊化開(kāi)發(fā)僧凰、版本控制探颈、文件合并與壓縮、mock數(shù)據(jù)等等一些原本后端的思想開(kāi)始...
    Charlot閱讀 5,431評(píng)論 1 32
  • 源碼地址:https://github.com/h2huanghui/WEBPACK-BASE 一训措、概念 webp...
    smartHui閱讀 1,736評(píng)論 0 1
  • 生活到底會(huì)去到何方伪节?一直在我的腦海里。一直的尋尋覓覓绩鸣,似乎也未曾找到答案怀大。二寶的到來(lái)更是打破了生活的原本...
    石華月閱讀 148評(píng)論 0 1