混合开发,原生+html5

时间:2022-11-22 15:33:47

混合开发也是一种很好的开发方式,比如这不京东618又来了,做做活动页,活动过后就换下,不用经过应用商店,方便快捷简洁。

这里记录下工作中用到的混合开发的场景:

一个专门的应用,叫订单中心,除了处理本应用的订单外,还承接三方应用,对订单数据详情进行展示,并能从中调用设备打印功能

对于数据的展示,这没什么好说的了,该页面是一个WebView就可以了

对于从html中调用设备的原生函数,就需要注意native与h5的交互了。

先上承载页面,一个activity

package com.yunnex.eshop.hybrid;

import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.webkit.WebSettings;
import android.webkit.WebView;

import com.yunnex.eshop.R;
import com.yunnex.eshop.hybrid.jsbridge.RainbowBridge;
import com.yunnex.eshop.hybrid.jsbridge.core.JsBridgeWebChromeClient;

public class OrderDetailH5Activity extends AppCompatActivity
{

   private WebView mWebView;

   @Override
   protected void onCreate(Bundle savedInstanceState)
   {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_order_detail_h5);
      mWebView = (WebView) findViewById(R.id.webview);
      WebSettings settings = mWebView.getSettings();
      settings.setJavaScriptEnabled(true);

      RainbowBridge.getInstance()
            .clazz(JsInvokeJavaScope.class)
            .inject();
      mWebView.setWebChromeClient(new JsBridgeWebChromeClient());

      //url can be from any other app or web site
      //in a word,this is a h5 ui
      Intent intent = getIntent();
      String uri = intent.getStringExtra("uri");
//    uri="file:///android_asset/test.html";
//    uri="http://m.zb25.com.cn/shebei/printInfo";
      uri="http://192.168.9.151:8080/Hybrid/functions.html";

      mWebView.loadUrl(uri);
   }
}

上面用到了RainBridge这个交互框架,关于该框架的对于交互的原理的讲解,使用,请参考

http://zhengxiaoyong.me/2016/04/20/Native%E4%B8%8EH5%E4%BA%A4%E4%BA%92%E7%9A%84%E9%82%A3%E4%BA%9B%E4%BA%8B/

交互的协议,可简单概括为:

 jsbridge://class:port/method?params  //(schema://host:port/path?params)
 
 jsBridge为固定,port自动生成不用管
 class由设备端提供,也为固定值
 因此h5调用native时只需关注method(方法名)与params(params是一串json字符串)
 举个例子,如果h5触发设备的登录逻辑
         <button
                onclick="RainbowBridge.callMethod('JsInvokeJavaScope','login',{'allowEscape':'0','userName':'yunnex','uri':'yunnex://native/login','requestCode':'8'},function(msg){alert(JSON.stringify(msg))});">
            登录
        </button>
 RainbowBridge为设备端提供的RainbowBridge.js文件,callMethod为对外的一级入口,固定值
 第一个参数 JsInvokeJavaScope为设备端业务处理类,固定值
 第四个参数为处理的回调
 因此h5调用设备只需关注第二个参数,该参数即为业务方法名称login(由设备端提供),和第三个参数(业务方法参数,组装为json串,{'allowEscape':'0','userName':'yunnex','uri':'yunnex://native/login','requestCode':'8'})

业务方法参数遵循的规则如下:
参数params保留字(uri,allowEscape,requestCode)
uri:页面需跳转时指定的值
本应用内跳转allowEscape可忽略,其他应用跳转allowEscape需要指定为1
requestCode,请求码,如需要,指定
非保留字参数,前端与设备端商量一致即可(如userName)

 设备端处理后若有返回值,格式为
 var resultData = {
    status: {
        code: 0,//0成功,1失败
        msg: '请求超时'//失败时候的提示,成功可为空
    },
    data: {}//数据,无数据可以为空
};
返回值会由回调函数function(msg){...}处理

uri="http://192.168.9.151:8080/Hybrid/functions.html";

这个是访问我本地服务器,function.html在webroot根目录下,内容如下

<!DOCTYPE HTML>
<html>
<head>
    <meta charset="utf-8">
    <title>JsBridge</title>
    <meta name="author" content="zhengxiaoyong">
    <meta name="viewport"
          content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1, target-densitydpi=medium-dpi, user-scalable=no">
    <meta property="og:site_name" content="JsBridge"/>
    <script src="RainbowBridge.js" type="text/javascript"></script>


    <style>
        .entry{
        -webkit-padding-start: 30px;
        }
        .entry li {
        line-height: 29px;
        margin-left: -10px;
        }
        .entry li div{
        margin-right: 10px;
        padding-left: 8px;
        padding-top: 8px;
        padding-bottom: 8px;
        background: #222222;
        color: white;
        word-break: break-all;
        -ms-word-wrap: break-word;
        word-wrap: break-word;
        line-height: 15px;
        font-size: 10pt;
        }
        .entry li button{
        margin-top: 5px;
        width: 60px;
        height: 35px;
        color: #111111;
        }
    </style>
</head>


<body>
<ul class="entry">
    <li>
        打印<br/>
        <br/>
        <div>
            我是Js调起的打印
        </div>
        <button
                onclick="RainbowBridge.callMethod('JsInvokeJavaScope','print',{'orderId':'20850258804864716800'},
                function(msg){alert(JSON.stringify(msg))});">
            打印
        </button>
    </li>
    <br/>
