优化 Webpack 打包流程:打包完成后再删除旧文件,确保网站访问稳定
前言
想法源于某个基于 vue-cli
的实际项目的部署方式是在服务器拉取最新代码,在服务器本地打包。
三种打包对比:
webpack
本身并不会自动删除旧的打包文件vue-cli
在打包前会直接清空输出目录vite
打包过程不会直接删除输出目录,在打包过程中,dist 目录中可能存在旧的打包文件,但它们会被新的打包文件所覆盖。因此,打包完成后,dist 目录中只包含最新的打包文件,旧的打包文件会被替换或删除
假设打包耗时三分钟,以上三种打包方式,在打包的三分钟内,网站都是无法访问的。生产环境下,影响太大。
在生产环境下,为了确保网站的稳定性和一致性,通常建议在打包完成后再部署新的打包文件,以确保网站始终可访问。因此,想到一个手动操作文件系统的方案: 将项目打包到临时文件夹下,完毕后,删除原 dist 文件夹,再将临时文件夹重命名为 dist。
下面以 vue-cli
为例
一、思路一,配置 webpack
示例
vue.config.js 相关配置如下:
const path = require('path')
const rimraf = require('rimraf')
const fs = require('fs')
module.exports = {
outputDir: 'dist-temp',
configureWebpack: {
plugins: [
{
apply: compiler => {
compiler.hooks.done.tap('optimize-build', () => {
rimraf(path.resolve(__dirname, 'dist'), err => {
if (err) {
console.error('Failed to delete dist folder:', err)
} else {
fs.renameSync(path.resolve(__dirname, 'dist-temp'), path.resolve(__dirname, 'dist'))
}
})
})
},
},
],
},
chainWebpack: config => {
config.when(process.env.NODE_ENV === 'production', config => {
config.plugin('optimize-build').use(require('webpack/lib/NormalModuleReplacementPlugin'), [
/(.*)dist-temp(.*)/,
resource => {
resource.request = resource.request.replace(/dist-temp/g, 'dist')
},
])
})
},
}
这段配置的作用如下:
- 设置输出目录为 dist-temp:
- outputDir: 'dist-temp' 配置项指定了打包输出的目录为 dist-temp。
- 在打包完成后进行优化:
- configureWebpack 配置项中的插件会在 Webpack 的构建过程中插入一个钩子函数。
- 钩子函数通过 rimraf 模块删除 dist 目录,然后使用 fs.renameSync 方法将 dist-temp 目录重命名为 dist。
- 这样做的目的是在打包完成后,将输出目录从 dist-temp 改为 dist,以替换旧的打包文件。
- 根据环境配置 Webpack 插件:
- chainWebpack 配置项中的方法根据当前环境变量 NODE_ENV 的值判断是否为生产环境。
- 如果是生产环境,使用 webpack/lib/NormalModuleReplacementPlugin 插件将所有匹配 /dist-temp/ 的模块请求替换成 /dist/,从而确保正确引用新的打包文件。
总体来说,这段配置的作用是在打包完成后,将输出目录从 dist-temp 改为 dist,并通过插件确保在生产环境下正确引用新的打包文件。
分模式打包
想兼具普通打包与上面的打包方式,可通过脚本区分。
安装开发依赖 cross-env
npm install -D cross-env
OR yarn add -D cross-env
增加打包命令"build-replace",命令中定义了 VUE 变量 “VUE_APP_BUILD_MODE”
{
"scripts": {
"dev": "vue-cli-service serve",
"build": "vue-cli-service build",
"build-replace": "cross-env VUE_APP_BUILD_MODE=replace vue-cli-service build"
}
}
vue.config.js 相关配置:
const path = require('path')
const rimraf = require('rimraf')
const fs = require('fs')
const fse = require('fs-extra')
const isProdENV = process.env.NODE_ENV === 'production'
const deleteAfterBuild = process.env.VUE_APP_BUILD_MODE === 'replace'
module.exports = {
outputDir: deleteAfterBuild ? 'dist-temp' : 'dist',
configureWebpack: {
plugins:
isProdENV && deleteAfterBuild
? [
{
apply: compiler => {
compiler.hooks.done.tap('optimize-build', () => {
rimraf(path.resolve(__dirname, 'dist'), err => {
if (err) {
console.error('Failed to delete dist folder:', err)
} else {
try {
// dist-temp 重命名为 dist
fs.renameSync(
path.resolve(__dirname, 'dist-temp'),
path.resolve(__dirname, 'dist')
)
} catch (err) {
// 重命名异常
console.error('Failed to rename file:', err)
// 异常处理:复制 dist-temp 到 dist
fse.copySync(
path.resolve(__dirname, 'dist-temp'),
path.resolve(__dirname, 'dist')
)
}
}
})
})
},
},
]
: [],
},
chainWebpack: config => {
config.when(isProdENV && deleteAfterBuild, config => {
config.plugin('optimize-build').use(require('webpack/lib/NormalModuleReplacementPlugin'), [
/(.*)dist-temp(.*)/,
resource => {
resource.request = resource.request.replace(/dist-temp/g, 'dist')
},
])
})
},
}
问题
如果出现无权重命名文件夹的 bug (文件权限不足或被其它进程占用),临时文件夹无法被删除。需手动删除 dist-temp 文件夹,即使不删除,后续打包时,也会被新的临时文件夹覆盖。
此 bug 在 vscode 编辑器终端上出现,其它条件下不清楚。
完整代码
vue.config.js
const path = require('path')
const rimraf = require('rimraf')
const fs = require('fs')
const fse = require('fs-extra')
const isProdENV = process.env.NODE_ENV === 'production'
const deleteAfterBuild = process.env.VUE_APP_BUILD_MODE === 'replace'
module.exports = {
/**
* 打包优化 (通过 vue环境变量 VUE_APP_BUILD_MODE === 'replace' 开启)
*
* 默认打包过程:删除 dist 文件夹,将项目打包至 dist 文件夹下。打包过程中,网站无法访问
* 优化打包过程:
* - 将项目打包至临时文件夹(dist-temp)下
* - 打包完成后,删除旧版本文件(dist)
* - 临时文件夹重命名为 dist
*/
outputDir: deleteAfterBuild ? 'dist-temp' : 'dist',
configureWebpack: {
plugins:
isProdENV && deleteAfterBuild
? [
{
// 通过 apply 方法注册插件“optimize-build”
apply: compiler => {
// compiler.hooks.done:Webpack 编译器对象的钩子函数,在编译完成后触发回调函数
// tap('optimize-build', () => {}):注册钩子函数“optimize-build”
compiler.hooks.done.tap('optimize-build', () => {
// 删除 dist 文件夹
rimraf(path.resolve(__dirname, 'dist'), err => {
// 删除失败
if (err) {
console.error('Failed to delete dist folder:', err)
} else {
// 删除成功
try {
// dist-temp 重命名为 dist
fs.renameSync(
path.resolve(__dirname, 'dist-temp'),
path.resolve(__dirname, 'dist')
)
} catch (err) {
// 重命名异常
console.error('Failed to rename file:', err)
// 异常处理:复制 dist-temp 到 dist
fse.copySync(
path.resolve(__dirname, 'dist-temp'),
path.resolve(__dirname, 'dist')
)
}
}
})
})
},
},
]
: [],
},
chainWebpack: config => {
config.when(isProdENV && deleteAfterBuild, config => {
// 使用 Webpack 时,通过 NormalModuleReplacementPlugin 插件来替换所有文件中的 dist-temp 字符串为 dist 字符串
// 获取一个 Webpack 配置对象,并使用 plugin 方法添加一个插件,该插件的名称为 optimize-build
config.plugin('optimize-build').use(
// 加载 NormalModuleReplacementPlugin 插件。这个插件可以用于替换模块的请求路径
require('webpack/lib/NormalModuleReplacementPlugin'),
[
/(.*)dist-temp(.*)/, // 匹配所有包含 dist-temp 字符串的模块请求路径
// 在匹配到符合条件的模块请求路径时,将被调用
resource => {
// 当前模块的请求路径
resource.request = resource.request.replace(/dist-temp/g, 'dist')
},
]
)
})
},
}
package.json
{
...
scripts: {
"dev": "vue-cli-service serve",
"build": "vue-cli-service build",
"build-replace": "cross-env VUE_APP_BUILD_MODE=replace vue-cli-service build",
},
...
}
这样,可以通过 build 脚本正常打包,也可以选择 build-replace 脚本进行优化后的打包
由于存在名为 dist-temp 的临时打包文件夹,需要将它添加到 .gitignore
中
node_modules
/dist
/dist-temp
小结
请亲自调试测试,vite 项目或其它基于 webpack 的项目也类似
二、思路二,脚本形式
打包完成后,使用 fs
模块将临时文件夹对旧文件夹的替换操作放在一个脚本文件中,假设为 replace.js。
示例
添加 npm 脚本命令:
{
...
scripts: {
"dev": "vue-cli-service serve",
"build": "vue-cli-service build",
"build-replace": "cross-env VUE_APP_BUILD_MODE=replace vue-cli-service build && node replace.js",
},
...
}
安装开发依赖 cross-env
,兼容两种打包方式,同思路一一样,也需要配置 webpack
打包输出路径
const isProdENV = process.env.NODE_ENV === 'production'
const deleteAfterBuild = process.env.VUE_APP_BUILD_MODE === 'replace'
module.exports = {
outputDir: deleteAfterBuild ? 'dist-temp' : 'dist',
// ...
}
小结
两种方案其实进行的操作是一样的,区别在于,后者将对文件的操作放在独立的脚本文件中,打包完成后执行
总结
本文内容仅为个人经验总结,目的是避免构建过程中对网站访问的影响,优化仅在特定应用场景下有效。
方案实现细节可依据项目需求更改,比如“删除 dist”、“重命名 dist-temp”的错误处理,对旧文件的备份等等。
除了在 vscode 终端内遇到的文件占用 bug,可能还存在其它潜在 bug,期待更安全、高效的方案。
Last updated: