Webpack浅应用
Webpack安装
Webpack
的安装目前分为两个:webpack
、webpack-cli
webpack和webpack-cli之间的关系
- 执行
Webpack
命令,会执行node_modules
下的.bin
目录下的Webpack
Webpack
在执行时是依赖Webpack-cli
的,如果没有安装就会报错Webpack-cli
中代码执行时,才是真正利用Webpack
进行编译和打包的过程- 在安装
Webpack
时,我们需要同时安装Webpack-cli
(第三方的脚手架事实上是没有使用Webpack-cli
的,而是类似于vue-service-cli
)
npm install webpack webpack-cli –D
或
yarn add webpack webpack-cli –D
Webpack默认打包
在根目录下创建一个webpack.config.js文件,来作为webpack的配置文件
const path = require('path')
// 导出配置信息
module.exports = {
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, './dist')
}
}
指定配置文件
Webpack
默认配置文件名称为webpack.config.js
,支持修改
- 我们可以通过
--config
来指定对应的配置文件
webpack --config custom.config.js
- 也可以在
package.json
中增加一个新的脚本
{
"scripts": {
"build": "webpack --config custom.config.js"
}
}
工作模式
Mode
配置选项,可以告知Webpack
使用响应模式的内置优化:
- 默认值是
production
- 可选值有:
'none' | 'development' | 'production'
Loader和Plugin
通过上述配置我们已经可以实现打包了,但是 Webpack
默认支持处理 js
文件,其他类型都处理不了,必须借助 Loader
来对不同类型的文件的进行处理
Loader
Loader
从字面的意思理解,是加载的意思。
由于Webpack
本身只能打包commonjs
规范的js
文件,所以,针对css
、图片等格式的文件没法打包,就需要引入第三方的模块进行打包。
配置方式
在webpack.config.js中写明配置信息
module.rules
中允许我们配置多个loader
(因为我们也会继续使用其他的loader
,来完成其他文件的加载)- 这种方式可以更好的表示
loader
的配置,也方便后期的维护,同时也让你对各个loader
有一个全局的概览
module.rules
的配置如下:
rules
属性对应的值是一个数组:[Rule]
- 数组中存放的是一个个的
Rule
,Rule
是一个对象,对象中可以设置多个属性
Rule
属性说明:
test
属性:用于对resource
(资源)进行匹配的,通常会设置成正则表达式use
属性:对应的值是一个数组:[UseEntry]
UseEntry
是一个对象,有以下三个属性loader
:必须有一个loader
属性,对应的值是一个字符串;options
:可选的属性,值是一个字符串或者对象,值会被传入到loader
中query
:目前已经使用options
来替代
loader
属性:Rule.use
:[ { loader } ]
的简写
Plugin
在Webpack
运行的生命周期中会广播出许多事件,Plugin
可以监听这些事件,在合适的时机通过Webpack
提供的API
来改变构建结果或做你想要的事情,作⽤于整个构建过程
Loader和Plugin的区别
Loader
本质就是一个函数,在该函数中对接收到的内容进行转换,返回转换后的结果。 因为Webpack
只认识JavaScript
,所以Loader
就成了翻译官,对其他类型的资源进行转译的预处理工作
Loader
在module.rules
中配置,作为模块的解析规则,类型为数组。每一项都是一个Object
,内部包含了test
(类型文件)、loader
、options
(参数)等属性
Plugin
是一个扩展器,它丰富了Wepack
本身,针对是Loader
结束后,Webpack
打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听Webpack
打包过程中的某些节点,执行广泛的任务
Plugin
在plugins
中单独配置,类型为数组,每一项是一个Plugin
的实例,参数都通过构造函数传入
Webpack基础配置
样式配置
css-loader
示例
通过JavaScript
创建一个元素,并给它添加样式,运行打包命令发现报错
对于加载css
文件我们需要一个可以读取css
文件的loader
:css-loader
css-loader配置
npm install css-loader -D
const path = require('path')
// 导出配置信息
module.exports = {
mode: 'development',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, './dist')
},
module: {
rules: [
{
test: /\.css$/,
// loader: 'css-loader' // 写法一
// use: ['css-loader'] // 写法二
use: [
{ loader: 'css-loader' }
]
}
]
}
}
style-loader配置
css-loader
配置完成后,页面还是没有效果,这是为什么?
- 因为
css-loader
只是负责将.css
文件进行解析,并不会将解析之后的css
插入到页面中 - 如果我们希望再完成插入
style
的操作,那么我们还需要另外一个loader
,就是style-loader
npm install style-loader -D
注意:因为loader的执行顺序是从右向左(或者说从下到上,或者说从后到前的),所以我们需要将styleloader写到css-loader的前面
// 注意:style-loader在css-loader前面
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' }
]
此时执行npm run build
,可以发现打包后的css
已经生效了
less-loader
在开发中,我们可能会使用less
、sass
、stylus
的预处理器来编写css
样式,如何处理这些样式呢
less样式
:
@fontSize: 30px;
@fontWeight: 700;
.content {
font-size: @fontSize;
font-weight: @fontWeight;
}
npm install less-loader -D
{
test: /\.less&/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' },
{ loader: 'less-loader' }
]
}
执行npm run build
,less
就可以自动转换成css
,并且页面生效
兼容性配置
Browserslist
在不同的前端工具之间,共享目标浏览器和Node.js版本的配置(如:babel、autoprefixer等共享兼容性配置)
在很多的脚手架配置中,都能看到类似于这样的配置信息
// 这里的百分之一,就是指市场占有率
> 1%
last 2 versions
not dead
上述配置信息其实就是Browserslist
的配置信息
Browserslist编写规则
加粗部分是比较常用的
Browserslist配置
// package.json配置
"browserslist": [
"last 2 versions",
"not dead",
"> 0.2%"
]
// 新建.browserslistrc文件
> 0.5%
last 2 versions
not dead
默认配置和条件关系
// 如果没有配置,那么也会有一个默认配置
browserslist.defualts = [
'> 0.5%',
'last 2 versions',
'Firefox ESR',
'not dead'
]
编写多个条件,多个条件之间的关系是什么?
PostCSS
PostCSS是一个通过JavaScript来转换样式的工具,这个工具可以帮助我们进行一些CSS的转换和适配,比如自动添加浏览器前缀、css样式的重置
postcss-loader
npm install postcss-loader -D
npm install autoprefixer -D
注意:postcss需要有对应的插件才会起效果,所以我们需要配置它的plugin
// 配置方式一,写在webpack.config.js
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
require('autoprefixer')
]
}
}
}
// 配置方式二,根目录新建postcss.config.js
module.exports = {
plugins: [
require('autoprefixer')
]
}
postcss-preset-env
事实上,在配置postcss-loader时,我们配置插件并不需要使用autoprefixer。我们可以使用另外一个插件:postcss-preset-env
postcss-preset-env
也是一个postcss
的插件- 它可以帮助我们将一些现代的
css
特性,转成大多数浏览器认识的css
,并且会根据目标浏览器或者运行时环境添加所需的polyfill
- 包括会自动帮助我们添加
autoprefixer
(所以相当于已经内置了autoprefixer
)
npm install postcss-preset-env -D
module.exports = {
plugins: [
require('postcss-preset-env')
]
}
Babel
Babel是一个工具链,主要用于旧浏览器或者环境中将ECMAScript 2015+代码转换为向后兼容版本的JavaScript
命令行使用
Babel本身可以作为一个独立的工具(和postcss一样),不和webpack等构建工具配合使用
npm install @babel/cli @babel/core
// src:是源文件的目录
// --out-dir:指定要输出的文件夹dist
npx babel src --out-dir dist
插件使用
箭头函数转换
npm install @babel/plugin-transform-arrow-functions -D
npx babel src --out-dir dist --plugins=@babel/plugin-transform-arrow-functions
const转换
npm install @babel/plugin-transform-block-scoping -D
npx babel src --out-dir dist --plugins=@babel/plugin-transform-block-scoping,@babel/plugin-transform-arrow-functions
Webpack中配置Babel
babel-loader
npm install babel-loader @babel/core
// 配置
module.exports = {
module: {
rules: [
{
test: /\.m?js$/,
use: {
loader: 'babel-loader',
options: {
plugins: [
'@babel/plugin-transform-block-scoping',
'@babel/plugin-transform-arrow-functions'
]
}
}
}
]
}
}
预设preset
如果要转换的内容过多,一个个设置是比较麻烦的,我们可以使用预设(preset)
比如常见的预设有三个:
- env
- react
- TypeScript
npm install @babel/preset-env
module.exports = {
module: {
rules: [
{
test: /\.m?js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env']
]
}
}
}
]
}
}
Stage-X
TC39:
TC39
是指技术委员会(Technical Committee
)第39
号- 它是
ECMA
的一部分,ECMA
是 “ECMAScript
” 规范下的JavaScript
语言标准化的机构 ECMAScript
规范定义了JavaScript
如何一步一步的进化、发展
TC39 遵循的原则是:分阶段加入不同的语言特性,新流程涉及四个不同的 Stage:
Stage 0
:strawman
(稻草人),任何尚未提交作为正式提案的讨论、想法变更或者补充都被认为是第 0 阶段的"稻草人"Stage 1
:proposal
(提议),提案已经被正式化,并期望解决此问题,还需要观察与其他提案的相互影响Stage 2
:draft
(草稿),Stage 2
的提案应提供规范初稿、草稿。此时,语言的实现者开始观察 runtime 的具体实现是否合理Stage 3
:candidate
(候补),Stage 3
提案是建议的候选提案。在这个高级阶段,规范的编辑人员和评审人员必须在最终规范上签字。Stage 3
的提案不会有太大的改变,在对外发布之前只是修正一些问题Stage 4
:finished
(完成),进入Stage 4
的提案将包含在ECMAScript
的下一个修订版中
Babel的Stage-X设置
在babel7
之前(比如babel6
中),我们会经常看到这种设置方式:
module.exports = {
"presets": ["stage-0"]
}
- 它表达的含义是使用对应的
babel-preset-stage-x
预设 - 但是从
babel7
开始,已经不建议使用了,建议使用preset-env
来设置
Babel配置文件
我们可以将babel
的配置信息放到一个独立的文件中,babel
给我们提供了两种配置文件的编写:
babel.config.json
(或者.js
,.cjs
,.mjs
)文件.babelrc.json
(或者.babelrc
,.js
,.cjs
,.mjs
)文件
它们有什么区别呢?目前很多的项目都采用了多包管理的方式(babel
、element-plus
等):
.babelrc.json
:早期使用较多的配置方式,但是对于配置Monorepos
项目是比较麻烦的babel.config.json(babel7)
:可以直接作用于Monorepos
项目的子包,更加推荐
polyfill
就像是一个补丁,可以帮助我们更好的使用JavaScript
什么时候使用polyfill
?
比如我们使用了一些语法特性(例如:Promise
, Generator
,Symbol
等以及实例方法例如=Array.prototype.includes
等),但是某些浏览器压根不认识这些特性,必然会报错,我们可以使用polyfill
来填充或者说打一个补丁,那么就会包含该特性了
为什么有了Babel
还要polyfill
?
真实的情况是babel
只是提供了一个“平台”,让更多有能力的plugins
入驻平台,是这些plugins
提供了将ECMAScript 2015+
版本的代码转换为向后兼容的JavaScript
语法的能力
babel
将ECMAScript 2015+
版本的代码分为了两种情况处理:
- 语法层:
let
、const
、class
、箭头函数等,这些需要在构建时进行转译,是指在语法层面上的转译。这种使用preset-env
api
方法层:Promise
、includes
、map
等,这些是在全局或者Object
、Array
等的原型上新增的方法,它们可以由相应es5
的方式重新定义。这种使用polyfill
如何使用polyfill
babel7.4.0
之前,可以使用@babel/polyfill
的包,但是该包现在已经不推荐使用了babel7.4.0
之后,可以通过单独引入core-js
和regenerator-runtime
来完成polyfill
的使用
npm install core-js regenerator-runtime --save
module.exports = {
module: {
rules: [
{
test: /\.m?js$/,
exclude: /node_modules/,
use: 'babel-loader'
}
]
}
}
配置babel.config.js
我们需要在babel.config.js
文件中进行配置,给preset-env
配置一些属性:
useBuiltIns
:设置以什么样的方式来使用polyfill
;corejs
:设置corejs
的版本,目前使用较多的是3.x
的版本;另外corejs
可以设置是否对提议阶段的特性进行支持,设置proposals
属性为true
即可;
useBuiltIns
useBuiltIns属性有三个常见的值:false、usage、entry
false
- 打包后的文件不使用
polyfill
来进行适配 - 这个时候是不需要设置
corejs
属性的
- 打包后的文件不使用
usage
- 根据源代码中出现的语言特性,自动检测所需要的
polyfill
- 确保最终包里的
polyfill
数量的最小化,打包的包相对会小一些 - 设置
corejs
属性来确定使用的corejs
的版本
- 根据源代码中出现的语言特性,自动检测所需要的
module.exports = {
module: {
rules: [
{
test: /\.m?js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage',
corejs: 3.8
}]
]
}
}
}
]
}
}
entry
:- 如果我们依赖的某一个库本身使用了某些
polyfill
的特性,但是因为我们使用的是usage
,所以之后用户浏览器可能会报错 - 如果担心出现这种情况,我们可以使用
entry
- 在入口文件中添加
import 'core-js/stable'; import 'regenerator-runtime/runtime'
,这样做会根据browserslist
目标导入所有的polyfill
,但是对应的包会变大
- 如果我们依赖的某一个库本身使用了某些
// 在入口文件添加
import 'core-js/stable'
import 'regenerator-runtime/runtime
module.exports = {
module: {
rules: [
{
test: /\.m?js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', {
useBuiltIns: 'entry',
corejs: 3.8
}]
]
}
}
}
]
}
}
TypeScript编译
通过TypeScript的compiler来转换成JavaScript
npm install typescript -D
// 生成tsconfig.json
tsc --init
// 编译ts代码
npx tsc
Webpack中使用TypeScript
使用ts-loader或babel-loader来处理ts文件
ts-loader
npm install ts-loader -D
module.exports = {
module: {
rules: [
{
test: /\.ts$/,
exclude: /node_modules/,
use: ['ts-loader']
}
]
}
}
babel-loader
npm install @babel/preset-typescript -D
module.exports = {
module: {
rules: [
{
test: /\.m?js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-typescript']
]
}
}
}
]
}
}
ts-loader和babel-loader选择
-
ts-loader(TypeScript Compiler)
:- 直接编译
TypeScript
,那么只能将ts
转换成js
- 我们还希望在这个过程中添加对应的
polyfill
,那么ts-loader
是无能为力的 - 需要借助于
babel
来完成polyfill
的填充功能
- 直接编译
-
babel-loader(Babel)
- 来直接编译
TypeScript
,也可以将ts
转换成js
,并且可以实现polyfill
的功能 babel-loader
在编译的过程中,不会对类型错误进行检测
- 来直接编译
综合考虑性能和扩展性,目前比较推荐的是babel + fork-ts-checker-webpack-plugin
方案
module: {
rules: [
{
test: /\.(t|j)s$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
},
],
},
],
},
plugins: [
// fork-ts-checker-webpack-plugin,创建一个新进程,专门来运行Typescript类型检查。这么做的原因是为了利用多核资源来提升编译的速度
new ForkTsCheckerWebpackPlugin(),
],
加载和处理其他资源
file-loader
file-loader的作用就是帮助我们处理import/require()方式引入的一个文件资源,并且会将它放到我们输出的文件夹中
file-loader使用
npm install file-loader -D
{
test: /\.(png|jpe?g|gif|svg)$/i,
use: {
loader: 'file-loader'
}
}
文件的名称规则
有时候我们处理后的文件名称按照一定的规则进行显示,比如保留原来的文件名、扩展名,同时为了防止重复,包含一个hash
值等,这个时候我们可以使用PlaceHolders
常用的placeholder
:
[ext]
: 处理文件的扩展名[name]
:处理文件的名称[hash]
:文件的内容,使用MD4
的散列函数处理,生成的一个128
位的hash
值(32个十六进制
)[contentHash]
:在file-loader
中和[hash]
结果是一致的[hash:<length>]
:截图hash
的长度[path]
:文件相对于Webpack
配置文件的路径
设置文件名称
{
test: /\.(png|jpe?g|gif|svg)$/i,
use: {
loader: 'file-loader',
options: {
name: 'img/[name].[hash:8].[ext]'
}
}
}
设置文件存放路径
{
test: /\.(png|jpe?g|gif|svg)$/i,
use: {
loader: 'file-loader',
options: {
name: '[name].[hash:8].[ext]',
outputPath: 'img'
}
}
}
url-loader
url-loader和file-loader的工作方式是相似的,但是可以将较小的文件,转成base64的URL
npm install url-loader -D
{
test: /\.(png|jpe?g|gif|svg)$/i,
use: {
loader: 'url-loader',
options: {
name: '[name].[hash:8].[ext]',
outputPath: 'img'
}
}
}
图片可以正常展示,但是dist
文件夹中会看不到图片文件,因为默认情况下url-loader会将所有的图片文件转成base64编码
limit
url-loader有一个options属性limit,可以用于设置转换的限制
下面的代码100kb
以下的图片都会进行base64
编码
{
test: /\.(png|jpe?g|gif|svg)$/i,
use: {
loader: 'url-loader',
options: {
limit: 100 * 1024,
name: '[name].[hash:8].[ext]',
outputPath: 'img'
}
}
}
asset module type
在webpack5之前,加载这些资源我们需要使用一些loader,比如raw-loader 、url-loader、file-loader 在webpack5之后,我们可以直接使用资源模块类型(asset module type),来替代上面的这些loader
资源模块类型(asset module type)
,通过添加 4
种新的模块类型,来替换所有这些loader
:
asset/resource
发送一个单独的文件并导出URL
。之前通过使用file-loader
实现asset/inline
导出一个资源的data URI
。之前通过使用url-loader
实现asset/source
导出资源的源代码。之前通过使用raw-loader
实现asset
在导出一个data URI
和发送一个单独的文件之间自动选择。之前通过使用url-loader
,并且配置资源体积限制实现
加载图片
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource'
}
自定义文件的输出路径和文件名
方式一:修改output,添加assetModuleFilename属性
output: {
filename: 'js/bundle.js',
path: path.resolve(__dirname, './dist'),
assetModuleFilename: 'img/[name].[hash:6][ext]'
}
方式二:在Rule中,添加一个generator属性,并且设置filename
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
generator: {
filename: 'img/[name].[hash:6][ext]'
}
}
url-loader的limit效果
// 将type修改为asset,添加一个parser属性,并且制定dataUrl的条件,添加maxSize属性
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset',
generator: {
filename: 'img/[name].[hash:6][ext]'
},
parser: {
dataUrlCondition: {
maxSize: 100 * 1024
}
}
}
字体文件处理
{
test: /\.(woff2?|eot|ttf)$/,
type: 'asset/resource',
generator: {
filename: 'font/[name].[hash:6][ext]'
}
}
Plugin
Plugin可以用于执行更加广泛的任务,比如打包优化、资源管理、环境变量注入等
CleanWebpackPlugin
CleanWebpackPlugin可以帮助我们重新打包时,自动删除dist文件夹
npm install clean-webpack-plugin -D
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
plugins: [
new CleanWebpackPlugin()
]
}
HtmlWebpackPlugin
我们的HTML文件是编写在根目录下的,而最终打包的dist文件夹中是没有index.html文件的,在进行项目部署时,必然也是需要有对应的入口文件index.html,这时我们可以用到HtmlWebpackPlugin
npm install html-webpack-plugin -D
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
plugins: [
new HtmlWebpackPlugin({
title: '示例'
})
]
}
现在dist
文件夹默认生成了一个index.html
文件,这个文件是怎么生成的呢?
- 默认情况下是根据
ejs
的一个模板来生成的 - 在
html-webpack-plugin
的源码中,有一个default_index.ejs
模块
自定义HTML模板
template
:指定我们要使用的模块所在的路径title
:在进行htmlWebpackPlugin.options.title
读取时,就会读到该信息
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: '示例',
template: './plubic/index.html',
})
]
}
DefinePlugin
按照上述步骤编译的时候还是会报错,因为在我们的模块中还使用到一个BASE_URL
的常量:<link rel="icon" href="<%= BASE_URL %>favicon.ico">
DefinePlugin
允许在编译时创建配置的全局常量,是Webpack
内置的插件(不需要单独安装)
const { DefinePlugin } = require('webpack')
module.exports = {
plugins: [
new DefinePlugin({
BASE_URL: '"./"'
})
]
}
这时候,编译template
就可以正确的编译了,会读取到BASE_URL
的值
Webpack搭建本地服务器
我们希望当文件发生变化时,可以自动的完成编译和展示,Webpack提供了几种可选的方式:webpack watch mode、webpack-dev-server、webpack-dev-middleware
webpack watch
在该模式下,webpack依赖图中的所有文件,只要有一个发生了更新,那么代码将被重新编译,我们不需要去手动运行npm run build指令了
如何开启watch
?
- 在导出的配置中,添加
watch: true
- 启动
Webpack
的命令中,添加--watch
的标识
"scripts": {
"watch": "webpack --watch"
}
webpack-dev-server
上面的方式可以监听到文件的变化,但是它本身没有自动刷新浏览器的功能
npm install --save-dev webpack-dev-server
// 添加一个新的script脚本
"serve": "webpack serve --config webpack.config.js"
webpack-dev-server
在编译之后不会写入到任何输出文件。而是将bundle
文件保留在内存中:webpack-dev-server
使用了一个库叫memfs
(memory-fs
Webpack
自己写的)
webpack-dev-middleware
webpack-dev-server已经帮助我们做好了一切,比如通过express启动一个服务,比如HMR(热模块替换),但是如果我们想要更好的自由度,可以使用webpack-dev-middleware
什么是webpack-dev-middleware
webpack-dev-middleware
是一个封装器(wrapper
),它可以把webpack
处理过的文件发送到一个server
webpack-dev-server
在内部使用了它,然而它也可以作为一个单独的package
来使用,以便根据需求进行更多自定义设置
npm install --save-dev express webpack-dev-middleware
HMR(模块热替换)
在应用程序运行过程中,替换、添加、删除模块,无需重新刷新整个页面
如何使用HMR
默认情况下,webpack-dev-server已经支持HMR,我们只需要开启即可。在不开启HMR的情况下,当我们修改了源代码之后,整个页面会自动刷新,使用的是live reloading
devServer: {
hot: true
}
但是我们会发现,当我们修改了某一个模块的代码时,依然是刷新的整个页面:因为我们需要去指定哪些模块发生更新时,进行HMR
if(module.hot) {
module.hot.accept('./utils.js', () => {
console.log('HMR')
})
}
框架的HMR
在Vue和React项目,我们不需要手动写入module.hot.accept相关API,因为社区已经有成熟的解决方案。
vue
:使用vue-loader
,该loader
支持vue
组件的HMR
,提供开箱即用的体验。
react
:React Hot Loader
,实时调整react
组件(目前React
官方已经弃用了,改成使用reactrefresh
)
Vue的HMR
Vue的加载我们需要使用vue-loader,而vue-loader加载的组件默认会帮助我们进行HMR的处理
npm install vue-loader vue-template-compiler -D
const VueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
module: {
rules: [
{
test: /\.vue$/,
use: 'vue-loader'
}
]
},
plugins: [
new VueLoaderPlugin()
]
}
Source Map
source-map可以让我们能够调试经过打包压缩的代码
Source Map文件解析
大致文件结构:
{
"version": 3,
"sources": [
"webpack://webpack5-template/./src/index.js"
],
"names": [],
"mappings": ";;;;;AAAA;AACA;AACA;AACA;AACA,eAAe;AACf;AACA;AACA,mB",
"file": "main.bundle.js",
"sourcesContent": [
"console.log('Interesting!!!')"
],
"sourceRoot": ""
}
version
:当前使用的版本,也就是最新的第三版;psources
:从哪些文件转换过来的source-map
和打包的代码(最初始的文件);names
:转换前的变量和属性名称(因为我目前使用的是development
模式,所以不需要保留转换前的名 称);mappings
:source-map
用来和源文件映射的信息(比如位置信息等),一串base64 VLQ
(veriablelength quantity
可变长度值)编码;file
:打包后的文件(浏览器加载的文件);sourceContent
:转换前的具体代码信息(和sources
是对应的关系);sourceRoot
:所有的sources
相对的根目录;
Webpack配置Source Map
Webpack为我们提供了非常多的选项(目前是26个),来处理source-map devtools
简单介绍
source-map
:最标准的source-map
打包形式,生成一个独立的source-map
文件,并且在bundle
文件中有一个注释,指向source-map
文件cheap-source-map
:生成sourcemap
,但是会更加高效一些(cheap
低开销),因为它没有生成列映射(Column Mapping
)cheap-module-source-map
:生成sourcemap
,类似于cheap-source-map
,但是对源自loader
的sourcemap
处理会更好
开发最佳实践
- 开发阶段:推荐使用
source-map
或者cheap-module-source-map
- 测试阶段:推荐使用
source-map
或者cheap-module-source-map
- 发布阶段:
false
、缺省值(不写)
Webpack优化
Terser
Terser可以帮助我们压缩、丑化我们的代码,让我们的bundle变得更小
什么是Terser
Terser
是一个JavaScript
的解释(Parser)
、Mangler
(绞肉机)/Compressor
(压缩机)的工具集- 早期我们会使用
uglify-js
来压缩、丑化我们的JavaScript
代码,但是目前已经不再维护,并且不支持ES6+
的语法 Terser
是从uglify-es
fork
过来的,并且保留它原来的大部分API
以及适配uglify-es
和uglify-js@3
等
Terser
是一个独立的工具,所以它可以单独安装
npm install terser -g
// 可以在命令行使用terser
teser [input files] [options]
// 例子
teser js/test.js -c -m
在Webpack中使用
在webpack中有一个minimizer属性,在production模式下,默认就是使用TerserPlugin来处理我们的代码的 如果我们对默认的配置不满意,也可以自己来创建TerserPlugin的实例,并且覆盖相关的配置
注意:在生产环境下打包默认会开启 js 压缩,但是当我们手动配置 optimization 选项之后,就不再默认对 js 进行压缩,需要我们手动去配置
Webpack5
内置了terser-webpack-plugin
插件,所以我们不需重复安装,直接引用即可
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimize: true, // 开启最小化
minimizer: [
new TerserPlugin()
]
},
}
CSS压缩
CSS压缩通常是去除无用的空格等,因为很难去修改选择器、属性的名称、值等 CSS的压缩我们可以使用另外一个插件:css-minimizer-webpack-plugin css-minimizer-webpack-plugin是使用cssnano工具来优化、压缩CSS(也可以单独使用)
npm install css-minimizer-webpack-plugin -D
在optimization.minimizer
中配置
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
optimization: {
minimize: true, // 开启最小化
minimizer: [
new CssMinimizerPlugin({
parallel: true
}),
]
},
}
Tree Shaking
什么是Tree Shaking
Tree Shaking
是一个术语,在计算机中表示消除死代码(dead_code
)- 最早的想法起源于
LISP
,用于消除未调用的代码(纯函数无副作用,可以放心的消除,这也是为什么要求我们在进行函数式编程时,尽量使用纯函数的原因之一)
JavaScript的Tree Shaking
- 对
JavaScript
进行Tree Shaking
是源自打包工具rollup
,这是因为Tree Shaking
依赖于ES Module
的静态语法分析(不执行任何的代码,可以明确知道模块的依赖关系) Webpack2
正式内置支持了ES2015
模块,和检测未使用模块的能力- 在
Webpack4
正式扩展了这个能力,并且通过package.json
的sideEffects
属性作为标记,告知Webpack
在编译时,哪里文件可以安全的删除掉 Webpack5
中,也提供了对部分CommonJS
的Tree shaking
的支持commonjs-tree-shaking
Webpack实现Tree Shaking
Webpack实现Tree Shaking采用了两种不同的方案 usedExports:通过标记某些函数是否被使用,之后通过Terser来进行优化的 sideEffects:跳过整个模块/文件,直接查看该文件是否有副作用
usedExports
// 开发环境配置
module.exports = {
mode: 'development',
optimization: {
usedExports: true,
}
};
// 在生产环境下,`Webpack`默认会添加`Tree Shaking`的配置,因此只需写一行`mode: 'production'`即可
module.exports = {
mode: 'production',
};
sideEffects
告知webpack compiler哪些模块时有副作用的,副作用的意思是这里面的代码有执行一些特殊的任务,不能仅仅通过export来判断这段代码的意义
sideEffects配置
在package.json
中设置sideEffects
的值:
sideEffects
默认为true
, 告诉Webpack
,所有文件都有副作用,他们不能被Tree Shaking
。sideEffects
为false
时,告诉Webpack
,没有文件是有副作用的,他们都可以Tree Shaking
。sideEffects
为一个数组时,告诉Webpack
,数组中那些文件不要进行Tree Shaking
,其他的可以Tree Shaking
sideEffects 对全局 CSS 的影响
对于那些直接引入到 js 文件的文件,例如全局的 css,它们并不会被转换成一个 CSS 模块
/* reset.css */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
background-color: #eaeaea;
}
// main.js
import "./styles/reset.css"
这些代码打包后,你就会发现样式并没有起作用,原因在于:上面我们将 sideEffects 设置为 false 后,所有的文件都会被 Tree Shaking,通过 import 这样的形式引入的 CSS 就会被当作无用代码处理掉
为了解决这个问题,可以在loader
的规则配置中,添加sideEffects: true
,告诉Webpack
这些文件不要Tree Shaking
{
test: /\.css/i,
use: [
'style-loader',
'css-loader'
],
sideEffects: true
}
CSS实现Tree Shaking
CSS的Tree Shaking需要借助于其他插件,如:PurgeCSS
安装PurgeCss
的Webpack
插件
npm install purgecss-webpack-plugin -D
module.exports = {
plugins: [
new PurgecssPlugin({
// paths: 表示要检测哪些目录下的内容需要被分析,这里我们可以使用glob
paths: glob.sync(`${resolveApp('./src')}/**/*`, { nodir: true }),
// 默认情况下,Purgecss会将我们的html标签的样式移除掉,如果我们希望保留,可以添加一个safelist的属性
safelist: function() {
return {
standard: ['html']
}
}
})
]
}
HTTP压缩
HTTP压缩是一种内置在 服务器 和 客户端 之间的,以改进传输速度和带宽利用率的方式
压缩流程
- 第一步:
HTTP
数据在服务器发送前就已经被压缩了;(可以在Webpack
中完成) - 第二步:兼容的浏览器在向服务器发送请求时,会告知服务器自己支持哪些压缩格式
- 第三步:服务器在浏览器支持的压缩格式下,直接返回对应的压缩后的文件,并且在响应头中告知浏览器
目前的压缩格式
compress
–UNIX
的“compress”
程序的方法(历史性原因,不推荐大多数应用使用,应该使用gzip
或deflate
)deflate
– 基于deflate
算法(定义于RFC 1951RFC 1951
)的压缩,使用zlib
数据格式封装gzip
–GNU zip
格式(定义于RFC 1952
),是目前使用比较广泛的压缩算法br
– 一种新的开源压缩算法,专为HTTP
内容的编码而设计
Webpack对文件压缩
webpack中相当于是实现了HTTP压缩的第一步操作,我们可以使用CompressionPlugin
CompressionPlugin
npm install compression-webpack-plugin -D
module.exports = {
plugins: [
new CompressionPlugin({
test: /\.(css|js)$/, // 匹配哪些文件需要压缩
threshold: 500, // 设置文件从多大开始压缩
minRatio: 0.7, // 至少压缩的比例
algorithm: 'gzip', // 采用的压缩算法
}),
]
}
Scope Hoisting
Scope Hoisting从webpack3开始增加的一个新功能 功能是对作用域进行提升,并且让webpack打包后的代码更小、运行更快
默认情况下webpack
打包会有很多的函数作用域,包括一些(比如最外层的)IIFE
,无论是从最开始的代码运行,还是加载一个模块,都需要执行一系列的函数,Scope Hoisting
可以将函数合并到一个模块中来运行
使用Scope Hoisting
非常的简单,Webpack
已经内置了对应的模块:
- 在
production
模式下,默认这个模块就会启用 - 在
development
模式下,我们需要自己来打开该模块
new webpack.optimize.ModuleConcatenationPlugin()
DLL
DLL全称是动态链接库(Dynamic Link Library),是为软件在Windows中实现共享函数库的一种实现方式 Webpack中也有内置DLL的功能,它指的是可以将可以共享,并且不经常改变的代码,抽取成一个共享的库 这个库在之后编译的过程中,会被引入到其他项目的代码中,减少的打包的时间
Webpack DLL
webpack内置DllPlugin帮助生成DLL文件
DLL 打包
module.exports = {
mode: 'production',
entry: {
react: ['react', 'react-dom']
},
output: {
path: path.resovle(__dirname, './dll'),
filename: 'dll_[name].js',
library: 'dll_[name]'
},
plugins: [
new webpack.DllPlugin({
name: 'dll_[name]',
path: path.resolve(__dirname, './dll/[name].manifest.json')
})
]
}
DLL使用
如果我们在我们的代码中使用了react、react-dom,我们有配置splitChunks的情况下,他们会进行分包,打包到一个独立的chunk中,但是我们现在有了dll_react,不再需要单独去打包它们,可以直接去引用dll_react即可
// 通过DllReferencePlugin插件告知要使用的DLL库
new DllReferencePlugin({
context:path.resolve(__dirname, "../"),
manifest:path.resolve(__dirname,"../dll/react.manifest.json")
}),
// 通过AddAssetHtmlPlugin插件,将我们打包的DLL库引入到Html模块中
new AddAssetHtmlWebpackPlugin({
outputPath:"../build/js",
filepath:path.resolve(__dirname, "../dll/dll_react.js")
})
代码分离
认识代码分离
- 它主要的目的是将代码分离到不同的
bundle
中,之后我们可以按需加载,或者并行加载这些文件 - 默认情况下,所有的
JavaScript
代码(业务代码、第三方依赖、暂时没有用到的模块)在首页全部都加载,就会影响首页的加载速度 - 代码分离可以分出更小的
bundle
,以及控制资源加载优先级,提供代码的加载性能
常用的代码分离有三种:
- 入口起点:使用
entry
配置手动分离代码 - 防止重复:使用
Entry Dependencies
或者SplitChunksPlugin
去重和分离代码 - 动态导入:通过模块的内联函数调用来分离代码
入口起点
配置多入口
entry: {
index: './src/index.js',
main: './src/main.js'
}
Entry Dependencies(入口依赖)
假如我们的index.js
和main.js
都依赖两个库:lodash
、dayjs
:
如果我们单纯的进行入口分离,那么打包后的两个bunlde
都有会有一份lodash
和dayjs
,事实上我们可以对他们进行共享
entry: {
index: { import: './src/index.js', dependOn: 'shared' },
main: { import: './scr/main.js', dependOn: 'shared' },
shared: ['lodash', 'axios']
},
output: {
filename: '[name].bundle.js',
path: resolveApp('./build'),
publicPath: ''
}
SplitChunks
使用SplitChunksPlugin来实现,Webpack已经默认安装和集成
Webpack
提供了SplitChunksPlugin
默认的配置,我们也可以手动来修改它的配置:
// 默认配置中,chunks仅仅针对于异步(async)请求,我们可以设置为initial或者all
optimization: {
splitChunks: {
chunks: 'all'
}
}
关键属性介绍:
参数 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
Chunks | initial:对同步的代码进行处理,all:同步异步代码都处理 | string | all / initial / async | async |
minSize | 拆分包的大小,至少为minSize | number | - | - |
maxSize | 将大于maxSize的包,拆分为不小于minSize的包 | number | - | - |
minChunks | 至少被引入的次数 | number | - | 1 |
name | 设置拆包的名称, 设置为false后,需要在cacheGroups中设置名称 | string / false | - | - |
cacheGroups | 用于对拆分的包就行分组,比如一个lodash在拆分之后,并不会立即打包,而是会等到有没有其他符合规则的包一起来打包 | object | - | - |
动态导入(dynamic import)
Webpack
提供了两种实现动态导入的方式:
- 使用
ECMAScript
中的import()
语法来完成,也是目前推荐的方式 - 使用
Webpack
遗留的require.ensure
,目前已经不推荐使用
比如我们有一个模块bar.js
:
该模块我们希望在代码运行过程中来加载它(比如判断一个条件成立时加载),因为我们并不确定这个模块中的代码一定会用到,所以最好拆分成一个独立的js
文件,这样可以保证不用到该内容时,浏览器不需要加载和处理该文件的js
代码,这个时候我们就可以使用动态导入
动态导入的文件命名
因为动态导入通常是一定会打包成独立的文件的,所以并不会在cacheGroups
中进行配置,那么它的命名我们通常会在output
中,通过 chunkFilename
属性来命名
output: {
filename: '[name].bundle.js',
path: resolveApp('./build'),
chunkFilename: 'chunk_[id]_[name].js'
}
但是默认情况下我们获取到的[name]
是和id
的名称保持一致的:
// 通过magic comments(魔法注释)修改name
import(/* webpackChunkName: 'bar' */"./bar").then({ default: bar }) => {
bar()
}
Webpack5新特性
大致方向如下:
- 尝试用持久性缓存来提高构建性能。
- 尝试用更好的算法和默认值来改进长期缓存。
- 尝试用更好的
Tree Shaking
和代码生成来改善包大小。 - 尝试改善与网络平台的兼容性。
- 尝试在不引入任何破坏性变化的情况下,清理那些在实现
v4
功能时处于奇怪状态的内部结构。 - 试图通过现在引入突破性的变化来为未来的功能做准备,尽可能长时间地保持在
v5
版本上。
支持崭新的 Web 平台功能
JSON 模块
对JSON
模块,会与现在的提案保持一致,并且要求进行默认的导出,否则会有警告信息。即使使用默认导出,未使用的属性也会被optimization.usedExports
优化丢弃,属性会被 optimization.mangleExports
优化打乱。
如果想用自定义的JSON
解析器,可以在Rule.parser.parse
中指定一个自定义的JSON
解析器来导入类似JSON
的文件(例如针对 toml
、yaml
、json5
等)
内置静态资源构建能力(Asset Modules)
Webpack5
提供了内置的静态资源构建能力,我们不需要安装额外的loader
,仅需要简单的配置就能实现静态资源的打包和分目录存放
原生 Worker 支持
当把资源的new URL
和new Worker/new SharedWorker/navigator.serviceWorker.register
结合起来时,webpack
会自动为web worker
创建一个新的入口点(entrypoint
)。new Worker(new URL("./worker.js", import.meta.url))
选择这种语法也是为了允许在没有打包工具的情况下运行代码。这种语法在浏览器的原生 ECMAScript 模块中也可以使用
内置FileSystem Cache能力加速二次构建
Webpack5
之前,我们会使用cache-loader
缓存一些性能开销较大的 loader
,或者是使用hard-source-webpack-plugin
为模块提供一些中间缓存。在Webpack5
之后,默认就为我们集成了一种自带的缓存能力(对 module
和chunks
进行缓存)。通过如下配置,即可在二次构建时提速
module.exports = {
cache: {
type: 'filesystem',
// 可选配置
buildDependencies: {
config: [__filename], // 当构建依赖的config文件(通过 require 依赖)内容发生变化时,缓存失效
},
name: '', // 配置以name为隔离,创建不同的缓存文件,如生成PC或mobile不同的配置缓存
},
}
生产环境下默认的缓存存放目录在 node_modules/.cache/webpack/default-production
中,如果想要修改,可通过配置name
,来实现分类存放。
深度Tree Shaking能力支持
Webpack5能够支持深层嵌套的export 的Tree Shaking 官网案例
资源打包策略更优
Prepack 是 Facebook 开源的一个 JavaScript 代码优化工具,运行在 “编译” 阶段,生成优化后的代码。下面是 Prepack 官网上的一个示例,我们可以看到,在对于任何输入,函数都能得到一个固定输出的时候,Prepack 就能在编译时,将结果帮我们计算出来。对于一些复杂且固定的计算逻辑而言,这种“预计算”能力,既能减小我们包的体积,又能加快运行时的速度。 官方案例
Webpack5
内置了Prepack
的部分能力,能够在极致之上,再度优化你的项目产物体积
Top Level Await
Webpack5还支持Top Level Await。即允许开发者在async函数外部使用await 字段。它就像巨大的async函数,原因是import它们的模块会等待它们开始执行它的代码,因此,这种省略async的方式只有在顶层才能使用
module.exports = {
experiments: {
topLevelAwait: true,
},
}
为了eslint
语法检测的支持,我们还需要添加babel
插件@babel/plugin-syntax-top-level-await
来让我们的babel
能够识别top level await
语法
转载自:https://juejin.cn/post/7044057737948299271