loader 基础
- loader 是 webpack 中用于处理模块的转换器;
- 它们可以将文件从一种格式转换为另一种格式,比如将 TypeScript 转换为 JavaScript,将 SCSS 转换为 CSS 等;使用 loader 可以使 webpack 处理各种类型的文件,而不仅仅是 JavaScript;
- webpack 做的事情,仅仅是分析出各种模块的依赖关系,然后形成资源列表,最终打包生成到指定的文件中;更多的功能需要借助 webpack loaders 和 webpack plugins 完成;
- loader 本质上是一个函数,它的作用是将某个源码字符串转换成另一个源码字符串返回;loader 函数将在模块解析的过程中被调用,从而得到最终的源码;
基础使用
-
loader 就像是一个翻译员,能把源文件 经过转化后 输出新的结果,并且一个文件还可以链式地经过多个翻译员翻译;
-
以处理 SCSS 文件为例:
- SCSS 源代码会先交给 sass-loader 把 SCSS 转换成 CSS;
- 把 sass-loader 输出的 CSS 交给 css-loader 处理,找出 CSS 中依赖的资源、压缩 CSS 等;
- 把 css-loader 输出的 CSS 交给 style-loader 处理,转换成通过脚本加载的 JavaScript 代码;
-
可以看出以上的处理过程需要有顺序的链式执行,先 sass-loader 再 css-loader 再 style-loader;
-
以上处理的 webpack 相关配置如下:
module.exports = { module: { rules: [ { test: /\.scss$/, use: [ 'style-loader', { loader: 'css-loader', options: { minimize: true, }, }, 'sass-loader', ], }, ], }, };
loader 的职责
-
由上面的例子可以看出:
- 一个 loader 的职责是单一的,只需要完成一种转换;
- 如果一个源文件需要经历多步转换才能正常使用,就通过多个 loader 去转换;
- 在调用多个 loader 去转换一个文件时,每个 loader 会链式的顺序执行,第一个 loader 将会拿到需处理的原内容,上一个 loader 处理后的结果会传给下一个接着处理,最后的 loader 将处理后的最终结果返回给 webpack;
-
所以,开发一个 loader 时,请保持其职责的单一性,只需关心输入和输出;
常用 loader 盘点
-
babel-loader
:将 ES6+ 代码转换为 ES5module.exports = { module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'], }, }, }, ], }, };
-
css-loader 和 style-loader
:css-loader 解析 CSS 文件,style-loader 将 CSS 插入到 DOM 中module.exports = { module: { rules: [ { test: /\.css$/, use: ['style-loader', 'css-loader'], }, ], }, };
-
sass-loader
:将 SCSS 文件转换为 CSSmodule.exports = { module: { rules: [ { test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'], }, ], }, };
-
file-loader
:处理文件(如图片、字体),并返回其 URLmodule.exports = { module: { rules: [ { test: /\.(png|jpg|gif|svg)$/, use: [ { loader: 'file-loader', options: { name: '[name].[hash].[ext]', outputPath: 'images/', }, }, ], }, ], }, };
-
url-loader
:类似于 file-loader,但在文件小于设定的限制时返回 Data URLmodule.exports = { module: { rules: [ { test: /\.(png|jpg|gif|svg)$/, use: [ { loader: 'url-loader', options: { limit: 8192, name: '[name].[hash].[ext]', outputPath: 'images/', }, }, ], }, ], }, };
-
html-loader
:导入 HTML 文件,并处理其中的资源 (如 img 标签中的 src)module.exports = { module: { rules: [ { test: /\.html$/, use: ['html-loader'], }, ], }, };
-
ts-loader
:将 TypeScript 转换为 JavaScriptmodule.exports = { module: { rules: [ { test: /\.ts$/, use: 'ts-loader', exclude: /node_modules/, }, ], }, resolve: { extensions: ['.ts', '.js'], }, };
-
vue-loader
:处理 Vue 单文件组件const { VueLoaderPlugin } = require('vue-loader'); module.exports = { module: { rules: [ { test: /\.vue$/, use: 'vue-loader', }, ], }, plugins: [ new VueLoaderPlugin(), ], };
-
postcss-loader
:使用 PostCSS 处理 CSS 文件module.exports = { module: { rules: [ { test: /\.css$/, use: ['style-loader', 'css-loader', 'postcss-loader'], }, ], }, };
loader 进阶
loader 实现原理
-
loader 是一个导出为函数的 Node.js 模块;函数接收源代码作为参数,并返回转换后的代码;loader 之间通过链式调用,前一个 loader 的输出作为下一个 loader 的输入;以下是 loader 的执行步骤:
- 资源文件:首先 webpack 从入口开始读取资源文件;
- 匹配规则:根据配置中的规则,找到对应的 loader;
- 链式调用:loader 按照配置中的顺序链式调用,将处理后的结果传递给下一个 loader;
- 返回结果:最终结果返回给 webpack,由 webpack 生成最终的 bundle 文件;
-
以下是一个更详细的 loader 示例,它不仅转换字符串,还通过 webpack 提供的 loader API 与其他 loader 进行交互:
JavaScriptJavaScript// custom-loader.js const loaderUtils = require('loader-utils'); module.exports = function (source) { const options = loaderUtils.getOptions(this); const transformedSource = source.replace(/console\.log\(/g, `console.log('${options.prefix}: ' + `); return transformedSource; };
// 在 webpack 配置中使用 module.exports = { module: { rules: [ { test: /\.js$/, use: { loader: path.resolve(__dirname, 'custom-loader.js'), options: { prefix: 'DEBUG', }, }, }, ], }, };
获得 loader 的 options
-
在处理 SCSS 文件的 webpack 配置中,给 css-loader 传了 options 参数,以控制 css-loader;
-
如何在自己编写的 loader 中获取到用户传入的 options 呢?需要这样做:
const loaderUtils = require('loader-utils'); module.exports = function(source) { const options = loaderUtils.getOptions(this); return source; };
返回其他结果
-
上面的 loader 都只是返回了原内容转换后的内容,但有些场景下还需要返回除了内容之外的东西;
-
例如以 babel-loader 转换 ES6 代码为例,它还需要输出转换后的 ES5 代码对应的 Source Map,以方便调试源码;
-
为了把 Source Map 也一起随着 ES5 代码返回给 webpack,可以这样写:
module.exports = function(source) { this.callback(null, source, sourceMaps); return; };
同步与异步
-
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;
缓存加速
-
在有些情况下,有些转换操作需要大量计算非常耗时,如果每次构建都重新执行重复的转换操作,构建将会变得非常缓慢;为此,webpack 会默认缓存所有 loader 的处理结果,也就是说在需要被处理的文件或者其依赖的文件没有发生变化时,是不会重新调用对应的 loader 去执行转换操作的;
-
如果想让 webpack 不缓存该 loader 的处理结果,可以这样:
module.exports = function (source) { this.cacheable(false); // 关闭该 loader 的缓存功能 return source; };
其他 loader API
除了以上提到的在 loader 中能调用的 webpack API 外,还存在以下常用 API:
其它没有提到的 API 可以去 webpack 官网 查看;
api | 描述 |
---|---|
this.context | 当前处理文件的所在目录,假如当前 loader 处理的文件是 /src/main.js,则 this.context 就等于 /src |
this.resource | 当前处理文件的完整请求路径,包括 querystring,例如 /src/main.js?name=1 |
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 | 给当前处理文件添加其依赖的文件,以便在其依赖的文件发生变化时,会重新调用 loader 处理该文件;使用方法为 addDependency(file: string) |
this.addContextDependency | 和 addDependency 类似,但 addContextDependency 是把整个目录加入到当前正在处理文件的依赖中;使用方法为 addContextDependency(directory: string) |
this.clearDependencies | 清除当前正在处理文件的所有依赖,使用方法为 clearDependencies() |
this.emitFile | 输出一个文件,使用方法为 emitFile(name: string, content: Buffer|string, sourceMap: {...}) |
加载本地 loader
-
在开发 loader 的过程中,为了测试编写的 loader 是否能正常工作,需要把它配置到 webpack 中后,才可能会调用该 loader;
-
上文中使用的 loader 都是通过 npm 安装的,要使用 loader 时会直接使用 loader 的名称,代码如下:
module.exports = { module: { rules: [ { test: /\.css$/, use: ['style-loader'], }, ], }, };
-
如果还采取以上的方法去使用本地开发的 loader 将会很麻烦,因为需要确保编写的 loader 的源码是在 node_modules 目录下;为此需要先把编写的 loader 发布到 npm 仓库后再安装到本地项目使用;
使用 npm link
-
npm link
专门用于开发和调试本地 npm 模块,能做到在不发布模块的情况下,把本地的一个正在开发的模块的源码链接到项目的 node_modules 目录下,让项目可以直接使用本地的 npm 模块;由于是通过软链接的方式实现的,编辑了本地的 npm 模块代码,在项目中也能使用到编辑后的代码; -
完成
npm link
的步骤如下:- 确保正在开发的本地 npm 模块 (也就是正在开发的 loader) 的 package.json 已经正确配置好;
- 在本地 npm 模块根目录下执行
npm link
,把本地模块注册到全局; - 在项目根目录下执行
npm link loader-name
,把第 2 步注册到全局的本地 npm 模块链接到项目的 node_modules 下,其中的 loader-name 是指在第 1 步中的 package.json 文件中配置的模块名称;
-
链接好 loader 到项目后就可以像使用一个真正的 npm 模块一样使用本地的 loader 了;
使用 resolveLoader
-
为了让 webpack 加载放在本地项目中的 loader 需要修改 resolveLoader.modules;
-
假如本地的 loader 在项目目录中的 ./loaders/loader-name 中,则需要如下配置:
module.exports = { resolveLoader: { // 去哪些目录下寻找 Loader,有先后顺序之分 modules: ['node_modules', './loaders/'], }, };
-
加上以上配置后,webpack 会先去 node_modules 项目下寻找 loader,如果找不到,会再去 ./loaders/ 目录下寻找;
手写 loader
手写 style-loader
// index.js
var content = require("./assets/index.css");
console.log(content); // css 的源码字符串
/* assets/index.css */
body {
background: #333;
color : #fff;
}
// loaders/style-loader.js
module.exports = function (sourceCode) {
var code = `
var style = document.createElement("style");
style.innerHTML = \`${sourceCode}\`;
document.head.appendChild(style);
module.exports = \`${sourceCode}\`;
`;
return code;
}
// webpack.config.js
module.exports = {
mode: "development",
devtool: "source-map",
module: {
rules: [{
test: /\.css$/,
use: ["./loaders/style-loader"]
}]
}
}
手写 img-loader
// index.js
var src = require("./assets/webpack.png");
console.log(src);
var img = document.createElement("img");
img.src = src;
document.body.appendChild(img);
// loaders/img-loader.js
var loaderUtil = require("loader-utils"); // 用于获取 webpack 中配置的 options 中的参数
/***
* 将 buffer 数组转成base64 字符串
* @param {*} buffer
* @returns
*/
function getBase64(buffer) {
return "data:image/png;base64," + buffer.toString("base64");
}
/***
* 获取文件名字
* @param {*} buffer
* @param {*} name [contenthash].[ext]
* @returns
*/
function getFilePath(buffer, name) {
// 根据 [单文件 buffer] 生成 hash
var filename = loaderUtil.interpolateName(this, name, { content: buffer });
this.emitFile(filename, buffer); // 向最终的文件列表中增加文件
return filename;
}
function loader(buffer) {
// 获取原始数据 buffer
console.log("文件数据大小:(字节)", buffer.byteLength);
var { limit = 1000, filename = "[contenthash].[ext]" } = loaderUtil.getOptions(this);
if (buffer.byteLength >= limit) {
var content = getFilePath.call(this, buffer, filename);
} else {
var content = getBase64(buffer);
}
return `module.exports = \`${content}\``;
}
// 设置为 true 后,loader 返回的就是原始数据,而不是字符串了
loader.raw = true;
module.exports = loader;
// webpack.config.js
module.exports = {
mode: "development",
devtool: "source-map",
module: {
rules: [
{
test: /\.(png)|(jpg)|(gif)$/,
use: [
{
loader: "./loaders/img-loader.js",
options: {
limit: 3000, // 3000 字节以上使用图片,3000 字节以内使用 base64
filename: "img-[contenthash:5].[ext]"
}
}]
}
]
}
}
面试题
最后的输出结果
-
题目代码:
JavaScriptJavaScriptJavaScriptJavaScriptJavaScriptJavaScript// index.js require("./a.js"); // a.js 中内容为空
// loaders/loader1.js module.exports = function (sourceCode) { console.log("loader1"); return sourceCode; }
// loaders/loader2.js module.exports = function (sourceCode) { console.log("loader2"); return sourceCode; }
// loaders/loader3.js module.exports = function (sourceCode) { console.log("loader3"); return sourceCode; }
// loaders/loader4.js module.exports = function (sourceCode) { console.log("loader4"); return sourceCode; }
// webpack.config.js module.exports = { mode: "development", module: { rules: [ { test: /index\.js$/, //正则表达式,匹配模块的路径 use: ["./loaders/loader1", "./loaders/loader2"] //匹配到了之后,使用哪些加载器 }, //规则1 { test: /\.js$/, //正则表达式,匹配模块的路径 use: ["./loaders/loader3", "./loaders/loader4"] //匹配到了之后,使用哪些加载器 } //规则2 ], //模块的匹配规则 } }
-
输出:看 chunk 中解析模块的更详细流程
// index.js loader4 loader3 loader2 loader1 // a.js loader4 loader3
Module:模块化
上一篇