切换至深色主题
Vite构建工具
Vite是下一代前端开发与构建工具,Vite意在提供开箱即用的配置,同时它的插件API和JavaScript API 带来了高度的可扩展性,并有完整的类型支持。
- 官网:https://vitejs.dev/
- 中文文档:https://cn.vitejs.dev/
- GitHub:https://github.com/vitejs/vite
- 插件列表:https://github.com/vitejs/awesome-vite
安装配置
> yarn create vite
# 升级
yarn add vite@latest命令行参数
npm run devvite.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的代码工程构建性能分析器。
- GitHub:https://github.com/btd/rollup-plugin-visualizer
- pnpm i rollup-plugin-visualizer -D
- vite.config.ts
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。
- GitHub:https://github.com/WarrenJones/vite-plugin-require-transform
- vite.config.js
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-commonjsvite-plugin-compression
压缩资源
- GitHub:https://github.com/alloc/vite-plugin-compress
- 默认压缩规则
- 默认情况下,小于 1501 字节的文件不会被压缩,因为 TCP 数据包的MTU是 1500 字节
rollup-plugin-external-globals
将外部导入转换为全局变量,如 Rollup 的output.globals选项。
- GitHub:https://github.com/eight04/rollup-plugin-external-globals
- vite.config.js
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,但现在只支持浏览器环境。
- GitHub:https://www.npmjs.com/package/vite-plugin-externals
- vite.config.js
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 运行时。
- GitHub:https://github.com/vitejs/vite/tree/main/packages/plugin-react
- 功能特征
- 在开发中启用快速刷新
- 使用自动 JSX 运行时
- 避免手动import React输入.jsx和.tsx模块
- react对和react-dom包进行重复数据删除
- 使用自定义 Babel 插件/预设
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.svgindex.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:devVite常见问题与释疑
- 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'
})
}