就在上上週Dennis Snell發佈了Calypso離線開發模式的最後一個pull request,詳細使用說明請服用README,概念大概是這樣:
- 啟動API回應錄製
- 開始這樣那樣玩弄各種等一下離線開發會用到的功能
- 啟動API回應錄像回放
之後calypso就不會真的發送任何API requests,而是從之前的回應錄像中取用預存的回應,如果不存在,就會像是一般API request失敗那樣。我不太確定這種模式有沒有標準用語,不過我們目前是把這叫Priming。目前整體差不多是在MVP,步驟繁瑣,但咬著牙屁股一夾走完一趟,就可以開開心心離線閉關去。
在開發初期有跟Dennis稍微聊過他打算怎麼做這一塊,他說他希望能從redux middleware來處理。由於Calypso是個行之有年的專案了,要從這角度下去幹需要非常多的重構,我當下覺得找幾個人全職在做大概也要個半年吧 … 沒想到這傢伙兼職著做也是半年就做完了,實在佩服。
說到支援離線開發,個人過去接觸過的方式主要有三種:
- 實作可離線作用的service worker
- 將service API寫成抽象介面,抽換實作
- 悄悄地我跑個local server,正如我的remote server
1. 實作可離線作用的service worker
利用service worker的方法在CSS-Tricks上有一篇很完整的教學:Making a Simple Site Work Offline with ServiceWorker,今年作者群還將該教學的範例網站上線為simpleoffline.website。老實說我覺得service worker這名字取得挺鳥,因為worker實在太廣義了,誰知道這service worker到底在忙啥呢?根據MDN的解釋,service worker坐落在web app與瀏覽器對網路的存取間,從各種ajax requests到資源下載,service worker都可以聽取、變動。MDN中也有這句:
They are intended to (amongst other things) enable the creation of effective offline experiences
因此Service Worker API的設計和行為上本來就很適合實作web app的離線行為。CSS-Tricks上該篇教學就是用Service Worker搭配Web Cache來達成。
個人覺得這個方法的好處是相當泛用,就算你的web app一開始沒有設計成要能離線使用,從service worker把這部分接上去不會太複雜,而且不會動到太多既有的source code。
2. 將service API寫成抽象介面的方法
簡單說就是把下面這種code:
fetch( 'https://public-api.wordpress.com/wpcom/v2/what-ever' ).then( ( response ) => { ... } );
加一層包裝變成這樣:
serviceApi.fetchWhatever().then( ( response ) => { ... } );
serviceApi
就可以視需要換成離線實作。
在古早沒service worker也沒有Sinon之類方便mock http reqeust response的時代,利用這個結構可以讓app與service API脫鉤,從而讓app變得非常容易測試。例如以前參與過的某專案的integration test下就有一堆xxx.responses.json
檔定義了各種錯誤的回應來自動測試app在各種錯誤下是否行為正確,非常方便。
這個結構其實是我個人偏好的,但我得承認它並不適合JS;因為JS缺乏維持介面與實作一致性的工具。結果就是不同介面實作常常漸行漸遠也沒人發現,導致更多bug。如果是elm的話就 … 🔥🔥🔥
3. 跑一個local server
顧名思義,就是跑一個local server起來,然後在離線時讓app去對這個local server發請求。這在理論上可行,但實務上常常有困難,因為你發佈的service API所處的環境不見得可以完全複製到本地端,甚至它根本就不歸你管。如果很幸運碰到整個server logic都是自己寫的專案,花一滴滴功夫讓它可以用一個docker image發佈到任何地方,自然而然就可以解決離線開發的問題。
Calypso Priming: 介於1與2之間的方法
回到咱們Calypso的Priming,它可說是座落於1與2兩種方法之間。
首先,calypso對WordPress.com public API的requests大多透過state/data-layer這層redux middleware來完成——說是『大多』,是因為這一層是後來加入,才逐步把requests移進來,所以還有很多沒移過來的。這就很像方法2將app與service API脫鉤的方法,但溝通介面從一個集中的介面換成了redux middleware。透過這個方式,calypso app state不再知曉『發送GET https://public-api.wordpress.com/wpcom/v2/whatever
,處理其response』這件事,它只知道『送出一個REQUEST_WHATEVER』以及『收到RECEIVE_WHATEVER』這兩個redux action的往返而已,與這兩個redux action實際上從哪裡來毫無關係。舉例來說,在開發模式下我們可以在console中用dispatch( { type: REQUEST_WHATEVER } )
來做到一模一樣的事。在寫這篇的時候,在data-layer下還沒看到任何跟離線模式有關的code,但就我所知未來會從這邊加入一些離線行為的處理。
再來就可以做跟方法1一樣的事了:存下service API的回應供離線使用(priming),這邊就是靠自幹的lib/wp/offline-library.js。我個人覺得這邊自幹和service worker比的好處是overhead較低,因為我們一但register了一個service worker所有request都會經過它,不管你要還是不要,它就是坐在那邊了,但這個自幹的版本只會對WordPress.com API做處理,未來要調整的彈性也較大。
為什麼要離線?
能夠離線開發,代表我幾乎可以去任何地方工作,就算是完全沒有網路連線的山頂溪邊也一樣。把網路切掉的時候,通常也是我效率最高的時候,因為沒有任何網站、信件、通知會干擾我,甚至電量也省很大,畢竟今日的應用程式偷偷在背後收發的東西可多了。因此如果能讓我決定專案需求,我一定會把這一項列進去;一開始可能會覺得多了些東西要維護,產品上線又用不到好像挺雞肋,但提供這項工具保護所有專案開發人員的『心流』,絕對是項好投資。