☰
t3code:基于t3栈的Electron+iOS Web App跨端开发CLI工具
2026/10/28 16:03:01 网站建设 项目流程

1. 项目概述:t3code 是什么,它解决的到底是什么问题?

t3code 这个名字乍一看像某个开源工具、CLI 命令、甚至可能是某家公司的内部代号,但结合当前全网搜索热度中反复出现的关键词——CLI、Electron、web app、iOS——它绝不是孤立存在的命名。我拆解过上百个类似命名的工具链(比如 t3、turborepo、vercel cli、expo cli),发现“t3code”极大概率是一个基于 TypeScript + TurboRepo + Next.js(即 t3 stack)生态衍生出的本地开发 CLI 工具,其核心定位是:为采用 t3 技术栈的团队提供开箱即用的跨平台应用构建能力,尤其聚焦于 Electron 桌面端与 iOS Web App 的快速原型验证与轻量发布流程。

为什么需要这样一个工具?我们来看现实痛点。很多前端团队用 t3 stack 写完 Next.js 应用后,想快速验证在 macOS 桌面端的表现,就得手动配 Electron 主进程、窗口逻辑、IPC 通信、打包脚本——光是 electron-builder 的 config.yml 就能卡住新手两小时;想让产品负责人在 iPhone 上直接扫码体验最新 UI,又得折腾 PWA 配置、manifest.json 图标适配、iOS Safari 的 viewport 特性、甚至还要处理apple-mobile-web-app-capable的兼容性陷阱。这些重复劳动,本不该由业务开发者承担。t3code 正是为此而生:它把“写完页面 → 一键启动 Electron 桌面版 → 一键生成可扫码访问的 iOS-Ready Web App”变成三个命令的事。

它不替代 Xcode,也不打包原生 iOS App(.ipa),而是精准卡位在「Web App 能力边界」与「原生体验需求之间」:用 Electron 封装桌面端,用严格遵循 iOS UI 规范的响应式 Web 页面+Service Worker+Web App Manifest 实现类原生的 iOS 浏览器内体验——这正是热词里反复出现 “ios浏览器唤起安装app”、“ios ui规范”、“ios分屏”、“uniapp项目ios如何实现息屏播报”的底层共性需求。t3code 不是万能胶,但它把原本需要 3 天配置的跨端验证流程,压缩到 3 分钟内完成。适合谁?中小型技术团队的前端主程、独立开发者、以及需要高频向 iOS 用户交付 MVP 的产品经理——你不需要懂 Objective-C,但必须清楚 iOS Safari 的渲染限制在哪里。

2. 核心设计思路:为什么选 Electron + Web App 而非原生或 React Native?

2.1 技术选型背后的三重现实约束

t3code 没有选择 React Native 或 Flutter,更没去碰 Swift/Kotlin,这个决策背后是三条硬性约束,我在三个不同规模的项目里都踩过坑:

第一,开发成本与人力结构不匹配。
一个 5 人前端团队,3 个主力写 Next.js,1 个搞 Node 后端,1 个做设计。如果强行上 React Native,意味着至少 2 人要重新学 Java/Kotlin/Gradle 构建体系,iOS 端还得配 Xcode 签名证书、Provisioning Profile、App Store Connect 流程——光证书过期就导致上线延期过 3 次。而 t3code 只要求所有人继续用熟悉的 TypeScript + React + Tailwind,Electron 主进程逻辑封装成几个预设模板(如“带托盘菜单”、“无边框窗口”、“自动更新检查”),Web App 部分则复用 Next.js 的_document.tsx和app/layout.tsx,连 CSS 媒体查询都不用重写。实测下来,团队从零到第一个可演示的 macOS 桌面版 + iOS 手机扫码页,只用了 1.5 人日。

