手把手教你在Webpack寫一個(gè)Loader

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

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

Loader是什么?
loader本質(zhì)上是一個(gè)node模塊,符合Webpack中一切皆模塊的思想。由于它是一個(gè) node 模塊,它必須導(dǎo)出一些東西。loader本身就是一個(gè)函數(shù),在該函數(shù)中對(duì)接收到的內(nèi)容進(jìn)行轉(zhuǎn)換,然后返回轉(zhuǎn)換后的結(jié)果

下面小浪為你簡(jiǎn)單介紹下webpack中的loader

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

那么開始吧,首先先介紹 處理 CSS 相關(guān)的 Loader

css-loader 和 style-loader
安裝依賴

npm install css-loader style-loader
復(fù)制代碼
使用加載器

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

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

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

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

style-loader加到了css-loader前面,這是因?yàn)樵赪ebpack打包時(shí)是按照數(shù)組從后往前的順序?qū)①Y源交給loader處理的,因此要把最后生效的放在前面

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

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

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

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

rules: [
    {
        test: /.css$/,
        use: ['style-loader', 'css-loader'],
        exclude: /node_modules/,
        include: /src/,
    }
],

復(fù)制代碼
是否都還記得呢,現(xiàn)在有現(xiàn)成的腳手架,很多人都很少自己去配置這些了,欸~當(dāng)然還有相關(guān)的 sass/less等等預(yù)處理器loader這里就不一一介紹了。

babel-loader
babel-loader 這個(gè)loader十分的重要,把高級(jí)語(yǔ)法轉(zhuǎn)為ES5,常用于處理 ES6+ 并將其編譯為 ES5。它允許我們?cè)陧?xiàng)目中使用最新的語(yǔ)言特性(甚至在提案中),而無需特別注意這些特性在不同平臺(tái)上的兼容性。

介紹下主要的三個(gè)模塊

babel-loader:使 Babel 與 Webpack 一起工作的模塊
@babel/core:Babel核心模塊。
@babel/preset-env:是Babel官方推薦的preseter,可以根據(jù)用戶設(shè)置的目標(biāo)環(huán)境,自動(dòng)添加編譯ES6+代碼所需的插件和補(bǔ)丁
安裝

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

rules: [
  {
    test: /.js$/,
    exclude: /node_modules/, //排除掉,不排除拖慢打包的速度
    use: {
      loader: 'babel-loader',
      options: {
        cacheDirectory: true, // 啟用緩存機(jī)制以防止在重新打包未更改的模塊時(shí)進(jìn)行二次編譯
        presets: [[
          'env', {
            modules: false, // 將ES6 Module的語(yǔ)法交給Webpack本身處理
          }
        ]],
      },
    },
  }
],
復(fù)制代碼
html-loader
Webpack 可不認(rèn)識(shí) html,直接報(bào)錯(cuò),需要loader轉(zhuǎn)化

html-loader 用于將 HTML 文件轉(zhuǎn)換為字符串并進(jìn)行格式化,它允許我們通過 JS 加載一個(gè) HTML 片段。

安裝

npm install html-loader
復(fù)制代碼
配置

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

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

安裝

npm install file-loader
復(fù)制代碼
配置

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',
            }
        ],
    },
};
復(fù)制代碼
url-loader
既然介紹了 file-loader 就不得不介紹 url-loader,它們很相似,但是唯一的區(qū)別是用戶可以設(shè)置文件大小閾值。大于閾值時(shí)返回與file-loader相同的publicPath,小于閾值時(shí)返回文件base64編碼。

安裝

npm install url-loader
復(fù)制代碼
配置

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

安裝

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

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

安裝

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





寫一個(gè)簡(jiǎn)單的Loader
介紹了幾個(gè)常見的loader的安裝配置,我們?cè)诰唧w的業(yè)務(wù)的實(shí)現(xiàn)的時(shí)候,可能遇到各種需求,上面介紹的或者npm上都沒有的加載器都不適合當(dāng)前的業(yè)務(wù)場(chǎng)景,那我們可以自己去實(shí)現(xiàn)一個(gè)自己的loader來滿足自己的需求,小浪下面介紹一下如何自定義一個(gè)loader

1.初始化項(xiàng)目
初始化項(xiàng)目

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

npm init -y
復(fù)制代碼
安裝依賴

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

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

npm install webpack@4.39.2 webpack-cli@3.3.6 webpack-dev-server@3.11.0 -D
復(fù)制代碼
新建一個(gè)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>

復(fù)制代碼
新建一個(gè)入口文件 index.js 文件

src/index.js

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

配置出口和入口文件

配置devServer服務(wù)

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,
    },
}
復(fù)制代碼
在 package.json 中配置啟動(dòng)命令

  "scripts": {
    "dev": "Webpack-dev-server"
  },
復(fù)制代碼
啟動(dòng) npm run dev

devServer幫我們啟動(dòng)一個(gè)服務(wù)器,每次修改index.js不需要自己在去打包,而是自動(dòng)幫我們完成這項(xiàng)任務(wù)

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








當(dāng)前的文件目錄

Webpack-demo
 ├── dist
 │   └── index.html
 ├── package-lock.json
 ├── package.json
 ├── src
 │   └── index.js
 └── Webpack.config.js
復(fù)制代碼
2.實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 loader
在 src/MyLoader/my-loader.js

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

