现实需求
在vite开发过程中,一些变量可以放在.env(基础公共部分变量).env.dev(开发环境)、.env.production(生产环境)中管理,通常分成开发和生产两个不同的配置文件管理,方便调试和部署。但是在应用部署在若干个不同的运行环境,则需要修改.env.production中的部分变量(如api地址)重新打包会比较麻烦。
怎么样实现不重新打包的前提下修改配置呢?
答:在build打包时将.env.production和.env文件统一打包成额外的配置js文件,在通过外部挂载的方式做成全局变量即可。
文件结构如下:
1、构建脚本的文件夹结构
程序内部调用的文件结构
前提条件
.env文件
VITE_GLOB_APP_TITLE=测试平台
VITE_BASE_URL=/newpath/
VITE_GLOB_APP_SHORT_NAME=GIS_APP
.env.production文件
VITE_API_BASE_URL=/
VITE_LOGIN_API_URL=https://www.baidu.com/login
一、新建打包脚本(build.ts及依赖文件)
脚本目的是在打包目录下生成_app.config.js,效果如下:
_app.config.js结构如下:
window.__PRODUCTION__APP__CONF__ = {
"VITE_GLOB_APP_TITLE": "测试平台",
"VITE_BASE_URL":"/newpath/",
"VITE_GLOB_APP_SHORT_NAME": "APP",
"VITE_API_BASE_URL":"/",
"VITE_LOGIN_API_URL": "https://www.baidu.com/login"
};
Object.freeze(window.__PRODUCTION__APP__CONF__);
Object.defineProperty(window, "__PRODUCTION__APP__CONF__", {
configurable: false,
writable: false,
});
1、build.ts
import { runBuildConfig } from "./buildConf"
import pkg from "../../package.json"
export const runBuild = async () => {
try {
const argvList = process.argv.splice(2)
if (!argvList.includes("disabled-config")) {
runBuildConfig()
}
console.log(`[${pkg.name}] - 构建成功!`)
} catch (error) {
console.log("虚拟构建错误:\n" + error)
process.exit(1)
}
}
runBuild()
2、buildConf.ts文件
/**
* 用于打包时生成额外的配置文件。该文件可以配置一些全局变量,这样就可以直接在外部修改,而无需重新打包
*/
import fs, { writeFileSync } from "fs-extra"
import { getEnvConfig, getRootPath } from "../utils"
import { getConfigFileName } from "../getConfigFileName"
import pkg from "../../package.json"
// 打包脚本的名称
const GLOB_CONFIG_FILE_NAME = "_app.config.js"
// 输出文件的根目录
const OUTPUT_DIR = "dist"
interface CreateConfigParams {
configName: string;
config: any;
configFileName?: string;
}
function createConfig(params: CreateConfigParams) {
const { configName, config, configFileName } = params
try {
const windowConf = `window.${configName}`
// 确保变量不会被修改
const configStr = `${windowConf}=${JSON.stringify(config)};
Object.freeze(${windowConf});
Object.defineProperty(window, "${configName}", {
configurable: false,
writable: false,
});
`.replace(/\s/g, "")
// 拼接新的输出根目录地址
const filePath = `${OUTPUT_DIR}${config.VITE_BASE_URL || "/"}`
// 创建根目录
fs.mkdirp(getRootPath(filePath))
writeFileSync(getRootPath(filePath + configFileName), configStr)
console.log(`✨ [${pkg.name}]` + ` - 配置文件构建成功:`)
console.log(filePath + "\n")
} catch (error) {
console.log("配置文件配置文件打包失败:\n" + error)
}
}
export function runBuildConfig() {
const config = getEnvConfig()
const configFileName = getConfigFileName(config)
createConfig({ config, configName: configFileName, configFileName: GLOB_CONFIG_FILE_NAME })
}
3、getConfigFileName.ts文件
/**
* 获取配置文件变量名
* @param env
*/
export const getConfigFileName = (env: Record<string, any>) => {
return `__PRODUCTION__${env.VITE_GLOB_APP_SHORT_NAME || "__APP"}__CONF__`
.toUpperCase()
.replace(/\s/g, "")
}
4、utils.ts文件
import fs from "fs"
import path from "path"
import dotenv from "dotenv"
export function isDevFn(mode: string): boolean {
return mode === "development"
}
export function isProdFn(mode: string): boolean {
return mode === "production"
}
/**
* 是否生成包预览
*/
export function isReportMode(): boolean {
return process.env.REPORT === "true"
}
/**
* 读取所有环境变量配置文件到process.env
* @param envConf
* @returns
*/
export function wrapperEnv(envConf: any) {
const ret: any = {}
for (const envName of Object.keys(envConf)) {
let realName = envConf[envName].replace(/\\n/g, "\n")
realName = realName === "true" ? true : realName === "false" ? false : realName
if (envName === "VITE_PORT") {
realName = Number(realName)
}
if (envName === "VITE_PROXY" && realName) {
try {
realName = JSON.parse(realName.replace(/'/g, '"'))
} catch (error) {
realName = ""
}
}
ret[envName] = realName
if (typeof realName === "string") {
process.env[envName] = realName
} else if (typeof realName === "object") {
process.env[envName] = JSON.stringify(realName)
}
}
return ret
}
/**
* 获取当前环境下生效的配置文件名
*/
function getConfFiles() {
const script = process.env.npm_lifecycle_script
// eslint-disable-next-line prefer-regex-literals
const reg = new RegExp("--mode ([a-z_\\d]+)")
const result = reg.exec(script as string) as any
if (result) {
const mode = result[1] as string
return [".env", `.env.${mode}`]
}
return [".env", ".env.production"]
}
/**
* 获取以指定前缀开头的环境变量
* @param match prefix
* @param confFiles ext
*/
export function getEnvConfig(match = "VITE_", confFiles = getConfFiles()) {
let envConfig = {}
confFiles.forEach((item) => {
try {
const env = dotenv.parse(fs.readFileSync(path.resolve(process.cwd(), item)))
envConfig = { ...envConfig, ...env }
} catch (e) {
console.error(`解析错误:${item}`, e)
}
})
const reg = new RegExp(`^(${match})`)
Object.keys(envConfig).forEach((key) => {
if (!reg.test(key)) {
Reflect.deleteProperty(envConfig, key)
}
})
return envConfig
}
/**
* 获取用户根目录
* @param dir file path
*/
export function getRootPath(...dir: string[]) {
return path.resolve(process.cwd(), ...dir)
}
二、修改vite.config.ts文件
脚本目的是将生成_app.config.js在index.html文件中添加引用,效果如下:
1、安装 vite-plugin-html
yarn add vite-plugin-html -D
或
npm i vite-plugin-html -D
2、vite.config.ts中引用createHtmlPlugin和package.json,并添加createHtmlPlugin
import createHtmlPlugin from "vite-plugin-html"
import pkg from "./package.json"
const getAppConfigSrc = () => {
return `${ENV.VITE_BASE_URL || "/"}${GLOB_CONFIG_FILE_NAME}?v=${pkg.version}-${new Date().getTime()}`
}
createHtmlPlugin({
minify: isBuild,
inject: {
data: {
title: ""
},
// Embed the generated app.config.js file
tags: isBuild
? [
{
tag: "script",
attrs: {
src: getAppConfigSrc()
}
}
]
: []
}
})
三、代码内部引用
1、env.ts文件
import { getConfigFileName } from "../../../build/getConfigFileName"
import pkg from "../../../package.json"
export function getCommonStoragePrefix() {
const { VITE_GLOB_APP_SHORT_NAME } = getAppEnvConfig()
return `${VITE_GLOB_APP_SHORT_NAME}__${getEnv()}`.toUpperCase()
}
/**
* 根据版本生成缓存键
* @returns
*/
export function getStorageShortName() {
return `${getCommonStoragePrefix()}${`__${pkg.version}`}__`.toUpperCase()
}
export function getAppEnvConfig() {
const ENV_NAME = getConfigFileName(import.meta.env)
// 根据当前运行状态判断取值,如果是开发环境取.env.dev配置,如果是生产环境取_app.config.js引用的全局变量
const ENV = (import.meta.env.DEV ? (import.meta.env as unknown as any) : window[ENV_NAME as any]) as unknown as any
// 此处可以进行过滤判断根据业务需求处理,示例未做任何处理
const { VITE_GLOB_APP_SHORT_NAME } = ENV
if (!/^[a-zA-Z\_]*$/.test(VITE_GLOB_APP_SHORT_NAME)) {
console.log(`VITE_GLOB_APP_SHORT_NAME变量只能是字符/下划线,请在环境变量中修改后重新运行。`)
}
return ENV
}
/**
* @description: Development mode
*/
export const devMode = "development"
/**
* @description: Production mode
*/
export const prodMode = "production"
/**
* @description: Get environment variables
* @returns:
* @example:
*/
export function getEnv(): string {
return import.meta.env.MODE
}
/**
* @description: Is it a development mode
* @returns:
* @example:
*/
export function isDevMode(): boolean {
return import.meta.env.DEV
}
/**
* @description: Is it a production mode
* @returns:
* @example:
*/
export function isProdMode(): boolean {
return import.meta.env.PROD
}
2、业务调用index.ts文件
import { getAppEnvConfig } from "@mars/hooks/setting/env"
export const useGlobSetting = (): Readonly<any> => {
const setting = getAppEnvConfig()
return setting
}
四、修改package.json文件build命令
1、安装cross-env和esno
yarn add cross-env -D
yarn add esno -D
或
npm i cross-env -D
npm i esno -D
1、修改build命令,增加"&& esno ./build/script/postBuild.ts"
"build": "cross-env npm run lint && vite build && esno ./build/script/postBuild.ts",
五、测试命令
在命令行执行下面的语句可以测试生成_app.config.js文件
yarn esno ./build/script/postBuild.ts
以上操作就能生成外部配置并加载了,使用时在开发环境、演示环境、生产环境部署修改_app.config.js中对应的api地址即可!