Loader 是 Webpack 的核心概念之一,它的基本工作流是将一个文件以字符串的形式读入,对其进行语法分析及转换,然后交由下一环节进行处理,所有载入的模块最终都会经过 moduleFactory
处理,转成 JavaScript 可以识别和运行的代码,从而完成模块的集成。
本质上 loader
只是一个导出为函数的 JavaScript 模块
// 导出一个函数,source 为 webpack 传递给 loader 的文件源内容module.exports = function (source) {const content = doSomeThing2JsString(source);// 如果 loader 配置了 options 对象,那么this.query将指向 optionsconst options = this.query;// 可以用作解析其他模块路径的上下文console.log('this.context');/** this.callback 参数:* error:Error | null,当 loader 出错时向外抛出一个 error* content:String | Buffer,经过 loader 编译后需要导出的内容* sourceMap:为方便调试生成的编译后内容的 source map* ast:本次编译生成的 AST 静态语法树,之后执行的 loader 可以直接使用这个 AST,进而省去重复生成 AST 的过程*/this.callback(null, content); // 异步return content; // 同步};
多 Loader 时的执行顺序
module.exports = {module: {rules: [{test: /\.less$/,use: ['style-loader', 'css-loader', 'less-loader'],},],},};
函数组合的两种情况:
compose = (f, g) => (...args) => f(g(...args));
由于 Webpack 是运行在 Node.js 之上的,一个 Loader 其实就是一个 Node.js 模块,这个模块需要导出一个函数。这个导出的函数的工作就是获得处理前的元内容,对元内容的执行处理后,返回处理后的内容。
module.exports = function (source) {// source 为 compiler 传递给 Loader 的一个文件的原内容// 该函数需要返回处理后的内容给你,这里简单起见,直接把原内容返回了,相当于该 Loader 没有做任何转换return source;};
由于 Loader 运行在 Node.js 中,可以调用任何 Node.js 中自带的 API,或者安装第三方模块进行调用:
const sass = require('node-sass');module.exports = function (source) {return sass(source);};
定义:loader-runner
允许你不安装 Webpack 的情况下运行 loaders
作用:
在 Loader 中获取用户传入的 options
,通过 loader-utils
的 getOptions
方法获取:
const loaderUtils = require('loader-utils');module.exports = function (source) {// 获取到用户句给当前 Loader 传入的 optionsconst options = loaderUtils.getOptions(this);return source;};
上面的 Loader 都只是返回了原内容转换后的内容,但有些场景下还需要返回除了内容之外的东西。
例如以用 babel-loader 转换 ES6 代码为例,它还需要输出转换后的 ES5 代码对应的 Source Map,以方便调试源码。
为了把 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
的详细使用方法如下:
this.callback(// 当无法转换原内容时,给 Webpack 返回一个 Errorerr: Error | null,// 原内容转换后的内容content: string | Buffer,// 用于把转换后的内容得出原内容的 SourceMap,方便调试sourceMap?: SourceMap,// 如果本次转换为原内容生成了 AST 语法树,可以把这个 AST 返回// 以方便之后需要 AST 的 Loader 复用该 AST,以避免重复生成 AST,提升性能abstractSyntaxTree?: AST)
Source Map 的生成很耗时,通常在开发环境下才会生成 Source Map,其他环境下不用生成,以加速构建。为此 Webpack 为 Loader 提供了
this.sourceMap
API 去告诉 Loader 当前构建环境下用户是否需要 Source Map。如果你编写的 Loader 会生成 Source Map,请考虑到这点。
Loader 有同步和异步之分,上面的 Loader 都是同步的 Loader,因为它们的转换流程都是同步的,转换完成后再返回结果。但有些场景下转换的步骤只能是异步完成的,例如你需要通过网络请求才能得出结果,如果采用同步的方式 网络请求
就会阻塞整个构建,导致构建非常缓慢。
module.exports = function (source) {// 告诉 Webpack 本次转换是异步的,Loader 会在 callback 中回调结果var callback = this.async();someAsyncOperation(source, function (err, result, sourceMaps, ast) {// 通过 callback 返回异步执行后的结果callback(err, result, sourceMaps, ast);});};
在默认的情况下,Webpack 传给 Loader 的原内容都是 UTF-8 格式编码的字符串。但有些场景下 Loader 不是处理文本文件,而是处理二进制文件,例如 file-loader
,就需要 Webpack 给 Loader 传入二进制格式的数据。为此,你需要这样编写 Loader:
module.exports = function (source) {// 在 exports.raw = true 时,Webpack 传给 Loader 的 source 是 Buffer 类型的source instanceof Buffer === true;// Loader 返回的类型也可以是 Buffer 类型的// 在 exports.raw !== true 时,Loader 也可以返回 Buffer 类型的结果return source;};// 通过 exports.raw 属性告诉 Webpack 该 Loader 是否需要二进制数据module.exports.raw = true;
以上代码中最关键的是最后一行 module.exports.raw = true
,没有改行 Loader 只能拿到字符串。
通过 this.emitFile
进行文件写入
const loaderUtil = require('loader-utils');module.exports = function (content) {const url = loaderUtil.interpolateName(this, '[hash].[ext]', {content,});this.emitFile(url, content);const path = `__webpack_public_path__+${JSON.stringify(url)}`;return `export default ${path}`;};
在有些情况下,有些转换操作需要大量计算非常耗时,如果每次构建都重新执行重复的转换操作,构建将会变得非常缓慢。为此,Webpack 会默认缓存所有 Loader 的处理结果,也就是说在需要被处理的文件或者其他依赖的文件没有发生变化时,是不会重新调用对应的 Loader 去执行转换操作的。
如果你想让 Webpack 不缓存该 Loader 的处理结果,可以这样:
module.exports = function (source) {// 关闭该 Loader 的缓存功能this.cacheable(false);return source;};
Webpack 编译流程非常复杂,但其中设计 Loader 的部分主要包括:
rule.modules
创建 RulesSet 规则集loader-runner
运行 Loaderloader 和 plugin 有什么不同
module.rules
配置,作为模块的解析规则而存在。