7. React Native 热更新

RN热更新

相信用过RN的朋友们都知道 , 就算只写一行代码 , 打包出来的apk都很大, 和electron有异曲同工之妙
手里刚好有个RN项目 , 一些小的更改(没动原生)的情况下, 都要重新前往应用市场下载 , 又费流量又不方便
所以新增了关于热更的优化
由于我们的项目只有android端 , 所以以下内容全部针对android

看网上很多人都是使用的CodePush , 甚至连ChartGpt都让我用CodePush , 但是根据各种因素考虑, 我不想使用CodePush , 而且根据我对RN的粗浅了解 , 要实现也没想象的复杂, 没必要使用三方库

首先,要具备基础的RN与原生通信的基础,因为核心功能还得使用java实现

其次看自己怎么设计了

原理

在RN中 , js会被打包成index.android.bundle,

然后在java入口处, 也就是MainApplication.java文件中, 会调用getJsBundleFile方法 , 来确定用于渲染的bundle文件

默认是返回null , 使用系统包内自带的index.android.bundle文件;

通过重写该方法 , 让其 return 我们所需的目标bundle的路径 , 即可实现功能了

index.android.bundle 有两种获取方式;

  1. 通过打包apk, 解压缩apk, 在assets目录中 , 获取index.android.bundle (不推荐)
  2. 使用Metro打包生成相应的文件:
npx react-native bundle --entry-file index.js --dev false --minify true --bundle-output ./build/index.android.bundle --assets-dest ./build --platform android 

注: 如果使用上述命令 ,要自己新建build文件夹 , 否则会出错, 输出文件(夹)名可以自定义, 但是下载的文件名必须和getJsBundleFile中返回的一致

其实到这里, 都可以去自己实现了

我的实现步骤

定义了一个接口 , 用来保存需要的数据

public interface FileConstants{
    // 获取JSBundle的完整路径
    static String getJsBundleLocalPath(Context context){
        return context.getExternalFilesDir("").getAbsoluteFile() + "/"+JS_BUNDLE_RELATIVE_PATH;
    }
    // 文件名
    String JS_BUNDLE_NAME = "index.android.bundle";
    // 文件夹名
    String JS_BUNDLE_DIR_NAME = "HotUpdate";
    // 相对路径
    String JS_BUNDLE_RELATIVE_PATH =JS_BUNDLE_DIR_NAME+"/"+JS_BUNDLE_NAME;
}

重写getJSBundleFile

MainApplication

  private final ReactNativeHost mReactNativeHost =
      new ReactNativeHost(this) {
		// ...
        @Nullable
        @Override
        protected String getJSBundleFile() {
		  // 获取jsBundle所在的文件夹
          String dir = FileConstants.getJsBundleLocalPath(this.getApplication());
          File file = new File(dir);
		  // 判断文件是否存在 , 存在就说明有新的jsBundle , 不存在就使用默认的bundle
		  if (file.exists()){
            return dir;
          }
          return super.getJSBundleFile();
        }
		// ....
      };

实现热更功能

下载JSBundle

  
	// 定义广播
    private  BroadcastReceiver receiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            long completeId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID,-1);
            if (completeId == mDownloadId){
				// 也没干啥 , 就是下载成功后 , 弹个消息
                Toast.makeText(context,"下载成功!",Toast.LENGTH_SHORT).show();
                WritableMap map = Arguments.createMap();
                // 传值
                map.putString("status","success");
                map.putInt("progress",100);
                // 发送事件, 在js中监听该事件, 获取数据
                context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("onUpdateDownload",map);
            }
        }
    };
    /**
     * 下载JSBundle
     * @param {String} url 下载地址
     */
    private void downloadBundle(String url){
        File file = new File(FileConstants.getJsBundleLocalPath(context));
        if (!file.getParentFile().exists()){
            file.getParentFile().mkdirs();
        }else if(file.exists()){
            file.delete();
        }
        downloadManager = (DownloadManager)context.getSystemService(Context.DOWNLOAD_SERVICE);
        DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
        request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN);
        request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE | DownloadManager.Request.NETWORK_WIFI);
        request.setDestinationUri(Uri.fromFile(file));
        mDownloadId = downloadManager.enqueue(request);
		// 注册广播
        context.registerReceiver(receiver,new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
    }

