production模式打包自带优化
tree shaking
tree shaking 是一个术语,通常用于打包时移除 JavaScript 中的未引用的代码(dead-code),它依赖于 ES6 模块系统中 import和 export的静态结构特性。、
以为import是必须要在顶级作用域值使用的,而require是可以放在别的作用域值进行使用,这是一种动态的引入,那么tree-shaking就无法在初始化的时候去判断,因为他不知道你是否在后面的代码中用到这个
开发时引入一个模块后,如果只使用其中一个功能,上线打包时只会把用到的功能打包进bundle,其他没用到的功能都不会打包进来,可以实现最基础的优化
scope hoisting
// 这个内部是由一个插件起作用的,在开发环境下自动关闭,可以手动配置开启。基本不会
scope hoisting的作用是将模块之间的关系进行结果推测, 可以让 Webpack 打包出来的代码文件更小、运行的更快
scope hoisting 的实现原理其实很简单:分析出模块之间的依赖关系,尽可能的把打散的模块合并到一个函数中去,但前提是不能造成代码冗余。因此只有那些被引用了一次的模块才能被合并。
由于 scope hoisting 需要分析出模块之间的依赖关系,因此源码必须采用 ES6 模块化语句,不然它将无法生效。原因和tree shaking一样。
代码压缩
css优化
将css提取到独立的文件中
mini-css-extract-plugin是用于将CSS提取为独立的文件的插件,对每个包含css的js文件都会创建一个CSS文件,支持按需加载css和sourceMap
只能用在webpack4中,有如下优势:
- 异步加载
- 不重复编译,性能很好
- 容易使用
- 只针对CSS
- 使用方法:
- 安装
npm i -D mini-css-extract-plugin 在webpack配置文件中引入插件
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
3.创建插件对象,配置抽离的css文件名,支持placeholder语法
new MiniCssExtractPlugin({filename: '[name].css' // 这里子所以使用这个语法,这个语法会读取入口文件的名字})
4.将原来配置的所有
style-loader替换为MiniCssExtractPlugin.loader{test: /\.css$/,// webpack读取loader时 是从右到左的读取, 会将css文件先交给最右侧的loader来处理// loader的执行顺序是从右到左以管道的方式链式调用// css-loader: 解析css文件// style-loader: 将解析出来的结果 放到html中, 使其生效// use: ['style-loader', 'css-loader']use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']},// { test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader'] },{ test: /\.less$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader'] },// { test: /\.s(a|c)ss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },{ test: /\.s(a|c)ss$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'] },
自动添加css前缀 (为了兼容性)
使用postcss,需要用到postcss-loader和autoprefixer插件
安装
npm i -D postcss-loader autoprefixer- 修改webpack配置文件中的loader,将postcss-loader放置在css-loader的右边(调用链从右到左)
项目根目录下添加{test: /\.css$/,// webpack读取loader时 是从右到左的读取, 会将css文件先交给最右侧的loader来处理// loader的执行顺序是从右到左以管道的方式链式调用// css-loader: 解析css文件// style-loader: 将解析出来的结果 放到html中, 使其生效// use: ['style-loader', 'css-loader']use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']},// { test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader'] },{ test: /\.less$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'less-loader'] },// { test: /\.s(a|c)ss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },{ test: /\.s(a|c)ss$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'sass-loader'] },
postcss的配置文件:postcss.config.js
在postcss的配置文件中使用插件
module.exports = {plugins: [require('autoprefixer')]}
开启css压缩
webpack5开始好像内部已经继承了css压缩
需要使用optimize-css-assets-webpack-plugin插件来完成css压缩
但是由于配置css压缩时会覆盖掉webpack默认的优化配置,导致JS代码无法压缩,所以还需要手动把JS代码压缩插件导入进来:terser-webpack-plugin
- 安装
npm i -D optimize-css-assets-webpack-plugin terser-webpack-plugin - 导入插件
在webpack配置文件中添加配置节点const TerserJSPlugin = require('terser-webpack-plugin')const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
tips: webpack4默认采用的JS压缩插件为:uglifyjs-webpack-plugin,在mini-css-extract-plugin上一个版本中还推荐使用该插件,但0.6中建议使用teser-webpack-plugin来完成js代码压缩,具体原因未在官网说明,我们就按照最新版的官方文档来做即可optimization: {minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],},
js代码分离
Code Splitting是webpack打包时用到的重要的优化特性之一,此特性能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。
有三种常用的代码分离方法:
- 入口起点(entry points):使用
entry配置手动地分离代码。 - 防止重复(prevent duplication):使用
SplitChunksPlugin去重和分离 chunk。 - 动态导入(dynamic imports):通过模块的内联函数调用来分离代码。
手动配置多入口
在webpack配置文件中配置多个入口
在main.js和other.js中都引入同一个模块,并使用其功能entry: {main: './src/main.js',other: './src/other.js'},output: {// path.resolve() : 解析当前相对路径的绝对路径// path: path.resolve('./dist/'),// path: path.resolve(__dirname, './dist/'),path: path.join(__dirname, '..', './dist/'),// filename: 'bundle.js',filename: '[name].bundle.js',publicPath: '/'},
main.js ```javascript import $ from ‘jquery’
$(function() { $(‘
‘).html(‘main’).appendTo(‘body’) })
other.js```javascriptimport $ from 'jquery'$(function() {$('<div></div>').html('other').appendTo('body')})
运行npm run dev-build,进行打包
查看打包后的结果,发现other.bundle.js和这种方法存在一些问题:
- 如果入口 chunks 之间包含重复的模块,那些重复模块都会被引入到各个 bundle 中。
- 这种方法不够灵活,并且不能将核心应用程序逻辑进行动态拆分代码。main.bundle.js都同时打包了jQuery源文件抽取公共代码
tips: Webpack v4以上使用的插件为SplitChunksPlugin,以前使用的CommonsChunkPlugin已经被移除了,最新版的webpack只需要在配置文件中的optimization节点下添加一个splitChunks属性即可进行相关配置
修改webpack配置文件
运行npm run dev-build重新打包optimization: { splitChunks: { chunks: 'all' } },
查看vendors~main~other.bundle.js,其实就是把都用到的jQuery打包到了一个单独的js中动态导入 (懒加载)
webpack4默认是允许import语法动态导入的,但是需要babel的插件支持,最新版babel的插件包为:@babel/plugin-syntax-dynamic-import,以前老版本不是@babel开头,已经无法使用,需要注意
动态导入最大的好处是实现了懒加载,用到哪个模块才会加载哪个模块,可以提高SPA应用程序的首屏加载速度,Vue、React、Angular框架的路由懒加载原理一样
- 安装babel插件
npm install -D @babel/plugin-syntax-dynamic-import - 修改.babelrc配置文件,添加@babel/plugin-syntax-dynamic-import插件
将jQuery模块进行动态导入{ "presets": ["@babel/env"], "plugins": [ "@babel/plugin-proposal-class-properties", "@babel/plugin-transform-runtime", "@babel/plugin-syntax-dynamic-import" ] }
给某个按钮添加点击事件,点击后调用getComponent函数创建元素并添加到页面function getComponent() { return import('jquery').then(({ default: $ }) => { return $('<div></div>').html('main') }) }// 如果浏览器实在版本太低,还需要导入promise,请参照插件的官方设置
注意这样修改完后,在开发和生产环境中有店不同。我们更关注生产环境,他会单独把jq放在一个js文件中,之后事件触发的时候才会动态的导入,这样可以大大减少初始化消耗的资源window.onload = function () { document.getElementById('btn').onclick = function () { getComponent().then(item => { item.appendTo('body') }) } }
SplitChunksPlugin配置参数
webpack4之后,使用SplitChunksPlugin插件替代了以前CommonsChunkPlugin
而SplitChunksPlugin的配置,只需要在webpack配置文件中的optimization节点下的splitChunks进行修改即可,如果没有任何修改,则会使用默认配置
默认的SplitChunksPlugin 配置适用于绝大多数用户
webpack 会基于如下默认原则自动分割代码:
- 公用代码块或来自 node_modules 文件夹的组件模块。
- 打包的代码块大小超过 30k(最小化压缩之前)。
- 按需加载代码块时,同时发送的请求最大数量不应该超过 5。
- 页面初始化时,同时发送的请求最大数量不应该超过 3。
- 以下是
SplitChunksPlugin的默认配置:module.exports = { //... optimization: { splitChunks: { chunks: 'async', // 只对异步加载的模块进行拆分,可选值还有all | initial minSize: 30000, // 模块最少大于30KB才拆分 maxSize: 0, // 模块大小无上限,只要大于30KB都拆分 。如果超出限制会被拆分成多个小模块,更碎一点而已 minChunks: 1, // 模块最少引用一次才会被拆分 maxAsyncRequests: 5, // 异步加载时同时发送的请求数量最大不能超过5,超过5的部分不拆分 maxInitialRequests: 3, // 页面初始化时同时发送的请求数量最大不能超过3,超过3的部分不拆分 automaticNameDelimiter: '~', // 默认的连接符 name: true, // 拆分的chunk名,设为true表示根据模块名和CacheGroup的key来自动生成,使用上面连接符连接 cacheGroups: { // 缓存组配置,上面配置读取完成后进行拆分,如果需要把多个模块拆分到一个文件,就需要缓存,所以命名为缓存组 vendors: { // 自定义缓存组名 test: /[\\/]node_modules[\\/]/, // 检查node_modules目录,只要模块在该目录下就使用上面配置拆分到这个组 priority: -10 // 权重-10,决定了哪个组优先匹配,例如node_modules下有个模块要拆分,同时满足vendors和default组,此时就会分到vendors组,因为-10 > -20 }, default: { // 默认缓存组名 minChunks: 2, // 最少引用两次才会被拆分 priority: -20, // 权重-20 reuseExistingChunk: true // 如果主入口中引入了两个模块,其中一个正好也引用了后一个,就会直接复用,无需引用两次 } } } } };IgnorePlugin
在引入一些第三方模块时,例如moment,内部会做i18n国际化处理,所以会包含很多语言包,而语言包打包时会比较占用空间,如果我们项目只需要用到中文,或者少数语言,可以忽略掉所有的语言包,然后按需引入语言包
从而使得构建效率更高,打包生成的文件更小
需要忽略第三方模块内部依赖的其他模块,只需要三步:
- 首先要找到moment依赖的语言包是什么
- 使用IgnorePlugin插件忽略其依赖
- 需要使用某些依赖时自行手动引入
使用IgnorePlugin插件来忽略掉moment模块的locale目录
在webpack配置文件中安装插件,并传入配置项
参数1:表示要忽略的资源路径
参数2:要忽略的资源上下文(所在哪个目录)
两个参数都是正则对象
new webpack.IgnorePlugin(/\.\/locale/, /moment/)
使用moment时需要手动引入语言包,否则默认使用英文
import moment from 'moment'
import 'moment/locale/zh-cn' // 因为上面忽略了文件,这里需要手动去引入文件
moment.locale('zh-CN')
console.log(moment().subtract(6, 'days').calendar())
DllPlugin
在引入一些第三方模块时,例如vue、react、angular等框架,这些框架的文件一般都是不会修改的,而每次打包都需要去解析它们,也会影响打包速度,哪怕做拆分,也只是提高了上线后用户访问速度,并不会提高构建速度,所以如果需要提高构建速度,应该使用动态链接库的方式,类似于Windows中的dll文件。
借助DllPlugin插件实现将这些框架作为一个个的动态链接库,只构建一次,以后每次构建都只生成自己的业务代码,可以大大提高构建效率!
主要思想在于,将一些不做修改的依赖文件,提前打包,这样我们开发代码发布的时候就不需要再对这部分代码进行打包,从而节省了打包时间。
涉及两个插件:
- DllPlugin
使用一个单独webpack配置创建一个dll文件。并且它还创建一个manifest.json。DllReferencePlugin使用该json文件来做映射依赖性。(这个文件会告诉我们的哪些文件已经提取打包好了)
配置参数:- context (可选): manifest文件中请求的上下文,默认为该webpack文件上下文。
- name: 公开的dll函数的名称,和output.library保持一致即可。
- path: manifest.json生成的文件夹及名字
- DllReferencePlugin
这个插件用于主webpack配置,它引用的dll需要预先构建的依赖关系。- context: manifest文件中请求的上下文。
- manifest: DllPlugin插件生成的manifest.json
- content(可选): 请求的映射模块id(默认为manifest.content)
- name(可选): dll暴露的名称
- scope(可选): 前缀用于访问dll的内容
- sourceType(可选): dll是如何暴露(libraryTarget)
将Vue项目中的库抽取成Dll
- 准备一份将Vue打包成DLL的webpack配置文件
在build目录下新建一个文件:webpack.vue.js
配置入口:将多个要做成dll的库全放进来
配置出口:一定要设置library属性,将打包好的结果暴露在全局
配置plugin:设置打包后dll文件名和manifest文件所在地
使用这个的时候需要注意,不可以使用claenHtml(就是那个清楚dist文件,再重新构建的那个插件)
const path = require('path')
const webpack = require('webpack')
module.exports = {
mode: 'development',
entry: {
vue: [
'vue/dist/vue.js',
'vue-router'
]
},
output: {
filename: '[name]_dll.js',
path: path.resolve(__dirname, '../dist'),
library: '[name]_dll'
},
plugins: [
new webpack.DllPlugin({
name: '[name]_dll',
path: path.resolve(__dirname, '../dist/manifest.json')
})
]
}
在webpack.base.js中进行插件的配置
使用DLLReferencePlugin指定manifest文件的位置即可
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '../dist/manifest.json')
})
安装add-asset-html-webpack-pluginnpm i add-asset-html-webpack-plugin -D
浏览器缓存
在做了众多代码分离的优化后,其目的是为了利用浏览器缓存,达到提高访问速度的效果,所以构建项目时做代码分割是必须的,例如将固定的第三方模块抽离,下次修改了业务代码,重新发布上线不重启服务器,用户再次访问服务器就不需要再次加载第三方模块了
但此时会遇到一个新的问题,如果再次打包上线不重启服务器,客户端会把以前的业务代码和第三方模块同时缓存,再次访问时依旧会访问缓存中的业务代码,所以会导致业务代码也无法更新
需要在output节点的filename中使用placeholder语法,根据代码内容生成文件名的hash:
output: {
// path.resolve() : 解析当前相对路径的绝对路径
// path: path.resolve('./dist/'),
// path: path.resolve(__dirname, './dist/'),
path: path.join(__dirname, '..', './dist/'),
// filename: 'bundle.js',
filename: '[name].[contenthash:8].bundle.js',
publicPath: '/'
},
之后每次打包业务代码时,如果有改变,会生成新的hash作为文件名,浏览器就不会使用缓存了,而第三方模块不会重新打包生成新的名字,则会继续使用缓存
