1 如何区分开发环境
目前我们所有的webpack配置信息都是放到一个配置文件中的:webpack.config.js
- 当配置越来越多时,这个文件会变得越来越不容易维护;
- 并且某些配置是在开发环境需要使用的,某些配置是在生成环境需要使用的,当然某些配置是在开发和生成环
境都会使用的; - 所以,我们最好对配置进行划分,方便我们维护和管理;
那么,在启动时如何可以区分不同的配置呢?
新建文件:
方案一:编写两个不同的配置文件,开发和生成时,分别加载不同的配置文件即可;
package.json
{
"scripts": {
"build": "webpack --config ./config/webpack.prod.js",
"serve": "webpack serve --config ./config/webpack.dev.js"
}
}
webpack.dev.js
const isProduction = false;
module.exports = {
// ...
}
webpack.prod.js
const isProduction = true;
module.exports = {
// ...
}
方式二:使用相同的一个入口配置文件,通过设置参数来区分它们
package.json
{
"scripts": {
"build": "webpack --config ./config/webpack.common.js --env production",
"serve": "webpack serve --config ./config/webpack.common.js --env development"
}
}
通过--env xxxx
来区分开发环境
webpack.common.js
module.exports = function (env) {
return {
entry: "src/main.js"
}
}
入口文件解析:
我们之前编写入口文件的规则是这样的:./src/index.js,但是如果我们的配置文件所在的位置变成了 config 目录,
我们是否应该变成 ../src/index.js呢?
- 如果我们这样编写,会发现是报错的,依然要写成 ./src/index.js;
- 这是因为入口文件其实是和另一个属性时有关的 context;
context的作用是用于解析入口(entry point)和加载器(loader):
-
官方说法:默认是当前路径(但是经过我测试,默认应该是webpack的启动目录)
2 环境配置文件分离
现在采用第一节方案二(webpack.common.js)来实现配置文件分离:
webpack.dev.js
const resolveApp = require('./paths');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const isProduction = false;
console.log("加载devConfig配置文件");
module.exports = {
mode: "development",
devServer: {
hot: true,
hotOnly: true,
compress: true,
contentBase: resolveApp("./why"),
watchContentBase: true,
proxy: {
"/why": {
target: "http://localhost:8888",
pathRewrite: {
"^/why": ""
},
secure: false,
changeOrigin: true
}
},
historyApiFallback: {
rewrites: [
{from: /abc/, to: "/index.html"}
]
}
},
plugins: [
// 开发环境
new ReactRefreshWebpackPlugin(),
]
}
webpack.prod.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const isProduction = true;
module.exports = {
mode: "production",
plugins: [
// 生成环境
new CleanWebpackPlugin({}),
]
}
封装一个路径处理函数config/path.js
const path = require('path');
// node中的api,获取项目启动的目录
const appDir = process.cwd();
const resolveApp = (relativePath) => path.resolve(appDir, relativePath);
module.exports = resolveApp;
把公共的配置放到webpack.common.js
里,并合并dev或者prod的配置
const resolveApp = require("./paths");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const VueLoaderPlugin = require("vue-loader/lib/plugin");
const { merge } = require("webpack-merge"); // webpack官方合并插件
const prodConfig = require("./webpack.prod");
const devConfig = require("./webpack.dev");
const commonConfig = {
entry: "./src/index.js",
output: {
filename: "bundle.js",
path: resolveApp("./build"),
},
resolve: {
extensions: [".wasm", ".mjs", ".js", ".json", ".jsx", ".ts", ".vue"],
alias: {
"@": resolveApp("./src"),
pages: resolveApp("./src/pages"),
},
},
module: {
rules: [
{
test: /\.jsx?$/i,
use: "babel-loader",
},
{
test: /\.vue$/i,
use: "vue-loader",
},
{
test: /\.css/i,
use: ["style-loader", "css-loader"],
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: "./index.html",
}),
new VueLoaderPlugin(),
]
};
module.exports = function(env) {
const isProduction = env.production;
process.env.NODE_ENV = isProduction ? "production": "development";
const config = isProduction ? prodConfig : devConfig;
const mergeConfig = merge(commonConfig, config);
return mergeConfig;
};
babel.config.js
分离:
const presets = [
["@babel/preset-env"],
["@babel/preset-react"],
];
const plugins = [];
const isProduction = process.env.NODE_ENV === "production";
// React HMR -> 模块的热替换 必然是在开发时才有效果
if (!isProduction) {
plugins.push(["react-refresh/babel"]);
} else {
}
module.exports = {
presets,
plugins
}
3 代码分离
代码分离(Code Splitting)是webpack一个非常重要的特性:
- 它主要的目的是将代码分离到不同的bundle中,之后我们可以按需加载,或者并行加载这些文件;
- 比如默认情况下,所有的JavaScript代码(业务代码、第三方依赖、暂时没有用到的模块)在首页全部都加载,就会影响首页的加载速度;
- 代码分离可以分出出更小的bundle,以及控制资源加载优先级,提供代码的加载性能;
Webpack中常用的代码分离有三种:
- 入口起点:使用entry配置手动分离代码;
- 防止重复:使用Entry Dependencies或者SplitChunksPlugin去重和分离代码;
- 动态导入:通过模块的内联函数调用来分离代码;
3.1 多入口起点
入口起点的含义非常简单,就是配置多入口。
比如有两个文件src/main.js
、src/index.js
,在webpack.common.js
里配置:
entry: {
main: "./src/main.js",
index: "./src/index.js"
},
output: {
path: resolveApp("./build"),
filename: "[name].bundle.js"
}
3.2 Entry Dependencies(入口依赖)
假如我们的index.js和main.js都依赖两个库:lodash、dayjs
- 如果我们单纯的进行入口分离,那么打包后的两个bunlde都有会有一份lodash和dayjs;
- 事实上我们可以对他们进行共享;
webpack.common.js
entry: {
main: { import: "./src/main.js", dependOn: "shared" },
index: { import: "./src/index.js", dependOn: "shared" },
lodash: "lodash",
// dayjs: "dayjs"
shared: ["lodash", "dayjs"]
}
3.3 SplitChunks
另外一种分包的模式是splitChunk,它是使用SplitChunksPlugin来实现的,用于import文件或者第三方包的时候:
- 因为该插件webpack已经默认安装和集成,所以我们并不需要单独安装和直接使用该插件;
- 只需要提供SplitChunksPlugin相关的配置信息即可;
Webpack提供了SplitChunksPlugin默认的配置,我们也可以手动来修改它的配置:
- 比如默认配置中,chunks仅仅针对于异步(async)请求,我们可以设置为initial(同步)或者all;
optimization: {
splitChunks: {
// async异步导入
// initial同步导入
// all 异步/同步导入
chunks: "all",
}
}
3.3.1 SplitChunks自定义配置
Chunks:
- 默认值是async
- 另一个值是initial,表示对同步的代码进行处理
- all表示对同步和异步代码都进行处理
minSize:
- 拆分包的大小, 至少为minSize;
- 如果一个包拆分出来达不到minSize,那么这个包就不会拆分;
maxSize:
- 将大于maxSize的包,拆分为不小于minSize的包;
minChunks:
- 至少被引入的次数,默认是1
- 如果我们写一个2,但是引入了一次,那么不会被单独拆分;
name:设置拆包的名称
- 可以设置一个名称,也可以设置为false;
- 设置为false后,需要在cacheGroups中设置名称;
cacheGroups:
- 用于对拆分的包就行分组,比如一个lodash在拆分之后,并不会立即打包,而是会等到有没有其他符合规则的包一起来打包;
- test属性:匹配符合规则的包;
- name属性:拆分包的name属性;
- filename属性:拆分包的名称,可以自己使用placeholder属性;
optimization: {
// natural: 使用自然数(不推荐),
// named: 使用包所在目录作为name(在开发环境推荐)
// deterministic: 生成id, 针对相同文件生成的id是不变
// chunkIds: "deterministic",
splitChunks: {
// async异步导入
// initial同步导入
// all 异步/同步导入
chunks: "all",
// 最小尺寸: 如果拆分出来一个, 那么拆分出来的这个包的大小最小为minSize
minSize: 20000,
// 将大于maxSize的包, 拆分成不小于minSize的包
maxSize: 20000,
// minChunks表示引入的包, 至少被导入了几次
minChunks: 1,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
filename: "[id]_vendors.js",
// name: "vendor-chunks.js",
priority: -10 //优先级
},
// bar: {
// test: /bar_/,
// filename: "[id]_bar.js"
// }
default: {
minChunks: 2,
filename: "common_[id].js",
priority: -20
}
}
},
// true/multiple
// single
// object: name
runtimeChunk: {
name: function(entrypoint) {
return `why-${entrypoint.name}`
}
}
}
3.4 动态导入(dynamic import)
另外一个代码拆分的方式是动态导入时,webpack提供了两种实现动态导入的方式:
- 第一种,使用ECMAScript中的 import() 语法来完成,也是目前推荐的方式;
- 第二种,使用webpack遗留的 require.ensure,目前已经不推荐使用;
比如我们有一个模块 bar.js:
- 该模块我们希望在代码运行过程中来加载它(比如判断一个条件成立时加载);
- 因为我们并不确定这个模块中的代码一定会用到,所以最好拆分成一个独立的js文件;
- 这样可以保证不用到该内容时,浏览器不需要加载和处理该文件的js代码;
- 这个时候我们就可以使用动态导入
注意:
- 在webpack中,通过动态导入获取到一个对象;
- 真正导出的内容,在改对象的default属性中,所以我们需要做一个简单的解构;
import('xxx').then(({ default }) => {})
3.4.1 optimization.chunkIds配置
optimization.chunkIds配置用于告知webpack模块的id采用什么算法生成。
有三个比较常见的值:
- natural:按照数字的顺序使用id;
- named:development下的默认值,一个可读的名称的id;
- deterministic:确定性的,在不同的编译中不变的短数字id
- 在webpack4中是没有这个值的;
- 那个时候如果使用natural,那么在一些编译发生变化时,就会有问题;
最佳实践:
- 开发过程中,我们推荐使用named;
- 打包过程中,我们推荐使用deterministic;
3.4.2 动态导入的文件命名
optimization.chunkIds值为deterministic
动态导入的文件命名:
- 因为动态导入通常是一定会打包成独立的文件的,所以并不会再cacheGroups中进行配置;
- 那么它的命名我们通常会在output中,通过 chunkFilename 属性来命名;
output: {
chunkFilename: "[name].[hash:6].chunk.js"
},
但是,你会发现默认情况下我们获取到的 [name] 是和id的名称保持一致的,如果我们希望修改name的值,可以通过magic comments(魔法注释)的方式;
import(/* webpackChunkName: "foo" */"./foo").then(res => {
console.log(res);
});
3.4.3 optimization. runtimeChunk配置
配置runtime相关的代码是否抽取到一个单独的chunk中:
- runtime相关的代码指的是在运行环境中,对模块进行解析、加载、模块信息相关的代码;
- 比如我们的component、bar两个通过import函数相关的代码加载,就是通过runtime代码完成的;
抽离出来后,有利于浏览器缓存的策略:
- 比如我们修改了业务代码(main),那么runtime和component、bar的chunk是不需要重新加载的;
- 比如我们修改了component、bar的代码,那么main中的代码是不需要重新加载的;
设置的值:
- true/multiple:针对每个入口打包一个runtime文件;
- single:打包一个runtime文件;
- 对象:name属性决定runtimeChunk的名称;
runtimeChunk: {
name: function(entrypoint) {
return `why-${entrypoint.name}`
}
}
3.5 代码的懒加载
动态import使用最多的一个场景是懒加载(比如路由懒加载):
- 新建一个
element.js
文件
const element = document.createElement('div');
element.innerHTML = "Hello Element";
export default element;
- 我们可以在一个按钮点击时,加载这个对象;
main.js
const button = document.createElement("button");
button.innerHTML = "加载元素";
button.addEventListener("click", () => {
// prefetch -> 魔法注释(magic comments)
/* webpackPrefetch: true */
/* webpackPreload: true */
import(
/* webpackChunkName: 'element' */
/* webpackPrefetch: true */
"./element"
).then(({default: element}) => {
document.body.appendChild(element);
})
});
document.body.appendChild(button);
3.5.1 Prefetch和Preload
webpack v4.6.0+ 增加了对预获取和预加载的支持。
在声明 import 时,使用下面这些内置指令,来告知浏览器:
- prefetch(预获取):将来某些导航下可能需要的资源
- preload(预加载):当前导航下可能需要资源
与 prefetch 指令相比,preload 指令有许多不同之处:
- preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
- preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。
- preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻。
import(
/* webpackChunkName: 'element' */
/* webpackPrefetch: true */
/* webpackPreload: true */
"./element"
).then(({default: element}) => {
document.body.appendChild(element);
})