由于面試需要,先來幾發(fā) element 源碼學習博客。Vue 源碼還將繼續(xù)更新。
好,現(xiàn)在我們開始學習 element —— 最受歡迎的 Vue UI 框架。
package.json
我覺得要看一個前端項目,首先必須得看看 package.json 這個文件。
編譯入口
來看看編譯的入口
"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",
# 構建umd
"build:umd": "node build/bin/build-locale.js"
}
這里我們以看源碼的角度,先了解構建文件命令。其實就是 node 執(zhí)行了幾個 js 腳本。我們深入看下 iconInit、 build-entry、 i18n、 version 這些腳本文件。
// build/bin/build-all.js
'use strict';
const components = require('../../components.json');
const execSync = require('child_process').execSync;
const existsSync = require('fs').existsSync;
const path = require('path');
let componentPaths = [];
delete components.index;
delete components.font;
// 遍歷 components 的 key,找到相應 component 的路徑,將路徑保存到 componentPaths 數(shù)組
Object.keys(components).forEach(key => {
const filePath = path.join(__dirname, `../../packages/${key}/cooking.conf.js`);
if (existsSync(filePath)) {
componentPaths.push(`packages/${key}/cooking.conf.js`);
}
});
// pathA,pathB,pathC
const paths = componentPaths.join(',');
// 拼接為 shell 命令,并調用 execSync 方法執(zhí)行。
const cli = path.join('node_modules', '.bin', 'cooking') + ` build -c ${paths} -p`;
execSync(cli, {
stdio: 'inherit'
});
以上方法主要是獲取所有組件名,然后拼接為 shell 命令,執(zhí)行 shell 命令進行 build。
// 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 = [];
// 遍歷匹配正則,符合則傳入到數(shù)組中
nodes.forEach((node) => {
var selector = node.selector || '';
var reg = new RegExp(/\.el-icon-([^:]+):before/); // 正則: .el-icon-(多個非:字符):before
var arr = selector.match(reg);
if (arr && arr[1]) {
classList.push(arr[1]);
}
});
// 導出 icon.json 文件
fs.writeFile(path.resolve(__dirname, '../../examples/icon.json'), JSON.stringify(classList));
以上方法通過解析 icon.scss 最終導出 icon.json 文件,該文件保存了各種圖標。
// build/bin/i18n.js
'use strict';
var fs = require('fs');
var path = require('path');
// 獲取 page.json
var langConfig = require('../../examples/i18n/page.json');
langConfig.forEach(lang => {
try {
// 獲取文件信息
fs.statSync(path.resolve(__dirname, `../../examples/pages/${ lang.lang }`));
} catch (e) {
// 創(chuàng)建文件夾
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);
});
});
以上代碼是國際化的過程,最終將會在 examples/pages/ 目錄中生成不同語言的內(nèi)容。國際化具體內(nèi)容請參照 國際化。

// build/bin/version.js
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' };
if (!content[version]) content[version] = '2.2';
fs.writeFileSync(path.resolve(__dirname, '../../examples/versions.json'), JSON.stringify(content));
獲取 version,定義了一個 content,如果當前版本不在 content 中,那么再添加一個版本數(shù)據(jù)。由于我學習的版本是 2.2.1,最終生成的結果是:
{"1.4.13":"1.4","2.0.11":"2.0","2.1.0":"2.1","2.2.1":"2.2"}
出現(xiàn)這幾個版本號的原因么,看下官網(wǎng)就能發(fā)現(xiàn)端倪,應該是幾個重要的穩(wěn)定版本。

