webpack 是个很强大的构建工具,其丰富灵活的配置决定了使用也不简单。在面试中经常能遇到 webpack 相关的问题,如果平常只是使用脚手架如 vue-cli 而没有好好深入学习研究 webpack 的话,估计答不上什么。我相信,如果没有深入了解,部分面试官也问不出什么。可能也就变成了两个人侃侃如何配置出入口,常见的 loader,plugin 有哪些。
作为一名多年油条前端,一直没有正视 webpack 相关知识,面对 webpack 相关的面试题更是一问三不知。这次准备好好学习研究 webpack相关内容,并且将学习内容记录成 webpack 系列,希望可以让不了解 webpack 的小白能对其有所掌握。
编写一个loader
由于 webpack 只能处理 .js 文件,所以其它类型的文件(如 .vue,.css)需要使用 loader 将其转化为 JS 对象进行处理。当然还有其它一些功能,我们也会放在 loader 中实现,如将 ES6 编译成 ES5。
webpack中lodaer的设置
{
test: /\.js$/
use: [
{
loader: path.resolve('path/to/loader.js'),
options: {/* ... */}
}
]
}
lodaer的实现
loader 是导出为一个函数的 node 模块。该函数在 loader 转换资源的时候调用。给定的函数将调用 loader API,并通过 this 上下文访问。
loader 的第一个入参 source 为包含包含资源文件内容的字符串(类型为buffer或string),map 为资源的 sourceMap,meta 为元数据。
一般 loader 会返回一个或者两个值。第一个值的类型是 JavaScript 代码的字符串或者 buffer。第二个参数值是 SourceMap,它是个 JavaScript 对象。
我们来编写个简单的lodaer,此 loader 帮助我们替换资源中的 [name] 占位符
// loader-utils包含一些lodaer工具函数
import { getOptions } from 'loader-utils';
export default function loader(source, map, meta) {
const options = getOptions(this);
source = source.replace(/\[name\]/g, options.name);
return `export default ${ JSON.stringify(source) }`;
};
一般我们使用 return 来返回编译后的资源,如果需要返回其它值可以使用 callback
this.callback(err, source, map, meta);
其实写一个简单的 loader 真的非常简单,上面我们只用几行代码就实现了一个简单的 loader。但是像 css-loader babel-loader 的复杂度就高了,有兴趣的话可以去研究研究其源码实现。
如何编写一个 lodaer 已经写完了。下面是 loader 的编写准则,和如何对我们编写的 loader 进行单元测试,有兴趣的可以继续看下去。
loader的编写准则
编写 loader 时应该遵循以下准则。它们按重要程度排序,有些仅适用于某些场景。
- 简单易用(Simple)
loaders 应该只做单一任务。这不仅使每个 loader 易维护,也可以在更多场景链式调用
- 使用链式传递(Chaining)
loader 可以被链式调用意味着不一定要输出 JavaScript。只要下一个 loader 可以处理这个输出,这个 loader 就可以返回任意类型的模块。
- 模块化的输出(Modular)
保证输出模块化。loader 生成的模块与普通模块遵循相同的设计原则。
- 无状态(Stateless)
确保 loader 在不同模块转换之间不保存状态。每次运行都应该独立于其他编译模块以及相同模块之前的编译结果。
- 使用 loader utilities
充分利用 loader-utils 包。它提供了许多有用的工具,但最常用的一种工具是获取传递给 loader 的选项。schema-utils 包配合 loader-utils,用于保证 loader 选项,进行与 JSON Schema 结构一致的校验。这里有一个简单使用两者的例子:
import { getOptions } from 'loader-utils';
import validateOptions from 'schema-utils';
const schema = {
type: 'object',
properties: {
test: {
type: 'string'
}
}
}
export default function(source) {
const options = getOptions(this);
validateOptions(schema, options, 'Example Loader');
// 对资源应用一些转换……
return `export default ${ JSON.stringify(source) }`;
};
- 记录 loader 的依赖
如果一个 loader 使用外部资源(例如,从文件系统读取),必须声明它。这些信息用于使缓存 loaders 无效,以及在观察模式(watch mode)下重编译。下面是一个简单示例,说明如何使用 addDependency 方法实现上述声明:
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 函数解析路径
-
提取通用代码
避免在 loader 处理的每个模块中生成通用代码。相反,你应该在 loader 中创建一个运行时文件,并生成 require 语句以引用该共享模块。
- 避免绝对路径
不要在模块代码中插入绝对路径,因为当项目根路径变化时,文件绝对路径也会变化。loader-utils 中的 stringifyRequest 方法,可以将绝对路径转化为相对路径。
- 使用 peer dependencies
如果你的 loader 简单包裹另外一个包,你应该把这个包作为一个 peerDependency 引入。这种方式允许应用程序开发者在必要情况下,在 package.json 中指定所需的确定版本。
例如,sass-loader 指定 node-sass 作为同等依赖,引用如下:
"peerDependencies": {
"node-sass": "^4.0.0"
}
测试
我们使用 jest 来测试我们编写的 loader
在 package.json 中添加测试命令
"scripts": {
"test": "jest"
}
安装相关依赖
npm i npm install --save-dev jest babel-jest babel-preset-env @babel/preset-env @babel/plugin-transform-runtime
配置 .babelrc
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"browsers": [
"last 2 versions"
]
}
}
]
],
"plugins": [
"@babel/plugin-transform-runtime"
]
}
添加测试目录
test
compiler.js
example.txt
loader.test.js
compiler.js 添加webpack配置及运行编译
import path from 'path';
import webpack from 'webpack';
import memoryfs from 'memory-fs';
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, '../loader/lower.js'),
options: {
name: 'Alice'
}
}
}]
}
});
compiler.outputFileSystem = new memoryfs();
return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
if (err) reject(err);
resolve(stats);
});
});
}
example.txt 添加编译前内容
Hey [name]!
lodaer.test.js 为单元测试代码
import compiler from './compiler.js';
test('Inserts name and outputs JavaScript', async () => {
const stats = await compiler('example.txt');
const output = stats.toJson().modules[0].source;
expect(output).toBe(`export default "Hey Alice!"`);
});
接下来运行 npm 命令即可检验我们编写的 loader 了
npm run test
参考
欢迎到前端菜鸟群一起学习交流~516913974