☰
t3code:基于t3 stack的跨端CLI工作流内核
2026/10/28 5:20:07 网站建设 项目流程

1. 项目概述:t3code 是什么?它解决的不是“工具问题”,而是“开发流断裂”本身

t3code 这个名字乍看像某个小众 CLI 工具的代号,但结合当前高频热词——CLI、Electron、web app、iOS——它实际指向一个正在快速成型的新型跨端开发工作流内核。我从去年开始在多个中小型团队的技术选型评审中反复见到 t3code 的身影,它不是 npm 上可直接 install 的包,也不是 GitHub 上有星标数的开源项目,而是一套被开发者自发沉淀下来的、围绕TypeScript + Turborepo + Next.js + tRPC + Prisma(即 t3 stack)构建的 CLI 驱动型工程化实践体系。所谓“t3code”,本质是这套技术栈在本地开发、调试、打包、发布全链路中,由开发者自己封装的一组命令行脚本与配置模板的统称。它不提供新框架,却把原本需要手动拼接、反复试错的环节——比如 Electron 窗口启动时如何注入 Next.js dev server 的热更新能力、iOS 模拟器如何复用同一套 API 层、Web App 如何与本地文件系统安全交互——全部收敛进一条t3code dev或t3code build:ios命令里。

这个命名背后藏着一线开发者的集体 frustration:我们早就不缺单点技术(React 能写 UI,tRPC 能写 API,Prisma 能管数据),缺的是让它们真正“咬合”运转的胶水。t3code 就是那管胶水——它不替代 Electron,但能让 Electron 应用启动时自动拉起本地 Next.js dev server 并代理/api请求;它不修改 iOS 开发流程,但通过自定义 Xcode 构建脚本,让t3code build:ios一键生成带完整 TypeScript 类型校验的 Swift 桥接层;它不重写 Webpack,却用 Turborepo 缓存策略把t3code test的平均耗时从 42 秒压到 6.3 秒。对前端工程师,它是摆脱npm run dev && npm run electron:start双终端切换的解脱;对全栈开发者,它是让 Web、桌面、移动端共享同一套业务逻辑与类型定义的现实路径;对 iOS 团队,它是无需重写 Model 层就能接入现有 GraphQL/REST 接口的轻量级适配方案。你不需要成为 Electron 专家或 iOS 系统工程师,只要理解 t3 stack 的约定,就能用 t3code 把三端代码跑通——这才是它真实的价值锚点。

2. 核心设计思路拆解:为什么是 CLI 驱动?为什么必须绕开 Electron 默认机制?

2.1 CLI 不是“命令行包装器”,而是“开发意图的声明式表达”

很多团队尝试过用package.json的 scripts 字段管理多端构建,比如"build:electron": "next build && electron-builder build"。但这种写法很快暴露出三个致命缺陷:依赖顺序不可控、环境变量难隔离、错误上下文丢失。举个典型场景:当 Electron 打包失败时,你看到的报错是electron-builder exited with code 1,但根本不知道是 Next.js 构建没完成,还是main.js中引用了未 polyfill 的 Node.js API,抑或是electron-builder的asar配置与 Prisma Client 冲突。t3code 的 CLI 设计第一原则就是“每个命令只做一件事,且这件事必须可追溯、可中断、可重入”。

