webpack原理

构建 webpack 知识体系分为下面三个层级: 1.基础–会配置 2.进阶–能优化 3.深入–懂原理

一、基础篇

1.简单配置

该部分需掌握:
1.Webpack 常规配置项有哪些? 2.常用 loader 有哪些?如何配置? 3.常用插件 plugin 有哪些?如何配置?
4.Babel 如何配置?Babel 插件如何使用?

1.1 安装依赖

需要现在本地安装webpackwebpack-cli

1
$ npm install webpack webpack-cli -D

1.2 开始工作

webpack 在 4 以后就支持 0 配置打包,我们可以测试一下

  1. 新建 webpack-work 项目,新建 ./src/index.js文件,写一段简单的代码
1
2
3
const a = 'hello foolishmax';
console.log(a);
module.exports = a;
  1. 直接运行npx webpack,启动打包
    webpack-mode-error)

打包完成,会有一个提示:The 'mode' option has not been set,...

意思就是我们没有配置 mode(告知 webpack 使用相应模式的内置优化,默认为production,另外还有developmentnone

选项 描述
development 开发模式,打包更加快速,省了代码优化步骤
production 生产模式,打包比较慢,开启 tree-shaking 和压缩代码
none 不使用任何默认优化选项

webpack.config.js 中配置:

1
2
3
module.exports = {
mode: 'development',
}

1.3 配置文件

虽然可以零配置打包,但是实际工作中,还是需要使用配置文件的方式来满足不同项目的需求。 1.跟路径下创建配置文件webpack.config.js 2.新增基本配置信息

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

module.exports = {
mode: 'development', // 模式
entry: './src/index.js', // 打包入口地址
output: {
filename: 'bundle.js', // 输出文件名
path: path.join(__dirname, 'dist') // 输出文件目录
}
}

1.4 loader

这里我们把入口文件改成 css 文件,看下打包结果如何 1.新增./src/main.css

1
2
3
4
5
6
7
body {
margin: 0 auto;
padding: 0 20px;
max-width: 800px;
background: #f4f8fb;
}

2.修改 entry 配置

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

module.exports = {
mode: 'development',
entry: './src/main.css',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist')
}
}

3.运行打包命令: npx webpack
loader
这里的报错信息是因为:webpack 默认支持处理 js 文件,其他类型都处理不了,需要借助 loader 来对不同类型的文件进行处理。

4.安装css-loader来处理 CSS

1
npm install css-loader -D

5.配置资源加载模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const path = require('path');

module.exports = {
mode: 'development',
entry: './src/main.css',
output: {
filename: 'bundle.css',
path: path.join(__dirname, 'dist')
},
module: {
rules: [ // 转换规则
{
test: /.css$/, // 匹配所有css文件
use: 'css-loader', //对应的loader名称
}
]
}
}

6.重新运行打包命令npx webpack,就可以打包成功了

总结:loader 就是将 webpack 不认识的内容转化为认识的内容

插件(plugin)

插件可以贯穿 webpack 打包的生命周期,执行不同的任务

1.新建./src/index.html文件

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>

</body>
</html>

如果想要打包后的资源文件,例如 js 或者 css 文件可以自动引入到 html 中,就需要使用插件html-webpack-plugin来做这件事情。

2.本地安装 html-webpack-plugin

1
npm install html-webpack-plugin -D

3.配置插件

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 HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');

module.exports = {
mode: 'development',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist')
},
module: {
rules: [ // 转换规则
{
test: /.css$/, // 匹配所有css文件
use: 'css-loader', //对应的loader名称
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
})
]
}

运行一下打包,打开 dist 目录下的 index.html 文件

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script defer src="bundle.js"></script></head>
<body></body>
</html>

可以看到它自动引入打包好的 bundle.js

1.6 自动清空打包目录

每次打包的时候,打包目录都会遗留上次打包的文件,为了保持打包目录的纯净,打包前需将上次打包目录清空。

1.安装 clean-webpack-plugin

1
$ npm install clean-webpack-plugin -D

2.配置

1
2
3
4
5
6
7
8
9
10
11
12
const HtmlWebpackPlugin = require('html-webpack-plugin');
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
const path = require('path');

module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
}),
new CleanWebpackPlugin()
]
}

1.7 区分环境

本地环境

  • 需要更快的构建速度
  • 需要打印 debug 信息
  • 需要 live reload 或者 hot reload 功能
  • 需要 sourcemap 方便定位问题

生产环境:

  • 需要更小的包体积,代码压缩+tree-shaking
  • 需要进行代码分割
  • 需要压缩图片体积

掘金

webpack 常用 loader 和 plugin

1
2
3
4
5
6
7
8
9
10
11
12
13
样式:style-loader、css-loader、less-loader、sass-loader
文件:file-loader、url-loader
编译:babel-loader、ts-loader
校验:eslint-loader

