التواصل بين المكون الأصيل وReact Native

من موسوعة حسوب


يمكنك تعلّم كيفية تضمين React Native في مكون أصيل والعكس من صفحة الدمج مع تطبيقات قائمة وصفحة مكونات واجهة المستخدم الأصيلة. عندما نمزج المكونات الأصيلة ومكونات React Native، سنجد حتما الحاجة للتواصل بين هاذين العالمين. ذكرنا بعض الطرق لتحقيق ذلك في أدلّة أخرى مسبقا. يلخص هذا المقال التقنيات المتاحة.

مقدمة

React Native مستوحًى من React، وبالتالي فإن الفكرة الأساسية لتدفق المعلومات (information flow) متشابهة. التدفق في React أحادي الاتجاه. نحافظ على تسلسل هرمي للمكونات، حيث يعتمد كل مكون فقط على مكوِّنه الأب وحالته الداخلية. وذلك باستعمال الخاصيات: تُمرَّر البيانات من أحد المكونات الآباء إلى مكوناته الأبناء من أعلى إلى أسفل. إذا كان أحد مكونات الأسلاف (ancestor component) يعتمد على حالة سليله (descendant)، فيجب أن تُمرَّر دالة رد نداء (callback) ليستخدمها المكون السليل لتحديث المكون السلف.

ينطبق نفس المفهوم على React Native. طالما نقوم ببناء تطبيقنا حصريا داخل إطار العمل، يمكننا التحكم بتطبيقنا بالخاصيات ودوال رد النداء. ولكن، عندما نخلط بين مكونات React Native والمكونات الأصيلة، فإننا نحتاج إلى بعض الآليات الخاصة متعددة اللغات (cross-language) التي ستسمح لنا بتمرير المعلومات بينها.

الخاصيات

الخاصيات هي أبسط طريقة للتواصل عبر المكونات. لذلك نحتاج إلى طريقة لتمرير الخاصيات من كل من المكون الأصيل إلى React Native، ومنReact Native إلى المكون الأصيل.

تمرير الخاصيات من المكون الأصيل إلى React Native

لتضمين عرض React Native في مكون أصيل، نستخدم RCTRootView، وهو صنفٌ من النوع UIView تطبيق React Native. كما يوفر واجهة بين الجانب الأصيل والتطبيق المستضاف.

يحتوي RCTRootView على مُهيئ يسمح لك بتمرير خاصيات عشوائية إلى تطبيق React Native. يجب أن يكون المعامل initialProperties نسخةً (instance) من NSDictionary. يُحوَّل القاموس داخليًا إلى كائن JSON الذي يمكن لمكون JavaScript ذي مستوى عال (top-level) الرجوع إليه.

NSArray *imageList = @[@"http://foo.com/bar1.png",
                       @"http://foo.com/bar2.png"];

NSDictionary *props = @{@"images" : imageList};

RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
                                                 moduleName:@"ImageBrowserApp"
                                          initialProperties:props];
import React from 'react';
import {AppRegistry, View, Image} from 'react-native';

class ImageBrowserApp extends React.Component {
  renderImage(imgURI) {
    return <Image source={{uri: imgURI}} />;
  }
  render() {
    return <View>{this.props.images.map(this.renderImage)}</View>;
  }
}

AppRegistry.registerComponent('ImageBrowserApp', () => ImageBrowserApp);

يوفر RCTRootView كذلك الخاصية appProperties للقراءة والكتابة (read-write property). بعد تعيين appProperties، يُعاد تصيير تطبيق React Native بخاصيات جديدة. يُنفَّذ التحديث فقط عندما تختلف الخاصيات المحدثة الجديدة عن الخاصيات السابقة.

NSArray *imageList = @[@"http://foo.com/bar3.png",
                       @"http://foo.com/bar4.png"];

rootView.appProperties = @{@"images" : imageList};

لا بأس في تحديث الخاصيات في أي وقت. ومع ذلك، يجب إجراء التحديثات على السلسلة الرئيسية. يمكنك استخدام دالة getter على أي سلسلة.

ملاحظة: هناك حاليًا مشكلة معروفة عند تعيين appProperties أثناء بدء تشغيل الجسر، يمكن فقد التغيير. انظر هذه الصفحة لمزيد من المعلومات.

لا توجد وسيلة لتحديث بعض الخاصيات فقط في كل مرة. نقترح بناءه داخل غلاف (wrapper) خاص بك بدلاً من ذلك.

ملاحظة: حاليًا، لن تُستدعَى الدالة ‎‎componentWillUpdateProps‎‎ في JavaScript لمكون React Native ذي المستوى الأعلى بعد تحديث خاصية ما. ومع ذلك، يمكنك الوصول إلى الخاصيات الجديدة داخل الدالة componentDidMount.

تمرير الخاصيات منReact Native إلى المكون الأصيل

