前言
对于 loader 大家都不陌生,比较常见的 loader 有 css-loader、style-loader、vue-loader、babel-loader等,这篇文章将带你学习如何实现一个自定义 loader。项目地址在文末,期待大家的一键三连~。
一、准备工作
在实现 loader 前,我们先简单的搭建一个 webpack 运行环境。关于如何搭建 webpack 项目可以参考:手把手教你搭建 Webpack 5 + React 项目。
1.1 初始化项目
新建一个文件夹 create-loader,在该目录执行以下命令来完成初始化工作。
js
pnpm init
关于 pnpm 的更多知识参考:包管理工具 ------ 更推荐的 pnpm。
1.2 安装依赖
安装 webpack 、webpack-cli 以及 webpack-dev-server。前两者打包必备,后者是一个提供热更新的开发服务器,对开发阶段友好。
js
pnpm add webpack webpack-cli webpack-dev-server --save-dev
1.3 创建入口文件
创建 src 目录,并创建 index.js 入口文件。
js
// index.js
document.write('hello wp')
1.4 创建 Webpack 配置文件 webpack.config.js
虽然 Webpack v4 + 开箱即用,可以无需配置文件就可以打包, Webpack 会默认打包入口文件为 src/index.js
文件,打包产物为 dist/main.js
,并且开启生产环境的压缩和优化。但大多数的项目还是需要一些复杂的配置,配置文件 webpack.config.js 文件还是很有必要的 。 Webpack 在打包时会自动识别这个文件,根据里面的配置来进行打包。
js
const path = require('path')
module.exports = {
mode: 'development', // 以什么模式进行打包
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'), // 打包后的代码放在dist目录下
filename: 'bundle.js', // 文件名为 bundle.js
},
devServer: {
static: './dist'
}
}
💡 如果我们想更改为指定的配置文件 prod.config.js
来打包,可以使用--config
标志来修改。
js
"scripts": {
"build": "webpack --config prod.config.js"
}
1.5 添加 npm script
我们可以在 package.json
文件中创建快捷方式来启动开发和打包。
js
{
// ...省略
"main": "src/index.js", // 修改入口文件
"scripts": {
"dev": "webpack serve --open",
"build": "webpack"
},
// ...省略
}
使用 pnpm run build
可以实现打包:
我们可以看到目录里生成了一个 dist
目录,里面有一个 bundle.js
文件。
1.6 创建 index.html 文件
上面打包的产物只有一个js 文件,我们想要使用浏览器访问里面的内容就要手动创建一个 html 文件,并引入 bundle.js
脚本文件,使用浏览器打开即可。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="./bundle.js"></script>
</body>
</html>
这样可以通过 pnpm run dev
来启动项目,浏览器里可以看到下面的内容:
以上的步骤简单的完成了一个基于 webpack 打包的最简单例子。
二、Loader 的简单介绍
2.1 loader 是什么?
loader 是一个导出为函数的 JavaScript 模块,用于对模块的源码进行转换。形如:
js
moudle.exports = (source) => {
// 按照自己的转换要求进行处理
return source
}
2.2 为什么需要 loader?
webpack 本身只能识别 js 和 json 文件,对于 css、ts 等其他文件就需要 loader 对齐进行处理,转换为 webpack 能识别的模板。
2.3 如何定义 loader?
官方文档上有很多种 loader,但个人认为可总结为两种 loader,一种是normal loader,另一种是pitch loader。
- normal loader
js
// normal loader
module.exports = function normalLoader() {}
- pitch loader :新增一种
pitch loader
的概念,类似于dom
事件模型中的捕获,先执行pitch loader
,然后在执行normal loader
,且如果picth loader
有返回非undefined
的值,则直接中断后续loader
的执行
js
// pitch loader
module.exports.pitch = function pitchLoader() {}
关于 loader 和 pitch loader 后面会单独出一篇文章来进行说明。
2.4 loader 原则
- 单一: 一个loader 只做一件事。
- 链式调用:从右往左依次调用。
- 模块化:保证输出的是模块化。
- 无状态: 每一个 loader 的运行相对独立,不与其他 loader
- loader-utils 工具库: 提供了很多工具
三、如何实现一个同步 loader
我们先来编写一个简单的字符串替换 loader,目的是将 wp 被替换为 xiaoqi 。
3.1 创建 loader
在 src 下创建 xq-loader 目录,创建 my-loader.js
文件。
js
// my-loader.js
module.exports = function (source) {
// 在这里按照你的需求处理 source
// 可以通过 this.getOptions 或者 this.query 来获取参数
const options = this.getOptions()
console.log(options)
return source.replace('wp', 'xiaoqi')
}
// 或者 使用this.callback
module.exports = function (source) {
// 在这里按照你的需求处理 source
// 可以通过 this.getOptions 或者 this.query 来获取参数
const options = this.getOptions()
const result = source.replace('wp', 'xiaoqi')
this.callback(null, result)
}
这里编写的是一个简单的同步loader,我们可以通过 return 一个表示已转换模块的单一值。如果情况比较复杂,也可以通过 this.callback(err,values...)
这个回调函数来返回转换后的结果。
对于同步 loader 而言,使用return
或者this.callback
均可以达到想要的效果。只是说,相对于return
,this.callback
可以返回更多的信息。
js
this.callback(
// 转换异常时,抛出错误
err: Error || null,
// 转换后的结果
content: string | Buffer,
// 用于把转换后的内容得出原内容的 Source Map,方便调试
sourceMap?: SourceMap,
)
3.2 使用 loader
在 webpack.config.js 中配置 loader 有两种方式。一种是 path.resolve
,另一种是 ResolveLoader
。
- 使用 path.resolve 指定 loader 的文件路径
js
// webpack.config.js
{
// ...省略
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: path.resolve(__dirname, './src/xq-loader/my-loader.js'),
options: {
name: 'xiaoqi',
},
},
],
},
],
}
}
- 使用 ResolveLoader
js
{
module: {
rules: [
{
test: /\.js$/,
use: ['my-loader'],
},
],
},
resolveLoader: {
// webpack 将会从这些目录中依次搜索 loader,
modules: ['node_modules', './src/xq-loader'],
},
}
运行 npm run dev 可以看到 wp 被替换为 xiaoqi 。
四、如何实现一个异步 loader
在某些耗时久的场景下,比如处理网络请求的结果,我们可以使用异步 loader ,这样不会阻塞整个构建。
我们实现的这个异步 loader 的功能为读取 async.txt
中的内容并返回。 在 src 目录下创建 async.txt
文件,随便写点内容。
4.1 创建 loader
在 src 下创建 xq-loader 目录,创建 my-async-loader.js
文件。
对于异步loader而言,需要通过this.async()
,来获取到callback
函数。
js
// my-async-loader.js
const fs = require('fs')
const path = require('path')
module.exports = function (source) {
// 通过 this.async 来返回一个异步函数,第一个参数 Error, 第二个参数是处理的结果。
const callback = this.async()
fs.readFile(path.join(__dirname, '../async.txt'), 'utf-8', (err, data) => {
const html = `module.exports = ${JSON.stringify(data)}`
callback(null, html)
})
}
4.2 添加 loader 配置
在 webpack.config.js 文件中的 module.rules
下添加 my-async-loader
。
js
// webpack.config.js
// ...
{
test: /\.txt$/,
use: {
loader: 'my-async-loader',
},
},
// ...
4.3 引入 txt 文件
在 index.js 入口文件中引入。
js
// index.js
import txt from './async.txt'
document.write('hello wp')
document.write(`</br>异步loader: ${txt}`)
运行 || 打包
pnpm run dev
可以看到 async.txt 文件下的内容被打印出来。
五、实现一个渲染 markdown 的 loader
简易版 mark-loader,借助 markdown-it
库的能力。
首先我们在 src 下创建一个 md 文件。
5.1 创建 loader
在 src 下创建 xq-loader 目录,创建 mark-loader.js
文件。
js
// mark-loader.js
const MarkdownIt = require('markdown-it')
module.exports = function (source) {
const options = this.getOptions()
const md = new MarkdownIt({
html: true,
...options,
})
let html = md.render(source)
// webpack 无法直接去解析html模板,所以要返回一段 js 代码
html = `module.exports = ${JSON.stringify(html)}`
this.callback(null, html)
}
5.2 添加配置 loader
js
// webpack.config.js
{
test: /\.md$/,
use: [
{
loader: 'mark-loader',
},
],
},
5.3 引入 md 文件
创建一个 div ,将生成的 html 插入其中。
js
import txt from './async.txt'
import md from './mk.md'
document.write('hello wp')
document.write(`</br>异步loader: ${txt}`)
const div = document.createElement('div')
div.innerHTML = `${md}`
document.body.appendChild(div)
运行 || 打包
pnpm run dev
运行,可以看到 md 文件以html 的形式渲染出来。
六、实现一个生成雪碧图的 loader
简易版 sprite-loader
,借助 spritesmith
库的能力。
6.1 创建 css 文件
首先我们在 src 下创建一个 css 文件,以 ?__sprite
来标志是需要合成的图片。
6.2 创建 sprite-loader
找到需要合成的图片,组合生成一个数组,再使用 spritesmith
的能力将多张图合并成一张图。
js
const path = require('path')
const fs = require('fs')
const Spritesmith = require('spritesmith')
module.exports = function (source) {
const callback = this.async()
// 匹配 url(开头 ?__sprite 结尾的
const imgs = source.match(/url\((\S*)\?__sprite/g)
const matchImgs = []
for (let i = 0; i < imgs.length; i++) {
// 解析出图片路径
const img = imgs[i].match(/url\((\S*)\?__sprite/)
matchImgs.push(path.join('./src', img[1]))
}
Spritesmith.run({ src: matchImgs }, (err, result) => {
// 将合成的图片写入到 src/images/sprite.jpg 中
fs.writeFileSync(path.join(process.cwd(), 'src/images/sprite.jpg'), result.image)
// 替换原引入为合成图片的路径
source = source.replace(/url\((\S*)\?__sprite/g, () => {
return `url('./images/sprite.jpg'`
})
callback(null, source)
})
}
6.3 添加 loader 配置
由于 webpack 没法识别 css 文件,这里先用 sprite-loader 合成图片;再调用 css-loader,它会帮我们对 @import
和 url()
进行处理,就像 js 解析 import/require()
一样;最后调用 style-loader 帮我们将 css 代码以 <style>
的形式插入到 DOM 中。
js
// webpack.config.js
{
test: /\.css$/,
use: ['style-loader', 'css-loader', 'sprite-loader'],
},
6.4 引入 css 文件
在 index.js 中引入 css 文件,给上述的 div 加上类名 img2。
js
import './index.css'
import txt from './async.txt'
import md from './mk.md'
// ...
const div = document.createElement('div')
div.className = 'img2'
div.innerHTML = `${md}`
document.body.appendChild(div)
6.5 运行 || 打包
可以看到背景图生效了,并且在 dist 下会生成一个图片文件。
七、小结
整篇文章介绍了几个自定义 loader 的实例,想必大家对如何实现 loader 已经有了一个整体的认知,感兴趣的小伙伴可以自己手动实现一下,会有更大的收获。欢迎大家评论交流,相互学习~
项目地址:手把手教你实现 Loader