Skip to content

Vite构建工具 ​

Vite是下一代前端开发与构建工具,Vite意在提供开箱即用的配置,同时它的插件API和JavaScript API 带来了高度的可扩展性,并有完整的类型支持。

安装配置 ​

> yarn create vite


# 升级
yarn add vite@latest

命令行参数 ​

npm run dev

vite.config.js ​

import { fileURLToPath, URL } from "node:url";
import { defineConfig, loadEnv } from 'vite';
import path from 'path';
import vue from '@vitejs/plugin-vue'; // 提供 Vue 3 单文件组件支持。
import vueJsx from '@vitejs/plugin-vue-jsx'; // 提供 Vue 3 JSX 支持(通过 专用的 Babel 转换插件)。
import react from '@vitejs/plugin-react'; // 提供完整的 React 支持。
import legacy from '@vitejs/plugin-legacy'; // 为打包后的文件提供传统浏览器兼容性支持。
import typescript2 from 'rollup-plugin-typescript2'; // 
import image from '@rollup/plugin-image'; //
import vuePlugin from 'rollup-plugin-vue'; // 
import { visualizer } from "rollup-plugin-visualizer"; // 
import ElementPlus from 'unplugin-element-plus/vite'; //

import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'; // 用于生成 svg 雪碧图
import commonjs from 'vite-plugin-commonjs'; // 支持commonJS语法
import checker from 'vite-plugin-checker'; // 

import viteSass from "vite-plugin-sass";


export default defineConfig((params) => {
  // npm run dev:params = { mode: 'development', command: 'serve', ssrBuild: false }
  // npm run build:params = { mode: 'production', command: 'build', ssrBuild: false }
  const { mode, command, ssrBuild } = params;

  // 获取环境变量
	const envDir = path.resolve(__dirname, './env');
  
  // const nodeEnv = loadEnv(mode, process.cwd());
  const nodeEnv = loadEnv(mode, envDir);
  console.info('环境变量:', params, nodeEnv);

  console.log('nodeEnv:', nodeEnv);
  console.log(process.cwd(), process); // '/home/meta/project/vite-example-react-js'
  // process对象:process.pid | PWD | env | pwd() | platform | verisons



  // 根据当前工作目录中的 `mode` 加载 .env 文件
  // 设置第三个参数为 '' 来加载所有环境变量,而不管是否有 `VITE_` 前缀。
  const env = loadEnv(mode, envDir, '');




  const configOptions = {
    envDir,


    // vite 配置环境变量
    define: {
      // __APP_ENV__: env.APP_ENV,

      // 定义全局变量
      __APP_VERSION__: ""
    }





    json: {},





    css: {
      // 指定传递给 CSS 预处理器的选项
      preprocessorOptions: {
				less: {
					math: 'parens-division',
				},

				styl: {
					define: {
						$specialColor: new stylus.nodes.RGBA(51, 197, 255, 1),
					},
				},

        scss: {
          charset: false,
					api: 'modern-compiler', // 或 "modern","legacy"
					importers: [
						// ...
					],

					// 每一段样式内容添加额外的代码
					additionalData: `$injectedColor: orange;`,
        }
      },
			postcss: {
				plugins: [
					{
						postcssPlugin: 'internal:charset-removal',
						AtRule: {
							charset: (atRule) => {
								if (atRule.name === 'charset') {
									atRule.remove();
								}
							}
						}
					}
				]
			}
    },





    resolve: {
      alias: {
        '~/': `${path.resolve(__dirname, 'src')}/`,
        '@': path.resolve(__dirname, 'src'),
        '@assets': path.resolve(__dirname, 'src/assets')
      }
    },





    // 插件列表
    plugins: [
      vue(),
      checker({
        vueTsc: true,
      }),
      vueJsx(),
      react(),
      legacy(),
      typescript2(),
      ...image(),
      vuePlugin(/* options */),
      commonjs(/* options */),

			viteSass()
    ],





    // 开发服务器配置项
    server: {},





    // 生产环境构建项
    build: {
      // yarn add -D terser
      minify: 'terser',
      // 移除console与注释
      terserOptions: {
        drop_console: true,
        drop_debugger: true
      },

      // Rollup 打包配置 https://rollupjs.org/guide/en/#big-list-of-options
      rollupOptions: {
        output: {
					chunkFileNames(chunkInfo) {
						if (chunkInfo.name) {
							const [name, ext] = path.basename(chunkInfo.name).split('.')
							if (name === 'index') {
								// console.log('chunkInfo.name', chunkInfo.exports)
							}
							return 'static/js/[name]-[hash].js'
						}
						return 'static/js/[name]-[hash].js'
					},
					manualChunks(id) { // id为资源路径
            // id1: /Users/apple/Downloads/devpt/erp_fe/node_modules/lodash/_arraySome.js?commonjs-proxy
            // id2: /Users/apple/Downloads/devpt/erp_fe/node_modules/lodash/_cacheHas.js
						console.info(id);
						//大资源包单独抽出s
						if (id.includes('node_modules')) {
							const arr = id.toString().split('node_modules/')[1].split('/')
							switch (arr[0]) {
								case 'element-plus': // UI 库
								case '@element-plus': // 图标
									return '_' + arr[0]
								default:
									return '__vendor'
							}
						}
					},
					entryFileNames: 'static/js/[name]-[hash].js',
					assetFileNames: 'static/[ext]/[name]-[hash].[ext]'
				}
      }
    },





    // 依赖优化选项
    optimizeDeps: {}
  };


	configOptions.plugins.push(ElementPlus());





	// 代码工程构建性能分析器
	if (mode === 'build-test') {
		configOptions.plugins.push(visualizer({
			filename: './dist/report.html',
			open: false,
			emitFile: true,
			gzipSize: true,
			brotliSize: true
		}));
	}


  // devlopment
  if (mode === 'devlopment') {
    configOptions.server.host = "0.0.0.0";
  }


  // production
  if (mode === 'production') {
    //
  }


  // npm run dev
  if (command === 'dev') {
    configOptions.mode = 'devlopment';
  }


  // npm run serve
  if (command === 'serve') {
    configOptions.mode = 'devlopment';
  }


  // npm run build
  if (command === 'build') {
    configOptions.mode = 'production';
  }


  return configOptions;
});

