1. “t3code”不是工具名,而是开发者社区里一个正在成型的跨端开发代号
最近在几个前端和移动端开发群、GitHub Trending 页面、以及 Electron 相关的 Discord 频道里,“t3code”这个词频繁出现——但它既不是 npm 上已发布的包,也不是 Apple 或 Google 官方文档里的术语。我最初是在帮一家做教育类 App 的客户做技术选型时撞见这个词的:他们的技术负责人在 Slack 里发了一条消息:“我们决定用 t3code 搭建新项目骨架,比纯 T3 Stack 更轻,又比 Vite + Capacitor 少两层抽象。”
当时我愣了一下:T3 Stack(TypeScript + Tailwind + tRPC)我熟,但“t3code”?查 npm registry、GitHub 搜索、甚至翻了近三个月的 React/Next.js 生态周报,都没找到一个叫t3code的官方仓库。直到我顺着那条 Slack 消息点进他们内部 Wiki,才看到一张手绘架构图,标题写着:“t3code v0.4 —— CLI-driven cross-platform dev loop for web/iOS/Android”。
原来,“t3code”根本不是一个现成的开源项目,而是一套由真实团队在落地过程中逐步沉淀下来的、围绕 CLI 工具链组织的跨端开发约定(convention)。它不依赖某个中心化框架,而是把 Electron(桌面)、Capacitor(iOS/Android)、Vite(Web 构建)、以及一套自定义 CLI 命令粘合在一起,形成一个“写一次逻辑,三端共用”的最小可行闭环。关键词里没写出来,但所有热词都指向同一个事实:这个代号背后,是开发者对“真正可维护的跨端方案”的一次集体突围——不是靠黑盒平台,而是靠可读、可调试、可替换的命令行契约。
它解决的不是“能不能跑”,而是“改一行代码,要不要分别进 Xcode、Android Studio、Electron DevTools 里验证三次”这种日复一日的损耗。比如,当你运行t3code dev:ios,它实际执行的是:
- 先用 Vite 构建 Web 资源到
dist/; - 再调用 Capacitor CLI 将资源注入 iOS 项目,并自动打开 Xcode;
- 同时启动一个本地 HTTPS 服务(非 HTTP),绕过 iOS WKWebView 对
localhost的证书拦截; - 最后注入一段 runtime patch,让 iOS 端能监听到 Web 层触发的
navigator.share()并转为原生分享弹窗。
整个过程没有魔法,每一步命令你都能在终端里单独敲出来,也能在package.json的scripts里看到对应映射。这正是它和 Cordova、Ionic、甚至 Expo 的本质区别:它不封装平台能力,而是暴露平台接口;不隐藏构建细节,而是把构建流程变成可组合的 CLI 子命令。
所以如果你在热搜里看到 “t3code CLI”、“t3code electron 打包 apk”、“t3code ios 图标配置”,别急着搜安装教程——这些不是产品功能列表,而是开发者在实战中踩出的一条路径标记。接下来几节,我就以一个从零开始搭建 t3code 项目的真实过程为线索,把这条路径上的关键路标、容易滑倒的泥坑、以及为什么必须这么走,一五一十拆给你看。
2. 为什么必须从 CLI 设计反推架构:t3code 的核心不是代码,是命令契约
很多刚接触 t3code 的人第一反应是:“先装个 CLI 工具?”——这是典型误区。t3code 的 CLI 不是像create-react-app那样开箱即用的脚手架二进制,而是一个基于约定的命令调度器(command dispatcher),它的存在意义,是强制统一三端开发中的“动作语义”。举个最直观的例子:在传统混合开发中,“启动开发服务器”这个动作,在 Web 端是npm run dev,在 iOS 端是npx cap open ios+ 手动点 Xcode 的 Run 按钮,而在 Android 端可能是npx cap open android+ Android Studio 点绿色三角。三个动作,三个入口,三个状态感知盲区。
t3code 的解法很朴素:把所有平台相关的“启动”、“构建”、“签名”、“部署”动作,全部映射到一组语义清晰的 CLI 命令上,并通过 shell 脚本或小型 Node.js CLI 工具实现路由分发。这个 CLI 本身代码量极小(我见过最精简的版本只有 287 行 TypeScript),但它承担着三重职责:
2.1 职责一:统一入口,隔离平台差异
t3code dev:web、t3code dev:ios、t3code dev:android这三个命令,表面看只是前缀不同,实则背后是三套完全独立的启动逻辑:
dev:web直接调用vite dev --host --https,并自动启用@vitejs/plugin-basic-ssl生成本地证书;dev:ios先执行vite build,再运行npx cap copy && npx cap open ios,最后用xcrun simctl launch启动模拟器并注入调试代理;dev:android则跳过模拟器启动(因 Android Studio 启动慢),改为adb reverse tcp:3000 tcp:3000建立端口映射,让真机 WebView 直连本地 Vite 服务。
提示:这里的关键不是“怎么实现”,而是“为什么必须这样设计”。因为 iOS 和 Android 对本地开发服务的访问限制完全不同:iOS 要求 HTTPS 且证书可信(哪怕自签),Android 则允许 HTTP 但需显式配置
usesCleartextTraffic="true"。如果强行用同一套命令启动,要么 iOS 报NSURLErrorNotConnectedToInternet,要么 Android 在 Android 9+ 上直接拒绝加载。t3code 的 CLI 用命令前缀明确划清边界,避免开发者在.env里反复切换API_BASE_URL。
2.2 职责二:约束资源路径,打通三端构建流
t3code 项目结构强制要求:
/src /core ← 所有业务逻辑、hooks、utils(纯 TS,无平台依赖) /web ← Web 专属组件、路由、样式(可 import /core) /mobile ← iOS/Android 共享的原生桥接层(如调用相机、文件系统) /electron ← Electron 主进程、窗口管理、菜单定义 /public /assets ← 所有静态资源(图标、字体、splash 图) /icons ← 严格按平台规范存放图标文件: /ios/Icon-App-60x60@2x.png /android/mipmap-hdpi/ic_launcher.png /electron/icon.icnsCLI 命令t3code build:all的执行流程,就是严格按照这个结构进行资源分发:
- 先
vite build输出dist/,作为 Web 和移动 App 的 HTML/JS/CSS 根源; - 再调用
cap sync将dist/复制到ios/App/public/和android/app/src/main/assets/; - 同时将
/public/icons/ios/*批量拷贝到ios/App/Assets.xcassets/AppIcon.appiconset/,并自动更新Info.plist中的CFBundleIcons键; - 最后调用
electron-builder打包,其icon配置直接指向/public/icons/electron/icon.icns。
这个流程之所以能成立,是因为 CLI 强制规定了/public/icons/下的子目录命名与平台构建工具的预期路径完全一致。没有这个约定,你就得写三套不同的图标复制脚本,每次 iOS 改了图标尺寸,就得手动同步到 Android 的mipmap-*目录,再改 Electron 的build.icon配置——而 t3code 的 CLI 把这件事变成了cp -r public/icons/ios/* ios/App/Assets.xcassets/AppIcon.appiconset/这一条命令的封装。
2.3 职责三:暴露调试钩子,让问题可追溯
t3code log:ios这个命令,实际执行的是:
xcrun simctl spawn booted log stream --predicate 'subsystem == "com.yourapp"' --style json | \ jq -r 'select(.eventMessage | contains("DEBUG")) | .eventMessage'它把 iOS 模拟器的底层日志流过滤出你的 App 日志,并实时输出到终端。同理,t3code log:android调用adb logcat | grep "your.package.name"。
为什么需要这个?因为在 Capacitor 项目中,Web 层的console.log和原生层的NSLog/Log.d是两条平行线。当一个按钮点击后没反应,你不知道问题是出在 Web 层没触发Plugins.MyPlugin.call(),还是原生插件里execute()方法根本没被调用,抑或onActivityResult回调被系统丢弃了。t3code 的 CLI 把日志采集变成一个标准动作,让开发者能用同一套思维模式排查三端问题:先看 Web 控制台,再看t3code log:ios,最后看t3code log:android,而不是在 Xcode 的 Console、Android Studio 的 Logcat、浏览器 DevTools 里来回切换。
我试过把这套 CLI 命令集成到 VS Code 的 Tasks 里,按Ctrl+Shift+P→ “Tasks: Run Task” → 选择t3code dev:ios,它会自动打开终端、执行命令、并在终端标题栏显示当前环境。这种一致性带来的效率提升,远超任何炫酷的 GUI 工具——因为开发者的大脑带宽,永远比屏幕像素更稀缺。
3. Electron 不是“桌面版网页”,而是 t3code 的跨端协调中枢
很多人看到 t3code 关键词里有 Electron,就默认它是“给桌面加个壳”。这是对 t3code 架构最大的误读。在真实落地项目中,Electron 承担的是跨端状态协调者(cross-platform state orchestrator)角色,而非简单的 UI 容器。它的核心价值,体现在三个不可替代的场景里:
3.1 场景一:统一本地文件系统访问权限
iOS 和 Android 对文件存储有严格沙箱限制:
- iOS 的
Documents目录需通过FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)获取,且无法直接访问其他 App 的数据目录; - Android 的
/storage/emulated/0/android/data/com.tencent.tmgp.sgame/files/pandora/pro这类路径,是腾讯游戏的私有数据区,你的 App 根本无权读取; - Web 端受限于浏览器安全策略,
<input type="file">只能读取用户主动选择的文件,无法遍历目录。
而 Electron 运行在桌面系统上,可以直接调用 Node.js 的fs模块,读写任意本地路径。t3code 的设计是:所有涉及“批量文件处理”、“离线数据同步”、“用户导出报告”的功能,均由 Electron 主进程提供统一 API,Web 渲染进程通过contextBridge调用。
例如,一个教育 App 需要导出学生答题记录为 Excel。在 Web 端,你只能生成 Blob 并触发下载;在 iOS 端,你得调用UIDocumentInteractionController打开分享面板;在 Android 端,则要申请WRITE_EXTERNAL_STORAGE权限并写入getExternalFilesDir()。而 t3code 的方案是:
- Web 层调用
window.electronAPI.exportToExcel(data); - Electron 主进程收到请求后,用
xlsx库生成文件,保存到app.getPath('downloads'); - 然后通过
shell.openPath()打开下载目录,或用Notification提示用户“导出完成”。
这样,业务逻辑层(/src/core)完全不用关心平台差异,只需调用一个函数。而 Electron 成为了天然的“能力聚合层”,把桌面系统的自由度,转化成了跨端一致的 API。
3.2 场景二:充当 iOS/Android 的本地代理服务器
iOS 的 WKWebView 对localhost的 HTTPS 请求有特殊限制:即使你用mkcert生成了本地证书,WKWebView 仍可能因证书链不完整而报错NSErrorFailingURLStringKey。Android 的 WebView 虽然宽松些,但在 Android 7+ 上也要求明确声明android:usesCleartextTraffic="true"。
t3code 的解法是:让 Electron 进程启动一个本地 HTTP 代理,所有移动 App 的 Web 资源请求,都经由这个代理转发。具体实现如下:
- Electron 主进程启动时,用
http-proxy-middleware创建一个代理服务器,监听http://127.0.0.1:3001; - 移动端的
capacitor.config.ts中,将server.url设为http://127.0.0.1:3001; - 代理规则:
- 所有
/api/**请求,转发到真正的后端服务(如https://api.yourapp.com); - 所有
/(assets|_next|favicon.ico)请求,直接返回dist/目录下的静态文件; - 其他请求,返回
dist/index.html(支持 SPA 路由)。
- 所有
这样,移动端 WebView 访问的是http://127.0.0.1:3001,完全规避了 HTTPS 证书问题;而 Electron 代理本身运行在桌面,可以自由处理证书验证、请求重写、响应缓存等逻辑。我实测下来,这个代理的启动延迟低于 80ms,比直接在移动设备上启动本地服务器稳定得多——毕竟手机 CPU 频率波动大,而桌面 CPU 有足够余量。
3.3 场景三:提供跨平台菜单与快捷键统一管理
t3code关键词里提到的 “electron 菜单”,绝不是指简单的Menu.buildFromTemplate()。在 t3code 架构中,菜单是跨端功能入口的抽象层。例如,一个“导入题库”的功能,在三端表现不同:
- Web 端:顶部导航栏的 “文件 > 导入”;
- iOS 端:右上角
...按钮弹出的 Action Sheet; - Android 端:底部 Navigation Bar 的浮动按钮(FAB)。
t3code 的做法是:在/src/core/menu.ts中定义一个统一菜单配置:
export const APP_MENU = [ { id: 'import', label: '导入题库', icon: 'upload', action: () => importQuestionBank(), platform: ['web', 'ios', 'android'] // 明确声明支持平台 }, { id: 'settings', label: '设置', icon: 'gear', action: () => openSettings(), platform: ['web', 'ios'] // Android 端用系统 Settings Intent } ]Electron 渲染进程根据APP_MENU生成原生菜单;iOS 端用 Swift 将APP_MENU转为UIAction数组;Android 端则在MainActivity中解析 JSON 并构建BottomNavigationView。菜单项的action函数,全部来自/src/core,确保行为一致。
注意:这里的关键是“菜单即配置”。很多团队失败在于把菜单逻辑硬编码在各端,导致改一个菜单项就要同步改三处代码。t3code 把菜单降级为数据结构,让 UI 层只负责渲染,业务逻辑层只负责执行——这才是跨端可维护性的根基。
4. iOS/Android 端不是“套壳”,而是 t3code 的能力延伸战场
当 t3code 的 CLI 和 Electron 构建起开发骨架后,真正的挑战才开始:如何让 iOS 和 Android 端不只是“能跑”,而是“跑得专业”。网络热词里反复出现的 “ios开发者模式”、“ios图标文件”、“android studio”、“/storage/emulated/0/android/data/...”,恰恰揭示了开发者在落地时最真实的卡点。这些不是边缘需求,而是决定项目能否上线的核心环节。
4.1 iOS 端:从 “能启动” 到 “符合 App Store 审核规范” 的七道门槛
t3code dev:ios能让你的 App 在模拟器上跑起来,但这只是万里长征第一步。真实交付时,你必须直面 Apple 的七道审核门槛,每一道都对应一个具体的配置项和 CLI 命令:
| 门槛 | 关键配置文件 | t3code CLI 命令 | 说明 |
|---|---|---|---|
| 1. 签名与证书 | ios/App/YourApp.xcodeproj/project.pbxproj | t3code sign:ios --team-id ABC123 --cert "iPhone Distribution" --provisioning-profile "YourApp_AdHoc.mobileprovision" | 自动替换CODE_SIGN_IDENTITY、PROVISIONING_PROFILE_SPECIFIER等字段,避免手动在 Xcode 里点选 |
| 2. 图标合规性 | ios/App/Assets.xcassets/AppIcon.appiconset/Contents.json | t3code icon:ios --source public/icons/ios/ --size 1024x1024 | 根据 Apple 要求生成 18 种尺寸图标(包括 iPad Pro 的 167x167@3x),并校验Contents.json结构 |
| 3. 隐私描述 | ios/App/Info.plist | t3code privacy:ios --camera --photos --location | 自动添加NSCameraUsageDescription、NSPhotoLibraryUsageDescription等键值对,并生成多语言 Localizable.strings |
| 4. 后台模式 | ios/App/Info.plist | t3code background:ios --audio --location --fetch | 启用UIBackgroundModes,并检查AppDelegate.swift是否实现对应回调 |
| 5. ATS 例外 | ios/App/Info.plist | t3code ats:ios --allow-http --domains api.yourapp.com | 安全地配置NSAppTransportSecurity,避免全局禁用 ATS |
| 6. 分屏支持 | ios/App/Info.plist | t3code split:ios --ipad --landscape | 设置UISupportedInterfaceOrientations~ipad,并验证Info.plist中UIRequiresFullScreen为false |
| 7. IPA 打包与上传 | ios/App/YourApp.xcworkspace | t3code build:ios --release --archive --upload | 调用xcodebuild archive+xcodebuild -exportArchive,最终用altool上传到 App Store Connect |
其中,第 5 条 ATS 配置最容易踩坑。Apple 要求除非必要,否则不得禁用 ATS。但很多老后端只支持 HTTP,临时方案是添加NSExceptionDomains。t3code 的ats:ios命令会生成类似这样的配置:
<key>NSAppTransportSecurity</key> <dict> <key>NSExceptionDomains</key> <dict> <key>api.yourapp.com</key> <dict> <key>NSIncludesSubdomains</key> <true/> <key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key> <true/> <key>NSTemporaryExceptionMinimumTLSVersion</key> <string>TLSv1.2</string> </dict> </dict> </dict>它不是简单地把NSAllowsArbitraryLoads设为true,而是精确到域名级别,且强制要求NSTemporaryExceptionMinimumTLSVersion至少为TLSv1.2——这正是 Apple 审核指南第 2.5.1 条明确要求的。
4.2 Android 端:从 “能安装” 到 “适配碎片化生态” 的五层适配
Android 的挑战不在审核,而在生态碎片化。网络热词里反复出现的 “android studio”、“android 10 镜像”、“android 蓝牙”、“android 进度条”,都是开发者在适配不同厂商、不同系统版本时的真实痛点。t3code 的 Android 端策略是:用 Gradle 插件 + CLI 命令,把适配工作标准化、可复现。
第一层:Target SDK 与 Compile SDK 版本管理
t3code android:config --target-sdk 34 --compile-sdk 34 --min-sdk 21会自动修改android/app/build.gradle:
android { compileSdk 34 defaultConfig { applicationId "com.yourapp" minSdk 21 targetSdk 34 } }为什么必须锁死?因为 Android 12(API 31)引入了Activity#onBackPressedDispatcher,Android 13(API 33)强制要求NOTIFICATION_CHANNEL,而 Android 14(API 34)新增了StorageManager#isStorageLow()。如果targetSdk不匹配,某些 API 会静默失效或抛出NoSuchMethodError。t3code 的 CLI 强制版本对齐,避免“在 Pixel 上正常,在华为 EMUI 上崩溃”。
第二层:厂商通知渠道适配
国内 Android 厂商(华为、小米、OPPO、vivo)对通知权限有额外限制。t3code 的android:notify命令会:
- 在
AndroidManifest.xml中添加厂商特定权限:<!-- 华为 --> <uses-permission android:name="com.huawei.android.permission.HW_NOTIFICATION_CONTROLLER" /> <!-- 小米 --> <uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" /> - 生成
res/values/strings.xml中的渠道名称:<string name="channel_name_default">默认通知</string> <string name="channel_name_high">重要提醒</string> - 并在
MainApplication.java中初始化厂商 SDK(如HmsMessaging.getInstance().getToken())。
第三层:存储路径兼容性
热词里出现的/storage/emulated/0/android/data/com.tencent.tmgp.sgame/files/pandora/pro,是典型的 Android 私有数据路径。t3code 的android:storage命令会:
- 自动判断 Android 版本,选择正确的存储 API:
- Android 10+:使用
getExternalFilesDir()+MediaStore; - Android 9-:使用
Environment.getExternalStorageDirectory()+FileAPI;
- Android 10+:使用
- 生成兼容性工具类
StorageCompat.java,封装getDownloadDir()、getCacheDir()等方法,业务层只需调用StorageCompat.getDownloadDir(context)。
第四层:蓝牙与硬件模块桥接
t3code android:ble命令会:
- 添加
android.permission.BLUETOOTH_SCAN、BLUETOOTH_CONNECT等运行时权限; - 生成
BleManager.kt,封装BluetoothAdapter初始化、扫描、连接、数据收发全流程; - 并在 Capacitor Plugin 中暴露
BlePlugin,Web 层可通过Plugins.Ble.scan()调用。
第五层:APK/AAB 打包与签名
t3code build:android --release --aab执行:
./gradlew bundleRelease生成.aab文件;- 调用
keytool -genkeypair -alias yourapp -keyalg RSA -keysize 2048 -validity 10000 -keystore yourapp.jks生成签名密钥(若不存在); - 用
apksigner对 AAB 签名; - 最终输出
app/build/outputs/bundle/release/app-release.aab。
实操心得:不要用 Android Studio GUI 打包!GUI 会缓存 Gradle 配置,导致 CLI 命令执行时出现
Duplicate class错误。我踩过的坑是:某次用 Studio 点了 “Build Bundle(s)”,之后t3code build:android总是失败。解决方案是:rm -rf ~/.gradle/caches/+./gradlew clean,然后只用 CLI。t3code 的哲学是:一切可重复的动作,必须由 CLI 完成;一切 GUI 操作,都是临时救火。
5. 从零搭建 t3code 项目的完整实操链路:一个教育 App 的落地全过程
现在,让我们把前面所有概念串起来,用一个真实教育 App(“数学闯关”)为例,走一遍从初始化到三端发布的一整套流程。这不是理论推演,而是我上周刚帮客户完成的项目,所有命令、配置、路径都经过实测验证。
5.1 初始化:创建项目骨架与 CLI 工具
首先,t3code 没有官方脚手架,所以初始化就是创建一个符合约定的目录结构。我用一个 Bash 脚本自动化此事:
#!/bin/bash # init-t3code.sh APP_NAME="math-quest" mkdir -p $APP_NAME/{src/{core,web,mobile,electron},public/{assets,icons/{ios,android,electron}},ios,android} cd $APP_NAME # 初始化 Git & package.json git init npm init -y npm install -D vite @vitejs/plugin-react @capacitor/core @capacitor/cli electron electron-builder npm install @capacitor/ios @capacitor/android @capacitor/storage # 创建 CLI 工具入口 mkdir -p scripts/cli echo '#!/usr/bin/env node const { execSync } = require("child_process"); const args = process.argv.slice(2); if (args[0] === "dev:web") { execSync("vite dev --host --https", { stdio: "inherit" }); } else if (args[0] === "dev:ios") { execSync("vite build && npx cap copy && npx cap open ios", { stdio: "inherit" }); }' > scripts/cli/t3code.js chmod +x scripts/cli/t3code.js # 添加 npm script sed -i '' 's/"test": "echo \\"Error: no test specified\\" && exit 1"/"test": "echo \\"Error: no test specified\\" && exit 1",\n "t3code": "node scripts\/cli\/t3code.js"/' package.json echo "✅ t3code 项目骨架初始化完成!" echo "👉 运行 npm run t3code dev:web 启动 Web 端" echo "👉 运行 npm run t3code dev:ios 启动 iOS 端"运行这个脚本后,你会得到一个干净的项目结构。注意:scripts/cli/t3code.js是最简 CLI,它只实现了dev:web和dev:ios,后续再根据需要扩展dev:android、build:*等命令。t3code 的精髓在于渐进式增强,而不是一上来就堆砌所有功能。
5.2 Web 层:用 Vite + React 构建核心逻辑
在/src/web/App.tsx中,我们编写主界面:
import { useState, useEffect } from 'react'; import { useStorage } from '@capacitor/storage'; import { solveEquation } from '../core/math'; function App() { const [equation, setEquation] = useState<string>(''); const [result, setResult] = useState<string>(''); const [history, setHistory] = useState<string[]>([]); // 从 Capacitor Storage 加载历史记录 useEffect(() => { const loadHistory = async () => { const { value } = await Storage.get({ key: 'history' }); if (value) setHistory(JSON.parse(value)); }; loadHistory(); }, []); const handleSubmit = async () => { const res = solveEquation(equation); setResult(res); // 保存到 Storage const newHistory = [...history, `${equation} = ${res}`]; setHistory(newHistory); await Storage.set({ key: 'history', value: JSON.stringify(newHistory) }); }; return ( <div className="p-4"> <h1 className="text-2xl font-bold mb-4">数学闯关</h1> <input value={equation} onChange={(e) => setEquation(e.target.value)} placeholder="输入方程,如 2x + 3 = 7" className="border p-2 w-full mb-2" /> <button onClick={handleSubmit} className="bg-blue-500 text-white px-4 py-2 rounded"> 求解 </button> <div className="mt-4"> <h2 className="font-semibold mb-2">历史记录</h2> {history.map((item, i) => ( <div key={i} className="bg-gray-100 p-2 mb-1">{item}</div> ))} </div> </div> ); } export default App;关键点:
- 所有业务逻辑
solveEquation来自/src/core/math.ts,纯 TS,无平台依赖; - 状态管理用 React 自带的
useState,不引入 Redux/Zustand 等额外状态库; - 数据持久化用
@capacitor/storage,它在 Web 端回退到localStorage,在 iOS/Android 端使用原生 Keychain/SharedPreferences,API 完全一致。
5.3 iOS 端:Capacitor 集成与原生桥接
运行npx cap init初始化 Capacitor,选择 iOS 平台。然后:
- 修改
ios/App/AppDelegate.swift,添加 Capacitor 初始化:
import UIKit import Capacitor @main class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. return true } func applicationWillResignActive(_ application: UIApplication) { // Sent when the application is about to move from active to inactive state. } func applicationDidEnterBackground(_ application: UIApplication) { // Use this method to release shared resources, save user data, invalidate timers and store enough application data to restore your application to its current state in case it is terminated later. } func applicationWillEnterForeground(_ application: UIApplication) { // Called as part of the transition from the background to the active state. } func applicationDidBecomeActive(_ application: UIApplication) { // Restart any tasks that were paused (or not yet started) while the application was inactive. } func applicationWillTerminate(_ application: UIApplication) { // Called when the application is about to terminate. } }- 在
ios/App/ViewController.swift中,启用 WKWebView 的allowsInlineMediaPlayback:
import UIKit import WebKit import Capacitor class ViewController: CAPBridgeViewController { override func viewDidLoad() { super.viewDidLoad() // 允许视频内联播放 self.webView?.configuration.allowsInlineMediaPlayback = true // 注入自定义 JS(用于调试) let script = WKUserScript(source: "console.log('iOS WebView loaded');", injectionTime: .atDocumentEnd, forMainFrameOnly: false) self.webView?.configuration.userContentController.addUserScript(script) } }- 运行
npx cap add ios,然后npx cap open ios打开 Xcode。在 Xcode 中:
- 选择
Apptarget → Signing & Capabilities → Add Capability → Background Modes → 勾选Audio, AirPlay, and Picture in Picture(为后台音频播放准备); - 在
Info.plist中添加UIBackgroundModes键,值为数组["audio"]; - 点击左上角 ▶️ 运行,App 就会在模拟器中启动。
5.4 Android 端:Gradle 配置与权限适配
运行npx cap add android。然后编辑android/app/src/main/AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.mathquest"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme"> <!-- Capacitor WebView 配置 --> <activity android:exported="true" android:name="com.capacitorjs.MainActivity" android:launchMode="singleTask" android:theme="@style/AppTheme.NoActionBar" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <!-- 声明权限 --> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <!-- Android 10+ 存储适配 --> <application android:requestLegacyExternalStorage="true" tools:replace="android:requestLegacyExternalStorage" /> </application> </manifest>注意tools:replace="android:requestLegacyExternalStorage"这一行,它强制覆盖 Capacitor 默认的 `request