Hybrid App: 了解JavaScript如何与Native实现混合开发
一、简介
Hybrid Development混合开发是目前移动端开发异常火热的新兴技术,它能够实现跨平台开发,极大地节约了人力和资源成本。跨平台开发催生了很多新的开源框架,就目前而言,在混合开发中比较流行的有FaceBook开源React Native,有Goggle开源的Flutter。React Native实现的是通过下发JS脚本的方式达到JS与Native交互。Flutter实现的则是通过采用现代响应式框架来构建UI,Flutter与ReactiveCocoa框架配合使用最佳。当然开发者也可以在Native中内嵌WebView的方式(WebKit)实现混合开发。虽然方式不同,但目的相同,都是跨平台,殊途同归吧。对跨平台有了粗略的了解后,再来看看iOS系统中对JS与Native是如何交互的,其实,系统是给开发者提供了一个极其强大的框架来实现这个功能的,即JavaScriptCore框架。这个框架通过定义JSValue值对象和声明JSExport协议作为桥梁完成Native与JS的通信。JS虽然是单线程语言,但是iOS是支持多线程执行任务的,开发者可以在异步情况下执行任意一个环境的JavaScript代码。大概结构图如下:
二、分析
参考这上图,可以看出JavaScriptCore框架结构还是很清晰的,JavaScriptCore中有那么几个核心的类在开发者是很常用的,需要弄懂它们代表的意思。
三、API
知道了这几个核心类的概念已经对这个框架有了个基本的认识,具体的API如何使用,我们可以选择性点击去深入研究一下。只有对它们的属性和方法都了如指掌,开发起来才能得心应手,手到擒来。哎呀妈,不废话了。。。例如JSContext和JSValue开发中必用的类,额外的可能还会用JSManagerValue,如下:
JSContetxt类:
//初始化,可以选择对应的虚拟机- (instancetype)init;- (instancetype)initWithVirtualMachine:(JSVirtualMachine *)virtualMachine;//执行js代码,返回js值对象- (JSValue *)evaluateScript:(NSString *)script;- (JSValue *)evaluateScript:(NSString *)script withSourceURL:(NSURL *)sourceURL;//获取当前的js上下文+ (JSContext *)currentContext;//获取当前的js执行函数,返回js值对象+ (JSValue *)currentCallee;//获取当前的js函数中this指向的对象,返回js值对象+ (JSValue *)currentThis;//获取当前的js函数中的所有参数+ (NSArray *)currentArguments;//js的全局对象@property (readonly, strong) JSValue *globalObject;//js执行的异常数据@property (strong) JSValue *exception; @property (copy) void(^exceptionHandler)(JSContext *context, JSValue *exception);//js运行的虚拟机@property (readonly, strong) JSVirtualMachine *virtualMachine;//js上下文名称@property (copy) NSString *name;//分类@interface JSContext (SubscriptSupport)//获取和设置属性为js全局对象- (JSValue *)objectForKeyedSubscript:(id)key;- (void)setObject:(id)object forKeyedSubscript:(NSObject <NSCopying> *)key;@end//分类(C函数风格)@interface JSContext (JSContextRefSupport)//获取和设置全局上下文+ (JSContext *)contextWithJSGlobalContextRef:(JSGlobalContextRef)jsGlobalContextRef; @property (readonly) JSGlobalContextRef JSGlobalContextRef;@end
JSValue类:
//js上下文@property (readonly, strong) JSContext *context;//使用OC数据初始化js值对象,创建有值的JSValue+ (JSValue *)valueWithObject:(id)value inContext:(JSContext *)context;+ (JSValue *)valueWithBool:(BOOL)value inContext:(JSContext *)context;+ (JSValue *)valueWithDouble:(double)value inContext:(JSContext *)context;+ (JSValue *)valueWithInt32:(int32_t)value inContext:(JSContext *)context;+ (JSValue *)valueWithUInt32:(uint32_t)value inContext:(JSContext *)context;+ (JSValue *)valueWithPoint:(CGPoint)point inContext:(JSContext *)context;+ (JSValue *)valueWithRange:(NSRange)range inContext:(JSContext *)context;+ (JSValue *)valueWithRect:(CGRect)rect inContext:(JSContext *)context;+ (JSValue *)valueWithSize:(CGSize)size inContext:(JSContext *)context;//使用OC数据初始化js值对象,创建空的JSValue+ (JSValue *)valueWithNewObjectInContext:(JSContext *)context;+ (JSValue *)valueWithNewArrayInContext:(JSContext *)context;+ (JSValue *)valueWithNewRegularExpressionFromPattern:(NSString *)pattern flags:(NSString *)flags inContext:(JSContext *)context;+ (JSValue *)valueWithNewErrorFromMessage:(NSString *)message inContext:(JSContext *)context;+ (JSValue *)valueWithNewPromiseInContext:(JSContext *)context fromExecutor:(void (^)(JSValue *resolve, JSValue *reject))callback;+ (JSValue *)valueWithNewPromiseResolvedWithResult:(id)result inContext:(JSContext *)context;+ (JSValue *)valueWithNewPromiseRejectedWithReason:(id)reason inContext:(JSContext *)context;+ (JSValue *)valueWithNewSymbolFromDescription:(NSString *)description inContext:(JSContext *)context;+ (JSValue *)valueWithNullInContext:(JSContext *)context;+ (JSValue *)valueWithUndefinedInContext:(JSContext *)context;//js数据转OC数据- (id)toObject;- (id)toObjectOfClass:(Class)expectedClass;- (BOOL)toBool;- (double)toDouble;- (int32_t)toInt32;- (uint32_t)toUInt32;- (NSNumber *)toNumber;- (NSString *)toString;- (NSDate *)toDate;- (NSArray *)toArray;- (NSDictionary *)toDictionary;- (CGPoint)toPoint;- (NSRange)toRange;- (CGRect)toRect;- (CGSize)toSize;//js值对象判断@property (readonly) BOOL isUndefined; @property (readonly) BOOL isNull; @property (readonly) BOOL isBoolean; @property (readonly) BOOL isNumber; @property (readonly) BOOL isString; @property (readonly) BOOL isObject; @property (readonly) BOOL isArray; @property (readonly) BOOL isDate; @property (readonly) BOOL isSymbol;- (BOOL)isEqualToObject:(id)value;- (BOOL)isEqualWithTypeCoercionToObject:(id)value;- (BOOL)isInstanceOf:(id)value;//js调用函数- (JSValue *)callWithArguments:(NSArray *)arguments;- (JSValue *)constructWithArguments:(NSArray *)arguments;- (JSValue *)invokeMethod:(NSString *)method withArguments:(NSArray *)arguments;//js属性设置- (JSValue *)valueForProperty:(JSValueProperty)property;- (void)setValue:(id)value forProperty:(JSValueProperty)property;- (BOOL)deleteProperty:(JSValueProperty)property;- (BOOL)hasProperty:(JSValueProperty)property;- (void)defineProperty:(JSValueProperty)property descriptor:(id)descriptor;- (JSValue *)valueAtIndex:(NSUInteger)index;- (void)setValue:(id)value atIndex:(NSUInteger)index;- (JSValue *)objectForKeyedSubscript:(id)key;- (JSValue *)objectAtIndexedSubscript:(NSUInteger)index;- (void)setObject:(id)object forKeyedSubscript:(id)key;- (void)setObject:(id)object atIndexedSubscript:(NSUInteger)index;+ (JSValue *)valueWithJSValueRef:(JSValueRef)value inContext:(JSContext *)context;//OC与JS类型对应关系 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) ---------------------+---------------------
JSManagerValue类:
//对JSValue进行一层包装,对内存进行有效的管理,防止提前或者过度释放+ (JSManagedValue *)managedValueWithValue:(JSValue *)value;+ (JSManagedValue *)managedValueWithValue:(JSValue *)value andOwner:- (instancetype)initWithValue:(JSValue *)value; @property (readonly, strong) JSValue *value;
四、案例
[1] 首先打开Safari浏览器的web检查器,会用来查看js运行的效果 ,控制台打印
[2] 导入JavaScriptCore框架
[3] 导入头文件开始测试,Native调用JS
[3-1] 调用无参数的JS函数
native.js
-(void)nativeCallJs { //方式一//从js文件获取js代码NSString *path = [[NSBundle mainBundle] pathForResource:@"native" ofType:@"js"]; NSData *jsData = [NSData dataWithContentsOfFile:path]; NSString *script = [[NSString alloc] initWithData:jsData encoding:NSUTF8StringEncoding]; //执行js代码 [self.jsContext evaluateScript:script]; }
-(void)nativeCallJs { //方式二//js代码写在端上NSString *script = @" (function(){ console.log("native call js ------- Wellcome Native");})();"; //执行js代码 [self.jsContext evaluateScript:script]; }
- (void)viewDidLoad { [super viewDidLoad];//js上下文self.jsContext = [[JSContext alloc] init]; //native调用js [self nativeCallJs]; }
[3-2] 调用有参数的JS函数
native.js
-(void)nativeCallJsWithArguments:(NSString *)argument { //方式一//从js文件获取js代码NSString *path = [[NSBundle mainBundle] pathForResource:@"native" ofType:@"js"]; NSData *jsData = [NSData dataWithContentsOfFile:path]; NSString *jsString = [[NSString alloc] initWithData:jsData encoding:NSUTF8StringEncoding]; //拼接js参数NSString *script = [NSString stringWithFormat:jsString,argument]; //执行js代码 [self.jsContext evaluateScript:script]; }
-(void)nativeCallJsWithArguments:(NSString *)argument { //方式二//js代码写在端上NSString *jsString = @" function receive(argument) { console.log("native call js ------- Wellcome "+argument); }; receive('%@')";//拼接js参数NSString *script = [NSString stringWithFormat:jsString,argument]; //执行js代码 [self.jsContext evaluateScript:script]; }
- (void)viewDidLoad { [super viewDidLoad]; //js上下文self.jsContext = [[JSContext alloc] init]; //native调用js[self nativeCallJsWithArguments:@"我的老哥"]; }
[4] 导入头文件开始测试,JS调用Native
注意:调用包括无参数和有参数的OC方法,这里使用代码块Block为例
-(void)jsCallNative { //定义无参数blockvoid (^Block1)(void) = ^(){ NSLog(@"js call native ------- hello JavaScript"); }; //定义有参数blockvoid (^Block2)(NSString *) = ^(NSString *argument){ NSLog(@"js call native ------- hello JavaScript----Wellcome %@",argument); }; //设置block为JSContext全局对象的属性,然后可以在safari控制台执行函数oc_block()输出打印;[self.jsContext setObject:Block1 forKeyedSubscript:@"oc_block1"]; [self.jsContext setObject:Block2 forKeyedSubscript:@"oc_block2"]; }
- (void)viewDidLoad { [super viewDidLoad]; //js上下文self.jsContext = [[JSContext alloc] init];//js调用native [self jsCallNative]; }
[5]导入头文件开始测试, OC和JS对象的映射
//OC与JS数据传递的数据就是JSValue值对象,存储在JS的全局对象中//存和取的过程[self.jsContext setObject:(id) forKeyedSubscript:(NSObject<NSCopying> *)]; [self.jsContext objectForKeyedSubscript:(id)]
[5-1] 系统提供的OC数据类型,不用特殊存储,可以直接存取
//系统提供的OC数据类型,不用特殊存储,可以直接存取[self.jsContext setObject:@"mac" forKeyedSubscript:@"os"]; JSValue *osValue = [self.jsContext objectForKeyedSubscript:@"os"]; NSString *osName = [osValue toString]; NSLog(@"-----osName = %@-----",osName);
2019-11-12 14:58:17.471840+0800 混合开发[10499:365654] -----osName = mac-----
[5-2] 特殊的OC类型,如自定义对象,则必须遵守JSExport协议,JS才能拿到自定义对象的所有属性和方法
#import <UIKit/UIKit.h>#import <JavaScriptCore/JavaScriptCore.h>//遵守JSExport协议,使得JS在上下文中可以获取到OC中定义的属性和方法@protocol PersonProtocol <JSExport>@property (nonatomic, copy) NSString *name; @property (nonatomic, assign) int age; @property (nonatomic, assign) int grade; @property (nonatomic, assign) float score;-(void)description;@end@interface Person : NSObject<PersonProtocol>@property (nonatomic, copy) NSString *name; @property (nonatomic, assign) int age; @property (nonatomic, assign) int grade; @property (nonatomic, assign) float score;-(void)description;@end#import "Person.h"@implementation Person-(void)description { NSLog(@"姓名:name = %@",self.name); NSLog(@"年龄:age = %d",self.age); NSLog(@"年级:grade = %d",self.grade); NSLog(@"分数:score = %.1f",self.score); }@end
//特殊的OC类型,自定义对象,则必须遵守JSExport协议,JS才能拿到自定义对象的所有属性和方法Person *person = [[Person alloc] init]; person.name = @"张三"; person.age = 20; person.grade = 5; person.score = 98; [self.jsContext setObject:person forKeyedSubscript:@"personEntity"]; JSValue *personValue = [self.jsContext objectForKeyedSubscript:@"personEntity"]; //personEntity为OC在JS的对象形式Person *xyq_person = (Person *)[personValue toObject]; [xyq_person description];
2019-11-12 14:58:17.472563+0800 混合开发[10499:365654] 姓名:name = 张三2019-11-12 14:58:17.472709+0800 混合开发[10499:365654] 年龄:age = 202019-11-12 14:58:17.472810+0800 混合开发[10499:365654] 年级:grade = 52019-11-12 14:58:17.472889+0800 混合开发[10499:365654] 分数:score = 98.0
五、实践
到现在为止,相信我们对JS和Native的交互原理有了自己的理解。在案例中使用了js文件下发和解析的方式实现了Native执行JS代码,这个正是Facebook开源的React Native的设计思路。React Native支持跨平台,通过一套js文件就可以在Andriod和iOS上完成Native的界面渲染。现在我们通过一个小测试来模拟Hybrid App的构建原理,通过按钮点击切换控制器视图的背景色。
(1) 创建JavaScript脚本,在脚本中创建Native需要的任意UI控件存到数组,作为函数的返回值
UIKit.js
//定义一个自调用函数,JavaScript脚本加载完成立即执行(function(){return renderUI(); })();/* JavaScript脚本 定义一个Label类 * rect:尺寸 text:文本 color:颜色 */function Label(rect,text,fontSize,textColor,textAlignment,bgColor){this.rect = rect;this.text = text;this.fontSize = fontSize;this.textColor = textColor;this.textAlignment = textAlignment; //NSTextAlignmentCenter = 1this.bgColor = bgColor;this.type = "Label"; }/* JavaScript脚本 定义一个Button类 * rect:尺寸 text:文本 color:颜色 callFunction:函数 */function Button(rect,title,fontSize,titleColor,bgColor,callFunction){this.rect = rect;this.title = title;this.fontSize = fontSize;this.titleColor = titleColor;this.bgColor = bgColor;this.callFunction = callFunction;this.type = "Button"; }/* JavaScript脚本 Rect类 * x:坐标x y:坐标y w:宽度 h:高度 */function Rect(x,y,w,h){this.x = x;this.y = y;this.w = w;this.h = h; }/* JavaScript脚本 Color类 * r:red g:green b:black a:alpa */function Color(r,g,b,a){this.r = r;this.g = g;this.b = b;this.a = a; }//渲染方法,实例化上面类的对象function renderUI() { //创建js标签对象var screenWidth = 375.0;var labeWidth = 200;var labelRect = new Rect((screenWidth-labeWidth)*0.5, 100, labeWidth, 44);var labeFontSize = 20;var labelTextColor = new Color(1,0,0,1);var labelBgColor = new Color(1,1,0,1);var label = new Label(labelRect, "I From JS", labeFontSize, labelTextColor, 1, labelBgColor); //创建js按钮对象var buttonWidth = 200;var buttonRect = new Rect((screenWidth-buttonWidth)*0.5, 200, buttonWidth, buttonWidth);var buttonFontSize = 40;var buttonTitleColor = new Color(1,0,1,1);var buttonbgColor = new Color(1,1,1,1);var button = new Button(buttonRect,"Button",buttonFontSize,buttonTitleColor,buttonbgColor,function(r,g,b){var randColor = new Color(r,g,b,1); configEntity.chageViewColor(randColor); }); //返回js对象return [label, button]; }
(2) 创建自定义对象,遵守JSExport协议,添加为JS的全局对象的属性,作为与Native交互的桥接器
//自定义Config类#import <Foundation/Foundation.h>#import <UIKit/UIKit.h>#import <JavaScriptCore/JavaScriptCore.h>NS_ASSUME_NONNULL_BEGIN@protocol ConfigProtocol <JSExport> -(void)chageViewColor:(JSValue *)colorValue;@end@interface Config : NSObject<ConfigProtocol>@property (nonatomic, strong) UIViewController *currentVc;-(void)chageViewColor:(JSValue *)colorValue; //改变当前控制器视图背景色@endNS_ASSUME_NONNULL_END
//Created by 夏远全 on 2019/11/12.#import "Config.h"@implementation Config-(void)chageViewColor:(JSValue *)colorValue { CGFloat red = colorValue[@"r"].toDouble; CGFloat green = colorValue[@"g"].toDouble; CGFloat blue = colorValue[@"b"].toDouble; CGFloat alpha = colorValue[@"a"].toDouble; self.currentVc.view.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:alpha]; }@end
(3) 在VC中解析JavaScript脚本,获取UI控件元素,进行界面的渲染
#import "ViewController.h"#import <JavaScriptCore/JavaScriptCore.h>#import "Person.h"#import "Config.h"@interface ViewController () @property (nonatomic, strong) JSContext *jsContext; @property (nonatomic, strong) NSMutableArray *actions; //所有的回调函数@end@implementation ViewController- (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor redColor]; //js上下文self.jsContext = [[JSContext alloc] init]; //从JS文件获取UI进行渲染 [self renderUIFromJs]; }-(void)renderUIFromJs { //创建Config对象Config *config = [[Config alloc] init]; config.currentVc = self; [self.jsContext setObject:config forKeyedSubscript:@"configEntity"]; //从js文件获取js代码NSString *path = [[NSBundle mainBundle] pathForResource:@"UIKit" ofType:@"js"]; NSData *jsData = [NSData dataWithContentsOfFile:path]; NSString *script = [[NSString alloc] initWithData:jsData encoding:NSUTF8StringEncoding]; //执行js代码JSValue *jsValue = [self.jsContext evaluateScript:script];for (int i=0; i<jsValue.toArray.count; i++) { //取出每一个控件对象值JSValue *subValue = [jsValue objectAtIndexedSubscript:i]; //创建控件NSString *type = [subValue objectForKeyedSubscript:@"type"].toString;if ([type isEqualToString:@"Label"]) { //this.rect = rect;//this.text = text;//this.fontSize = fontSize;//this.textColor = textColor;//this.textAlignment = textAlignment; //NSTextAlignmentCenter = 1//this.bgColor = bgColor;//this.type = "Label"; CGFloat X = subValue[@"rect"][@"x"].toDouble; CGFloat Y = subValue[@"rect"][@"y"].toDouble; CGFloat W = subValue[@"rect"][@"w"].toDouble; CGFloat H = subValue[@"rect"][@"h"].toDouble; NSString *text = subValue[@"text"].toString; NSInteger fontSize = subValue[@"fontSize"].toInt32; UIColor *textColor = [UIColor colorWithRed:subValue[@"textColor"][@"r"].toDouble green:subValue[@"textColor"][@"g"].toDouble blue:subValue[@"textColor"][@"b"].toDouble alpha:subValue[@"textColor"][@"a"].toDouble]; UIColor *bgColor = [UIColor colorWithRed:subValue[@"bgColor"][@"r"].toDouble green:subValue[@"bgColor"][@"g"].toDouble blue:subValue[@"bgColor"][@"b"].toDouble alpha:subValue[@"bgColor"][@"a"].toDouble]; NSTextAlignment alignment = subValue[@"textAlignment"].toInt32; UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(X, Y, W, H)]; label.text = text; label.font = [UIFont systemFontOfSize:fontSize]; label.textColor = textColor; label.textAlignment = alignment; label.backgroundColor = bgColor; [self.view addSubview:label]; }if ([type isEqualToString:@"Button"]) { //this.rect = rect;//this.title = title;//this.fontSize = fontSize;//this.titleColor = titleColor;//this.bgColor = bgColor;//this.type = "Button";//this.callFunction = callFunction; CGFloat X = subValue[@"rect"][@"x"].toDouble; CGFloat Y = subValue[@"rect"][@"y"].toDouble; CGFloat W = subValue[@"rect"][@"w"].toDouble; CGFloat H = subValue[@"rect"][@"h"].toDouble; NSInteger fontSize = subValue[@"fontSize"].toInt32; NSString *title = subValue[@"title"].toString; UIColor *titleColor = [UIColor colorWithRed:subValue[@"titleColor"][@"r"].toDouble green:subValue[@"titleColor"][@"g"].toDouble blue:subValue[@"titleColor"][@"b"].toDouble alpha:subValue[@"titleColor"][@"a"].toDouble]; UIColor *bgColor = [UIColor colorWithRed:subValue[@"bgColor"][@"r"].toDouble green:subValue[@"bgColor"][@"g"].toDouble blue:subValue[@"bgColor"][@"b"].toDouble alpha:subValue[@"bgColor"][@"a"].toDouble]; JSValue *actionValue = subValue[@"callFunction"]; [self.actions addObject:actionValue]; UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(X, Y, W, H)]; [button setTitleColor:titleColor forState:UIControlStateNormal]; [button addTarget:self action:@selector(buttonClick:) forControlEvents:UIControlEventTouchUpInside]; [button setTitle:title forState:UIControlStateNormal]; button.titleLabel.font = [UIFont systemFontOfSize:fontSize]; button.backgroundColor = bgColor; button.tag = self.actions.count-1; [self.view addSubview:button]; } } }-(void)buttonClick:(UIButton *)button { JSValue *actionValue = [self.actions objectAtIndex:button.tag]; [actionValue callWithArguments:@[@(arc4random_uniform(2)),@(arc4random_uniform(2)),@(arc4random_uniform(2))]]; }- (NSMutableArray *)actions {if (!_actions) { _actions = [NSMutableArray array]; }return _actions; }
(4) 演示gif
六、思考
这个js文件目前是写死的放在了Bundle目录下,试想一下,如果在本地每次更改js文件后再重新渲染界面,是不是都得端上重新发版,例如样式相同改一个颜色啥的,发个版就太繁琐了。最好的做法是将js文件部署到服务器上,每次只需要更新js内容后提交到服务器,端上进入当前页面时,从服务器拉取新的js文件渲染界面即可,效率高,成本低,可以实现快速更新迭代。好开心,吃个竹子吧,😆