常用的plugin
html-webpack-plugin
webpack-bundle-analyzer 打包分析
HotModuleReplacementPlugin 热更新


自己写webpack插件:
主要就是通过访问compliler和compilation拦截webpack的执行

如何利用 webpack 来优化前端性能?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
用webpack优化前端性能是指优化webpack的输出结果,
让打包的最终结果在浏览器运行快速高效。

压缩代码。删除多余的代码、注释、简化代码的写法等等方式。
可以利用webpack的UglifyJsPlugin和ParallelUglifyPlugin来压缩JS文件
,利用cssnano(css-loader?minimize)来压缩css

利用CDN加速。在构建过程中,将引用的静态资源路径修改为CDN上对
应的路径。可以利用webpack对于output参数和各loader的publicPath
参数来修改资源路径

删除死代码(Tree Shaking)。将代码中永远不会走到的片段删除掉。

可以通过在启动webpack时追加参数--optimize-minimize来
实现提取公共代码。

谈谈你对 webpack 的理解?

webpack 是一个打包模块化 js 的工具,在 webpack 里一切文件皆模块
,通过 loader 转换,通过 plugin 注入钩子,最后输出由多个
模块组合成的文件,webpack 专注构建模块化。WebPack 可
以看做是模块的打包机器:它做的事情是,分析你的项目
结构,找到 js 模块及其它的一些浏览器不能直接运行的
拓展语言,例如:Scss,TS 等,并将其打包为合适的格式
以供浏览器使用。

说说 webpack 与 grunt、gulp 的不同?

三者都是前端构建工具,grunt 和 gulp 在早期比较流行,
现在 webpack 相对来说比较主,不过一些轻量化的任务
还是会用 gulp 来处理,比如单独打包 CSS 文件等。

webpack 是基于入口的,webpack 会自动地递归解析
入口所需要加载的所有资源文件,然后用不同的
Loader 来处理不同的文件,用 Plugin 来扩展 webpack 功能。

grunt 和 gulp 是基于任务和流(Task、Stream)的。
类似 jQuery,找到一个(或一类)文件,对其做一
系列链式操作,更新流上的数据,整条链式操作
构成了一个任务,多个任务就构成了 web 的构建流程。

所以,从构建来说,gulp 和 grunt 需要开发者将整个
前端构建过程拆分成多个Task,并合理控制所有Task
调用关系;webpack 需要开发者找到入口,并需要清楚对
于不同的资源应该使什么 Loader 做何种解析和加工对
于知识背景来说,gulp 更像后端开发者的思路,
需要对于整个流程了如指掌 webpack 更倾向于前端开发者的思路

什么是 bundle,什么是 chunk,什么是 module?

  • bundle:是由 webpack 打包出来的文件
  • chunk:代码块,一个 chunk 由多个模块组合而成,用于代码的合并和分割
  • module:是开发中的单个模块,在 webpack 的世界,一切皆模块,一个模块对应一个文件,webpack 会从配置的 entry 中递归开始找出所有依赖的模块

Loader 和 Plugin 的不同?

不同的作用
Loader 直译为”加载器”。Webpack 将一切文件视为模块,
但是 webpack 原生是只能解析 js 文件,如果想将其他文件
也打包的话,就会用到 loader。 所以 Loader 的作用是让 webpack 拥有了加载和解析非 JavaScript 文件的能力。

Plugin 直译为”插件”。Plugin 可以扩展 webpack 的功能,让 webpack 具有更多的灵活性。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

不同的用法
Loader 在 module.rules 中配置,也就是说他作为模块的解析规则而存在。 类型为数组,每一项都是一个 Object,里面描述了对于什么
类型的文件(test),使用什么加载(loader)和使用的参数(options)

Plugin 在 plugins 中单独配置。 类型为数组,每一项是一个 plugin 的实例,参数都通过构造函数传入。

描述一下编写 loader 或 plugin 的思路?

Loader 像一个”翻译官”把读到的源文件内容转义成新的文件内容,
并且每个 Loader 通过链式操作,将源文件一步步翻译成想要的样子。
编写 Loader 时要遵循单一原则,每个 Loader 只做一种”转义”工作。 每个 Loader 的拿到的是源文件内容(source),可以通过返回值的方式将处理后的内容输出,也可以调用 this.callback()方法,将内容返回给 webpack。 还可以通过 this.async()生成一个 callback 函数,再用这个 callback 将处理后的内容输出出去。 此外 webpack 还为开发者准备了开发 loader 的工具函数集——loader-utils。
相对于 Loader 而言,Plugin 的编写就灵活了许多。 webpack 在运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

构建打包优化(webpack-bundle-analyzer 分析各个打包文件)

速度优化:

  • exclude/include

    通过 exclude、include 配置来确保转译尽可能少的文件

  • cache

  • thread-loader

  • resolve.alias

    webpack 直接根据对应别名的目录查找模块,减少搜索时间

  • 模块懒加载

  • externals + CDN

