【手寫loader】- 在webpack打包階段檢測失效圖片資源並替換

語言: CN / TW / HK
ead>

theme: smartblue highlight: a11y-dark


「這是我參與2022首次更文挑戰的第4天,活動詳情檢視:2022首次更文挑戰

首發於我的公眾號「前端面壁者」,歡迎關注。

loader 本質上是匯出為函式的 JavaScript 模組。loader runner 中包含實用的方法this.async()可以使 loader 呼叫方式變為非同步。

一、環境搭建

1. 專案初始化

或許你期望按照官方給出的指南來起步

新建目錄並初始化專案

js mkdir webpack-loader cd webpack-loader npm init

安裝webpackcli

js npm i -D webpack webpack-cli

安裝html-webpack-pluginclean-webpack-plugin

js npm i -D html-webpack-plugin npm i -D clean-webpack-plugin

  • html-webpack-plugin 會將 webpack 打包出來的 js 檔案自動引入到 html 中
  • 每次執行 npm run build 會發現 dist 資料夾裡會殘留上次打包的檔案,clean-webpack-plugin 在打包輸出前會清空資料夾

2. 專案配置(模擬使用場景)

新建一個資料夾 src,然後新建一個檔案 index.js,編寫一段程式碼模擬使用場景

js const checkUrlList = [ "http://d-dev.zhgcloud.com/image/2021/6/2/60b75680287c1.png", "http://d-dev.zhgcloud.com/image/2021/6/2/60b73e58b38d0.jpg", "http://d-dev.zhgcloud.com/image/2021/6/2/60b75758164c6.png", "http://d-dev.zhgcloud.com/image/2021/6/2/60b752cd01b1c.png", "http://webpack.docschina.org/site-logo.1fcab817090e78435061.svg", ]; for (let i = 0; i < 5; i++) { console.log(i); var img = document.createElement("img"); img.setAttribute("src", checkUrlList[i]); document.getElementById("body").appendChild(img); }

  • 前四張圖片地址是失效的,第五張可訪問

新建一個資料夾 public,然後新建一個檔案 index.html,用於在瀏覽器除錯

```html

Document ```

  • body設定一個id="body"

3. webpack 配置

新建一個 build 資料夾,裡面新建一個 webpack.config.js,進行如下配置

```js const path = require("path"); const HtmlWebpackPlugin = require("html-webpack-plugin"); const { CleanWebpackPlugin } = require("clean-webpack-plugin"); module.exports = { mode: "development", // 開發模式 entry: { index: path.resolve(__dirname, "../src/index.js"), }, // 入口檔案 output: { filename: "[name].[chunkhash:8].js", // 打包後的檔名稱 path: path.resolve(__dirname, "../dist"), // 打包後的目錄 },

plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
        template: path.resolve(__dirname, "../public/index.html"),
        filename: "index.html",
        chunks: ["index"], // 與入口檔案對應的模組名
    }),
],

}; ```

package.json新增打包命令

build.png

Tips:需要注意的是webpack是執行在node環境中的,謹記這一點下面會用到

4. 使用 loader 前打包

執行打包命令

run-build.png

在瀏覽器檢視未使用 loader 打包的 index.js

result.png

  • 頁面按序載入了四張失效圖和一張有效圖

二、手寫 loader

1. 自定義 replace-legal-img

新建一個 lib 資料夾,裡面新建一個 replace-legal-img.js,編寫如下程式碼

js const https = require("https"); // 載入待檢測的 圖片`url`,根據其返回值決定是否替換成合法圖片`url` const parser = require("@babel/parser"); // 會將原始碼解析成 `AST` const traverse = require("@babel/traverse").default; // 對 `AST` 節點進行遞迴遍歷,生成一個便於操作、轉換的 `path` 物件 const generator = require("@babel/generator").default; // 將 `AST` 解碼生成 `js` 程式碼 module.exports = function (source) { const ast = parser.parse(source, { sourceType: "module" }); var callback = this.async(); let count = 0; let promiseAll = []; traverse(ast, { enter(path) { if ( path.node.type === "StringLiteral" && /^http[s]{0,1}:\/\/([\w.]+\/?)\S*[png|jpg|image|svg]$/.test( path.node.value ) ) { promiseAll[`${count}`] = new Promise((resolve, reject) => { https.get(path.node.value, function (res) { if (res.statusCode === 404) { path.node.value = "http://p26-passport.byteacctimg.com/img/mosaic-legacy/3793/3114521287~300x300.image"; resolve(path.node.value); } else if (res.statusCode === 200) { resolve(path.node.value); } }); }); count++; } }, }); Promise.all(promiseAll) .then((result) => { output = generator(ast, {}, source); callback(null, output.code); }) .catch((error) => { console.log(error); }); };

  • 由於 webpack 基於 node,所以使用https發起網路請求
  • 圖片urlString 型別的字面量即 StringLiteral,配合正則表示式可以排出非圖片url型別的StringLiteral
  • 由於網路請求是非同步的,需要配合this.async()告知 loader runner 等待非同步結果返回
  • source若包含多個圖片URL會發起多個網路請求,這時需要使用 promiseAll組織所有promise的返回,使用 count 計數
  • source中所有圖片URL的請求全部執行完畢時Promise.all(promiseAll)給出返回值,這裡再使用generator將檢測替換新 value 後的 AST轉換成 js,並使用 callback告知loader runner執行完畢

2. webpack配置自定義loader

在 webpack.config.js 中引入這個 loader

```js module.exports = { ...

module: {
    rules: [
        {
            test: /\.js$/,
            use: path.resolve(__dirname, "../lib/replace-legal-img.js"),
        },
    ],
},

...

}; ```

3. 效果展示

在瀏覽器檢視未使用 loader 打包的 index.js

new-result.png

  • 頁面按序載入了四張被替換的圖片和一張有效圖

三、總結

  注意到掘金和CSDN等平臺有許多手寫loader的文,蒐羅下來感覺值得借鑑的並不多,大多都是一些很簡單的示例,真正有生產意義的很少。所以對於新手來說,如何寫一個有效的loader,或者說實現有效loader的難點吧,個人感覺難點有兩個,一是邁出第一步,二是知識儲備。

  我自己的實踐方式是多看多寫,看一是看文件,二是看成熟專案的原始碼和優秀的blog,寫就儘可能的放到一個具體的場景裡,這樣寫出來的東西多少優點意義。比如在上面的loader中只是實現了最簡單的圖片資源檢測,且同意替換成固定的資源連結。如果source裡有來自多個圖床地址,替換的資源支援自定義,都可以在這個基礎上擴充套件。

四、參考

1. 2020 年了,再不會 webpack 敲得程式碼就不香了(近萬字實戰)

2. AST 抽象語法樹——最基礎的 javascript 重點知識,99%的人根本不瞭解

3. Webpack 手寫 loader 和 plugin

4. 手寫清除 console 的 loader

5. babel-doc webpack-doc