手把手教你在Webpack寫一個Loader

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

為什么需要Loader?
Webpack 它只能處理 js 和 JSON 文件。面對 css 文件還有一些圖片等等,Webpack 它自己是不能夠處理的,它需要loader 處理其他類型的文件并將它們轉換為有效的模塊以供應用程序使用并添加到依賴關系圖中,

Loader是什么?
loader本質上是一個node模塊,符合Webpack中一切皆模塊的思想。由于它是一個 node 模塊,它必須導出一些東西。loader本身就是一個函數,在該函數中對接收到的內容進行轉換,然后返回轉換后的結果

下面小浪為你簡單介紹下webpack中的loader

常見的loader
我們先來回顧下常見的 Loader 基礎的配置和使用吧(僅僅只是常見的,npm上面開發(fā)者大佬們發(fā)布的太多了)

那么開始吧,首先先介紹 處理 CSS 相關的 Loader

css-loader 和 style-loader
安裝依賴

npm install css-loader style-loader
復制代碼
使用加載器

module.exports = {
    // ...
    module: {
        rules: [{
            test: /.css$/,
            use: ['style-loader', 'css-loader'],
        }],
    },
};
復制代碼
其中module.rules代表模塊的處理規(guī)則。每個規(guī)則可以包含很多配置項

test 可以接收正則表達式或元素為正則表達式的數組。只有與正則表達式匹配的模塊才會使用此規(guī)則。在此示例中,/.css$/ 匹配所有以 .css 結尾的文件。

use 可以接收一個包含規(guī)則使用的加載器的數組。如果只配置了一個css-loader,當只有一個loader時也可以為字符串

css-loader 的作用只是處理 CSS 的各種加載語法(@import 和 url() 函數等),如果樣式要工作,則需要 style-loader 將樣式插入頁面

style-loader加到了css-loader前面,這是因為在Webpack打包時是按照數組從后往前的順序將資源交給loader處理的,因此要把最后生效的放在前面

還可以這樣寫成對象的形式,里面options傳入配置

module.exports = {
    // ...
    module: {
        rules: [{
            test: /.css$/,
            use: [
                'style-loader',
                  {
                    loader: 'css-loader',
                    options: {
                        // css-loader 配置項
                 },
               }
            ],
        }],
    },
};
復制代碼
exclude與include

include代表該規(guī)則只對正則匹配到的模塊生效

exclude的含義是,所有被正則匹配到的模塊都排除在該規(guī)則之外

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,可以根據用戶設置的目標環(huán)境,自動添加編譯ES6+代碼所需的插件和補丁
安裝

npm install babel-loader @babel/core @babel/preset-env
復制代碼
配置

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 片段。

安裝

npm install html-loader
復制代碼
配置

rules: [
    {
        test: /.html$/,
        use: 'html-loader',
    }
],
復制代碼
// index.js
import otherHtml from './other.html';
document.write(otherHtml);
復制代碼
這樣你可以在js中加載另一個頁面,寫刀當前index.html里面

file-loader
用于打包文件類型的資源,比如對png、jpg、gif等圖片資源使用file-loader,然后就可以在JS中加載圖片了

安裝

npm install file-loader
復制代碼
配置

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,它們很相似,但是唯一的區(qū)別是用戶可以設置文件大小閾值。大于閾值時返回與file-loader相同的publicPath,小于閾值時返回文件base64編碼。

安裝

npm install url-loader
復制代碼
配置

rules: [
    {
        test: /.(png|jpg|gif)$/,
        use: {
            loader: 'url-loader',
            options: {
                limit: 1024,
                name: '[name].[ext]',
                publicPath: './assets/',
            },
        },
    }
],
復制代碼
ts-loader
TypeScript使用得越來越多,對于我們平時寫代碼有了更好的規(guī)范,項目更加利于維護...等等好處,我們也在Webpack中來配置loader,本質上類似于 babel-loader,是一個連接 Webpack 和 Typescript 的模塊

安裝

npm install ts-loader typescript
復制代碼
loader配置,主要的配置還是在 tsconfig.json 中

rules: [
    {
        test: /.ts$/,
        use: 'ts-loader',
    }
],
復制代碼
vue-loader
用來處理vue組件,還要安裝vue-template-compiler來編譯Vue模板,估計大家大部分都用腳手架了

安裝

npm install vue-loader  vue-template-compiler
復制代碼
rules: [
    {
        test: /.vue$/,
        use: 'vue-loader',
    }
],
復制代碼





寫一個簡單的Loader
介紹了幾個常見的loader的安裝配置,我們在具體的業(yè)務的實現的時候,可能遇到各種需求,上面介紹的或者npm上都沒有的加載器都不適合當前的業(yè)務場景,那我們可以自己去實現一個自己的loader來滿足自己的需求,小浪下面介紹一下如何自定義一個loader

1.初始化項目
初始化項目

先創(chuàng)建一個項目文件夾(名字可以隨意,當然肯定是英文名)后進行初始化

npm init -y
復制代碼
安裝依賴

安裝依賴:Webpack 和 Webpack腳手架 和 熱更新服務器

不同的版本 Webpack 可能有些差異,如果你跟著我的這個例子寫的話,小浪建議和我裝一樣的版本

npm install webpack@4.39.2 webpack-cli@3.3.6 webpack-dev-server@3.11.0 -D
復制代碼
新建一個index.html文件

dist/index.html

<!DOCTYPE html>
<html lang="zh-CN">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <meta http-equiv="X-UA-Compatible" content="ie=edge" />
        <title></title>
    </head>
    <body>
        <script src="./bundle.js"></script>
    </body>
</html>

復制代碼
新建一個入口文件 index.js 文件