</ul>
</body>
</html>

关键代码第一行 <script src="RainbowBridge.js" type="text/javascript"></script>

关键代码第二行             onclick="RainbowBridge.callMethod('JsInvokeJavaScope','print',{'orderId':'20850258804864716800'},
                 function(msg){alert(JSON.stringify(msg))});">

第一行表示该html页面引入的js文件,处理交互动作如onclick

第二行是交互动作的写法规范,参考交互协议

Rainbridge.js内容如下:

/**  *  * native结果数据返回格式:  * var resultData = {  status: {  code: 0,//0成功,1失败  msg: '请求超时'//失败时候的提示,成功可为空  },  data: {}//数据,无数据可以为空 };  协定协议:rainbow://class:port/method?params;  params是一串json字符串  */ (function () {
    var doc = document;
    var win = window;
    var ua = win.navigator.userAgent;
    var JS_BRIDGE_PROTOCOL_SCHEMA = "rainbow";
    var increase = 1;
    var RainbowBridge = win.RainbowBridge || (win.RainbowBridge = {});

    var ExposeMethod = {

        callMethod: function (clazz, method, param, callback) {
            var port = PrivateMethod.generatePort();
            if (typeof callback !== 'function') {
                callback = null;
            }
            PrivateMethod.registerCallback(port, callback);
            PrivateMethod.callNativeMethod(clazz, port, method, param);
        },

        onComplete: function (port, result) {
            PrivateMethod.onNativeComplete(port, result);
        }

    };

    var PrivateMethod = {
        callbacks: {},
        registerCallback: function (port, callback) {
            if (callback) {
                PrivateMethod.callbacks[port] = callback;//callbacks.port=callback  }
        },
        getCallback: function (port) {
            var call = {};
            if (PrivateMethod.callbacks[port]) {
                call.callback = PrivateMethod.callbacks[port];
            } else {
                call.callback = null;
            }
            return call;
        },
        unRegisterCallback: function (port) {
            if (PrivateMethod.callbacks[port]) {
                delete PrivateMethod.callbacks[port];
            }
        },
        onNativeComplete: function (port, result) {
            var resultJson = PrivateMethod.str2Json(result);
            var callback = PrivateMethod.getCallback(port).callback;
            PrivateMethod.unRegisterCallback(port);
            if (callback) {
                //执行回调  callback && callback(resultJson);
            }
        },
        generatePort: function () {
            return Math.floor(Math.random() * (1 << 50)) + '' + increase++;
        },
        str2Json: function (str) {
            if (str && typeof str === 'string') {
                try {
                    return JSON.parse(str);
                } catch (e) {
                    return {
                        status: {
                            code: 1,
                            msg: 'params parse error!'  }
                    };
                }
            } else {
                return str || {};
            }
        },
        json2Str: function (param) {
            if (param && typeof param === 'object') {
                return JSON.stringify(param);
            } else {
                return param || '';
            }
        },
        callNativeMethod: function (clazz, port, method, param) {
            if (PrivateMethod.isAndroid()) {
                var jsonStr = PrivateMethod.json2Str(param);
                var uri = JS_BRIDGE_PROTOCOL_SCHEMA + "://" + clazz + ":" + port + "/" + method + "?" + jsonStr;
                win.prompt(uri, "");
            }
        },
        isAndroid: function () {
            var tmp = ua.toLowerCase();
            var android = tmp.indexOf("android") > -1;
            return !!android;
        },
        isIos: function () {
            var tmp = ua.toLowerCase();
            var ios = tmp.indexOf("iphone") > -1;
            return !!ios;
        }
    };
    for (var index in ExposeMethod) {
        if (ExposeMethod.hasOwnProperty(index)) {
            if (!Object.prototype.hasOwnProperty.call(RainbowBridge, index)) {
                RainbowBridge[index] = ExposeMethod[index];
            }
        }
    }
})();
该文件也在webRoot根目录下。


理一下服务端执行逻辑,当触发onClick事件后,将会告知设备端调用print函数,打印的订单参数为orderId=20850258804864716800

 onclick="RainbowBridge.callMethod('JsInvokeJavaScope','print',{'orderId':'20850258804864716800'},
                function(msg){alert(JSON.stringify(msg))})


设备端JsInvokeJavaScope类将会调用print函数

public static void print(WebView webView, JSONObject data, JsCallback callback)
{
   initPrintData(webView.getContext(), data);
}

private static void initPrintData(final Context context, JSONObject params)
{
   /**  * 解析前端参数  */  String orderId = "";

   try
   {
      orderId = params.getString("orderId");
   }
   catch (JSONException e)
   {
      e.printStackTrace();
   }

   if (TextUtils.isEmpty(orderId))
   {
      Toast.makeText(context, "订单id为空", Toast.LENGTH_SHORT).show();
      return;
   }

   IOrderModel orderModel = new OrderModelImpl();
   orderModel.getOrderPrintData(context, orderId, new IOrderModel.OnGetOrderPrintDataListener()
   {

      @Override
      public void onSuccess(List<PrintData> printDatas)
      {
         handleData(context,printDatas);
      }

      @Override
      public void onFailed(String reason)
      {
         Toast.makeText(context, reason, Toast.LENGTH_SHORT).show();
      }
   });
}

设备端请求打印数据,然后自己就可以愉快的打印啦