تشغيل JavaScript في الخلفية (Headless JS) في React Native

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

تعد Headless JS طريقة لتشغيل المهام في JavaScript أثناء وجود التطبيق في الخلفية. يمكن استخدامها مثلًا لمزامنة البيانات الجديدة أو التعامل مع إشعارات الدفع (push notifications) أو تشغيل الموسيقى.

واجهة برمجة تطبيقات JavaScript

المهام (tasks) هي دوال غير متزامنة (async function) بسيطة تُسجّلها في AppRegistry، على غرار تسجيل تطبيقات React:

import { AppRegistry } from 'react-native';
AppRegistry.registerHeadlessTask('SomeTaskName', () =>
  require('SomeTaskName')
);

ثم في SomeTaskName.js (الذي يمثّل اسم الملفّ الذي يحمل المهمّة):

module.exports = async (taskData) => {
  // افعل ما تشاء
};

يمكنك القيام بأي شيء في مهمتك، كطلبات الشبكة والمؤقّتات وما إلى ذلك، شرطَ ألّا تتعلّق بواجهة المستخدم. بمجرد اكتمال مهمتك (أي بعد حل الوعد)، سينتقل React Native إلى الوضع "متوقف مؤقتًا (paused)" (ما لم تكن هناك مهام أخرى قيد التشغيل، أو ما لم يوجد تطبيق في المقدمة [foreground]).

واجهة برمجة تطبيقات Java

لا يزال هذا يتطلب القليل من الشيفرة الأصيلة. تحتاج إلى توسيع HeadlessJsTaskService وإعادة تعريف getTaskConfig، على سبيل المثال:

package com.your_application_name;
import android.content.Intent;
import android.os.Bundle;
import com.facebook.react.HeadlessJsTaskService;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.jstasks.HeadlessJsTaskConfig;
import javax.annotation.Nullable;

public class MyTaskService extends HeadlessJsTaskService {

  @Override
  protected @Nullable HeadlessJsTaskConfig getTaskConfig(Intent intent) {
    Bundle extras = intent.getExtras();
    if (extras != null) {
      return new HeadlessJsTaskConfig(
          "SomeTaskName",
          Arguments.fromBundle(extras),
          5000, // timeout for the task   مُهلَة المهمّة
          false // خيار اختياريّ يحدّد ما إذا كان من المسموح تشغيل المهمّة في المقدّمة. هذه هي القيمة الافتراضيّة
        );
    }
    return null;
  }
}

ثم أضف الخدمة إلى ملف AndroidManifest.xml الخاص بك:

<service android:name="com.example.MyTaskService" />

الآن، متى ما بدأت خدمتك كمهمة دورية مثلًا أو استجابةً لبعض أحداث النظام أو بثٍّ (broadcast) ما، ستعمل JS على بدء الدوران (spin up) وتشغيل المهمة ثم ستتوقّف عن الدوران (spin down).

مثال:

Intent service = new Intent(getApplicationContext(), MyTaskService.class);
Bundle bundle = new Bundle();

bundle.putString("foo", "bar");
service.putExtras(bundle);

getApplicationContext().startService(service);

إعادة المحاولة

لن تنفذ headless JS أي إعادة محاولة بشكل افتراضي، للقيام بذلك لا بد من إنشاء HeadlessJsRetryPolicy ورمي خطأ Error محدد. يعد LinearCountingRetryPolicy تنفيذًا لها، ويسمح بتحديد الحد الأعلى لعدد مرات إعادة المحاولة بفوارق ثابتة بينها. إذا لم يناسبك هذا يمكنك تنفيذ HeadlessJsRetryPolicy خاصة بك، والتي يمكن أن تمرَّر كمعامل إضافي للباني HeadlessJsTaskConfig، كما في المثال التالي:

HeadlessJsRetryPolicy retryPolicy = new LinearCountingRetryPolicy(
  3, // الحد لأعلى لكرات إعادة المحاولة
  1000 // التأخير بين كل محاولة
);

return new HeadlessJsTaskConfig(
  'SomeTaskName',
  Arguments.fromBundle(extras),
  5000,
  false,
  retryPolicy
);

تنفذ إعادة المحاولة فقط عندما يُرمى خطأ error محدد، يمكنك استيراد الخطأ ورميه عندما تحتاج لإعادة المحاولة، كما في المثال:

