前言
webpack
作为目前主流的前端构建工具,我们几乎每天都需要与它打交道。个人认为一个好的开源产品壮大的原因应该包括核心开发者的稳定输出以及对应生态的繁荣。对于生态来说, webpack
是一个足够开放的类库,提供了可插拔的方式去自定义一些配置,包括配置 loader
和 plugin
,本篇我们主要讨论loader。
loader
本质上是一个函数,webpack在打包过程中会按照规则顺序调用处理某种文件的 loader
,然后将上一个 loader
产生的结果或者资源文件传入进去,当前 loader
处理完成后再交给下一个 loader
。
loader的类型
开始之前,还是要先大概提一下 loader
的类型以及一些常用的 api
,不感兴趣的同学可以直接跳过这一小节,更详细的指引请参阅官方文档。
loader
主要有以下几种类型:
- 同步
loader
:return
或调用this.callback
都是同步返回值 - 异步
loader
:是用this.async()
获取异步函数,是用this.callback()
返回值 raw loader
:默认情况下接受utf-8
类型的字符串作为入参,若标记raw
属性为true
,则入参的类型为二进制数据pitch loader
:loader
总是从右到左被调用。有些情况下,loader
只关心 request 后面的 元数据(metadata
),并且忽略前一个loader
的结果。在实际(从右到左)执行loader
之前,会先从左到右调用loader
上的pitch
方法。
开发 loader
时常用的 API
如下:
this.async
:获取一个callback
函数,处理异步this.callback
:同步loader
中,返回的方法this.emitFile
:产生一个文件this.getOptions
:根据传入的schema
获取对应参数this.importModule
:用于子编译器在构建时编译和执行请求this.resourcePath
:当前资源文件的路径
Hello Loader
现在假设我们有这么一个需求:在每个文件的头部打上开发者的相关信息。比如打包之前的文件内容是这样的:
const name = 'jay'
打包之后文件内容可能是这样的:
/** * @Author:jay * @Email:email@qq.com * /const name = 'jay'
那废话不多说,直接开整。首先把相关依赖安装一下:
//package.json
"webpack": "^5.0.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.10.0"
再简单的配置一下webpack:
const HtmlWebpackPlugin = require('html-webpack-plugin')
const {
resolve } = require('path')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
path: resolve(__dirname, './dist'),
filename: 'js/[name].js',
clean: true
},
module: {
rules: [
{
test: /\.js$/,
loader: './loaders/hello-loader',
},
]
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html'
})
]
}
参考webpack视频讲解:进入学习
接下来就可以开始实现这个 loader
了,首先每一个 loader
都是一个函数,这个函数的返回结果要么是二进制数据要么是字符串,字符串就是文件的具体内容,二进制数据就是资源文件比如图片的内容。在上面这个需求中,显然我们只需要拿到文件的内容,做一些修改替换即可。所以可以比较容易的写出下面的代码:
module.exports = function (content) {
const newContent = ` /** * @Author:jay * @Email:email@qq.com * */ ${
content}
`
return newContent;
}
这样就会将对应的内容添加到打包结果中,如下图所示:
获取参数
上面对于文件的一些描述我们是已经写死了,但这样不太灵活,大多数时候是希望能通过 loader
对应的配置去获取对应的参数,我们可以在引入 loader
的时候这样改造一下:
{
test: /\.js$/,
loader: './loaders/hello-loader',
options: {
author: 'hello loader',
email: 'helloloader@qq.com'
}
}
那么在具体的loader中,可以使用 this.getOptions(schema)
去获取传入的配置。这个 schema
是对 option
的格式校验,代码改造如下:
const schema = {
type: 'object', //options是一个对象
properties: {
//author是一个字符串
author: {
type: 'string'
},
//email是一个字符串
email: {
type: 'string'
}
}
}
const options = this.getOptions(schema) || {
}
const {
author = 'null', email = 'null' } = options
const newContent = ` /** * @Author:"${
author}" * @Email:"${
email}" * */ ${
content}
`
这样就可以将用户自定义的参数传给处理的 loader
,对于一些需要提供可拓展能力的 loader
来说,获取参数这一步是必不可少的。webpack配置如下,即可使用loader获取参数的能力。
异步回调
这时候有了一个新需求,希望我们把当前处理的文件内容信息与文件名通过网络传输,以便后续做一些分析。那为了方便我们还是在上面的 loader
进行拓展,实际开发中最好不要这样做,要保证 loader
的单一职责。这时候就不能直接返回结果,而是要获取一个异步 callback
函数,使用这个函数把结果输出。代码实现如下:
const callback = this.async()
// 模拟网络请求
setTimeout(() => {
callback(null, JSON.stringify(newContent), null, {
})
console.log('net done');
}, 1000)
这里留意一下 callback
函数的用法:callback(error,content,map,meta)
,其中有四个参数,分别的作用是:
error
:错误信息,如果存在的话则会抛出异常,构建终止content
:处理后的内容map
:sourceMap
相关信息meta
:要传给下一个loader
的额外信息参数
JS处理
上面大致举例说明了同步l loader
、异步 loader
以及如何获取 loader
的参数。在这一小结,主要实现开发过程中经常用到的三个 JS
处理相关的 loader
:
eslint-loader
:使用eslint
做代码检测babel-loader
:将ES6+
语法转换为ES5
语法uglify-loader
:对代码进行压缩混淆
eslint-loader
首先先来实现 eslint-loader
,实现思路是对当前处理的文件调用 eslint
去扫描,如果无错误则继续正常调用下一个 loader
去处理。具体代码实现如下:
const childProcess =