手把手教你在Webpack寫一個Loader
theme: smartblue highlight: atelier-sulphurpool-light
前言
有的時候,你可能在從零搭建Webpack
專案很熟悉,配置過各種 loader
,面試官在 Webpack
方面問你,是否自己實現過一個loader
?如果沒有去了解過如果去實現,確實有點尷尬,其實呢,loader
實現其實很簡單的。下面說下loader
是什麼?
為什麼需要Loader?
Webpack
它只能處理js
和JSON
檔案。面對css
檔案還有一些圖片等等,Webpack
它自己是不能夠處理的,它需要loader
處理其他型別的檔案並將它們轉換為有效的模組以供應用程式使用並新增到依賴關係圖中,
Loader是什麼?
loader
本質上是一個node
模組,符合Webpack
中一切皆模組的思想。由於它是一個node
模組,它必須匯出一些東西。loader
本身就是一個函式,在該函式中對接收到的內容進行轉換,然後返回轉換後的結果
下面小浪為你簡單介紹下webpack
中的loader
常見的loader
我們先來回顧下常見的 Loader
基礎的配置和使用吧(僅僅只是常見的,npm
上面開發者大佬們釋出的太多了)
那麼開始吧,首先先介紹
處理 CSS
相關的 Loader
css-loader 和 style-loader
安裝依賴
bash
npm install css-loader style-loader
使用載入器
js
module.exports = {
// ...
module: {
rules: [{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
}],
},
};
其中module.rules
代表模組的處理規則。 每個規則可以包含很多配置項
test
可以接收正則表示式或元素為正則表示式的陣列。 只有與正則表示式匹配的模組才會使用此規則。 在此示例中,/\.css$/
匹配所有以 .css
結尾的檔案。
use
可以接收一個包含規則使用的載入器的陣列。 如果只配置了一個css-loader
,當只有一個loader
時也可以為字串
css-loader
的作用只是處理 CSS
的各種載入語法(@import
和 url()
函式等),如果樣式要工作,則需要 style-loader
將樣式插入頁面
style-loader
加到了css-loader
前面,這是因為在Webpack
打包時是按照陣列從後往前的順序將資源交給loader
處理的,因此要把最後生效的放在前面
還可以這樣寫成物件的形式,裡面
options
傳入配置
js
module.exports = {
// ...
module: {
rules: [{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
// css-loader 配置項
},
}
],
}],
},
};
exclude
與include
include
代表該規則只對正則匹配到的模組生效
exclude
的含義是,所有被正則匹配到的模組都排除在該規則之外
```js rules: [ { test: /.css$/, use: ['style-loader', 'css-loader'], exclude: /node_modules/, include: /src/, } ],
```
是否都還記得呢,現在有現成的腳手架,很多人都很少自己去配置這些了,欸~當然還有相關的 sass/less
等等前處理器loader
這裡就不一一介紹了。
babel-loader
babel-loader
這個loader
十分的重要,把高階語法轉為ES5
,常用於處理 ES6+
並將其編譯為 ES5
。 它允許我們在專案中使用最新的語言特性(甚至在提案中),而無需特別注意這些特性在不同平臺上的相容性。
介紹下主要的三個模組
- babel-loader:使
Babel
與Webpack
一起工作的模組 - @babel/core:
Babel
核心模組。 - @babel/preset-env:是
Babel
官方推薦的preseter
,可以根據使用者設定的目標環境,自動新增編譯ES6+
程式碼所需的外掛和補丁
安裝
bash
npm install babel-loader @babel/core @babel/preset-env
配置
js
rules: [
{
test: /\.js$/,
exclude: /node_modules/, //排除掉,不排除拖慢打包的速度
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true, // 啟用快取機制以防止在重新打包未更改的模組時進行二次編譯
presets: [[
'env', {
modules: false, // 將ES6 Module的語法交給Webpack本身處理
}
]],
},
},
}
],
html-loader
Webpack
可不認識 html
,直接報錯,需要loader
轉化
html-loader
用於將 HTML
檔案轉換為字串並進行格式化,它允許我們通過 JS
載入一個 HTML
片段。
安裝
bash
npm install html-loader
配置
js
rules: [
{
test: /\.html$/,
use: 'html-loader',
}
],
js
// index.js
import otherHtml from './other.html';
document.write(otherHtml);
這樣你可以在js中載入另一個頁面,寫刀當前index.html裡面
file-loader
用於打包檔案型別的資源,比如對png
、jpg
、gif
等圖片資源使用file-loader
,然後就可以在JS
中載入圖片了
安裝
bash
npm install file-loader
配置
js
const path = require('path');
module.exports = {
entry: './index.js',
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js',
},
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: 'file-loader',
}
],
},
};
url-loader
既然介紹了 file-loader
就不得不介紹 url-loader
,它們很相似,但是唯一的區別是使用者可以設定檔案大小閾值。 大於閾值時返回與file-loader
相同的publicPath
,小於閾值時返回檔案base64
編碼。
安裝
bash
npm install url-loader
配置
js
rules: [
{
test: /\.(png|jpg|gif)$/,
use: {
loader: 'url-loader',
options: {
limit: 1024,
name: '[name].[ext]',
publicPath: './assets/',
},
},
}
],
ts-loader
TypeScript
使用得越來越多,對於我們平時寫程式碼有了更好的規範,專案更加利於維護...等等好處,我們也在Webpack
中來配置loader,本質上類似於 babel-loader
,是一個連線 Webpack
和 Typescript
的模組
安裝
bash
npm install ts-loader typescript
loader配置,主要的配置還是在
tsconfig.json
中
js
rules: [
{
test: /\.ts$/,
use: 'ts-loader',
}
],
vue-loader
用來處理vue
元件,還要安裝vue-template-compiler
來編譯Vue
模板,估計大家大部分都用腳手架了
安裝
bash
npm install vue-loader vue-template-compiler
js
rules: [
{
test: /\.vue$/,
use: 'vue-loader',
}
],
寫一個簡單的Loader
介紹了幾個常見的loader的安裝配置,我們在具體的業務的實現的時候,可能遇到各種需求,上面介紹的或者npm上都沒有的載入器都不適合當前的業務場景,那我們可以自己去實現一個自己的loader
來滿足自己的需求,小浪下面介紹一下如何自定義一個loader
1.初始化專案
初始化專案
先建立一個專案資料夾(名字可以隨意,當然肯定是英文名)後進行初始化
bash
npm init -y
安裝依賴
安裝依賴: Webpack
和 Webpack
腳手架 和 熱更新伺服器
不同的版本 Webpack
可能有些差異,如果你跟著我的這個例子寫的話,小浪建議和我裝一樣的版本
bash
npm install [email protected] [email protected] [email protected] -D
新建一個
index.html
檔案
dist/index.html
```html
```
新建一個入口檔案
index.js
檔案
src/index.js
js
document.write('hello world')
建立
webpack.config.js
配置檔案
配置出口和入口檔案
配置devServer
服務
```js const path = require('path')
module.exports = { entry: './src/index.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'bundle.js', }, devServer: { contentBase: './dist', overlay: { warnings: true, errors: true, }, open: true, }, } ```
在
package.json
中配置啟動命令
json
"scripts": {
"dev": "Webpack-dev-server"
},
啟動
npm run dev
devServer
幫我們啟動一個伺服器,每次修改index.js不
需要自己在去打包,而是自動幫我們完成這項任務
頁面內容就是我們index.js
編寫的內容被打包成在dist/bundle.js
引入到index.html
了
當前的檔案目錄
bash
Webpack-demo
├── dist
│ └── index.html
├── package-lock.json
├── package.json
├── src
│ └── index.js
└── Webpack.config.js
2.實現一個簡單的 loader
在
src/MyLoader/my-loader.js
js
module.exports = function (source) {
// 在這裡按照你的需求處理 source
return source.replace('word', ', I am Xiaolang')
}
返回其它結果 this.callback
js
this.callback(
// 當無法轉換原內容時,給 Webpack 返回一個 Error
err: Error | null,
// 原內容轉換後的內容
content: string | Buffer,
// 用於把轉換後的內容得出原內容的 Source Map,方便除錯
sourceMap?: SourceMap,
// 如果本次轉換為原內容生成了 AST 語法樹,可以把這個 AST 返回,以方便之後需要 AST 的 Loader 複用該 AST,以避免重複生成 AST,提升效能
abstractSyntaxTree?: AST
);
開啟程式碼對應的source-map
,方便除錯原始碼。source-map
可以方便實際開發者在瀏覽器控制檯檢視原始碼。 如果不處理source-map
,最終將無法生成正確的map
檔案,在瀏覽器的開發工具中可能會看到混亂的原始碼。
為了在使用 this.callback
返回內容時將 source-map
返回給 Webpack
loader
必須返回 undefined
讓 Webpack
知道 loader
返回的結果在 this.callback
中,而不是在 return
js
module.exports = function(source) {
// 通過 this.callback 告訴 Webpack 返回的結果
this.callback(null, source.replace('word', ', I am Xiaolang'), sourceMaps);
return;
};
常用載入本地
loader
兩種方式
1.path.resolve
使用 path.resolve
指向這個本地檔案
```js const path = require('path')
module.exports = { module: { rules: [ { test: /.js$/, use: path.resolve('./src/myLoader/my-loader.js'), }, ], }, }
```
2.ResolveLoader
先去 node_modules
專案下尋找 my-loader
,如果找不到,會再去 ./src/myLoader/
目錄下尋找。
```js
module.exports = { //... module: { rules: [ { test: /.js$/, use: ['my-loader'], }, ], }, resolveLoader: { modules: ['node_modules', './src/myLoader'], }, }
```
一個 loader
的職責是單一的,使每個loader
易維護。
如果原始檔需要分多步轉換才能正常使用,通過多個Loader進行轉換。當呼叫多個loader
進行檔案轉換時,每個loader
都會鏈式執行。
第一個loader
會得到要處理的原始內容,將前一個loader處理的結果傳遞給下一個。 處理完畢,最終的Loader會將處理後的最終結果返回給 Webpack
所以,當你寫loader
記得保持它的職責單一,你只關心輸入和輸出。
3.option
引數
js
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: 'my-loader',
options: {
flag: true,
},
},
],
},
],
},
那麼我們如何在loader中獲取這個寫入配置資訊呢?
Webpack
提供了loader-utils
工具
在之前寫的loader修改
js
const loaderUtils = require('loader-utils')
module.exports = function (source) {
// 獲取到使用者給當前 Loader 傳入的 options
const options = loaderUtils.getOptions(this)
console.log('options-->', options)
// 在這裡按照你的需求處理 source
return source.replace('word', ', I am Xiaolang')
}
控制檯也列印了出來
4.快取
如果為每個構建重新執行重複的轉換操作,這樣Webpack
構建可能會變得非常慢。
Webpack
預設會快取所有loader
的處理結果,也就是說,當待處理的檔案或者依賴的檔案沒有變化時,不會再次呼叫對應的loader
進行轉換操作
js
module.exports = function (source) {
// 開始快取
this.cacheable && this.cacheable();
// 在這裡按照你的需求處理 source
return source.replace('word', ', I am Xiaolang')
}
一般預設開啟快取,如果不想Webpack
這個loader
進行快取,也可以關閉快取
js
module.exports = function (source) {
// 關閉快取
this.cacheable(false);
// 在這裡按照你的需求處理 source
return source.replace('word', ', I am Xiaolang')
}
5.同步與非同步
在某些情況下,轉換步驟只能非同步完成。
例如,您需要發出網路請求以獲取結果。 如果使用同步方式,網路請求會阻塞整個構建,導致構建非常緩慢。
js
module.exports = function(source) {
// 告訴 Webpack 本次轉換是非同步的,Loader 會在 callback 中回撥結果
var callback = this.async()
// someAsyncOperation 代表一些非同步的方法
someAsyncOperation(source, function (err, result, sourceMaps, ast) {
// 通過 callback 返回非同步執行後的結果
callback(err, result, sourceMaps, ast)
})
};
6.處理二進位制資料
預設情況下,Webpack
傳遞給 Loader
的原始內容是一個 UTF-8
格式編碼的字串。 但是在某些場景下,載入器處理的不是文字檔案,而是二進位制檔案
官網例子 通過 exports.raw
屬性告訴 Webpack
該 Loader
是否需要二進位制資料
js
module.exports = function(source) {
// 在 exports.raw === true 時,Webpack 傳給 Loader 的 source 是 Buffer 型別的
source instanceof Buffer === true;
// Loader 返回的型別也可以是 Buffer 型別的
// 在 exports.raw !== true 時,Loader 也可以返回 Buffer 型別的結果
return source;
};
// 通過 exports.raw 屬性告訴 Webpack 該 Loader 是否需要二進位制資料
module.exports.raw = true;
7.實現一個渲染markdown文件loader
安裝依賴
md
轉html
的依賴,當然可以選擇另外一個模組marked
我這裡使用的
markdown-it
bash
npm install [email protected] -D
輔助工具 用來新增
div
和class
```js module.exports = function ModifyStructure(html) { // 把h3和h2開頭的切成陣列 const htmlList = html.replace(/<h3/g, '$(<h3').replace(/<h2/g, '$(<h2').split('$*(')
// 給他們套上 .card 類名的 div
return htmlList
.map(item => {
if (item.indexOf('<h3') !== -1) {
return `<div class="card card-3">${item}</div>`
} else if (item.indexOf('<h2') !== -1) {
return `<div class="card card-2">${item}</div>`
}
return item
})
.join('')
}
```
新建一個loader
/src/myLoader/md-loader.js
js
const { getOptions } = require('loader-utils')
const MarkdownIt = require('markdown-it')
const beautify = require('./beautify')
module.exports = function (source) {
const options = getOptions(this) || {}
const md = new MarkdownIt({
html: true,
...options,
})
let html = beautify(md.render(source))
html = `module.exports = ${JSON.stringify(html)}`
this.callback(null, html)
}
這樣loader
也寫完了,this.callback(null, html)
和 return
在這裡差不多哈。
js
html = `module.exports = ${JSON.stringify(html)}`
這裡解析的結果是一個 HTML
字串。 如果直接返回,也會面臨Webpack
無法解析模組的問題。 正確的做法是把這個HTML
字串拼接成一段JS
程式碼。
這時候我們要返回的程式碼就是通過module.exports
匯出這個HTML
字串,這樣外界在匯入模組的時候就可以接收到這個HTML
字串。
然後在
webpack.config.js
使用這個載入器
```js const path = require('path')
module.exports = { entry: './src/index.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'bundle.js', }, module: { rules: [ { test: /.js$/, use: [ { loader: 'my-loader', options: { flag: true, }, }, ], }, { test: /.md$/, use: [ { loader: 'md-loader', }, ], }, ], }, resolveLoader: { modules: ['node_modules', './src/myLoader'], }, devServer: { contentBase: './dist', overlay: { warnings: true, errors: true, }, open: true, }, }
```
使用
最後在
index.js
中載入一個md
檔案,我這裡隨便整個,新建github
的readme.md
```js document.write('hello word')
import mdHtml from './test.md' const content = document.createElement('div') content.className = 'content' content.innerHTML = mdHtml document.body.appendChild(content) ```
結果圖
目錄結構
bash
Webpack-demo
├── dist
│ └── index.html
├── package-lock.json
├── package.json
├── src
│ ├── index.js
│ ├── myLoader
│ │ ├── beautify.js
│ │ ├── md-loader.js
│ │ └── my-loader.js
│ └── test.md
└── webpack.config.js
結語
感謝大家能看到這裡哈~ ,現在打包構建工具也慢慢增多了vue-cli
,vite
等等,但是 webpack
仍然有一席之地,很多值得學習的地方,繼續努力學習~~