作為微信小程序底層 API 維護(hù)者之一,經(jīng)歷了風(fēng)風(fēng)雨雨、各種各樣的吐槽。為了讓大家能更好的寫一手小程序,特地梳理一篇文章介紹。如果有什么吐槽的地方,歡迎去 https://developers.weixin.qq.... 開發(fā)者社區(qū)吐槽。
PS: 老板要找人,對自己有實(shí)力的前端er,可以直接發(fā)簡歷到我的郵箱: villainthr@gmail.com
為了大家能更好的開發(fā)出一些高質(zhì)量、高性能的小程序,這里帶大家理解一下小程序在不同端上架構(gòu)體系的區(qū)分,更好的讓大家理解小程序一些特有的代碼寫作方式。
整個(gè)小程序開發(fā)生態(tài)主要可以分為兩部分:
一開始的考慮是使用雙線程模型來解決安全和可控性問題。不過,隨著開發(fā)的復(fù)雜度提升,原有的雙線程通信耗時(shí)對于一些高性能的小程序來說,變得有些不可接受。也就是每次更新 UI 都是通過 webview 來手動(dòng)調(diào)用 API 實(shí)現(xiàn)更新。原始的基礎(chǔ)架構(gòu),可以參考官方圖:
不過上面那張圖其實(shí)有點(diǎn)誤導(dǎo)行為,因?yàn)椋瑆ebview 渲染執(zhí)行在手機(jī)端上其實(shí)是內(nèi)核來操作的,webview 只是內(nèi)核暴露的一下 DOM/BOM 接口而已。所以,這里就有一個(gè)性能突破點(diǎn)就是,JSCore 能否通過 Native 層直接拿到內(nèi)核的相關(guān)接口?答案是可以的,所以上面那種圖其實(shí)可以簡單的再進(jìn)行一下相關(guān)劃分,新的如圖所示:
簡單來說就是,內(nèi)核改改,然后將規(guī)范的 webview 接口,選擇性的抽一份給 JsCore 調(diào)用。但是,有個(gè)限制是 Android 端比較自由,通過 V8 提供 plugin 機(jī)制可以這么做,而 IOS 上,蘋果爸爸是不允許的,除非你用的是 IOS 原生組件,這樣的話就會(huì)扯到同層渲染這個(gè)邏輯。其實(shí)他們的底層內(nèi)容都是一致的。
后面為了大家能更好理解在小程序具體開發(fā)過程中,手機(jī)端調(diào)試和在開發(fā)者工具調(diào)試的大致區(qū)分,下面我們來分析一下兩者各自的執(zhí)行邏輯。
Native 端運(yùn)行的通信體系:
一開始考慮到安全可控的原因使用的是雙線程模型,簡單來說你的所有 JS 執(zhí)行都是在 JSCore 中完成的,無論是綁定的事件、屬性、DOM操作等,都是。
開發(fā)者工具,主要是運(yùn)行在 PC 端,它內(nèi)部是使用 nwjs 來做,不過為了更好的理解,這里,直接按照 nwjs 的大致技術(shù)來講。開發(fā)者工具使用的架構(gòu)是 基于 nwjs 來管理一個(gè) webviewPool,通過 webviewPool 中,實(shí)現(xiàn) appservice_webview 和 content_webview。
所以在小程序上的一些性能難點(diǎn),開發(fā)者工具上并不會(huì)構(gòu)成很大的問題。比如說,不會(huì)有 canvas 元素上不能放置 div,video 元素不能設(shè)置自定義控件等。整個(gè)架構(gòu)如圖:
當(dāng)你打開開發(fā)者工具時(shí),你第一眼看見的其實(shí)是 appservice_webview 中的 Console 內(nèi)容。
content_webview 對外其實(shí)沒必要暴露出來,因?yàn)槔锩鎴?zhí)行的小程序底層的基礎(chǔ)庫和 開發(fā)者實(shí)際寫的代碼關(guān)系不大。大家理解的話,可以就把顯示的 WXML 假想為 content_webview。
當(dāng)你在實(shí)際預(yù)覽頁面執(zhí)行邏輯時(shí),都是通過 content_webview 把對應(yīng)觸發(fā)的信令事件傳遞給 service_webview。因?yàn)槭请p線程通信,這里只要涉及到 DOM 事件處理或者其他數(shù)據(jù)通信的都是異步的,這點(diǎn)在寫代碼的時(shí)候,其實(shí)非常重要。
如果在開發(fā)時(shí),需要什么困難,歡迎聯(lián)系: 開發(fā)者專區(qū) | 微信開放社區(qū)
前面簡單了解了開發(fā)者工具上,小程序模擬的架構(gòu)。而實(shí)際運(yùn)行到手機(jī)上,里面的架構(gòu)設(shè)計(jì)可能又會(huì)有所不同。主要的原因有:
一開始做小程序的雙線程架構(gòu)和開發(fā)者工具比較類似,content_webview 控制頁面渲染,appservice 在手機(jī)上使用 JSCore 來進(jìn)行執(zhí)行。它的默認(rèn)架構(gòu)圖其實(shí)就是這個(gè):
但是,隨著用戶量的滿滿增多,對小程序的期望也就越高:
這些,我們都知道,所以都在慢慢一點(diǎn)一點(diǎn)的優(yōu)化。考慮到原生 webview 的渲染性能很差,組內(nèi)大神 rex 提出了使用同層渲染來解決性能問題。這個(gè)辦法,不僅搞定了 video 上不能覆蓋其他元素,也提高了一下組件渲染的性能。
開發(fā)者在手機(jī)上具體開發(fā)時(shí),對于某些 高階組件,像 video、canvas 之類的,需要注意它們的通信架構(gòu)和上面的雙線程通信來說,有了一些本質(zhì)上的區(qū)別。為了性能,這里底層使用的是原生組件來進(jìn)行渲染。這里的通信成本其實(shí)就回歸到 native 和 appservice 的通信。
為了大家更好的理解 appservice 和 native 的關(guān)系,這里順便簡單介紹一下 JSCore 的相關(guān)執(zhí)行方法。
在 IOS 和 Android 上,都提供了 JSCore 這項(xiàng)工程技術(shù),目的是為了獨(dú)立運(yùn)行 JS 代碼,而且還提供了 JSCore 和 Native 通信的接口。這就意味著,通過 Native 調(diào)起一個(gè) JSCore,可以很好的實(shí)現(xiàn) Native 邏輯代碼的日常變更,而不需要過分的依靠發(fā)版本來解決對應(yīng)的問題,其實(shí)如果不是特別嚴(yán)謹(jǐn),也可以直接說是一種 "熱更新" 機(jī)制。
在 Android 和 IOS 平臺(tái)都提供了各自運(yùn)行的 JSCore,在國內(nèi)大環(huán)境下運(yùn)行的工程庫為:
這里我們主要以具有官方文檔的 webkit-JavaScriptCore 來進(jìn)行講解。
普遍意義上的 JSCore 執(zhí)行架構(gòu)可以分為三部分 JSVirtualMachine、JSContext、JSValue。由這三者構(gòu)成了 JSCore 的執(zhí)行內(nèi)容。具體解釋參考如下:
大體內(nèi)容可以參考這張架構(gòu)圖:
當(dāng)然,除了正常的執(zhí)行邏輯的上述是三個(gè)架構(gòu)體外,還有提供接口協(xié)議的類架構(gòu)。
使用 JSCore 可以在一個(gè)上下文環(huán)境中執(zhí)行 JS 代碼。首先你需要導(dǎo)入 JSCore:
import JavaScriptCore //記得導(dǎo)入JavaScriptCore
然后利用 Context 掛載的 evaluateScript 方法,像 new Function(xxx) 一樣傳遞字符串進(jìn)行執(zhí)行。
let contet:JSContext = JSContext() // 實(shí)例化 JSContext context.evaluateScript("function combine(firstName, lastName) { return firstName + lastName; }") let name = context.evaluateScript("combine('villain', 'hr')") print(name) //villainhr // 在 swift 中獲取 JS 中定義的方法 let combine = context.objectForKeyedSubscript("combine") // 傳入?yún)?shù)調(diào)用: // 因?yàn)?function 傳入?yún)?shù)實(shí)際上就是一個(gè) arguemnts[fake Array],在 swift 中就需要寫成 Array 的形式 let name2 = combine.callWithArguments(["jimmy","tian"]).toString() print(name2) // jimmytian
如果你想執(zhí)行一個(gè)本地打進(jìn)去 JS 文件的話,則需要在 swift 里面解析出 JS 文件的路徑,并轉(zhuǎn)換為 String 對象。這里可以直接使用 swift 提供的系統(tǒng)接口,Bundle 和 String 對象來對文件進(jìn)行轉(zhuǎn)換。
lazy var context: JSContext? = { let context = JSContext() // 1 guard let commonJSPath = Bundle.main.path(forResource: "common", ofType: "js") else { // 利用 Bundle 加載本地 js 文件內(nèi)容 print("Unable to read resource files.") return nil } // 2 do { let common = try String(contentsOfFile: commonJSPath, encoding: String.Encoding.utf8) // 讀取文件 _ = context?.evaluateScript(common) // 使用 evaluate 直接執(zhí)行 JS 文件 } catch (let error) { print("Error while processing script file: \(error)") } return context }() |
JSExport 是 JSCore 里面,用來暴露 native 接口的一個(gè) protocol,能夠使 JS 代碼直接調(diào)用 native 的接口。簡單來說,它會(huì)直接將 native 的相關(guān)屬性和方法,直接轉(zhuǎn)換成 prototype object 上的方法和屬性。
那在 JS 代碼中,如何執(zhí)行 Swift 的代碼呢?最簡單的方式是直接使用 JSExport 的方式來實(shí)現(xiàn) class 的傳遞。通過 JSExport 生成的 class,實(shí)際上就是在 JSContext 里面?zhèn)鬟f一個(gè)全局變量(變量名和 swift 定義的一致)。這個(gè)全局變量其實(shí)就是一個(gè)原型 prototype。而 swift 其實(shí)就是通過 context?.setObject(xxx) API ,來給 JSContext 導(dǎo)入一個(gè)全局的 Object 接口對象。
那應(yīng)該如何使用該 JSExport 協(xié)議呢?
首先定義需要 export 的 protocol,比如,這里我們直接定義一個(gè)分享協(xié)議接口:
@objc protocol WXShareProtocol: JSExport { // js調(diào)用App的微信分享功能 演示字典參數(shù)的使用 func wxShare(callback:(share)->Void) // setShareInfo func wxSetShareMsg(dict: [String: AnyObject]) // 調(diào)用系統(tǒng)的 alert 內(nèi)容 func showAlert(title: String,msg:String) } |
在 protocol 中定義的都是 public 方法,需要暴露給 JS 代碼直接使用的,沒有在 protocol 里面聲明的都算是 私有 屬性。接著我們定義一下具體 WXShareInface 的實(shí)現(xiàn):
@objc class WXShareInterface: NSObject, WXShareProtocol { weak var controller: UIViewController? weak var jsContext: JSContext? var shareObj:[String:AnyObject] func wxShare(_ succ:()->{}) { // 調(diào)起微信分享邏輯 //... // 成功分享回調(diào) succ() } func setShareMsg(dict:[String:AnyObject]){ self.shareObj = ["name":dict.name,"msg":dict.msg] // ... } func showAlert(title: String, message: String) { let alert = AlertController(title: title, message: message, preferredStyle: .Alert) // 設(shè)置 alert 類型 alert.addAction(AlertAction(title: "確定", style: .Default, handler: nil)) // 彈出消息 self.controller?.presentViewController(alert, animated: true, completion: nil) } // 當(dāng)用戶內(nèi)容改變時(shí),觸發(fā) JS 中的 userInfoChange 方法。 // 該方法是,swift 中私有的,不會(huì)保留給 JSExport func userChange(userInfo:[String:AnyObject]) { let jsHandlerFunc = self.jsContext?.objectForKeyedSubscript("\(userInfoChange)") let dict = ["name": userInfo.name, "age": userInfo.age] jsHandlerFunc?.callWithArguments([dict]) } } |
類是已經(jīng)定義好了,但是我們需要將當(dāng)前的類和 JSContext 進(jìn)行綁定。具體步驟是將當(dāng)前的 Class 轉(zhuǎn)換為 Object 類型注入到 JSContext 中。
lazy var context: JSContext? = { let context = JSContext() let shareModel = WXShareInterface() do { // 注入 WXShare Class 對象,之后在 JSContext 就可以直接通過 window.WXShare 調(diào)用 swift 里面的對象 context?.setObject(shareModel, forKeyedSubscript: "WXShare" as (NSCopying & NSObjectProtocol)!) } catch (let error) { print("Error while processing script file: \(error)") } return context }() |
這樣就完成了將 swift 類注入到 JSContext 的步驟,余下的只是調(diào)用問題。這里主要考慮到你 JS 執(zhí)行的位置。比如,你可以直接通過 JSCore 執(zhí)行 JS,或者直接將 JSContext 和 webview 的 Context 綁定在一起。
直接本地執(zhí)行 JS 的話,我們需要先加載本地的 js 文件,然后執(zhí)行?,F(xiàn)在本地有一個(gè) share.js 文件:
// share.js 文件 WXShare.setShareMsg({ name:"villainhr", msg:"Learn how to interact with JS in swift" }); WXShare.wxShare(()=>{ console.log("the sharing action has done"); }) |
然后,我們需要像之前一樣加載它并執(zhí)行:
// swift native 代碼 // swift 代碼 func init(){ guard let shareJSPath = Bundle.main.path(forResource:"common",ofType:"js") else{ return } do{ // 加載當(dāng)前 shareJS 并使用 JSCore 解析執(zhí)行 let shareJS = try String(contentsOfFile: shareJSPath, encoding: String.Encoding.utf8) self.context?.evaluateScript(shareJS) } catch(let error){ print(error) } } |
如果你想直接將當(dāng)前的 WXShareInterface 綁定到 Webview Context 中的話,前面實(shí)例的 Context 就需要直接修改為 webview 的 Context。對于 UIWebview 可以直接獲得當(dāng)前 webview 的Context,但是 WKWebview 已經(jīng)沒有了直接獲取 context 的接口,wkwebview 更推崇使用前文的 scriptMessageHandler 來做 jsbridge。當(dāng)然,獲取 wkwebview 中的 context 也不是沒有辦法,可以通過 KVO 的 trick 方式來拿到。
// 在 webview 加載完成時(shí),注入相關(guān)的接口 func webViewDidFinishLoad(webView: UIWebView) { // 加載當(dāng)前 View 中的 JSContext self.jsContext = webView.valueForKeyPath("documentView.webView.mainFrame.javaScriptContext") as! JSContext let model = WXShareInterface() model.controller = self model.jsContext = self.jsContext // 將 webview 的 jsContext 和 Interface 綁定 self.jsContext.setObject(model, forKeyedSubscript: "WXShare") // 打開遠(yuǎn)程 URL 網(wǎng)頁 // guard let url = URL(string: "https://www.villainhr.com") else { // return //} // 如果沒有加載遠(yuǎn)程 URL,可以直接加載 // let request = URLRequest(url: url) // webView.load(request) // 在 jsContext 中直接以 html 的形式解析 js 代碼 // let url = NSBundle.mainBundle().URLForResource("demo", withExtension: "html") // self.jsContext.evaluateScript(try? String(contentsOfURL: url!, encoding: NSUTF8StringEncoding)) // 監(jiān)聽當(dāng)前 jsContext 的異常 self.jsContext.exceptionHandler = { (context, exception) in print("exception:", exception) } } |
然后,我們可以直接通過上面的 share.js 調(diào)用 native 的接口。
JSCore 實(shí)際上就是在 native 的一個(gè)線程中執(zhí)行,它里面沒有 DOM、BOM 等接口,它的執(zhí)行和 nodeJS 的環(huán)境比較類似。簡單來說,它就是 ECMAJavaScript 的解析器,不涉及任何環(huán)境。
在 JSCore 中,和原生組件的通信其實(shí)也就是 native 中兩個(gè)線程之間的通信。對于一些高性能組件來說,這個(gè)通信時(shí)延已經(jīng)減少很多了。
那兩個(gè)之間通信,是傳遞什么呢?
就是 事件,DOM 操作等。在同層渲染中,這些信息其實(shí)都是內(nèi)核在管理。所以,這里的通信架構(gòu)其實(shí)就變?yōu)椋?/p>
Native Layer 在 Native 中,可以通過一些手段能夠在內(nèi)核中設(shè)置 proxy,能很好的捕獲用戶在 UI 界面上觸發(fā)的事件,這里由于涉及太深的原生知識(shí),我就不過多介紹了。簡單來說就是,用戶的一些 touch 事件,可以直接通過 內(nèi)核暴露的接口,在 Native Layer 中觸發(fā)對應(yīng)的事件。這里,我們可以大致理解內(nèi)核和 Native Layer 之間的關(guān)系,但是實(shí)際渲染的 webview 和內(nèi)核有是什么關(guān)系呢?
在實(shí)際渲染的 webview 中,里面的內(nèi)容其實(shí)是小程序的基礎(chǔ)庫 JS 和 HTML/CSS 文件。內(nèi)核通過執(zhí)行這些文件,會(huì)在內(nèi)部自己維護(hù)一個(gè)渲染樹,這個(gè)渲染樹,其實(shí)和 webview 中 HTML 內(nèi)容一一對應(yīng)。上面也說過,Native Layer 也可以和內(nèi)核進(jìn)行交互,但這里就會(huì)存在一個(gè) 線程不安全的現(xiàn)象,有兩個(gè)線程同時(shí)操作一個(gè)內(nèi)核,很可能會(huì)造成泄露。所以,這里 Native Layer 也有一些限制,即,它不能直接操作頁面的渲染樹,只能在已有的渲染樹上去做節(jié)點(diǎn)類型的替換。
這篇文章的主要目的,是讓大家更加了解一下小程序架構(gòu)模式在開發(fā)者工具和手機(jī)端上的不同,更好的開發(fā)出一些高性能、優(yōu)質(zhì)的小程序應(yīng)用。這也是小程序中心一直在做的事情。最后,總結(jié)一下前面將的幾個(gè)重要的點(diǎn):
手機(jī)端上,會(huì)根據(jù)組件性能要求的不能對應(yīng)優(yōu)化使用不同的通信架構(gòu)。
工作日 8:30-12:00 14:30-18:00
周六及部分節(jié)假日提供值班服務(wù)