小程序自發布以來,爲開發者和用戶提供了一種輕量級的App。做爲一種不須要下載安裝便可使用的應用,它實現了應用「觸手可及」的夢想,用戶掃一掃或者搜一下便可打開應用。小程序也體現了「用完即走」的理念,用戶不用關心是否安裝太多應用的問題。 微信客戶端爲小程序的運行提供了框架支持,如service運行環境、頁面緩存機制以及控件原生化支持等,本文將對這些部分實現原理作一一介紹。html
1. 內容概要前端
微信小程序採用了傳統的移動端H5瀏覽器做爲頁面運行環境,可是與傳統的B/S結構的WEB應用不一樣,小程序爲用戶提供了普通H5頁面沒法達到、近似原生App的控件體驗,同時也向開發者提供了功能豐富的API。本文將從小程序運行運行環境及框架開始,詳細介紹iOS微信客戶端對小程序控件層的框架支撐:用戶的開發代碼如何與用戶界面交互、API的功能分類和設計,另外會簡單介紹小程序的頁面緩存機制。web
另外,對於某些H5沒法實現,或實現性能較差的控件,微信小程序採用了「控件原生化」方式,將客戶端實現的原生控件提供給開發者使用,本文將對原生控件的設計和體驗優化作詳細的介紹。canvas
2. 小程序運行環境及框架簡介小程序
爲了對小程序的運行機制展開討論,咱們將從一個簡單的小程序按鈕開始,對小程序的事件處理流程做一個簡單的瞭解。微信小程序
在不一樣操做系統平臺作應用開發時,一般開發工具都會以XML語言來描述應用的界面佈局,如iOS採用storyboard文件,安卓使用了layout文件。在小程序中, 自定義了wxml文件來描述界面佈局。如下是一個簡單的界面文件示例,展現一個普通的按鈕,並綁定了點擊事件:瀏覽器
圖1. 只有一個按鈕的小程序界面佈局緩存
一個小程序界面除了必須的wxml來描述界面佈局外,還能夠提供wxss文件做爲樣式描述(可選)。另外,還須要編寫這個頁面對應的js文件,開發者的開發代碼邏輯都在這個js文件中完成,在該js中處理用戶事件、控制對應的界面的變化等等。下面是對圖1的界面邏輯進行處理的js文件示例,腳本響應按鈕的點擊事件,並輸出日誌信息:微信
圖2. js腳本中響應處理按鈕事件cookie
微信客戶端經過 WKWebView以及JavaScriptCore提供了小程序的運行環境。WKWebView負責對wxml和wxss進行解析執行,並渲染展現;JavaScriptCore提供了開發者所寫的邏輯代碼(JavasScript)的運行環境,該運行環境咱們稱之爲Service,Service中的代碼與WebView中的代碼徹底隔離,如圖3所示。
圖3. 小程序運行環境框架
上圖中,綠色部分爲客戶端提供的支持框架,白色部分爲前端邏輯。如圖所示,一個小程序就對應了一個Service,客戶端經過JavaScriptCore爲開發者的Service代碼提供了運行環境;一個小程序可能有一個或多個Page做爲向用戶展現內容的交互界面,客戶端由WKWebView提供了Page解析和渲染支持;頁面與頁面之間的通訊經過Service環境中轉。
用戶點擊頁面中的Button控件後,點擊流消息數據在微信客戶端的流轉時序如圖4:
圖4. 小程序按鈕點擊事件時序圖
當前端Web JS監聽到用戶的按鈕點擊行爲後,經過WebKit提供的消息傳遞機制(PostMessage)將點擊事件發送給微信客戶端當前頁面的WKWebView,WKWebView再將該點擊事件交由當前小程序的客戶端Native Service環境,經過Native JSCore(JavaScriptCore),回調執行到前端Service Js代碼中的onClick監聽函數。
下面依舊以按鈕爲例,經過僞代碼實現來理解上述過程:
a、開發者在界面wxml中爲button綁定監聽函數:
b、JSSDK將onClick事件發送到service
c、service中監聽並執行綁定函數
上述流程中使用到的WeixinJSBridge對象承擔了發送和監聽事件消息的任務,publish函數負責發送消息到客戶端,subsribe負責接收客戶端publish的消息。將在下一節作詳細的介紹
3. 數據傳輸框架****與WeixinJSBridge的實現
在普通的H5頁面開發模式下,每個WebView頁面是一個相對獨立的運行環境,若是頁面與頁面之間有數據交互的需求,能夠選擇的通訊方式較爲單一,如採用cookie、localstorage,甚至經過query參數來進行數據傳遞。如前所述:一個小程序由多個WebView構成,H5的常規開發結構遠遠達不到小程序App開發的數據傳輸需求,也不符合App開發的習慣。
鑑於上述緣由,微信客戶端爲每一個小程序提供了獨立的運行環境(小程序內部稱爲Service),該運行環境保持與小程序一致的生命週期,提供了該小程序運行中所有WebView的邏輯支撐能力:
A. 處理WebView控件上用戶交互事件的能力
B. 爲開發者提供相對隔離的邏輯開發環境
C. 提供WebView與WebView之間的數據通訊能力
D. 監控小程序以及每一個頁面(WebView)的生命週期,以App事件的方式通知到開發者
上一節經過對按鈕點擊事件的處理,介紹了A能力的實現;對於B能力,iOS客戶端採用了JavaScriptCore庫做爲小程序用戶代碼的運行環境,保證了運行環境的隔離;同時JavaScriptCore也提供了小程序能正常運行的核心功能C:即前端JavaScript腳本與客戶端之間的數據通訊能力的支持,該能力主要經過WeixinJSBridge對象來實現,下面就對WeixinJSBridge的設計作詳細介紹。
爲了知足小程序的通訊需求,WeixinJSBridge需支持以下基本的通訊接口:
l 經過JavaScript調用微信客戶端(Objective C)中的函數;
l 微信客戶端(Objective C)執行JavaScript腳本的function。
爲了前端開發方便,WeixinJSBridge提供了同一套代碼,同時對Webview和Service進行了能力支持。
WeixinJSBridge.publish
在Webview端,經過webkit提供的postMessage來將網頁數據傳輸到Objective C監聽函數,客戶端直接透傳到小程序service;在Service端調用執行Objective C中的block將數據傳輸到客戶端,客戶端再將數據透傳到當前Webview。
WeixinJSBridge.subscribe
註冊監聽函數,監聽客戶端Objective C代碼的函數調用。webview端監聽Service中的publish調用;Service端則監聽Webview中的publish調用。
WeixinJSBridge.invoke
傳輸邏輯與publish函數相同,不過該函數用來提供JSAPI的調用,函數調用到Objective C後,微信客戶端將執行對應的JSAPI。
WeixinJSBridge.on
監聽客戶端主動拋出來的系統事件,好比小程序啓動事件,頁面切換事件,以及小程序切換後臺事件。
客戶端經過提供WeixinJSBridge對象,開發者就能夠經過publish和subscribe實如今Service中經過js代碼與小程序的WebView通訊;經過invoke調用微信客戶端的原生能力;並經過on接口監聽微信傳遞過來的通知事件。
4. 頁面預加載與緩存機制
在小程序中,爲了提升頁面運行速度,達到類原生體驗,提供了頁面預加載機制,開發者提交代碼後,開發工具後臺編譯代碼包時,會預生成page-frame.html(包含一些描述頁面結構的 JavaScript 代碼和全部頁面通用樣式的 CSS 代碼):
a、當小程序任務建立時,建立首頁webview後,經過WKWebView提供的loadHTMLString接口,加載page-frame.html,頁面特有的邏輯經過evaluateJavaScript執行插入到當前頁面;
c、首頁加載成功後,小程序會在後臺預加載新的WebView,並經過loadHTMLString加載page-frame.html;
d、當須要跳轉頁面時,取緩存中的預加載頁,並執行evaluateJavaScript執行頁面特有的邏輯,同時須要補充緩存預加載頁,爲下一次跳轉準備;
這種預加載機制極大減小了小程序頁面跳轉執行耗時,提升了用戶的點擊體驗。
5. 兩種類型的API的設計與執行流程
小程序的API分爲兩類:「組件API」和「開發API」。組件API並不直接暴露給開發者,開發API是直接提供給開發者調用的功能性API。開發者在開發過程當中能夠見到的API只有開發API;對於組件API,前端SDK會封裝成組件提供給開發者使用,因此當開發者的頁面中使用到了某個組件,而且這個組件使用到了客戶端的某些原生功能,那麼這個組件在初始化或運行過程當中就會調用組件API。
圖5展現的是兩類API調用時,從前端調用到進入到微信客戶端Objective C代碼時,所通過的依賴模塊,其中WeixinJSBridge在上一節已經作了詳細的介紹,Service SDK和Webview SDK分別是前端對WeixinJSBridge的進一步功能性封裝。
圖5. 小程序組件相關模塊依賴關係
6. 原生控件的創建與交互機制
小程序內部提供了部分非H5實現的原生控件。原生控件能夠提供H5控件沒法實現的一些功能, 原生控件的用戶體驗感覺上也會更加流暢,另外,使用原生控件減小了Objective C代碼與WebView通訊的流程,下降了通訊開銷。
以畫布爲例,前端提供了wx-canvas控件給開發者,當開發者在頁面中設置一個畫布標籤
圖6. 畫布控件原生化建立邏輯
如上圖所示,wx-canvas控件初始化時,將會經過Webview SDK的封裝調用,執行客戶端提供的「組件API」:insertCanvas接口以及updateCanvas接口(可選),繪製時經過調用客戶端的drawCanvas接口,將繪製命令傳遞給客戶端,客戶端解析drawCanvas接口所帶的參數,獲取繪製命令集,並使用了Quarz2D來進行圖形繪製。
insertCanvas通知客戶端,在當前WebView上插入一個畫布控件,客戶端根據傳入的位置和寬高參數來決定插入控件的位置和大小;
當開發者改變了wx-canvas控件的位置大小時,經過updateCanvas接口通知客戶端,客戶端對原生控件frame位置大小屬性作對應的修改;
頁面離開時,removeCanvas接口的調用將畫布控件從webview上移除。
除了畫布之外,Video組件對AVPlayer進行了封裝,利用系統組件功能提供了邊下邊播的功能,並定製了原生化全屏等更加友好的用戶操做界面;Map組件對QQ地圖組件的封裝將QQ地圖的豐富功能引入到小程序,讓開發者具備更廣闊的開發想象空間;輸入控件分別引入了iOS原生的UITexField和UITextView,提供了HTML輸入框沒法知足的定製化輸入鍵盤等功能。
爲了提供更加靈活可控的控件功能,小程序還對H5中的Toast、Alert、Picker、ActionSheet等控件作了原生化。這些組件是採用「開發API」的方式提供給開發者。
控件原生化帶來了更加流暢的原生化體驗和更加豐富的控件功能,可是同時也帶來了新的難題。如前所述,原生控件是插入到webview控件上(實際實現時是插入到WKWebView下的WKScrollView下),如圖7,網頁元素老是繪製在WKContentView控件上——WKContentView負責繪製網頁中的所有HTML元素,視頻控件插入後將覆蓋網頁中的全部HTML元素:
圖7. 原生控件插入到WKWebView後將覆蓋控件樹中的HTML節點
如上圖,插入的原生控件必然老是蓋住網頁(節點樹中越靠下的節點,顯示層級越高),這樣就會致使:
a、若是開發者指望在原生控件上覆蓋一些自定義HTML元素,將沒法被支持到。
b、全部的H5彈出元素都會被原生控件遮擋,好比alert對話框。這一問題能夠經過將H5的彈出組件都原生化得以解決,如上節提到的Toast、Alert、Picker、ActionSheet的原生化;
c、若是開發者在div滾動條中插入原生控件做爲div的子節點,預期原生控件應該隨着父節點div滾動條的滾動而移動,而且超出div區域的內容應該被裁掉,可是因爲原生控件是直接插入到webview下,與div之間沒有關聯,因此不會跟隨移動也不會被裁減,在表現上會出現與開發者預期不一致的狀況,影響用戶體驗。
爲了解決這一問題,客戶端嘗試對WKWebView解析HTML元素的原理進行分析,WKWebView在進行HTML解析時,會根據頁面DOM元素在WKWebView控件下生成對應的iOS原生控件,經過分析,普通狀況下生成的原生控件與HTML節點無對應關係,可是在某些特殊狀況下,一些特殊DOM元素會在WebView的對應位置生成位置、大小徹底一致的原生控件,如包含overflow屬性的DIV標籤,以下圖所示:
圖6. WKWebView解析HTML在客戶端生成對應的原生控件示例
如上圖所示,WKWebView將在解析HTML時將該標籤位置生成一個對應的UIScrollView控件。利用這個屬性,咱們能夠在開發者指望插入原生控件的位置,預生成一個包含overflow標籤的DIV節點,而後在插入原生控件時,將原生控件插入到該標籤對應的UIScrollView上,就能夠作到「原生控件不遮擋HTML元素」。例如將一個視頻播放器插入到DOM節點之後,節點樹以下:
圖9. 將視頻控件插入到網頁DOM節點後的節點樹
客戶端採用的「原生控件插入到網頁DOM節點」方案,具體實現原理以下:
a、WEB端預先在須要插入原生控件的預留位置插入一個具備overflow屬性的DIV標籤,並經過「組件API」insertContainer通知客戶端該滾動條的位置、大小;
b、客戶端根據insertContainer傳入的位置和大小,在WKWebView下遍歷找到這個DIV標籤對應的UIScrollView(大小位置均一致),保存其對象指針,並分配一個id返回給WEB端;
c、當WEB端插入原生控件時,經過接口傳入id通知客戶端:該原生控件屬於哪一個div滾動條,客戶端找到該滾動條對應的原生UIScrollView,並將控件插入到該UIScrollView下;
d、當頁面的DOM元素髮生變化時,須要經過updateContainer告訴客戶端調整指定的原生控件的大小,客戶端根據參數調整原生控件的大小(位置不須要調整,由於老是在相對於父控件的原點位置)。
插入DOM節點後原生控件事件處理。因爲WKWebView會接管用戶的全部操做事件,所以按照上述方案插入後,原生控件是沒法響應用戶事件的。所以須要對事件作特殊處理:經過重載WKWebView的hitTest方法,在該方法的處理邏輯中優先處理網頁上的事件,若是網頁未處理,再傳遞給原生控件。
8. 總結
微信客戶端爲小程序提供了整套運行環境:包括js腳本的運行時支持、小程序任務管理、service中的js腳本與webview之間的通訊橋接機制,以及對複雜控件進行了原生化。從而爲開發者及用戶提供了良好的小程序體驗。
歡迎和我一塊交流技術開發
微信公衆號:終端研發部