webpack简介

简介

本质上,webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具。当 webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个 依赖图(dependency graph),然后将你项目中所需的每一个模块组合成一个或多个 bundles,它们均为静态资源,用于展示你的内容。

webpack的核心概念为以下模块,后续会对以下概念针对性输出文章。

  • 入口(entry)

  • 输出(output)

  • loader

  • 插件(plugin)

安装

webpack 对操作系统没有要求,使用 Windows、Mac、Linux 操作系统均可,它唯一的依赖是 Node.js,所以使用 webpack 前需要先安装 Node.js,安装地址为 [Nodejs]](http://nodejs.cn/download/) 推荐安装TLS(长期维护)版本。node 安装完成后,在命令行输入 node -v,可显示版本号即安装成功。
接下来,我们需要使用 Node.js 的包管理器 npm 来安装 webpack ,安装模块的方式有两种:一种是全局安装,一种是本地安装,此处我们选择本地安装,主要有以下两点原因:

  1. 如果采用全局安装,那在与他人协作时,由于每个人的 webpack 版本不同,可能导致打包出的内容不一致
  2. 通常会通过运行一个或多个 npm scripts 以在本地 node_modules 目录中查找安装的 webpack,来运行 webpack,所以在这种情况下可能导致本地全局都有,容易造成混淆
1
2
3
"scripts": {
"build": "webpack"
}

基于以上两点,我们选择在项目内安装,首先新建一个工程目录,并执行 npm 的初始化命令

1
2
mkdir webpack_init && cd webpack_init
npm init

命令行输入项目的基本信息,如:名称、版本、描述、仓库地址等信息。成功后工程目录中会出现一个 package.json 文件。
接下来输入安装webpack的命令:

1
npm install webpack webpack-cli --save-dev

webpack v4+ 版本都需要手动安装 webpack-cli,webpack-cli 提供了许多命令用于在命令行中运行 webpack。具体包含命令可查看文档[webpack-cli]](https://webpack.docschina.org/api/cli/) 检查安装是否成功执行 webpack -v , webpack-cli -v ,显示对应版本则安装成功

打包第一个应用

在 webpack_init 文件夹下添加 index.html、src 文件夹下添加 index.js、hello.js 文件

index.html

1
2
3
4
5
6
7
8
9
<!doctype html>
<html>
<head>
<title>demo</title>
</head>
<body>
<script src="./dist/main.js"></script>
</body>
</html>

src/index.js

1
2
3
4
5
6
7
8
9
import helloWorld from './hello.js';

function component() {
const element = document.createElement('div');
element.innerHTML = helloWorld();
return element;
}

document.body.appendChild(component());

src/hello.js

1
2
3
export default function() {
return 'hello world!!!'
}

在控制台输入打包命令 npx webpack,

1
npx webpack

因为 npm 会从全局模块中找 webpack ,但是我并非全局安装,而是项目本地安装,所以报错。所以需要 npx 命令运行命令。npx 会先找当前目录下的 node_modules/.bin 目录下的二进制可执行文件,如果当前目录下没有 npx 会在 PATH 目录下继续寻找,如果没有会自动下载安装 webpack。此时,第一步手动安装 npm install webpack –save-dev 可省略。执行命令成功后会在项目中增加一个 dist 文件,文件中有一个打包后的 main.js 文件。
2.png

此时我们在浏览器中打开 index.html 文件,在页面中看到 hello world!!!
1.png

在上面的命令中,我们采用了 webpack 的默认设置,我们也可以在命令行中指定入口文件、输出文件、打包模式等信息

1
npx webpack --entry=./src/index.js --output-filename=bundle.js --mode=development

删除 webpack 默认配置打包的内容,重新执行带配置的命令,打包后在 dist 目录下生成了 bundle.js 文件,将 index.html 中引入 js 文件地址 main.js 改为 bundle.js,在浏览器打开 index.html 发现控制台再一次输出了 hello world!!!。

使用 npm scripts

从上面的命令行配置打包例子来看,每次打包都需要在命令行输入冗长的命令,这样做会很耗时和容易出错,为了使命令更加简洁,可以在 package.json 文件中的”scripts”对象下添加一个打包命令脚本 build。
scripts 是 npm 提供的脚本命令功能,所以在这里我们可以直接使用 webpack 命令取代之前的 npx webpack。

1
2
3
"scripts": {
"build": "webpack"
}

在控制台输入npm命令,打包成功
3.png

1
npm run build

使用配置文件

在 webpack4 中,可以无需配置采用默认方式打包,直接在控制台输入 npx webpack 即可打包,然后大多数项目需要很复杂的设置,如项目分包、压缩、懒加载等,所以大多数情况下还是需要一个配置文件,webpack 默认加载的配置文件为 webpack.config.js,也可以指定 webpack 配置文件,需要命令行指定。

webpack.config.js

在项目 webpack_init 中新建一个 webpack.config.js 文件

webpack.config.js

1
2
3
4
5
6
7
8
9
const path = require('path');

module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
};

上面通过 module.exports 导出了一个对象,将之前命令行中输入的 key-value 形式的参数放在这个对象中。
这个对象包含连个属性,entry 为项目入口文件,output 为打包出口文件,其中 filename 为打包文件名称,path 为文件打包路径。通过调用 node.js 的path模块,将 __dirname(node.js内置全局变量,__dirname 为当前文件所在目录的绝对路径)与输出路径连接起来,得到了最终资源输出路径。

现在我们去掉 package.json 中 build 后面的参数去掉,只保留 “build”: “webpack”,然后在控制台输入 npm run build,打包成功了
4.png

使用不同的配置文件

出于某种原因,需要根据特定情况使用不同的配置文件,则可以通过在命令行中使用 –config 标志修改
删除项目中的 webpack-config.js,新建一个 webpack-config-dev.js,内容和 webpack-config.js 一致。在命令行输入npm run build –config webpack.config.dev.js 或者在 package.json 的 scripts 中增加配置

1
2
3
"scripts": {
"build": "webpack --config webpack.config.dev.js"
},

执行打包命令后。打包结果如下
5.png

总结

在上面我们介绍了

  • webpack 的作用及包含核心模块
  • 如何从零安装一个 webpack 运行环境
  • 打包第一个 webpack 项目
  • 告别命令行,通过配置文件打包 webpack

下一节我们将介绍下 webpack 的配置文件,包含的参数及作用。

资源处理流程

借用 webpack 官网对 webpack 的描述: webpack 是一个现代 JavaScript 应用程序的静态模块打包工具。当 webpack 处理应用程序时,它会在内部构建一个 依赖图(dependency graph),此依赖图会映射项目所需的每个模块,并生成一个或多个 bundle

什么是bundle

代码分离是 webpack 的特性之一,使用 entry 配置入口起点,会将代码分成源代码和分发代码
其中源代码是开发编辑的代码,分发代码是经过 webpack 构建,可能经过转义、压缩或优化后的代码,这些代码存放于 bundle 中,可以被浏览器等环境直接运行

什么是dependency graph

6.png

看了上面的内容,其实我们还是不清楚 webpack 到底做了哪些事情使浏览器不支持的语法变得可以执行,而去查看源码,会发现源码中代码对我们不是特别友好,经常左跳右跳无法持续跟踪,所以针对 webpack 打包流程,总结了下面的一个简版打包代码,让我们能大体看清 webpack 做了哪些操作,以下只是简版 demo,没有强行靠近 webpack 打包结果,让我们更能清晰的梳理流程。

打包步骤总结

  1. 我们工程的依赖路径 index.js -> hello.js -> message.js
  2. 根据 webpack.config.js 中定义的 entry 解析入口(index.js)文件,找到他的依赖
  3. 递归的构建依赖关系图
  4. 将所有内容打包到 webpack.config.js 定义的 output 文件

在整理打包内容之前,我们先来看下我们现在项目的结构,项目名称为 webpack_demo,其中包含 webpack.config.js,package.json,dist/main.js,src/index.js,src/hello.js,src/message.js,src/bundler.js,src/complier.js。

webpack.config.js 定义对象,导出项目入口出口文件

