آموزشگاه برنامه نویسی تحلیل داده
آموزشگاه برنامه نویسی تحلیل داده

آموزش React Native: ماژول های native در iOS

دوره های مرتبط با این مقاله

آموزش React Native: ماژول های native در iOS

گاهی اوقات یک برنامه نیاز به دسترسی به یک API از پلتفرم را دارد که React Native هنوز ماژول مربوط به آن را ندارد. ممکن است بخواهید کد Objective-C، Swift یا C++ موجود را بدون پیاده سازی مجدد آن در JavaScript دوباره استفاده کنید یا کدی با عملکرد خوب و multi thread مثلا برای پردازش تصویر، دیتابیس، یا هر افزونه ی پیشرفته ای بنویسید.

React Native طوری طراحی شده است که بتوانید کد native بنویسید و از همه ی پتانسیل های پلتفرم استفاده کنید. این یک قابلیت پیشرفته تر است و انتظار نداریم که بخشی از روند معمولی توسعه باشد، اما ضروری است که ارائه شود. اگر React Native از یک قابلیت native که میخواهید پشتیبانی نمی کند، باید بتوانید خودتان آن را بسازید.

این بخش، یک راهنمای پیشرفته تر است که نحوه ساخت یک ماژول native را نشان می دهد. فرض بر این است که خواننده با Objective-C یا Swift و کتابخانه های اصلی آن ها (Foundation و UIKit) آشناست.

راه اندازی ماژول native

ماژول های native معمولا به شکل package های npm توزیع می شوند، و علاوه بر فایل ها و resource های JavaScriptی حاوی یک پروژه Xcode نیز هستند. برای راه اندازی زیرساخت اولیه برای این کار، بخش راه اندازی ماژول های native را ببینید.

مثال ماژول iOS Calendar

از مثال iOS Calendar API برای این بخش استفاده می کنیم. فرض کنید می خواهیم از کد JavaScript به تقویم iOS دسترسی پیدا کنیم.

یک ماژول native فقط یک کلاس Objective-C است که پروتکل RCTBridgeModule را پیاده سازی می کند.


// CalendarManager.h
#import < React/RCTBridgeModule.h >
@interface CalendarManager : NSObject < RCTBridgeModule >
@end

علاوه بر پیاده سازی پروتکل RCTBridgeModule، کلاس باید ماکروی RCT_EXPORT_MODULE() را نیز داشته باشد که یک آرگومان اختیاری می گیرد. این آرگومان نام ماژول را که از طریق کد JavaScript قرار است مورد استفاده قرار بگیرد، مشخص می کند. اگر نامی مشخص نکنید، نام ماژول JavaScript با نام کلاس Objective-C یکی خواهد بود. اگر نام کلاس Objective-C با RCT شروع شود، نام ماژول JavaScript معادل آن بدون پیشوند RCT خواهد بود.


// CalendarManager.m
#import "CalendarManager.h"
@implementation CalendarManager
// To export a module named CalendarManager
RCT_EXPORT_MODULE();
// This would name the module AwesomeCalendarManager instead
// RCT_EXPORT_MODULE(AwesomeCalendarManager);
@end

React Native هیچ متدی از CalendarManager را در دسترس JavaScript نمی گذارد، مگر اینکه صریح آن را مشخص کنیم. این کار از طریق ماکروی RCT_EXPORT_METHOD() انجام می شود.


#import "CalendarManager.h"
#import < React/RCTLog.h>
@implementation CalendarManager
RCT_EXPORT_MODULE();
RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location)
{
  RCTLogInfo(@"Pretending to create an event %@ at %@", name, location);
}
@end

حال، از فایل JavaScript می توانید متد را به شکل زیر صدا بزنید:


import {NativeModules} from 'react-native';
var CalendarManager = NativeModules.CalendarManager;
CalendarManager.addEvent('Birthday Party', '4 Privet Drive, Surrey');

توجه: نام متدهای JavaScript

نام متدی که به JavaScript export می شود تا ستون اول با نام آن در کد native یکسان است. React Native یک ماکرو به نام RCT_REMAP_METHOD() دارد که نام متد JavaScriptی را مشخص می کند. این متد برای هنگامی که چند متد native تا ستون اول نام یکسانی دارند و ممکن است موجب ایجاد مغایرتی در کد شوند، مناسب است.

