Open kangyana opened 2 years ago
loader
支持链式调用。loader
支持同步,也支持异步。package.json
的 main
指定 npm包 为 loader
,还可以在 module.rules
中使用 loader
字段直接引用一个模块。plugin
可以为 loader
带来更多特性。loader
能够产生额外的文件。可以通过 loader
的预处理函数,进行压缩、打包、编译等功能。
loader
遵循标准 模块解析 规则。
多数情况下,loader
将从 模块路径 加载(通常是从 npm install, node_modules 进行加载)。
预期 loader
模块导出为一个函数,并且编写为 Node.js 兼容的 JavaScript。
通常使用 npm 进行管理 loader
,但是也可以将应用程序中的文件作为自定义 loader
。
按照约定,loader
通常被命名为 xxx-loader(例如 json-loader)。
loader
是导出为一个函数的 node 模块。
该函数在 loader
转换资源的时候调用。
给定的函数将调用 Loader API
,并通过 this
上下文访问。
匹配单个 loader
,你可以通过在 rule
对象使用 path.resolve
指定一个本地文件:
// webpack.config.js
const path = require('path');
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: path.resolve('path/to/loader.js'),
options: {},
},
],
},
],
},
};
匹配多个 loaders
,你可以使用 resolveLoader.modules
配置,webpack
将会从这些目录中搜索这些 loaders
。
例如,如果你的项目中有一个 /loaders
本地目录:
// webpack.config.js
const path = require('path');
module.exports = {
resolveLoader: {
modules: ['node_modules', path.resolve(__dirname, 'loaders')],
},
};
当一个 loader
在资源中使用,这个 loader
只能传入一个参数 - 一个包含资源文件内容的字符串。
同步 loader
可以 return
一个代表已转换模块的单一值。
在更复杂的情况下,loader
也可以通过使用 this.callback(err, values...)
函数,返回任意数量的值。
错误要么传递给这个 this.callback
函数,要么抛给同步 loader
。
loader
会返回一个或者两个值。
第一个值的类型是 JavaScript 代码的字符串或者 buffer。
第二个可选值是 SourceMap
,它是个 JavaScript 对象。
当链式调用多个 loader
的时候,请记住它们是倒序执行的。
最后的 loader
最早调用,将会传入原始资源内容。
第一个 loader
最后调用,期望值是传出 JavaScript 和 source map
(可选)。
中间的 loader
执行时,会传入前一个 loader
的结果。
在下例中,foo-loader
被传入原始资源,bar-loader
将接收 foo-loader
的产出,
返回最终转化后的模块和一个 source map
(可选)。
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.js/,
use: ['bar-loader', 'foo-loader'],
},
],
},
};
编写 loader
时应该遵循以下准则:
loaders
应该只做单一任务。
这不仅使每个 loader
易维护,也可以在更多场景链式调用。
利用 loader
可以链式调用的优势。
写五个简单的 loader
实现五项任务,而不是一个 loader
实现五项任务。
功能隔离不仅使 loader
更简单,还利于后续维护与开发。
以通过 loader
选项或者查询参数得到的数据渲染模板为例。
可以把源代码编译为模板,执行并输出包含 HTML 代码的字符串写到一个 loader
中。
但是根据用法准则,已经存在这样一个 apply-loader
,可以将它和其他开源 loader
串联在一起调用。
pug-loader
: 导出一个函数,把模板转换为模块。apply-loader
: 根据 loader 选项执行函数,返回原生 HTML。html-loader
: 接收 HTML,输出一个合法的 JS 模块。注意:loader
可以被链式调用意味着不一定要输出 JavaScript。只要下一个 loader
可以处理这个输出,这个 loader
就可以返回任意类型的模块。
保证输出模块化。
loader
生成的模块与普通模块遵循相同的设计原则。
确保 loader
在不同模块转换之间不保存状态。
每次运行都应该独立于其他编译模块以及相同模块之前的编译结果。
充分利用 loader-utils
包。
它提供了许多有用的工具,但最常用的一种工具是获取传递给 loader
的选项。
schema-utils
包配合 loader-utils
,用于保证 loader
选项,进行与 JSON Schema 结构一致的校验。
这里有一个简单使用两者的例子:
// loader.js
import { urlToRequest } from 'loader-utils';
import { validate } from 'schema-utils';
const schema = {
type: 'object',
properties: {
test: {
type: 'string',
},
},
};
export default function (source) {
const options = this.getOptions();
validate(schema, options, {
name: 'Example Loader',
baseDataPath: 'options',
});
console.log('请求文件路径:', urlToRequest(this.resourcePath));
// 对资源应用一些转换……
return `export default ${JSON.stringify(source)}`;
}
如果一个 loader
使用外部资源(例如,从文件系统读取),必须声明它。
这些信息用于使缓存 loaders
无效,以及在 观察模式 下重编译。
下面是一个简单示例,说明如何使用 addDependency
方法实现上述声明:
// loader.js
import path from 'path';
export default function (source) {
var callback = this.async();
var headerPath = path.resolve('header.js');
this.addDependency(headerPath);
fs.readFile(headerPath, 'utf-8', function (err, header) {
if (err) return callback(err);
callback(null, header + '\n' + source);
});
}
根据模块类型,可能会有不同的模式指定依赖关系。
例如在 CSS 中,使用 @import
和 url(...)
语句来声明依赖。
这些依赖关系应该由模块系统解析。
可以通过以下两种方式中的一种来实现:
require
语句。this.resolve
函数解析路径。css-loader
包 是第一种方式的一个例子。
它将 @import
语句替换为 require
其他样式文件,将 url(...)
替换为 require
引用文件,
从而实现将依赖关系转化为 require
声明。
而less-loader
包,无法将每个 @import
转化为 require
,因为所有 .less
的文件中的变量和混合跟踪必须一次编译。
因此,less-loader
将 less 编译器进行了扩展,自定义路径解析逻辑。
然后,利用第二种方式,通过 webpack
的 this.resolve
解析依赖。
避免在 loader
处理的每个模块中生成通用代码。
你应该在 loader
中创建一个运行时文件,并生成 require
语句以引用该共享模块:
src/loader-runtime.js
const { someOtherModule } = require('./some-other-module');
module.exports = function runtime(params) {
const x = params.y * 2;
return someOtherModule(params, x);
};
src/loader.js
import runtime from './loader-runtime.js';
export default function loader(source) {
// 自定义的 loader 逻辑
return `${runtime({
source,
y: Math.random(),
})}`;
}
不要在模块代码中插入 绝对路径,因为当项目根路径变化时,文件绝对路径也会变化。
loader-utils
中的 stringifyRequest
方法,可以将绝对路径转化为相对路径。
如果你的 loader
简单包裹另外一个包,你应该把这个包作为一个 peerDependency
引入。
这种方式允许应用程序开发者在必要情况下,在 package.json
中指定所需的确定版本。
例如,sass-loader
指定 node-sass
作为同等依赖,引用如下:
saas-loader/package.json
{
"peerDependencies": {
"node-sass": "^4.0.0"
}
}
我们将使用 Jest 框架。
然后还需要安装 babel-jest
和允许我们使用 import / export
和 async / await
的一些预设环境(presets)。
让我们开始安装,并且将这些依赖保存为 devDependencies
:
npm install --save-dev jest babel-jest @babel/core @babel/preset-env
babel.config.js
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: {
node: 'current',
},
},
],
],
};
我们的 loader
将会处理 .txt 文件,并且将任何实例中的 name 直接替换为 loader
选项中设置的 name。
然后返回包含默认导出文本的 JavaScript 模块:
src/loader.js
export default function loader(source) {
const options = this.getOptions();
source = source.replace(/\[name\]/g, options.name);
return `export default ${JSON.stringify(source)}`;
}
使用memfs 去执行 webpack
。
npm install --save-dev webpack memfs
test/compiler.js
import path from 'path';
import webpack from 'webpack';
import { createFsFromVolume, Volume } from 'memfs';
export default (fixture, options = {}) => {
const compiler = webpack({
context: __dirname,
entry: `./${fixture}`,
output: {
path: path.resolve(__dirname),
filename: 'bundle.js',
},
module: {
rules: [
{
test: /\.txt$/,
use: {
loader: path.resolve(__dirname, '../src/loader.js'),
options,
},
},
],
},
});
compiler.outputFileSystem = createFsFromVolume(new Volume());
compiler.outputFileSystem.join = path.join.bind(path);
return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
if (err) reject(err);
if (stats.hasErrors()) reject(stats.toJson().errors);
resolve(stats);
});
});
};
注意:这种情况下,我们可以内联 webpack
配置,也可以把配置作为参数传给导出的函数。这允许我们使用相同的编译模块测试多个设置。
最后,我们来编写测试,并且添加 npm script 运行它: test/loader.test.js
/**
* @jest-environment node
*/
import compiler from './compiler.js';
test('Inserts name and outputs JavaScript', async () => {
const stats = await compiler('example.txt', { name: 'Alice' });
const output = stats.toJson({ source: true }).modules[0].source;
expect(output).toBe('export default "Hey Alice!\\n"');
});
package.json
{
"scripts": {
"test": "jest"
},
"jest": {
"testEnvironment": "node"
}
}
准备就绪后,我们可以运行它,然后看新的 loader
是否能通过测试:
PASS test/loader.test.js
✓ Inserts name and outputs JavaScript (229ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.853s, estimated 2s
Ran all test suites.
1. loader
loader
用于对 模块源代码 进行转换。loader
可以使你在import
或 加载模块 时预处理文件。 因此,loader
类似于其他构建工具中task
,并提供了处理前端构建步骤的得力方式。loader
可以将文件从不同的语言(如 TypeScript)转换为 JavaScript 或将内联图像转换为data URL
。loader
甚至允许你直接在 JavaScript 模块中import
CSS 文件!2. 示例
例如,你想要加载 CSS 文件、讲TypeScript 转为 JavaScript。 为此,首先安装相对应的 loader:
然后对每个
.css
文件 使用css-loader
,对每个.ts
文件使用ts-loader
:3. 使用 loader
loader
有两种使用的方式:webpack.config.js
文件中指定loader
。import
语句中显式指定loader
。配置方式 Configuration
module.rules
允许你在webpack
配置中指定多个loader
。 这种方式是展示loader
的一种简明方式,并且有助于使代码变得简洁和易于维护。loader
的执行顺序是从上到下。内联方式
可以在
import
语句或任何 与 "import" 方法同等的引用方式 中指定loader
。 使用!
将资源中的loader
分开。每个部分都会相对于当前目录解析。通过为内联
import
语句添加前缀,可以覆盖configuration
中的所有loader
,preLoader
和postLoader
:!
前缀,将禁用所有已配置的 普通 loader。!!
前缀,将禁用所有已配置的loader
(preLoader, loader, postLoader)。-!
前缀,将禁用所有已配置的preLoader
和loader
,但是不禁用postLoaders
。注意:尽可能使用
module.rules
,这样可以减少源码中样板文件的代码量。并且可以在出错时快速定位loader
的问题。