react native React Native 是一个使用React和应用平台的原生功能来构建 Android 和 iOS 应用的开源框架。通过 React Native,您可以使用 JavaScript 来访问移动平台的 API,以及使用 React 组件来描述 UI 的外观和行为:一系列可重用、可嵌套的代码。你可以在下一节了解更多关于 React 的信息。但首先,让我们介绍一下组件在 React Native 中是如何工作的。
react-native-cli 方便在命令行执行一些命令
npm install -g react-native-cli
react-native run-ios
初始化项目
react-native init project-name
npx
1. npx react-native@latest init AwesomeProject 2. npx react-native@X.XX .X init AwesomeProject --version X.XX .X
cd AwesomeProject
1 2 3 4 5 yarn ios yarn react-native run-ios
Andriod studio 启动 andriod
Xcode 启动 ios
调试 APP Command + D
打开调试模式,如果无效,也可以通过 Device + Shake
弹出,需要开启的功能如下:
remote js debugge
enable hot reloading
也可以安装 react native debugger.app
来调试
处理 ios 和 android 兼容性 特定平台后缀 方式一,创建指定平台的 入口文件
1 2 3 index.ios .js index.android .js
某个组件
1 2 BigButton .ios .js BigButton .android .js
去掉平台后缀直接引用,React Native 会根据运行平台的不同自动引入正确对应的组件。
1 import BigButton from './BigButton' ;
适用于细粒度控制兼容性 Platform
.OS
在 iOS 上会返回ios,而在 Android 设备或模拟器上则会返回android。
1 2 3 import { Platform } from 'react-native' ;Platform .OS === 'ios' ? ...
还有个实用的方法是 Platform.select()
,它可以以 Platform.OS 为 key,从传入的对象中返回对应平台的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import {Platform , StyleSheet } from 'react-native' ;const styles = StyleSheet .create ({ container : { flex : 1 , ...Platform .select ({ ios : { backgroundColor : 'red' , }, android : { backgroundColor : 'blue' , }, }), }, });
这一方法可以接受任何合法类型的参数,因此你也可以直接用它针对不同平台返回不同的组件,像下面这样:
1 2 3 4 5 6 7 const Component = Platform .select ({ ios : () => require ('ComponentIOS' ), android : () => require ('ComponentAndroid' ), })();<Component /> ;
WebView
https://github.com/UnPourTous/react-native-0.51.0/blob/master/Libraries/Components/WebView/WebView.ios.js#L559
现在 Android App大多嵌入了 Android Webview 组件进行 Hybrid 开发,它具备开发周期短、灵活性好的优点,但是缺点也很明显,加载速度慢 & 消耗流量。引起缺点的主要原因如下:
js解析效率,以及手机硬件设备的性能
页面资源的下载(图片、js文件、css文件)
安装 react-native-webview npm install --save react-native-webview
https://github.com/react-native-webview/react-native-webview/blob/master/docs/Getting-Started.md
Hybrid技术原理 Hybrid App的本质,其实是在原生的 App 中,使用 WebView 作为容器直接承载 Web页面。因此,最核心的点就是 Native端 与 H5端 之间的双向通讯层,其实这里也可以理解为我们需要一套跨语言通讯方案,来完成 Native(Java/Objective-c/…) 与 JavaScript 的通讯。这个方案就是我们所说的 JSBridge(JS桥接)
,而实现的关键,便是作为容器的 WebView,一切的原理都是基于 WebView 的机制。
优势
一般来说, App 一个功能的上线需要经过漫长流程,版本的发布存在铺量的问题;而 WebView 加载远端页面的方式,远端页面一经发布,立即全量。所以,页面需要频繁更新时可以考虑 WebView 实现。
H5 页面是远端资源,能有效减少 App 安装包的大小。
一次开发,多处运行。新开发的 H5 页面可以在 RN App WebView、微信/QQ的内置浏览器、微信小程序 WebView 等 WebView 组件上运行。页面在 iOS/Android 上都能获得不错表现。
原生组件 在 Android 开发中是使用 Kotlin 或 Java 来编写视图;
在 iOS 开发中是使用 Swift 或 Objective-C 来编写视图。
在 React Native 中,则使用 React 组件通过 JavaScript 来调用这些视图。在运行时,React Native 为这些组件创建相应的 Android 和 iOS 视图。
由于 React Native 组件就是对原生视图的封装
,因此使用 React Native 编写的应用外观、感觉和性能与其他任何原生应用一样。我们将这些平台支持的组件称为原生组件。
核心组件 React Native 具有许多核心组件,从表单控件到活动指示器,应有尽有。
长列表 React Native 提供了几个适用于展示长列表数据的组件,一般而言我们会选用FlatList或是SectionList。
FlatList更适于长列表数据,且元素个数可以增删。和ScrollView不同的是,FlatList并不立即渲染所有元素,而是优先渲染屏幕上可见的元素。
FlatList组件必须的两个属性是data和renderItem。data是列表的数据源,而renderItem则从数据源中逐个解析数据,然后返回一个设定好格式的组件来渲染。
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 import React from 'react' ;import { FlatList , StyleSheet , Text , View } from 'react-native' ;const styles = StyleSheet .create ({ container : { flex : 1 , paddingTop : 22 }, item : { padding : 10 , fontSize : 18 , height : 44 , }, });const FlatListBasics = ( ) => { return ( <View style ={styles.container} > <FlatList data ={[ {key: 'Devin '}, {key: 'Dan '}, {key: 'Dominic '}, {key: 'Jackson '}, {key: 'James '}, {key: 'Joel '}, {key: 'John '}, {key: 'Jillian '}, {key: 'Jimmy '}, {key: 'Julie '}, ]} renderItem ={({item}) => <Text style ={styles.item} > {item.key}</Text > } /> </View > ); }
awesome-react-native https://github.com/jondot/awesome-react-native
使用 Expo 创建项目
Expo官网: https://docs.expo.dev/tutorial/create-your-first-app/
1 2 3 4 5 npx create-expo-app StickerSmash && cd StickerSmash yarn install npx expo install react-dom react-native-web @expo/webpack-config
启动项目 1 2 3 4 5 6 "scripts" : { "start" : "expo start" , "android" : "expo start --android" , "ios" : "expo start --ios" , "web" : "expo start --web" } ,
手机安装 Expo Go,实时预览效果 启动项目后,控制台会有个二维码,手机使用相机扫码即可打开 Expo Go
App,我们修改的代码,也会实时在手机端更新。
Text numberOfLines
表示文本需要显示几行,超出的用 省略号表示
onLongPress
长按触发事件
onPress
点击触发事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import { StatusBar } from 'expo-status-bar' ;import { StyleSheet , Text , View } from 'react-native' ;export default function App ( ) { return ( <View style ={styles.container} > <Text numberOfLines ={1} onLongPress ={() => alert(1)}>First AppFirst AppFirst AppFirst AppFirst AppFirst AppFirst AppFirst AppFirst AppFirst AppFirst App</Text > <StatusBar style ="auto" /> </View > ); }const styles = StyleSheet .create ({ container : { flex : 1 , backgroundColor : '#fff' , alignItems : 'center' , justifyContent : 'center' , }, });
如果要渲染的是一组需要分组的数据,也许还带有分组标签的,那么SectionList将是个不错的选择
StatusBar
https://docs.expo.dev/versions/latest/sdk/status-bar/
可以控制 StatusBar 显示隐藏、动画之类的
ActivityIndicator(loading效果) <ActivityIndicator color={'red'} />
Image
Image API
使用图片的三种方式
安装
npx expo install expo-image
contentFit
替代了原来的 resizeMode
contentFit
确定图像应如何调整大小以适合其容器。这个属性告诉图像以多种方式填充容器,例如“保持纵横比”或“拉伸并尽可能地占用空间”。它反映了 CSS 对象匹配属性。
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 import { Image } from 'expo-image' ;import { StyleSheet , View } from 'react-native' ;const blurhash = '|rF?hV%2WCj[ayj[a|j[az_NaeWBj@ayfRayfQfQM{M|azj[azf6fQfQfQIpWXofj[ayj[j[fQayWCoeoeaya}j[ayfQa{oLj?j[WVj[ayayj[fQoff7azayj[ayj[j[ayofayayayj[fQj[ayayj[ayfjj[j[ayjuayj[' ;export default function App ( ) { return ( <View style ={styles.container} > <Image style ={styles.image} source ="https://picsum.photos/seed/696/3000/2000" placeholder ={blurhash} contentFit ="cover" transition ={1000} /> </View > ); }const styles = StyleSheet .create ({ container : { flex : 1 , backgroundColor : '#fff' , alignItems : 'center' , justifyContent : 'center' , }, image : { flex : 1 , width : '100%' , backgroundColor : '#0553' , }, });
支持图片加载过程中的几种方法
onLoad
onLoadStart
onLoadEnd
onError
ImageBackground 背景图的使用
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 { View , Text , StyleSheet , ImageBackground } from 'react-native' import { Image } from 'expo-image' const ImageDemo = ( ) => { return ( <View style ={styles.container} > <ImageBackground style ={styles.image} source ={require( '.. /.. /assets /chrome.jpg ')} > <Text > Hello ImageBackground</Text > </ImageBackground > </View > ) }const styles = StyleSheet .create ({ container : { flex : 1 , backgroundColor : '#fff' , }, image : { flex : 1 , width : '100%' , backgroundColor : '#0553' , }, })export default ImageDemo
https://www.reactnative.cn/docs/scrollview
一个封装了平台的 ScrollView(滚动视图)的组件,同时还集成了触摸锁定的“响应者”系统。
记住 ScrollView 必须有一个确定的高度才能正常工作,因为它实际上所做的就是将一系列不确定高度的子组件装进一个确定高度的容器(通过滚动操作)。
一般来说我们会给 ScrollView 设置flex: 1以使其自动填充父容器的空余空间,但前提条件是所有的父容器本身也设置了 flex 或者指定了高度,否则就会导致无法正常滚动,你可以使用元素查看器来查找具体哪一层高度不正确。
ScrollView 会简单粗暴地把所有子元素一次性全部渲染出来。其原理浅显易懂,使用上自然也最简单。然而这样简单的渲染逻辑自然带来了性能上的不足。想象一下你有一个特别长的列表需要显示,可能有好几屏的高度。创建和渲染那些屏幕以外的 JS 组件和原生视图,显然对于渲染性能和内存占用都是一种极大的拖累和浪费。
这就是为什么我们还有专门的FlatList组件。FlatList会惰性渲染子元素,只在它们将要出现在屏幕中时开始渲染。这种惰性渲染逻辑要复杂很多,因而 API 在使用上也更为繁琐。除非你要渲染的数据特别少,否则你都应该尽量使用FlatList,哪怕它们用起来更麻烦。
此外FlatList还可以方便地渲染行间分隔线,支持多列布局,无限滚动加载等等。
这个属性控制在滚动过程中,scroll 事件被调用的频率(单位是 ms,事件触发的间隔时间)。更小的间隔时间能够更及时的跟踪滚动位置,不过可能会带来性能问题,因为更多的信息会通过 bridge 传递。由于 JS 事件循环需要和屏幕刷新率同步,因此设置为 1-16 之间的数值不太可能有实质区别(一般屏幕刷新率为 60 帧,即每帧间隔不低于 16 ms)。默认值为 0,意味着每次视图被滚动,scroll 事件都会被调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <ScrollView style={styles.container } horizontal={false } onScroll={() => { console .log ('ScrollView scroll ...' ) }} scrollEventThrottle={1000 } > { new Array (10 ).fill ('' ).map ((item, idx ) => { return ( <View style ={styles.box} > <Text style ={styles.text} > {idx} </Text > </View > ) }) } </ScrollView >
解决键盘遮挡问题 关键组件
: KeyboardAvoidingView
本组件用于解决一个常见的尴尬问题:手机上弹出的键盘常常会挡住当前的视图。本组件可以自动根据键盘的高度,调整自身的 height 或底部的 padding,以避免被遮挡。
关键属性
: behavior
,可选值为 'height', 'position', 'padding'
关键代码
1 2 3 4 5 6 import { KeyboardAvoidingView , Platform } from 'react-native' <KeyboardAvoidingView style={{ flex : 1 }} behavior={Platform .OS == "ios" ? "padding" : "height" } />
整体代码如下
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 React , { useState } from 'react' import { Text , StyleSheet , View , TextInput , Switch , Button , KeyboardAvoidingView , Platform } from 'react-native' import { Image } from 'expo-image' const index = ( ) => { const [isEnabled, setIsEnabled] = useState (false ); const toggleSwitch = ( ) => setIsEnabled (previousState => !previousState); return ( <KeyboardAvoidingView style ={styles.container} behavior ={Platform.OS == "ios" ? "padding " : "height "} > <Image style ={{ width: 200 , height: 200 , alignSelf: 'center '}} source ={require( '.. /.. /assets /avatar.jpg ')} /> <TextInput style ={styles.inputStyle} placeholder ='请输入账号' /> <TextInput style ={styles.inputStyle} placeholder ='请输入密码' /> <Switch title ='记住密码' trackColor ={{ false: "#767577 ", true: "#81b0ff " }} // 卡槽颜色 thumbColor ={isEnabled ? "#f5dd4b " : "#f4f3f4 "} // 开关上圆形按钮的背景颜色 onValueChange ={toggleSwitch} value ={isEnabled} /> <Button title ='登录' color ="#f194ff" > </Button > </KeyboardAvoidingView > ) }const styles = StyleSheet .create ({ container : { flex : 1 , backgroundColor : '#ccc' , alignItems : 'center' , justifyContent : 'center' }, inputStyle : { height : 40 , width : '100%' , backgroundColor : '#fff' , marginTop : 12 } })export default index
效果如下
webview
安装 npx expo install react-native-webview
使用 注意,包裹 WebView
组件的容器,必须使用 flex: 1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import { StyleSheet , SafeAreaView } from 'react-native' ;import { WebView } from 'react-native-webview' export default function App ( ) { return ( <SafeAreaView style ={styles.container} > <WebView source ={{ uri: 'https: //m.baidu.com ' }} /> </SafeAreaView > ); }const styles = StyleSheet .create ({ container : { backgroundColor : 'pink' , flex : 1 } });
显示 HTML 注意:设置 html 源需要原始 WhiteList 属性设置为[’*’]。
1 2 3 4 <WebView source ={{ originWhitelist ={[ '*']} html: '<h1 > <center > Hello webview</center > </h1 > ' }} />
加载本地 HTML 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import React , {Component } from 'react' ;import {View , Text , Alert , TextInput , Button } from 'react-native' ;import { WebView } from 'react-native-webview' ;const localHtmlFile = require ('../assets/test.html' );export default class LocalPageLoad extends Component <Props , State > { render ( ) { return ( <View style ={{ width: '100 %', height: '100 %' }}> <WebView source ={localHtmlFile}/ > </View > ); } }
获取 webview 实例 class 组件
1 2 3 4 5 6 7 8 9 10 11 class MyWeb extends Component { webview = null ; render ( ) { return ( <WebView ref ={(ref) => (this.webview = ref)} /> ); } }
函数组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import {createRef} from 'react' import { WebView } from 'react-native-webview' const WebViewDemo = ( ) => { const webviewRef = createRef () return ( <WebView ref ={webviewRef} /> ) }export default WebViewDemo
useRef
1 2 3 4 5 6 7 8 9 const webViewRef = useRef (null );if (webViewRef.current ) { webViewRef.current .goBack (); } <WebView ref={webViewRef} />
接入 H5 调试工具 vConsole
1 <script src ="https://cdn.bootcss.com/vConsole/3.3.0/vconsole.min.js" > </script >
1 2 3 <script> const vConsole = new VConsole () </script>
控制导航状态的更改 有时候你想拦截一个用户在你的 webview 中点击一个链接,然后做一些不同于在 webview 中导航的事情。我们可以使用 onNavigationStateChange
方法监听。
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 const WebViewDemo = ( ) => { const webviewRef = createRef () const onNavigationStateChange = newNavState => { const { url } = newNavState if (url.includes ('.pdf' )) { this .webview .stopLoading (); Alert .alert ('打开 PDF modal' ) } Alert .alert ('执行其他逻辑!' ) } return ( <WebView ref ={webviewRef} source ={{ uri: 'https: //reactnative.dev /' }} onNavigationStateChange ={onNavigationStateChange} /> ) }
JS(HTML)与本机之间的通信 我们需要发送消息到网页浏览加载的网页,也可以接收来自这些网页的消息。
为此,React NativeWebView 公开了三个不同的选项
React Native -> Web: The injectedJavaScript prop
React Native -> Web: The injectJavaScript method
Web -> React Native: The postMessage method and onMessage prop
The injectedJavaScript prop injectedJavaScript
字符串是一个脚本,首次加载网页后立即执行脚本的内容。它只运行一次,即使页面被重新加载或导航离开。
下面的代码中,当 webview 加载了 html 页面,首先会将 body
设置为 粉色
,同时在 window
对象上挂在一个方法 sayHello
。
html
中,我们可以定义一个按钮,点击按钮后,可以调用 window.sayHello
。
注意 injectedJavaScript
字符串脚本最后,有一个 true
,这是必须的,否则有时候你会无声地失败
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const injectedJavaScript = ` document.body.style.backgroundColor = 'pink'; window.sayHello = function() { alert('我是 RN 通过 injectedJavaScript 注入的方法') }; true; ` return ( <WebView source ={localH5} injectedJavaScript ={injectedJavaScript} /> )const button = document .querySelector ('button' ) button.addEventListener ('click' , () => { if (typeof window .sayHello === 'function' ) { window .sayHello () } })
injectedJavaScriptBeforeContentLoaded prop 这是在网页首次加载之前运行的脚本。即使页面重新加载或导航离开,它也只运行一次。
如果您想在执行 Web 代码之前将任何内容注入到窗口、本地存储或文档中,这非常有用。
1 2 3 4 5 6 7 8 9 10 11 12 const runFirst = ` window.isNativeApp = true; true; // note: 这是必须的,否则有时候你会无声地失败 ` ;<WebView source ={{ uri: 'xxx ', }} injectedJavaScriptBeforeContentLoaded ={runFirst} />
Warning
: On Android, this may work, but it is not 100% reliable (see #1609 and #1099).
The injectJavaScript method 虽然方便,但是前面提到的 injectedJavaScript
prop的缺点是它只运行一次
。这就是为什么还在 webview 参考文献中公开了一个名为 injectJavaScript
的方法(注意这个名字稍有不同!
).
如下例子,当首次加载 localH5.html
时,背景色是 粉色
,三秒过后,我们通过 webview本身提供的 injectJavaScript 方法,将 html 的背景色改为了 橙色
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 const WebViewDemo = ( ) => { const webviewRef = createRef () const injectedJavaScript = ` document.body.style.backgroundColor = 'pink'; window.sayHello = function() { alert('我是 RN 通过 injectedJavaScript 注入的方法') }; true ` setTimeout (() => { webviewRef.current .injectJavaScript (` document.body.style.backgroundColor = 'orange'; ` ) }, 3000 ) return ( <WebView ref ={webviewRef} source ={localH5} injectedJavaScript ={injectedJavaScript} /> ) }
在 iOS 上,injectJavaScript 调用 WebView 的 evaluateJS
在 Android 上,injectJavaScript 调用 Android WebView 的 evaluateJavascriptWithFallback 方法
window.ReactNativeWebView.postMessage web page 可以给 React Native code
发送消息啦!
你必须设置 webview onMessage
属性,否则window.reactativewebview.postmessage
方法将不会被注入到网页中。
window.ReactNativeWebView.postMessage only accepts one argument which must be a string.
rn
1 2 3 4 5 6 <WebView source={localH5} onMessage={event => { console .log ('🔥' , event.nativeEvent .data ) }} />
localH5.html
1 2 3 4 5 const button = document .querySelector ('button' ) button.addEventListener ('click' , () => { window .ReactNativeWebView .postMessage ('你好 ReactNativeWebview' ) })
处理 H5 的消息类型 WebView onMessage
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 onMessage = (event ) => { const data = JSON .parse (event.nativeEvent .data ) const { type } = data switch (type) { case 'route' : break case 'emitCallback' : const routeParams = this .props ?.navigation ?.state ?.params routeParams[callbackName] && routeParams[callbackName](callbackResult) break case 'tel' : break case 'captureCheck' : break case 'msg' : break case 'upload' : this .beforeUpload (data) break case 'eventEmit' : key && RCTDeviceEventEmitter .emit (key, params) break case 'console' : const [ tag, ...restVal ] = val if (typeof tag === 'string' && tag.startsWith ('console.' )) { const type = tag.replace ('console.' , '' ) console [type].call ?.(this , ...restVal) } else console .log (...val) break } }
自定义 headers, sessions, and cookies 自定义 headers
1 2 3 4 5 6 7 8 <WebView source ={{ uri: 'http: //example.com ', headers: { 'my-custom-header-key ': 'my-custom-header-value ', }, }} />
这将在第一次加载时设置标头,但不会在后续页面导航中设置标头
。
为了解决这个问题,我们可以跟踪当前的 URL,拦截新的页面加载,然后自己导航到它们(这项技术的原创者是来自 Big Binary 的 Chirag Shah )
解决方法
WebView 提供了onLoadStart
属性 ,它接受 WebView 开始加载时调用的函数
我们可以使用此 prop 来了解何时单击链接,然后使用新的 url 重新渲染 WebView 组件。重新渲染 WebView 组件将加载该页面,就好像它是第一页一样,然后将传递请求标头。
我们知道,在 React 中,当组件的任何状态发生变化时,组件都会重新渲染自身。这里唯一改变的是 url,所以让我们将 url 移动到一个状态并将其初始化为欢迎页面,该页面是应用程序的根目录。然后使用onLoadStart属性将 url 状态更改为单击的 url。
webview 的 prop onShouldStartLoadWithRequest
,允许自定义处理任何 Web 视图请求的函数。从函数返回 true 以继续加载请求,并返回 false 以停止加载。
webview 代码
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 const CustomHeaderWebView = (props ) => { const { uri, onLoadStart, ...restProps } = props; const [currentURI, setURI] = useState (props.source .uri ); const newSource = { ...props.source , uri : currentURI }; return ( <WebView {...restProps } source ={newSource} onShouldStartLoadWithRequest ={(request) => { // If we're loading the current URI, allow it to load if (request.url === currentURI) return true; // We're loading a new URL -- change state first setURI(request.url); return false; }} /> ); };<CustomHeaderWebView source ={{ uri: 'http: //example.com ', headers: { 'my-custom-header-key ': 'my-custom-header-value ', }, }} /> ;
server 代码
,服务端通过 request.headers["my-custom-header-key"]
获取请求头。
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 var http = require ("http" );var port = 9000 ;function logRequest (request ) { console .log ("Processing request for: " , request.url ); console .log ("Custom Header: " , request.headers ["my-custom-header-key" ]); console .log ("Request Processed\n" ); } http .createServer (function (request, response ) { response.writeHead (200 , { "Content-Type" : "text/html" }); switch (request.url ) { case "/" : response.write ( "<html><body>Welcome<a href='/bye'>Bye</a></body></html>" ); logRequest (request); break ; case "/bye" : response.write ("<html><body>Bye<a href='/'>Welcome</a></body></html>" ); logRequest (request); break ; default : break ; } response.end (); }) .listen (port);
renderError 渲染错误的 WebView 页面 renderError
函数返回一个视图以显示是否存在错误。
注意
: 渲染错误的容器 view
,样式需要设置 height: '100%'
,而不是 flex: 1
,否则 错误页面显示不全。
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 <WebView originWhitelist={[ '*' ]} source={{ uri : 'http://127.0.0.1:1500/components/webview/h5.html' }} renderError={() => { return ( <TouchableOpacity style ={styles.errorPage} > <Text style ={styles.errorContent} > Error Page</Text > </TouchableOpacity > ) }} />const styles = StyleSheet .create ({ errorPage : { height : '100%' , backgroundColor : '#333' , alignItems : 'center' , justifyContent : 'center' }, errorContent : { textAlign : 'center' , fontSize : 24 , fontWeight : 'bold' , color : '#fff' , } })
当出现错误到了错误页面之后,我们可以点击错误页面,刷新 webview
1 2 3 4 5 6 7 8 9 <TouchableOpacity style={styles.errorPage } onPress={() => { webviewRef.current ?.reload () }} > <Text style ={styles.errorContent} > Error Page</Text > </TouchableOpacity >
我们来给错误页面,加个背景图,并且给背景图设置透明度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 renderError={() => { return ( <ImageBackground source ={require( '.. /.. /assets /avatar.jpg ')} style ={styles.errorPage} contentFit ='fill' imageStyle ={{ opacity: 0.5 , // 背景图片设置透明度 }} > <TouchableOpacity onPress ={() => { webviewRef.current?.reload() // 或者返回 props.navigation.goBack() }} > <Text style ={styles.errorContent} > Error Page...</Text > </TouchableOpacity > </ImageBackground > ) }}
效果如下
Expo Router
https://docs.expo.dev/routing/introduction/
React Navigation 版本 > 4.x
建立两个组件 Home 和 HomeDetails
1 2 3 4 5 6 7 8 import { createAppContainer } from 'react-navigation' import { createStackNavigator } from 'react-navigation-stack' const AppNavigator = createStackNavigator ({ Home : HomeScreen , HomeDetails : HomeDetailsScreen })export default createAppContainer (AppNavigator )
跳转路由,可以通过组件 props 上的 this.props.navigation.navigate
方法
1 2 3 4 5 6 <Button onPress={() => { this .props .navigation .navigate ('HomeDetails' ) }} title='Home Details' />
完整示例如下:
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 import React , { Component } from 'react' import { Text , StyleSheet , View , Button } from 'react-native' import { createAppContainer } from 'react-navigation' import { createStackNavigator } from 'react-navigation-stack' class HomeDetailsScreen extends React.Component { render ( ) { return ( <View style ={{ flex: 1 , alignItems: 'center ', justifyContent: 'center ' }}> <Text > Details Screen</Text > </View > ); } }class HomeScreen extends Component { render ( ) { return ( <View > <Text > HomeScreen </Text > <Button onPress ={() => { this.props.navigation.navigate('HomeDetails') }} title='Home Details' /> </View > ) } }const AppNavigator = createStackNavigator ({ Home : HomeScreen , HomeDetails : HomeDetailsScreen })const styles = StyleSheet .create ({})export default createAppContainer (AppNavigator )
回退
1 2 3 4 5 6 7 8 9 10 11 12 <Button onPress={() => { this .props .navigation .goBack () }} title='Go Back' /><Button onPress ={() => { this.props.navigation.navigate('Home') }} title='Go Home' />
路由参数 navigate
and push
accept an optional second argument
to let you pass parameters to the route you are navigating to. For example: this.props.navigation.navigate(‘RouteName’, {paramName: ‘value’}).
1 2 3 4 5 6 7 8 9 <Button onPress={() => { this .props .navigation .navigate ('HomeDetails' , { itemId : 86 , otherParam : 'anything you want here' , }) }} title='Home Details' />
目标页面,获取路由参数
1 2 3 4 5 6 const { navigation } = this .props ; navigation.getParam ('otherParam' , 'default value' ) this .props .navigation .state .params
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class HomeScreen extends React.Component { static navigationOptions = { title : 'Home' , }; }class DetailsScreen extends React.Component { static navigationOptions = { title : 'Details' , }; }
navigationOptions
也可以配置成函数,函数的参数是 navigation
,等同于组件内的 this.props.navigation
1 2 3 4 5 static navigationOptions = ({ navigation } ) => { return { title : `首页详情 itemId:${navigation.getParam('itemId' , '0' )} ` , } }
我们还可以动态的改变路由参数,路由参数变化,会引起组件的重新渲染。
1 2 3 4 5 6 7 8 <Button onPress={() => { this .props .navigation .setParams ({ itemId : 99 }) }} title='更新Params' />
1 2 3 4 5 6 7 8 9 10 11 12 13 static navigationOptions = ({ navigation } ) => { return { title : `首页详情 itemId:${navigation.getParam('itemId' , '0' )} ` , headerStyle : { backgroundColor : 'pink' }, headerTintColor : '#fff' , headerTitleStyle : { fontWeight : 'bold' , fontSize : 20 , }, } }
夸屏幕共享公共的 navigationOptions 我们不希望每个 Header 的样式都不一样,我们可以配置通用的 Header Style
createStackNavigator
第二个参数,可以配置通用的 Header style
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const AppNavigator = createStackNavigator ( { Home : HomeScreen , Details : DetailsScreen , }, { initialRouteName : 'Home' , defaultNavigationOptions : { headerStyle : { backgroundColor : '#f4511e' , }, headerTintColor : '#fff' , headerTitleStyle : { fontWeight : 'bold' , }, }, } );
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const HomeTitle = ( ) => { return ( <Image source ={require( '.. /.. /assets /icon.png ')} style ={{ width: 50 , height: 35 }} /> ); }class HomeScreen extends React.Component { static navigationOptions = { headerTitle : () => <HomeTitle /> , }; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 static navigationOptions = { headerTitle : <HomeTitle /> , headerRight : () => ( <Button onPress ={() => alert('This is a button!')} title="Info" color="#000" /> ), headerLeft : () => ( <Button onPress ={() => alert('This is a button!')} title="返回" color="#000" /> ), }
跳转到 H5 页面 我们先定义好 H5 页面(WebView
),如果 H5 页面中,不想要看到 Header
,可以单独给 H5Demo
配置 navigationOptions
,其中 headerShown
就可以控制 Header
的显示隐藏。
想要返回的话,从屏幕左侧向右滑动即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import WebviewDemo from '../webview/webview-demo' const AppNavigator = createStackNavigator ({ Home : HomeScreen , HomeDetails : HomeDetailsScreen , H5Demo : { screen : WebviewDemo , navigationOptions : { headerShown : null , } } }, { initialRouteName : 'Home' , defaultNavigationOptions : { headerStyle : { backgroundColor : '#fff' }, headerTintColor : '#000' , } })
H5如何打开 RN 页面? 其实就是 H5 和 RN 通信的逻辑,只不过是在 RN 侧做了路由的处理
首先,我们可以在 injectedJavaScript
向 Web 端window对象
注入一个全变量 isRN
,在 Web 端通过 window.isRN
判断当前 Web 页面是不是通过 RN
内嵌的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const injectedJavaScript = ` window.isRN = true; document.body.style.backgroundColor = 'pink'; window.sayHello = function() { alert('我是 RN 通过 injectedJavaScript 注入的方法') }; true ` <WebView source={localH5} onMessage={(event ) => { if (event.nativeEvent .data ) { const data = JSON .parse (event.nativeEvent .data ) console .log ('' , data) } }} injectedJavaScript={injectedJavaScript} />
localH5.html
H5这边通过 window.ReactNativeWebView.postMessage
将路由信息发送到 RN
这边,路由信息比如包括 type: route
、path: ComponentName
、以及可选的参数
如下示例,表示 H5 页面跳转到 RN
的 HomeDetails
页面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 goBackBtn.addEventListener ('click' , () => { if (window .isRN ) { const routeInfo = { type : 'route' , path : 'HomeDetails' , params : { itemId : 11 , } } window .ReactNativeWebView .postMessage (JSON .stringify (routeInfo)) } })
RN 这边,通过 WebView
的 onMessage
函数接收消息,在函数内部通过判断当前参数是不是路由信息,如果是路由信息,则通过 props
上的 navigation
去操控路由跳转。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <WebView source={localH5} onMessage={(event ) => { if (event.nativeEvent .data ) { const data = JSON .parse (event.nativeEvent .data ) if (data.type === 'route' ) { props.navigation .navigate (data.path , { itemId : data.query .itemId }) } } }} injectedJavaScript={injectedJavaScript} />
参考资料