由于 WKWebView 的安全沙箱机制,我们无法直接从 Objective/Swift 层访问网页的 DOM 树,所以必须通过 JavaScript 作为桥梁。

下面我将分步详细讲解,并提供完整的 Swift 代码示例。


核心原理

  1. 注入 JavaScript:使用 WKUserScript 将一段获取 HTML 的 JavaScript 代码注入到 WKWebView 中,这段代码会在网页加载完成后执行。
  2. 执行 JavaScript:通过 evaluateJavaScript:completionHandler: 方法,在当前网页的上下文中执行这段 JavaScript。
  3. 获取结果:JavaScript 执行完成后,会将页面的 HTML 字符串作为返回值,通过 completionHandler 回调传递给 iOS 代码。
  4. 处理字符串:在 iOS 代码中,你就可以对这个 HTML 字符串进行解析、显示或保存了。

步骤详解 (以 Swift 为例)

第 1 步:创建 WKWebView 和配置控制器

创建一个 WKWebView 和它的 WKWebViewConfiguration,为了安全起见,我们最好只允许在特定的时机(比如用户点击一个按钮时)执行获取源码的脚本,而不是一加载页面就执行。

import UIKit
import WebKit
class ViewController: UIViewController {
    var webView: WKWebView!
    var sourceCodeTextView: UITextView! // 用于显示源码的文本视图
    override func viewDidLoad() {
        super.viewDidLoad()
        // 1. 配置 WKWebView
        let webConfiguration = WKWebViewConfiguration()
        webView = WKWebView(frame: .zero, configuration: webConfiguration)
        webView.navigationDelegate = self
        view.addSubview(webView)
        // 2. 设置约束
        webView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            webView.heightAnchor.constraint(equalToConstant: 300) // 给 webView 一个固定高度
        ])
        // 3. 添加一个按钮来触发获取源码
        let getSourceButton = UIButton(type: .system)
        getSourceButton.setTitle("查看网页源代码", for: .normal)
        getSourceButton.addTarget(self, action: #selector(getSourceCode), for: .touchUpInside)
        view.addSubview(getSourceButton)
        getSourceButton.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            getSourceButton.topAnchor.constraint(equalTo: webView.bottomAnchor, constant: 20),
            getSourceButton.centerXAnchor.constraint(equalTo: view.centerXAnchor)
        ])
        // 4. 添加一个 UITextView 来显示源码
        sourceCodeTextView = UITextView()
        sourceCodeTextView.isEditable = false
        sourceCodeTextView.font = UIFont.systemFont(ofSize: 10) // 源码通常用小字体显示
        view.addSubview(sourceCodeTextView)
        sourceCodeTextView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            sourceCodeTextView.topAnchor.constraint(equalTo: getSourceButton.bottomAnchor, constant: 20),
            sourceCodeTextView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            sourceCodeTextView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            sourceCodeTextView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
        ])
        // 5. 加载一个网页
        if let url = URL(string: "https://www.apple.com") {
            let request = URLRequest(url: url)
            webView.load(request)
        }
    }
    @objc func getSourceCode() {
        // 这个方法将在按钮点击时调用
    }
}

第 2 步:编写获取 HTML 的 JavaScript 函数

这个 JavaScript 函数非常简单,它直接返回 document.documentElement.outerHTMLdocument.documentElement 代表 HTML 文档的根元素 <html>,而 outerHTML 属性会返回该元素及其所有后代的 HTML 代码。

function getPageSource() {
    return document.documentElement.outerHTML;
}

第 3 步:在 Swift 中执行 JavaScript 并处理结果

我们将 getSourceCode 方法实现完整,它将调用 evaluateJavaScript 来执行我们上面定义的 JavaScript 函数。

