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

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

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

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

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

AppRegistry.registerHeadlessTask('SomeTaskName', () => require('SomeTaskName'));

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

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

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

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

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

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, // مُهلَة المهمّة
          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);

المحاذير

  • افتراضيًّا، سيتعطل (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.

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());
    }


}

مصادر