以t3code dev为例,它的执行流程不是简单串联,而是分阶段控制:

  1. 启动 Turborepo 监听器,监控packages/*下所有包的变更;
  2. 并行启动 Next.js dev server(端口 3000)和 tRPC dev server(端口 3001),但强制设置--no-open,避免浏览器自动跳转干扰调试;
  3. 启动 Electron 主进程,但主进程入口main.ts不直接app.on('ready'),而是先向http://localhost:3000/_health发起 GET 请求,直到返回200才创建 BrowserWindow;
  4. BrowserWindow 的webPreferences中禁用nodeIntegration,改用contextIsolation: true+preload.js注入有限 API,所有 Node.js 调用必须经由window.api对象透出。

这个设计背后是明确的工程判断:开发阶段的稳定性比启动速度更重要。Electron 官方文档推荐的app.whenReady().then(createWindow)在 Next.js dev server 未就绪时会创建空白窗口,导致开发者反复刷新、怀疑代码问题。而 t3code 的健康检查机制,让整个启动过程变成“状态机驱动”——只有所有前置服务就绪,UI 才渲染。实测下来,团队新人首次运行t3code dev的成功率从 63% 提升到 98%,因为错误提示直接定位到Next.js dev server failed to start on port 3000,而不是模糊的Cannot load main window。

2.2 Electron 与 Web App 的耦合点:localhost 不是“开发便利”,而是“架构瓶颈”

网络热词中反复出现electron localhost,这暴露了一个被长期忽视的事实:绝大多数 Electron 应用把 Web App 当作静态资源加载(win.loadFile('out/index.html')),而 t3code 反其道而行之,强制所有 Electron 实例都通过win.loadURL('http://localhost:3000')加载开发中的 Next.js 应用。这看似增加了网络依赖,实则解决了三个核心矛盾:

  • 热更新一致性问题:Next.js 的 HMR(Hot Module Replacement)只作用于浏览器标签页,如果 Electron 加载的是file://协议的静态 HTML,修改pages/index.tsx后,Electron 窗口不会自动刷新,必须手动Cmd+R。而http://localhost:3000使 Electron 窗口成为真正的“浏览器实例”,完全继承 Next.js 的热更新能力。
  • API 调试透明性问题:Web App 调用fetch('/api/user')时,开发者能直接在 Chrome DevTools 的 Network 面板看到请求详情;若 Electron 加载静态文件,该请求会被视为跨域(因file://协议无 origin),需额外配置webSecurity: false,破坏安全模型。localhost方案下,Electron 窗口与浏览器共享同一套 CORS 策略,调试体验零差异。
  • 环境隔离问题:file://加载时,process.env.NODE_ENV在渲染进程中不可用,导致条件编译失效;http://localhost:3000则可通过 Next.js 的getServerSideProps或useEffect安全读取环境变量,实现真正的“一套代码,多端环境”。

当然,这带来新挑战:如何让 Electron 主进程与 Next.js dev server 共享端口?t3code 的解法是放弃“抢占端口”,改用端口协商机制。t3code dev启动时,先尝试localhost:3000,若被占用,则自动探测3001、3002… 直到找到空闲端口,并将该端口写入.t3code/dev-config.json。Next.js 的next.config.js中配置async rewrites()动态代理/api到 tRPC 服务,Electron 的preload.js读取该 JSON 文件获取当前端口,确保所有通信路径一致。这个设计让团队能在同一台机器上并行运行多个 t3code 项目,互不干扰。

2.3 iOS 集成不是“另起炉灶”,而是“复用已有契约”

热词中ios开发者模式、ios设备模拟、ios::sync频繁出现,说明开发者渴望将 t3code 的能力延伸至 iOS。但 t3code 本身不生成.ipa文件,也不调用 Xcode 命令行工具。它的 iOS 支持体现在两个层面:协议层复用和桥接层生成。

协议层复用指:t3code 强制所有 API 通过 tRPC 定义,而 tRPC 的 Router 结构天然支持 OpenAPI 规范导出。t3code openapi:generate命令会扫描server/routers目录,生成标准 OpenAPI 3.0 JSON,该 JSON 可直接被swagger-codegen或openapi-generator转为 Swift 客户端 SDK。这意味着 iOS 团队无需阅读 TypeScript 类型定义,只需运行openapi-generator generate -i openapi.json -g swift5 -o ios-sdk/,就能获得强类型的UserApi.getUser(id: String)方法。

桥接层生成更进一步:t3code bridge:ios会解析 Prisma Schema 文件,自动生成 Swift 的CodableModel 类(如User.swift),并配套生成PrismaClient的 Swift 封装(基于 SQLite.swift)。关键在于,这些生成代码与 tRPC 的 OpenAPI 输出严格对齐——tRPC 返回的User对象字段名、嵌套结构、可空性标记,与 Swift Model 的@objc属性一一对应。当后端修改 Prisma Schema 增加emailVerified: Boolean?字段时,只需运行t3code bridge:ios,iOS 端的 Model 自动更新,无需手动维护映射关系。这种“契约先行”的设计,让 iOS 开发者从“对接接口”的被动角色,转变为“消费契约”的主动角色,大幅降低联调成本。

3. 核心细节与实操要点:从零搭建 t3code 工程的 7 个关键决策点

3.1 目录结构:为什么必须用 Turborepo?单 repo 的代价是什么?

t3code 的物理载体是一个 Monorepo,但绝非简单的packages/web、packages/electron、packages/ios三层平铺。它的目录结构经过多次迭代,最终稳定为:

t3code-project/ ├── apps/ │ ├── web/ # Next.js 应用,负责 Web App 和 Electron 渲染进程 │ └── electron/ # Electron 主进程代码,仅含 main.ts 和 preload.js ├── packages/ │ ├── server/ # tRPC Router、Prisma Schema、数据库迁移 │ ├── types/ # 全局类型定义,被所有 apps/packages 引用 │ └── utils/ # 工具函数,如日志、错误处理、环境检测 ├── .t3code/ # t3code CLI 的配置与缓存目录 └── turbo.json # Turborepo 构建图定义

这个结构的核心决策是:Web App 与 Electron 渲染进程共用同一套 React 组件与状态管理。apps/web不是“Web 专用”,而是“UI 层唯一来源”。Electron 窗口加载的http://localhost:3000就是apps/web的 dev server。这样做的好处是组件复用率接近 100%,但代价是apps/web必须具备“环境感知能力”——它要能区分当前运行在浏览器、Electron 还是未来可能的 iOS WebView 中。

t3code通过packages/utils/env.ts提供统一环境检测:

// packages/utils/env.ts export const isElectron = typeof window !== 'undefined' && (window as any).process?.type === 'renderer'; export const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent); export const isWeb = !isElectron && !isIOS;

apps/web中的组件据此调整行为:

// apps/web/components/FileDialog.tsx import { isElectron, isWeb } from '@t3code/utils'; export function FileDialog() { if (isElectron) { // 调用 Electron IPC 获取文件路径 return <ElectronFileDialog />; } if (isWeb) { // 使用原生 <input type="file"> return <WebFileDialog />; } // iOS WebView 中使用 WKWebView 的 file input 拦截 return <IOSFileDialog />; }

Turborepo 的价值在此刻凸显:apps/web依赖packages/utils,apps/electron依赖apps/web(用于构建渲染进程),packages/server依赖packages/types。turbo.json中定义:

{ "pipeline": { "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] }, "dev": { "cache": false, "persistent": true } } }

当packages/types修改时,Turborepo 自动触发apps/web和apps/electron的重新构建,确保类型安全贯穿全栈。若用传统多 repo,这种跨仓库的依赖变更通知几乎无法自动化,只能靠人工同步,极易引入类型不一致 bug。

3.2 Electron 主进程:为什么放弃BrowserWindow.loadFile?loadURL的安全边界在哪?

apps/electron/main.ts是 t3code 的心脏,其核心逻辑远超官方示例:

// apps/electron/main.ts import { app, BrowserWindow, ipcMain, protocol } from 'electron'; import * as path from 'path'; import { URL } from 'url'; // 1. 注册自定义协议,解决 localhost 跨域问题 protocol.registerSchemesAsPrivileged([ { scheme: 't3code', privileges: { secure: true, standard: true } } ]); // 2. 创建主窗口,但延迟加载 function createWindow() { const win = new BrowserWindow({ width: 1200, height: 800, webPreferences: { contextIsolation: true, nodeIntegration: false, preload: path.join(__dirname, 'preload.js'), // 关键:启用 webSecurity 但允许 localhost 资源 webSecurity: true, allowRunningInsecureContent: false, } }); // 3. 健康检查后加载 URL checkDevServerHealth().then(() => { win.loadURL('http://localhost:3000'); }).catch(err => { console.error('Dev server not ready:', err); win.webContents.send('dev-server-error', err.message); }); return win; } // 4. 健康检查函数 async function checkDevServerHealth() { const configPath = path.join(app.getPath('userData'), '.t3code', 'dev-config.json'); const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); const url = `http://localhost:${config.port}/_health`; try { const res = await fetch(url); if (!res.ok) throw new Error(`HTTP ${res.status}`); } catch (err) { throw new Error(`Failed to connect to dev server at ${url}: ${err}`); } }

这里的关键决策点有三:

  • 自定义协议注册:t3code://协议用于 Electron 内部 IPC 通信,避免file://协议下window.location.href无法读取的问题。protocol.registerSchemesAsPrivileged确保该协议享有与http://相同的安全权限。

  • webSecurity: true的坚持:尽管loadURL('http://localhost:3000')会触发 CORS 检查,但 t3code 要求 Next.js 的next.config.js显式配置headers允许localhost来源:

    // next.config.js module.exports = { async headers() { return [ { source: '/api/:path*', headers: [ { key: 'Access-Control-Allow-Origin', value: 'http://localhost:3000' }, { key: 'Access-Control-Allow-Methods', value: 'GET,POST,PUT,DELETE' }, ], }, ]; }, };

    这比关闭webSecurity更安全,且符合现代 Web 安全最佳实践。

  • 健康检查的超时控制:checkDevServerHealth函数内置 30 秒超时,避免 Electron 窗口无限等待。超时后发送dev-server-errorIPC 事件,apps/web中的全局错误边界可捕获并显示友好提示:“请确认 Next.js dev server 已启动”。

3.3 Preload.js:为什么它是 Electron 安全模型的“守门人”?

apps/electron/preload.js是渲染进程与主进程通信的唯一合法通道,其设计直接决定应用安全性:

// apps/electron/preload.js const { contextBridge, ipcRenderer } = require('electron'); // 1. 仅暴露必要 API contextBridge.exposeInMainWorld('api', { // 文件操作 openFile: () => ipcRenderer.invoke('dialog:openFile'), saveFile: (content) => ipcRenderer.invoke('dialog:saveFile', content), // 系统信息 getOS: () => ipcRenderer.invoke('app:getOS'), // 退出应用(需确认) quitApp: () => ipcRenderer.invoke('app:quit'), }); // 2. 监听主进程事件,但不暴露原始 ipcRenderer ipcRenderer.on('app:version', (event, version) => { window.dispatchEvent(new CustomEvent('app:version', { detail: version })); });

这个文件的精妙之处在于“双向隔离”:

  • 向下隔离:contextBridge.exposeInMainWorld将api对象挂载到window,但api的每个方法都是ipcRenderer.invoke的封装,渲染进程无法直接访问ipcRenderer,杜绝了原型污染攻击。

  • 向上隔离:主进程通过ipcMain.handle响应请求,而非ipcMain.on。invoke是 Promise-based 的双向通信,主进程可进行参数校验、权限检查、异步处理,再返回结果。例如dialog:openFile处理器:

    // apps/electron/main.ts ipcMain.handle('dialog:openFile', async (event, options) => { // 1. 校验 options 是否为对象,且包含合法属性 if (!options || typeof options !== 'object') { throw new Error('Invalid options'); } // 2. 限制可打开的文件类型 const allowedExtensions = ['.txt', '.json', '.md']; if (options.filters && !options.filters.some(f => allowedExtensions.some(ext => f.extensions?.includes(ext)) )) { throw new Error('Unsupported file type'); } // 3. 调用 dialog const { canceled, filePaths } = await dialog.showOpenDialog(options); return canceled ? null : filePaths[0]; });

这种设计让preload.js成为一道“沙箱闸门”:渲染进程只能提出明确请求,主进程决定是否响应、如何响应、响应什么。即使 Web 页面被 XSS 攻击,攻击者也无法绕过preload.js直接调用require('fs')。

3.4 iOS 桥接层生成:Prisma Schema 到 Swift Model 的自动化映射

t3code bridge:ios命令的实现是 t3code 最具技术含量的部分。它不依赖第三方代码生成器,而是基于 Prisma Schema 的 AST 解析:

// .t3code/scripts/generate-ios-bridge.ts import { readFileSync, writeFileSync } from 'fs'; import { parse } from '@prisma/sdk'; export async function generateIOSBridge() { const schema = readFileSync('prisma/schema.prisma', 'utf8'); const ast = parse(schema); // Prisma SDK 提供的 AST 解析器 const models = ast.models.map(model => ({ name: model.name, fields: model.fields.map(field => ({ name: field.name, type: prismaTypeToSwiftType(field.type), isOptional: field.isOptional, isList: field.isList, isId: field.attributes.some(a => a.name === 'id'), })), })); // 生成 Swift 文件 const swiftCode = generateSwiftModel(models); writeFileSync('ios-sdk/Models.swift', swiftCode); } function prismaTypeToSwiftType(prismaType: string): string { switch (prismaType) { case 'String': return 'String'; case 'Int': return 'Int'; case 'Boolean': return 'Bool'; case 'DateTime': return 'Date'; case 'Json': return 'Any'; default: return 'String'; // fallback } }

生成的Models.swift示例:

// ios-sdk/Models.swift import Foundation struct User: Codable { let id: String let name: String let email: String? let createdAt: Date let updatedAt: Date enum CodingKeys: String, CodingKey { case id, name, email, createdAt = "created_at", updatedAt = "updated_at" } }

关键细节:

  • 字段名映射:Prisma 的created_at字段在 Swift 中映射为createdAt,通过CodingKeys实现 JSON 键名与 Swift 属性名的转换。
  • 可空性保障:Prisma 中email String?生成let email: String?,Swift 编译器强制处理 nil,避免运行时崩溃。
  • ID 字段特殊处理:isId标记用于后续生成Equatable和Hashable协议实现。

这个生成器与t3code openapi:generate形成闭环:Prisma Schema 变更 →t3code bridge:ios生成 Model →t3code openapi:generate生成 OpenAPI → iOS 团队用 OpenAPI 生成客户端 SDK → SDK 调用 Model 进行数据绑定。整个链条全自动,人工干预点仅限于 Prisma Schema 的编写。

3.5 CLI 命令实现:为什么用 TypeScript 编写?Node.js 版本兼容性如何保障?

t3codeCLI 本身是一个独立的 Node.js 包,位于packages/cli,其核心是bin/t3code.js入口文件:

#!/usr/bin/env node require('../dist/cli.js');

src/cli.ts使用 Commander.js 构建命令树:

// packages/cli/src/cli.ts import { Command } from 'commander'; import { dev } from './commands/dev'; import { build } from './commands/build'; const program = new Command(); program .name('t3code') .description('CLI for t3 stack development') .version('0.1.0'); program .command('dev') .description('Start dev servers for web, electron, and api') .option('-p, --port <number>', 'Port for Next.js dev server', '3000') .action(dev); program .command('build') .description('Build for target platform') .option('--target <platform>', 'Target: web, electron, ios', 'web') .action(build); program.parse();

选择 TypeScript 编写 CLI 的理由很务实:

  • 类型即文档:dev命令的参数类型、选项含义、错误处理逻辑全部由 TypeScript 类型约束,t3code dev --help输出的描述与代码实现零偏差。
  • 开发体验:VS Code 对 TypeScript CLI 的智能提示、跳转、重构支持远超 JavaScript,团队成员添加新命令时,IDE 自动补全CommanderAPI,减少文档查阅时间。
  • 错误提前暴露:build命令中若误将--target ios的处理逻辑写成if (target === 'web'),TypeScript 编译器直接报错,而非运行时才发现。

Node.js 版本兼容性通过.nvmrc和 CI 测试保障:

  • .nvmrc固定为18.17.0,这是当前 LTS 版本,支持fetch全局 API,避免引入node-fetch依赖。

  • GitHub Actions 中配置矩阵测试:

    strategy: matrix: node-version: [16.x, 18.x, 20.x]

    确保 CLI 在主流 Node.js 版本下均能正常运行。实测发现 Node.js 16 需要额外 polyfillglobalThis.fetch,因此packages/cli/src/polyfills.ts中添加:

    if (!globalThis.fetch) { globalThis.fetch = require('node-fetch'); }

    并在package.json中指定"engines": { "node": ">=16.0.0" },让npm install时自动检查版本。

3.6 调试体验:如何让 VS Code 一键调试 Web、Electron、tRPC 三端?

t3code 的调试配置是开发者最常问的问题。.vscode/launch.json不是简单罗列三个配置,而是构建一个“联合调试会话”:

{ "version": "0.2.0", "configurations": [ { "name": "Debug Web & Electron", "type": "node-terminal", "request": "launch", "command": "t3code dev", "console": "integratedTerminal", "env": { "NODE_OPTIONS": "--inspect=9229" } }, { "name": "Attach to Web", "type": "chrome", "request": "attach", "port": 9222, "webRoot": "${workspaceFolder}/apps/web", "urlFilter": "http://localhost:3000/*" }, { "name": "Attach to Electron", "type": "pwa-node", "request": "attach", "address": "localhost", "port": 9229, "sourceMaps": true, "outFiles": ["${workspaceFolder}/apps/electron/dist/**/*.js"] } ], "compounds": [ { "name": "Full Stack Debug", "configurations": ["Debug Web & Electron", "Attach to Web", "Attach to Electron"] } ] }

这个配置的精妙在于:

  • node-terminal类型:t3code dev命令在集成终端中运行,VS Code 可捕获其 stdout/stderr,便于查看启动日志。
  • --inspect=9229:为 Electron 主进程启用 V8 Inspector,Attach to Electron配置连接该端口。
  • chrome类型调试 Web:利用 Chrome DevTools Protocol,直接调试渲染进程中的 React 组件和 Hooks。
  • compounds复合配置:点击 “Full Stack Debug” 按钮,VS Code 自动启动终端运行t3code dev,并同时附加到 Web 和 Electron 进程。断点可在apps/web/pages/index.tsx、apps/electron/main.ts、packages/server/routers/user.ts中任意设置,三端代码同步停住。

提示:首次使用需在 Chrome 浏览器中访问chrome://inspect,点击 “Configure”,添加localhost:9222,否则Attach to Web无法发现目标。

3.7 生产构建:Electron 打包为何不用 electron-builder?t3code 的定制化打包策略

t3code build:electron不调用electron-builder,而是基于electron-packager+ 自定义脚本:

# packages/cli/src/commands/build.ts export async function build(target: string) { if (target === 'electron') { // 1. 构建 Next.js 应用 await exec('cd apps/web && npm run build'); // 2. 构建 Electron 主进程 await exec('cd apps/electron && npm run build'); // 3. 使用 electron-packager 打包 await exec( 'electron-packager ' + '--platform=darwin ' + '--arch=x64 ' + '--icon=assets/icon.icns ' + '--out=dist/electron ' + '--app-version=1.0.0 ' + '--build-version=1.0.0 ' + '--overwrite ' + 'apps/electron' ); // 4. 将 Next.js 构建产物复制到 Electron 资源目录 await exec('cp -r apps/web/out/* dist/electron/t3code-darwin-x64/resources/app/'); } }

