代碼地址前端
JSBox是鍾大創造的一個能夠用JavaScript
來編寫腳本的一個APP。它提供了一套界面方案,而後也提供了基本上全部的原生能力。必定程度上能夠看作是一個簡化版的小程序。而且它內部還實現了一個簡易的代碼編輯器,你能夠直接在APP上寫代碼啦~若是你感興趣仍是強烈建議你去下一個JSBox支持一下鍾大的。git
對於一個APP極度喜好的時候,我通常是會嘗試去實現一下它的功能。以前嘗試仿了下Cosmos(你們能夠看看點點star ^_^)。最近又花了一段時間實現了一下JSBox的基礎顯示功能和基礎的代碼編輯功能。在這過程中也遇到了一些問題。這裏就分兩篇文章一篇介紹引擎一篇介紹代碼編輯器。github
整個引擎是創建在JavaScriptCore
上建立的,這裏對它作一個簡單的介紹。JavaScriptCore
提供了js和native交互的能力。你能夠不經過瀏覽器直接執行一段js的代碼,你也能夠直接往js裏注入一個原生的對象。須要注意一點JavaScriptCore
裏沒有Dom
window
之類的這些內容。小程序
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
,介紹一下簡單的用法:bash
JSContext *context = [[JSContext alloc] init];
[context evaluateScript:@"var a = 'hello word';"];
NSLog(@"%@",[context[@"a"] toString]);
複製代碼
這段代碼裏我先在js環境裏聲明瞭一個a變量,而後經過context拿到了這個對象打印了對象的值。這裏的這個變量也能夠是一個js的方法。若是是一個方法能夠經過callWithArguments:
直接調用。weex
native:數據結構
context[@"log"] = ^(JSValue *value) {
NSLog(@"%@",[value toString]);
};
複製代碼
js:app
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咱們能直接把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文檔
$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的屬性。
結合上面提到的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
方法。想要實現這個功能有如下兩種方式
用JSExport
給MASConstraintMaker
添加所須要的屬性,給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。下面可能還會寫一篇文章分析一下如何去實現一個簡單的代碼編輯器,敬請期待!!!