前言

啊哈,这是一个系列文章,用于记录webpack的学习,因为每次都是学了就忘,所以打算写个笔记总结一下各个操作

这是这个系列的第一篇

为什么要使用webpack

嘛,其实就是为了解决一些开发和线上运行时的一些需求而已

开发时

  1. 支持模块化开发(提高代码的可维护性,避免手动维护JS代码的加载顺序和全局作用域污染等问题)
  2. 支持使用第三方模块,比如npm下载的模块(调库一时爽,一直调库一直爽)
  3. 支持多种模块化标准 (各个库使用的模块化规范不同)
  4. 能使用scss,less这样的预处理语言
  5. 能够解决其他工程化的问题

运行时

  1. 文件尽可能少 (在http2没出现时,传输文件需要建立新的tcp连接,开销很高)
  2. 文件体积尽可能小 (毕竟用户的流量要钱,而且可以加快页面打开的速度)
  3. 代码内容尽可能乱 (代码混淆)
  4. 代码的浏览器兼容性要好
  5. 能够解决其他运行时的问题,主要是执行效率问题

解决方案

我们需要一个工具,这个工具在开发时能让开发者使用前端工程化的思想对项目代码进行更好的组织和管理,也能让开发者不必担心全局作用域污染这样和业务逻辑无关的问题而是专注于开发业务代码,在运行时要让代码更高效,这个工具就是代码构建工具,webpack代码构建工具之一,但是它是最完善,生态最好的的,所以我们一般都用这个

其他的包管理器还有

  1. gulp
  2. grunt
  3. browserify

webpack的基本使用

初始化项目

1
npm init

这一步会创建一个package.json文件用于记录项目的基本信息

安装webpack和webpack-cli

1
npm install webpack webpack-cli -D

webpack的安装方式有两种,一个是全局安装,一个是本地安装,不过我们一般都使用本地安装,全局安装的话,npm会帮我们绑定一个命令行环境变量,可以让我们在每个项目中都可以运行命令,本地安装则会添加其成为项目中的依赖,只能在项目内使用,那我们为什么一般都用本地安装呢,主要有下面两个原因

  1. 如果采用全局安装,那么在多人协作时,和他人的Webpack版本不同,就有可能导致输出结果不一致的问题
  2. 部分依赖于Webpack的插件可能会调用Webpack中的内部模块,这种情况需要在本地安装Webpack,如果全局和本地都有,可能造成混淆

在安装后运行

1
2
npx webpack -v
npx webpack-cli -v

这样可以显示各自的版本号,如果成功显示证明安装成功

零配置打包第一个项目

webpack支持零配置打包,我们先来尝试下

在项目中新建几个文件

/src/index.js

1
2
import add from "./add";
console.log(add(1, 2))

/src/add.js

1
2
3
export default function add(a, b) {
return a + b;
}

运行命令

1
npx webpack

然后我们就发现项目里多了一个dist文件夹,文件夹里多了一个main.js,这就是我们打包出来的文件,如果你配置了nodejs的执行环境,可以直接执行它,它会在控制台打印出3

使用配置文件打包

在真正的项目中常用的打包方式是使用配置文件,webpack的默认打包的配置文件是webpack.config.js,在工程根目录下创建webpack.config.js,然后添加下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  
const path = require("path");
module.exports = {
// 打包入口
entry: {
// 属性值是chunk的名称, 属性值是chunk的入口模块
main : "./src/index.js",
},
// 打包出口
output: {
path: path.resolve(__dirname, "dist"), // 一个绝对路径 表示打包后的输出文件夹
// [name] : chunk name
// [hash] : hash, 标识文件是不同的, 防止一些缓存问题, 这个hash是所有chunk的总和hash
// [chunkhash] : 单个chunk的hash :8
// [id] : 模块的id
filename: "[name][hash].js" // 输出文件名规则, 如果有多个chunk, 就不能使用静态文件名
},
// 打包的模式
mode : 'production'
};

如果要使用指定的配置文件,可以在命令行传入传入config参数

1
webpack --config webpack.config.js

自动打包

如果我们想在修改代码后自动进行打包,我们可以使用watch参数

1
npx webpack --watch

在设置这个参数后,webpack会自动观测项目里的文件,如果文件发生了改变就会自动打包

本地开发服务器

你可能已经发现,单纯使用webpack和它的命令行工具进行开发调试的效率并不高,以前只需要编辑项目源文件然后刷新页面就可以看到效果,现在还需要进行打包操作,那么有什么更方便的方法提高开发效率吗,其实是有的,那就是本地开发服务器

在dist目录下新建index.html

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="./bundle.js"></script>
</head>
<body>

</body>
</html>