ماژول CalendarManager در سمت کد Objective-C با فراخوانی [CalendarManager new] ساخته شده. نوع بازگشتی توابع این چنینی (bridge methods) همواره void است. ارتباط React Native بین جاوا و JavaScript (bridge) async است، درنتیجه تنها راه ارسال یک نتیجه به JavaScript با استفاده از callbackها یا event است.

نوع آرگومان ها

RCT_EXPORT_METHOD همه ی انواع استاندارد JSON object را پشتیبانی می کند، برای مثال:


  • string (NSString)
  • number (NSInteger, float, double, CGFloat, NSNumber)
  • boolean (BOOL, NSNumber)
  • array (NSArray) of any types from this list
  • object (NSDictionary) with string keys and values of any type from this list
  • function (RCTResponseSenderBlock)

بعلاوه با تمام نوع هایی که توسط کلاس RCTConvert پشتیبانی می شود نیز کار می کند. (برای جزئیات بیشتر RCTConvert را ببینید). RCTConvert helper function ها همه یک JSON به عنوان ورودی می گیرند و آن را به یک نوع یا کلاس native Objective-C نگاشت می کنند.

در مثال CalendarManager، نیاز داریم event date را به متد native ارسال کنیم. نمی توانیم Date را به صورت object ارسال کنیم، باید آن را به string یا عدد تبدیل کنیم. می توانیم متدی برای این کار بنویسیم:


RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(nonnull NSNumber *)secondsSinceUnixEpoch)
{
  NSDate *date = [RCTConvert NSDate:secondsSinceUnixEpoch];
}

یا شبیه این:


RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(NSString *)ISO8601DateString)
{
  NSDate *date = [RCTConvert NSDate:ISO8601DateString];
}

اما با استفاده از قابلیت تبدیل نوع خودکار، می توانیم تبدیل دستی را کلا حذف کنیم، و فقط بنویسیم:


RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(NSDate *)date)
{
  // Date is ready to use!
}
                        

سپس می توانید این را از JavaScript به صورت زیر صدا بزنید:


CalendarManager.addEvent(
  'Birthday Party',
  '4 Privet Drive, Surrey',
  date.getTime(),
); // passing date as number of milliseconds since Unix epoch

یا به این شکل:


CalendarManager.addEvent(
  'Birthday Party',
  '4 Privet Drive, Surrey',
  date.toISOString(),
); // passing date as ISO-8601 string

و هر دو مقدار به درستی به NSDate تبدیل می شوند. یک مقدار نامناسب مثل Array، باعث پیغام خطا می شود.

با پیچیده شدن متد CalendarManager.addEvent، تعداد آرگومان ها بیشتر می شود. بعضی از آن ها ممکن است اختیاری باشند. می توانیم API را طوری تغییر دهیم که یک دیکشنری از event attribute ها بگیرد. مثلا:


#import < React/RCTConvert.h>
RCT_EXPORT_METHOD(addEvent:(NSString *)name details:(NSDictionary *)details)
{
  NSString *location = [RCTConvert NSString:details[@"location"]];
  NSDate *time = [RCTConvert NSDate:details[@"time"]];
  ...
}

و میتوانیم در جاوا اسکریپت اینطور آن را صدا بزنیم:


CalendarManager.addEvent('Birthday Party', {
  location: '4 Privet Drive, Surrey',
  time: date.getTime(),
  description: '...',
});

نکته درمورد array و map

تضمینی برای نوع مقادیر درون این داده ساختارها در Objective-C doesn't نیست. ماژول native ممکن است آرایه ای از string ها بخواهد ولی JavaScript متد را با آرایه ای از string ها و اعداد صدا بزند و در نتیجه شما یک NSArray حاوی ترکیبی از NSNumber و NSString دارید. برای آرایه ها RCTConvert تعدادی collection با نوع در اختیار می گذارد که می توانید در تعریف متد از آن استفاده کنید مثل NSStringArray یا UIColorArray. هنگام استفاده از map، مسئولیت توسعه دهنده است که نوع مقادیر را با صدا زدن متدهای RCTConvert helper دستی چک کند.

Callbacks

ماژول های native یک نوع خاص آرگومان را نیز می پذیرند، Callback ها. در اغلب موارد از این قابلیت برای برگرداندن نتیجه به JavaScript استفاده می شود.


