前端工程化
- 技術選型
- 統一規範——eslint、husky
- 測試、部署、監控——ut、e2e、mock
- 性能優化
- 模塊化重構
webpack流程
webpack的構建流程可以分為以下三大階段:
- 初始化:啓動構建,讀取與合併配置參數,加載
Plugin,實例化Compiler。 - 編譯:從Entry出發,針對每個Module串行調用對應Loader去翻譯文件的內容,再找到該Module依賴的Module,遞歸地進行編譯處理。
- 輸出:將編譯後的Module組合成Chunk,將Chunk轉換成文件,輸出到文件系統中。
Loader
Loader就像一個翻譯員,能將源文件經過轉化後輸出新的結果,並且一個文件還可以鏈式地經過多個翻譯員翻譯。
在開發一個Loader時,請確保其職責的單一性,我們只需要關心輸入和輸出。
基礎
Webpack是運行在Node.js上的,一個Loader其實就是一個Node.js模塊,這個模塊需要導出一個函數。這個導出的函數的工作就是獲得處理前的原內容,對原內容執行處理後,返回處理後的內容。
一個最簡的的Loader的源碼如下:
// source為compiler傳遞給Loader的一個文件的原內容
module.exports = function(source) {
// TODO: 對文件內容進行處理
return source;
}
由於Loader運行在Node.js中,所以我們可以調用任意Node.js自帶的API,或者安裝第三方模塊進行調用:
const sass = require('node-sass');
module.exports = function(source) {
return sass(source);
}
進階
獲得Loader的options
const loaderUtils = require('loader-utils'); // getOptions方法要求loader-utils版本為2.x
module.exports = function(source) {
// 獲取用户為當前Loader傳入的options
const options = loaderUtils.getOptions(this);
return source;
}
返回其他結果
上面的Loader都只是返回了原內容轉換後的內容,但是在某些場景下還需要返回除了內容之外的東西。
以用babel-loader轉換ES6為例,它還需要輸出轉換後的ES5代碼對應的Source Map,以方便調試源碼。
module.exports = function(source) {
this.callback(null, source, sourceMaps);
return;
}
其中的this.callback是Webpack向Loader注入的API(The Loader Context),以方便Loader和Webpack之間通信。
this.callback
可以同步或者異步調用的並返回多個結果的函數。預期的參數是:
this.callback(
err: Error | null,
content: string | Buffer,
sourceMap?: SourceMap,
meta?: any
);
如果這個函數被調用的話,你應該返回 undefined 從而避免含糊的 loader 結果。
異步
module.exports = function(source) {
let callback = this.async();
someAsyncOperation(source, function(err, result, sourceMaps, meta) {
callback(err, result, sourceMaps, meta);
});
}
this.async
告訴loader-runner這個loader將會異步地回調。返回this.callback。
處理二進制數據
在默認情況下,Webpack傳給Loader的原內容都是UTF-8格式編碼的字符串。但在某些場景下Loader不會處理文本文件,而會處理二進制文件如file-loader,這時就需要Webpack為Loader傳入二進制格式的數據。
module.exports = function(source) {
if(source instanceof Buffer === true) {
// TODO: 處理二進制內容
}
// Loader返回的類型也可以是Buffer類型
return source;
}
// 通過exports.raw屬性告訴webpack該Loader是否需要二進制數據
module.exports.raw = true;
緩存加速
Webpack會默認緩存所有Loader的處理結果,以避免每次構建都重新執行重複的轉換操作,從而加快構建速度。
關閉緩存功能:
module.exports = function(source) {
this.cacheable(false);
return source;
}
其他Loader API
詳見Loader Interface
加載本地Loader
Npm link
Npm link專門用於開發和調試本地的Npm模塊,能做到在不發佈模塊的情況下,將本地的一個正在開發的模塊的源碼鏈接到項目的node_modules目錄下,這讓項目可以直接使用本地的Npm模塊。
步驟如下:
- 確保正在開發的本地Npm模塊的
package.json已配置好; - 在本地的Npm根目錄下執行npm link,將本地模塊註冊到全局;
- 在項目根目錄下執行npm link loader-name,將第二步註冊到全局的本地Npm模塊鏈接到項目的
node_modules下,其中loader-name是指在第一步的package.json中配置的模塊名稱。
ResolveLoader
ResolveLoader用於配置Webpack如何尋找Loader,它在默認情況下只會去node_modules目錄下尋找。
module.exports = {
//...
resolveLoader: {
modules: ['node_modules'],
extensions: ['.js', '.json'],
mainFields: ['loader', 'main'],
},
};
Plugin
Webpack通過Plugin機制讓其更靈活,以適應各種場景。在Webpack運行的生命週期中會廣播許多事件,Plugin可以監聽這些事件,在合適的時機通過Webpack提供的API改變輸出結果。
一個最基礎的Plugin的代碼是這樣的:
// webpack3
class BasicPlugin {
constructor(options) {}
apply(compiler) {
compiler.plugin('compilation', function(compilation) {});
}
}
module.exports = BasicPlugin;
在webpack5的官方文檔中對plugin的解釋如下
plugin的目的在於解決loader無法實現的其他事,是對於webpack功能的擴展。
webpack plugin是一個具有apply方法的JavaScript對象。apply方法會被webpack compiler調用,並且在整個編譯生命週期都可以訪問compiler對象。
// webpack4,webpack5
const pluginName = 'ConsoleLogOnBuildWebpackPlugin';
class ConsoleLogOnBuildWebpackPlugin {
constructor(options) {}
apply(compiler) {
compiler.hooks.run.tap(pluginName, (compilation) => {
console.log('webpack 構建正在啓動!');
});
}
}
module.exports = ConsoleLogOnBuildWebpackPlugin;
Compiler和Compilation
- Compiler對象包含了Webpack環境的所有配置信息,包含options、loaders、plugins等信息。這個對象在Webpack啓動時被實例化,它是全局唯一的,可以簡單地將它理解為Webpack實例。
- Compilation對象包含了當前的模塊資源、編譯生成資源、變化的文件等。當Webpack以開發模式運行時,每當檢測到一個文件發生變化,便有一次新的Compilation被創建。Compilation對象也提供了很多事件回調供插件進行擴展。通過Compilation也能讀取到Compiler對象。
Compiler和Compilation的區別在於:Compiler代表了整個Webpack從啓動到關閉的生命週期,而Compilation只代表一次新的編譯。
Compiler鈎子
Compiler模塊是webpack的主要引擎,它通過CLI或者Node API傳遞的所有選項創建出一個compilation實例。它擴展(extends)自Tapable類,用來註冊和調用插件。 大多數面向用户的插件會首先在Compiler上註冊。
// 鈎子函數調用方式
compiler.hooks.someHook.tap('MyPlugin', (params) => {
/* ... */
});
// 示例
compiler.hooks.entryOption.tap('MyPlugin', (context, entry) => {
/* ... */
});
Compilation鈎子
Compilation模塊會被Compiler用來創建新的compilation對象(或新的 build 對象)。 compilation 實例能夠訪問所有的模塊和它們的依賴(大部分是循環依賴)。 它會對應用程序的依賴圖中所有模塊, 進行字面上的編譯(literal compilation)。 在編譯階段,模塊會被加載(load)、封存(seal)、優化(optimize)、 分塊(chunk)、哈希(hash)和重新創建(restore)。
// 鈎子函數調用方式
compilation.hooks.someHook.tap(/* ... */);
// 示例
compilation.hooks.buildModule.tap(
'SourceMapDevToolModuleOptionsPlugin',
(module) => {
module.useSourceMap = true;
}
);
事件流
Webpack就像一條生產線,要經過一系列處理流程後才能將源文件轉換成輸出結果。Webpack通過Tabable來組織這條複雜的生產線。Webpack在運行的過程中會廣播事件,插件只需要監聽它關心的事件,就能加入這條生產線中,去改變生產線的運作。
// webpack3
// 廣播事件,注意不要和現有事件重名
compiler.apply('event-name', params);
compilation.apply('event-name', params);
// 監聽事件
compiler.plugin('event-name', function(params) {});
compilation.plugin('event-name', function(params) {});
Tabable
這個小型庫是webpack的一個核心工具,但也可用於其他地方,以提供類似的插件接口。 在webpack中的許多對象都擴展自
Tapable類。 它對外暴露了tap,tapAsync和tapPromise等方法, 插件可以使用這些方法向webpack中注入自定義構建的步驟,這些步驟將在構建過程中觸發。根據使用不同的鈎子(hooks)和 tap 方法, 插件可以以多種不同的方式運行。 這個工作方式與 Tapable 提供的鈎子(hooks)密切相關。 compiler hooks 分別記錄了 Tapable 內在的鈎子, 並指出哪些 tap 方法可用。
自定義鈎子
// 需要簡單的從`tapable`中require所需的hook類,並創建
const SyncHook = require('tapable').SyncHook;
if (compiler.hooks.myCustomHook) throw new Error('已存在該鈎子');
compiler.hooks.myCustomHook = new SyncHook(['a', 'b', 'c']);
// 在你想要觸發鈎子的位置/時機下調用……
compiler.hooks.myCustomHook.call(a, b, c);
All Hook constructors take one optional argument, which is a list of argument names as strings.
const hook = new SyncHook(["arg1", "arg2", "arg3"]);
常用的鈎子
-
讀取Webpack的處理結果/修改輸出資源:
emit鈎子,輸出asset到output目錄之前執行compiler.hooks.emit.tap('MyPlugin', (compilation, callback) => { // do something callback(); }); -
監聽文件的變化:
afterCompile鈎子,compilation 結束和封印之後執行compiler.hooks.afterCompile.tap('MyPlugin', (compilation, callback) => { // 將指定文件添加到文件依賴列表中 commpilation.fileDependencies.push(filePath); callback(); });