構建專欄系列目錄入口javascript
焦傳鍇,微醫前端技術部平臺支撐組。學習也要有呼有吸。css
webpack 只能理解 JavaScript 和 JSON 文件,這是 webpack 開箱可用的自帶能力。**loader **讓 webpack 可以去處理其餘類型的文件,並將它們轉換爲有效模塊,以供應用程序使用,以及被添加到依賴圖中。前端
也就是說,webpack 把任何文件都看作模塊,loader 能 import 任何類型的模塊,可是 webpack 原生不支持譬如 css 文件等的解析,這時候就須要用到咱們的 loader 機制了。 咱們的 loader 主要經過兩個屬性來讓咱們的 webpack 進行聯動識別:java
那麼問題來了,你們必定想知道本身要定製一個 loader 的話須要怎麼作呢?node
俗話說的好,沒有規矩不成方圓,編寫咱們的 loader 時,官方也給了咱們一套用法準則(Guidelines),在編寫的時候應該按照這套準則來使咱們的 loader 標準化:webpack
根據模塊類型,可能會有不一樣的模式指定依賴關係。例如在 CSS 中,使用@import 和 url(...)語句來聲明依賴。這些依賴關係應該由模塊系統解析。git
能夠經過如下兩種方式中的一種來實現:github
- 經過把它們轉化成 require 語句。
- 使用 this.resolve 函數解析路徑。
一個 loader 就是一個 nodejs 模塊,他導出的是一個函數,這個函數只有一個入參,這個參數就是一個包含資源文件內容的字符串,而函數的返回值就是處理後的內容。也就是說,一個最簡單的 loader 長這樣:web
module.exports = function (content) {
// content 就是傳入的源內容字符串
return content
}
複製代碼
當一個 loader 被使用的時候,他只能夠接收一個入參,這個參數是一個包含包含資源文件內容的字符串。 是的,到這裏爲止,一個最簡單 loader 就已經完成了!接下來咱們來看看怎麼給他加上豐富的功能。api
咱們基本能夠把常見的 loader 分爲四種:
通常的 loader 轉換都是同步的,咱們能夠採用上面說的直接 return 結果的方式,返回咱們的處理結果:
module.exports = function (content) {
// 對 content 進行一些處理
const res = dosth(content)
return res
}
複製代碼
也能夠直接使用 this.callback()
這個 api,而後在最後直接 **return undefined **的方式告訴 webpack 去 this.callback()
尋找他要的結果,這個 api 接受這些參數:
this.callback(
err: Error | null, // 一個沒法正常編譯時的 Error 或者 直接給個 null
content: string | Buffer,// 咱們處理後返回的內容 能夠是 string 或者 Buffer()
sourceMap?: SourceMap, // 可選 能夠是一個被正常解析的 source map
meta?: any // 可選 能夠是任何東西,好比一個公用的 AST 語法樹
);
複製代碼
接下來舉個例子: 這裏注意
[this.getOptions()](https://webpack.docschina.org/api/loaders/#thisgetoptionsschema)
能夠用來獲取配置的參數
從 webpack 5 開始,this.getOptions 能夠獲取到 loader 上下文對象。它用來替代來自loader-utils中的 getOptions 方法。
module.exports = function (content) {
// 獲取到用戶傳給當前 loader 的參數
const options = this.getOptions()
const res = someSyncOperation(content, options)
this.callback(null, res, sourceMaps);
// 注意這裏因爲使用了 this.callback 直接 return 就行
return
}
複製代碼
這樣一個同步的 loader 就完成了!
再來講說異步: 同步與異步的區別很好理解,通常咱們的轉換流程都是同步的,可是當咱們遇到譬如須要網絡請求等場景,那麼爲了不阻塞構建步驟,咱們會採起異步構建的方式,對於異步 loader 咱們主要須要使用 this.async()
來告知 webpack 此次構建操做是異步的,很少廢話,看代碼就懂了:
module.exports = function (content) {
var callback = this.async()
someAsyncOperation(content, function (err, result) {
if (err) return callback(err)
callback(null, result, sourceMaps, meta)
})
}
複製代碼
默認狀況下,資源文件會被轉化爲 UTF-8 字符串,而後傳給 loader。經過設置 raw 爲 true,loader 能夠接收原始的 Buffer。每個 loader 均可以用 String 或者 Buffer 的形式傳遞它的處理結果。complier 將會把它們在 loader 之間相互轉換。你們熟悉的 file-loader 就是用了這個。 簡而言之:你加上 module.exports.raw = true;
傳給你的就是 Buffer 了,處理返回的類型也並不是必定要是 Buffer,webpack 並無限制。
module.exports = function (content) {
console.log(content instanceof Buffer); // true
return doSomeOperation(content)
}
// 劃重點↓
module.exports.raw = true;
複製代碼
咱們每個 loader 均可以有一個 pitch
方法,你們都知道,loader 是按照從右往左的順序被調用的,可是實際上,在此以前會有一個按照從左往右執行每個 loader 的 pitch 方法的過程。 pitch 方法共有三個參數:
!
做爲鏈接符組成的字符串。!
做爲鏈接符組成的字符串。在 pitch 中傳給 data 的數據,在後續的調用執行階段,是能夠在 this.data
中獲取到的:
module.exports = function (content) {
return someSyncOperation(content, this.data.value);// 這裏的 this.data.value === 42
};
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
data.value = 42;
};
複製代碼
注意! 若是某一個 loader 的 pitch 方法中返回了值,那麼他會直接「往回走」,跳事後續的步驟,來舉個例子: 假設咱們如今是這樣:
use: ['a-loader', 'b-loader', 'c-loader'],
那麼正常的調用順序是這樣: 如今 b-loader 的 pitch 改成了有返回值:
// b-loader.js
module.exports = function (content) {
return someSyncOperation(content);
};
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
return "誒,我直接返回,就是玩兒~"
};
複製代碼
那麼如今的調用就會變成這樣,直接「回頭」,跳過了原來的其餘三個步驟:
resolve(context: string, request: string, callback: function(err, result: string))
'/abc/resource.js?rrr'
'/abc/resource.js'
官方還提供了不少實用 Api ,這邊值列舉一些可能經常使用的,更多能夠戳連接👇 更多詳見官方連接
接下來咱們簡單實踐製做兩個 loader ,功能分別是在編譯出的代碼中加上 /** 公司@年份 */
格式的註釋和簡單作一下去除代碼中的 console.log
,而且咱們鏈式調用他們:
company-loader.js
module.exports = function (source) {
const options = this.getOptions() // 獲取 webpack 配置中傳來的 option
this.callback(null, addSign(source, options.sign))
return
}
function addSign(content, sign) {
return `/** ${sign} */\n${content}`
}
複製代碼
console-loader.js
module.exports = function (content) {
return handleConsole(content)
}
function handleConsole(content) {
return content.replace(/console.log\(['|"](.*?)['|"]\)/, '')
}
複製代碼
功能就簡單的進行了一下實現,這裏咱們主要說一下如何測試調用咱們的本地的 loader,方式有兩種,一種是經過 Npm link 的方式進行測試,這個方式的具體使用就不細說了,你們能夠簡單查閱一下。 另一種就是直接在項目中經過路徑配置的方式,有兩種狀況:
webpack.config.js
{
test: /\.js$/
use: [
{
loader: path.resolve('path/to/loader.js'),
options: {/* ... */}
}
]
}
複製代碼
webpack.config.js
resolveLoader: {
// 這裏就是說先去找 node_modules 目錄中,若是沒有的話再去 loaders 目錄查找
modules: [
'node_modules',
path.resolve(__dirname, 'loaders')
]
}
複製代碼
咱們這裏的 webpack 配置以下所示:
module: {
rules: [
{
test: /\.js$/,
use: [
'console-loader',
{
loader: 'company-loader',
options: {
sign: 'we-doctor@2021',
},
},
],
},
],
},
複製代碼
項目中的 index.js:
function fn() {
console.log("this is a message")
return "1234"
}
複製代碼
執行編譯後的 bundle.js: 能夠看到,兩個 loader 的功能都體現到了編譯後的文件內。
/******/ (() => { // webpackBootstrap
var __webpack_exports__ = {};
/*!**********************!*\ !*** ./src/index.js ***! \**********************/
/** we-doctor@2021 */
function fn() {
return "1234"
}
/******/ })()
;
複製代碼
plugin 提供了不少比 loader 中更完備的功能,他使用階段式的構建回調,webpack 給咱們提供了很是多的 hooks 用來在構建的階段讓開發者自由的去引入本身的行爲。
一個最基本的 plugin 須要包含這些部分:
apply
方法,apply
方法在 webpack 裝載這個插件的時候被調用,而且會傳入 compiler
對象。callback
或者經過 Promise
的方式(後續異步編譯部分會詳細說)class HelloPlugin{
apply(compiler){
compiler.hooks.<hookName>.tap(PluginName,(params)=>{
/** do some thing */
})
}
}
module.exports = HelloPlugin
複製代碼
Compiler 和 Compilation 是整個編寫插件的過程當中的**重!中!之!重!**由於咱們幾乎全部的操做都會圍繞他們。
compiler
對象能夠理解爲一個和 webpack 環境總體綁定的一個對象,它包含了全部的環境配置,包括 options,loader 和 plugin,當 webpack 啓動時,這個對象會被實例化,而且他是全局惟一的,上面咱們說到的 apply
方法傳入的參數就是它。
compilation
在每次構建資源的過程當中都會被建立出來,一個 compilation 對象表現了當前的模塊資源、編譯生成資源、變化的文件、以及被跟蹤依賴的狀態信息。它一樣也提供了不少的 hook 。
Compiler 和 Compilation 提供了很是多的鉤子供咱們使用,這些方法的組合可讓咱們在構建過程的不一樣時間獲取不一樣的內容,具體詳情可參見官網直達。
上面的連接中咱們會發現鉤子會有不一樣的類型,好比 SyncHook
、SyncBailHook
、AsyncParallelHook
、AsyncSeriesHook
,這些不一樣的鉤子類型都是由 tapable
提供給咱們的,關於 tapable
的詳細用法與解析能夠參考咱們前端構建工具系列專欄中的 tapable
專題講解。
基本的使用方式是:
compiler/compilation.hooks.<hookName>.tap/tapAsync/tapPromise(pluginName,(xxx)=>{/**dosth*/})
複製代碼
Tip: 之前的寫法是
compiler.plugin
,可是在最新的 webpack@5 可能會引發問題,參見 webpack-4-migration-notes
plugin 的 hooks 是有同步和異步區分的,在同步的狀況下,咱們使用 <hookName>.tap
的方式進行調用,而在異步 hook 內咱們能夠進行一些異步操做,而且有異步操做的狀況下,請使用 tapAsync
或者 tapPromise
方法來告知 webpack 這裏的內容是異步的,固然,若是內部沒有異步操做的話,你也能夠正常使用 tap
。
使用 tapAsync
的時候,咱們須要多傳入一個 callback
回調,而且在結束的時候必定要調用這個回調告知 webpack 這段異步操做結束了。👇 好比:
class HelloPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync(HelloPlugin, (compilation, callback) => {
setTimeout(() => {
console.log('async')
callback()
}, 1000)
})
}
}
module.exports = HelloPlugin
複製代碼
當使用 tapPromise
來處理異步的時候,咱們須要返回一個 Promise
對象而且讓它在結束的時候 resolve
👇
class HelloPlugin {
apply(compiler) {
compiler.hooks.emit.tapPromise(HelloPlugin, (compilation) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log('async')
resolve()
}, 1000)
})
})
}
}
module.exports = HelloPlugin
複製代碼
接下來咱們經過實際來作一個插件梳理一遍總體的流程和零散的功能點,這個插件實現的功能是在打包後輸出的文件夾內多增長一個 markdown 文件,文件內記錄打包的時間點、文件以及文件大小的輸出。
首先咱們根據需求肯定咱們須要的 hook ,因爲須要輸出文件,咱們須要使用 compilation 的 emitAsset 方法。 其次因爲須要對 assets 進行處理,因此咱們使用 compilation.hooks.processAssets
,由於 processAssets 是負責 asset 處理的鉤子。
這樣咱們插件結構就出來了👇 OutLogPlugin.js
class OutLogPlugin {
constructor(options) {
this.outFileName = options.outFileName
}
apply(compiler) {
// 能夠從編譯器對象訪問 webpack 模塊實例
// 而且能夠保證 webpack 版本正確
const { webpack } = compiler
// 獲取 Compilation 後續會用到 Compilation 提供的 stage
const { Compilation } = webpack
const { RawSource } = webpack.sources
/** compiler.hooks.<hoonkName>.tap/tapAsync/tapPromise */
compiler.hooks.compilation.tap('OutLogPlugin', (compilation) => {
compilation.hooks.processAssets.tap(
{
name: 'OutLogPlugin',
// 選擇適當的 stage,具體參見:
// https://webpack.js.org/api/compilation-hooks/#list-of-asset-processing-stages
stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE,
},
(assets) => {
let resOutput = `buildTime: ${new Date().toLocaleString()}\n\n`
resOutput += `| fileName | fileSize |\n| --------- | --------- |\n`
Object.entries(assets).forEach(([pathname, source]) => {
resOutput += `| ${pathname} | ${source.size()} bytes |\n`
})
compilation.emitAsset(
`${this.outFileName}.md`,
new RawSource(resOutput),
)
},
)
})
}
}
module.exports = OutLogPlugin
複製代碼
對插件進行配置: webpack.config.js
const OutLogPlugin = require('./plugins/OutLogPlugin')
module.exports = {
plugins: [
new OutLogPlugin({outFileName:"buildInfo"})
],
}
複製代碼
打包後的目錄結構:
dist
├─ buildInfo.md
├─ bundle.js
└─ bundle.js.map
複製代碼
buildInfo.md 能夠看到按照咱們但願的格式準確輸出了內容,這樣一個簡單的功能插件就完成了!
Writing a Loader | webpack Writing a Plugin | webpack 深刻淺出 Webpack webpack/webpack | github
本文完整代碼直通車:github