تمّت تغطية مشكلة توفير الوصول لخاصيات المكونات الأصيلة بالتفصيل في هذه الصفحة. باختصار، صدِّر (export) الخاصيات باستخدام ماكرو RCT_CUSTOM_VIEW_PROPERTY في مكونك الأصيل المخصص، ثم استخدمها في React Native كما لو كان المكون مُجرَّد مكونِ React Native عادي.

حدود الخاصيات

العيب الرئيسي للخاصيات متعددة اللغات هو أنها لا تدعم دوال رد النداء، والتي تسمح لنا بمعالجة ارتباطات البيانات من أسفل إلى أعلى. تخيل أن لديك عرض React Native صغير تريد إزالته من عرض المكون الأصيل الأب كنتيجة لإجراءٍ في JavaScript. لا توجد طريقة للقيام بذلك باستخدام الخاصيات، لأن المعلومات ستحتاج إلى الانتقال من الأسفل إلى الأعلى.

رغم أن لدينا نوع من أنواع دوال رد النداء متعددة اللغات (الموصوفة هنا)، إلا أن دوال رد النداء هذه ليست دائمًا ما نحتاجه. المشكلة الرئيسية هي أنها غير مُصمَّمةٍ لتُمرَّر كخاصيات. بدلاً من ذلك، تسمح لنا هذه الآلية بتشغيل إجراء أصيل من JavaScript، والتعامل مع نتيجة هذا الإجراء في JavaScript.

طرق أخرى للتفاعلات متعددة اللغات (الأحداث والوحدات الأصيلة)

كما هو مذكور في الفصل السابق، يأتي استخدام الخاصيات مع بعض القيود. أحيانًا لا تكفي الخاصيات للتحكم بمنطق تطبيقنا، ونحتاج حلا يمنح مزيدًا من المرونة. يغطي هذا الفصل تقنيات اتصال أخرى متاحة في React Native. يمكن استخدامها للاتصال الداخلي (بين JavaScript والطبقات الأصيلة في React Native) وكذلك للاتصال الخارجي (بين React Native والجزء "الأصيل الخالص" من تطبيقك).

يمكّنك React Native من إجراء استدعاءات الدوال متعددة اللغات. يمكنك تنفيذ شيفرة أصيلة مخصصة من JavaScript والعكس. لسوء الحظ، بناءً على الجانب الذي نعمل عليه، نحقق نفس الهدف بطرق مختلفة. بالنسبة للجهة الأصيلة - نستخدم آلية الأحداث لجدولة تنفيذ دالة معالِجةٍ (handler function) في JavaScript، بينما بالنسبة لإطار React Native، فإننا نستدعي مباشرةً التوابع المصدَّرة من الوحدات الأصيلة.

استدعاء دوال React Native من المكون الأصيل (الأحداث)

وُصفت الأحداث بالتفصيل في هذه الصفحة. لاحظ أن استخدام الأحداث لا يمنحنا أي ضمانات بشأن وقت التنفيذ (execution time)، إذ يعالج الحدث في سلسلة منفصلة.

الأحداث مفيدة جدًّا، لأنها تسمح لنا بتغيير مكونات React Native دون الحاجة إلى أي مرجع يشير إليها. ومع ذلك، هناك بعض المشاكل التي قد تقع فيها أثناء استخدامها:

  • لأنه يمكن إرسال الأحداث من أي مكان، فيمكنها إدخال تبعيات متشابكة -بشكل مربك- داخل مشروعك.
  • تشترك الأحداث في مجال الأسماء (namespace)، مما يعني أنك قد تواجه بعض تضارب الأسماء. لن تُكتشف التضاربات بشكل ساكن (statically)، مما يجعلها صعبة التنقيح (debug).
  • إذا كنت تستخدم عدة نسخ لنفس مكون React Native وتريد تمييزها من منظور الحدث الخاص بك، فستحتاج على الأرجح إلى تقديم معرّفات (identifiers) وتمريرها مع الأحداث (يمكنك استخدام وسم reactTag الخاص بالعرض الأصيل كمعرّف) .

يتمثل النمط الشائع الذي نستخدمه عند تضمين المكون الأصيل في React Native في جعل RCTViewManager للمكون الأصيل مفوَّضًا للعروض، وإرسال الأحداث مرة أخرى إلى JavaScript عبر الجسر. هذا يُبقي استدعاءات الحدث ذات الصلة في مكان واحد.

استدعاء الدوال الأصيلة من React Native (الوحدات الأصيلة)

الوحدات الأصيلة هي أصناف Objective-C المتوفرة في JavaScript. تُنشأُ نسخة وحدة واحدة في كل جسر JavaScript. يمكنها تصدير دوال وثوابت اعتباطيّة إلى React Native. تمّت تغطيتها بالتفصيل في هذه الصفحة.

