以下内容都是基于 Webpack 4.x

什么是 Webpack

Webpack 可以理解为模块打包工具, 它所做的事情: 分析项目结构, 找到 JS 模块及其它一些浏览器不能直接运行的拓展语言(如 SCSS、TypeScript 等), 并将其打包为合适的格式以供浏览器使用

构建就是把源代码转换成发布到线上的可执行 JS、CSS、HTML 代码, 包括以下内容

  • 代码转换: TS 编译为 JS, SCSS 编译为 CSS 等
  • 文件优化: 压缩 JS、CSS、HTML 代码, 压缩合并图片等
  • 代码分割: 提取多个页面的公共代码, 提取首屏不需要执行部分的代码让其异步加载等
  • 模块合并: 在采用模块化的项目中会有很多个模块和文件, 需要构建功能把模块分类合并成一个文件等
  • 自动刷新: 监听本地源代码的变化, 自动重新构建、刷新浏览器等
  • 代码校验: 在代码被提交到仓库前需要校验代码是否符合规范, 以及单元测试是否通过等
  • 自动发布: 更新完代码, 自动构建出线上发布代码并传输给发布系统

构建其实是工程化、自动化思想在前端开发中的体现, 把一系列流程用代码实现, 让代码自动化执行这一系列复杂流程; 构建给前端开发注入了更大的活力, 解放生产力 (/假的

初始化项目 & 简单配置

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
# 初始化项目
mkdir webpack-proj && cd webpack-proj
npm init -y

# 创建 dist、src 文件夹
mkdir dist
mkdir src && cd src && touch index.js

# 安装 webpack & webpack-cli
# 注: 因为将 webpack 安装在项目中, 所以要运行 webpack, 有以下两种方式
# 1. npx 可以直接运行 node_modules/.bin 目录下的命令
# 2. 通过配置 package.json 中的 script「"script": "webpack"」(常用)
# 3. webpack4.x 对应的 webpack-cli3.x, 版本不对应会导致很多问题产生, 切记切记~
npm i webpack@4 webpack-cli@3 -D

# 其他插件
npm i css-loader style-loader file-loader url-loader html-loader
webpack-dev-server html-webpack-plugin clean-webpack-plugin -D
# webpack-dev-server 使用 websocket 实现打包后自动刷新功能, 如以下打包信息
# [./node_modules/webpack-dev-server/client/overlay.js] (webpack)-dev-server/client/overlay.js 3.51 KiB {main} [built]
# [./node_modules/webpack-dev-server/client/socket.js] (webpack)-dev-server/client/socket.js 1.53 KiB {main} [built]
# [./node_modules/webpack-dev-server/client/utils/createSocketUrl.js] (webpack)-dev-server/client/utils/createSocketUrl.js 2.91 KiB {main} [built]
# [./node_modules/webpack-dev-server/client/utils/log.js] (webpack)-dev-server/client/utils/log.js 964 bytes {main} [built]
# [./node_modules/webpack-dev-server/client/utils/reloadApp.js] (webpack)-dev-server/client/utils/reloadApp.js 1.59 KiB {main} [built]
# [./node_modules/webpack-dev-server/client/utils/sendMessage.js] (webpack)-dev-server/client/utils/sendMessage.js 402 bytes {main} [built]

webpack 核心概念

  • entry: 入口, webpack 执行构建的第一步从 entry 开始, 可抽象成输入
  • module: 模块, 在 webpack 中一切皆模块, 一个模块对应着一个文件, webpack 会从配置的 entry 开始递归找出所有依赖的模块
  • chunk: 代码块, 一个 chunk 由多个模块组合而成, 用于代码合并与分割
  • loader: 模块转换器, 用于把模块原内容按照需求转换成新内容
  • plugin: 插件, 在 webpack 构建流程中的特定时机注入扩展逻辑来改变构建结果或自定义动作
  • output: 输出结果, 在 webpack 经过一系列处理并得出最终想要的代码后输出结果

webpack 启动后会从 entry 中配置的 module 开始递归解析 entry 以来的所有 module; 每找到一个 module, 就会根据配置的 loader 去找出对应的转换规则, 对 module 进行转换后, 在解析出当前 module 依赖的 module; 这些模块会以 entry 为单位进行分组, 一个 entry 和其所有依赖的 module 被分到一个组(chunk); 最后 webpack 会把所有 chunk 转换成输出文件; 在整个流程中 webpack 会在适当的时机执行 plugin 中定义的逻辑、插件

1
2
3
4
5
6
7
8
9
10
11
12
<!-- src 下的 index.html, 以此为模版产出所需的 html 文件 -->
<head>
<!-- ... -->
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<!-- index.js 中获取元素: document.getElementById('app') -->
<div id='app'></div>
<!-- <script src='bundle.js'></script> -->
<!-- 同样的也可以引入很多 cdn 文件, 减少打包体积 -->
<script src='https://g.alicdn.xxx.com'></script>
</body>
1
2
3
4
5
6
7
8
9
// src/index.js
require('./index.css');
document.getElementById('app').innerHTML = 'edsyang';

// 图片, 会返回一个打包后的地址
import src from './images/avatar.png';
const img = new Images();
img.src = src;
document.body.appendChild(img);
1
2
3
4
5
// package.json 配置
"script": {
"build": "webpack --mode development",
"dev": "webpack-dev-server --open --mode development"
}
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
// webpack.config.js
// webapck 内部有一个事件流: tapable 1.0
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
// 入口
// 工作模式: 先找到每个入口, 然后从各个入口分别出发, 找到依赖的模块(module),
// 生成一个代码块(Chunk), 最后会把 Chunk 写到文件系统(Assets)中
// 对应关系: 一个模块(module)对应一个代码块(Chunk), 但是一个代码块(Chunk)并不对应一个文件系统(Assets)
// 1. 字符串:「entry: 'xxx'」
// 2. 数组:「entry: ['xxx', 'yyy']」, 文件都会打包到 bundle.js 中
// 3. 对象(多入口):「entry: { index: 'xxx', base: 'yyy', vendor: 'zzz' }」
entry: './src/index.js',
output: {
// 输出的文件夹, 只能是绝对路径
path: path.join(__dirname, 'dist'),
// 打包后的文件名
// name 即 entry 名字, 默认 main;
// hash 是根据打包后的文件内容计算出来的一个 hash 值, 默认 20 位, 可通过 :n 来指定位数
// filename: 'bundle.js'
filename: '[name].[hash:8].js'
},
module: {
// 转换: 因为 css 并不是 js 模块, 所以需要转换, 这些转换的工作就是 loader
// loader 的三种写法
// 1. use:「use: 'expose-loader?React'」
// 2. loader:「loader: 'expose-loader?React'」
// 3. use+loader:「use: { loader: 'expose-loader', options: 'React' }」
// ? 后面的参数可以通过 options 传递
rules: [{
// 转换文件的匹配正则
test: /\.css$/,
// css-loader 用来解析处理 css 文件中的 url 路径, 要把 css 文件变成一个模块
// style-loader 可以把 css 文件转换成 style 标签插入 head 中
// 注意: 多个 loader 是有顺序要求的, 从右往左写, 因为转换的时候是从右往左转换
// 比较不常见的写法: 在引入的地方 require(style-loader!css-loader!./src/index.css);
loader: ['style-loader', 'css-loader']
}, {
// file-loader 解析图片地址, 把图片从原位置拷贝到目标位置并且修改原引用地址
// 可以处理任意的二进制数据
test: /\.(png|jpg|gif|svg|bmp)$/,
loader: 'file-loader'
}, {
// 处理直接在 html 页面中通过标签引入的图片或其它静态资源(使用较少)
test: /\.(html|htm)$/,
loader: 'html-loader'
}]
},
plugins: [
// 清除 dist 目录, 保证 dist 目录下都是最新打包生成的文件
new CleanWebpackPlugin(),
// 该插件可以根据模版自动生成 html 文件到目标目录下
new HtmlWebpackPlugin({
// 指定产出的 HTML 模版
template: './src/index.html',
// 产出的 HTML 文件名
filename: 'index.html',
// 自动设置标题, 需要在 index.html 中的 title 配置
title: 'edsyang\'s blog',
// 在产出的 html 文件中引入哪些代码块(和 entry 中定义的名字相对应)
// 所以有多个入口的时候可以配置多个 HtmlWebpackPlugin 来保证不同文件隔离, 此时就需要配置 chunks
chunks: ['vendor', 'index'],
// 会在引入的 JS 中加入查询字符串, 避免缓存
// e.g
// 取名为 bundle.js, 那么修改内容后其生成物仍为 bundle.js,
// webpack 会识别 bundle.js 为已缓存过的, 仍然会走缓存, 内容不会更新
hash: true,
// 压缩文件
minify: {
// 去掉生成物的双引号
removeAttributeQuotes: true
}
})
],
// 配置此静态文件服务器, 可以用来预览打包后的项目
devServer: {
// 静态文件目录
contentBase: './dist',
// 主机
host: 'localhost',
// 端口号
port: 8080,
// 服务器返回给浏览器的时候是否启动 gzip 压缩
compress: true
}
}