实现RN方法 , 用于js调用

	@ReactMethod
    public void update(String url){
        downloadBundle(url);
        // 刷新下载进度
        Timer timer = new Timer();
        // 上一个时间点的下载进度 , 用于计算下载速度
        final int[] last = {0};
        // 定时任务, 每秒钟请求下下载状态
        TimerTask task = new TimerTask() {
            @SuppressLint("Range")
            @Override
            public void run() {
                // 这玩意儿相当于给js传json
                WritableMap map = Arguments.createMap();
                DownloadManager.Query query = new DownloadManager.Query();
                Cursor cursor = downloadManager.query(query.setFilterById(mDownloadId ));
                if (cursor != null && cursor.moveToFirst()) {
                    // 检查下载状态
                    String downloadFlag = checkDownloadStatus(mDownloadId,timer);
                    // 已下载大小
                    int downloaded = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
                    // 文件总大小
                    int total = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
                    // 下载进度(百分比)
                    int pro = (downloaded * 100) / total;
                    // 传值
                    map.putString("status",downloadFlag);
                    map.putInt("progress",pro);
                    map.putInt("fileSize",total);
                    map.putInt("downloaded",downloaded);
                    map.putInt("speed",downloaded - last[0]);
                    last[0] = downloaded;
                    // 发送事件, 在js中监听该事件, 获取数据
                    context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("onUpdateDownload",map);
                }
                cursor.close();
            }
        };
        timer.schedule(task, 0, 1000);
    }

检查下载状态

	//检查下载状态
    private String checkDownloadStatus(long downloadId,Timer timer) {
        DownloadManager.Query query = new DownloadManager.Query();
        query.setFilterById(downloadId );//筛选下载任务,传入任务ID,可变参数
        Cursor c = downloadManager.query(query);
        boolean hasCancel = false;
        String flag = "";
        if (c.moveToFirst()) {
            @SuppressLint("Range") int status = c.getInt(c.getColumnIndex(DownloadManager.COLUMN_STATUS));
            switch (status) {
                case DownloadManager.STATUS_PAUSED:
                    flag = "pause";
                    break;
                case DownloadManager.STATUS_PENDING:
                    hasCancel = true;
                    flag = "timeout";
                    break;
                case DownloadManager.STATUS_RUNNING:
                    flag = "downloading";
                    break;
                case DownloadManager.STATUS_SUCCESSFUL:
                    hasCancel = true;
                    flag = "success";
                    break;
                case DownloadManager.STATUS_FAILED:
                    hasCancel = true;
                    flag = "fail";
                    break;
            }

            if (timer != null && hasCancel)timer.cancel();



        }
        return flag;
    }

完整java代码

package com.mypackage.reactnative.hotUpdate;

import android.annotation.SuppressLint;
import android.app.DownloadManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.net.Uri;
import android.os.Environment;
import android.util.Log;
import android.widget.Toast;

import androidx.annotation.NonNull;

import com.facebook.react.ReactInstanceManager;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.onetoone.FileConstants;
import com.onetoone.MainApplication;
import com.umeng.commonsdk.statistics.common.MLog;

import java.io.File;
import java.util.Timer;
import java.util.TimerTask;


