配置能簡化項(xiàng)目復(fù)雜度签孔,最經(jīng)典的例子就是webpack。
以webpack 的watch
舉例华临,一行代碼足以:
{
watch: true芯杀,
// 其他選項(xiàng)...
}
如果使用Webpack Node API, 代碼臃腫很多
const webpack = require("webpack");
const compiler = webpack({});
const watching = compiler.watch({},
(err, stats) => {
// 回調(diào)處理
});
配置可以隱藏細(xì)節(jié),暴露我們關(guān)心的參數(shù)雅潭,分而治之揭厚。 另一面,配置靈活性差扶供,無法定制處理棋弥,需要學(xué)習(xí)成本。如果設(shè)計(jì)配置文件诚欠?兼具簡明和靈活性顽染,降低學(xué)習(xí)成本可通過如下方法:
- 開箱即用
- 使用中間件
- 利用函數(shù),“計(jì)算”屬性
- 最小化配置
- 提高可讀性
1. 開箱即用
最好的配置是不用配置轰绵。 所謂開箱即用粉寞,你不用修改配置,默認(rèn)配置足以支持程序的運(yùn)行左腔。所以請給你的配置準(zhǔn)備份默認(rèn)配置吧唧垦,盡量為所有選項(xiàng)提供默認(rèn)值, 這樣有幾個(gè)好處:
- 對(duì)于特殊配置液样,只需關(guān)注特殊點(diǎn)振亮,隱藏共有的細(xì)節(jié)
- 修改默認(rèn)配置,即可影響所有配置鞭莽,維護(hù)方便
- 默認(rèn)配置可作為配置樣本坊秸,作為新人學(xué)習(xí)、自動(dòng)化測試的依據(jù)
1.1 實(shí)現(xiàn)默認(rèn)配置
雖然可以將默認(rèn)配置作為基類/原型鏈澎怒,按照J(rèn)S通常的繼承方式實(shí)行繼承褒搔, 但這種方式靈活性低,我不推薦喷面。
我更喜歡用混同的方式實(shí)現(xiàn)星瘾, 即提供一個(gè)合并函數(shù),通過此函數(shù)將新配置和默認(rèn)配置合并惧辈,比如webapck-merge
const merge = require('webpack-merge')
const finalConfig = // finalConfig 最終的config
merge(defaultConfig, // 默認(rèn)配置
confg // 新配置
)
常見的開源合并函數(shù)有l(wèi)odash中的merge, 你也可以寫自己的合并函數(shù)琳状,有些注意事項(xiàng):
- 淺合并與深合并。
- 數(shù)組的處理盒齿∧畛眩可能的選擇有: 1)替換整個(gè)數(shù)組生宛, 2)合并兩個(gè)數(shù)組, 3) 合并兩個(gè)數(shù)組并去掉重復(fù)項(xiàng)肮柜。
-
undefind
、null
倒彰、0
审洞、''
、fasle
這種假值的處理待讳。 假值可能有兩種含義: 1)不需要這個(gè)選項(xiàng)芒澜,需要在合并時(shí)刪除此選項(xiàng)。 2)這個(gè)選項(xiàng)的值有意義,需要保留 - 使用純函數(shù)创淡,避免影響原有數(shù)據(jù)
- 定制合并痴晦,如果常見的合并規(guī)則無法滿足常見,你需要傳一個(gè)函數(shù)琳彩,進(jìn)行定制化合并誊酌,比如loadash 中的mergeWith。如無特別需求露乏,最好不使用定制合并碧浊,會(huì)嚴(yán)重降低代碼可讀性。
一個(gè)項(xiàng)目最好用相同的合并函數(shù)瘟仿。如無特別需求箱锐,最好不使用定制合并,這會(huì)降低代碼可讀性劳较。
1.3 配置的再組合
對(duì)于復(fù)雜項(xiàng)目驹止,一套默認(rèn)配置可能不夠,我們可以準(zhǔn)備多套配置观蜗,利用合并函數(shù)進(jìn)行組合臊恋。比如webapck4 中提供有mode
選項(xiàng), 如果是development
, webpack會(huì)啟用開發(fā)階段的默認(rèn)配置墓捻。 如果是production
, 啟用生產(chǎn)階段的默認(rèn)配置捞镰。
在網(wǎng)頁應(yīng)用中,默認(rèn)值可能來源于后端毙替,再根據(jù)用戶的交互組合成新的配置岸售,可能出現(xiàn)如下層級(jí)的組合:
- 靜態(tài)資源中規(guī)定的默認(rèn)配置。寫在js靜態(tài)文件中厂画,作為兜底配置凸丸,用于白屏或后端加載失敗時(shí)的配置。
- 后端加載的默認(rèn)配置袱院。 加載來自后端的配置規(guī)則屎慢。
- 根據(jù)用戶的交互選擇相應(yīng)配置瞭稼。 比如一個(gè)博客網(wǎng)站,針對(duì)訪客腻惠、注冊用戶使用不同的規(guī)則环肘。
- 最終的配置。
function async getConfig() {
const staticConfig = { .. } // 寫在前端JS靜態(tài)資源中的默認(rèn)值
const remoteConfig = await getRemoteCofing() // 獲取遠(yuǎn)端配置
const orginConfig = merge(staticConfig, remoteConfig)
const userConfig = await getUserConfig() // 獲取用戶交互的配置
return merge(orginConfig, userConfig)
}
真實(shí)場景中集灌,配置的組合可能更復(fù)雜悔雹,有更多的層級(jí)。合理的使用合并函數(shù)欣喧,可以輕松組合配置腌零,并可追溯配置。如果使用繼承的方法唆阿,則無法靈活使用益涧,且難以溯源。
合并函數(shù)使用方便驯鳖,如果需復(fù)雜判斷闲询,特別深的層級(jí),配置的可讀性將降低浅辙,你需要選用其他更合適的方式嘹裂。
2. 使用中間件
遇到下面的情況,中間件可能更適合:
1) 需要全局調(diào)整
2) 需要臨時(shí)性動(dòng)態(tài)調(diào)整
你可以在項(xiàng)目的各個(gè)文件摔握,針對(duì)不同情況修改配置寄狼,但隨著業(yè)務(wù)的擴(kuò)展,會(huì)對(duì)配置失去了控制氨淌,不清楚某個(gè)選項(xiàng)在哪個(gè)地方突然被改了泊愧。代碼中到處有充斥的if else
語句:
if (user.isVister) {
config.site.banner = '歡迎光臨'
}
如果你使用中間件思想,則可以解決問題盛正,把對(duì)配置的修改集中到一起删咱, 修改配置的函數(shù)可以放到各個(gè)模塊中
require 'config' from 'config'
require 'updateUserConfig' from 'user/updateUserConfig'
require 'updateSiteConfig' from 'site/updateSiteConfig'
config = updateUserConfig(config)
config = updateSIteConfig(config)
更進(jìn)一步,你也可以使用類似koa
中的中間件風(fēng)格代碼豪筝,比如
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()) // 輸出經(jīng)過中間件處理的config
由于沒有找到現(xiàn)成的中間件庫痰滋,你需要自己寫創(chuàng)建中間件createMidddle
的方法,實(shí)現(xiàn)這一方便续崖。這一架構(gòu)不是很復(fù)雜敲街,花一點(diǎn)時(shí)間你一定寫的出來。
3. 利用函數(shù)严望,“計(jì)算”屬性
配置中的選項(xiàng)通常是靜態(tài)的棚点,比如webpack 中:
webpackConfig = {
entry: 'main.js',
watch: isDevelopment
? true : false, // 雖然不是靜態(tài)值滔韵,但加載配置后值就固定,所以也是靜態(tài)屬性值
plugins: [
new ProgressBarPlugin()
]
}
有時(shí)靜態(tài)屬性值不夠用,如需要根據(jù)網(wǎng)站的語言返回不同的價(jià)格
{
price: 12 // 常規(guī)靜態(tài)寫法
price (payload) { // 函數(shù)寫法
const {language, price} = payload
return language === 'cn' ? price * 1.2: price * 1.1
},
// ....
}
我們在讀取配置時(shí)增加判斷烂瘫,如果選項(xiàng)是function
則讀取函數(shù)結(jié)果,如果是其他類型值則直接返回。此情況類似Vue的計(jì)算屬性, 如果想給函數(shù)緩存挽拂,可使用memoizee。
有時(shí)你真的需要將函數(shù)寫入配置中骨饿,由于函數(shù)被占用亏栈,你需要在第一個(gè)函數(shù)中返回一個(gè)函數(shù):
{
getPrice (payload) {
// 配置屬性讀取時(shí),執(zhí)行
return function(payload.price) {
// 函數(shù)體
}
}
}
或者用箭頭模式簡寫:
{
getPrice: payload =>
payload.price => {
// ...函數(shù)體
}
}
使用函數(shù)方法處理特殊情況样刷, 使用靜態(tài)值處理一般情況,配置會(huì)靈活強(qiáng)大览爵。
JSON無法解析函數(shù)置鼻,如果配置中包含函數(shù),將配置保存至服務(wù)端會(huì)存在問題蜓竹』福可以將函數(shù)轉(zhuǎn)為字符串存儲(chǔ),并通過eval
函數(shù)調(diào)用俱济。我建議不要這么做嘶是, 你可以把函數(shù)寫在JS靜態(tài)文件中,在配置中保存函數(shù)名稱即可蛛碌。
4. 最小配置化
請?jiān)谂渲弥写娣疟M可能少的選項(xiàng):
- 設(shè)置盡可能少的選項(xiàng)聂喇。越少的選項(xiàng),學(xué)習(xí)成本越低蔚携,對(duì)新手越友好
- 如果默認(rèn)默認(rèn)配置滿足需求希太,不要重寫。第一酝蜒,不方便查找該特殊配置的特殊點(diǎn)誊辉。第二, 如果全局變更默認(rèn)值亡脑,對(duì)該特殊配置將無效堕澄。
- 及時(shí)刪除無用配置、廢棄配置霉咨。 無用配置蛙紫、廢棄配置會(huì)讓人產(chǎn)生誤解,不清楚到底是哪個(gè)選項(xiàng)是有效的途戒。
- 通過函數(shù)計(jì)算能得到的值不要存在配置中惊来。隨著業(yè)務(wù)的開展,函數(shù)可能改變棺滞, 如果將計(jì)算得到的值寫入配置裁蚁,函數(shù)改變后存的值可能還會(huì)保留矢渊, 需要做前后兼容。如果配置存在服務(wù)器中枉证,兼容處理會(huì)更復(fù)雜矮男。
5. 提高可讀性
使用配置,正是利用配置的高可讀性室谚。為提高可讀性毡鉴,有如下建議:
- 準(zhǔn)備文檔,原因不多說秒赤。
- 準(zhǔn)備可以正常運(yùn)行的配置樣本猪瞬。對(duì)于配置,最好有文檔入篮,而如果實(shí)在沒法創(chuàng)建陈瘦、更新文檔,至少準(zhǔn)備一個(gè)配置樣本潮售,約束配置規(guī)則痊项、方便新人學(xué)習(xí)。同時(shí)酥诽,如果沒條件給所有配置做自動(dòng)化測試鞍泉,可以僅對(duì)配置樣本做自動(dòng)化配置。如果默認(rèn)配置覆蓋面足夠肮帐,可以將默認(rèn)配置作為配置樣本咖驮。
- 使用TypeScript或flow.js做配置的校驗(yàn)。
- 將配置存于狀態(tài)管理中训枢,方便配置的查看與修改游沿。