本文所用示例的仓库地址: gayhubjavascript
上一节咱们解决了工程的开发调试问题,项目的生产和开发环境也已配置完成,还约定了 Webpack 配置文件规范。但它还很粗糙,这一节咱们就来一块儿打磨这套配置。css
在以前的配置中咱们使用使用 MiniCssExtractPlugin.loader
来代替 style-loader
,由于咱们须要把 CSS 从 JS 中分离出来。但 MiniCssExtractPlugin 目前还存在一个隐患,那就是它可能会影响到 hmr (热模块替换)功能,在它对 hmr 的支持前,咱们只能在生产环境中使用它。html
webpack.base.conf.js
vue
module.exports = {
module: {
rules: [
{
test: /\.styl(us)?$/,
use: [
process.env.NODE_ENV !== 'production' ?
'vue-style-loader' : {
loader: resolve('node_modules/mini-css-extract-plugin/dist/loader.js'),
options: {
publicPath: '../'
}
},
{
loader: 'css-loader',
options: {
importLoaders: 2 // 在 css-loader 前执行的 loader 数量
}
},
'postcss-loader',
{
loader: 'stylus-loader',
options: {
preferPathResolver: 'webpack' // 优先使用 webpack 用于路径解析,找不到再使用 stylus-loader 的路径解析
}
}
]
}
]
}
}
复制代码
实际上我上次使用它时看见有 hmr 配置项,就觉得已经支持了,具体支持与否请看 MiniCssExtractPlugin Docsjava
当项目达到必定体量,打包速度、热加载性能优化的需求就会被提出来,毕竟谁也不肯意修改后花上十几秒甚至几分钟等待修改视图更新。接下里我会介绍一些通用的优化策略,但须要注意的是,项目自己不能去踩一些没法优化的坑,已知两坑:超多页( html-webpack-plugin 热更新时更新全部页面)和动态加载未指明明确路径(打包目录下全部页面)。node
DllPlugin 和 DllReferencePlugin 绝对是优化打包速度的最佳利器,它能够把部分公共依赖提早打包好,在以后的打包中就再也不打包这些依赖而是直接取用已经打包好的代码,一般状况能下降 20% ~ 40% 打包时间,固然它也有缺点:webpack
.html
文件中引入,滥用会致使首屏加载变慢但总归来讲是利大于弊。git
新增 webpack.dll.conf.js
程序员
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const { resolve } = require('./utils')
const libs = {
_frame: ['vue', 'vue-router', 'vuex'],
_utils: ['lodash']
}
module.exports = {
mode: 'production',
entry: { ...libs },
performance: false,
output: {
path: resolve('dll'),
filename: '[name].dll.js',
library: '[name]' // 与 DllPlugin.name 保持一致
},
plugins: [
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: []
}),
new webpack.DllPlugin({
name: '[name]',
path: resolve('dll', '[name].manifest.json'),
context: resolve('')
})
]
}
复制代码
在 webpack.common.conf.js
使用 DllReferencePlugines6
webpack.common.conf.js
const { generateDllReferences, generateAddAssests } = require('./utils')
module.exports = {
plugins: [
...generateAddAssests(),
...generateDllReferences()
]
}
复制代码
# add-asset-html-webpack-plugin 用于把 dll 添加到 `index.html` 的 script 标签中
# glob 支持正则匹配文件
yarn add add-asset-html-webpack-plugin glob -D
复制代码
utils.js
const webpack = require('webpack')
const glob = require('glob')
const AddAssestHtmlWebpackPlugin = require('add-asset-html-webpack-plugin')
const generateDllReferences = function() {
const manifests = glob.sync(`${resolve('dll')}/*.json`)
return manifests.map(file => {
return new webpack.DllReferencePlugin({
// context: resolve(''),
manifest: file
})
})
}
const generateAddAssests = function() {
const dlls = glob.sync(`${resolve('dll')}/*.js`)
return dlls.map(file => {
return new AddAssestHtmlWebpackPlugin({
filepath: file,
outputPath: '/dll',
publicPath: '/dll'
})
})
}
复制代码
添加 npm scripts
package.json
"scripts": {
"dll": "webpack --config build/webpack.dll.conf.js"
},
复制代码
而后就能够用 yarn dll
打包配置好的全局公共依赖了,打包后会在 src/dll
目录生成 *dll.js
和 *dll.json
,前者是依赖经压缩合并后的文件( mode: production
),后者是 *dll.js
文件和原始依赖的映射文件,用于被 DllReferencePlugin 解析创建引用和 *dll.js
之间的映射关系。
构建中最耗时的两步是 babel 和压缩, babel 通常会配置忽略 node_modules
因此 DllPlugin 节约的是部分公共依赖的压缩时间,因此你若是不想用 DllPlugin 也能够在 externals
中将他们配置为外部依赖,用其余方式去压缩并引入他们
在 webpack 4 中,是否生成 Source Map 以及生成怎样的 Source Map 是由 devtool
配置控制的,选择合理的 Source Map 能够有效的缩短打包时间。在选择前咱们仍是应该明白,不设置 Source Map 时打包是最快的,之因此须要 Source Map ,是由于打包后的代码结构、文件名和打包前彻底不一致,当存在报错时咱们只能直接定位到打包后的某个文件,没法定位到源文件,极大程度增长了调试难度。而 Source Map 就是为了加强打包后代码的可调试性而存在的,因此咱们在开发环境老是须要它,在生产环境则有更多选择。
devtool
可选配置有 none
、 eval
、 cheap-eval-source-map
等 13 种,各自功能和性能比较在 文档 中有详细介绍。
配置项由一个或多个单词和连字符组成,每一个单词都有其含义和性能损耗,每一个配置项最终意义就由这些单词决定:
none
不生成 Source Map ,性能 +++eavl
每一个模块由 eval
执行,不能正确显示行数,不能用生产模式,性能 +++module
报错显示原始代码,性能 -source
报错显示行列信息,显示 babel 转译后代码,性能 --cheap
低开销模式,不映射列,性能 +inline
不生成单独的 Source Map 文件,性能 o因为开发模式建议显示报错源码和行信息,因此 module
和 source
都是须要的,为了性能咱们又须要 eval
和 cheap
,因此参照配置项能找到最适合开发环境的配置是 devtool: cheap-module-eval-source-map
。
生产环境因为几乎不存在调试需求( JS 相关调试),因此建议你们设置 devtool: none
,在须要调试的时候再更改设置为 devtool: cheap-module-source-map
。
本小节中提到的优化其实几乎都是咱们以前配置中的某一个默认配置
以前有提到过,压缩是构建中耗时占比较大的一环,咱们能够启用 terser-webpack-plugin 的多线程压缩,减小压缩时间。
module.exports = {
optimization: {
minimizer: [
new TerserJSPlugin({
parallel: true // 开启多线程压缩
})
]
}
}
复制代码
module.exports = {
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
// 不转译 node_modules
// exclude: [resolve('node_modules')]
// 转译 src 目录下的文件
include: [
resolve('src')
],
options: {
cacheDirectory: true // 默认目录 node_modules/.cache/babel-loader
// cacheDirectory: resolve('/.cache/babel-loader')
}
}
]
}
}
复制代码
vue-loader 的 cacheDirectory
配置项依赖 cache-loader
yarn add cache-loader -D
复制代码
module.exports = {
module: {
rules: [
{
test: /\.vue$/,
use: {
loader: 'vue-loader',
options: {
prettify: false,
cacheDirectory: resolve('node_modules/.cache/vue-loader'),
cacheIdentifier: 'vue'
}
}
}
]
}
}
复制代码
resolve.modules
告知 Webpack 解析模块时应该搜索的目录,能够设置相对路径和绝对路径。设置相对路径时,好比 resolve.modules: [node_modules]
,在解析依赖时会从当前目录向上查找,直到找到 node_modules
目录。设置绝对路径时能减小了这个遍历过程,直接定位目录。
module.exports = {
resolve: {
modules: [resolve('node_modules')]
}
}
复制代码
我以前也说了,这个优化项聊胜于无。
ES6 不只在原有对象上添加了一些经常使用方法,还新增了一些新的词法和语法给开发者带来了极大便利,它天然是不能缺席本项目的。但不一样浏览器、不一样版本对 ES6 的支持不一致,致使使用 ES6 是还存在些许阻碍,咱们须要用 babel-loader 把 ES6 的词法和语法转换为 ES5。
前段时间刚介绍过 babel-loader / babel 7 ,这里就再也不重复介绍,详情见 babel-loader 使用指南
多人合做项目约定编码规范是很是重要的,由于它能有效提升协做效率并抑制程序员怒气值增加。固然我认为我的项目也是须要的,由于 6 个月前的代码和别人的代码同样。
EditorConfig 是一个跨编辑器的代码规范解决方案,得到了众多编辑器的支持(编辑器或插件实现支持),这意味着不一样编辑器能够格式化出一样风格的代码,好比 vscode 和 sublime 。配置方式是在项目根目录增长一个 .editorconfig
文件,部分编辑器能够经过命令一键生成,一般其配置以下:
.editorconfig
root = true
[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false
复制代码
root
是否为顶级配置文件,一般设置为 true
表示搜索到该配置文件后再也不继续向上查找配置indent_style
Tab 缩进方式indent_size
缩进大小,上面示例就表示:缩进表现为两个空格charset
编码方式trim_trailing_whitespace
是否删除行尾空格insert_final_newline
文件是否以空行结束Javascript 是一门动态语言(弱类型语言),灵活但易错,因此在协做开发中须要制定一些规则保证各个成员输出风格一致的代码。 eslint 正是用于应对这个问题的开源工具,你能够设定规则,它则基于规则检查 Javascript 是否合法,不合法则返回错误或警告。
安装
yarn add eslint -D
复制代码
初始化配置文件
npx eslint --init
复制代码
根据提示选择须要的项并安装对应的 plugin 和 config ,我这里还须要安装 eslint-config-standard
和 eslint-plugin-vue
yarn add eslint-config-standard eslint-plugin-vue -D
复制代码
.eslintrc.js
module.exports = {
"env": {
"browser": true,
"es6": true
},
"extends": [
"plugin:vue/essential",
"standard"
],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"plugins": [
"vue"
],
"rules": {
}
}
复制代码
在 rules 配置相应规则
见中文文档
有时候咱们会发现有些生成物的大小不对劲,但在控制台又很难看出来缘由,这个时候就须要模块分析工具的帮助。这里我推荐使用 webpack-bundle-analyzer ,它会启动一个服务,在浏览器中很清楚地展示生成物和源文件的映射关系和层级,如图所示(图来源于 Github ):
yarn add webpack-bundle-analyzer -D
复制代码
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
复制代码
在构建层面优化用户体验在于如下几个方面: 缩小生成代码体积、合理的加载方式、合理减小 HTTP 请求数,本小节主要讲前二者。
减小 HTTP 的优化,咱们在使用 url-loader 处理图片时就说起到了
Webpack 4 在 production
模式下是会默认压缩 JS 代码的,使用 TerserWebpackPlugin,但 CSS 不会( Webpack 5 会做为内置功能 ),因此咱们须要 OptimizeCSSAssetsPlugin 的帮助。
yarn add optimize-css-assets-webpack-plugin -D
复制代码
Webpack 插件使用大多大同小异,但在 Webpack 4 中使用这个插件须要特别注意,使用它时会重写 optimization.minimizer
选项,而压缩 JS 的插件 TerserWebpackPlugin 刚好就在这个选项的默认值中,重写会致使默认值失效,因此你还须要显式地声明 TerserWebpackPlugin 实例。
webpack.prod.conf.js
const TerserJSPlugin = require("terser-webpack-plugin")
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin")
module.exports = {
optimization: {
minimizer: [
new TerserJSPlugin({
parallel: true // 开启多线程压缩
}),
new OptimizeCSSAssetsPlugin({})
]
}
}
复制代码
压缩是生产环境下的优化,开发环境去设置它反而会影响到热加载性能、拔苗助长
代码分离有两个优势:
剥离公共代码和依赖,避免重复打包
避免单个文件体积过大
总加载体积一致,浏览器加载多个文件一般快于单个文件
咱们先在项目中加上会被 home 和 page-a 公共引用的资源: src/utils/index.js
& src/styles/main.styl
,而后再在两个页面分别引用他们,以 page-a.vue
举例。
为了方便检查生成代码,咱们设置 mode: development
以得到未被压缩的代码
page-a.vue
<template>
<div class="page-a">
<h1>
This is page-a
</h1>
</div>
</template>
<script>
import { counter } from '@/utils'
export default {
name: 'page-a',
created() {
counter()
console.log('page-a:', counter.count)
}
}
</script>
<style lang="stylus" scoped>
@import '~@/styles/main.styl';
.page-a {
background: blue;
}
</style>
复制代码
执行 yarn build
打包项目,而后咱们就会在 home.vue
对应的生成物( dist/css/views/home.[contentHash].css
和 dist/views/home.[contentHash].js.
)中看到,他们包含了 src/styles/main.styl
和 src/utils/index.js
文件中的所需内容。然而,咱们再去检查 page-a.vue
对应的生成物,发现他们一样包含了这些内容,因此一份源码被打包到了两个页面对应的生成物中。
被重复打包是由于这两个页面同时引用了他们,当引用次数是 3 次、 10 次或者更多,这些公共资源(包括公共依赖)甚至能够占到生成物体积的 95% 以上,这显然是不可接受的。
为了解决公共资源被重复打包问题,咱们就须要 SplitChunksPlugin 的帮助,它能够把代码分离成不一样的 bundle ,在页面须要时被加载。另外 SplitChunksPlugin 是 webpack 4 的内置插件,因此咱们不须要去独立安装它。
webpack.prod.conf.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
minSize: 1, // 正常设置 20000+ 即 20k+ ,但这里咱们的公共文件只有几行代码,因此设置为 1
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '/',
name(mod, chunks) {
return ${chunks[0].name}
},
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}
}
复制代码
执行 yarn build
打包项目,咱们能够看到只有 dist/views/home.[contentHash].js.
(或者一个单独的 JS 中) 才会包含 utils.js
内容,而 dist/views/page-a.[contentHash].js.
中只有引用: _utils__WEBPACK_IMPORTED_MODULE_0__["counter"]
。
咱们可使用 VSCode 来调试打包配置代码( nodejs ),获得 name
函数中的 mod / chunks
的对象结构,根据信息返回咱们须要的文件名。
固然你也能够用 -inspect
来调试代码。
// 命名和代码分离息息相关,这里仅为使用示例,具体命名请根据项目状况更改
name(mod, chunks) {
if (chunks[0].name === 'app') return 'app.vendor'
if (/src/.test(mod.request)) {
let requestName = mod.request.replace(/.*\\src\\/, '').replace(/"/g, '')
if (requestName) return requestName
} else if (/node_modules/.test(mod.request)) {
return 'dependencies/' + mod.request.match(/node_modules.[\w-]+/)[0].replace(/node_modules./, '')
}
return null
}
复制代码
更多的状况是设置魔法注释来规定文件名,而不是经过 name 函数设置,由于后者每每会将一些不应分离的代码分离
上面咱们分离代码,解决了项目中部分代码被重复打包到多个生成物中的问题,有效地缩小了生成物体积,但其实咱们还能够在此基础上进一步缩小体积,这就涉及本小节的概念 tree shaking 。
tree shaking 是一个术语,一般用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块语法的 静态结构 特性,例如 import 和 export。
你能够将应用程序想象成一棵树。绿色表示实际用到的 source code(源码) 和 library(库),是树上活的树叶。灰色表示未引用代码,是秋天树上枯萎的树叶。为了除去死去的树叶,你必须摇动这棵树,使它们落下。
咱们回头看在使用 SplitChunksPlugin 时生成的文件,能够发现 say
函数没有使用可是却被打包进来了,它其实是无用的代码,也就是文档中说的 dead-code 。要删除这些代码,只须要把 mode
修改成 production
(让 tree shaking 生效),再次打包~
不过须要注意的是, tree shaking 能移除无用代码的同时,也有必定的反作用(错误识别无用代码)。好比你可能会遇到 UI 组件库没有样式的问题,这个问题缘由在于 tree shaking 不只对 JS 生效,也对 CSS 生效 。咱们一般在导入 CSS 时使用 import 'xxx.min.css'
, ES6 的静态导入 + 生产环境知足了 tree shaking 的生效条件,而且 Webpack 没法判断 CSS 有效,因此它被当作了 dead-code 而后被删除。为了解决这个问题,你能够在 package.json
中添加一个 sideEffects
选项,告知 Webpack 那些文件是能够直接引入而不用 tree shaking 检查的,使用以下:
package.json
{
"sideEffects": [
"*.css",
"*.styl(us)?"
]
}
复制代码
合理的资源加载方式有时比缩小代码体积更重要
按需加载又名懒加载,是指当须要依赖的页面被打开采起加载这个依赖,这样就减小了主页的负担,提高首屏渲染速度。而要作到按需加载,你只需在导入依赖的时候用 import()
或 require.ensure
这两种动态加载方式。咱们添加 lodash
依赖来作测试: yarn add lodash
page-a.vue
// 静态加载
import _ from 'lodash'
// 懒加载
// import(/* webpackChunkName: "dependencies/lodash" */ 'lodash')
export default {
name: 'page-a',
created() {
console.log(_.now())
}
}
复制代码
此时启动一下开发服务,咱们能够看到,虽然这里用了静态加载,但其实 lodash 依赖仍是在点击进入了 page-a 才会被加载(懒加载)。由于咱们在使用设置路由的时候,就已经使用过了 import()
动态加载(这一点我忘记了),因此 page-a 页面的静态资源也一块儿变做了懒加载。
咱们再看一下以前的动态加载语句 import(/* webpackChunkName: "views/home" */ '@/views/home/main.vue')
,这其中有一个值得注意的知识点 /* webpackChunkName: "views/home" */
,它是 Webpack 的魔法注释,这里是经过魔法注释指定生成 chunk 的文件名,因此该 src/views/home/main.vue
文件打包后的 JS 就在 dist/views/home.[contentHash].js
。
上面讲到使用魔法注释为生成物命名,其实预加载 preload 和预取 prefetch 也是经过魔法注释来设置的。这里是官方文档上有他们的异同介绍:
- preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
- preload chunk 具备中等优先级,并当即下载。prefetch chunk 在浏览器闲置时下载。
但在个人测试中,不管是 preload 仍是 prefetch 都是并行加载的,但他们优先级会比当前页面所需依赖更低,不会影响到页面加载。你能够在 main.js
中添加如下代码进行测试:
src/main.js
// 对比测试
// import 'lodash'
// 预加载
// import(/* webpackPreload: true, webpackChunkName: "dependencies/lodash" */ 'lodash')
// 预取
import(/* webpackPrefetch: true, webpackChunkName: "dependencies/lodash" */ 'lodash')
复制代码
本小节的测试结果因为和文档不符,但愿你们自行验证,不可偏信
用预加载和预取处理体积较大的依赖效果尤其明显,好比图表、富文本编辑器
有时咱们会在项目中直接引入一些体积不小 JS 库(本文以 lodash 举例), Webpack 会去解析并压缩它们。但仔细想一想,lodash 自己就存在已经压缩好的版本 lodash/lodash.min.js
,再加上其体积也不小不必再与其余 JS 合并( 未压缩 700k ,压缩后 70k ),咱们去解析和压缩它的意义并不大。因此咱们能够把它放到 static
文件夹(或 CDN),并在 index.html
中用 script
标签引入,若是项目中有引入 lodash ( import 'lodash'
)则能够配置 externals
在打包时忽略它,没有则不用。
若是这里想用 link[ref=prefetch/preload]
进行预加载,那么必定不要忘了在合适的地方再用 script
标签引入,预加载只是为了缓存
新增 /static
文件夹,加入 lodash.min.js
代码改动
yarn add copy-webpack-plugin -D
复制代码
/build/webpack.prod.conf.js
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
externals: {
lodash: {
commonjs: 'lodash',
umd: 'lodash',
root: '_' // 默认执行环境已经存在全局变量: _ ,浏览器中就是 window._
}
},
plugins: [
new CopyWebpackPlugin([
{
from: 'static/',
to: 'static/'
}
])
]
}
复制代码
/src/main.js
import _ from 'lodash'
console.log(_.now())
复制代码
/index.html
<body>
<script type="text/javascript" src="static/lodash.min.js"></script>
</body>
复制代码
有些朋友可能认为解析 lodash 可让 Webpack 知道哪些函数是没用到的,而后 tree shaking 掉它们,但其实 lodash 并非 ES6 模块语法的静态导出,因此 tree shaking 不会生效。若是项目并非重度依赖 lodash ,只是使用了其中几个函数,建议导入单个函数,以下:
import now from 'lodash/now'
console.log(now())
复制代码
module.noParse
在文档中被介绍也能够忽略打包某些模块,但遗憾的是当前它还无甚用处。由于要让它生效你就不能在代码中去引用相关依赖,实际上没有引用就不会记录到依赖图中,天然就不会被打包(因此这个配置项什么都没作)。