背景什麼的就不說了,你們都懂!不懂的請百度!既然看到了這篇文章,說明你仍是對動態化有本身的訴求噠,那麼但願文章中的內容能夠幫到你。javascript
技術選型永遠是項目肯定以後遇到的第一個難題,市面上能夠解決項目問題的選型有不少,究竟是時髦驅動開發
仍是熱鬧驅動開發
嘞?其實你們在選型過程當中最應該關心的不是技術,而是項目。由於技術應該爲項目服務,而不是項目爲技術服務,分清楚權重以後,就很清晰了。接下來就是從項目入手,從項目入手須要從三個因素考慮:css
項目在不一樣的階段須要考慮的狀況是徹底不同的。
html
好比項目剛啓動或者在基礎功能鋪設的階段,那麼關心就應該是快速試錯
、需求快速更新迭代
、需求變動緊急
、運營活動頻繁
、其餘的非功能性需求
。在項目的擴張期也就是中期,可能會經歷一次重構,將前期各類臨時的解決方案統一進行升級和整改,以此增長系統的穩定性而且能夠足夠應對項目對內的產品類目、功能需求以及對外的市場和產品擴張。在項目的穩按期大多數的項目架構都已經定型,可是並非那麼的"盡善盡美",會有不少歷史遺留問題讓人頭疼,因此這個時候更須要有一個技術視野和能力很強的人來帶領團隊把項目的技術深度和高度達到一個更高的高度,固然,這個過程當中是十分考驗開發人員的能力的。前端
棋局裏有一句善弈者通盤無妙手
,好的架構都是潤物細無聲
的,任何需求和功能的變化均可以讓開發人員十分便捷的完成,擁有足夠的靈活性和反脆弱性,固然這裏就不作過多討論了。java
選型是針對團隊的考慮比重也是十分重要的,由於項目不是一我的在作,而是一羣人。當你選定了某一項技術以後團隊裏確定存在對這個技術不熟悉的人。因此你須要考慮到團隊成員的學習成本,另外在團隊招納新人的時候也會把該技術的要求添加到新人的技能列表裏,若是你選的技術大部分人都不會甚至不知道那就很尷尬了,項目就會越作越死。ios
通過前兩個因素的綜合考慮以後,就該考慮技術因素了。選定的技術方案或者解決方案技術程度度怎麼樣,是否是已經達到了stable的狀態。web
技術方案的穩定性怎麼樣,須要多少人力支持所謂的穩定性這也是須要考慮的地方。另外就是擴展性了,固然這個是相對於需求和功能的擴展來講的。npm
最後,把備選的多個技術方案進行三個方面的多維度對比,就會獲得一個比較滿意的方案了。json
咱們在評審前,找了三端(FE、IOS、安卓)的高工一塊兒討論了選型的問題,通過綜合考慮,咱們選擇了Weex和Hybrid兩種方案。具體細節包括但不侷限於技術選型
、適用場景
、功能邊界
、切入點
、交互協議
等方面,在這裏不作贅述。小程序
選定方案以後,咱們從上述的三種因素基於團隊當前的項目階段
進行綜合對比,具體以下圖。
至於爲何沒有選Weex,緣由是咱們FE團隊裏的人都不太瞭解Weex(大可能是新人),並且深刻學習的成本太大(不要告訴我肯定項目徹底基於某個技術方案開發以後不須要深刻學習和掌握,那你不太適合這篇文章)
。遇到阻塞性問題怎麼解決咱們也不是頗有把握,畢竟咱們不是阿里系的。而使用Hybrid的話,這些問題就不須要考慮了。
你們都知道,javascript是單線程的,即使js引擎底層引入了非阻塞(non-blocking)
的機制,也改變不了運行邏輯較多時頁面卡頓的問題(webWorker不在討論範圍內)。因此高級點的Hybrid方案使用了多線程以此拆分前端的邏輯和視圖。
關於異步
和非阻塞
的區別請參考 asynchronous-vs-non-blocking
RAIL 是一種以用戶爲中心的性能模型。每一個網絡應用均具備與其生命週期有關的四個不一樣方面,且這些方面以不一樣的方式影響着性能:
TL;DR
最後基於上述的RAIL模型,咱們獲得告終論:Hybrid並無比Weex的體驗差不少,在可控範圍內。好比小程序的體驗效果
。
選擇Hybrid以後,咱們有對比了行業的契合度。由於咱們的項目是一個相似與電商的項目,就是買東西的。因此仍是蠻符合的。
接下來介紹下Hybrid方案在項目功能內的使用場景,目前咱們的項目因爲是處於初期階段,因此功能較少,主要有如下四類:
圖中依次是:首頁、二級頁、詳情頁、單品詳情頁。依據於咱們的場景,除了我的中心、訂單列表、收銀臺以外,Hybrid何以適用於項目的其餘任何場景。
由於項目剛開始到目前爲止,咱們三個端都是各自實現業務邏輯,因此在實現動態化方案的過程當中,不能一刀切。時間、人力、項目各類因素也不容許咱們這麼作。所以咱們選擇了一個切入點,循循漸進得完成咱們需求,就像是給一輛高速駕駛的汽車更換地盤同樣。
項目肯定以後,咱們優先考慮了單品詳情頁
做爲咱們的技術切入點。具體緣由以下:
後面咱們依次的遷移順序爲:單品詳情頁 -> 詳情頁 -> 二級頁 -> 首頁。
上面聊了那麼多,沒多少技術的乾貨,如今開始介紹下總體架構的設計。在需求實現的過程當中咱們常常會發生統一套頁面需求在WAP和Native端上都須要實現,爲了解決這種需求帶來的重複工做量的問題,咱們在設計時加入對宿主環境兼容
的考慮。
主要分爲三層:
視圖層主要負責視圖的展示,包括H5的頁面和模板、業務的框架實現還有內嵌在視圖層的bridge,若是視圖是在APP的webView中,那麼也會包含Native Activities控件。至於原生的Native Activities如何設計,後面會講到。
容器就是視圖層的執行環境,多是移動端瀏覽器,也多是App的webView。瀏覽器的話這裏暫且不提,webView的話會提供一個Bridge Provider用來將端封裝好的能力輸出給視圖層,通常使用API注入和Schema的方式實現。裏面封裝的都是Native級別的業務API和硬件設備的API。
最下面的就是Native的OS層,主要提供一切必要的基礎能力,因爲我對Native瞭解的並不深刻,因此這裏暫不討論。
因爲視圖層和容器的
層隔離
,讓視圖層不須要關心容器的實現,可是它們之間的bridge卻必須得關心這個。以致於bridge如何兼容不一樣的容器(wap瀏覽器、Native App),這是個值得深刻考慮的問題。
這是一個簡單的分層架構
。其中每一層都有着特定的角色和職能。架構裏的層次是具體工做的高度抽象,它們都是爲了實現某種特定的業務請求而存在的。還有一個突出特性是關注點分離
,每層都只會處理本層的邏輯。從另外一方面說,分層隔離使得層與層之間都是相互獨立的。架構中的每一層都必須符合最少知識原則
,正由於這種高度獨立,才使得咱們能夠很好的兼容WAP瀏覽器和Native APP。
UI的本質是什麼?是將從服務器獲取數據狀態(state),通過必定的操做使之展現出來。咱們能夠用一個數學表達式來表現它們的關係:UI = f(state)
。state是經過bridge或異步化接口獲取的數據,UI就是用戶看到的界面,對於Hybrid模式的來講,真正關心的就是f
這個函數到底如何實現。
咱們能夠簡單的把f
往大了想,把它理解成爲一個web容器,也就是Web Container。至於Container裏怎麼作?請看下圖:
前文咱們說過了,Hybrid中能夠將視圖層中的視圖(View)與邏輯(Service)分開達到體驗提高的目的。在Native App中通常是將一個頁面拆成這兩部分放在兩個不一樣WebView中,一個WebView放View部分,一個WebView放Service部分(也就是說,每一個頁面都須要2個WebView)。
他們之間通過各自的Bridge對即將發送或剛接收到的數據進行包裝,而後再通過封裝在bridge中的tunnel
進行數據交互,完成後續操做。不過,在wap瀏覽器中徹底不須要考慮這些,該怎麼作就這麼作。可是也會出現一個兼容問題,視圖層和容器之間經過bridge交互,也就是說在wap瀏覽器(wap瀏覽器也是容器)中也須要存在bridge,不過這個bridge提供的是瀏覽器的能力。
bridge中存在一個叫作tunnel
的東西,主要負責傳遞Service和View之間的數據和事件。在不一樣的宿主環境中,tunnel的組成也不一樣,在Native App中,tunnel是一個IPC的實現。在瀏覽器中,tunnel是一個發佈訂閱事件機制的實現。
在Service中會遇到數據本地存儲的問題。數據的存儲和獲取統一經過bridge將操做內容發送給Native,而後Native根據不一樣的操做內容進行處理,完畢以後再經過處理完畢以後的數據發送給Bridge,進而bridge再行通知Service。
Service包含視圖層中除了視圖渲染以外的其餘任何邏輯。它把獲取到的state通過framework的API處理以後會生成一個視圖元信息(View Metadata)
,視圖元信息是對將要渲染視圖的簡單描述,經過它咱們能夠預想到視圖長什麼樣子。以後framework會把視圖元信息經過bridge中的tunnel發送給View。
注意:
tunnel
發送數據的過程是異步的。好比小程序中的setData()方法。
View只包含視圖層中的頁面渲染。渲染對象主要包括兩個:html以及須要展現的Native Activities控件。當Service發送過來的視圖元信息中包含Native級別控件時,bridge會把該部分的視圖元數據發送給Native。Native收到以後,就會根據元數據在視圖層的WebView上展現原生控件(注意:原生組件是Cover
在WebView上的)。當Service發送過來的數據爲html的視圖元數據時,會先根據視圖元數據進行DOM Diff,而後根據生成的Patch對象來進行頁面的渲染。
View渲染完成以後,就會等待用戶操做。View會將用戶操做的事件區別對待:html的事件和Native控件事件。先說Native的事件,Native的事件WebView把控不了,須要Native在封裝業務原生控件時多作注意,對控件可能遇到的事件作統一梳理。原生控件會經過Native框架把事件源和事件參數進行序列化,而後Native框架再將序列化後的事件數據經過bridge發送給Service。
若是是html的事件,View這邊中bridge會經過js獲取事件源和事件參數,而後統一進行序列化。而後在經過tunnel將序列化後的事件數據發送給Service。
Web Container中請求的數據主要分爲兩類:
靜態資源請求
會直接經過webView對外發起請求,這裏不作贅述。
除了靜態資源請求以外的異步化接口請求
咱們會經過Native進行代理,讓Native幫咱們發送請求,而不是使用XMLHttpRequest對象進行請求。
bridge層位於視圖層和Native之間,負責連接雙方,一個好的bridge設計,可讓咱們在開發的過程當中事半功倍。
咱們對Bridge的關注點:
js執行環境
和 宿主環境
之間,負責連接雙方編譯階段
解決宿主環境兼容能力js執行環境
多是wap瀏覽器,也多是Native中的WebView。宿主環境
也多是瀏覽器和Native App。對接的業務方提供的橋連能力各不相同,統一套方案須要對接至少三種不一樣的功能需求平臺。而這些問題就是須要在Bridge中解決的。
上一節提到過『除了靜態資源請求以外的異步化接口請求
咱們會經過Native進行代理,讓Native幫咱們發送請求』。至於爲何要這樣作主要緣由爲:
先說第一個鑑權問題,常規的作法是App用戶登陸後,將用戶的認證標識存在在webView的cookies中,而後WebView裏的業務代碼發送AJAX請求時就會將cookies攜帶到服務器完成用戶鑑權。這種狀況下若是服務器端校驗用戶token失敗的話是沒法第一時間讓APP跳轉到登陸窗口的。另外在WebView中發送了一個退出登陸的異步接口請求,這時APP也須要同步退出登陸。很顯然,最好的辦法就是讓APP幫咱們代爲發送異步化接口請求。這樣咱們還能夠利用上APP的持久化緩存能力來存儲接口數據。
在瀏覽器和Native APP的差別性方面,咱們總結了如下5點:
咱們會在有差別的功能上封裝統一的API,以此減小FE開發人員在開發過程當中的兼容問題。
這裏僅以異步化接口請求舉例,咱們封裝一個統一request方法。開發人員不須要關心本身寫的代碼將要在哪一個平臺上運行。藉助WebPack和Rollup等工具的tree shaking
功能,咱們能夠很好的完成差別化編譯。
// tools.js import Axios from 'Axios'; import bridgeRequest from '@/bridge/request.js'; export default { request: process.env.TARGET === 'app' ? bridgeRequest : Axios } // main.js import {request} from 'tools'; request.get('http://www.test.com/test', {a: 1}).then(data => { console.log('this is test data -> ', data); });
在編譯時咱們只須要指定target就能夠作差別化編譯了:
# 編譯爲app版本 $ npm run build --TARGET=app # 編譯爲wap瀏覽器版本 $ npm run build --TARGET=browser
咱們定製一個Bridge的標準接口,用來規範各類操做,好比Native的調起彈出層控件。業務方根據本身往WebView注入的API或schema協議,填寫一個配置Json文件,而後注入到bridge中,該文件中聲明瞭alert操做要訪問的協議或方法以及參數名稱。這樣bridge在調用alert方法的時候就會根據json完成指定操做。
業務方只須要根據Bridge定義好的標準接口,注入本身的schema協議便可。
// system.schema.json export default { alert: { schema: 'xxxx', params: {} }, request: { schema: 'xxxx', params: {} } } // interactive,js import schema from '@/schemas/system.schema.json'; // 注入業務方本身的alert schema interactive.injectSchema(schema); export default { alert(options) { return interactive.api.alert(options) } } // main.js import {bridge} from '@/bridge/index.js'; import {alert} from '@/bridge/interactive.js'; // view層準備完畢 bridge.on('ready', () => { alert('這是一個alert!').then(data => { console.log(data.state ? '肯定' : '取消'); }).catch(e => { console.log('調起alert失敗'); }); })
因爲我自己不是Native的開發人員因此這裏就列一張Native的架構圖,具體的大家本身看吧。
注意:
這張圖是我這個FE畫的,被安卓的大佬吐槽說畫的結構不清晰。大家將就着看吧!
到這裏,咱們就把架構裏最主要的三層:視圖層、Bridge和Native層介紹完了。下面開始介紹功能設計,主要包括三個方面:原生組件交互、路由系統(統跳協議)、資源包的緩存與更新。
原生組件與webView中用javascript實現的組件是不同的。它們是由Native直接在WebView之上渲染的原生控件,沒法受到javascript影響,只會受到Native的控制和影響。對於WebView中的javascirpt代碼來講就是:超乎三界以外,不在五行之中
。
爲何不能夠所有使用WebView中的js組件哪?那就是WebView中前端組件的影響面太小,就跟唐朝末年的朝廷同樣,政令不出長安城
。好比Alert提示在顯示狀態下,不能夠作其餘交互操做,只能點擊Alert的肯定和取消按鈕。還有Header上左側按鈕的後退以及點擊右側Icon返回APP首頁的操做等,這樣的例子還能夠往下舉不少。因此遇到這種狀況,就必須請原生的Native控件出馬控場了。
咱們這裏梳理了一下能夠用到的Native級別控件:
那些老是須要在視圖裏第一時間展現(Header、TabBar等)的原生組件必須區別對待。不能在WebView加載完以後再去渲染那些原生控件,由於這樣會出現因需渲染原生控件而對WebView從新計算大小致使Service中數據錯誤
以及頁面閃爍
的問題,從而影響用戶體驗。
最好的方法就是把這類原生組件的視圖元數據單獨放在一個控制版本管理的json文件(下文有寫到)
中,而不是放在包含bundle內容的zip包中。這樣Native就能夠根據json文件中的視圖元信息提早渲染好原生控件,而後加載WebView並執行javascript代碼。
在設計整個路由系統以前咱們有個前提條件,那就是每一個視圖頁面都是獨立的一個WebView
(其實包含兩個,一個存放View邏輯,一個存放Service邏輯),而不是在同一個WebView中加載渲染多個頁面。由於只有這樣才能夠完美的模擬原生應用的頁面跳轉的各類操做。這個必定要注意,若是你不注意你就不會理解下文到底在說什麼!
咱們遇到的場景有如下幾種:
跳轉場景:
加載場景:
存在的問題:
最後咱們商定的WebView能夠同時存在的數量爲9個,和微信小程序同樣。當頁面棧已經達到9個的時再打開新頁面就會沒法打開新頁面。頁面之間的參數傳遞統一使用querystring格式。歷史棧的管理由Native統一實現。
咱們維護一個歷史棧的目的就是讓Native中的視圖能夠像瀏覽器的歷史同樣,進行前進和後退。惟一的不一樣是,瀏覽器的歷史存的是URL字符串,而咱們的歷史棧存的是視圖對象。每次Native APP打開都會從新從頭記錄,只會記錄APP運行期間的歷史,APP關閉後歷史棧清空。
正常的操做路徑訪問,會將每一級的視圖存放在歷史棧中。最多存入9級,超過9級則沒法加載新頁面。
當最新的單品頁新打開一個二級頁時,即使這個二級頁已經打開過,歷史管理器也會在棧的頂部新打開一個二級頁。注意,兩個二級頁是徹底獨立的。不存在視圖提高。
在最新的單品頁重定向爲二級頁時,和上面的重複打開頁面狀況相似,都是將當前頁重定向爲二級頁並渲染。注意,這兩個二級頁是徹底獨立的。不存在視圖提高。
當點擊Header左側的後退按鈕(一級一級後退)或者經過Hybrid Router API(能夠多級後退)進行後退操做時,就是消費當前的歷史管理棧。
當全部的步驟都已經就緒以後,就該到這一步了,bundle資源的緩存和更新。這裏咱們引入的分包加載的機制,並且分包的級別是以頁面爲緯度的,而不是功能。其實就是小程序的那套分包加載機制。
爲了實現這套機制,咱們拋棄了WebView的緩存,和Native同窗一塊兒開發並創建起了這套緩存機制。而且只緩存bundle資源(一個一個的zip包)。咱們規定每一個業務只有一個入口zip包,全部的子包zip都必須依賴入口zip包中的subConf.json
進行更新和加載。
只有在Native每次更新入口包是纔會顯示loading,除此以外都會顯示入口包中攜帶的骨架圖html。
APP每次打開的時候都是先去服務器獲取conf.json,conf.json中內容以下:
{ "version": "v1.0.1", // 此內容僅爲示例 "skeletonURL": "https://pan.baidu.com/nt-static/hybrid/app.skeleton_v1.0.1.d3a938346f1ab825.html", // 須要下載的入口zip包 命名方式 {version}.{md5}.zip "zip": "https://issue.pcs.baidu.com/packages/bybrid/v1.0.1.d3a938346f1ab825.zip", // zip包的md5,根據此md5判斷該zip包是否須要更新 "md5": "d3a938346f1ab825", // 簽名校驗 "signature": "342876ba19d34aba92f7536e42992a45", // 須要提早渲染的原生控件 "header": { "title": "this is a title" }, "tabBar": { { "text": "log", "icon": "" }, { "text": "home", "icon": "" } } }
入口zip包解壓完畢以後的目錄結構爲:
$ tree ./v1.0.1.d3a938346f1ab825 ./v1.0.1.d3a938346f1ab825 ├── app.bundle.css //樣式文件 ├── app.bundle.js // js邏輯文件 ├── app.index.html // 入口html ├── app.skeleton_v1.0.1.d3a938346f1ab825.html // 骨架圖,和conf.json中的skeletonURL一致 └── subConf.json // 子包的加載及校驗配置
subConf.json中的內容:
{ // 和conf.json一致 "signature": "342876ba19d34aba92f7536e42992a45", // 子包入口 "subRoutes": [ { // 入口的路由 "routes": ["/go/to/path/1", "/go/to/path/2"], // 骨架圖URL "skeletonURL": { "/go/to/path/1": "https://pan.baidu.com/nt-static/hybrid/app.skeleton_v1.0.1.bfa31a2ae5f55a7f.html" }, "zip": "https://issue.pcs.baidu.com/packages/bybrid/v1.0.1.bfa31a2ae5f55a7f.zip" "md5": "bfa31a2ae5f55a7f" }, { "routes": ["/go/to/path/3"], "skeletonURL": {} "zip": "https://issue.pcs.baidu.com/packages/bybrid/v1.0.1.7af5f492a74499e7.zip", "md5": "7af5f492a74499e7" } ] }
本文主要介紹了Hybrid的總體架構的三層和功能設計的三個點,基本涵蓋了端動態化方向的所有要點。但願本文能夠幫到你。
歡迎你們訂閱公衆號《web手藝人》