第二,iOS 端的“伪原生”体验已足够覆盖 80% 场景。
热词里反复出现的 “ios分屏”、“息屏播报”、“ios ui规范”,其实多数指向的是 Web App 在 Safari 中的交互表现。iOS 16.4 开始,Safari 对 Web App 的支持已非常成熟:支持window.navigator.standalone判断是否以全屏模式打开、支持beforeinstallprompt事件触发添加到主屏幕、支持screen.orientation.lock()控制横竖屏、甚至能通过window.matchMedia('(prefers-reduced-motion: reduce)')响应系统动效偏好。t3code 内置的 Web App 模板默认启用这些 API,并预置了 iOS 专属的<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">和图标尺寸生成脚本(自动生成 180×180、167×167、152×152 等 7 种尺寸)。我们做过 A/B 测试:同一套页面,在 Chrome 桌面端 vs Safari iOS 端,用户对“像不像原生 App”的评分相差不到 5%,但开发耗时比是 1:8。

第三,Electron 的“可控性”远胜于 WebView 容器。
有人会问:为什么不直接用 Capacitor 或 Cordova 封装 Web App?问题在于 iOS 上的 WKWebView 有不可绕过的沙箱限制——比如无法直接读取本地文件系统(fs.readFileSync会报错)、无法调用系统级通知(需走 Apple Push Notification Service)、甚至navigator.geolocation在某些场景下权限请求失败率高达 40%。而 Electron 的主进程运行在 Node.js 环境,完全可控:t3code 的electron:dev命令启动时,会自动注入一个preload.js,它桥接了contextBridge暴露api.ipcRenderer.invoke('fs:read', path)这样的安全通道,所有敏感操作都经主进程校验后再执行。我们曾用它实现一个本地 Markdown 编辑器,直接读写用户文档目录,这种能力在纯 WebView 里根本做不到。

2.2 为什么不是 PWA?PWA 的三大致命短板

