小程式自發布以來,為開發者和使用者提供了一種輕量級的App。作為一種不需要下載安裝即可使用的應用,它實現了應用“觸手可及”的夢想,使用者掃一掃或者搜一下即可開啟應用。小程式也體現了“用完即走”的理念,使用者不用關心是否安裝太多應用的問題。 微信客戶端為小程式的執行提供了框架支援,如service執行環境、頁面快取機制以及控制元件原生化支援等,本文將對這些部分實現原理做一一介紹。
1. 內容概要
微信小程式採用了傳統的移動端H5瀏覽器作為頁面執行環境,但是與傳統的B/S結構的WEB應用不同,小程式為使用者提供了普通H5頁面無法達到、近似原生App的控制元件體驗,同時也向開發者提供了功能豐富的API。本文將從小程式執行執行環境及框架開始,詳細介紹iOS微信客戶端對小程式控制元件層的框架支撐:使用者的開發程式碼如何與使用者介面互動、API的功能分類和設計,另外會簡單介紹小程式的頁面快取機制。
另外,對於某些H5無法實現,或實現效能較差的控制元件,微信小程式採用了“控制元件原生化”方式,將客戶端實現的原生控制元件提供給開發者使用,本文將對原生控制元件的設計和體驗優化做詳細的介紹。
2. 小程式執行環境及框架簡介
為了對小程式的執行機制展開討論,我們將從一個簡單的小程式按鈕開始,對小程式的事件處理流程作一個簡單的瞭解。
在不同作業系統平臺做應用開發時,通常開發工具都會以XML語言來描述應用的介面佈局,如iOS採用storyboard檔案,安卓使用了layout檔案。在小程式中, 自定義了wxml檔案來描述介面佈局。以下是一個簡單的介面檔案示例,展示一個普通的按鈕,並繫結了點選事件:
圖1. 只有一個按鈕的小程式介面佈局
一個小程式介面除了必須的wxml來描述介面佈局外,還可以提供wxss檔案作為樣式描述(可選)。另外,還需要編寫這個頁面對應的js檔案,開發者的開發程式碼邏輯都在這個js檔案中完成,在該js中處理使用者事件、控制對應的介面的變化等等。下面是對圖1的介面邏輯進行處理的js檔案示例,指令碼響應按鈕的點選事件,並輸出日誌資訊:
圖2. js指令碼中響應處理按鈕事件
微信客戶端通過 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”的方式提供給開發者。
- 原生控制元件插入到網頁DOM****節點
控制元件原生化帶來了更加流暢的原生化體驗和更加豐富的控制元件功能,但是同時也帶來了新的難題。如前所述,原生控制元件是插入到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之間的通訊橋接機制,以及對複雜控制元件進行了原生化。從而為開發者及使用者提供了良好的小程式體驗。
歡迎和我一塊交流技術開發