iOS混合開發庫(GICXMLLayout)七、JavaScript篇

搬磚的碼農發表於2019-01-04

介紹

GIC從0.3.0版本開始正式支援JavaScript,也就意味你可以直接使用JavaScript來寫業務邏輯,至此開始,結合XMLjs檔案圖片資源等靜態檔案,完全可以將整個的APP做成一個可以熱更新的應用。另外,在開發的時候也可以通過HotReload的方式,無需編譯整個APP就能實時重新整理應用,進一步的加快應用的開發效率。

在最新的0.4.8版本中,GIC已經支援大部分的ES6特性,包括但不限於yieldgeneratorPromise等等,並且也支援了ES8中的asyncawait

GIC在最初的架構中根本就沒有考慮對JavaScript提供支援,資料繫結事件繫結等等功能統統都是為native code設計的。而後來當我考慮想要對JavaScript進行支援的時候,也僅僅是通過behavior來實現。事實上,如果你看過GIC的原始碼,你會發現GIC可以實現對任意指令碼的支援,而實現的方式也僅僅只是通過自定義behavior來實現。從這一點來說,側面反映了behavior功能的強大之處。

GIC對於JavaScript的支援是基於JavaScriptCore這個蘋果官方framework實現的,其實這個framework本來就是開源的,而且還是屬於大名鼎鼎的Webkit其中的模組。再加上Webkit的跨平臺能力,也就意味著只要是基於JavaScriptCore開發的功能,同樣可以移植到安卓上面,也就是意味著GIC在對JavaScript的支援上具備了跨平臺的潛力

對ES6的支援

首先各位要知道的是,iOS對於Es6規範的支援程度在不同的iOS版本中是不同的,iOS8對於ES6是完全不支援的,iOS9部分支援,最新的iOS12基本已經完全支援了ES6的規範。當然,這裡就不列出不同的iOS版本具體支援哪些ES6特性,你只需要知道,不同的iOS版本對於不同的ES6規範支援程度不一就行了。

然而我們的app肯定是要執行在低版本上的,那麼如何解決這個問題?

事實上,GIC本身並沒有提供原生的解決方案,雖然也嘗試過內建babel來實現實時轉碼,但是後來發現效能太差,對於複雜的JS檔案進行轉碼會耗費大量的CPU資源。因此就斷了內建babel的想法。

但是GIC通過為VSCode開發外掛的方法,曲線實現了對ES6的完整支援,VSCode的外掛(GICVSCodeExtension)可以一次性將整個工程的JS程式碼編譯成ES5的程式碼。也就意味你可以放心的在你的工程中編寫ES6的程式碼,然後通過VSCode進行編譯,進而使得你的JS程式碼可以執行在不同的iOS版本上。

事實上,VSCode外掛也是通過babel來轉碼的,並且GIC也自定義了babel外掛。

你只需要通過GIC提供的腳手架來建立專案就能獲得這樣的能力,詳細的腳手架以及IDE支援的介紹可以檢視這篇文章

在最新的0.4.8版本中,新增了對yieldgeneratorPromiseasyncawait的支援。

require

GIC中,開啟一個新頁面相當於在瀏覽器中開啟一個新頁面,頁面跟頁面之間並不能共享JavaScript執行環境(JSContext),每個頁面都有一個獨立的sand box(JSContext)。因此,當一個頁面需要某個功能的時候你需要通過require方法手動的將這個功能引入,每個頁面都是如此,同一個功能如果在不同的頁面中使用,那麼意味著你需要在不同的頁面單獨引入。

當前require的功能更多的類似Nodejs的用法,如果你以前接觸過nodejs的話,那麼很容易理解。

其實,GIC提供的require函式相較於Nodejs中的require函式來說只是一個簡化版的實現,功能比較簡單。

使用module.exports來匯出,使用require函式來匯入,當前require函式支援jsjson檔案的匯入。

比如: 在a.js中定義

class ClassA {
}module.exports = ClassA;
複製程式碼

b.js中引用

const ClassA = require('/js/a.js');
複製程式碼

或者一次性匯出多個,比如: 在a.js中定義

class ClassA {
}class ClassB extends ClassA {
}class ClassC {
}module.exports = {
ClassA, ClassB, ClassC
};
複製程式碼

b.js中引用

const { 
ClassA, ClassB, ClassC
} = require('/js/a.js');
複製程式碼

以上用法,寫過node.js的都很容易明白。唯一的區別就是,GIC中的路徑全部是絕對路徑。但是如果您是使用VSCode開發的話,那麼可以藉助外掛自動將相對路徑編譯成絕對路勁

起初設計require函式實現的時候並沒有參考nodejs,但是後來發現功能實在太弱,無法實現模組化,然後又重新設計了require的實現,但是神奇的是,等我寫完以後才發現,這他喵的不就是nodejs中的require嘛!如果你用VSCode開發,那麼你會發現,當你使用require匯入JS檔案後,VSCode竟然也能準確的識別,並且提供了完整的智慧提示功能。

引入第三方庫

GIC不支援npm包管理工具,如果你想在工程中使用第三方庫的話只能直接將JS檔案引入的方式來使用。拿axios舉例

  1. 下載axios的JS檔案。

    地址如下:unpkg.com/axios@0.18.…

  2. 將下載的JS檔案拷貝到工程的js資料夾下面。
  3. 使用require函式以非module模式引入axios,
    require('./axios.js', false);
    複製程式碼
  4. 直接在程式碼中使用
    axios.get('https://www.sojson.com/open/api/lunar/json.shtml')    .then((response) =>
    {
    console.log(JSON.stringify(response));

    }) .catch(function (err) {
    console.log(err);

    });
    複製程式碼

