如何實現一個乞丐版JSBox (一) 引擎篇

小和山吳彥祖發表於2018-06-12

前言

程式碼地址

JSBox是鍾大創造的一個可以用JavaScript來編寫指令碼的一個APP。它提供了一套介面方案,然後也提供了基本上所有的原生能力。一定程度上可以看做是一個簡化版的小程式。並且它內部還實現了一個簡易的程式碼編輯器,你可以直接在APP上寫程式碼啦~如果你感興趣還是強烈建議你去下一個JSBox支援一下鍾大的。

對於一個APP極度喜愛的時候,我一般是會嘗試去實現一下它的功能。之前嘗試仿了下Cosmos(大家可以看看點點star ^_^)。最近又花了一段時間實現了一下JSBox的基礎顯示功能和基礎的程式碼編輯功能。在這過程當中也遇到了一些問題。這裡就分兩篇文章一篇介紹引擎一篇介紹程式碼編輯器。

JavaScriptCore介紹

整個引擎是建立在JavaScriptCore上建立的,這裡對它做一個簡單的介紹。JavaScriptCore 提供了js和native互動的能力。你可以不通過瀏覽器直接執行一段js的程式碼,你也可以直接往js裡注入一個原生的物件。需要注意一點JavaScriptCore裡沒有Dom window之類的這些內容。

JSValue

JSValue就是js環境裡的物件。它可能是任何的型別,可能是陣列,可能是字串也可能是一個js的方法。js和原生資料傳遞的時候有一套基礎的型別轉換的對應表。JavaScriptCore會幫我們做一層基礎的轉換。

   Objective-C type  |   JavaScript type
 --------------------+---------------------
         nil         |     undefined
        NSNull       |        null
       NSString      |       string
       NSNumber      |   number, boolean
     NSDictionary    |   Object object
       NSArray       |    Array object
        NSDate       |     Date object
       NSBlock (1)   |   Function object (1)
          id (2)     |   Wrapper object (2)
        Class (3)    | Constructor object (3)
複製程式碼

JSValue也有一些toXXX方法能夠將js資料轉換為原生的資料。

JSContext

我們會大量的使用到JSContext,介紹一下簡單的用法:

用法一

JSContext *context = [[JSContext alloc] init];
[context evaluateScript:@"var a = 'hello word';"];
NSLog(@"%@",[context[@"a"] toString]);
複製程式碼

這段程式碼裡我先在js環境裡宣告瞭一個a變數,然後通過context拿到了這個物件列印了物件的值。這裡的這個變數也可以是一個js的方法。如果是一個方法可以通過callWithArguments:直接呼叫。

用法二

native:

 context[@"log"] = ^(JSValue *value) {
    NSLog(@"%@",[value toString]);
 };
複製程式碼

js:

log('hello word');
複製程式碼

這段程式碼裡我們先往js注入了一個加log的方法,這個方法接受一個引數。然後js裡直接通過log這個名字就能呼叫這個方法。方法的實現就是block裡的程式碼邏輯。

用法三

我們可以直接拿到js裡定義的方法,直接在native呼叫。 js

var sum = function(a,b) {
    return a + b
}
複製程式碼

native

[context[@"sum"] callWithArguments:@[@(1),@(2)]];
複製程式碼

JSExport

通過JSExport我們能直接把native的物件傳遞給js。js可以直接拿到屬性呼叫物件的方法。具體使用方式如下

@protocol studentExport <JSExport>
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSInteger age;
- (void)study;
@end

@interface student : NSObject <studentExport>
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSInteger age;
- (void)study;
@end
複製程式碼

我們建立了一個繼承自JSExport的協議,協議裡實現了兩個屬性和一個方法。然後我們建立的物件繼承自這個協議實現了協議裡的方法和屬性。這樣一來如果我們建立一個student物件,然後把這個物件傳遞給js的時候,js能夠直接拿到name age屬性,並且能夠直接呼叫study方法。非常的神奇~

JSBox基礎用法介紹

