前言
我們知道 webpack 只能處理 JavaScript 和 Json 文件,面對 CSS、圖片等資源是無能為力的,它需要通過 loader 將這些資源轉換為可處理的模塊。
loader 的本質是一個解析資源的函數模塊,該函數對接受到的內容進行轉換,然後返回 webpack 可處理的資源。
loader的執行順序
loader 可根據執行順序區分為:
- pre: 前置 loader
- normal: 普通 loader
- inline: 內聯 loader
- post: 後置 loader
通過配置 enforce,限定 loader 類型
{
enforce: "[pre|normal(缺省)|inline|post]",
test: /.js$/,
loader: "xxx-loader",
}
loader執行順序:pre > normal > inline > post,同級的 loader 根據配置順序自上而下(從右到左)執行
常見的 Loader
在手寫自定義 loader 前,先來回顧一下 webpack 中常見的 loader。
樣式處理loader
webpack 處理樣式資源,提供了兩個 loader:style-loader、css-loader。
css-loader 用於將 css 資源轉化成 webpack 可處理的模塊。而 style-loader 將模塊導出的內容作為樣式並添加到 DOM 中。下面是兩個 loader 的用法:
安裝依賴
npm install css-loader style-loader -D
配置
module.exports = {
// ...
module: {
rules: [{
test: /.css$/,
use: ['style-loader', 'css-loader'],
}]
},
};
module.rules代表模塊的處理規則。test 選項接收一個匹配需要處理資源的正則表達式。use 選項接收一個處理資源的 loader 數組。
babel-loader
babel-loader 是一個基於 Babel 實現的,用於加載 ES2015+ 代碼並將其轉換為 ES5。
在安裝依賴時,同時需要安裝 Babel 的核心包@babel/core以及 Babel 官方的預設@babel/preset-env
npm install babel-loader @babel/core @babel/preset-env -D
在 webpack.config.js 中配置,這裏需要將 Babel 預設@babel/preset-env通過 options 傳遞給babel-loader
module: {
rules: [{
test: /.js$/,
include: path.resolve(__dirname, 'src'),
use: ['babel-loader'],
options: {
presets: ['@babel/preset-env'],
}
}]
},
這裏配置的 options,在loader中提供了一個工具包loader-utils,通過裏面的getOptions方法獲取。
const loaderUtils = require('loader-utils');
module.exports = function (content, map, meta) {
const options = loaderUtils.getOptions(this);
// ...
}
ts-loader
隨着 JavaScript 的超集 TypeScript 的發展,越來越的項目引用了 TypeScript 作為開發語言和規範。而 TypeScript 同樣是需要編譯轉換成 JavaScript 代碼運行,ts-loader 就是用於編譯轉換 TypeScript 的工具。
安裝依賴
npm install ts-loader -D
在規則中配置
rules: [
{
test: /.ts$/,
use: 'ts-loader',
}
]
vue-loader
如果你引入了 vue 庫,並使用單文件組件的寫法,vue 官方提供了這個loader處理 .vue 的單文件組件。
更多細節參考:https://vue-loader.vuejs.org/zh/
自定義 Loader
現在,我們動手擼一個自定義的 loader。
需求是:解析/src/utils包下的工具函數,根據函數的註釋生成md文檔
首先,搭建好 webpack 的基本環境
安裝依賴
npm init
npm install webpack webpack-cli webpack-dev-server html-webpack-plugin babel-loader @babel/core @babel/preset-env -D
webpack基本配置
const path = require('path');
const HtmlWebpackPlugin = require("html-webpack-plugin");
const title = 'webpack-template'
module.exports = env => {
const isProd = env['production']
return {
mode: isProd ? 'production' : 'development',
entry: {
app: './src/index.js'
},
output: {
filename: `static/js/[name].[contenthash].js`,
clean: true
},
module: {
rules: [
{
test: /.js$/,
include: path.resolve(__dirname, 'src'),
use: ['babel-loader']
}
]
},
plugins: [
new HtmlWebpackPlugin({
title,
template: path.resolve(__dirname, 'index.html')
}),
],
devServer: {
port: 88,
open: true
}
};
};
接着,我們在根目錄下創建一個 loader 目錄,用於存放我們開發的 loader。同時創建 js 文件utils2md-loader,寫入基本的暴露函數。
module.exports = function (content) {}
我們在 webpack 配置中,將loader配置上。
我們只需要解析/src/utils包下的工具函數,因此設定 include 為 /src/utils。
同時配置 options ,用於配置些其他參數。比如這裏,我們配置了文檔的輸出路徑。
const path = require('path');
module.exports = env => {
return {
module: {
rules: [
{
test: /.js$/,
include: path.resolve(__dirname, 'src', 'utils'),// 我們只需要
use: [{
loader: './loader/utils2md-loader',
options: {
outputFile: 'utils.md',// 可選參數,指定輸出文件路徑
}
}],
},
]
}
};
};
接下來,我們要做的是解析代碼裏的註釋,得到一個對象形式的註釋。這裏具體實現細節就不多説明了
const commentRegex = //*[\s\S]*?*//g
function parseComment(content){
// 使用正則表達式提取註釋
const comments = content.match(commentRegex)
return comments.map(comment => {
const commentMap = new Map()
const lines = comment.split('\r\n').slice(1, -1)
let key = ''
for (const commentItem of lines) {
// 去除行首行尾的無效字符( * )
const line = commentItem.match(/^\s**\s(.*)/)[1];
// @字符開頭,存下key
if (line.charAt(0) === '@') {
const lineMap = line.split(' ')
key = lineMap[0].slice(1)
const value = lineMap.slice(1, lineMap.length).join(' ')
commentMap.set(key, commentMap.get(key) ? [commentMap.get(key), value].join(',') : value)
} else {
commentMap.set(key, commentMap.get(key).concat(line))
}
}
return Object.fromEntries(commentMap)
})
}
接着,我們通過 webpack 提供的 loader-utils 工具包下的 getOptions 獲取我們在配置中配置的options參數
const loaderUtils = require('loader-utils');
const defaultOutputPath = 'utils.md'
module.exports = function (content) {
const commentList = parseComment(content)
const title = path.basename(this.resourcePath)
// 獲取輸出文檔的路徑
const options = loaderUtils.getOptions(this);
const outputPath = options.outputFile || defaultOutputPath;
};
最終,我們將註釋對象輸出到目標文件裏。
function output(commentList, path, title) {
return new Promise((resolve, reject) => {
if (!commentList || !commentList.length) {
reject('comment is not defined')
}
const beginTime = Date.now()
const ws = fs.createWriteStream(path, { flags: 'a' })
ws.on('finish', () => {
console.log(`寫入完成,耗時:${Date.now() - beginTime} ms`);
});
ws.on('error', (err) => {
ws.destroy();
reject(`寫入錯誤:${err}`)
});
ws.write(`# ${title}\r\n`)
for (const [index, comment] of commentList.entries()) {
for (const key in comment) {
ws.write(`##### ${key}\r\n`)
ws.write(`${comment[key]}\r\n`)
}
if (index < commentList.length - 1) {
ws.write('---\r\n')
}
}
ws.end();
resolve()
})
}
這裏可以看到,輸出函數中採用了異步的流式寫入,因此返回的是一個Promise。而在 loader 函數中,需要採用異步 loader 的方式處理。
異步 loader 的處理方式就是,調用 loader 提供的
async()方法得到 callback 回調函數,再由callback 函數返回文件處理結果。它包含四個參數:err: Error | null
content: string | Buffer
sourceMap?: SourceMap
meta?: any
const callback = this.async()
output(commentList, outputPath, title).then(() => {
callback(null, content.replace(commentRegex, ''));
}).catch(err => {
callback(err, content.replace(commentRegex, ''));
})
最後,我們執行一下打包命令,查看生成的md文檔。到這,就算是完成了一次 loader 的開發
Github:webpack-template/loader
更多開發loader的細節可參考官方網站:https://webpack.js.org/contribute/writing-a-loader/