其他的第三方庫都可以採用這樣的方式引入。但是需要注意的是,第三方庫可能用到了GIC本身不支援的API,這時候就需要你自己以JSAPI擴充套件的方式來提供支援了。

除錯

很遺憾,當前由於JavaScriptCore的限制,GIC並不具備JS除錯的能力。目前除錯手段只能是通過console.log來追蹤,更進一步的是通過直接列印呼叫堆疊的方式來實現方法呼叫追蹤,但是呼叫堆疊的資訊在JSContext中並不詳細。

另外,你也可以通過safari來檢視某個JSContext物件,來檢視JSContext的內容。

目前的除錯手段有限,而作者也在研究如何配合VSCode實現除錯功能,不過目前進展不大。如果有哪位大能之士有方案的話還請告知下。

JSAPI擴充套件。

上面已經提過,GICJavaScript的支援,是通過JavaScriptCore來實現的。具體一點是通過JSContextJSValue來實現的。然而JavaScriptCore本身提供的API是有限的,比如consoleXMLHttpRequestsetTimeout等API是沒有的,只能通過擴充套件來實現。GIC已經提供了一些JavaScriptCore所沒有的API,而其他的API就需要你自己來實現了。

然而對於目前很多從npm安裝的庫來說,很遺憾,GIC不支援。但是你可以使用直接載入js檔案的方式來引入您的工程,但是注意這些庫有可能用到了其他的一些api,那麼這時候就需要你自己擴充套件實現這些API以便提供支援。

其實對於第三方庫的支援已經跟GIC本身沒有關係了,完全可以通過擴充套件+後期編譯的方式來實現支援。不過這樣的工程會比較大,你不可能做到對所有庫的支援,只能針對特定庫做有限的支援。

JSValue注意事項。

在實際的專案開發過程中,免不了要自定義JSAPI的擴充套件,而實現JSAPI的擴充套件那麼你就回避不了對JSValue的使用,但是JSValue還是有些地方需要注意的,否則會為你的程式碼埋下記憶體管理的隱患。

各位在看這篇部落格之前如果沒有接觸過JavaScriptCore相關的內容,我還是建議各位先去了解下JavaScriptCore,尤其是裡面的JSContextJSValue

JSValue的最大問題就在於記憶體管理的問題。

JS和OC在記憶體管理方面是不一樣的,JS的記憶體管理機制被叫做垃圾回收,而OC的記憶體管理是基於引用計數的,因此,這兩種語言如果想要實現互調,那麼必須得解決記憶體管理的問題。而JSValue就是幹這個事的,但是千萬不要以為只要使用了JSValue就萬事無憂了。

JSValue是OC的物件,需要遵循引用計數的記憶體管理機制,而JSValue指向的JS物件卻是垃圾回收的,如果你在程式碼中直接將JSValue作為變數儲存下來那麼等待你的有可能就是迴圈引用。這時候為了解決迴圈引用的問題,就需要JSManagedValue出場了,JSManagedValue專門為了解決這個問題的,即能讓你擁有JS物件,也不會引起迴圈引用的問題,可以理解為OC對JSValue的弱引用。

如果出現了迴圈引用,系統有可能報Attempting to release access but the mutator does not have access這樣的錯誤,這時候App會直接崩潰。那麼這時候就需要檢查你的程式碼中是否迴圈引用了JSValue

RootDataContext

如果要在XML中直接寫JS程式碼,這裡幾個概念需要注意的。具體參考文件

RootDataContext–根資料來源,也即是你在一個頁面中第一個設定的資料來源。比如:

<
page title="Home">
<
behaviors>
<
script path="./Home.js" />
<
script private="true">
$el.dataContext = new Home();
<
/script>
<
/behaviors>
<
/page>
複製程式碼

上面的JS程式碼$el.dataContext = new Home();
就是為page元素設定資料來源,並且這時候,RootDataContext就指向Home的例項

在實際的開發過程中你不會直接接觸RootDataContext,而是通過事件繫結等形式間接的接觸。你可以在資料繫結的表示式事件表示式中通過this指標來訪問。比如:

<
lable text="按鈕" event-tap="js:this.onClicked()"/>
複製程式碼

這樣,在tap事件中,繫結了一個JS回撥,this.onClicked()指向classHomeonClicked方法。

而有的時候你可能需要直接在事件回撥中修改元素的屬性,那麼可以通過$el來訪問該元素本身,然後設定屬性。比如:

<
lable text="按鈕" event-tap="js:$el.text = 'i clicked'"/>
複製程式碼

這樣,當該lable被點選的時候,文字內容就會被修改成i clicked

事實上,上面在為page設定資料來源的時候,就是通過$el來設定的。

yield、generator

GIC在最新的版本中已經提供了對yieldgenerator的支援,而且generator是由native code開發的,並不是babel轉碼支援的,babel轉碼只能提供對yield的支援,但是generator轉碼過後就無法在iOS上執行了,因此我參考了generator的API,以JSAPI擴充套件的方式,用objective-c實現了generator整套API。實現過程我後面準備單寫一篇部落格來介紹如何實現。

GIC也已經提供了對Promise的支援,因此在Promiseyield的基礎上,提供對asyncawait支援就變得順理成章了。事實上,GIC已經支援ES8中的asyncawait功能了。

資料繫結

GIC本身是支援資料繫結的,在引入JavaScript後也提供了資料繫結的功能。但是JS的資料繫結跟naitve coed的資料繫結在實現方式上是不一樣的,GIC在實現JS的資料繫結過程中,充分參考了VUE中的實現方式,並且研究了VUE的原始碼,最終將VUE的實現方式移植到了GIC中。可以參考這篇文章

來源:https://juejin.im/post/5c0093376fb9a049d131dc9a

相關文章