开发中,webpack文件一般分为3个:
webpack.base.conf.js
(基础文件)webpack.dev.conf.js
(开发环境使用的webpack
,需要与webpack.base.conf.js
结合使用)webpack.prod.conf.js
(上线环境使用的webpack
,需要与webpack.base.conf.js
结合使用)webpack
在启动后,会根据Entry
配置的入口,递归解析所依赖的文件。这个过程分为「搜索文件」和「把匹配的文件进行分析、转化」的两个过程,因此可以从这两个角度来进行优化配置。
resolve
字段告诉webpack
怎么去搜索文件,所以首先要重视resolve
字段的配置:参考文档:https://webpack.docschina.org/configuration/resolve/#resolve
resolve
用来「配置模块如何解析」。例如,当在 ES2015
中调用 import 'lodash'
,resolve
选项能够对webpack
查找'lodash'
的方式去做修改(查看模块)。
// webpack.config.js
module.exports = {
//...
resolve: {
// configuration options
}
};
module.export = {
resolve: {
modules:[path.resolve(__dirname, 'node_modules')]
extensions: ['.js', '.jsx'],
mainFiles: ['index', 'child'],
alias: {
'@/src': path.resolve(__dirname, `../src`), // 当看到@/src这个路径或字符串的时候,实际上指向的是../src目录
}
}
}
(1). resolve.modules
参考文档:https://www.webpackjs.com/configuration/resolve/#resolve-modules
resolve.modules
告诉webpack
解析时「应该搜索的目录」。
「绝对路径和相对路径」都能使用,但是要知道他们之间有一点差异。通过查看当前目录以及祖先路径(即 ./node_modules, ../node_modules
等等),相对路径将类似于 Node
查找 'node_modules
' 的方式进行查找。「使用绝对路径,将只在给定目录中搜索」。
// webpack.config.js
module.exports = {
//...
resolve: {
modules: ['node_modules'] // 相对路径写法,会按./node_modules, ../node_modules的方式查找
}
};
如果你想要添加一个目录到模块搜索目录,此目录优先于 node_modules/
搜索:
// webpack.config.js
module.exports = {
//...
resolve: {
modules: [path.resolve(__dirname, 'src'), 'node_modules'] // 绝对路径写法
}
};
「因此:设置resolve.modules:[path.resolve(__dirname, 'node_modules')]
避免层层查找」。(2). resolve.mainFields
参考文档:https://www.webpackjs.com/configuration/resolve/#resolve-mainfields
当从 npm
包中导入模块时(例如,import * as D3 from "d3"
),此选项将决定在 package.json
中使用哪个字段导入模块。根据 webpack
配置中指定的 target
不同,默认值也会有所不同。
当target
属性设置为webworker
, web
或者没有指定,默认值为:
module.exports = {
//...
resolve: {
mainFields: ['browser', 'module', 'main']
}
};
对于其他任意的 target
(包括 node
),默认值为:
module.exports = {
//...
resolve: {
mainFields: ['module', 'main']
}
};
例如,考虑任意一个名为 upstream
的 library
,其 package.json
包含以下字段
{
"browser": "build/upstream.js",
"module": "index"
}
在我们 import * as Upstream from 'upstream'
时,这实际上会从browser
属性解析文件。在这里 browser
属性是最优先选择的,因为它是 mainFields
的第一项。同时,由 webpack
打包的Node.js
应用程序首先会尝试从 module
字段中解析文件。
(3).resolve.alias
参考文档:https://www.webpackjs.com/configuration/resolve/#resolve-alias
创建 import
或 require
的别名,来「确保模块引入变得更简」单。例如,一些位于 src/
文件夹下的常用模块:
alias: {
Utilities: path.resolve(__dirname, 'src/utilities/'),
Templates: path.resolve(__dirname, 'src/templates/')
}
现在,「替换「在导入时使用相对路径」这种方式」,就像这样:
import Utility from '../../utilities/utility';
你可以这样使用别名:
import Utility from 'Utilities/utility';
也可以在给定对象的键后的末尾添加 $
,以表示精准匹配:
module.exports = {
//...
resolve: {
alias: {
xyz$: path.resolve(__dirname, 'path/to/file.js')
}
}
};
这将产生以下结果:
import Test1 from 'xyz'; // 精确匹配,所以 path/to/file.js 被解析和导入
import Test2 from 'xyz/file.js'; // 非精确匹配,触发普通解析
PS
: 如果你使用了TS
,在webpack
中使用了resolve.alias
,一般需要在tsconfig.json
文件中对其进行配置,否则使用alias
会导致无法找到响应目录而报错:
// tsconfig.json
"compilerOptions": {
"paths": {
"@/src/*": ["./src/*"],
'Templates': ["./src/templates/"],
},
}
对庞大的第三方模块设置resolve.alias
, 使webpack
直接使用库的min
文件,避免库内解析
(4). resolve.extensions
参考文档:https://www.webpackjs.com/configuration/resolve/#resolve-extensions
配置resolve.extensions
可以自动解析确定的扩展。合理配置resolve.extensions
,以减少文件查找
resolve.extensions
默认值:extensions:['.wasm', '.mjs', '.js', '.json']
,当导入语句没带文件后缀时,Webpack
会根据extensions
定义的后缀列表进行文件查找,所以:
require(./data)
要写成require(./data.json)
常用写法:
extensions: ['.js', '.json', '.ts', '.tsx', '.scss']
module.noParse
字段告诉Webpack
不必解析哪些文件,可以用来排除对非模块化库文件的解析参考文档:https://webpack.docschina.org/configuration/module/#module-noparse
如jQuery、ChartJS
,另外如果使用resolve.alias
配置了react.min.js
,则也应该排除解析,因为react.min.js
经过构建,已经是可以直接运行在浏览器的、非模块化的文件了。noParse
值可以是RegExp、[RegExp]、function
module:{ noParse:[/jquery|chartjs/, /react\.min\.js$/]}
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: `/fonts/[name].[hash:8].[ext]`
}
}
开发过程中修改代码后,需要自动构建和刷新浏览器,以查看效果。这个过程可以使用Webpack
实现自动化,Webpack
负责监听文件的变化,DevServer
负责刷新浏览器。
Webpack
监听文件Webpack
可以开启监听:启动webpack
时加上--watch
参数
https://webpack.js.org/configuration/dev-server/#root
// package.json
"scripts": {
"dev": "webpack --watch" // --watch监听打包文件,只要发生变化,就会重新打包。只要有这个参数就生效。
}
但我们想要更丰富的功能:执行npm run dev
就会自动打包,并自动打开浏览器,同时可以模拟一些服务器上的特性,此时就要借助WebpackDevServer
来实现。
devServer:{
contentBase: './dist' // 服务器起在哪个文件夹下。WebpackDevServer会帮助我们在这个文件夹下起一个服务器
}
配置
devServer:{
port: 8080, // 默认8080
contentBase: './dist',
open: true, // 自动打开浏览器,并访问服务器地址。file协议不行,不能发送ajax请求
proxy: {
'./api': 'http://localhost:3000' // 用户访问 /api 这个路径会被转发到 http://localhost:3000,支持跨域代理
}
}
devServer: {
contentBase: config.build.assetsRoot,
host: config.dev.host,
port: config.dev.port,
open: true,
inline: true,
hot: true,
overlay: {
warnings: true,
errors: true
},
historyApiFallback: {
rewrites: [
{ from: /^\/index\//, to: `http://${config.dev.host}:${config.dev.port}/index.html` },
]
},
noInfo: true,
disableHostCheck: true,
proxy: {
// '/user/message': {
// target: `http://go.buy.test.mi.com`,
// changeOrigin: true,
// secure: false
// },
}
},
DevServer
刷新浏览器有两种方式:iframe
,通过刷新iframe
实现刷新效果默认情况下,以及 devserver: {inline:true}
都是采用第一种方式刷新页面。第一种方式DevServer
因为不知道网页依赖哪些Chunk
,所以会向每个chunk
中都注入客户端代码,当要输出很多chunk
时,会导致构建变慢。而一个页面只需要一个客户端,所以关闭inline
模式可以减少构建时间,chunk
越多提升越明显。关闭方式:
webpack-dev-server --inline false
devserver:{inline:false}
关闭inline
后入口网址变为http://localhost:8080/webpack-dev-server/
另外devServer.compress
参数可配置是否采用Gzip
压缩,默认为false
HMR
模块热替换不刷新整个网页而只重新编译发生变化的模块,并用新模块替换老模块,所以预览反应更快,等待时间更少,同时不刷新页面能保留当前网页的运行状态。原理也是向每一个chunk
中注入代理客户端来连接DevServer
和网页。开启方式:
webpack-dev-server --hot
使用HotModuleReplacementPlugin
,比较麻烦
// package.json
"scripts": {
"start": "webpack-dev-server" ,
}
webpack-dev-server
打包后的dist
中的内容放到了内存中,加快访问速度
const webpack = require('webpack')
module.exports = {
devServer:{
port: 8080, // 默认8080
contentBase: './dist',
open: true,
hot: true, // 让webpack-dev-server开启Hot Module Replacement功能
hotOnly: true, // 即使HMR功能没有生效,也不让浏览器自动刷新,
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
'postcss-loader',
]
},
]
},
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html',
}),
new CleanWebpackPlugin(['dist']), // 开发环境不需要此配置
new webpack.HotModuleReplacementPlugin() // 使用webpack插件,可用于开发环境
],
}
开启后如果修改子模块就可以实现局部刷新,但如果修改的是根JS
文件,会整页刷新,原因在于,子模块更新时,事件一层层向上传递,直到某层的文件接收了当前变化的模块,然后执行回调函数。如果一层层向外抛直到最外层都没有文件接收,就会刷新整页。使用 NamedModulesPlugin
可以使控制台打印出被替换的模块的名称而非数字ID
,另外同webpack
监听,忽略node_modules
目录的文件可以提升性能。
代码运行环境分为「开发环境」和「生产环境」,代码需要根据不同环境做不同的操作,许多第三方库中也有大量的根据开发环境判断的if else
代码,构建也需要根据不同环境输出不同的代码,所以需要一套机制可以在源码中区分环境,区分环境之后可以使输出的生产环境的代码体积减小。Webpack
中使用DefinePlugin
插件来定义配置文件适用的环境。
Webpack
内置UglifyJS
插件、ParallelUglifyPlugin
使用terser-webpack-plugin
插件压缩JS
代码: 参考文档:https://webpack.js.org/plugins/terser-webpack-plugin/
optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
safari10: true
}
})
],
}
取代了 UglifyJsPlugin
// 取代 new UglifyJsPlugin(/* ... */)
CSS
2.1 mini-css-extract-plugin
:https://webpack.js.org/plugins/mini-css-extract-plugin/ 。该插件将CSS
提取到单独的文件中。它为每个包含CSS
的JS
文件创建一个CSS
文件。它支持CSS
和SourceMap
的按需加载。它基于新的webpack v4
功能(模块类型)构建,并且需要webpack 4
才能正常工作。
2.2 optimize-css-assets-webpack-plugin
: https://www.npmjs.com/package/optimize-css-assets-webpack-plugin 。主要是用来压缩css
文件
plugins: [
new MiniCssExtractPlugin({
filename: path.join('css/[name].css?[contenthash:8]'),
chunkFilename: path.join('css/[name].chunk.css?[contenthash:8]')
}),
new OptimizeCssAssetsPlugin({
assetNameRegExp: /\.css\?\w*$/
})
],
2.3 cssnano
基于PostCSS
,不仅是删掉空格,还能理解代码含义,例如把color:#ff0000
转换成 color:red
,css-loader
内置了cssnano
,只需要使用 css-loader?minimize
就可以开启cssnano
压缩。另外一种压缩CSS
的方式是使用PurifyCSSPlugin,需要配合 extract-text-webpack-plugin
使用,它主要的作用是可以去除没有用到的CSS
代码,类似JS
的Tree Shaking
。
Tree Shaking
剔除JS
死代码参考文档:https://webpack.docschina.org/guides/tree-shaking/
Tree Shaking
可以剔除用不上的死代码,它依赖ES6
的import、export
的模块化语法,最先在Rollup
中出现,Webpack 2.0
将其引入。适合用于Lodash、utils.js
等工具类较分散的文件。它正常工作的前提是代码必须采用ES6
的模块化语法,因为ES6
模块化语法是静态的(在导入、导出语句中的路径必须是静态字符串,且不能放入其他代码块中)。如果采用了ES5
中的模块化,例如module.export = {...}、require( x+y )、if (x) { require( './util' ) }
,则Webpack
无法分析出可以剔除哪些代码。
tree shaking
是一个术语,通常用于描述移除 JavaScript
上下文中的未引用代码(dead-code
)。它依赖于 ES2015
模块语法的 静态结构 特性,例如import
和 export
。这个术语和概念实际上是由 ES2015
模块打包工具 rollup
普及起来的。
webpack 4
正式版本扩展了此检测能力,通过package.json
的 "sideEffects"
属性作为标记,向 compiler
提供提示,表明项目中的哪些文件是 "pure(纯的 ES2015 模块)
",由此可以安全地删除文件中未使用的部分。
参考文档:https://webpack.docschina.org/guides/tree-shaking/#%E5%B0%86%E5%87%BD%E6%95%B0%E8%B0%83%E7%94%A8%E6%A0%87%E8%AE%B0%E4%B8%BA%E6%97%A0%E5%89%AF%E4%BD%9C%E7%94%A8
注意,所有导入文件都会受到tree shaking
的影响。这意味着,如果在项目中使用类似css-loader
并import
一个 CSS
文件,则需要将其添加到side effect
列表中,以免在生产模式中无意中将它删除:
{
"name": "your-project",
"sideEffects": [
"./src/some-side-effectful-file.js",
"*.css"
]
}
参考文档:https://webpack.docschina.org/guides/tree-shaking/#%E5%8E%8B%E7%BC%A9%E8%BE%93%E5%87%BA%E7%BB%93%E6%9E%9C
通过 import
和 export
语法,我们已经找出需要删除的“未引用代码(dead code)
”,然而,不仅仅是要找出,还要在 bundle
中删除它们。为此,我们需要将 mode
配置选项设置为 production
。
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
- mode: 'development',
- optimization: {
- usedExports: true
- }
+ mode: 'production'
};
注意,也可以在命令行接口中使用 --optimize-minimize 标记,来启用 TerserPlugin。
准备就绪后,然后运行另一个 npm script npm run build
,就会看到输出结果发生了改变。
在 dist/bundle.js
中,现在整个 bundle
都已经被 minify
(压缩) 和 mangle
(混淆破坏),但是如果仔细观察,则不会看到引入 square
函数,但能看到 cube
函数的混淆破坏版本(function r(e){return e*e*e}n.a=r
)。现在,随着 minification
(代码压缩) 和tree shaking
,我们的bundle
减小几个字节!虽然,在这个特定示例中,可能看起来没有减少很多,但是,在有着复杂依赖树的大型应用程序上运行 tree shaking
时,会对 bundle
产生显著的体积优化。
运行 tree shaking 需要 ModuleConcatenationPlugin。通过 mode: "production" 可以添加此插件。如果你没有使用 mode 设置,记得手动添加 ModuleConcatenationPlugin。
参考文档:https://webpack.docschina.org/guides/tree-shaking/#%E7%BB%93%E8%AE%BA
「结论:」我们已经知道,想要使用 tree shaking
必须注意以下几点:
ES2015
模块语法(即 import
和 export
)。compiler
将 ES2015
模块语法转换为 CommonJS
模块(这也是流行的 Babel preset
中 @babel/preset-env
的默认行为 - 更多详细信息请查看 文档)。package.json
文件中,添加一个"sideEffects"
属性。mode
选项设置为 production
,启用 minification
(代码压缩) 和tree shaking
。你可以将应用程序想象成一棵树。绿色表示实际用到的 source code(源码)
和 library(库)
,是树上活的树叶。灰色表示未引用代码,是秋天树上枯萎的树叶。为了除去死去的树叶,你必须摇动这棵树,使它们落下。
CDN
通过将资源部署到世界各地,使得用户可以就近访问资源,加快访问速度。要接入CDN
,需要把网页的静态资源上传到CDN
服务上,在访问这些资源时,使用CDN
服务提供的URL
。
由于CDN
会为资源开启长时间的缓存,例如用户从CDN
上获取了index.html
,即使之后替换了CDN
上的index.html
,用户那边仍会在使用之前的版本直到缓存时间过期。业界做法:
HTML
文件:放在自己的服务器上且关闭缓存,不接入CDN
JS、CSS、图片等资源
:开启CDN
和缓存,同时文件名带上由内容计算出的Hash
值,这样只要内容变化hash
就会变化,文件名就会变化,就会被重新下载而不论缓存时间多长。另外,HTTP1.x
版本的协议下,浏览器会对于向同一域名并行发起的请求数限制在4~8
个。那么把所有静态资源放在同一域名下的CDN
服务上就会遇到这种限制,所以可以把他们分散放在不同的CDN
服务上,例如JS
文件放在js.cdn.com
下,将CSS
文件放在css.cdn.com
下等。这样又会带来一个新的问题:增加了域名解析时间,这个可以通过dns-prefetch
来解决 <link rel='dns-prefetch' href='//js.cdn.com'>
来缩减域名解析的时间。形如**//xx.com 这样的URL省略了协议**
,这样做的好处是,浏览器在访问资源时会自动根据当前URL
采用的模式来决定使用HTTP
还是HTTPS
协议。
当浏览器从第三方服务跨域请求资源的时候,在浏览器发起请求之前,这个第三方的跨域域名需要被解析为一个IP
地址,这个过程就是DNS
解析,DNS
缓存可以用来减少这个过程的耗时,DNS
解析可能会增加请求的延迟,对于那些需要请求许多第三方的资源的网站而言,DNS
解析的耗时延迟可能会大大降低网页加载性能。
参考文章:https://developer.mozilla.org/zh-CN/docs/Web/Performance/dns-prefetch
URL
要变成指向CDN
服务的绝对路径的URL
Hash
值CDN
上const ExtractTextPlugin = require('extract-text-webpack-plugin');
const {WebPlugin} = require('web-webpack-plugin');
//...
output:{
filename: '[name]_[chunkhash:8].js',
path: path.resolve(__dirname, 'dist'),
publicPatch: '//js.cdn.com/id/', //指定存放JS文件的CDN地址
},
module:{
rules:[{
test: /\.css/,
use: ExtractTextPlugin.extract({
use: ['css-loader?minimize'],
publicPatch: '//img.cdn.com/id/', //指定css文件中导入的图片等资源存放的cdn地址
}),
},{
test: /\.png/,
use: ['file-loader?name=[name]_[hash:8].[ext]'], //为输出的PNG文件名加上Hash值
}]
},
plugins:[
new WebPlugin({
template: './template.html',
filename: 'index.html',
stylePublicPath: '//css.cdn.com/id/', //指定存放CSS文件的CDN地址
}),
new ExtractTextPlugin({
filename:`[name]_[contenthash:8].css`, //为输出的CSS文件加上Hash
})
]
大型网站通常由多个页面组成,每个页面都是一个独立的单页应用,多个页面间肯定会依赖同样的样式文件、技术栈等。如果不把这些公共文件提取出来,那么每个单页打包出来的chunk
中都会包含公共代码,相当于要传输n
份重复代码。如果把公共文件提取出一个文件,那么当用户访问了一个网页,加载了这个公共文件,再访问其他依赖公共文件的网页时,就直接使用文件在浏览器的缓存,这样公共文件就只用被传输一次。
把多个页面依赖的公共代码提取到common.js中,此时common.js包含基础库的代码
把多个页面依赖的公共代码提取到common.js中,此时common.js包含基础库的代码
找出依赖的基础库,写一个base.js
文件,再与common.js
提取公共代码到base
中,common.js
就剔除了基础库代码,而base.js
保持不变
//base.js
import 'react';
import 'react-dom';
import './base.css';
//webpack.config.json
entry:{
base: './base.js'
},
plugins:[
new CommonsChunkPlugin({
chunks:['base','common'],
name:'base',
//minChunks:2, 表示文件要被提取出来需要在指定的chunks中出现的最小次数,防止common.js中没有代码的情况
})
]
base.js
,不含基础库的公共代码common.js
,和页面各自的代码文件xx.js
。页面引用顺序如下:base.js
--> common.js
--> xx.js
单页应用的一个问题在于使用一个页面承载复杂的功能,要加载的文件体积很大,不进行优化的话会导致首屏加载时间过长,影响用户体验。做按需加载可以解决这个问题。具体方法如下:
Chunk
,按需加载对应的Chunk
Chunk
,这样首次加载少量的代码,其他代码要用到的时候再去加载。最好提前预估用户接下来的操作,提前加载对应代码,让用户感知不到网络加载一个最简单的例子:网页首次只加载main.js
,网页展示一个按钮,点击按钮时加载分割出去的show.js
,加载成功后执行show.js
里的函数
//main.js
document.getElementById('btn').addEventListener('click',function(){
import(/* webpackChunkName:"show" */ './show').then((show)=>{
show('Webpack');
})
})
//show.js
module.exports = function (content) {
window.alert('Hello ' + content);
}
「import(/* webpackChunkName:show */ './show').then()
是实现按需加载的关键」,Webpack
内置对import( *)
语句的支持,Webpack
会以./show.js
为入口重新生成一个Chunk
。代码在浏览器上运行时只有点击了按钮才会开始加载show.js
,且import
语句会返回一个Promise
,加载成功后可以在then
方法中获取加载的内容。这要求浏览器支持Promise API
,对于不支持的浏览器,需要注入Promise polyfill
。/* webpackChunkName:show */
是定义动态生成的Chunk
的名称,默认名称是[id].js
,定义名称方便调试代码。为了正确输出这个配置的ChunkName
,还需要配置Webpack
:
//...
output:{
filename:'[name].js',
chunkFilename:'[name].js', // 指定动态生成的Chunk在输出时的文件名称
}
Prepack
提前求值Prepack
是一个部分求值器,编译代码时提前将计算结果放到编译后的代码中,而不是在代码运行时才去求值。通过在便一阶段预先执行源码来得到执行结果,再直接将运行结果输出以提升性能。但是现在Prepack
还不够成熟,用于线上环境还为时过早。
参考文档:https://github.com/facebook/prepack
const PrepackWebpackPlugin = require('prepack-webpack-plugin').default;
module.exports = {
plugins:[
new PrepackWebpackPlugin()
]
}
Scope Hoisting
译作“作用域提升”,是在Webpack3
中推出的功能,它分析模块间的依赖关系,尽可能将被打散的模块合并到一个函数中,但不能造成代码冗余,所以只有被引用一次的模块才能被合并。由于需要分析模块间的依赖关系,所以源码必须是采用了ES6
模块化的,否则Webpack
会降级处理不采用Scope Hoisting
。
const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');
//...
plugins:[
new ModuleConcatenationPlugin();
],
resolve:{
mainFields:['jsnext:main','browser','main']
}
webpack --display-optimization-bailout
输出日志中会提示哪个文件导致了降级处理
启动Webpack
时带上这两个参数可以生成一个json
文件,输出分析工具大多依赖该文件进行分析:webpack --profile --json > stats.json
其中 --profile
记录构建过程中的耗时信息,--json
以JSON
的格式输出构建结果,>stats.json
是UNIX / Linux
系统中的管道命令,含义是将内容通过管道输出到stats.json
文件中。
Webpack Analyse
打开该工具的官网http://webpack.github.io/analyse/
上传stats.json
,就可以得到分析结果
webpack-bundle-analyzer
可视化分析工具,比Webapck Analyse
更直观。使用也很简单:
npm i -g webpack-bundle-analyzer
安装到全局 按照上面方法生成stats.json
文件 在项目根目录执行webpack-bundle-analyzer
,浏览器会自动打开结果分析页面。