1
2
3
4
5
6
7
8
9
const path = require('path')

module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'main.js'
}
}

src/index.js 代码中导入 hello.js方法并执行

1
2
import hello from './hello.js'
console.log(hello())

src/hello.js 代码中导入 message.js中的message参数

1
2
3
4
import { message } from './message.js'
export default function() {
return 'hello ' + message;
}

src/message.js 中定义了一个message变量

1
export const message = 'world!!!'

上面的代码层层引用下来,可以在控制台输出 ‘hello world!!!’,就代表打包成功了。

上面的环境已经定义完成,接下来让我们按照步骤完成打包操作:

  1. 在src/complier.js文件中创建complier构造函数,在构造函数中获取webpack.config.js中定义的入口出口参数
1
2
3
4
5
6
7
8
module.exports = class Complier {
constructor(options) {
const { entry, output } = options
this.entry = entry
console.log(options)
this.output = output
}
}
  1. 在 src/bundler.js 文件中引入 webpack.config.js 文件,创建 Complier 的实例化对象并传入 options
1
2
3
const complier = require('./complier')
const options = require('../webpack.config')
new complier(options)

在命令行执行 node src/bundler.js 后,在控制台会打印出 options 内容
12.png

  1. 拿到配置参数后,开始根据入口文件解析文件内容,解析单流程整体为:
    1. 根据入口文件名称通过 nodejs 提供的方法 fs.readFileSync 读取到文件内容
    2. 使用 @babel/parser 将文件内容转换成ast语法树
    3. ast 语法树中 node 节点中包含了文件依赖的文件名称,使用 @babel/traverse 方法提取出依赖的文件名称并存储到一个数组中
    4. 通过 @babel/core 中的 babel.transformFromAst 方法将 ast 转换成目标浏览器可执行的代码
    5. 将上述获取的参数返回个对象,对象包含文件名,依赖数组,文件可执行代码,这个对象即为一个依赖图谱中的一个节点
    6. 遍历入口文件的依赖数组,由于数组中是文件名,则递归执行上述方法,直到找到所有依赖
    7. 返回所有依赖对象

根据上面总结内容我们在 src/complier.js 中创建一个 createAsset 方法

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
36
37
38
39
40
41
42
43
44
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')
...
// 开始编译,构建ast语法树 filename: ./src/index.js
createAsset(filename) {
// 1
// content 内容即为index.js中书写的内容
const content = fs.readFileSync(filename, 'utf-8')
// 2
// ast 内容为对象,具体内容可以console.log(ast)查看
// https://astexplorer.net/ 在官网输入index.js内容即可看到对应的树
const ast = parser.parse(content, {
sourceType: 'module'
})
// 创建依赖对象
const dependencies = {}
// 3
// 获取抽象语法树中的依赖文件名
traverse(ast, {
ImportDeclaration: ({node}) => {
// 获取文件的路径名如 './src/index.js' dirname='./src'
const dirname = path.dirname(filename)
const absPath = path.join(dirname, node.source.value)
// node.source.value: .hello.js
// absPath: ./src/index.js
dependencies[node.source.value] = absPath
}
})
// 4
// 将ast转换成可执行代码
// https://www.babeljs.cn/docs/babel-core 将index.js内容直接放在官网即可看到转译后代码
const { code } = babel.transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
// 5
return {
filename,
dependencies,
code
}
}

入口文件的依赖关系已经定义好,接下来根据入口文件的 dependencies ,递归遍历出所有子依赖,在 src/complier.js 文件中定义 run 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 拿到参数、执行、分析入口文件
run() {
// 拿到入口文件的依赖
const mainAsset = this.createAsset(this.entry)
const queue = [mainAsset]
// 6
// 遍历对象
for (const asset of queue) {
// 遍历文件的依赖文件,递归创建依赖图
Object.values(asset.dependencies).forEach(filename => {
const child = this.createAsset(filename)
queue.push(child)
})
}
// 7
return queue
}

命令行执行 node src/bundler.js 看下 queue 的内容如下
8.png

  1. 依赖树已经拿到,接下来在 src/bundler.js 中获取 complier 中返回的 queue
1
2
// 获取dependence graph
const graph = new complier(options).run()
  1. 在 src/bundler.js 中创建函数 bundle,解析 graph 树,定义 require 函数,定义 modules,通过 eval 函数执行依赖树中的 code,在此我们可以知道 webpack 重写了 require 函数,所以 babel 中转换的函数可以正常执行
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
function bundle(graph){
// 得到依赖文件名的对象
let modules = {};
graph.forEach(item => {
// 将文件名作为key, value为依赖文件,code为文件名对应的函数
modules[item.filename] = {
dependencies: item.dependencies,
code: item.code
}
})
modules = JSON.stringify(modules)
const result = `(function(graph){
function require(filepath) {
function localRequire(relativePath) {
// 将代码中的require中的路径转换成dependencies存储的带文件夹名的路径
return require(graph[filepath].dependencies[relativePath])
}
var exports = {}
function fn(require, exports, code) {
eval(code)
}
fn(localRequire, exports, graph[filepath].code)
return exports
}
require('${entry}')
})(${modules})`
return result
}

const graph = new complier(options).run()
// 执行bundle函数
const result = bundle(graph)

命令行输出 result 内容,粘贴内容到浏览器控制台并回车执行,发现我们预期的 ‘hello world!!!’ 已经可以正常打印
11.png

  1. 以上我们已经拿到了编译后的代码,最后将它输出到 dist/main.js 中,在 src/bundler.js 中创建方法 createFile(),使用 fs 对象的 writeFileSync 将内容输出,在命令行执行命令后可以看到 src/main.js 中输出了对应内容
1
2
3
function createFile(code) {
fs.writeFileSync(path.join(output.path, output.filename), code)
}
  1. 下面是 src/complier.js和 src/bundler.js文件全部内容

complier.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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 文件操作模块,读取文件内容
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')

module.exports = class Complier {
constructor(options) {
const { entry, output } = options
this.entry = entry
this.output = output
}
// 拿到参数、执行、分析入口文件
run() {
const mainAsset = this.createAsset(this.entry)
const queue = [mainAsset]
for (const asset of queue) {
Object.values(asset.dependencies).forEach(filename => {
const child = this.createAsset(filename)
queue.push(child)
})
}
console.log(queue)
return queue
}
// 开始编译,构建ast语法树 filename: ./src/index.js
createAsset(filename) {
const content = fs.readFileSync(filename, 'utf-8')
const ast = parser.parse(content, {
sourceType: 'module'
})
// 创建依赖
const dependencies = {}
traverse(ast, {
ImportDeclaration: ({node}) => {
// 获取文件的路径名如 './src/index.js' dirname='./src'
const dirname = path.dirname(filename)
const absPath = path.join(dirname, node.source.value)
dependencies[node.source.value] = absPath
}
})
// 将ast转换成代码
// https://www.babeljs.cn/docs/babel-core
const { code } = babel.transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})

return {
filename,
dependencies,
code
}
}
}

bundler.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
36
37
38
39
40
41
42
43
44
45
46
47
// 引入配置
const fs = require('fs');
const path = require('path')
const options = require('../webpack.config')

const complier = require('./complier')

const { entry, output } = options

function bundle(graph){
// 得到以依赖文件名的对象
let modules = {};
graph.forEach(item => {
modules[item.filename] = {
dependencies: item.dependencies,
code: item.code
}
})
modules = JSON.stringify(modules)
const result = `(function(graph){
function require(filepath) {
function localRequire(relativePath) {
// 将代码中的require中的路径转换成dependencies存储的带文件夹名的路径
return require(graph[filepath].dependencies[relativePath])
}
var exports = {}

function fn(require, exports, code) {
eval(code)
}

fn(localRequire, exports, graph[filepath].code)

return exports
}
require('${entry}')
})(${modules})`
return result
}

function createFile(code) {
fs.writeFileSync(path.join(output.path, output.filename), code)
}

const graph = new complier(options).run()
const result = bundle(graph)
createFile(result)

Entry 和 Context

配置入口context和entry