import {HeadlessJsTaskError} from 'HeadlessJsTask';

module.exports = async (taskData) => {
  const condition = ...;
  if (!condition) {
    throw new HeadlessJsTaskError();
  }
};

إذا أردت أن تسبب كل الأخطاء إعادة محاولة فعليك التقاطها ورمي الخطأ السابق.

المحاذير

  • لا تتصرف الدالة التي تمرر إلى setTimeout كما هو متوقع دائمًا، بدلًا من ذلك تستدعى الدالة فقط عندما يفتح التطبيق من جديد، إذا كنت بحاجة إلى الانتظار فقط ، فاستخدم وظيفة إعادة المحاولة
  • افتراضيًّا، سيتعطل (crash) تطبيقك إذا حاولت تشغيل مهمةٍ أثناء وجود التطبيق في المقدمة. يمنع هذا المطورين من الوقوع في خطأ القيام بالكثير من العمل في مهمة وإبطاء واجهة المستخدم. يمكنك تمرير مُعاملٍ منطقيّ رابعٍ للتحكم في هذا السلوك.
  • إذا بدأت تشغيل خدمتك من BroadcastReceiver، فتأكد من استدعاء ‎‎HeadlessJsTaskService.acquireWakeLockNow()‎‎ قبل الإعادة من ‎‎onReceive()‎‎.

مثال للاستخدام

يمكن بدء الخدمة من واجهة برمجة Java. تحتاج أولًا إلى تحديد موعد بدء الخدمة وإجراء (implement) الحل الخاص بك وفقًا لذلك. فيما يلي مثال بسيط يتفاعل مع تغيير اتصال الشبكة.

الأسطر التالية تعرض جزءًا من ملف Android manifest لتسجيل مستقبل البث (broadcast receiver).

<receiver android:name=".NetworkChangeReceiver" >
  <intent-filter>
    <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
  </intent-filter>
</receiver>

يتلقى مُستقبِل البَث بعد ذلك النية (intent) التي تم بثها في الدالة onReceive. هذا مكان جيّد للتحقق مما إذا كان تطبيقك في المقدمة أم لا. إذا لم يكن التطبيق في المقدمة، فيمكننا إعداد نيتنا لبدئها، مع عدم وجود معلومات أو معلومات إضافية مجمَّعة باستخدام putExtra (ضع في الحسبان أن الحزمة يمكن أن تتعامل مع القيم القابلة للتحزيم parcelable فقط). في النهاية، تبدأ الخدمة ويتم الحصول على wakelock (تعد wake lock آلية للإشارة إلى أن تطبيقك بحاجة إلى امتلاك تركيز الجهاز وإبقاءه).

public class NetworkChangeReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(final Context context, final Intent intent) {
        /**
          سيُستدعى هذا الجزء في كل مرّة يتغيّر فيها اتصال الشبكة
          e.g. Connected -> Not Connected
        **/
        if (!isAppOnForeground((context))) {
            /**
              سنبدأ خدمتنا ونرسل معلومات إضافيّة حول اتصالات الشبكة
            **/
            boolean hasInternet = isNetworkAvailable(context);
            Intent serviceIntent = new Intent(context, MyTaskService.class);
            serviceIntent.putExtra("hasInternet", hasInternet);
            context.startService(serviceIntent);
            HeadlessJsTaskService.acquireWakeLockNow(context);
        }
    }

    private boolean isAppOnForeground(Context context) {
        /**
          نحتاج إلى التحقق من وجود التطبيق في المقدّمة وإلا سيتعطل التطبيق
         http://stackoverflow.com/questions/8489993/check-android-application-is-in-foreground-or-not
        **/
        ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        List<ActivityManager.RunningAppProcessInfo> appProcesses =
        activityManager.getRunningAppProcesses();
        if (appProcesses == null) {
            return false;
        }
        final String packageName = context.getPackageName();
        for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
            if (appProcess.importance ==
            ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND &&
             appProcess.processName.equals(packageName)) {
                return true;
            }
        }
        return false;
    }

    public static boolean isNetworkAvailable(Context context) {
        ConnectivityManager cm = (ConnectivityManager)
        context.getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo netInfo = cm.getActiveNetworkInfo();
        return (netInfo != null && netInfo.isConnected());
    }


}

مصادر