接下来只需要执行 script 的命令「npm run build」即可在 dist 文件夹中得到打包后的文件

webpack-result

打包结果解析

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
// 一个自执行函数, 上面是函数体, 下面是参数
/******/ (function(modules) { // webpackBootstrap
/******/ // 模块的缓存: 提高模块加载速度
/******/ var installedModules = {};
/******/
/******/ // 声明了一个 require 方法, 模拟 commonjs require
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // 如果缓存中有了, 表示模块加载过了, 直接把对象返回即可
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // 如果没有加载过, 创建一个新的模块, 并放到缓存中
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // 执行模块函数
// modules 函数体; moduleId 即函数体的参数
// module.exports 导入对象; module 模块本身; __webpack_require__ require 方法
// 为什么要引入 __webpack_require__ 方法? 在模块的函数体中用 require 加载其它模块
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // 标记模块为加载过的
/******/ module.l = true;
/******/
/******/ // 返回模块的导出对象
/******/ return module.exports;
/******/ }
/******/
/******/ // 向外暴露模块对象
/******/ __webpack_require__.m = modules;
/******/
/******/ // 向外暴露模块缓存
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // 定义 getter 方法, 为了兼容 expoets
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
// 在 exports 对象上定义 name 属性, 它的值是不可配置的, 可枚举的, 并指定获取器
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // 在导出对象上定义 __esModule, 处理 es6 和 commonjs 的关系
// es6 module commonjs 模块: export default xx => require('mod').default;
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // 它对应 output: publicPath 公开路径
/******/ __webpack_require__.p = "";
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = "./src/index.js");
/******/ })
/************************************************************************/
/******/ ({

// 将以下内容作为参数传给了 modules
/***/ "./src/index.js":
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
/*! no static exports found */
/***/ (function(module, exports) {
// 代码内容
eval("console.log('hello');\ndocument.getElementById('app').innerHTML = 'edsyang';\n\n//# sourceURL=webpack:///./src/index.js?");

/***/ })

/******/ });

