全栈地形图
给后端工程师的通识课 SHEET 01 / 14 · 2026-10-30

全栈地形图A Field Guide to the Full Stack — for backend engineers

写给一个写了很多年 C++ 服务端、现在靠 AI 把产品整条做出来、但对前端与客户端这片地形仍然没有心智模型的人。这本书不教你怎么写 React,它告诉你每样东西是什么、以什么形式存在、谁在什么时候运行它。

已完成的章节 示例在本页真实运行 拿你本机的项目当教具
14 章 / 4 部分 第 1 章已完成 React 18 · Vue 3 · lucide · GSAP

00教材提纲

每一章只回答你已经问出来的问题,并且用你本机真实存在的那个项目当教具。顺序可以改;标「待写」的章节随时挑一章让我展开。

第一部分 地形 东西都在哪、谁在运行

01
一个全栈应用由什么组成 已完成
可执行物与运行时的边界;React 和 Vue 到底是什么关系;库以什么形式存在、最后怎么进到产物里;桌面端四个框架一眼对比。
教具 filetrans · gen_note/web · llm-wiki · agent-archive
02
浏览器这台虚拟机 待写
DOM 树、单线程事件循环、渲染管线(layout / paint / composite)、为什么前端里一个同步死循环就等于界面卡死,以及 Web Worker 和你的线程池差在哪。
教具 gen_note/web/src
03
从 node_modules 到 dist:构建链全过程 待写
npm / pnpm / bun 各管什么;ESM 与 CommonJS 两套模块制度为什么共存;Vite 在开发期和构建期其实是两个不同的程序;TypeScript 的类型在哪一步被擦掉;source map 怎么让线上报错还能定位到 .tsx。
教具 gen_note/web/vite.config.ts · omp(bun 工作区)

第二部分 界面 怎么把状态变成像素

04
React 心智模型 待写
组件就是函数、状态变更触发重渲染、hooks 的规则从哪来、key 为什么是正确性问题、useEffect 什么时候是你真正需要的东西(多数时候不是)。
教具 llm-wiki/src · agent-archive/frontend
05
Vue、Svelte、Solid:同一个问题的另外三种解法 待写
单文件组件与 JSX 的分歧在哪;响应式系统(依赖追踪)和「整个组件重跑一遍」的区别;编译期做功 vs 运行期做功;你学会一个之后迁移成本到底有多大。
教具 cod(VitePress 底层是 Vue)
06
Next.js 与「元框架」 待写
CSR / SSG / SSR / ISR / RSC 五条渲染路径分别在哪台机器上跑;文件路由约定;水合(hydration)是什么、为什么会报 mismatch;你的内部工具到底需不需要它。
教具 oh-my-pi-web/pi-web · knowledge_repo/apps/web
07
界面材料学:CSS、Tailwind、UI 库、图标库、动效库 待写
这一层最让你头大,所以单独一章:原子化 CSS 为什么能不写样式表;shadcn/ui 为什么「不是依赖」而是粘贴进你仓库的代码;Antd / Element Plus / Radix 的定位差异;图标走 SVG 数据、字体还是雪碧图;动效三层(CSS / GSAP / framer-motion)各自什么时候用。
教具 llm-wiki(tailwind + shadcn + lucide-react)

第三部分 连接 前端和你的 Go 服务怎么对上

08
状态与数据流 待写
useState / zustand / react-query 分别在管什么;「服务端状态」和「界面状态」为什么要分开;缓存失效与乐观更新;表单为什么在前端是个专门的话题。
教具 llm-wiki(zustand)
09
前后端契约 待写
REST + JSON 的惯例;SSE 和 WebSocket 什么时候选哪个(你的 agent 产品基本都要流式);大文件上传与断点续传;cookie 会话与 token 的取舍;CORS 到底在拦什么、为什么开发期要配代理。
教具 filetrans · paseo(ws)· gen_note
10
部署形态 待写
静态托管、go:embed 单二进制、Docker 多阶段构建、Nginx 反代与 SPA 回退、Vercel / Cloudflare 这类平台替你做了什么;缓存与 hash 文件名的关系。
教具 filetrans(embed)· subtrans(compose)

