【前端开发】Vue项目多客户配置自动化方案【二】

背景

在开发面向多学校的Vue项目时,每个学校都需要独立的配置(名称、Logo、背景图、API地址等)。传统的多环境配置方案会产生大量脚本命令,维护成本较高。为此,设计了一套更简洁的单一入口 方案,通过交互式选择 实现配置的动态切换

核心思想

使用一个统一的入口文件,在运行或构建前动态生成配置。用户只需选择目标学校,系统会自动完成所有配置更新并执行相应命令。相比传统的多脚本方案,它更加简洁、直观,特别适合需要频繁切换不同客户配置的项目。

整体架构

详细实现

1. package.json

javascript 复制代码
{
  "scripts": {
    "dev": "node src/config/index.js dev",
    "build": "node src/config/index.js build"
  }
}

2. 环境变量文件配置 (.env.*)

  • .env.development 和 .env.production 保持变量占位符:
bash 复制代码
// .env.development
# 开发环境配置
ENV = 'development'
// .env.production
# 生产环境配置
ENV = 'production'

VUE_APP_BASE_TARGET = ''
VUE_APP_BASE_API = ''

VUE_APP_CLIENT_NAME = ''
VUE_APP_CLIENT_LOGO = ''
VUE_APP_CLIENT_BACKGROUND = ''

设计思路:

  • 使用空值模板,运行时动态填充
  • 保持文件结构一致性
  • 避免硬编码,提高灵活性

3. 学校配置数据 (config/js/schools.js)

javascript 复制代码
// 学校配置选项
const schools = [
  {
    name: 'XX学校',                // 显示名称
    key: 'school1',                // 项目标识符
    logo: '@/assets/logo.png', // 学校Logo路径
    background: '@/assets/school1-bg.png', // 背景图
    https: [                        // 环境配置数组
      {
        key: 'development',        // 开发环境
        target: 'http://xxx:8080', // 代理目标
        base: '/dev-api'           // API基础路径
      },
      {
        key: 'production',         // 生产环境
        target: 'http://xxx:8888',
        base: '/pro-api'
      }
    ]
  },
  // 可扩展更多学校配置
  {
    name: 'XX学校',                // 显示名称
    key: 'school2',                // 项目标识符
    logo: '@/assets/logo.png', // 学校Logo路径
    background: '@/assets/school2-bg.png', // 背景图
    https: [                        // 环境配置数组
      {
        key: 'development',        // 开发环境
        target: 'http://xxx:8080', // 代理目标
        base: '/dev-api'           // API基础路径
      },
      {
        key: 'production',         // 生产环境
        target: 'http://xxx:8888',
        base: '/pro-api'
      }
    ]
  }
]

module.exports = schools

配置特点:

  • 统一管理所有学校配置
  • 支持不同环境的API配置
  • 清晰的JSON结构,易于扩展

4. 配置管理工具 (config/js/utils.js)

javascript 复制代码
const readline = require('readline');
const {execSync} = require('child_process');
const fs = require('fs');
const path = require('path');

class ConfigManager {
    constructor(mode) {
        this.mode = mode;
        this.validateMode();

        this.config = {
            dev: {
                envFile: '.env.development',
                env: 'development',
                command: 'vue-cli-service serve',
                message: '正在启动项目...'
            },
            build: {
                envFile: '.env.production',
                env: 'production',
                command: 'vue-cli-service build',
                message: '正在构建项目...'
            }
        }[this.mode];
    }

    // 显示选择菜单
    showWelcomeMessage(schools) {
        console.log('========================================');
        console.log(`  请选择要使用的学校配置 (${this.mode}模式)  `);
        console.log();
        schools.forEach((school, index) => {
            console.log(`${index + 1}. ${school.name}`);
        });
        console.log('========================================');
        console.log();
    }

    // 获取用户输入
    async getUserInput(schools) {
        return new Promise((resolve, reject) => {
            const rl = readline.createInterface({
                input: process.stdin,
                output: process.stdout
            });

            rl.question('请输入对应数字选择配置: ', (answer) => {
                const choice = parseInt(answer);

                if (isNaN(choice) || choice < 1 || choice > schools.length) {
                    rl.close();
                    reject(new Error('请输入有效的数字选项!'));
                    return;
                }

                const selectedSchool = schools[choice - 1];
                console.log(`\n\x1b[32m已选择:${selectedSchool.name}\x1b[0m`);
                rl.close();
                resolve(selectedSchool);
            });
        });
    }

    // 更新package.json
    updatePackageJson(selectedSchool) {
        try {
            const packageJsonPath = path.join(__dirname, '../../../package.json');
            const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));

            packageJson.name = selectedSchool.key;
            packageJson.description = selectedSchool.name;

            fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
            console.log('\x1b[32m已更新 package.json 配置\x1b[0m');
        } catch (error) {
            throw new Error(`更新 package.json 失败:${error.message}`);
        }
    }

    // 更新环境变量文件
    updateEnvFile(selectedSchool) {
        try {
            const envPath = path.join(__dirname, `../../../${this.config.envFile}`);
            let envContent = fs.readFileSync(envPath, 'utf8');

            // 根据当前模式选择对应的API配置
            const envConfig = selectedSchool.https.find(config =>
                config.key === this.config.env
            ) || selectedSchool.https[0];

            const updates = {
                'VUE_APP_CLIENT_NAME': selectedSchool.name,
                'VUE_APP_CLIENT_LOGO': selectedSchool.logo,
                'VUE_APP_CLIENT_BACKGROUND': selectedSchool.background,
                'VUE_APP_BASE_TARGET': envConfig.target,
                'VUE_APP_BASE_API': envConfig.base
            };

            // 动态替换环境变量
            Object.entries(updates).forEach(([key, value]) => {
                const regex = new RegExp(`^${key} = '.*'`, 'gm');
                envContent = envContent.replace(regex, `${key} = '${value}'`);
            });

            fs.writeFileSync(envPath, envContent);
            console.log(`\x1b[32m已更新 ${this.config.envFile} 配置\x1b[0m\n`);
        } catch (error) {
            throw new Error(`更新 ${this.config.envFile} 失败:${error.message}`);
        }
    }

    // 执行Vue命令
    executeCommand() {
        console.log(`${this.config.message}\n`);

        try {
            execSync(this.config.command, {stdio: 'inherit'});
        } catch (error) {
            throw new Error(`执行失败:${error.message}`);
        }
    }
}

// 辅助工具类
class Logger {
    static success(message) {
        console.log(`\x1b[32m✓ ${message}\x1b[0m`);
    }

    static error(message) {
        console.log(`\x1b[31m✗ ${message}\x1b[0m`);
    }
}

class Validator {
    static validateArgs(args) {
        if (args.length < 3) {
            Logger.error('请指定运行模式');
            Logger.info('用法: node common.js [dev|build]');
            process.exit(1);
        }
    }
}

module.exports = {
    ConfigManager,
    Logger,
    Validator
};

5. 统一入口文件 (config/index.js)

javascript 复制代码
const {ConfigManager, Logger, Validator} = require('./js/utils');
const schools = require('./js/schools');

class Application {
    constructor() {
        this.mode = process.argv[2];
        Validator.validateArgs(process.argv);
        this.configManager = new ConfigManager(this.mode);
    }

    async run() {
        try {
            // 1. 显示欢迎信息
            this.configManager.showWelcomeMessage(schools);

            // 2. 获取用户选择的学校
            const selectedSchool = await this.configManager.getUserInput(schools);

            // 3. 更新配置文件
            this.configManager.updatePackageJson(selectedSchool);
            this.configManager.updateEnvFile(selectedSchool);

            // 4. 执行命令
            this.configManager.executeCommand();

        } catch (error) {
            Logger.error(error.message);
            process.exit(1);
        }
    }
}

// 启动应用
(async () => {
    const app = new Application();
    await app.run();
})();
}

工作流程

项目中使用配置

javascript 复制代码
<template>
  <div class="school-app">
    <!-- 使用动态Logo -->
    <img :src="schoolLogo" alt="学校Logo" class="logo" />
    
    <!-- 显示学校名称 -->
    <h1>{{ schoolName }}</h1>
    
    <!-- 动态背景 -->
    <div class="login-container" :style="backgroundStyle">
      <!-- 登录表单 -->
    </div>
  </div>
</template>

<script>
export default {
  computed: {
    schoolName() {
      return process.env.VUE_APP_CLIENT_NAME;
    },
    
    schoolLogo() {
      // 处理Webpack别名路径
      const logoPath = process.env.VUE_APP_CLIENT_LOGO;
      return require(logoPath.replace('@', '..'));
    },
    
    backgroundStyle() {
      const bgPath = process.env.VUE_APP_CLIENT_BACKGROUND;
      return {
        backgroundImage: `url(${require(bgPath.replace('@', '..'))})`,
        backgroundSize: 'cover',
        backgroundPosition: 'center'
      };
    },
    
    apiBaseUrl() {
      return process.env.VUE_APP_BASE_API;
    }
  },
  
  mounted() {
    // 设置页面标题
    document.title = this.schoolName;
    
    // 配置axios实例
    this.$axios.defaults.baseURL = process.env.VUE_APP_BASE_TARGET;
  }
};
</script>

方案优势

  1. 统一入口:一个入口文件处理所有配置逻辑
  2. 动态配置:运行时生成配置,无需预定义大量环境文件
  3. 用户友好:交互式选择,降低使用门槛
  4. 易于维护:配置集中管理,扩展方便
相关推荐
Mr Xu_2 小时前
深入解析 getBoundingClientRect 与 offsetTop:解决 Vue 平滑滚动偏移误差问题
前端·javascript·vue.js
沛沛老爹2 小时前
从Web到AI:多模态Agent Skills开发实战——JavaScript+Python全栈赋能视觉/语音能力
java·开发语言·javascript·人工智能·python·安全架构
2501_941329722 小时前
气压表智能读数检测:基于YOLOv8的指针与刻度识别实现自动化读数
运维·yolo·自动化
阿里巴啦2 小时前
照片隐私清理工具:基于Taro 4 + Vue 3 + piexifjs开发实践项目
vue.js·照片隐私清除·piexifjs·exif 解析
吃吃喝喝小朋友2 小时前
HTML DOM
前端·javascript·html
HWL56792 小时前
HTML中,<video> 和 <source> 标签
前端·javascript·html
偷心伊普西隆2 小时前
Python EXCEL 半自动化切分数据集
python·自动化·excel
沛沛老爹2 小时前
从Web到AI:多模态Agent图像识别Skills开发实战——JavaScript+Python全栈图像处理方案
java·javascript·图像处理·人工智能·python·rag