写作不易,Star是最大鼓励,感觉写的不错的可以给个Star⭐,请多多指教。本博客的Github地址。
[TOC] 原则上webpack只能处理js模块,如果要处理其他类型的文件,需要使用对应的loader进行转换处理。
举个🌰:处理SCSS文件
- 先将SCSS源代码提交给sass-loader,将SCSS转换成CSS;
- 将sass-loader输出的CSS提交给css-loader处理,找出CSS中依赖的资源、压缩CSS等;
- 将css-loader输出的CSS提交给style-loader处理,转换成通过脚本加载的JavaScript代码;
Webpack相关配置如下:
module.exports = {
module: {
rules: [
{
// 增加对 SCSS 文件的支持
test: /\.scss$/,
// SCSS文件的处理顺序为先sass-loader,再css-loader,再style-loader
use: [
'style-loader',
{
loader: 'css-loader',
// 给css-loader传入配置项
options: {
minimize: true,
}
},
'sass-loader']
},
]
},
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
可以看出,以上处理过程需要有顺序的链式执行,先sass-loader,再css-loader,再style-loader。是从后向前依次执行对应的loader,那么为什么loader的执行顺序是从后向前的呢?这就要说到函数组合了。
函数组合的两种情况
- Unix中的pipline:执行顺序是从左往右
- 函数组合Compose(webpack采用的就是这种):执行顺序是从右往左
什么是loader
loader是webpack中一个重要的概念,用来将一段代码转换成另一段代码的webpack加载器。loader本质上就是导出为一个函数的node模块,该函数在loader转换资源的时候调用。给定的函数将调用loader API,并通过this上下文访问。loader就像是一个翻译员,能将源文件经过转化后输出新的结果,并且一个文件还可以链式地经过多个翻译员翻译。
loader特点
- 第一个loader要返回js脚本
- 每个loader的职责是单一的,即只做一件事(只需要完成一种转换)
- 每一个loader都是一个Node模块
- 每个loader都是无状态的,确保loader在不同模块转换之间不保存状态
需要注意:在开发一个loader时,请保持其职责的单一性,我们只需关心输入和输出。如果一个源文件需要经历多步转换才能正常使用,就通过多个loader去转换。 在调用多个loader`转换一个文件时,每个loader都会链式地顺序执行。第一个loader将会拿到需处理的原内容,上一个loader处理后的结果会被传给下一个loader接着处理,最后的loader将处理后的最终结果返回给Webpack。
loader基础
Webpack是运行在Node.js之上的,一个loader其实就是一个Node.js模块,这个模块需要导出一个函数。这个导出函数的工作就是:获得处理前的原内容,对原内容执行处理后,返回处理后的内容。
一个最简单的loader的源码如下:
module.exports = function(source) {
// source为compiler传递给loader的一个文件的原内容
// 该函数需要返回处理后的内容,这里为了简单起见,直接把原内容返回了,相当于该loader没有做任何转换
return source;
};
2
3
4
5
由于loader运行在Nodejs中,所以我们可以调用任意Nodejs自带的API,或者安装第三方模块进行调用:
const sass = require('node-sass');
module.exports = function(source) {
return sass(source);
};
2
3
4
初始化loader创建
// 先安装webpack-cli
webpack-cli generate-loader
2
loader进阶
以上只是个最简单的loader,Webpack还提供一些API供loader调用,下面来一一介绍:
使用loader-runner高效进行loader的调试
loader-runner
允许我们在不安装webpack的情况下运行loaders。
作用:
- 作为webpack的依赖,webpack中使用它执行loader;
- 进行loader的开发和调试。
const {runLoaders} = require('loader-runner');
const fs = require('fs');
const path = require('path');
runLoaders({
resource: path.join(__dirname, './src/demo.txt'),
loaders: [
{
loader: path.join(__dirname, './src/raw-loader.js'),
options: { // loader参数
name: 'test-loader'
}
}
],
context: {
emitFile: () => {}
},
readResource: fs.readFile.bind(fs)
}, (err, result) => {
console.log(result.resourceBuffer.toString()); // foobar
err ? console.log(err) : console.log(result);
});
// result的内容:
/*
{ result: [ 'export default "bar"' ], // loader执行结果
resourceBuffer: <Buffer 66 6f 6f 62 61 72>,
cacheable: true, // 结果是否被缓存
fileDependencies:
[ '/Users/liujie26/geektime-webpack-course/code/chapter07/raw-loader/src/demo.txt' ],
contextDependencies: [] }
*/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
loader参数的获取(通过loader-utils)
在最上面处理SCSS文件的Webpack配置中,将options参数传给了css-loader,以控制css-loader。如何在自己编写的Loader中获取到用户传入的options呢?需要这样做:
const loaderUtils = require('loader-utils');
module.exports = function(source) {
// 获取用户为当前Loader传入的options
const options = loaderUtils.getOptions(this);
return source;
};
2
3
4
5
6
返回其它结果
上面的loader都只是返回了原内容转换后的内容,但在某些场景下还需要返回除了内容之外的东西。
以用babel-loader转换ES6代码为例,它还需要输出转换后的ES5代码对应的Source Map,以方便调试源码。为了将Source Map也一起随着ES5 代码返回给Webpack,可以这样写:
module.exports = function(source) {
// 通过 this.callback 告诉 Webpack 返回的结果
this.callback(null, source, sourceMaps);
// 当我们使用 this.callback 返回内容时,该 Loader 必须返回 undefined,
// 以让 Webpack 知道该 Loader 返回的结果在 this.callback 中,而不是 return 中
return;
};
2
3
4
5
6
7
其中的this.callback
是 Webpack 给 Loader 注入的 API,以方便 Loader 和 Webpack 之间通信。this.callback
的详细使用方法如下:
this.callback(
// 当无法转换原内容时,给 Webpack 返回一个 Error
err: Error | null,
// 原内容转换后的内容
content: string | Buffer,
// 用于把转换后的内容得出原内容的 Source Map,方便调试
sourceMap?: SourceMap,
// 如果本次转换为原内容生成了 AST 语法树,可以把这个 AST 返回,
// 以方便之后需要 AST 的 Loader 复用该 AST,以避免重复生成 AST,提升性能
abstractSyntaxTree?: AST
);
2
3
4
5
6
7
8
9
10
11
Source Map
的生成很耗时,通常在开发环境下才会生成Source Map
,其他环境下不用生成,以加速构建。因此,Webpack为Loader提供了 this.sourceMap API去告诉Loader
当前构建环境下用户是否需要 Source Map。如果我们编写的Loader会生成Source Map,请考虑到这点。
loader返回值处理
return `export default ${json}`;
// loader返回一个值的话可以采用return的方式
// 如果要返回多个值的话就需要采用this.callback的方式了
// this.callback(null, json, 2, 3, 4);
2
3
4
同步与异步
Loader有同步和异步之分,上面介绍的Loader都是同步的Loader,因为它们的转换流程都是同步的,转换完成后再返回结果。但在某些场景下转换的步骤只能是异步完成的,例如:我们需要通过网络请求才能得出结果,如果采用同步的方式,则网络请求会阻塞整个构建,导致构建非常缓慢。
如果是异步转换,我们可以这样:通过this.async来返回一个异步函数。
module.exports = function(source) {
// 告诉 Webpack 本次转换是异步的,Loader 会在 callback 中回调结果
var callback = this.async();
someAsyncOperation(source, function(err, result, sourceMaps, ast) {
// 通过 callback 返回异步执行后的结果
// callback是this.async返回的异步函数
// 第一个参数是Error,第二个参数是处理的结果
callback(err, result, sourceMaps, ast);
});
};
2
3
4
5
6
7
8
9
10
同步loader异常处理
- loader内直接通过throw抛出
- 通过this.callback传递错误
this.callback(
err: Error | null,
content: string | Buffer,
sourceMap?: SourceMap,
meta?: any
);
2
3
4
5
6
处理二进制数据
在默认的情况下,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;
2
3
4
5
6
7
8
9
以上代码中最关键的代码是最后一行module.exports.raw = true;
,如果没有该行代码,则loader只能拿到字符串。
开启loader缓存加速
在某些情况下,有些转换操作需要大量的计算,非常耗时,如果每次构建都重新执行重复的转换操作,则构建将会变得非常缓慢。因此,Webpack会默认缓存所有loader的处理结果,也就是说:在需要被处理的文件或者其依赖的文件没有发生变化时,是不会重新调用对应的Loader去执行转换操作的。
如果我们想让Webpack不缓存该loader的处理结果,可以这样设置:
module.exports = function(source) {
// 关闭loader的缓存功能
this.cacheable(false);
return source;
};
2
3
4
5
缓存条件:loader的结果在相同的输入下有确定的输出。需要注意:有依赖的loader无法使用缓存。
loader如何进行文件输出?
可以通过this.emitFile
进行文件写入。
const loaderUtils = require('loader-utils');
module.exports = function(source) {
console.log('Loader a is excuted!');
const url = loaderUtils.interpolateName(this, '[name].[ext]', source);
console.log(url);
this.emitFile(url, source);
return source;
}
2
3
4
5
6
7
8
9
10
11
其它Loader API
除了以上提到的在Loader
中能调用的Webpack API
,还存在以下常用 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: {...})。
4种本地开发测试loader的方法(加载本地 Loader)
在开发Loader的过程中,为了测试编写的Loader是否能正常工作,需要把它配置到 Webpack中后,才可能会调用该Loader。在前面的章节中,使用的Loader都是通过npm安装的,要使用Loader时会直接使用Loader的名称,代码如下:
module.exports = {
module: {
rules: [
{
test: /\.css/,
use: ['style-loader']
},
]
},
};
2
3
4
5
6
7
8
9
10
如果还采取以上的方法去使用本地开发的Loader将会很麻烦,因为你需要确保编写的 Loader 的源码是在node_modules
目录下。为此你需要先把编写的 Loader发布到Npm仓库后再安装到本地项目使用。
解决以上问题的便捷方法有如下几种:
匹配单个loader,你可以简单通过在rule对象设置path.resolve指向这个本地文件
module.exports = {
//...
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: path.resolve('path/to/loader.js'),
options: {/* ... */}
}
]
}
]
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
resolveLoader.modules
配置
匹配多个loaders,可以使用ResolveLoader
用于配置Webpack
如何寻找Loader
。默认情况下,只会去 node_modules
目录下寻找,为了让Webpack
加载放在本地项目中的Loader
需要修改resolveLoader.modules
。
webpack
将会从这些目录中搜索这些loaders
。假如本地的 Loader 在项目目录中的./loaders/loader-name
中,则需要如下配置:
module.exports = {
//...
resolveLoader: {
// 去哪些目录下寻找 Loader,有先后顺序之分
modules: [
'node_modules',
path.resolve(__dirname, 'loaders')
]
}
};
2
3
4
5
6
7
8
9
10
加上上述配置后,Webpack会先去node_modules
项目下寻找Loader
,如果找不到,会再去./loaders/
目录下寻找。
resolveLoader.alias
resolveLoader: {
// 给loader配置别名
alias: {
'my-loader': path.resolve(__dirname, 'loaders', 'my-loader')
}
// 默认去node_modules目录下找对应的loader,找不到了再去loaders目录下寻找
// 推荐使用
// modules: ['node_modules', path.resolve(__dirname, 'loaders')]
}
2
3
4
5
6
7
8
9
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_moduels
下,其中的loader-name
是指在第1步中的package.json
文件中配置的模块名称。
链接好Loader
到项目后你就可以像使用一个真正的Npm模块一样使用本地的Loader了。
loader分类
- 前置(pre)loader
- 普通(normal)loader
- 后置(post)loader
- 行内(inline)loader
配置多个loader
数组形式
rules: [
{
test: /\.js$/,
loader: ['my-loader3', 'my-loader2', 'my-loader']
}
2
3
4
5
6
对象形式(可以配置loader参数)
rules: [
{
test: /\.js$/,
use: {
loader: 'my-loader'
}
}, {
test: /\.js$/,
use: {
loader: 'my-loader2'
}
}, {
test: /\.js$/,
use: {
loader: 'my-loader3'
}
}
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
多个loader执行顺序
总结:从上面的两个例子中可以看出,多个loader执行顺序是:多个loader是串行执行的,从右到左,从下到上。
配置参数改变loader执行顺序
rules: [
{
test: /\.js$/,
use: {
loader: 'my-loader'
},
enforce: 'pre' // 指定my-loader最先使用
}, {
test: /\.js$/,
use: {
loader: 'my-loader2' // 默认为normal
}
}, {
test: /\.js$/,
use: {
loader: 'my-loader3'
},
enforce: 'post' // 最后使用
}
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
需要注意:loader带参数执行的顺序: pre -> normal -> inline -> post,inline为行内loader。
inline-loader(行内loader)
// ./前面的!表示将当前引入的文件交给行内loader处理
const a = require('inline-loader!./a'); // 不加限制,所有loader都调用来处理a.js
2
npx webpack
// 输出
my-loader1
my-loader2
my-loader3
my-loader1 // pre
my-loader2 // normal
inline-loader
my-loader3 // post
2
3
4
5
6
7
8
9
!禁用normal-loader
const a = require('!inline-loader!./a'); // !禁用normal-loader,只使用pre-loader、post-loader和inline-loader处理处理a.js
npx webpack
// 输出
my-loader1
my-loader2
my-loader3
my-loader1 // pre
inline-loader
my-loader3 // post
2
3
4
5
6
7
8
-!禁用pre-loader和normal-loader
const a = require('-!inline-loader!./a'); // -!禁用pre-loader、normal-loader,只使用post-loader和inline-loader处理处理a.js
npx webpack
// 输出
my-loader1
my-loader2
my-loader3
inline-loader
my-loader3 // post
2
3
4
5
6
7
!!禁用pre-loader、normal-loader、post-loader,只能行内处理
const a = require('!!inline-loader!./a'); // !!禁用pre-loader、normal-loader、post-loader,只使用inline-loader处理处理a.js
npx webpack
// 输出
my-loader1
my-loader2
my-loader3
inline-loader
2
3
4
5
6
loader组成
loader默认由pitch和normal两部分组成。
每个loader模块都支持一个.pitch
属性,上面的方法会优先于loader的实际方法执行。实际上,webpack官方也给出了pitch与loader本身方法的执行顺序图:
|- a-loader `pitch`
|- b-loader `pitch`
|- c-loader `pitch`
|- requested module is picked up as a dependency
|- c-loader normal execution
|- b-loader normal execution
|- a-loader normal execution
2
3
4
5
6
7
pitch和normal的执行顺序正好相反,当pitch没有定义或者没有返回值时,会先依次执行再获取资源执行loader。如果定义的某个pitch有返回值,则会跳过读取资源和自己的loader。
所有loader均无返回值
use: ['my-loader3', 'my-loader2', 'my-loader']
如果'my-loader3', 'my-loader2', 'my-loader'3个loader均无返回值,那么先执行pitch
方法,从左到右,再获取资源。
所有的pitchLoader都没有返回值。
pitch loader3 → loader2 → loader1
↘
资源
↙
normal loader3 ← loader2 ← loader1
2
3
4
5
我是loader3 的pitch
我是loader2 的pitch
我是loader1 的pitch
my-loader1
my-loader2
my-loader3
2
3
4
5
6
有pitchLoader存在返回值
有返回值:直接跳过后续所有的loader包括自己的,跳到之前的loader,可用于阻断loader。
const loader = source => {
// 当前loader仅仅在源码后面追加了一段文字
console.log('my-loader2');
return source + '我是手写loader处理过的';
}
loader.pitch = () => {
return '我有返回值-阻断了';
}
module.exports = loader;
2
3
4
5
6
7
8
9
10
11
输出:
my-loader3
loader-runner
定义:loader-runner 允许你在不安装 webpack 的情况下运行 loaders。
作用:
- 作为 webpack 的依赖,webpack 中使用它执行 loader
- 进行 loader 的开发和调试
实战
首先在项目目录下新建loaders目录,用来存放自定义的loader。在loaders目录中新建my-webpack-loader
目录,这就是我们的第一个自定义loader,并在该目录下新建index.js和package.json文件。具体内容如下:
index.js:
const loaderUtils = require('loader-utils');
module.exports = function(content) {
// 获取用户配置的options
const options = loaderUtils.getOptions(this);
console.log('***options***', options);
// 该loader的作用就是在原内容前加上如下字符串
return '{test123};' + content;
}
2
3
4
5
6
7
8
9
package.json:
{
"name": "my-webpack-loader",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "liujie",
"license": "ISC"
}
2
3
4
5
6
7
8
9
10
11
12
在webpack.config.js中配置:
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'my-webpack-loader',
options: {
test: 'wangwu'
}
}
}
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
执行npm run dev
,结果如下:
raw-loader
\u2028
:行分隔符(行结束符)\u2029
:段落分隔符(行结束符)
\u2028
为行分隔符,会被浏览器解析为换行,而在Javascript的字符串表达式中是不允许换行的,从而导致错误。
把特殊字符转义替换即可,代码如下所示:
str = str.replace(/\u2028/g, '\\u2028');
// raw-loader作用:将文件转化为字符串
const loaderUtils = require('loader-utils');
const fs = require('fs');
const path = require('path');
module.exports = function(source) {
const {name} = loaderUtils.getOptions(this); // 获取loader参数
const url = loaderUtils.interpolateName(this, '[name].[ext]', {
source
}); // 获取文件名
this.emitFile(path.join(__dirname, url), JSON.stringify(source).replace('foo', ''));
// 关闭loader缓存
// this.cacheable(false);
// const callback = this.async();
// console.log('name', name);
// \u2028 行分隔符 \u2029 段落分隔符
const json = JSON.stringify(source)
.replace('foo', '')
.replace(/\u2028/g, '\\u2028') // 为了安全起见,处理ES6模板字符串的问题
.replace(/\u2029/g, '\\u2029'); // ES6模板字符串替换
// 异步loader模拟
// fs.readFile(path.join(__dirname, './async.txt'), 'utf-8', (err, data) => {
// if (err) {
// callback(err, ''); // this.callback方式处理错误
// }
// callback(null, data);
// });
// throw new Error('Error'); // throw方式处理错误
return `export default ${json}666`; // loader转换后的结果
// loader返回一个值的话可以采用return的方式
// 如果要返回多个值的话就需要采用this.callback的方式了
// this.callback(null, json, 2, 3, 4);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38