一套代码适配多个项目的解决方案1-卡片化

146 阅读4分钟

引言

在现代前端开发中,我们经常面临一个挑战:如何维护一套代码库同时支持多个项目?这些项目可能有不同的品牌风格、功能配置或业务规则。本文将介绍两种高效的前端多项目管理方案,帮助您实现代码复用最大化。

方案一:构建时配置注入(推荐)

实现原理

在构建阶段通过命令行参数指定项目名称,前端代码根据项目名称加载对应的配置对象,实现不同项目的差异化配置。

具体实现

1. 项目配置文件结构

src/
  ├── config/
  │   ├── projects/
  │   │   ├── projectA.ts
  │   │   ├── projectB.ts
  │   │   └── default.ts
  │   └── index.ts
  └── main.ts

2. 项目配置文件示例

// src/config/projects/default.ts
export default {
  themeColor: "#1890ff",
  logo: "/logo-default.png",
  enableAdvancedFeatures: false,
  paymentMethods: ["alipay", "wechat"]
};

// src/config/projects/projectA.ts
export default {
  themeColor: "#ff4d4f",
  logo: "/logo-projectA.png",
  enableAdvancedFeatures: true,
  paymentMethods: ["alipay", "wechat", "paypal"]
};

// src/config/projects/projectB.ts
export default {
  themeColor: "#52c41a",
  logo: "/logo-projectB.png",
  enableAdvancedFeatures: true,
  paymentMethods: ["stripe", "paypal"]
};

3. 注册中心

// src/config/index.ts
import defaultConfig from './projects/default';

// 获取当前项目名称(由构建工具注入)
declare const __PROJECT_NAME__: string;

// 动态加载项目配置
let projectConfig = defaultConfig;

try {
  // 尝试加载指定项目的配置
  const configModule = await import(`./projects/${__PROJECT_NAME__}.ts`);
  projectConfig = {
    ...defaultConfig,
    ...configModule.default
  };
} catch (error) {
  console.warn(`Project config for "${__PROJECT_NAME__}" not found, using default config`);
}

export default projectConfig;

4. 构建脚本配置

Vite 配置示例 (vite.config.js):

import { defineConfig } from 'vite';

// 从命令行参数获取项目名称
const projectName = process.env.PROJECT_NAME || 'default';

export default defineConfig({
  define: {
    '__PROJECT_NAME__': JSON.stringify(projectName) // 注入全局常量
  },
  // 其他配置...
});

Webpack 配置示例 (webpack.config.js):

const webpack = require('webpack');

// 从命令行参数获取项目名称
const projectName = process.env.PROJECT_NAME || 'default';

module.exports = {
  plugins: [
    new webpack.DefinePlugin({
      __PROJECT_NAME__: JSON.stringify(projectName)
    })
  ],
  // 其他配置...
};

5. 使用项目配置

// 在应用中使用配置
import appConfig from '@/config';

// 使用主题色
document.documentElement.style.setProperty('--primary-color', appConfig.themeColor);

// 根据配置启用功能
if (appConfig.enableAdvancedFeatures) {
  initAdvancedFeatures();
}

6. 运行命令

在 package.json 中添加脚本:

{
  "scripts": {
    "dev": "vite --config ./config/vite.config.dev.ts  --",
    "build": "vite build --config ./config/vite.config.prod.ts  --",

  }
}
运行时:
npm run dev -- --PROJECT_NAME  xxx
打包时:
npm run build -- --PROJECT_NAME xxx

方案优势

  1. 编译时优化:未使用的配置代码会被tree-shaking移除
  2. 配置不可变:构建后配置固定,避免运行时意外修改
  3. 高性能:无需网络请求,配置立即可用
  4. 强类型:TypeScript支持提供配置项的智能提示和类型检查

方案二:运行时接口配置

实现原理

通过API从后端获取配置数据,前端根据配置动态渲染界面和功能。

具体实现

1. 配置服务端接口

// Express 示例
const express = require('express');
const app = express();

// 项目配置数据
const projectsConfig = {
  projectA: {
    themeColor: "#ff4d4f",
    logo: "/logo-projectA.png",
    enableAdvancedFeatures: true
  },
  projectB: {
    themeColor: "#52c41a",
    logo: "/logo-projectB.png",
    enableAdvancedFeatures: false
  }
};

app.get('/api/config/:projectId', (req, res) => {
  const projectId = req.params.projectId;
  const config = projectsConfig[projectId] || projectsConfig.default;
  res.json(config);
});

2. 前端配置注册中心

import defaultConfig from './default-config';

let appConfig = defaultConfig;
let configLoaded = false;

export async function loadConfig(projectId: string) {
  try {
    const response = await fetch(`/api/config/${projectId}`);
    const remoteConfig = await response.json();
    
    appConfig = {
      ...defaultConfig,
      ...remoteConfig
    };
    
    configLoaded = true;
    return appConfig;
  } catch (error) {
    console.error('Failed to load config, using default', error);
    return defaultConfig;
  }
}

export function getConfig() {
  if (!configLoaded) {
    console.warn('Config not loaded yet, returning default config');
  }
  return appConfig;
}

方案优势

  1. 配置实时更新:无需重新部署即可更新配置
  2. 集中管理:所有配置集中在后端服务
  3. 动态性强:可根据用户、环境等因素返回不同配置
  4. A/B测试友好:可动态切换不同配置进行测试

两种方案对比

特性构建时配置运行时配置
配置更新时间需要重新构建部署实时生效
性能影响⭐⭐⭐⭐⭐ (无额外请求)⭐⭐ (需要网络请求)
安全性⭐⭐⭐⭐ (配置打包在代码中)⭐⭐⭐ (需保护配置接口)
灵活性⭐⭐ (需重新构建)⭐⭐⭐⭐⭐ (动态调整)
复杂度⭐⭐ (简单)⭐⭐⭐ (需前后端协作)
适用场景稳定项目、品牌定制需要频繁调整配置、多租户系统

