在 iOS 混合开发(Hybrid App)中,WKWebView 是目前唯一官方推荐、主流 App 必用的 Web 容器(iOS 8.0+ 支持,目前主流 App 最低版本均 ≥ iOS 10,无兼容性顾虑)。WKWebView 相比已废弃的 UIWebView,拥有更高的性能、更优的安全性和更完善的 JS 交互能力,其与 JS 的无缝通信,是实现“Web 灵活迭代 + Native 强大能力”的核心。
本文将完全聚焦最新 WKWebView,抛弃所有旧版本交互方式,重点讲解 WKWebView 与 JS 交互的底层原理、可直接复用的交互类封装(Swift/OC 双版本),结合实战案例拆解细节,补充避坑技巧与最佳实践,适合 iOS 开发者快速集成、高效开发。
一、核心基础:WKWebView 与 JS 交互的本质
JS 运行在 WKWebView 的 JS 引擎中,而 Native 代码(Swift/OC)运行在 iOS 系统原生 runtime 中,两者属于完全独立的运行环境,无法直接通信。
因此,WKWebView 与 JS 交互的本质是:通过 WKWebView 自带的“消息桥梁”,实现两种环境的数据传递和方法调用。核心依赖两个核心能力:
- JS 调用 Native:通过 WKScriptMessageHandler 协议,JS 发送消息,Native 监听接收并执行对应逻辑;
- Native 调用 JS:通过 WKWebView 的 evaluateJavaScript(_:completionHandler:) 方法,执行 JS 代码并获取回调结果。
关键前提(必懂)
- 交互方向:分为 JS 调用 Native(H5 触发原生功能,如调用相机、支付)和 Native 调用 JS(原生触发 Web 逻辑,如同步用户信息);
- 数据格式:优先使用 JSON(跨平台通用、解析便捷),避免传递函数、Date 等复杂对象(无法 JSON 序列化,易报错);
- 核心原则:交互类封装需兼顾“易用性、可扩展性、避免内存泄漏”,统一接口规范,降低业务层集成成本。
二、核心封装:WKWebView 交互类(Swift 版,最新语法)
实战开发中,直接在控制器中编写 WKWebView 交互逻辑,会导致代码冗余、复用性差、内存泄漏风险高。以下封装一个通用的 WKWebView 交互管理类(WKWebViewInteractionManager),集成初始化、消息注册、双向交互、生命周期管理等核心功能,可直接复制到项目中使用。
1. 封装思路
- 单例模式:全局统一管理,避免多次初始化 WKWebView 造成资源浪费;
- 协议回调:通过代理将 JS 调用 Native 的事件回调给业务层,解耦封装层与业务层;
- 自动管理:自动注册/移除消息监听、销毁 WKWebView,避免内存泄漏;
- 统一接口:提供简洁的 API 供业务层调用(注册消息、调用 JS 方法、加载页面等)。
2. 完整封装代码(Swift 5.0+)
import UIKit
import WebKit
// MARK: - JS 调用 Native 的代理协议
protocol WKWebViewInteractionDelegate: AnyObject {
/// JS 调用 Native 方法回调
/// - Parameters:
/// - methodName: JS 调用的方法名(与 JS 约定一致)
/// - params: JS 传递的参数(自动解析为 Dictionary,无参数则为 nil)
/// - callback: 回调给 JS 的闭包(需回传结果时调用)
func webViewDidReceiveJSCall(methodName: String, params: [String: Any]?, callback: @escaping (Any?) -> Void)
}
// MARK: - WKWebView 交互管理类(单例,可直接复用)
class WKWebViewInteractionManager: NSObject {
// 单例实例
static let shared = WKWebViewInteractionManager()
// 私有化构造方法,禁止外部初始化
private override init() {}
// 核心属性
private var webView: WKWebView!
private weak var delegate: WKWebViewInteractionDelegate?
// 已注册的 JS 消息名(用于页面销毁时统一移除)
private var registeredMessageNames: [String] = []
// MARK: - 初始化 WKWebView(对外暴露,业务层调用)
/// 初始化 WKWebView 并配置交互参数
/// - Parameters:
/// - frame: WKWebView Frame
/// - delegate: 交互代理(接收 JS 调用回调)
/// - messageNames: 需要注册的 JS 消息名(与 JS 约定)
/// - Returns: 配置好的 WKWebView(业务层直接添加到视图)
func setupWebView(frame: CGRect, delegate: WKWebViewInteractionDelegate?, registerMessageNames: [String]) -> WKWebView {
self.delegate = delegate
self.registeredMessageNames = registerMessageNames
// 1. 配置 WKWebView 偏好设置(开启 JS 执行、允许跨域)
let config = WKWebViewConfiguration()
config.preferences.javaScriptEnabled = true
// 允许跨域(解决远程 H5 调用 Native 失败问题)
config.setValue(true, forKey: "allowUniversalAccessFromFileURLs")
// 2. 注册 JS 消息(监听 JS 调用)
let userContentController = WKUserContentController()
registerMessageNames.forEach { messageName in
userContentController.add(self, name: messageName)
}
config.userContentController = userContentController
// 3. 初始化 WKWebView(设置代理,监听页面加载状态)
webView = WKWebView(frame: frame, configuration: config)
webView.uiDelegate = self
webView.navigationDelegate = self
// 禁止侧滑返回(可选,根据业务需求配置)
webView.allowsBackForwardNavigationGestures = false
return webView
}
// MARK: - Native 调用 JS 方法(对外暴露,业务层直接调用)
/// Native 调用 JS 方法
/// - Parameters:
/// - methodName: JS 中定义的方法名
/// - params: 传递给 JS 的参数(可选,自动转为 JSON 字符串)
/// - completion: 调用完成后的回调(接收 JS 返回的结果或错误)
func callJSMethod(methodName: String, params: [String: Any]? = nil, completion: ((Any?, Error?) -> Void)? = nil) {
// 拼接 JS 代码(处理参数,无参数则直接调用方法)
var jsCode = "(methodName)()"
if let params = params, let jsonData = try? JSONSerialization.data(withJSONObject: params),
let jsonStr = String(data: jsonData, encoding: .utf8) {
jsCode = "(methodName)((jsonStr))"
}
// 主线程执行 JS 调用(WKWebView 必须在主线程操作)
DispatchQueue.main.async { [weak self] in
guard let self = self, let webView = self.webView else { return }
webView.evaluateJavaScript(jsCode) { result, error in
completion?(result, error)
}
}
}
// MARK: - 加载页面(对外暴露,支持本地 HTML 和远程 URL)
/// 加载本地 HTML 文件
/// - Parameters:
/// - fileName: HTML 文件名(不带后缀)
/// - fileType: HTML 文件后缀(默认 html)
func loadLocalHTML(fileName: String, fileType: String = "html") {
guard let htmlPath = Bundle.main.path(forResource: fileName, ofType: fileType),
let htmlUrl = URL(fileURLWithPath: htmlPath) else { return }
webView.loadFileURL(htmlUrl, allowingReadAccessTo: htmlUrl.deletingLastPathComponent())
}
/// 加载远程 URL
/// - Parameter urlString: 远程 URL 字符串
func loadRemoteURL(urlString: String) {
guard let url = URL(string: urlString) else { return }
webView.load(URLRequest(url: url))
}
// MARK: - 资源释放(对外暴露,业务层页面销毁时调用)
func destroy() {
// 移除所有 JS 消息监听(避免内存泄漏)
registeredMessageNames.forEach { messageName in
webView.configuration.userContentController.removeScriptMessageHandler(forName: messageName)
}
// 停止加载,销毁 webView
webView.stopLoading()
webView.uiDelegate = nil
webView.navigationDelegate = nil
webView.removeFromSuperview()
webView = nil
delegate = nil
}
}
// MARK: - WKScriptMessageHandler(接收 JS 消息)
extension WKWebViewInteractionManager: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
// 1. 获取 JS 传递的方法名和参数
let methodName = message.name
var params: [String: Any]? = nil
// 解析参数(JS 传递的参数需为 JSON 格式,否则解析失败)
if let body = message.body as? [String: Any] {
params = body
} else if let bodyStr = message.body as? String,
let jsonData = bodyStr.data(using: .utf8),
let jsonDict = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] {
params = jsonDict
}
// 2. 回调给业务层,由业务层处理具体逻辑
delegate?.webViewDidReceiveJSCall(methodName: methodName, params: params) { [weak self] response in
// 3. 将业务层的处理结果回传给 JS(如果 JS 需要回调)
guard let self = self, let response = response else { return }
if let jsonData = try? JSONSerialization.data(withJSONObject: response),
let jsonStr = String(data: jsonData, encoding: .utf8) {
let jsCode = "window.receiveNativeResponse('(methodName)', (jsonStr))"
self.webView.evaluateJavaScript(jsCode, completionHandler: nil)
}
}
}
}
// MARK: - WKUIDelegate + WKNavigationDelegate(页面加载、弹窗管理)
extension WKWebViewInteractionManager: WKUIDelegate, WKNavigationDelegate {
// 页面加载完成(可在此处主动调用 JS 方法,如同步设备信息)
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
print("WKWebView 页面加载完成")
// 示例:主动调用 JS 方法,传递设备型号
let deviceModel = UIDevice.current.model
callJSMethod(methodName: "jsReceiveDeviceModel", params: ["deviceModel": deviceModel]) { result, error in
if let error = error {
print("调用 JS 失败:(error.localizedDescription)")
}
}
}
// 拦截 JS 弹窗(alert、confirm、prompt),替换为原生弹窗(优化用户体验)
func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
let alert = UIAlertController(title: "提示", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "确定", style: .default, handler: { _ in
completionHandler()
}))
UIApplication.shared.keyWindow?.rootViewController?.present(alert, animated: true)
}
// 解决跨域问题(远程 H5 调用 Native 必加)
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
decisionHandler(.allow)
}
}
3. 封装类使用示例(业务层控制器)
import UIKit
import WebKit
class WKWebViewDemoVC: UIViewController, WKWebViewInteractionDelegate {
private var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
title = "WKWebView 交互演示"
// 1. 初始化交互管理类,获取 WKWebView
let messageNames = ["callCamera", "showToast", "submitUserName"] // 与 JS 约定的消息名
webView = WKWebViewInteractionManager.shared.setupWebView(
frame: view.bounds,
delegate: self,
registerMessageNames: messageNames
)
view.addSubview(webView)
// 2. 加载页面(本地 HTML 或远程 URL)
WKWebViewInteractionManager.shared.loadLocalHTML(fileName: "index")
// WKWebViewInteractionManager.shared.loadRemoteURL(urlString: "https://xxx.com")
}
// MARK: - WKWebViewInteractionDelegate(接收 JS 调用)
func webViewDidReceiveJSCall(methodName: String, params: [String: Any]?, callback: @escaping (Any?) -> Void) {
// 根据 JS 调用的方法名,执行对应原生逻辑
switch methodName {
case "callCamera":
openCamera(callback: callback)
case "showToast":
guard let msg = params?["msg"] as? String else {
callback(["status": "fail", "msg": "参数错误"])
return
}
showToast(message: msg)
callback(["status": "success", "msg": "提示显示成功"])
case "submitUserName":
guard let userName = params?["userName"] as? String else {
callback(["status": "fail", "msg": "用户名不能为空"])
return
}
let isLegal = userName.count > 3
let result = isLegal ? "用户名验证通过" : "用户名长度不能小于4位"
callback(["status": isLegal ? "success" : "fail", "msg": result])
default:
callback(["status": "fail", "msg": "未找到对应方法"])
}
}
// 原生相机逻辑
private func openCamera(callback: @escaping (Any?) -> Void) {
guard UIImagePickerController.isSourceTypeAvailable(.camera) else {
callback(["status": "fail", "msg": "相机不可用"])
return
}
let picker = UIImagePickerController()
picker.sourceType = .camera
picker.delegate = self
present(picker, animated: true)
}
// 原生提示框逻辑
private func showToast(message: String) {
let alert = UIAlertController(title: "提示", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "确定", style: .default))
present(alert, animated: true)
}
// 页面销毁时,释放资源(避免内存泄漏)
deinit {
WKWebViewInteractionManager.shared.destroy()
print("WKWebViewDemoVC 销毁,资源释放完成")
}
}
// MARK: - 相机代理(获取图片路径,回传给 JS)
extension WKWebViewDemoVC: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
picker.dismiss(animated: true)
// 模拟图片路径(实际开发需保存图片并获取真实路径)
let imagePath = "file:///var/mobile/Containers/Data/Application/xxx/photo.jpg"
// 调用 JS 方法,回传图片路径
WKWebViewInteractionManager.shared.callJSMethod(
methodName: "jsReceiveImagePath",
params: ["imagePath": imagePath]
)
}
}
三、核心封装:WKWebView 交互类(OC 版,兼容最新系统)
针对 OC 项目,封装对应的 WKWebView 交互管理类(WKWebViewInteractionManager),逻辑与 Swift 版一致,提供统一的接口,可直接集成使用。
1. 头文件(WKWebViewInteractionManager.h)
#import <UIKit/UIKit.h>
#import <WebKit/WebKit.h>
@protocol WKWebViewInteractionDelegate <NSObject>
@optional
/// JS 调用 Native 方法回调
/// @param methodName JS 调用的方法名
/// @param params JS 传递的参数(字典类型,无参数则为 nil)
/// @param callback 回调给 JS 的 block(需回传结果时调用)
- (void)webViewDidReceiveJSCallWithMethodName:(NSString *)methodName
params:(NSDictionary *)params
callback:(void(^)(id _Nullable response))callback;
@end
@interface WKWebViewInteractionManager : NSObject
/// 单例实例
+ (instancetype)sharedManager;
/// 初始化 WKWebView 并配置交互参数
/// @param frame WKWebView 的 Frame
/// @param delegate 交互代理
/// @param messageNames 需要注册的 JS 消息名数组
/// @return 配置好的 WKWebView
- (WKWebView *)setupWebViewWithFrame:(CGRect)frame
delegate:(id<WKWebViewInteractionDelegate>)delegate
registerMessageNames:(NSArray<NSString *> *)messageNames;
/// Native 调用 JS 方法
/// @param methodName JS 中定义的方法名
/// @param params 传递给 JS 的参数(可选,自动转为 JSON 字符串)
/// @param completion 调用完成后的回调(接收 JS 返回的结果或错误)
- (void)callJSMethodWithMethodName:(NSString *)methodName
params:(NSDictionary *)params
completion:(void(^_Nullable)(id _Nullable result, NSError * _Nullable error))completion;
/// 加载本地 HTML 文件
/// @param fileName HTML 文件名(不带后缀)
/// @param fileType HTML 文件后缀(默认 html)
- (void)loadLocalHTMLWithFileName:(NSString *)fileName
fileType:(NSString *)fileType;
/// 加载远程 URL
/// @param urlString 远程 URL 字符串
- (void)loadRemoteURLWithUrlString:(NSString *)urlString;
/// 资源释放(页面销毁时调用)
- (void)destroy;
@end
2. 实现文件(WKWebViewInteractionManager.m)
#import "WKWebViewInteractionManager.h"
@interface WKWebViewInteractionManager () <WKScriptMessageHandler, WKUIDelegate, WKNavigationDelegate>
@property (nonatomic, strong) WKWebView *webView;
@property (nonatomic, weak) id<WKWebViewInteractionDelegate> delegate;
@property (nonatomic, strong) NSArray<NSString *> *registeredMessageNames;
@end
@implementation WKWebViewInteractionManager
+ (instancetype)sharedManager {
static WKWebViewInteractionManager *manager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [[WKWebViewInteractionManager alloc] init];
});
return manager;
}
- (instancetype)init {
self = [super init];
if (self) {
// 初始化配置(可选)
}
return self;
}
#pragma mark - 对外暴露方法
- (WKWebView *)setupWebViewWithFrame:(CGRect)frame
delegate:(id<WKWebViewInteractionDelegate>)delegate
registerMessageNames:(NSArray<NSString *> *)messageNames {
self.delegate = delegate;
self.registeredMessageNames = messageNames;
// 配置 WKWebView
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
config.preferences.javaScriptEnabled = YES;
[config setValue:@YES forKey:@"allowUniversalAccessFromFileURLs"];
// 注册 JS 消息
WKUserContentController *userContentController = [[WKUserContentController alloc] init];
for (NSString *messageName in messageNames) {
[userContentController addScriptMessageHandler:self name:messageName];
}
config.userContentController = userContentController;
// 初始化 WKWebView
self.webView = [[WKWebView alloc] initWithFrame:frame configuration:config];
self.webView.uiDelegate = self;
self.webView.navigationDelegate = self;
self.webView.allowsBackForwardNavigationGestures = NO;
return self.webView;
}
- (void)callJSMethodWithMethodName:(NSString *)methodName
params:(NSDictionary *)params
completion:(void(^_Nullable)(id _Nullable result, NSError * _Nullable error))completion {
// 拼接 JS 代码
NSMutableString *jsCode = [NSMutableString stringWithFormat:@"%@()", methodName];
if (params) {
NSError *error = nil;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:params options:0 error:&error];
if (!error) {
NSString *jsonStr = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
jsCode = [NSMutableString stringWithFormat:@"%@(%@)", methodName, jsonStr];
}
}
// 主线程执行
dispatch_async(dispatch_get_main_queue(), ^{
[self.webView evaluateJavaScript:jsCode completionHandler:^(id _Nullable result, NSError * _Nullable error) {
if (completion) {
completion(result, error);
}
}];
});
}
- (void)loadLocalHTMLWithFileName:(NSString *)fileName
fileType:(NSString *)fileType {
NSString *htmlPath = [[NSBundle mainBundle] pathForResource:fileName ofType:fileType ?: @"html"];
NSURL *htmlUrl = [NSURL fileURLWithPath:htmlPath];
[self.webView loadFileURL:htmlUrl allowingReadAccessToURL:[htmlUrl deletingLastPathComponent]];
}
- (void)loadRemoteURLWithUrlString:(NSString *)urlString {
NSURL *url = [NSURL URLWithString:urlString];
if (url) {
[self.webView loadRequest:[NSURLRequest requestWithURL:url]];
}
}
- (void)destroy {
// 移除消息监听
for (NSString *messageName in self.registeredMessageNames) {
[self.webView.configuration.userContentController removeScriptMessageHandlerForName:messageName];
}
// 销毁 webView
[self.webView stopLoading];
self.webView.uiDelegate = nil;
self.webView.navigationDelegate = nil;
[self.webView removeFromSuperview];
self.webView = nil;
self.delegate = nil;
}
#pragma mark - WKScriptMessageHandler
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
NSString *methodName = message.name;
NSDictionary *params = nil;
// 解析参数
if ([message.body isKindOfClass:[NSDictionary class]]) {
params = (NSDictionary *)message.body;
} else if ([message.body isKindOfClass:[NSString class]]) {
NSString *bodyStr = (NSString *)message.body;
NSData *jsonData = [bodyStr dataUsingEncoding:NSUTF8StringEncoding];
if (jsonData) {
params = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:nil];
}
}
// 回调给业务层
__weak typeof(self) weakSelf = self;
if ([self.delegate respondsToSelector:@selector(webViewDidReceiveJSCallWithMethodName:params:callback:)]) {
[self.delegate webViewDidReceiveJSCallWithMethodName:methodName params:params callback:^(id _Nullable response) {
// 回传结果给 JS
if (response) {
NSError *error = nil;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:response options:0 error:&error];
if (!error) {
NSString *jsonStr = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
NSString *jsCode = [NSString stringWithFormat:@"window.receiveNativeResponse('%@', %@)", methodName, jsonStr];
[weakSelf.webView evaluateJavaScript:jsCode completionHandler:nil];
}
}
}];
}
}
#pragma mark - WKUIDelegate + WKNavigationDelegate
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
NSLog(@"WKWebView 页面加载完成");
// 示例:主动调用 JS 传递设备型号
NSString *deviceModel = [UIDevice currentDevice].model;
[self callJSMethodWithMethodName:@"jsReceiveDeviceModel" params:@{@"deviceModel": deviceModel} completion:nil];
}
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提示" message:message preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
completionHandler();
}]];
[[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:alert animated:YES completion:nil];
}
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
decisionHandler(WKNavigationActionPolicyAllow);
}
@end
3. OC 版使用示例(业务层控制器)
#import "WKWebViewDemoVC.h"
#import "WKWebViewInteractionManager.h"
#import <MobileCoreServices/MobileCoreServices.h>
@interface WKWebViewDemoVC () <WKWebViewInteractionDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate>
@property (nonatomic, strong) WKWebView *webView;
@end
@implementation WKWebViewDemoVC
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
self.title = @"WKWebView 交互演示";
// 初始化 WKWebView
NSArray *messageNames = @[@"callCamera", @"showToast", @"submitUserName"];
self.webView = [[WKWebViewInteractionManager sharedManager] setupWebViewWithFrame:self.view.bounds
delegate:self
registerMessageNames:messageNames];
[self.view addSubview:self.webView];
// 加载本地 HTML
[[WKWebViewInteractionManager sharedManager] loadLocalHTMLWithFileName:@"index" fileType:@"html"];
}
#pragma mark - WKWebViewInteractionDelegate
- (void)webViewDidReceiveJSCallWithMethodName:(NSString *)methodName
params:(NSDictionary *)params
callback:(void (^)(id _Nullable))callback {
if ([methodName isEqualToString:@"callCamera"]) {
[self openCameraWithCallback:callback];
} else if ([methodName isEqualToString:@"showToast"]) {
NSString *msg = params[@"msg"];
if (!msg || msg.length == 0) {
callback(@{@"status": @"fail", @"msg": @"参数错误"});
return;
}
[self showToastWithMessage:msg];
callback(@{@"status": @"success", @"msg": @"提示显示成功"});
} else if ([methodName isEqualToString:@"submitUserName"]) {
NSString *userName = params[@"userName"];
if (!userName || userName.length == 0) {
callback(@{@"status": @"fail", @"msg": @"用户名不能为空"});
return;
}
BOOL isLegal = userName.length > 3;
NSString *result = isLegal ? @"用户名验证通过" : @"用户名长度不能小于4位";
callback(@{@"status": isLegal ? @"success" : @"fail", @"msg": result});
} else {
callback(@{@"status": @"fail", @"msg": @"未找到对应方法"});
}
}
- (void)openCameraWithCallback:(void (^)(id _Nullable))callback {
if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {
callback(@{@"status": @"fail", @"msg": @"相机不可用"});
return;
}
UIImagePickerController *picker = [[UIImagePickerController alloc] init];
picker.sourceType = UIImagePickerControllerSourceTypeCamera;
picker.delegate = self;
[self presentViewController:picker animated:YES completion:nil];
}
- (void)showToastWithMessage:(NSString *)message {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提示" message:message preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]];
[self presentViewController:alert animated:YES completion:nil];
}
#pragma mark - UIImagePickerControllerDelegate
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<UIImagePickerControllerInfoKey, id> *)info {
[picker dismissViewControllerAnimated:YES completion:nil];
// 模拟图片路径
NSString *imagePath = @"file:///var/mobile/Containers/Data/Application/xxx/photo.jpg";
// 回传图片路径给 JS
[[WKWebViewInteractionManager sharedManager] callJSMethodWithMethodName:@"jsReceiveImagePath" params:@{@"imagePath": imagePath} completion:nil];
}
- (void)dealloc {
[[WKWebViewInteractionManager sharedManager] destroy];
NSLog(@"WKWebViewDemoVC 销毁,资源释放完成");
}
@end
四、JS 端配合代码(通用版,适配 Swift/OC 封装类)
JS 端需与 Native 约定统一的方法名、参数格式,以下是配合上述封装类的完整 JS 代码(index.html),可直接复用:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>WKWebView 与 JS 交互测试</title>
<style>
button { padding: 10px 20px; margin: 10px; font-size: 16px; }
#imageContainer { margin: 20px 0; width: 300px; height: 300px; border: 1px solid #ccc; }
.result { margin-top: 10px; font-size: 14px; color: #333; }
</style>
</head>
<body>
<h1>WKWebView 与 JS 交互测试</h1>
<!-- JS 调用 Native 相机 -->
<button onclick="callNativeCamera()">调用原生相机</button>
<div id="cameraResult" class="result"></div>
<div id="imageContainer"></div>
<!-- JS 调用 Native 提示 -->
<button onclick="callNativeToast()">调用原生提示</button>
<div id="toastResult" class="result"></div>
<!-- JS 调用 Native 验证用户名 -->
<input type="text" id="userName" placeholder="请输入用户名" style="padding: 10px; font-size: 16px;">
<button onclick="submitUserName()">提交用户名</button>
<div id="userNameResult" class="result"></div>
<!-- 显示 Native 主动传递的设备型号 -->
<div class="result">设备型号:<span id="deviceModel"></span></div>
<script>
// 1. 接收 Native 回传的结果(与 Native 封装类约定的回调方法)
function receiveNativeResponse(methodName, response) {
switch (methodName) {
case "callCamera":
document.getElementById("cameraResult").innerText = response.msg;
break;
case "showToast":
document.getElementById("toastResult").innerText = response.msg;
break;
case "submitUserName":
document.getElementById("userNameResult").innerText = response.msg;
break;
default:
break;
}
}
// 2. JS 调用 Native 相机(方法名与 Native 注册的一致)
function callNativeCamera() {
// 传递空参数(如需传递参数,直接传入 JSON 对象)
window.webkit.messageHandlers.callCamera.postMessage(null);
}
// 3. JS 调用 Native 提示(传递参数)
function callNativeToast() {
const params = { msg: "我是 JS 传递给 Native 的提示信息" };
window.webkit.messageHandlers.showToast.postMessage(params);
}
// 4. JS 调用 Native 验证用户名(传递参数)
function submitUserName() {
const userName = document.getElementById("userName").value;
const params = { userName: userName };
window.webkit.messageHandlers.submitUserName.postMessage(params);
}
// 5. 接收 Native 主动传递的设备型号(方法名与 Native 调用的一致)
function jsReceiveDeviceModel(params) {
document.getElementById("deviceModel").innerText = params.deviceModel;
}
// 6. 接收 Native 回传的图片路径(方法名与 Native 调用的一致)
function jsReceiveImagePath(params) {
const imageContainer = document.getElementById("imageContainer");
imageContainer.innerHTML = `<img src="${params.imagePath}" width="300" height="300" />`;
}
</script>
</body>
</html>
五、WKWebView 交互避坑指南(最新版)
基于最新 WKWebView 特性,结合封装类的使用场景,整理 5 个高频坑及解决方案,避免开发踩坑。
坑 1:JS 调用 Native 无响应
原因及解决方案:
- 消息名不匹配:JS 调用的 messageName 必须与 Native 注册的完全一致(大小写敏感);
- 未开启 JS 执行:封装类已默认开启(config.preferences.javaScriptEnabled = true),无需额外配置;
- 跨域限制:远程 H5 需在 Native 端配置允许跨域(封装类已实现 decisionHandler(.allow));
- 页面未加载完成:JS 调用需在页面加载完成(didFinishNavigation)后执行,或在 JS 中判断页面加载状态。
坑 2:参数传递失败(解析报错)
原因及解决方案:
- 参数格式错误:JS 传递参数必须是 JSON 可序列化类型(对象、数组、字符串等),禁止传递函数、Date;
- 参数解析失败:封装类已处理参数解析(字符串转 JSON、JSON 转字典),JS 只需传递标准 JSON 格式即可;
- 空参数处理:JS 无参数时,传递 null,避免传递 undefined(Native 解析会报错)。
坑 3:内存泄漏(WKWebView 无法释放)
原因及解决方案:
- 未移除消息监听:封装类在 destroy 方法中统一移除所有注册的消息,业务层只需在页面销毁时调用 destroy 即可;
- 循环引用:WKWebView 的代理(uiDelegate、navigationDelegate)已用 weak 修饰,避免与控制器形成循环引用;
- 单例持有:封装类为单例,销毁时需置空 webView 和 delegate,避免内存占用。
坑 4:Native 调用 JS 时,JS 方法未定义
原因及解决方案:
- JS 方法未加载完成:Native 调用 JS 需在页面加载完成后执行(封装类在 didFinishNavigation 中调用示例);
- JS 方法作用域错误:确保 JS 方法定义在全局作用域(window 下),避免定义在局部函数中。
坑 5:WKWebView 加载本地 HTML 无法访问本地资源
原因:WKWebView 加载本地 HTML 时,默认限制访问本地资源,需配置允许访问。
解决方案:封装类已配置 config.setValue(true, forKey: "allowUniversalAccessFromFileURLs"),同时加载本地 HTML 时,调用 loadFileURL:allowingReadAccessToURL: 方法(已在封装类中实现)。
六、最佳实践(最新版)
- 交互规范:与 JS 约定统一的方法名(camelCase 命名)、参数格式(JSON)、回调格式(统一包含 status 和 msg),避免混乱;
- 封装复用:直接使用本文封装的 WKWebViewInteractionManager,无需重复编写初始化、消息注册等代码,降低开发成本;
- 安全性:限制 H5 来源(仅允许官方 H5 调用 Native 方法),对 JS 传递的关键参数(如支付金额、用户信息)做校验;
- 容错处理:Native 调用 JS 时,捕获错误并处理;JS 调用 Native 时,Native 做参数非空校验,避免崩溃;
- 性能优化:减少频繁交互,复杂数据传递时用 JSON 压缩,Native 调用 JS 时避免在主线程执行复杂 JS 代码。
七、总结
目前 iOS 混合开发中,WKWebView 是唯一主流的 Web 容器,其与 JS 交互的核心是 WKScriptMessageHandler(JS 调用 Native)和 evaluateJavaScript(Native 调用 JS)。本文封装的 WKWebView 交互类(Swift/OC 双版本),已集成所有核心功能,可直接复制到项目中使用,兼顾易用性、可扩展性和安全性。