开发中,webpack文件通常分为3个:css
webpack.base.conf.js
(基础文件)webpack.dev.conf.js
(开发环境使用的webpack
,须要与webpack.base.conf.js
结合使用)webpack.prod.conf.js
(上线环境使用的webpack
,须要与webpack.base.conf.js
结合使用)webpack
在启动后,会根据Entry
配置的入口,递归解析所依赖的文件。这个过程分为搜索文件和把匹配的文件进行分析、转化的两个过程,所以能够从这两个角度来进行优化配置。html
resolve
字段告诉webpack
怎么去搜索文件,因此首先要重视resolve
字段的配置:参考文档:webpack.docschina.org/configurati…node
resolve
用来配置模块如何解析。例如,当在 ES2015
中调用 import 'lodash'
,resolve
选项可以对webpack
查找'lodash'
的方式去作修改(查看模块)。react
// 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
参考文档:www.webpackjs.com/configurati…jquery
resolve.modules
告诉webpack
解析时应该搜索的目录。webpack
绝对路径和相对路径都能使用,可是要知道他们之间有一点差别。经过查看当前目录以及祖先路径(即 ./node_modules, ../node_modules
等等),相对路径将相似于 Node
查找 'node_modules
' 的方式进行查找。使用绝对路径,将只在给定目录中搜索。git
// webpack.config.js
module.exports = {
//...
resolve: {
modules: ['node_modules'] // 相对路径写法,会按./node_modules, ../node_modules的方式查找
}
};
复制代码
若是你想要添加一个目录到模块搜索目录,此目录优先于 node_modules/
搜索:github
// webpack.config.js
module.exports = {
//...
resolve: {
modules: [path.resolve(__dirname, 'src'), 'node_modules'] // 绝对路径写法
}
};
复制代码
所以:设置resolve.modules:[path.resolve(__dirname, 'node_modules')]
避免层层查找。 (2). resolve.mainFields
参考文档: www.webpackjs.com/configurati…web
当从 npm
包中导入模块时(例如,import * as D3 from "d3"
),此选项将决定在 package.json
中使用哪一个字段导入模块。根据 webpack
配置中指定的 target
不一样,默认值也会有所不一样。ajax
当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
参考文档:www.webpackjs.com/configurati…
建立 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
参考文档:www.webpackjs.com/configurati…
配置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
没必要解析哪些文件,能够用来排除对非模块化库文件的解析参考文档:webpack.docschina.org/configurati…
如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
参数
// 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
代码: 参考文档: webpack.js.org/plugins/ter…
optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
safari10: true
}
})
],
}
复制代码
取代了 UglifyJsPlugin
// 取代 new UglifyJsPlugin(/* ... */)
复制代码
CSS
2.1 mini-css-extract-plugin
:webpack.js.org/plugins/min… 。该插件将CSS
提取到单独的文件中。它为每一个包含CSS
的JS
文件建立一个CSS
文件。它支持CSS
和SourceMap
的按需加载。它基于新的webpack v4
功能(模块类型)构建,而且须要webpack 4
才能正常工做。
2.2 optimize-css-assets-webpack-plugin
: www.npmjs.com/package/opt… 。主要是用来压缩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
死代码参考文档:webpack.docschina.org/guides/tree…
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 模块)
",由此能够安全地删除文件中未使用的部分。
参考文档:webpack.docschina.org/guides/tree…
注意,全部导入文件都会受到tree shaking
的影响。这意味着,若是在项目中使用相似css-loader
并import
一个 CSS
文件,则须要将其添加到side effect
列表中,以避免在生产模式中无心中将它删除:
{
"name": "your-project",
"sideEffects": [
"./src/some-side-effectful-file.js",
"*.css"
]
}
复制代码
参考文档:webpack.docschina.org/guides/tree…
经过 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。
复制代码
参考文档:webpack.docschina.org/guides/tree…
结论: 咱们已经知道,想要使用 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
解析的耗时延迟可能会大大下降网页加载性能。
参考文章: developer.mozilla.org/zh-CN/docs/…
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
还不够成熟,用于线上环境还为时过早。
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
,浏览器会自动打开结果分析页面。