第四部分 容器 把同一套前端装进别的壳

11
桌面客户端:Electron / Tauri / Wails / MyGo / Fyne / Gio 待写
谁提供 WebView、谁提供主进程、IPC 怎么过桥、安装包为什么差一个数量级、自动更新与代码签名、哪些系统能力只有壳才拿得到。
教具 llm-wiki/src-tauri · agent-archive(Wails v3)· paseo/desktop-local(Electron)· egoist/mygo
12
移动与跨端 待写
React Native 和 Flutter 的根本分歧(用原生控件 vs 自绘);PWA 能走多远;什么时候老老实实写 Swift / Kotlin;一个人维护多端的现实成本。
教具 uuremote(Swift 小工具)· rn
13
工程化与质量 待写
TS 严格模式值不值得;lint / format 工具链(eslint 与 oxlint);vitest 与 playwright 的分工;前端的 CI 该跑什么;版本号、发布与回滚。
教具 omp · skill_eval
14
AI 时代的选型与读码法 待写
一张技术选型决策树;五分钟读懂任意陌生前端仓库(第 1 章给了雏形,这里补齐);让 AI 写前端时你必须自己握住的四件事:数据契约、目录边界、构建产物、依赖清单。
教具 你自己接下来要做的那个项目

第 1 章

01一个全栈应用由什么组成

你现在的困惑不是「不会写」,而是「不知道这些东西各自是什么级别的存在」。所以第一章先不碰语法,先把这片地形的坐标系立起来:有几个程序在跑、各自跑在什么运行时上、边界在哪里。后面十三章都挂在这张图上。

1.1先数清楚:有几个东西在跑

在你熟悉的世界里,一个服务基本等于一个进程 + 一个二进制。编译出 ./server,扔到机器上,它自己跑。前端之所以让你觉得飘,是因为那边的产物不是可执行文件,而是一堆要被别的程序加载的资源。

把你本机四个项目摊开,一共只有四类「可执行物」:

产物运行时在谁的机器上你项目里的实例
原生二进制 操作系统 服务器 filetrans 的 Go 二进制,gen_note/server
一堆 .js / .css / .html 静态文件 浏览器 用户的机器 gen_note/web/dist/assets/index-CHPP6raO.js
Node 进程 Node / Bun 运行时 开发期是你的笔记本;上线后只有 SSR 才需要 vite dev 开发服务器;pi-web 的 Next.js
带内嵌 WebView 的宿主程序 操作系统 + 系统 WebView 用户的桌面 llm-wiki(Tauri)、agent-archive(Wails)
本章最该带走的一句话

前端的「编译产物」是资源,不是程序。它必须由另一个程序(浏览器,或你自己嵌的 WebView)加载才会活。所以「前端框架」这个词里的「运行」,运行的位置永远是某个浏览器引擎内部。

这件事在你自己的 filetrans 里是字面意义上的真实:那堆前端文件最后变成了 Go 二进制里的一段字节数组。

filetrans/main.go 第 20 行附近 · 真实代码
//go:embed web/*
var webFiles embed.FS

两行代码,边界就清楚了:构建期前端被压成文件,编译期这些文件被塞进二进制,运行期 Go 的 HTTP handler 把它们吐给浏览器,浏览器把它们跑起来。「全栈」在这里不是一个模糊的形容词,而是三个明确的时刻。

1.2浏览器是你的运行时,不是你的 UI 库

把浏览器放在和「操作系统 + 消息循环 + 渲染器」同一级去理解,前端的所有古怪规则都会立刻变得有道理。下面这张对照表是整本书里对你性价比最高的东西,值得单独记下来:

你熟悉的浏览器里的对应物关键差异
进程一个标签页(独立渲染进程)崩一个不影响别的;内存和 CPU 配额由浏览器管
main()HTML 解析到 <script> 的那一刻没有入口函数,只有「文档解析到这里了」
epoll 事件循环单线程事件循环 + 任务队列同步阻塞就是界面冻死,所以一切 IO 都是回调 / Promise / async
Qt 的 widget 树、场景图DOM 树保留模式:你改树,浏览器负责重排(layout)和重绘(paint)
线程池Web Worker不共享内存,只能传消息;拿不到 DOM
dlopen 动态库<script src> / ESM import能运行期加载,但实践中几乎都提前打包成少数几个文件
头文件 .h.d.ts 类型声明只存在于编译期,运行期被完全擦除,零开销
RAII / 析构GC没有确定的释放时机,监听器、定时器要手动解绑

最后一行那个「保留模式」是理解 React 的前提。DOM 是一棵常驻的对象树,像 Qt 的控件树:你不是每帧重画,而是改这棵树的属性,浏览器自己决定什么时候重排重绘。这意味着界面的当前样子,是你历史上所有改动的累积结果,而不是某个状态的函数。所有的 UI bug,一大半来自这里。

1.3React 和 Vue 到底是什么关系

答案先给:它们是同一层的两个竞品,解决的是同一个问题,心智模型重合度大概九成,不是上下游、不是互补、不需要都学。它们共同对抗的敌人,是上一节最后那句话——界面状态要靠你手动同步。

下面这个计数器在这页上真的在跑,三个标签页是三种写法的同一个功能:数字、加减、以及一个「偶数时高亮」的小标记。注意第一种写法里,你必须记得每次改了数字,都要去改两个地方的 DOM。

同一个计数器,三种写法 真的在运行
0 偶数
手动同步状态与 DOM
let n = 0;
const out = document.getElementById("d-out");
const badge = document.getElementById("d-parity");

function sync() {
  out.textContent = n;                        // 第 1 处同步
  badge.textContent = n % 2 ? "奇数" : "偶数"; // 第 2 处同步
  badge.dataset.even = n % 2 ? "no" : "yes";    // 第 3 处同步
}
document.getElementById("d-inc").onclick = () => { n++; sync(); };
document.getElementById("d-dec").onclick = () => { n--; sync(); };

界面上每多一处依赖 n 的东西,sync() 就要多一行。漏写一行,界面和数据就不一致——这就是前端 bug 的主要来源,和你在后端见过的「缓存和数据库不一致」是同一种病。

所以该怎么选

ReactVue
自我定位一个库,只管「状态到界面」渐进式框架,官方给全套
写界面JSX,本质是 JavaScript 表达式模板 .vue,也支持 JSX
路由 / 全局状态自己选(react-router、zustand…)官方 vue-router、pinia
更新粒度组件级重跑 + diff依赖级精确更新
生态规模最大,AI 训练语料也最多很大,中文资料尤其多
你项目里的现状几乎全部(llm-wiki、gen_note、agent-archive…)间接用到(cod 的 VitePress 底层)

对你的建议很简单:继续用 React,不用纠结。不是因为它更好,而是因为你的仓库已经全在 React 上,而且让 AI 产出 React 代码时它的「见识」最广。学会之后看 Vue 代码不需要专门学习,一个下午就能读懂。

1.4名词层级表:哪些是同一层的,哪些根本不是

你列的那串名字(React、Vue、Next.js、图标库、动效库、Tauri、Wails)之所以搅在一起,是因为它们根本不在同一层。按层排一遍,整个混乱就散了:

