iOS 中 WKWebView 与 JS 交互深度解析 & 封装

6 阅读15分钟

在 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 双版本),已集成所有核心功能,可直接复制到项目中使用,兼顾易用性、可扩展性和安全性。