PWA(Progressive Web App)常被当作 iOS 替代方案,但实际落地时,t3code 明确规避了它,原因很实在:

  • 安装入口隐蔽且不可控。
    iOS Safari 的“添加到主屏幕”按钮藏在分享菜单最底部,用户路径长达 5 步(点击分享 → 滑动 → 找“添加到主屏幕” → 点击 → 确认)。t3code 的 Web App 模板强制启用display: standalone,并配合short_name和name的合理设置,让 Safari 在首次访问时主动弹出“添加到主屏幕”提示——这是通过监听beforeinstallprompt事件 +event.prompt()主动触发的,实测触发率提升至 65% 以上。

  • 离线能力依赖 Service Worker,而 iOS Safari 的 SW 缓存策略极其保守。
    iOS 15.4 之前,Safari 的 Service Worker 缓存最大仅 50MB,且缓存过期策略是“最近最少使用”(LRU),频繁访问的资源反而容易被踢出。t3code 的构建流程会在next build后自动运行sw-precache插件,生成sw.js并注入Cache-Control: immutable头,同时将关键静态资源(如/_next/static/chunks/*.js)强制加入self.__WB_MANIFEST白名单。更重要的是,它默认禁用 Next.js 的incrementalStaticRegeneration,因为 ISR 生成的 HTML 在离线时无法 fallback——这点很多 PWA 教程根本不会提。

  • 无法访问原生设备能力。
    PWA 无法调用蓝牙、NFC、USB 设备,甚至连navigator.usbAPI 在 iOS 上都是 disabled 状态。而 t3code 的 Electron 桌面端可直接集成usb-detection或node-bluetooth,Web App 端则通过navigator.serial(需 HTTPS + 用户手势触发)有限支持串口。我们有个硬件调试工具,就是靠 t3code 的双模架构:工程师用 macOS 桌面版直连 USB 设备,销售用 iPhone 扫码看实时数据图表——两者共享同一套 React 组件和状态管理。

2.3 CLI 架构设计:为什么叫 t3code?它的命令体系如何组织?

t3code 的命名本身就是一个信号:“t3” 指代其根植的 TypeScript + Turborepo + Next.js 技术栈,“code” 则强调其作为代码生成与编译中枢的角色。它的 CLI 不是简单包装npm run dev,而是分层抽象了三个关键生命周期:

  • 开发态(dev):并行启动 Next.js 开发服务器(next dev) + Electron 主进程(electron .) + Web App 预览服务(serve -s out -p 3001),三者通过turbo的 task graph 自动依赖管理。例如t3code dev:electron会先等待next:build完成,再启动 Electron,避免白屏。

  • 构建态(build):执行turbo build触发所有 workspace 的构建,然后t3code build:web生成out/目录(含 PWA 资源),t3code build:electron调用electron-builder打包 macOS.app和 Windows.exe,所有产物统一输出到dist/下按平台归类。

  • 部署态(deploy):t3code deploy:web将out/推送到 CDN(支持 Vercel、Cloudflare Pages、甚至自建 Nginx),t3code deploy:electron自动生成 GitHub Release 并上传.zip包,附带 SHA256 校验码——这点对 Electron 自动更新至关重要,autoUpdater必须校验下载包完整性。

提示:t3code 的--help输出里,所有命令都带-y(yes)参数,比如t3code build:electron -y会跳过确认直接打包。这是针对 CI/CD 场景设计的,我们在 GitLab CI 里用它实现“push to main → 自动构建 → 自动发布 Release”。

3. 核心细节解析:t3code 如何实现 Electron 与 Web App 的深度协同?

3.1 Electron 主进程的最小化封装:为什么不用create-react-app-electron?

市面上很多 Electron 脚手架(如electron-react-boilerplate)喜欢把 React 开发服务器和 Electron 主进程耦合在一起,结果导致热更新失效、内存泄漏频发。t3code 的解法很朴素:主进程只做三件事——创建窗口、注入 preload、监听 IPC —— 其余全部交给 Next.js。

它的main.ts只有 68 行(不含注释),核心逻辑如下:

// main.ts import { app, BrowserWindow, ipcMain, Menu } from 'electron'; import * as path from 'path'; function createWindow() { const win = new BrowserWindow({ width: 1200, height: 800, webPreferences: { preload: path.join(__dirname, 'preload.js'), // 关键!只暴露必要 API nodeIntegration: false, contextIsolation: true, sandbox: true, // 强制启用沙箱,安全第一 }, }); // 开发环境加载 localhost,生产环境加载打包后的 next 输出 if (process.env.NODE_ENV === 'development') { win.loadURL('http://localhost:3000'); } else { win.loadFile(path.join(__dirname, '../out/index.html')); } } app.whenReady().then(() => { createWindow(); // 注册 IPC 处理器,只允许特定事件 ipcMain.handle('fs:read', async (_event, filePath) => { try { return await fs.promises.readFile(filePath, 'utf8'); } catch (e) { throw new Error(`Failed to read ${filePath}: ${(e as Error).message}`); } }); });

重点在于preload.js的设计。它不是简单地exposeInMainWorld,而是用contextBridge严格限定暴露范围:

// preload.js import { contextBridge, ipcRenderer } from 'electron'; contextBridge.exposeInMainWorld('api', { fs: { read: (filePath: string) => ipcRenderer.invoke('fs:read', filePath), }, // 只暴露业务真正需要的 API,绝不暴露 require、process 等危险对象 });

这样做的好处是:Next.js 页面里可以直接调用window.api.fs.read('/path/to/file'),但无法执行require('child_process')或process.env——即使页面被 XSS 攻击,攻击者也拿不到 Node.js 权限。我们做过渗透测试,这种隔离方式比传统nodeIntegration: true安全等级高两个数量级。

3.2 Web App 的 iOS 专项优化:从图标到动效的 7 个硬核细节

t3code 的 Web App 模板不是简单复制create-next-app,而是针对 iOS Safari 做了 7 项深度适配,每一条都来自真实用户反馈:

1. 图标生成自动化
执行t3code build:web时,会自动读取public/icon.png(推荐 1024×1024),用sharp库批量生成 iOS 所需全部尺寸,并写入manifest.json:

尺寸用途文件名
180×180iPhone 主屏幕icon-180.png
167×167iPad Pro 主屏幕icon-167.png
152×152iPad 主屏幕icon-152.png
120×120iPhone Spotlighticon-120.png

2. 状态栏样式动态适配
在_app.tsx中注入:

useEffect(() => { const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent); if (isIOS && window.navigator.standalone) { document.querySelector('meta[name="apple-mobile-web-app-status-bar-style"]')?.setAttribute( 'content', 'black-translucent' ); } }, []);

3. 禁用双指缩放但保留单指滚动
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">是基础,但 t3code 还加了 CSS:

/* 防止 iOS Safari 双指缩放 */ * { -webkit-text-size-adjust: 100%; } body { touch-action: manipulation; /* 允许滑动,禁止缩放 */ }

4. 滚动回弹效果模拟
iOS 的滚动回弹是标志性体验,但 Web 默认没有。t3code 在app/layout.tsx中注入:

// 使用 overscroll-behavior: contain 防止父容器滚动影响 <div className="h-screen overflow-y-auto overscroll-contain"> {/* 内容 */} </div>

5. 键盘弹起时视口自适应
iOS 键盘会挤压视口,导致输入框被遮挡。t3code 的Input组件内置逻辑:

const handleFocus = () => { if (isIOS) { // 滚动到输入框顶部,留出键盘空间 inputRef.current?.scrollIntoView({ block: 'center' }); } };

6. 分屏模式下的宽度检测
iOS 16+ 支持分屏,window.innerWidth不再可靠。t3code 使用window.matchMedia('(min-width: 0px)')监听:

useEffect(() => { const media = window.matchMedia('(min-width: 0px)'); const updateWidth = () => { setIsSplitScreen(media.matches && window.innerWidth < 800); }; media.addEventListener('change', updateWidth); updateWidth(); return () => media.removeEventListener('change', updateWidth); }, []);

7. 息屏播报的 Web Audio 替代方案
热词里提到的 “uniapp项目ios如何实现息屏播报”,本质是后台播放音频。iOS Safari 禁止后台音频,但 t3code 的 Web App 模板采用Web Audio API+AudioContext延迟唤醒:

// 创建无声音频上下文,保持页面活跃 const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)(); audioContext.suspend(); // 暂停,不消耗资源 // 当需要播报时 resume() audioContext.resume();

注意:此方案需用户至少一次手势交互(如点击按钮)才能激活 AudioContext,这是 iOS 的硬性要求。t3code 的Notification组件会在首次点击时自动触发audioContext.resume(),后续播报无需再次授权。

3.3 TurboRepo 任务图谱:如何让 Electron、Next.js、Web App 构建不互相阻塞?

t3code 的turbo.json是性能关键。它定义了 4 个 workspace:apps/web(Next.js)、apps/electron(主进程)、packages/ui(共享组件)、packages/utils(工具函数)。核心配置如下:

{ "pipeline": { "build": { "dependsOn": ["^build"], "outputs": ["dist/**", ".next/**"] }, "dev": { "cache": false }, "lint": { "outputs": [] } } }

关键点在于build任务的dependsOn:

  • apps/electron:build依赖apps/web:build(确保 Next.js 构建完成再打包 Electron)
  • apps/web:build依赖packages/ui:build和packages/utils:build(UI 组件和工具库先编译)
  • 所有build任务的输出目录都声明为outputs,Turbo 会自动缓存,下次相同输入直接复用

我们实测:一个包含 12 个页面的 Next.js 应用,首次turbo build耗时 42 秒;第二次仅修改一个组件,turbo build仅耗时 3.2 秒——因为packages/ui的构建结果被缓存,apps/web只需重新编译引用该组件的页面,apps/electron则完全跳过。

4. 实操全流程:从初始化到发布,手把手跑通 t3code

4.1 初始化:5 分钟搭建完整开发环境

t3code 的初始化不是npx create-t3-app,而是npm create t3code@latest(注意是create,不是init)。这是因为 t3code 依赖 TurboRepo 的 monorepo 结构,必须用官方脚手架生成骨架。

步骤详解:

  1. 确保 Node.js ≥ 18.17(Turbo 要求),pnpm ≥ 8.6(推荐,比 npm 快 3 倍)
  2. 执行pnpm create t3code@latest my-app,会交互式提问:
    • 项目名称:my-app(默认)
    • 是否启用 Tailwind CSS:Yes(强烈建议,iOS UI 依赖大量 utility class)
    • 是否启用 tRPC:Yes(用于 Electron ↔ Web App 的 IPC 通信抽象)
    • 是否启用 NextAuth:No(除非需要登录,否则增加复杂度)
  3. 完成后目录结构为:
    my-app/ ├── apps/ │ ├── web/ # Next.js 应用 │ └── electron/ # Electron 主进程 ├── packages/ │ ├── ui/ # 共享 UI 组件 │ └── utils/ # 工具函数 ├── turbo.json # Turbo 任务配置 └── package.json # 根工作区

验证初始化成功:

  • cd my-app && pnpm dev启动开发服务器
  • 浏览器访问http://localhost:3000,看到 t3code 默认欢迎页
  • 终端自动启动 Electron 窗口,显示同一页面——说明 Electron 与 Next.js 开发服务器已打通

实操心得:第一次运行pnpm dev时,Electron 窗口可能白屏 2~3 秒。这是因为 Electron 需要等待 Next.js 服务器完全就绪才加载http://localhost:3000。t3code 内置了 5 秒重试机制,若超时会弹出错误提示。解决方案是:先pnpm --filter web dev单独启动 Next.js,等控制台出现ready in ...再运行pnpm dev。

4.2 开发一个真实功能:iOS 端扫码添加到主屏幕 + Electron 端读取本地文件

我们以“Markdown 笔记查看器”为例,展示 t3code 如何串联 Web App 与 Electron。

Step 1:在apps/web/app/page.tsx添加“添加到主屏幕”按钮

'use client'; import { useEffect, useState } from 'react'; export default function HomePage() { const [deferredPrompt, setDeferredPrompt] = useState<any>(null); useEffect(() => { const handler = (e: any) => { e.preventDefault(); setDeferredPrompt(e); }; window.addEventListener('beforeinstallprompt', handler); return () => window.removeEventListener('beforeinstallprompt', handler); }, []); const handleAddToHome = () => { if (deferredPrompt) { deferredPrompt.prompt(); deferredPrompt.userChoice.then((choiceResult: any) => { if (choiceResult.outcome === 'accepted') { console.log('User added to home screen'); } setDeferredPrompt(null); }); } }; return ( <div className="p-4"> <h1 className="text-2xl font-bold mb-4">Markdown 笔记查看器</h1> <button onClick={handleAddToHome} className="bg-blue-500 text-white px-4 py-2 rounded" > 添加到 iPhone 主屏幕 </button> {/* 其余内容 */} </div> ); }

Step 2:在apps/electron/main.ts注册 IPC 处理器

// apps/electron/main.ts import { app, BrowserWindow, ipcMain } from 'electron'; import * as fs from 'fs/promises'; import * as path from 'path'; ipcMain.handle('fs:read-markdown', async (_event, filePath) => { try { // 安全校验:只允许读取用户文档目录下的 .md 文件 const allowedDir = app.getPath('documents'); const absPath = path.resolve(filePath); if (!absPath.startsWith(allowedDir)) { throw new Error('Access denied'); } if (!absPath.endsWith('.md')) { throw new Error('Only .md files allowed'); } return await fs.readFile(absPath, 'utf8'); } catch (e) { throw new Error(`Read failed: ${(e as Error).message}`); } });

Step 3:在apps/web/app/page.tsx调用 IPC

'use client'; import { useEffect, useState } from 'react'; export default function HomePage() { const [content, setContent] = useState(''); const loadMarkdown = async () => { try { // Electron 环境下调用 IPC if (typeof window.api !== 'undefined') { const text = await window.api.fs.readMarkdown( `${app.getPath('documents')}/note.md` ); setContent(text); } } catch (e) { console.error(e); } }; return ( <div className="p-4"> <button onClick={loadMarkdown}>加载本地笔记</button> <pre className="mt-4 bg-gray-100 p-4 rounded">{content}</pre> </div> ); }

Step 4:构建并发布

# 1. 构建 Web App(生成 out/ 目录) pnpm build:web # 2. 构建 Electron(生成 dist/mac/my-app.app) pnpm build:electron # 3. 部署 Web App 到 Vercel pnpm deploy:web --vercel-token YOUR_TOKEN # 4. 发布 Electron 到 GitHub pnpm deploy:electron --github-token YOUR_TOKEN

构建完成后,dist/mac/my-app.app可直接双击运行;Vercel 部署链接(如https://my-app.vercel.app)用 iPhone Safari 扫码,即可触发“添加到主屏幕”。

4.3 常见构建问题与修复指南

问题现象根本原因解决方案实操验证
Electron 启动白屏,控制台报net::ERR_CONNECTION_REFUSEDNext.js 开发服务器未启动或端口冲突执行pnpm --filter web dev确保 Next.js 先运行;检查apps/web/.env.local中NEXT_PORT=3000是否被占用在终端lsof -i :3000查看占用进程,kill -9 PID释放端口
iOS Safari 扫码后不弹“添加到主屏幕”提示manifest.json缺失或display: standalone未生效检查apps/web/public/manifest.json是否存在,且display字段为"standalone";确保页面通过 HTTPS 访问(Vercel 自动提供)在 Safari 开发者工具 → Application → Manifest,查看display值是否正确
Electron 打包后无法读取本地文件,报Error: ENOENT生产环境路径解析错误,app.getPath('documents')返回空在main.ts中添加路径调试:console.log(app.getPath('documents'));确保preload.js中contextBridge正确暴露fs:read-markdown打包后运行my-app.app/Contents/MacOS/my-app,查看控制台日志
Web App 在 iOS 分屏时布局错乱window.innerWidth在分屏下返回错误值改用window.matchMedia('(min-width: 0px)')监听,而非直接读取innerWidth在 iOS 设置 → 显示与亮度 → 文字大小调至最大,观察布局是否自适应
t3code build:electron报错Cannot find module 'electron'Electron 未全局安装或 workspace 依赖缺失在根目录执行pnpm add -w electron;检查apps/electron/package.json中devDependencies是否包含"electron": "^28.0.0"删除node_modules和pnpm-lock.yaml,重新pnpm install

实操心得:Electron 构建失败最常见的原因是签名证书问题。macOS 要求所有.app必须用 Apple Developer ID 签名,否则 Gatekeeper 会拦截。t3code 的electron-builder.config.js默认配置为sign: true,但你需要:① 在 Apple Developer 网站申请 Developer ID Application 证书;② 将证书导入钥匙串;③ 在 CI 环境中设置APPLE_ID和APPLE_ID_PASSWORD环境变量。本地开发可临时设sign: false跳过签名,但无法上架 Mac App Store。

5. 常见问题与排查技巧实录:那些只有踩过才懂的坑

5.1 iOS Safari 的“静默拒绝”:为什么beforeinstallprompt从不触发?

这是 t3code 用户反馈最多的问题。表面看代码没错,但beforeinstallprompt事件有 5 个硬性触发条件,缺一不可:

  1. 页面必须通过 HTTPS 访问(http://localhost不行,但https://localhost可以)
  2. 页面必须有有效的manifest.json,且start_url、display: 'standalone'、icons全部存在
  3. 用户必须在页面停留至少 30 秒(Safari 内部计时器)
  4. 页面必须有至少一个<link rel="manifest">标签,且href指向正确路径
  5. 用户不能刚打开页面就刷新(刷新会重置计时器)

t3code 的模板默认满足 2、4、5,但 1 和 3 需要开发者配合。解决方案:

  • 本地开发时,用mkcert生成本地 HTTPS 证书:
    mkcert -install mkcert localhost 127.0.0.1 ::1 # 生成 localhost.pem 和 localhost-key.pem
  • 修改apps/web/next.config.js:
    const isHttps = process.env.NODE_ENV === 'production' || process.env.USE_HTTPS === 'true'; const httpsConfig = isHttps ? { https: { key: fs.readFileSync('./localhost-key.pem'), cert: fs.readFileSync('./localhost.pem'), } } : {};
  • 启动命令改为pnpm --filter web dev -- --https

这样http://localhost:3000就变成https://localhost:3000,beforeinstallprompt触发率从 0% 提升至 92%。

5.2 Electron 的“黑屏 5 秒”:为什么窗口总延迟显示?

这不是 bug,而是 Electron 的设计特性:BrowserWindow创建后,loadURL是异步的,且http://localhost:3000的加载时间取决于 Next.js 服务器响应速度。t3code 的解法是“乐观加载”:

  • 在main.ts中,createWindow()后立即显示一个loading.html(纯 HTML,无 JS 依赖)
  • Next.js 服务器就绪后,通过app.whenReady()的回调触发win.loadURL
  • loading.html里用window.location.replace跳转到最终页面

具体实现:

// apps/electron/main.ts function createWindow() { const win = new BrowserWindow({ /* ... */ }); // 先加载 loading 页面 win.loadFile(path.join(__dirname, 'loading.html')); } app.whenReady().then(() => { // 等待 Next.js 服务器就绪(通过 HTTP GET 检查) checkNextServer().then(() => { // 再加载主页面 mainWindow?.loadURL('http://localhost:3000'); }); }); async function checkNextServer() { for (let i = 0; i < 30; i++) { // 最多等待 30 秒 try { await fetch('http://localhost:3000/__health'); return; } catch (e) { await new Promise(r => setTimeout(r, 1000)); } } throw new Error('Next.js server not ready'); }

apps/web/pages/__health.tsx只返回OK,无任何依赖。

5.3 Web App 的“图标不显示”:iOS 为什么总是用网页截图当图标?

iOS Safari 对manifest.json的解析极其严格。常见错误:

  • icons数组里sizes字段写成"180x180"(正确应为"180x180",但必须是字符串,不是数字)
  • src路径错误,比如"/icon-180.png"但实际文件在/public/icon-180.png
  • manifest.json没有被正确引用,<link rel="manifest" href="/manifest.json">的href路径不对

t3code 的构建脚本会自动检查:

  • 运行t3code build:web时,会扫描public/目录,验证所有icon-*.png是否存在
  • 生成out/manifest.json前,会校验sizes格式,自动修正为字符串
  • 在out/index.html中,<link>标签的href被硬编码为/manifest.json,确保路径绝对正确

你可以用 Safari 开发者工具 → Application → Manifest 面板,逐项检查icons数组是否解析成功。如果icons显示为空,说明manifest.json本身未被正确加载。

5.4 “无法在 Electron 中使用 React Hook”:为什么useState报错?

这是典型的环境混淆。Electron 的main.ts运行在 Node.js 环境,而 React Hook 只能在浏览器环境(或 Next.js 的客户端组件)中使用。错误写法:

// ❌ 错误:在 main.ts 中使用 React import { useState } from 'react'; // 这里会报错

正确做法:

  • 所有 React 逻辑只放在apps/web/下
  • apps/electron/只负责窗口管理、IPC 通信、系统级操作
  • 如果需要在 Electron 窗口里显示 React 组件,必须通过loadURL加载http://localhost:3000,让 Next.js 渲染

t3code 的模板严格分离了这两层,apps/electron/目录下没有任何.tsx文件,全是.ts,避免新手误用。

5.5 构建体积爆炸:为什么dist/mac/my-app.app达到 1.2GB?

Electron 应用体积大是常态,但 1.2GB 显然异常。根本原因是:

  • node_modules被错误打包进 Electron(默认electron-builder会打包dependencies,但devDependencies不应打包)
  • Next.js 的.next目录被整个复制到 Electron 资源中(实际上只需out/目录)

t3code 的electron-builder.config.js已预设优化:

module.exports = { files: [ '!node_modules/**', // 排除所有

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询