名字它是哪一层一句话定位怎么进到你的产物里
HTML / CSS / JS平台本身浏览器天然就懂的三样东西,所有上层最终都编译到这里就是产物
TypeScript语言层JS 加静态类型;类型在构建时被擦掉,运行期零开销编译成 JS
React / Vue / Svelte / SolidUI 库 / 框架把「状态 → 界面」自动化npm 依赖,打进 bundle
Next.js / Nuxt / SvelteKit元框架(框架之上)在 UI 库之上补齐路由、服务端渲染、构建、部署约定npm 依赖 + 一个 Node 服务
Vite / webpack / esbuild / Rollup构建工具开发服务器 + 打包器,不参与运行期只在构建期存在(devDependencies)
VitePress / Astro静态站点生成器把 Markdown 编译成一堆 HTML构建期跑完就退场,产物是纯静态页面
Tailwind CSSCSS 方案用类名拼样式,构建时只生成你用到的那些规则构建期生成一个 .css 文件
shadcn/ui组件「配方」不是依赖,是把组件源码复制进你仓库,你自己维护直接变成你的源文件
Antd / Element Plus / MUI组件库成套现成控件,换皮靠主题变量npm 依赖,打进 bundle
Radix / Base UI / Headless UI无样式组件库只给行为和无障碍,样式你自己写(shadcn 建在它上面)npm 依赖
lucide / Font Awesome / iconfont图标库一堆 SVG 路径数据(或图标字体)+ 一个渲染方式npm 依赖,按需打包;见 1.6
CSS transition / GSAP / framer-motion动效三层:声明式、命令式时间轴、组件化前者零依赖,后两者 npm 依赖
Electron / Tauri / Wails / MyGo桌面壳把同一份前端产物装进自带 WebView 的本地程序构建出安装包;见 1.7
React Native / Flutter移动端换一套渲染后端,不再是浏览器构建出 APK / IPA
最常见的误解

「Vite 是不是一个框架?」不是,它是构建工具,等于你的 CMake + 链接器 + 本地调试服务器。「Next.js 和 React 要选哪个?」不选,Next.js 里面就跑着 React。「Tailwind 和 Antd 冲突吗?」不同层,能共存,但审美上通常只挑一条路走。

1.5Next.js 到底解决什么

一句话:Next.js 把「HTML 在哪台机器上生成」变成了一个可选项,顺带把路由、打包、部署约定一起包了。

你的 gen_note 是最朴素的那种前端,叫 CSR(客户端渲染)。看它构建出来的首页,<body> 里真的只有一个空壳:

gen_note/web/dist/index.html · 真实产物(节选)
<head>
  <title>gen_note</title>
  <script type="module" crossorigin src="/assets/index-CHPP6raO.js"></script>
  <link rel="stylesheet" crossorigin href="/assets/index-C2uaYuKR.css">
</head>
<body>
  <div id="root"></div>  <!-- 整个界面都是这个空 div 里由 JS 渲染出来的 -->
</body>

两条请求路径对比,差别就在「谁生成 HTML」:

CSR:你的 gen_note / 所有桌面端应用
GET /浏览器
→
Go 二进制吐出空 HTML + JS
→
浏览器执行 JS渲染界面骨架
→
GET /api/...取数据
→
渲染完成首屏稍慢,爬虫看不到内容
SSR:Next.js / Nuxt
GET /notes浏览器
→
Node 进程在服务端跑一遍 React,拼出完整 HTML
→
浏览器直接显示首屏即内容
→
hydrate 水合同一份 JS 再接管交互

所以 Next.js 的代价很具体:线上多了一个必须常驻的 Node 进程(或者托管在 Vercel / Cloudflare 这类平台上)。而 CSR 的产物是纯静态文件,所以你的 Go 二进制 go:embed 一下就全搞定了,不需要 Node 出现在生产环境。

什么时候需要 / 不需要

需要:要被搜索引擎收录、要分享链接有预览卡片、首屏速度是生意(落地页、文档站、内容站)、想用 RSC 把取数据这件事挪到服务端。

不需要:内部工具、登录后才有内容的后台、桌面客户端。这几类场合里,CSR + 你的 Go 服务是更省心的组合,少一个运行时就少一摊运维。你现在的几个项目基本都属于「不需要」。

1.6一个库到底以什么形式存在,最后怎么用

这是你问得最准的一个问题。完整链条有五段,而且全都能在你本机的文件里看到:

1 npm 包一个目录:JS 文件 + package.json + .d.ts
→
2 import你在源码里按名字引用
→
3 打包Vite / Rollup 当链接器,产出带 hash 的文件
→
4 dist/几个静态文件,库的代码已经混在里面了
→
5 交付go:embed 进二进制 / 放 CDN / 装进 WebView