الوحدات الأصيلة هي وحداتٌ مفردة (singletons) ما يحدّ من الآلية (mechanism) في سياق التضمين (embedding). لنقل أن لدينا مكون React Native مُضمَّن في عرض أصيل ونريد تحديث العرض الأب الأصيل. باستخدام آلية الوحدة الأصيلة، سنُصدِّر دالةً لا تأخذ المعاملات المتوقعة فحسب، بل وأيضًا مُعرّفاََ للعرض الأب الأصيل. سيُستخدم المعرِّف لاسترداد مرجعِِ (reference) إلى العرض الأب الذي نريد تحديثه. ومع ذلك، سنحتاج إلى الاحتفاظ بارتباطٍ (mapping) من المعرِّفات إلى العروض الأصيلة في الوحدة.

رغم أن هذا الحل معقد، إلا أنه يُستخدَم في RCTUIManager، وهو صنف React Native داخليّ يُدير جميع عروض React Native.

يمكن أيضًا استخدام الوحدات الأصيلة لتوفير الوصول إلى المكتبات الأصيلة الموجودة مسبقا للغة JavaScript. مكتبة تحديد الموقع الجغرافي Geolocation library مثال للفكرة.

تحذير: تشترك كافة الوحدات الأصيلة في نفس مجال الأسماء. احذر من تضارب الأسماء عند إنشاء وحدات جديدة.

تدفق حساب التخطيط (Layout computation flow)

عند دمج المكونات الأصيلة وReact Native، نحتاج أيضًا إلى طريقة لدمج نظامي تخطيط (layout systems) مختلفين. يغطي هذا القسم مشاكل التخطيط الشائعة ويوفر وصفًا موجزًا للآليات اللازمة لمعالجتها.

تخطيط مكون أصيل مضمّن في React Native

غطّينا هذه الحالة في هذه الصفحة. نظرًا لأن جميع عروض React الأصيلة لدينا هي أصناف فرعية من الصنف UIView، فمعظم سمات النمط والحجم (style and size) ستعمل كالمتوقع افتراضيا.

تخطيط مكون React Native مضمّن في المكون الأصيل

محتوى React Native ذو حجم ثابت

أبسط سيناريو هو عندما يكون لدينا تطبيق React Native بحجم ثابتٍ معروفٍ عند الجانب الأصيل. على وجه الخصوص، يقع عرض React Native بوضع ملء الشاشة في هذه الدائرة. إذا أردنا عرضًا جذرًا أصغر، فيمكننا تعيين إطار RCTRootView بشكل صريح.

على سبيل المثال، لجعل تطبيق React Native بارتفاع 200 بكسل (منطقي)، و اتّساع (width) العرضِ (view) المضيف، يمكننا القيام بما يلي:

// SomeViewController.m

- (void)viewDidLoad
{
  [...]
  RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
                                                   moduleName:appName
                                            initialProperties:props];
  rootView.frame = CGRectMake(0, 0, self.view.width, 200);
  [self.view addSubview:rootView];
}

عندما يكون لدينا عرضٌ جذرٌ بحجمٍ ثابتٍ، لابد من احترام حدوده على جانب JavaScript. بمعنى آخر، نحتاج إلى التأكد من إمكانية احتواء محتوى React Native في العرض الجذر ذي الحجم الثابت. أسهل طريقة لضمان ذلك هي استخدام تخطيط flexbox. إذا استخدمت التموضع المطلق (absolute positioning)، وكانت مكونات React مرئية خارج حدود العرض الجذر، فستحصل على تداخل مع عروض المكون الأصيل، مما يؤدي إلى سلوك غير متوقع لبعض الميزات. على سبيل المثال، لن يُبرز "TouchableHighlight" لمساتك خارج حدود العرض الجذر.

لا بأس بتحديث حجم العرض الجذر بشكل ديناميكي عن طريق إعادة ضبط خاصية إطاره (frame property). سيهتم React Native بتخطيط المحتوى.

محتوىReact Native بحجم مرن

في بعض الحالات، نود تصيير محتوى بحجم غير معروف مبدئيا. لنفترض أن الحجم سيُحدَّد ديناميكيًّا في JavaScript. لدينا حلان لهذه المشكلة:

  1. يمكنك تغليف عرض React Native الخاص بك في مكون ScrollView. هذا يضمن أن محتواك سيكون متاحًا دائمًا ولن يتداخل مع العروض الأصيلة.
  2. يتيح لك React Native تحديد حجم تطبيق React Native في JavaScript وتزويده لمالك الصنف RCTRootView المُضيف. المالك هو المسؤول عن إعادة تخطيط العروض الفرعية والحفاظ على توافق واجهة المستخدم. نحقق هذا من خلال أوضاع مرونة (flexibility modes) الصنف RCTRootView.

