如何編寫Web前端配置: 一個小痛點的解決思路與技術沉澱
但凡稍微複雜一點的軟體,總免不了與配置文件打交道,不管是服務應用還是客戶端應用,如果我們打開其文件目錄,常常會看到一大堆xxx.rc、xxx.conf、xxx.ini,一般情況下,這些都是配置文件。
而在Web前端,情況有點特殊。
- 首先,前端代碼的打包方式決定,一個作為庫來用的js模塊,不太適合擁有外置的配置文件。
- 第二,前端代碼的部署實際上有三方參與:前端、後端模板、用戶。如果分配三者的配置職權,就成了個新問題。
第一步:先搞清楚我們要解決什麼問題
現實中我們會遇到哪些問題呢?我們先來看幾個場景:
場景一
假設有一個前端頁面,各個模塊之間的依賴關係是這樣的:
page -> A -> B -> C | B -> D
假設A、B、C、D都有若干配置項,那在實際工程中應該怎麼把配置項配進去呢?
一般有兩種方案:
- 第一種,配置全部通過初始化參數來傳入。
- 第二種,每個模塊定義自己的全局配置變數名,通過html(模板)中賦值全局變數傳入配置。
第一種方案的問題在於,假設C、D有配置項,可是我在編寫B的時候,我並不知道,或者並不關心C、D具體要配成什麼樣,要上層來決定。怎麼辦呢?只能靠參數一層一層的往下傳。
第二種方案的問題在於,當項目中的package比較多的時候,我們就會看到這樣的html模板代碼:
<script>window.__a_config = {...}window.BConf__ = {...}window.$settingsForC = {...}...</script>
不僅全局變數雜亂無章,更討厭的是ABC這些模塊處理配置項的策略往往是不一樣的,有的有默認值,有的沒有,有的可以輸入正則表達式,有的只能輸入字元串……要知道很多時候html模板是在後端工程中的,天天跟後埠述這些,真的很難不出錯。
場景二
為了調試方便,我們常常打開網頁時臨時修改配置項,可能你會寫這樣的代碼:
if (window.location.search.indexOf("isdev")) { urlPrefix = "..." isDev = true}//...function foobar() { if (isDev) { console.debug(`...`) }}
這種代碼可能你在每個項目、每個模塊中都要寫一遍,而且如果有些模塊是你同事寫的,你會發現他用來區別開發模式的標誌可能和你不一樣——What the fuck!
場景三
考慮如下代碼:
if (window.__config.foo.bar) { //...}
代碼看起來沒什麼問題,但是假設window.__config沒有foo欄位,代碼就崩了。這種情況如何應對呢?難道要每次使用都判斷一下foo是否存在嗎?
通過上面三個場景,我們基本上能看到一下問題了,經過梳理,大致是下面這些:
- 我的各個模塊能否使用同一模式讀取配置,什麼時候寫配置,什麼時候改配置,能否規定好?
- 我的配置散落在js(開發期)、html(部署期)、url(運行期)中,能否統一管理?
- 我需要配置項,但是外部使用者沒配怎麼辦?配的值錯了怎麼辦?我不想把這些判斷邏輯散落在業務代碼中。
- 根據最小依賴原則,我不想拿到所有config,我只拿我感興趣的。
- 配置項A和B之間有依賴怎麼辦,有沒有簡單的語法處理依賴?
至此,我們的需求基本上就清楚了。
第二步:梳理解決思路,確定方案
下面我們要思考一下如何解決問題了。
上面的問題,通過編碼規範、文檔,都可以一定程度上解決,但都不是最好的選擇。我們可以考慮一下,為什麼會有那麼多js框架和庫?其實大多數時候,這些框架和庫並不是封裝了一坨所有人都看不懂、寫不出的「高級代碼」,而是提供一個模式,讓開發者按照既定的路線編寫應用。
也就是說,框架和庫的意義在於——通過代碼引導行為。
再看回我們的問題,我們要做的恰恰是通過某種方法引導上層開發者的行為,從而將「前端應用配置」這個領域治理好。那麼,我們首先解決了選型問題:我們通過提供一個js庫來規範配置行為,輔以命名規範、文檔等規定,使得配置問題得到解決。
下面就是具體的方案了,我們按照第一part中的提問逐條分析:
我的各個模塊能否使用同一模式讀取配置,什麼時候寫配置,什麼時候改配置,能否規定好?
答:可以,通過提供一個js庫,就能提供一個統一的模式。上層模塊統一使用這個庫,所有的行為都是一樣的。
我的配置散落在js(開發期)、html(部署期)、url(運行期)中,能否統一管理?
答:我們首先要搞清楚,為什麼配置會散落在各個階段?
- js中的配置,一般是默認值,或者不想暴露的私有配置。
- html中的配置,一般是部署的時候由不同工況決定的(比如開發、測試、預發、線上),所以不寫在js而是寫在html中。
- url中的配置,一般是開發人員在打開網頁的時候,由於某種原因想臨時改動一下配置項,覆蓋原有的配置(比如是否使用mock)。
綜合以上三條,我們就可以編寫一個統一的ConfigReader,網頁打開之後,依次讀取配置來源,輸出一個按照優先順序覆蓋過的config表。
注意這裡有個優先順序的問題,即三種來源配置的值不同,誰說了算?按照使用邏輯,正確的優先順序是:運行期最高,部署期次之,開發期最低。這裡可以參考下傳統的命令行程序是怎麼寫的,一定是命令行參數的優先順序高於配置文件,道理是同樣的。
我需要配置項,但是外部使用者沒配怎麼辦?配的值錯了怎麼辦?我不想把這些判斷邏輯散落在業務代碼中。
答:第一個問題可以通過提供默認值介面解決,第二個問題可以通過提供後處理函數解決。關鍵點是,默認值和後處理函數統一在ConfigReader中設置,這樣就避免了這些東西污染業務代碼。
這裡的思路就是一個典型的「抽取」思路——把與業務相對獨立的模塊或者「切面」抽取出來,是的業務代碼更清爽,可讀性更高。
根據最小依賴原則,我不想拿到所有config,我只拿我感興趣的。
答:沒問題,我們把配置的編寫和配置的讀取分離,ConfigReader聲明了哪些欄位就讀哪些,沒有聲明的一律忽略。
配置項A和B之間有依賴怎麼辦,有沒有簡單的語法處理依賴?
答:可以,這裡的思考過程我就不說了,直接說結論——仿照VUE的計算屬性語法即可。
通過以上分析,我們基本上已經理清楚了功能點,思路清楚了,方案也就呼之欲出了,總結一下就是下面這張圖:
第三步:詳細設計與實現細節
對於技術障礙不大的項目,這一步反倒是最簡單的,有前面的結論作為指導,按部就班的實現就好了。
首先我們確定如何寫配置,按照剛才的分析,寫配置主要是部署期和運行期(開發期的默認值放在Reader中)。
URL search 中編寫配置:
http://www.foobar.com/index.html?some-item=1
全局變數中編寫配置:
window.__Konph = { "some-item": 1, "another-item": "222"}
我們只提供這兩個寫配置的入口,保證所有模塊的配置都只有這兩個來源,並且行為都一致,再輔以配置命名規範(比如大小寫、前綴),就初步結束了寫配置的亂象。
然後再來確定如何讀配置,同樣根據剛才的分析,將默認值,後處理函數等特性添加之後,設計出這樣的介面:
// somepackage/config.jsimport conf from "konph"export default conf({ "item-with-default-value": { // 默認值 def: 1 }, "item-with-fit-function": { def: 2, // 後處理函數 fit: value => value > 2 ? 2 : value }, "another-item": { // 配置間依賴第一種寫法 fit: (value, context) => value + context["item-with-default-value"] + 1 }, "another-item": { // 配置間依賴第二種寫法 fit: function(value) { value + this["item-with-default-value"] + 1 } }, // 私有配置, 不會被全局變數或者url參數覆蓋 "some-private-item": conf.private("I"m a private config item.")})// somepackage/index.jsimport config from "./config" const item1 = config["item-with-default-value"]//...
通過上面的介面形式,我們成功地將配置讀取與寫配置分離,同時添加了默認值、後處理函數、配置間依賴的支持,語法也比較簡單。
至於實現細節,我給出一個參考實現:yusangeng/konph 有興趣可以參考下。
第四步:反思 & 討論
下面我們來複盤一下整個事情。
首先是路徑,本文提出的問題實際上是比較簡單的,在技術上沒有障礙,但是它體現了一個解決問題的常見思路。我們解決問題經歷了哪些步驟呢?
- 首先,提出問題,或者說我們要先看到問題。如果看到任何問題都覺得很正常,那就談不上解決了。
- 第二,分析問題,繼而問題分解,把要解決的問題一條一條列清楚,把不要解決的問題也列清楚。搞清楚自己要面對的問題邊界,該放棄的就放棄。
- 第三,回答每一個分解後的問題,提出新模型的運行流程,繼而綜合起來,提出解決方案。
- 第四,詳細設計和具體實現,設計一個好用方便的介面,並且一定程度上考慮擴展性。根據上一條的方案加介面,再去思考如何實現的問題。設計歸設計,實現歸實現,設計的時候盡量追求簡練優雅靈活,不清楚如何實現的到實現的時候再研究,而不是「當下會寫成啥樣」就寫成啥樣。
這裡想著重討論的是第二條,哪些問題我們選擇不解決呢?我舉兩個例子:
- 比如:要不要做運行時監聽配置。配置改變時實時通知到各個模塊?答:不做!服務端有這種功能是因為服務端長期運行,需要不停機改動配置,你一個網頁要這種功能幹什麼?況且實現起來又不容易。
- 比如:要不要做樹形的配置列表,否則就一層命名空間,重名了咋辦?答:不做!樹形的配置列表會導致ConfigReader邏輯變得很複雜,而且這個問題通過命名規範可以防止。況且你的網頁有多複雜?連配置都需要用到樹形的命名空間?
當然以上只是我根據我的需求得出的結論,面對不同的實際項目,完全可能得出不同的結論。
推薦閱讀:
※reactive programming[intro]
※【譯】唯快不破:Web 應用的 13 個優化步驟
※前端利器之Bootstrap
※減少前端代碼耦合
※基於正態分布的前端性能數據分析(一)