第 1 段:npm 包就是「带头文件的静态库」

一个 npm 包解包后是一个普通目录,里面是 JS 文件、一个描述自己的 package.json、以及给 TypeScript 看的 .d.ts 类型声明。对照你的世界:

  • node_modules/ ≈ vendor 目录,不进 git,可以随时删掉重装
  • package.json ≈ CMakeLists + 依赖清单(dependencies 进产物,devDependencies 只在构建期用)
  • package-lock.json ≈ 锁定整张依赖图的精确版本,保证两台机器装出一样的东西
  • .d.ts ≈ 头文件,只在编译期存在

第 2 段:一个图标库的真身

你的 llm-wiki 依赖 lucide-react。那个包里,每个图标就是这么一小段数据——下面是从 npm 上直接拉下来的真实文件:

lucide@0.414.0 / dist/esm/icons/house.js · 原样引用,ISC 许可
const House = [
  "svg",
  defaultAttributes,              // viewBox、stroke: "currentColor"、宽高 24…
  [
    ["path", { d: "M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8" }],
    ["path", { d: "M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 ..." }]
  ]
];
export { House as default };

就这样。图标库 = 一堆描述 SVG 的数组 + 一个把数组变成 DOM 的渲染函数。不是图片,不是字体,没有网络请求。下面这个示例把真的 lucide 包从 CDN 载进本页,然后用我手写的十行渲染函数把那段数据画出来:

图标库解剖:数据 → SVG 真的在运行
载入中…
把 icon 数据渲染成 DOM 的全部代码
function renderIcon([tag, attrs, kids = []], size) {
  const el = document.createElementNS("http://www.w3.org/2000/svg", tag);
  for (const [k, v] of Object.entries(attrs || {})) el.setAttribute(k, v);
  if (size) { el.setAttribute("width", size); el.setAttribute("height", size); }
  kids.forEach(k => el.appendChild(renderIcon(k)));
  return el;
}

图标颜色跟着文字走,是因为 stroke: "currentColor"。这也解释了为什么图标库给的都是描边图标、能随意换色换大小:它本质是矢量数据,不是位图资源。

第 3 段:打包器就是链接器

整包 lucide 有一千多个图标、三百多 KB。但你 import { House } from "lucide-react" 时,最终产物里只会留下 House 那一个数组。这个过程叫 tree-shaking,和你用 --gc-sections 做死代码消除是同一件事。

你的 gen_note 里有这件事最干净的证据。开发期的入口文件指向源码,构建后指向一个带内容 hash 的打包产物:

gen_note/web/index.html · 开发期(Vite 直接喂源码给浏览器)
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
gen_note/web/dist/index.html · 构建后(230 KB,React + 你的代码 + 用到的库全在里面)
<div id="root"></div>
<script type="module" crossorigin src="/assets/index-CHPP6raO.js"></script>

文件名里的 CHPP6raO 是内容哈希。内容变了名字就变,于是可以让浏览器永久缓存这个文件而不怕更新不生效。这是前端对「缓存失效」问题的标准答案,你在 CDN 层干过同样的事。

第 4 段:开发期的那个 Node 进程

开发时你是两个服务一起跑的:Vite 开发服务器(Node)伺候页面,Go 服务伺候接口。跨端口会撞上浏览器的同源策略,所以 Vite 在中间当反向代理——你项目里就是这么配的:

gen_note/web/vite.config.ts · 真实配置
export default defineConfig({
  plugins: [react()],
  server: { proxy: { "/api": { target: "http://127.0.0.1:8787", changeOrigin: false } } },
});

于是页面请求 /api/notes 时,Vite 把它转给 8787 的 Go 服务。上线后这个 Node 进程完全消失,由 Go 同时负责静态文件和接口,路径天然同源,代理也就不需要了。开发期和生产期的拓扑不一样,这件事值得记住,因为「本地好的、上线 404」十次有八次出在这里。

第 5 段:动效库的三层

动效没有「一个库」,是三层不同的东西。下面两条跑道并排对比,左边零依赖,右边是真的 GSAP:

CSS 过渡 vs GSAP 时间轴 真的在运行
CSS
GSAP
两种写法
/* 1 CSS:声明「变化要花多久」,由浏览器插值。零依赖,优先选它 */
.mover { transition: transform .5s cubic-bezier(.2,.9,.25,1.1); }
.mover.is-out { transform: translateX(120px) rotate(45deg); }

// 2 GSAP:命令式时间轴,能编排序列、暂停、倒放、物理缓动
gsap.timeline()
  .to(el, { x: 120, rotate: 45, duration: .5, ease: "back.out(1.7)" })
  .to(el, { x: 0, rotate: 0, duration: .4 }, "+=0.15");

// 3 framer-motion:把动效包成 React 组件,适合「元素进出场」
<motion.div animate={{ x: 120 }} transition={{ type: "spring" }} />

选择顺序很简单:能用 CSS 就用 CSS;需要编排多步时间轴、或者要暂停倒放,上 GSAP;在 React 里做列表进出场动画,framer-motion 最省事。Tailwind 不在这一层,它只是帮你生成 CSS 类。

本页自己就是个例子

本页没有构建步骤,所以它用的是库的另一种分发形态:UMD 包通过 <script src> 挂到全局变量上(window.React、window.Vue、window.gsap、window.lucide)。你项目里用的是 ESM 形态 + 打包器。同一个库,两种分发形态,这就是为什么你有时在教程里看到 <script src>、有时看到 import ——它们不是两种技术,是同一个包的两种出口。

1.7桌面端:套壳派与自绘派

先说共同点,这点最重要:Electron、Tauri、Wails、MyGo 用的前端产物和网页版一模一样,还是那堆 dist/ 文件。变的只是宿主——从「用户的浏览器」变成「你自己的程序里嵌的 WebView」。你会写网页,就已经会写它们的界面部分了。

ElectronTauriWailsMyGo
WebView 来源自带整个 Chromium系统的(WKWebView / WebView2 / WebKitGTK)系统的,同左系统的;原生 UI 模式下完全不用
主进程语言Node.jsRustGoGo(纯 Go,不依赖 cgo)
安装包量级80–150 MB3–10 MB5–15 MB约 7 MB
前端怎么调后端IPC 消息(ipcMain / ipcRenderer)invoke("命令名", 参数) + 事件自动生成 TS 绑定,像调本地函数一样调 Go 方法同样生成 TS 客户端,带类型,还能用 channel 流式推送
渲染一致性完全一致,内核你自己带随用户系统版本浮动随用户系统版本浮动WebView 模式随系统;原生 UI 自绘,完全一致
你的实例paseo/desktop-localllm-wiki/src-tauriagent-archive(Wails v3)还没用过
什么时候选它要绝对一致的渲染、重度依赖 Node 生态要小体积、愿意写点 Rust后端本来就是 Go后端是 Go,想要更小的包、更低的内存,或者干脆不要 WebView

对你来说 Wails 的吸引力很直白:主进程是 Go,你那些现成的 Go 代码能直接变成桌面应用的后端,前端产物还是 React 那一套。agent-archive 用的就是这条路(它的 frontend/ 是 React + Vite,依赖里有 @wailsio/runtime)。

MyGo:你说的那个,确实存在

egoist/mygo,仓库 2026 年 9 月才建,现在 v0.7.3、三千多 star、MIT 许可。我上一版写「Go 里没有这个框架」是错的:它比我的知识截止日期还新。

它特别在一个框架同时提供两条路,而且同一个应用里可以混用:

① WebView 模式——和 Wails 同路,借系统 WebView,前端产物照旧是那堆 dist/。Go 这边 mygo.Bind(Greeter{}) 一句,框架自动生成 TypeScript 客户端,前端像调本地函数一样调 Go,还能用 channel 做流式推送。

