参考:css
- Webpack Book --- Extending with Loaders。
- Webpack Doc --- Loader Interface
Loader 是 Webpack 几大重要的模块之一。当你须要加载资源,就须要设置对应的 Loader,这样就能够对其源代码进行转换。node
因为 Webpack 社区的繁荣,使得大部分的业务场景所使用的资源都有对用的 loader,能够参考官网的 available loaders,可是因为业务的独特性,也可能没有适用的 loader。webpack
接下来会经过几个示例来让你学会如何开发一个本身的 loader。但在此以前,最好先了解如何单独调试它们。web
loader-runner 容许你不依靠 webpack 单独运行 loader,首先安装它npm
mkdir loader-runner-example
npm init
npm install loader-runner --save-dev
复制代码
接下来,建立一个 demo-loader,来进行测试bash
mkdir loaders
echo "module.exports = input => input + input;" > loaders/demo-loader.js
复制代码
这个 loader 会将引入模块的内容复制一次并返回。建立所引入的模块异步
echo "Hello world" > demo.txt
复制代码
接下来,经过loader-runner运行加载器:async
// 建立 run-loader.js
const fs = require("fs");
const path = require("path");
const { runLoaders } = require("loader-runner");
runLoaders(
{
resource: "./demo.txt",
loaders: [path.resolve(__dirname, "./loaders/demo-loader")],
readResource: fs.readFile.bind(fs),
},
(err, result) =>
(err ? console.error(err) : console.log(result))
);
复制代码
当你运行 node run-loader.js
,会看到终端上 log 出来函数
{ result: [ 'Hello world\nHello world\n' ],
resourceBuffer: <Buffer 48 65 6c 6c 6f 20 77 6f 72 6c 64 0a>, cacheable: true, fileDependencies: [ './demo.txt' ], contextDependencies: [] } 复制代码
从输出结果中能够看出工具
若是,须要将转换后的文件输出出来,只须要修改 runLoaders 的第二个参数,如
runLoaders(
{
resource: "./demo.txt",
loaders: [path.resolve(__dirname, "./loaders/demo-loader")],
readResource: fs.readFile.bind(fs),
},
(err, result) => {
if (err) console.error(err)
fs.writeFileSync("./output.txt", result.result)
}
);
复制代码
尽管你能够经过上述这种同步式接口(synchronous interface)实现一系列的 loader,可是这种形式并不能适用全部场景,例如将第三方软件包包装为 loader 时就会强制要求你执行此操做。
为了将上述例子调整为异步的形式,咱们使用 webpack 提供的 this.async()
API。经过调用这个函数能够返回一个遵照 Node 规范的回调函数(error first,result second)。
上述例子能够改写为:
loaders/demo-loader.js
module.exports = function(input) {
const callback = this.async();
// No callback -> return synchronous results
// if (callback) { ... }
callback(null, input + input);
};
复制代码
webpack 经过
this
进行注入,因此不能使用 () => {}。
以后运行 node run-loader.js
会在终端上打印出相同的结果。若是你想要在对 loader 执行期间产生的异常进行处理,则能够
module.exports = function(input) {
const callback = this.async();
callback(new Error("Demo error"));
};
复制代码
终端上打印的日志会包含错误:demo error,堆栈跟踪显示错误发生的位置。
loader 也能够用于单独输出代码,能够这样实现
module.exports = function() {
return "foobar";
};
复制代码
为何要这么作呢?你能够将 webpack 的入口文件传递给 loader。来代替指向预先设定的文件的状况,这样能够动态地生成对应 code 的 loader。
若是你想要 return 一个 Buffer 形式的输出,能够设定 module.exports.raw = true,将原有的 string 改成 buffer。
有一些 loader,像 file-loader,会生成文件。对此 webpack 提供了一个方法,this.emitFile
,可是 loader-runner 暂时还不支持,因此须要主动实现
runLoaders(
{
resource: "./demo.txt",
loaders: [path.resolve(__dirname, "./loaders/demo-loader")],
// 为 this 添加 emitFile method
context: {
emitFile: () => {},
},
readResource: fs.readFile.bind(fs),
},
(err, result) => (err ? console.error(err) : console.log(result))
);
复制代码
要实现 file-loader 的基本思想,您必须作两件事:找出文件并返回它的路径。 你能够按以下方式实现:
const loaderUtils = require("loader-utils");
module.exports = function(content) {
const url = loaderUtils.interpolateName(this, "[hash].[ext]", {
content,
});
this.emitFile(url, content);
const path = `__webpack_public_path__ + ${JSON.stringify(url)};`;
return `export default ${path}`;
};
复制代码
Webpack 提供了额外的两个 emit
方法:
this.emitWarning(<string>)
this.emitError(<string>)
这些方法都是用来替代控制台。 与 this.emitFile
同样,你必须模拟它们才能使loader-runner工做。
接下来的问题是,如何将文件名传递给 loader。
为了将所需的配置传递给 loader,咱们须要作一些修改
run-loader.js
const fs = require("fs");
const path = require("path");
const { runLoaders } = require("loader-runner");
runLoaders(
{
resource: "./demo.txt",
loaders: [
{
loader: path.resolve(__dirname, "./loaders/demo-loader"),
options: {
name: "demo.[ext]",
},
},
],
context: {
emitFile: () => {},
},
readResource: fs.readFile.bind(fs),
},
(err, result) => (err ? console.error(err) : console.log(result))
);
复制代码
能够看到,咱们将 loaders 从原有的
loaders: [path.resolve(__dirname, "./loaders/demo-loader")]
复制代码
改成了,从而能够传递 options
loaders: [
{
loader: path.resolve(__dirname, "./loaders/demo-loader"),
options: {
name: "demo.[ext]",
},
},
]
复制代码
为了可以获取到,咱们传递的 options,依然利用 loader-utils 来解析 options。
别忘了 npm install loader-utils --save-dev
为了将它与 loader 进行链接
loaders/demo-loader.js
const loaderUtils = require("loader-utils");
module.exports = function(content) {
// 获取 options
const { name } = loaderUtils.getOptions(this);
const url = loaderUtils.interpolateName(this, "[hash].[ext]", {
content,
});
const url = loaderUtils.interpolateName(this, name, { content });
);
};
复制代码
运行 node run-loader.js,你会发如今终端上打印出了
{ result:
[ 'export default __webpack_public_path__ + "f0ef7081e1539ac00ef5b761b4fb01b3.txt";' ],
resourceBuffer: <Buffer 48 65 6c 6c 6f 20 77 6f 72 6c 64 0a>,
cacheable: true,
fileDependencies: [ './demo.txt' ],
contextDependencies: [] }
复制代码
能够看出结果与 loader 应返回的内容一致。 你能够尝试将更多选项传递给 loader 或使用查询参数来查看不一样组合会发生什么。
为了进行一步地使用 loader,咱们须要将它与 webpack 联系起来。在这里,咱们采用内联的形式引入自定义 loader
// webpack.config.js 中引入
resolveLoader: {
alias: {
"demo-loader": path.resolve(
__dirname,
"loaders/demo-loader.js"
),
},
},
// 在文件中指定 loader,引入
import "!demo-loader?name=foo!./main.css"
复制代码
固然你还能够经过规则处理 loader。一旦它足够稳定,就创建一个基于 webpack-defaults 的项目,将逻辑推送到 npm,而后开始将 loader 做为包使用。
尽管咱们使用 loader-runner 来做为开发、测试 loader 的环境。可是它与 webpack 仍是有细微的不一样的,因此还须要在 webpack 上测试一下。
webpack 分为两个阶段来执行 loader:pitching、evaluating。若是你熟悉 web 的事件系统,它与事件的捕获、冒泡很类似。webpack 容许你在 pitching 阶段进行拦截执行。它的顺序是,从左到右pitch,从右到左执行。
一个 pitch loader 容许你对请求进行修改,甚至终止它。 例如,建立
loaders/pitch-loader.js
const loaderUtils = require("loader-utils");
module.exports = function(input) {
const { text } = loaderUtils.getOptions(this);
return input + text;
};
module.exports.pitch = function(remainingReq, precedingReq, input) {
console.log(` Remaining request: ${remainingReq} Preceding request: ${precedingReq} Input: ${JSON.stringify(input, null, 2)} `);
return "pitched";
};
复制代码
并将其添加到 run-loader.js 中,
...
loaders: [
{
loader: path.resolve (__dirname, './loaders/demo-loader'),
options: {
name: 'demo.[ext]',
},
},
path.resolve(__dirname, "./loaders/pitch-loader"),
],
...
复制代码
执行 node run-loader.js
Remaining request: ./demo.txt
Preceding request: .../webpack-demo/loaders/demo-loader?{"name":"demo.[ext]"}
Input: {}
{ result: [ 'export default __webpack_public_path__ + "demo.txt";' ],
resourceBuffer: null,
cacheable: true,
fileDependencies: [],
contextDependencies: [] }
复制代码
你会发现 pitch-loader 完成了信息的插入以及执行的拦截。
webpack loader 实质上就是在描述一种文件格式如何转换为另外一种文件格式。你能够经过研究 API 文档或现有的 loader 来弄清楚如何实现特定的功能。
回顾下:
loader-runner
是一个很是实用的工具,用来开发、调试 loader;this.async
来编写异步的 loader;loader-utils
可以编译 loader 的配置,还能够经过 schema-utils
进行验证;resolveLoader.alias
来完成局部的自定义 loader 引入,防止影响全局;