手把手教你在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)小兵