Webpack 支持使用 loader 对文件进行预处理。你可以构建包括 JavaScript 在内的任何静态资源。
Loader 是一个文件加载器,能够加载不同的资源,并对这些文件进行操作,如编译,压缩,最终打包到指定的文件。
Loader 最核心的只能是实现内容转换器 ------ 将各式各样的资源转化为标准 JavaScript 内容格式
Why?
本质上是因为 Webpack 只认识符合 JavaScript 规范的文本(Webpack 5之后增加了其它 parser):在构建(make)阶段,解析模块内容时会调用 acorn 将文本转换为 AST 对象,进而分析代码结构,分析模块依赖;这一套逻辑对 image、json、Vue SFC等场景就不适用,需要通过 Loader 介入将资源转化成 Webpack 可以理解的内容形态。
常用 loader
- babel-loader 使用 Babel 加载 ES2015+ 代码并将其转换为 ES5
- ts-loader 像加载 JavaScript 一样加载 TypeScript 2.0+
- html-loader 将 HTML 导出为字符串,需要传入静态资源的引用路径
- markdown-loader 将 Markdown 编译为 HTML
- style-loader 将模块导出的内容作为样式并添加到 DOM 中
- css-loader 加载 CSS 文件并解析 import 的 CSS 文件,最终返回 CSS 代码
- less-loader 加载并编译 LESS 文件
- sass-loader 加载并编译 SASS/SCSS 文件
- postcss-loader 使用 PostCSS 加载并转换 CSS/SSS 文件
- vue-loader 加载并编译 Vue 组件
处理一个文件可以使用多个loader进行解析,loader的加载顺序是相反的,会从最后一个loader向上执行
认识 Loader
代码层面,Loader 通常是一个函数,结构如下:
js
module.exports = function(source, sourceMap?, data?) {
// source 为 loader 的输入,可能是文件内容,也可能是上一个 loader 处理结果
return source;
};
Loader 函数接收三个参数,分别为:
- source:资源输入,对于第一个执行的 loader 为资源文件的内容;后续执行的 loader 则为前一个 loader 的执行结果
- sourceMap: 可选参数,代码的 sourcemap 结构
- data: 可选参数,其它需要在 Loader 链中传递的信息,比如 posthtml/posthtml-loader 就会通过这个参数传递参数的 AST 对象
同步 Loader
其中 source 是最重要的参数,大多数 Loader 要做的事情就是将 source 转译为另一种形式的输入,比如
js
// loaders/cleanLogger.loader.js
module.exports = function (source) {
const reg = /console.log([\s\S]*?);/g;
source = source.replace(reg, '');
return source;
};
// webpack.config.js
module.epxorts = {
//...
module: {
rules: [
{
test: /.m?js$/,
exclude: /(node_modules|bower_components)/,
use: [
// {
// loader: 'babel-loader',
// options: {
// presets: ['@babel/preset-env'],
// },
// },
{
loader: path.join(__dirname, 'loaders/cleanLogger.loader.js'),
},
],
},
],
},
}
js
const div = document.createElement('div');
const a = document.createElement('a');
a.innerHTML = '快点我';
class Person {
name = '';
constructor(name) {
this.name = name;
}
run() {
console.log(this.name, '正在奔跑中...');
}
}
const person = new Person('张三');
person.run(1, 2);
div.appendChild(a);
div.addEventListener('click', () => {
let i = 0;
while (i <= 5) {
console.log(i);
i++;
}
});
document.querySelector('#app').appendChild(div);
执行 webpack 命令后生产文件中,我们会发现打包文件中无法找到 console.log 的代码
异步Loader
另外,loader 是可以返回 3 个参数的,我们可以调用 callback 进行传递,这里我们用less文件来说明
js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
mode: 'development',
// mode: 'production',
output: {
clean: true, // 在生成文件之前清空 output 目录
filename: 'bundle.[hash:8].js',
path: path.join(__dirname, 'dist'),
},
module: {
rules: [
{
test: /.m?js$/,
exclude: /(node_modules|bower_components)/,
use: [
{
loader: path.join(__dirname, 'loaders/cleanLogger.loader.js')
},
],
},
{
test: /.less$/,
use: [
{
loader: path.join(__dirname, 'loaders/styleLoader.js'),
},
{
loader: path.join(__dirname, 'loaders/lessLoader.js'),
},
],
},
],
},
};
less
@primaryColor: #1e80ff;
body {
background-color: @primaryColor;
color: #fff;
}
js
import './index.less';
这里我们定义了 lessLoader 和 styleLoader,并在 index.js 文件中引入了 index.less 文件
因为 loader 的 use 规则是从右到左,所以我们先实现 lessLoader
js
let less = require('less');
module.exports = function (source) {
const callback = this.async(); // 转译比较耗时,采用异步方式
// const options = this.getOptions(); // 获取配置文件中less-loader的options
less.render(source).then(
(output) => {
// 将生成的css代码传递给下一个loader
callback(null, output.css);
},
(err) => {
// handler err
}
);
};
这里我们调用了 this.async 这个方法,调用这个方法的意思是告诉当前 loader 为异步加载器,会挂起当前执行队列直到 callback 被触发
-
使用this.async来告诉上下文当前loader是一个异步loader需要loader runner等待异步处理结果
-
this.asycn()方法会返回一个callback回调函数,在异步任务处理完之后调用callback并将需要传递的数据(一般是异步操作处理后的数据)通过callback函数传递给下一个loader
js
module.exports = (source, map, data) => {
return `
const style = document.createElement('style');
style.innerHTML = ${JSON.stringify(source)};
document.head.appendChild(style);
`;
};
这里的 source 就是 lessLoader 编译后的内容,执行 webpack 命令,我们可以看到输出后的内容打开打包出来的index.html后,效果如下:
Context & Side Effect
我们在上文中调用了 this.getOptions,那么这个 this 是什么呢?这里面涉及到了 webpack 里的源码,我就不细说,这里的 this 对象由 NormolModule.createLoaderContext 函数在调用 Loader 前创建,里面的 api 有:
js
const loaderContext = {
// 获取当前 Loader 的配置信息
getOptions: schema => {},
// 添加警告
emitWarning: warning => {},
// 添加错误信息,注意这不会中断 Webpack 运行
emitError: error => {},
// 解析资源文件的具体路径
resolve(context, request, callback) {},
// 直接提交文件,提交的文件不会经过后续的chunk、module处理,直接输出到 fs
emitFile: (name, content, sourceMap, assetInfo) => {},
// 添加额外的依赖文件
// watch 模式下,依赖文件发生变化时会触发资源重新编译
addDependency(dep) {},
};
其中 addDependency、emitFile 、emitError、emitWarning 都会对后续编译流程产生副作用,例如 less-loader 源码中包含下面这段代码
js
try {
result = await (options.implementation || less).render(data, lessOptions);
} catch (error) {
// ...
}
const { css, imports } = result;
imports.forEach((item) => {
// ...
this.addDependency(path.normalize(item));
});
代码中首先调用 less 编译文件内容,之后遍历所有 import 语句,也就是上例 result.imports 数组,一一调用 this.addDependency 函数将 import 到的其它资源都注册为依赖,之后这些其它资源文件发生变化时都会触发重新编译
Loader Pitch
Webpack 允许在这个函数上挂载名为 pitch 的函数,运行时 pitch 会比 Loader 本身更早执行
js
const loader = (source, map) => {
console.log('后执行');
return source;
};
loader.pitch = function (remainingRequest, precedingRequest, data) {
console.log('先执行');
};
module.exports = loader;
我们看看执行结果那么这个 pitch 函数是用来干什么的?
js
function pitch(remainingRequest: string, previousRequest: string, data = {}): void {
// todo
}
- remainingRequest : 当前 loader 之后的资源请求字符串
- previousRequest : 在执行当前 loader 之前经历过的 loader 列表
- data : 与 Loader 函数的 data 相同,用于传递需要在 Loader 传播的信息
这些参数不复杂,但与 requestString 紧密相关,我们看个例子加深了解:
js
module.exports = {
module: {
rules: [
{
test: /.less$/i,
use: [
"style-loader", "css-loader", "less-loader"
],
},
],
},
};
将会发生这些步骤:
js
|- style-loader `pitch`
|- css-loader `pitch`
|- less-loader `pitch`
|- requested module is picked up as a dependency
|- less-loader normal execution
|- css-loader normal execution
|- style-loader normal execution
那么,为什么 loader 可以利用 "pitching" 阶段呢?
首先,传递给 pitch 方法的 data,在执行阶段也会暴露在 this.data 之下,并且可以用于在循环时,捕获并共享前面的信息
js
const loader = function (source, map, data) {
console.log(this.data.value);
return source;
};
loader.pitch = function (remainingRequest, precedingRequest, data) {
data.value = 42;
};
module.exports = loader;
其次,如果将一个 loader 的 pitch 函数设置一个返回值,那么后面的所有 loader 将不再执行
js
const loader = function (source, map, data) {
// console.log(this.data.value);
return source;
};
loader.pitch = function (remainingRequest, precedingRequest, data) {
return (
'module.exports = require(' + JSON.stringify('-!' + remainingRequest) + ');'
);
};
module.exports = loader;
比如 css-loader 中的 pitch 设置了返回值,那么 css-loader 和 style-loader 将不再执行,起到了一个阻断的作用
工具
- loader-utils:提供了一系列诸如读取配置、requestString 序列化与反序列化、计算 hash 值之类的工具函数
- schema-utils:参数校验工具
js
const schemaUtils = require('schema-utils'); // 校验options
const loaderUtils = require('loader-utils');
const schema = {
// ...
}
// 获取配置项
const options = loaderUtils.getOptions(this);
// 校验配置项
schemaUtils.validate(schema, options, {
name: "CSS Loader",
baseDataPath: "options",
});