webpack5学习 --- 环境分离和代码分割
目前我们所有的webpack配置信息都是放到一个配置文件中的:webpack.config.js
当配置越来越多时,这个文件会变得越来越不容易维护;
并且某些配置是在开发环境需要使用的,某些配置是在生成环境需要使用的,当然某些配置是在开发和生成环 境都会使用的
所以,我们最好对配置进行划分,方便我们维护和管理;
区分开发环境
环境: 在项目根目录下存在一个文件夹为config
,用于存放webpack的配置文件
方式1: 编写两个不同的配置文件,开发和生成时,分别加载不同的配置文件即可
package.json
"scripts": {
"dev": "webpack --config ./config/config.dev.js",
"prod": "webpack --config ./config/config.prod.js"
}
方式2:使用相同的一个入口配置文件,通过设置参数来区分它们
package.json
"scripts": {
"dev": "webpack --config ./config/config.common.js --env development",
"prod": "webpack --config ./config/config.common.js --env production"
}
// package.json
// env选项的值如果只设置了key, 默认的value值为true
"start": "webpack serve --env dev", // => { dev: true }
// 如果设置了value值,在解析的时候,会根据=进行划分 [=前后不要加上空格]
"start": "webpack serve --env env=dev", // => {env: 'dev'}
config.common.js
const path = require('path')
module.exports = (env, argv) => {
// development ---> { WEBPACK_BUNDLE: true, WEBPACK_BUILD: true, development: true }
// production ---> { WEBPACK_BUNDLE: true, WEBPACK_BUILD: true, production: true }
console.log(env)
// argv是一个对象,用于存储所有传入的键值对
// 当执行的指令为 webpack serve --mode development --env env=dev
// argv -> { mode: 'development', env: { WEBPACK_SERVE: true, env: 'dev' } }
console.log(argv)
return {
// 这里必须写成 ./ 不能写 ../ 不然无法正确找到入口文件
entry: './src/index.js',
output: {
// 因为当前目录在config下边,所以构建目录需要存放在上一级的目录下
path: path.resolve(__dirname, '../dist'),
filename: 'bundle.js'
}
}
}
context
我们之前编写入口文件的规则是这样的:./src/index.js
,但是如果我们的配置文件所在的位置变成了 config 目录, 我们是否应该变成 ../src/index.js
呢? 显然是不可以的
这是因为入口文件其实是和另一个属性时有关的 context
context的作用是用于解析入口(entry point)和加载器(loader)
const path = require('path')
module.exports = env => {
return {
// context的值 被要求是一个绝对路径
// 实际入口文件路径为 path.resolve(context, entry)
// 即entry的相对路径其实是相对于context所设置的那个路径的
context: path.resolve(__dirname, './'),
// 因为此时context被设置成了当前路径,也就是config目录
// 所以此时的路径是上一层的src下的index.js
entry: '../src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
}
}
}
默认的context的值为 webpack对应命令行执行时所处的路径
# 所以 context的默认路径是项目的根目录
webpack --config ./config/config.common.js --env development
因此,默认情况下,entry的路径应该被设置为./src/index.js
,而不是../src/index.js
配置文件分离
当我们设置了mode的时候,webpack会自动将我们设置的mode值,设置到process.env.NODE_ENV
中
例如mode: 'development
===> process.env.NODE_ENV
的值就是development
path.js
const path = require('path')
// node中 process.cwd() 方法可以输出当前node进程的执行目录
// 这里的node进程开启是在项目根目录下开启的(pacakge.json中启动)
// 所以process.cwd() 的结果就是项目的根目录
module.exports = (relativePath) => path.resolve(process.cwd(), relativePath)
config.common.js
const path = require('path')
// webpack-merge这个库可以帮助我们进行配置文件的相互合并
const { merge } = require('webpack-merge')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const prodConfig = require('./config.prod')
const devConfig = require('./config.dev')
const resolvePath = require('../utils/path')
module.exports = env => {
// 这里设置process.env.NODE_ENV的作用是 提前设置全局环境值
// 因为此时还没有设置mode的具体值,所以需要手动进行设置
// tips:如果设置process.env.NODE_ENV的某个属性值为undefined的时候
// 再取出process.env.NODE_ENV中对应的属性值的时候,结果为undefined,但是类型是字符串类型,不是undefined类型
process.env.NODE_ENV = env.production ? 'production' : 'development'
const commonConfig = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, '../dist'),
filename: 'bundle.js'
},
devServer: {
hot: true
},
plugins: [
new HtmlWebpackPlugin({
template: resolvePath('./public/index.html')
})
]
}
return env === 'production' ? merge(commonConfig, prodConfig) : merge(commonConfig, devConfig)
}
config.dev.js
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')
module.exports = {
mode: 'development',
plugins: [
new ReactRefreshWebpackPlugin()
]
}
config.prod.js
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
mode: 'production',
plugins: [
new CleanWebpackPlugin()
]
}
babel.config.js
const presets = [
'@babel/preset-env'
]
const plugins = []
// process.env.NODE_ENV 这里可以取到值是因为在common.config.js中进行了环境变量的设置, 而不是因为设置了mode的值后webpack帮助我们进行设置的
// 因为babel设置是位于config.common.js,此时还没有设置具体的mode的值
if (process.env.NODE_ENV === 'development') {
plugins.push('react-refresh/babel')
}
module.exports = {
presets,
plugins
}
package.json
"scripts": {
"dev": "webpack serve --config ./config/config.common.js --env development",
"prod": "webpack --config ./config/config.common.js --env production"
}
代码分离
代码分离(Code Splitting)是webpack一个非常重要的特性:
默认情况下,所有的JavaScript代码(业务代码、第三方依赖、暂时没有用到的模块)在首页全部都加载, 就会影响首页的加载速度;
主要的目的是将代码分离到不同的bundle中,之后我们可以按需加载,或者并行加载这些文件
代码分离可以分出更小的bundle,以及控制资源加载优先级,提供代码的加载性能
Webpack中常用的代码分离有三种:
-
入口起点:使用entry配置手动分离代码
-
抽离重复模块:使用Entry Dependencies或者SplitChunksPlugin去重和分离代码
-
动态导入:通过模块的内联函数调用来分离代码;
入口起点
入口起点的含义非常简单,就是配置多入口, 从而得到多个输出的bundle文件
module.exports = {
entry: {
main: './src/main.js',
index: './src/index.js'
},
output: {
path: path.resolve(__dirname, '../dist'),
// 此时name的取值就是entry的key值
// 打包后会形成 main.bundle.js 和 index.bundle.js
filename: '[name].bundle.js'
}
}
抽离重复模块
Entry Dependencies(入口依赖)
假如我们的index.js和main.js都依赖两个库:lodash、dayjs
如果我们单纯的进行入口分离,那么打包后的两个bunlde都有会有一份lodash和dayjs
事实上我们可以对他们共同的依赖进行抽取
entry: {
main: {
import: './src/main.js',
// main.js 需要提取出来的依赖,值为字符串(值只能是字符串)
dependOn: 'lodash'
},
index: {
import: './src/index.js',
dependOn: 'lodash'
},
// 抽离出来的公共模块
lodash: 'lodash',
dayjs: 'dayjs'
}
如果有多个需要共享的模块存在,可以将多个模块抽离为一个数组后子再一并进行使用
entry: {
main: {
import: './src/main.js',
dependOn: 'shared'
},
index: {
import: './src/index.js',
dependOn: 'shared'
},
// 多个需要被公用的模块组成的数组
shared: ["lodash", "dayjs"]
}
SplitChunks
另外一种分包的模式是splitChunk,它是使用SplitChunksPlugin来实现的:
因为该插件webpack已经默认安装和集成,所以我们并不需要单独安装和直接使用该插件,只需要提供SplitChunksPlugin相关的配置信息即可
optimization: {
splitChunks: {
// chunks 可以设置的值有三个
// 1. async 对异步(async)引入的包进行分包操作 默认值
// 2. inital 对同步引入的包进行分包操作
// 3. all 对所有引入的包全部进行分包操作
chunks: 'all'
// 默认情况下,所有被抽取出去的包都会合并到[id].bundle.js中
}
}
splitChunks
还可以有更多的配资选项
optimization: {
splitChunks: {
chunks: 'all',
// 默认值是20000Byte,表示大于这个大小的引入文件都需要抽离出来
minSize: 20000,
// 表示的是大于多少字节的包需要进行二次拆分,拆分为不小于minSize的包
// 多数情况下,如果设置maxSize的值的时候,minSize和maxSize的值一般是一致的
maxSize: 20000,
// 某一个包引入了多少次就需要被抽离出来
minChunks: 1,
// cacheGroups的含义是 所有的模块输出,会存放在缓存中,最后一起执行对应的操作
// 在这个属性里面可以自己自定义的代码分割配置
// cacheGroups的优先级小于minSize和maxSize,所以当两种冲突的时候,cacheGroup中的配置会默认失效
cacheGroups: {
// key可以任意取,在这边只是一个占位符
// value是一个配置对象
vendor: {
// 正则,用以匹配对应的模块路径
test: /[\\/]node_modules[\\/]/,
// 输出文件名 输出文件会以 输出文件名-hash值.js的形式输出
// name: "vender",
// filename 输出文件名,和name不同的是,filename中可以使用placeholder
filename: 'vendor_[id].js',
// 优先级 在这个配置中约定俗称,一般设置为负数
priority: -10
},
default: {
minChunks: 2,
filename: "common_[id].js",
priority: -20
}
}
}
}
多数情况下,同步的代码一般会被拆分成4个文件
文件名 | 说明 |
---|---|
main.bundle.js | 主代码文件 |
runtime.bundle.js | webpack的辅助函数抽取文件 |
vendor.bundle.js | 所有第三方包所在的文件 |
common.bundle.js | 如果存在多入口的时候,所有重复引入的模块,当引用次数超过一定次数的时候时候,会抽取到的一个文件 |
动态导入
当某一个模块我们希望在代码运行过程中来加载它(比如判断一个条件成立时加载)
我们并不确定这个模块中的代码一定会用到,所以最好拆分成一个独立的js文件
这样可以保证不用到该内容时,浏览器不需要加载和处理该文件的js代码
这个时候我们就可以使用动态导入
只要是异步导入的代码, webpack都会进行代码分离, 无论这个文件的大小
即使webpack在设置的时候,将chunks设置为了inital,webpack的其它默认配置项也会将动态导入的模块抽离出来,形成一个独立的js文件
import('./main').then(res => console.log(res))
动态导入的文件命名
因为动态导入通常是一定会打包成独立的文件的,默认情况下这些异步模块的命名规则和output.filename的命令规则是一致的
但是我们通常希望异步的模块取名叫做xxx.chunk.js
,此时,我们可以通过 output.chunkFilename 属性来命名
output: {
path: path.resolve(__dirname, '../dist'),
// 如果是同步模块的时候,一般命名为bundle
filename: '[name].bundle.js',
// 如果是动态引入,一般命名为chunk
chunkFilename: '[name].chunk.js'
}
你会发现默认情况下我们获取到的 [name] 是和id的名称保持一致的
如果我们希望修改name的值,可以通过magic comments(魔法注释)的方式
import(/* webpackChunkName: 'foo' */'./foo').then(res => console.log(res))
此时,打包出来的模块名就会是foo.chunk.js
去除comments的抽离
在production模式下,打包后的代码,默认会将一些comments信息提取出来,形成对应的txt文件
const TerserPlugin = require('terser-webpack-plugin')
optimization: {
// 压缩配置
minimizer: [
new TerserPlugin({
// 关闭comments的抽离
extractComments: false,
})
]
}
chunkIds
optimization.chunkIds配置用于告知webpack模块的id采用什么算法生成
值 | 说明 |
---|---|
natural | 按照数字的顺序使用id如2.bundle.js, 3.bundle.js不推荐,因为如果移除某一个模块的时候,每个模块对应的自然数会发生改变不利于浏览器的缓存操作 |
named | development下的默认值,一个可读的名称的id;(开发过程中推荐使用) |
deterministic | production下的默认值,表示确定性的,根据内部算法生成在不同的编译中不变的短数字id(打包过程中推荐使用) |
转载自:https://juejin.cn/post/6994251135540264996