- Webpack 中 一切(JS, 图片…)皆为模块。
- Webpack 配置灵活 & 具备强大的插件化扩展能力。
- Webpack 需要了解诸多概念
- entry
- output
- mode
- loaders
- plugins
- 热更新
- code spliting
- tree shaking
- …
- Webpack 的学习曲线
- Webpack 打包速度
- Webpack 打包体积
- 页面加载时性能优化
- …
本文档分为四大章节
1. 基础篇
掌握 Webpack 的核心概念和开发必备技巧
2. 进阶篇
编写 Webpack 的构建配置并掌握优化策略
3. 原理篇
剖析内部运行原理并编写自定义Loader & 插件
4. 实战篇
通过 Webpack 商城项目实战牢固知识
目录
01 | 基础篇:Webpack 与 构建发展史
02 | 基础篇:Webpack 基础用法
03 | 基础篇:Webpack 进阶用法
04 | 进阶篇:编写可维护的 Webpack 构建配置
05 | 进阶篇:Webpack 构建速度和体积优化策略
06 | 原理篇:通过源码掌握 Webpack 打包原理
07 | 原理篇:编写 Loader 和插件
08 | 实战篇:React 全家桶和 webpack 开发商城项目
01 | 基础篇:Webpack 与 构建发展史
Q: 为什么需要构建工具?
A: 转换 ES6 语法 & 转换JSX & CSS 前缀补全/预处理器 & 压缩混淆 & 图片压缩 …
前端构建演变之路
Q: 为什么选择Webpack?
A:
社区生态活跃
配置灵活 & 插件化扩展
官方更迭速度快
初识 Webpack: 配置文件名称
webpack 默认配置文件: webpack.config.js
可以通过 webpack --config 指定配置文件
初始 Webpack:Webpack 配置组成
Q: 零配置 webpack 包含那些内容?
A:
环境搭建:安装 Node.js 和 NPM
安装 nvm (https://github.com/nvm-sh/nvm)(node版本管理工具)
- 通过 curl 安装
curl -o- https://raw.gethubsuercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash
- 通过 wget 安装
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash
注意配置 nvm 环境变量
安装 Node.js 和 NPM
nvm install v10.15.3
- 检查是否安装成功
node -v
npm -v
环境搭建: 安装 webpack 和 webpack-cli
创建空目录和 package.json
mkdir my-project
cd my-project
npm init -y
安装 webpack 和 webpack-cli
npm install webpack webpack-cli --save-dev
// 检查是否安装成功
./node_module/.bin/webpack -v
一个简单的例子
通过 npm script 运行 webpack
02 | 基础篇:Webpack 基础用法
介绍 entry output loader plugins mode 在实际中如何使用
介绍 代码压缩 & 代码指纹如何处理
介绍 代码解析 JS JSX less 等如何解析
介绍 如何实现热更新,几种常用的热更新方式以及差异
核心概念之 Entry
- entry 用来指定 webpack 的打包入口
- 依赖的入口是 entry
- 对于非代码(图片/ 字体)也会不断加入到依赖图中
Entry 的用法
核心概念之 Output
- Output 用来告诉 webpack 如何将编译后的文件输出到磁盘
Output 的用法:单入口配置
Output 的用法:多入口配置
核心概念之 Loaders
- webpack 开箱即用只支持 JS 和 JSON 两种文件类型, 通过 Loaders 去支持其他文件类型并且把他们转化为有效的模块,并且可以添加到依赖图中。
- 本身是一个函数,接收源文件作为参数,返回转换的结果。
常见的 Loaders 有哪些?
Loader 的用法
核心概念之 Plugins
- 插件用于 bundle 文件的优化, 资源管理和环境变量注入
- 作用于整个构建过程
常见的 Plugins 有那些?
Plugins 的用法
核心概念之 Mode
- Mode 用来指定当前构建环境是:production/ development/ none
- 设置 mode 可以使用 webpack 内置的函数, 默认值为 production
Mode 的内置函数功能
资源解析: 解析 ES6
使用 babel-loader
babel 的配置文件是:.babelrc
资源解析: 增加 ES6 的babel preset 配置
安装 babel
// i: install D: --save-dev
npm i @babel/core @babel/preset-env babel-loader -D
资源解析: 解析React JSX
安装 react 相关库
npm i react react-dom @babel/preset-react -D
资源解析: 解析 CSS
css-loader 用于加载 .css 文件, 并且转换成 commonjs对象
style-loader 将样式通过<style>标签插入到 head 中
*要注意 loader调用是链式调用执行顺序是从右到左 因此要将css-loader放在右边,这样css-loader会先执行
安装 css 相关
npm i style-loader css-loader -D
资源解析:解析 Less 和 SaSS
less-loader 将用于将 less 转换成 css
安装 less 相关
npm i less less-loader -D
资源解析: 解析图片
file-loader 用于处理文件
安装 file 相关
npm i file-loader -D
资源解析: 解析字体
file-loader 也可以用于处理字体
资源解析: 使用 url-loader
url-loader 也可以处理图片和字体
可以设置较小资源自动 base64
url-loader 内部用的也是 file-loader
安装 url-loader
npm i url-loader -D
Webpack 中的文件监听
文件监听是在发现源码发生变化时,自动重新构建出新的输出文件。
webpack 开启监听模式, 有两种办法:
- 启动 webpack 命令时, 带上 --watch 参数
- 在配置 webpack.config.js 中设置 watch: true
webpack 中的文件监听使用
唯一的缺陷: 每次需要手动刷新浏览器
文件监听的原理分析
轮询判断文件的最后编译时间是否发生变化
某个文件发生了变化,并不会立刻告诉监听者,而是先缓存起来,等aggregateTimeout
热更新: webpack-dev-server
- WDS 不刷新浏览器
- WDS 不输出文件,而是放在内存中
- 使用 HotModuleReplacementPlugin 插件
安装 webpack-dev-server
npm i webpack-dev-server -D
// 注意版本对应,若执行报错,可能是版本不匹配
// 下面是卸载命令
npm uninstall webpack-dev-server-D
热更新:使用webpack-dev-middleware
WDM 将 webpack 输出的文件传输给服务器
适用于灵活的定制场景
热更新的原理分析
webpack Compile: 将 JS 编译成 Bundle
HMR Server: 将热更新的文件输出给 HMR Runtime
Bundle server: 提供文件在浏览器访问
HMR Runtime: 会被注入到浏览器,更新文件的变化
bundle.js: 构建输出的文件
什么是文件指纹?
打包后输出的文件名的后缀
进行版本管理
文件指纹是如何生成?
- hash: 和整个项目的构建相关, 只要项目文件有修改,整个项目构建的 hash 值就会更改
- Chunkhash:和 webpack 打包的 chunk 有关,不同的 entry 会生成不同的 chunkhash 值
- ContentHash: 根据文件内容来定义 hash , 文件内容不变,则 contenthash 不变 (对于CSS 一般使用 ContentHash)
JS 的文件指纹设置
设置 output 的 filename, 使用[chunkhash]
CSS 的文件指纹设置
设置 MiniCssExtracPlugin 的 filename, 使用[contenthash]
因为,我们通过css-loader解析后直接通过 style-loader 将css文件插入到style标签并且放到到 head 头部,并没有一个css文件,于是使用 这种方法,将style-loader的css提取出来成一个css文件。
安装 mini-css-extract-plugin -D
npm i mini-css-extract-plugin -D
注意 mini-css-extract-plugin 和 style-loader 是互斥的,一个是要提取,一个是放在 head 中
将MiniCssExtractPlugin.loader 替换 style-loader
图片的文件指纹设置
设置 file-loader 的 name, 使用 [hash]
这里的 [hash] 和之前 js css 设置文件指纹中的 hash 是不一样的
md5 默认 32 位,这里取前 8 位
图片和字体的设置是一样的
代码压缩
- HTML 压缩
- CSS 压缩
- JS 压缩
JS 文件的压缩
内置了 uglifyjs-webpack-plugin
CSS 文件的压缩
使用 optimize-css-assets-webpack-plugin
同时使用 cssnano
安装 css 压缩相关
npm i optimize-css-assets-webpack-plugin -D
npm i cssnano -D
HTML 文件的压缩
修改 html-webpack-plugin, 设置压缩参数
安装 html 压缩相关
npm i html-webpack-plugin -D
template:指定模板所在位置
filaname:打包出来后 html 的名称
chunks:指定生成的 html 使用那些 chunk
inject: css js 会自动注入到 html 中
当前构建时的问题
- 每次构建的时候不会清理目录,造成构建目录的输出目录 output 文件越来越多
通过 npm scripts 清理构建目录
rm -rf ./dist && webpack
rimraf ./dist && webpack
自动清理构建目录
避免构建前每次都需要手动删除 dist
使用 clean-webpack-plugin: 默认会删除 output 指定的输出目录
安装 clean-webpack-plugin
npm i clean-webpack-plugin -D
CSS3 的属性为什么需要前缀?
举个例子:
PostCSS 插件 autoprefixer 自动补齐 CSS3 前缀
使用 autoprefixer 插件
根据 Can I Use 规则 ( https://caniuse.com)
安装 补全 相关
npm i postcss-loader autoprefixer -D
浏览器的分辨率
CSS 媒体查询实现响应式布局
缺陷: 需要写多套适配样式代码
rem 是什么?
W3C 对 rem 的定义: font-size of root element
rem 和 px 的对比
- rem 是相对单位
- px 是绝对单位
移动端 CSS px 自动转换成 rem
使用 px2rem-loader
页面渲染时计算根元素的 font-size 值
- 可以使用手淘的 lib-flexible 库
- https://github.com/amfe/lib-flexible
安装 px2rem-loader lib-flexible
npm i px2rem-loader -D
npm i lib-flexible -S // -S: 安装到项目依赖中
资源内联的意义
代码层面:
- 页面框架的初始化脚本
- 上报相关打点
- css 内联避免页面闪动
请求层面: 减少 HTTP 网络请求数
- 小图片或者字体内联(url-loader)
HTML 和 JS 内联
raw-loader 内联 html
raw-loader 内联 JS
CSS 内联
- 方案1:借助 style-loader
- 方案2:html-inline-css-webpack-plugin
安装 raw-loader
npm i raw-loader@0.5.1 -D
多页面应用(MPA)概念
每一次页面跳转的时候,后台服务器都会给返回一个新的 html 文档, 这种类型的网站也就是多页网站,也叫多页应用。
优点: 页面间解耦, SEO更加友好
多页面打包基本思路
每个页面对应一个 entry , 一个 html-webpack-plugin
缺点: 每次新增或删除页面需要改 webpack 配置
多页面打包通用方案
动态获取 entry 和设置 html-webpack-plugin 数量
利用 glob.sync
- entry: glob.sync(path.join(__dirname, ‘./src/*/index.js’))
安装 打包相关
npm i glob -D
使用 source map
作用: 通过 source map 定位到 源代码
开发环境开启,线上环境关闭
- 线上排查问题的时候可以将 sourcemap 上传到错误监控系统
source map 关键字
- eval: 使用eval包裹模块代码
- source map: 产生.map 文件
- cheap: 不包含列信息
- inline: 将.map 作为DataURI 嵌入,不单独生成.map文件
- module: 包含loader的sourcemap
souce map 类型
基础库分离
思路: 将 react/ react-dom 基础包通过 cdn 引入, 不打入 bundle中
方法: 使用html-webpack-externals-plugin
利用 SplitChunksPlugin 进行公共脚本分离
Webpack4 内置的, 替代CommonsChunkPlugin 插件
chunks 参数说明:
- async 异步引入的库进行分离(默认)
- initial 同步引入的库进行分离
- all 所有引入的库进行分离(推荐)
利用 SplitChunksPlugin 分离 基础包
test: 匹配出需要分离的包
安装 html-webpack-externals-plugin
npm i html-webpack-externals-plugin -D
利用 SplitChunksPlugin 分离页面公共文件
minChunks: 设置最小引用次数为2次
minuSize:分离的包体积的大小
tree shaking(摇树优化)
-
概念:1个模块可能有多个方法,只要其中的某个方法用到了,则整个我呢见都会被打到bundle里面去,tree shaking 就是只把用到的方法打到bundle,没有用到的方法会在uglify 阶段被擦除掉。
-
使用:webpack默认支持,在.babelrc 里面设置 modules: false 即可production mode 的情况下默认开启
-
要求:必须是 ES6 的语法, CJS的方式不支持
DCE (Elimination)
- 代码不会被执行,不可到达
- 代码执行的结果不会被用到
- 代码只会影响死变量(只写不读)
Tree-shaking 原理
利用 ES6 模块的特点:
- 只能作为模块顶层的语句出现
- import 的模块名只能是字符串常量
- import binding 是 immutable 的
代码擦除: uglify 阶段删除无用代码
Scope Hoisting 使用和原理分析
现象: 构建后的代码存在大量闭包代码
会导致什么问题?
大量函数闭包包裹代码,导致体积增大(模块越多越明显)
运行代码时创建的函数作用域变多,内存开销变大
模块转换分析
结论:
- 被 webpack 转换后的模块会带上一层包裹
- import 会被转换成__webpack_require
进一步分析 webpack 的模块机制
分析:
- 打包出来的是一个 IIFE (匿名闭包)
- modules 是一个数组,每一项是一个模块初始化函数
- __webpack_require 用来加载模块,返回module.exports
- 通过 WEBPACK_REQUIRE_METHOD(0) 启动程序
scope hoisting 原理
- 原理: 将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以放置变量名冲突
- 对比:通过 scope hoisting 可以减少函数声明代码和内存开销
scope hoisting 使用
webpack mode 为 production 默认开启
必须是 ES6 语法, CJS 不支持
代码分割的意义
对于大的 web 应用而言,所有的代码放在一个文件中显然是不够有效的,特别是当你的某些代码块是在某些特殊的情况下才会使用到。webpack提供一个功能就是将你的代码库分割成 chunks ,当代码运行到需要它们的时候再进行加载。
使用场景:
- 抽离相同代码到一个共享块
- 脚本懒加载,使得初始下载的代码更小
懒加载 JS 脚本的方式
CommonJS: require.ensure
ES6: 动态 import (目前没有原生支持,需要babel转换)
如何使用动态 import?
- 安装babel 插件
npm install @babel/plugin-syntax-dynamic-import -D
- ES6 动态 import (目前没有原生支持,需要babel转换)
{
"plugins": ["@babel/plugin-syntax-dynamic-import"],
}
** webpack 如何打包库和组件 **
webpack 除了可以用来打包应用,也可以用来打包 js 组件和库
实现一个大整数加法库的打包
- 需要打包压缩版和非压缩版
- 支持 AMD/CJS/ESM模块引入