WebPack打包
- 1、模块打包工具-由来
- 2、模块打包工具-概要
- 3、webpack-快速上手
- 4、webpack-配置文件
- 5、webpack-工作模式
- 6、Webpack打包结果运行原理
- 7、Webpack资源模块加载
- 8、Webpack导入资源模块
- 9、Webpack文件资源加载器
- 10、Webpack URL加载器
- 11、Webpack 常用加载器分类
- 12、Webpack 处理ES2015
- 13、Webpack 模块加载方式
- 14、Webpack 核心工作原理
- 15、Webpack Loader的工作原理
- 16、Webpack 插件机制
- 17、Webpack 常用插件
- 18、Webpack 插件机制的工作原理
- 19、Webpack 开发体验的设想
- 20、Webpack 增强开发体验
- 21 生产环境优化
- 22 DefinePlugin
- 23 Tree-shaking
- 24 Tree-shaking & Babel
- 25 Webpack sideEffects
- 25 Webpack Code Splitting
- 26 Webpack MiniCssExtractPlugin
- 27 Webpack OptimizeCssAssetsWebpackPlugin
- 28 Webpack 输出文件名 Hash
1、模块打包工具-由来
模块化解决了代码组织问题,但是也会带来一些问题
1、ES Modules存在环境兼容问题
2、模块文件过多,网络请求频繁
3、所有的前端资源都需要模块化
开发阶段ES6 —》编译—》生产阶段ES5S
开发阶段ES6 —》打包—》生产阶段Bundle.js
开发阶段ES6 —》打包—》生产阶段Bundle.js、.css、.png
新特性代码编译
模块化JavaScript打包
支持不同类型的资源模块
2、模块打包工具-概要
webpack、rollup、parcel等
webpack:
1、模块打包器(Module bundler)。本身就可以解决javaScript模块化打包的问题
2、模块加载起(Loader)。对有环境兼容问题的代码进行编译转换
3、代码拆分(Code Splitting)。webpack具有代码拆分的能力,可以将应用所有的代码按照我们的需要去打包。这样一来就不用担心所有的代码打包到一起,产生的文件会比较大的问题。可以把应用加载过程中初次运行的时候所必需的那些模块打包到一起。其他的那些模块在单独存放,等到应用工作过程中实际需要某个模块再异步去加载这个模块,从而实现增量加载或者叫渐进式加载。这样就不用担心文件太碎或者文件太大这两个极端的问题。
4、资源模块(Asset Module)。对于前端模块类型的问题,webpack支持在javascript中以模块化的方式去载入任意类型的文件。
打包工具解决的是前端整体的模块化,并不单指JavaScript模块化。
5、作用:可以让在开发阶段更好享受模块化所带来的优势,又不必担心模块化对生产环境所产生的影响
3、webpack-快速上手
1、webpack是一个npm的工具模块
用serve .
yarn init
yarn add webpack webpack-cli --dev
yarn webpack --version
yarn webpack 从src下的index.js开始打包 到dist目录
也可以在package.json中自定义
"script":{"build": "webpack"}
4、webpack-配置文件
webpack4以后的版本支持0配置方式直接启动打包
约定:src/index.js
->dist/main.js
但是项目中会修改配置路径
根目录下添加webpack.config.js文件。运行在node环境的js文件。所以要遵循commonjs规范
const path = require('path')
module.exports = {
entry: './src/main.js',
output: {
filename: 'bundle.js', // 输出文件名称
path: path.join(__dirname, 'output') // path必须要是绝对路径。在项目根目录下output文件夹下生成bundle.js文件
}
}
5、webpack-工作模式
可以理解成针对不同环境几组预设的配置
yarn build打包过程中会生成一个警告。webpack会默认使用production模式工作。在这个模式下,webpack会自动启动一些优化的插件。如对代码压缩等,对生成环境友好。但是没有办法阅读。可以通过cli参数去指定打包的模式。具体用法就是给webpack命令传入一个–node的参数
yarn webpack --node development
开发模式webpack会自动去优化打包的速度。会添加一些调试过程中需要的辅助到代码中
yarn webpack --node none
webpack会运行最原始状态的打包,不会做任何处理
也可以在webpack配置文件中配置工作模式
// webpack.config.js
const path = require('path')
module.exports = {
// 这个属性有三种取值,分别是 production、development 和 none。
// 1. 生产模式下,Webpack 会自动优化打包结果;
// 2. 开发模式下,Webpack 会自动优化打包速度,添加一些调试过程中的辅助;
// 3. None 模式下,Webpack 就是运行最原始的打包,不做任何额外处理;
mode: 'development',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist')
}
}
警告
WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/
6、Webpack打包结果运行原理
只是把所有模块放到同一个文件当中,它还提供些基础代码,让模块与模块之间的依赖关系还可以保持原有的状态
这边需要yarn webpack后index.html中引入,然后在bundle.js中 Ctrl + K,Ctrl + 0 查看结构。再serve .启动服务在浏览器中打断点调试
7、Webpack资源模块加载
webpack内部默认只处理js文件。也就是说打包时所遇到的所有的文件都当作JavaScript去解析。内部loader只能处理js文件
// wbpack.config.js
yarn add css-loader --dev
yarn add style-loader --dev
const path = require('path')
module.exports = {
mode: 'none',
entry: './src/main.css',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist')
},
module: {
// 针对资源模块加载规则的一个配置
rules: [
{
test: /.css$/, // 匹配打包过程中所遇到的文件路径
//注意执行顺序是从后往前
use: [ // 指定匹配到的文件需要使用的loader
'style-loader', // 把css-loader转换后的结果追加到style标签上
'css-loader' // 将css文件转换为一个js模块
]
}
]
}
}
Loader是webpack实现整个前端模块化的核心
借助Loader就可以加载任何类型的资源
8、Webpack导入资源模块
打包入口-》运行入口
Javascript驱动整个前端应用的业务
1、根据代码的需要动态导入资源
webpack不仅仅建议在js文件中引入css,而是建议在编写代码过程当中引入任何当前代码所需要的资源
2、需要资源的不是应用,而是代码
Javascript驱动整个前端应用
逻辑合理,JS确实需要这些资源文件
确保上线资源不缺失,都是必要的
9、Webpack文件资源加载器
有些资源文件如图片和字体没有办法通过js方式去表示的,所以需要用到文件加载器
yarn add file-loader --dev
工作过程:
1、webpack在打包时遇到了图片文件,
2、然后根据配置文件中的配置,匹配到对应的文件加载器,
3、此时文件加载器开始工作,先是将导入的文件拷贝到输出目录,
4、然后再将文件拷贝到输出目录的那个路径,作为当前这个模块的返回值返回。
5、这样对于应用来说所需要的资源就被发布出来了,同时也可以通过模块导出成员拿到这个资源的访问路径
10、Webpack URL加载器
Data URLs 与 url-loader
除了file-loader通过拷贝物理文件的形式去处理文件资源以外,还有一种Data URL形式表示文件,也很常见
Data URLs是一种特殊的URL协议,它可以直接用来表示一个文件。传统的url要求服务器上有一个对应的文件,通过请求这个地址得到这个服务器上对应的文件。
Data URLs是一种当前url就可以直接去表示文件内容的方式。也就是说这种url中的文本就已经包含了文件的内容,使用这种url就不会发送任何http请求
如:data:text/html;charset=UTF-8,
html content
浏览器可以解析出这是一个html类型的文件内容,编码是UTF-8
图片字体等无法直接通过文本去表示的二进制类型文件,可以通过将文件内容进行base64编码,以编码过后的结果也就是一个字符串表示这个文件的内容
如:data:image/png;base64,iVB0…CC
通过data-ur可以表示任何类型的文件
yarn add url-loader --dev
适合体积比较小的资源,如果资源比较大,那么打包就比较大,会影响运行速度
最佳实践
小文件使用Data URLs,减少请求次数
大文件单独提取存放,提高加载速度
超过10KB文件单独提取存放
小于10KB文件转换为Data URLs嵌入代码中
这边需要注意,使用时url-loader必须要同时安装file-loader
module: {
rules: [
{
test: /.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /.png$/,
use: {
loader: 'url-loader',
options: {
// 10KB以下使用url-lodaer,以上使用file-loader
limit: 10 * 1024 // 10 KB
}
}
}
]
}
11、Webpack 常用加载器分类
加载器处理打包过程中的资源文件
编译转换类:把加载到的资源模块转换成js代码,从而实现通过js运行css
文件操作类:将加载到的资源模块拷贝到输出目录,同时将文件的访问路径向外导出。
代码检查类:统一代码风格,一般不会修改生成环境的代码
12、Webpack 处理ES2015
webpack可以自动处理代码中的import和export,但并不能支持转换es6语法
因为模块打包需要,所以才处理import和export
yarn add babel-loader @babel/core @babel/preset-env --dev
babel 只是转换js代码的一个平台。需要基于babel平台通过不同插件来转换代码中的具体特性
rules: [
{
test: /.js$/,
use: {
loader: 'babel-loader', // 只是转换js代码的一个平台
options: {
presets: ['@babel/preset-env'] // 插件集合包含了全部的ES最新特性
}
}
}
]
13、Webpack 模块加载方式
1、遵循 ES Modules 标准的import声明
import createHeading from './heading.js'
import better from './better.png'
import './main.css'
const heading = createHeading()
const img = new Image()
img.src = better
document.body.append(heading)
document.body.append(img)
2、遵循 CommonJS标准的require函数
⚠️如果通过require函数载入ESModules的话,对于ESModules的默认导出,需要通过require函数的.default去获取
const createHeading = require('./heading.js').default
3、遵循ADM标准的define函数和require函数
define(['./heading.js', './better.png', './main.css'], (createHeading, better) => {
const heading = createHeading.default()
const img = new Image()
img.src = better
document.body.append(heading)
document.body.append(img)
})
require(['./heading.js', './better.png', './main.css'], (createHeading, better) => {
const heading = createHeading.default()
const img = new Image()
img.src = better
document.body.append(heading)
document.body.append(img)
})
不要混合使用
4、Loader加载的非JavaScript也会触发资源加载
a. 样式代码中的@important指令和url函数 也会触发相应的模块资源加载
// main.js中
import './main.css'
@import url(reset.css);
/*css-loader 同样支持 sass/less 风格的 @import 指令*/
/*@import 'reset.css';*/
body {
min-height: 100vh;
background: #f4f8fb;
background-image: url(background.png);
background-size: cover;
}
b. HTML代码中图片标签的src属性 也会触发相应的模块资源加载
<footer>
<!-- <img src="better.png" alt="better" width="256"> -->
<a href="better.png">download png</a>
</footer>
// main.js
import footerHtml from './footer.html' // 默认会将html代码当成字符串导出
document.write(footerHtml)
yarn add html-loader --dev
{
test: /.html$/,
use: {
loader: 'html-loader', // 默认只会处理image标签的src属性
options: {
// 需要其他标签属性也能够触发打包的化,可以添加一些配置
attrs: ['img:src', 'a:href'] // 配置
}
}
}
14、Webpack 核心工作原理
找到打包入口,根据import或require等语句解析推断这个文件所依赖资源模块,分别解析每个资源模块的依赖,有了依赖树,然后webpack会去递归这个依赖树找到每个节点所对应的资源文件。最后根据配置文件中relus属性找到模块对应的加载器,交给这个加载器去加载这个模块。最后把加载到的结果放到打包结果bundle.js中。从而实现整个项目的打包。loader机制很重要。
15、Webpack Loader的工作原理
1、markdown-loader
markdown是被转换成html过后再被呈现到页面上的
每个webpack loader都需要导出一个函数,这个函数就是对loader加载到的资源的一个处理过程
Loader类似管道,结果必需是js代码
Loader负责资源文件从输入到输出的转换
对于一个资源文件可以依次使用多个Loader
yarn add marked --dev markdown解析模块
16、Webpack 插件机制
增强webpack自动化能力
Loader专注实现资源模块加载,从而实现整体项目打包
Plugin解决其他自动化功能
如:清楚dist目录、拷贝静态文件到输出目录、压缩输出代码
webpack+plugin 实现大多数前端工程化工作
webpack=前端工程化
17、Webpack 常用插件
1、clean-webpack-plugin
自动清除目录 yarn add clean-webpack-plugin --dev
2、html-webpack-plugin
1、使用
自动生成使用bundle.js的HTML yarn add html-webpack-plugin --dev 不同于clean插件,它默认导出的就是一个插件类型,不需要解构内部成员
通过webpack输出HTML文件
const path = require('path')
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
// publicPath: 'dist/' 不注释的化,打包后index.html路径就有点问题了
},
module: {
rules: [
{
test: /.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /.png$/,
use: {
loader: 'url-loader',
options: {
limit: 10 * 1024 // 10 KB
}
}
}
]
},
plugins: [
new webpack.ProgressPlugin(),
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'webpack plugins Sample', // 设置html标题
meta: { // 设置页面当中的源数据标签
viewport: 'width=device-width'
},
template: './src/index.html'
})
]
}
2、多实例
1个HtmlWebpackPlugin用于生成1个html文件
plugins: [
new CleanWebpackPlugin(),
// 用于生成 index.html
new HtmlWebpackPlugin({
title: 'Webpack Plugin Sample',
meta: {
viewport: 'width=device-width'
},
template: './src/index.html'
}),
// 用于生成 about.html
new HtmlWebpackPlugin({
filename: 'about.html'
}),
new CopyWebpackPlugin([
// 'public/**'
'public'
])
]
3、copy-webpack-plugin
项目中还有一些不需要参与构建的静态文件,但最终也需要发布到线上,例如favicon。打包时一并复制到输出目录
yarn add copy-webpack-plugin@5.0.4 --dev
// 开发阶段最好不要使用,上线前使用
new CopyWebpackPlugin([ // 传入一个数组用于指定需要拷贝文件的路径
// 'public/**'
'public' // 可以是目录、通配符、相对路径
])
18、Webpack 插件机制的工作原理
相比与Loader,Plugin能力更宽
Plugin 通过钩子机制实现 类似web中事件
webpack工作每个环节都埋下了钩子
webpack插件要求必需是一个函数或者是一个包含apply方法的对象
实例:去除输出文件中不必要的注释
class MyPlugin {
apply (compiler) {
// 接收的对象参数,是webpack工作中最核心的一个对象,
// 包含了此次构建所有配置信息
console.log('MyPlugin 启动')
// 通过这个对象注册钩子函数 参数:函数名称,挂载到钩子上的函数
compiler.hooks.emit.tap('MyPlugin', compilation => {
// compilation => 可以理解为此次打包的上下文,所有打包过程当中产生的结果都会放到这个对象当中
// compilation.assets获取即将写入目录当中的资源文件信息
for (const name in compilation.assets) {
// console.log(name)
// console.log(compilation.assets[name].source())
if (name.endsWith('.js')) { // 判断是否是js文件
const contents = compilation.assets[name].source() // 拿到文件内容
const withoutComments = contents.replace(/\/\*\*+\*\//g, '')
compilation.assets[name] = {
source: () => withoutComments,
size: () => withoutComments.length // size用来返回内容的大小,webpack内部要求要有
}
}
}
})
}
}
通过在生命周期的钩子中挂载函数实现扩展
19、Webpack 开发体验的设想
1、理想的开发环境
以http serve运行
自动编译+自动刷新
提供source map支持
20、Webpack 增强开发体验
1、实现自动编译
watch工作模式,监听文件变化,自动重新打包
用法就是启动webpack时添加一个 --watch 参数
yarn webpack --watch
就会一直监视自动编译,除非手动退出
2、自动刷新浏览器
browser-sync 同时开启 npx webpack --watch
运行 browser-sync dist --files ‘**/*’
发现可以实时更新
操作上太麻烦,效率降低
webpack不断将文件写入磁盘,browser-sync再从磁盘中把它读出来
待改善
3、Webpack Dev Server
提供用于开发的HTTP Server
集成 自动编译 和 自动刷新浏览器 等功能
yarn add webpack-dev-server --dev 提供了一个webpack-dev-server的cli程序
yarn wevpack-dev-server
提高工作效率,没有将打包结果写入磁盘当中
打包结果暂时存放到内存当中,内部的http-server从内存当中把这些文件读出来发送给浏览器,减少很多不必要的磁盘读写操作,提高构建效率
yarn wevpack-dev-server --open 自动唤起浏览器打开运行地址
4、Webpack Dev Server 静态资源访问
Webpack Dev Server 默认会将构建结果输出的文件全部作为开发服务器的资源文件
也就是说:只要是webpack打包输出的文件都可以被访问到。
其他静态资源文件也需要serve
// 为webpack-dev-serve配置选项
devServer: {
// 额外为开发服务器指定查找资源目录
contentBase: './public', //字符串或数组 指定额外的静态资源路径
}
5、Webpack Dev Server 代理API服务
生产环境 应用和api会部署到同源地址下 可以直接访问API
开发环境 开发环境会产生跨域请求问题
使用CORS前提是API支持,并不是任何情况下API都应该支持
Webpack Dev Server支持配置代理
// ======================== fetch proxy api example ========================
const ul = document.createElement('ul')
document.body.append(ul)
// 跨域请求,虽然 GitHub 支持 CORS,但是不是每个服务端都应该支持。
// fetch('https://api.github.com/users')
fetch('/api/users') // http://localhost:8080/api/users
.then(res => res.json())
.then(data => {
data.forEach(item => {
const li = document.createElement('li')
li.textContent = item.login
ul.append(li)
})
})
// 为webpack-dev-serve配置选项
devServer: {
// 额外为开发服务器指定查找资源目录
contentBase: './public', //字符串或数组 指定额外的静态资源路径
// 代理服务配置
proxy: {
'/api': { // 需要被代理的请求路径前缀
// http://localhost:8080/api/users -> https://api.github.com/api/users
target: 'https://api.github.com',
// http://localhost:8080/api/users -> https://api.github.com/users
pathRewrite: {
'^/api': '' // 代理路径重写
},
// 不能使用 localhost:8080 作为请求 GitHub 的主机名
changeOrigin: true // 会以实际代理请求这次发生过程中的主机名去请求
}
}
}
6、Source Map介绍
源代码与生产环境运行代码完全不同
如何调试 错误信息无法定位
调试和报错都是基于运行代码
Source Map 源代码地图 映射转换过后的代码与源代码之间的关系
通过Source Map 逆向解析 得到源代码
目前很多第三方的库在发布过程中都会有一个.map后缀的sourcemap文件
"version": 3,
"sources": ["jquery-3.4.1.js"], // 转换之前的源文件名称
"names": [ // 源代码中使用的一些成员名称
"$",
"noConflict"
],
// 核心属性 base64-vlq编码的字符串 记录的信息就是转换过后代码中的字符与转换之前所对应的映射关系
"mappings": ";CAaA,SAAYA,EAAQC,GAEnB"
有了这个文件之后,一般会在转换过后的代码中通过添加一行注释的方式来引入sourcemap文件。
对生产环境没有什么意思
源文件中最后一行添加注释
//# sourceMappingURL=jquery-3.4.1.min.map
浏览器会自动请求sourcemap文件,根据文件的内容逆向解析出来源代码便于调试
Source Map 解决了源代码与运行代码完全不一致所产生的那些调试的问题
7、Webpack配置Source Map
1、devtool: ‘source-map’
打包时可生成.map的sourcemap文件
可调试与不可调试在浏览器上所呈现出的不同效果如下
Heading~ bundle.js:125
main.js running bundle.js:111
Heading~ heading.js:4
main.js running main.js:14
webpack支持12中不同的方式,效率与效果各不相同
效果最好的一般生成速度最慢
速度最快的生成出来的sourcemap效果最差
2、eval模式下的Source Map
1、eval模式
eval是js中的函数,可以运行字符串中的js代码
eval(console.log(123)
) 默认这段代码会运行在一个临时的虚拟机环境中,可以用sourceURL来声明这段代码所属的文件路径
eval(console.log(123) //# sourceURL=./foo/bar.js
)
意味着可以通过sourceURL来去改变我们通过eval执行的这段代码所属环境的名称,它还是运行在虚拟机环境中 ,只不过告诉执行引擎这段代码所属的文件路径,这只是一个标识
devtool: ‘eval’ yarn webpack打包后yarn serve dist访问 查看信息确实打包后的模块代码。这是因为在这种模式下,它会将每个模块所转换过后的代码都放在eval函数中去执行,并且在这个eval函数的字符串最后通过sourceURL的方式去说明所对应的文件路径,这样浏览器在执行这段代码的时候就知道这段代码所对应的源代码所属文件,从而实现定位错误所出现的文件,只能定位文件。这种模式下不会生成sourcemap文件,和sourcemap没什么关系,所以
其构建速度最快,效果很简单,只能定位文件名称,不能定位行列信息。
3、不同devtool之间的差异
webpack的配置对象可以是一个数组,数组中每一个元素就是单独的打包配置
const HtmlWebpackPlugin = require('html-webpack-plugin')
const allModes = [
'eval',
'cheap-eval-source-map',
'cheap-module-eval-source-map',
'eval-source-map',
'cheap-source-map',
'cheap-module-source-map',
'inline-cheap-source-map',
'inline-cheap-module-source-map',
'source-map',
'inline-source-map',
'hidden-source-map',
'nosources-source-map'
]
module.exports = allModes.map(item => {
return {
devtool: item,
mode: 'none', // 确保wepack内部不会做额外的处理
entry: './src/main.js',
output: {
filename: `js/${item}.js`
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader', // 为了对比可以辨别其中一类的差异
options: {
presets: ['@babel/preset-env']
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
filename: `${item}.html`
})
]
}
})
// module.exports = [
// {
// entry: './src/main.js',
// output: {
// filename: 'a.js'
// }
// },
// {
// entry: './src/main.js',
// output: {
// filename: 'b.js'
// }
// }
// ]
yarn webpack
yarn serve dist
1、eval-source-map
除了定位到出问题的文件,还可以定位到行列信息,因为在这种模式下生成了sourcemap
2、cheap-eval-source-map
阉割版的sourcemap,只能定位到行没有列,显示的是es6转换过后的结果
3、cheap-module-eval-source-map
只能定位到行 显示的和源代码一样的代码
带有module的模式解析出来的源代码没有经过loader加工,
不带module的模式解析出来的是加工过后的代码
小点:
eval 是否使用eval执行模块代码
cheap Source Map是否包含行信息
module 是否能够得到Loader处理之前的源代码
4、inline-source-map
与普通source-map效果一样。只不过source-map模式下,sourcemap文件是以物理方式存在
inline-source-map使用的是DataURL的方式将sourcemap以dataURL嵌入到代码当中。eval-source-map 也是使用inline的这种方式把sourcemap嵌入进来。会导致体积大很多。最不可能用到
5、hidden-source-map
开发工具中看不到sourcemap的效果,但是生成了sourcemap文件,和jquery一样,代码中并没有通过注释的方式引入这个文件。开发第三方包的时候会用到
6、nosource-source-map
能看到错误出现的位置,但是点击错误信息进去之后看不到源代码,但是提供了行列信息。为了在生产环境当中保护源代码不会被暴露的情况。
8、选择合适的Source Map
个人经验
开发模式 cheap-module-eval-source-map
原因:
代码每行不超过80个字符
代码经过Loader转换过后差异较大
首次打包速度慢无所谓,重写打包相对较快
生成模式 none
原因:
source Map 会暴露源代码
调试时开发阶段的事情
对代码没有信心可以选择:nosource-source-map
9、自动刷新的问题
Webpack Dev Server 提供对开发者友好的开发服务器
问题如调试编辑器中文本样式
每次修改完代码,webpack监视文件变化过后就会自动打包然后自动刷新到浏览器 然后编辑器中操作内容会丢失
办法1:代码中写死编辑器的内容
办法2:额外代码实现刷新前保存,刷新后读取
问题核心:自动刷新导致的页面状态丢失
要实现页面不刷新的前提下,模块也可以及时更新
10、Webpack HMR体验
Hot Module Replacement 模块热更新
热拔插:在一个正在运行的机器上随时插拔设备,如电脑上的usb接口
模块热替换:应用运行过程中实时替换某个模块,应用运行状态不受影响
热替换只将修改的模块实时替换至应用中,不必完全刷新应用
HMR是 Webpack中最强大的功能之一,极大提高了开发者的工作效率
11、开启 HMR
集成在webpack-dev-server 中
webpack-dev-server --hot
也可配置webpack.config.js
const webpack = require('webpack') // 第二步
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'development',
entry: './src/main.js',
output: {
filename: 'js/bundle.js'
},
devtool: 'source-map',
devServer: {
hot: true // 第一步
// hotOnly: true // 只使用 HMR,不会 fallback 到 live reloading
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.(png|jpe?g|gif)$/,
use: 'file-loader'
}
]
},
plugins: [
new HtmlWebpackPlugin({
title: 'Webpack Tutorial',
template: './src/index.html'
}),
// webpack内置的插件
new webpack.HotModuleReplacementPlugin() // 第三步
]
}
yarn webpack-dev-serve --open 启动
发现修改css热更新了,但是修改js文件却刷新了
12、HMR的疑问
Webpack中的HMR并不可以开箱即用,还需要做些额外的操作
Webpack中的HMR需要手动处理模块热替换逻辑
Q1 为什么样式文件的热更新开箱即用
这是因为样式文件是经过loader处理的,style-loader自动处理了热更新
Q2 凭什么样式可以自动处理
因为在样式模块更新过后,他只需要把更新过后的css及时替换到页面当中就可以覆盖掉之前的样式,从而实现样式文件的更新
而编写js模块是没有任何规律的,因为在一个模块中可能导出对象、字符串、函数等,对导出成员的使用也各不相同。所以webpack不知如何处理更新过后的模块,也就没法实现通用的模块替换方案
Q3 项目没有手动处理,JS照样可以热替换
因为使用了某个框架,框架下的开发,每种文件都是有规律的 如react中要求每个模块必须导出一个函数或者一个类。
通用脚手架创建的项目内部都集成了HMR方案
总结:需要手动处理JS模块更新后的热替换
13、HMR APIS
Hot Module Replacement Plugin为js提高了一套用于处理HMR的API。在js代码处理当某个模块更新过后如何替换到当前运行的页面当中
// 打包入口文件main.js中
import createEditor from './editor'
import background from './better.png'
import './global.css'
const editor = createEditor()
document.body.appendChild(editor)
const img = new Image()
img.src = background
document.body.appendChild(img)
// ============ 以下用于处理 HMR,与业务代码无关 ============
// console.log(createEditor)
// 这套API中为module对象提供了一个hot属性,也是一个对象,就是HMR API的核心对象
if (module.hot) { // 没启动HMR的情况下HMR API报错的问题
// 处理js模块热更新
let lastEditor = editor
// accept方法用于注册当某一个模块更新过后的处理函数
// 参数:依赖模块路径,依赖路径更新过后的处理函数
module.hot.accept('./editor', () => {
// console.log('editor 模块更新了,需要这里手动处理热替换逻辑')
// console.log(createEditor)
const value = lastEditor.innerHTML
document.body.removeChild(lastEditor)
const newEditor = createEditor()
newEditor.innerHTML = value
document.body.appendChild(newEditor)
lastEditor = newEditor
})
// 处理图片模块热更新
module.hot.accept('./better.png', () => {
img.src = background // 拿到更新过后的路径
console.log(background)
})
}
14、HMR 注意事项
1、处理HMR的代码报错会导致自动刷新,不容易发现
默认hot方式如果热替换失败就会回退使用自动刷新的功能
如果hotonly情况则不会使用自动刷新,会看到错误信息
hotOnly: true // 只使用 HMR,不会 fallback 到 live reloading
2、没启动HMR的情况下,HMR API报错
module.hot这个对象时HMR插件提供的
3、代码中多了一些与业务无关的代码。
不会影响生产环境的代码
21 生产环境优化
生产环境注重运行效率,开发环境注重开发效率
webpack4 模式(mode)提供了不同模式下的预设配置,其中生产模式中就已经包括了很多在生产环境中所需要的优化配置
1、不同环境下的配置
方式1:配置文件根据环境不同导出不同配置
webpack配置文件支持导出一个函数,函数中返回所需的对象
// env 通过cli传递的环境名参数
// argv 运行cli过程中所传递的所有参数
module.exports = (env, argv) => {}
webpack.config.js
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = (env, argv) => {
const config = {}
if (env === 'production') {
config.mode = 'production'
config.devtool = false // 禁用source-map
config.plugins = [
...config.plugins,
new CleanWebpackPlugin(), // 开发阶段可以省略的插件
new CopyWebpackPlugin(['public']) // 开发阶段可以省略的插件
]
}
return config
}
yarn webpack // 默认以开发模式运行打包
yarn webpack --env production // 以生产模式运行打包
方式2:多配置文件 一个环境对应一个配置文件
Object.assgin方法会完全覆盖掉前一个对象中的同名属性。值类型覆盖没有问题,但是像配置中的plugins这种数组,希望在公共原有配置之上添加一两个插件。但这个方法会覆盖所以不合适
yarn add webpack-merge --dev
// webpack.prod.js
const merge = require('webpack-merge')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const common = require('./webpack.common') // 专门用来满足合并webpack配置需求 导出一个merge函数,会自动处理合并逻辑
module.exports = merge(common, {
mode: 'production',
plugins: [
new CleanWebpackPlugin(),
new CopyWebpackPlugin(['public'])
]
})
// webpack.dev.js
const webpack = require('webpack')
const merge = require('webpack-merge')
const common = require('./webpack.common')
module.exports = merge(common, {
mode: 'development',
devtool: 'cheap-eval-module-source-map',
devServer: {
hot: true,
contentBase: 'public'
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
})
// webpack.common.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './src/main.js',
output: {
filename: 'js/bundle.js'
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.(png|jpe?g|gif)$/,
use: {
loader: 'file-loader',
options: {
outputPath: 'img',
name: '[name].[ext]'
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
title: 'Webpack Tutorial',
template: './src/index.html'
})
]
}
运行时,因为没有了默认的配置文件,所以运行webpack时需要通过–config 参数来指定配置文件 yarn webpack --config webpack.prod.js
22 DefinePlugin
为代码注入全局成员
production模式下,默认这个插件就会启用起来,并且往代码中注入了一个process.env.NODE_ENV常量。很多第三方模块都是通过这个成员判断当前的运行环境,从而去决定是否去执行例如打印日志这样的一些操作
// webpack.config.js
const webpack = require('webpack')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js'
},
plugins: [
// 内置的插件
new webpack.DefinePlugin({ // 这个插件构造函数接收一个对象。对象中每一个键值都会被注入到代码当中
// 值要求的是一个代码片段
API_BASE_URL: JSON.stringify('https://api.example.com') // 用来为代码注入API服务地址 或者其他用处
})
]
}
23 Tree-shaking
"摇掉"代码中未用到的部分或者说是为引用代码(dead-code)
webpack生产模式中就有这么一个非常有用的功能,可以自动检查出代码中未引用的代码然后移除掉它们
使用
Tree Shaking 不是指某个配置选项,是一组功能搭配使用后的优化效果,生产模式下自动启动
// webpack.config.js
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
// 该属性是集中配置webpack中的优化功能
optimization: {
// 模块只导出被使用的成员
usedExports: true, // 负责标记枯树叶枯树枝
// 普通打包结果是将每一个模块单独放在一个函数当中,模块很多函数就很多
// 尽可能将多有模块合并输出到一个函数中,既提升了运行效率又减少了输出代码体积 这个特性又被称之为 Scope Hoisting也就是作用域提升,是webpack3中添加的特性
// concatenateModules: true,
// 压缩输出结果,此时未引用到的代码就被移除掉了
minimize: true // 负责摇掉它们
}
}
24 Tree-shaking & Babel
很多资料表示如果使用了babel-loader会导致 Tree-shaking 实现。
这里特别说明一下
Tree-shaking 的实现的前提是必需使用ES Modules组织代码 也就是说
由 Webpack打包的代码必须使用 ESM方式来去实现的模块化 为什么这么说呢
我们都知道webpack在打包所有模块之前,先是将模块根据配置交给不同的loader去处理,最后再将所哟loader处理后的结果打包到一起。Babel处理代码时就有可能处理掉代码中ES Modules,把它们转换成CommonJS。当然这取决于我们有没有使用转换ESModules的插件,例如项目中使用的preset-env里面就又这么个插件。最后webpack打包时拿到的就是CommonJS组织的代码。所以Tree-shaking不能生效。
但是最新的Babel-loader版本中自动去除了ESModules转换的插件
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
// 如果 Babel 加载模块时已经转换了 ESM,则会导致 Tree Shaking 失效
// ['@babel/preset-env', { modules: 'commonjs' }] // 强制转换成CommonJS
// ['@babel/preset-env', { modules: false }] // 确保不会转换
// 也可以使用默认配置,也就是 auto,这样 babel-loader 会自动关闭 ESM 转换
['@babel/preset-env', { modules: 'auto' }] // 注意是个数组
]
}
}
}
]
},
optimization: {
// 模块只导出被使用的成员
usedExports: true,
// 尽可能合并每一个模块到一个函数中
// concatenateModules: true,
// 压缩输出结果
// minimize: true
}
}
25 Webpack sideEffects
副作用:模块执行时除了导出成员之外所做的事情
webpack4中新增。可以通过的配置的方式去标识代码是否有副作用,从而为TreeShaking提供更大的压缩空间
sideEffects一般用于npm包标记是否有副作用
这会导致所有components下的组件都会被加载执行
webpack.config.js
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
},
optimization: {
// 开启这个特性后,webpack在打包时就会先检查当前代码所属的package.json中有没有sideEffects的标识,以此来判断这个模块是否有副作用,如果这个模块没有副作用,那这些没有用到的模块就不再会打包。
sideEffects: true, // production模式下自动开启
// 模块只导出被使用的成员
// usedExports: true,
// 尽可能合并每一个模块到一个函数中
// concatenateModules: true,
// 压缩输出结果
// minimize: true,
}
}
package.json中
// false 标识当前package.json所影响的这个项目它当中所有的代码都没有副作用。那么没有用到的模块就不再会被打包
"sideEffects": false
注意:使用sideEffects的前提就是确保代码中真的没有副作用,否则的话,在webpack打包时就会误删掉那些有副作用的代码
//extend.js
// 为 Number 的原型添加一个扩展方法 副作用原型上多个方法
Number.prototype.pad = function (size) {
// 将数字转为字符串 => '8'
let result = this + ''
// 在数字前补指定个数的 0 => '008'
while (result.length < size) {
result = '0' + result
}
return result
}
这样因为声明中没有副作用,所以上述代码就会被移除
// index.js
import { Button } from './components'
// 样式文件属于副作用模块
import './global.css'
// 副作用模块
import './extend' // 因为没有导出,所以不需要提取任何成员
console.log((8).pad(3))
document.body.appendChild(Button())
解决pakcage.json中关掉副作用或者标识项目中哪些文件有副作用
"sideEffects": [
"./src/extend.js",
"*.css"
]
这样有副作用的两个模块也会被打包进来
25 Webpack Code Splitting
代码分割
webpack打包弊端:所有代码最终都被打包到一起,bundle体积过大。
并不是每个模块在启动时都是必要的。
分包,按需加载,提高应用运行速度与响应效率
http1.1 同域并行请求限制,每次请求都会有一定的延迟,请求的Header浪费带宽流量
模块打包是必要的
分包方式
1、多入口打包
2、动态导入
1、多入口打包
多页应用程序,一个页面对应一个打包入口,公共部分单独提取
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'none',
entry: { // 对象 配置两个入口
index: './src/index.js',
album: './src/album.js'
},
output: {
filename: '[name].bundle.js'// [name]占位符的方式动态输出文件名index/album
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/index.html',
filename: 'index.html',
chunks: ['index'] // 指定输出的html所使用的bundle
}),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/album.html',
filename: 'album.html',
chunks: ['album']
})
]
}
问题:不同的打包结果中会有相同的模块实现
2、提取公共模块
optimization: {
splitChunks: {
// 自动提取所有公共模块到单独 bundle
chunks: 'all'
}
},
打包后会生成公共的部分album~index.bundle.js
3、动态导入与魔法注释
按需加载:应用运行过程中需要某个模块时,再加载这个模块.
可以极大减少带宽和流量
动态导入的模块会被自动分包
vue中可以在路由映射组件中使用
通过import函数
// import posts from './posts/posts'
// import album from './album/album'
const render = () => {
const hash = window.location.hash || '#posts'
const mainElement = document.querySelector('.main')
mainElement.innerHTML = ''
if (hash === '#posts') {
// mainElement.appendChild(posts())
// 魔法注释 给分包所产生的bundle起名字,起相同的名字就会被打包到一起
import(/* webpackChunkName: 'components' */'./posts/posts').then(({ default: posts }) => {
mainElement.appendChild(posts())
})
} else if (hash === '#album') {
// mainElement.appendChild(album())
import(/* webpackChunkName: 'components' */'./album/album').then(({ default: album }) => {
mainElement.appendChild(album())
})
}
}
render()
window.addEventListener('hashchange', render)
26 Webpack MiniCssExtractPlugin
提取css到单个文件
webpack内置的压缩仅仅是针对js文件
27 Webpack OptimizeCssAssetsWebpackPlugin
压缩输出的css文件
yarn add mini-css-extract-plugin --dev
yarn add optimize-css-assets-webpack-plugin --dev
yarn add terser-webpack-plugin --dev
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
// 自动提取css到一个文件中
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
// 压缩输出的css文件
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')
module.exports = {
mode: 'none',
entry: {
main: './src/index.js'
},
output: {
filename: '[name].bundle.js'
},
optimization: {
minimizer: [ // 使用了这个数组,webpack会认为需要自定义所使用的压缩插件,内部的js压缩器就会被覆盖掉,需要手动配置
new TerserWebpackPlugin(), // webpack内置的js压缩插件
// 如果这个插件配置plugins中,那这个插件在任何情况下都会正常工作,而配置minimizer中只会在minimizer这个特性开启时才会工作。 yarn webpack --mode production 该压缩插件就会自动工作。如此也会有问题,原本压缩的js却不能自动压缩了,
new OptimizeCssAssetsWebpackPlugin()
]
},
module: {
rules: [
{
test: /\.css$/,
use: [
// 'style-loader', // 将样式通过 style 标签注入
MiniCssExtractPlugin.loader, // 样式存放在单独文件之中就不需要style标签,而是通过link方式注入。如果样式不是很大(<150kb)提取到单个文件中效果可能适得其反
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Dynamic import',
template: './src/index.html',
filename: 'index.html'
}),
new MiniCssExtractPlugin()
]
}
28 Webpack 输出文件名 Hash
substitutions
一般部署前端资源文件时会启用服务器静态资源缓存,这样对于用户浏览器而言就可以缓存住前端的静态资源,响应速度提升
如果缓存时间设置过短不明显,过长的话不能及时更新
生产模式下,文件名使用 Hash,一旦资源文件改变,文件名称也会改变,对于客户端而言,全新的文件名也就是全新的请求也就没有缓存的问题。这样就可以把服务端缓存策略中时间设置的非常长,不用担心文件更新过后的问题
webpack中filename属性和绝大多数插件的filename属性都支持通过占位符的方式来去为文件名设置hash,支持3种hash效果各不相同
1、[name]-[hash].bundle.js 这是项目级别的hash,也就是项目中有任何地方的改动这个打包的hash都会发生变化
2、[name]-[chunkhash].bundle.js chunk级别的hash 打包过程当中只要是同一路的打包,那么chunkhash都是相同的
3、[name]-[contenthash].bundle.js 文件级别的hash,根据文件内容生成的hash,不同的文件就有不同的hash值。解决缓存最好的方式
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')
module.exports = {
mode: 'none',
entry: {
main: './src/index.js'
},
output: {
filename: '[name]-[contenthash:8].bundle.js'
},
optimization: {
minimizer: [
new TerserWebpackPlugin(),
new OptimizeCssAssetsWebpackPlugin()
]
},
module: {
rules: [
{
test: /\.css$/,
use: [
// 'style-loader', // 将样式通过 style 标签注入
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Dynamic import',
template: './src/index.html',
filename: 'index.html'
}),
new MiniCssExtractPlugin({
filename: '[name]-[contenthash:8].bundle.css'
})
]
}