同一套前端与后端,如何在不改代码的前提下,让「我的」页面多一个入口、少一个提示,或换一套文案?还在逐个调后端接口实现吗?本文给你提供一个新方案。
一、开发痛点
多地区、多环境部署时,常见诉求是:
- 同一套前端包:如果打多份包,维护成本高、易出错,所以需要共用前端包;
- 同一套后端服务:共用后端包,但可以根据配置不同,实现差异化;
- 行为差异:例如「我的」页是否展示「用户管理」「联系电话」「操作手册」等,都应可配置。
因此,把「差异」收敛到配置里,用「通用配置 + 地区/环境覆盖」的方式,实现改配置即切换前端行为,是一个比较直接、可维护的做法。
二、整体思路:三层配置 + 一条接口
核心就三件事:
- 后端:用 common → 地区 diff → override 三层 properties 决定「最终配置」;
override优先级最高,改它就相当于「直接改当前环境的生效配置」。 - 后端:把最终配置通过 一条「拉取全部配置」的接口 暴露出去(如
GET /configs/fetch/all)。 - 前端:首屏或关键页调一次该接口,把返回的
global、user等写入 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.global、res.module、res.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 用在哪里:页面里的「配置获取方法」
页面通过 computed 或 getters 使用配置,保证展示和逻辑都跟着「当前生效配置」走。例如「我的」页:
computed: {
globalConfigs: vm => vm.$configsStore.getters.globalConfigs() || {},
// ...
}
模板中直接用 globalConfigs / userConfigs 控制显隐和文案:
<view v-if="userConfigs.xxx" class="xxx">...</view>
要点:所有「按省/按环境要变的」内容,都来自 globalConfigs / userConfigs 等,而这些又来自接口;接口数据又由后端的 common + diff + override 决定。因此,直接改 override,就能切换这些前端行为。
六、几个注意点
-
新增或修改一项「可配置行为」时
- 后端:在对应 Helper 里增加
@Value("${xxx}")及 getter,并在 common 或 diff 里给默认值。 - 需要仅当前环境覆盖时:在
override.properties里加一行即可。
- 后端:在对应 Helper 里增加
-
只做「前端配置切换」、不动代码
- 找到控制该行为的 key(如
module.showPhoneInput)。 - 在 override.properties 里写上同名 key 和新值,重启或刷新后前端重新拉配置即可生效。
- 找到控制该行为的 key(如
-
配置 key 与前端用到的字段名
- 后端 JSON 序列化时,Java Bean 的 getter 会变成驼峰字段名。
- 前端 配置要与后端 getter 命名一致;
-
override 的放置与安全
- override 不提交到仓库(如放入 .gitignore),由部署或环境在机器上单独放置,避免把环境相关配置提交进版本库。
七、总结:
这样即可实现动态切换前端配置,只需修改override,重启服务器即可。
后续还可以考虑把配置做成可视化面板,方便维护及运维操作。