webpack 在构建打包时,通过 context 和 entry 这两个配置来找到打包入口路径。在配置入口时其实做了两件事:

  • 确认入口文件位置,告诉 webpack 从哪个文件开始打包

  • 描述 chunk name。如果传入一个字符串或字符串数组,那么默认的 chunk name 为 “main”,如果传入的是一个对象,则每个属性的 key 会是 chunk 的名称,该属性的值描述了 chunk 的入口点

context

context 可以理解为配置资源入口的基础目录,在配置时要求必须使用绝对路径。如:现有的目录结构入口为 /src/script/index.js,则我们可以通过下面的配置来指定入口目录

1
2
3
4
5
6
7
8
9
const path = require('path');
module.exports = {
context: path.resolve(__dirname, './src/script'),
entry: './index.js'
};
module.exports = {
context: path.resolve(__dirname, './src'),
entry: './script/index.js'
};

这样配置后,命令行执行打包,发现依然成功的找到了入口并顺利打包(我们使用 hello world 的那个 demo 来执行现有配置)
1.png

配置 context 的目的可以使 entry 的写法更加简洁,尤其是在多入口文件的情况下。不过 context 是可以省略的,默认值为当前工程的根目录。

entry

在 webpack 配置中有多种方式定义 entry 属性,如:字符串、数组、对象、函数,接下来我们展示下每种类型如何配置

字符串类型

直接定义入口名称

1
2
3
4
5
6
7
8
9
module.exports = {
entry: './src/script/index.js',
};
// entry 单入口语法,是下面的简写
module.exports = {
entry: {
main: './src/script/index.js',
},
};

数组类型

传入一个数组的作用是将多个文件预先合并,最终将多个依赖的内容绘制在一个 chunk 中,在打包时 webpack 会将数组中的最后一个元素作为实际的入口路径,其余文件会预先构建到入口文件。

1
2
3
module.exports = {
entry: ['lodash', './src/script/index.js'],
};

这种配置和下面是等效的

1
2
3
4
5
6
// index.js
import * from 'lodash'
// webpack.config.js
module.exports = {
entry: './src/script/index.js',
}

这种写法会将 lodash 打包到我们的 bundle.js 中。这种写法类似于在 index.js 中引入 lodash,在控制台执行打包命令我们来看下生成的文件,从下面两张图可以看到在 index 中我们没有引入 lodash,但打包的文件中已经引入了 lodash
2.png

对象类型

如果要定义多入口文件则需要使用对象的形式,通过这种方式可以自定义 chunk name,其中对象的key即为 chunk name,对象的 value 为入口路径。在使用对象描述入口时,我们可以使用以下属性

  • dependOn: 当前入口所依赖的入口。它们必须在该入口被加载前被加载
  • filename: 指定要输出的文件名称
  • import: 启动时需加载的模块
  • library: 指定 library 选项,为当前 entry 构建一个 library
  • runtime: 运行时 chunk 的名字。如果设置了,就会创建一个新的运行时 chunk。在 webpack 5.43.0 之后可将其设为 false 以避免一个新的运行时 chunk
  • publicPath: 当该入口的输出文件在浏览器中被引用时,为它们指定一个公共 URL 地址

多入口配置本质上打包后生成多个js文件

1
2
3
4
5
6
module.exports = {
entry: {
index: ['lodash', './src/script/index.js'],
vendor: './vendor'
}
}

函数类型

使用函数类型定义入口时,只要返回上面介绍的几种形式即可,如

1
2
3
4
5
6
7
8
9
10
11
// 返回字符串
module.exports = {
entry: () => './src/script/index.js'
}
// 返回对象
module.exports = {
entry: () => ({
index: ['lodash', './src/script/index.js'],
vendor: './vendor'
})
}

传入函数的优点是我们可以通过函数中增加一些逻辑来动态改变打包入口

总结

本章我们梳理了 webpack 入口配置的几种方式,包括字符串、对象、数组、函数几种

Entry 配置实例

entry 配置实例

webpack 的 entry 配置在实际的应用中可以分几个场景。

  • 单页应用
  • 多页应用
  • 分离应用程序和第三方库

下面我们来介绍下这几种应用

单页应用

对于单页应用,我们一般来定义单一入口即可

1
2
3
module.exports = {
entry: './src/index.js',
};

通过单一入口打包文件,可以将所有入口文件依赖的 框架、引入的工具库、各个页面内容打包到一起。这样的好处是将所有内容都打包成一个 js 文件,依赖关系清晰。但是这种方式也有个弊端,即所有模块都打到一个包中,当应用规模上升到一定程度后导致打包资源体积过大,导致页面首次加载速度变慢

多页应用

对于多页应用的场景,为了尽可能减少打包资源的体积,我们希望每个页面都只加载各自必要的逻辑,而不是将所有内容都打包到一个 bundle 中,我们来看下多应用的配置

1
2
3
4
5
6
7
8
9
10
const path = require('path');
module.exports = {
entry: {
index: './src/index.js',
hello: './src/hello.js'
},
output: {
path: path.resolve(__dirname, 'dist')
}
};

打包后的文件如下,可以看到打包的内容中包含了 index.js 和 hello.js 两个文件。
1.png

分离应用程序和第三方库

在一个应用中,我们使用的框架、库、第三方依赖等往往很少会有改动,如果我们将所有内容都打包到一个 bundle 文件,一旦业务代码有一点点变更,那用户就要重新下载整个资源,这对于页面的性能是很不友好的。为了解决这个问题,我们可以使用应用程序和第三方库分离的方式来打包文件。也就是将业务代码和不频繁变更的第三方依赖分 bundle 打包,示例如下
webpack.config.js

1
2
3
4
5
6
7
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
vendor: ['lodash']
}
};

index.js

1
import * as _ from 'lodash'

在上面的配置中,index.js 仍然和之前一样不做任何处理,只是我们新添加了一个 chunk name 为 vendor 的入口,并通过数组的形式将第三方依赖添加进去,执行打包命令我们看到输出了两个打包文件。
2.png

其实上面的代码虽然打包成功了,也成功提取了 vender 文件,但是打开打包后的 dist/index.js 我们发现 lodash 还是被打到文件中了,对于这种情况我们可以配合使用 optimization.splitChunks,将 vender 和 index 中的公共代码提取出来,这个方法我们后面的文章在详细介绍。

通过上面的配置,我们可以业务依赖的第三方模块抽取成一个独立的 bundle,由于这个 bundle 不经常变动,因此可以有效的利用客户端缓存,在用户后续请求页面时加快整体渲染速度。

配置参数详解

output 输出配置详解

output 属性告诉 webpack 在哪里输出它所创建的 bundle,以及如何命名这些文件。主要输出文件的默认值是 ./dist/main.js,其他生成文件默认放置在 ./dist 文件夹中。

我们可以通过在配置中指定一个 output 对象,来配置这些处理过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const path = require('path');
module.exports = {
mode: 'development',
entry: {
index: './src/index.js'
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
publicPath: '/assets/',
library: 'DemoLibrary', // 导出名称
libraryTarget: 'window' // 挂载目标
}
};

output 对象中可以包含数十个配置项,其中大部分开发中使用频率不高,我们在本章内容中只介绍几个常用配置,对其他配置感兴趣的同学可以查看官网 output配置

filename

filename 决定了每个输出 bundle 的名称。这些 bundle 将写入到 output.path 选项指定的目录下。
对于单个入口起点,filename 会是一个静态名称。 filename 支持以字符串和函数的形式定义参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 字符串形式
module.exports = {
...
output: {
filename: 'bundle.js',
}
};
// 函数形式
module.exports = {
...
output: {
filename: (pathData) => {
console.log(pathData)
return '[name].js';
}
}
};

字符串形式的 filename,会在输出文件中生成 bundle.js,函数形式的 filename 会在输出文件中生成 index.js (以 chunk name 命名),在控制台输出下 pathData,我们可以看到返回了一个包含 chunk 内容等信息的对象。

2.png

filename 可以不仅仅是 bundle 的名字,还可以使用像 ‘js/[name]/bundle.js’ 这样的文件路径,即便路径中的目录不存在也没关系,webpack 会在输出资源时创建该目录。例子如下:

1
2
3
4
5
6
module.exports = {
...
output: {
filename: 'js/[name]/bundle.js'
}
};

3.png

当通过多个入口起点(entry point)、代码拆分(code splitting)或各种插件(plugin)创建多个 bundle,应该使用以下一种替换方式,来赋予每个 bundle 一个唯一的名称

替换方式 变量名称 功能描述 使用方式 打包结果
入口名称 [name] 如果设置,则为此 chunk 的名称,否则使用 chunk 的 ID filename: ‘[name].bundle.js’ 4.png
chunk id [id] 指代 此 chunk 的 id filename: ‘[id].bundle.js’ 5.png
chunk hash [chunkhash] 此 chunk 的 hash 值,包含该 chunk 的所有元素 filename: ‘[chunkhash].bundle.js’ 6.png
content hash [contenthash] 指代由生成的内容产生的 hash filename: ‘[contenthash].bundle.js’ 7.png

上面的配置除了可以对不同的 bundle 进行名称区分,还能起到一个控制客户端缓存的作用,表中的[chunkhash] 和 [contenthash] 都与文件内容直接相关,在 filename 中使用了这些变量后,当对文件内容做了修改,可以引起 bundle 文件名的修改,从而用户在下一次请求文件资源时会重新加载文件,而不会直接命中缓存资源。

在实际的工程中,我们一般使用较多的是[name],一般与定义的 chunk 一一对应,可读性较高,为了控制客户端缓存,我们一般还加上 [contenthash],如:

1
2
3
4
5
6
module.exports = {
...
output: {
filename: '[name]-[contenthash].js'
}
};

打包结果如下
8.png

path

path 可以指定资源输出位置,要求必须使用绝对路径,如

1
2
3
4
5
6
7
8
9
10
const path = require('path');
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
},
output: {
path: path.resolve(__dirname, 'dist')
}
};

上述配置将工程的dist目录设置为资源的输出目录,在 webpack 4 之后,output.path 已经默认为 dist 目录,除非我们需要修改他,否则可以不用单独配置。

publicPath

publicPath 从功能上来说,用于指定资源的请求位置。页面中的资源分为两种,一种是由 HTML 页面直接请求的,比如通过 script 标签加载 js,通过 link 标签加载 css。另一种是由 js 或 css 请求的,如加载图片字体文件等。 publicPath 就用来指定第二种间接资源的请求位置。如果指定了一个错误的值,则在加载这些资源时会收到 404 错误。

publicPath 有以下三种形式

  1. 相对于 HTML

  2. 相对于 HOST

  3. 相对于 CDN

相对于 HTML

在请求资源时,会以当前 html 页面所在路径加上 publicPath 的相对路径来构成实际请求的 URL,如

1
2
3
4
5
6
7
8
9
10
// 假设当前 html 页面地址为 http://demo.com/webpack/index.html
// 需要请求文件名为 demo.png
module.exports = {
...
output: {
publicPath: '' // 实际请求路径 http://demo.com/webpack/demo.png
publicPath: './css' // 实际请求路径 http://demo.com/webpack/css/demo.png
publicPath: '../assets/' // 实际请求路径 http://demo.com/assets/demo.png
}
};

相对于 HOST

若 publicPath 的值以 “/” 开始,则代表此时 publicPath 是以当前页面的域名加上 publicPath 的相对路径来构成实际请求的 URL,如

1
2
3
4
5
6
7
8
9
10
// 假设当前 html 页面地址为 http://demo.com/webpack/index.html
// 需要请求文件名为 demo.png
module.exports = {
...
output: {
publicPath: '/' // 实际请求路径 http://demo.com/demo.png
publicPath: '/css' // 实际请求路径 http://demo.com/css/demo.png
publicPath: '../assets/' // 实际请求路径 http://demo.com/assets/demo.png
}
};

相对于 CDN

上面两种配置都是相对路径,我们也可以使用绝对路径的形式配置 publicPath,这种情况一般发生在将静态资源放在 CDN 上面,如

1
2
3
4
5
6
7
8
9
10
// 假设当前 html 页面地址为 http://demo.com/webpack/index.html
// 需要请求文件名为 demo.png
module.exports = {
...
output: {
publicPath: 'http://cdn.example.com/assets/' // 实际请求路径 http://cdn.example.com/assets/demo.png
publicPath: 'https://cdn.example.com/assets/' // 实际请求路径 https://cdn.example.com/assets/demo.png
publicPath: '//cdn.example.com/assets/' // 实际请求路径 //cdn.example.com/assets/demo.png
}
};

webpack-dev-server 也会默认从 publicPath 为基准,使用它来决定在哪个目录下启用服务,来访问 webpack 输出的文件。

library

library 的作用是将打包的内容生成一个库,可以供其他工程加载使用。这一点在目前流行的微前端架构实战上面很有用,如子应用通过输出类库的形式将内容输出到一个对象上,这样主应用就可以通过加载 js 的方式去引入子应用,并且可以通过子应用输出的对象名称来加载子应用的内容。library 具体的使用方法,我们来看下面的例子:

webpack.config.js

1
2
3
4
5
6
7
module.exports = {
...
entry: './src/index.js',
output: {
library: 'DemoLibrary'
}
};

src/index.js 的入口中导出了如下函数

1
2
3
export function hello(webpack) {
console.log(`hello ${webpack}`);
}

此时,变量 DemoLibrary 将与入口文件所导出的文件进行绑定,下面是如何使用打包生成的index.js文件:
index.html

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
<title>测试DemoLibrary库</title>
</head>
<body>
<script src='./dist/index.js'></script>
<script>
DemoLibrary.hello('webpack');
</script>
</body>
</html>

在浏览器中可以看到成功输出 hello webpack。

9.png

library 的类型可以为字符串、数组、和对象,字符串的参数类型则直接指代库的名称,与对象中设置 name 属性作用相同。如果 entry 入口设置为 object,所有入口都可以通过 library 的 array 语法暴露:

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
// …
entry: {
a: './src/a.js',
b: './src/b.js',
},
output: {
filename: '[name].js',
library: ['DemoLibrary', '[name]'], // [name] 为 chunk name
},
};

假设 a.js 与 b.js 导出名为 hello 的函数,下面就是如何使用这些库的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html lang="en">
<head>
<title>测试DemoLibrary库</title>
</head>
<body>
<script src='./dist/a.js'></script>
<script src='./dist/b.js'></script>
<script>
DemoLibrary.a.hello('webpack');
DemoLibrary.b.hello('webpack');
</script>
</body>
</html>

请注意,如果你打算在每个入口点配置 library 配置项的话,以上配置将不能按照预期执行。这里是如何 在每个入口点下 做的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module.exports = {
// …
entry: {
main: {
import: './src/index.js',
library: {
// `output.library` 下的所有配置项可以在这里使用
name: 'MyLibrary',
type: 'umd',
umdNamedDefine: true,
},
},
another: {
import: './src/another.js',
library: {
name: 'AnotherLibrary',
type: 'commonjs2',
},
},
},
};

library 包含以下可配置参数

参数名称 功能描述 支持类型
name 指定库的名称 字符串、数组、对象
type 配置将库暴露的方式 字符串
export 指定哪一个导出应该被暴露为一个库 字符串、数组
auxiliaryComment 在 UMD 包装器中添加注释 字符串、对象
umdNamedDefine 将 AMD 模块命名为 UMD 构建 布尔

这里我们说下 type 类型,在实际的使用中,我们可能根据工程运行环境的需要,而需要将类库暴露为不同的类型,如 支持 esModule、amd、cmd、umd 等,type 配置就可以帮我们完成不同输出方式。

type 类型默认包括 ‘var’、’module’、’assign’、’assign-properties’、’this’、’window’、’self’、’global’、’commonjs’、’commonjs2’、’commonjs-module’、’commonjs-static’、’amd’、’amd-require’、’umd’、’umd2’、’jsonp’ 以及 ‘system’,除此之外也可以通过插件添加。官方文档对每种类型给了详细说明和事例,具体我们可查看官方文档,output.target.type 配置

总结