來看下 build-entry.js 文件
// build/bin/build-entry.js
var Components = require('../../components.json'); // 組件數(shù)據(jù)
var fs = require('fs'); // node文件系統(tǒng)
var render = require('json-templater/string');
var uppercamelcase = require('uppercamelcase'); // 駝峰大小寫寫法
var path = require('path'); // node路徑系統(tǒng)
var endOfLine = require('os').EOL;
// 導出路徑
var OUTPUT_PATH = path.join(__dirname, '../../src/index.js');
// 導入template、安裝組件template、主要template
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.map(component => {
Vue.component(component.name, component);
});
Vue.use(Loading.directive);
const ELEMENT = {};
ELEMENT.size = opts.size || '';
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;
Vue.prototype.$ELEMENT = ELEMENT;
};
/* istanbul ignore if */
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue);
}
module.exports = {
version: '{{version}}',
locale: locale.use,
i18n: locale.i18n,
install,
CollapseTransition,
Loading,
{{list}}
};
module.exports.default = module.exports;
`;
delete Components.font;
// 組件名
var ComponentNames = Object.keys(Components);
var includeComponentTemplate = [];
var installTemplate = [];
var listTemplate = [];
// 遍歷組件名解析template
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}`);
});
// 主要template
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);
以上代碼中,先是定義了三個 template,然后使用 render 方法來渲染這些 template。最后生成一個主要 template 導出為文件。render 函數(shù)中的第二個參數(shù)為 template 中 {{name}} 的數(shù)據(jù)。
這個 render 方法來自 json-templater 庫,這個庫可以將字符串編譯為 js 代碼。
依賴關系
看看 element 都 depend 了些什么?下面對 element 的依賴作了注釋。
"dependencies": {
// 異步驗證器
"async-validator": "~1.8.1",
// vue和jsx合并參數(shù)的語法轉譯器?
"babel-helper-vue-jsx-merge-props": "^2.0.0",
// 深入合并
"deepmerge": "^1.2.0",
// 鼠標滾輪在多個瀏覽器之間的標準化。
"normalize-wheel": "^1.0.1",
// 方法的 Throttle/debounce? https://www.npmjs.com/package/throttle-debounce
"throttle-debounce": "^1.0.1"
},
"peerDependencies": {
// vue核心源碼
"vue": "^2.5.2"
},
"devDependencies": {
// 一個托管的全文、數(shù)字和分面搜索引擎,能夠在第一次按鍵時提供實時結果。
"algoliasearch": "^3.24.5",
// babel
"babel-cli": "^6.14.0",
"babel-core": "^6.14.0",
"babel-loader": "^6.2.5",
"babel-plugin-add-module-exports": "^0.2.1",
"babel-plugin-module-resolver": "^2.2.0",
"babel-plugin-syntax-jsx": "^6.8.0",
"babel-plugin-transform-vue-jsx": "^3.3.0",
"babel-preset-es2015": "^6.14.0",
// chai斷言庫
"chai": "^3.5.0",
// 為服務器專門設計的核心jQuery的快速、靈活、精益的實現(xiàn)。
"cheerio": "^0.18.0",
// node fs工具
"chokidar": "^1.7.0",
// cooking前端構建工具
"cooking": "^1.5.4",
"cooking-lint": "0.1.3",
// 繼承vue2配置項的cooking插件
"cooking-vue2": "^0.3.3",
// 復制webpack插件
"copy-webpack-plugin": "^4.1.1",
// 代碼測試覆蓋率
"coveralls": "^2.11.14",
// 跨平臺支持UNIX命令
"cp-cli": "^1.0.2",
// 運行在平臺上設置和使用環(huán)境變量的腳本。
"cross-env": "^3.1.3",
// css 加載器
"css-loader": "^0.28.7",
// es6 promise支持
"es6-promise": "^4.0.5",
// eslint語法檢測
"eslint": "4.14.0",
"eslint-config-elemefe": "0.1.1",
"eslint-loader": "^1.9.0",
"eslint-plugin-html": "^4.0.1",
"eslint-plugin-json": "^1.2.0",
"extract-text-webpack-plugin": "^3.0.1",
// 文件加載和保存
"file-loader": "^1.1.5",
"file-save": "^0.2.0",
// 將文件發(fā)布到github的 gh-pages 分支
"gh-pages": "^0.11.0",
// gulp打包
"gulp": "^3.9.1",
"gulp-autoprefixer": "^4.0.0",
"gulp-cssmin": "^0.1.7",
"gulp-postcss": "^6.1.1",
"gulp-sass": "^3.1.0",
// js 高亮
"highlight.js": "^9.3.0",
// html加載器
"html-loader": "^0.5.1",
// html webpack插件
"html-webpack-plugin": "^2.30.1",
// A Webpack loader for injecting code into modules via their dependencies
"inject-loader": "^3.0.1",
// isparta instrumenter loader for webpack,用于測試
"isparta-loader": "^2.0.0",
// json加載器
"json-loader": "^0.5.7",
// json和js的模板生成工具
"json-templater": "^1.0.4",
// karma測試庫
"karma": "^1.3.0",
"karma-chrome-launcher": "^2.2.0",
"karma-coverage": "^1.1.1",
"karma-mocha": "^1.2.0",
"karma-sinon-chai": "^1.2.4",
"karma-sourcemap-loader": "^0.3.7",
"karma-spec-reporter": "0.0.26",
"karma-webpack": "^1.8.0",
// 用于管理具有多個包的JavaScript項目的工具。
"lerna": "^2.0.0-beta.32",
// 模擬時間工具
"lolex": "^1.5.1",
// markdown解析器
"markdown-it": "^6.1.1",
"markdown-it-anchor": "^2.5.0",
"markdown-it-container": "^2.0.0",
// mocha測試庫
"mocha": "^3.1.1",
// node.js 的 sass
"node-sass": "^4.5.3",
// 視差滾動 https://perspective.js.org/#/zh-cn/
"perspective.js": "^1.0.0",
// postcss
"postcss": "^5.1.2",
"postcss-loader": "0.11.1",
"postcss-salad": "^1.0.8",
// node深度刪除模塊
"rimraf": "^2.5.4",
// sass加載器
"sass-loader": "^6.0.6",
// sinon測試框架
"sinon": "^1.17.6",
"sinon-chai": "^2.8.0",
// 樣式加載器
"style-loader": "^0.19.0",
// utf-8 字符轉換
"transliteration": "^1.1.11",
// 駝峰寫法
"uppercamelcase": "^1.1.0",
"url-loader": "^0.6.2",
// vue
"vue": "^2.5.2",
"vue-loader": "^13.3.0",
"vue-markdown-loader": "1",
"vue-router": "2.7.0",
"vue-template-compiler": "^2.5.2",
"vue-template-es2015-compiler": "^1.6.0",
// webpack
"webpack": "^3.7.1",
"webpack-dev-server": "^2.9.1",
"webpack-node-externals": "^1.6.0"
}
阿西吧,這依賴庫真心多~~不知道他們?nèi)绾握业竭@么多庫的。
src目錄
再來看看項目結構部分。按常理源碼肯定是放在 src 目錄中的,我們找到 src/index.js。代碼有點長,只貼出 install 方法部分了。說下都干了什么:導入所有組件,定義安裝方法,判斷環(huán)境執(zhí)行 install 方法,最后整體導出。
const install = function(Vue, opts = {}) {
locale.use(opts.locale);
locale.i18n(opts.i18n);
components.map(component => {
// 遍歷將組件加入到Vue中
Vue.component(component.name, component);
});
// 加載中
Vue.use(Loading.directive);
const ELEMENT = {};
ELEMENT.size = opts.size || '';
// 定義Vue的原型 prototype
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;
Vue.prototype.$ELEMENT = ELEMENT;
};
/* istanbul ignore if */
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue);
}
從組件的導入 import Button from '../packages/button/index.js'; 可以看到所有組件都是在 packages 目錄下的。這部分我們會在之后重點學習。
那么問題來了,既然組件都在 packages 中了,那么 src 目錄下都干了些什么呢?來看看各個目錄的功能:
- directive 實現(xiàn)滾輪優(yōu)化和避免重復點擊。
- locale 用于 i18n 國際化功能。
- mixins 看樣子應該是用于混合到 Vue 實例的 options 中的。
- transition 在渲染是操作style做過渡效果處理。
- utils 工具文件夾。
主要目的是項目結構,就不深入展開了。如有需要后面再講。
其他目錄
上面說過,package 目錄中存放了所有 component 組件的代碼。另外也存放了組件的樣式 .scss 文件。
而對于type目錄中,存放的 .ts 文件。都是 TypeScript 文件。但是有個問題,我不太清楚這些 .ts 都用在何處。而且在 package.json 中也未導入 TypeScript 的庫,只是在更新日志中有 新增 TypeScript 類型聲明 這么一句話。這點有所疑惑。
test 目錄下是各個組件的單元測試用例,這部分是學習單元測試寫法的很好的參考代碼(我學習測試框架就是在這里學的)。需要學習單元測試的可以深入看看。
example 目錄下是 element 的示例項目。我們的目的是學習源碼,所以這部分先忽略~
最后
簡單了解了下項目的編譯、項目的依賴庫情況、項目的機構。下一篇開始學習一些組件的實現(xiàn)。逐步深入扒開element的神秘面紗。
打個廣告
上海鏈家-鏈家上海研發(fā)中心需求大量前端、后端、測試,需要內(nèi)推請將簡歷發(fā)送至 dingxiaojie001@ke.com。