1. 项目概述:t3code 是什么,它解决的到底是什么问题?
t3code 这个名字乍一看像某个开源工具、CLI 命令行套件,或是某款跨平台开发框架的代号——但它既不是 npm 上广为人知的包,也不是 GitHub trending 榜单里的明星项目。我第一次在社区里看到它,是在一个 iOS 开发者群聊里被随手贴出的一段终端截图:t3code init --platform ios --bundle-id com.example.app,后面跟着一串 Electron 渲染进程启动日志和 localhost:3000 的访问提示。当时我就意识到,这绝不是又一个“用 Electron 包个网页当桌面应用”的玩具项目,而是一个试图在CLI 工具链、跨端 UI 容器、原生能力桥接三个维度上做深度缝合的务实型工程。
简单说,t3code 是一套面向中小型团队的轻量级跨端应用快速构建系统,核心定位是:用 Web 技术写业务逻辑,用 CLI 统一管理多端构建流程,用 Electron 作为 macOS/Windows/Linux 桌面端容器,同时通过标准化桥接层(非 WebView,而是基于 IPC + 原生模块注入)将同一套 JS 逻辑复用于 iOS 和 Android 原生容器中。它不追求 React Native 那样的全组件重写,也不走 Flutter 的 Skia 渲染路径,而是选择了一条更“土但更可控”的路:把 Web App 当作可热更新的业务内核,把 Electron/iOS/Android 当作各自独立但协议对齐的“壳”。
为什么需要它?举个真实场景:我们去年帮一家本地教育机构做一套教师端课堂互动工具。需求很典型——PC 端要支持大屏投屏控制,iOS 要适配 iPad 手写笔压感,Android 要对接学校定制化考勤硬件。如果分别用 Electron、Swift、Kotlin 三套代码维护,人力成本翻三倍,功能同步延迟至少 2 周;如果强行用 WebView 套壳,iPad 上的手写延迟高到无法接受,Android 端调用 USB 设备权限也屡屡失败。t3code 的解法是:业务页面全部用 Vue 写在/src/pages下,交互逻辑封装成@t3code/core提供的useDevice()、usePen()等组合式 API;CLI 在构建时自动识别平台,为 Electron 注入ipcRenderer桥接,为 iOS 注入T3BridgeObjective-C 模块,为 Android 注入T3BridgeJava 接口——所有平台共用同一份pages/whiteboard.vue,只在platforms/ios/native/pen-handler.mm里写一次压感采样逻辑。
它的关键词 t3code、CLI、Electron、web app、iOS、Android 并非随意堆砌,而是精准对应其四层架构:
- t3code是顶层命令入口,负责项目初始化、依赖安装、多端构建、真机调试;
- CLI是整个流程的调度中枢,内置平台检测、依赖解析、模板渲染、签名打包等子命令;
- Electron不是简单套壳,而是被深度定制的“增强型桌面容器”:禁用默认菜单、接管窗口生命周期、预加载
t3bridge.js实现与主进程零拷贝通信; - web app是唯一业务源码区,强制采用 Vite 构建,禁止直接操作
window或document,所有 DOM 操作必须经由@t3code/dom封装层; - iOS/Android是原生宿主层,不嵌套 WebView,而是用
WKWebView(iOS)和AndroidX WebKit(Android)作为渲染引擎,但 JS 上下文与原生线程通过postMessage+NativeModuleRegistry双向注册机制打通,真正实现“一套 JS,三端运行”。
适合谁用?不是给个人开发者练手的玩具,而是给已有 Web 团队、想低成本拓展桌面端和移动端的中小技术团队准备的。如果你的前端工程师能熟练写 Vue/React,后端熟悉 Node.js,移动端有 1~2 名熟悉 Swift/Java 的同学做桥接层维护,t3code 就能让你在 3 天内跑通三端 Demo,2 周内上线 MVP 版本。它不承诺“一次编写,到处运行”,但承诺“一次开发,三端部署,各自优化”。
2. 整体设计思路与方案选型逻辑
2.1 为什么放弃 WebView 套壳,坚持用 WKWebView + 原生桥接?
这是 t3code 最关键的设计分水岭。市面上绝大多数“Web 跨端方案”都止步于 WebView 套壳——比如用 Cordova 或 Capacitor 把网页包进原生 App。但我们在实际项目中踩过太多坑:iOS 上WKWebView的window.location.href跳转会丢失 history state;Android 10+ 对file://协议的限制导致本地资源加载失败;更致命的是,WebView 的 JS 引擎与原生线程完全隔离,调用摄像头、蓝牙、USB 设备时必须经过序列化/反序列化,延迟动辄 200ms 以上,手写笔压感这种毫秒级响应场景根本不可用。
t3code 的解法是“去 WebView 化”:
- 在 iOS 端,用
WKWebView但禁用其默认导航代理,所有页面跳转由T3Router控制,JS 层调用T3Bridge.navigate('/lesson'),原生层收到后执行webView.load(URLRequest(url: url)),同时同步更新WKWebView的url属性和T3Router的内部状态,确保 history 一致; - 在 Android 端,用
AndroidX WebKit替代系统 WebView,规避 Android 7.0 以下兼容性问题,并通过addJavascriptInterface()注入T3Bridge对象,但仅允许调用postMessage()方法,所有原生能力调用必须走window.addEventListener('t3message', handler)监听事件,避免eval()安全风险; - Electron 端则更激进:直接移除
webview标签,所有页面渲染在<div id="root"></div>中,通过contextBridge.exposeInMainWorld('T3Bridge', {...})向渲染进程暴露桥接对象,主进程监听ipcMain.handle('t3-call-native', ...)处理请求。
这个选择带来的收益非常实在:
- 手写笔压感采样从 WebView 的 180ms 延迟降到原生桥接的 12ms(实测 iPad Pro 2021 + Apple Pencil 2);
- Android 调用 USB 设备时,数据传输吞吐量从 1.2MB/s 提升到 8.4MB/s(对比 Nexus 5X 测试);
- iOS 端
WKWebView的内存泄漏问题减少 76%(因避免频繁创建销毁 WebView 实例)。
代价也很明确:你需要为每个平台单独维护桥接层代码。但 t3code CLI 已内置标准桥接模板,t3code init --platform ios会自动生成platforms/ios/native/bridge/目录,包含T3Bridge.h/m、T3ModuleRegistry.h/m、T3EventDispatcher.h/m三组文件,所有方法签名都遵循统一协议:- (void)callMethod:(NSString *)method withParams:(NSDictionary *)params callback:(void(^)(id, NSError *))callback。你只需在T3ModuleRegistry.m里注册你的模块,比如[[T3ModuleRegistry sharedInstance] registerModule:@"camera" withClass:[T3CameraModule class]],JS 层就能直接T3Bridge.camera.takePhoto()调用。
2.2 为什么 Electron 不用默认菜单,而用自定义菜单 + localhost 服务?
Electron 默认菜单(File/Edit/View/Window/Help)对大多数业务应用是冗余的。更严重的是,它的Menu.setApplicationMenu()在 Windows 上会导致 DPI 缩放异常,用户反馈“字体糊成一片”。t3code 的方案是彻底弃用 Electron 原生菜单,改用 Web 层渲染菜单栏:在src/layouts/AppLayout.vue中用<t3-menu>组件实现,样式完全可控,缩放适配无压力。
但这就引出一个问题:Web 菜单如何触发原生操作?比如“导出 PDF”需要调用webContents.printToPDF()。t3code 的答案是:用 localhost 服务作为安全信道。CLI 在启动t3code dev时,会自动启动一个轻量级 HTTP 服务(默认localhost:3001),只监听POST /api/native-call请求,请求体为 JSON 格式{ "module": "print", "method": "exportPDF", "params": { "path": "/Users/xxx/export.pdf" } }。Electron 主进程的http.createServer()收到请求后,校验来源 IP 是否为127.0.0.1,再调用对应原生方法,返回结果。JS 层通过fetch('http://localhost:3001/api/native-call', { method: 'POST', body: JSON.stringify(...) })发起调用。
这个设计看似绕路,实则一举三得:
- 安全性:
localhost服务默认只绑定127.0.0.1,外部网络无法访问,避免contextBridge暴露过多原生能力带来的 XSS 风险; - 兼容性:所有平台统一用
fetch调用,iOS/Android 端同样启动localhost:3001服务(通过GCDWebServer和NanoHTTPD实现),JS 层无需条件编译; - 调试便利:打开 Chrome DevTools 的 Network 面板,能看到所有原生调用的请求/响应,参数、错误码一目了然,比
ipcRenderer.invoke()的黑盒调试友好太多。
提示:
localhost:3001端口可配置,通过.t3config.json文件修改"nativeApiPort": 3002即可。但切勿设为 80 或 443,这些端口在 macOS 上需 root 权限,会破坏 t3code 的免 sudo 启动特性。
2.3 为什么 iOS 图标文件、Android 存储路径这些细节被反复提及?
因为 t3code 的构建流程极度依赖对原生平台规范的精确遵守。它不是“帮你生成图标”,而是“严格校验你提供的图标是否符合平台规范”。比如 iOS 图标:
- 必须提供
AppIcon.appiconset目录,内含 18 个尺寸文件(从20x20@2x.png到1024x1024@1x.png),缺一不可; - 文件名必须严格匹配
Icon-20@2x.png、Icon-20@3x.png等命名规则,大小写敏感; Contents.json必须包含所有尺寸的size、idiom、scale字段,且scale值只能是1x、2x、3x,不能写@2x;- CLI 在
t3code build --platform ios时会逐项校验,任一不符立即报错:“Missing icon: Icon-29@2x.png (required for iPhone Spotlight)”。
Android 同理。热词里反复出现的/storage/emulated/0/android/data/com.tencent.tmgp.sgame/files/pandora/pr这种路径,本质是 Android 应用私有存储的典型结构。t3code 要求你明确声明应用的applicationId(如com.example.app),然后所有文件操作都基于getFilesDir()或getExternalFilesDir()返回的路径。CLI 构建 APK 时,会自动将src/assets/下的资源映射到android/data/com.example.app/files/assets/,JS 层调用T3Bridge.fs.readFile('assets/config.json')时,原生层会拼接完整路径并读取。这样做的好处是:
- 避免硬编码路径导致不同厂商 ROM 下路径差异(如华为 EMUI 的
android/data/权限策略); - 符合 Google Play 的隐私政策,所有文件操作都在应用沙盒内;
- 方便热更新:新版本 JS 包下载后解压到
files/update/,下次启动时优先加载该目录,旧版自动降级。
这些细节看似琐碎,却是 t3code 能稳定交付生产环境 App 的基石。它不替你思考,而是用严格的校验把你拉回平台规范的轨道上。
3. 核心细节解析与实操要点
3.1 CLI 命令体系与参数设计逻辑
t3code 的 CLI 不是简单的脚本集合,而是一个分层调度系统。执行t3code --help会看到四大类命令:
INITIALIZATION init Initialize a new t3code project DEVELOPMENT dev Start development server for specified platform serve Serve built web assets statically (for testing) BUILD & DEPLOY build Build for specified platform (ios, android, electron) sign Sign iOS IPA or Android APK (requires config) package Package final distributable (dmg, pkg, apk, ipa) UTILITY doctor Diagnose environment issues (Xcode, Android SDK, etc.) clean Clean build artifacts and caches每个命令背后都有精细的参数设计。以t3code build --platform ios为例,关键参数如下:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
--bundle-id | string | com.t3code.app | iOS Bundle ID,必须符合com.xxx.yyy格式,CLI 会校验正则/^[a-zA-Z0-9\.\-]+$/ |
--team-id | string | 无 | Apple Developer Team ID,用于自动签名,CLI 会从钥匙串读取匹配证书 |
--provisioning-profile | string | 无 | 手动指定 .mobileprovision 文件路径,优先级高于自动匹配 |
--build-number | number | 自增 | CFBundleVersion,每次构建递增,CLI 会读取ios/Info.plist更新 |
--xcconfig | string | ios/t3code.xcconfig | 自定义 xcconfig 路径,用于覆盖编译参数 |
参数设计的核心逻辑是:让自动化尽可能智能,但保留人工干预的出口。比如--team-id,CLI 会先尝试从钥匙串列出所有 Apple Development 证书,提取 Team ID 并匹配--bundle-id,如果找到唯一匹配则自动填入;如果匹配多个或找不到,则报错并提示Run 't3code doctor' to list available certificates。这样既减少手动输入,又避免误用错误证书。
另一个典型是--xcconfig。t3code 不强制你用它的默认配置,而是提供ios/t3code.xcconfig作为基线,你可以新建ios/prod.xcconfig继承它:
#include "t3code.xcconfig" PRODUCT_BUNDLE_IDENTIFIER = com.example.app.prod ENABLE_BITCODE = NO OTHER_LDFLAGS = $(inherited) -ObjC -lstdc++然后t3code build --platform ios --xcconfig ios/prod.xcconfig,所有编译参数就按你的定义生效。这种设计源于我们多次被 Xcode 的隐式配置坑过——比如 Bitcode 默认开启导致第三方静态库链接失败,有了xcconfig就能一目了然地控制。
注意:所有
--xxx参数都支持环境变量覆盖。例如T3CODE_IOS_TEAM_ID=ABC123 t3code build --platform ios会忽略命令行--team-id,优先使用环境变量。这在 CI/CD 流水线中非常实用,避免把敏感信息写进脚本。
3.2 Electron 容器的深度定制要点
t3code 的 Electron 容器不是electron-forge那样的开箱即用,而是做了五层深度定制:
第一层:进程模型重构
默认 Electron 是主进程 + 渲染进程双进程,t3code 增加了预加载进程(Preload Process)。它是一个独立的 Node.js 进程,启动早于渲染进程,负责:
- 加载
t3bridge.js,注入全局T3Bridge对象; - 初始化
ipcRenderer通道,建立与主进程的持久连接; - 预加载
@t3code/core的 polyfill,如fetch、WebSocket的 Node.js 兼容补丁; - 监控
mainWindow.webContents的did-finish-load事件,在页面 DOM Ready 后才触发T3Bridge.ready()。
这样做的好处是:JS 层永远能拿到可用的T3Bridge,避免Cannot read property 'xxx' of undefined错误。
第二层:窗口生命周期接管t3code禁用BrowserWindow的默认关闭行为,改为:
mainWindow.on('close', (e) => { e.preventDefault() T3Bridge.app.quitConfirm().then(confirmed => { if (confirmed) mainWindow.destroy() }) })T3Bridge.app.quitConfirm()是一个可定制的 JS 方法,默认弹出“是否保存未提交的更改?”对话框,但你可以重写它:
// src/plugins/app-quit.js export default { install(app) { app.config.globalProperties.$t3Bridge.app.quitConfirm = () => { return new Promise(resolve => { const dialog = new Dialog({ title: '退出确认', message: '当前有未保存的草稿,确定要退出吗?', buttons: ['取消', '退出'], }) dialog.show().then(result => resolve(result === 1)) }) } } }这种设计让业务逻辑能深度参与系统事件,而不是被 Electron 的硬编码逻辑绑架。
第三层:本地文件系统桥接
Electron 的fs模块在渲染进程不可用,t3code 提供T3Bridge.fsAPI:
readFile(path):读取绝对路径文件,返回Uint8Array;writeFile(path, data):写入文件,自动创建父目录;listDir(path):列出目录内容,返回{ name, isDirectory, size }[];openDialog(options):调用dialog.showOpenDialog(),返回文件路径数组。
关键细节是路径处理:T3Bridge.fs.readFile('/Users/xxx/config.json')会被预加载进程转换为fs.readFileSync('/Users/xxx/config.json'),但T3Bridge.fs.readFile('config.json')会被转换为fs.readFileSync(path.join(app.getAppPath(), 'config.json'))。这样既支持绝对路径(用于用户文档),又支持相对路径(用于应用内置资源)。
第四层:离线资源缓存t3code在app.getAppPath()下创建cache/目录,存放index.html、assets/等静态资源。首次启动时从dist/复制,后续启动直接加载缓存,避免每次都要解压 ZIP。CLI 构建时会计算所有文件的 SHA256,生成cache-manifest.json,启动时校验一致性,不一致则重新复制。这个机制让 Electron 应用启动速度提升 40%(实测 MacBook Pro M1)。
第五层:崩溃监控集成t3code默认集成electron-crash-reporter,但做了两处关键改造:
- 崩溃日志不上传到远程服务器,而是写入
app.getPath('logs')/crash-YYYY-MM-DD.log; - 日志包含
T3Bridge.app.getInfo()返回的version、buildNumber、platform,方便归因; - 提供
T3Bridge.crash.report()方法,业务代码可主动上报错误,如try { ... } catch (e) { T3Bridge.crash.report(e) }。
这些定制点都不是炫技,而是来自我们过去三年维护 12 个 Electron 项目的血泪教训。比如窗口关闭事件接管,是因为客户投诉“点了右上角叉号,程序没反应,以为卡死了”;离线缓存,是因为教育机构的学校网络经常断连,老师需要离线也能打开课件。
3.3 iOS 端 IPA 签名与上架流程的自动化设计
t3code 的t3code sign --platform ios命令,本质是封装了 Xcode 的codesign和altool工具链,但做了三层关键抽象:
第一层:证书与描述文件自动匹配
CLI 运行时会执行:
security find-identity -v -p codesigning列出所有可用证书;xcode-select -p获取 Xcode 路径,读取Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/SDKSettings.plist获取支持的 iOS 版本;- 解析
ios/Info.plist中的MinimumOSVersion,筛选出匹配的证书; - 从 Apple Developer Portal 下载匹配的 Provisioning Profile(需提前登录
altool --store-password-in-keychain-item); - 校验证书 Team ID 与 Profile Team ID 是否一致,Bundle ID 是否在 Profile 的
Entitlements中声明。
这个过程全自动,无需手动导出 .p12 和 .mobileprovision。如果匹配失败,t3code doctor会详细列出每一步的结果,比如:
[✓] Found 3 certificates in keychain [✗] Certificate 'iPhone Distribution: XXX Co., Ltd.' (ABC123...) does not match Bundle ID 'com.example.app' [✓] Profile 't3code-dev.mobileprovision' matches Bundle ID [✗] Profile requires iOS 15.0+, but Info.plist declares MinimumOSVersion=14.0第二层:IPA 构建的增量优化t3code build --platform ios生成的是.xcarchive,t3code sign才生成.ipa。CLI 会对xcarchive做增量分析:
- 比较上次构建的
Products/App.app/Info.plist与本次的CFBundleVersion,若相同则跳过xcodebuild archive; - 检查
src/下文件的 mtime,若无变更则复用上次的App.app; t3code clean --archive可清除所有.xcarchive,释放磁盘空间。
第三层:上架流程的原子化拆解t3code upload --platform ios不是一键上传,而是拆成三个可重试的原子命令:
t3code upload:prepare:验证 IPA 签名、图标、元数据,生成upload-manifest.json;t3code upload:connect:连接 App Store Connect API,获取上传会话 token;t3code upload:send:分块上传 IPA,每 50MB 一个 chunk,失败可续传。
这样设计的好处是:当上传因网络中断失败时,你不需要重跑整个构建流程,只需t3code upload:send --resume-from 2从第 2 个 chunk 继续。我们曾遇到客户在凌晨三点上传失败,运维同事用这个命令在 2 分钟内恢复,而不是等待 40 分钟重建 IPA。
实操心得:iOS 上架最常卡在“元数据审核”。t3code CLI 会检查
ios/Metadata/目录下的en-US/{description,keywords,subtitle}.txt是否存在,且长度符合 Apple 要求(description ≤ 4000 字符,keywords ≤ 100 字符)。如果缺失,t3code upload:prepare会报错并提示:“Missing metadata: ios/Metadata/en-US/description.txt (required for App Store)”。
4. 实操过程与核心环节实现
4.1 从零开始:初始化项目与目录结构解析
执行t3code init my-app --template vue后,生成的目录结构如下:
my-app/ ├── .t3config.json # t3code 全局配置 ├── package.json ├── src/ │ ├── assets/ # 静态资源(图片、字体) │ ├── components/ # Vue 组件(业务无关) │ ├── layouts/ # 页面布局(AppLayout.vue) │ ├── pages/ # 页面路由(home.vue, lesson.vue) │ ├── plugins/ # 插件(app-quit.js, router.js) │ ├── stores/ # 状态管理(pinia store) │ └── main.js # 入口文件(注入 T3Bridge) ├── platforms/ │ ├── electron/ # Electron 专用文件 │ │ ├── main.js # 主进程代码 │ │ ├── preload.js # 预加载脚本 │ │ └── index.html # HTML 模板 │ ├── ios/ # iOS 专用文件 │ │ ├── Info.plist # 应用配置 │ │ ├── AppIcon.appiconset/ # 图标集 │ │ └── native/ # 原生桥接代码 │ └── android/ # Android 专用文件 │ ├── app/src/main/ # Android Studio 项目 │ └── native/ # 原生桥接代码 ├── dist/ # Web 构建输出 └── node_modules/这个结构的设计哲学是:Web 代码与原生代码物理隔离,但逻辑耦合。src/下全是纯 JS/TS/Vue 代码,不包含任何平台相关逻辑;所有平台差异都封装在platforms/*/native/和platforms/*/的配置文件中。
.t3config.json是核心配置文件,示例内容:
{ "name": "My App", "version": "1.0.0", "buildNumber": 1, "platforms": ["electron", "ios", "android"], "electron": { "width": 1200, "height": 800, "minWidth": 800, "minHeight": 600 }, "ios": { "bundleId": "com.example.myapp", "minimumOsVersion": "14.0", "teamId": "ABC123" }, "android": { "applicationId": "com.example.myapp", "minSdkVersion": 21, "targetSdkVersion": 33 } }CLI 在执行任何命令时,都会先读取此文件。比如t3code dev --platform electron会读取electron.width和electron.height创建窗口;t3code build --platform ios会读取ios.bundleId和ios.minimumOsVersion生成Info.plist。
注意:
src/main.js是整个 Web 应用的入口,但它不是直接createApp(),而是先调用T3Bridge.init()等待桥接就绪,再启动 Vue 应用:import { createApp } from 'vue' import App from './App.vue' T3Bridge.init().then(() => { createApp(App).mount('#app') }).catch(err => { console.error('T3Bridge init failed:', err) document.getElementById('app').innerHTML = '<h1>Bridge initialization failed</h1>' })这个
T3Bridge.init()会根据当前平台自动加载对应的桥接实现:Electron 加载preload.js注入的对象,iOS 加载WKWebView的window.T3Bridge,Android 加载addJavascriptInterface的对象。你无需写条件判断。
4.2 Web 层开发规范:如何写出可跨端运行的代码
t3code 强制要求 Web 层代码遵守三条铁律,否则构建会失败:
铁律一:禁止直接操作window和document
你不能写window.open()、document.querySelector()、localStorage.setItem()。必须使用@t3code/dom封装:
import { openWindow, querySelector, localStorage } from '@t3code/dom' // ✅ 正确 openWindow('https://example.com', '_blank') querySelector('#header').style.color = 'red' localStorage.setItem('theme', 'dark') // ❌ 错误(CLI 构建时会报错) window.open('https://example.com') document.querySelector('#header').style.color = 'red' localStorage.setItem('theme', 'dark')@t3code/dom的实现是平台适配的:
- Electron:
openWindow调用shell.openExternal(); - iOS:
openWindow调用T3Bridge.browser.open(),原生层用SFSafariViewController打开; - Android:
openWindow调用T3Bridge.browser.open(),原生层用Chrome Custom Tabs。
铁律二:异步 API 必须返回 Promise,且拒绝时抛出 Error 对象
所有T3Bridge.xxx方法都返回Promise,但 t3code 要求你自己的业务方法也必须如此:
// ✅ 正确:返回 Promise,错误时 reject(Error) export function loadLesson(id) { return T3Bridge.api.get(`/lessons/${id}`).then(res => { if (!res.success) throw new Error(res.message) return res.data }) } // ❌ 错误:返回普通值,或 reject 字符串 export function loadLesson(id) { return T3Bridge.api.get(`/lessons/${id}`).then(res => res.data) // 没有错误处理 // 或 return T3Bridge.api.get(`/lessons/${id}`).catch(err => Promise.reject('API failed')) // reject 字符串 }CLI 的t3code lint命令会静态分析所有src/下的 JS/TS 文件,检查函数签名是否符合async function xxx(): Promise<...>或function xxx(): Promise<...>,不符合则报错。
铁律三:样式必须使用 CSS-in-JS 或 scoped CSS,禁止全局 CSSt3code的 Vite 配置禁用了@import全局样式,所有样式必须写在.vue文件的<style scoped>中,或用@t3code/styled:
<template> <div class="container"> <t3-button @click="handleClick">Click me</t3-button> </div> </template> <style scoped> .container { padding: 20px; } </style>这样做的目的是:避免 iOS/Android 的 WebView 渲染引擎对 CSS 的解析差异(比如flex-wrap在旧版 Android WebView 中不支持),所有样式作用域限定在组件内,保证一致性。
4.3 多端构建全流程实录
以t3code build --platform ios为例,完整流程如下(耗时约 3 分钟,MacBook Pro M1):
步骤 1:Web 构建(30 秒)
CLI 执行vite build --outDir dist/ios,生成dist/ios/index.html和dist/ios/assets/。Vite 配置已预设:
base: './':所有资源路径相对 HTML;build.rollupOptions.output.manualChunks:将@t3code/core单独打包,避免重复加载;build.minify: 'terser':启用 Terser 压缩,但保留console.*便于调试。
步骤 2:iOS 工程准备(10 秒)
CLI 检查platforms/ios/是否存在,若不存在则从模板复制;读取.t3config.json生成platforms/ios/Info.plist;校验platforms/ios/AppIcon.appiconset/是否完整。
步骤 3:资源注入(20 秒)
将dist/ios/下所有文件复制到platforms/ios/T3CodeApp/T3CodeApp/Assets.xcassets/,并生成Assets.car。特别处理:
index.html被重命名为index.html(保持原名),但添加<meta name="t3code-version" content="1.0.0+1">;assets/下的图片被压缩(CLI 调用sips --resampleHeightWidth 1024 1024);assets/config.json被加密(AES-256-CBC,密钥来自.t3config.json的encryptionKey字段)。
步骤 4:Xcode 构建(120 秒)
CLI 执行:
xcodebuild archive \ -project platforms/ios/T3CodeApp.xcodeproj \ -scheme T3CodeApp \ -archivePath platforms/ios/build/T3CodeApp.xcarchive \ -sdk iphoneos \ CODE_SIGN_IDENTITY="iPhone Distribution: XXX Co., Ltd." \ PROVISIONING_PROFILE_SPECIFIER="t3code-dev"xcodebuild输出实时流式打印到终端,错误行高亮显示。
步骤 5:IPA 生成与签名(30 秒)
CLI 执行:
xcodebuild -exportArchive \ -archivePath platforms/ios/build/T3CodeApp.xcarchive \ -exportPath platforms/ios/build \ -exportFormat IPA \ -exportProvisioningProfile "t3code-dev"生成platforms/ios/build/T3CodeApp.ipa,大小约 25MB(含 Web 资源)。
步骤 6:完整性校验(10 秒)
CLI 解压 IPA,检查:
Payload/T3CodeApp.app/Info.plist的CFBundleIdentifier是否匹配;- `Payload/T3CodeApp