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 的入口文件有兩點十分值得我們學習:

  1. 初始化時,提供選項用於配置全局屬性,大大方便了組件的使用,具體的可以參考我之前的那篇文章。
  2. 自動化生成入口文件

自動化生成入口文件

下面我們來聊聊自動化生成入口文件,在此之前,有幾位同學發現了入口文件是自動化生成的?說來羞愧,我也是在寫這篇文章的時候才發現入口文件是自動化生成的。

我們先來看看入口文件的第一句話:

/* 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 文件也是自動化生成的。由於本文篇幅有限,接下來就需要同學們自己去鑽研啦。

總結

壞的代碼各有不同,但是好的代碼思想總是一致的,那就是高性能易維護,隨着一個項目代碼量越來越大,在很多時候,易維護的代碼甚至比高性能但是難以維護的代碼更受歡迎,高內聚低耦合的思想無論在何時都不會過時。

我一直堅信,我們學習各種源碼不是為了盲目模仿它們的寫法,而是為了學習它們的思想。畢竟,代碼的寫法很快就會被更多更優秀的寫法替代,但是這些思想將是最寶貴的財富。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM