1. 程式人生 > >JavaScript 與 Native互動(包括ReactNative)

JavaScript 與 Native互動(包括ReactNative)

一. 概念

JavaScriptCore框架 是蘋果在iOS7引入的框架,該框架讓 Objective-C 和 JavaScript 程式碼直接的互動變得更加的簡單方便。
JavaScriptCore框架 其實就是基於 webkit 中以C/C++實現的JavaScriptCore
的一個包裝

二. JavaScriptCore框架常用類

  1. JSContext: JS執行的環境, 為JS程式碼的執行提供了上下文環境, 並且通過JSContext獲取JS的資料
  2. JSValue: 用於接受JS中獲取的資料型別, 可以使物件, 方法
  3. : JavaScript虛擬機器, 每一個JavaScript上下文(也就是一個JSContext物件)歸屬於一個虛擬機器。每一個虛擬機器可以包含多個不同的上下文,而且可以在不同的上下文之間傳值(JSValue物件)。但是,每一個虛擬機器都是獨立的——你不能將在一個虛擬機器中建立的值傳到另一個虛擬機器的一個上下文中
  4. JSExport: protocol,如果JS物件想直接呼叫OC物件裡面的方法和屬性,那麼這個OC物件需要實現這個JSExport協議

js OC type 轉換

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)

三. JS 和 OC 互調

必須建立JS執行環境

1. OC呼叫JS

本質: JS程式碼中已經定義好變數和方法, 用過OC去獲取, 並且呼叫
步驟:
1. 建立JS執行環境
2. 執行JS程式碼
3. 獲取JS資料(變數, 方法)
4. 使用JS資料, 方法

變數的呼叫

    JSContext *context = [[JSContext alloc] init];

    NSString
*jsCode = @"let arr = [1, 2, 3]"; [context evaluateScript:jsCode]; // 只有執行JS程式碼才能獲取資料 // 變數定義在JS中, 所以直接通過JSContext獲取, 根據變數名稱獲取, 相當於字典的key JSValue *reJsValue = context[@"arr"]; NSLog(@"%@", reJsValue); // 1,2,3 // 修改JS的值 reJsValue[0] = @9; NSLog(@"%@", reJsValue); // 9,2,3 // 值轉換 if (reJsValue.isArray) { NSLog(@"jsValue to Array %@", reJsValue.toArray); }

方法的呼叫

    NSString *jsCode = @"function hello(params) {return 'hello ' + params;}";

    // 建立JS執行環境
    JSContext *context = [[JSContext alloc] init];

    // 執行JS程式碼
    [context evaluateScript:jsCode];

    JSValue *hello = context[@"hello"];

    // OC 呼叫JS方法, 獲取返回值
    JSValue *reValue = [hello callWithArguments:@[@"javascript"]];
    NSLog(@"reValue: %@", reValue); // reValue: hello javascript

2. JS呼叫OC方法

  • Block: 使用block可以很方便的將OC中的單個方法暴露給JS呼叫
  • JSExport 協議: 使用JSExport協議,可以將OC的中某個物件直接暴露給JS使用,而且在JS中使用就像呼叫JS的物件一樣自然

簡而言之,Block是用來 暴露單個方法的,而JSExport 協議可以暴露一個OC物件

block

    //1. 建立JS執行的環境
    JSContext *context = [[JSContext alloc] init];

    //2. @selector(eat) 物件型別的值
    context[@"eat"] = ^(NSString *sm, NSString *string) {
        NSLog(@"eat call: %@  %@", sm, string);
    };

    //3. 執行JS
    [context evaluateScript:@"eat('aaaa', 'bbbb')"]; // eat call: aaaa  bbbb

class

本質: 一開始JS中並沒有OC的類, 需要先在JS中生成OC的類, 然後通過JS呼叫
步驟:
1. OC類必須遵守JSExport協議, 只要遵守JSExport協議, JS才會生成這個類(但是不會生成屬性和方法)
2. 自定義一個整合JSExport的協議的, 在自己的協議中暴露需要在JS中用到的屬性和方法
3. 自定義的類遵守自定義的協議, JS就會自動生成類, 包括協議中的屬性和方法

自定義類
    Person *person = [[Person alloc] init];
    person.name = @"tom";

    // 1. 建立JS執行環境
    JSContext *context = [[JSContext alloc] init];

    // 會在JS中生成person物件, 並且擁有協議定義好的值
    context[@"Person"] = person;

    // 執行JS程式碼
    NSString *jsCode = @"Person.playGame('足球', '中午')";
    [context evaluateScript:jsCode]; // playWith 足球 time: 中午

    NSString *jsCode2 = @"var name = Person.name";
    [context evaluateScript:jsCode2];
    JSValue *name = context[@"name"];
    NSLog(@"%@", name.toString); // tom

    NSString *jsCode3 = @"Person.name = '11111'";
    [context evaluateScript:jsCode3];
    NSLog(@"%@", person.name); // 11111

