本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2021-05-27
一个 Loader
的职责是单一的,只需要完成一种转换。如果一个源文件需要经历多步转换才能正常使用,就通过多个Loader
去转换。在调用多个Loader
去转换一个文件时,每个Loader
都会链式地顺序执行。第1个Loader
将会拿到需处理的内容,上一个Loader
处理后的结果会被传给下一个Loader
接着处理,最后的Loader
将处理后的最终结果返回给 Webpack
。所以,在开发一个 Loader
时,请保持其职责的单一性,我们只需关心输入和输出。
Webpack
是运行在 Node.js
上的,一个 Loader
其实就是一个 Node.js
模块,这个模块需要导出一个函数。这个导出的函数的工作就是获得处理前的原内容,对原内容执行处理后,返回处理后的内容。
// 最基本的结构
// Loader运行在Node.js中,所以可以调用任何Node.js的API
const sass = require('sass');
module.exports = function (source) {
// source为compiler传递给Loader的一个文件的原内容
return sass(source)
}
// 借助loader-utils
const loaderUtils = require('loader-utils');
module.exports = function (source) {
// 获取用户为当前Loader 传入的options
const options = loaderUtils.getOptions(this);
return source
}
上面的 Loader
都只是返回了原内容转换后的内容,但在某些场景下还需要返回除了内容之外的东西。
以用babel-loader
转换ES6 代码为例,它还需要输出转换后的ES5
代码对应的 SourceMap
,以方便调试源码。为了将 Source Map
也一起随着 ES5
代码返回给 Webpack
,还可以这样写:
module.exports = function (source) {
// 通过this.callback告诉webpack 返回的结果
this.callback(null, source, sourceMaps);
// 当使用this.callback返回内容时,该Loader必须返回undefined
// 以让Webpack知道该Loader返回的结果在this.callback中,而不是return中
return;
// 其中this.callback 是webpack向Loader中注入的API,以方便Loader和webpack之间的通信。
}
this.callback
的详细使用方法,即可以接受哪些内容从Loader
中传到webpack
。
this.callback(
// 当无法转换原内容时,为Webpack 返回一个Error
err: Error | null,
// 原内容转换后的内容
content: string | Buffer,
// 可选,用于通过转换后的内容得出原内容的Source Map ,以方便调试
// webpack 会通过 this.sourceMap API 告诉Loader用户是否配置需要sourceMap
sourceMap?: SourceMap,
// 如果本次转换为原内容生成了AST 语法树,则可以将这个AST 返回,
// 以方便之后需要AST 的Loader 复用该AST ,避免重复生成AST ,提升性能
abstractSyntaxTree?: AST
)
Loader
有同步和异步之分,上面介绍的都是同步的 Loader
,因为它们的转换流程都是同步的,转换完成后再返回结果。但在某些场景下转换的步骤只能是异步完成的,例如我们需要通过网络请求才能得出结果,如果采用同步的方式,则网络请求会阻塞整个构建,导致构建非常缓慢。
如果是异步转换,则我们可以这样做:
module.exports = function(source) {
// 告诉Webpack 本次转换是异步的, Loader 会在callback 中回调结果
var callback = this.async();
// 异步操作函数
function someAsyncOperation(source, cb) {
var err, result, sourceMaps, ast;
// 模拟异步
new Promise((res, rej) => {
resolve();
}).then(res => {
// ...异步操作成功后 将正确处理过需要返回的值返回
cb(err, result, sourceMaps, ast)
})
}
someAsyncOperation(source, function(err, result, sourceMaps, ast){
// 通过callback 返回异步执行后的结果
callback(err, result, sourceMaps, ast)
// this.async()(err, result, sourceMaps, ast)
})
}
在默认情况下, Webpack
传给 Loader
的原内容都是UTF-8
格式编码的字符串。但在某些场景下 Loader
不会处理文本文件,而会处理二进制文件如fil e-loader
,这时就需要 Webpack
为 Loader
传入二进制格式的数据。为此,我们需要这样编写Loader
:
module.exports = function(source) {
console.log(source instanceof Buffer);
// true , webpack 传进来的source会是二进制数据
// 但是不管module.exports.raw 是不是 true, Loader都可以返回二进制数据
return source
}
// 通过exports.raw 属性告诉 Webpack 该 Loader 是否需要二进制数据
// 如果没有这一行 则Loader只能拿到字符串
module.exports.raw = true;
在某些情况下,有些转换操作需要大量的计算,非常耗时,如果每次构建都重新执行重复的转换操作,则构建将会变得非常缓慢。为此,Webpack
会默认缓存所有Loader
的处理结果,也就是说在需要被处理的文件或者其依赖的文件没有发生变化时,是不会重新调用对应的Loader
去执行转换操作的。
// 如果我们不想让Webpack 不缓存该Loader 的处理结果,则可以这样:
module.exports = function(source) {
// 关闭该Loader 的缓存功能
this.cacheable(false);
return source
}
在Loader
中还可以使用以下API
:
this.context
:当前处理的文件所在的目录,假如当前 Loader
处理的文件是/src/main.js
,则this.context
等于/src
。
this.resource
:当前处理的文件的完整请求路径,包括querystring
,例如/src/main.js?name=l
。
this.resourcePath
:当前处理的文件的路径,例如/src/main.js
。
this.resourceQuery
:当前处理的文件的querystring
。
this.target
:等于 Webpack
配置中的Target。
this.loadModule
: 当 Loader
在处理一个文件时,如果依赖其他文件的处理结果才能得出当前文件的结果,就可以通过this.loadModule(request:string,callback:function(err,source,sourceMap,module))
去获取request
对应的文件的处理结果。
this.resolve
:像require
语句一样获得指定文件的完整路径,使用方法为resolve(context:string, request:string, callback:function(err,result:string))
。
this.addDependency(file:string)
: 为当前处理的文件添加其依赖的文件,以便其依赖的文件发生发生变化时,重新调用 Loader
处理该文件
this.addContextDependency(file: string)
: 同上,是将整个目录加入当前正在处理的文件的依赖中,以便其依赖的文件发生发生变化时,重新调用Loader 处理该目录下的文件。
this.clearDependencies
:清除当前正在处理的文件的所有依赖
this.emitFile
:输出一个文件,使用方法为emitFile(name : string ,content: Buffer | string , sourceMap: { ... })
。
如果向采用第三方Loader
的方式去使用本地开发的 Loader
,将会很麻烦,因为我们需要确保编写的 Loader
的源码在 node_modules
目录下。为此需要先将编写的Loader
发布到 Npm
仓库, 再安装到本地项目中使用。
有两种方法,可以解决上述问题,不需要加本地 Loader
发布到 Npm
中。
Npm link
Npm link
专门用于开发和调试本地的 Npm
模块,能做到在不发布模块的情况下,将本地的一个正在开发的模块的源码链接到项目的node_modules
目录下,让项目可以直接使用本地的 Npm
模块。由于是通过软链接的方式实现的,编辑了本地的 Npm
模块的代码,所以在项目中也能使用到编辑后的代码。
Npm
模块(也就是正在开发的 Loader
) 的package.json
已经正确配置好。npm link
,将本地模块注册到全局。npm link loader-name
,将第2步注册到全局的本地Npm
模块链接到项目的node_moduels
下,其中的loader-name
是指在第1步的package.json
文件中配置的模块名称。Resolveloader
配置resolveLoader
,配置如何寻找Loader
,默认会去node_modules
中寻找。
module.exports = {
resolveLoader: {
// 去哪些目录下寻找Loader 有先后之分,先去node_modules中找,找不到再去./loader/目录中寻找。
modules: ['node_modules', './loaders/']
}
}
编写一个转换md文件的loader。
有一个 demo.md
文件
## 我是二级标题
### 我是三级标题
我是内容
我们在 main.js
中这样引入
// main.js
import demo from './demo.md';
console.log(demo);
由于webpack除了js以外的文件,都需要提供 loader 去处理,方便扩展。社区内也有处理 md 文件的loader,我们这里自己来写一个 非常简单的loader,以了解其流程。
// markdown-loader.js
const marked = require('marked'); // 可以将marked转换为html
module.exports = (source) => {
console.log('source', source);
// return source;
// error 接受到的source是md文件的内容,loader必须要返回一个合格js代码字符串,可以理解为能够被eval执行
const html = marked(source)
// return html;
// <h2 id="我是二级标题">我是二级标题</h2>
// <h3 id="我是三级标题">我是三级标题</h3>
// <p>我是内容</p>
// 也会报错,因为只能接受js代码字符串
// 所以需要将 html 字符串转换为 js代码字符安
// 第一种 自己拼接
// const result = `module.exports = ${JSON.stringify(html)}`;
// return result
// 第二种 使用 html-loader,在webpack配置里面,在针对.md文件的配置中
// 在自己写的 markdown-loader 之前插入 html-loader
// 那么 我们这个 markdown-loader处理之后返回的 html字符串,就会交给 html-loader去处理
// 这个loader会将html处理成js
return html;
}
需要注意的是,一个文件可以被多个loader去处理,在loader列表中从右往左进行处理,所以第一个loader(最后处理的loader)应该返回一个js代码字符串(webpack只接收这种)
关于 webpack 的配置,就要将我们自己写的 loader 配上去。
// webpack.config.js
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
},
module: {
rules: [
{
test: /\.md$/,
use: ['html-loader', './src/markdown-loader']
// 如果不使用 html-loader可以删掉,选择自己拼接一个
}
]
}
}
最后我们运行webpack
,就能从入口main.js
进入,开始解析文件,分析依赖树,碰到了引入的 .md 文件,开始使用我们设置匹配的 markdown-loader 去处理, 这个loader返回一个 html 字符串,再交给 html-loader 处理返回一个 js代码字符串,最终交给 webpack 将其打包进 ./dist/bound.js
中, dist
是默认的输出文件目录,也可以通过 output.path
去修改这个输出文件目录。