extension ViewController: WKNavigationDelegate {
    // MARK: - WKNavigationDelegate
    // 当网页开始加载时,可以清空源码显示区域
    func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
        sourceCodeTextView.text = "正在加载网页..."
    }
    // 当网页加载完成时,可以恢复按钮状态等
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        sourceCodeTextView.text = "网页加载完成,点击按钮查看源码。"
    }
}
// 在 ViewController 类中
@objc func getSourceCode() {
    print("正在获取网页源码...")
    // 执行 JavaScript 代码
    webView.evaluateJavaScript("document.documentElement.outerHTML") { (result, error) in
        // 在主线程更新 UI
        DispatchQueue.main.async {
            if let error = error {
                // 如果执行出错,显示错误信息
                self.sourceCodeTextView.text = "获取源码失败: \(error.localizedDescription)"
                print("获取源码失败: \(error.localizedDescription)")
            } else if let htmlString = result as? String {
                // 成功获取到 HTML 字符串
                print("成功获取到源码,长度: \(htmlString.count)")
                // 将源码显示在 UITextView 中
                self.sourceCodeTextView.text = htmlString
            } else {
                // 其他未知情况
                self.sourceCodeTextView.text = "获取源码失败,未知错误。"
                print("获取源码失败,返回结果为空或类型不正确。")
            }
        }
    }
}

完整代码 (Swift)

将以上所有部分组合起来,你就可以得到一个完整可用的示例。

import UIKit
import WebKit
class ViewController: UIViewController {
    var webView: WKWebView!
    var sourceCodeTextView: UITextView!
    override func viewDidLoad() {
        super.viewDidLoad()
        setupViews()
        loadWebPage()
    }
    private func setupViews() {
        // 1. 配置 WKWebView
        let webConfiguration = WKWebViewConfiguration()
        webView = WKWebView(frame: .zero, configuration: webConfiguration)
        webView.navigationDelegate = self
        view.addSubview(webView)
        // 2. 设置约束
        webView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            webView.heightAnchor.constraint(equalToConstant: 300)
        ])
        // 3. 添加一个按钮来触发获取源码
        let getSourceButton = UIButton(type: .system)
        getSourceButton.setTitle("查看网页源代码", for: .normal)
        getSourceButton.setTitleColor(.systemBlue, for: .normal)
        getSourceButton.addTarget(self, action: #selector(getSourceCode), for: .touchUpInside)
        view.addSubview(getSourceButton)
        getSourceButton.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            getSourceButton.topAnchor.constraint(equalTo: webView.bottomAnchor, constant: 20),
            getSourceButton.centerXAnchor.constraint(equalTo: view.centerXAnchor)
        ])
        // 4. 添加一个 UITextView 来显示源码
        sourceCodeTextView = UITextView()
        sourceCodeTextView.isEditable = false
        sourceCodeTextView.font = UIFont(name: "Courier", size: 8) // 使用等宽字体更好看
        sourceCodeTextView.layer.borderColor = UIColor.gray.cgColor
        sourceCodeTextView.layer.borderWidth = 1.0
        view.addSubview(sourceCodeTextView)
        sourceCodeTextView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            sourceCodeTextView.topAnchor.constraint(equalTo: getSourceButton.bottomAnchor, constant: 20),
            sourceCodeTextView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10),
            sourceCodeTextView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10),
            sourceCodeTextView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -10)
        ])
    }
    private func loadWebPage() {
        if let url = URL(string: "https://www.apple.com") {
            let request = URLRequest(url: url)
            webView.load(request)
        }
    }
    @objc func getSourceCode() {
        print("正在获取网页源码...")
        // 执行 JavaScript 代码获取整个页面的 HTML
        webView.evaluateJavaScript("document.documentElement.outerHTML") { (result, error) in
            // 必须在主线程更新 UI
            DispatchQueue.main.async {
                if let error = error {
                    self.sourceCodeTextView.text = "获取源码失败: \(error.localizedDescription)"
                    print("获取源码失败: \(error.localizedDescription)")
                } else if let htmlString = result as? String {
                    print("成功获取到源码,长度: \(htmlString.count)")
                    self.sourceCodeTextView.text = htmlString
                } else {
                    self.sourceCodeTextView.text = "获取源码失败,返回结果为空或类型不正确。"
                    print("获取源码失败,返回结果为空或类型不正确。")
                }
            }
        }
    }
}
// MARK: - WKNavigationDelegate
extension ViewController: WKNavigationDelegate {
    func webView(_ webView