前言
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
方法。想要实现这个功能有以下两种方式
方式一
用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。下面可能还会写一篇文章分析一下如何去实现一个简单的代码编辑器,敬请期待!!!