配置能简化项目复杂度,最经典的例子就是webpack。
以webpack 的watch
举例,一行代码足以:
{
watch: true,
// 其他选项...
}
如果使用Webpack Node API, 代码臃肿很多
const webpack = require("webpack");
const compiler = webpack({});
const watching = compiler.watch({},
(err, stats) => {
// 回调处理
});
配置可以隐藏细节,暴露我们关心的参数,分而治之。 另一面,配置灵活性差,无法定制处理,需要学习成本。如果设计配置文件?兼具简明和灵活性,降低学习成本可通过如下方法:
- 开箱即用
- 使用中间件
- 利用函数,“计算”属性
- 最小化配置
- 提高可读性
1. 开箱即用
最好的配置是不用配置。 所谓开箱即用,你不用修改配置,默认配置足以支持程序的运行。所以请给你的配置准备份默认配置吧,尽量为所有选项提供默认值, 这样有几个好处:
- 对于特殊配置,只需关注特殊点,隐藏共有的细节
- 修改默认配置,即可影响所有配置,维护方便
- 默认配置可作为配置样本,作为新人学习、自动化测试的依据
1.1 实现默认配置
虽然可以将默认配置作为基类/原型链,按照JS通常的继承方式实行继承, 但这种方式灵活性低,我不推荐。
我更喜欢用混同的方式实现, 即提供一个合并函数,通过此函数将新配置和默认配置合并,比如webapck-merge
const merge = require('webpack-merge')
const finalConfig = // finalConfig 最终的config
merge(defaultConfig, // 默认配置
confg // 新配置
)
常见的开源合并函数有lodash中的merge, 你也可以写自己的合并函数,有些注意事项:
- 浅合并与深合并。
- 数组的处理。可能的选择有: 1)替换整个数组, 2)合并两个数组, 3) 合并两个数组并去掉重复项。
-
undefind
、null
、0
、''
、fasle
这种假值的处理。 假值可能有两种含义: 1)不需要这个选项,需要在合并时删除此选项。 2)这个选项的值有意义,需要保留 - 使用纯函数,避免影响原有数据
- 定制合并,如果常见的合并规则无法满足常见,你需要传一个函数,进行定制化合并,比如loadash 中的mergeWith。如无特别需求,最好不使用定制合并,会严重降低代码可读性。
一个项目最好用相同的合并函数。如无特别需求,最好不使用定制合并,这会降低代码可读性。
1.3 配置的再组合
对于复杂项目,一套默认配置可能不够,我们可以准备多套配置,利用合并函数进行组合。比如webapck4 中提供有mode
选项, 如果是development
, webpack会启用开发阶段的默认配置。 如果是production
, 启用生产阶段的默认配置。
在网页应用中,默认值可能来源于后端,再根据用户的交互组合成新的配置,可能出现如下层级的组合:
- 静态资源中规定的默认配置。写在js静态文件中,作为兜底配置,用于白屏或后端加载失败时的配置。
- 后端加载的默认配置。 加载来自后端的配置规则。
- 根据用户的交互选择相应配置。 比如一个博客网站,针对访客、注册用户使用不同的规则。
- 最终的配置。
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. 最小配置化
请在配置中存放尽可能少的选项:
- 设置尽可能少的选项。越少的选项,学习成本越低,对新手越友好
- 如果默认默认配置满足需求,不要重写。第一,不方便查找该特殊配置的特殊点。第二, 如果全局变更默认值,对该特殊配置将无效。
- 及时删除无用配置、废弃配置。 无用配置、废弃配置会让人产生误解,不清楚到底是哪个选项是有效的。
- 通过函数计算能得到的值不要存在配置中。随着业务的开展,函数可能改变, 如果将计算得到的值写入配置,函数改变后存的值可能还会保留, 需要做前后兼容。如果配置存在服务器中,兼容处理会更复杂。
5. 提高可读性
使用配置,正是利用配置的高可读性。为提高可读性,有如下建议:
- 准备文档,原因不多说。
- 准备可以正常运行的配置样本。对于配置,最好有文档,而如果实在没法创建、更新文档,至少准备一个配置样本,约束配置规则、方便新人学习。同时,如果没条件给所有配置做自动化测试,可以仅对配置样本做自动化配置。如果默认配置覆盖面足够,可以将默认配置作为配置样本。
- 使用TypeScript或flow.js做配置的校验。
- 将配置存于状态管理中,方便配置的查看与修改。