写点什么

ReactNative | 项目复盘,涉及环境、RN 版本升级、安全等方案

用户头像
梁龙先森
关注
发布于: 2020 年 12 月 15 日
ReactNative | 项目复盘,涉及环境、RN版本升级、安全等方案

背景

此篇文章,复盘下,短期兼职 RN 开发遇到的一些问题,附:JS-OC 通信详解。

一、环境问题

如果你是首次使用 RN,最初搭环境下依赖包构建 APK,建立你科学上网,因为容易少包。

  1. 编译运行

react-native run-android
复制代码
  1. 报错:device is unauthoried

adb devices // 执行此命令,设备会弹窗授权
复制代码
  1. 清除编译报错缓存

gradlew.bat clean 或者 gradle clean
复制代码
  1. 打包

cd android && ./gradlew installRelease(安卓无需./)
复制代码
  1. 打包发布发行版 apk,出现 unable to process incoming event 'ProcessComplete'

gradlew.bat assembleRelease --console plain
复制代码
  1. 测试运行打包闪退,执行此命令,可以查看错误日志

adb logcat AndroidRuntime:E *:S
复制代码
  1. 报错:React-Native version mismatch

1. 删除模拟器app2. 删除构建包(gradlew.bat clean)3. 删除adb缓存(adb uninstall com.***)4. 重新执行
复制代码
  1. 报错:Activity class {} does not exist.Error while Launching activity

1、检测MainActivity文件是否存在2、AndroidManifest.xml内部的package名需要跟android/app/build.gradle里面的applicationId一致3、adb uninstall com.**(包名) 重新运行
复制代码

二、开发调试

  1. 调试工具

react-native-debugger

  1. 手机 APP 开启调试

手机摇一摇,会弹窗调试选项:reload/live reload...等选项

  1. app 调用本机服务

app 的 10.0.2.2 默认映射到本机地址 127.0.0.1:8080

  1. 查看调试工具 Network 面板

注释掉react-native/Libraries/Core/InitializeCore.js中的polyfillGlobal('XMLHttpRequest', () => require('XMLHttpRequest'));暴露问题:注释掉此行代码,10.0.2.2将无法继续映射到本机地址127.0.0.1
复制代码

三、RN 大版本升级方案

项目 2 年没人碰,版本为 0.52 版本,当时认为老旧,因此提出升级到 0.62 版本。

升级版本有 2 种方案,一种是对比新旧版本变更改动,在老版本代码一处一处改动,另一种是新建新版本项目,迁移业务代码过去。

1、改造老版本工程

版本升级,RN 社区也提供了方案,在老工程上改造,地址为:https://react-native-community.github.io/upgrade-helper,可以选择从指定版本到指定版本,系统会生成新旧代码的对照,自己比对,改造老项目。


考虑到比对工作量,也怕比对有出入,作为新手,难以把控,最终放弃这个方案。

2、新建工程承载业务代码

这是我采取的方案,罗列下升级流程,当然在运行中构建会报很多错,以及组件提示弃用或者不再支持,依次去修改即可,全程可控。

  1. 新建 0.62 RN 初始项目。

  2. 迁移业务代码 与 构建配置(android 目录底下)。

配置修改:build.gradle/MainApplication/...

迁移 keystore/gradle.properties

  1. 修改三方库版本问题(升级|改造)。

大版本升级后,依赖的三方库可能未升级,因此需要将三方库抽离出来改造,当做项目库,切莫继续放在 node_modules 底下,防止被覆盖。

  1. RN 基础组件升级修复(如,webview 组件的剥离)。

四、WebView 与 H5 通信

1、WebView 配置说明
  1. 属性

