原生与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>如下图所示

2. 原生如何注入脚本
在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只是提供一个示例。
每个模块的ScriptMessageHandler与ViewModel要是关联的。方法如下:在ViewModel中,我们设置好ScriptMessageHandler与ViewModel的关联方法:
/// 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