Hybrid App消息桥接(Android)
以前写过一篇 Hybrid App体验(Android),但是未动手验证Native和Web之间的双向消息通信。今天起床之后,想在Web中启动Android相机与振动通知玩玩,于是开始实现双向的通信,技术特点还是JSBridge。
比较坑的是Android权限管理,几个Android版本还不一样。新版本要调用相机拍照与保存,需要动态申请权限,还需要提供Provider才能给Intent传递文件路径,同时,想在getExternalFilesDir()之外创建文件困难重重。最后,为了避免消耗时间太大,只能放弃保存功能。也许在当前Activity中获取到相机数据之后,自己保存到JPG文件还更加方便。
代码仓库,见 android-webview-1
1. Web侧消息消费订阅与发送
我们主要实现addNativeMessageHandler
、removeNativeMessageHandler
、sendMessageToNative
这3个API函数。
再看看nativeMessageHandler
函数,我们约定msgId === ''
时为全局中间件注册的消息ID。其中,使用责任链模式 next()
调用多个Handler,且可终止。
// Handle message for Android Native
import Util from './util';
export interface MessageHandlerNext {
(): void
}
export interface MessageHandler {
(msgId: string, payload: any, next?: MessageHandlerNext)
}
interface MessageHandlersMapType {
[msgId: string]: MessageHandler[]
}
const messageHandlersMap : MessageHandlersMapType = {
};
function addNativeMessageHandler(msgId: string, handler: MessageHandler): void {
if (!Util.isFunction(handler)) {
throw new Error('Message handler must be a function');
}
const handlers = messageHandlersMap[msgId];
if (handlers) {
const index = handlers.indexOf(handler);
if (index === -1) {
handlers.push(handler);
}
return;
}
messageHandlersMap[msgId] = [handler];
}
function removeNativeMessageHandler(msgId: string, handler: MessageHandler): void {
const handlers = messageHandlersMap[msgId];
if (handlers) {
const index = handlers.indexOf(handler);
if (index !== -1) {
handlers.splice(index, 1);
}
}
}
function nativeMessageHandler(msgId: string, payload: string | null | undefined): void {
const handlers = [...(messageHandlersMap[''] || []), ...(messageHandlersMap[msgId] || [])];
if (handlers.length === 0) {
return;
}
let payloadObj = payload;
if (payload && typeof payload === 'string') {
try {
payloadObj = JSON.parse(payload);
} catch (e) {
console.error(`[ERROR]: Native Message "${msgId}", invalid JSON payload `, e);
}
}
let i = 0;
const next = () => {
if (i >= handlers.length) {
return;
}
const handler = handlers[i];
i += 1;
const argLen = handler.length;
if (argLen < 3) {
handler(msgId, payloadObj);
next();
} else {
handler(msgId, payloadObj, next);
}
};
next();
}
function logMessageToNative(msgId: string, payload: string): void {
console.info(`[INFO] Message "${msgId}" to native, payload: ${payload}`);
}
const msgLogger = {
sendMessage: logMessageToNative,
};
function sendMessageToNative(msgId: string, payload?: string): void {
const injectedNative = (window as any).injectedNative || msgLogger;
injectedNative.sendMessage(msgId, payload || '');
}
export {
addNativeMessageHandler,
removeNativeMessageHandler,
nativeMessageHandler as default,
sendMessageToNative,
};
最后,把nativeMessageHandler
添加到window
变量中。
Object.assign(window, {
nativeMessageHandler,
});
1.1. Web侧消息应用举例
触发Android Toast:
function toastShow() {
sendMessageToNative('toast_show', 'Text from home page');
}
消费Android定时器消息:
let nativeTick = 0;
function globalTickHandler(msgId: string, payload: any, next?: MessageHandlerNext) {
nativeTick += 1;
next();
}
addNativeMessageHandler('native_tick', globalTickHandler);
function AppExample(): JSX.Element {
const [count, setCount] = useState(nativeTick);
const [once] = useState(0);
useEffect(() => {
const tickHandler = (msgId: string, payload) => {
setCount(nativeTick);
};
addNativeMessageHandler('native_tick', tickHandler);
return () => removeNativeMessageHandler('native_tick', tickHandler);
}, [once]);
return (
<p>
Native tick:
{' '}
{count}
</p>
);
}
2. Android侧消息消费与发送
Android侧的消息消费就简单实现,没有像Web侧那样考虑可扩展性。先看看class JavaScriptInjectedNative
,我们添加一个msgHandler接口来处理消息:
public class JavaScriptInjectedNative {
private JsBridgeMsgHandler msgHandler;
public void SetJsBridgeMsgHandler(JsBridgeMsgHandler handler) {
msgHandler= handler;
}
@JavascriptInterface
public String sendMessage(String msgId, String payload) {
if (msgHandler != null) {
return msgHandler.onJsBridgeMsg(msgId, payload);
}
return "Native processed: " + msgId + (payload != null ? ", " + payload : "");
}
}
MainActivity中就实现该接口:
public class MainActivity extends AppCompatActivity implements JsBridgeMsgHandler {
private void initWebView() {
...
...
JavaScriptInjectedNative injectedNative = new JavaScriptInjectedNative();
injectedNative.SetJsBridgeMsgHandler(this);
webView.addJavascriptInterface(injectedNative, "injectedNative");
}
@Override
public String onJsBridgeMsg(String msgId, String payload) {
if (msgId == null) {
return "Empty msg id";
}
switch (msgId) {
case "toast_show":
toastShow(payload);
break;
case "camera_open":
cameraOpen();
break;
case "vibrator_notify":
vibratorNotify();
break;
case "event_round":
toastShow("Event round 1");
sendMessageToWeb("event_round_back", "");
break;
default:
break;
}
return "Message done by Main.";
}
private void sendMessageToWeb(String msgId, String payload) {
String jsContent = "if (this.nativeMessageHandler) { this.nativeMessageHandler("
+ Util.toJsString(msgId) + "," + Util.toJsString(payload) + ")}";
Log.i("MsgToWeb", "javascript:" + jsContent);
webView.post(new Runnable() {
@Override
public void run() {
if (Build.VERSION.SDK_INT >= 19 /*Might need 21*/) {
webView.evaluateJavascript("javascript:" + jsContent, null);
}else {
webView.loadUrl("javascript:" + jsContent);
}
}
});
}
}
sendMessageToWeb()
函数中,对高版本优先使用webView.evaluateJavascript(),避免页面刷新。因WebView执行JavaScript时只能在WebView的UI线程中执行,所以,使用webView.post()
发送只可执行对象。
2.1. Android侧消息应用举例
使用振动通知:
public class MainActivity extends AppCompatActivity implements JsBridgeMsgHandler {
private void vibratorNotify() {
String[] permissions = new String[]{
Manifest.permission.VIBRATE,
};
requestPermissions(permissions);
Vibrator vibrator = (Vibrator) getSystemService(Service.VIBRATOR_SERVICE);
if (!vibrator.hasVibrator()) {
toastShow("No vibrator");
return;
}
vibrator.cancel();
vibrator.vibrate(new long[]{100, 200, 100, 200}, -1);
}
}
定时发送Tick消息:
public class MainActivity extends AppCompatActivity implements JsBridgeMsgHandler {
private Timer tickTimer;
private void startTick() {
tickTimer = new Timer();
TimerTask task = new TimerTask() {
@Override
public void run() {
sendMessageToWeb("native_tick", "");
}
};
tickTimer.schedule(task, 1000, 1000);
}
private void closeTick() {
tickTimer.cancel();
}
}
3. 注意事项
因我们使用android_asset本地静态资源代替HTTP资源,需要注意以下事项:
- 把Web侧构建的dist目录复制到
app/src/main/assets
目录中。 - React Router中需要使用
HashRouter
。 - Public URL需要改成相当index.html的路径,如"./",见 web/script/build.js。
- 使用本地静态资源服务之后,HTTP请求不能跨域访问后端,这时需要考虑使用JsBridge技术让Android侧处理HTTP请求。
使用android_asset技术优化资源加载,会有新的问题待解决,:)
开始浏览页面:
webView.loadUrl("file:///android_asset/index.html");