一、webpack高级概念
1. tree shaking
可以将应用程序想象成一棵树。绿色表示实际用到的源码和 library,是树上活的树叶。灰色表示无用的代码,是秋天树上枯萎的树叶。为了除去死去的树叶,你必须摇动这棵树,使它们落下。
tree shaking依赖于ES2015模块系统中的静态结构特性,例如 import 和 export。很多时候,我们只需要引入库的某个部分,而非全部,利用它会去除其他部分,将减小打包后的体积。mode为production的时候,optimization: { usedExports: true}已经是配置好的,不需要再配置。
#webpack.config.js
//开发模式
optimization:{
usedExports:true
}
由于tree shaking会将未使用或者未导入的模块全部删除,这里的"sideEffects有很大的用途,比如我们在使用@babel/polyfill的时候,他的内部并没有使用export导出任何模块,他只是通过类似windows.Promise这样给全局T添加一些函数,但是我们使用Tree Shaking这种去打包的时候,他会发现这个模块我们并没有通过import引入任何模块,他会以为,我们并没有使用这个模块,不会对他进行打包,这时候,需要配置:"sideEffects": ["@babel/polyfill"],打包的时候不会对这个模块进行摇树。 如果引入代码无副作用,则设置为sideEffects:false;
#package.json
"sideEffects": false
2. production和development模式的区分打包
开发环境和生产环境的构建目标差异很大。在开发环境中,需要具有实时重新加载或热模块替换能力、source map和localhost server。而在生产环境中,我们的目标则转向于关注更小的 bundle,更轻量的source map,以及更优化的资源,以改善加载时间。由于要遵循逻辑分离,我们通常建议为每个环境编写彼此独立的 webpack配置。
新建webpack.dev.js、webpack.prod.js和webpack.common.js。将通用配置放到webpack.common.js中,借助webpack-merge插件将代码合并。
//安装merge
npm install --save-dev webpack-merge
- common.js
const path = require("path");
const {CleanWebpackPlugin} = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry:{
main:"./src/index.js",
other:'./src/other.js'
},
output:{
filename:"[name].[hash].js",
path:path.resolve(__dirname,"../dist")
},
module:{
rules:[
{
test:/\.css$/,
use:[
"style-loader",
{
loader:"css-loader",
options:{
importLoaders:1
}
},
"postcss-loader"
],
},
{
test:/\.less$/,
use:["style-loader","css-loader","less-loader"]
},
{
test:/\.js$/,
exclude:/node_modules/,
use: {
loader: "babel-loader"
}
},
{
test:/\.(png|jpg|gif)$/,
use:[{
loader:"url-loader",
options:{
name:"[name]_[hash].[ext]",
outputPath:"images/",
limit:3000
}
}]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html'
}),
],
optimization:{
usedExports:true,
splitChunks:{
chunks:"all",
minSize: 3
}
}
}
- webpack.dev.js
const merge = require("webpack-merge");
const webpack = require('webpack');
const commonConfig = require('./webpack.common')
module.exports = merge(commonConfig,{
mode:"development",
devtool:"cheap-module-eval-source-map",
plugins: [
new webpack.HotModuleReplacementPlugin(),
],
devServer:{
port:"8081",
open: true,
// 打包后访问的html所在路径
contentBase:"./dist",
hot:true
}
})
- webpack.prod.js 安装uglifyjs-webpack-plugin压缩代码,由于这个插件会影响webpack编译速度,一般在生产环境使用。
const merge = require("webpack-merge");
const commonConfig = require('./webpack.common');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
module.exports = merge(commonConfig,{
mode:"production",
devtool:"source-map",
plugins: [
new UglifyJSPlugin()
]
})
!注意:由于tree-shaking会将import的css和less文件去除。修改package.json:
"sideEffects": ["*.css","*.less"]
3.代码分离
代码分离能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。 有三种常用的代码分离方法:
- 入口起点:使用 entry 配置手动地分离代码。
- 防止重复:splitChunks提取被重复引入的文件,单独生成一个或多个文件,这样避免在多入口重复打包文件。是webpack内置插件,无需安装。
#webpack.common.js
optimization: {
splitChunks: {
chunks: 'async',//all:同步和异步;initial:同步
minSize: 30000,//文件在压缩前的最小尺寸
maxSize: 0,//设置输出文件的最大体积,如果需要打包的模块超过这个大小,他会进行分割成多个文件进行打包输出
minChunks: 1,//当一个模块被用了至少多少次的时候,才进行分割。
maxAsyncRequests: 5,//同时加载的模块数。如果页面引用的模块超过五个,不会对超过的模块进行代码分割
maxInitialRequests: 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.Lazy Loading 懒加载
懒加载即按需加载,在页面初始化的时候,不加载那些初始化不需要的包文件,只在需要包的函数中,进行异步加载包文件。异步代码(import):无需做任何配置,会自动进行代码分割。
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 await改写
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.CSS 文件的代码分割
- 使用MiniCssExtractPlugin 插件进行css代码分割
MiniCssExtractPlugin插件把css样式从js文件中提取到单独的css文件中。需要注意:这个插件不支持热更新,一般在线上环境使用这个插件。
npm install --save-dev mini-css-extract-plugin
在开发环境的配置中添加下面的配置,由于是将css单独抽出来,注意去除style-loader。
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: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader'
]
}
]
}
}
// 模块导出的是两个文件的合并
module.exports = merge(commonConfig, prodConfig)
当打包的文件直接引入到页面的时候他的命名规则会走filename的配置项,如果是间接引入到页面,就会走下面的chunkFilename的配置项。
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[name].chunk.css',
})
]
- 对打包输出的css文件进行压缩,在webpack.prod.js里面引入这个插件,进行配置如下:
npm install --save-dev optimize-css-assets-webpack-plugin
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const prodConfig = {
......
optimization: {
minimizer: [new OptimizeCssAssetsPlugin({})]
},
}
- 多个js入口文件引入的css文件打包输出为一个文件
splitChunks: {
cacheGroups: {
styles: {
name: 'styles',
test: /\.css$/,
chunks: 'all',
enforce: true,//忽略其他打包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,
},
},
},
6.浏览器缓存
contenthash指内容更改,hash值才会改变,生产环境中配置如下:
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].js'
}
对于新版本的webpack4.x打包之后如果文件没有更改,会保持不变,但是老版本的webpack4.x货值之前的版本,有可能会发生变化。这个时候需要在optimization选项中添加下面的配置:
optimization: {
runtimeChunk: {
name: 'runtime'
},
}
7.Shimming的作用
- Shimming的使用
webpack编译器能够识别遵循ES2015模块语法、CommonJS或 AMD规范编写的模块,Jquery这些库会创建一些需要被导出的全局变量。由于每个模块都是独立的,入口模块引入了jquery,其他模块也是无法使用的,如果每个模块都引入jquery文件,也不可行。可以利用shimming解决问题,在代码中检测到$,就引入jquery。
join: ['lodash', 'join']意思就是,当我们打包的时候遇到_join,就去引入lodash,将lodash中的join方法,赋值。
#webpack.common.js
const webpack = require('webpack')
module.exports = {
plugins: [
.....
new webpack.ProvidePlugin({
$: 'jquery',
_join: ['lodash', 'join']
})
],
}
- 利用Shimming改变模块中this指向window
由于模块的this并不是指向全局,借助imports-loader改变模块中this的指向
npm install imports-loader --save-dev
当我们打包js文件的时候,会走下面的规则,然后将我们每个模块中的this指向我们的window对象。
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
use: ["babel-loader","imports-loader?this=>window"]
}]
}
8.Webpack环境变量的使用方法
npm install cross-env --save-dev
#package.json
{
//注意在打包之前设置NODE_ENV
"build": "cross-env NODE_ENV=production webpack --config ./build/webpack.common.js"
}
}
#webpack.common.js
const env = process.env.NODE_ENV
module.exports = ()=>{
if (env && env=="production") {
return merge (commonConfig, prodConfig)
} else {
return merge (commonConfig, devConfig)
}
}
二、webpack实战配置
1.PWA(Progressive Web Application)的打包配置
首先安装http-server,让打包输出的dist的文件夹中启动一个服务器。服务器挂了,可以利用PWA,把之前访问过的页面显示出来。
npm install http-server -D
输入命令npm install workbox-webpack-plugin --save-dev进行安装,由于只需要在线上环境,使用pwa技术,让用户体验更好,所以,我们只需要修改webapck.prod.js的配置文件。
const { GenerateSW } = require('workbox-webpack-plugin')
const prodConfig = {
plugins: [
new GenerateSW()
]
}
npm run build后,会生成service-worker.js。接下来注册Service worker,在入口文件添加以下代码:
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js').then(registration => {
console.log('SW registered: ', registration);
}).catch(registrationError => {
console.log('SW registration failed: ', registrationError);
});
});
}
2. 使用 WebpackDevServer 实现请求转发
当请求'/react/api',接口会转发到'www.dell-lee.com';
访问http://localhost:8081/react/api/header.json相当于访问https://www.dell-lee.com/header.json;通过pathRewrite重写请求路径。
有时候我们请求的地址是https协议的网址,需要加一个配置项:secure: false,
devServer: {
// 服务器启动的根路径
contentBase: './dist',
open: true,
proxy: {
'/react/api': {
target: 'https://www.dell-lee.com',
secure: false,
pathRewrite: {
'header.json': 'demo.json'
}
},
},
hot: true,
}
在这里也可以进行拦截,比如下面的代码配置:bypass里面的配置项就是说当发送请求要接手的是html页面数据的时候,也就是说请求是一个html的地址的时候,直接跳转到index.html页面。
devServer: {
// 服务器启动的根路径
contentBase: './dist',
open: true,
proxy: {
'/react/api': {
target: 'https://www.dell-lee.com',
secure: false,
bypass: function(req, res, proxyOptions) {
if (req.headers.accept.indexOf('html') !== -1) {
console.log('Skipping proxy for browser request.');
return '/index.html';
}
},
pathRewrite: {
'header.json': 'demo.json'
}
},
},
hot: true,
},
下面代码表示遇到'/auth', '/api'这两个地址,都是转发到https://www.dell-lee.com这个服务器。
devServer: {
proxy: {
context: ['/auth', '/api'],
target: 'https://www.dell-lee.com',
},
hot: true,
}
如果我们想做一个根目录的路径的转发,也就是'/',我们需要将配置项的index设置为false或者为''空字符串,如下配置:
devServer: {
// 服务器启动的根路径
contentBase: './dist',
open: true,
proxy: {
index: '',
'/': {
target: 'https://www.dell-lee.com',
changeOrigin: true
...
3. 使用DllPlugin提高打包速度
引入第三方模块的时候,要去使用打包输出的第三方模块的文件进行引入。如果引入了过多的第三方库文件,会使打包的速度降低,如果能够实现在第一次打包的时候就去分析第三方模块的代码,然后再次打包的时候,就根据前面分析的结果进行打包,不用再次分析,提高打包效率。
新建webpack.dll.js。将引入的第三方模块打包到dll文件夹中,library: '[name]',将打包后的第三方模块通过变量的形式暴露到全局中;变量的名字叫vender。
然后利用使用DllPlugin进行分析,name: '[name]',是指要分析的库名,将将分析的结构放在../dll/[name].manifest.json。manifest.json会映射到相关的依赖。
#webpack.dll.js
const path = require('path')
const webpack = require('webpack')
nodule.exports = {
mode: 'production',
entry: {
vendors: ['react', 'react-dom', 'lodash']
},
output: {
filename: '[name].dll.js',
path: path.resolve(__dirname, '../dll'),
library: '[name]'
},
plugins: [
new webpack.DllPlugin({
name: '[name]',
path: path.resolve(__dirname, '../dll/[name].manifest.json')
})
]
}
修改package.json
"scripts":{
"build:dll": "webpack --config ./build/webpack.dll.js",
}
接下来需要安装dd-asset-html-webpack-plugin,将打包后的文件进行挂载到我们打包输出的页面中,作用就是往html页面中去增加静态资源。使用DllReferencePlugin读取vendor-manifest.json文件,查看是否有第三方库,如果有就不需要再引入了。
npm install add-asset-html-webpack-plugin --save
#webpack.common.js
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin')
......
module.exports = {
......
plugins: [
new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, '../dll/vendors.dll.js')
}),
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '../dll/vendors.manifest.json')
})
]
.......
}
执行npm build:dll命令后,在打包,会发现打包速度提高了很多。
我们还可以对第三方库进行拆分。
const plugins = [
new CleanWebpackPlugin(),
new webpack.HotModuleReplacementPlugin(),
new HtmlWebpakcPlugin({
template: './src/index.html'
})
]
const fs = require('fs')
const files = fs.readdirSync(path.resolve(__dirname, './dll'))
files.forEach(file => {
if(/.*\.dll.js/.test(file)) {
plugins.push(
new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, '../dll', file)
}))
}
if(/.*\.manifest.json/.test(file)) {
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '../dll', file)
})
}
})
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin')
......
module.exports = {
......
plugins: plugins
.......
}