zhangsanshi / issue-blog

It's a blog rather than issue
0 stars 0 forks source link

多环境配置管理 #58

Open zhangsanshi opened 5 years ago

zhangsanshi commented 5 years ago

配置管理

随着业务发展,同一份代码被部署到越来越多的环境去了,同时每个环境都有一些个性化设置,导致代码越来越无法维护。

1. 引言

产品说,在 A 环境下希望对按钮 A 隐藏,简单 if (env !== 'A') { // xxx }。随着业务的发展,类似的需求都过来了。在 A、C 环境下隐藏按钮 A,在 (B + a)(a 表示另一个影响因素) 环境下隐藏 C 选项。此时代码里充斥着各种 if。突然来了,一个新的环境 D,要求在 D 环境隐藏 A 按钮、C 选项,整个项目有 10+ 模块,每个模块都需要对代码进行调整,那么工作量是一方面,测试又是另一方面。

2. 解决问题

2.1. 版本0.1

代码里充斥着是下面的代码

// a.js
if (env === 'A' || env === 'C') {
    // hide A button
}

// b.js
if (env === 'A' || env === 'C') {
    // hide A button
}
// 可能有人会采取优化点的写法
if (['A', 'C'].includes(env)) {
    // hide A button
}

可以想象,代码膨胀下去的后果(在多因素的影响下,代码会变得不可维护)

if (['A', 'C', 'E', ...].includes(env)) {
    // hide A button
}

2.2. 版本1.0

此时有人对这些代码有了一些想法,会将代码优化为下面的样式,在不同的地方进行引用。但是此时没有形成全局性的,可能命名写法以及判断逻辑都不一致。

// config.js
const hideA = ['A', 'C'].includes(env);
export default {
    hideA,
};

// a.js
import config from './config.js';
const hideA = config.hideA;
if (hideA) {
    // hide A button
}
// b.js
import config from './config.js';
const hideA = config.hideA;
if (hideA) {
    // hide A button
}

2.3. 版本1.1

有人开始将此类信息往全局进行抽取,保证此类信息由全局维护,同时由于信息在全局,也不怕重复实现的问题。这个版本看起来相对可以用了。

// global.config.js
const hideA = ['A', 'C'].includes(env.AZ);
const hideOptions = ['a'].includes(env.az) && ['B'].includes(env.AZ); // 多因素影响
export default {
    hideA,
};

// a.js
import config from 'global.config.js';
const hideA = config.hideA;
if (hideA) {
    // hide A button
}

2.4. 版本2.0

注意 1.1 版本那个多因素影响,实际项目中,有 5 个因素的影响,以后还有扩展的可能性。那么就意味着 y = f(a, b, c, d, e ...)。不同的因素会影响取值,但是实际中,影响取值的不会太多(5 个中有2,3个)。

现在有一个问题来了,对于一个给定的环境,它的影响因子是限定的,那能很快的说出在这个环境内,哪些东西是隐藏的吗?又或者,希望在项目里的 i、j 模块内,即使在 A 环境下,也展示按钮 A?基于上面的问题,此时 1.1 版本就稍显不足了。

新的设计方案:

首先规定出每个环境(可以是物理,也可以是逻辑,一般来说逻辑会比物理的宽泛一些)自身的五元组成,这部分设计是独立的,可以随时修改,只要环境名以及环境覆盖逻辑不变即可。

// env.name.js
const nameList = {
    base: '*/*/*/*/*',
    '环境A': 'A/a/*/*/*',
    '环境B': 'B/a/*/*/*',
    '环境C': 'B/b/*/*/*', 
}; // 这里既可以映射物理环境还可以映射逻辑上的环境

export default function (path) {
    return nameList.filter(() => {
        // path
    }).sort(() => {
        // 星少的放在前面,星越多,代表信息越全,在后续合并配置的时候,越详细的信息对应的配置会覆盖宽泛的信息对应的配置。
    });
}; // path 即任意环境的五元组成

接着设计一个全局的模块,这时候如果回想需求,那么此时的表达形式和需求是一致的,在环境 A、C 隐藏按钮A

// global/config/base.js
export default {
    hideA: false,
    hideOptions: false,
    ...others,
};

// global/config/环境A.js
// 真正环境A的全局配置:mergeDeep({}, base, 环境A)
export default {
    hideA: true,
};
// global/config/环境C.js
// 真正环境C的全局配置:mergeDeep({}, base, 环境C)
export default {
    hideA: true,
};

还有另一个需求,模块的配置有一定的可能覆盖全局的,即希望在项目里的 i、j 模块内,即使在 A 环境下,也展示按钮 A。

// i/config/base.js
export default {
    // 如果默认的状态下和全局一致,可以不存在此文件
};
// i/config/环境A.js
// 真正环境A的 i 模块配置:mergeDeep({}, base, 环境A全局配置, i模块的base, 环境A的i模块配置)
export default {
    hideA: false,
};

在模块内使用的时候,直接引用模块配置即可

import config from './config.js';
const hideA = config.hideA;
if (hideA) {
    // hide A button
}

2.5. 引申

回头看,版本 2.0 和 版本 1.1 的差距,如果按照 2.0 的想法,不是以变量为主,而是以环境为主。那么改造 1.1 的写法,会发现也是可行的,但是多项目部署代码是隔离的,所以代码会有一定程度的增加,需要通过其他手段调整。

3. 优势

4. 劣势

5. 后续

后续的实施步骤:

产品有一份粗略的配置信息是需要抽取出来的,同时前端结合业务的需求,沉淀了一些配置项可以进行抽取的,后续在业务发展或者产品规划中,也可以明确一下,哪些东西是需要进行配置的(配置不配置对工作量的影响不是过于的大),可以提早做好准备,应对未来的需求。在整个稳定后,就可以将此类信息迁移到后端服务中,由接口进行返回(部分代码需要拿到多个环境的配置,这个会影响后续的实施),后续就可以动态调整配置了。

2.0 版本在陆续改造中,由于之前没有此类概念,所以目前开展的工作还不多,只能简单谈一下遇到的问题。