WebView 是 Android 嵌入網頁的核心元件。本文深入解析 WebViewClient 如何攔截 URL 跳轉、處理自定義 Scheme(如 myapp://)、配合 shouldOverrideUrlLoading() 實現原生頁面跳轉、JS 交互與安全防護,附完整最佳實踐程式碼。
此方法在 WebView 即將載入新 URL 時回調,**回傳 true 表示攔截並自行處理,false 交由 WebView 繼續載入**。
webView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
String url = request.getUrl().toString();
return handleUrl(url);
}
// 相容 API 23 以下
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
return handleUrl(url);
}
private boolean handleUrl(String url) {
if (url.startsWith("myapp://")) {
handleCustomScheme(url);
return true; // 攔截
}
if (url.startsWith("tel:") || url.startsWith("mailto:")) {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
return true;
}
// 其餘 http/https 讓 WebView 自行載入
return false;
}
});
定義協定格式:myapp://action?param1=value1¶m2=value2
private void handleCustomScheme(String url) {
Uri uri = Uri.parse(url);
String action = uri.getHost(); // action 部分
String param1 = uri.getQueryParameter("param1");
switch (action) {
case "openDetail":
String id = uri.getQueryParameter("id");
startActivity(new Intent(this, DetailActivity.class).putExtra("id", id));
break;
case "login":
startActivity(new Intent(this, LoginActivity.class));
break;
case "pay":
// 唤起支付流程
break;
default:
Log.w(TAG, "未知 Scheme action: " + action);
}
}
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" android:host="openDetail" />
</intent-filter>
</activity>
onPageStarted() 記錄主框架 URL,防止 iframe 注入惡意跳轉。setAllowFileAccess(false)、setAllowUniversalAccessFromFileURLs(false)。@JavascriptInterface 並針對 Android 4.2+ 加上註解。public class SafeWebViewClient extends WebViewClient {
private static final Set<String> ALLOWED_DOMAINS = Set.of("example.com", "api.example.com");
private final Context context;
public SafeWebViewClient(Context context) { this.context = context; }
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
String url = request.getUrl().toString();
return handleNavigation(url);
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
return handleNavigation(url);
}
private boolean handleNavigation(String url) {
// 1. 自定義 Scheme
if (url.startsWith("myapp://")) { handleCustomScheme(url); return true; }
// 2. 系統 Scheme
if (url.startsWith("tel:") || url.startsWith("mailto:") || url.startsWith("intent:")) {
try { context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url))); } catch (Exception ignored) {}
return true;
}
// 3. 域名白名單
Uri uri = Uri.parse(url);
if (!ALLOWED_DOMAINS.contains(uri.getHost())) {
// 非白名單域名,用外部瀏覽器開啟
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
return true;
}
// 4. 允許 WebView 載入
return false;
}
private void handleCustomScheme(String url) { /* 同上 */ }
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
// 生產環境嚴禁 handler.proceed(),應顯示錯誤頁或取消載入
handler.cancel();
showSslErrorDialog();
}
}
| 問題 | 原因 | 解決 |
|---|---|---|
| shouldOverrideUrlLoading 不觸發 | 未設定 WebViewClient 或 API 版本不匹配 | 同時覆寫兩個版本的方法 |
| 頁面跳轉後無法返回 | 未將歷史記錄加入 WebView | 攔截前呼叫 view.loadUrl(url) 而非 startActivity |
| JS 無法呼叫原生 | 未加 @JavascriptInterface 或 ProGuard 混淆 | 加註解 + keep 規則 |
| SSL 錯誤頁面空白 | onReceivedSslError 預設取消載入 | 實作自定義錯誤頁或引導用戶 |
掌握 WebViewClient 的跳轉攔截機制,能構建安全、流暢的混合式 App 體驗。