最近公司项目开发中涉及到了大量的混合开发,这里开一个系列,把开发中的经验和遇到的问题和大家分享下

讲到移动端的混合开发,绕不开的一个话题就是原生和Js的交互,关于iOS、Android怎么和js交互,网上的资料很多,这里先简单介绍几个方法。

js部分

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>

    <script>
            function iOSShowDialog()
            {
                alert("iOS btn");

                //showDialog 为iOS内定义好的方法

                var params = {"title":"js 调用 iOS", "type":0, "callBack":"nativeCallBack"};

                var paramsStr = JSON.stringify(params);

                alert("iOS params:" + paramsStr);

                window.webkit.messageHandlers.showDialog.postMessage(paramsStr);
            }

            function androidShowDialog()
            {
                alert("Android btn");

                var params = {"title":"js 调用 Android", "type":1, "callBack":"nativeCallBack"};

                var paramsStr = JSON.stringify(params);

                alert("iOS params:" + paramsStr);

                androidProxy.showDialog(paramsStr);
            }

            function nativeCallBack(paramsStr)
            {
//              var str:String = paramsStr;

                var params = JSON.parse(paramsStr);

                document.getElementById("info").innerHTML = params.message;
            }
        </script>

    <body>
        <body>
        <div>
            <button id="iOSBtn" onclick="iOSShowDialog()">click me call iOS show dialog</button>
        </div>
        <div>
            <button id="AndroidBtn" onclick="androidShowDialog()">click me call Android show dialog</button>
        </div>
        <div>
            <textarea id="info">...</textarea>
        </div>
    </body>
    </body>
</html>

简单说一下,js里一共定义了三个方法,前两个iOSShowDialog,androidShowDialog看名字应该就能猜出来,是分别用来调用iOS和Android原生的方法的,第三个方法nativeCallBack是用来给原生回调的。

这里稍微多说一点,虽然js调用原生的方法是可以有返回值的,但是在设计这个构架时,还是建议使用异步回调的方式。因为在js和原生的通信场景中,有很多是异步流程,最典型的就是js向原生要一个数据用来在页面中展示,但是这个数据原生是需要通过服务器请求返回的。这个场景里,很明显函数返回值是没有办法满足需求的。

页面刷出来是这个样子:

android 混合开发h5 ios和h5混合开发_android


js准备完毕

iOS部分

override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.

        // 创建配置
        let config = WKWebViewConfiguration()
        // 创建UserContentController(提供JavaScript向webView发送消息的方法)
        let userContent = WKUserContentController()
        // 添加消息处理,注意:self指代的对象需要遵守WKScriptMessageHandler协议,结束时需要移除
        userContent.add(self, name: "showDialog");
        // 将UserConttentController设置到配置文件
        config.userContentController = userContent;

        webView = WKWebView(frame: CGRect(x:0, y:0, width:self.view.bounds.width, height:300), configuration: config);

//        let url = Bundle.main.url(forResource: "demo", withExtension: "html");

        let url = URL(string: "http://你的域名/demo.html");

        webView?.navigationDelegate = self;

        webView?.uiDelegate = self;

//        webView?.loadFileURL(url!, allowingReadAccessTo: Bundle.main.bundleURL);

        webView?.load(URLRequest(url: url!));

        self.view.addSubview(webView!);
    }


func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        if(message.name == "showDialog")
        {
            let paramsStr = message.body as! String;

            let paramsData = paramsStr.data(using: .utf8);

            let params = try! JSONSerialization.jsonObject(with: paramsData!, options: .allowFragments) as! Dictionary<String, Any>;

            let title = params["title"] as! String;

            let type = params["type"] as! Int;

            let callBack = params["callBack"] as! String;

            let alert = UIAlertController(title: title, message: "type is "+type.description, preferredStyle: .alert);
            alert.addAction(UIAlertAction(title: "确定", style: .default, handler: nil))
            self.present(alert, animated: true, completion: nil);


            let callBackParams = ["message":"iOS的回调来了"];

            let callBackParamsStrData = try! JSONSerialization.data(withJSONObject: callBackParams, options: []);

            let callBackParamsStr = String(data: callBackParamsStrData, encoding: .utf8);

            let callBackFunc = callBack+"("+"'\(callBackParamsStr!)'"+")";

            webView?.evaluateJavaScript(callBackFunc, completionHandler: { (item, error) in
                print(item, error);
            })
        }
    }

这里只上两个核心的方法,iOS我没有用UIWebView+JavascriptCore,而是直接用了WKWebView,毕竟WKWebView效率和内存消耗好很多。但是WKWebView貌似不支持JavascriptCore,它有自己的js交互方式,并且也很简单:

// 创建配置
        let config = WKWebViewConfiguration()
        // 创建UserContentController(提供JavaScript向webView发送消息的方法)
        let userContent = WKUserContentController()
        // 添加消息处理,注意:self指代的对象需要遵守WKScriptMessageHandler协议,结束时需要移除
        userContent.add(self, name: "showDialog");
        // 将UserConttentController设置到配置文件
        config.userContentController = userContent;

