给我们的mini-webpack简单加个loader吧
那么添加 loader 功能的实现思路就很简单了:就是在我们加载文件之后,先别急着解析,看有没有匹配的 loader,如果有就调一下 loader 去解析,否则再去执行我们原来的默认的 js 文件解析逻辑。
为什么需要 loader
我们知道其实 webpack 最基础的功能就是打包,而这个功能其实就是将多个资源文件整合成一个文件(一个捆 bundle 资源)。那么要实现从多个到一个必须要解决两个核心的问题:
- 确定依赖关系
- 解决合并冲突
对于 js 文件,这两个问题其实还好解决。我们可以将 js 文件解析成 ast,然后通过 ast 分析依赖关系,通过函数包裹的方式解决合并冲突。那对于其他类型的文件呢?
比如 txt,其实这个类型特别好处理:只需要全部读出来,然后加个换行符拼起来即可。再比如 css 文件,那我们可能就需要去分析 @import
了,至于合并冲突:由于 css 文件没有模块化,所以可以直接合并。
总的来说:不同的文件类型意味着文件信息的组织方式不一样,这也意味着我们需要对不同类型的文件进行不同的处理。那么文件类型能不能穷举呢?其实理论上也可以,但是比较困难。而与穷举相对的一种解决方案,那就是暴露接口(由系统外部去实现,再将结果通过接口回传给系统)。那么 webpack 就是通过这种方式来解决不同文件类型的处理问题,也就是 loader。
loader 编写
接下来我们就根据 loader 的特点和功能,一步一步地编写我们的 loader。
修改 Compiler,支持 loader 解析文件
根据前面的思路,我们先调整一下 Compiler 的结构:分析文件依赖之前,先看看有没有匹配的 loader,有的话就交给 loader 去处理。代码如下:
class Compiler {
// 其他代码省略
async build(relativePath) {
const { parser, loader } = this
// 1、加载(fs读取文件)
parser.load(relativePath)
let code = ''
let deps = new Set()
// 交给loader处理
const isMatch = loader.match(parser.absolutePath)
if (isMatch) {
// 使用loader处理
code = await loader.process(parser.file)
} else {
// 默认处理
// 2、解析(将文件解析成ast)
parser.parser()
// 3、解析并转化导入导出语句(import、require变成__webpack_require__) => deps、code
const { deps: _deps, code: _code } = parser.transform()
deps = _deps
code = _code
}
// 加入缓存
const module = { deps, code }
this.modulesCacheMap[relativePath] = code
return module
}
}
值得注意的是:由于我们的 loader 有同步的和异步的,所以我们需要将整个打包链路修改成异步链路。
loader 配置方式
首先我们先关注的是 loader 的配置方式,我们知道如果是单个 loader 就配置一个对象,如果多个就配置 use 数组。
// 单个 loader
module.exports = {
module: {
rules: [
{
test: /\.js$/,
loader: 'loader1',
options: { a: 1 }
},
],
},
}
// use数组
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: ['loader1']
},
],
},
}
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: [{ loader: 'loader1', options: { a: 1 } }]
},
],
},
}
为了方便用户使用,我们系统会提供多种使用方式给用户使用;为了方便程序使用,我们需要将多种方式转化成一种完整的数据格式。也就是需要对配置数据进行数据格式整理,转化成统一的、方便程序处理的格式。代码如下:
// 参数归一化(将用户方便转化成程序方便)
// 简化配置:方便用户使用
// 复杂配置:方便程序处理
const parserRules = (rule) => {
if (rule.loader) {
delete rule.use // 在 webpack 中 loader 和 use 这两个字段同时存在会报错,我们这里做一个优先级处理
return { ...rule, use: [rule] }
}
if (isArray(rule.use)) {
return {
...rule,
use: rule.use.map((item) => (isString(item) ? { loader: item } : item)),
}
}
return rule
}
loader 执行顺序
根据 loader 的执行顺序,我们可以将 loader 划分成四类:pre、normal、inline、post。且执行顺序的优先级为:post
> normal
> pre
(inline loader 我们先不考虑)。值得注意的是:优先级越高,pitch 越先执行,loader 越后执行。
同种类 loader 的执行顺序为:逆序执行,无论是顶层配置(多个匹配条件),还是内层配置(同一个匹配条件下的多个 loader)。
pre
和 post
是通过 enforce
字段进行配置,inline
是通过路径配置 loader(不推荐使用),例如:const data = require('raw-loader?raw=true!./data.txt');
。
所以我们key对 loader 根据配置提前排序,代码如下:
const sortRules = (rules = []) => {
const postArr = []
const preArr = []
const normalArr = []
rules.forEach((rule) => {
if (rule.enforce === 'post') {
postArr.push(rule)
} else if (rule.enforce === 'pre') {
preArr.push(rule)
} else {
normalArr.push(rule)
}
})
return [...postArr, ...normalArr, ...preArr]
}
熔断机制
根据前面的 loader 执行顺序以及接下来的熔断机制,我们就可以来编写我们的 loader 执行链路了。loader 的执行链路是这样的:先顺序执行所有 loader 的 pitch 方法,再逆序执行 loader。
当 pitch 方法存在返回结果时,中断 pitch 遍历,直接返回到上一个 loader 执行逆序遍历。如下图:
值得一提的是:pitch 用于控制熔断,所以 pitch 接收参数是需要处理的文件以及应用的 loader(控制信息);而 loader 用于流水线式解析处理文件,所以 loader 接收的是上一个 loader 处理的结果(结果信息)。
根据 loader 执行顺序与熔断机制,编写代码如下:
process(code) {
const { matchRules } = this
const rules = matchRules
.map((rule) => rule.use)
.flat()
.map((v) => ({ ...v, loader: require(v.loader) }))
// 先顺序调用 pitch
let startIndex = rules.length
for (let i = 0; i < rules.length; i++) {
const { loader, options } = rules[i]
if (!isFunction(loader.pitch)) {
continue
}
const deps = []
const result = loader.pitch(...deps, options)
if (result) {
startIndex = i
code = result
break
}
}
// 再逆序执行loader
for (let i = startIndex - 1; i >= 0; i--) {
const { loader, options } = rules[i]
code = loader(code, options)
}
return code
}
同步 loader 与 异步 loader
对于同步 loader,有两种方式返回处理结果:return
和 this.callback
,如下:
module.exports = function (source) {
return 'loader'
}
module.exports = function (source) {
this.callback(null, 'loader')
}
我们知道对于 js 语言来说,其实异步结果的返回就两种解决方案:回调 和 promise。所以 webpack 也采用这两种方式来获取异步 loader 的处理结果。
- 回调方式:
module.exports = function (source) {
const callback = this.async()
setTimeout(() => {
callback(null, 'loader')
}, 1000)
}
- promise 方式:
module.exports = function (source) {
return new Promise((r) => {
setTimeout(() => {
r('loader')
}, 1000)
})
}
我们为了简单处理,可以全部统一成异步的方式,代码如下:
class Loader {
// 其他代码省略
await() {
return new Promise((resolve, reject) => {
this.promiseResolve = resolve
this.promiseReject = reject
})
}
callback(err, ...args) {
if (err) {
this.promiseReject(err)
}
this.promiseResolve(...args)
}
async() {
return this.callback
}
async process(code) {
const { matchRules } = this
const rules = matchRules
.map((rule) => rule.use)
.flat()
.map((v) => ({ ...v, loader: require(v.loader) }))
// 先顺序调用 pitch
let startIndex = rules.length
for (let i = 0; i < rules.length; i++) {
const { loader, options } = rules[i]
if (!isFunction(loader.pitch)) {
continue
}
const deps = []
this.query = options
const result = await Promise.race([
this.await(),
loader.pitch.call(this, ...deps),
])
if (result) {
startIndex = i
code = result
break
}
}
// 再逆序执行loader
for (let i = startIndex - 1; i >= 0; i--) {
const { loader, options } = rules[i]
this.query = options
code = await Promise.race([this.await(), loader.call(this, code)])
}
return code
}
}
raw 配置
我们知道,如果 loader 配置了 raw 属性为 true 的话,webpack 会将 buffer 数据交给我们的 loader。所以我们需要在调用loader之前判断一下这个字段,代码如下:
for (let i = startIndex - 1; i >= 0; i--) {
const { loader, options } = rules[i]
if (loader.raw) {
res = Buffer.from(res)
} else {
res = res.toString('utf8')
}
res = await Promise.race([this.await(), loader.call(this, res, options)])
}
return res
附录
const { isString, isArray, isObject, isFunction } = require('./utils')
// 参数归一化(将用户方便转化成程序方便)
// 简化配置:方便用户使用
// 复杂配置:方便程序处理
const parserRules = (rule) => {
if (rule.loader) {
delete rule.use // 在 webpack 中 loader 和 use 这两个字段同时存在会报错,我们这里做一个优先级处理
return { ...rule, use: [rule] }
}
if (isArray(rule.use)) {
return {
...rule,
use: rule.use.map((item) => (isString(item) ? { loader: item } : item)),
}
}
return rule
}
const sortRules = (rules = []) => {
const postArr = []
const preArr = []
const normalArr = []
rules.forEach((rule) => {
if (rule.enforce === 'post') {
postArr.push(rule)
} else if (rule.enforce === 'pre') {
preArr.push(rule)
} else {
normalArr.push(rule)
}
})
return [...postArr, ...normalArr, ...preArr]
}
class Loader {
constructor(rules = []) {
this.rules = sortRules(rules.map(parserRules))
this.matchRules = []
this.promiseResolve = null
this.promiseReject = null
}
match(filePath) {
this.matchRules = this.rules.filter((rule) => rule.test.test(filePath))
return this.matchRules.length > 0
}
await() {
return new Promise((resolve, reject) => {
this.promiseResolve = resolve
this.promiseReject = reject
})
}
callback(err, ...args) {
if (err) {
this.promiseReject(err)
}
this.promiseResolve(...args)
}
async() {
return this.callback
}
async process(buffer) {
const { matchRules } = this
const rules = matchRules
.map((rule) => rule.use)
.flat()
.map((v) => ({ ...v, loader: require(v.loader) }))
let res = buffer
// 先顺序调用 pitch
let startIndex = rules.length
for (let i = 0; i < rules.length; i++) {
const { loader, options } = rules[i]
if (!isFunction(loader.pitch)) {
continue
}
const deps = []
const result = await Promise.race([
this.await(),
loader.pitch.call(this, ...deps, options),
])
if (result) {
startIndex = i
res = result
break
}
}
// 再逆序执行loader
for (let i = startIndex - 1; i >= 0; i--) {
const { loader, options } = rules[i]
if (loader.raw) {
res = Buffer.from(res)
} else {
res = res.toString('utf8')
}
res = await Promise.race([this.await(), loader.call(this, res, options)])
}
return res
}
}
module.exports = Loader
转载自:https://juejin.cn/post/7352075810935209993