本文用 react native 做的 app 很简单,首页 + 列表页 + 详情页 + 关于页。总的感觉,涉及到原生方面,对于不会 android 和 ios 的 js 菜鸡,比较棘手,要折腾。
关于Headless JS后台任务,可以参考《React Native Android 端Headless JS后台 GPS 持续定位》这篇文章
1. 前置基础
React 基础
React Native 文档
2. 本文主要 package version
这里页面路由应该使用 react-navigation
更方便些,react-router-native6.x 版本感觉对 react native 支持不是很好
对于 UI 库,没找到合适的,找了一个老掉牙的ant-design-react-native
package
version
react
18.2.0
react-native
0.71.2
react-router-native
^6.8.0
react-native-webview
^11.26.1
@ant-design/react-native
^5.0.3
3. 关于 React Native 的一些疑问 3.1 React Native 和 Flutter 优缺点对比
来自 chat gpt 的回答,感觉有些落后啊
优点
缺点
React Native
1. 拥有大量第三方库和社区支持; 2. 可以复用 React 的开发经验,语法上非常接近 React; 3. 支持热重载,开发效率高
1. 部署和打包过程较复杂; 2. 动画和性能体验略逊色于原生; 3. 升级过程可能会比较痛苦
Flutter
1. 组件丰富,UI 体验接近原; 2. 性能很高,特别是动画方面; 3. 打包简单,可以编译成原生代码; 4. 升级过程比较顺畅
1. 第三方库和社区还在发展,不如 React Native 成熟 ; 2. 对 Dart 语言的依赖,有一定学习成本; 3. 热重载不能像 React Native 一样方便
3.2 如何看待 react native 到目前为止,还没出 1.0 版本?
同样来自 chat gpt 的回答
React Native 虽然至今还没发布 1.0 版本,但这不意味着它不成熟或不可用。
Facebook 的版本发布策略是非常慢慢的,要达到 1.0 需要非常稳定和完善,这需要长期的迭代和磨合。
React Native 生态庞大,涉及的平台和版本组合非常多,要保证在所有环境下 100% 稳定并不容易。
React Native 的升级过程中,会涉及到原生代码的迁移,这也增加了版本发布的难度。
社区提供的第三方库也需要跟上版本迭代,这需要协调和校准,也是版本发布的阻碍。
3.3 react native 使用的是原生 view 还是 webview?
同样来自 chat gpt 的回答
React Native 使用的是原生 View 而不是 WebView
React Native 的核心思想是使用 JavaScript 来开发移动应用,而不是 Web 应用。它允许你使用 React 来开发原生移动应用,最终编译成真实的原生 UI 元素
具体来说,React Native 将 React 组件映射到原生 View 和 Text 组件上,而不是 WebView。所以当你在 JavaScript 中构建一个 Button 时,在本质上它会变成 Android 的 Button 或 iOS 的 UIButton。
4. 开发环境准备
本文选择了本地搭建环境,没有选择依赖线上的 expo 沙盒环境
本地环境搭建文档 ,对着文档一步步来即可
创建项目
npx react-native init AwesomeProject
使用安卓手机调试
也可以使用模拟器,由于我手机是安卓的,就用了安卓手机。还有就是安卓模拟器上的显示和真机有些差距,问题没及时暴露。
手机打开开发者模式,并打开 usb 调试,数据线连接电脑即可,运行项目的时候,会自动在你的手机上安卓 debug 安装包
5. 开发时遇到的一些问题总结 5.1 安装第三方 ui 库,link 字体时报错
根据 Ant Design Mobile RN of React 文档链接字体图标 ,安装完@ant-design/icons-react-native
库后,需要执行npx react-native link @ant-design/icons-react-native
,然后出现如下报错
解决:
安装 react-native-asset
yarn add react-native -asset --save
根目录 react-native.config.js,assets 添加字体图标文件的路径
module .exports = { assets: ['node_modules/@ant-design/icons-react-native/fonts' ] };
执行 yarn react-native-asset
检查是否链接成功,android\app\src\main\assets 下是否有 fonts 文件夹
关于项目中使用了@dr.pogodin/react-native-static-server
感谢 @小白菜 大佬的提供,react-native-static-server
是一款可以在react native中,本地启动静态服务的库,文档中有提到android/app/build.gradle
配置:
android { sourceSets { main { assets.srcDirs = [ '../../assets' // This array may contain additional asset folders to bundle-in. // Paths in this array are relative to "build.gradle" file, and // should be comma-separated. ] } } // ... Other stuff. }
这样配置过后,会导致@ant-design/icons-react-native
字体无法正确加载。如果app内有其它静态资源,在配置assets.srcDirs的时候需要把 =
换成 +=
,这样就可以加载字体图标了:
android { sourceSets { main {- assets.srcDirs = [ + assets.srcDirs += [ '../../assets' // This array may contain additional asset folders to bundle-in. // Paths in this array are relative to "build.gradle" file, and // should be comma-separated. ] } } // ... Other stuff. }
5.2 如何像 web 项目一样使用 env 环境变量?
如何像 web 项目一样,本地开发使用.env.development
文件,生产打包的时候使用.env.production
文件,.env.local
文件只在本地生效?
安装react-native-config
及配置步骤
yarn add react-native-config
+ include ':react-native-config' + project(':react-native-config').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-config/android')
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 android { // project.ext.defaultEnvFile = "path/to/.env.file" // 写在 apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle" 这行之前+ project.ext.envConfigFiles = [ + debug: ".env.development", + release: ".env.production" + ] defaultConfig { applicationId "com.xxx.xxx" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 2 versionName "0.0.2" // for react-native-config+ resValue "string", "build_config_package", "com.xxx.xxx" } } dependencies { implementation "com.facebook.react:react-native:+" // From node_modules+ implementation project(':react-native-config') } // 最后一行添加+ apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle"
android/app/src/main/java/com/your-app-name/MainApplication.java
+ import com.lugg.RNCConfig.RNCConfigPackage;
package.json 中 添加 scripts 脚本命令
.env.local
加入到.gitignore
文件中忽略,文件中可以放入一些私有敏感变量,不会被提交到仓库里;
本地开发和生产打包时将.env.local
文件里的变量复制到.env.development
或.env.production
文件里,启动项目完成或者打包完后,再将新复制进来的变量删除;
这样就解决了 debug 和 release 变量区分,而本地私有变量也不会直接暴露出去。
NODE_ENV =developmentBASE_URL =https://xxx.dev.api.comMY_VARIABLE_ENV =123 dev
NODE_ENV =productionBASE_URL =https://xxx.prod.api.comMY_VARIABLE_ENV =123 prod
{ "scripts" : { "preandroid" : "node ./scripts/checkEnvAmapKey .env.local && cat .env.local >> .env.development" , "android" : "ENVFILE=.env.development react-native run-android" , "postandroid" : "node ./scripts/revertEnvFile .env.development .env.local" , "prebuild:android" : "node ./scripts/checkEnvAmapKey .env.local && cat .env.local >> .env.production" , "build:android" : "cd android && ENVFILE=.env.production ./gradlew assembleRelease" , "postbuild:android" : "node ./scripts/revertEnvFile .env.production .env.local" , "prebuild:android:aab" : "node ./scripts/checkEnvAmapKey .env.local && cat .env.local >> .env.production" , "build:android:aab" : "cd android && ENVFILE=.env.production ./gradlew bundleRelease" , "postbuild:android:aab" : "node ./scripts/revertEnvFile .env.production .env.local" } }
scripts/checkEnvAmapKey.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 const path = require ("path" );const fs = require ("fs" );const { Buffer } = require ("buffer" );const argv = process.argv .slice (2 );const rootDir = process.cwd ();const fsPromise = fs.promises ;const chalk = require ("chalk" );console .log ("argv:" , argv); const envLocalFilePath = path.resolve (rootDir, `./${argv[0 ]} ` );const envLocalFileTemplate = ` # 高德地图key # android AMP_ANDROID_API_KEY=请输入您的android api key ` ;const handleWrite = async ( ) => { try { const controller = new AbortController (); const { signal } = controller; const data = new Uint8Array (Buffer .from (envLocalFileTemplate)); const promise = await fsPromise.writeFile (envLocalFilePath, data, { signal, encoding : "utf8" , flags : "w" , }); controller.abort (); await promise; } catch (error) { console .log (chalk.red ("写入文件失败" ), error); } finally { process.exit (1 ); } };async function writeEnvAmapKeyFile ( ) { fs.open (envLocalFilePath, "wx" , async (err, fd) => { if (err) { if (err.code === "EEXIST" ) { console .log (`${envLocalFilePath} 文件已存在` ); } process.exit (1 ); return ; } try { await handleWrite (); } finally { fs.close (fd, (error ) => { if (error) { throw error; } }); process.exit (1 ); } }); }async function checkEnvAmapKeyFile ( ) { try { await fsPromise.stat (envLocalFilePath); console .log (`${chalk.green("项目根目录已存在 `.env.local` 文件" )} ;` ); } catch (error) { console .log (` ${chalk.red.bgYellow("项目根目录不存在 `.env.local` 文件,将进行创建" )} ; ${chalk.yellow( "请在生成的`.env.local`文件中填入高德地图API KEY,未申请的请前往高德地图 https://console.amap.com/dev/key/app 中申请创建,申请成功后,请复制key到 `.env.local`文件相应位置,然后重新启动项目" )} ` ); await writeEnvAmapKeyFile (); } }checkEnvAmapKeyFile ();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 const path = require ("path" );const fs = require ("fs" );const { Buffer } = require ("buffer" );const argv = process.argv .slice (2 );const rootDir = process.cwd ();const fsPromise = fs.promises ;const chalk = require ("chalk" );console .log ("argv:" , argv); const envLocalFilePath = path.resolve (rootDir, `./${argv[1 ]} ` );const envFilePath = path.resolve (rootDir, `./${argv[0 ]} ` );const handleWrite = async (filePath, data ) => { try { const controller = new AbortController (); const { signal } = controller; const str = new Uint8Array (Buffer .from (data)); const promise = await fsPromise.writeFile (filePath, str, { signal, encoding : "utf8" , flags : "w" , }); controller.abort (); await promise; } catch (error) { console .log (chalk.red ("写入文件失败" ), error); } finally { process.exit (1 ); } };async function writeEnvAmapKeyFile (filePath, data ) { fs.open (filePath, "wx" , async (err, fd) => { if (err) { if (err.code === "EEXIST" ) { await handleWrite (filePath, data); } process.exit (1 ); return ; } try { await handleWrite (filePath, data); } finally { fs.close (fd, (error ) => { if (error) { throw error; } }); process.exit (1 ); } }); }async function handleReadFile (fileName ) { try { const promise = fsPromise.readFile (fileName, { encoding : "utf8" }); return await promise; } catch (err) { console .error (err); return "" ; } }async function handleRevertEnvFile ( ) { try { await fsPromise.stat (envLocalFilePath); const envAmapFileContent = await handleReadFile (envLocalFilePath); const envFileContent = await handleReadFile (envFilePath); if (envAmapFileContent === "" ) { return ; } const replaceStr = envFileContent.replace (envAmapFileContent, "" ); await writeEnvAmapKeyFile (envFilePath, replaceStr); } catch (error) { console .log (error); console .log ( `${chalk.red.bgYellow( "请检查 `.env.[development|production]` 文件,若有新增加的高德地图 API KEY,请撤回更改,勿提交到远程仓库" )} ` ); } }handleRevertEnvFile ();
关于打 release 生产包时获取不到自定义 env 变量
https://github.com/luggit/react-native-config/issues/640
android/app/proguard-rules.pro 添加下面这行:
-keep class com .mypackage.BuildConfig { *; }
遗留的小问题
本地开发时,执行npm run android
,npm scripts 钩子执行不了postandroid
,导致复制到.env.development
文件里的变量删除不了。npm run build:android
打包的时候不存在这个问题。
5.3 优雅的修改包的版本号
https://github.com/stovmascript/react-native-version
yarn add react-native-version
package.json 中添加 scripts 脚本
{ "name": "AwesomeProject", "version": "0.0.1", "scripts": { "start": "node node_modules/react-native/local-cli/cli.js start",+ "postversion": "react-native-version" } }
每次打包发布前,先执行npm version x.x.x
它会自动把新的版本号更新到 android 和 ios 中,然后再执行打包命令即可
5.4 修改包的名称 修改 android/app/src/main/res/values/strings.xml
<resources > <string name ="app_name" > 修改成你想要的包名</string > </resources >
5.5 打包图片报错 mergeReleaseResources FAILED
import { ImageBackground , useWindowDimensions } from "react-native" ;<ImageBackground source ={require( ".. /.. /assets /img /mybg.png ")} resizeMode ="cover" style ={{ ...styles.backgroundImg , height: useWindowDimensions ().height , }} /> ;
* What went wrong: Execution failed for task ':app:mergeReleaseResources' . > A failure occurred while executing com.android.build.gradle.internal.res.Aapt2CompileRunnable > Android resource compilation failed ERROR:/android/ app/build/g enerated/res/ react/release/ drawable-mdpi/src_assets_img_bg.png: AAPT: error: file failed to compile .
android\app 下的 build.gradle 文件中添加如下代码
android { ...+ // 解决打包png图片报错 + aaptOptions.cruncherEnabled = false + aaptOptions.useNewCruncher = false ... }
5.6 本地开发 http 请求失败 两种方式,推荐第一种
使用 https 请求
修改配置
在 res 下新增加一个 xml 目录,然后创建一个名为 network_security_config.xml 文件,文件内容
<?xml version="1.0" encoding="utf-8"?> <network-security-config > <base-config cleartextTrafficPermitted ="true" /> </network-security-config >
在 android/app/src/main/AndroidManifest.xml 文件中添加:
<application>+ android:networkSecurityConfig="@xml/network_security_config" </application>
5.7 安卓 Text 组件文字显示不全 设置 fontFamily 为 lucida grande,或者为空
fontfamily: "lucida grande" ;
5.8 android 绝对定位时点击事件失效
https://segmentfault.com/q/1010000022868789
解决: 在最外面在加一个 View,固定高度,点击就可以触发
5.9 android 侧滑退出应用问题 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 import React , { useEffect, useCallback, memo, Fragment , useRef } from "react" ;import { Platform , BackHandler , Text , StyleSheet , AppState , ToastAndroid , } from "react-native" ;import { useNavigate, useLocation } from "react-router-native" ;import { Toast } from "@ant-design/react-native" ;const ResetBack = ( ) => { const navigate = useNavigate (); const location = useLocation (); const numRef = useRef (1 ); const _handleAppStateChange = function (nextAppState ) { console .log ("nextAppState==>" , nextAppState); if (nextAppState && nextAppState === "background" && numRef.current > 0 ) { console .log ("numRef.current==>" , numRef.current ); if (Platform .OS === "android" ) { ToastAndroid .showWithGravity ( "已切到后台" , ToastAndroid .SHORT , ToastAndroid .BOTTOM ); } } }; const handleHardwareBackPress = useCallback (() => { if (location.pathname === "/" ) { numRef.current --; if (numRef.current === 0 ) { Toast .info ({ content : <Text style ={styles.txt} > 在滑一次退出</Text > , }); BackHandler .removeEventListener ( "hardwareBackPress" , handleHardwareBackPress ); return true ; } } navigate (-1 ); return true ; }, [navigate, location]); useEffect (() => { const listener = AppState .addEventListener ("change" , _handleAppStateChange); return () => { listener.remove (); }; }, []); useEffect (() => { if (Platform .OS === "android" ) { BackHandler .addEventListener ( "hardwareBackPress" , handleHardwareBackPress ); } return () => { if (Platform .OS === "android" ) { BackHandler .removeEventListener ( "hardwareBackPress" , handleHardwareBackPress ); numRef.current = 1 ; } }; }, [handleHardwareBackPress]); return <Fragment /> ; };const styles = StyleSheet .create ({ txt : { fontFamily : "" , color : "#ffffff" , }, });export default memo (ResetBack );
chatgpt 给我的答案
在 rn 0.71 及更高版本中,可以使用 TouchableWithoutFeedback 组件来捕获双击事件。
TouchableWithoutFeedback 提供了 onPress 和 onLongPress 回调函数以及 delayPressIn 和 delayPressOut 属性,这些属性可以用于检测双击事件
您可以根据需要调整 delayPressIn 和 delayPressOut 属性的值。例如,如果您希望用户必须在 500 毫秒内双击按钮,则可以将 DOUBLE_PRESS_DELAY 常量设置为 500。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import React , { useRef } from "react" ;import { TouchableWithoutFeedback , Text } from "react-native" ;const MyDoubleClickButton = (props ) => { const { onPress, title, doubleClickTime, textStyle } = props; const lastPress = useRef (0 ); const handlePress = ( ) => { const now = new Date ().getTime (); const DOUBLE_PRESS_DELAY = doubleClickTime; if (now - lastPress.current < DOUBLE_PRESS_DELAY ) { onPress (); } lastPress.current = now; }; return ( <TouchableWithoutFeedback onPress ={handlePress} delayPressIn ={doubleClickTime / 2 } delayPressOut ={doubleClickTime / 2 } > <Text style ={textStyle} > {title}</Text > </TouchableWithoutFeedback > ); };export default MyDoubleClickButton ;
5.11 设置剪切板
https://github.com/react-native-clipboard/clipboard
import Clipboard from "@react-native-clipboard/clipboard" ;Clipboard .setString ("hello world" );const fetchCopiedText = async ( ) => { const text = await Clipboard .getString (); setCopiedText (text); };
5.12 关于使用react-native-webview
https://github.com/react-native-webview/react-native-webview/blob/HEAD/docs/Reference.md
安装
yarn add react-native -webview
注意: 一定要在安装完 react-native-webview 之后,然后重新启动,才会生效,不然会报错 (重启后,会重新下载依赖,这个过程,视网络情况而定,可能有点慢)
TypeError: Cannot read property 'isFileUploadSupported' of null, js engine: hermes
网页 js 和原生进行通信
如下示例,给网页插入一段脚本,获取网页上的图片点击事件,并且拿到图片的 src 链接
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import React , { Component } from "react" ;import { View } from "react-native" ;import { WebView } from "react-native-webview" ;const INJECTED_JAVASCRIPT = ` var imgList = document.querySelectorAll("img"); Array.from(imgList).forEach(el => { el.addEventListener("click", function () { window.ReactNativeWebView.postMessage(JSON.stringify({type: 'previewimg', data: this.src})); }); }); // 注意:这行是必须添加的,否则添加失败 true; // note: this is required, or you'll sometimes get silent failures ` ;const MyWebView = ( ) => { return ( <WebView source ={{ uri: `http: //xxx /posts /detail /123 ` }} injectedJavaScript ={INJECTED_JAVASCRIPT} onMessage ={(event) => { alert(event.nativeEvent.data); }} /> ); };
IOS 报错 RNCWebView 未找到 UIManager
报错如下:
Invariant Violation: requireNativeComponent: "RNCWebView" was not found in the UIManager
解决:
然后重启项目即可
WebView 页面不显示
关键原因是,WebView 的父组件没有设置高度,导致 WebView 页面不显示
解决: 给 WebView 父组件设置一个高度即可
5.13 关于使用高德地图
https://github.com/qiuxiang/react-native-amap3d
定位模块 :@react-native-community/geolocation
获取的坐标是 gps 坐标,高德地图使用时要转换, react-native-amap-geolocation
可以直接获取到位置信息,无需再进行转换
安装
yarn add react-native -amap3d
android/app/src/main/AndroidManifest.xml
添加权限
<uses-permission android:name ="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name ="android.permission.ACCESS_COARSE_LOCATION" />
使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 import { AMapSdk , MapView , MapType } from "react-native-amap3d" ;import { Platform , PermissionsAndroid } from "react-native" ;const handleAndroidPermissin = useCallback (async () => { try { const granted = await PermissionsAndroid .requestMultiple ( [ PermissionsAndroid .PERMISSIONS .ACCESS_FINE_LOCATION , PermissionsAndroid .PERMISSIONS .ACCESS_COARSE_LOCATION , ], { title : "位置信息授权" , message : "获取当前位置信息测试" , buttonNeutral : "跳过" , buttonNegative : "取消" , buttonPositive : "同意" , } ); console .log ("granted==>" , granted); if ( granted["android.permission.ACCESS_FINE_LOCATION" ] === PermissionsAndroid .RESULTS .GRANTED ) { console .log ("可以定位了" ); } else if ( granted["android.permission.ACCESS_FINE_LOCATION" ] === PermissionsAndroid .RESULTS .DENIED ) { Toast .fail ({ content : "已拒绝获取位置信息" , }); } else { Toast .fail ({ content : "用户已拒绝,且不愿被再次询问" , }); } } catch (error) { console .warn (error); } }, []);useEffect (() => { AMapSdk .init ( Platform .select ({ android : Config .AMP_ANDROID_API_KEY , }) ); }, []);return <MapView mapType ={MapType.Satellite} /> ;
关于 GPS 坐标转高德坐标
https://www.jianshu.com/p/dd0c017250e4
推荐使用高德坐标转换
高德地图逆地理编码服务
https://lbs.amap.com/api/javascript-api-v2/guide/services/geocoder#t2
AMap .plugin ("AMap.Geocoder" , function ( ) { var geocoder = new AMap .Geocoder ({ city : "010" , }); var lnglat = [111 , 30 ]; geocoder.getAddress (lnglat, function (status, result ) { if (status === "complete" && result.info === "OK" ) { } }); });
安卓从地图页返回上一页 app 闪退
https://github.com/qiuxiang/react-native-amap3d/issues/742
解决 :android/app/src/main/AndroidManifest.xml文件application
添加android:allowNativeHeapPointerTagging="false"
打正式 release 包闪退
https://github.com/qiuxiang/react-native-amap3d/issues/762
解决 :在 android/appproguard-rules.pro
文件添加下面代码,重新打包
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 # 高德地图release包闪退问题 # 3D 地图 V5.0.0之前: -keep class com .amap.api.maps.**{*;} -keep class com .autonavi.amap.mapcore.*{*;} -keep class com .amap.api.trace.**{*;}# 3D 地图 V5.0.0之后: -keep class com .amap.api.maps.**{*;} -keep class com .autonavi.**{*;} -keep class com .amap.api.trace.**{*;}# 定位 -keep class com .amap.api.location.**{*;} -keep class com .amap.api.fence.**{*;} -keep class com .autonavi.aps.amapapi.model.**{*;}# 搜索 -keep class com .amap.api.services.**{*;}# 2D地图 -keep class com .amap.api.maps2d.**{*;} -keep class com .amap.api.mapcore2d.**{*;}# 导航 -keep class com .amap.api.navi.**{*;} -keep class com .autonavi.**{*;}
6. 自定义安卓原生模块供 js 端使用
https://www.reactnative.cn/docs/native-modules-android
基本步骤是:
创建模块:在android/app/src/main/java/com/your-app-name
下新建一个 java 文件,如ToastModule.java
注册模块:在android/app/src/main/java/com/your-app-name
下新建一个 java 文件,如CustomToastPackage.java
在android/app/src/main/java/com/your-app-name/MainApplication.java
中引入自己的包
js 端通过暴露出来的模块名调用原生方法
6.1 调用自定义原生安卓模块-手电筒
自定义封装一个手电筒模块,供 js 端调用,可以打开/关闭手机的手电筒
android/app/src/main/AndroidManifest.xml
加入如下权限
<uses-permission android:name ="android.permission.CAMERA" /> <uses-permission android:name ="android.permission.FLASHLIGHT" /> <uses-feature android:name ="android.hardware.camera" />
android/app/src/main/java/com/your-app-name
下新建 FlashlightManModule.java
文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 package com.your-app-name;import android.hardware.Camera;import android.hardware.camera2.CameraAccessException;import android.hardware.camera2.CameraManager;import android.hardware.camera2.CameraCharacteristics;import android.os.Build;import android.content.pm.PackageManager;import com.facebook.react.bridge.NativeModule;import com.facebook.react.bridge.ReactApplicationContext;import com.facebook.react.bridge.ReactContext;import com.facebook.react.bridge.ReactContextBaseJavaModule;import com.facebook.react.bridge.ReactMethod;import com.facebook.react.bridge.Promise;import java.util.Map;import java.util.HashMap;public class FlashlightManModule extends ReactContextBaseJavaModule { private static ReactApplicationContext reactContext; private Camera camera; private Camera.Parameters mParameters; private CameraManager mCameraManager; private boolean hasClosed = true ; private static final int FLASH_LIGHT_ON = 1 ; private static final int FLASH_LIGHT_OFF = 0 ; private static final String E_LAYOUT_ERROR = "E_LAYOUT_ERROR" ; private static final String HAS_FLASH = "HAS_FLASH" ; private static final String NOT_HAS_FLASH = "NOT_HAS_FLASH" ; public FlashlightManModule (ReactApplicationContext context) { super (context); reactContext = context; } @Override public String getName () { return "FlashlightManager" ; } @ReactMethod public void isSuportFlashlight (Promise promise) { try { PackageManager packageManager = reactContext.getPackageManager(); boolean hasFlash = packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH); String hasFlashStr = hasFlash ? HAS_FLASH : NOT_HAS_FLASH; if (!hasFlash) { promise.reject(E_LAYOUT_ERROR, hasFlashStr); return ; } CameraManager mCameraManager = (CameraManager) reactContext.getSystemService(ReactContext.CAMERA_SERVICE); String[] cameraIds = mCameraManager.getCameraIdList(); String cameraId = cameraIds[0 ]; boolean r = mCameraManager.getCameraCharacteristics(cameraId).get(CameraCharacteristics.FLASH_INFO_AVAILABLE); promise.resolve(r); } catch (CameraAccessException e) { e.printStackTrace(); promise.reject(E_LAYOUT_ERROR, e); } } @ReactMethod public void toggleLight (int lightType, Promise promise) { try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { CameraManager mCameraManager = (CameraManager) reactContext.getSystemService(ReactContext.CAMERA_SERVICE); String[] cameraIds = mCameraManager.getCameraIdList(); String cameraId = cameraIds[0 ]; hasClosed = lightType == FLASH_LIGHT_ON; mCameraManager.setTorchMode(cameraId, lightType == FLASH_LIGHT_ON); } else { if (lightType == FLASH_LIGHT_ON) { camera = Camera.open(); mParameters = camera.getParameters(); mParameters.setFlashMode(mParameters.FLASH_MODE_TORCH); camera.setParameters(mParameters); hasClosed = false ; } else { mParameters.setFlashMode(mParameters.FLASH_MODE_OFF); camera.setParameters(mParameters); camera.release(); hasClosed = true ; } } promise.resolve(lightType); } catch (CameraAccessException e) { promise.reject(E_LAYOUT_ERROR, e); } } }
android/app/src/main/java/com/your-app-name
下新建 FlashlightManModulePackage.java
文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package com.your-app-name;import com.facebook.react.ReactPackage;import com.facebook.react.bridge.NativeModule;import com.facebook.react.bridge.ReactApplicationContext;import com.facebook.react.uimanager.ViewManager;import java.util.ArrayList;import java.util.Collections;import java.util.List;public class FlashlightManModulePackage implements ReactPackage { @Override public List<ViewManager> createViewManagers (ReactApplicationContext reactContext) { return Collections.emptyList(); } @Override public List<NativeModule> createNativeModules (ReactApplicationContext reactContext) { List<NativeModule> modules = new ArrayList <>(); modules.add(new FlashlightManModule (reactContext)); return modules; } }
android/app/src/main/java/com/your-app-name/MainApplication.java
...import com.your-app-name.FlashlightManModulePackage; ...protected List<ReactPackage> getPackages () { @SuppressWarnings("UnnecessaryLocalVariable") List<ReactPackage> packages = new PackageList (this ).getPackages(); packages.add(new FlashlightManModulePackage ()); return packages; }
js 端调用上面写的原生模块方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 import {NativeModules , PermissionsAndroid } from 'react-native' ;let FlashlightManager = NativeModules .FlashlightManager ;const lightTypeRef = useRef (false );const handleAndroidPermissin = async ( ) => { try { const granted = await PermissionsAndroid .request ( PermissionsAndroid .PERMISSIONS .CAMERA , { title : '闪光灯授权' , message : '获取闪光灯权限' , buttonNeutral : '跳过' , buttonNegative : '取消' , buttonPositive : '同意' , }, ); console .log ('授权结果granted==>' , granted); if (granted === PermissionsAndroid .RESULTS .GRANTED ) { console .log ('可以打开闪光灯了' ); return Promise .resolve (); } else if (granted === PermissionsAndroid .RESULTS .DENIED ) { Toast .fail ({ content : '已拒绝打开闪光灯' , }); return Promise .reject (); } else { Toast .fail ({ content : '用户已拒绝,且不愿被再次询问' , }); return Promise .reject (); } } catch (error) { console .error (error); } };const toggleLight = async ( ) => { try { await handleAndroidPermissin (); const = await FlashlightManager .isSuportFlashlight (); console .log ('当前手机是否支持闪光灯:' , suportRes); console .log ('准备打开/关闭手电筒:' , lightTypeRef.current ? 0 : 1 ); const res = await FlashlightManager .toggleLight ( lightTypeRef.current ? 0 : 1 , ); lightTypeRef.current = res; console .log ('手电筒打开/关闭结果:' , res); } catch (error) { console .error ('打开/关闭手电筒异常' , error); } };
6.2 调用自定义原生安卓模块-Notification 通知
点击某按钮(线上应该是收到服务端消息),手机上出现 app 的通知(自定义通知标题和通知内容,并且携带路径参数),用户点击通知打开 app,接收路径参数,跳转指定页面
android/app/src/main/java/com/your-app-name
新建 MyNotificationModule.java
文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 package com.your-app-name;import android.os.Build;import android.app.Notification;import android.app.NotificationChannel;import android.app.NotificationManager;import android.content.Intent;import android.app.PendingIntent;import com.facebook.react.bridge.NativeModule;import com.facebook.react.bridge.ReactApplicationContext;import com.facebook.react.bridge.ReactContext;import com.facebook.react.bridge.ReactContextBaseJavaModule;import com.facebook.react.bridge.ReactMethod;public class MyNotificationModule extends ReactContextBaseJavaModule { private static ReactApplicationContext reactContext; private static final String CHANNEL_ID = "notification_channel" ; private static final String CHANNEL_NAME = "notification_channel_name" ; private Class getActivityClass (String activityName) { try { if (activityName.equals("PageToJumpTo" )) { return Class.forName("com.your-app-name.MainActivity" ); } } catch (ClassNotFoundException e) { e.printStackTrace(); } return null ; } public MyNotificationModule (ReactApplicationContext context) { super (context); reactContext = context; } @Override public String getName () { return "MyNotificationManager" ; } @ReactMethod public void show (int contextId, String title, String content, String activityName, String routePath) { NotificationManager mNotificationManager = (NotificationManager) reactContext.getSystemService(ReactContext.NOTIFICATION_SERVICE); Notification notification = null ; Intent intent = new Intent (reactContext, getActivityClass(activityName)); intent.putExtra("pageToJumpKey" , routePath); PendingIntent pendingIntent; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { pendingIntent = PendingIntent.getActivity(reactContext, contextId, intent, PendingIntent.FLAG_IMMUTABLE); } else { pendingIntent = PendingIntent.getActivity(reactContext, contextId, intent, PendingIntent.FLAG_UPDATE_CURRENT); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationChannel channel = new NotificationChannel (CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH); mNotificationManager.createNotificationChannel(channel); notification = new Notification .Builder(reactContext, CHANNEL_ID) .setContentTitle(title) .setContentText(content) .setSmallIcon(R.mipmap.ic_launcher) .setAutoCancel(true ) .setContentIntent(pendingIntent) .build(); } else { notification = new Notification .Builder(reactContext) .setContentTitle(title) .setContentText(content) .setSmallIcon(R.mipmap.ic_launcher) .setAutoCancel(true ) .setContentIntent(pendingIntent) .build(); } mNotificationManager.notify(100 , notification); } }
android/app/src/main/java/com/your-app-name
新建 MyNotificationModulePackage.java
文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package com.your-app-name;import com.facebook.react.ReactPackage;import com.facebook.react.bridge.NativeModule;import com.facebook.react.bridge.ReactApplicationContext;import com.facebook.react.uimanager.ViewManager;import java.util.ArrayList;import java.util.Collections;import java.util.List;public class MyNotificationModulePackage implements ReactPackage { @Override public List<ViewManager> createViewManagers (ReactApplicationContext reactContext) { return Collections.emptyList(); } @Override public List<NativeModule> createNativeModules (ReactApplicationContext reactContext) { List<NativeModule> modules = new ArrayList <>(); modules.add(new MyNotificationModule (reactContext)); return modules; } }
android/app/src/main/java/com/your-app-name/MainApplication.java
文件中引入自己的模块
+ import com.your-app-name.MyNotificationModulePackage; public class MainApplication extends Application implements ReactApplication { ... @Override protected List<ReactPackage> getPackages() { @SuppressWarnings("UnnecessaryLocalVariable") List<ReactPackage> packages = new PackageList(this).getPackages();+ packages.add(new MyNotificationModulePackage());// <-- 添加这一行,类名替换成你的Package类的名字 name. return packages; } ... }
android/app/src/main/java/com/your-app-name/MainActivity.java
添加代码监听 onNewIntent 事件,获取 MyNotificationModulePackage
文件intent.putExtra
的参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 + import android.content.Intent; + import com.facebook.react.bridge.WritableMap; + import com.facebook.react.bridge.Arguments; + import com.facebook.react.modules.core.DeviceEventManagerModule; + import com.facebook.react.bridge.ReactApplicationContext; + import com.facebook.react.ReactInstanceManager;public class MainActivity extends ReactActivity { ... + @Override + public void onNewIntent (Intent intent) { + + super .onNewIntent(intent); + String data = intent.getStringExtra("pageToJumpKey" ); + if (data != null ) { + WritableMap params = Arguments.createMap(); + params.putString("pageToJumpKey" , data); + ReactInstanceManager mReactInstanceManager = getReactNativeHost().getReactInstanceManager(); + ReactApplicationContext context= (ReactApplicationContext) mReactInstanceManager.getCurrentReactContext(); + context + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("pageToJumpKey" , params); + } + } }
js 端调用 MyNotificationModule 模块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import { NativeModules } from "react-native" ;let MyNotificationManager = NativeModules .MyNotificationManager ;const handleNotification = async ( ) => { try { await handleNotificationPermission (); const ctxId = Math .ceil (Math .random () * 10000000 ); MyNotificationManager .show ( ctxId, "文章更新啦!" , "查看新文章" , "PageToJumpTo" , "/list" ); } catch (error) { console .log (error); } };
js 端监听点击通知传递进来的参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import React , { memo, useEffect, Fragment } from "react" ;import { NativeEventEmitter , NativeModules } from "react-native" ;import { useNavigate } from "react-router-native" ;const PageJumpTo = ( ) => { const navigate = useNavigate (); useEffect (() => { const eventEmitter = new NativeEventEmitter ( NativeModules .MyNotificationModule ); const eventListener = eventEmitter.addListener ("pageToJumpKey" , (data ) => { console .log ("pageToJumpKey==>" , data); navigate (data.pageToJumpKey ); }); return () => { eventListener && eventListener.remove (); }; }, [navigate]); return <Fragment /> ; };export default memo (PageJumpTo );
7. 调试
Chrome 浏览器 的 DevTools 来调试 Hermes 上的 JS
文档
react-devtools 调试
yarn global add react-devtools
如果用的是本地手机,还需要执行
adb reverse tcp:8097 tcp:8097
8. 打包发布 跟着文档 来即可
9. 总结 练习时长 2 月半,远远不够,app 很粗糙,距离上架还有一段距离,况且代码里现在只兼顾到了 android,忽略了 ios。