My Webpack5 Advanced notes
# 0、课程介绍
# 为什么学习 Webpack?
# 为什么是 Webpack 呢?
而全新版本 webpadk5 则是具备了比以往版本更强大的功能,甚至是诸多企业级前端工程化的技术选型的不二选择。
# 学习 Webpack5 的前提?
_ 前端基础知识:htm、css、es6 以及以上的版本
_ nodejs 和工程化
# 课程安排?
# 课程收获?
# 第一章:为什么需要 webpack
# 1.0 为什么需要 webpack
我们先回顾一下历史,在打包工具出现之前,我们是如何的在 web 网页中去使用 JavaScript 代码的?
我们先来看一个文档:
这是一个 html 文档,在这个文档中,我们共加载了 11 个 js 文件,这 11 个 js 文件,我们又可以分为两部分来看:
第一部分:是引入的外部或者叫第三方的库以及框架文件。这里我们引入了 jquery lodash bootstrap 等等。jquery 和 lodash 是第三方的 js 库,可以提高我们项目的开发效率。bootstrap 是第三方的 U 框架,可以协助我们做页面的布局,以及交互等等。
第二部分是我们自己的 JavaScript 文件,是项目的业务代码。比如 commonjs,一些公共的文件,user 用户相关的,authentication 授权相关的,像 product 产品,inventory 库存,payment 付款,checkout 结算,shipping 物流等等。可见,这是一个电商平台的一些 js 文件。
那么一般情况下,我们自己的业务代码可能会需要依赖上面的第三方的代码。根据我们 js 在浏览器上的加载特性,那么所有的 js 代码,是从上至下来加载的。因此如果我们的业务代码,依赖上面的那些库,或者是框架代码,那么顺序一定不能颠倒。那假如我们把两组文件加载的顺序来颠倒一下,那么这个项目可能就崩溃掉了。这种传统的方式对我们开发人员来说的话,会有很大的开发的心智负担,代码也很难扩展。
我们把这些文件按照一些预定的顺序来合并到一个文件里,不就解决问题了吗?
他只加载了一个 js 文件,这个文件包含了 11 个文件,虽然解决了我们上个页面加载多个 js 的问题,但可能会导致其他的问题:作用域问题、文件太大、可读性差、可维护性弱等。
作用域问题:我们使用过 jquery lodash bootstrap 同学应该知道,这些库文件会分别在 window 对象上面来绑定全局的变量,比如 jqurey 可能会绑定 $,lodash 可能会绑定下滑线_等等。就连我们自己的业务文件,可能也会在全局上面绑定一些变量。这些变量会严重的污染我们的 window 对象,使我们的 window 对象变得臃肿,这就是变量的作用域问题。
文件太大:如果我们 11 个文件分散加载,那么页面会随着文件的加载而逐渐来显示内容。可如果我们将这 11 个文件合并成一个 js 文件,那这个脚本会带来网络瓶颈,用户得等待一段时间才能看到页面的内容,会有短暂的白屏,用户体验非常差。
可读性差、可维护性弱:如果我们把所有代码都合并在一个超大的文件里,那么对于程序的可读性以及可维护性就带来了灾难,最终是加重了我们编程人员的负担。
那如何解决这些问题?那我们接下来将继续研究。
# 1.1 如何解决作用域问题
# IIFE
早先前我们使用的是 grunt 以及 gulp 两个工具, 来管理我们的项目资源,这两个工具我们称之为任务执行器,他们是将所有的项目文件拼接在一起。其实事实上呢,是利用了 js 的立即调用函数表达式,(立即调用函数表达式 Immediately invoked function expressions IIFE)。这样就解决了大型项目的作用域问题。当脚本被封装在 IIFE 内部的时候,我们可以安全的拼接,或者是组合所有的文件了,而不必担心作用域冲突。
那什么是 IIFE?
IIFE 既解决了作用域的问题,又可以在外部去暴露我们想暴露的内容。
如果我们想要去修改我们两段代码里的一段代码,那么我们得需要重新编译这段代码。假如说我们这个代码呢有 10,000 行,但是我只改了一行,那么这个文件也会重新编译,或者说这个文件也会被同时的加载。
我们为了使用 lodash 的一个 join 方法,我们却把整个 lodash 这个大的库文件全都加载下来了。那么我们能不能想一个办法把这个文件给拆成一个一个的方法的模块呢?或者说,我们来实现一个这个方法的懒加载呢?如果是我们通过手工的方法来实现,那工作量可就大了,所以下个视频我们来研究如何的去实现代码的拆分。
# 1.2 如何解决代码拆分问题
# node.js
我们先感谢一下 node.js,它使 javascript 模块诞生了。我们知道 node.js 是一个 javascript 运行环境,我们可以在浏览器环境之外的计算机或者是服务器上使用它。
webpack 其实就是运行在 node.js 中的。
当 node.js 发布的时候,一个新的时代就开始了。
既然我们不是在浏览器中运行 javascript,那么现在就没有了可以添加到我们浏览器的 html 文件,以及我们编写的 script 标签了。那么 node.js 究竟是如何的去加载新的代码文件?
# common.js
common.js 的问世引入了一个叫做 require 的机制,他允许在我们当前的文件中,去加载和使用某个模块,导入需要的每个模块。这个开箱即用的功能帮助我们解决了代码的拆分问题。
我们通过代码来给大家演示一下。
现在 node.js 俨然已经成为一种语言,一个平台,乃至快速开发和创建应用的一种方式了,他接管了我们整个 JavaScript 世界。
通过刚才的例子我们看到,虽然 commonjs 是 node.js 项目的绝佳模块拆分的解决方案,但是浏览器是不支持这个模块化的。我们在 node.js 里边开箱即用的一个加载模块的方法 require (),在浏览器上失效了。
那我们该如何的解决这个何题?下一个小节我们再见。
# 1.3 如何让浏览器支持模块
在早期,我们使用类似于像 browserify、requirejs 这样的打包工具分别来将 CommonJS、AMD 模块化语法的代码转换为能够在浏览器中运行的代码。
Webpack 可以帮助我们打包 javascript 的应用程序,并且同时支持 es 的模块化以及 commonjs,还可以扩展支持很多的静态资源打包,比如像图片、字体文件、样式文件等等。
# 1.4 webpack 与竞品
下面我们来看一下 webpack、PARCEL、rollup.js、Vite 的 pk。
PARCEL 号称是零配置,用户一般无需做其他的配置开箱即用。
rollup.js 用标准化的格式来编写代码,比如像 es6,通过减少无用的代码,来尽可能的缩小包的体积。一般只能用来打包 js。
如果你想要构建一个简单的应用,并且让他快速的运行起来,那你可以使用 PARCEL。
如果你想要构建一个类库,只需要导入很少的第三方的库,那你可以使用 rollup。
如果你想要构建一个复杂的应用,并且想集成很多的第三方的库,并且还要拆分代码,还要使用静态的资源文件,还要支持 common.js、esmodule 等等,那只能是 webpack 了。
最后的一匹黑马那就是 Vite,在刚刚结束的这个 VueConf 2021 大会里边,除了 Vue3.0 以外,另外的一个亮点就是下一代的构建工具 Vite。Vite 将成为 Vue 的现代标配,甚至最近还推出了一个框架叫 petite Vue,从开发、编译、发布、demo,几乎全部都使用的是 Vite 来完成的。Vite 这种基于 esmodule 的构建方式,日益的受到了用户的青睐,不仅可以按需编译、热模块的更新等等,它还有其他的丝滑的开发体验,以及和 Vue3 的完美结合。
webpack、Vite 作为前端热门的工程化工具,他们各自有各自的使用场景,其实并不存在取代的这一说法。
# 第二章:小试 webpack
# 2.1 开发准备
在进入 Webpack 世界之前,我们先来用原生的方法构建一个 web 应用。
//hello-world.js
function helloworld(){
console.log('hello world')
}
//index.js
helloworld()
//index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<script src="./hello-world.js" type="text/javascript" charset="utf-8"></script>
<script src="./index.js" type="text/javascript" charset="utf-8"></script>
</body>
</html>
在浏览器中直接打开 index.html,控制台输出:
原始应用 存在的问题:如果 js 文件非常多,它们的引入顺序将难以维护。
# 2.2 安装 Webpack
# 安装 Node.js
在开始之前,请确保安装了 Node.js 的最新版本。使用 Node.js 最新的长期支持版本 (LTS -Long Term Support),是理想的起步。使用旧版本,你可能遇到各种问题,因为它们可能缺少 webpack 功能,或者缺少相关 package。
检查 Node.js 是否安装成功:在命令行中,执行 node -v
,如果打印出版本号,则说明安装成功。
注意:Node.js V14 版本 只支持 Windows 8.1、Windows Server 2012 R2 或更高版本。Windows7 装不了???
Node.js 自带 npm。npm 全称为 node package manager,它是 node 的一个包管理工具。
检查 npm 版本: npm -v
webpack 是基于 node 来开发的,所以安装 webpack 就得需要通过 npm。把我们的 webpack 当成是一个 node 的包,来进行安装和管理。
# 安装 webpack
webpack 的安装有两种情况:一种我们可以在全局里安装 webpack,另一种我们可以在本地的工作目录下安装 webpack。
webpack 含两个包:一个是 webpack 主包,一个是 webpack-cli。
webpack-cli 表示我们可以在命令行里边执行 webpack 命令。
1. 全局安装
npm install webpack webpack-cli --global
全局安装的好处:可以让我们在任何的目录下面去执行 webpack。 检查是否安装成功:
webpack -v
卸载全局的 webpack:
npm uninstall webpack webpack-cli --global
2. 本地安装(推荐)
我们并不推荐大家在全局里安装 webpack,因为这样会使你项目的 webpack 锁定到某个版本里,并且你在使用不同的 webpack 版本的项目里边可能会导致构建失败。另外还有一个问题,如果是一个团队协作的项目,你的小伙伴如果不知道在全局里安装 webpack,构建也会有问题。推荐大家还是在本地的工作目录下去安装 webpack。
生成 包管理文件 package.json
npm init -y
本地安装 webpack
npm install webpack webpack-cli --save-dev
基本的目录结构如下:
# 2.3 运行 Webpack
准备测试项目
1. 使用 esmodule 模块化语法,编写以下测试代码。
//hello-world.js
function helloworld(){
console.log('hello world')
}
//导出模块
export {
helloworld
}
//index.js
//导入模块
import { helloworld } from './hello-world.js'
helloworld()
2. 执行打包命令。
在项目 根目录 命令行 执行:
webpack
3. 打包结果。
报错了。 查看 webpack 打包信息:
webpack --stats detailed
# 方式 1:使用全局的 webpack 进行打包
直接执行打包命令 webpack
,默认会去使用全局安装的 webpack。如果全局没有安装 webpack,也不会使用本地的,而是会报错。
# 方式 2:使用本地的 webpack 进行打包 (推荐)
需要使用一个新的工具 npx,npx 依托于 npm,npm 自带 npx。 npx 的作用:观察我们当前文件夹里面有没有你想要去运行的这个命令,如果没有则往上一层父级目录中查找,如果有则执行。
npx webpack
# 2.4 自定义 Webpack 配置
查看命令行参数
实际上,webpack-cli 给我们提供了丰富的终端命令行指令,可以通过 webpack --help
来查看:
# 方式 1:命令行参数
1. 执行打包命令,同时添加一些参数,来自定义打包配置。
npx webpack --entry ./src/js/index.js --mode production
–entry 指定入口文件
–mode 指定打包模式
2. 执行结果。
可以发现,打包成功,输出了一个目录 dist,以及一个文件 main.js。
缺点: 添加命令行参数的方式,不直观,也不方便,而且还不能保存下来。
# 方式 2:配置文件(推荐)
1、在项目根目录中创建 webpack.config.js 文件。
注意:
- 配置文件的名字不能修改,Node.js 会自动读取。
- 必须使用 CommonJS 模块化语法。
2、编写配置文件内容。
const path = require('path')
module.exports = {
//指定入口文件
entry: './src/js/index.js',
//指定打包输出目录
output:{
//指定输出文件名
filename: 'bundle.js',
//指定输出路径
path: path.resolve(__dirname, './dist')
},
mode: 'none'
}
path 模块
path.resolve () 方法
__dirname 表示 webpack.config.js 所在的绝对路径。
3、执行打包命令:npx webpack
4、打包结果。
可以看到,打包成功,输出了一个 dist/bundle.js 文件。
5、引入打包文件。
在 index.html 中,引入打包文件 bundle.js。
//index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<script src="../dist/bundle.js" type="text/javascript" charset="utf-8"></script>
</body>
</html>
6、浏览器执行。
可以看到,代码正常执行,也就说明 webpack 打包已经成功了。
缺点:在 html 文件中,手动引入打包文件,不方便,如何才能做自动引入呢?
# 第三章:自动引入资源
# 3.1 什么是插件
webpadk 就像一条生产线,他要经过一系列的处理流程以后,才能将源文件转化成输出的结果。 入口文件 还可以依赖于其他的 js 模块,其中被依赖的 js 模块可能还依赖其他的 js 模块。并且 js 模块还可以引入 css 文件。
webpack 会把整个依赖的关系都记录下来,然后交给 webpadk 编译器。webpack 编译器经过加工以后会生成目标文件,比如 css 和 js 文件。
webpack 编译的过程需要应用一些工具来帮忙,这些工具可以帮助 webpack 来执行一些特定的任务,比如:打包优化、资原管理等等。这些工具就是我们所谓 plugins 插件。
webpack 插件有三种类型:
1.Community 社区插件
2.Webpack 官方插件
3.Webpack Contrib 第三方插件
查看 webpack 有哪些插件? webpack Plugins
# 3.2 使用 HtmlWebpackPlugin
# plugins 选项
webpack 中使用插件的步骤:
0 . 安装插件。
1 . webpack.config.js 中,添加一个 plugins 选项。
2 . webpack.config.js 中,引入插件,再在 plugins 选项中,通过 new 来实例化插件的构造函数。
3 . 实例化插件的构造函数时,可以传入一个配置对象,来进行一些插件配置。
1、首先安装插件:
npm install --save-dev html-webpack-plugin
2、并且调整 webpack.config.js 文件:
const path = require('path')
//引入插件
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './src/js/index.js',
output:{
filename: 'bundle.js',
path: path.resolve(__dirname, './dist')
},
mode: 'none',
//配置插件
plugins: [
new HtmlWebpackPlugin()
]
}
3、打包:
npx webpack
4、打包结果
可以看到,打包生成了一个新的文件 index.html,并且它自动引入了 bundle.js。
# 插件的配置对象
HtmlWebpackPlugin 默认会创建一个新的空白的 html 文件,然后在 head 标签中,自动引入 bundle.js。如果我们想在我们自己的 html 文件的 body 标签中自动引入 bundle.js,该怎么办呢?
我们可以给插件传入一个配置对象,来对插件进行一些配置。
//webpack.config.js
...
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry...
output...
mode...
plugins: [
new HtmlWebpackPlugin({ //配置对象
//指定html模板
template: './index.html',
//指定输出名字
filename: 'app.html',
//指定在body标签中引入打包文件
inject: 'body'
})
]
}
# 3.3 清理 dist
如何才能自动地将上一次打包的输出文件清除呢? 在 output 选项,添加一个配置即可: clean:true
//webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './src/js/index.js',
output:{
filename: 'bundle.js',
path: path.resolve(__dirname, './dist'),
//自动删除上一次的打包文件
clean: true
},
mode: 'none',
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'app.html',
inject: 'body'
})
]
}
# 第四章:搭建开发环境
# 4.1 mode 选项
可以通过搭建一个开发环境(也称本地服务器)解决以下的重复性的人工问题,以提高开发效率:
1. 手动在浏览器中打开 html 文件。
2. 每次修改代码,需要手动重新执行打包命令。
mode 选项的作用:指定打包的模式。
‘development’ 表示开发模式。
mode: 'development'
# 4.2 使用 source map
source-map 的作用:当 js 代码出错时,点击浏览器控制台右侧的报错行数,可以精确定位到源代码的出错位置,而不是打包文件中的出错位置,有助于我们调试代码。
配置文件中,添加一个 devtool 选项。
devtool: 'inline-source-map'
# 4.3 使用 watch mode
在每次编译代码时,手动运行 npx webpack 会显得很麻烦。如果才能修改源代码后,自动进行打包呢?
执行打包命令时,添加一个命令行参数 watch 即可。
npx webpack --watch
如果其中一个文件被更新,代码将被重新编译,所以你不必再去手动运行整个构建。
–watch 参数的作用:打包命令不会立即结束,而是光标一直在闪烁,只要修改源代码并且保存,就会自动重新执行打包,继续监听下一次修改。
Ctrl+C:可以终止命令行窗口中执行的命令。
# 4.4 使用 webpack-dev-server
webpack-dev-server 为你提供了一个基本的 web server,并且具有 live reloading (实时重新加载) 功能。
注意:
1、需要手动创建 dist 目录。
2、有时候自动打开浏览器后,在页面上看不到 dist 目录中的文件,需要手动在地址栏访问 app.html 文件。
1、先安装。
npm install --save-dev webpack-dev-server
2、修改配置文件,添加一个 devServer 选项,告知 dev server,从什么位置查找文件。
//...
module.exports = {
//...
// dev-server
devServer: {
static: './dist'
}
}
以上配置告知 webpack-dev-server ,将 dist 目录下的文件作为 web 服务的根目录。
提示:webpack-dev-server 在编译之后不会写入到任何输出文件。而是将 bundle 文件保留在内存中,然后将它们 serve 到 server 中,就好像它们是挂载在 server 根路径上的真实文件一样。
3、执行命令。
npx webpack serve
或
npx webpack-dev-server --open
或者
npx webpack serve --open
–open:自动打开浏览器,并访问 http://localhost:8080/ 页面。
4、在浏览器里可以直接访问 http://localhost:8080/ ,来查看页面。
watch mode 和 webpack-dev-server 的区别:
–watch:监听源文件的修改,自动执行打包命令。
webpack-dev-server:不仅可以监听源文件的修改,自动执行打包命令。还可以自动刷新浏览器,方便查看最新的 js 代码的执行效果。
所以,后者完全可以取代前者来使用。
# 小结
经过第 2、3、4 章的学习,我们在配置文件中,进行了以下的设置:
//webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './src/js/index.js',
output:{
filename: 'bundle.js',
path: path.resolve(__dirname, './dist'),
clean: true
},
mode: 'development',
devtool: 'inline-source-map',
devServer: {
static: './dist'
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'app.html',
inject: 'body'
})
]
}
# 第五章:资源模块
# 5.1 资源模块介绍
到目前为止我们的项目只能加载 JS,那现在我们能不能尝试混合一些其他的资源呢?比如像 images,看看 webpack 是如何处理的?
其实 webpack 最出色的功能之一就是除了引入 js,还可以使用内置的资源模块 asset modules,来引入任何的其他类型资源。
资源模块 (asset module) 是一种模块类型,它允许我们应用 Webpack 来打包其他资源文件(如字体,图标等)。
资源模块类型 (asset module type),通过添加 4 种新的模块类型,来替换所有这些 loader:
- asset/resource 导出一个单独的文件并导出 URL。
- asset/inline 导出一个资源的 data URI。
- asset/source 导出资源的源代码。
- asset 在导出一个 data URI 和发送一个单独的文件之间自动选择。
# 5.2 Resource 资源
原理:
- js 文件中,import 导入图片等资源时,可以得到它的 url。
- 以 url 的方式来使用图片。
- 打包时,会把图片等资源以单独的文件的形式输出到 dist 目录中。
# module 选项
配置文件中,添加一个新的 module 选项,来配置资源模块。
# rules 选项
rules 数组:里面可以配置多个对象,来去加载不同类型的文件。
test 属性:值为一个正则表达式,指定要匹配的文件类型。
type 属性:指定资源模块类型。
# 处理图片资源
webpack 在打包时,如何处理图片模块呢?
原理:图片也可以看成一个 js 模块,在 js 文件中,可以对图片进行导入、使用和打包。
1、在入口文件中,使用 esmodule 模块化语法,引入图片。
//index.js
import { helloworld } from './hello-world.js'
//导入图片
import imgsrc from './assets/img-1.png'
helloworld()
//imgsrc是图片的url
const img = document.createElement('img')
img.src = imgsrc
document.body.appendChild(img)
2、修改配置文件。
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
...
//配置资源模块
module: {
rules: [
//处理图片资源
{
test: /\.png$/,
type: 'asset/resource'
}
]
}
}
3、执行打包命令。
npx webpack
注意:
我在使用 webpack-dev-server 时,执行 npm install --save-dev webpack-dev-server 打包命令后,打包成功,但是访问 8080 端口时,无法访问页面,不知道为什么?所以,这里在配置文件中,注释掉 devServer 选项,就不使用本地服务器来自动编译和自动打开浏览器了。
4、查看打包结果。
5、查看浏览器执行效果。
复制打包输出的 html 文件的绝对路径,在浏览器地址栏中打开。
# 自定义输出目录和文件名
webpack 默认把图片资源打包到 dist 目录下,并且帮助我们自动的起好文件名。那我们能不能自己定义图片的输出目录和文件名呢?
有两种方法:
# 方法 1:assetModuleFilename 属性
output 选项中,添加一个新的属性 assetModuleFilename,可以指定图片的输出目录和文件名。
指定输出目录和文件名:
output:{
...
assetModuleFilename: 'images/test.png'
},
但是我们不可能一一为每个资源都指定名字,所以需要设置成自动生成名字。 我们可以使用 webpack 系统自带的默认的生成文件名的方法:
contenthash 表示根据文件的内容来生成一个哈希的字符串。
ext 表示使用资源原来的扩展名。
output:{
...
assetModuleFilename: 'images/[contenthash][ext]'
},
# 方法 2:generator 属性
配置方式和 assetModuleFilename 属性是一样的。
...
module: {
rules: [
{
test: /\.png$/,
type: 'asset/resource',
//指定资源的输出路径和文件名
generator: {
filename: 'images/[contenthash][ext]'
}
}
]
}
注意:generator 属性优先级高于 assetModuleFilename 属性,如果两者同时设置了,则只有前者生效。
# 5.3 inline 资源
原理:将资源文件(如 svg 图片)转换为 data uri,嵌入到 bundle.js,而没有单独输出成一个文件。
1. 引入 svg 图片。
//index.js
...
//入口文件中,引入svg图片
import logosvg from '../assets/webpack-logo.svg'
...
const img2 = document.createElement('img')
img2.style.cssText = 'width: 600px; height: 200px'
img2.src = logosvg
document.body.appendChild(img2)
2. 修改配置文件。
...
module: {
rules: [
...
//打包svg图片
{
test: /\.svg$/,
type: 'asset/inline',
}
]
}
3. 打包。
4. 浏览器查看效果。
# 5.4 source 资源
source 资源,导出资源的源代码。
特点:webpack 会将 asset/source 资源 的源代码 原样嵌入到 js 中,而不会单独输出成一个文件。
1. 修改配置文件,添加:
...
module: {
rules: [
...
//打包asset/source资源,如txt
{
test: /\.txt$/,
type: 'asset/source'
}
]
}
2. 引入 txt
//example.txt
hello webpack
//index.js
...
//导入txt
import exampleTxt from '../assets/example.txt'
...
const block = document.createElement('div')
block.textContent = exampleTxt
block.style.cssText = 'width: 200px; height: 200px; background: aliceblue'
document.body.appendChild(block)
3. 打包。
npx webpack
4. 浏览器执行效果。
# 5.5 通用数据类型
通用资源类型 asset
, 在导出一个 data URI 和发送一个单独的文件之间自动选择。
module: {
rules: [
test: /\.jpg/,
type: 'asset' //设置通用数据类型
]
}
现在,webpack 将按照默认条件,自动地在 resource 和 inline 之间进行选择: 小于 8kb 的文件,将会视为 inline 模块类型,否则会被视为 resource 模块类型。
可以通过在 webpack 配置的 module rules 层级中,设置 Rule.parser.dataUrlCondition.maxSize 选项来修改阈值的大小。
rules: [
{
test: /\.jpg/,
type: 'asset',
parser: {
dataUrlCondition: {
//设置阈值
maxSize: 4 * 1024 // 4kb
}
}
}
]
1. 修改配置文件。
...
module: {
rules: [
{
test: /\.png$/,
//设置通用数据类型
type: 'asset',
generator: {
filename: 'images/[contenthash][ext]'
},
parser: {
dataUrlCondition: {
//设置阈值
maxSize: 6*1024 // 6kb
}
}
},
...
]
}
2. 引入 png。
//index.js
//导入png图片
import imgsrc from '../assets/pic1.png' //4kb
import img2src from '../assets/pic2.png' //13kb
const img = document.createElement('img')
img.src = imgsrc
document.body.appendChild(img)
const img2 = document.createElement('img')
img2.src = img2src
document.body.appendChild(img2)
3. 打包。
4. 浏览器执行效果。
# 小结
webpad 给我们提供了四种资原类型,来加载除了 js 以外的资源,比如图片等。
resource 资源:asset/resource。
它可以生成一个单独的文件,并导出 url。这个 url 是个资源路径,可以在业务代码里边直接使用。
inline 资源: asset/inline。
它导出一个资源的 dataUrl。 比如可以将一个 svg 转换为一个 base64 编码的字符串,在业务代码中可以直接使用这个字符串。
source 资源:asset/source。
它可以导出资源的源代码。
通用资源类型: asset。
经过前 5 章的学习,我们学会了以下配置:
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './src/js/index.js',
output:{
filename: 'bundle.js',
path: path.resolve(__dirname, './dist'),
clean: true,
//assetModuleFilename: 'images/[contenthash][ext]'
},
mode: 'development',
devtool: 'inline-source-map',
/*
devServer: {
static: './dist'
} */
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'app.html',
inject: 'body'
})
],
module: {
rules: [
{
test: /\.png$/,
//type: 'asset/resource',
type: 'asset',
generator: {
filename: 'images/[contenthash][ext]'
},
parser: {
dataUrlCondition: {
maxSize: 6*1024 // 6kb
}
}
},
{
test: /\.svg$/,
type: 'asset/inline',
},
{
test: /\.txt$/,
type: 'asset/source'
}
]
}
}
# 第六章:管理资源
# 6.1 什么是 loader
实际上,webpack 除了资源模块以外,还可以通过 loader,去引入其他类型的文件。
webpack 本身只能理解和打包 js 和 json 文件,而 loader 可以让 webpack 去解析他的类型的文件,并且将这些文件转化为有效的模块,以供我们应用程序使用。
哪如何使用 loaders 呢?
loader 定义有两个 module.rules 选项中重要的属性:
test 属性:指定处理哪种类型的文件,通过正则表达式来匹配文件名。
use 属性:指定使用哪个 loader 来处理。
# 6.2 加载 CSS
# css-loader
# style-loader
less-loader、sass-loader、stylus-loader
为了在 JavaScript 模块中 import 一个 CSS 文件,你需要安装 style-loader 和 css-loader,并在 module 配置中添加这些 loader。
npm install --save-dev style-loader css-loader
修改配置文件。
module: {
rules: [
//处理css文件
{
test: /\.css$/,
use: ['style-loader','css-loader']
}
]
}
注意:
-
css-loader 和 style-loader 的区别:
css-loader:将 css 文件转换为有效的模块,使得 webpack 能够识别和打包。
style-loader:会通过 js 代码,将 css 代码以 style 标签的形式插入到 html 中,使得其样式生效。 -
css-loader 和 style-loader 的顺序不能颠倒,先使用 css-loader,再使用 style-loader。
-
webpack 支持 loaders 的链式调用,上一个 loader 将转换后的代码传递给下一个 loader 继续进行处理,最后一个 loader 会转换为 js 代码,传递为 webpack。
-
use 数组 是从末尾到头部的顺序,依次使用 loader 进行处理的。
1、安装 css-loader、style-loader。
2、修改配置文件。
...
module: {
...
rules: [
...
//处理css文件
{
test: /\.css$/,
use: ['style-loader','css-loader']
}
]
}
3、引入 css 文件。
//style.css
.hello{
color: red;
font-size: 40px;
}
//index.js
//导入css
import '../css/style.css'
...
4、打包。
5、浏览器执行效果。
# less-loader
如何打包.sass、.less 文件呢?使用 less-loader。
1、安装 less-loader。
npm install less less-loader --save-dev
2、修改配置文件。
module: {
rules: [
//处理css、less文件
{
test: /\.(css|less)$/,
use: ['style-loader', 'css-loader', 'less-loader']
}
]
}
3、引入 less 文件。
//style2.less
@backgroundColor: pink;
p{
background-color: @backgroundColor;
}
//index.js
...
//导入less
import '../css/style2.less'
4、打包。
5、浏览器执行效果。
# 6.3 抽离和压缩 CSS
通过 css-loader
、 style-loader
、 less-loader
,可以将样式文件(.css、.less)文件转化为 js 代码,这些 js 代码会被合并到 bundle.js 中,它们会在执行时,创建 style 标签,将样式字符串包裹进 style 标签中,最后将 style 标签添加到 html 文件中,使得样式代码生效。
缺点:css 代码并没有单独输入成一个文件,容易导致 bundle.js 文件体积过大。
哪如何将 css 代码单独打包成一个文件呢?然后再通过 link 标签自动引入它呢?
# mini-css-extract-plugin
在多数情况下,我们也可以进行压缩 CSS,以便在生产环境中节省加载时间,同时还可以将 CSS 文件抽离成一个单独的文件。
实现这个功能,需要 mini-css-extract-plugin 这个插件来帮忙。
该插件会将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件,并且支持 CSS 和 SourceMaps 的按需加载。本插件基于 webpack v5 的新特性构建,并且需要 webpack 5 才能正常工作。
单独的 mini-css-extract-plugin 插件不会将这些 CSS 加载到页面中。 html-webpack-plugin 可以帮助我们自动生成 link 标签或者在创建 index.html 文件时使用 link 标签,自动引入打包输出的样式文件。
注意: style-loader
和 mini-css-extract-plugin
只能使用其中一个。
1、安装插件。
npm install mini-css-extract-plugin --save-dev
2、修改配置文件。
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
...
module: {
rules: [
...
//抽离css文件
{
test: /\.(css|less)$/,
use: [MiniCssExtractPlugin.loader,'css-loader','less-loader']
}
]
},
plugins: [
...
//实例化MiniCssExtractPlugin
new MiniCssExtractPlugin()
],
指定输出目录
如何指定 css 打包后的输出目录呢?实例化插件时,传入一个配置对象。
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
...
plugins: [
...
//指定css输出目录
new MiniCssExtractPlugin({
filename: 'css/[contenthash].css'
})
],
# css-minimizer-webpack-plugin
发现文件并没有压缩和优化,为了压缩输出文件,请使用类似于 css-minimizer-webpack-plugin 这样的插件。
注意:
1.css-minimizer-webpack-plugin 插件 需要 optimization 选项 中进行配置。
2.css-minimizer-webpack-plugin 插件 一般在生产模式下使用。
1、安装插件。
npm install css-minimizer-webpack-plugin --save-dev
2、修改配置文件。
...
//引入插件
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
...
//生产模式
mode: 'production'
...
//优化配置
optimization:{
minimizer: [
new CssMinimizerPlugin()
]
}
# 示例
1、安装 mini-css-extract-plugin、css-minimizer-webpack-plugin。
2、修改配置文件。
...
//引入插件
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
...
//生产模式
mode: 'production',
...
plugins: [
..
//实例化插件
new MiniCssExtractPlugin({
//指定css输出目录
filename: 'css/[contenthash].css'
})
],
module: {
rules: [{
...
//抽离css文件
{
test: /\.(css|less)$/,
use: [MiniCssExtractPlugin.loader,'css-loader','less-loader']
}
]
},
//优化配置
optimization:{
minimizer: [
new CssMinimizerPlugin()
]
}
4、引入样式文件。
5、打包。
6、浏览器执行效果。
# 6.4 加载 images 图像
css 文件中,如果某些 css 属性引用了图片,则 webpack 要如何进行打包呢?
其实,在第 5 章 资源模块中,webpack 通过内置的资源模块就可以打包 js 文件中引用的 png、svg、jpg 等图片。同样,也可以打包 css 文件中引用的图片。配置方法是一样的。
示例:
1、css 文件引用图片。
//style.css
...
.bg-img{
background-image: url(../assets/img-1.png);
}
2、入口文件。
//index.js
//导入css
import '../css/style.css'
const block = document.createElement('div')
block.style.cssText = 'width: 200px; height: 200px;'
block.classList.add('bg-img')
document.body.appendChild(block)
3、配置文件。
...
module: {
rules: [
{
test: /\.png$/,
//type: 'asset/resource',
type: 'asset',
generator: {
filename: 'images/[contenthash][ext]'
},
parser: {
dataUrlCondition: {
maxSize: 6 * 1024 // 6kb
}
}
},
{
test: /\.svg$/,
type: 'asset/inline',
},
]
}
4、打包。
5、浏览器执行效果。
# 6.5 加载 fonts 字体
那么,像字体这样的其他资源如何处理呢?使用 Asset Modules 可以接收并加载任何文件,然后将其输出到构建目录。这就是说,我们可以将它们用于任何类型的文件,也包括字体。
...
module: {
rules: [{
...
//处理字体文件
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
type: 'asset/resource',
}
]
},
示例:
1、修改配置文件。
//处理字体文件
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
type: 'asset/resource',
//指定输出目录
generator: {
filename: 'fonts/[contenthash][ext]'
},
}
2、引入字体。
//style.css
/* 声明字体 */
@font-face {
font-family: 'webfont';
font-display: swap;
src: url('../font/webfont.eot');
/* IE9 */
src: url('../font/webfont.eot?#iefix') format('embedded-opentype'),
/* IE6-IE8 */
url('../font/webfont.woff2') format('woff2'),
url('../font/webfont.woff') format('woff'),
/* chrome、firefox */
url('../font/webfont.ttf') format('truetype'),
/* chrome、firefox、opera、Safari, Android, iOS 4.2+*/
url('../font/webfont.svg#webfont') format('svg');
/* iOS 4.1- */
}
/* 使用字体 */
.fun {
font-family: "webfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
3、打包。
4、浏览器执行效果。
# 6.6 加载数据
# csv-loader
# xml-loader
此外,可以加载的有用资源还有数据,如 JSON 文件,CSV、TSV 和 XML。类似于 NodeJS,JSON 支持实际上是内置的,也就是说 import Data from ‘./data.json’ 默认将正常运行。要导入 CSV、TSV 和 XML,你可以使用 csv-loader 和 xml-loader 来加载这三类文件。
1、安装 csv-loader、xml-loader。
npm install --save-dev csv-loader xml-loader
2、修改配置文件。
{
test: /\.(csv|tsv)$/,
use: ['csv-loader']
},
{
test: /\.xml$/,
use: ['xml-loader']
}
3、现在,你可以 import 这四种类型的数据 (JSON, CSV, TSV, XML) 中的任何一种,所导入的 Data 变量,将包含可直接使用的已解析 JSON。.xml 文件转化为一个 JS 对象, .cvs 转化为一个数组。
示例:
1、引入 xml、csv。
//test.xml
<?xml version="1.0" encoding="UTF-8"?>
<note>
<to>Mary</to>
<from>John</from>
<heading>Reminder</heading>
<body>Call Cindy on Tuesday</body>
</note>
//test2.csv
to,from,heading,body
Mary,John,Reminder,Call Cindy on Tuesday
Zoe,Bill,Reminder,Buy orange juice
Autumn,Lindsey,Letter,I miss you
//index.js
//导入xml
import xmlData from '../data/test.xml'
//导入csv
import csvData from '../data/test2.csv'
...
console.log(xmlData)
console.log(csvData)
2、修改配置文件。 3、打包。
4、浏览器执行效果。
# 6.7 自定义 JSON 模块 parser
yaml 文件我们在做项目过程中经常的看到,它有 key、value,同时通过缩进来去完成子项的设置。注意:yaml 文件只能用空格来缩进,而不能用 Tab 键。
toml 相比较 yaml 就会好一些,不是通过缩进而是通过一个 key 等于 value 的方式。如果有子项,则通过一个中括号来定义。
json5 格式 是对 json 格式的一个升级。json 格式不能使用注释,并且 key 必须使用双引号。json5 中 key 可以不使用引号,value 可以使用单引号,还可以使用 /n、/r 等等。
webpack 中,如何处理 yaml、toml、json5 文件呢?
通过使用 自定义 parser 替代特定的 webpack loader,可以将任何 toml 、 yaml 或 json5 文件作为 JSON 模块导入。
1、首先安装 toml、 yamljs、json5 的 packages。
npm install toml yamljs json5 --save-dev
2、配置。
const toml = require('toml');
const yaml = require('yamljs');
const json5 = require('json5');
module: {
rules: [
//处理toml、yaml、json5
{
test:/\.toml$/,
type: 'json',
parser: {
parse: toml.parse
}
},
{
test:/\.yaml$/,
type: 'json',
parser: {
parse: yaml.parse
}
},
{
test:/\.json5$/,
type: 'json',
parser: {
parse: json5.parse
}
}
]
},
3、引入 toml、yaml、json5。
//data.toml
title = "TOML Example"
[owner]
name = "Tom Preston-Werner"
organization = "GitHub"
bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."
dob = 1979-05-27T07:32:00Z
//data.yaml
title: YAML Example
owner:
name: Tom Preston-Werner
organization: GitHub
bio: |-
GitHub Cofounder & CEO
Likes tater tots and beer.
dob: 1979-05-27T07:32:00.000Z
//data.json5
{
//comment
title: 'JSON5 Example',
owner: {
name: 'Tom Preston-Werner',
organization: 'GitHub',
bio: 'GitHub Cofounder & CEO\n\
Likes tater tots and beer.',
dob: '1979-05-27T07:32:00.000Z',
},
}
//index.js
//导入toml、yaml、json5
import toml from '../json/data.toml'
import yaml from '../json/data.yaml'
import json5 from '../json/data.json5'
console.log(toml)
console.log(toml.title)
console.log(toml.owner.name)
console.log(yaml)
console.log(yaml.title)
console.log(yaml.owner.name)
console.log(json5)
console.log(json5.title)
console.log(json5.owner.name)
4、打包。
npx webpack
5、浏览器执行效果。
# 第七章:使用 babel-loader
# 7.1 为什么需要 babel-loader
webpack 自身可以自动加载 JS 文件,就像加载 JSON 文件一样,无需任何 loader。可是,加载的 JS 文件会原样输出,即使你的 JS 文件里包含 ES6 + 的代码,也不会做任何的转化。
低版本浏览器可能不支持最新的 JS 语法。该怎么办呢?
这时我们就需要 Babel 来帮忙。Babel 是一个 JavaScript 编译器,可以将 ES6 + 转化成 ES5。在 Webpack 里使用 Babel,需要使用 babel-loader。
# 7.2 使用 babel-loader
1、安装 babel-loader、@babel/core、@babel/preset-env。
npm install -D babel-loader @babel/core @babel/preset-env
babel-loader: 在 webpack 里应用 babel 解析 ES6 的桥梁。
@babel/core: babel 核心模块。
@babel/preset-env: babel 预设,一组 babel 插件的集合。
2、安装 @babel/runtime。
npm install --save @babel/runtime
@babel/runtime 中包含了 regeneratorRuntime,运行时需要(运行时依赖 )。
3、安装 @babel/plugin-transform-runtime。
npm install -D @babel/plugin-transform-runtime
作用:在需要 regeneratorRuntime 的地方,自动 require 导包。编译时需要(开发依赖)。
它是 babel-loader 的一个插件,需要在 options.plugins 选项中进行配置。
3、配置。
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: [
[
'@babel/plugin-transform-runtime'
]
]
},
}
}
]
},
exclude 选项 :node_modules 目录中第三方库的 js 文件不需要 babel 来进行转换,使用 exclude 选项来排除。
options 选项 :loader 的配置对象。
# 7.3 regeneratorRuntime 插件
示例:
1、入口文件。
//test.js
function getString(){
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('hello,world!')
}, 2000)
})
}
async function helloWorld(){
let string = await getString()
console.log(string)
}
//导出函数
export default helloWorld
//index.js
import helloWorld from './test.js'
helloWorld()
2、打包。
3、浏览器执行效果。
# 第八章:代码分离
代码分离是 webpack 中最引人注目的特性之一。此特性能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大减少加载时间。
常用的代码分离方法有三种:
- 入口起点:使用 entry 配置手动地分离代码。
- 防止重复:使用 Entry dependencies 或者 SplitChunksPlugin 去重和分离 chunk。
- 动态导入:通过模块的内联函数调用来分离代码。
# 8.2 入口起点
这是迄今为止最简单直观的分离代码的方式。不过,这种方式手动配置较多,并有一些隐患,我们将会解决这些问题。先来看看如何从 main bundle 中分离 another module (另一个模块):
bundle:指打包输出的文件。
1、安装 lodash。
npm install lodash
2、在 src 目录下创建 another-module.js 文件。
这两个 js 文件都使用了 lodash 库,都作为入口文件。
//another-module.js
import _ from 'lodash'
console.log(_.join(['Another', 'module', 'loaded!'], ' '))
//index.js
import _ from 'lodash'
console.log(_.join(['index', 'module', 'loaded!'], ' '))
3、配置多入口。
entry: {
index: './src/js/index.js',
another: './src/js/another-module.js'
},
output: {
filename: '[name].bundle.js'
},
4、打包。
5、结论。
1、打包输出了两个 bundle:index.bundle.js、another.bundle.js。它们都被自动引入 html 中,浏览器执行正常。说明多入口配置成功。
2、这种方式的确存在一些隐患:
● 如果入口 chunk 之间包含一些重复的模块,那些重复模块都会被引入到各个 bundle 中。
● 这种方法不够灵活,并且不能动态地将核心应用程序逻辑中的代码拆分出来。
# 8.3 防止重复
# 入口依赖
配置 dependOn option 选项,这样可以在多个 chunk 之间共享模块。
entry: {
index: {
import: './src/js/index.js',
dependOn: 'shared'
},
another: {
import: './src/js/another-module.js',
dependOn: 'shared'
},
shared: 'lodash'
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, './dist'),
clean: true,
},
示例:
1、配置。
2、打包。
3、结论。
index.bundle.js 与 another.bundle.js 共享的模块 lodash.js 被打包到一个单独的文件 shared.bundle.js 中。
# SplitChunksPlugin
SplitChunksPlugin 是 webpack 内置的一个插件。
SplitChunksPlugin 插件可以自动地将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk。
1、配置。
entry: {
index: './src/js/index.js',
another: './src/js/another-module.js'
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, './dist'),
clean: true,
},
optimization: {
...
splitChunks: {
chunks: 'all'
}
}
2、打包。
3、结论。
使用 optimization.splitChunks 配置选项之后,现在应该可以看出,index.bundle.js 和 another.bundle.js 中已经移除了重复的依赖模块。需要注意的是,插件将 lodash 分离到单独的 chunk,并且将其从 main bundle 中移除,减轻了大小。
# 8.4 动态导入
当涉及到动态代码拆分时,webpack 提供了两个类似的技术。第一种,也是推荐选择的方式是,使用符合 ECMAScript 提案 的 import() 语法 来实现动态导入。第二种,则是 webpack 的遗留功能,使用 webpack 特定的 require.ensure 。
# import()
静态导入: 全局作用域中执行 import ()。
动态导入: 函数作用域中执行 import ()。
1、创建 async-module.js 文件。
function getComponent(){
return import('loadsh')
.then(({default: _}) => {
const element = document.createElement('div')
element.innerHTML = _.join(['Hello', 'webpack'], ' ')
return element
})
.catch((error) => {
return 'An error occurred while loading the component'
})
}
getComponent().then((component) => {
document.body.appendChild(component)
})
2、配置。
optimization: {
splitChunks: {
chunks: 'all'
}
}
3、在入口文件中导入。
//index.js
import './async-module.js'
4、打包、浏览器执行效果。
4、结论。
我们看到,静态和动态载入的模块都正常工作了。而且它们公共的 lodash 模块也被单独打包成一个文件。(another-module.js 中静态导入 lodash, async-module.js 中动态导入 lodash)
# 8.5 懒加载
懒加载或者按需加载,是一种很好的优化网页或应用的方式。这种方式实际上是先把你的代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或即将引用另外一些新的代码块。这样加快了应用的初始加载速度,减轻了它的总体体积,因为某些代码块可能永远不会被加载。
懒加载 是动态导入的一种应用场景。
1、创建一个 math.js 文件,在主页面中通过点击按钮调用其中的函数。
我们希望 math.js 文件是懒加载的。
// math.js
export const add = (x, y) => {
return x + y
}
export const minus = (x, y) => {
return x - y
}
2、编辑 index.js 文件。
//index.js
const button = document.createElement('button')
button.textContent = '点击执行加法运算'
button.addEventListener('click', ()=>{
import(/* webpackChunkName: 'math' */ './math.js').then(({ add })=>{
console.log(add(3, 4))
})
})
document.body.appendChild(button)
这里有句注释,我们把它称为 webpack 魔法注释: webpackChunkName: 'math'
,告诉 webpack 打包生成的文件名为 math 。
3、打包。
4、启动服务,在浏览器上查看。
5、结论。
- 懒加载模块( math.js)被单独打包成一个文件。
- 页面第一次加载时,不会立即加载懒加载模块。点击按钮时,浏览器才会向服务器请求 懒加载模块( math.js)。可以节省用户的网络的流量。
# 8.6 预获取、预加载模块
预获取、预加载 是 动态导入 的第二个应用场景。
Webpack v4.6.0+ 增加了对预获取和预加载的支持。
在声明 import 时,使用下面这些内置指令,可以让 webpack 输出 “resource hint (资源提示)”,来告知浏览器:
- prefetch (预获取):将来某些导航下可能需要的资源
- preload (预加载):当前导航下可能需要资源
# prefetch 预获取
示例 1:prefetch。
1、编辑 index.js 文件。
const button = document.createElement('button')
button.textContent = '点击执行加法运算'
button.addEventListener('click', ()=>{
import(/* webpackChunkName: 'math', webpackPrefetch: true */ './math.js').then(({ add })=>{
console.log(add(3, 4))
})
})
document.body.appendChild(button)
添加第二句魔法注释: webpackPrefetch: true 告诉 webpack 执行预获取。这会生成 <link rel="prefetch" href="math.js">
并追加到页面头部,指示着浏览器在闲置时间预取 math.js 文件。
2、打包。
3、启动服务,在浏览器上查看。
4、结论。
我们发现,在还没有点击按钮时, math.bundle.js 就已经下载下来了。同时,在 app.html 里 webpack 自动添加了一句: <link rel="prefetch" as="script" href="http://localhost:8080/math.bundle.js">
。点击按钮,会立即调用已经下载好的 math.bundle.js 文件中的 add 方法。
# preload 预加载
与 prefetch 指令相比,preload 指令有许多不同之处:
- preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
- preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。
- preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻。
- 浏览器支持程度不同。
示例:preload。
1、创建一个 print.js 文件。
export const print = () => {
console.log('preload chunk.')
}
2、修改 index.js 文件。
import(/* webpackChunkName: 'print', webpackPreload: true */ './print.js')
.then(({ print }) => {
print()
})
3、打包,启动服务,打开浏览器。
npx webpack-dev-server --open
4、结论。
print.bundle.js 被加载下来,是和当前 index.bundle.js 并行加载的。
# 小结
代码分离 是 webpack 是一个非常好的一个功能,可以把多个模块共享的代码抽离出去,减少入口文件的大小,从而提高首屏的加载速度。
预获取 prefetch:在网络空闲的时侯,把将来很可能会使用的代码请求下载下来。这样既不影响首屏的加载速度,又省去了将来模块加载的延迟。
预加载 preload:实现多个模块的并行加载。
# 第九章:缓存
以上,我们使用 webpack 来打包我们的模块化后的应用程序,webpack 会生成一个可部署的 /dist 目录,然后把打包后的内容放置在此目录中。只要 /dist 目录中的内容部署到 server 上,client(通常是浏览器)就能够访问此 server 的网站及其资源。而最后一步获取资源是比较耗费时间的,这就是为什么浏览器使用一种名为缓存 的技术。可以通过命中缓存,以降低网络流量,使网站加载速度更快,然而,如果我们在部署新版本时不更改资源的文件名,浏览器可能会认为它没有被更新,就会使用它的缓存版本。由于缓存的存在,当你需要获取新的代码时,就会显得很棘手。
本节通过必要的配置,以确保 webpack 编译生成的文件能够被客户端缓存,而在文件内容变化后,能够请求到新的文件。
# 9.1 输出文件的文件名
原理:当我们修改自己的代码后,打包后的文件名(哈希值部分)就会发生变化,浏览器就会重新请求新的资源,而不是使用缓存的旧资源。
我们可以通过替换 output.filename 中的 substitutions 设置,来定义输出文件的名称。webpack 提供了一种使用称为 substitution (可替换模板字符串) 的方式,通过带括号字符串来模板化文件名。其中, [contenthash]
substitution 将根据资源内容创建出唯一 hash。当资源内容发生变化时, [contenthash]
也会发生变化。
修改配置文件:
output: {
filename: '[name].[contenthash].js',
...
},
执行打包编译:
可以看到,bundle 的名称是它内容(通过 hash)的映射。如果我们不做修改,然后再次运行构建,文件名会保持不变。
# 9.2 缓存第三方库
将第三方库 (library)(例如 lodash )提取到单独的 vendor chunk 文件中,是比较推荐的做法,这是因为,它们很少像本地的源代码那样频繁修改。因此通过实现以上步骤,利用 client 的长效缓存机制,命中缓存来消除请求,并减少向 server 获取资源,同时还能保证 client 代码和 server 代码版本一致。
我们在 optimization.splitChunks 添加如下 cacheGroups 参数并构建:
optimization: {
...
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
}
执行编译:
# 9.3 将 js 文件放到一个文件夹中
目前,全部 js 文件都在 dist 文件夹根目录下,我们尝试把它们放到一个文件夹中,这个其实也简单,修改配置文件:
output: {
filename: 'js/[name].[contenthash].js',
},
我们在输出配置中修改 filename ,在前面加上路径即可。执行编译:
npx webpack
截止目前,我们已经把 JS 文件、样式文件及图片等资源文件分别放到了 scripts、styles、 images 三个文件夹中。
...
output: {
//指定js文件输出目录
filename: 'js/[name].[contenthash].js',
},
module: {
rules: [
{
test: /\.png$/,
type: 'asset',
generator: {
//指定图片输出目录
filename: 'images/[contenthash][ext]'
}
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
type: 'asset/resource',
generator: {
//指定字体文件输出目录
filename: 'fonts/[contenthash][ext]'
},
},
...
]
}
plugins: [
...
new MiniCssExtractPlugin({
//指定css文件输出目录
filename: 'css/[contenthash].css'
})
],
# 第十章:拆分开发环境和生产环境配
现在,我们只能手工的来调整 mode 选项,实现生产环境和开发环境的切换,且很多配置在生产环境和开发环境中存在不一致的情况,比如开发环境没有必要设置缓存,生产环境还需要设置公共路径等等。
本节介绍拆分开发环境和生产环境,让打包更灵活。
# 10.1 公共路径
在开发环境中,我们通常有一个 assets/ 文件夹,它与索引页面位于同一级别。这没太大问题,但是,如果我们将所有静态资源托管至 CDN,然后想在生产环境中使用呢?
publicPath 配置选项在各种场景中都非常有用。你可以通过它来指定应用程序中所有资源的基础路径。
示例:output.publicPath。
1、设置 publicPath 选项。
output: {
publicPath: 'http://localhost:8080/'
},
2、打包效果。
3、结论。
通过 output.publicPath 选项可以在打包资源的引用路径前添加一个域名,这个域名可以指定为项目的前端域名,或者 cdn 服务器的域名等等都可以的。
# 10.2 环境变量
想要消除 webpack.config.js 在 开发环境 和 生产环境 之间的差异,你可能需要环境变量 (environment variable)。
webpack 命令行 环境配置 的 --env 参数,可以允许你传入任意数量的环境变量。而在 webpack.config.js 中可以访问到这些环境变量。例如, --env production 或–env goal=local 。
对于我们的 webpack 配置,有一个必须要修改之处。通常, module.exports 指向配置对象。要使用 env 变量,你必须将 module.exports 转换成一个函数。函数会默认传入一个参数环境变量 env,根据命令行参数 env 来设置不同环境的 mode。
示例:环境变量。
1、修改配置文件。
- module.exports 一个函数,函数中返回一个对象。
- 函数中可以使用环境变量。
module.exports = (env) => {
console.log(env)
console.log(env.goal)
return {
...
// 根据命令行参数 env 来设置不同环境的 mode
mode: env.production ? 'production' : 'development',
...
}
}
2、执行打包命令时,添加–env 参数,设置环境变量。
npx webpack --env production --env goal=local
3、打包效果。
# 压缩 js 代码
在前面,我们使用了 mini-css-extract-plugin
、 css-minimizer-webpack-plugin
插件来抽离和压缩 css 代码,但是它们会导致 webpack 默认的压缩 js 的功能失效,所以需要手动进行配置 terser 插件。
1、安装 terser-webpack-plugin。
npm install terser-webpack-plugin --save-dev
2、修改配置。
const TerserPlugin = require('terser-webpack-plugin')
module.exports = (env) => {
return {
...
optimization: {
minimizer: [
...
//实例化Terser插件
new TerserPlugin()
],
}
}
}
3、重新打包。
npx webpack --env production --env goal=local
4、打包效果。
5、结论。
打包输出的 js 文件成功压缩了。
注意: webpack 只会在生产打包时,对 js 代码进行压缩。而开发打包时,不会压缩 js 代码。
# 10.3 拆分配置文件
在前面,我们可以通过 环境变量 和 三元运算符 来判断当前是开发打包还是生产打包,从而进行各自的配置,但是这种方式比较糟糕。推荐划分两个不同的配置文件的方式来执行不同环境的打包。
目前,生产环境和开发环境使用的是一个配置文件,我们需要将这两个文件单独放到不同的配置文件中。如 webpack.config.dev.js (开发环境配置)和 webpack.config.prod.js (生产环境配置)。在项目根目录下创建一个配置文件夹 config 来存放他们。
注意:因为配置文件被放在 config/ 目录中了,所以需要将 output.path 中的’./dist’修改为’…/dist’。
# webpack.config.dev.js
webpack.config.dev.js 配置如下:
1.module.exports 是一个对象。
2. 不需要缓存:去掉 output.filename 中的 contenthash。
3. 不需要公共路径: 去掉 publicPath。
4.mode 选项指定为’development’。
5. 需要调试代码 devtool。
6. 需要开发服务器 devServer。
7. 不需要 css、js 压缩:去掉 css-minimizer-webpack-plugin、terser-webpack-plugin、optimization.minimizer。
8. 其它项保留。
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
//const MiniCssExtractPlugin = require('mini-css-extract-plugin')
//const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const toml = require('toml')
const yaml = require('yamljs')
const json5 = require('json5')
//const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
//entry: './src/js/index.js',
entry: {
index: './src/js/index.js',
another: './src/js/another-module.js'
},
/*
entry: {
index: {
import: './src/js/index.js',
dependOn: 'shared'
},
another: {
import: './src/js/another-module.js',
dependOn: 'shared'
},
shared: 'lodash'
}, */
output: {
//filename: 'bundle.js',
filename: '[name].bundle.js',
//filename: 'js/[name].[contenthash].js',
path: path.resolve(__dirname, './dist'),
clean: true,
assetModuleFilename: 'images/[contenthash][ext]',
//publicPath: 'http://localhost:8080/'
},
//生产模式
//mode: 'production',
//mode: 'development',
mode: 'development',
devtool: 'inline-source-map',
//开发服务器
devServer: {
static: './dist'
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'app.html',
inject: 'body'
}),
//指定css输出目录
new MiniCssExtractPlugin({
filename: 'css/[contenthash].css'
})
],
module: {
rules: [{
test: /\.png$/,
//type: 'asset/resource',
type: 'asset',
generator: {
filename: 'images/[contenthash][ext]'
},
parser: {
dataUrlCondition: {
maxSize: 6 * 1024 // 6kb
}
}
},
{
test: /\.svg$/,
type: 'asset/inline',
},
{
test: /\.txt$/,
type: 'asset/source'
},
//处理css文件、less文件
/*
{
test: /\.(css|less)$/,
use: ['style-loader','css-loader','less-loader']
} */
//抽离css文件
{
test: /\.(css|less)$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader']
},
//处理字体文件
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
type: 'asset/resource',
generator: {
filename: 'fonts/[contenthash][ext]'
},
},
{
test: /\.(csv|tsv)$/,
use: ['csv-loader']
},
{
test: /\.xml$/,
use: ['xml-loader']
},
//处理toml、yaml、json5
{
test:/\.toml$/,
type: 'json',
parser: {
parse: toml.parse
}
},
{
test:/\.yaml$/,
type: 'json',
parser: {
parse: yaml.parse
}
},
{
test:/\.json5$/,
type: 'json',
parser: {
parse: json5.parse
}
},
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: [
[
'@babel/plugin-transform-runtime'
]
]
}
}
}
]
},
//优化配置
optimization: {
/* minimizer: [
new CssMinimizerPlugin(),
new TerserPlugin()
], */
/*
splitChunks: {
chunks: 'all'
}, */
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
}
}
# webpack.config.prod.js
webpack.config.prod.js 配置如下:
1.module.exports 是一个对象。
2. 需要缓存:output.filename 中使用 contenthash。
3. 需要公共路径:output.publicPath。
4.mode 指定为’production’。
5. 不需要开发服务器:去掉 devtool。
6. 不需要调试代码:去掉 devServer。
7. 其它项保留。
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const toml = require('toml')
const yaml = require('yamljs')
const json5 = require('json5')
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
//entry: './src/js/index.js',
entry: {
index: './src/js/index.js',
another: './src/js/another-module.js'
},
/*
entry: {
index: {
import: './src/js/index.js',
dependOn: 'shared'
},
another: {
import: './src/js/another-module.js',
dependOn: 'shared'
},
shared: 'lodash'
}, */
output: {
//filename: 'bundle.js',
//filename: '[name].bundle.js',
filename: 'js/[name].[contenthash].js',
path: path.resolve(__dirname, '../dist'),
clean: true,
assetModuleFilename: 'images/[contenthash][ext]',
publicPath: 'http://localhost:8080/'
},
//生产模式
//mode: 'production',
//mode: 'development',
mode: 'production',
//devtool: 'inline-source-map',
//开发服务器
/*
devServer: {
static: './dist'
}, */
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'app.html',
inject: 'body'
}),
//指定css输出目录
new MiniCssExtractPlugin({
filename: 'css/[contenthash].css'
})
],
module: {
rules: [{
test: /\.png$/,
//type: 'asset/resource',
type: 'asset',
generator: {
filename: 'images/[contenthash][ext]'
},
parser: {
dataUrlCondition: {
maxSize: 6 * 1024 // 6kb
}
}
},
{
test: /\.svg$/,
type: 'asset/inline',
},
{
test: /\.txt$/,
type: 'asset/source'
},
//处理css文件、less文件
/*
{
test: /\.(css|less)$/,
use: ['style-loader','css-loader','less-loader']
} */
//抽离css文件
{
test: /\.(css|less)$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader']
},
//处理字体文件
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
type: 'asset/resource',
generator: {
filename: 'fonts/[contenthash][ext]'
},
},
{
test: /\.(csv|tsv)$/,
use: ['csv-loader']
},
{
test: /\.xml$/,
use: ['xml-loader']
},
//处理toml、yaml、json5
{
test:/\.toml$/,
type: 'json',
parser: {
parse: toml.parse
}
},
{
test:/\.yaml$/,
type: 'json',
parser: {
parse: yaml.parse
}
},
{
test:/\.json5$/,
type: 'json',
parser: {
parse: json5.parse
}
},
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: [
[
'@babel/plugin-transform-runtime'
]
]
}
}
}
]
},
//优化配置
optimization: {
minimizer: [
new CssMinimizerPlugin(),
new TerserPlugin()
],
/*
splitChunks: {
chunks: 'all'
}, */
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
}
}
拆分成两个配置文件后,分别运行这两个文件:
开发环境:
npx webpack -c ./config/webpack.config.dev.js
或
npx webpack serve -c ./config/webpack.config.dev.js
生产环境:
npx webpack -c ./config/webpack.config.prod.js
或
npx webpack -c ./config/webpack.config.prod.js
说明:
1、-c 参数,–config,指定使用哪个配置文件。
2、serve 参数 表示启动开发服务器。开发打包时,可以使用该参数启动一个开发服务器,方便查看页面效果和调试代码。
示例 1:使用 开发配置文件 进行打包。
结论:开发打包 输出的文件没有被压缩,文件名中也不包含 hash 值。
示例 2:使用 生产配置文件 进行打包。
结论:生产打包 输出的文件被压缩,文件名中包含 hash 值。
# 10.4 npm 脚本
# 简化打包指令
每次打包或启动服务时,都需要在命令行里输入一长串的命令。能不能优化一下呢?
可以通过在 package.json 的 scripts
选项中添加 自定义指令 的方式来简化打包指令。
1、我们将父目录的 package.json 、 node_modules 与 package-lock.json 拷贝到当前目录下。
2、package.json/scripts 选项中,添加自定义指令。
配置 npm 脚本来简化命令行的输入,这时可以省略 npx。
//package.json
...
"scripts": {
"start": "npx webpack serve -c ./config/webpack.config.dev.js",
"build": "npx webpack -c ./config/webpack.config.prod.js",
},
或
"scripts": {
"start": "webpack serve -c ./config/webpack.config.dev.js",
"build": "webpack -c ./config/webpack.config.prod.js",
},
3、执行打包指令。
//开发打包
npm run start
//生产打包
npm run build
# 关闭性能提示
生产打包时,会输出一些性能方面的警告信息。如果我们不需要这些提示信息,则可以: 在配置文件中添加一个选项 performance 来关闭它。
//webpack.config.prod.js
performance: {
hints: false
}
# 10.5 提取公共配置
这时,我们发现这两个配置文件里存在大量的重复代码,可以手动的将这些重复的代码单独提取到一个文件里。
# webpack.config.common.js
1、创建 webpack.config.common.js ,配置公共的内容。
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
//const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const toml = require('toml')
const yaml = require('yamljs')
const json5 = require('json5')
//const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
//entry: './src/js/index.js',
entry: {
index: './src/js/index.js',
another: './src/js/another-module.js'
},
/*
entry: {
index: {
import: './src/js/index.js',
dependOn: 'shared'
},
another: {
import: './src/js/another-module.js',
dependOn: 'shared'
},
shared: 'lodash'
}, */
output: {
//filename: 'bundle.js',
//filename: '[name].bundle.js',
//filename: 'js/[name].[contenthash].js',
path: path.resolve(__dirname, '../dist'),
clean: true,
assetModuleFilename: 'imgs/[contenthash][ext]',
//publicPath: 'http://localhost:8080/'
},
//生产模式
//mode: 'production',
//mode: 'development',
//mode: env.production ? 'production' : 'development',
//devtool: 'inline-source-map',
//开发服务器
/*
devServer: {
static: './dist'
}, */
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'app.html',
inject: 'body'
}),
//指定css输出目录
new MiniCssExtractPlugin({
filename: 'css/[contenthash].css'
})
],
module: {
rules: [{
test: /\.png$/,
//type: 'asset/resource',
type: 'asset',
generator: {
filename: 'images/[contenthash][ext]'
},
parser: {
dataUrlCondition: {
maxSize: 6 * 1024 // 6kb
}
}
},
{
test: /\.svg$/,
type: 'asset/inline',
},
{
test: /\.txt$/,
type: 'asset/source'
},
//处理css文件、less文件
/*
{
test: /\.(css|less)$/,
use: ['style-loader','css-loader','less-loader']
} */
//抽离css文件
{
test: /\.(css|less)$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader']
},
//处理字体文件
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
type: 'asset/resource',
generator: {
filename: 'fonts/[contenthash][ext]'
},
},
{
test: /\.(csv|tsv)$/,
use: ['csv-loader']
},
{
test: /\.xml$/,
use: ['xml-loader']
},
//处理toml、yaml、json5
{
test:/\.toml$/,
type: 'json',
parser: {
parse: toml.parse
}
},
{
test:/\.yaml$/,
type: 'json',
parser: {
parse: yaml.parse
}
},
{
test:/\.json5$/,
type: 'json',
parser: {
parse: json5.parse
}
},
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: [
[
'@babel/plugin-transform-runtime'
]
]
}
}
}
]
},
//优化配置
optimization: {
/*
minimizer: [
new CssMinimizerPlugin(),
new TerserPlugin()
], */
/*
splitChunks: {
chunks: 'all'
}, */
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
}
}
# webpack.config.dev.js
module.exports = {
output: {
filename: '[name].js',
},
mode: 'development',
devtool: 'inline-source-map',
//开发服务器
devServer: {
static: './dist'
}
}
# webpack.config.prod.js
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
output: {
filename: 'js/[name].[contenthash].js',
publicPath: 'http://localhost:8080/'
},
//生产模式
mode: 'production',
//优化配置
optimization: {
minimizer: [
new CssMinimizerPlugin(),
new TerserPlugin()
]
}
}
# 10.6 合并配置文件
配置文件拆分好后,新的问题来了,如何保证配置合并没用问题呢?webpack-merge 这个工具可以完美解决这个问题。
1、安装 webpack-merge 。
npm install webpack-merge -D
2、创建 webpack.config.js ,合并代码。
在 config 目录中,新建一个 webpack.config.js 文件,对前面三个文件进行合并。写入以下内容:
const { merge } = require('webpack-merge')
const commonConfig = require('./webpack.config.common.js')
const developmentConfig = require('./webpack.config.dev.js')
const productionConfig = require('./webpack.config.prod.js')
module.exports = (env) => {
switch(true){
case env.development:
return merge(commonConfig, developmentConfig)
case env.production:
return merge(commonConfig, productionConfig)
default:
throw new Error('No matching configuration was found!')
}
}
3、修改自定义脚本。
//package.json
"scripts": {
"start": "npx webpack serve -c ./config/webpack.config.js --env development",
"build": "npx webpack -c ./config/webpack.config.js --env production",
},
4、打包。
开发打包:
npm run start
生产打包:
npm run build
# 小结
1、bundle、chunk、vendor
bundle:n. 捆;(一) 包;(一) 扎;一批 (同类事物或出售的货品);
chunk:n. 厚块;厚片;大块;相当大的量;
vendor:n. 小贩;摊贩;
# 官方链接
webpack
webpack Plugins
css-loader
csv-loader
xml-loader
自定义 parser
entry
Entry dependencies
SplitChunksPlugin
dependOn option
SplitChunksPlugin
output.publicPath
开发环境
生产环境
环境配置
webpack-merge
# 视频教程
_ 千锋最新前端 webpack5 全套教程,全网最完整的 webpack 教程(基础 + 高级)
webpack5 课程分为四大部分,分别是 webpack 基础应用篇,webpack 高级应用篇,webpack 项目实战篇以及 webpack 内部原理篇。在本课程中,我们将通过前后呼应的 demo 来一步步从 0 到 1 地进行 webpack5 教学,在课程后期我们也将学到更低层的原理知识。从而做到知其然并知其所以然的精熟掌握程度。
# 配套资料
资料目录:H:\ 学习课程 \ediary 日记 \ 学习课程 \webpack5_千峰 \ 资料
练习代码目录:H:\ 学习课程 \ediary 日记 \ 学习课程 \webpack5_千峰 \wepack5_千峰_练习代码目录
作者:良仔 75
链接:https://juejin.cn/post/7048971886696497183
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。