跳到主要內容

Headless JS

Headless JS 是一種在應用程式於背景執行時,在 JavaScript 中執行任務的方式。例如,它可以用於同步最新資料、處理推播通知或播放音樂。

JS API

任務是一個非同步函數,您可以在 AppRegistry 上註冊,類似於註冊 React 應用程式

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

然後,在 SomeTaskName.js

tsx
module.exports = async taskData => {
// do stuff
};

您可以在任務中執行任何操作,例如網路請求、計時器等等,只要不接觸 UI 即可。一旦您的任務完成(即 Promise 被解析),React Native 將進入「暫停」模式(除非有其他任務正在執行,或者有前景應用程式)。

平台 API

是的,這仍然需要一些原生程式碼,但非常精簡。您需要擴展 HeadlessJsTaskService 並覆寫 getTaskConfig,例如:

java
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 in milliseconds for the task
false // optional: defines whether or not the task is allowed in foreground. Default is false
);
}
return null;
}
}

然後將服務新增到 AndroidManifest.xml 檔案的 application 標籤內

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

現在,每當您啟動您的服務時,例如作為定期任務或回應某些系統事件/廣播,JS 將會啟動、執行您的任務,然後關閉。

範例

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

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

getApplicationContext().startForegroundService(service);

重試

預設情況下,headless JS 任務不會執行任何重試。為了做到這一點,您需要建立一個 HeadlessJsRetryPolicy 並拋出特定的 Error

LinearCountingRetryPolicyHeadlessJsRetryPolicy 的一個實作,它允許您指定最大重試次數,並在每次嘗試之間設定固定的延遲。如果這不符合您的需求,您可以實作自己的 HeadlessJsRetryPolicy。這些策略可以作為額外參數傳遞給 HeadlessJsTaskConfig 建構子,例如:

java
HeadlessJsRetryPolicy retryPolicy = new LinearCountingRetryPolicy(
3, // Max number of retry attempts
1000 // Delay between each retry attempt
);

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

只有在拋出特定的 Error 時才會進行重試嘗試。在 headless JS 任務中,您可以匯入錯誤,並在需要重試嘗試時拋出它。

範例

tsx
import {HeadlessJsTaskError} from 'HeadlessJsTask';

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

如果您希望所有錯誤都導致重試嘗試,您需要捕獲它們並拋出上述錯誤。

注意事項

  • 預設情況下,如果您嘗試在應用程式於前景時執行任務,您的應用程式將會崩潰。這是為了防止開發人員因在任務中執行大量工作並減慢 UI 速度而自找麻煩。您可以傳遞第四個 boolean 參數來控制此行為。
  • 如果您從 BroadcastReceiver 啟動服務,請務必在從 onReceive() 返回之前呼叫 HeadlessJsTaskService.acquireWakeLockNow()

範例用法

服務可以從 Java API 啟動。首先,您需要決定何時應該啟動服務,並相應地實作您的解決方案。以下是一個回應網路連線變更的範例。

以下幾行顯示了用於註冊廣播接收器的 Android manifest 檔案的一部分。

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

然後,廣播接收器處理在 onReceive 函數中廣播的 intent。這是檢查您的應用程式是否在前台的好地方。如果應用程式不在前台,我們可以準備要啟動的 intent,不使用任何資訊或使用 putExtra 捆綁額外資訊(請記住 bundle 只能處理 parcelable 值)。最後,服務啟動並取得 wakelock。

java
import android.app.ActivityManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.os.Build;

import com.facebook.react.HeadlessJsTaskService;

public class NetworkChangeReceiver extends BroadcastReceiver {

@Override
public void onReceive(final Context context, final Intent intent) {
/**
This part will be called every time network connection is changed
e.g. Connected -> Not Connected
**/
if (!isAppOnForeground((context))) {
/**
We will start our service and send extra info about
network connections
**/
boolean hasInternet = isNetworkAvailable(context);
Intent serviceIntent = new Intent(context, MyTaskService.class);
serviceIntent.putExtra("hasInternet", hasInternet);
context.startForegroundService(serviceIntent);
HeadlessJsTaskService.acquireWakeLockNow(context);
}
}

private boolean isAppOnForeground(Context context) {
/**
We need to check if app is in foreground otherwise the app will crash.
https://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);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Network networkCapabilities = cm.getActiveNetwork();

if(networkCapabilities == null) {
return false;
}

NetworkCapabilities actNw = cm.getNetworkCapabilities(networkCapabilities);

if(actNw == null) {
return false;
}

if(actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || actNw.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) || actNw.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {
return true;
}

return false;
}

// deprecated in API level 29
NetworkInfo netInfo = cm.getActiveNetworkInfo();
return (netInfo != null && netInfo.isConnected());
}
}