如何设计配置文件?

配置能简化项目复杂度,最经典的例子就是webpack。
以webpack 的watch 举例,一行代码足以:

{
  watch: true,
  // 其他选项...
} 

如果使用Webpack Node API, 代码臃肿很多

const webpack = require("webpack");
const compiler = webpack({});

const watching = compiler.watch({},
   (err, stats) => {
    // 回调处理   
});

配置可以隐藏细节,暴露我们关心的参数,分而治之。 另一面,配置灵活性差,无法定制处理,需要学习成本。如果设计配置文件?兼具简明和灵活性,降低学习成本可通过如下方法:

  1. 开箱即用
  2. 使用中间件
  3. 利用函数,“计算”属性
  4. 最小化配置
  5. 提高可读性

1. 开箱即用

最好的配置是不用配置。 所谓开箱即用,你不用修改配置,默认配置足以支持程序的运行。所以请给你的配置准备份默认配置吧,尽量为所有选项提供默认值, 这样有几个好处:

  1. 对于特殊配置,只需关注特殊点,隐藏共有的细节
  2. 修改默认配置,即可影响所有配置,维护方便
  3. 默认配置可作为配置样本,作为新人学习、自动化测试的依据

1.1 实现默认配置

虽然可以将默认配置作为基类/原型链,按照JS通常的继承方式实行继承, 但这种方式灵活性低,我不推荐。

我更喜欢用混同的方式实现, 即提供一个合并函数,通过此函数将新配置和默认配置合并,比如webapck-merge

const merge = require('webpack-merge')
const finalConfig =           // finalConfig 最终的config
    merge(defaultConfig,   // 默认配置
          confg                 // 新配置
          )

常见的开源合并函数有lodash中的merge, 你也可以写自己的合并函数,有些注意事项:

  1. 浅合并与深合并。
  2. 数组的处理。可能的选择有: 1)替换整个数组, 2)合并两个数组, 3) 合并两个数组并去掉重复项。
  3. undefindnull0''fasle 这种假值的处理。 假值可能有两种含义: 1)不需要这个选项,需要在合并时删除此选项。 2)这个选项的值有意义,需要保留
  4. 使用纯函数,避免影响原有数据
  5. 定制合并,如果常见的合并规则无法满足常见,你需要传一个函数,进行定制化合并,比如loadash 中的mergeWith。如无特别需求,最好不使用定制合并,会严重降低代码可读性。

一个项目最好用相同的合并函数。如无特别需求,最好不使用定制合并,这会降低代码可读性。

1.3 配置的再组合

对于复杂项目,一套默认配置可能不够,我们可以准备多套配置,利用合并函数进行组合。比如webapck4 中提供有mode选项, 如果是development, webpack会启用开发阶段的默认配置。 如果是production, 启用生产阶段的默认配置。

在网页应用中,默认值可能来源于后端,再根据用户的交互组合成新的配置,可能出现如下层级的组合:

  1. 静态资源中规定的默认配置。写在js静态文件中,作为兜底配置,用于白屏或后端加载失败时的配置。
  2. 后端加载的默认配置。 加载来自后端的配置规则。
  3. 根据用户的交互选择相应配置。 比如一个博客网站,针对访客、注册用户使用不同的规则。
  4. 最终的配置。
function async getConfig() {
    const staticConfig = { .. }   // 写在前端JS静态资源中的默认值
    const remoteConfig = await getRemoteCofing() // 获取远端配置
    const orginConfig = merge(staticConfig, remoteConfig) 
    const userConfig = await getUserConfig() // 获取用户交互的配置
    return merge(orginConfig, userConfig) 
}

真实场景中,配置的组合可能更复杂,有更多的层级。合理的使用合并函数,可以轻松组合配置,并可追溯配置。如果使用继承的方法,则无法灵活使用,且难以溯源。

合并函数使用方便,如果需复杂判断,特别深的层级,配置的可读性将降低,你需要选用其他更合适的方式。

2. 使用中间件

遇到下面的情况,中间件可能更适合:
1) 需要全局调整
2) 需要临时性动态调整

你可以在项目的各个文件,针对不同情况修改配置,但随着业务的扩展,会对配置失去了控制,不清楚某个选项在哪个地方突然被改了。代码中到处有充斥的if else语句:

