手機百度前端工程化之路

本文將圍繞我半年來在移動前端工程化作的一些工做作的總結,主要從 localstorage緩存和版本號管理 , 模塊化 , 靜態資源渲染方式 三個方面總結手機百度前端一年內沉澱的解決方案,但願對你們移動開發有所幫助。php

一年前存在的問題

可能由於以前項目節奏緊,人力不足緣由,一部分phper承擔了前端的工做,因而暴漏了一些問題。css

粗暴的一刀切

從第一次在廠子寫代碼開始,就被前輩告訴移動頁面,因此的靜態資源都要內嵌,即寫在scriptstyle內,這樣的好處是,網絡狀況很差的時候,減小http請求。由於2G等網絡不穩定的狀況下,多開一個http請求,對手機資源消耗是巨大的,好比咱們在手機信號很差的地方,訪問網絡,耗電量會急劇增高。html

可是隨着3G,甚至4G的普及,實際統計顯示,手機百度上2G用戶不到30%,因此上面提到的這種一刀切的方案是不妥的。前端

不成規矩

第二個問題是沒有規範和模塊化的問題。你們寫碼都是 意識流 ,除了都是用zepto.js以外,沒有沉澱下模塊。碰到之前寫過的代碼,都是ctrl+c + ctrl+v。這種粗放的方式,雖然能夠暫時解決問題,可是當出現以前的一段代碼不能知足需求的時候(好比新版app發佈,以前的代碼須要作兼容和升級),須要遍歷全部的代碼,挨個修改,麻煩!html5

高度耦合的工做流程

第三個問題是前端角色問題,如今組內的開發是先後端分離的,使用smarty模板,由於產品是hybridAPP,因此較傳統前端,增長了客戶端RD的聯調成本。前端幾乎都是在聯調和等待的狀態,跟後端聯調smarty數據接口和客戶端聯調js接口。有時候必需要等接口出來聯調經過了以後,才能繼續寫碼,形成了人力的浪費。如何解放前端人力,解決開發聯調耦合的問題迫在眉睫。web

前端在開發中被耦合在聯調中

引入FIS解決方案

FIS 是廠子用的一套前端集成解決方案,從開發、調試到打包上線各個環節都覆蓋了。用成龍大哥的話就是:「抱着試一試的心態,後來發現很黑很亮很柔。。。無論本身用,還推薦給其餘團隊使用。」chrome

Fex那邊不少文章在說FIS,我本身也寫過一篇《FIS和FISP的使用心得》,因此這裏就不贅餘,直接重點說下我基於FIS作的一些解決方案。json

解決聯調成本

第一部分提到的高度耦合的工做流程,分別使用fis本地聯調和chrome擴展來切斷phper、crd跟fe的聯調線路,達到提早自測,提早跑通整個流程。後端

FIS本地調試

FIS的本地調試功能能夠用於解決phper和FEer的問題,分別有模擬smarty模板數據,模擬Ajax接口等功能。咱們將rewrite規則和聯調的模擬數據,分別寫在了server.conftest文件夾前端工程化

關於FIS的本地聯調工做,就很少說了,FIS的官網文檔有詳細的說明

chrome擴展模擬webview接口

爲了解決客戶端注入js接口的方法,咱們經過chrome擴展來實現了。經過chrome的content script方式,在頁面渲染以前提早注入模擬webview的js,這樣頁面在下載渲染的時候在調用js接口就不會報錯。

除了模擬webview接口以外,還將手機百度APP開發中經常使用的工具,和調試功能都集成到這個chrome擴展中。整體的效果以下圖:

注入js模擬手機百度客戶端js接口界面

選項頁面之配置手機百度user-agent

icon點擊popup頁面,自動生成當前url二維碼,方便手機訪問

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版本號細化的工做。

  1. 梳理能夠緩存的靜態資源,將文件分爲:基礎層、通用層和業務邏輯層,緩存的主要是基礎層和通用層的代碼
  2. 指定cookie的value值格式,爲了緩存更多的版本號信息,咱們再也不使用md5作版本號信息,而是規定了下面的格式:jA-V_cB-V,即jA和cB表明緩存的文件名,保持兩位(j表明js,c表明css,t表明前端js模板文件,);V表明版本號,保持一位,版本號是36進制的,當版本號要超過一位時,從0開始從新記錄;按照每週上線一次的狀況,cookie時間是一週,36個版本號能夠夠咱們用的
  3. 將須要緩存的文件統一放在一個路徑下管理

這樣作了以後,就是用腳本作緩存文件自動更新版本號了,開始想到的是經過svn hook的方式,當有新的ci時,計算md5值,寫入一個版本號config文件。上線時比較線上config和svn中的config,若是不同就升版本號。可是每次ci都作一次的方式又畫蛇添足、略顯蛋疼,最終的方案是在上線腳本中作了一些工做,沒有使用svn hook:

  1. 對緩存文件路徑下的文件作md5,生成一張map
  2. 去線上拉取最新的版本號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的模塊化和靜態資源管理,廠子FIS開發團隊同窗有一篇文章《如何高效地管理網站靜態資源

模塊化舉例

如今頁面要引入 moduleA 模塊,而 moduleA 依賴於 moduleB 和 moduleC ,moduleB 和 moduleC 又有本身的依賴模塊,若是不先輸出 moduleB 和 moduleC 的依賴模塊,直接執行 moduleA的 define 函數會報錯的,由於 moduleA 模塊依賴的moduleB 和 moduleC 尚未達到 ready 的狀態。

有時候甚至更加複雜的依賴關係: moduleA的依賴關係

這時候經過《如何高效地管理網站靜態資源》文章提到的,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 的渲染方式上,這得減小了多少工做量啊:)

相關文章
相關標籤/搜索