webpack-loader是怎樣煉成的
囉嗦兩句
學習這件事從學習動機上來看,可以分成兩種情況:主動學習和被動學習。主動學習就是,某天你瀏覽網頁的時候,看到一個酷到沒朋友的效果,趕緊開啟開發者工具,看看用了什麼 css 屬性,用了什麼庫或者框架實現的,這是主動學習。
還有一種是被動學習。就拿我來說,之前用 mpvue 寫小程式的時候,頁面的 json 配置都是寫在 main.js 裡面的,loader 會從 main.js 解析出對應的程式碼塊,然後為我生成對應的配置檔案。但是前兩天,當我又初始化一個新專案的時候(使用的是 mpvue-loader1.1.4),這個好用的特性居然消失了,我需要在目錄下自己手動建一個 json 檔案寫頁面配置。
人有這麼一種本性,從不好的體驗切換到好的體驗很快,但是再切回去就很難受?。所以,這回只有硬著頭皮寫個 webpack loader 來回歸原來的體驗了。實現的功能很簡單,就是重新實現 mpvue 原有的功能,從 js 檔案中解析出配置項的內容,並生成到對應的資料夾中。
loader 是幹什麼的
無圖言卵,先上個圖: 
把 webpack 想像成一個工廠,loader 就是一個個身懷絕技的流水線工人,有的會處理 svg,有的會壓縮 css 或者圖片,有的會處理 less,有的會將 es6 轉換為 es5。他們在 webpack 的排程下 (確切的說是 loader-runer),井井有條的完成自己工作後,把自己處理的結果交給下一個工人,直到最後由 webpack 將他們的勞動成果生成 dist 目錄下的檔案。
所以一個 loader 用一個函式來表示,應該是這樣的:
module.exports = function(content, map, meta) {
return content;
};
上面我們就定義了一個什麼都不幹還拿工資的 loader,它就是拿到內容後原樣交給下一個 loader 同學。但是,它現在其實還只是一個函式 -- 因為它還沒有混入 webpack loader 內部啊,現在我們幫他打入 webpack 的 loader 內部:
// webpack.config.js ... module: { rules: [ { test: /\.js$/, include: [resolve('src'), resolve('test')], use: [ { loader: path.resolve('path/to/my-loader.js'), // 本 loader? options: {a: 1} }, ... 其他 loader ] } ] }
作為一個什麼都不做的 loader,它在 rules 下面使用 /\.js$/
這個正則表示式,告 (hu) 訴 (you) webpack 它可以處理 js 檔案, 還通過 includes 欄位,說明了它的業務範圍只負責 src 和 test 目錄下的 js 檔案。
現在回到上面的圖,大部分 loader 還是實實在在辦事的。有的可以處理文字檔案,如 css 預處理,進去的是 less 語法的檔案,出來的是 css 語法的檔案;有的可以處理二進位制檔案,比如將較小的圖片變成 base64 字串。還有的 loader 買一送二,比如 mpvue-loader, 輸入的是 vue 檔案,但是會輸出 wxss,wxml,js 三個檔案。但是,這些工作僅靠 loader 自己是辦不到的,它需要和 webpack 溝通。也就是說,幹活是需要工具的,這個工具就是 loader 的上下文 (context)。
loader 的工具箱 --context
根據 官方文件 的解釋,loader context 表示在 loader 內使用 this 可以訪問的一些方法或屬性。還是在上面的那個啥都不幹 loader 上說明:
const path = require('path')
module.exports = function(content){
console.log('resource', this.resource) // 檔案路徑帶 query
console.log('query', this.query)// 對應配置中的 options {a: 1}
console.log('resourcePath', this.resourcePath)// 檔案路徑
this.emitFile('main.json', JSON.stringify({hello: 'world'}))// dist目錄下生成一個 json 檔案
this.emitWarning('這個 loader 啥都不幹')// 會觸發一個警告⚠️
// this.emitError('這個 loader 啥都不幹')// 會導致本次編譯過程失敗
return content
}
正如上面的例子那樣,有了上下文提供的工具包,loader 就可以幹更多的事情而不只是對 content 進行處理,比如:
- 通過 this.emitError 向 webpack 丟擲一個錯誤,中斷本次編譯
- 通過 this.emitFile 生成一個新的檔案,emitFile接受的第一個引數是相對於dist目錄的檔案路徑
- 通過 this.resource 獲得資源路徑
此外還有很多工具,大家可以看文件瞭解。
loader 實戰
把這些瞭解清楚之後,我們就可以實現之前想要的功能了:從所有檔名為 main.js 的檔案中提取 export default 的內容,並在同級目錄下生成一個 json 檔案,如下所示:
// 生成前 src 目錄
page
|-main.js
// 生成後 dist 目錄
page
|-main.js
|-main.json
全部程式碼如下,解釋見註釋:
module.exports = function(source){
if(/main\.js$/.test(this.resource)){// 只處理 main.js 檔案
let jsonPath = this.resource.replace(/.+src\//, '') +'on'// 生成 json 檔案相對於 dist 目錄的路徑
let re = /export\s+?default\s*(\{[\s\S]+\})/m; // 解析出檔案中的 export default 程式碼塊
if (re.test(source)) {
let config = eval('a=' + re.exec(source)[1]); // 將配置轉成物件
console.log(config)
this.emitFile(jsonPath, JSON.stringify(config)); // 寫到檔案中
}
}
return source;
}
這就是本文的全部內容了,今天參加了同學的婚禮,見了幾個老同學,包括還在船廠? 打拼的同學。吃飯的時候聊著大家的現狀,真的很開心,沒有想象中的侷促和無話可說。轉眼畢業都三年了,時間過的真 tm 快。(完)