高级优化技巧

1. 混合模式

结合两种方案的优势:

// 默认使用构建时配置
import buildTimeConfig from '@/config';

// 运行时覆盖某些配置
if (process.env.RUNTIME_CONFIG_ENABLED) {
  const runtimeConfig = await fetchRuntimeConfig();
  Object.assign(buildTimeConfig, runtimeConfig);
}

2. 配置版本控制

// 构建时生成配置版本号
const configVersion = Date.now();

export default {
  ...config,
  _version: configVersion
};

// 应用启动时检查版本
if (localStorage.getItem('configVersion') !== configVersion) {
  localStorage.clear();
  localStorage.setItem('configVersion', configVersion);
}

3. 配置热更新

// 监听配置变化
const eventSource = new EventSource('/config-updates');

eventSource.onmessage = (event) => {
  const newConfig = JSON.parse(event.data);
  applyNewConfig(newConfig);
};

4. 配置回退机制

try {
  const config = await loadConfig(projectId);
  // 验证配置完整性
  if (!validateConfig(config)) {
    throw new Error('Invalid configuration');
  }
  applyConfig(config);
} catch (error) {
  // 使用本地缓存或默认配置
  const fallback = getCachedConfig() || defaultConfig;
  applyConfig(fallback);
}

完整代码示例

构建时配置方案核心代码

// src/config/index.ts
import defaultConfig from './projects/default';

// 声明由构建工具注入的全局变量
declare const __PROJECT_NAME__: string;

// 配置加载器
export const loadProjectConfig = async () => {
  try {
    const module = await import(
      /* webpackChunkName: "project-config" */
      `./projects/${__PROJECT_NAME__}.ts`
    );
    return {
      ...defaultConfig,
      ...module.default
    };
  } catch (error) {
    console.error(`Project config for "${__PROJECT_NAME__}" not found`, error);
    return defaultConfig;
  }
};

// 全局配置实例
const config = await loadProjectConfig();
export default config;

运行时配置方案核心代码

// src/config/runtime-config.ts
import defaultConfig from './default-config';

// 配置状态管理
let currentConfig = defaultConfig;
let configLoaded = false;
let configListeners: Array<(config: any) => void> = [];

// 加载远程配置
export const loadRemoteConfig = async (projectId: string) => {
  try {
    const response = await fetch(`/api/config/${projectId}?v=${Date.now()}`);
    if (!response.ok) throw new Error('Config response not OK');
    
    const remoteConfig = await response.json();
    
    // 合并配置
    const newConfig = {
      ...defaultConfig,
      ...remoteConfig
    };
    
    // 更新配置
    currentConfig = newConfig;
    configLoaded = true;
    
    // 通知所有监听者
    configListeners.forEach(listener => listener(newConfig));
    
    return newConfig;
  } catch (error) {
    console.error('Failed to load remote config', error);
    return currentConfig;
  }
};

// 获取当前配置
export const getCurrentConfig = () => {
  if (!configLoaded) {
    console.warn('Config not loaded, returning default config');
  }
  return currentConfig;
};

// 注册配置变更监听
export const onConfigChange = (listener: (config: any) => void) => {
  configListeners.push(listener);
  // 立即返回当前配置
  listener(currentConfig);
  
  // 返回取消监听函数
  return () => {
    configListeners = configListeners.filter(l => l !== listener);
  };
};

跨平台适配方案

上述方案不仅适用于Vite,也可在其他构建工具中实现:

Create React App (Webpack)

// config-overrides.js
const { override, addWebpackPlugin } = require('customize-cra');
const webpack = require('webpack');

module.exports = override(
  (config, env) => {
    // 从环境变量获取项目名称
    const projectName = process.env.PROJECT_NAME || 'default';
    
    // 添加DefinePlugin注入变量
    config.plugins.push(
      new webpack.DefinePlugin({
        __PROJECT_NAME__: JSON.stringify(projectName)
      })
    );
    
    return config;
  }
);

Vue CLI (Webpack)

// vue.config.js
module.exports = {
  configureWebpack: {
    plugins: [
      new webpack.DefinePlugin({
        __PROJECT_NAME__: JSON.stringify(process.env.PROJECT_NAME || 'default')
      })
    ]
  }
};

Rollup

// rollup.config.js
import replace from '@rollup/plugin-replace';

const projectName = process.env.PROJECT_NAME || 'default';

export default {
  plugins: [
    replace({
      __PROJECT_NAME__: JSON.stringify(projectName)
    })
  ]
};

总结

本文介绍了两种实现前端多项目管理的方案:

  1. 构建时配置注入:适合配置相对固定、需要高性能的场景

    • 通过构建工具注入项目标识
    • 加载项目特定配置
    • 合并默认配置与项目配置
    • 编译时优化移除未使用代码
  2. 运行时接口配置:适合需要动态调整配置的场景

    • 从后端API获取配置
    • 支持热更新和动态切换
    • 可实现A/B测试等高级功能

两种方案各有优势,您可以根据项目需求选择或组合使用。构建时方案提供了更好的性能和安全性,而运行时方案则提供了更大的灵活性。

最佳实践建议

  • 小型项目或品牌定制项目:优先使用构建时配置
  • 大型SaaS平台或多租户系统:使用运行时配置
  • 关键配置使用构建时注入,次要配置使用运行时加载

通过合理的配置管理,您可以大幅提高代码复用率,降低维护成本,同时保持不同项目间的灵活性和独立性。