这里注意一下userContent.add(self, name: "showDialog");这里的是向H5注入方法的过程,因此showDialog必须和js里调用的方法名一致。并且,第一个参数填入的对象必须遵守WKScriptMessageHandler接口。
再来看响应,

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage)

刚才说了,userContent传入的对象必须遵守WKScriptMessageHandler接口,上面的函数就是接口里必须实现的方法。

if(message.name == "showDialog")

message对象的name属性是方法名

let paramsStr = message.body as! String;

message 对象的body是方法的参数

android 混合开发h5 ios和h5混合开发_ios_02


再来看回调,我在js传给iOS的json里添加了一个字段callBack,因此,回调只需要将callBack字段里的方法名取出,拼装成一个函数调用语句,通过WKWebView调用js的方法,调一下就可以了。

let callBackParams = ["message":"iOS的回调来了"];

            let callBackParamsStrData = try! JSONSerialization.data(withJSONObject: callBackParams, options: []);

            let callBackParamsStr = String(data: callBackParamsStrData, encoding: .utf8);

            let callBackFunc = callBack+"("+"'\(callBackParamsStr!)'"+")";

            webView?.evaluateJavaScript(callBackFunc, completionHandler: { (item, error) in
                print(item, error);
            })

注意这句,let callBackFunc = callBack+"("+"'\(callBackParamsStr!)'"+")";

刚才说了,拼装成一句函数调用语句。刚开始,我拼出来的是这样的:"nativeCallBack(["message":"iOS的回调来了"])"

乍一看,没毛病,但始终报错,后来才意识到,我在js的回调方法里定义的参数是一个String,也就是说,我调用的语句参数必须有引号包围。所以,正确的应该是这样的:"nativeCallBack( ' ["message":"iOS的回调来了"] ' )"

android 混合开发h5 ios和h5混合开发_android 混合开发h5_03


嗯,没毛病!

Android部分

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mWebView = (WebView) findViewById(R.id.webview);
        WebSettings webSettings = mWebView.getSettings();

        // 设置与Js交互的权限
        webSettings.setJavaScriptEnabled(true);

        // 通过addJavascriptInterface()将Java对象映射到JS对象
        //参数1:Javascript对象名
        //参数2:Java对象名


        // 加载JS代码
        // 格式规定为:file:///android_asset/文件名.html

        mWebView.post(new Runnable() {
            @Override
            public void run() {
//                mWebView.loadUrl("file:///android_asset/demo.html");
                mWebView.loadUrl("http://你的域名/demo.html");

                mWebView.addJavascriptInterface(new AndroidtoJs(mWebView), "androidProxy");//AndroidtoJS类对象映射到js的test对象
            }
        });

    }

@JavascriptInterface
    public void showDialog(String dataStr)
    {
        JSONObject jsonObject = null;
        try {
            jsonObject = new JSONObject(dataStr);

            String title = jsonObject.getString("title");

            int type = jsonObject.getInt("type");

            Toast.makeText(AppApplication.getInstance(), title + "type:" + type, Toast.LENGTH_LONG).show();

            final String callBack = jsonObject.getString("callBack");

            Map<String,String> callBackParams = new HashMap<>();
            callBackParams.put("message", "这是从Android原生返回的数据");

            JSONObject paramsJsonObjct = new JSONObject(callBackParams);
            final String paramsJsonObjctStr = paramsJsonObjct.toString();

            Log.d("AndroidtoJs",paramsJsonObjctStr);

            wView.post(new Runnable() {
                @Override
                public void run() {
                    wView.evaluateJavascript("javascript:" + callBack + "('" + paramsJsonObjctStr + "')", new ValueCallback<String>() {
                        @Override
                        public void onReceiveValue(String value) {

                        }
                    });
                }
            });

        } catch (JSONException e) {
            e.printStackTrace();
        }
    }

Android相对简单点,直接用WebView就行,方式其实和iOS类似,向H5注入一个对象,对象中定义的方法就可以背js调用。mWebView.addJavascriptInterface(new AndroidtoJs(mWebView), "androidProxy");//AndroidtoJS类对象映射到js的test对象

注意,androidProxy就是你在js里调用的对象名,AndroidtoJs是在Android项目里定义的对象,里面封装了需要暴露给js的方法public void showDialog(String dataStr)。这个方法前一定要加上@JavascriptInterface

android 混合开发h5 ios和h5混合开发_android 混合开发h5_04


回调也很简单,

wView.post(new Runnable() {
                @Override
                public void run() {
                    wView.evaluateJavascript("javascript:" + callBack + "('" + paramsJsonObjctStr + "')", new ValueCallback<String>() {
                        @Override
                        public void onReceiveValue(String value) {

                        }
                    });

这里有一点注意的地方,Android规定调用webUiew对象的方法必须在同一个线程,不然会报错。因此,在调用webView.loadUrl和webView.evaluateJavascript时,都加上了webView.post(new Runnable() {}

尾声

第一篇就写这些,有什么问题望大家多多指教。