Optimize the Webpack packaging process: delete old files after packaging is complete to ensure stable website access

foreword

The idea comes from the deployment method of an vue-cliactual project based on the server to pull the latest code and package it locally on the server.
Three packaging comparisons:

  1. webpackdoes not automatically delete old package files by itself
  2. vue-cliThe output directory will be emptied directly before packaging
  3. viteThe packaging process will not directly delete the output directory. During the packaging process, there may be old packaging files in the dist directory, but they will be overwritten by the new packaging files. Therefore, after packaging is complete, only the latest packaged files will be included in the dist directory, and the old packaged files will be replaced or deleted

Assuming that the packaging takes three minutes, the above three packaging methods will make the website inaccessible within three minutes of packaging. In a production environment, the impact is too great.
In a production environment, in order to ensure the stability and consistency of the website, it is generally recommended to deploy a new package file after the packaging is completed to ensure that the website is always accessible. Therefore, I thought of a solution to manually operate the file system: package the project into a temporary folder, delete the original dist folder after completion, and then rename the temporary folder to dist.
The following is vue-clian example

1. Idea 1, configure webpack

example

The relevant configuration of vue.config.js is as follows:

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')
        },
      ])
    })
  }
}

The role of this configuration is as follows:

  1. Set the output directory to dist-temp:
    • outputDir: The 'dist-temp' configuration item specifies that the packaged output directory is dist-temp.
  2. Optimize after packaging is complete:
    • The plugin in the configureWebpack configuration item inserts a hook function into the Webpack build process.
    • The hook function deletes the dist directory through the rimraf module, and then uses the fs.renameSync method to rename the dist-temp directory to dist.
    • The purpose of this is to change the output directory from dist-temp to dist after the packaging is complete to replace the old packaged files.
  3. Configure the Webpack plugin according to the environment:
    • The method in the chainWebpack configuration item judges whether it is a production environment according to the value of the current environment variable NODE_ENV.
    • If it is a production environment, use the webpack/lib/NormalModuleReplacementPlugin plug-in to replace all module requests matching /dist-temp/ with /dist/, so as to ensure that the new packaging file is correctly referenced.

Generally speaking, the function of this configuration is to change the output directory from dist-temp to dist after the packaging is completed, and ensure that the new packaging file is correctly referenced in the production environment through the plug-in.

Packing by mode

If you want to have both normal packaging and the above packaging methods, you can distinguish them through scripts.
Install and develop dependencies cross-env
npm install -D cross-envOR yarn add -D cross-env
add the packaging command "build-replace", which defines the VUE variable "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 related configuration:

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')
        },
      ])
    })
  }
}

question

If there is a bug of naming folders without rights (insufficient file permissions or occupied by other processes), the temporary folder cannot be deleted. The dist-temp folder needs to be deleted manually, even if it is not deleted, it will be overwritten by the new temporary folder during subsequent packaging.
This bug appears on the terminal of the vscode editor, and it is not clear under other conditions.

full code

view.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",
  },
  ...
}

In this way, the build script can be used for normal packaging, or the build-replace script can be selected for optimized packaging
Since there is a temporary packaging folder named dist-temp, it needs to be added .gitignoreto

node_modules
/dist
/dist-temp

summary

Please debug and test yourself, the vite project or other webpack-based projects are similar

2. Idea 2, script form

After the packaging is complete, use fsthe module to replace the temporary folder with the old folder in a script file, assuming replace.js.

example

Add npm script command:

{
    
    
  ...
  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",
  },
  ...
}

Install and develop dependencies cross-env, compatible with two packaging methods, same as idea 1, also need to configure webpackpackaging output path

const isProdENV = process.env.NODE_ENV === 'production'
const deleteAfterBuild = process.env.VUE_APP_BUILD_MODE === 'replace'

module.exports = {
    
    
  outputDir: deleteAfterBuild ? 'dist-temp' : 'dist',
  // ...
}

summary

The operations of the two schemes are actually the same, the difference is that the latter puts the operation on the file in an independent script file, and executes it after the packaging is completed.

Summarize

The content of this article is only a summary of personal experience. The purpose is to avoid the impact on website access during the construction process. Optimization is only effective in specific application scenarios.
The implementation details of the scheme can be changed according to the project requirements, such as the error handling of "delete dist", "rename dist-temp", backup of old files, etc.
In addition to the file occupation bug encountered in the vscode terminal, there may be other potential bugs, looking forward to a safer and more efficient solution.

Guess you like

Origin blog.csdn.net/ymzhaobth/article/details/131378746