接下去看的過程中,如果有一些內容有些疑問的話你可以先看看JSBox文件

$ui.render({
  props: {
    id: "label",
    title: "Hello, World!"
  },
  views: [
    {
      type: "label",
      porps: {
          text : 'hello word'
      }
      layout: function(make, view) {
        make.center.equalTo(view.super)
        make.size.equalTo($size(100, 100))
      }
    }
  ]
})
複製程式碼

上面這段程式碼實現的功能就是彈出一個控制器,在這個控制器當中新增了一個label。我們傳遞進去了一個js的物件。首先這個物件有一個props的屬性,這個屬性一看就是一些當前控制器的一些屬性,比如title肯定就是設定標題了。之後是一個views陣列,顯而易見這個views裡存放的是一些view的資料結構。裡面是一個type屬性這個屬性就是對應著當前view的型別,layout就是對應著當前view的佈局。porps裡存放著view的屬性。

JSBox基礎功能實現細節

結合上面提到的JavaScriptCore的使用,我們很容易就能推斷出JSContext肯定需要定義一個方法來和$ui.render這個方法呼叫相對應的方法。我們傳遞進去了一個js物件,上面我們已經對這個資料結構進行了一下大致的分析。接下來就是分析一下要如何解析這個物件並顯示這個物件。傳遞到native的jsvalue物件我們可以通過jsvalue[@'xxx'] 這樣的方式拿到具體的資料,拿到的這個資料可以是js方法也可以是一些基礎的資料。

控制元件的建立

結合上面的分析,控制元件的建立也就是通過拿到jsvalue裡的views引數然後解析出views陣列裡的每個view,通過view的type屬性和文件裡的原生控制元件一一對應建立出控制元件就可以了。

控制元件的屬性

屬性的賦值

賦值操作相對理解還是很容易如果這個屬性名和原生想要賦值的屬性名一一對應我們只需要用kvc設定一下就ok了。如果名字和屬性不一致我們只需用category加一個當前名字的屬性,在set方法裡做正確的引數設定就ok了。

屬性的獲取

屬性的獲取要求js能夠拿到原生物件的屬性,要實現這個功能我們需要用到JSExport,我們需要把支援獲取的屬性都新增到自定義繼承自JSExport的協議裡,然後建立一個category繼承這個協議。這樣一來我們把原生物件傳遞給js的時候,js端就能拿到屬性。

@protocol ZHNJSBoxUILabelExport <JSExport>
@property (nonatomic, copy) NSString *text;
@property (nonatomic, strong) UIFont *font;
@property (nonatomic, strong) UIColor *textColor;
@property (nonatomic, strong) UIColor *shadowColor;
@property (nonatomic, assign) NSInteger align;
@property (nonatomic, assign) NSInteger lines;
@property (nonatomic, assign) BOOL autoFontSize;
@end

@interface UILabel (ZHNJSBoxUILabel) <ZHNJSBoxUILabelExport>
@property (nonatomic, assign) NSInteger align;
@property (nonatomic, assign) NSInteger lines;
@property (nonatomic, assign) BOOL autoFontSize;
@end
複製程式碼

控制元件的位置

layout: function(make, view) {
    make.center.equalTo(view.super)
    make.size.equalTo($size(100, 100))
}
複製程式碼

作為iOS開發我們一眼就能看出來,這是Masonry的寫法。js裡面想要make.center.equalTo(view.super)呼叫不出錯,我們需要make裡有center裡有屬性center裡有equalTo方法。想要實現這個功能有以下兩種方式

方式一

JSExportMASConstraintMaker新增所需要的屬性,給MASConstraint新增方法。有一個需要注意的點是JSBox裡統一用的是equalTo方法,但是Masonry裡類似height,width等等的基礎資料型別是需要mas_equalTo把傳遞的值包一下,所以需要特殊處理。equalTo傳遞進去的引數也需要做一層轉換,原生是採用mas_xxx而JSBox追求簡潔是直接取消了前面的mas_