以上为我们在实际开发中使用的 output 配置,包含 path、filename、publicPath、library,日常使用中可能还会用到 libraryTarget ,不过 webpack 未来会放弃对 output.libraryTarget 的支持,所以可以使用 output.library.type 替代 output.libraryTarget。

输出配置实例

output 输出配置实例

到目前为止,我们都是在 index.html 文件中手动引入打包生成的资源,然而随着应用程序增长,并且一旦开始在文件名中使用 hash 并输出 多个 bundle,如果继续手动管理 index.html 文件,就会变得困难起来。然而,通过一些插件可以使这个过程更容易管控。HtmlWebpackPlugin 可以帮我们解决这个问题。

设置 HtmlWebpackPlugin

继续使用之前的工程文件,目录结构为:

1.png

首先安装插件,并且调整 webpack.config.js 文件:
安装 html-webpack-plugin 插件

1
npm install --save-dev html-webpack-plugin

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
hello: './src/hello.js'
},
output: {
filename: '[name].js'
},
plugins: [
new HtmlWebpackPlugin({
title: '管理输出',
}),
],
};

执行构建命令 npm run build,我们看下打包后的结果,我们可以看到,打包文件包含两个入口文件对应的 js 文件,还包含一个 index.html 文件

2.png

在 dist 目录下我们看下打包的 index.html 文件,我们可以看到 HtmlWebpackPlugin 创建了一个全新的 index.html 文件,所有的 bundle 会自动添加到 html 中。

3.png

清理 /dist 文件夹

你可能已经注意到,由于遗留了之前指南中的代码示例,我们的 /dist 文件夹显得相当杂乱。webpack 将生成文件并放置在 /dist 文件夹中,但是它不会追踪哪些文件是实际在项目中用到的。

通常比较推荐的做法是,在每次构建前清理 /dist 文件夹,这样 /dist 文件夹中只有最近一次生成的文件。让我们使用 output.clean( webpack 5.20.0 及以上版本支持)配置项实现这个需求。

webpack.config.js

1
2
3
4
5
6
module.exports = {
...
output: {
clean: true
}
};

现在,执行 npm run build,检查 /dist 文件夹。如果一切顺利,现在只会看到构建后生成的文件,而没有旧文件!

总结

本章我们介绍了一个优化开发效率的插件和一个配置项,使用 HtmlWebpackPlugin 插件可以动态的生成 index.html 文件,以及动态的向 index.html 文件插入 bundle。了解了如何在编译时清空 dist 文件内容。

核心概念-Loader

概述

loader 概述

到目前为止,我们的案例都是都是介绍的如何打包 js 文件,对于工程中的其他类型资源,如 CSS、图片、字体等, webpack 会如何处理呢?在实际的项目开发中,我们经常会用到 Sass 或者 Less 来编写样式,我们使用 Typescript 增加静态类型检查,我们使用浏览器不支持的 ECMAScript 新特性,如何让 webpack 来对所有的编译进行统一管理呢?

本章我们会介绍 loader(预处理器),它赋予了 webpack 可以处理不同资源的能力,极大丰富了其可扩展性。

loader 作用

在 webpack 中,一切皆模块,我们可以使用 import、require 等方式 在JavaScript 模块中导入 JS、CSS、图片、字体等多种类型的静态资源,loader 用于对模块的源代码进行转换。loader 可以使我们在导入模块时预处理文件。loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript。loader 甚至允许我们直接在 JavaScript 模块中 import CSS 文件!

loader 本质上是 node module 导出的一个函数,当资源需要被转换时,调用这个函数。下面我们通过自定义 loader,来看 loader 的使用方法。

src/index.js

1
2
const demoName = 'webpack loader'
console.log(demoName)

webpack 默认支持解析 js 文件,我们增加解析 js 的 loader 只为展示 loader 是如何工作的

src/js-loader.js

1
2
3
4
module.exports = function (source) {
console.log(source)
return `module.exports=${JSON.stringify(source)}`
}

webpack.config.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
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: {
index: './src/index.js'
},
output: {
filename: '[name].js',
clean: true
},
plugins: [
new HtmlWebpackPlugin({
title: 'loader',
}),
],
module: {
rules: [
{
test: /\.js$/,
use: "./src/js-loader.js"
}
]
}
};

执行构建命令,在控制台可以看到 js-loader 文件的 console.log 输出了 index.js 文件内容,从上面的 简易版 js-loader.js 文件中可以看出,loader 本身就是一个函数,在该函数中对接收的内容进行转换,然后返回转换后的结果。

1.png

loader 使用方式配置

在我们的应用中,有两种使用 loader 的方式,分别是 配置方式(推荐),内联方式。

配置方式(推荐)

module.rules 允许我们在 webpack 配置中指定多个 loader。 这种方式是展示 loader 的一种简明方式,并且有助于使代码变得简洁和易于维护。同时让我们对各个 loader 有个全局概览:

loader 从右到左(或从下到上)地取值(evaluate)/执行(execute)。在下面的示例中,从 css-loader 开始执行,最后以 style-loader 为结束。

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
...
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader','css-loader']
}
]
}
};

src/index.js

1
import './index.css';

src/index.css

1
2
3
4
5
body {
color: red;
padding: 20px;
text-align: center;
}

index.html

1
2
3
4
5
...
<body>
<div>import css</div>
<script src='./dist/index.js'></script>
</body>

webpack 无法处理 CSS 语法,此时我们执行打包命令 控制台会报 “请使用合适的loader来处理这个文件类型”

2.png

下面我们将 css-loader,style-loader 加到工程中,loader 都是一些第三方 npm 模块,webpack 本身不包含任何 loader, 所以使用前我们需要先安装这些 loader,在工程中执行以下命令安装。

1
npm install css-loader style-loader

安装成功后,在控制台执行打包命令,我们可以看到错误已经消失了,在浏览器打开index.html 文件我们可以看到,样式正常展示。

3.png

内联方式

loader 除了使用配置的方式,还有一种内联的用法,可以在 import 语句或任何 与 “import” 方法同等的引用方式 中指定 loader。使用 ! 将资源中的 loader 分开。每个部分都会相对于当前目录解析。

在上面的例子中,我们注释掉 webpack.config.js 中 module.rules 的配置,将引入方式改为内联方式。

src/index.js

1
import '!style-loader!css-loader!./index.css';

执行打包命令后,我们在浏览器中打开 index.html 文件,可以看到样式正常显示。我们在工程中尽可能使用 module.rules,因为这样可以减少源码中的代码量,并且可以在出错时,更快地调试和定位 loader 中的问题。

loader 特性

  • loader 支持链式调用。链中的每个 loader 会将转换应用在已处理过的资源上。一组链式的 loader 将按照相反的顺序执行。链中的第一个 loader 将其结果(也就是应用过转换后的资源)传递给下一个 loader,依此类推。最后,链中的最后一个 loader,返回 webpack 所期望的 JavaScript。
  • loader 可以是同步的,也可以是异步的。
  • loader 运行在 Node.js 中,并且能够执行任何操作。
  • loader 可以通过 options 对象配置(仍然支持使用 query 参数来设置选项,但是这种方式已被废弃)。
  • 除了常见的通过 package.json 的 main 来将一个 npm 模块导出为 loader,还可以在 module.rules 中使用 loader 字段直接引用一个模块。
  • 插件(plugin)可以为 loader 带来更多特性。
  • loader 能够产生额外的任意文件。

HTML-Loader

html-loader

html-loader 用于将 html 文件转换为字符串,支持压缩、导出、对内容预处理。下面让我们来看一个例子。

使用

index.html

1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<html lang="en">
<head>
<title>html-loader</title>
</head>
<body>
<img src="./src/assets/card-mark.png" />
</body>
</html>

webpack.config.js

1
2
3
4
5
6
7
8
9
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
...
plugins: [
new HtmlWebpackPlugin({
// 使用 index.html 内容作为输出模版
template: "./index.html",
}),
],

在index.html 中我们使用 img 标签展示一张图片,图片为相对路径,在 src/assets 文件夹下增加名为 card-mark.png 的图片,在浏览器直接打开 index.html 我们可以看到,图片可以正常展示。

