0. 前言
本文將 webpack
的 Loader
相關的知識點整理了一下,部分文字是從官方文檔中直接摘錄過來的,並附上自己的理解。如果覺得看起來和官方文檔差不多,直接看官方文檔最好啦~
1. 簡述 webpack 工作流程
本文不過多描述 webpack
的作用和使用方法,如果還不是太熟悉,可以打開 https://webpack.js.org/ 先熟悉一下。
關於 webpack
的工作流程,簡單來說可以概括爲以下幾步:
- 參數解析
- 找到入口文件
- 調用
Loader
編譯文件 - 遍歷
AST
,收集依賴 - 生成
Chunk
- 輸出文件
其中,真正起編譯作用的便是 Loader
,本文也就 Loader
進行詳細的闡述,其餘部分暫且不談。
2. 關於 Loader
Loader allow webpack to process other types of files and convert them into valid modules.
Loader
的作用很簡單,就是處理任意類型的文件,並且將它們轉換成一個讓 webpack
可以處理的有效模塊。
2.1 Loader 的配置和使用
2.1.1 在 config 裏配置
Loader
可以在 webpack.config.js
裏配置,這也是推薦的做法,定義在 module.rules
裏:
// webpack.config.js module.exports = { module: { rules: [ { test: /\.js$/, use: 'babel-loader' }, { test: /\.css$/, use: [ { loader: 'style-loader' }, { loader: 'css-loader' }, { loader: 'postcss-loader' }, ] } ] } };
每一條 rule
會包含兩個屬性:test
和 use
,比如 { test: /\.js$/, use: 'babel-loader' }
意思就是:當 webpack
遇到擴展名爲 js
的文件時,先用 babel-loader
處理一下,然後再打包它。
use
的類型:string|array|object|function
:
string
: 只有一個Loader
時,直接聲明Loader
,比如babel-loader
。array
: 聲明多個Loader
時,使用數組形式聲明,比如上文聲明.css
的Loader
。object
: 只有一個Loader
時,需要有額外的配置項時。function
:use
也支持回調函數的形式。
關於 use
的多種配置方式,這裏就不多說了,可以點擊 更多關於 use
注意: 當 use
是通過數組形式聲明 Loader
時,Loader
的執行順序是從右到左,從下到上。比如暫且認爲上方聲明是這樣執行的:
postcss-loader
-> css-loader
-> style-loader
其實就是:
styleLoader(cssLoader(postcssLoader(content)))
爲什麼說是暫且呢,因爲 style-loader
有點特殊,有興趣的看看這個 webpack loader 從上手到理解系列:style-loader。
webpack
提供了多種配置 Loader
的方法,不過一般來說,use
就已經足夠用了,如果想了解更多,可以點擊 更多關於 rule 的配置
2.1.2 內聯
可以在 import
等語句裏指定 Loader
,使用 !
來將 Loader
分開:
import style from 'style-loader!css-loader?modules!./styles.css';
內聯時,通過 query
來傳遞參數,例如 ?key=value
。
一般來說,推薦使用統一 config
的形式來配置 Loader
,內聯形式多出現於 Loader
內部,比如 style-loader
會在自身代碼裏引入 css-loader
:
require("!!../../node_modules/css-loader/dist/cjs.js!./styles.css");
2.2 Loader 類型
2.2.1 同步 Loader
module.exports = function(source) { const result = someSyncOperation(source); // 同步邏輯 return result; }
一般來說,Loader
都是同步的,通過 return
或者 this.callback
來同步地返回 source
轉換後的結果。
2.2.2 異步 Loader
有的時候,我們需要在 Loader
裏做一些異步的事情,比如說需要發送網絡請求。如果同步地等着,網絡請求就會阻塞整個構建過程,這個時候我們就需要進行異步 Loader
,可以這樣做:
module.exports = function(source) { // 告訴 webpack 這次轉換是異步的 const callback = this.async(); // 異步邏輯 someAsyncOperation(content, function(err, result) { if (err) return callback(err); // 通過 callback 來返回異步處理的結果 callback(null, result, map, meta); }); };
2.2.3 Pitching Loader
Pitching Loader
是一個比較重要的概念,之前在 style-loader
裏有提到過。
{ test: /\.js$/, use: [ { loader: 'aa-loader' }, { loader: 'bb-loader' }, { loader: 'cc-loader' }, ] }
我們知道,Loader
總是從右到左被調用。上面配置的 Loader
,就會按照以下順序執行:
cc-loader
-> bb-loader
-> aa-loader
每個 Loader
都支持一個 pitch
屬性,通過 module.exports.pitch
聲明。如果該 Loader
聲明瞭 pitch
,則該方法會優先於 Loader
的實際方法先執行,官方也給出了執行順序:
|- aa-loader `pitch`
|- bb-loader `pitch`
|- cc-loader `pitch`
|- requested module is picked up as a dependency
|- cc-loader normal execution
|- bb-loader normal execution
|- aa-loader normal execution
也就是會先從左向右執行一次每個 Loader
的 pitch
方法,再按照從右向左的順序執行其實際方法。
2.2.4 Raw Loader
我們在 url-loader
裏和 file-loader
最後都見過這樣一句代碼:
export const raw = true;
默認情況下,webpack
會把文件進行 UTF-8
編碼,然後傳給 Loader
。通過設置 raw
,Loader
就可以接受到原始的 Buffer
數據。
2.3 Loader 幾個重要的 api
所謂 Loader
,也只是一個符合 commonjs
規範的 node
模塊,它會導出一個可執行函數。loader runner
會調用這個函數,將文件的內容或者上一個 Loader
處理的結果傳遞進去。同時,webpack
還爲 Loader
提供了一個上下文 this
,其中有很多有用的 api
,我們找幾個典型的來看看。
2.3.1 this.callback()
在 Loader
中,通常使用 return
來返回一個字符串或者 Buffer
。如果需要返回多個結果值時,就需要使用 this.callback
,定義如下:
this.callback( // 無法轉換時返回 Error,其餘情況都返回 null err: Error | null, // 轉換結果 content: string | Buffer, // source map,方便調試用的 sourceMap?: SourceMap, // 可以是任何東西。比如 ast meta?: any );
一般來說如果調用該函數的話,應該手動 return
,告訴 webpack
返回的結果在 this.callback
中,以避免含糊不清的結果:
module.exports = function(source) { this.callback(null, source, sourceMaps); return; };
2.3.2 this.async()
同上,異步 Loader
。
2.3.3 this.cacheable()
有些情況下,有些操作需要耗費大量時間,每一次調用 Loader
轉換時都會執行這些費時的操作。
在處理這類費時的操作時, webapck
會默認緩存所有 Loader
的處理結果,只有當被處理的文件發生變化時,纔會重新調用 Loader
去執行轉換操作。
webpack
是默認可緩存的,可以執行 this.cacheable(false)
手動關閉緩存。
2.3.4 this.resource
當前處理文件的完整請求路徑,包括 query
,比如 /src/App.vue?type=templpate
。
2.3.5 this.resourcePath
當前處理文件的路徑,不包括 query
,比如 /src/App.vue
。
2.3.6 this.resourceQuery
當前處理文件的 query
字符串,比如 ?type=template
。我們在 vue-loader
裏有見過如何使用它:
const qs = require('querystring'); const { resourceQuery } = this; const rawQuery = resourceQuery.slice(1); // 刪除前面的 ? const incomingQuery = qs.parse(rawQuery); // 解析字符串成對象 // 取 query if (incomingQuery.type) {}
2.3.7 this.emitFile
讓 webpack
在輸出目錄新建一個文件,我們在 file-loader
裏有見過:
if (typeof options.emitFile === 'undefined' || options.emitFile) { this.emitFile(outputPath, content); }
更多的 api
可在官方文檔中查看:Loader Interface
3. Loader 工作流程簡述
我們來回顧一下 Loader
的一些特點:
Loader
是一個node
模塊;Loader
可以處理任意類型的文件,轉換成webpack
可以處理的模塊;Loader
可以在webpack.config.js
裏配置,也可以在require
語句裏內聯;Loader
可以根據配置從右向左鏈式執行;Loader
接受源文件內容字符串或者Buffer
;Loader
分爲多種類型:同步、異步和pitching
,他們的執行流程不一樣;webpack
爲Loader
提供了一個上下文,有一些api
可以使用;- ...
我們根據以上暫時知道的特點,可以對 Loader
的工作流程有個猜測,假設有一個 js-loader
,它的工作流程簡單來說是這樣的:
webpack.config.js
裏配置了一個js
的Loader
;- 遇到
js
文件時,觸發了js-loader
; js-loader
接受了一個表示該js
文件內容的source
;js-loader
使用webapck
提供的一系列api
對source
進行轉換,得到一個result
;- 將
result
返回或者傳遞給下一個Loader
,直到處理完畢。
webpack
的編譯流程非常複雜,暫時還不能看明白並且梳理清楚,在這裏就不誤導大家了。
關於 Loader
的工作流程以及源碼分析可以看 【webpack進階】你真的掌握了loader麼?- loader十問。
4. 如何編寫一個 Loader
雖然我們對於 webpack
的編譯流程不是很熟悉,但是我們可以試着編寫一個簡單功能的 Loader
,從而加深對 Loader
的理解。
4.1 Loader 用法準則
編寫 Loader
時需要遵循一些準則,官方有很詳細的文檔,就不重複闡述了。點擊 Loaders 用法準則 查看。
這裏說一下單一任務和鏈式調用。
一個 Loader
應該只完成一個功能,如果需要多步的轉換工作,則應該編寫多個 Loader
來進行鏈式調用完成轉換。比如 vue-loader
只是處理了 vue
文件,起到一個分發的作用,將其中的 template/style/script
分別交給不同的處理器來處理。
這樣會讓維護 Loader
變得更簡單,也能讓不同的 Loader
更容易地串聯在一起,而不是重複造輪子。
4.2 Loader 工具庫
編寫 Loader
的過程中,最常用的兩個工具庫是 loader-utils
和 schema-utils
,在現在常見的 Loader
中都能看到它們的身影。
4.2.1 loader-utils
它提供了許多有用的工具,但最常用的一種工具是獲取傳遞給 Loader
的選項:
import { getOptions } from 'loader-utils'; export default function loader(src) { // 加載 options const options = getOptions(this) || {}; }
4.2.2 schema-utils
配合 loader-utils
,用於保證 Loader
選項,進行與 JSON Schema
結構一致的校驗。
import validateOptions from 'schema-utils'; import schema from './options.json'; export default function loader(src) { // 校驗 options validateOptions(schema, options, { name: 'URL Loader', baseDataPath: 'options', }); }
更多關於如何編寫一個 Loader
,傳送門。