automaticallyAdjustContentInsets控制是否调整放置在导航条、标签栏或工具栏后面的web视图的内容。默认值为true
contentInset {top: number, left: number, bottom: number, right: number}设置网页内嵌边距
injectedJavaScript设置在网页加载之前注入一段js代码
mediaPlaybackRequiresUserAction设置页面中的HTML5音视频是否需要在用户点击后再开始播放。默认值为true
scalesPageToFit设置是否要把网页缩放到适应视图的大小,以及是否允许用户改变缩放比例。
source在WebView中指定加载内容html或者url,可以指定header,method等
startInLoadingState强制WebView在第一次加载时先显示loading视图。默认为true
domStorageEnabled(android)布尔值,指定是否开启DOM本地存储
javaScriptEnabled(android)布尔值,指定WebView中是否启用JavaScript。只在Android上使用,因为在iOS上默认启用了JavaScript。
mixedContentMode(android)指定混合内容模式。即WebView是否应该允许安全链接(https)页面中加载非安全链接(http)的内容,
'never' (默认) - WebView不允许安全链接页面中加载非安全链接的内容'always' - WebView允许安全链接页面中加载非安全链接的内容。'compatibility' - WebView会尽量和浏览器当前对待此情况的行为一致userAgent(android)为WebView设置user-agent字符串标识。这一字符串也可以在原生端用WebViewConfig来设置,但js端的设置会覆盖原生端的设置。
allowsInlineMediaPlayback(ios)指定HTML5视频是在网页当前位置播放还是使用原生的全屏播放器播放。 默认值为false,视频在网页播放还需要设置webkit-playsinline
bounces(ios)指定滑动到边缘后是否有回弹效果。
decelerationRate(ios)指定一个浮点数,用于设置在用户停止触摸之后,此视图应以多快的速度停止滚动。也可以指定预设的字符串值,如"normal"和"fast",
scrollEnabled(ios)是否启用滚动
复制代码
  1. 函数

injectJavaScript函数接受一个字符串,该字符串将传递给WebView,并立即执行为JavaScript
onError加载失败时回调
onLoad完成加载时回调
onLoadEnd加载成功或者失败都会回调
onLoadStart开始加载的时候回调
onMessage在webView内部网页中,调用window.postMessage可以触发此属性对应的函数,通过event.nativeEvent.data获取接收到的数据,实现网页和RN之间的数据传递
renderError返回一个视图用来提示用户错误
renderLoading返回一个加载指示器
onShouldStartLoadWithRequest(ios)请求自定义处理,返回true或false表示是否要继续执行响应的请求。
复制代码
2、WebView 向 H5 注入 Js

通过 injectedJavaScript 注入 JS ,在 H5 页面加载之后立即执行。相当于 webview 端主动调用 H5 的方法。注入的内容可以是方法实现,也可以是方法名字。

// 注入H5页面的代码const H5AppBridge = `   document.cookie = "from=RN;path=/"`
const TestWebView = createReactClass({ getInitialState(){ return{ url:'http://****' } }, render(){ return( <SafeAreaView> <WebView ref='webView' source={{uri:this.state.url}} injectedJavaScript={H5AppBridge} /> </SafeAreaView> ) }})
复制代码
3、WebView 和 H5 相互发送监听消息
  1. RN 向 H5 发送消息

// RN端 推送消息const TestWebView = createReactClass({   getInitialState(){      return{         url:'http://****'      }   },   render(){      return(        <SafeAreaView>           <WebView              ref='webView'             source={{uri:this.state.url}}             onLoadEnd={()=>{                // 也可以在其他地方获取webview,执行消息推送                this.refs.webView.postMessage("我是RN,呼叫H5")             }}           />        </SafeAreaView>      )   }})
// H5端 监听消息window.onload = function(){ // 监听的名字只能是"message" document.addEventListener("message",function(e){ console.log("我是H5,收到消息",e.data) })}
复制代码
  1. H5 向 RN 发送消息

// RN端  监听const TestWebView = createReactClass({   getInitialState(){      return{         url:'http://****'      }   },   render(){      return(        <SafeAreaView>           <WebView              ref='webView'             source={{uri:this.state.url}}             onMessage={(event) => {                console.log("RN收到H5呼叫",event.nativeEvent.data);             }}           />        </SafeAreaView>      )   }})
// H5端 推送
window.postMessage('网页向rn发送的消息');
复制代码

注意:webview 组件在被官方放弃之后,react-native-webview 作为替代,react-native-webview 5.0 版本需要往 H5 注入 js:

const injectedJavascript = `(function() {  window.postMessage = function(data) {    window.ReactNativeWebView.postMessage(data);  };})()`;
复制代码