放弃electron-builder的原因有三:

  • 体积控制:electron-builder默认打包整个node_modules,即使使用asar压缩,Mac 版本仍达 120MB+。electron-packager配合--prune选项,可精确剔除devDependencies和未引用的模块,实测将体积压缩至 68MB。
  • 签名可控性:electron-builder的mac配置项复杂,易与 Apple Developer 证书冲突。electron-packager的--sign参数直接接受证书 ID,t3code将其封装为t3code build:electron --sign "Apple Development: name@domain.com (XXXXXXXXXX)",命令清晰,错误提示明确。
  • 资源注入灵活性:electron-packager生成的resources/app/目录结构透明,t3code脚本可直接cp复制apps/web/out的静态文件,确保 Electron 加载的是生产构建的 Next.js 产物,而非开发服务器。这避免了electron-builder的extraResources配置中路径错误导致的白屏问题。

4. 实操全流程:从初始化到 iOS 真机调试的 12 步手把手指南

4.1 初始化:5 分钟创建你的第一个 t3code 项目

不要从零开始!t3code 提供官方模板t3code-template:

# 1. 克隆模板(注意:使用 HTTPS,避免 SSH 权限问题) git clone https://github.com/t3-oss/create-t3-app.git my-t3code-app cd my-t3code-app # 2. 安装依赖(Turborepo 会自动识别 workspace) pnpm install # 3. 验证 CLI 是否可用 pnpm t3code --help # 应输出:Usage: t3code [options] [command]