② 原生 UI 模式——完全不要 WebView,用 package ui 在 Go 里写界面,MyGo 自己用 Metal / Direct3D / OpenGL 画。官方数字:二进制约 7 MB,原生 UI 内存约 44 MB、带 WebView 约 62 MB,空闲 CPU 0%。纯 Go 不依赖 cgo,一台机器就能交叉编译三个平台;打包、签名公证、带增量的自动更新也都在框架里。

第二种模式对你另有一层意义。看它官方的计数器例子,和本章 1.3 那个是同一个东西:

MyGo 原生 UI · 官方示例,MIT
type counter struct{ n int }

func (s *counter) view(c *ui.Context) {
	ui.Column(c).Fill().Center().Gap(12).Children(func() {
		ui.Text(c, fmt.Sprint(s.n)).FontSize(40).Bold()
		if ui.PrimaryButton(c, "Increment").Clicked() {
			s.n++
		}
	})
}

没有 HTML,没有 JavaScript,没有 DOM,但思想和 React 一模一样:view 是应用状态的函数,框架调用它来构建每一帧,再画到 GPU 上。1.3 里那句「UI = f(state)」可以原样平移过来,换掉的只是渲染后端。这种做法在图形界面里叫即时模式(immediate mode),最有名的代表是 C++ 那边的 Dear ImGui——如果你见过它,MyGo 的原生 UI 会让你觉得很亲切。顺带说,这也正是理解 React 最快的一句话:React 相当于在保留模式的 DOM 上,盖了一层即时模式的壳。

它和 Fyne、Gio 属于同一派(自绘,不碰前端技术栈),区别是 MyGo 把两种模式放进了同一个框架,还把发布链路一起包了。

选它之前要知道的代价

它太新了。API 还在变,生态、第三方控件和踩坑问答都远不如 Electron / Tauri / Wails。对你还有一条更现实的:多数 AI 模型的训练数据里还没有它(包括我),所以让 AI 写 MyGo 代码时,它大概率会编出不存在的 API。用它意味着你得自己读文档和源码——这恰好是第 14 章要讲的那种判断。第 11 章会把这四个框架连同签名、自动更新一起展开。

1.8五分钟读懂任何陌生前端仓库

这是本章能立刻用上的工具。拿到一个你没见过的仓库,按顺序做六件事,五分钟内就能说清它是什么、怎么跑、产物去哪:

这六步对你本机任意一个仓库都成立。不信随便挑一个试:先猜,再按这六步验证。

1.9自测

能把下面四题用自己的话讲清楚,第 1 章就算过了。

为什么 gen_note 上线后不需要在服务器上跑 Node,而 pi-web 需要?

因为 gen_note 是 CSR:构建产物是纯静态文件,Go 服务直接把它们吐给浏览器,渲染发生在用户机器上。Next.js 做 SSR / RSC 时,HTML 是在服务端由 Node 跑 React 生成的,所以线上必须有一个常驻的 JS 运行时(自己部署 Node,或交给 Vercel / Cloudflare 这类平台)。

JSX 是浏览器的特性吗?如果不是,谁把它变成浏览器能跑的东西?

不是。JSX 是语法糖,浏览器完全不认。构建期由 Vite 内部的 esbuild / SWC 这类编译器把它转成 React.createElement(...) 或新版的 jsx(...) 函数调用。本页 React 示例直接写的就是编译后的形态,因为本页没有构建步骤。

把 node_modules 整个删掉,线上的服务会挂吗?

不会。node_modules 只在开发和构建时需要,库的代码在打包那一刻就已经被复制进 dist/ 里了。删掉它只影响你下一次构建(跑一次 npm install 就恢复)。这和「删掉 vendor 目录不影响已编译的二进制」是一样的道理。

同一份 React 代码,怎么同时变成网页、桌面应用和一个 Go 单二进制?

因为它们消费的是同一份 dist/ 静态产物,只换宿主:浏览器直接访问;Tauri / Wails 把这堆文件装进程序内嵌的 WebView;Go 用 go:embed 把它们编译进二进制再由 HTTP handler 提供。前端部分不需要任何改动,唯一要动的是「怎么调后端」那一层——网页走 HTTP,桌面端走壳提供的 IPC。