五、RCTDeviceEventEmitter

RCTDeviceEventEmitter 是 RN 的原生模块,初始化项目的时候已经下载到 node_modules,地址为:node_modules/react-native/Libraries/EventEmitter 目录下,所以在项目中可以通过 require("RCTDeviceEventEmitter")直接引用,因此在 package.json 也就找不到依赖。


RCTDeviceEventEmitter 主要用于 native 向 js 发送消息,当然也可以通过 js 向 js 发送消息,使用例子如下:

const RCTDeviceEventEmitter = require('RCTDeviceEventEmitter');
// 监听消息 RCTDeviceEventEmitter.addListener("test",function(res)=>{ console.log("接收到监听消息",res) })
// 发送消息 RCTDeviceEventEmitter.emit('test', {})
复制代码

当然,应用上也可以使用 redux 来共享数据状态。

六、安全问题

1、so 库未使用编译器堆栈保护技术

修复方案:

位置:node_modules\react-native\ReactAndroid\src\main\jni\Application.mk  

新增指令:LOCAL_CFLAGS := -Wall -O2 -U_FORTIFY_SOURCE -fstack-protector-all

2、Janus 漏洞
修复方案一:用开发软件签名并打包安卓工程时,同时勾选V1和V2签名选项;修复方案二:命令行签名方式,使用keytool、jarsigner||apksigner工具签名  1) 使用keytool工具生成数字证书:   keytool -genkeypair -keystore 密钥库名 -alias 密钥别名 -validity 天数 -keyalg RSA   (或者也可以采用开发软件生成签名文件.jsk)  2) 使用jarsigner||apksigner工具为Android应用程序签名   jarsigner -keystore 密钥库名 xxx.apk 密钥别名   apksigner sign --ks 密钥库名 --ks-key-alias 密钥别名 xxx.apk
复制代码
3、应用数据任意备份风险

修复方案:android:allowBackup 属性值设为 false。

4、使用 360 加固助手加固应用

七、总结

初探 RN,让人感慨跨端的强大,之前有过短暂原生 Android 开发经历,对于前端人员跨门槛高,混合开发大大降低成本。文章主要介绍通信相关,下面一起看看 JS-OC 通信原理。

附:JS-OC 通信详解

1、概述

React Native 用 iOS 自带的 JavaScriptCore 作为 JS 的解析引擎,但并没有用到 JavaScriptCore 提供的一些可以让 JS 与 OC 互调的特性,而是自己实现了一套机制,这套机制可以通用于所有 JS 引擎上,在没有 JavaScriptCore 的情况下也可以用 webview 代替,实际上项目里就已经有了用 webview 作为解析引擎的实现,应该是用于兼容 iOS7 以下没有 JavascriptCore 的版本。


普通的 JS-OC 通信实际上很简单,OC 向 JS 传信息有现成的接口,像 webview 提供的-stringByEvaluatingJavaScriptFromString 方法可以直接在当前 context 上执行一段 JS 脚本,并且可以获取执行后的返回值,这个返回值就相当于 JS 向 OC 传递信息。React Native 也是以此为基础,通过各种手段,实现了在 OC 定义一个模块方法,JS 可以直接调用这个模块方法并还可以无缝衔接回调。


举个例子,OC 定义了一个模块 RCTSQLManager,里面有个方法-query:successCallback:,JS 可以直接调用 RCTSQLManager.query 并通过回调获取执行结果。

// OC:@implement RCTSQLManager- (void)query:(NSString *)queryData successCallback:(RCTResponseSenderBlOCk)responseSender{     RCT_EXPORT();     NSString *ret = @"ret"     responseSender(ret);}@end
复制代码


// JS:RCTSQLManager.query("SELECT * FROM table", function(result) {     //result == "ret";});
复制代码

接下来看看它是怎样实现。

2、模块配置

首先 OC 要告诉 JS 它有什么模块,模块里有什么方法,JS 才知道有这些方法后才有可能去调用这些方法。这里的实现是 OC 生成一份模块配置表传给 JS,配置表里包括了所有模块和模块里方法的信息。例:

{    "remoteModuleConfig": {        "RCTSQLManager": {            "methods": {                "query": {                    "type": "remote",                    "methodID": 0                }            },            "moduleID": 4        },        ...     },}
复制代码

OC 端和 JS 端分别各有一个 bridge,两个 bridge 都保存了同样一份模块配置表,JS 调用 OC 模块方法时,通过 bridge 里的配置表把模块方法转为模块 ID 和方法 ID 传给 OC,OC 通过 bridge 的模块配置表找到对应的方法执行之。

3、JS 调用 OC 详细流程


  1. JS 端调用某个 OC 模块暴露出来的方法。

  2. 上一步的调用分解为 ModuleName,MethodName,arguments,再扔给 MessageQueue 处理。

在初始化时模块配置表上的每一个模块都生成了对应的 remoteModule 对象,对象里也生成了跟模块配置表里一一对应的方法,这些方法里可以拿到自身的模块名,方法名,并对 callback 进行一些处理,再移交给 MessageQueue。

  1. 在这一步把 JS 的 callback 函数缓存在 MessageQueue 的一个成员变量里,用 CallbackID 代表 callback。在通过保存在 MessageQueue 的模块配置表把上一步传进来的 ModuleName 和 MethodName 转为 ModuleID 和 MethodID。

  2. 把上述步骤得到的 ModuleID,MethodId,CallbackID 和其他参数 argus 传给 OC。

JS 不会主动传递数据给 OC,在调 OC 方法时,会在上述第 4 步把 ModuleID,MethodID 等数据加到一个队列里,等 OC 过来调 JS 的任意方法时,再把这个队列返回给 OC,此时 OC 再执行这个队列里要调用的方法,便通过返回值把数据传给 OC。

  1. OC 接收到消息,通过模块配置表拿到对应的模块和方法。

实际上模块配置表已经经过处理了,跟 JS 一样,在初始化时 OC 也对模块配置表上的每一个模块生成了对应的实例并缓存起来,模块上的每一个方法也都生成了对应的 RCTModuleMethod 对象,这里通过 ModuleID 和 MethodID 取到对应的 Module 实例和 RCTModuleMethod 实例进行调用。

  1. RCTModuleMethod 对 JS 传过来的每一个参数进行处理。

RCTModuleMethod 可以拿到 OC 要调用的目标方法的每个参数类型,处理 JS 类型到目标类型的转换,所有 JS 传过来的数字都是 NSNumber,这里会转成对应的 int/long/double 等类型,更重要的是会为 block 类型参数的生成一个 block。

这些参数组装完毕后,通过 NSInvocation 动态调用相应的 OC 模块方法。

  1. OC 模块方法调用完,执行 block 回调。

  2. 调用到第 6 步说明的 RCTModuleMethod 生成的 block。

  3. block 里带着 CallbackID 和 block 传过来的参数去调 JS 里 MessageQueue 的方法 invokeCallbackAndReturnFlushedQueue。

  4. MessageQueue 通过 CallbackID 找到相应的 JS callback 方法。

  5. 调用 callback 方法,并把 OC 带过来的参数一起传过去,完成回调。

整个流程就是这样,简单概括下,差不多就是:JS 函数调用转 ModuleID/MethodID -> callback 转 CallbackID -> OC 根据 ID 拿到方法 -> 处理参数 -> 调用 OC 方法 -> 回调 CallbackID -> JS 通过 CallbackID 拿到 callback 执行。


附:JS-OC 通信详解篇幅参考来源于:React Native通信机制详解。


发布于: 2020 年 12 月 15 日阅读数: 299
用户头像

梁龙先森

关注

脚踏V8引擎的无情写作机器 2018.03.17 加入

还未添加个人简介

评论 (2 条评论)

发布
用户头像
严谨一点的说法是指 enable live reload
 - live reload: 动态加载
 - hot reloading: 热加载

个人理解“热更新”是指利用codePush等方案实现APP内页面更新的方案(无需更新APP即可更新页面的一种技术方案)

app开启热更新

2020 年 12 月 15 日 09:51
回复
👍 👍 马上改正
2020 年 12 月 15 日 10:58
回复
没有更多了
ReactNative | 项目复盘,涉及环境、RN版本升级、安全等方案