1.png

此时我们在控制台执行打包命令 npm run build,在 dist 文件夹中 输出了 index.html 文件,我们直接在浏览器打开 index.html 文件,此时图片无法打开。

2.png

图片打不开的原因为 src 地址使用了相对路径,dist 文件夹与 src 文件夹同目录,所以 src=”./src/assets/card-mark.png” 这个地址无法找到图片,导致图片无法展示。此时我们就可以借助 html-loader 来帮我们解决这个问题。

安装 html-loader

1
npm install html-loader -D

安装成功后,将 html-loader 配置到 webpack.config.js 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
...
plugins: [
new HtmlWebpackPlugin({
template: "./index.html",
}),
],
module: {
rules: [
{
test: /\.html$/,
use: {
loader: 'html-loader',
}
}
]
}
};

此时在执行打包命令,可以看到在 dist 文件夹下除了index.html 文件和 index.js 文件,又多了一个 扩展名为 .png 的文件,我们打开 dist 文件夹下的 index.html 发现 src 的引用地址已修改,此时在浏览器中直接打开 dist 文件夹下的 index.html 发现图片可以正常显示。

3.png

配置项

html-loader 包含下面四个配置项

参数名称 支持类型 默认值 功能描述
sources {Boolean、Object} true 启用/禁用 sources 处理
preprocessor {Function} undefined 允许在处理前对内容进行预处理
minimize {Boolean、Object} 在生产模式下为 true,其他情况为 false 通知 html-loader 压缩 HTML
esModule {Boolean} true 启用/禁用 ES modules 语法

sources

sources 默认值为 true

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = {
module: {
rules: [
{
test: /\.html$/,
loader: "html-loader",
options: {
// 设置为 false 则不会对可加载属性做任何处理
sources: false,
},
},
],
},
};

默认情况下,每个可加载属性(例如 img 图片导入)都将被导入( const img = require (‘./image.png’) 或 import img from “./image.png” )。 你可能需要为配置中的图片指定 loader(我们前面的例子,如果配置 html-loader 的参数 esModule: false,则需要使用 loader 对图片进行处理,否则打包报错)。

html-loader 支持处理的 可加载属性 包括:

source 设置为 Object

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module.exports = {
module: {
rules: [
{
test: /\.html$/,
loader: "html-loader",
options: {
sources: {
list: [
{
tag: "img",
attribute: "data-src",
type: "src",
}
]
},
},
},
],
},
};

index.html

1
2
3
<body>
<img data-src="./src/assets/card-mark.png" src="./src/assets/card-mark.png" />
</body>

执行打包命令,看下 dist 文件夹下的 index.html 文件,发现 html-loader 只对 img 标签的 data-src 属性做了转换。

4.png

sources 对象中 支持 list 和 urlFilter 属性,详情可查看文档

preprocessor

允许在处理之前对内容进行预处理。

webpack.config.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
const Handlebars = require("handlebars");
module.exports = {
module: {
rules: [
{
test: /\.html$/,
use: {
loader: 'html-loader',
options: {
preprocessor: (content, loaderContext) => {
let result;

try {
result = Handlebars.compile(content)({
firstname: "Value",
lastname: "OtherValue",
});
} catch (error) {
loaderContext.emitError(error);

return content;
}
return result;
}
}
}
},
},
],
},
};

index.html

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<title>html-loader</title>
</head>
<body>
<p>{{firstname}} {{lastname}}</p>
<img src="./src/assets/card-mark.png" />
</body>
</html>

在 webpack.config.js 中我们使用了 handlebars ,需要先安装 handlebars。

Handlebars 是一种简单的模板语言。
它使用模板和输入对象来生成 HTML 或其他文本格式。Handlebars 模板看起来像带有嵌入式 Handlebars 表达式的常规文本。

1
<p>{{firstname}} {{lastname}}</p> 

执行打包命令后,查看 dist 文件夹下的 index.html 文件,可以看到 p 标签内容已被替换。

5.png

minimize

告诉 html-loader 编译时需要压缩 HTML 字符串。
默认情况下,启用压缩的规则如下:

1
2
3
4
5
6
7
8
9
10
11
12
({
caseSensitive: true,
collapseWhitespace: true,
conservativeCollapse: true,
keepClosingSlash: true,
minifyCSS: true,
minifyJS: true,
removeComments: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
});

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module.exports = {
module: {
rules: [
{
test: /\.html$/i,
loader: "html-loader",
options: {
// boolean
minimize: true,
// 对象
minimize: {
removeComments: false,
collapseWhitespace: false,
},
},
},
],
},
};

esModule

默认情况下, html-loader 生成使用 ES modules 语法的 JS 模块。 在某些情况下,使用 ES modules 会更好,例如在进行模块合并和 tree shaking 时。

你可以使用以下方法启用 CommonJS 模块语法:
webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
module: {
rules: [
{
test: /\.html$/i,
loader: "html-loader",
options: {
esModule: false,
},
},
],
},
};

总结

本节我们介绍了 html-loader 的使用方法和 html-loader 包含的 4 个参数 sources、preprocessor、minimize、esModule, 它们分别对应 html-loader 在项目实践中的 4 个重要功能:

  1. 将 HTML 中标签的可加载属性引入的文件作为模块导入
  2. 预处理 HTML,常用来支持模板引擎
  3. 压缩 HTML
  4. 默认导出 ES modules 便于模块合并和 tree shaking

URL-Loader

url-loader

当我们在文件中加载图片、字体等资源时,webpack 无法直接处理以上资源,在 webpack 5 之前需要使用相应 loader 来处理资源文件,url-loader 可以将一个文件转换为 base64 编码来代替访问地址,这样做的好处是可以减少一次网络请求,下面我们来看看如何使用 url-loader 及 url-loader 有哪些常用配置。

使用

index.html

1
2
3
4
5
6
7
8
<!DOCTYPE html>
<html lang="en">
<head>
<title>url-loader</title>
</head>
<body>
</body>
</html>

index.js

index.js 文件中引入一张图片,创建image标签后,将导入的图片赋值给 image 标签的 src 属性,将 image 标签添加到页面中

1
2
3
4
5
6
7
8
9
import Back from './img/back.png';

function component() {
var element = document.createElement('img');
element.src=Back
return element;
}

document.body.appendChild(component());

安装 url-loader

1
npm install url-loader -D

安装成功后,将 url-loader 配置到 webpack.config.js 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = {
...
module: {
rules: [
{
test: /\.(png|jpg)$/,
use: [
{
loader: 'url-loader'
}
]
}
]
}
};

此时在执行打包命令,此时在浏览器中直接打开 dist 文件夹下的 index.html 发现页面中展示一张图片。

配置项

url-loader 包含下面 3 个配置项

参数名称 支持类型 默认值 功能描述
limit Boolean、Number、String true 需要转换为 base64 的资源大小限制
mimetype Boolean、String 基于mime-types 查找 为文件指定MIME类型
fallback String file-loader 文件大小等于或者超过 limit 限制后使用的资源处理 loader

limit

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module.exports = {
module: {
rules: [
{
test: /\.(png|jpg)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 2048 // 2Kb
}
}
]
}
]
}
};

一般情况下,当资源文件大小小于 2Kb 时,我们需要将资源路径转换为 base64 的格式将资源打包到 bundle 中,这样可以减少一次网络请求,当一个页面中引入多个资源文件时可以明显减少请求次数,但这种方式带来了另外一个问题,如果资源文件体积较大,就会导致 bundle 的体积增大,体积大的情况下网络请求时间变长,会导致页面白屏时间变长,非常影响用户体验。所以在处理资源文件时,一般会加上 limit 配置,文件资源体积超过配置的大小后,更改资源文件的处理方式,默认使用 file-loader 来处理。

上面的配置在执行打包命令时会报 “Cannot find module ‘file-loader’” 的错误,所以在使用 limit 配置时,我们先下载安装 file-loader。

安装 file-loader

1
npm install file-loader -D

再次执行打包命令,在 dist 文件夹下输出了一个扩展名为 .png 的图片,让我们来对比下增加 limit 配置前和增加 limit 配置后 dist 文件夹和 index.js 文件的变化。