环境变量与package.json ​

为了防止意外地将一些环境变量泄漏到客户端,只有以 VITE_ 为前缀的变量才会暴露给经过 vite 处理的代码。

  • 根目录相关文件: /env/*
  • 环境变量优先级:.env.[mode].local > .env.[mode] > .env.local > .env
  • 根据优先级覆盖相关变量
  • 这里的[mode]名称与package.json中"scripts": {"dev": "vite --mode serve-dev --host"}的--mode声明值相对应,例如.env.serve-dev。
.env                # 所有情况下都会加载
.env.[mode]         # 只在指定模式下加载,development | production
.env.local          # 所有情况下都会加载,但会被 git 忽略
.env.[mode].local   # 只在指定模式下加载,但会被 git 忽略
  • .env
PROJECT_NAME="vite-example-react-js"
  • .env.local
PROJECT_MODE="local"
  • .env.development | .env.development-test
ADMIN_API_BASE_URL="http://192.180.144.80:8080/api/"
  • .env.production | .env.production-test
ADMIN_API_BASE_URL="https://www.admin.com/api/"
  • .env.development.local
MODE_NAME="development"
  • .env.production.local
MODE_NAME="production"
  • package.json
{
  "scripts": {
    "dev": "vite --mode development --host",
		"dev:ts": "vue-tsc --noEmit && vite --mode development --host",
		"build": "vite build --mode production",
		"build:test": "vite build --mode  production-test",
		"build:ts": "vue-tsc --noEmit && vite build --mode production",
		"test": "vite --mode development-test --host",
		"test:ts": "vue-tsc --noEmit && vite --mode development-test --host",
		"preview": "vite preview ",
		"lint": "eslint --ext .js,.jsx,.vue,.ts,.tsx src --fix --ignore-path .gitignore",
		"prepare": "husky install",
		"view": "rollup-plugin-visualizer --help",


		"build:dev": "NODE_OPTIONS='--max-old-space-size=2048' vite build --mode build-dev",
  }
}
  • 业务文件,例如react组件
export default function App() {
  console.log(import.meta.env);

  if (import.meta.env.SSR) {
    //
  } else {
    //
  }

  return (
    <div></div>
  );
}

cross-env自定义命令行变量 ​

> cross-env vite --mode dev
  • package.json
{
  "scripts": {
    //
  }
}
  • vite.config.js读取自定义命令行变量
export default defineConfig((params) => {
	// { mode: 'development', command: 'serve', ssrBuild: false }
	const { mode, command, ssrBuild } = params
	// 获取环境变量
	const envDir = path.resolve(__dirname, './env')
	const nodeEnv = loadEnv(mode, envDir)
	// console.info('\n\n\n环境参数:', params, nodeEnv, '\n\n\n');

  const configOptions = {
    envDir,
    plugins: []
  };

  return configOptions;
});

反向代理配置 ​

注意,配置反向代理需要在具体的业务代码中使用const origin = ref(window.location.origin)来实现同域请求

  • 前端业务请求API接口
<template>
	<div class="test-mock">
		<el-button type="primary" @click="initTest">操作</el-button>
	</div>
</template>

<script lang="ts" setup>
import { ref } from 'vue'
import { ElButton } from 'element-plus'

import { fetchAPI } from '@/core/fetch'
import { get, post } from '@/utils/http.js'

const origin = ref(window.location.origin)

// 初始化依赖数据
const initDependData = async (data = {}) => {
	const result = await getListAPI()
	console.info('result: ', result)

	const detail = await getDetailAPI()
	console.info('detail: ', detail)
}

initDependData()

function initTest() {
	//
}

// 获取列表数据
function getListAPI(data = {}) {
	return get({
		baseURL: origin.value,
		url: 'api/get',
		data,
		bfLoading: false
	})
}

// 接口重命名
function getDetailAPI(data = {}) {
	return get({
		baseURL: origin.value,
		url: '/user/menu/list/',
		data,
		bfLoading: false
	})
}
</script>

<style lang="scss" scoped></style>
  • vite配置
export default defineConfig((params) => {
  const { mode, command, ssrBuild } = params
	const envDir = path.resolve(__dirname, './env')
	const nodeEnv = loadEnv(mode, envDir)

  const configOptions = {}

	/**
	 * 本地开发服务器
	 */
	configOptions.server = {
    hmr: { overlay: false },
    port: 5001,
    open: false,
    cors: true,
		proxy: {
			'/api': 'https://api.ysunlight.com',
			'/user/menu/list/': {
				target: 'https://api.ysunlight.com',
				changeOrigin: true,
				rewrite: (path) => path.replace(/^\/user\/menu\/list\//, '/api/get'),
			}
		}
  }
})
  • public/package-proxy.json
{
  "name": "data",
  "profile": "这个package.json的作用,主要用于提测阶段为node.js脚本提供反向代理的数据服务。",
  "desc": "请注意,目录proxyConfig.xxx[x].list中存在两种类型,如果是字符串,则默认代理前与代理后的路径基本一致。如果是对象,则api_path是代理前路径,pathRewrite是代理后路径",
  "examples": {
    "https://api.ysunlight.com": {
      "author": "王军",
      "path": "",
      "list": ["/api/get", "/api/get/eshen-unique", {"api_path": "/user/menu/list/", "pathRewrite": "/api/get"}]
    }
  },
  "proxyConfig": {
    "https://api.ysunlight.com": {
      "author": "王军",
      "path": "",
      "list": ["/api/get", "/api/get/eshen-unique"]
    },
    "http://42.192.173.202:20003": {
      "author": "刘路浩",
      "path": "/ares/v1",
      "list": ["/stocking/plan/list/"]
    },
    "http://42.192.173.202:20016": {
      "author": "蔡坚森",
      "path": "/ares/v1",
      "list": []
    },
    "http://42.192.173.202:20005": {
      "author": "郑炳委",
      "path": "/ares/v1",
      "list": ["/product/list/"]
    }
  }
}
  • node.js配置
/**
 * 进程捕捉异常与错误
 * 
 */
process.on('uncaughtException', (err) => {
  console.log(`\n\n
    -----------------${new Date()} ${(new Date()).getMilliseconds()}-----------------
      uncaughtException: ${err}
    ------\n\n
  `);
});

const fs = require('fs');

const express = require('express');
const app = express();

// 配置方向代理
const { createProxyMiddleware } = require('http-proxy-middleware');

// 静态资源服务目录
const STATIC_PATH = "../erp_fe/dist";

const baseConfig = require(STATIC_PATH + '/package-proxy.json')

// 反向代理业务
const configs = Object.entries(baseConfig.proxyConfig)

if (configs && Array.isArray(configs) && configs.length) {
  for (let i = 0; i < configs.length; i++) {
    const config = configs[i];

    if (
      !config[0] ||
      !config[1] ||
      !config[1].list ||
      !Array.isArray(config[1].list) ||
      !config[1].list.length
    ) continue;

    // 反向代理域名
    const origin = config[0];

    // 白名单列表
    for (let j = 0; j < config[1].list.length; j++) {
      const apiPath = config[1].list[j];

      // 是否需要重写API路径
      if (apiPath && apiPath.api_path && apiPath.pathRewrite) {
        const proxyOptions = {};

        proxyOptions[apiPath.api_path] = apiPath.pathRewrite

        app.use(
          apiPath.api_path,
          createProxyMiddleware({
            target: origin,
            changeOrigin: true,
            pathRewrite: proxyOptions,
            on: {
              proxyReq: (proxyReq, req, res) => {
                console.info('代理请求: ', proxyReq, req, res)
              },
              proxyRes: (proxyRes, req, res) => {
                console.info('代理响应: ', proxyRes, req, res)
              },
              error: (err, req, res) => {
                /* handle error */
              },
            },
          })
        );
      }

      if (apiPath && typeof apiPath === 'string') {
        const proxyOptions = {};

        proxyOptions[apiPath] = config[1].path + apiPath

        app.use(
          apiPath,
          createProxyMiddleware({
            target: origin,
            changeOrigin: true,
            pathRewrite: proxyOptions,
            on: {
              proxyReq: (proxyReq, req, res) => {
                console.info('代理请求: ', proxyReq, req, res)
              },
              proxyRes: (proxyRes, req, res) => {
                console.info('代理响应: ', proxyRes, req, res)
              },
              error: (err, req, res) => {
                /* handle error */
              },
            },
          })
        );
      }
    }
  }
}

// 刷新问题
const history = require('connect-history-api-fallback');
app.use(history());

// 静态资源服务
const serveStatic = require('serve-static');
app.use(serveStatic(STATIC_PATH));

// 跨域问题
const cors = require('cors');
app.use(cors());


// 路由拦截
app.all('*', (req, res, next) => {
  console.log('路由:', req, res);
  next()
})






/*--------------------------------------------------*/
const port = 5111;
app.listen(port);

console.log(`

··································${new Date()}··································
PORT: ${port}    Server is Running...
····· 
·  
http://localhost:${port}
server is starting ...
`);

/**
 * 异常机制
 */
app.on('error', err => {
  //err.stack
  console.log(`\n\n Express异常错误机制  \n ${err}  \n\n`);
});

rollup-plugin-visualizer插件 ​

rollup-plugin-visualizer插件是一个类似于webpack-bundle-analyzer的代码工程构建性能分析器。

import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig((params) => {
  const { mode, command, ssrBuild } = params;
  const configOptions: Record<string, any> = {
    plugins: []
  };

  // production
  if (mode === 'production') {
    configOptions.plugins.push(visualizer({
      filename: './dist/report.html',
      open: true,
      emitFile: true,
      gzipSize: true,
      brotliSize: true
    }));
  }

  return configOptions;
});
  • package.json
scripts:{
  report: "rimraf dist && cross-env vite build",
}

vite-plugin-require-transform ​

将代码从require语法转换为import。

import { defineConfig } from 'vite';
import requireTransform from 'vite-plugin-require-transform';

export default defineConfig({
  plugins: [
    // passing string type Regular expression
    requireTransform({}),
  ],
});

vite-plugin-commonjs ​

将代码从require语法转换为import。

> pm i vite-plugin-commonjs --save-dev
  • vite.config.js
import { defineConfig } from 'vite';
import commonjs from 'vite-plugin-commonjs';

export default defineConfig({
  plugins: [
    // passing string type Regular expression
    commonjs(),
  ],
});

@originjs/vite-plugin-commonjs ​

// yarn add -D @originjs/vite-plugin-commonjs

vite-plugin-compression ​

压缩资源

rollup-plugin-external-globals ​

将外部导入转换为全局变量,如 Rollup 的output.globals选项。

import { defineConfig } from 'vite';
import externalGlobals from "rollup-plugin-external-globals";

export default defineConfig({
  build: {
    rollupOptions: {
      external: ['react'],
      plugins: [
        externalGlobals({
          react: 'React'
        })
      ]
    }
  }  
});

vite-plugin-externals ​

使用外部库,类似webpack的externals,但现在只支持浏览器环境。

import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    rollupOptions: {
      plugins: [
        viteExternalsPlugin({
          vue: 'Vue',
          react: 'React',
          'react-dom': 'ReactDOM',
          // 支持值链式取值,会转换成 window['React']['lazy']
          lazy: ['React', 'lazy']
        }),
      ]
    }
  }
});

@vitejs/plugin-react ​

在开发中启用快速刷新,使用自动 JSX 运行时。

npm i @vitejs/plugin-react

vite-plugin-checker ​

自动检测TypeScript, ESLint, vue-tsc等。

vite-plugin-ssr ​

vite-plugin-svg-icons ​

svg-icon.vue ​

<template>
	<svg :class="svgClass" >
		<use :xlink:href="iconName" />
	</svg>
</template>

<script lang="ts">
import { computed, defineComponent } from 'vue';
export default defineComponent({
	props: {
		iconClass: {
			type: String,
			required: true
		},
		className: {
			type: String,
			default: ''
		},
		fillC: {
			type: String,
			default: '#fff'
		}
		// color: {
		// 	type: String,
		// 	default: '#889aa4'
		// }
	},
	setup(props) {
		return {
			iconName: computed(() => `#icon-${props.iconClass}`),
			svgClass: computed(() => {
				if (props.className) {
					return `svg-icon ${props.className}`;
				}
				return 'svg-icon';
			}),
			fill: props?.fillC
		};
	}
});
</script>

<style scoped lang="scss">
.sub-el-icon,
.nav-icon {
	display: inline-block;
	font-size: 15px;
	margin-right: 12px;
	position: relative;
}

.svg-icon {
	width: 1.2em;
	height: 1.2em;
	position: relative;
	fill: currentColor !important;
	// stroke: currentColor;
	vertical-align: -2px;
}
</style>

vite.config.js ​

// 第三方资源类库或插件
import { defineConfig, loadEnv } from 'vite';
import viteSvgIcons from 'vite-plugin-svg-icons';

/**
 * 深度自定义配置
 */
export default defineConfig((params) => {
	// { mode: 'development', command: 'serve', ssrBuild: false }
	const { mode, command, ssrBuild } = params;
	// 获取环境变量
	const envDir = path.resolve(__dirname, './env');
	const nodeEnv = loadEnv(mode, envDir);

	const configOptions = {
		plugins: [
			viteSvgIcons({
				// config svg dir that can config multi
				iconDirs: [
					path.resolve(process.cwd(), 'src/icons/common'),
					path.resolve(process.cwd(), 'src/icons/nav-bar'),
					path.resolve(process.cwd(), 'src/icons/side-bar')
				],
				// appoint svg icon using mode
				symbolId: 'icon-[dir]-[name]'
			})
		],
		build: {}
	};

	return configOptions;
});

icons目录 ​

# src/icons

icon1.svg
icon2.svg
icon3.svg
dir/icon1.svg

index.vue ​

<svg-icon icon-class="icon1"></svg-icon>
<svg-icon icon-class="icon2"></svg-icon>
<svg-icon icon-class="icon3"></svg-icon>
<svg-icon icon-class="dir-icon1"></svg-icon>


<script lang="ts" setup>
	import svgIcon from './components/svg-icon.vue';
</script>

自定义Vite业务插件 ​

  • vite.config.js文件
// 第三方资源类库或插件
import { defineConfig, loadEnv } from 'vite'
import path, { resolve } from 'path'

// 全局资源库
import { initVersionFile } from './build/file'


export default defineConfig((params) => {
  const { mode, command, ssrBuild } = params;
	const envDir = path.resolve(__dirname, './env');
	const nodeEnv = loadEnv(mode, envDir);

  const configOptions = {
    envDir,
		base: "/",
    plugins: []
  };

  // 生成版本文件
  configOptions.plugins.push(initVersionFile(JSON.stringify({ version: now })))

  return configOptions
});
  • build/file.ts
/**
 * 生成版本文件
 */
import fs from 'fs'
import path from 'path'

/**
 * 
 * @param options 
  {
    config: configOptions,
    content: ''
  }
 * @returns 
 */
export function initVersionFile(content = '') {
	let config: Record<string, any> = {}

	function configResolved(configResolved = {}) {
		config = configResolved
	}

	function buildStart() {
		const { publicDir } = config || {}
		const path = publicDir + '/version.json'

		console.info('path: ', path)

		if (fs.existsSync(publicDir)) {
			fs.writeFile(path, content, (err) => {
				if (err) throw new Error('生成版本文件出现异常错误!')
			})
			return
		}

		fs.mkdir(publicDir, (err) => {
			if (err) throw new Error('生成版本文件目录出现异常错误!')

			fs.writeFile(path, content, (err) => {
				if (err) throw new Error('生成版本文件出现异常错误!')
			})
		})
	}

	return {
		configResolved,
		buildStart
	}
}

相关问题 ​

  • 在vite.config.js配置build: {sourcemap: true}后,运行npm run huild 出现 FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
NODE_OPTIONS=--max-old-space-size=4096 npm run build
NODE_OPTIONS=--max-old-space-size=12000 npm run build:dev

Vite常见问题与释疑 ​

  • Vue 在vite中运行无法使用外部ip访问
export default defineConfig({
  server: {
    host: "0.0.0.0"
  }
});
  • [vite:css] @charset must precede all other statements

@charset 规则放在第一位,具体的原因与font-family有关,这里可以配置vite.config.js

export default defineConfig({
  css: {
    postcss: {
      plugins: {
        postcssPlugin: 'internal:charset-removal',
        AtRule: {
          charset: (atRule) => {
            if (atRule.name === 'charset') {
              atRule.remove();
            }
          }
        }
      }
    }
  }
});
  • [vite] Internal server error: Cannot read properties of undefined (reading 'length')

  • warning: "@charset" must be the first rule in the file

export default defineConfig((params) => {
	const configOptions = {
		css: {
			preprocessorOptions: {
				//define global scss variable
				scss: {
					charset: false,
					additionalData: `@use "@/styles/variables.scss";`
				}
			},
			postcss: {
				plugins: [
					{
						postcssPlugin: 'internal:charset-removal',
						AtRule: {
							charset: (atRule) => {
								if (atRule.name === 'charset') {
									atRule.remove();
								}
							}
						}
					}
				]
			}
		}
	};

	return configOptions;
});
  • warnings when minifying css: warning: Expected identifier but found "."

前端工程非覆盖式构建部署 ​

基于vite实施的前端工程非覆盖式构建部署解决方案

  • 前端工程目录
├─ dist:                      存放打包后的工程文件
├─ env                        环境变量文件夹
│ ├─ .env                     所有环境都参与构建
│ ├─ .env.local               所有环境都参与构建,但会被git忽略
│ ├─ .env.development         development参与构建
│ ├─ .env.production          production参与构建
│ ├─ .env.development.local   development参与构建,但会被git忽略
│ └─ .env.production.local    production参与构建,但会被git忽略
├─ public:                    [公共资源]
├─ src:                       [存放项目开发内容]
  • .env.build文件
# 前端工程非覆盖式构建发布基础根路径域名
# 访问资源路径: VITE_APP_BASE_DOMAIN + ROOT_DIRNAME(/2023-06-06) + "/static/js"
# http://localhost:5112/erp_fe/dist/static/css/404-25bc9844.css
# http://localhost:5112/erp_fe/dist/static/js/api-7f45a8c2.js
VITE_APP_BUILD_ROOT_DOMAIN = 'https://erp.feierfit.com'
  • .env.build-test文件
# 前端工程非覆盖式构建发布基础根路径域名
# 访问资源路径: VITE_APP_BASE_DOMAIN + ROOT_DIRNAME(/2023-06-06) + "/static/js"
# http://localhost:5112/erp_fe/dist/static/css/404-25bc9844.css
# http://localhost:5112/erp_fe/dist/static/js/api-7f45a8c2.js
VITE_APP_BUILD_ROOT_DOMAIN = 'https://erp.feierfit.com'
VITE_APP_BUILD_ROOT_DOMAIN = 'http://localhost:5112'
  • .env.local文件
# 前端工程非覆盖式构建发布
VITE_APP_BUILD_ROOT_DIRNAME = '/erp_fe/dist/'
  • vite.config.js文件
// 第三方资源类库或插件
import { defineConfig, loadEnv } from 'vite'
import path, { resolve } from 'path'

// 全局资源库
import { initVersionFile } from './build/file'