this.callback(    
    // 當(dāng)無法轉(zhuǎn)換原內(nèi)容時(shí),給 Webpack 返回一個(gè) Error   
    err: Error | null,    
    // 原內(nèi)容轉(zhuǎn)換后的內(nèi)容    
    content: string | Buffer,    
    // 用于把轉(zhuǎn)換后的內(nèi)容得出原內(nèi)容的 Source Map,方便調(diào)試
    sourceMap?: SourceMap,    
    // 如果本次轉(zhuǎn)換為原內(nèi)容生成了 AST 語(yǔ)法樹,可以把這個(gè) AST 返回,以方便之后需要 AST 的 Loader 復(fù)用該 AST,以避免重復(fù)生成 AST,提升性能
    abstractSyntaxTree?: AST
);
復(fù)制代碼
打開代碼對(duì)應(yīng)的source-map,方便調(diào)試源代碼。source-map 可以方便實(shí)際開發(fā)者在瀏覽器控制臺(tái)查看源代碼。如果不處理source-map,最終將無法生成正確的map文件,在瀏覽器的開發(fā)工具中可能會(huì)看到混亂的源代碼。

為了在使用 this.callback 返回內(nèi)容時(shí)將 source-map 返回給 Webpack

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

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

1.path.resolve

使用 path.resolve 指向這個(gè)本地文件

const path = require('path')

module.exports = {
    module: {
        rules: [
            {
                test: /.js$/,
                use: path.resolve('./src/myLoader/my-loader.js'),
            },
        ],
    },
}

復(fù)制代碼
2.ResolveLoader

先去 node_modules 項(xiàng)目下尋找 my-loader,如果找不到,會(huì)再去 ./src/myLoader/ 目錄下尋找。


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

復(fù)制代碼
一個(gè) loader的職責(zé)是單一的,使每個(gè)loader易維護(hù)。

如果源文件需要分多步轉(zhuǎn)換才能正常使用,通過多個(gè)Loader進(jìn)行轉(zhuǎn)換。當(dāng)調(diào)用多個(gè)loader進(jìn)行文件轉(zhuǎn)換時(shí),每個(gè)loader都會(huì)鏈?zhǔn)綀?zhí)行。

第一個(gè)loader會(huì)得到要處理的原始內(nèi)容,將前一個(gè)loader處理的結(jié)果傳遞給下一個(gè)。處理完畢,最終的Loader會(huì)將處理后的最終結(jié)果返回給 Webpack

所以,當(dāng)你寫loader記得保持它的職責(zé)單一,你只關(guān)心輸入和輸出。








3.option參數(shù)
module: {
    rules: [
        {
            test: /.js$/,
            use: [
                {
                    loader: 'my-loader',
                    options: {
                        flag: true,
                    },
                },
            ],
        },
    ],
},
復(fù)制代碼
那么我們?nèi)绾卧趌oader中獲取這個(gè)寫入配置信息呢?

Webpack 提供了loader-utils工具






在之前寫的loader修改

const loaderUtils = require('loader-utils')
module.exports = function (source) {
    // 獲取到用戶給當(dāng)前 Loader 傳入的 options
    const options = loaderUtils.getOptions(this)
    console.log('options-->', options)
    // 在這里按照你的需求處理 source
    return source.replace('word', ', I am Xiaolang')
}
復(fù)制代碼
控制臺(tái)也打印了出來




4.緩存
如果為每個(gè)構(gòu)建重新執(zhí)行重復(fù)的轉(zhuǎn)換操作,這樣Webpack構(gòu)建可能會(huì)變得非常慢。

Webpack 默認(rèn)會(huì)緩存所有l(wèi)oader的處理結(jié)果,也就是說,當(dāng)待處理的文件或者依賴的文件沒有變化時(shí),不會(huì)再次調(diào)用對(duì)應(yīng)的loader進(jìn)行轉(zhuǎn)換操作

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

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

例如,您需要發(fā)出網(wǎng)絡(luò)請(qǐng)求以獲取結(jié)果。如果使用同步方式,網(wǎng)絡(luò)請(qǐng)求會(huì)阻塞整個(gè)構(gòu)建,導(dǎo)致構(gòu)建非常緩慢。

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

官網(wǎng)例子 通過 exports.raw 屬性告訴 Webpack 該 Loader 是否需要二進(jìn)制數(shù)據(jù)

module.exports = function(source) {    
    // 在 exports.raw === true 時(shí),Webpack 傳給 Loader 的 source 是 Buffer 類型的    
    source instanceof Buffer === true;    
    // Loader 返回的類型也可以是 Buffer 類型的    
    // 在 exports.raw !== true 時(shí),Loader 也可以返回 Buffer 類型的結(jié)果    
    return source;
};
// 通過 exports.raw 屬性告訴 Webpack 該 Loader 是否需要二進(jìn)制數(shù)據(jù)
module.exports.raw = true;
復(fù)制代碼
7.實(shí)現(xiàn)一個(gè)渲染markdown文檔loader
安裝依賴 md 轉(zhuǎn) html 的依賴,當(dāng)然可以選擇另外一個(gè)模塊 marked

我這里使用的 markdown-it

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

module.exports = function ModifyStructure(html) {
    // 把h3和h2開頭的切成數(shù)組
    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('')
}

復(fù)制代碼
新建一個(gè)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)
}
復(fù)制代碼
這樣loader也寫完了,this.callback(null, html) 和 return 在這里差不多哈。

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

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

然后在webpack.config.js使用這個(gè)加載器

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,
    },
}

復(fù)制代碼
使用

最后在index.js中加載一個(gè)md文件,我這里隨便整個(gè),新建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)
復(fù)制代碼
結(jié)果圖


目錄結(jié)構(gòu)

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
復(fù)制代碼
github倉(cāng)庫(kù)地址[1]

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

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

作者:小浪努力學(xué)前端



歡迎關(guān)注微信公眾號(hào) :前端晚間課

更多文章,收錄于小程序-互聯(lián)網(wǎng)小兵