在React Native 0.37版本中,合并入了
react-native-webview-bridge作者的某個PR,從此React Native中自帶的WebView擁有了和Web通信的功能。此版本之前的版本也可以用react-native-webview-bridge
或者其他WebView Bridge的方案進行通信。
本文結構從講官方支持的方法開始,到定義消息結構來擴展功能,到如何透明化通信,優化通信接口設計,到更通用的實現方案。
很多手機應用的開發場景中,我們都會涉及到React Native與WebView通信。以前Hybrid App中Native做的工作,到RN中依然存在。
0x0 React Native WebView 的通信功能
首先介紹一下React Native WebView自帶的通信功能
0x01 React Native 側
在0.37的WebView中新增了:
- 一個屬性
onMessage
- 一個方法
postMessage
在某個組件(頁面)中初始化(渲染)一個WebView
import * as React from 'react'
import { WebView } from 'react-native'
class Example extends React.Component {
webview: WebView
handleMessage = (evt: any) => {
const message = evt.nativeEvent.data
}
render() {
return (
<WebView
ref={w => this.webview = w}
onMessage={this.handleMessage}
/>
)
}
}
這樣你就可以在該組件(頁面)的handleMessage
方法中處理消息了(消息必須是字符串)。
同時,你可以在該組件(頁面)渲染完畢,WebView
的ref
屬性執行之后,在組件(頁面)中的任何地方使用this.webview.postMessage
向WebView
中的頁面發送消息。
const message: string = 'hello web!'
this.webview.postMessage(message)
其中postMessage
接受一個字符串參數。
0x02 Web 側
不知道為啥官方文檔沒寫這個,只能去看示例文件
并且在網頁中擴展了:
- 一個
message
事件 - 一個
postMessage
方法
監聽從React Native發過來的消息:
window.document.addEventListener('message', function (e) {
const message = e.data
})
給React Native發消息:
window.postMessage('hi! RN')
0x1 定義傳輸協議,擴展message
的的功能
當然,大多數情況下,需求不可能只是給網頁發一段字符串這么簡單。很多時候,一側向另一側發送消息,是為了得到另一側的某些數據,或者觸發另一側的某些動作。這時候,我們就需要定義通信協議來處理復雜的請求。
可以借鑒Hybrid
方案中常用的command + paylaod
的方式,這樣定義:
const message = {
command: 'example', // string
payload: { // any
arg1: 1,
arg2: '',
}
}
舉個例子,假設在web側,有一個可以獲取信息的方法:
// web side
const me = {
name: 'Qi', age: 26,
nickname: 'pinqy',
}
// 提供我的信息
function info(property: string) {
return me[property] || 'I don\'t know'
}
Native也想要獲取這個信息,可以發一個消息:
const data = {
command: 'get info', // 表明意圖
payload: { // 表明內容
property: 'nickname'
}
}
this.webview.postMessage(JSON.stringify(data))
Web這邊監聽從Native發過來的消息并且做相應的處理:
window.document.addEventListener('message', function (e) {
const message = JSON.parse(e.data)
if (message.command === 'get info') {
const property = info(message.payload.property)
// 向Native發消息通知結果
}
})
處理完得到結果應該給native發送消息告知結果了
const data = {
command: 'get info',
payload: {
[message.payload.property]: property
}
}
window.postMessage(JSON.stringify(data))
然后Native要接受這個結果:
handleMessage = (evt: any) => {
const message = JSON.parse(evt.nativeEvent.data)
if (message.command === 'get info') {
const nickname = message.payload.nickname
// 得到了數據隨你怎么辦吧
}
}
現在問題來了:
- 假設有多個
get info
的請求,有的要獲得nikename
,有的要獲得age
怎么區分,或者每個請求如何知道哪個回應是針對哪個請求的。 - 我自己也監聽了一個叫
get info
的命令,我咋知道發過來的是回復給我的還是請求我的。
第一個問題,為了區分不同的請求,我們可以在payload中新增一個id
字段,用全局唯一的ID(可用自增函數實現)來區分每個消息,返回結果的時候也將這個id
帶上,就知道了。
第二個問題,為了區分是不是response
,我們可以再加一個isReply
字段,請求的時候為false
,回復結果的時候置為true
。
下面講。
0x2 讓通信透明,我只想知道怎么發,怎么收,怎么響應就行了
上面這些真的是麻煩到不行了,使用的時候
id
什么的,我都不想關心
記得使用ajax
或者fetch
發送一個請求么?
$.post('URL', {}, function (res) {})
fetch('URL').then(res => res.json()).then()
我也想發送信息,傳入回調函數處理結果就好。
還有一個問題是監聽,就像express
一樣,我能直接監聽一個具體的命令然后給出處理結果就好了。
基于以上愿望,我們可以這樣設計接口:
send(command: string, payload: any): Promise<any>
listen(command: string, handler: Function)
回到在通信協議中增加id
和isReply
,首先我們需要增加一個map
來記錄我們每次發出的請求的響應函數,等待收到回復的信息之后,執行。
const transactions = {}
function addTransaction(command, fn) {
const id = getUID()
transactions[`${command}(${id})`] = fn
return id
}
function executeTransaction(command, id, data) {
if (transactions[`${command}(${id})`]) {
transactions[`${command}(${id})`](data)
delete transactions[`${command}(${id})`]
}
}
接著就可以定義send
接口函數了:
function send(command, data, callback) {
const payload = {
command, data,
isReply: false,
id: addTransaction(callback),
}
// 發送message,Web或者Native均可
}
接下來就是listen
接口了。這個接口的作用實際記錄下收到某種命令請求的時候的執行函數,因此我們也需要新建一個map
來存下所有命令對應的處理函數:
const callbacks = {}
function listen(command, fn) {
callbacks[command] = fn
}
// 這里默認每個命令只有一個處理方法,如果需要多個可用也可用其他方案實現。
此外,我們還得實現一個listener
來監聽所有來自另一邊的消息:
// Native Side
<WebView onMessage={e => listener(e.nativeEvent.data)} />
// Web Side
window.document
.addEventListener('message', (e) => listener(e.data))
listener
也是環境無關的處理方法:
function listener(message) {
const payload = JSON.parse(message)
const { command, id, isReply, data } = payload
if (isReply) {
// 如果是自己請求的回復,則調取之前存下的回調函數
executeTransaction(command, id, data)
} else {
// 如果是請求,則得到結果后回復
if (callbacks[command]) {
callbacks[command](data, reply(command, id))
}
}
}
function reply(command, id) {
return function (data) {
const payload = {
command, id, data,
isReply: true
}
// 發送message,環境無關
}
}
到這里,一個簡單的通信細節透明的React Native和Web通信接口的實現就完成了。
0x3 進一步改進接口,讓RN獲取Web中函數就像直接調用函數那么簡單
我理想中的獲得Web上信息的方式應該是這樣的。
以上面的例子來說的話,希望能直接在native
側調取web
側的函數info
// native side
const nickname = await info('nickname')
當然,給Native暴露哪些函數應該是web設置好的
// web side
function info(property: string) {
return me[property] || 'I don\'t know'
}
define('info', info) // 把info方法暴露給native
// native side
const info = bind('info') // bind的名字和web發布的相同
// 這一步同樣可以通過代碼自動做。
要完成上面接口的改造,我們得做兩件事:
- 回調處理改成Promise的。
- 將
data
變成參數的Array。
實現define
function define(name, fn) {
listen(name, (data) => fn(...data))
}
實現bind
function bind(name) {
return (...args) => {
return new Promise(resolve => {
send(name, args, resolve)
})
}
}
是不是超簡單。
0x4 不止適配自帶的WebView,任何WebView Bridge都可以
這部分主要是關于如何將通用功能抽取成通用工具讓其可以適用于各種場景的。
進一步分析整個通信過程的實現來看,其實關于通信細節這一塊,都是通用的,不通用的是發送方法和監聽方法,因此,我們完全可以把這兩塊單獨抽出來,作為生成一個通信器的參數:
function createMessager(sender): messager
例如,在RN原生支持的messge接口里:
// react native side
messager = createMessager(this.webview.postMessage)
// web side
messager = createMessager(window.postMessage)
然后在react-native-webview-bridge#v2
中就可以:
// react native side
messager = createMessager(this.webviewBridge.send)
// web side
messager = createMessager(window.WebViewBridge.send)
監聽函數也同樣。
具體可以看這里:react-native-webview-invoke/factory
0x5 核心思想上面都講過了,具體實現可以看源碼
里面也有demo。當然,我已經抽取成庫了,可以直接用在你的項目里。
react-native-webview-invoke - 中文文檔
最后,如果這篇文章有幫到你,麻煩不吝給個star吧,謝謝啦^^