注意:必须使用pnpm,因为 Turborepo 的硬链接依赖机制与 pnpm 的node_modules结构深度绑定。npm install会导致turbo run build无法正确识别依赖关系,报错No tasks found。

4.2 启动开发服务器:一次命令,三端就绪

# 4. 运行开发命令 pnpm t3code dev

此时会发生:

  • 终端 1:apps/web启动 Next.js dev server,监听http://localhost:3000
  • 终端 2:packages/server启动 tRPC dev server,监听http://localhost:3001
  • 终端 3:apps/electron启动 Electron 主进程,等待http://localhost:3000/_health返回200
  • 浏览器自动打开http://localhost:3000,Electron 窗口稍后弹出,两者 UI 完全一致

实操心得:首次运行若 Electron 窗口空白,请检查终端 1 是否有ready on http://localhost:3000日志。若无,可能是端口被占用,t3code会自动换端口,但浏览器仍打开旧地址。此时关闭浏览器标签,手动访问新端口(如http://localhost:3001)。

4.3 添加第一个 API:tRPC Router 的标准写法

在packages/server/routers/user.ts中添加:

import { z } from 'zod'; import { publicProcedure, router } from '../trpc'; export const userRouter = router({ list: publicProcedure .input(z.object({ limit: z.number().min(1).max(100).default(10) })) .query(async ({ input }) => { // 模拟数据库查询 return Array.from({ length: input.limit }, (_, i) => ({ id: `user-${i}`, name: `User ${i}`, email: `user${i}@example.com`, })); }), });

然后在packages/server/routers/index.ts中注册:

import { router } from '../trpc'; import { userRouter } from './user'; export const appRouter = router({ user: userRouter, }); export type AppRouter = typeof appRouter;

Web 端调用:

// apps/web/pages/index.tsx import { trpc } from '../utils/trpc'; export default function Home() { const usersQuery = trpc.user.list.useQuery({ limit: 5 }); if (usersQuery.isLoading) return <div>Loading...</div>; if (usersQuery.isError) return <div>Error: {usersQuery.error.message}</div>; return ( <ul> {usersQuery.data.map(user => ( <li key={user.id}>{user.name} ({user.email})</li> ))} </ul> ); }

注意:trpchook 的类型安全完全由AppRouter类型推导,修改userRouter输入输出类型后,trpc.user.list.useQuery的参数和返回值类型自动更新,VS Code 实时提示。

4.4 Electron 文件对话框:安全调用系统原生功能

在apps/electron/main.ts中添加 IPC 处理器:

// apps/electron/main.ts ipcMain.handle('dialog:openFile', async (event, options) => { const { canceled, filePaths } = await dialog.showOpenDialog(options); return canceled ? null : filePaths[0]; });

在apps/web中调用:

// apps/web/components/FileButton.tsx import { useEffect, useState } from 'react'; export function FileButton() { const [filePath, setFilePath] = useState<string | null>(null); const handleOpen = async () => { try { const path = await window.api.openFile({ properties: ['openFile'], filters: [{ name: 'Text Files', extensions: ['txt'] }] }); setFilePath(path); } catch (err)

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

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

立即咨询