手把手教你在Webpack寫一個Loader

語言: CN / TW / HK

theme: smartblue highlight: atelier-sulphurpool-light


前言

有的時候,你可能在從零搭建Webpack 專案很熟悉,配置過各種 loader ,面試官在 Webpack 方面問你,是否自己實現過一個loader?如果沒有去了解過如果去實現,確實有點尷尬,其實呢,loader實現其實很簡單的。下面說下loader是什麼?

為什麼需要Loader?

Webpack 它只能處理 jsJSON 檔案。面對 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 的各種載入語法(@importurl() 函式等),如果樣式要工作,則需要 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 配置項 }, } ], }], }, };

excludeinclude

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:使 BabelWebpack 一起工作的模組
  • @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

用於打包檔案型別的資源,比如對pngjpggif等圖片資源使用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,是一個連線 WebpackTypescript 的模組

安裝

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

安裝依賴

安裝依賴: WebpackWebpack腳手架 和 熱更新伺服器

不同的版本 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

image.png

當前的檔案目錄

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 必須返回 undefinedWebpack 知道 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記得保持它的職責單一,你只關心輸入和輸出。

image-20220522142823507

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') }

控制檯也列印了出來

image-20220522143828316

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 屬性告訴 WebpackLoader 是否需要二進位制資料

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

安裝依賴 mdhtml 的依賴,當然可以選擇另外一個模組 marked

我這裡使用的 markdown-it

bash npm install [email protected] -D

輔助工具 用來新增 divclass

```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檔案,我這裡隨便整個,新建githubreadme.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) ```

結果圖

image-20220522165928553

目錄結構

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

github倉庫地址

結語

感謝大家能看到這裡哈~ ,現在打包構建工具也慢慢增多了vue-clivite等等,但是 webpack 仍然有一席之地,很多值得學習的地方,繼續努力學習~~