圖片來源:picography.co/rocket-lift…javascript
本文做者:潘萬強html
在這裏「增量」這個概念的對立面是「全量」。在 Linux 系統中當須要備份數據或者跨服務器同步文件時,會用到一個叫 rsync 的工具,它的速度會比 scp/cp 命令更快,由於它會先判斷已經存在的數據和新數據的差別,只傳輸不一樣的部分,即「增量」同步。前端
在前端開發工程化領域,本文將介紹用「增量」思想提高代碼檢查、打包構建環節的速度,從而實現開發過程的效率提高。java
前端使用 ESLint 作代碼規範靜態檢查。隨着前端工程化的發展,咱們會將代碼檢查與開發工做流集成,在代碼提交前和代碼交付前自動作 ESLint 檢查。代碼提交檢查即在開發者每一次 commit 時經過 git hooks 觸發 ESLint 檢查,當工程代碼量很大時開發者每一次提交代碼甚至要等數分鐘時間才能檢查完。代碼交付檢查即藉助持續集成流程,好比在 MR 時觸發代碼檢查,這是會阻斷 MR 的流程的,常常會出現這樣一種狀況,某個 MR 僅僅修改了一行代碼,卻要掃瞄整個項目,這會嚴重影響持續集成的效率。因此大部分狀況下並不須要進行 ESLint 的全量掃描,咱們更關心的是新增代碼是否存在問題。node
接下來咱們經過自定義 git 的 pre-commit 鉤子腳原本爲一個工程實現增量代碼提交檢查能力。linux
本腳本中 ESLint 檢查執行到文件這一粒度。實現增量代碼檢查首先就是要能找到增量代碼,即修改了哪些文件。咱們藉助 git 版本管理工具尋找提交時暫存區和 HEAD 之間的差別,找到修改的文件列表。webpack
git diff
找到本次提交修改的文件,加 --diff-filter=ACMR
參數是爲了去掉被刪除的文件,刪除的文件不須要再作檢查了;const exec = require('child_process').exec;
const GITDIFF = 'git diff --cached --diff-filter=ACMR --name-only';
// 執行 git 的命令
exec(GITDIFF, (error, stdout) => {
if (error) {
console.error(`exec error: ${error}`);
}
// 對返回結果進行處理,拿到要檢查的文件列表
const diffFileArray = stdout.split('\n').filter((diffFile) => (
/(\.js|\.jsx)(\n|$)/gi.test(diffFile)
));
console.log('待檢查的文件:', diffFileArray);
});
複製代碼
ESLint 提供了同名類函數(ESLint)做爲 Node.js API 調用(低於 7.0.0 版本使用 CLIEngine 類),這樣咱們能在 node 腳本中執行代碼檢查並拿到檢查結果。git
const { ESLint } = require('eslint');
const linter = new ESLint();
// 上文拿到待檢查的文件列表後
let errorCount = 0;
let warningCount = 0;
if (diffFileArray.length > 0) {
// 執行ESLint代碼檢查
const eslintResults = linter.lintFiles(diffFileArray).results;
// 對檢查結果進行處理,提取報錯數和警告數
eslintResults.forEach((result) => {
// result的數據結構以下:
// {
// filePath: "xxx/index.js",
// messages: [{
// ruleId: "semi",
// severity: 2,
// message: "Missing semicolon.",
// line: 1,
// column: 13,
// nodeType: "ExpressionStatement",
// fix: { range: [12, 12], text: ";" }
// }],
// errorCount: 1,
// warningCount: 1,
// fixableErrorCount: 1,
// fixableWarningCount: 0,
// source: "\"use strict\"\n"
// }
errorCount += result.errorCount;
warningCount += result.warningCount;
if (result.messages && result.messages.length) {
console.log(`ESLint has found problems in file: ${result.filePath}`);
result.messages.forEach((msg) => {
if (msg.severity === 2) {
console.log(`Error : ${msg.message} in Line ${msg.line} Column ${msg.column}`);
} else {
console.log(`Warning : ${msg.message} in Line ${msg.line} Column ${msg.column}`);
}
});
}
});
}
複製代碼
if (errorCount >= 1) {
console.log('\x1b[31m', `ESLint failed`);
console.log('\x1b[31m', `✖ ${errorCount + warningCount} problems(${errorCount} error, ${warningCount} warning)`);
process.exit(1);
} else if (warningCount >= 1) {
console.log('\x1b[32m', 'ESLint passed, but need to be improved.');
process.exit(0);
} else {
console.log('\x1b[32m', 'ESLint passed');
process.exit(0);
}
複製代碼
到這裏 pre-commit 鉤子腳本就完成了,只須要在 package.json 文件中配置下腳本的執行就能實現增量代碼檢查了。最終實現的效果是開發者在提交代碼時不再用等待全量代碼檢查完成了,腳本會很快找到有改動的文件並檢查完。github
若是想要在本身的項目中實現這一功能,能夠直接使用開源庫 lint-staged 結合 husky 一塊兒用。web
代碼交付時的增量檢查實現方式和上面的步驟相似,關鍵點就是找到增量的部分。
以一個包含 460 個 js 文件的中等規模工程爲例,下圖中左邊爲全量代碼檢查的耗時,右邊爲增量代碼檢查的耗時:
若是開發者只修改了一個文件,在提交代碼時全量檢查須要耗時 38 秒,而增量檢查只須要耗時 2 秒。
前文實現了文件粒度的增量檢查,考慮到大型項目中可能存在不少大文件,若是隻修改了幾行代碼就須要對整個文件進行 ESLint 檢查依然是個低效的操做,咱們能夠嘗試找到代碼行粒度的增量代碼作檢查。
首先仍然用 git diff
命令找到修改的部分,這裏須要作一些字符串的處理提取出代碼塊;而後使用 ESLint Node.js API 中的 lintText 方法對代碼塊作檢查。感興趣的同窗能夠本身嘗試實現一下哦。
考慮這樣一個業務場景,在一個有數百個頁面的大型多頁 Web 應用中(MPA),每一次全量打包構建都須要幾十分鐘的時間。有時候開發者只改了一個頁面或者一個公共組件,卻須要等上好久才能發佈上線,嚴重影響持續集成、線上問題解決的效率。
以使用 webpack 進行打包構建爲例,咱們一樣嘗試用「增量」思想來優化這個問題。
和前文相似,第一步依舊是找到增量代碼,即本次發佈修改了哪些文件。最簡單的仍然是選擇用 git diff
命令來實現。和增量代碼檢查不同的是,這裏要對比待發布的集成分支和主幹,找到之間的差別文件列表。
const execSync = require('child_process').execSync;
const path = require('path').posix;
const GITDIFF = 'git diff origin/master --name-only';
// 執行 git 的命令
const diffFiles = execSync(GITDIFF, {
encoding: 'utf8',
})
.split('\n')
.filter((item) => item)
.map((filePath) => path.normalize(filePath));
複製代碼
得到了修改的文件列表後,並不能直接觸發 webpack 打包,須要根據文件之間的引用關係找到入口文件,把須要從新打包的頁面入口傳給 webpack。
思路是先構建每一個頁面入口文件的依賴樹,若是這棵樹包含了上述被修改的文件,就說明這個頁面須要被從新打包。如圖所示:
被修改的文件前文已經能獲取到,接下來是要構建每一個入口文件的依賴樹。前端模塊化規範不少,本身去實現每一個文件的依賴分析須要兼顧各類狀況,這裏推薦一個開源庫 Madge,它將代碼轉成抽象語法樹進行分析,最終返回一棵依賴樹。
以上圖中兩個入口文件爲例,它們的依賴樹以下:
// 被修改的文件列表
const diffFiles = ['util/fetch.js'];
// 用 madge 庫計算依賴樹的示例代碼,具體可查看官方文檔
// Promise.all([madge('./demo/index.js'), madge('./demo/buy.js')]).then((result) => {
// result.forEach((e) => {
// console.log(e.obj());
// })
// });
// 最後獲得的依賴樹以下
const relyTree = {
// demo/index.js 文件的依賴樹
{
'demo/a.jsx': ['util/fetch.js'],
'demo/b.js': [],
'demo/index.js': ['demo/a.jsx', 'demo/b.js'],
'util/fetch.js': []
},
// demo/buy.js 文件的依賴樹
{
'util/env.js': [],
'demo/buy.js': ['demo/c.js', 'demo/d.js'],
'demo/c.js': ['util/env.js'],
'demo/d.js': []
}
};
複製代碼
深度遍歷每一個入口文件的依賴樹,根據是否包含被修改的文件列表中的文件來判斷是否須要從新打包構建,示例代碼以下:
/* 計算增量入口示例代碼 */
// 全量頁面入口
const entries = [
'demo/index.js',
'demo/buy.js',
];
// 判斷兩個數組是否存在交集
function intersection(arr1, arr2) {
let flag = false;
arr1.forEach((ele) => {
if (arr2.includes(ele)) {
flag = true;
}
});
return flag;
}
// 計算增量入口
const incrementEntries = [];
for (const i in relyTree) {
for (const j in relyTree[i]) {
if (intersection(relyTree[i][j], diffFiles)) {
incrementEntries.push(i);
}
}
}
複製代碼
好比咱們已知本次發佈是修改了 util/fetch.js
這個文件,遍歷以上 2 個依賴樹就得知只有 demo/index
這個頁面受影響,修改 webpack 的配置只把這個文件做爲入口參數觸發打包就能夠極大提高打包構建的速度。
前端工程還有一些依賴是 package.json 文件裏描述的 npm 包,安裝在 node_modules 文件夾裏,模塊之間的依賴關係很是複雜。簡單起見,當第一步 git diff
發現 package.json 裏有模塊升級時,考慮到這不是高頻事件,能夠直接觸發全量打包。
以一個包含 50 個頁面的 MPA 工程爲例,下圖中左邊爲全量打包構建的耗時,右邊爲增量打包構建的耗時: 假設開發者修改了2個頁面,增量打包機制經過計算只傳入這兩個頁面入口給 webpack,整個打包構建流程將從 7 分鐘縮短爲 50 秒,極大提高持續集成的效率。
本文以增量代碼檢查和增量打包構建兩個特定業務場景爲例,介紹了「增量」思想在前端開發工程化中如何作效率提高。這兩個案例不必定能直接照搬到你們的前端工程化實踐中去,旨在介紹這樣一種編程設計思想,你們能夠發揮各自的想象力運用到更多的地方。
本文發佈自 網易雲音樂大前端團隊,文章未經受權禁止任何形式的轉載。咱們常年招收前端、iOS、Android,若是你準備換工做,又剛好喜歡雲音樂,那就加入咱們 grp.music-fe(at)corp.netease.com!