增加 limit 配置前

dist 文件夹

3.png

index.js

4.png

增加 limit 配置后

dist 文件夹

5.png
index.js

6.png

mimetype

设置文件的转换类型。如果未指定,将使用文件扩展名来查找MIME 类型。

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
rules: [
{
test: /\.(png|jpg)$/,
use: [
{
loader: 'url-loader',
options: {
mimetype: 'image/jpg'
}
}
]
}
]

fallback

指定当目标文件的大小等于或超过限制选项中设置的限制时,使用的替代加载 loader,默认为 file-loader。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module: {
rules: [
{
test: /\.(png|jpg)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 2048,
fallback: 'responsive-loader'
}
}
]
}
]
}

总结

本节我们介绍了 url-loader 的使用方法和 url-loader 包含的 3 个参数 limit、minitype、fallback, url-loader 设置了 limit 参数后,超过设置的限制大小后,默认使用 file-loader 加载资源文件,所以 file-loader 的可配置参数在 url-loader 中也可配置生效,剩余可配置参数在 file-loader 中继续总结。

File-Loader

file-loader

在 webpack 5 之前处理图片、字体等资源,除了使用 url-loader 之外还经常使用 file-loader,file-loader 的处理方式和 url-loader 有些不同,url-loader 通过 limit 参数判断如果没有超过配置大小,则将文件转做 base64 编码,直接嵌入到 CSS/JS/HTML 代码中。而 file-loader 并不会对文件内容进行任何转换,只是复制一份文件内容,并根据配置为他生成一个唯一的文件名, 下面让我们梳理下 file-loader 如何使用及有哪些可配置参数。我们继续使用 url-loader 的例子, 只是对个别配置做些修改。

使用

index.html

1
2
3
4
5
6
7
8
<!DOCTYPE html>
<html lang="en">
<head>
<title>file-loader</title>
</head>
<body>
</body>
</html>

index.js

index.js 文件中引入一张图片,创建 image 标签后,将导入的图片赋值给 image 标签的 src 属性,将 image 标签添加到页面中。

1
2
3
4
5
6
7
8
9
import Back from './img/back.png';

function component() {
var element = document.createElement('img');
element.src=Back
return element;
}

document.body.appendChild(component());

安装 file-loader

1
npm install file-loader -D

安装成功后,将 file-loader 配置到 webpack.config.js 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = {
...
module: {
rules: [
{
test: /\.(png|jpg)$/,
use: [
{
loader: 'file-loader'
}
]
}
]
}
};

执行打包命令,此时在浏览器中直接打开 dist 文件夹下的 index.html 发现页面中展示一张图片,此时我们在 dist 目录下可以看到一张扩展名为 .png 的图片,直接点击图片打开,可以看到与我们引入的图片一致。

配置项

file-loader 包含下面几个配置项

参数名称 支持类型 默认值 功能描述
name String、Function [contenthash].[ext] 为文件配置自定义文件名模板
context String this.options.context 配置自定义文件 context,默认为 webpack.config.js 的 context
publicPath String、Function output.publicPath + outputPath 为文件配置自定义 public 发布目录
outputPath String、Function undefined 为文件配置自定义 output 输出目录
useRelativePath Boolean false 当设置为 true, 为每个文件生成一个相对 url 的 context
emitFile Boolean true 设置为 false, 禁止复制文件

name

可以使用查询参数名称为您的文件配置一个自定义的文件名模板。默认情况下,不配置 name 属性生成的文件的文件名就是文件内容的 MD5 哈希值与原始扩展名。name 属性支持传入字符串或函数配置。

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module.exports = {
module: {
rules: [
{
test: /\.(png|jpg)$/,
use: [
{
loader: 'file-loader',
options: {
name: [name].[ext]
}
}
]
}
]
}
};

name 参数可以传入以下常用占位符

名称 类型 默认值 功能描述
[ext] String file.extname 资源扩展名
[name] String file.basename 资源的名称
[path] String file.dirname 资源相对于 context 的路径
[hash] String md5 内容的哈希值, [hashes]]https://www.webpackjs.com/loaders/file-loader/#hashes 配置中有更多信息

常见的打包命名方式是:assets/[name]-[hash].[ext],即将所有 file-loader 处理的图片按照 name 传入的文件名称 + hash 值.扩展名的方式打包到 assets 目录下。其中

  1. [ext] 表示是原文件的扩展名,如 back.png 就是指 png

  2. [name] 表示原文件的文件名。如 back.png 就是指 back,但一般生产环境不推荐直接使用 [name],一般和 [hash] 一起使用,这样可以防止命名冲突。

  3. [path] 相对于 context 的路径,context 默认是 webpack.config.js 的路径

  4. [hash:6]可以控制 hash 值的长度,6 表示长度为 6,默认是 32

context

修改打包文件生成路径,其实影响的 是 path 占位符,context 需要和 path 占位符同时配置才会影响文件生成路径。

webpack.config.js

1
2
3
4
5
6
7
{
loader: 'file-loader',
options: {
name: '[path][name].[ext]',
context: __dirname + '/../'
}
}

打包后,当前项目根文件夹和图片所在路径形成了打包文件的新路径,如果不设置 context 则打包路径相对于 webpack.config.js 的 context 的路径。

1.png

publicPath

publicPath 一般会用 webpack 本身配置的,和那个效果也一样,但假如你想单独配置,就用这个。设置 publicPath 后,文件的请求地址会被打包进 js 文件。

webpack.config.js

1
2
3
4
5
6
7
{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'https://www.abc.cn/img/'
}
}

2.png

outputPath

outputPath 在文件前增加路径,也就是增加文件夹。

webpack.config.js

1
2
3
4
5
6
7
{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'images/'
}
}

总结

本节我们介绍了 file-loader 的使用方法和 file-loader 包含的几个常用参数配置。file-loader 的可配置选项在 url-loader 中配置也可生效(limit 生效的情况下),在 webpack5 以前对于资源文件的处理一般使用这两种插件,webpack5 提供了一种模块Asset Modules,它允许人们在不配置额外加载器的情况下使用资源文件(字体、图标等)

CSS-Loader

css-loader

css-loader 会对 import 和 url() 进行处理,就像 js 解析 import/require() 一样。

使用

index.html

1
2
3
4
5
6
7
8
<!DOCTYPE html>
<html lang="en">
<head>
<title>css-loader</title>
</head>
<body>
</body>
</html>

index.js

src/index.js 文件中使用 import 导入 index.css 并输出导入的样式内容。

1
2
3
import indexCss from "./index.css";

console.log(indexCss.toString())

index.css

src/index.css 文件中使用 import 导入 index.css。

1
2
3
4
5
6
body {
background-color: aqua;
width: 500px;
height: 500px;
border: 1px solid red;
}

安装 css-loader

1
npm install css-loader -D

安装成功后,将 css-loader 配置到 webpack.config.js 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = {
...
module: {
rules: [
{
test: /\.(css)$/,
use: [
{
loader: 'css-loader'
}
]
}
]
}
};

执行打包命令,此时在浏览器中打开 dist/index.html 发现页面没有正确展示我们定义的样式,但是控制台中输出了我们定义的样式,到了这一步,css-loader 我们就正确引入并使用了,没有正确展示效果的原因是 css-loader 对 index.js 中的 import 进行处理,默认生成一个数组存放处理后的样式字符串,并将其导出。而 style-loader 负责将 css 插入到 html 中,style-loader 的使用我们在下一节展示。

配置项

css-loader 包含下面几个配置项

参数名称 支持类型 默认值 功能描述
url Boolean true 启用/禁用 url() 处理
import Boolean true 启用/禁用 @import 处理
modules Boolean false 启用/禁用 CSS 模块
sourceMap Boolean 取决于 compiler.devtool 值 启用/禁用 Sourcemap
esModule Boolean/String true 是否使用 ES 模块语法
importLoaders Number 0 在 css-loader 前应用的 loader 的数量
exportType “array”/“string”/“css-style-sheet” [] 允许导出样式为模块数组、字符串或者可构造样式(即 CSSStyleSheet)

