1. 程式人生 > >Hybrid App: 瞭解JavaScript如何與Native實現混合開發

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 {
    
    //定義無引數block
    void (^Block1)(void) = ^(){
        NSLog(@"js call native ------- hello JavaScript");
    };
    
    //定義有引數block
    void (^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 = 20
2019-11-12 14:58:17.472810+0800 混合開發[10499:365654] 年級:grade = 5
2019-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 = 1
    this.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; //改變當前控制器檢視背景色
@end

NS_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檔案渲染介面即可,效率高,成本低,可以實現快速更新迭代。好開心,吃個竹子吧,