模块公用代码库的处理方案

  1. (模块中不需要再引入)使用插件: webpack.ProvidePlugin「自动向模块内部注入变量」
    1
    2
    3
    4
    5
    plugin: [
    new webpack.ProvidePlugin({
    React: 'react',
    })
    ]
  2. (在最外层引入)使用插件: expose-loader「expose-loader?全局变量名!模块名」会先加载该模块, 然后得到模块的导出对象, 并且挂载到 window 上
    1
    2
    3
    4
    5
    6
    7
    8
    9
    const React = require('expose-loader?React!react');
    // 等同于以下 webpack 配置, 一般是这种配置方式
    module: [
    rules: [{
    test: /^react$/, // require.resolve('react') 得到一个模块的绝对路径
    loader: 'expose-loader?React'
    }]
    ]
    const React = require('react');
  3. (使用需引入, 减少本身包的体积)利用 webpack: externals 配置
    1
    2
    3
    4
    5
    6
    7
    8
    // html 中引入
    <script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
    // webpack 中配置
    externals: {
    'react': 'var window.React',
    'react-dom': 'var window.ReactDOM'
    }

为什么要区分生产环境和开发环境?
开发环境(development)和生产环境(production)的构建目标差异很大; 在开发环境中, 我们需要具有强大的、具有实时重新加载(live reloading)或热模块替换(hot module replacement)能力的 source map 和 localhost server; 而在生产环境中, 我们的目标则转向于更小的 bundle, 更轻量的 source map, 以及更优化的资源, 以改善加载时间; 由于要遵循逻辑分离, 我们通常建议像每个环境编写彼此独立的 webpack 配置.