public class HotUpdate  extends ReactContextBaseJavaModule {
    private ReactApplicationContext context;
    long mDownloadId;
    DownloadManager downloadManager;
    private  BroadcastReceiver receiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            long completeId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID,-1);
            if (completeId == mDownloadId){
                Toast.makeText(context,"下载成功!",Toast.LENGTH_SHORT).show();
            }
        }
    };
    @NonNull
    @Override
    public String getName() {
        return "HotUpdate";
    }
    //检查下载状态
    private String checkDownloadStatus(long downloadId,Timer timer) {
        DownloadManager.Query query = new DownloadManager.Query();
        query.setFilterById(downloadId );//筛选下载任务,传入任务ID,可变参数
        Cursor c = downloadManager.query(query);
        boolean hasCancel = false;
        String flag = "";
        if (c.moveToFirst()) {
            @SuppressLint("Range") int status = c.getInt(c.getColumnIndex(DownloadManager.COLUMN_STATUS));
            switch (status) {
                case DownloadManager.STATUS_PAUSED:
                    flag = "pause";
                    break;
                case DownloadManager.STATUS_PENDING:
                    hasCancel = true;
                    flag = "timeout";
                    break;
                case DownloadManager.STATUS_RUNNING:
                    flag = "downloading";
                    break;
                case DownloadManager.STATUS_SUCCESSFUL:
                    hasCancel = true;
                    flag = "success";
                    break;
                case DownloadManager.STATUS_FAILED:
                    hasCancel = true;
                    flag = "fail";
                    break;
            }

            if (timer != null && hasCancel)timer.cancel();
        }
        return flag;
    }
    public HotUpdate(ReactApplicationContext context){
        super(context);
        this.context = context;
    }

    //下载JSBundle
    private void downloadBundle(){
        File file = new File(FileConstants.getJsBundleLocalPath(context));
        if (!file.getParentFile().exists()){
            file.getParentFile().mkdirs();
        }else if(file.exists()){
            file.delete();
        }
        downloadManager = (DownloadManager)context.getSystemService(Context.DOWNLOAD_SERVICE);
        DownloadManager.Request request = new DownloadManager.Request(Uri.parse(FileConstants.JS_BUNDLE_DOWNLOAD_URL));
		// 设置通知栏显示, 我这里不需要, 就隐藏吧
        request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN);
		// 设置网络模式
		// 这里得记得加入对应的网络权限
        request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE | DownloadManager.Request.NETWORK_WIFI);
        request.setDestinationUri(Uri.fromFile(file));
        mDownloadId = downloadManager.enqueue(request);
		// 对了  <uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" /> 这个权限也得加上

        context.registerReceiver(receiver,new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
    }

    @ReactMethod
    public void update(){
        downloadBundle();
        // 刷新下载进度
        Timer timer = new Timer();
        // 上一个时间点的下载进度 , 用于计算下载速度
        final int[] last = {0};
        // 定时任务, 每秒钟请求下下载状态
        TimerTask task = new TimerTask() {
            @SuppressLint("Range")
            @Override
            public void run() {
                // 这玩意儿相当于给js传json
                WritableMap map = Arguments.createMap();
                DownloadManager.Query query = new DownloadManager.Query();
                Cursor cursor = downloadManager.query(query.setFilterById(mDownloadId ));
                if (cursor != null && cursor.moveToFirst()) {
                    // 检查下载状态
                    String downloadFlag = checkDownloadStatus(mDownloadId,timer);
                    // 已下载大小
                    int downloaded = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
                    // 文件总大小
                    int total = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
                    // 下载进度(百分比)
                    int pro = (downloaded * 100) / total;
                    // 传值
                    map.putString("status",downloadFlag);
                    map.putInt("progress",pro);
                    map.putInt("fileSize",total);
                    map.putInt("downloaded",downloaded);
                    map.putInt("speed",downloaded - last[0]);
                    last[0] = downloaded;
                    // 发送事件, 在js中监听该事件, 获取数据
                    context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("onUpdateDownload",map);
                }
                cursor.close();
            }
        };
        timer.schedule(task, 0, 1000);
    }


}

然后需要注册热更模块,不然没法在js中使用, 不会的参考下这篇文章吧: 8.RN js调用原生方法

接下来的任务 就是在js中做相应的逻辑处理

// 调用方法 , 执行更新
 NativeModules.HotUpdate.update(下载地址);
// 监听更新事件 , 获取到对应的数据
DeviceEventEmitter.addListener("onUpdateDownload",处理方法)
// 然后重启就可以了

如何与后端交互就自己去沟通吧, 很简单的 , 就没必要讲了

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
回答: React Native更新是指在不需要重新发布和重新安装应用程序的情况下,通过更新应用程序的JavaScript代码来实现应用程序的更新。在安卓平台上,可以通过插件实现应用的更新,但是在iOS上却无法实现。这是因为苹果对应用的审核标准非常严格,并且发版速度较慢,所以更新能力对于iOS应用显得尤为重要。在React Native中,可以使用CodePush插件来实现更新。CodePush插件提供了一组API,可以用于获取当前已安装更新的元数据、检查是否有更新可用并下载更新等操作。具体的代码可以参考CodePush的示例应用中的demo.js文件,这个文件中包含了每次启动应用程序时检查更新和下载的代码。通过使用CodePush插件,开发者可以方便地实现React Native应用的更新功能。123 #### 引用[.reference_title] - *1* [快速了解react native 更新](https://blog.csdn.net/wangbaobao512/article/details/127241775)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}} ] [.reference_item] - *2* *3* [React Native CodePush 最新配置](https://blog.csdn.net/maomaoyu3211/article/details/84908556)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值