EasyApp

原生与H5交互

了解原生如何与H5交互

EasyApp 内置了 WebView 组件,可以方便地与 H5 进行交互。

基础用法

1. 加载远程网页

struct ContentView: View {
    var body: some View {
        WebView(request: URLRequest(url: URL(string: "https://example.com/")!))
    }
}

2. 加载离线网页

struct LocalWeb: View {

    @StateObject var viewModel = LocalWebViewModel()

    var body: some View {
        WebViewReader { proxy in
            VStack {
                WebView(configuration: viewModel.configuration)
            }
            .onAppear {
                proxy.loadHTMLString(viewModel.htmlString, baseURL: viewModel.htmlURL)
            }
        }

    }
}

class LocalWebViewModel: CommonEAWebViewModel {

    let htmlURL = Bundle.main.url(forResource: "sample", withExtension: "html")!
    let htmlString: String

    override init() {
        htmlString = try! String(contentsOf: htmlURL, encoding: .utf8)
        super.init()
    }
}

我们需要把名为sample.html的文件添加到项目中

离线H5如果想要做的特别完善,要牵扯到增量更新、服务端下发H5等。这一块暂不详细展开,如有需要,请自行搜索解决。

原生与H5交互

1. H5如何调用原生的一些能力

我们提供了一个例子,可以参考EasyApp-WebView-Debugger

该例子中,我们实现了以下功能:

  • 调用原生的Alert组件

  • 调用原生的Confirm组件

  • 调用原生的Alert Input组件

  • 唤起电话功能

  • 唤起发送短信功能

  • 唤起facetime

  • 唤起邮件功能

  • 其他的您可以在EasyApp-WebView-Debugger中自行实现即可。

  • 支持选择选择文件/照片

  • 支持分析照片

  • 支持获取当前定位

  • 支持唤起摄像头/麦克风功能

注意:EasyApp默认不会添加以下权限。因为如果您的应用没有用到这些权限,按照Apple的审核规则,您不应该添加权限申请。EasyApp考虑到审核的便捷性,默认不会添加这些权限。

如果您需要用到H5的相机、麦克风、地理位置功能,您需要添加以下权限:

<key>NSCameraUsageDescription</key>
<string>We need access to your camera to let you share them with your friends.</string>

麦克风权限

<key>NSMicrophoneUsageDescription</key>
<string>We need access to your microphone to let you share them with your friends.</string>

地理位置权限

<key>NSLocationWhenInUseUsageDescription</key>
<string>We need access to your location to let you share them with your friends.</string>

如下图所示 privateAuth.png

2. 原生如何注入脚本

ScriptMessageHandler.swift文件中,这里有一个例子,您可以参考:

EasyAppSwiftUI/App/Developer/SubPages/OnlineWeb/ViewModel/ScriptMessageHandler.swift
    /// Setup WebView JavaScript bridge
    private func setupWebViewBridge() {
        let source = """
            injectYourScriptHere
            """
        
        let userScript = WKUserScript(
            source: source, 
            injectionTime: .atDocumentStart, 
            forMainFrameOnly: true
        )
        userContentController.addUserScript(userScript)
    }

setupWebViewBridge方法中,我们在H5加载之前,可以在这个时机注入一段source脚本。

比如你想提前注入一个方法,让H5可以调用。

window.easyapp = {
    alert: function(message) {
        alert(message);
    }
}

通常我们都需要将方法挂载到Window上,这样js才可以直接通过window.easyapp.alert调用。

3. 如何注入cookie

模板中提供了一个公共类CommonEAWebViewModel,该类中默认提供了一个方法

要介绍下CommonEAWebViewModel的作用:

该类抽象出了原生与H5交互的公共逻辑,比如弹窗、内部WebView的协议代理、cookie注入等。

你只需要继承该类即可。

/// Set Cookie
/// - Parameters:
///   - request: The request to set the cookie for
///   - name: Cookie name
///   - value: Cookie value
func setCookie(request: URLRequest, name: String, value: String) {
    if let domain = request.url?.host(),
        let cookie = HTTPCookie(properties: [
            HTTPCookiePropertyKey.name: name,
            HTTPCookiePropertyKey.value: value,
            HTTPCookiePropertyKey.domain: domain,
            HTTPCookiePropertyKey.path: "/",
        ])
    {
        configuration.websiteDataStore.httpCookieStore.setCookie(cookie)
    }
}

你可以继承CommonEAWebViewModel类,然后在init方法中调用setCookie方法。

class OnlineWebViewModel: CommonEAWebViewModel {

    /// Request to the webview
    let request = URLRequest(url: URL(string: "https://easyapp-webview-debugger.vercel.app/")!)

    /// Init
    override init() {
        super.init()
        setCookie(request: request, name: "SampleKey", value: "SampleValue")
    }
}

注入cookie的目的是什么?

比如您在App中登录了,您希望在H5中也能获取到这个登录状态。此时您就需要在App中注入cookie,H5获取到cookie之后,就可以在H5中获取到登录状态。

4. 原生如何调用H5的方法/发送消息

首先您必须要在H5中,提供该方法,在我们提供的EasyApp-WebView-Debugger示例中

