微信錢包中58到家首頁為什麼這麼快
原文標題《前後端分離和模組化-58到家微信首頁重構之路》【深度前端乾貨】
微信錢包內的58到家全新首頁已經上線,感興趣的同學們可以在微信中開啟“我的->錢包->58到家”檢視。
58到家全新首頁提出重構主要是為了解決以下問題:
- 1、每個城市開通的服務專案不同,有些內容是寫死在tpl中,維護非常頭疼;
- 2、開通新服務或者某些UI調整(比如更換服務項的圖片造成更改雪碧圖)時必須走程式碼上線流程;
- 3、原有的前端切圖、後端寫邏輯的開發模式造成開發週期拉長和上線流程繁瑣;
- 4、原有配置後臺操作複雜,且可配置細節不完善;
- 5、首頁載入速度太慢,使用者體驗欠佳。
58到家目前兩年左右的發展期,整個技術生態還不完善。以上的問題有的是由於創業初期遺留的歷史原因造成,比如程式碼寫死和粗糙的配置後臺;而有的問題是由落後的開發模式和協作模式造成的,比如前後端分工不明確、首頁載入速度慢。
基於上文提到的問題,重構從以下幾方面入手:
- 1、完善配置後臺,細化可配置項;
- 2、資料驅動UI,輕量化tpl,內容更新無需上線流程;
- 3、前後端分離,縮短開發週期,簡化上線流程;
- 4、模組化開發,提高載入速度,同事增強程式碼的可維護性。
配置後臺可以理解為一個簡易的CMS系統,配置的內容是一些量化的欄位,比如圖片地址、連結、價錢等等。此專案中本人並不負責配置後臺的開發,所以不再班門弄斧。
下面詳細描述重構過程中前端的解決方案。
1. 技術選型
根據上文提到的問題,此專案中前端的技術選型如下:
- 客戶端(瀏覽器)
- 使用Vue作為渲染框架(資料驅動UI);
- 圖片懶載入使用Vue-lazyload實現;
- 模組化方案使用CommonJS;
- 因為首頁沒有很多的使用者互動功能,大部分是連結跳轉,所以不使用第三方的touch event工具;
- 開發環境
- 使用58到家前端工程框架boi作為開發和構建工具;
看到以上的技術選型,可能會有讀者疑惑:不就是一個前端模板+模組化方案嗎,有什麼值得介紹呢?
首先,以上的技術選型的背景如下:
- 1、目前58到家FE團隊統一使用vue作為開發框架,元件易複用;
- 2、此次重構後的58到家首頁並非SPA,選用vue的另外一個原因是為了後續的SPA化做預備;
- 3、客戶端渲染html的缺點是首次進入頁面載入較慢,但利用瀏覽器快取機制可以另再次進入頁面的載入時間大大縮短;
- 4、選用CommonJS實現按需載入(load on demand),首屏以外的內容在首屏渲染完成之後載入;
- 5、boi是58到家前端工程框架,以webpack為構建核心,選用CommonJS另一個原因是webpack的原生支援。
2. 前後端分離方案
目前58到家的前後端協作模式仍然很原始,本次重構採用的前後端分離方案並非是最優解,只能算是一種折中的過渡方案。總結有以下幾點:
- 1、初始tpl中包含以下部分:
- js、css引用;
- 頁面初始資料;
- vue元件容器;
- 統計用初始資料。
- 2、客戶端採用vue作為渲染html;
- 3、js和css更新時,FE獨立部署靜態檔案,RD需要將url更新時間戳;
下面分別簡單描述以上的幾點。
2.1 輕量化tpl
tpl的內容如下:
<!DOCTYPE html><html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
<title>58到家</title>
<link rel="stylesheet" href = "http://static.daojia.com/assets/project/wx_index/style/main.wx-index.css?2016082001">
</head>
<body>
<div class="window">
<app :data="data"></app>
</div>
<script type="text/javascript">
// initial page data
var pageData = {}; </script>
<script type="text/javascript">
// for traker
var bi_params = {pagetype:'activity'}; </script>
<script src="https://static.daojia.com/bi/buried_point/js/tracker.js"></script>
<script src='http://static.daojia.com/assets/common/js/zepto.min.js'></script>
<script src='http://static.daojia.com/assets/common/js/vue.min.js'></script>
<script src = "http://static.daojia.com/assets/project/wx_index/js/main.wx-index.js?2016082001"></script>
</body></html>
-
tracker.js
、zepto.min.js
、vue.min.js
是依賴的第三方檔案,全域性變數bi_params
是bi統一用的初始欄位; -
<app :data="data"></app>
是vue元件的容器; - tpl檔案由RD維護,以上提到的兩點是固定不變的,不需要維護。RD需要維護的包括:
-
main.wx-index.js
和main.wx-index.css
的時間戳; - 全域性變數
pageData
。這是首屏的初始資料,之所以選擇以全域性變數的方式暴露,而不是請求api,是為了減少一次http請求,儘快渲染首屏。
-
tpl輕量化是為了減少FE和RD的耦合,當然最佳的方式是tpl交由FE維護,但是目前58到家的開發模式並不適合。所以採用了折中的過渡方案。
使用url query作為js和css檔案的快取策略也並非最優解,理想的方案是使用hash指紋。但是hash指紋需要FE在編譯完成之後將hash值告知RD,而時間戳可以任意修改成與當前不同的值即可,減輕了溝通成本和失誤率。
2.2 客戶端渲染
選擇客戶端渲染有以下幾個優點:
- 1、tpl輕量化,前後端解耦;
- 2、初始html資料量非常小,能夠快速到達客戶端。採用一些loading互動儘快給使用者視覺反饋;
- 3、js檔案初次請求之後快取到本地,只要不更新版本,後續每次進入頁面後初次請求的資料就只有少量的html資料;
- 4、減小伺服器(解析模板)壓力;
當然客戶端渲染也有一些缺點,比如:
- 1、效能比較差的裝置執行渲染過程吃力,不過按照目前手機的迭代速度,這一點基本可以忽略不計;
- 2、SEO不友好。但是這個專案是微信錢包的服務,並不直接提供外部瀏覽器入口,SEO可以不考慮。
具體到客戶端渲染的技術選型,其實從實現功能上來講隨意選用一種js模板工具即可,比如artTemplate。最終選擇vue的原因有以下幾點:
- 1、資料驅動UI的方式利於編寫清晰的邏輯;
- 2、為後續迭代做預備。後期有計劃將整站SPA化,vue+vuex是比較不錯的技術選型;
- 3、58到家FE團隊統一使用vue,部分業務元件可複用;
2.3 更新和快取策略
此次重構採用的快取策略仍然比較原始,比如前文提到的url加query的方式。這也是後續有待優化的一個重點。
3. 模組化方案
3.1 客戶端模組化方案
58到家首頁的內容非常多,大部分尺寸的手機需要三屏才能載入完成。一次性載入的使用者體驗肯定不太順暢,按照主流的手機尺寸,將整站分成三部分:首屏+次屏+尾屏。基本的載入流程如下圖:
簡單概括如下:
- 1、使用者進入頁面後,客戶端發起第一次請求,服務端返回包含首屏json資料的html文件;
- 2、
main.wx-index.js
根據首屏json渲染首屏; - 3、首屏渲染完畢之後或者使用者scroll到頁面底部觸發次屏js檔案
-
wx-index.themes.js
的載入; - 4、
wx-index.themes.js
載入成功後發起jsonp請求次屏和尾屏資料; - 5、渲染次屏;
- 6、次屏渲染完成之後或者使用者scroll到頁面底部觸發尾屏js檔案
wx-index.tail.js
的載入; - 7、
wx-index.tail.js
載入成功後渲染尾屏; - 8、至此,流程結束。
次屏的wx-index.themes.js
和尾屏的wx-index.tail.js
是按需載入的,是為了減少首屏的請求數和資料體積。
按需載入功能使用require.ensure
API實現。之所以選擇使用它有以下幾點考慮:
- 1、58到家前端工程框架boi構建核心基於webpack,webpack runtime內建
require.ensure
API的支援,不需要額外的模組化工具; - 2、AMD方案(比如require.js)的按需載入模組不能定義模組名稱(
wx-index.themes.js
的themes
,wx-index.tail.js
的tail
),而require.ensure
則可以定義模組名稱,使檔名更語義化;
下面簡單介紹一下如何結合vue和require.ensure
實現按需載入和動態元件。
3.1.1 vue結合require.ensure
實現按需載入和動態元件
回顧上文的tpl程式碼可以看出,頁面整體是一個vue元件。頂層元件是<app></app>
。首屏元件是第一級子元件,次屏是第二級子元件,尾屏是第三級子元件。整體結構如下圖:
vue實現按需載入動態元件要考慮以下幾點:
- 1、元件容器位置;
- 2、元件資料如何傳遞。
對比上圖可以看出子元件容器的位置:
- Themes元件作為第一級子元件App的一個子元件,容器位置如下程式碼: <div class="main"> <!-- activity banner --> <wx-activity v-if="data_activity" :data="data_activity"></wx-activity> <!-- nav --> <wx-nav v-if="data_nav&&data_nav.length!==0" :data="data_nav"></wx-nav> <!-- headline --> <wx-headline v-if="data_headline&&data_headline.length!==0" :data="data_headline"></wx-headline> <!-- service --> <wx-service v-if="data_service" :data="data_service" :test="test"></wx-service> <!-- fresh --> <wx-fresh v-if="data_fresh" :data="data_fresh"></wx-fresh> <!-- banner --> <wx-banner v-if="data_banner&&data_banner.length!==0" :data="data_banner"></wx-banner> <!-- themes --> <div class="wx-index__themes"> <themes></themes> </div> <wx-footer :notice="data.showReddot"></wx-footer></div>
- Tails元件作為第二級子元件Themes的一個子元件,容器位置如下程式碼: <template> <template v-for="theme in data.themes"> <slider v-if="theme.type==='slider'" :data="theme"></slider> <single-slider v-if="theme.type==='singleSlider'" :data="theme"></single-slider> <list v-if="theme.type==='list'" :data="theme"></list> <grid v-if="theme.type==='grid'" :data="theme"></grid> </template> <div class="tail"> <tail></tail> </div></template>
wx-index.themes.js
載入成功,在渲染Themes元件之前需要請求次屏的資料,jsonp請求放在vue元件的activate
鉤子函式內:
activate: function(done) { let _this = this; let _url = '/home/ajaxGetSecondIndexPage'; let _cityId = pageData.cityId||$.fn.cookie('comm_cityid'); let _openId = pageData.openId||'';
$.ajax({
url: _url,
data: {
cityId: _cityId,
openId: _openId
},
dataType: 'jsonp',
success: function(res){ if(!res||!res.data){ return;
}
_this.data = Object.assign({},res.data); // 將底部固定模組的資料寫入全域性變數,以便懶載入所需
window.dj_index_data_tail = Object.assign({},{
layidle: _this.data.layidle,
recommend: _this.data.recommend
});
},
complete: function(){
done();
}
});
}
vue元件在
activate
鉤子函式返回done()
之後才會繼續執行後續工作。
請求成功之後將返回的資料賦值給vue元件的data
,然後vue根據data
渲染UI。
上述程式碼有一點需要注意。大家看到程式碼將一些資料賦值給了全域性變數window.dj_index_data_tail
,這些資料是尾屏的資料。由於尾屏的資料量比較小,所以與次屏的資料合併成一個API。這個全域性變數是為了尾屏的Tail元件渲染使用。這就是上文提到的“元件資料如何傳遞”。
使用全域性變數傳遞資料的方式固然不是很優雅,但是不失為一個適合快速開發的方案。這也是後續迭代的優化點之一。
次屏渲染完畢之後觸發尾屏的載入,這個行為實在Themes元件的ready
鉤子函式內進行,如下:
ready: function(){ let loadTail = function() { if(!window.isTailLoaded){ // 主題推薦位渲染完成之後載入底部模組
require.ensure([], function(require) { require("../../_tail.js");
}, 'tail'); window.isTailLoaded = true;
}
};
loadTail();
}
由於之前將Tail元件的資料儲存在全域性變數中,Tail元件的activate
鉤子函式內可以直接讀取次全域性變數:
activate: function(done){ this.data_layidle = window.dj_index_data_tail.layidle&&Object.assign({},window.dj_index_data_tail.layidle); this.data_rec = window.dj_index_data_tail.layidle&&Object.assign({},window.dj_index_data_tail.recommend);
done();
}
以上,便完成了整個頁面的按需載入流程。當然,每種方案都不是最優解,但都是適用於目前狀態的一種比較好的方案,後續迭代中持續優化。
3.2 開發環境模組化方案
開發環境的模組化方案比較隨意,藉助於boi框架中的babel模組,可以將新規範的語法編譯為瀏覽器適用的語法。
此次重構的開發環境的模組化開發使用的是ES6 Modules,語法簡潔易懂,並且開發環境沒有載入動態模組的需求,靜態的ES6 Modules完全適合。
插播廣告:58到家前端工程框架boi支援多種模組化方案,包括ES6 Modules、CommenJS和AMD。
4. 後續迭代需求
依前文所述,本次重構中的仍然有很多問題,這些問題是後續迭代中急需解決的。總結如下:
- 1、工作流程優化
- 進一步解耦tpl層,實現前後端完全分離;
- 2、程式碼優化
- 優化快取策略,使用hash指紋代替url query;
- 優化vue元件間資料傳遞;
- 後臺可配置化引起的零散圖片太多的問題;
- 3、使用者體驗優化
- 新增初始loading效果,增強使用者體驗;
5. 總結
58到家微信錢包重構專案告一段落,其中採用的諸多解決方案中有好的也有不好的。不過總體來說,此次重構中58到家技術團隊向前端工程化、前後端分離邁出了標誌性的一步。後續需要做的事情還很多,不論從專案本身,還是從團隊整體架構的角度,都有很大的進步空間。也歡迎各位同行提出意見和建議。