export default defineConfig((params) => {
  const { mode, command, ssrBuild } = params;
	const envDir = path.resolve(__dirname, './env');
	const nodeEnv = loadEnv(mode, envDir);

  const configOptions = {
    envDir,
		base: "/",
    plugins: []
    build: {
      assetsDir: 'static/assets',
			rollupOptions: {
				output: {
					chunkFileNames(chunkInfo) {
						if (chunkInfo.name) {
							return 'static/js/[name]-[hash].js'
						}
						return 'static/js/[name]-[hash].js'
					},
					manualChunks(id) {
						//大资源包单独抽出 getModuleIds():所有资源,包括node_modules与项目中其他资源
						if (id.includes('node_modules')) {
							const arr = id.toString().split('node_modules/')[1].split('/')
							const chunk = arr[0]

							switch (chunk) {
								case 'cos-js-sdk-v5':
									return 'chunk-cos-js-sdk-v5'
								case 'lodash':
									return 'chunk-lodash'
							}
						}
					},
					entryFileNames: 'static/js/[name]-[hash].js',
					assetFileNames: 'static/[ext]/[name]-[hash].[ext]'
				}
			}
		}
  };


	/**
	 * 前端工程非覆盖式构建发布部署
	 * 应用范围: 排除开发环境、提测环境外的其他环境
	 */
	const {
		VITE_APP_BUILD_ROOT_DOMAIN,
		VITE_APP_BUILD_ROOT_DIRNAME
	} = nodeEnv || {};
	if (
		VITE_APP_BUILD_ROOT_DOMAIN &&
		VITE_APP_BUILD_ROOT_DIRNAME
	) {
		const rootPath = VITE_APP_BUILD_ROOT_DOMAIN + VITE_APP_BUILD_ROOT_DIRNAME;
		configOptions.base = rootPath;
		console.info("前端工程非覆盖式构建发布部署: ", rootPath);

    // 生成版本文件
		configOptions.plugins.push(initVersionFile(JSON.stringify({ version: now })))
	}

  return configOptions
});
  • build/file.ts
/**
 * 生成版本文件
 */
import fs from 'fs'
import path from 'path'

/**
 * 
 * @param options 
  {
    config: configOptions,
    content: ''
  }
 * @returns 
 */
export function initVersionFile(content = '') {
	let config: Record<string, any> = {}

	function configResolved(configResolved = {}) {
		config = configResolved
	}

	function buildStart() {
		const { publicDir } = config || {}
		const path = publicDir + '/version.json'

		console.info('path: ', path)

		if (fs.existsSync(publicDir)) {
			fs.writeFile(path, content, (err) => {
				if (err) throw new Error('生成版本文件出现异常错误!')
			})
			return
		}

		fs.mkdir(publicDir, (err) => {
			if (err) throw new Error('生成版本文件目录出现异常错误!')

			fs.writeFile(path, content, (err) => {
				if (err) throw new Error('生成版本文件出现异常错误!')
			})
		})
	}

	return {
		configResolved,
		buildStart
	}
}
  • public/version.json
{"version":1686121073080}
  • nginx配置

不缓存html与json文件

location ~ .*\.(htm | html | json)?$ {
  expires -1;
}
  • 前端工程监听版本更新
<template>
	<el-config-provider :locale="locale">
		<router-view />

		<!--前端工程非覆盖式构建发布部署-->
		<div v-if="visibleBuildUI" class="com-flex h center show-version-container">
			<el-icon><WarningFilled /></el-icon>
			<p>
				当前系统已经迭代更新到最新版本,建议立即
				<a href="" target="_self" rel="noopener noreferrer">刷新</a>
				体验最新功能!
			</p>
			<div class="com-flex h center">
				<span class="seconds">{{ buildConfig.seconds }}</span>
				s后自动关闭

				<el-icon @click="closeBuild"><Close /></el-icon>
			</div>
		</div>
	</el-config-provider>
</template>

<script setup>
import { ref, reactive, computed, onBeforeMount, onMounted, onBeforeUnmount } from 'vue'
import { ElConfigProvider, ElIcon } from 'element-plus'
import { WarningFilled, Close } from '@element-plus/icons-vue'

import defaultSettings from '@/settings'
import { setToken } from '@/utils/auth'
import { getVersionAPI } from '@/api/project'
import { getStandardDateFormat } from '@/utils/date'

import { globalNetworkCheck, removeNetworkStatusListener } from '@/utils/common'
globalNetworkCheck()
onBeforeUnmount(() => {
	removeNetworkStatusListener()
})

const buildEnvDomain = import.meta.env.VITE_APP_BUILD_ROOT_DOMAIN

const visibleBuildEnv = ref(false)
const visibleTimingEnv = ref(false)
const buildConfig = reactive({
	limit: 120,
	seconds: 120
})

let settings = computed(() => {
	return defaultSettings
})

// 在ERP存在更新的情况下,24小时提示一次
const visibleBuildUI = computed(() => {
	return visibleTimingEnv.value && visibleTimingEnv.value
})

onBeforeMount(() => {
	//set tmp token when setting isNeedLogin false
	if (!settings.value.isNeedLogin) setToken(settings.value.tmpToken)
})

const locale = ElementPlusLocaleZhCn

// 关闭版本迭代更新提示框
function closeBuild() {
	window.sessionStorage.setItem('close-build-time', Date.now())
	visibleTimingEnv.value = false
}

