如何实现一个乞丐版JSBox (一) 引擎篇

前言

代码地址前端

JSBox是钟大创造的一个能够用JavaScript来编写脚本的一个APP。它提供了一套界面方案,而后也提供了基本上全部的原生能力。必定程度上能够看作是一个简化版的小程序。而且它内部还实现了一个简易的代码编辑器,你能够直接在APP上写代码啦~若是你感兴趣仍是强烈建议你去下一个JSBox支持一下钟大的。git

对于一个APP极度喜好的时候,我通常是会尝试去实现一下它的功能。以前尝试仿了下Cosmos(你们能够看看点点star ^_^)。最近又花了一段时间实现了一下JSBox的基础显示功能和基础的代码编辑功能。在这过程中也遇到了一些问题。这里就分两篇文章一篇介绍引擎一篇介绍代码编辑器。github

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,介绍一下简单的用法: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

经过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。下面可能还会写一篇文章分析一下如何去实现一个简单的代码编辑器,敬请期待!!!

相关文章
相关标签/搜索