方式二

方式一思路理清楚了,程式碼實現起來是沒啥大的難度的。但是我最後發現它需要去改動Masonry這個庫的細節。所以我嘗試去看看有沒有其他的方式來實現,最後我嘗試用的是JSPatch的實現方式通過正則匹配然後讓屬性走一個統一的方法,方法走一個統一的方法。make.center.equalTo(view.super) 正則完的結構是 make.__lp('center').__lr('equalTo')('view.super')。我們要明確一個點就是原生Masonry實現鏈式呼叫是通過屬性+block的方式的,也就是.left 或者.right等等之類的屬性我們是可以用[make left]來代替的。我們先給js的基類新增__lp__lr兩個方法。屬性都會走統一的方法把屬性名傳遞給原生,原生直接用[maker performSelector:NSSelectorFromString(property)]的方式呼叫就ok了。方法稍微有些不同,在js端我們需要把__lr('equalTo')('view.super')合成一個方法的呼叫。

var args = Array.prototype.slice.call(arguments);
return oc_LayoutRelation(slf,methodName,args[0]);
複製程式碼

拿到方法名和引數傳遞給原生呼叫。原生先用[maker performSelector:NSSelectorFromString(seletName)]拿到block,然後在直接呼叫block返回引數就ok了。

控制元件的事件

前面已經提到了,js可以直接傳遞一個方法到native。native拿到這個方法直接callWithArguments:就直接可以了。也就是說我們只需要把這個js方法儲存一下。原生方法的邏輯裡呼叫一下這個js方法就ok了。這個地方遇到了一個比較蛋疼的記憶體問題,首先JavaScriptCore它有一個自己的記憶體管理機制,然後native也有一個記憶體管理機制。如果我們直接把傳遞進來的jsvalue設為屬性,那麼當js端想要釋放這個js物件的時候,它會發現它的記憶體被原生管理了,所以就沒有許可權釋放那麼它就會直接奔潰。翻了一下文件,發現有一個叫JSManagedValue這個物件對內部的jsvalue是一個weak引用,看著好像是解決引用問題的。試了一下之後發現,它不會對js物件的生命週期產生影響,也就是說js物件被釋放了之後我們在native是拿不到這個方法了。

一籌莫展的時候我去看了一下JSPatch的實現,JSPAtch用的是一個全域性的字典來存放。因為JSPAtch的JSContext是一個單例物件,也就是說它裡的JSValue的釋放是和整個app的生命週期繫結在一起了。所以不存在說上面的問題。但是我們這裡的JSContext顯然是要針對每一個指令碼的,所以還是不太一樣的。又一籌莫展的被卡了好幾天沒找到方法,然後我嘗試去看了下weex的程式碼,整個專案工程量有點大,沒很仔細看但是我發現它呼叫這些事件方法的時候都是通過context['name']拿到js的方法然後直接呼叫的。結合JSPatch裡的程式碼我想到,當解析到一個JS方法的時候我可以往js的基類物件裡新增這麼一個方法屬性。然後需要用到的時候通過名字拿到這個方法就ok了。這樣這個方法的生命週期就和JSContext繫結在一起了,當它釋放的時候那麼這些方法也就被釋放了。當然JSContext裡搞一個全域性的字典存一下方法也是可行的。

想法發散

按照我現在的眼光看來,其實類似的框架基礎的實現思路是類似的。類似weex 小程式之類的只是在這個的基礎上加了一層編譯操作,你可以直接編寫前端程式碼,然後它們最終會把這些前端程式碼編譯成js的程式碼。如果你有一些動態化的需求,但是你又不想引入weex之類的很重的框架,你其實可以自己嘗試去實現一套自己的動態化框架。

總結

上面大致介紹了一些基本的實現思路和一些問題。這篇主要講的是JSBox的基礎引擎,我仿的差不多隻實現了1/100。下面可能還會寫一篇文章分析一下如何去實現一個簡單的程式碼編輯器,敬請期待!!!

相關文章