src/index.js

document.write('hello world')
復制代碼
創(chuàng)建 webpack.config.js 配置文件

配置出口和入口文件

配置devServer服務

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 中配置啟動命令

  "scripts": {
    "dev": "Webpack-dev-server"
  },
復制代碼
啟動 npm run dev

devServer幫我們啟動一個服務器,每次修改index.js不需要自己在去打包,而是自動幫我們完成這項任務

頁面內容就是我們index.js編寫的內容被打包成在dist/bundle.js引入到index.html了








當前的文件目錄

Webpack-demo
 ├── dist
 │   └── index.html
 ├── package-lock.json
 ├── package.json
 ├── src
 │   └── index.js
 └── Webpack.config.js
復制代碼
2.實現一個簡單的 loader
在 src/MyLoader/my-loader.js

module.exports = function (source) {
    // 在這里按照你的需求處理 source
    return source.replace('word', ', I am Xiaolang')
}
復制代碼
返回其它結果 this.callback

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 可以方便實際開發(fā)者在瀏覽器控制臺查看源代碼。如果不處理source-map,最終將無法生成正確的map文件,在瀏覽器的開發(fā)工具中可能會看到混亂的源代碼。

為了在使用 this.callback 返回內容時將 source-map 返回給 Webpack

loader 必須返回 undefined 讓 Webpack 知道 loader 返回的結果在 this.callback 中,而不是在 return

module.exports = function(source) {
    // 通過 this.callback 告訴 Webpack 返回的結果
    this.callback(null, source.replace('word', ', I am Xiaolang'), sourceMaps);   
    return;
};
復制代碼
常用加載本地 loader 兩種方式

1.path.resolve

使用 path.resolve 指向這個本地文件

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/ 目錄下尋找。


module.exports = {
 //...
    module: {
        rules: [
            {
                test: /.js$/,
                use: ['my-loader'],
            },
        ],
    },
    resolveLoader: {
        modules: ['node_modules', './src/myLoader'],
    },
}

復制代碼
一個 loader的職責是單一的,使每個loader易維護。

如果源文件需要分多步轉換才能正常使用,通過多個Loader進行轉換。當調用多個loader進行文件轉換時,每個loader都會鏈式執(zhí)行。

第一個loader會得到要處理的原始內容,將前一個loader處理的結果傳遞給下一個。處理完畢,最終的Loader會將處理后的最終結果返回給 Webpack

所以,當你寫loader記得保持它的職責單一,你只關心輸入和輸出。








3.option參數
module: {
    rules: [
        {
            test: /.js$/,
            use: [
                {
                    loader: 'my-loader',
                    options: {
                        flag: true,
                    },
                },
            ],
        },
    ],
},
復制代碼
那么我們如何在loader中獲取這個寫入配置信息呢?

Webpack 提供了loader-utils工具






在之前寫的loader修改

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.緩存
如果為每個構建重新執(zhí)行重復的轉換操作,這樣Webpack構建可能會變得非常慢。

Webpack 默認會緩存所有l(wèi)oader的處理結果,也就是說,當待處理的文件或者依賴的文件沒有變化時,不會再次調用對應的loader進行轉換操作

module.exports = function (source) {
    // 開始緩存
    this.cacheable && this.cacheable();
    // 在這里按照你的需求處理 source
    return source.replace('word', ', I am Xiaolang')
}
復制代碼
一般默認開啟緩存,如果不想Webpack這個loader進行緩存,也可以關閉緩存

module.exports = function (source) {
    // 關閉緩存
    this.cacheable(false);
    // 在這里按照你的需求處理 source
    return source.replace('word', ', I am Xiaolang')
}
復制代碼
5.同步與異步
在某些情況下,轉換步驟只能異步完成。

例如,您需要發(fā)出網絡請求以獲取結果。如果使用同步方式,網絡請求會阻塞整個構建,導致構建非常緩慢。

module.exports = function(source) {    
    // 告訴 Webpack 本次轉換是異步的,Loader 會在 callback 中回調結果
    var callback = this.async()
    // someAsyncOperation 代表一些異步的方法
    someAsyncOperation(source, function (err, result, sourceMaps, ast) {
        // 通過 callback 返回異步執(zhí)行后的結果
        callback(err, result, sourceMaps, ast)
    })
};
復制代碼
6.處理二進制數據
默認情況下,Webpack 傳遞給 Loader 的原始內容是一個 UTF-8 格式編碼的字符串。但是在某些場景下,加載器處理的不是文本文件,而是二進制文件

官網例子 通過 exports.raw 屬性告訴 Webpack 該 Loader 是否需要二進制數據

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

npm install markdown-it@12.0.6 -D
復制代碼
輔助工具 用來添加 div 和 class

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

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 在這里差不多哈。

html = `module.exports = ${JSON.stringify(html)}`
復制代碼
這里解析的結果是一個 HTML 字符串。如果直接返回,也會面臨Webpack無法解析模塊的問題。正確的做法是把這個HTML字符串拼接成一段JS代碼。

這時候我們要返回的代碼就是通過module.exports導出這個HTML字符串,這樣外界在導入模塊的時候就可以接收到這個HTML字符串。

然后在webpack.config.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

document.write('hello word')

import mdHtml from './test.md'
const content = document.createElement('div')
content.className = 'content'
content.innerHTML = mdHtml
document.body.appendChild(content)
復制代碼
結果圖


目錄結構

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倉庫地址[1]

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

來源:小浪努力學前端,https://juejin.cn/post/7100534685134454815

作者:小浪努力學前端



歡迎關注微信公眾號 :前端晚間課

更多文章,收錄于小程序-互聯網小兵