Skip to content
Go back

用 Capacitor 将 Web 打包成 APP

Updated:
Edit

1. 创建项目并安装依赖

npm init @capacitor/app@latest
cd my-app
npm install

2. 安装依赖

本文以 Android 为例,iOS 操作查看文档

npm install @capacitor/android
npx cap add android

3. 配置 APP 启动加载地址

在 capacitor.config.json 中增加 server.url 配置,并将 plugins.SplashScreen.launchAutoHide 改为 true。更多配置查看文档

修改后的配置:

{
  "appId": "com.deepseek.chat",
  "appName": "DeepSeek",
  "webDir": "dist",
  "server": {
    "url": "https://chat.deepseek.com"
  },
  "plugins": {
    "SplashScreen": {
      "launchAutoHide": true
    }
  }
}

4. 编译 APP

npm run build
npx cap sync android

5. 运行 APP

本文以 Android 为例,iOS 操作查看文档

用 Android Studio 打开项目下的 android 目录,点击运行即可。

无法加载时用模拟器的浏览器测试能否打开网站。Web View 加载时显示白屏,超时后显示报错页面。

6.1 自定义 Web View 错误页面

在 capacitor.config.json 中增加 server.errorPath 节点。

修改后的配置:

{
  "appId": "com.deepseek.chat",
  "appName": "DeepSeek",
  "webDir": "dist",
  "server": {
    "url": "https://chat.deepseek.com",
    "errorPath": "error.html"
  },
  "plugins": {
    "SplashScreen": {
      "launchAutoHide": true
    }
  }
}

6.2 创建 error.html

在 dist 目录下创建 error.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <title>连接错误</title>
</head>
<body>
    <div class="error-container">
        <h1>无法连接到服务器</h1>
        <p>请检查您的网络连接,然后重试。</p>
    </div>
</body>
</html>

7. 修改闪屏时间

在 capacitor.config.json 中增加 plugins.SplashScreen.launchShowDuration 配置。

修改后的配置:

{
  "appId": "com.deepseek.chat",
  "appName": "DeepSeek",
  "webDir": "dist",
  "server": {
    "url": "https://chat.deepseek.com",
    "errorPath": "error.html"
  },
  "plugins": {
    "SplashScreen": {
      "launchAutoHide": true,
      "launchShowDuration": 500
    }
  }
}

8. 限制 Web View 可加载地址,禁用 debug,禁止加载 http

在 capacitor.config.json 中增加 server.androidScheme、server.allowNavigation、android.allowMixedContent、android.webContentsDebuggingEnabled。

修改后设为配置:

{
  "appId": "com.deepseek.chat",
  "appName": "DeepSeek",
  "webDir": "dist",
  "server": {
    "url": "https://chat.deepseek.com",
    "androidScheme": "https",
    "allowNavigation": [
      "https://chat.deepseek.com",
      "https://*.deepseek.com"
    ],
    "errorPath": "error.html"
  },
  "android": {
    "allowMixedContent": false,
    "webContentsDebuggingEnabled": false
  },
  "plugins": {
    "SplashScreen": {
      "launchAutoHide": true,
      "launchShowDuration": 500
    }
  }
}

9. 修改 Web View 超时时间

修改 MainActivity.java。

package com.deepseek.chat;

import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.webkit.WebView;
import com.getcapacitor.BridgeActivity;
import com.getcapacitor.Bridge;

public class MainActivity extends BridgeActivity {
    private static final String TAG = "MainActivity";
    private static final int TIMEOUT_MS = 5000; // 5秒超时
    private Handler timeoutHandler;
    private Runnable timeoutRunnable;
    private WebView webView;
    private long pageLoadStartTime = 0;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        timeoutHandler = new Handler(Looper.getMainLooper());
        
        // 在 onCreate 中设置超时检测,因为页面在应用启动时就开始加载
        setupTimeout();
    }
    
    @Override
    public void onStart() {
        super.onStart();
        
        // 等待 Bridge 初始化完成后再开始监控
        getBridge().getWebView().postDelayed(new Runnable() {
            @Override
            public void run() {
                webView = getBridge().getWebView();
                if (webView != null) {
                    // 开始页面加载监控
                    startPageLoadMonitoring();
                }
            }
        }, 1000);
    }
    
    private void setupTimeout() {
        timeoutRunnable = new Runnable() {
            @Override
            public void run() {
                if (webView != null) {
                    Log.w(TAG, "Page load timeout after " + TIMEOUT_MS + "ms");
                    // 检查当前 URL,如果还在加载目标网站,则显示错误页面
                    String currentUrl = webView.getUrl();
                    if (currentUrl == null || currentUrl.contains("chat.deepseek.com") || currentUrl.isEmpty()) {
                        String errorUrl = "https://localhost/error.html";
                        webView.post(new Runnable() {
                            @Override
                            public void run() {
                                webView.loadUrl(errorUrl);
                            }
                        });
                    }
                }
            }
        };
    }
    
    private void startPageLoadMonitoring() {
        if (webView == null) return;
        
        // 记录开始时间
        pageLoadStartTime = System.currentTimeMillis();
        
        // 启动超时检测
        timeoutHandler.postDelayed(timeoutRunnable, TIMEOUT_MS);
        
        Log.d(TAG, "Started page load monitoring with " + TIMEOUT_MS + "ms timeout");
        
        // 定期检查页面加载状态,如果加载完成则取消超时
        final Handler checkHandler = new Handler(Looper.getMainLooper());
        final Runnable checkRunnable = new Runnable() {
            @Override
            public void run() {
                if (webView != null && pageLoadStartTime > 0) {
                    String url = webView.getUrl();
                    // 如果 URL 不再是目标网站或已加载完成,取消超时
                    if (url != null && !url.contains("chat.deepseek.com") && !url.isEmpty()) {
                        cancelTimeout();
                        Log.d(TAG, "Page loaded successfully, cancelled timeout");
                    } else {
                        // 继续检查
                        checkHandler.postDelayed(this, 500);
                    }
                }
            }
        };
        checkHandler.postDelayed(checkRunnable, 1000);
        
        // 使用 JavaScript 注入来监听页面加载状态(作为备用检测)
        webView.postDelayed(new Runnable() {
            @Override
            public void run() {
                if (webView != null) {
                    String js = "javascript:(function() {" +
                        "var startTime = Date.now();" +
                        "var timeout = " + TIMEOUT_MS + ";" +
                        "var checkInterval = setInterval(function() {" +
                        "  if (document.readyState === 'complete' || document.readyState === 'interactive') {" +
                        "    clearInterval(checkInterval);" +
                        "  } else if (Date.now() - startTime > timeout) {" +
                        "    clearInterval(checkInterval);" +
                        "    window.location.href = 'https://localhost/error.html';" +
                        "  }" +
                        "}, 100);" +
                        "})();";
                    webView.evaluateJavascript(js, null);
                }
            }
        }, 500);
    }
    
    private void cancelTimeout() {
        if (timeoutHandler != null && timeoutRunnable != null) {
            timeoutHandler.removeCallbacks(timeoutRunnable);
        }
        pageLoadStartTime = 0;
    }
    
    @Override
    public void onDestroy() {
        super.onDestroy();
        cancelTimeout();
    }
}

Edit
Share this post on:

Next Post
用 Go 语言新特性简化代码