跳到主要内容

Headless JS

Headless JS 是一种在应用处于后台时用 JavaScript 运行任务的方式。例如,它可用于同步最新数据、处理推送通知或播放音乐。

JS API

任务是一个你在 AppRegistry 上注册的异步函数,类似于注册 React 应用:

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

然后,在 SomeTaskName.js 中:

React TSX
module.exports = async taskData => {
// 做一些事情
};

你可以在任务中做任何事情,比如网络请求、定时器等等,只要不触及 UI。任务完成后(即 promise 被 resolve 时),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, // 任务超时时间(毫秒)
false // 可选:定义该任务是否允许在前台运行。默认值为 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, // 最大重试次数
1000 // 每次重试之间的延迟
);

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

只有在抛出特定的 Error 时才会进行重试。在 headless JS 任务中,你可以导入该错误,并在需要重试时抛出它。

示例:

React 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) {
/**
每次网络连接发生变化时都会调用这部分
例如:已连接 -> 未连接
**/
if (!isAppOnForeground((context))) {
/**
我们将启动服务并发送有关
网络连接的额外信息
**/
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) {
/**
我们需要检查应用是否在前台,否则应用会崩溃。
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;
}

// 在 API 级别 29 中已弃用
NetworkInfo netInfo = cm.getActiveNetworkInfo();
return (netInfo != null && netInfo.isConnected());
}
}