webpack4.0之loader

loader

在默认情况下,webpack只支持.js的加载。如果要处理其他类型的文件模块,需要自行实现loader或者使用第三方实现。
大体来讲, loader 可以说成是一个在 requeire 读取到内容之后执行的一个钩子函数。期间,webpack会把文件内容传给该函数,最后函数还需要将其返回,则否会报错。如下(真实例子看文末):

1
2
3
4
5
function loader(source) {
// to do
return source
}
module.exports = loader

loader 的基本使用

首先新建一个文件夹 loaderdemo. 进入该文件夹初始化,快速创建一个新的工作目录

1
yarn init - y

添加webpack.config.js,并构建基本骨架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let path = require("path");

module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
filename: "bundle.[hash:8].js",
path: path.resolve(__dirname, "dist"),
},
module: {

},
plugins: []
}

现在开始添加一个简单的loader规则,比如说我们想把代码里的es6新语法转换成es5。只需要在module配置项下添加rules规则,这样子webpack在处理模块时如果命中了这项规则的正则表示式,那么就会使用use数据里的loader模块去处理。

1
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
let path = require("path");

module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
filename: "bundle.[hash:8].js",
path: path.resolve(__dirname, "dist"),
},
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
}
}
]
}
]
},
plugins: []
}

有时候可能需要对一个文件使用多个loader来处理。比如less,需要转换成css代码,css又需要处理自身的文件依赖,最后还要将代码导出到html。这个过程一共需要使用3个loader,分别是 [ 'style-loader','css-loader','less-loader'],并且它们是有执行顺序要求的!less-loader => css-loader => style-loader。loader的执行顺序是数组从后往前开始执行。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let path = require("path");

module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
filename: "bundle.[hash:8].js",
path: path.resolve(__dirname, "dist"),
},
module: {
rules: [
{
test: /\.css$/,
use: [
"style-loader",
"css-loader",
"postcss-loader"
]
}
]
},
plugins: []
}

也可以使用另一种写法,这种写法默认情况下也是从下向上的执行顺序执行。并且这种写法还支持使用enforceloader进行一个分类来控制执行顺序,不同分类顺序 pre > normal > inline > post,默认为 normal

1
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
let path = require("path");

module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
filename: "bundle.[hash:8].js",
path: path.resolve(__dirname, "dist")
},
module: {
rules: [
{
test: /\.css$/,
use: ['style'],
enforce: "post"
},
{
test: /\.css$/,
use: ["css-loader"]
},
{
test: /\.css$/,
use: ["less-loader"],
enforce: "pre"
}
]
},
plugins: []
}

自己实现一个loader

loader 本质是一个在 requeire 读取到内容之后执行的一个钩子函数,所以只需要在这个钩子函数里实现我们的功能就行了。拿我们最初的例子babel-loader为例,实现一个简化版的babel-loader
首先分割一下任务, babel-loader做了什么事情:

  1. es6代码转换成es5语法
  2. 返回转换好的es5代码

准备测试用代码 index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let a = () => {
console.log('hello i,m a')
}

class Lee{
constructor(){
this.name = 'lee'
}
info() {
console.log(this.name)
}
}

let l = new Lee()
l.info()

我们需要借助一些第三方库帮我们完成代码的转换,安装第三方库@babel/core

1
yarn add @babel/core -D

babel-loader还有一些配置项,同样需要一个第三方库来帮我们读取配置

1
yarn add loader-utils -D

对于用户输入的配置,还需要做一个验证

1
yarn add schema-utils -D

准备工作完成就开始直接撸代码。首先配置 webpack.config.js, resolveLoader配置项告诉webpack应该去哪里找寻loader模块,它会依次去数组里的路径下找寻loader,这在调试时很有用。

1
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
let path = require("path");

module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
filename: "bundle.[hash:8].js",
path: path.resolve(__dirname, "dist"),
},
resolveLoader: {
modules: ["node_modules", path.resolve(__dirname, "src", "loaders")]
},
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
}
}
]
}
]
},
plugins: []
}

然后是 babel-loader.js的代码

1
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
let babel = require('@babel/core')
let loaderUtils = require('loader-utils')
let validateOptions = require('schema-utils')

function loader (source) {
// 获取用户配置 { presets: ["@babel/preset-env"]}
let options = loaderUtils.getOptions(this)
//验证用户配置项是否正确
// 如果不正确validateOptions会抛出一个异常,结束loader和该模块后续的loader
let schema = {
type: 'object',
properties: {
presets: {
type: 'Array'
}
}
}
validateOptions(schema, options, 'banner-loader')

// 将该Loader设置成异步loader
// 异步Loader只能通过cb(err, source) 返回结果。
let cb = this.async()
// 将es6代码转换成es5代码
babel.transform(source, {
...options,
sourceMap: true,
filename: this.resourcePath.split('/').pop(),
}, function(err, result) {
// 转换完成的代码需要返回给下一个loader
// 或者调用者
cb(err, result.code, result.map)
})
}

module.exports = loader

最后在 终端中输入

1
npx webpack

成功生成dist目录,成功生成文件并且将代码转换成了es5代码