ReactNative 热更新bundle

准备服务器,用node.js启动以下服务器,提供index.android.bundle访问:

const http = require('http');
const fs = require('fs');

const server = http.createServer((req, res) => {
  if (req.url === '/index.android.bundle') {
    fs.readFile('./index.android.bundle', (err, data) => {
      if (err) {
        console.error('Failed to read index.android.bundle:', err);
        res.writeHead(500);
        res.end('Internal Server Error');
        return;
      }

      res.writeHead(200, {
        'Content-Type': 'application/javascript', // Adjust according to the actual MIME type of the bundle
        'Content-Length': data.length,
      });
      res.end(data);
    });
  } else {
    res.writeHead(404);
    res.end('Not Found');
  }
});

server.listen(80, () => {
  console.log('HTTP server running on port 8080, http://10.56.238.168:80/index.android.bundle');
});

主要Android代码

package com.demohot

import android.annotation.SuppressLint
import android.app.DownloadManager
import android.content.BroadcastReceiver
import android.content.ContentValues.TAG
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.database.Cursor
import android.net.Uri
import android.util.Log
import android.widget.Toast
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.modules.core.DeviceEventManagerModule
import okio.IOException
import java.io.File
import java.util.Timer
import java.util.TimerTask

class HotUpdate(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext)  {
    // 将接收器作为类成员变量,并在构造函数中注册
    private val downloadCompleteListener = DownloadCompleteReceiver { downloadedFilePath ->
        Log.d(TAG, "Stored at: $downloadedFilePath")
        Toast.makeText(this@HotUpdate.reactApplicationContext, "请手动重启APP. Stored at: "+downloadedFilePath, Toast.LENGTH_SHORT).show()

        // 在这里处理下载成功的逻辑,如通知RN端等
    }
    init {
        reactApplicationContext.registerReceiver(downloadCompleteListener, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))

    }

    override fun getName(): String {
        return "HotUpdate"
    }

    var mDownloadId: Long = 0
    var downloadManager: DownloadManager? = null
    private val receiver: BroadcastReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            val completeId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
            if (completeId == mDownloadId) {
                Toast.makeText(context, "下载成功!", Toast.LENGTH_SHORT).show()
            }
        }
    }
    //检查下载状态
    private fun checkDownloadStatus(downloadId: Long, timer: Timer?): String {
        val query = DownloadManager.Query()
        query.setFilterById(downloadId) //筛选下载任务,传入任务ID,可变参数
        val c = downloadManager!!.query(query)
        var hasCancel = false
        var flag = ""
        if (c.moveToFirst()) {
            @SuppressLint("Range") val status =
                c.getInt(c.getColumnIndex(DownloadManager.COLUMN_STATUS))
            when (status) {
                DownloadManager.STATUS_PAUSED -> flag = "pause"
                DownloadManager.STATUS_PENDING -> {
                    hasCancel = true
                    flag = "timeout"
                }

                DownloadManager.STATUS_RUNNING -> flag = "downloading"
                DownloadManager.STATUS_SUCCESSFUL -> {
                    hasCancel = true
                    flag = "success"
                }

                DownloadManager.STATUS_FAILED -> {
                    hasCancel = true
                    flag = "fail"
                }
            }
            if (timer != null && hasCancel) timer.cancel()
        }
        return flag
    }

    //下载JSBundle
    private fun downloadBundle(url:String) {
        val file = File(FileConstants.getJsBundleLocalPath())
        if (!file.parentFile.exists()) {
            file.parentFile.mkdirs()
        } else if (file.exists()) {
            file.delete()
        }

        downloadManager = reactApplicationContext.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
        val request = DownloadManager.Request(Uri.parse(url))
        // 设置通知栏显示, 我这里不需要, 就隐藏吧
        request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN)
        // 设置网络模式
        // 这里得记得加入对应的网络权限
        request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE or DownloadManager.Request.NETWORK_WIFI)
        request.setDestinationUri(Uri.fromFile(file))
        mDownloadId = downloadManager!!.enqueue(request)

        // 对了  <uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" /> 这个权限也得加上
    }

    @ReactMethod
    fun update(url:String) {

        // 刷新下载进度
        val timer = Timer()
        // 上一个时间点的下载进度 , 用于计算下载速度
        val last = intArrayOf(0)

        downloadBundle(url)

        // 定时任务, 每秒钟请求下下载状态
        val task: TimerTask = object : TimerTask() {
            @SuppressLint("Range")
            override fun run() {
                // 这玩意儿相当于给js传json
                val map = Arguments.createMap()
                val query = DownloadManager.Query()
                val cursor = downloadManager!!.query(query.setFilterById(mDownloadId))
                if (cursor != null && cursor.moveToFirst()) {
                    // 检查下载状态
                    val downloadFlag = checkDownloadStatus(mDownloadId, timer)
                    // 已下载大小
                    val downloaded =
                        cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
                    // 文件总大小
                    val total =
                        cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
                    // 下载进度(百分比)
                    val 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中监听该事件, 获取数据
                    reactApplicationContext.getJSModule(
                        DeviceEventManagerModule.RCTDeviceEventEmitter::class.java
                    ).emit("onUpdateDownload", map)
                }
                cursor!!.close()
            }
        }
        timer.schedule(task, 0, 1000)


    }


    @ReactMethod
    fun createUpdateEvent(name: String, location: String, callback: Callback) {
        Toast.makeText(this.reactApplicationContext,FileConstants.getJsBundleLocalPath(),Toast.LENGTH_LONG).show()
        Log.d("CalendarModule","Create event called with name: $name and location: $location")
        val eventId =FileConstants.getJsBundleLocalPath()
        callback.invoke(eventId)
    }

    inner class DownloadCompleteReceiver(private val onDownloadSuccess: (String) -> Unit) : BroadcastReceiver() {

        override fun onReceive(context: Context?, intent: Intent?) {
            if (intent?.action == DownloadManager.ACTION_DOWNLOAD_COMPLETE) {
                val downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1L)
                if (downloadId == mDownloadId) {
                    val downloadManager = context?.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
                    val query = DownloadManager.Query().setFilterById(downloadId)
                    val cursor = downloadManager.query(query)

                    if (cursor.moveToFirst()) {
                        var status =this.getStatus(DownloadManager.COLUMN_STATUS,cursor)
                        when (status) {
                            DownloadManager.STATUS_SUCCESSFUL -> {
                                val columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)
                                var localUri="";
                                if (columnIndex != -1) {
                                    localUri = cursor.getString(columnIndex)
                                }
                                // 下载成功,调用回调函数并传入存储路径
                                onDownloadSuccess(localUri)
                            }
                            DownloadManager.STATUS_PENDING,
                            DownloadManager.STATUS_FAILED,
                            DownloadManager.STATUS_PAUSED,
                            DownloadManager.STATUS_PENDING,
                            DownloadManager.STATUS_RUNNING -> {
                                var error = this.getStatus(DownloadManager.COLUMN_REASON, cursor)
                                val errorMsg = when (error) {
                                    DownloadManager.ERROR_FILE_ALREADY_EXISTS -> "File already exists"
                                    DownloadManager.ERROR_HTTP_DATA_ERROR -> "HTTP data error"
                                    DownloadManager.ERROR_INSUFFICIENT_SPACE -> "Insufficient space"
                                    DownloadManager.ERROR_TOO_MANY_REDIRECTS -> "Too many redirects"
                                    DownloadManager.ERROR_UNHANDLED_HTTP_CODE -> "Unhandled HTTP code"
                                    DownloadManager.ERROR_UNKNOWN -> "Unknown error"
                                    else -> "Unknown error code: $error"
                                }
                                Log.e(TAG, "Download failed with status $status and reason: $errorMsg")
                            }
                        }
                    }
                    cursor.close()
                }
            }
        }

        private fun getStatus(status: String, cursor: Cursor):Int{
            val columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)
            if (columnIndex != -1) {
                return cursor.getInt(columnIndex)

            }
            return  0;
        }
    }
}
package com.demohot

