標籤:
本文將圍繞我半年來在移動前端工程化做的一些工作做的總結,主要從 localstorage緩衝和版本號碼管理 , 模組化 , 靜態資源渲染方式 三個方面總結手機百度前端一年內沉澱的解決方案,希望對大家移動開發有所協助。
一年前存在的問題
可能因為之前項目節奏緊,人力不足原因,一部分phper承擔了前端的工作,於是暴漏了一些問題。
粗暴的一刀切
從第一次在廠子寫代碼開始,就被前輩告訴移動頁面,所以的靜態資源都要內嵌,即寫在script和style內,這樣的好處是,網路情況不好的時候,減少http請求。因為2G等網路不穩定的情況下,多開一個http請求,對手機資源消耗是巨大的,比如我們在手機訊號不好的地方,訪問網路,耗電量會急劇增高。
但是隨著3G,甚至4G的普及,實際統計顯示,手機百度上2G使用者不到30%,所以上面提到的這種一刀切的方案是不妥的。
不成規矩
第二個問題是沒有規範和模組化的問題。大家寫碼都是 意識流 ,除了都是用zepto.js之外,沒有沉澱下模組。碰到以前寫過的代碼,都是ctrl+c + ctrl+v。這種粗放的方式,雖然可以暫時解決問題,但是當出現之前的一段代碼不能滿足需求的時候(比如新版app發布,之前的代碼需要做相容和升級),需要遍曆所有的代碼,挨個修改,麻煩!
高度耦合的工作流程
第三個問題是前端角色問題,現在組內的開發是前後端分離的,使用smarty模板,因為產品是hybridAPP,所以較傳統前端,增加了用戶端RD的聯調成本。前端幾乎都是在聯調和等待的狀態,跟後端聯調smarty資料介面和用戶端聯調js介面。有時候必須要等介面出來聯調通過了之後,才能繼續寫碼,造成了人力的浪費。如何解放前端人力,解決開發聯調耦合的問題迫在眉睫。
引入FIS解決方案
FIS 是廠子用的一套前端整合解決方案,從開發、調試到打包上線各個環節都覆蓋了。用成龍大哥的話就是:“抱著試一試的心態,後來發現很黑很亮很柔。。。不管自己用,還推薦給其他團隊使用。”
Fex那邊很多文章在說FIS,我自己也寫過一篇《FIS和FISP的使用心得》,所以這裡就不贅餘,直接重點說下我基於FIS做的一些解決方案。
解決聯調成本
第一部分提到的高度耦合的工作流程,分別使用fis本地聯調和chrome擴充來切斷phper、crd跟fe的聯調線路,達到提前自測,提前跑通整個流程。
FIS本地調試
FIS的本地調試功能可以用於解決phper和FEer的問題,分別有類比smarty模板資料,類比Ajax介面等功能。我們將rewrite規則和聯調的類比資料,分別寫在了server.conf和test檔案夾下
關於FIS的本地聯調工作,就不多說了,FIS的官網文檔有詳細的說明
chrome擴充類比webview介面
為瞭解決用戶端注入js介面的方法,我們通過chrome擴充來實現了。通過chrome的content script方式,在頁面渲染之前提前注入類比webview的js,這樣頁面在下載渲染的時候在調用js介面就不會報錯。
除了類比webview介面之外,還將手機百度APP開發中常用的工具,和調試功能都整合到這個chrome擴充中。總體的效果如:
chrome擴充的開發過程中,遇見了很多困難,最後通過查資料一一解決了,整個工具開發就用了一個周末的時間,之後是零零碎碎的需求。因為更新比較頻繁,還引入了自動檢測更新的功能。
內嵌靜態資源做localstorage緩衝
因為上面說的原因,頁面用到的靜態資源都是嵌入到頁面中,這種渲染的方式我們叫做inline模式。
inline模式每次都下發全量代碼的方式的確蛋疼,影響頁面速度。不難想到後來大家都用了localstorage來緩衝inline的代碼,這種渲染方式可以叫:localstorage+inline的方式。
手機上的 webview 對 html5 的 localstorage做了不錯的支援,經過我們抽樣統計,手機百度的搜尋結果頁面使用者中,大約有76%支援localstorage。嗯,做localstorage緩衝。
localstorage緩衝解決方案
現在有很多localstorage的解決方案,是每次都下發一個版本號碼資訊的config檔案,頁面載入完畢後,拿著這個config檔案跟緩衝的localstorage檔案校正版本號碼,發現有有更新,則二次拉取新的內容,再緩衝新內容和新版本號碼到localstorage。
在移動上,我們想避免這次二次拉取,於是我們採用cookie的方式來儲存版本號碼資訊,這樣一次訪問,http要求標頭會將cookie帶到後端,後端直接判斷版本號碼,並且下發代碼。
具體方案如下:
- 使用cookie記錄localstorage版本號碼資訊
- 上線時通過打包工具,將所有需要緩衝的檔案依次計算md5值之和string,然後對string取md5作為版本號碼
- 使用者訪問頁面的時候,將cookie帶給後端程式,判斷兩個版本號碼是否相等,如果不相等就下發全量代碼
- 前端負責判斷localstorage支援情況,不支援則寫一個特定cookie值,支援則寫入localstorage版本號碼
- cookie到期時間是一周
當然,這種解決方案相對簡單,相信很多移動前端團隊也在使用,也會有人說:“我們都用外鏈。” 前面說了,我們產品網路比較複雜,只能為了2G使用者做了妥協
上面解決方案的問題
上線之後,因為頁面內嵌的js和css都緩衝到localstorage,頁面大小變小了,的確使用者訪問速度有了很大的提高。嗯,看上去很好~
但是,這又是一個 一刀切 的方案:
- 業務層代碼和基礎層代碼層級一樣:像zepto這種一年更新一次都算多的基礎層代碼,會因為商務邏輯代碼頻發更改而重新下發
- 對於一個網域名稱只有一個頁面的頁面是個好辦法,頁面多了,公用的代碼就少了
- 對於一個版本號碼來說,不能將所有的頁面緩衝代碼都控制住,最後的結果就是在不停的權衡究竟緩衝的是什麼
現在也許部分童鞋就想到了,為啥不多存幾個版本號碼cookie,那樣不就可以多緩衝一些代碼嗎?
cookie多了,http要求標頭會變大,http要求標頭太大,會對速度產生很大影響,當cookie總量超過800位元組,速度會陡升,加上我們用的網域名稱很多兄弟團隊都在使用,如果放開口子任其發展,最後一定會一發不可收拾。
PS:年前參與一個速度最佳化項目,其中一個最佳化方式就是減少cookie,減少要求標頭中的cookie,在慢速網路的速度提升有明顯提高!
ok,繼續探索……
localstorage細粒度緩衝
上面的localstorage版本號碼解決方案,是將md5值存在一個cookie,一個md5值32位,即使使用一半也16位,加上cookie的key,怎麼也要20個位元組以上,我們能不能利用20~100個位元組,盡量緩衝更多的快取檔案版本號碼資訊呢?
於是我們開始了localstorage版本號碼細化的工作。
- 梳理可以緩衝的靜態資源,將檔案分為:基礎層、通用層和商務邏輯層,緩衝的主要是基礎層和通用層的代碼
- 指定cookie的value值格式,為了緩衝更多的版本號碼資訊,我們不再使用md5做版本號碼資訊,而是規定了下面的格式:jA-V_cB-V,即jA和cB代表緩衝的檔案名稱,保持兩位(j代表js,c代表css,t代表前端js模板檔案,);V代表版本號碼,保持一位,版本號碼是36進位的,當版本號碼要超過一位時,從0開始重新記錄;按照每周上線一次的情況,cookie時間是一周,36個版本號碼可以夠我們用的
- 將需要緩衝的檔案統一放在一個路徑下管理
這樣做了之後,就是用指令碼做快取檔案自動更新版本號碼了,開始想到的是通過svn hook的方式,當有新的ci時,計算md5值,寫入一個版本號碼config檔案。上線時比較線上config和svn中的config,如果不一樣就升版本號碼。但是每次ci都做一次的方式又多此一舉、略顯蛋疼,最終的方案是在上線指令碼中做了一些工作,沒有使用svn hook:
- 對快取檔案路徑下的檔案做md5,產生一張map
- 去線上拉取最新的版本號碼config檔案,跟第一步產生的map做比較,不一樣則版本升高
localstorage多維度緩衝
上面的解決方案還是不夠完美,總感覺存的東西還是少,所以又做了一個 多維度cookie版本方案 。
我們把cookie看成可以兩個維度來儲存: 網域名稱 和 路徑 。
舉例
網域名稱A.baidu.com下,有三個產品:新聞、視頻和小說,分別放在三個path:
- A.baidu.com/news
- A.baidu.com/video
- A.baidu.com/novel
那麼新聞、視頻和小說,各自有各自的通用代碼,比如:通用樣式,通用js組件。這樣我們在設定cookie的時候指定相應的path,則可以實現多維度緩衝
開啟localstorage緩衝
為了實現localstorage的緩衝,我們增加了FISLocalstorage類來處理cookie,下發緩衝代碼,將FIS擴充smarty的 <%html%> 標籤進行了修改,增加了localstorage屬性,即下面代碼就可以將頁面開啟緩衝:
<%html localstorage="true"%>//something~<%/html%>
模組化
為瞭解決重複代碼的問題,我們開始結合FIS來做模組化,像seajs、requirejs這些CMD、AMD架構,是後載入的,即用什麼就拉取什麼,屬於非同步模組。js為了實現非同步模組,而大量的代碼在處理模組依賴關係。在移動上,我們不希望這樣,我們希望在後端維護模組的依賴關係,當我require一個模組的時候,會按照依賴關係,依次輸出。
我寫了一個Bdbox的AMD規範的模組化基礎庫,然後在FIS編譯時間,包裹AMD的define外層,並且可以產生一張載入資源表,當使用<%widget%>、<%require%>和<%script%>標籤內使用require這些smarty擴充標籤時,會通過php來動態維護模組依賴。
關於FIS的模組化和靜態資源管理,廠子FISTeam Dev同學有一篇文章《如何高效地管理網站靜態資源》
模組化舉例
現在頁面要引入 moduleA 模組,而 moduleA 依賴於 moduleB 和 moduleC ,moduleB 和 moduleC 又有自己的相依模組,如果不先輸出 moduleB 和 moduleC 的相依模組,直接執行 moduleA的 define 函數會報錯的,因為 moduleA 模組依賴的moduleB 和 moduleC 還沒有達到 ready 的狀態。
有時候甚至更加複雜的依賴關係:
這時候通過《如何高效地管理網站靜態資源》文章提到的,FIS編譯後會得到的模組依賴關係表:map.json,來做動態模組依賴管理。
通過修改fis編譯指令碼,將模組依賴檔案內容放到map.json中,當使用smarty擴充文法標籤的時,php會自動讀取map.json,然後將依賴解析出來,提前將moduleA依賴的模組都在其 code>define 之前引入,所以下面的兩種代碼寫法:
```smarty
<%require name="common:bdbox/moduleA"%>`
<%或者%>
<%script%>
var moduleA = require(‘common:bdbox/moduleA‘); <%/script%>
```
實際輸出的html代碼是:
<script> define(‘common:bdbox/moduleB‘, function(){ //A相依模組B }); define(‘common:bdbox/moduleC‘, function(){ //A相依模組C }); define(‘common:bdbox/moduleA‘, function(){ //模組A var C = require(‘common:bdbox/moduleC‘); var B = require(‘common:bdbox/moduleA‘); }); var moduleA = require(‘common:bdbox/moduleA‘);</script>
對於不是模組的js或者css檔案,如果使用了<%require%>,則主動使用file_get_contents來讀取內容。
Q & A
- 為啥不直接用seajs和requirejs?
- 為啥不用FIS自己的modjs,而自己重複造輪子?
- Bdbox不僅僅是個AMD庫,還是一個基礎庫,維護命名空間和工具類
- 為什麼命名不是標準的AMD規範?
- 命名中的
common:bdbox/moduleA,common是命名空間,一個項目會由很多頁面模組(此模組是產品template模組,不是前端模組)組成,通過命名空間可以快速定位對應的map.json, 而bdbox/moduleA是實際的AMD模組名
靜態資源引入模式
上面所有的關於靜態資源管理的解決方案,都是圍繞 一刀切 的方案在做最佳化,而沒有利用http本身的cache,實際上:在3G、wifi甚至4G的環境中,http cache的方案,在易用性和相容性上面要比localstorage+inline內嵌靜態資源的方式要好。
而且從手機百度真實的使用者網路類型統計來說,3G+wifi已經達到75%以上,如果能結合wise團隊提供的ip測速庫和公司的CDN服務,會有一種更好的解決方案,進一步來說,如果可以根據網路類型和使用者真實網路速度,自由選擇在localstorage+inline和CDN方案之間切換就更好了。於是我們做到了!一種新的渲染方式出現了:CDN+combo。
再說這種渲染方式之前,先梳理下上面提到的一些名詞:
- inline模式 :即所有的靜態資源都內嵌到頁面,最古老的一刀切方案
- tag模式 :即使用script和link標籤,引入外鏈的js和css,pc上面常用,2G滿網速不適合
- localstorage+inline模式 :
一刀切的最佳化版,將inline的公用靜態資源利用html5 的localstorage緩衝做本機存放區
- CDN+combo模式 :即利用tag模式,將資源外鏈,結合CDN和http cache做好緩衝,combo提供模組化代碼的打包合并服務
好,繼續那模組化說的moduleA模組依賴moduleB和moduleC來說,經過 tag模式 ,會輸出下面的html:
<script src="http://xxx/bdbox/moduleC.js"></script><script src="http://xxx/bdbox/moduleB.js"></script><script src="http://xxx/bdbox/moduleA.js"></script><script> var moduleA = require(‘common:bdbox/moduleA‘);</script>
這樣模組化的代碼經常成了網頁的瓶頸,因為模組化存在,造成了更多的外鏈!下面我們需要一個CDN+combo服務,來合并http請求。
因為smarty的擴充文法,結合之前產生的map.json,我們實現了模組化依賴關係後端自動處理依賴,然後選擇最合理的輸出順序。這時候我們不是直接輸出對應的tag或者inline內容,而是將它合并到一個combo服務對應的URL,統一輸出!
<script src="cdn-combo-server?file=bdbox/moduleC,bdbox/moduleB,bdbox/moduleA"></script>
渲染模式智能切換
如何根據使用者網路環境智能切換渲染方式呢?我繼續改造了smarty的<%html%>標籤,添加屬性rendermode,通過wise測速庫和手機百度用戶端傳給我們的網路類型,選擇不同的rendermode方式:
<%if ($slow_network || $nettype==‘2G‘) && $support_localstorage %> <%html rendermode="inline" localstorage="true"%><%elseif $fast_network%> <%html rendermode="combo"%><%else%> <%html rendermode="inline"%><%/if%>//……<%/html%>
拆分父子模板
上面的方案,我們如果逐個頁面去寫代碼,改方案,想想就蛋疼,所以我們拆分了父子模板,從架構本身來分,一個module對應一個父模板,其他子模板使用smarty的extends標籤實現繼承關係。
經過模板拆分後,子模板專註於做業務,父模板專註於做解決方案,而且也方便了抽樣和統計。
其他
- 規範方面,已經整理了詳細的編碼規範和js常見編碼問題;
- 引入jslint和csshint對代碼品質進行把控
- 前端文檔,在js代碼中增加jsdoc規範的注釋,自動通過jsdoc產生前端文檔
總結
- FIS帶給我們一整套的前端繼承解決方案,是上面所有解決方案的骨架
- 開發流程上,通過工具來解耦,減少聯調等待時間,提高前端工作效率
- 父子模板拆分,有利於父模板做解決方案
- 拒絕一刀切的解決方案,做可擴充的解決方案
- 最後,我們把上面所有的解決方案都放在一個單獨的前端common模組中
試想一下,如果2015年,使用者都用上了4G,那麼我們需要將父模板的rendermode改成 rendermode="combo"就可以全部切到 CDN+combo 的渲染方式上,這得減少了多少工作量啊:)
手機百度前端工程化之路