1. Tree Shaking 概念详解
1.1 babel/polyfill与babel/preset-env之间的引用小冲突
在webpack 4.x
的版本中,如果我们在业务代码里面引用了impport @babel/polyfill
,而且我们还对babel-loader
中的这个插件@babel/preset-env
配置了useBuiltIns: 'usage'
,我们在打包的时候,会提示下面的信息:
提示的意思是,如果我们在babel-loader
中的这个插件@babel/preset-env
配置了useBuiltIns: 'usage'
,可以不再业务代码里面,再次引入impport @babel/polyfill
,@babel/preset-env
插件会自动进行查看代码中es6
语法进行添加对应的实现函数。
1.2 Tree Shaking 概念
我们在打包文件的时候,比如下面代码,有一个math.js
文件,然后在index.js
进行引入,代码如下:
math.js
文件
export const add = (a, b) =>{
console.log(a + b)
}
export const minus = (a, b) =>{
console.log(a - b)
}
·index.js
里面内容
import { add } from './math.js';
add(1, 2);
这样,我们只是引入了add
方法,但是我们查看打包的内容,会发现,他会将math.js
文件所有的文件,都会打包到打包输出文件中,我们并不想让没有引入的方法等打包输出:
/*! exports provided: add, minus */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"add\", function() { return add; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"minus\", function() { return minus; });\nconst add = (a, b) => {\n console.log(a + b);\n};\nconst minus = (a, b) => {\n console.log(a - b);\n};//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiLi9zcmMvbWF0aC5qcy5qcyIsInNvdXJjZXMiOlsid2VicGFjazovLy8uL3NyYy9tYXRoLmpzPzVhMDMiXSwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGNvbnN0IGFkZCA9IChhLCBiKSA9PntcclxuICBjb25zb2xlLmxvZyhhICsgYilcclxufVxyXG5leHBvcnQgY29uc3QgbWludXMgPSAoYSwgYikgPT57XHJcbiAgY29uc29sZS5sb2coYSAtIGIpXHJcbn0iXSwibWFwcGluZ3MiOiJBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///./src/math.js\n");
这个时候,我们需要使用Tree Shaking
功能来实现,不过需要注意的是Tree Shaking
只支持ES Module
,也就是只支持import
这种方式的引入,不支持require
这种CommonJS
的引入方式,是因为ES Module
这种底层是静态引入的方式,而import
这种方式底层是动态的引入方式,Tree Shaking
只支持静态引入的这种方法。我们在webpack.config.js
中进行下面的配置:这里需要注意的是,我们下面的配置在开发模式下的配置mode
为development
;
....
module.exports = {
// 配置打包模式
mode: 'development',
....
optimization: {
usedExports: true
},
....
}
usedExprots: true
意思就是我们去查看哪些导出的模块被使用,然后再进行打包;然后我们在package.json
中进行下面的配置:添加"sideEffects": false,
意思就是,对所有的模块都进行Tree Shaking
也就是将没有引入的方法等不进行打包到打包输出文件中。
{
"name": "webpack",
"version": "1.0.0",
"sideEffects": false,
"description": "",
"private": true,
"scripts": {
"bundle": "webpack",
"watch": "webpack --watch",
"start": "webpack-dev-server",
"server": "node server.js"
},
这里的
"sideEffects
有很大的用途,比如我们在使用@babel/polyfill
的时候,他的内部并没有使用export
导出任何模块,他只是通过类似windows.Promise
这样给全局T添加一些函数,但是我们使用Tree Shaking
这种去打包的时候,他会发现这个模块我们并没有通过import
引入任何模块,他会以为,我们并没有使用这个模块,不会对他进行打包,这时候,我们需要这样配置:添加"sideEffects": ["@babel/polyfill"]
这样,我们在打包的时候不会对这个模块进行Tree Shaking
检查。
一般我们在配置sideEffects
选项的时候会配置成下面的:意思就是除了我们通过这种import "./strle.css"
也不进行Tree Shaking
检查,其他的对进行Tree Shaking
检查,因为如果进行检查,会忽略我们的样式。
"sideEffects": [
"*.css"
]
然后我们对上面的inedx.js
进行重新打包,查看打包内容:exports used: add
意思只有add
方法被使用了。Tree Shaking
并没有生效,因为开发环境下Tree Shaking
会保留我们没用引入的代码,因为我们在查看报错的时候,如果去除了没有引入的代码,显示的行数会跟源代码不一致。如果我们的mode
为production
的时候,Tree Shaking
就会生效了,其实在mode
为production
的时候,optimization: { usedExports: true}
已经是配置好的,我们没必要再次进行配置,但是package.json
中的sideEffects
配置,还是需要的;同时将devtool
改成:devtool: 'cheap-module-source-map',
/*! exports provided: add, minus */
/*! exports used: add */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"a\", function() { return add; });\n/* unused harmony export minus */\nconst add = (a, b) => {\n console.log(a + b);\n};\nconst minus = (a, b) => {\n console.log(a - b);\n};//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiLi9zcmMvbWF0aC5qcy5qcyIsInNvdXJjZXMiOlsid2VicGFjazovLy8uL3NyYy9tYXRoLmpzPzVhMDMiXSwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGNvbnN0IGFkZCA9IChhLCBiKSA9PntcclxuICBjb25zb2xlLmxvZyhhICsgYilcclxufVxyXG5leHBvcnQgY29uc3QgbWludXMgPSAoYSwgYikgPT57XHJcbiAgY29uc29sZS5sb2coYSAtIGIpXHJcbn0iXSwibWFwcGluZ3MiOiJBQUFBO0FBQUE7QUFBQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///./src/math.js\n");
2. Develoment 和 Production 模式的区分打包
我们一般写项目的时候,会对webpack
进行两个配置,一个是生成版本的配置,一个是开发版本的配置;这里我的Develoment
模式的配置文件在webpack.dev.js
,我的Production
模式的配置文件在webapck.prod.js
中,然后我们在package.json
进行配置我们允许的命令:
"scripts": {
"dev": "webpack-dev-server --config webpack.dev.js",
"build": "webpack --config webpack.prod.js",
},
这样,方面我们进行开发;webpack.dev.js
配置如下:主要不同的是配置了打包的模式,开发环境的devtool
配置为cheap-module-eval-source-map
,生产环境的配置为:cheap-module-source-map
,;我们在开发环境中使用下面的配置,每次修改js文件,需要进行手动刷新一次页面,我们去掉hotOnly: true
,这样如果改变了js
文件,就会自动刷新。
// 引入node核心模块path
const path = require('path')
// 将我们写的html文件,进行打包;
const HtmlWebpakcPlugin = require('html-webpack-plugin')
// 清除上次打包生成的js文件
const CleanWebpackPlugin = require('clean-webpack-plugin')
const webpack = require('webpack');
module.exports = {
// 配置打包模式
mode: 'development',
devtool: 'cheap-module-eval-source-map',
// 入口文件
entry: {
main: './src/index.js',
},
devServer: {
// 服务器启动的根路径
contentBase: './dist',
open: true,
proxy: {
'/api': 'http://localhost:3000'
},
hot: true,
hotOnly: true
},
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader"
},
{
test: /\.(png|jpe?g|gif)$/,
use: {
loader: 'url-loader',
options: {
name: '[name].[ext]',
outputPath: 'images/',
limit: 204800
}
}
}, {
test: /\.vue$/,
use: {
loader: 'vue-loader'
}
}, {
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2
}
},
'sass-loader',
'postcss-loader'
]
}, {
test: /\.(eot|ttf|svg|woff)$/,
use: {
loader: 'file-loader'
}
}, {
test: /\.css$/,
use: [
'style-loader',
'css-loader',
'postcss-loader'
]
}]
},
plugins: [
new CleanWebpackPlugin(),
new webpack.HotModuleReplacementPlugin(),
new HtmlWebpakcPlugin({
template: './src/index.html'
}),
],
optimization: {
usedExports: true
},
// 打包出的文件配置
output: {
// 文件引入的cnd地址
// publicPath: 'http://cdn.com.cn',
publicPath: './',
// 文件名
filename: '[name].js',
// 打包后的文件放在哪个文件夹,是一个绝对路径
// __dirname就是webpack.config.js所在的当前目录的路径,当前模块的目录名,改成bundle就是说,打包后的文件放在dist文件夹中
path: path.resolve(__dirname, 'dist')
}
}
webpack.prod.js
配置如下:
// 引入node核心模块path
const path = require('path')
// 将我们写的html文件,进行打包;
const HtmlWebpakcPlugin = require('html-webpack-plugin')
// 清除上次打包生成的js文件
const CleanWebpackPlugin = require('clean-webpack-plugin')
const webpack = require('webpack');
module.exports = {
// 配置打包模式
mode: 'production',
devtool: 'cheap-module-source-map',
// 入口文件
entry: {
main: './src/index.js',
},
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader"
},
{
test: /\.(png|jpe?g|gif)$/,
use: {
loader: 'url-loader',
options: {
name: '[name].[ext]',
outputPath: 'images/',
limit: 204800
}
}
}, {
test: /\.vue$/,
use: {
loader: 'vue-loader'
}
}, {
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2
}
},
'sass-loader',
'postcss-loader'
]
}, {
test: /\.(eot|ttf|svg|woff)$/,
use: {
loader: 'file-loader'
}
}, {
test: /\.css$/,
use: [
'style-loader',
'css-loader',
'postcss-loader'
]
}]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpakcPlugin({
template: './src/index.html'
})
],
// 打包出的文件配置
output: {
// 文件引入的cnd地址
// publicPath: 'http://cdn.com.cn',
publicPath: './',
// 文件名
filename: '[name].js',
// 打包后的文件放在哪个文件夹,是一个绝对路径
// __dirname就是webpack.config.js所在的当前目录的路径,当前模块的目录名,改成bundle就是说,打包后的文件放在dist文件夹中
path: path.resolve(__dirname, 'dist')
}
}
我们查看我们的生产环境的配置以及开发环境的配置,会发现,有很多相同的配置,比如打包的规则,入口出口的配置等等,我们可以新建一个webpack.common.js
文件,来存放两个配置中相同的部分,然后删除公共的部分;如下面的代码:
webpack.dev.js
配置如下:
const webpack = require('webpack');
module.exports = {
// 配置打包模式
mode: 'development',
devtool: 'cheap-module-eval-source-map',
devServer: {
// 服务器启动的根路径
contentBase: './dist',
open: true,
proxy: {
'/api': 'http://localhost:3000'
},
hot: true,
},
optimization: {
usedExports: true
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
],
}
webpack.prod.js
配置如下:
module.exports = {
// 配置打包模式
mode: 'production',
devtool: 'cheap-module-source-map',
}
webpack.common.js
配置如下:
// 引入node核心模块path
const path = require('path')
// 将我们写的html文件,进行打包;
const HtmlWebpakcPlugin = require('html-webpack-plugin')
// 清除上次打包生成的js文件
const CleanWebpackPlugin = require('clean-webpack-plugin')
module.exports = {
// 入口文件
entry: {
main: './src/index.js',
},
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader"
},
{
test: /\.(png|jpe?g|gif)$/,
use: {
loader: 'url-loader',
options: {
name: '[name].[ext]',
outputPath: 'images/',
limit: 204800
}
}
}, {
test: /\.vue$/,
use: {
loader: 'vue-loader'
}
}, {
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2
}
},
'sass-loader',
'postcss-loader'
]
}, {
test: /\.(eot|ttf|svg|woff)$/,
use: {
loader: 'file-loader'
}
}, {
test: /\.css$/,
use: [
'style-loader',
'css-loader',
'postcss-loader'
]
},{
test: /\.(html)$/,
use: {
loader: 'html-loader',
}
}]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpakcPlugin({
template: './src/index.html'
})
],
// 打包出的文件配置
output: {
path: path.resolve(__dirname, 'dist')
}
}
然后我们通过webpack-merage
插件来进行合并我们的配置文件,输入命令:npm install webpack-merge -D
进行安装;然后修改我们的
webpack.prod.js
以及webpack.dev.js
中的代码,修改后如下:
webpack.dev.js
配置如下:
const webpack = require('webpack');
const merge = require('webpack-merge')
const commonConfig = require('./webpack.common.js')
const devConfig = {
// 配置打包模式
mode: 'development',
devtool: 'cheap-module-eval-source-map',
devServer: {
// 服务器启动的根路径
contentBase: './dist',
open: true,
proxy: {
'/api': 'http://localhost:3000'
},
hot: true,
},
optimization: {
usedExports: true
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
],
}
module.exports = merge(commonConfig, devConfig)
webpack.prod.js
配置如下:
const merge = require('webpack-merge')
const commonConfig = require('./webpack.common.js')
const prodConfig = {
// 配置打包模式
mode: 'production',
devtool: 'cheap-module-source-map',
}
//模块导出的是两个文件的合并
module.exports = merge(commonConfig, prodConfig)
一般情况,我们会新建一个build
文件夹来存放我们的这三个配置文件,然后修改我们的package.json
中的命令:这样就可以了。
"scripts": {
"dev": "webpack-dev-server --config ./build/webpack.dev.js",
"build": "webpack --config ./build/webpack.prod.js",
},
3. Webpack 和 Code Splitting
3.1 打包配置的一些问题
我们在上面将开发版本以及生产版本的配置都通过提取方式写在了一个文件中,并放在了build
文件夹中,但是我们没有修改打包输出文件的地址以及clean-webpack-plugin
插件中清除文件夹的地址,所以我们修改webpack.common.js
配置如下:
plugins: [
new CleanWebpackPlugin(['dist'], {
root: path.resolve(__dirname, '../')
}),
new HtmlWebpakcPlugin({
template: './src/index.html'
})
],
// 打包出的文件配置
output: {
publicPath: './',
filename: '[name].js',
path: path.resolve(__dirname, '../dist')
}
clean-webpack-plugin
添加的root
的配置意思的,重新设置了根目录,默认是认为配置文件所在的地址为根目录,这里我们的配置文件在build
中,所以根目录在他的上一级。我查看clean-webpack-plugin
的插件,并没有配置这个root
选项;不知道哪个版本里面的,我们直接用如下的配置即可:还有我删除了打包输出文件的一些配置,因为html-webpack-plugin
插件会有bug
在我的基础文章笔记里面写了,可以去看看。
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpakcPlugin({
template: './src/index.html'
})
],
// 打包出的文件配置
output: {
path: path.resolve(__dirname, '../dist')
}
3.2 Code Splitting代码分割
我们在写代码的时候,经常会遇到引用很多第三方的包,来方便我们处理业务逻辑,比如我们使用lodash
,但是这样会导致我们打包输出的时候,会将业务逻辑代码以及第三方库的代码打包到到一起;如果我们的业务网逻辑代码很多,会导致打包成功后的文件很大;页面加载速度很慢,如果我们修改了业务代码;这样整个页面得重新加载我们的代码。这个时候我们可以进行代码的分割:
3.2.1 第一种方式:自己实现代码分割:将其他引用的第三方库进行分离写在另一个js脚本中
将其他引用的第三方库进行分离写在另一个js脚本中,然后我们进行修改打包配置,进行配置多个入口如下:
我们之前写的index.js
代码:
import _ from 'lodash';
// ...业务逻辑
console.log(_.join(['a','b','c'],'***'))
修改后的配置文件
// 入口文件
entry: {
main: './src/index.js',
lodash: './src/lodash.js',
},
将index.js
分离出一个lodash.js
文件,内容如下:
import _ from 'lodash';
windows._ = _;
在index.js
中我们不在引入lodash
,因为他已经被挂载到了全局对象上,我们直接可以使用,这样,减少了如果业务逻辑代码频繁修改后页面重新加载很大的打包输出文件;减少了业务逻辑代码的体积;如果我们改变了业务代码,页面只会重新加载业务代码,第三方的lodash.js
文件会被缓存起来,不会重新加载。
3.2.2 第二种方式:使用webpack的代码分割:同步加载包进行打包,使用插件配置进行Code Splitting
我们在webpack.common.js
中添加一个配置,代码如下:意思就是帮我们做代码分割
optimization: {
splitChunks: {
chunks: 'all'
}
},
然后我们查看打包输出文件,会打包出一个main.js
的业务逻辑代码,以及vendors~main.js
的第三方库文件的代码:
vendors~main.js
文件内容:
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["vendors~main"],{
/***/ "./node_modules/_lodash@4.17.11@lodash/lodash.js":
/*!*******************************************************!*\
!*** ./node_modules/_lodash@4.17.11@lodash/lodash.js ***!
\*******************************************************/
webpack
插件会很智能的分割我们的代码;
3.2.3 第三种方式:使用webpack的代码分割:异步引入模块的代码分割
上面的包模块是一种同步的引入方式,然后webpack
会先处理这些,然后进行处理我们的业务逻辑代码;我们这里再说一种异步b包模块引入的代码分割:我们的index.js
代码如下
function getComponent () {
return import('lodash').then(({default: _}) => {
var element = document.createElement('div')
element.innerHTML = _.join(['a','b','c'],'***')
return element
})
}
getComponent ().then((element) => {
document.body.appendChild(element)
})
上面的代码通过return import
这种引入方式是实验性质的语法会报错,我们可以通过一个babel
的插件来解决这个问题,输入命令:npm install babel-plugin-dynamic-import-webpack --save-dev
,然后在我们的.babelrc
文件里面进行配置:多加一个plugins: ["dynamic-import-webpack"]
配置
{
presets: [
[
"@babel/preset-env", {
targets: {
chrome: "67",
},
useBuiltIns: 'usage'
}
],
"@babel/preset-react"
],
plugins: ["dynamic-import-webpack"]
}
可以查看打包的日志:会生成两个js
文件,0.js
里面就是我们引入包的打包文件;
这里需要注意的是:首先代码分割跟我们的
webpack
是无关的,我们只是通过代码分割这种思想来提升我们项目的性能;webpack
中实现代码分割两种方式:
- 同步代码:只需要在
webpack.common.js
中做optimization
的配置;- 异步代码(
import
):无需做任何配置,会自动进行代码分割;
4 SplitChunksPlugin 配置参数详解
4.1 修改打包输出的文件名
其实我们上面讲的webpack
代码分割是使用了SplitChunksPlugin
这个插件来实现的;我们查看打包结果,他会将lodash
的第三方库打包成0.js
的文件,我们如果想要改这个生成的文件名,可以通过魔法注释的方法来实现:我们在引入lodash
前加了一个/* webpackChunkName = "lodash"*/
意思就是打包后,这个模块的名字叫做lodash.js
function getComponent () {
return import(/* webpackChunkName:"lodash"*/'lodash').then(({default: _}) => {
var element = document.createElement('div')
element.innerHTML = _.join(['a','b','c'],'***')
return element
})
}
getComponent ().then((element) => {
document.body.appendChild(element)
})
然后移除掉我们前面安装的babel-plugin-dynamic-import-webpack
插件,因为这个插件不支持我们的魔法注释这种功能;在package.json
删除babel-plugin-dynamic-import-webpack
,然后在.babelrc
文件里面去除我们的配置;然后我们使用官方提供的动态引入第三库的插件;输入命令:npm install --save-dev @babel/plugin-syntax-dynamic-import
;然后在.babelrc
文件中我们引入这个插件:
{
presets: [
[
"@babel/preset-env", {
targets: {
chrome: "67",
},
useBuiltIns: 'usage'
}
],
"@babel/preset-react"
],
plugins: ["@babel/plugin-syntax-dynamic-import"]
}
这样修改还是不行,他打包出的文件会在前面加一个vendors~lodash.js
这样,我们需要去除这个前缀,打开webpack.common.js
,修改optimization
的配置如下:这样,打包之后的文件名就是我们所想要的了。
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendors: false,
default: false
}
}
},
其实我们不进行splitChunks
配置,也可以进行打包的,因为,它本身有一个默认的配置,配置内容如下:
optimization: {
splitChunks: {
chunks: 'async',//代码分割只对异步加载的代码生效,如果想对同步、异步都进行分割设置为all
minSize: 30000,//设置模块大小大于30kb才会进行代码分割
maxSize: 0,//设置打包输出文件的最大体积,如果需要打包的模块超过这个大小,他会进行分割成多个文件进行打包输出
minChunks: 1,//当一个模块被用了至少多少次的时候,才进行分割。
maxAsyncRequests: 5,//同时加载的模块数。如果页面引用的模块超过五个,不会对超过的模块进行代码分割
maxInitialRequests: 3,//入口文件进行加载引入的模块最多数,这个设置为3,就是如果入口文件引入模块超过三个,超过的就不会进行代码分割
automaticNameDelimiter: '~',//打包输出文件的连接符,例如vendors~main.js;vendors是组名,后面就是连接符;vendors~main.js意思是vendors组的入口文件是main.js
name: true,
cacheGroups: {
// 如果引入的包是node_modules里面的内容,会进入到这里的配置
vendors: {
test: /[\\/]node_modules[\\/]/,//检测引入的第三方库是不是node_modules里面的内容
priority: -10,
filename: 'vendors.js' //如果是node_modules里面的内容,会打包到这个文件里面
},
// 如果引入的包不是node_modules里面的内容,会进入到这里的配置
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
filename: 'common.js'
}
}
}
}
4.2 SplitChunksPlugin 配置参数的意义
各个参数的意义:
chunks
:设置打包是对异步代码(async
)f分割;还是对同步代码(initial
)做代码分割;还是对所有(all
)的代码都打包;如果设置all
或者initial,他会进入到
cacheGroups`这个配置项,查看打包的配置;vendors
:test
是检测引入的包是不是node_modules
里面的内容;如果是,他会将这库打包到vendors
这个组中,打包后的文件会加一个vendors~
前缀,代表是vendors
这个组中;也就是这个库是node_modules
里面的内容;我们可以加一个filename: 'vendors.js'
这个配置项,这样,打包出来的所有的文件都会在vendors.js
这个文件中;如果不是node_modules
里面的内容,他会进入到default
的配置中。minSize
:设置模块大小大于30kb才会进行代码分割,设置打包文件的最小体积。maxSize
:设置打包输出文件的最大体积,如果需要打包的模块超过这个大小,他会进行分割成多个文件进行打包输出minChunks
:当一个模块被用了至少多少次的时候,才进行分割。maxAsyncRequests
:同时加载的模块数。如果页面引用的模块超过五个,不会对超过的模块进行代码分割maxInitialRequests
:入口文件进行加载引入的模块最多数,这个设置为3,就是如果入口文件引入模块超过三个,超过的就不会进行代码分割automaticNameDelimiter
:打包输出文件的连接符,例如vendors~main.js
;vendors
是组名,后面就是连接符;vendors~main.js
意思是vendors
组的入口文件是main.js
cacheGroups
:具体的打包输出文件的规则;注意这里的priority
,其实大部分的包都是满足既是node_modules
中的,又是满足默认的配置,我们通过priority
设置优先级,优先执行哪个配置;数越大,优先级越高;越先执行。reuseExistingChunk
:配置为true
就是如果之前打包过该模块,再次遇到不会进行打包,只是复用以前打包的模块。
5. Lazy Loading 懒加载,Chunk 是什么?
5.1 懒加载
我们的页面有时候会引入很多的包,或者第三方库文件;这时候整个页面加载速度会特别慢,我们可以通过懒加载的方式去加载这些包;来提高页面的响应速度。懒加载也就是说,我们在页面初始化的时候,不加载那些初始化不需要的包文件,只在需要包的函数中,进行异步加载包文件,如下面代码:这里,只要我们点击页面的时候才会需要lodash
包,所以,我们通过异步加载,在页面首次加载的时候,不对该包进行加载;这样来提高页面的响应速度。
function getComponent () {
return import(/* webpackChunkName:"lodash"*/'lodash').then(({default: _}) => {
var element = document.createElement('div')
element.innerHTML = _.join(['a','b','c'],'***')
return element
})
}
// 点击页面才会执行
document.addEventListener('click', () => {
getComponent ().then((element) => {
document.body.appendChild(element)
})
})
可以使用异步函数async
来改写上面的函数:
async function getComponent () {
const {default: _} = await import(/* webpackChunkName:"lodash"*/'lodash');
const element = document.createElement('div')
element.innerHTML = _.join(['a','b','c'],'***')
return element
}
// 点击页面才会执行
document.addEventListener('click', () => {
getComponent ().then((element) => {
document.body.appendChild(element)
})
})
5.2 Chunk 是什么?
我们打包输出的每一个js
文件,都是一个Chunk
;可以查看我们打包输出的日志:
比如我们前面配置的chunks: 'async',
代码分割只对异步加载的代码生效,如果想对同步、异步都进行分割设置为all,还有就是我们配置进行打包最小引用次数;minChunks: 1
当一个模块被用了至少多少次的时候,才进行分割。
6. 打包分析,Preloading, Prefetching
6.1 打包分析工具
webpack
打包分析工具:https://github.com/webpack/analyse
,如果要使用这个工具对我们打包生成的代码进行分析,我们首先需要生成一个打包过程的描述文件;通过这样命令webpack --profile --json > stats.json
,我们在package.json
里面进行配置我们的打包命令:代码意思是,我们会将打包过程的描述信息放置到stats.json
这个文件中;
"scripts": {
"dev-build": "webpack --profile --json > stats.json --config ./build/webpack.dev.js"
},
然后点击进入这个网站http://webpack.github.io/analyse/
(需要科学上网),上传我们打包的描述信息文件,会生成一个分析结果:
我的webpack
版本是4.31.0
这里,显示的是不合适。我们也可以使用其他的检测工具https://webpack.js.org/guides/code-splitting#bundle-analysis
这里介绍了很多。比如这个https://alexkuz.github.io/webpack-chart/
也可以进行检查;
6.2 Preloading, Prefetching
6.2.1 异步加载交互代码提高性能
页面中一些交互的代码,比如点击页面才会执行的事件,或者点击按钮执行的事件这些;
document.addEventListener('click', () => {
const element = document.createElement('div')
element.innerHTML = 'jiegiser'
document.body.appendChild(element)
})
我们在页面初始化的时候,并没有用到这些;我们可以打开控制台,按ctrl+shift+p
然后输入>show coverage
来查看我们文件的利用率:绿色的是页面加载有用的内容。
可以看到我们在与页面交互的代码,没有被利用;页面一开始并没有使用这个交互式的函数,所以在页面初始化加载的时候,将这些代码全部下载进行加载,会浪费项目的性能。这种交互的代码最好是放在一个异步加载的模块里面,我们新建一个click.js
文件,里面写我们异步加载模块等实现的交互式代码:
function handleClick () {
const element = document.createElement('div')
element.innerHTML = 'jiegiser'
document.body.appendChild(element)
}
export default handleClick;
然后在index.js
这样去引入我们的 click.js
模块:
document.addEventListener('click', () => {
// func就是我们导出的handleclick方法
import('./click.js').then(({default: func}) => {
func();
})
})
再次打开控制台的>show coverage
来查看我们文件的利用率:会发现比之前的高跟多;
这也就说明了为什么webpack
的chunks: 'async'
默认的配置项是打包异步的代码,webpack
真正希望的是我们多写这种异步加载模块的代码,进行打包,来提升性能。
6.2.2 利用Preloading, Prefetching优化异步加载交互代码提高性能
我们前面写的只有页面需要展示的内容在页面初始化的时候进行加载,其他的交互式的代码可以通过异步加载的方式提高性能;但是比如我们有一个交互式的是一个点击按钮之后,打开一个模态框;这种交互式的如果等到用户点击按钮的时候再进行异步加载代码,是会等待很长时间的,我们可以通过Preloading, Prefetching
(https://webpack.js.org/guides/code-splitting#prefetchingpreloading-modules
)来优化加载,等到页面全部加载完成,网络空闲之后,再进行加载我们的异步交互代码;通过添加一个/* webpackPrefetch: true */
魔法注释来实现,等到页面主要的js
文件加载完成之后,再进行加载我们的交互代码;
document.addEventListener('click', () => {
// func就是我们导出的handleclick方法
import(/* webpackPrefetch: true */ './click.js').then(({default: func}) => {
func();
})
})
Preloading
,和Prefetching
基本是一样的;Prefetching
是等到页面主要核心的js
文件加载完成之后,带宽空闲的时候再进行加载异步加载的代码;Preloading
是跟主要的业务逻辑代码一起加载的。
7. CSS 文件的代码分割
7.1 使用MiniCssExtractPlugin 插件进行css代码分割
我们在输出配置的时候可以添加一个chunkFilename
的配置项;我们配置的入口文件在打包输出的时候其实是根据filename: '[name].js',
我们配置的这个进行打包输出,而其他的打包输出文件会根据我们配置的chunkFilename
的配置项,来进行打包输出;我们之前打包css
,打包成功之后会将css
与js
文件打包在一起;我们现在想把我们的css
文件打包输出的时候也跟打包js
文件一样单独输出,这时候就需要这个MiniCssExtractPlugin
插件,来帮助我们:
输入命令npm install --save-dev mini-css-extract-plugin
进行安装这个插件,需要注意的是,这个插件不支持热更新,在开发环境的时候使用,开发效率较低,我们修改css
样式之后,得手动刷新浏览器;一般我们在线上环境会使用这个插件。在开发环境的配置中添加下面的配置:
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const merge = require('webpack-merge')
const commonConfig = require('./webpack.common.js')
const prodConfig = {
// 配置打包模式
mode: 'production',
devtool: 'cheap-module-source-map',
plugins: [
new MiniCssExtractPlugin({})
]
}
// 模块导出的是两个文件的合并
module.exports = merge(commonConfig, prodConfig)
然后在打包的规则中,配置打包css
文件的时候使用该插件提供的loader
,把css
单独打包成一个文件。然我们修改生产环境的配置,配置打包css
文件的规则,如下代码:开发环境的打包css
的规则可以不用修改
module: {
rules: [
{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
importLoaders: 2
}
},
'sass-loader',
'postcss-loader'
]
}, {
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader'
]
},
]
},
然后我们只想打包会发现并没有生成一个css
文件,是因为我们前面配置了tree shaking
会检测引入的包是否使用,如果没有使用就会去除,不会进行打包;我们可以修改package.json
里面的配置,让其不对css
文件进行检查:
"sideEffects": [
"*.css"
],
然后将配置时候启用tree shaking
检查的配置放在公用的配置文件(webpack.common.js
)中:
optimization: {
usedExports: true
},
修改后如下:
// 修改配置,进行代码分割进行打包,以及去除打包成功之后添加的vendors~前缀
optimization: {
usedExports: true,
splitChunks: {
chunks: 'all',
cacheGroups: {
vendors: false,
default: false
}
}
},
我们还可以在MiniCssExtractPlugin
添加很多配置项,如下面代码:当打包的文件直接引入到页面的时候他的命名规则会走filename
的配置项,如果是间接引入到页面,就会走下面的chunkFilename
的配置项。如果页面直接引入了多个css
文件,会直接将这些文件合并打包到一个main.css
文件中。
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[name].chunk.css',
})
]
7.2 对打包输出的css文件进行压缩
需要压缩css
文件,我们使用一个插件,输入命令进行安装:npm install --save-dev optimize-css-assets-webpack-plugin
,然后在webpack.prod.js
里面引入这个插件,进行配置如下:
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const prodConfig = {
......
optimization: {
minimizer: [new OptimizeCssAssetsPlugin({})]
},
}
......
7.3 多个js入口文件引入的css文件打包输出为一个文件
我们需要通过代码分割的配置添加一个styles
的组,配置如下:enforce: true,
就是说忽略掉我们配置的其他有关css
文件打包的默认参数;
splitChunks: {
cacheGroups: {
styles: {
name: 'styles',
test: /\.css$/,
chunks: 'all',
enforce: true,
},
},
},
7.4 根据配置入口js文件不同,对其中引入的css文件进行单独打包
我们还可以根据我们配置的js
文件,对每个文件中引入的css
文件打包输出成单独的一个文件。如下面配置:入口文件有foo
跟bar
,分别进行打包输出为不同的文件。
splitChunks: {
cacheGroups: {
fooStyles: {
name: 'foo',
test: (m, c, entry = 'foo') =>
m.constructor.name === 'CssModule' && recursiveIssuer(m) === entry,
chunks: 'all',
enforce: true,
},
barStyles: {
name: 'bar',
test: (m, c, entry = 'bar') =>
m.constructor.name === 'CssModule' && recursiveIssuer(m) === entry,
chunks: 'all',
enforce: true,
},
},
},
8. Webpack 与浏览器缓存( Caching )
8.1 去除打包输出性能警告提示
我们有时候在打包文件的时候,控制台会抛出一个警告:警告我们打包输出的文件太大了超过了最大的244kb
的大小。
我们可以去除这个性能的警告,在webpack.common.js
中添加配置
module.exports = {
........
//去除控制台提示性能的问题
performance: false,
........
}
8.2 浏览器缓存
我们每次打包之后的文件,如果不做添加文件修改,打包输出的文件每次都是一样的;这会导致浏览器加在我们的代码的时候出现使用缓存中已经缓存好的文件;这时候我们可以在打包输出的配置进行修改添加一个打包输出文件的唯一标识符:contenthash
是文件的hash
值;如果打包输出的文件没有变化,这个值不变,如果有变化,对应的这个值也会变化。
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].js'
}
对于新版本的webpack4.x
打包之后如果文件没有更改,会保持不变,但是老版本的webpack4.x
货值之前的版本,有可能会发生变化,这个时候我们需要在optimization
选项中添加下面的配置:
optimization: {
runtimeChunk: {
name: 'runtime'
},
}
我们会发现打包生成后,多出来一个runtime--.js
的文件,这个文件保存着库代码与业务代码之间的引用关系。
9. Shimming的作用
9.1 Shimming的使用
webpack
编译器(compiler
)能够识别遵循ES2015
模块语法、CommonJS
或 AMD
规范编写的模块。然而,一些第三方的库(library
)可能会引用一些全局依赖(例如 jQuery
中的$
)。这些库也可能创建一些需要被导出的全局变量。这些“不符合规范的模块”就是 shimming
发挥作用的地方,在webpack
中,每个库文件是单独的,彼此隔离的;如下面的代码:
新建的一个jquery.ui.js
我们使用了jquery
但事故我们的库文件没有引入他。
export function ui () {
$('body').css('background', 'red')
}
index.js
文件:
import _ from 'lodash'
import $ from 'jquery'
import { ui } from './jquery.ui.js'
ui();
const dom = $('<div>')
dom.html(_.join(['dell', 'lee'], '---'))
$('body').append(dom)
这样会提示$
没有定义,我们原本想着前面引入了jquery
为什么还是没用,就是因为在webpack
中,每个库文件是单独的,彼此隔离的,我们一般引入的库文件是第三方的,存放在node_module
中,我们也不可能去修改它里面的内容,所以这时候就需要使用Shimming
来解决问题;修改我们的webpack.common.js
:new webpack.ProvidePlugin
这个webpack
自带的插件会进行对打包文件进行检查,如果检测到你的代码中有$
这个符号,他会自动帮你引入jquery
模块。然后将jwuery
模块赋值给$
字符串。
const webpack = require('webpack')
module.exports = {
// 添加插件清空打包路径以及根据模板进行打包html文件
plugins: [
.....
new webpack.ProvidePlugin({
$: 'jquery'
})
],
}
同样这里的配置我们还可以直接将某一个包的方法进行配置到这里,比如:我想使用lodash
模块中的join
方法,之前我们是引入import _ from 'lodash'
这样引入,然后_.join
这样调用他的join
方法,但是我这里就想_join
这样去调用join
方法,我们可以在webpack.common.js
进行配置
export function ui () {
$('body').css('background', 'red')
$('body').html(_join(['ddd', 'ddd'],'----'))
}
配置如下:_join: ['lodash', 'join']
意思就是,当我们打包的时候遇到_join
,就去引入lodash
,将lodash
中的join
方法,赋值给他。
// 添加插件清空打包路径以及根据模板进行打包html文件
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpakcPlugin({
template: './src/index.html'
}),
new webpack.ProvidePlugin({
$: 'jquery',
_join: ['lodash', 'join']
})
],
9.2 利用Shimming改变模块中this指向window
我们在模块中,比如我们的入口文件,我们打印console.log(this === window);
会发现,里面的this
指向并不是window
对象,我们可以借助一些loader
:输入命令进行安装npm install imports-loader --save-dev
,然后进行修改webpack.common.js
中打包js
文件的配置:loader: "imports-loader?this=>window"
当我们打包js
文件的时候,会走下面的规则,然后将我们每个模块中的this
指向我们的windos
对象。
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: "babel-loader",
},
{
loader: "imports-loader?this=>window"
}
]
}]
}
深入学习:查看
webpack
官网中guides
里所有内容。
10. Webpack环境变量的使用方法
之前我们是将我们的配置文件分成了三个部分webpack.dev.js webapck.common.js webapck.prod.js
然后在package.json
配置命令如下:进行运行不同的文件:
package.json
之前的配置:
"scripts": {
"dev": "webpack-dev-server --config ./build/webpack.dev.js",
"build": "webpack --config ./build/webpack.prod.js",
"dev-build": "webpack --config ./build/webpack.dev.js",
},
我们现在可以修改一下我们之前的配置,将webpack.dev.js
配置如下:直接导出配置;
const webpack = require('webpack');
// const merge = require('webpack-merge')
// const commonConfig = require('./webpack.common.js')
const devConfig = {
// 配置打包模式
mode: 'development',
devtool: 'cheap-module-eval-source-map',
devServer: {
// 服务器启动的根路径
contentBase: './dist',
open: true,
proxy: {
'/api': 'http://localhost:3000'
},
hot: true,
},
module: {
rules: [
{
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2
}
},
'sass-loader',
'postcss-loader'
]
}, {
test: /\.css$/,
use: [
'style-loader',
'css-loader',
'postcss-loader'
]
},
]
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
],
// output: {
// filename: '[name].js',
// chunkFilename: '[name].js'
// }
}
// module.exports = merge(commonConfig, devConfig)
module.exports = devConfig;
对应的webpack.prod.js
修改为:
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
// const merge = require('webpack-merge')
// const commonConfig = require('./webpack.common.js')
const prodConfig = {
// 配置打包模式
mode: 'production',
// 暂时先屏蔽输出source文件
// devtool: 'cheap-module-source-map',
module: {
rules: [
{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
importLoaders: 2
}
},
'sass-loader',
'postcss-loader'
]
}, {
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader'
]
},
]
},
optimization: {
minimizer: [new OptimizeCssAssetsPlugin({})]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[name].chunk.css',
})
],
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].js'
}
}
// 模块导出的是两个文件的合并
// module.exports = merge(commonConfig, prodConfig)
module.exports = prodConfig;
然后我们在webpack..common.js
进行根据我们在package.json
中配置的命令进行判断是使用哪一个配置文件:
首先修改package.json
:--env.production
为传入参数;表示是生产环境的配置
"scripts": {
"dev": "webpack-dev-server --config ./build/webpack.common.js",
"build": "webpack --env.production --config ./build/webpack.common.js",
"test": "webpack-dev-server",
"dev-build": "webpack --config ./build/webpack.common.js",
},
然后在webpack.common.js
配置如下:
module.exports = (env) => {
if (env && env.production) {
return merge (commonConfig, prodConfig)
} else {
return merge (commonConfig, devConfig)
}
}
webpack.common.js
文件内容:
// 引入node核心模块path
const path = require('path')
// 将我们写的html文件,进行打包;
const HtmlWebpakcPlugin = require('html-webpack-plugin')
// 清除上次打包生成的js文件
const CleanWebpackPlugin = require('clean-webpack-plugin')
//重新配置环境变量
/**************** */
const merge = require('webpack-merge');
const devConfig = require('./webpack.dev.js')
const prodConfig = require('./webpack.prod.js')
/*************** */
const webpack = require('webpack')
// module.exports = {
const commonConfig = {
// 入口文件
entry: {
main: './src/index.js',
},
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: "babel-loader",
},
{
loader: "imports-loader?this=>window"
}
]
},
{
test: /\.(png|jpe?g|gif)$/,
use: {
loader: 'url-loader',
options: {
name: '[name].[ext]',
outputPath: 'images/',
limit: 204800
}
}
}, {
test: /\.vue$/,
use: {
loader: 'vue-loader'
}
}, {
test: /\.(eot|ttf|svg|woff)$/,
use: {
loader: 'file-loader'
}
},{
test: /\.(html)$/,
use: {
loader: 'html-loader',
}
}]
},
// 添加插件清空打包路径以及根据模板进行打包html文件
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpakcPlugin({
template: './src/index.html'
}),
new webpack.ProvidePlugin({
$: 'jquery',
_join: ['lodash', 'join']
})
],
//去除控制台提示性能的问题
performance: false,
// 修改配置,进行代码分割进行打包,以及去除打包成功之后添加的vendors~前缀
optimization: {
runtimeChunk: {
name: 'runtime'
},
usedExports: true,
splitChunks: {
chunks: 'all',
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
name: 'vendors'
},
}
}
},
// optimization: {
// splitChunks: {
// chunks: 'async',//代码分割只对异步加载的代码生效,如果想对同步、异步都进行分割设置为all
// minSize: 30000,//设置模块大小大于30kb才会进行代码分割
// maxSize: 0,//设置打包输出文件的最大体积,如果需要打包的模块超过这个大小,他会进行分割成多个文件进行打包输出
// minChunks: 1,//当一个模块被用了至少多少次的时候,才进行分割。
// maxAsyncRequests: 5,//同时加载的模块数。如果页面引用的模块超过五个,不会对超过的模块进行代码分割
// maxInitialRequests: 3,//入口文件进行加载引入的模块最多数,这个设置为3,就是如果入口文件引入模块超过三个,超过的就不会进行代码分割
// automaticNameDelimiter: '~',//打包输出文件的连接符,例如vendors~main.js;vendors是组名,后面就是连接符;vendors~main.js意思是vendors组的入口文件是main.js
// name: true,//
// cacheGroups: {
// // 如果引入的包是node_modules里面的内容,会进入到这里的配置
// vendors: {
// test: /[\\/]node_modules[\\/]/,//检测引入的第三方库是不是node_modules里面的内容
// priority: -10,
// filename: 'vendors.js' //如果是node_modules里面的内容,会打包到这个文件里面
// },
// // 如果引入的包不是node_modules里面的内容,会进入到这里的配置
// default: {
// minChunks: 2,
// priority: -20,
// reuseExistingChunk: true,
// filename: 'common.js'
// }
// }
// }
// },
// 打包出的文件配置/*这里的配置会跟打包HTML的插件冲突 */
output: {
// 文件引入的cnd地址
// publicPath: 'http://cdn.com.cn',
// publicPath: './',
// // 文件名
// filename: '[name].js',
// 打包后的文件放在哪个文件夹,是一个绝对路径
// __dirname就是webpack.config.js所在的当前目录的路径,当前模块的目录名,改成bundle就是说,打包后的文件放在dist文件夹中
// 非入口文件打包输出走下面的配置项
// chunkFilename: '[name].chunk.js',
path: path.resolve(__dirname, '../dist')
}
}
module.exports = (env) => {
if (env && env.production) {
return merge (commonConfig, prodConfig)
} else {
return merge (commonConfig, devConfig)
}
}
我们也可以进行传参数,这样:
"scripts": {
"dev": "webpack-dev-server --config ./build/webpack.common.js",
"build": "webpack --env production --config ./build/webpack.common.js",
"dev-build": "webpack --config ./build/webpack.common.js",
},
然后webpack.common.js
这样:
module.exports = (production) => {
if (production) {
return merge (commonConfig, prodConfig)
} else {
return merge (commonConfig, devConfig)
}
}
或者在传参数的时候传入一个字符串:
"scripts": {
"dev": "webpack-dev-server --config ./build/webpack.common.js",
"build": "webpack --env.production =abc --config ./build/webpack.common.js",
"dev-build": "webpack --config ./build/webpack.common.js",
},
然后webpack.common.js
这样:
module.exports = (env) => {
if (env && env.production === 'abc) {
return merge (commonConfig, prodConfig)
} else {
return merge (commonConfig, devConfig)
}
}