import android.app.Application
import android.os.Environment
import android.util.Log
import android.widget.Toast
import androidx.core.content.PackageManagerCompat.LOG_TAG
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.flipper.ReactNativeFlipper
import com.facebook.soloader.SoLoader
import java.io.File

class MainApplication : Application(), ReactApplication {

  override val reactNativeHost: ReactNativeHost =
      object : DefaultReactNativeHost(this) {
        override fun getPackages(): List<ReactPackage> =
            PackageList(this).packages.apply {
              // Packages that cannot be autolinked yet can be added manually here, for example:
              add(MyAppPackage())
              add(MyUpdatePackage())

            }
        override fun getJSBundleFile(): String? {
//          val bundleUrl = UpdateContext.getBundleUrl(this@MainApplication)
//            Log.d("MainApplication", "Bundle URL: $bundleUrl")
//                  return "http://10.56.238.168:8080/index.android.bundle"
        //          return "ws://localhost:8080/message?device=Android%20SDK%20built%20for%20x86_64%20-%2013%20-%20API%2033&app=com.awesomeproject&clientid=BridgeDevSupportManager"
//            // 获取jsBundle所在的文件夹
//            val dir = FileConstants.getJsBundleLocalPath(this.application)
//            val file = File(dir)
//            // 判断文件是否存在 , 存在就说明有新的jsBundle , 不存在就使用默认的bundle
//             if (file.exists()) {
//                 return dir
//            }
//          return bundleUrl
            // 获取jsBundle所在的文件夹
            Log.d(LOG_TAG, "================================")
            Log.d("path", application.externalCacheDir!!.absolutePath)

            Log.d(LOG_TAG, "================================")

            val dir = FileConstants.getJsBundleLocalPath(application.externalCacheDir!!.absolutePath)
            val file = File(dir);
            Toast.makeText(this.application.applicationContext,dir, Toast.LENGTH_LONG).show()

            // 判断文件是否存在 , 存在就说明有新的jsBundle , 不存在就使用默认的bundle
            if (file.exists()){
                Toast.makeText(this.application.applicationContext, "文件存在:$dir",Toast.LENGTH_LONG).show()
                return dir;
            }
            return super.getJSBundleFile();
//            return Environment.getExternalStorageDirectory().absolutePath+File.separator+"bundles/index.android.bundle"

        }
        override fun getJSMainModuleName(): String = "index"

        override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG

        override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
        override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
      }

  override val reactHost: ReactHost
    get() = getDefaultReactHost(this.applicationContext, reactNativeHost)

  override fun onCreate() {
    super.onCreate()
    SoLoader.init(this, false)
    if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
      // If you opted-in for the New Architecture, we load the native entry point for this app.
      load()
    }
    ReactNativeFlipper.initializeFlipper(this, reactNativeHost.reactInstanceManager)
  }
}

demo下载地址:【免费】ReatNative热更新demo,完整自创的demo,通用ReatNative热更新方案资源-CSDN文库

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值