ElementUI 原始碼簡析——原始碼結構篇
ElementUI 作為當前運用的最廣的 Vue PC 端元件庫,很多 Vue 元件庫的架構都是參照 ElementUI 做的。作為一個有夢想的前端(鹹魚),當然需要好好學習一番這套比較成熟的架構。
目錄結構解析
首先,我們先來看看 ElementUI 的目錄結構,總體來說,ElementUI 的目錄結構與 vue-cli2
相差不大:
- .github:存放貢獻指南以及 issue、PR 模板,這些是一個成熟的開源專案必須具備的。
- build:毫無疑問,看資料夾名稱就知道是存放打包工具的配置檔案。
- examples:存放 ElementUI 元件示例。
- packages:存放元件原始碼,也是之後原始碼分析的主要目標。
- src:存放入口檔案以及各種輔助檔案。
- test:存放單元測試檔案,合格的單元測試也是一個成熟的開源專案必備的。
- types:存放宣告檔案,方便引入 typescript 寫的專案中,需要在
package.json
中指定 typing 欄位的值為 宣告的入口檔案才能生效。
說完了資料夾目錄,拋開那些常見的 .babelrc
、.eslintc
等檔案,我們來看看根目錄下的幾個看起來比較奇怪的檔案:
- .travis.yml:持續整合(CI)的配置檔案,它的作用就是在程式碼提交時,根據該檔案執行對應指令碼,成熟的開源專案必備之一。
- CHANGELOG:更新日誌,土豪的 ElementUI 準備了 4 個不同語言版本的更新日誌。
- components.json:配置檔案,標註了元件的檔案路徑,方便 webpack 打包時獲取元件的檔案路徑。
- element_logo.svg:ElementUI 的圖示,使用了 svg 格式,合理使用 svg 檔案,可以大大減少圖片大小。
- FAQ.md:ElementUI 開發者對常見問題的解答。
- LICENSE:開源許可證,ElementUI 使用的是 MIT 協議,使用 ElementUI 進行二次開發的開發者建議注意該檔案。
- Makefile:在 .github 資料夾下的貢獻指南中提到過,元件開發規範中的第一條:通過
make new
建立元件目錄結構,包含測試程式碼、入口檔案、文件make new
就是make
命令中的一種。make
命令是一個工程化編譯工具,而 Makefile 定義了一系列的規則來制定檔案變異操作,常常使用 Linux 的同學應該不會對 Makefile 感到陌生。
入口檔案解析
接下來,我們來看看專案的入口檔案。正如前面所說的,入口檔案就是 src/index.js
:
/* Automatically generated by './build/bin/build-entry.js' */
import Pagination from '../packages/pagination/index.js';
// ...
// 引入元件
const components = [
Pagination,
Dialog,
// ...
// 元件名稱
];
const install = function(Vue, opts = {}) {
// 國際化配置
locale.use(opts.locale);
locale.i18n(opts.i18n);
// 批量全域性註冊元件
components.forEach(component => {
Vue.component(component.name, component);
});
// 全域性註冊指令
Vue.use(InfiniteScroll);
Vue.use(Loading.directive);
// 全域性設定尺寸
Vue.prototype.$ELEMENT = {
size: opts.size || '',
zIndex: opts.zIndex || 2000
};
// 在 Vue 原型上掛載方法
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: '2.9.1',
locale: locale.use,
i18n: locale.i18n,
install,
CollapseTransition,
// 匯出元件
};
總體來說,入口檔案十分簡單易懂。由於使用 Vue.use
方法呼叫外掛時,會自動呼叫 install
函式,所以只需要在 install
函式中批量全域性註冊各種指令、元件,掛載全域性方法即可。
ElementUI 的入口檔案有兩點十分值得我們學習:
- 初始化時,提供選項用於配置全域性屬性,大大方便了元件的使用,具體的可以參考我之前的那篇文章。
- 自動化生成入口檔案
自動化生成入口檔案
下面我們來聊聊自動化生成入口檔案,在此之前,有幾位同學發現了入口檔案是自動化生成的?說來羞愧,我也是在寫這篇文章的時候才發現入口檔案是自動化生成的。
我們先來看看入口檔案的第一句話:
/* Automatically generated by './build/bin/build-entry.js' */
這句話告訴我們,該檔案是由 build/bin/build-entry.js
生成的,所以我們來到該檔案:
var Components = require('../../components.json');
var fs = require('fs');
var render = require('json-templater/string');
var uppercamelcase = require('uppercamelcase');
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(InfiniteScroll);
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 = [];
// 根據 components.json 檔案批量生成模板所需的引數
ComponentNames.forEach(name => {
var componentName = uppercamelcase(name);
includeComponentTemplate.push(render(IMPORT_TEMPLATE, {
name: componentName,
package: name
}));
if (['Loading', 'MessageBox', 'Notification', 'Message', 'InfiniteScroll'].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);
build-entry.js
使用了 json-templater
來生成了入口檔案。在這裡,我們不關注 json-templater
的用法,僅僅研究這個檔案的思想。
它通過引入 components.json
這個我們前面提到過的靜態檔案,批量生成了元件引入、註冊的程式碼。這樣做的好處是什麼?我們不再需要每新增或刪除一個元件,就在入口檔案中進行多處修改,使用自動化生成入口檔案之後,我們只需要修改一處即可。
另外,再說一個鬼故事:之前提到的 components.json
檔案也是自動化生成的。由於本文篇幅有限,接下來就需要同學們自己去鑽研啦。
總結
壞的程式碼各有不同,但是好的程式碼思想總是一致的,那就是高效能易維護,隨著一個專案程式碼量越來越大,在很多時候,易維護的程式碼甚至比高效能但是難以維護的程式碼更受歡迎,高內聚低耦合的思想無論在何時都不會過時。
我一直堅信,我們學習各種原始碼不是為了盲目模仿它們的寫法,而是為了學習它們的思想。畢竟,程式碼的寫法很快就會被更多更優秀的寫法替代,但是這些思想將是最寶貴的財富