vite 为什么快?

vite 借助了浏览器对 ESM 规范的支持,采取了和 webpack 不同的 unbundle 机制,unbundle 机制中模块之间的依赖关系的解析是由浏览器实现的(浏览器本身支持 ESM 的规范),不会对源文件做合并捆绑操作
webpack 工作机制:构建模块依赖图,然后再将模块依赖图分解为最终供浏览器使用的几个输出文件。所以 webpack 在这些阶段也做了一些优化,loader 的缓存功能,webpack5 的持久化缓存等

构建 module graph 的过程可以简单归纳为:

获取配置文件中 entry 对应的 url (这个 url 一般为相对路径);
resolve - 将 url 解析为绝对路径,找到源文件在本地磁盘的位置,并构建一个 module 对象;
load - 读取源文件的内容;
transform - 使用对应的 loader 将源文件内容转化为浏览器可识别的类型;
parse - 将转化后的源文件内容解析为 AST 对象,分析 AST 对象,找到源文件中的静态依赖(import xxx from ‘xxx’) 和动态依赖(import(‘xx’))对应的 url, 并收集到 module 对象中;
遍历第 5 步收集到的静态依赖、动态依赖对应的 url,重复 2 - 6 步骤,直到项目中所有的源文件都遍历完成。

分解 module graph 的过程也可以简单归纳为:

预处理 module graph,对 module graph 做 tree shaking;
遍历 module graph,根据静态、动态依赖关系,将 module graph 分解为 initial chunk、async chunks;
优化 initial chunk、 async chunks 中重复的 module;
根据 optimization.splitChunks 进行优化,分离第三方依赖、被多个 chunk 共享的 module 到 common chunks 中;
根据 chunk 类型,获取对应的 template;
遍历每个 chunk 中收集的 module,结合 template,为每个 chunk 构建最后的输出内容;
将最后的构建内容输出到 output 指定位置;

  • vite 的快:快速的冷启动和热更新
  • vite 的慢:首屏性能和懒加载性能
    • 不对源文件做合并捆绑操作,会导致大量的 http 请求
    • 把本该在 dev server 启动时做的工作(resolve、load、transform、parse 等)放在了响应浏览器请求的过程中
    • 但是第二次就快了,再次 reload 页面时 dev server 会将之前已经完成转换的内容缓存起来

前端 webpack 分包

  • 默认情况下 webpack 会将所有代码构建成一个单独的包,随着项目的推进,包体积会越来越大导致应用响应耗时增长。

  • 两个弊端:

    • 资源冗余,用户访问内容只有一部分,但是客户端加载了整个代码包,浪费资源
    • 缓存失效,所有资源打成一个包后只修改一个字符也会导致客户端重新下载整个资源,缓存命中率低
  • 解决:

    • 通过 webpack 的 optimization 属性来配置分包规则

webpack5 和 webpack4 的区别?

可以参考 webpack 官网的从 v4 升级到 v5 的版本迁移解决方案

  • tree Shaking -只支持 esmodule,但是 vue、react 框架都是用 babel-loader 编译的,可以设置 modules:false 防止 babel 将模块类型转译为 commonjs 类型,导致 tree-shaking 失效

    • webpack5 中 mode=’production’自动开启 tree-shaking
  • 代码压缩

    • webpack4 需要下载 terser-webpack-plugin 插件
    • webpack5 内置了这个插件,并在 mode=’production’时自动开启 js 压缩功能
  • sideEffects 副作用

    • optimization.sideEffects = true 开启副作用功能
    • package.json 中设置 sideEffects:false 标记所有模块无副作用
    • webpack 在打包前会检查项目所属的 package.json 文件中的 sideEffects 标识,如果没有副作用那么这个模块就不需要打包。
  • webpack 缓存

    • webpack4 的缓存配置需要安装对应缓存插件
    • webpack5 内置 cache 缓存机制,直接配置。
    • 使用持久化缓存
      cache: {
      type: ‘filesystem’, // cache 会在开发模式下设置成 memory,并在生产模式下把 chache 给禁用掉
      cacheDirectory: path.join(__dirname, ‘node_modules/.cac/webpack’)
      }
  • loader 的优化,通过 type 替换 loader

  • 启动服务

    • webpack4 是通过 webpack-dev-server 启动服务
    • webpack5 内置 webpack serve 启动
  • 模块联邦(微前端)

    • 实现应用程序之间的隔离
  • 热更新

webpack的构建流程

  • 从入口文件entry出发,分析module模块并递归整个项目模块间的依赖关系

  • 加载执行响应loader将module模块解析成webpack能够识别的有效模块,生成模块依赖图

  • 编译过程触发多个事件钩子,执行配置的plugin插件

  • 将解析后的模块分组生成chunk

  • 根据output输出配置生成最后的bundle

评论