يدعم RCTRootView أربعة أوضاع مرونة مختلفة الحجم:

// RCTRootView.h

typedef NS_ENUM(NSInteger, RCTRootViewSizeFlexibility) {
  RCTRootViewSizeFlexibilityNone = 0,
  RCTRootViewSizeFlexibilityWidth,
  RCTRootViewSizeFlexibilityHeight,
  RCTRootViewSizeFlexibilityWidthAndHeight,
};

القيمة RCTRootViewSizeFlexibilityNone هي الافتراضية، والتي تجعل حجم عرضٍ جذرٍ ثابتًا (مع إمكانية تحديثه باستخدام ‎‎setFrame:‎‎). تسمح لنا الأوضاع الثلاثة الأخرى بتتبع تحديثات حجم محتوى React Native . على سبيل المثال، سيؤدي تعيين الوضع إلى RCTRootViewSizeFlexibilityHeight إلى جعل React Native يقيس ارتفاع المحتوى وتمرير هذه المعلومات إلى مفوَّض RCTRootView. يمكن تنفيذ إجراء اعتباطيّ داخل المفوَّض، بما في ذلك تعيين إطار العرض الجذر، بحيث يتناسب المحتوى. يُستدعى المفوَّض فقط عندما يتغير حجم المحتوى.

تحذير: يؤدي جعل بُعدِِ مرنًا في كل من JavaScript والمكون الأصيل إلى سلوك غير محدد. على سبيل المثال، لا تجعل عرض مكون React ذو مستوى عالٍ مرنًا (مع flexbox) أثناء استخدام RCTRootViewSizeFlexibilityWidth على RCTRootView المستضيف.

مثال:

// FlexibleSizeExampleView.m

- (instancetype)initWithFrame:(CGRect)frame
{
  [...]

  _rootView = [[RCTRootView alloc] initWithBridge:bridge
  moduleName:@"FlexibilityExampleApp"
  initialProperties:@{}];

  _rootView.delegate = self;
  _rootView.sizeFlexibility = RCTRootViewSizeFlexibilityHeight;
  _rootView.frame = CGRectMake(0, 0, self.frame.size.width, 0);
}

#pragma mark - RCTRootViewDelegate
- (void)rootViewDidChangeIntrinsicSize:(RCTRootView *)rootView
{
  CGRect newFrame = rootView.frame;
  newFrame.size = rootView.intrinsicContentSize;

  rootView.frame = newFrame;
}

في المثال، لدينا عرض FlexibleSizeExampleView المحتوِي على عرضٍ جذرٍ. نقوم بإنشاء العرض الجذر وتهيئته وتعيين المفوَّض. سيقوم المفوَّض بمعالجة تحديثات الحجم. بعد ذلك، نقوم بتعيين مرونة حجم العرض الجذر على RCTRootViewSizeFlexibilityHeight، مما يعني أنه سيتم استدعاء التابع ‎‎rootViewDidChangeIntrinsicSize:‎‎ في كل مرة يُغيِّر فيها محتوى React Native ارتفاعه. أخيرًا، نعيّن اتّساع (width) العرض الجذر وموضعه. لاحظ أننا حددنا ارتفاعه أيضًا، لكن ليس له أي تأثير لأنّنا جعلنا الارتفاع معتمداً على React Native.

يمكنك تفقد شيفرة المصدر الكاملة للمثال هنا.

لا بأس في تغيير وضع مرونة حجم العرض الجذر بشكل ديناميكي. سيؤدي تغيير وضع مرونة العرض الجذر إلى جدولة إعادة حساب تخطيطٍ (layout recalculation) و سيُستدعَى التابع المفوَّض ‎‎rootViewDidChangeIntrinsicSize:‎‎ بمجرد معرفة حجم المحتوى.

ملاحظة: يُنفَّذ حساب تخطيط React Native في سلسلة (thread) خاصة، في حين أن تحديثات عرض واجهة المستخدم في الجهة الأصيلة تؤدّى في السلسلة الرئيسية. قد يتسبب هذا في عدم تناسق مؤقت في واجهة المستخدم بين الجهة الأصيلة وجهة React Native. هذه مشكلة معروفة. ويعمل فريق React Native على مزامنة تحديثات واجهة المستخدم الواردة من مصادر مختلفة.

ملاحظة: لا يُنفِّذ React Native أي حسابات تخطيط حتى يصبح العرض الجذر عرضا فرعيا (subview) لبعض العروض الأخرى. إذا كنت تريد إخفاء عرض React Native حتى تُعرف أبعاده، فأضف العرض الجذر كعرض فرعي واجعله مخفيًّا مبدئيًّا (استخدم خاصية hidden الخاصة بالصنف UIView). ثم غيّر قابلية رؤيته (visibility) في التابع المفوَّض.

مصادر