配置webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
let path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "/dist"),
filename: "bundle.js"
},
devServer: {
contentBase: path.resolve(__dirname, 'dist'),
port: 9000
}
}

安装webpack-dev-server

1
npm install webpack-dev-server -D

为了方便启动,在package.json中配置下面的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"scripts": {
"build": "webpack --mode production",
"dev": "webpack --mode development",
"build-watch" : "webpack --watch",
"serve": "webpack serve"
},
"devDependencies": {
"webpack": "^5.10.1",
"webpack-cli": "^4.2.0",
"webpack-dev-server": "^3.11.0"
}
}

运行npm run serve

image-20201215142725377

如果你修改代码,开发服务器会通知浏览器进行刷新

1
2
import add from "./add.js"
console.log(add(1,3));

image-20201215141938119

顺带一提,如果webpack报了这个错,是因为webpack-cli4和webpack-dev-server的兼容性问题

1
2
3
4
5
6
7
8
> webpacktest@1.0.0 server E:\Workspaces\Project\WebPro\WebpackTest
> webpack-dev-server

internal/modules/cjs/loader.js:969
throw err;
^

Error: Cannot find module 'webpack-cli/bin/config-yargs'

可以看看这个issue

如果你还想使用旧版webpack的配置方式,可以试试下面这样的配置

1
2
3
4
5
6
7
8
9
10
11
{
"scripts": {
"build": "webpack",
"serve": "webpack-dev-server"
},
"devDependencies": {
"webpack": "^4.43.0",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0"
}
}

删掉node_module目录重新运行npm install,然后运行npm run serve,就不再报错了

image-20201215142607167

打包结果分析

因为webpack5对打包结果做了优化,所以要改一下代码,不然会被优化掉

src/util/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function randomNum(minNum, maxNum) {
switch (arguments.length) {
case 1:
return parseInt(Math.random() * minNum + 1, 10);
break;
case 2:
return parseInt(Math.random() * (maxNum - minNum + 1) + minNum, 10);
break;
default:
return 0;
break;
}
}
console.log(randomNum(1,2));
export default randomNum;

src/cal.js

1
2
3
4
5
6
7
8
import randomNum from "./util";

export default function cal(a, b) {
let flag = randomNum(0,1);
return flag ?
a + b :
a - b ;
}

src/index.js

1
2
import add from "./add.js"
console.log(add(Math.random(),Math.random()));

打包出来的代码是这样的

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
(() => {
"use strict";
var __webpack_modules__ = ({

"./src/cal.js":

((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => /* binding */ cal\n/* harmony export */ });\n/* harmony import */ var _util__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./util */ \"./src/util/index.js\");\n\r\n\r\nfunction cal(a, b) {\r\n let flag = (0,_util__WEBPACK_IMPORTED_MODULE_0__.default)(0,1);\r\n return flag ?\r\n a + b :\r\n a - b ;\r\n}\r\n\r\n\n\n//# sourceURL=webpack://webpacktest/./src/cal.js?");

}),

"./src/index.js":

((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _cal_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./cal.js */ \"./src/cal.js\");\n\r\nconsole.log((0,_cal_js__WEBPACK_IMPORTED_MODULE_0__.default)(Math.random(),Math.random()));\n\n//# sourceURL=webpack://webpacktest/./src/index.js?");

}),

"./src/util/index.js":
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => __WEBPACK_DEFAULT_EXPORT__\n/* harmony export */ });\nfunction randomNum(minNum, maxNum) {\r\n switch (arguments.length) {\r\n case 1:\r\n return parseInt(Math.random() * minNum + 1, 10);\r\n break;\r\n case 2:\r\n return parseInt(Math.random() * (maxNum - minNum + 1) + minNum, 10);\r\n break;\r\n default:\r\n return 0;\r\n break;\r\n }\r\n}\r\nconsole.log(randomNum(1,2));\r\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (randomNum);\n\n//# sourceURL=webpack://webpacktest/./src/util/index.js?");

})

});
// The module cache
var __webpack_module_cache__ = {};

// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
if (__webpack_module_cache__[moduleId]) {
return __webpack_module_cache__[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = __webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {}
};

// Execute the module function
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);

// Return the exports of the module
return module.exports;
}

/* webpack/runtime/define property getters */
(() => {
// define getter functions for harmony exports
__webpack_require__.d = (exports, definition) => {
for (var key in definition) {
if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
Object.defineProperty(exports, key, {enumerable: true, get: definition[key]});
}
}
};
})();

/* webpack/runtime/hasOwnProperty shorthand */
(() => {
__webpack_require__.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop)
})();

