前言
我们知道 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/
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。