鉴于 H5 丰富的表现力,产品决定将项目中的某个详情页改为 H5 展示,评论、点赞仍由原生实现,于是便需要原生与 H5 交互。之前对于这块少有涉及,恰巧合作的前端同事对于这块也不太熟悉,所以耗时良久。现在功能做得差不多了,稍微记录一下。
原生
先看下 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
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
63class VygWebView(context: Context, attr: AttributeSet) : BaseWebView(context, attr) {
private var callback: JSCallback? = null
private var isDestroyed: Boolean = false
init {
addJavascriptInterface(VygJavaScriptInterface(), "voyagerApp")
setWebContentsDebuggingEnabled(true)
settings.allowFileAccess = true
settings.allowFileAccessFromFileURLs = true
settings.allowUniversalAccessFromFileURLs = true
}
/**
* 注册js回调监听器
*/
fun registerJSCallback(callback: JSCallback) {
this.callback = callback
}
/**
* 将消息发送给 WebView 来处理
*/
fun sendMessageToWeb(message: String) {
if (isDestroyed) {
return
}
if (Config.isDebug()) {
LogUtils.e("MessageParser", "sendMessageToWeb:$message")
}
val function = "javascript:window.receiveMessage($message)"
if (MainThreadUtils.isMainThread()) {
loadUrl(function)
} else {
MainThreadUtils.post(Runnable {
if (isDestroyed) {
return@Runnable
}
loadUrl(function)
})
}
}
override fun destroy() {
super.destroy()
isDestroyed = true
callback = null
}
inner class VygJavaScriptInterface {
fun sendMessage(message: String) {
if (isDestroyed) {
return
}
callback?.onReceiveWebMessage(message)
}
}
interface JSCallback {
fun onReceiveWebMessage(message: String)
}
}
所有的交互全部通过 Json 字符串来进行,双端协定对 Json 的解析规则。本地通过添加 VygJavaScriptInterface 实现 sendMessage 方法,来接受 H5 传递过来的消息,通过 sendMessageToWeb 向 H5 发送消息。
定义消息体如下: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
28class MessageEntity : Serializable {
/**
* 消息id
*/
var id: String? = null
/**
* 消息类型
*/
var type: String? = null
/**
* 版本号
*/
var version: Int? = null
/**
* 消息内容
*/
var content: Any? = null
/**
* 额外透传信息
*/
var extraData: String? = null
fun toJSONString(): String {
return JSON.toJSONString(this)
}
}
下面来看消息解析: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
105
106
107
108
109
110
111
112
113
114
115
116abstract class MessageParser(private val activity: VygBaseActivity, private val webView: VygWebView,
private val listener: MessageParser.MessageHandedListener?) : VygWebView.JSCallback {
private val TAG = "MessageParser"
override fun onReceiveWebMessage(message: String) {
if (StringUtils.isEmpty(message)) {
return
}
if (Config.isDebug()) {
LogUtils.e(TAG, message)
}
parse(message)
}
private fun parse(message: String) {
Config.execute {
try {
doParse(message)
} catch (t: Throwable) {
LogUtils.e(TAG, t.localizedMessage)
}
}
}
/**
* 这个json可能很大,放到异步去处理
*/
private fun doParse(message: String) {
val obj = JSON.parseObject(message)
val type = obj.getString("type")
if (StringUtils.isEmpty(type)) {
return
}
val version = try {
obj.getInteger("version")
} catch (t: Throwable) {
1
}
val e = getEvents(type)
if (e == null) {
MainThreadUtils.post(Runnable {
if (activity.hasDestroyed()) {
return@Runnable
}
if (version != null && version > NATIVE_WEB_VERSION) {
// 提示更新
notifyUpdateApp()
} else {
// 什么也不做
}
})
return
}
val entity = MessageEntity()
entity.type = type
try {
entity.id = obj.getString("id")
} catch (t: Throwable) {
}
entity.version = version
try {
entity.extraData = obj.getString("extraData")
} catch (t: Throwable) {
}
var content: String? = null
try {
content = obj.getString("content")
} catch (t: Throwable) {
}
if (StringUtils.isEmpty(content) || e.clazz == null) {
entity.content = null
} else {
// Content 为字符串类型直接赋值
if (String::class.java.isAssignableFrom(e.clazz)) {
entity.content = content
} else {
try {
entity.content = JSON.parseObject(content, e.clazz)
} catch (t: Throwable) {
LogUtils.e(TAG, t.localizedMessage)
}
}
}
if (activity.hasDestroyed()) {
return
}
MainThreadUtils.post(Runnable {
if (activity.hasDestroyed()) {
return@Runnable
}
MessageHandler(e, entity, activity, webView).handle()
listener?.onMessageHanded(entity)
})
}
protected abstract fun getEvents(type: String): Events?
/**
* 提示更新app
*/
private fun notifyUpdateApp() {
}
interface MessageHandedListener {
fun onMessageHanded(message: MessageEntity)
}
}
getEvents 为根据消息 type 来取得具体的消息体 Content 类型及处理消息的类:1
2
3
4
5
6
7
8
9
10
11
12
13public final class Events {
public final String eventName;
public final Class<?> clazz;
public final Class<? extends MessageHandler.HandleCallback> callback;
public Events(String name, Class<?> clazz,
Class<? extends MessageHandler.HandleCallback> callback) {
this.eventName = name;
this.clazz = clazz;
this.callback = callback;
}
}
看到消息处理类:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class MessageHandler(private val events: Events, private val entity: MessageEntity, private val activity: VygBaseActivity, private val webView: VygWebView) {
private val TAG = "MessageHandler"
fun handle() {
try {
events.callback?.newInstance()?.handleMessage(entity, entity.content, activity, webView)
} catch (t: Throwable) {
LogUtils.e(TAG, t.localizedMessage)
}
}
interface HandleCallback {
fun handleMessage(entity: MessageEntity, obj: Any?, activity: VygBaseActivity, webView: VygWebView)
}
}
举例一个具体的消息处理类: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
29class LoadImageHandler : MessageHandler.HandleCallback {
private val TAG = "LoadImageHandler"
override fun handleMessage(entity: MessageEntity, obj: Any?, activity: VygBaseActivity, webView: VygWebView) {
if (obj != null && obj is LoadImageEntity) {
if (UriUtils.isNetUrl(obj.url)) {
AsImage.downloadFile(obj.url).loadListener(object : ImageLoadListener<File> {
override fun onLoadingStarted(imageUri: String?, view: View?) {
LogUtils.e(TAG, "onLoadingStarted:$imageUri")
}
override fun onLoadingFailed(imageUri: String?, view: View?, t: Throwable?): Boolean {
LogUtils.e(TAG, "onLoadingFailed:$imageUri")
return true
}
override fun onLoadingComplete(imageUri: String?, view: View?, loaded: File?): Boolean {
loaded?.let {
obj.uri = "file://" + it.absolutePath
webView.sendMessageToWeb(entity.toJSONString())
}
return true
}
}).download()
}
}
}
}
根据修改页面的业务,定义所有的 Event: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
30object ArticleEvents {
private val events = HashMap<String, Events>()
operator fun get(eventName: String): Events? {
return events[eventName]
}
init {
events[HTML_FINISH_LOAD] = Events(HTML_FINISH_LOAD, null, HtmlFinishLoadHandler::class.java)
events[CONTENT_FINISH_LOAD] = Events(CONTENT_FINISH_LOAD, null, ContentFinishLoadHandler::class.java)
events[PROTOCOL] = Events(PROTOCOL, String::class.java, ProtocolHandler::class.java)
events[PLAY_VIDEO] = Events(PLAY_VIDEO, String::class.java, PlayVideoHandler::class.java)
events[IMAGE_BROWSE] = Events(IMAGE_BROWSE, String::class.java, ImageBrowseHandler::class.java)
events[LOAD_IMAGE] = Events(LOAD_IMAGE, LoadImageEntity::class.java, LoadImageHandler::class.java)
}
object EventName {
const val HTML_FINISH_LOAD = "htmlFinishLoad" // H5 加载完
const val LOAD_ARTICLE_CONTENT = "articleContent" // 发送 Content 给 H5 填充
const val CONTENT_FINISH_LOAD = "contentFinishLoad" // Content 加载完
const val UPDATE_COMMENT_COUNT = "updateCommentCount" // 更新回复数
const val PROTOCOL = "ProtocolPageJump" // 协议
const val PLAY_VIDEO = "playVideo" // 播放视频
const val IMAGE_BROWSE = "imgBrowse" // 大图浏览
const val LOAD_IMAGE = "loadImg" // 加载图片
}
}
消息解析这一套流程便完成了,根据 type 找到对应的消息处理类,然后传入 Content 进行处理。
H5
H5 除了界面渲染之外,还需要根据约定实现相关的方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17getNativeData: function() {
var me = this;
window.receiveMessage = function(data) {
var type = data.type;
var content = data.content;
if (type == 'articleContent') {
me.handleArticleData(content);
} else if (type == 'loadImg') {
me.handleImgData(content);
} else if (type == 'updateCommentCount') {
me.handleReplyCount(content);
}else if(type == 'changeOffset'){
me.handleUploadImg(content)
}
}
}
然后调用原生方法时,也是组装好 Message,然后调用 sendMessage 方法即可:1
2
3
4
5
6
7
8
9// 个人中心
$('[data-type="user"]').click(function() {
var uid = $(this).attr('data-uid');
var wxToken = $(this).attr('data-wxToken');
var link = `https://voyager.nav.cn/user/homePage?id=${uid}&wxToken=${wxToken}`;
var params = JSON.stringify({ id: 1, type: 'ProtocolPageJump', content: link });
voyagerApp.sendMessage(params);
})
至此两端的交互体系大体完成。
小坑
最好使用本地 html 文件来完成交互,可以比较方便的加载本地图片,前缀加上”file://“,同时 WebView 需要设置(具体效用尚未研究):
1
2
3settings.allowFileAccess = true
settings.allowFileAccessFromFileURLs = true
settings.allowUniversalAccessFromFileURLs = trueH5 调用本地方法需要 voyager.sendMessage,iOS 则是 voyager_sendMessage。