您可以看到registerReceiveMessageFunctions方法中,我们在window.sample上注册了showSwiftUIMessage方法。

/**
 * The functions registered here are specifically for SwiftUI client to call
 */
export function registerReceiveMessageFunctions() {
  //@ts-ignore
  window.sample.showSwiftUIMessage = showSwiftUIMessage;

  // If you want to receive messages from SwiftUI, you can register your functions here
}

我们推荐这样做: 由于JS没有命名空间的概念,我们推荐您根据不同的业务模块来区分。 如果您全部注册到window上,那么您需要确保方法名不会被覆盖。

拿例子来说:我们在sample上注册了showSwiftUIMessage方法. sample就可以作为您的业务模块。这样函数就不会被覆盖。

如果再来一个user模块,您可以同样的注册showSwiftUIMessage方法。

这样您就可以通过模块来区分方法了。

JS 端提供好方法后,客户端只需要这样就能调用JS方法:

Task {
    do {
        try await proxy.evaluateJavaScript(
            "window.sample.showSwiftUIMessage('Hello from SwiftUI')")
    } catch {
        print("error: \(error)")
    }
}

您可以在EasyAppSwiftUI/App/Developer/SubPages/OnlineWeb/View/OnlineView.swift中看到具体用法。

5. JS如何调用原生方法/发送消息

同样的,我们任然需要借助JS 的全局 window对象。我们在window上挂载一个sample对象。并且注册一个doSomething方法。 方法名字您可以随意命名。 这里仍然推荐你用业务模块作为命名空间,这样会更好的维护。

/**
 * common post message to SwiftUI
 * @param message message to send to SwiftUI
 */
function commonPostMessage(message: string) {
  // @ts-ignore
  webkit.messageHandlers.sample.postMessage({
    message: message,
  });
}

/**
 * The functions registered here are specifically for sending messages to the SwiftUI client
 */
export function registerSendMessageFunctions() {
  //@ts-ignore
  window.sample.doSomething = function (message: string) {
    // @ts-ignore
    commonPostMessage(message);
  };

  // if your SwiftUI has a function that you want to call, you can register it here
}

注册完成之后,你就可以通过window.sample.doSomething来调用原生方法。

export function sendSomethisToSwiftUI() {
  // @ts-ignore
  if (window.sample && typeof window.sample.doSomething === "function") {
    //  @ts-ignore
    window.sample.doSomething("Hello from H5");
  }
}

这一块逻辑,你可以在我们提供的EasyApp-WebView-Debugger中看到具体用法。

来到客户端中。我们要用到另外一个类ScriptMessageHandler, 该类是专门负责与JS交互的类。向H5注入脚本也是在该类中完成。

当客户端接收到H5发过来的消息时,会触发该方法:

/// Handle messages from the webview
/// - Parameters:
///   - userContentController: The user content controller
///   - message: The message from the webview   
func userContentController(
    _ userContentController: WKUserContentController,
    didReceive message: WKScriptMessage
) {
    // 在这里name就是H5的命名空间
    guard message.name == "sample" else {
        print("⚠️ Received message from unknown handler: \(message.name)")
        return
    }

    let json = JSON(message.body)

    let formattedJSON = json.rawString() ?? "{}"
    
    messageHandler?(formattedJSON)
}

注意:

message.name就是H5的命名空间。这里要跟H5一一对应才行,这里也是最容易出现问题的地方。要约定好。

你可以在这里的地方获取到H5发送过来的消息,剩下的逻辑就有您自己的业务决定了。

总结

在客户端最佳实践中

你可能有多个模块需要用到WebView的能力,我们推荐你根据模块来新增对应的ViewModel,每个ViewModel都继承CommonEAWebViewModel,这样你就可以获得公共的WebView能力。

加载Webview的URL也是在ViewModel中完成的。

   /// Request to the webview
let request = URLRequest(url: URL(string: "https://easyapp-webview-debugger.vercel.app/")!)

另外一个,需要客户端/H5原生交互的,我们推荐你新增XXX_ScriptMessageHandler类来处理。

不建议你把所有的逻辑全都放在我们提供的ScriptMessageHandler中,你应该要有跟模块对应的ScriptMessageHandler类来处理。 ScriptMessageHandler只是提供一个示例。

每个模块的ScriptMessageHandlerViewModel要是关联的。方法如下:在ViewModel中,我们设置好ScriptMessageHandlerViewModel的关联方法:

/// Message handler
let messageHandler = ScriptMessageHandler.shared

configuration.defaultWebpagePreferences.preferredContentMode = .mobile

configuration.userContentController = messageHandler.userContentController
  

然后ScriptMessageHandler接受到的消息,你可以通过闭包的方式来通知ViewModel

ViewModel中,你就可以自行处理你的业务逻辑了。

/// Start listening to messages from ScriptMessageHandler
private func startListeningToMessages() {
    messageHandler.messageHandler = { [weak self] message in
        guard let self = self else { return }
        // do your business logic here
    }
}

在H5端,就一点要注意。

由于JS没有命名空间的概念,我们推荐你根据不同的业务模块来区分。不推荐全部方法都直接挂载window上。当您的业务模块越来越多时,很容易出现方法名冲突。

Last updated on