url

允许启用/禁用处理 CSS 函数 url 和 image-set。如果设置为false, css-loader 将不会解析 url 或 image-set 中指定的任何路径。还可以通过传递函数来根据资源路径动态地控制这种行为。从版本4.0.0开始,绝对路径是基于服务器根目录进行解析的。

src/index.css

1
2
3
body {
background: url(./img/1.png);
}

webpack.config.js

1
2
3
4
5
6
7
8
9
10
module.exports = {
...
{
loader: 'css-loader',
options: {
url: false,
// url: true
}
}
};

在控制台我们在 index.js 中输出了 index.css 导出的字符串,我们来看下 url 设置为 false 和 true 的 background 区别。

1.png
2.png

当 url 设置为 false 时, url 中的图片地址没做任何处理,当 url 值为 true 时,编译后地址为图片的路径,并且 dist 文件夹下会生成一张图片。

import

允许启用/禁用 @import 处理。

src/index.css

1
2
3
4
5
@import url('./main.css');

body {
border: 1px solid red;
}

src/main.css

1
2
3
4
body {
width: 200px;
height: 200px;
}

webpack.config.js

1
2
3
4
5
6
7
8
9
10
module.exports = {
...
{
loader: 'css-loader',
options: {
import: false
// import: true
}
}
};

为了方便看到样式,我这安装了 style-loader 并添加到了 loader 中,我们来看下 import 设置为 false 和 true 的区别。

3.png
4.png

当设置为 false 时 index.css 中的 @import 没有解析导致运行代码时找不到 main.css。

modules

查询参数 modules 会启用 CSS 模块规范。

默认情况下,这将启用局部作用域 CSS。(你可以使用 :global(…) 或 :global 关闭选择器 and/or 规则。详情可查看 modules

webpack.config.js

1
2
3
4
5
6
{
loader: 'css-loader',
options: {
modules: true
}
}

sourceMap

设置 sourceMap 选项查询参数来引入 source map。

例如 extract-text-webpack-plugin 能够处理它们。

默认情况下取决于compiler.devtool 值,值为 false 和 eval 时,不会生成 source map,一般情况下不启用它,因为它们会导致运行时的额外开销,并增加了 bundle 大小 (JS source map 不会)。此外,相对路径是错误的,你需要使用包含服务器 URL 的绝对公用路径。

webpack.config.js

1
2
3
4
5
6
7
{
...
loader: 'css-loader',
options: {
sourceMap: true
}
}

esModule

css-loader 中有时生成 esModule 模块化的形式是有益的,比如 module-concatenation 和 tree-shaking 时必须使用 esModule 模式才会生效。如果想启用 CommonJS 模块语法,则 esModule 设置为 false。

webpack.config.js

1
2
3
4
5
6
{
...
options: {
esModule: true
}
}

importLoaders

在 src/index.css 中使用的 @import ‘./main.css’,importLoaders 选项可以定义在 @import 时使用哪些插件编译。

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
...
use: [
"style-loader",
{
loader: "css-loader",
options: {
// 0 => no loaders (default);
// 1 => postcss-loader;
// 2 => postcss-loader, sass-loader
importLoaders: 2,
},
},
"postcss-loader",
"sass-loader",
],
}

exportType

允许将样式导出为带有模块的数组、字符串或可构造样式表(如CSSStyleSheet)。默认值是 ‘array’。

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
{
...
use: [
{
loader: 'css-loader',
options: {
exportType: 'string'
}
}
]
}
}

src/index.js

1
2
3
import indexCss from "./index.css";

console.log(indexCss)

打包后,执行 dist/index.html 可以看到控制台输出了 index.css 中定义的样式字符串。

总结

本节我们介绍了 css-loader 的使用方法和 css-loader 包含的几个常用参数配置。css-loader 可以将 js 中的 import 导入样式文件进行编译并且拿到导出内容供其他插件使用。

Style-Loader

style-loader

style-loader 一般和 css-loader 配合使用,css-loader 识别模块,通过特定的语法规则进行内容转换最后导出,style-loader 将 css-loader 导出的内容插入到 DOM。

使用

index.html

1
2
3
4
5
6
7
8
<!DOCTYPE html>
<html lang="en">
<head>
<title>style-loader</title>
</head>
<body>
</body>
</html>

index.js

src/index.js 文件中使用 import 导入 index.css。

1
import indexCss from './index.css';

index.css

设置一个长宽均为 200 像素的带红色边框的正方形

1
2
3
4
5
body {
border: 1px solid red;
width: 200px;
height: 200px;
}

安装 style-loader

1
npm install style-loader -D

安装成功后,将 style-loader 配置到 webpack.config.js 中,配置中我们使用 css-loader 和 style-loader 两个加载器,webpack 中 loader 的解析一般由右向左,由下向上解析,所以 webpack 会先执行 css-loader,css-loader 导出内容传给 style-loader,最后在执行 style-loader。

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
...
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader','css-loader']
}
]
}
};

执行打包命令,此时在浏览器中打开 dist/index.html 发现页面可以正常展示我们设置的样式,我们在控制台可以看到样式被插入到 head 标签中。

1.png
2.png

配置项

style-loader 包含下面几个配置项

参数名称 支持类型 默认值 功能描述
injectType String styleTag 如何将样式注入到 DOM 中
attributes Object {} 向标签添加自定义属性
insert String、Function head 在 DOM 中给定的位置插入标签
styleTagTransform String、Function undefined 当插入 style 标签到 DOM 时转换标签和 css
esModule Boolean true 使用 ES 模块语法
base Number true 设置模块ID基数(DLLPlugin)

injectType

设置样式如何注入 DOM,默认为 styleTag,即使用多个 模式。

模式配置 功能描述
styleTag 多个 模式
singletonStyleTag 单个 模式
autoStyleTag 与 styleTag 相同,但如果在 IE6-9 中执行,则打开 singletonStyleTag 模式。
lazyStyleTag 按需加载模式插入多个 模式
lazySingletonStyleTag 按需加载模式插入单个 模式
lazyAutoStyleTag 与 lazyStyleTag 相同,但如果在 IE6-9 中执行,则打开 lazySingletonStyleTag 模式。
linkTag 多个 形式插入 DOM

styleTag

src/index.css

1
2
3
4
@import url('style.css');
.bar {
color: blue;
}

src/style.css

1
2
3
.foo {
color: red;
}

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
...
{
test: /\.(css)$/,
use: [
{
loader: 'style-loader',
options: { injectType: "styleTag" }
}, 'css-loader'
]
}
};

index.js

1
2
3
4
5
6
import index from "./index.css";

const divElement = document.createElement("div");
divElement.className = "foo";
divElement.innerHTML = 'style-loader'
document.body.appendChild(divElement)

执行打包命令,在浏览器中打开 dist/index.html 文件,我们可以看到 head 中插入了两个 style 标签,div 中的文字颜色可以正常显示。

3.png

singletonStyleTag

将多个样式文件内容在一个 style 标签中插入 DOM。

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
...
{
test: /\.(css)$/,
use: [
{
loader: 'style-loader',
options: { injectType: "singletonStyleTag" }
}, 'css-loader'
]
}
};

4.png

lazyStyleTag

按需注入到 DOM 。建议遵循 .lazy.css 惰性样式的命名约定和 .css 基本style-loader用法。当使用 lazyStyleTag 时,可以通过 style-loader 的 style.use()、style.unuse() 按需使用。

src/style.lazy.css

1
2
3
.foo {
color: red;
}

src/index.js

1
2
3
4
5
6
import styles from "./style.lazy.css";
styles.use();
const divElement = document.createElement("div");
divElement.className = "foo";
divElement.innerHTML = 'style-loader'
document.body.appendChild(divElement)

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
...
{
test: /\.(css)$/,
use: [
{
loader: 'style-loader',
options: { injectType: "lazyStyleTag" }
}, 'css-loader'
]
}
};

打包后,可以看到样式被插入到 DOM,并且颜色已生效,如果 index.js 中没有调用 styles.use(),则样式不会被插入到 DOM。

attributes

将指定的属性值附加到