if (user.isVister) {
  config.site.banner = '欢迎光临'
}

如果你使用中间件思想,则可以解决问题,把对配置的修改集中到一起, 修改配置的函数可以放到各个模块中

require 'config' from 'config'
require 'updateUserConfig' from 'user/updateUserConfig'
require 'updateSiteConfig' from 'site/updateSiteConfig'

config = updateUserConfig(config)
config = updateSIteConfig(config)

更进一步,你也可以使用类似koa中的中间件风格代码,比如

require 'config' from 'config'
require 'createMiddleUse' from 'createMiddleUse'
require 'updateUserConfig' from 'user/updateUserConfig'
require 'updateSiteConfig' from 'site/updateSiteConfig'

const [use, getConfig] = createMiddle(config)
use(updateUserConfig)
use(updateSiteConfig)
console.log(getConfig()) // 输出经过中间件处理的config

由于没有找到现成的中间件库,你需要自己写创建中间件createMidddle的方法,实现这一方便。这一架构不是很复杂,花一点时间你一定写的出来。

3. 利用函数,“计算”属性

配置中的选项通常是静态的,比如webpack 中:

webpackConfig = {
  entry: 'main.js',
  watch: isDevelopment 
    ? true : false,  // 虽然不是静态值,但加载配置后值就固定,所以也是静态属性值
  plugins: [
    new ProgressBarPlugin()
  ]
}

有时静态属性值不够用,如需要根据网站的语言返回不同的价格

{
  price: 12         // 常规静态写法
  price (payload) { // 函数写法
    const {language, price} = payload
    return language === 'cn' ? price * 1.2: price * 1.1
        }, 
   // ....
}

我们在读取配置时增加判断,如果选项是function则读取函数结果,如果是其他类型值则直接返回。此情况类似Vue的计算属性, 如果想给函数缓存,可使用memoizee

有时你真的需要将函数写入配置中,由于函数被占用,你需要在第一个函数中返回一个函数:

{
  getPrice (payload) {
      // 配置属性读取时,执行
      return function(payload.price) {
          // 函数体
      }
  }
}

或者用箭头模式简写:

{
  getPrice: payload =>
    payload.price => {
      // ...函数体  
    }
}

使用函数方法处理特殊情况, 使用静态值处理一般情况,配置会灵活强大。

JSON无法解析函数,如果配置中包含函数,将配置保存至服务端会存在问题。可以将函数转为字符串存储,并通过eval函数调用。我建议不要这么做, 你可以把函数写在JS静态文件中,在配置中保存函数名称即可。

4. 最小配置化

请在配置中存放尽可能少的选项:

  1. 设置尽可能少的选项。越少的选项,学习成本越低,对新手越友好
  2. 如果默认默认配置满足需求,不要重写。第一,不方便查找该特殊配置的特殊点。第二, 如果全局变更默认值,对该特殊配置将无效。
  3. 及时删除无用配置、废弃配置。 无用配置、废弃配置会让人产生误解,不清楚到底是哪个选项是有效的。
  4. 通过函数计算能得到的值不要存在配置中。随着业务的开展,函数可能改变, 如果将计算得到的值写入配置,函数改变后存的值可能还会保留, 需要做前后兼容。如果配置存在服务器中,兼容处理会更复杂。

5. 提高可读性

使用配置,正是利用配置的高可读性。为提高可读性,有如下建议:

  1. 准备文档,原因不多说。
  2. 准备可以正常运行的配置样本。对于配置,最好有文档,而如果实在没法创建、更新文档,至少准备一个配置样本,约束配置规则、方便新人学习。同时,如果没条件给所有配置做自动化测试,可以仅对配置样本做自动化配置。如果默认配置覆盖面足够,可以将默认配置作为配置样本。
  3. 使用TypeScript或flow.js做配置的校验。
  4. 将配置存于状态管理中,方便配置的查看与修改。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,914评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,935评论 2 383
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,531评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,309评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,381评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,730评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,882评论 3 404
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,643评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,095评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,448评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,566评论 1 339
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,253评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,829评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,715评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,945评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,248评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,440评论 2 348