/* webpack/runtime/make namespace object */
(() => {
// define __esModule on exports
__webpack_require__.r = (exports) => {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, {value: 'Module'});
}
Object.defineProperty(exports, '__esModule', {value: true});
};
})();

// startup
// Load entry module
__webpack_require__("./src/index.js");
// This entry module used 'exports' so it can't be inlined
})();

乍一看也许很复杂,其实并没有,我整理了一下

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// 自执行函数
(() => {
// 一个对象,保存了转化后的模块代码
var __webpack_modules__ = {
"./src/cal.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
"default": () => cal
});
var indexExports = __webpack_require__( "./src/util/index.js");
function cal(a, b) {
let flag = indexExports.default(0,1);
return flag ?
a + b :
a - b ;
}
}),
"./src/index.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
var calExports = __webpack_require__("./src/cal.js");
console.log(calExports.default(Math.random(),Math.random()));
}),
"./src/util/index.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
"default": () => randomNum
});
function randomNum(minNum, maxNum) {
switch (arguments.length) {
case 1:
return parseInt(Math.random() * minNum + 1, 10);
break;
case 2:
return parseInt(Math.random() * (maxNum - minNum + 1) + minNum, 10);
break;
default:
return 0;
break;
}
}
console.log(randomNum(1,2));

})
};
// 缓存模块代码执行后的导出对象
var __webpack_module_cache__ = {};

// require函数,用来执行模块代码并且获取模块代码的导出
function __webpack_require__(moduleId) {
// 检查有无缓存
if (__webpack_module_cache__[moduleId]) {
return __webpack_module_cache__[moduleId].exports;
}
// 创建一个新的模块对象并且缓存起来
// 这个模块对象会保存模块的导出数据
var module = __webpack_module_cache__[moduleId] = {
exports: {}
};

// 执行模块代码 把模块对象 require函数
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);

// 返回模块的导出
return module.exports;
}

// 用来解决es6导出时值会动态变化的情况
__webpack_require__.d = (exports, definition) => {
for (var key in definition) {
if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
Object.defineProperty(exports, key, {enumerable: true, get: definition[key]});
}
}
};

// 判断对象上有无属性
__webpack_require__.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop)

// 做一些字段标记
__webpack_require__.r = (exports) => {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, {value: 'Module'});
}
Object.defineProperty(exports, '__esModule', {value: true});
};

// 开始执行
__webpack_require__("./src/index.js");
})()

这样就好看多了,最外层是一个自执行函数,最开始定义了一个对象,保存了转化后的代码,接下来定义了一个缓存对象和一些工具函数,然后执行入口文件,在执行时会执行依赖模块的代码进行导出

顺带一提,如果你对为什么打包出来的结果是eval包裹的,可以看看文档的devTools部分

webpack编译过程

嘛,既然写了打包结果,顺便写写编译过程吧

2020-01-09-10-26-15

webpack会把所有的文件视作模块进行编译,形成最终的代码

整个过程大致分为三个步骤

  1. 初始化
  2. 编译
  3. 输出

初始化

此阶段,webpack会将CLI参数配置文件默认配置进行融合,形成一个最终的配置对象。

对配置的处理过程是依托一个第三方库yargs完成的

此阶段相对比较简单,主要是为接下来的编译阶段做必要的准备

目前,可以简单的理解为,初始化阶段主要用于产生一个最终的配置

编译

创建chunk

chunk是webpack在内部构建过程中的一个概念,译为,它表示通过某个入口找到的所有依赖的统称。

比如这里根据入口模块”./src/index.js”创建一个chunk

2020-01-09-11-54-08

每个chunk都有至少两个属性:

  • name:默认为main
  • id:唯一编号,开发环境和name相同,生产环境是一个数字,从0开始

处理依赖模块

2020-01-09-12-32-38

这一步会从入口文件开始,把文件转化成AST语法树,然后检查这个文件依赖了哪些项,进行记录,然后在AST中进行一些替换,比如把import转化成__webpack_require__,把引用的模块路径转化成模块id等,然后保存转化后的代码到模块记录中,接着递归加载依赖模块,直到加载处理完所有的模块为止

产生chunk assets

在第二步完成后,chunk中会产生一个模块列表,列表中包含了模块id模块转换后的代码

接下来,webpack会根据配置为chunk生成一个资源列表,即chunk assets,资源列表可以理解为是生成到最终文件的文件名和文件内容

2020-01-09-12-39-16

2020-01-09-12-43-52

合并chunk assets

2020-01-09-12-47-43

这一步把所有chunk assets整合到一起

输出

这一步,webpack利用nodejs,根据编译产生的总的assets,生成相应的文件。

2020-01-09-12-54-34

总过程

2020-01-09-15-51-07

相关链接

webpack官方文档

AST在线测试工具