RCT_EXPORT_METHOD(findEvents:(RCTResponseSenderBlock)callback)
{
  NSArray *events = ...
  callback(@[[NSNull null], events]);
}

متد RCTResponseSenderBlock فقط یک آرگومان می پذیرد: آرایه ای از پارامتر ها که آن را به callback JavaScript می فرستد. در این مثال ما از قرارداد نوشتاری Node برای تبدیل پارامتر اول به object error (که اگر خطایی نباشد null خواهد بود) استفاده کرده ایم و بقیه result های تابع خواهند بود:


CalendarManager.findEvents((error, events) = > {
  if (error) {
    console.error(error);
  } else {
    this.setState({events: events});
  }
});

اگر می خواهید object هایی شبیه error به JavaScript ارسال کنید، از RCTMakeError استفاده کنید. البته فعلا فقط یک dictionary درقالب Error به JavaScript برمی گرداند، ولی در آینده ساخت object های JavaScriptی نیز ممکن خواهد بود.

ماژول های native می توانند یک promise را fulfill کنند. این قابلیت می تواند کد JavaScript را ساده کند، بخصوص اگر از دستور async/await موجود در ES2016 استفاده کنید. وقتی پارامتر آخر یک متد native promise باشد، متد JavaScript معادل آن یک Object Promise جاواااسکریپتی برمی گرداند.

کد بالا را طوری تغییر داده ایم که به جای callback از promise استفاده کند:


RCT_REMAP_METHOD(findEvents,
                 findEventsWithResolver:(RCTPromiseResolveBlock)resolve
                 rejecter:(RCTPromiseRejectBlock)reject)
{
  NSArray *events = ...
  if (events) {
    resolve(events);
  } else {
    NSError *error = ...
    reject(@"no_events", @"There were no events", error);
  }
}

کد JavaScript این متد یک Promise برمیگرداند. می توانید از await در یک متد Async برای فراخوانی آن و دریافت نتیجه استفاده کنید:


async function updateEvents() {
  try {
    var events = await CalendarManager.findEvents();
    this.setState({events});
  } catch (e) {
    console.error(e);
  }
}
updateEvents();

Threading

ماژول های native نباید اطلاعاتی درمورد thread که روی آن فراخوانی می شوند داشته باشند. React Native متدهای ماژول های native را روی یک serial GCD queue مجزا فراخوانی می کند. اما این پیاده سازی ممکن است بعدا تغییر کند. متد - (dispatch_queue_t)methodQueue به ماژول های native امکان می دهد queue را که متد روی آن اجرا می شود تعیین کنند. برای مثال، اگر لازم است از یک main-thread-only iOS API استفاده کنیم، باید اینطور آن را مشخص کنیم:


- (dispatch_queue_t)methodQueue
{
  return dispatch_get_main_queue();
}

به طور مشابه اگر عملیاتی زمان گیر است، ماژول native نباید block شود. در این صورت می تواند queue خودش را برای اجرای عملیات تعیین کند. برای مثال، ماژول RCTAsyncLocalStorage ، queue خودش را می سازد و React queue را block نمی کند:


- (dispatch_queue_t)methodQueue
{
  return dispatch_queue_create("com.facebook.React.AsyncLocalStorageQueue", DISPATCH_QUEUE_SERIAL);
}

methodQueue معین شده بین همه متدهای درون ماژول مشترک است. اگر فقط یکی از متدها زمان بر است (یا به هردلیلی باید روی queue دیگری غیر از بقیه اجرا شود) می توانید از dispatch_async درون متد برای اجرای آن کد خاص درون یک queue دیگر بدون تاثیری روی بقیه، استفاده کنید:


RCT_EXPORT_METHOD(doSomethingExpensive:(NSString *)param callback:(RCTResponseSenderBlock)callback)
{
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // Call long-running code on background thread
    ...
    // You can invoke callback from any thread/queue
    callback(@[...]);
  });
}

تزریق وابستگی (Dependency Injection)

Bridge هر RCTBridgeModules رجیستر شده ای را instance می سازد ، با این حال ممکن است بخواهید instance های خود از ماژول را بسازید.

برای این کار می توانید کلاسی بسازید که پروتکل RCTBridgeDelegate را پیاده سازی کند، یک RCTBridge با delegate به عنوان آرگومان بسازد و با bridge ساخته شده، یک RCTRootView بسازد:


id< RCTBridgeDelegate > moduleInitialiser = [[classThatImplementsRCTBridgeDelegate alloc] init];
RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:moduleInitialiser launchOptions:nil];
RCTRootView *rootView = [[RCTRootView alloc]
                        initWithBridge:bridge
                            moduleName:kModuleName
                     initialProperties:nil];
					 

Exporting Constants

یک ماژول native می تواند مقادیر ثابت را export کند که درهنگام اجرا بافاصله در JavaScript قابل دسترسی باشند. این قابلیت برای مبادله ی دیتای ثابت مناسب است. چرا که از یک round-trip روی bridge جلوگیری می کند:


- (NSDictionary *)constantsToExport
{
  return @{ @"firstDayOfTheWeek": @"Monday" };
}

JavaScript می تواند از این مقادیر بلافاصله و به صورت sync استفاده کند.


console.log(CalendarManager.firstDayOfTheWeek);

توجه کنید مقادیر ثابت فقط در لحظه ی ساخته شدن Export می شوند. اگر مقادیر constantsToExport را درزمان اجرا تغییر دهید مقادیر جدید در JavaScript قابل دسترس نخواهند بود.

پیاده سازی requiresMainQueueSetup +

اگر constantsToExport - را override کنید باید requiresMainQueueSetup + را نیز پیاده سازی کنید. React Native از این طریق می فهمد که آیا لازم است ماژول روی thread اصلی ساخته شود یا نه. اگر این موضوع را رعایت نکنی، هشداری می بینید مبنی بر اینکه در آینده ماژول روی یک thread در پس زمینه ساخته می شود، مگر اینکه به صراحت با requiresMainQueueSetup + تعیین شود:


+ (BOOL)requiresMainQueueSetup
{
  return YES;  // only do this if your module initialization relies on calling UIKit!
}

اگر ماژول نیاز به دسترسی به UIKit ندارد، باید requiresMainQueueSetup + ، NO برگرداند.

Enum Constants

Enum ها با استفاده از NS_ENUM تعریف می شوند و نمی توانند به عنوان آرگومان های متد استفاده شوند، مگر اینکه RCTConvert را extend کنند.

برای Export کردن تعریف NS_ENUM زیر:


typedef NS_ENUM(NSInteger, UIStatusBarAnimation) {
    UIStatusBarAnimationNone,
    UIStatusBarAnimationFade,
    UIStatusBarAnimationSlide,
};

باید یک class extension از RCTConvert بصورت زیر بسازید:


@implementation RCTConvert (StatusBarAnimation)
  RCT_ENUM_CONVERTER(UIStatusBarAnimation, (@{ @"statusBarAnimationNone" : @(UIStatusBarAnimationNone),
                                               @"statusBarAnimationFade" : @(UIStatusBarAnimationFade),
                                               @"statusBarAnimationSlide" : @(UIStatusBarAnimationSlide)}),
                      UIStatusBarAnimationNone, integerValue)
@end

سپس می توانید متدهایی تعریف کنید و enum های خود را به شکل زیر export کنید:


- (NSDictionary *)constantsToExport
{
  return @{ @"statusBarAnimationNone" : @(UIStatusBarAnimationNone),
            @"statusBarAnimationFade" : @(UIStatusBarAnimationFade),
            @"statusBarAnimationSlide" : @(UIStatusBarAnimationSlide) };
};
RCT_EXPORT_METHOD(updateStatusBarAnimation:(UIStatusBarAnimation)animation
                                completion:(RCTResponseSenderBlock)callback)

ارسال event به JavaScript

ماژول های native می توانند Event هایی را به JavaScript بفرستند، بدون اینکه مستقیم فراخوانی شوند. بهترین راه برای این کار زیرکلاس کردن RCTEventEmitter، پیاده سازی supportedEvents و فراخوانی sendEventWithName است:


// CalendarManager.h
#import < React/RCTBridgeModule.h >
#import < React/RCTEventEmitter.h >
@interface CalendarManager : RCTEventEmitter < RCTBridgeModule >
@end
// CalendarManager.m
#import "CalendarManager.h"
@implementation CalendarManager
RCT_EXPORT_MODULE();
- (NSArray < NSString * > *)supportedEvents
{
  return @[@"EventReminder"];
}
- (void)calendarEventReminderReceived:(NSNotification *)notification
{
  NSString *eventName = notification.userInfo[@"name"];
  [self sendEventWithName:@"EventReminder" body:@{@"name": eventName}];
}
@end

کد JavaScript می تواند با ساخت یک instance از NativeEventEmitter دور ماژول، این eventها را listen و دریافت کند:


import { NativeEventEmitter, NativeModules } from 'react-native';
const { CalendarManager } = NativeModules;
const calendarManagerEmitter = new NativeEventEmitter(CalendarManager);
const subscription = calendarManagerEmitter.addListener(
  'EventReminder',
  (reminder) = > console.log(reminder.name)
);
...
// Don't forget to unsubscribe, typically in componentWillUnmount
subscription.remove();

برای مثال های بیشتر درمورد ارسال event به JavaScript، RCTLocationObserver را ببینید.

اگر یک event را فراخوانی کنید که کسی آن را listen نمی کند، هشدار خواهید گرفت. برای اجتناب از این مسئله، و بهینه سازی کار ماژول (مثلا با unsubscribe شدن از task های متوقف شده در background)، می توانید دو متد startObserving و stopObserving را در زیرکلاس RCTEventEmitter، override کنید.


@implementation CalendarManager
{
  bool hasListeners;
}
// Will be called when this module's first listener is added.
-(void)startObserving {
    hasListeners = YES;
    // Set up any upstream listeners or background tasks as necessary
}
// Will be called when this module's last listener is removed, or on dealloc.
-(void)stopObserving {
    hasListeners = NO;
    // Remove upstream listeners, stop unnecessary background tasks
}
- (void)calendarEventReminderReceived:(NSNotification *)notification
{
  NSString *eventName = notification.userInfo[@"name"];
  if (hasListeners) { // Only send events if anyone is listening
    [self sendEventWithName:@"EventReminder" body:@{@"name": eventName}];
  }
}

Exporting Swift

Swift از ماکرو پشتیبانی نمی کند، دسترسی به آن در React Native متفاوت است.

فرض کنید همان کلاس CalendarManager را این بار در swift داریم.


// CalendarManager.swift
@objc(CalendarManager)
class CalendarManager: NSObject {
  @objc(addEvent:location:date:)
  func addEvent(name: String, location: String, date: NSNumber) - > Void {
    // Date is ready to use!
  }
  @objc
  func constantsToExport() - > [String: Any]! {
    return ["someKey": "someValue"]
  }
}

توجه: حتما از @objc استفاده کنید، این کار برای اطمینان از اینکه کلاس ها و متدها به درستی به Objective-C runtime تبدیل می شوند، ضروری است.

سپس یک فایل پیاده سازی private بسازید که اطلاعات لازم را روی bridge، register می کند.


// CalendarManagerBridge.m
#import < React/RCTBridgeModule.h >
@interface RCT_EXTERN_MODULE(CalendarManager, NSObject)
RCT_EXTERN_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(nonnull NSNumber *)date)
@end

اگر با Swift و Objective-C کمی آشنا هستید می دانید وقتی دو زبان را در یک پروژه iOS ادغام می کنید، به bridging file اضافه ای به نام bridging header احتیاج دارید، که در Swift به فایل های Objective-C دسترسی داشته باشید. Xcode هنگام افزودن کد Swift از طریق File > New File به شما پیشنهاد ساختن این فایل را می دهد. در این فایل باید RCTBridgeModule.h را import کنید:


// CalendarManager-Bridging-Header.h
#import < React/RCTBridgeModule.h >

همچنین می توانید از RCT_EXTERN_REMAP_MODULE و _RCT_EXTERN_REMAP_METHOD برای تغییر نام ماژول ها در JavaScript استفاده کنید. برای اطلاعات بیشتر RCTBridgeModule را ببینید.

  • 464
  •    0
  • تاریخ ارسال :   1398/06/05

دانشجویان گرامی اگر این مطلب برای شما مفید بود لطفا ما را در GooglePlus محبوب کنید
رمز عبور: tahlildadeh.com یا www.tahlildadeh.com
ارسال دیدگاه نظرات کاربران
شماره موبایل دیدگاه
عنوان پست الکترونیک

ارسال

آموزشگاه برنامه نویسی تحلیل داده
آموزشگاه برنامه نویسی تحلیل داده

تمامی حقوق این سایت متعلق به آموزشگاه تحلیل داده می باشد .