iOS开发基础98-WebView 拦截 URL Scheme :以淘宝跳转为例
iOS WebView 拦截 URL Scheme 完全指南:以淘宝跳转为例
在 App 内用 WebView 加载网页时,经常需要跳转到第三方 App(如淘宝、京东、微信)。本文以"WebView 加载淘宝商品页,点击评论跳转淘宝客户端"为例,从 URL Scheme 原理讲起,详细讲解 Info.plist 白名单配置、WKWebView 拦截逻辑、openURL 方法变化、淘宝未安装的降级处理等关键细节,给出完整 OC 代码、Swift 版本对照和常见坑排查。
一、概述
一句话原理
URL Scheme 是 App 的"门牌号",每个 App 可以注册一个自定义协议(如 tbopen://),其他 App 通过这个协议就能打开它。WebView 拦截到这种协议的 URL 后,调用系统 API 打开对应 App,就实现了跳转。
实现目标
- App 内用 WKWebView 加载淘宝商品详情页。
- 用户点击"查看评论"等操作时,跳转到淘宝客户端查看详情。
- 淘宝未安装时,有降级处理(提示下载或继续在 WebView 浏览)。
二、核心原理
1. URL Scheme 是什么
URL Scheme 是 iOS 提供的 App 间通信机制:
- 每个 App 可以在 Info.plist 中注册自己的 URL Scheme(如淘宝注册了
tbopen、taobao)。 - 其他 App 用
[[UIApplication sharedApplication] openURL:url]就能打开对应 App。 - 常见 Scheme:
tbopen://(淘宝)、taobao://(淘宝)、tmall://(天猫)、openapp.jdmobile://(京东)、weixin://(微信)。
2. 白名单机制(LSApplicationQueriesSchemes)
iOS 9 引入了 URL Scheme 白名单机制:
canOpenURL:方法只能返回白名单内 Scheme 的结果。- 不在白名单中的 Scheme,
canOpenURL:一律返回 NO。 - 白名单最多 50 个 Scheme。
- 目的:防止 App 扫描用户设备上安装了哪些 App(隐私保护)。
注意:白名单只影响
canOpenURL:的查询结果,openURL:本身不受白名单限制(但系统会有弹窗提示)。
3. WKWebView 拦截机制
WKWebView 的 decidePolicyForNavigationAction: 代理方法会在每次导航发生前调用:
- 可以获取当前请求的 URL。
- 通过
decisionHandler决定允许(Allow)还是取消(Cancel)加载。 - 拦截到特定 Scheme 后,取消 WebView 加载,转而用
openURL打开第三方 App。
关键:
decisionHandler必须调用且只能调用一次,不调用会导致 App 崩溃。
三、实现步骤
第一步:Info.plist 配置白名单
在 Info.plist 中添加 LSApplicationQueriesSchemes,声明要查询的 Scheme:
<key>LSApplicationQueriesSchemes</key>
<array>
<string>tbopen</string>
<string>taobao</string>
<string>tmall</string>
<!-- 其他需要的 Scheme -->
</array>
图形化操作:Info.plist → 右键 → Add Row → 输入 LSApplicationQueriesSchemes → 类型选 Array → 添加 item。
第二步:拦截 URL 并跳转
在 WKWebView 的导航代理中拦截 tbopen 开头的 URL:
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
NSURL *url = navigationAction.request.URL;
NSString *scheme = url.scheme.lowercaseString;
// 拦截 tbopen / taobao 等淘宝 Scheme
if ([scheme isEqualToString:@"tbopen"] || [scheme isEqualToString:@"taobao"]) {
// 取消 WebView 加载
decisionHandler(WKNavigationActionPolicyCancel);
// 检查是否安装了淘宝
if ([[UIApplication sharedApplication] canOpenURL:url]) {
// 打开淘宝客户端(iOS 10+ 推荐用法)
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:^(BOOL success) {
if (!success) {
NSLog(@"打开淘宝失败");
}
}];
} else {
// 淘宝未安装,提示用户或降级处理
[self showAlert:@"未安装淘宝" message:@"是否前往 App Store 下载?"];
}
return;
}
// 其他 URL 正常加载
decisionHandler(WKNavigationActionPolicyAllow);
}
四、完整代码示例(OC)
// WebViewController.h
#import <UIKit/UIKit.h>
@interface WebViewController : UIViewController
@property (nonatomic, copy) NSString *urlString;
@end
// WebViewController.m
#import "WebViewController.h"
#import <WebKit/WebKit.h>
@interface WebViewController () <WKNavigationDelegate, WKUIDelegate>
@property (nonatomic, strong) WKWebView *webView;
@property (nonatomic, strong) UIProgressView *progressView;
@property (nonatomic, strong) UIBarButtonItem *backButton;
@property (nonatomic, strong) UIBarButtonItem *closeButton;
@end
@implementation WebViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
[self setupNavigationBar];
[self setupWebView];
[self loadURL];
}
#pragma mark - 设置导航栏
- (void)setupNavigationBar {
self.backButton = [[UIBarButtonItem alloc] initWithTitle:@"返回"
style:UIBarButtonItemStylePlain
target:self
action:@selector(backTapped)];
self.closeButton = [[UIBarButtonItem alloc] initWithTitle:@"关闭"
style:UIBarButtonItemStylePlain
target:self
action:@selector(closeTapped)];
self.navigationItem.leftBarButtonItems = @[self.backButton, self.closeButton];
}
- (void)backTapped {
if (self.webView.canGoBack) {
[self.webView goBack];
} else {
[self closeTapped];
}
}
- (void)closeTapped {
[self.navigationController popViewControllerAnimated:YES];
}
#pragma mark - 设置 WebView
- (void)setupWebView {
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
self.webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:config];
self.webView.navigationDelegate = self;
self.webView.UIDelegate = self;
self.webView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:self.webView];
// 进度条
self.progressView = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleDefault];
self.progressView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:self.progressView];
// 约束
UILayoutGuide *guide = self.view.safeAreaLayoutGuide;
[NSLayoutConstraint activateConstraints:@[
[self.webView.topAnchor constraintEqualToAnchor:guide.topAnchor],
[self.webView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[self.webView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[self.webView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
[self.progressView.topAnchor constraintEqualToAnchor:guide.topAnchor],
[self.progressView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[self.progressView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[self.progressView.heightAnchor constraintEqualToConstant:2]
]];
// KVO 监听加载进度
[self.webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"estimatedProgress"]) {
self.progressView.progress = self.webView.estimatedProgress;
self.progressView.hidden = (self.webView.estimatedProgress >= 1.0);
}
}
#pragma mark - 加载 URL
- (void)loadURL {
if (!self.urlString) return;
NSURL *url = [NSURL URLWithString:self.urlString];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[self.webView loadRequest:request];
}
#pragma mark - WKNavigationDelegate
// 核心:拦截 URL Scheme
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
NSURL *url = navigationAction.request.URL;
NSString *scheme = url.scheme.lowercaseString;
// 需要拦截的第三方 App Scheme 列表
NSArray *interceptSchemes = @[@"tbopen", @"taobao", @"tmall", @"openapp.jdmobile"];
if ([interceptSchemes containsObject:scheme]) {
// 1. 取消 WebView 加载(必须)
decisionHandler(WKNavigationActionPolicyCancel);
// 2. 尝试打开第三方 App
[self openThirdPartyApp:url];
return;
}
// 其他 URL 正常加载
decisionHandler(WKNavigationActionPolicyAllow);
}
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation {
self.progressView.hidden = NO;
}
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
self.progressView.hidden = YES;
}
- (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error {
self.progressView.hidden = YES;
NSLog(@"加载失败:%@", error.localizedDescription);
}
#pragma mark - 打开第三方 App
- (void)openThirdPartyApp:(NSURL *)url {
if ([[UIApplication sharedApplication] canOpenURL:url]) {
// iOS 10+ 推荐用法
if (@available(iOS 10.0, *)) {
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:^(BOOL success) {
if (!success) {
[self showOpenFailedAlert];
}
}];
} else {
// iOS 9 及以下
[[UIApplication sharedApplication] openURL:url];
}
} else {
// 未安装对应 App
[self showNotInstalledAlert:url];
}
}
- (void)showNotInstalledAlert:(NSURL *)url {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提示"
message:@"未安装对应客户端,是否前往 App Store 下载?"
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil]];
[alert addAction:[UIAlertAction actionWithTitle:@"去下载" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
// 跳转到 App Store(这里以淘宝为例)
NSURL *appStoreURL = [NSURL URLWithString:@"https://itunes.apple.com/app/id387682726"];
[[UIApplication sharedApplication] openURL:appStoreURL options:@{} completionHandler:nil];
}]];
[self presentViewController:alert animated:YES completion:nil];
}
- (void)showOpenFailedAlert {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提示"
message:@"打开失败,请稍后重试"
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]];
[self presentViewController:alert animated:YES completion:nil];
}
#pragma mark - 生命周期
- (void)dealloc {
[self.webView removeObserver:self forKeyPath:@"estimatedProgress"];
NSLog(@"WebViewController dealloc");
}
@end
使用方式
WebViewController *webVC = [[WebViewController alloc] init];
webVC.urlString = @"https://detail.taobao.com/item.htm?id=XXXXXX";
[self.navigationController pushViewController:webVC animated:YES];
五、关键细节与坑
1. decisionHandler 必须调用
常见错误:拦截 URL 后只调用了 openURL:,忘记调用 decisionHandler,导致 App 崩溃。
// 错误写法:没有调用 decisionHandler
if ([scheme isEqualToString:@"tbopen"]) {
[[UIApplication sharedApplication] openURL:url];
// 没有调用 decisionHandler!会崩溃
}
// 正确写法:拦截后调用 Cancel
if ([scheme isEqualToString:@"tbopen"]) {
decisionHandler(WKNavigationActionPolicyCancel); // 必须调用
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
return;
}
decisionHandler必须调用且只能调用一次,传Cancel表示取消这次导航,传Allow表示允许继续加载。
2. openURL 方法的变化
| 方法 | 系统版本 | 说明 |
|---|---|---|
openURL: |
iOS 2~9 | 旧方法,iOS 10 起废弃 |
openURL:options:completionHandler: |
iOS 10+ | 推荐,有完成回调 |
// 推荐写法(iOS 10+)
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:^(BOOL success) {
if (success) {
NSLog(@"打开成功");
} else {
NSLog(@"打开失败");
}
}];
3. Scheme 匹配要精确
常见错误:用 containsString:@"tbopen" 匹配 scheme,可能误匹配其他 URL。
// 不推荐:containsString 可能误匹配
if ([scheme containsString:@"tbopen"]) { ... }
// 推荐:精确匹配或前缀匹配
if ([scheme isEqualToString:@"tbopen"] || [scheme isEqualToString:@"taobao"]) { ... }
4. 淘宝未安装的降级处理
canOpenURL: 返回 NO 时(未安装淘宝),不能什么都不做,应该:
- 提示用户未安装,引导去 App Store 下载。
- 或者降级为在 WebView 中继续浏览(但 tbopen 协议 WebView 无法加载,需要转换成 https 链接)。
5. 白名单只影响 canOpenURL
LSApplicationQueriesSchemes 只限制 canOpenURL: 的查询结果,不限制 openURL:。但如果不把 scheme 加入白名单,canOpenURL: 永远返回 NO,就无法判断是否安装了对应 App。
6. 拦截 target="_blank" 的链接
有些网页链接是 target="_blank"(新窗口打开),WKWebView 默认不处理,需要实现 WKUIDelegate:
- (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures {
// 新窗口请求,直接在当前 WebView 加载
if (!navigationAction.targetFrame.isMainFrame) {
[webView loadRequest:navigationAction.request];
}
return nil;
}
六、扩展:其他电商 App 的 URL Scheme
| App | URL Scheme | 示例 |
|---|---|---|
| 淘宝 | tbopen:// / taobao:// |
tbopen://item.taobao.com/item.htm?id=xxx |
| 天猫 | tmall:// |
tmall://item.tmall.com/item.htm?id=xxx |
| 京东 | openapp.jdmobile:// |
openapp.jdmobile://virtual?params=... |
| 拼多多 | pinduoduo:// |
pinduoduo://goods.html?goods_id=xxx |
| 微信 | weixin:// |
weixin://dl/business/?t=xxx |
| 支付宝 | alipays:// |
alipays://platformapi/startapp?appId=xxx |
不同 App 的 Scheme 格式不同,需要参考各自的开放平台文档。
七、扩展:Universal Links(通用链接)
一句话原理
Universal Links 是 iOS 9+ 推出的更优雅的跳转方式,用普通的 https 链接就能打开 App,不需要配置 Scheme 白名单,用户没装 App 时自动在 Safari 中打开网页。
与 URL Scheme 的对比
| 对比 | URL Scheme | Universal Links |
|---|---|---|
| 协议 | 自定义协议(tbopen://) | 标准 https 链接 |
| 白名单 | 需要配置 | 不需要 |
| 未安装 App | 无反应,需要自己处理 | 自动在 Safari 打开网页 |
| 体验 | 会弹窗确认 | 直接跳转,无弹窗 |
| 配置 | 简单(Info.plist) | 需要服务端配置 apple-app-site-association 文件 |
淘宝等大厂通常同时支持 URL Scheme 和 Universal Links。如果 WebView 中加载的是淘宝的 https 链接,且开启了 Universal Links,系统会自动尝试打开淘宝 App,不需要手动拦截。
八、Swift 版本对照
import UIKit
import WebKit
class WebViewController: UIViewController, WKNavigationDelegate, WKUIDelegate {
var urlString: String?
private var webView: WKWebView!
private var progressView: UIProgressView!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setupWebView()
loadURL()
}
private func setupWebView() {
let config = WKWebViewConfiguration()
webView = WKWebView(frame: .zero, configuration: config)
webView.navigationDelegate = self
webView.uiDelegate = self
webView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(webView)
progressView = UIProgressView(progressViewStyle: .default)
progressView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(progressView)
NSLayoutConstraint.activate([
webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
webView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
progressView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
progressView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
progressView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
progressView.heightAnchor.constraint(equalToConstant: 2)
])
webView.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil)
}
private func loadURL() {
guard let urlString = urlString, let url = URL(string: urlString) else { return }
webView.load(URLRequest(url: url))
}
// MARK: - 拦截 URL Scheme
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
guard let url = navigationAction.request.url,
let scheme = url.scheme?.lowercased() else {
decisionHandler(.allow)
return
}
let interceptSchemes = ["tbopen", "taobao", "tmall", "openapp.jdmobile"]
if interceptSchemes.contains(scheme) {
decisionHandler(.cancel) // 必须调用
openThirdPartyApp(url)
return
}
decisionHandler(.allow)
}
private func openThirdPartyApp(_ url: URL) {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:]) { success in
if !success {
print("打开失败")
}
}
} else {
showNotInstalledAlert()
}
}
private func showNotInstalledAlert() {
let alert = UIAlertController(title: "提示", message: "未安装对应客户端", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "确定", style: .default))
present(alert, animated: true)
}
// MARK: - KVO
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "estimatedProgress" {
progressView.progress = Float(webView.estimatedProgress)
progressView.isHidden = webView.estimatedProgress >= 1.0
}
}
deinit {
webView.removeObserver(self, forKeyPath: "estimatedProgress")
}
}
九、常见问题与坑
Q1:拦截 URL 后 App 崩溃了?
大概率是没有调用 decisionHandler。decidePolicyForNavigationAction: 中必须调用 decisionHandler 且只能调用一次。拦截后传 Cancel,不拦截传 Allow。
Q2:canOpenURL 一直返回 NO?
检查 Info.plist 中是否添加了 LSApplicationQueriesSchemes 白名单。iOS 9+ 不在白名单中的 Scheme,canOpenURL: 一律返回 NO。
Q3:openURL 后没有反应?
- 检查设备是否安装了淘宝客户端。
- 检查 URL 是否正确(tbopen:// 格式是否合法)。
- iOS 10+ 用
openURL:options:completionHandler:,看 completionHandler 的 success 值。 - 某些 Scheme 需要特定参数才能正确跳转。
Q4:淘宝的 URL Scheme 格式是什么?
淘宝常用的 Scheme:
tbopen://通用跳转,后面跟网页 URL,如tbopen://item.taobao.com/item.htm?id=xxxtaobao://直接跳转,如taobao://item.taobao.com/item.htm?id=xxx
具体格式可能随淘宝版本变化,建议参考淘宝开放平台文档。
Q5:WKWebView 和 UIWebView 怎么选?
- WKWebView:iOS 8+,性能好,内存占用低,是苹果推荐的 WebView。
- UIWebView:iOS 2~12,已废弃,iOS 12 后不推荐使用,审核可能被拒。
- 新项目一律用 WKWebView。
Q6:ATS(App Transport Security)需要配置吗?
iOS 9+ 默认禁止 HTTP 请求。淘宝商品页是 HTTPS,一般不需要额外配置。如果加载的是 HTTP 页面,需要在 Info.plist 中配置 NSAppTransportSecurity:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
不建议全局开放 HTTP,最好用
NSExceptionDomains针对特定域名开放。
Q7:怎么拦截 JavaScript 调用原生?
用 WKScriptMessageHandler:
- 配置
WKUserContentController,添加脚本消息处理器。 - 实现
userContentController:didReceiveScriptMessage:。 - 网页用
window.webkit.messageHandlers.xxx.postMessage(data)调用原生。
Q8:WebView 中的视频无法全屏播放?
检查 WKWebViewConfiguration 的 allowsInlineMediaPlayback 属性,设为 YES 允许内嵌播放,设为 NO 会自动全屏。
Q9:返回按钮怎么处理?
- WebView 有历史记录时(
canGoBack),调用goBack返回上一页。 - 没有历史记录时,关闭 WebViewController。
- 建议导航栏放"返回"和"关闭"两个按钮。
Q10:这种跳转方式苹果审核会拒吗?
正常使用 URL Scheme 跳转不会被拒。但要注意:
- 不要用私有 API。
- 白名单不要加太多无关的 Scheme(最多 50 个,且苹果可能问为什么需要这么多)。
- 不要用 URL Scheme 做恶意引导(如强制跳转其他 App)。
- 淘宝客/广告类 App 要遵守苹果的广告规范。
十、总结
- 核心原理:URL Scheme 是 App 的"门牌号",WebView 拦截特定 Scheme 后调用
openURL打开第三方 App。 - 实现步骤:
- Info.plist 配置
LSApplicationQueriesSchemes白名单。 - WKWebView 的
decidePolicyForNavigationAction:中拦截 URL。 - 拦截后调用
decisionHandler(.cancel)+openURL。 - 未安装时降级处理(提示下载或继续浏览)。
- Info.plist 配置
- 关键坑:
decisionHandler必须调用且只能调用一次。- iOS 10+ 用
openURL:options:completionHandler:。 - Scheme 精确匹配,不要用 containsString。
- 白名单只影响
canOpenURL,不影响openURL。
- 扩展:其他电商 Scheme(天猫、京东、拼多多)、Universal Links(更优雅的 https 跳转)、JavaScript 与原生交互。
- 适用范围:不仅适用于淘宝,任何支持 URL Scheme 的第三方 App 都可以用这种方式跳转。

浙公网安备 33010602011771号