譯者水平有限,如果有錯誤歡迎指正!
未來你會編寫越來越多的 JS 和 CSS ,並且最終將找到一個合適的系統來管理它們。
這個是未雨綢繆專欄,彙總了一些web工程化問題,你應該在遇到它們之前就思考它們。
自動化處理依賴
在頁面中新增靜態資源最簡單的辦法,就是在頁面渲染之前,在頁面頂部一個個的引入它們,看 Facebook 之前的方式:
<?php
require_js('js/base.js');
require_js('js/utils.js');
require_js('js/ajax.js');
require_js('js/dialog.js');
// ...
複製程式碼
這樣做在早期是可行的,但從 2007 年開始情況變得不可控了。因為你需要按正確的順序逐條手動列出這些靜態檔案,導致其他人需要把這一坨檔案複製貼上到各個頁面。這樣頁面需要載入大量的 JS,影響了前端效能。
於是我們轉向了一個被稱作 Haste 的系統,該系統使用類似 docblock 的頭部宣告 JS 依賴:
/**
* @provides dialog
* @requires utils ajax base
*/
複製程式碼
我們給每個檔案手動新增註釋,雖然理論上可以用靜態分析工具代替(我們沒有真正這麼做是因為我們的 JS 是非結構化的)。這樣我們可以通過一次呼叫請求元件的所有依賴鏈:
require_static('dialog');
複製程式碼
...而不是複製貼上那一大坨依賴檔案。
按需載入
早期的方法帶來的另一個問題是,所有的靜態資源都在頁面頂部引入,而不是在實際需要的時候引用。這意味著2點:
- 你需要引入所有可能在頁面上出現的資源;
- 如果你要在2個及以上的頁面新增新內容,你最好把它放到 base.js 裡。
這樣每個頁面都會有一坨蠢程式碼,例如 CAPTCHA(因為某些工作流涉及未驗證的使用者,理論上應該在所有頁面上向所有使用者展示CAPTCHA),以及其他人在 base.js 裡時不時新增的東西。
我們轉用了一個系統,JS 和 CSS 標籤在頁面載入完之後被輸出(它們還是出現在頁面頂部,只是在輸出到瀏覽器之前被預先準備好,而不是被附加進去,這裡有些複雜,超出本文討論範圍),所以require_static()
可能出現在程式碼的任何地方。然後我們移動所有的require_static()
到呼叫的地方(只有對話方塊程式碼會引入其依賴的 CSS 和 JS,因為並不是每個頁面都有對話方塊),並且把 base.js 拆分成一系列更小的檔案。
打包
大部分情況下最大的前端效能殺手是大量的http請求,針對這個問題最有力的武器是把單獨的 JS 和 CSS 打包成一個大檔案,這樣就可以載入一個大的 JS 核心檔案而不是載入一堆小檔案。一旦其他基本工作到位,這是一個相對容易的更改。我們從手動定義包開始,最終轉向基於生產資料的自動生成。
快取與伺服器內容
用最簡單的方法引入靜態資源,比如用 src="/js/base.js"
寫下一個原生 JS 標籤。隨著站點規模的擴大,這將帶來災難性的問題,因為客戶端使用的可能是老版本資源。這會引起一系列微妙的問題(尤其是使用了 CDN 時),最大的問題是當你 push/deploy 站點時使用者正訪問你的站點,客戶端不會為已有的快取資源發起請求,因此即使你的伺服器能正確響應 If-None-Match (ETags) 和 If-Modified-Since (Expires),當你正將靜態資源變更 push 到站點時,此時的瀏覽者將見不到這些變化後的靜態資源。
解決這個問題最好的方法是給 URI 加版本號,這樣各版本資原始檔對應一個獨立的 URI,如:
rsrc/af04d14/js/base.js
當你 push 站點時,使用者將訪問引用新 URI 的頁面,瀏覽器會重新載入資源。
但是,還有一個大問題,一旦你有一堆前端頁面:
當你 push 站點時,使用者可能會發出一個請求,這個請求由執行新版本程式碼的伺服器處理,返回了一個包含新 URI 的頁面。然後瀏覽器發起了對新 URI 的請求,卻被分發到了還未安裝新版本程式碼的伺服器上,伺服器返回了舊版本的資源。這樣客戶端就有了髒快取:新版本的 URI 下快取了舊版本程式碼。
有很多巧妙的方法來解決這個問題,Facebook 解決的方法是用資料庫而不是硬碟提供靜態資源。當一個 push 開始之前,新的靜態資源被寫在資料庫裡,這樣每個伺服器都可以同時處理新舊資源的請求。
這種方式也使一些處理流程變得相對容易(例如刪除註釋和空格),只需要把壓縮/加工的 CSS 和 JS 版本插入資料庫即可。
參考實現:Celerity
這裡討論的一些想法在 Phabricator's 的 Celerity 系統中實現,該系統本質上是 Facebook使用的 Haste 系統簡化版。