// 自动关闭倒计时
function timingUIHandler() {
	buildConfig.seconds = buildConfig.limit
	visibleTimingEnv.value = true

	const interval = window.setInterval(() => {
		buildConfig.seconds -= 1

		if (buildConfig.seconds <= 0) {
			window.clearInterval(interval)
			closeBuild()
		}
	}, 1000)
}

function doLimitRange() {
	const closeBuildTime = window.sessionStorage.getItem('close-build-time')
	if (!closeBuildTime) return false

	const now = Date.now()
	const distance = now - Number(closeBuildTime)

	// 6个小时内有效
	return distance / 1000 / 3600 < 6
}

// 判断当前本地版本与远程最新版本是否一致
async function versionUpdateWatcher() {
	// 倒计时提示已经开启
	if (visibleTimingEnv.value) return

	// 处于限制时间范围内,不再提示
	const limitRange = doLimitRange()
	if (limitRange) return

	// 获取服务器最新版本信息
	const getData = await getVersionAPI()
	const { version } = getData || {}

	visibleBuildEnv.value = version && version !== __APP_VERSION__

	// 远程版本已经更新为最新版本,本地版本已经滞后
	if (visibleBuildEnv.value) {
		document.getElementById('app').setAttribute('data-app-lastest-version', version)
		timingUIHandler()
	}

	// 如果本地版本与远程版本相同,则清除记录
	if (version && __APP_VERSION__ && version === __APP_VERSION__) {
		window.sessionStorage.removeItem('close-build-time')
	}
}

onMounted(() => {
	// 非前端非覆盖式构建部署环境
	if (!buildEnvDomain || !__APP_VERSION__) return

	// 初始化变量
	visibleBuildEnv.value = false
	visibleTimingEnv.value = false
	window.sessionStorage.removeItem('close-build-time')
	document.getElementById('app').setAttribute('data-app-version', __APP_VERSION__)

	// 一个小时轮询一次
	const timing = 1000 * 60 * 60

	window.setInterval(() => {
		const { hours } = getStandardDateFormat()

		// 每天10点提醒一次
		if (hours >= 10 && hours < 17) {
			versionUpdateWatcher()
		}

		// 重置变量
		if (hours < 10) {
			window.sessionStorage.removeItem('close-build-time')
		}
	}, timing)
})
</script>

<style lang="scss">
@use '@/assets/style/common.scss';
@use '@/assets/style/element-plus.scss';
</style>

<style scoped lang="scss">
.show-version-container {
	z-index: 2;
	position: fixed;
	top: 0;
	left: 50%;
	margin-left: -310px;
	padding: 0 15px;
	width: 620px;
	height: 40px;
	opacity: 1;
	background-color: rgba(239, 245, 254, 1);
	border: 1px solid rgba(58, 134, 255, 0.5);
	border-radius: 2px;

	> .el-icon {
		margin-right: 10px;
		font-size: 20px;
		color: #3a86ff;

		transition-duration: 300ms;
		animation: version-update-watcher 1s infinite;
	}

	@keyframes version-update-watcher {
		0% {
			transform: scale(1);
		}
		50% {
			transform: scale(1.2);
			color: #f00;
		}
		100% {
			transform: scale(1);
		}
	}

	> p {
		flex: 1;
		position: relative;
		font-size: 14px;
		color: rgba(90, 96, 127, 1);

		a {
			color: rgba(24, 144, 255, 1);
			text-decoration: underline;
		}
	}

	> div {
		position: relative;
		font-size: 14px;
		color: rgba(24, 144, 255, 1);

		.seconds {
			font-weight: bold;
		}

		.el-icon {
			margin-left: 10px;
			font-size: 18px;
			color: #b3c0e7;
			cursor: pointer;
			transition-duration: 300ms;

			&:hover {
				transition-duration: 300ms;
				transform: scale(1.2);
				color: #f00;
			}
		}
	}
}
</style>
  • src/api/project.js
// 获取版本号
import { get } from '@/core/request'

/**
 * 警告:此接口必须基于VITE_APP_BUILD_ROOT_DOMAIN才能使用
 * @returns 
 */
export function getVersionAPI() {
	const buildEnvDomain = import.meta.env.VITE_APP_BUILD_ROOT_DOMAIN

	return get({
		// base: buildEnvDomain,
		base: 'https://api.ysunlight.com',
		path: '/oss/version.json'
	})
}

Released under the MIT License.