题图

缘起

智能手机兴起,移动端开发火热起来。起初只有Android和IOS应用,随着H5的问世,以及手机性能的提升,Web App开始斩露头角。

一份代码,多端运行。这预示着开发成本的降低,同时也能减少因开发者实现的差异导致Android和IOS应用出现不同的表示

发展

如果某一块的功能简单,仅仅是一些页面的展示,那么无用质疑,使用Web App技术现实更优,通过WebView嵌入到原生应用中。

最初公司的网页代码是放在服务端。但是这样导致一个问题,就是进入网页时,会有白屏的现象。后来就提出将网页直接放到原生应用中,因为我们采用的前端后分离的模式,数据全部通过异步请求去获取,所以该方案可以实现。

当然这种将网页放到原生应用中会有一个问题,就是安装包的体积会增大。这个需要根据实际情况去衡量,是否采用这种方案。我们认为相比于体积的增大,白屏的体验更糟糕。

开发

Hybrid App得以发展的核心就是WebView,而开发的重点就在于原生应用和Web App的通信。

Android 与 Web App

如果在Android中使用WebView记得在AndroidManifest.xml中配置网络权限

<uses-permission android:name="android.permission.INTERNET"/>

Android 9.0 默认使用加密连接,这意味着老旧项目在android 9.0 设备上运行,会遇到异常的情况。

在配置中加上下面内容即可,true是否使用明文传输,也就是可以使用http

android:usesCleartextTraffic="true"

Android 调用 JS

Android 调用 JS 有两种方式,都是通过 WebView 的方法:

  1. webview.loadUrl()
  2. webview.evaluateJavascript()

二者区别:

  1. loadUrl() 会刷新页面,evaluateJavascript() 则不会使页面刷新,所以 evaluateJavascript() 的效率更高
  2. loadUrl() 得不到 js 的返回值,evaluateJavascript() 可以获取返回值
  3. evaluateJavascript() 在 Android 4.4 之后才可以使用

代码示例:

package com.wit;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;

public class WebActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_web);

        final WebView webView = findViewById(R.id.wv);
        webView.loadUrl("file:///android_asset/index.html");
        webView.setWebViewClient(new WebViewClient() {
            @Override
            public void onPageFinished(WebView view, String url) {
                super.onPageFinished(view, url);

                webView.post(new Runnable() {
                    @Override
                    public void run() {
                        webView.evaluateJavascript("javascript:callMe('Hello World!')", new ValueCallback<String>() { // 注意这一行,调用的地方
                            @Override
                            public void onReceiveValue(String value) {
                                // TODO
                            }
                        });
                    }
                });
            }
        });
    }
}
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<h1>你好!</h1>
<script>
    window.callMe = function(s) { // 方法需要挂在 window 下面
        var title = document.querySelector('h1');
        title.innerHTML = s;
    }
</script>
</body>
</html>

JS 调用 Android

对于JS调用Android代码的方法有3种:

  1. 通过 WebView 的 addJavascriptInterface() 进行对象映射
  2. 通过 WebViewClient 的 shouldOverrideUrlLoading() 方法回调拦截 url
  3. 通过 WebChromeClient 的onJsAlert()、onJsConfirm()、onJsPrompt() 方法回调拦截 JS 对话框 alert()、confirm()、prompt() 消息

代码示例:

package com.wit;

import android.app.Activity;
import android.webkit.JavascriptInterface;

public class JsJavaBridge {
    private Activity activity;

    JsJavaBridge(Activity activity) {
        this.activity = activity;
    }

    @JavascriptInterface
    public void onFinishActivity() {
        activity.finish();
    }
}
package com.wit;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;

public class WebActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // ...
        // webView.getSettings().setJavaScriptEnabled(true);   
        webView.getSettings().setJavaScriptEnabled(true);                 // 注意
        webView.addJavascriptInterface(new JsJavaBridge(this), "$wv");    // 注意
        // webView.setWebViewClient(new WebViewClient() {
		// ...
    }
}
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<h1>你好!</h1>
<button>关闭页面</button>
<script>
    window.callMe = function(s) {
        var title = document.querySelector('h1');
        title.innerHTML = s;
    }

    var btn = document.querySelector('button');
    btn.onclick = function() {
        window.$wv.onFinishActivity();
    }
</script>
</body>
</html>

IOS 与 Web App

IOS中主要有两种WebView:

  • UIWebView
  • WKWebView:iOS 8.0之后才推出的新控件

相比与UIWebView,WKWebView存在很多优势:

  • 支持更多的HTML5的特性
  • 高达60fps滚动刷新频率与内置手势
  • 与Safari相容的JavaScript引擎
  • 在性能、稳定性方面有很大提升占用内存更少 协议方法及功能都更细致
  • 可获取加载进度等

这里主要介绍WKWebView与JS的交互

IOS 调用 JS

直接使用WebView实例的evaluateJavaScript方法即可。

self.webView.evaluateJavaScript("calledByNative()") { (str, error) in
    if error != nil {
        print("\(String(describing: error))")
    } else {
        print(str ?? "")
    }
}

这里的calledByNative是JS中挂在window对象的方法。

JS 调用 IOS

首页IOS启动WebView时,可以注入一些方法,通过window.webkit.messageHandlers暴露给JS去调用。

webConfiguratiojn.userContentController.add(self, name: "nativeFun")

这里的nativeFun就是IOS供原生调用的方法。

extension ViewController: WKScriptMessageHandler {
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        print("传来的数据为", message.name, message.body)
        // 处理完数据后,可以进行回调
        self.webView.evaluateJavaScript("calledByNative()") { (str, error) in
            if error != nil {
                print("\(String(describing: error))")
            } else {
                print(str ?? "")
            }
        }
    }
}
window.webkit.messageHandlers.nativeFun.postMessage({
	name: 'King'
})

在JS中可以从window.webkit.messageHandlers对象获取方法。

附完整代码

IOS

import UIKit
import WebKit

class ViewController: UIViewController {
    @IBOutlet var webView: WKWebView!
    
    override func loadView() {
        let webConfiguratiojn = WKWebViewConfiguration()
        webConfiguratiojn.userContentController.add(self, name: "nativeFun")
        webView = WKWebView(frame: .zero, configuration: webConfiguratiojn)
        view = webView
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let fileURL =  Bundle.main.url(forResource: "www/index", withExtension: "html" )
        webView.loadFileURL(fileURL!, allowingReadAccessTo:Bundle.main.bundleURL);
    }
}

extension ViewController: WKScriptMessageHandler {
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        print("传来的数据为", message.body)
        // 处理完数据后,可以进行回调
        self.webView.evaluateJavaScript("calledByNative()") { (str, error) in
            if error != nil {
                print("\(String(describing: error))")
            } else {
                print(str ?? "")
            }
        }
    }
}

JS

<!DOCTYPE html>
<html>
<head>
	<title>测试</title>
    <meta charset=UTF-8>
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
	<h1 id="title">hello</h1>
    <button id="btn">点我</button>

    <script>

    	var title = document.getElementById('title')
    	var btn = document.getElementById('btn')
    	btn.onclick = function() {
    		title.innerHTML = '改变'
    		window.webkit.messageHandlers.nativeFun.postMessage({
    			name: 'King'
    		})
    		alert('haha')
    	}

    	window.calledByNative = function() {
    		title.innerHTML = "我被原生调用了"
    	}
    </script>
</body>
</html>

参考

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。