动态实现前端配置切换方案

6 阅读4分钟

同一套前端与后端,如何在不改代码的前提下,让「我的」页面多一个入口、少一个提示,或换一套文案?还在逐个调后端接口实现吗?本文给你提供一个新方案。

一、开发痛点

多地区、多环境部署时,常见诉求是:

  • 同一套前端包:如果打多份包,维护成本高、易出错,所以需要共用前端包;
  • 同一套后端服务:共用后端包,但可以根据配置不同,实现差异化;
  • 行为差异:例如「我的」页是否展示「用户管理」「联系电话」「操作手册」等,都应可配置。

因此,把「差异」收敛到配置里,用「通用配置 + 地区/环境覆盖」的方式,实现改配置即切换前端行为,是一个比较直接、可维护的做法。


二、整体思路:三层配置 + 一条接口

核心就三件事:

  1. 后端:用 common → 地区 diff → override 三层 properties 决定「最终配置」;override 优先级最高,改它就相当于「直接改当前环境的生效配置」。
  2. 后端:把最终配置通过 一条「拉取全部配置」的接口 暴露出去(如 GET /configs/fetch/all)。
  3. 前端:首屏或关键页调一次该接口,把返回的 globaluser 等写入 Vuex;各页面用 configsStore 的 getters 读配置,控制展示与逻辑。

这样,切换前端行为 = 服务器改后端生效配置(尤其是 override),无需动前端代码或重新发包。


三、后端(BFF):配置从哪里来、谁优先

3.1 配置加载顺序(谁覆盖谁)

后端通过 Spring 的 PropertyPlaceholderConfigurer顺序加载多组 properties,后加载的同名 key 会覆盖先加载的:

<property name="locations">
    <list>
        <value>classpath:/spring/diff/common/*.properties</value>
        <value>classpath:/spring/diff/*.properties</value>
        <value>classpath:override.properties</value>
    </list>
</property>

含义可以理解为:

  • common:通用默认值(所有地区/环境共用)。
  • diff:按地区或模块拆分的差异化配置(如 diff/anhui/diff/jiangsu/ 等),部署时通过 classpath 或 profile 决定加载哪一套。
  • override优先级最高,一般放在当前部署目录当前环境专属位置,用于:
    • 临时覆盖某个 key,做联调/演示;
    • 或直接作为「当前环境的最终配置」,实现「改 override 即切换前端配置」。

因此:直接改 override.properties,就能在不动 common/diff 的前提下,改变当前环境的前端行为。

3.2 配置怎么写(以某省 diff 为例)

地区或模块的差异写在 diff 目录下diff-*.properties 里,例如某省:

diff-global.properties(全局类)

# 版本与名称
global.copyright=xxx
global.app.name=xxx

# 首页功能模块(JSON,前端自己书写)
global.wechat.modules=[\
  ${global.module.user},\
  ${global.module.search}\
]

# 差异化配置
global.home.link.show=true
global.wechat.smrz=[{"name":"人脸实名","img":"xxx","value":"1"}]

diff-sbf.properties(业务类)

# 业务开关与文案
module.showPhoneInput=false
module.successTips=签订成功。

书写要点:

  • key 与后端 Helper 的 @Value 一致:如 global.app.name,后端用 @Value("${...}") 注入,key 必须匹配。
  • override 中只写要改的项:例如在 override.properties 里只加几行,就能把「应用名称」「某开关」等改成当前环境想要的值,无需改 common/diff。

四、后端(BFF):配置如何变成接口数据

4.1 从 properties 到 Bean(@Value 注入)

后端按「领域」拆成多个 Helper,每个字段用 Spring 的 @Value 从已加载的 properties 里取值(此时已应用 common + diff + override 的覆盖关系):

@Component
public class GlobalDifferentHelper {
    @Value("${global.copyright}")
    private String copyright;
    @Value("${global.app.name}")
    private String appName;
    @Value("${global.wechat.modules}")
    private String wechatModules;
    // ... 其他 global.xxx
}
@Component
public class ModuleDifferentHelper {
    @Value("${module.showPhoneInput}")
    private boolean showPhoneInput;
    // ... 其他 module / diff 项
}

这些 Helper 由 Spring 管理,读到的已经是「合并 + 覆盖」后的最终值

4.2 统一出口:DifferentHelper + 配置接口

所有 Helper 被汇总到一个「总配置」Bean 里,并用 @JsonProperty 指定序列化后的 key,方便前端直接用 res.globalres.moduleres.user 使用:

@Component
public class DifferentHelper {
    @Autowired
    @JsonProperty(value = "global")
    private GlobalDifferentHelper globalDifferentHelper;

    @Autowired
    @JsonProperty(value = "user")
    private UserDifferentHelper userDifferentHelper;
    
    // ....
}

配置接口直接返回这个总 Bean,序列化后即前端需要的结构:

@RestController
@RequestMapping("/configs")
public class MobileConfigsController {
    @Autowired
    private MobileConfigsFacade mobileConfigsFacade;

    @RequestMapping("/fetch/all")
    public MobileResponse<Object> fetchAll() {
        return MobileResponseUtil.procSuccessResult(mobileConfigsFacade.fetchAll());
    }
}
public DifferentHelper fetchAll() {
    return differentHelper;  // 即上面的 DifferentHelper Bean
}

因此:前端拿到的 JSON 里,已经包含 global、module、user 等对象,且其内容完全由当前环境的 properties(含 override)决定。


五、前端:配置如何被拉取与使用

5.1 拉取与落库(Vuex)

前端在入口页(如首页)或需要用到配置的页面,先判断「是否已有配置」,没有则请求一次「拉取全部配置」接口,并把返回结果整体写入 Vuex:

// 示例:某入口页 methods
async fetchAllConfigs() {
  if (!this.$configsStore.getters.isNotEmpty()) {
    return this.$api.configs.fetchAll().then(res => {
      this.$configsStore.commit('setConfigs', res)
    })
  }
}

这里 res 即接口返回的 { global: {...}, module: {...}, user: {...}, message: {...}, ... }(具体字段以实际接口为准)。

5.2 存什么:configsStore 的结构

Vuex 的 configs 模块只存「一整份配置对象」,getters 按领域拆成方便页面使用的只读视图:

const configsStore = new Vuex.Store({
  state: {
    configs: {}
  },
  getters: {
    isNotEmpty: state => () => state.configs && Object.keys(state.configs).length !== 0,
    globalConfigs: state => () => state.configs.global,
    userConfigs: state => () => state.configs.user,
    // ...
  },
  mutations: {
    setConfigs: (state, configs) => {
      state.configs = Object.assign({}, state.configs, configs)
    }
  }
})

这样,改 override 后,只要前端再次请求一次 /configs/fetch/all(或刷新/重进应用触发拉取),拿到的就是新配置,无需改前端代码。

5.3 用在哪里:页面里的「配置获取方法」

页面通过 computedgetters 使用配置,保证展示和逻辑都跟着「当前生效配置」走。例如「我的」页:

computed: {
  globalConfigs: vm => vm.$configsStore.getters.globalConfigs() || {},
  // ...
}

模板中直接用 globalConfigs / userConfigs 控制显隐和文案:

<view v-if="userConfigs.xxx" class="xxx">...</view>

要点:所有「按省/按环境要变的」内容,都来自 globalConfigs / userConfigs 等,而这些又来自接口;接口数据又由后端的 common + diff + override 决定。因此,直接改 override,就能切换这些前端行为。


六、几个注意点

  1. 新增或修改一项「可配置行为」时

    • 后端:在对应 Helper 里增加 @Value("${xxx}") 及 getter,并在 common 或 diff 里给默认值。
    • 需要仅当前环境覆盖时:在 override.properties 里加一行即可。
  2. 只做「前端配置切换」、不动代码

    • 找到控制该行为的 key(如 module.showPhoneInput)。
    • override.properties 里写上同名 key 和新值,重启或刷新后前端重新拉配置即可生效。
  3. 配置 key 与前端用到的字段名

    • 后端 JSON 序列化时,Java Bean 的 getter 会变成驼峰字段名。
    • 前端 配置要与后端 getter 命名一致;
  4. override 的放置与安全

    • override 不提交到仓库(如放入 .gitignore),由部署或环境在机器上单独放置,避免把环境相关配置提交进版本库。

七、总结:

这样即可实现动态切换前端配置,只需修改override,重启服务器即可。
后续还可以考虑把配置做成可视化面板,方便维护及运维操作。