系統類

    // 給系統類新增協議
    class_addProtocol([UILabel class], @protocol(UILableJSExport));
    // 建立UILabel
    UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(50, 50, 200, 100)];
    [self.view addSubview:label];

    // 1. 建立JS執行環境
    JSContext *context = [[JSContext alloc] init];
    // JS自動生成label物件
    context[@"label"] = label;

    //執行JS程式碼
    NSString *jsCode = @"label.text = '顯示的什麼東西呢'";

    [context evaluateScript:jsCode];

注意:
使用 Block 的坑

不要在Block中直接使用JSValue
不要在Block中直接使用JSContext

因為Block會強引用它裡面用到的外部變數,如果直接在Block中使用JSValue的話,那麼這個JSvalue就會被這個Block強引用,而每個JSValue都是強引用著它所屬的那個JSContext的,這是前面說過的,而這個Block又是注入到這個Context中,所以這個Block會被context強引用,這樣會造成迴圈引用,導致記憶體洩露。不能直接使用JSContext的原因同理。

那怎麼辦呢,針對第一點,建議把JSValue當做引數傳到Block中,而不是直接在Block內部使用,這樣Block就不會強引用JSValue了。
針對第二點,可以使用[JSContext currentContext] 方法來獲取當前的Context。

self.jsContext[@"jsCallNative"] = ^(NSString *paramer){
        // 會引起迴圈引用
        JSValue *value1 =  [JSValue valueWithNewObjectInContext:self.jsContext];
        // 不會引起迴圈引用
        JSValue *value =  [JSValue valueWithNewObjectInContext:[JSContext currentContext]];
};

ReactNative 中互動

步驟:
1. 遵守RCTBridgeModule
2. RCT_EXPORT_MODULE 匯出js中對應的模組
3. 匯出方法, 屬性, 常量等

OC

#import "JSCallNativeModule.h"
#import <React/RCTBridgeModule.h>

@interface JSCallNativeModule ()<RCTBridgeModule>

@end

@implementation JSCallNativeModule

RCT_EXPORT_MODULE()  // js_name js模組的名字, 不傳則預設匯出本身

// 原生模組可以匯出一些常量,這些常量在JavaScript端隨時都可以訪問。用這種方法來傳遞一些靜態資料,可以避免通過bridge進行一次來回互動。
// Javascript端可以隨時同步地訪問這個資料:`console.log(CalendarManager.firstDayOfTheWeek)`
- (NSDictionary<NSString *,id> *)constantsToExport {
    return @{
             @"firstDayOfTheWeek": @"Monday"
    };
}

//JS中呼叫Native的方法:此處響應
RCT_EXPORT_METHOD(printMyself:(NSString *)name date:(NSDate *)date callback:(RCTResponseSenderBlock)callback) {

    NSLog(@"%@ %@", name, date); // 看看效果 Tue Mar  6 18:55:20 2018
    callback(@[@(2), @(3), @(4)]);
}

// 如果橋接原生方法的最後兩個引數是RCTPromiseResolveBlock和RCTPromiseRejectBlock,
// 則對應的JS方法就會返回一個Promise物件
RCT_REMAP_METHOD(findeEvents,
                 resolver:(RCTPromiseResolveBlock)resolver
                 rejecter:(RCTPromiseRejectBlock)rejecter) {
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

        NSArray *events = @[@"da", @"fasf"];
        if (events) {
            resolver(events);
        } else {
            rejecter(@"-1", @"error msg", nil);
        }
    });
}

@end

JS

import React from 'react';
import {
    AppRegistry,
    View,
    Text,
    NativeModules
} from 'react-native';
// import App from './callNative';

const JSCallNativeModule = NativeModules.JSCallNativeModule
const date = new Date;
class ContainterView extends React.Component {
    render() {
        return (
            <View style={{flex: 1, backgroundColor:'red'}}>
                <Text style={{top: 100, height: 35, backgroundColor:'blue'}} onPress={() => this._onClick()}> 點選call native </Text>
            </View>
        )
    }
    _onClick() {
        alert(JSCallNativeModule.firstDayOfTheWeek)
        JSCallNativeModule.printMyself('看看效果', date.getTime(), this._callback)
    }   

    _callback(firstParm, secondParm, thirdParm) {
        alert(firstParm + secondParm + thirdParm)
    }

}

AppRegistry.registerComponent('JSNativeDemo', () => ContainterView);

待解決問題:
1. 打包生成的jsbundle體積較大(>500k)
2. 首次載入js較慢