关于 React Native 与 WebView 的通信

在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方法中处理消息了(消息必须是字符串)。

同时,你可以在该组件(页面)渲染完毕,WebViewref属性执行之后,在组件(页面)中的任何地方使用this.webview.postMessageWebView中的页面发送消息。

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
        // 得到了数据随你怎么办吧
    }
}

现在问题来了:

  1. 假设有多个get info的请求,有的要获得nikename,有的要获得age怎么区分,或者每个请求如何知道哪个回应是针对哪个请求的。
  2. 我自己也监听了一个叫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)

回到在通信协议中增加idisReply,首先我们需要增加一个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发布的相同
// 这一步同样可以通过代码自动做。

要完成上面接口的改造,我们得做两件事:

  1. 回调处理改成Promise的。
  2. 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吧,谢谢啦^^

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,511评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,495评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,595评论 0 225
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,558评论 0 190
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,715评论 3 270
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,672评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,112评论 2 291
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,837评论 0 181
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,417评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,928评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,316评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,773评论 2 234
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,253评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,827评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,440评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,523评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,583评论 2 249

推荐阅读更多精彩内容