名称: Linux Lab(linux-learning)— 交互式 Linux 学习平台
技术栈:
- 框架: React 19 + TypeScript 6 + Vite 7
- 样式: Tailwind CSS 3(自定义 glass / accent / terminal 主题色板)
- 状态管理: Zustand 5(带
persist中间件,localStorage key =linux-lab-storage) - 路由: React Router v7(HashRouter,不是 BrowserRouter,便于静态托管)
- 终端: xterm.js +
@xterm/addon-fit+@xterm/addon-web-links - 内容渲染:
marked(Markdown) +dompurify(XSS 防护) - 测试: vitest(当前仅 1 个文件
tests/VirtualFS.test.ts) - 包管理: pnpm(
pnpm-lock.yaml严禁手动修改)
主要用途: 浏览器内模拟 bash / zsh / fish 三大 Shell,配有虚拟文件系统(VirtualFS)和 22 节分阶段课程(5 个阶段:Linux 基础 → 命令行核心 → Shell 深入 → 脚本编程 → 系统管理),用户可在终端中边学边练。
环境要求:
- Node.js: 20+
- pnpm: 10+
- 模块类型: ESM(
package.json中"type": "module")
linux-learning/
├── public/ # 静态资源(Vite 原样复制)
├── dist/ # 构建产物(gitignore,不要改动)
├── src/
│ ├── main.tsx # 入口,挂载 React 根
│ ├── App.tsx # 根组件 + HashRouter + 路由表 + resize
│ ├── index.css # Tailwind base + 自定义工具类 + 主题变量 + 动画 keyframes
│ ├── components/ # UI 组件(按职责分子目录)
│ │ ├── feedback/ # 反馈组件 (EmptyState / LoadingDots / Skeleton)
│ │ ├── glass/ # 毛玻璃原子组件 (GlassButton / GlassPanel)
│ │ ├── layout/ # 布局组件 (Sidebar / LiquidToolbar)
│ │ ├── lesson/ # 课程渲染器 (LessonViewer)
│ │ └── terminal/ # xterm 终端封装 (Terminal / injectCommand)
│ ├── pages/ # 页面级组件 (Home)
│ ├── lessons/ # 课程元数据 + Markdown 内容
│ │ ├── lessons.ts # 22 篇课程 + 5 个 stage + 导航辅助函数
│ │ └── content.ts # 课程正文 Markdown + runnableSnippets
│ ├── shells/ # Shell 模拟核心
│ │ ├── VirtualFS.ts # 内存虚拟文件系统(FHS 目录树)
│ │ ├── shells.ts # bash/zsh/fish 抽象 + tokenizer + 执行器
│ │ ├── commands.ts # 内置命令注册表 `builtinCommands`
│ │ ├── interactive.ts # InteractiveSession 抽象 + nano/vim/htop 工厂
│ │ └── types.ts # 命令上下文类型 + ANSI 颜色工具
│ └── store/
│ └── useAppStore.ts # 全局 Zustand store(主题 / 进度 / 笔记 / shell)
├── tests/
│ └── VirtualFS.test.ts # vitest 单元测试(VirtualFS.mkdir)
├── index.html # Vite 入口 HTML
├── vite.config.ts # Vite 配置(仅 react 插件)
├── tsconfig.json # 项目引用根
├── tsconfig.app.json # 应用代码 TS 配置(strict)
├── tsconfig.node.json # Node 环境 TS 配置
├── tailwind.config.js # Tailwind 主题(glass/accent/terminal + 动画 keyframes)
├── postcss.config.js # PostCSS(tailwindcss + autoprefixer)
└── eslint.config.js # Flat config(推荐规则集 + react-hooks + react-refresh)
所有命令在 linux-learning/ 目录下执行:
| 命令 | 用途 |
|---|---|
pnpm dev |
启动 Vite 开发服务器(HMR) |
pnpm build |
先 tsc -b 类型检查,再 vite build 打包到 dist/ |
pnpm lint |
运行 ESLint 检查(eslint .) |
pnpm preview |
本地预览 dist/ 构建产物 |
pnpm test |
运行 vitest 测试 |
pnpm test:watch |
监听模式下运行 vitest |
注意: Vite 构建产物约 741KB JS + 28KB CSS(未分块),单 chunk 超过 500KB 警告属于正常,不需要额外配置。
项目使用 GitHub Actions(.github/workflows/ci.yml)自动化质量检查:
- 触发条件: push 到
main分支或 PR 到main - 环境: Ubuntu latest + Node.js 20 + pnpm 10
- 流程:
pnpm install --frozen-lockfile→pnpm lint→pnpm build→pnpm test
注意: CI 使用 --frozen-lockfile 确保依赖版本一致,本地开发也应保持 pnpm-lock.yaml 不变。
- 组件文件: PascalCase(如
Terminal.tsx),目录也用 PascalCase(如components/terminal/) - 工具/逻辑文件: camelCase(如
useAppStore.ts、VirtualFS.ts、shells.ts) - 变量/函数: camelCase,英文(即使注释是中文)
- 类型/接口: PascalCase(
LessonMeta、ShellInfo、ExecResult) - 常量: UPPER_SNAKE 仅限真正常量(如
PATH),普通配置对象用 camelCase - 导出: 组件和工具函数统一用 named export(
export function Foo()),避免默认导出;只有App.tsx用 default export
- 严格 TypeScript: 开启
noUnusedLocals/noUnusedParameters/noFallthroughCasesInSwitch/verbatimModuleSyntax/erasableSyntaxOnly- 类型导入必须用
import type { ... } - 不能使用 enum、namespace、参数属性(
erasableSyntaxOnly限制)
- 类型导入必须用
- React: 全部函数组件 + Hooks;不写 class 组件
- 样式: 优先 Tailwind 工具类;玻璃拟态用自定义类(
glass-soft、glass、glass-strong);不要新建全局 CSS,能用@apply或工具类解决就用它们 - 可访问性: 所有 icon-only 按钮必须有
aria-label;折叠展开按钮必须有aria-expanded;模态面板必须有role="dialog" aria-modal="true";优先级 focus-visible 焦点环需保持 - 微动画: 用 tailwind.config.js 里的
animate-fade-in/animate-slide-up/animate-route-fade等预定义动画;不要随意内联关键帧 - 不可变性: Zustand 的
set调用务必返回新对象/数组,不要直接 mutate(如已完成列表用Array.from(new Set(...))) - 注释: 文件头用中文一行说明用途;行内注释可用中文;变量/函数命名仍用英文
- 错误处理: 不能省略;命令未找到统一返回
exitCode: 127并打印提示
- 不使用 path alias(
@/之类),全部相对路径../../shells/... verbatimModuleSyntax: true— 类型和值必须分开导入
| 文件/目录 | 原因 |
|---|---|
linux-learning/pnpm-lock.yaml |
自动生成的锁文件,手动改会导致依赖错乱 |
linux-learning/node_modules/ |
自动安装的依赖 |
linux-learning/dist/ |
Vite 构建产物(已 gitignore) |
linux-learning/src/index.css 中的 @tailwind 指令段 |
改顺序会破坏产物;自定义样式追加在 @layer 内 |
linux-learning/.gitignore |
仅在新增需忽略的产物/日志时可加 |
linux-learning/tailwind.config.js 的 content 字段 |
错误配置会导致样式丢失 |
linux-learning/tsconfig*.json 的核心编译选项 |
改动前必须确认影响面 |
- 路由用 HashRouter,新增路由不要切换为 BrowserRouter — 项目是为静态托管(无服务端 fallback)设计的。
- Shell 模拟是基于 in-memory
VirtualFS,刷新页面会重置文件系统;持久化只覆盖theme / sidebar / lesson progress / notes(看useAppStore.ts的 persist 配置)。 - 新增内置命令必须:
- 在
src/shells/commands.ts的builtinCommands注册(key 为命令名) - 如有别名,加入同对象的
aliases数组(shells.ts的runSingle会查找) - 返回
{ exitCode: number },输出通过ctx.print()/ctx.printRaw()写入 - 交互式命令(全屏 TUI)则注册到
src/shells/interactive.ts的interactiveCommands,实现InteractiveSession接口;runSingle会识别并返回ExecResult.session触发 Terminal 切换 fullscreen 模式
- 在
- 新增课程只需改
src/lessons/lessons.ts的lessons数组(一并加上 stage),正文内容放content.ts;App.tsx路由/lesson/:id会自动匹配。注意:课程 ID 同时出现在lessons.ts元数据 +content.ts的 commands 区 + content 区三处,修改时务必三处同步。 - 终端交互:在
Terminal.tsx中已有历史 / Tab 补全 / 光标移动 / fullscreen 模式路由,改这块逻辑前先读完整个 ~500 行组件,避免重复造轮子或破坏现有 ref(inputRef/cursorPosRef/historyRef/sessionRef/modeRef等)。 verbatimModuleSyntax+erasableSyntaxOnly双重约束下,import { Foo }中若 Foo 只是类型会编译失败 — 必须改import type { Foo };同理禁止enum Foo { A }写法。- Tailwind 主题色板:所有 UI 颜色统一走
accent.*/glass.*/terminal.*,不要在 JSX 里写裸色值(如#0a84ff),保持主题可统一替换。 - 持久化 store:
resetProgress()已封装在 store 里,调试时别直接localStorage.removeItem('linux-lab-storage')后忘了它属于 zustand persist。 - i18n:当前界面文案为中文,不要自己抽出 i18n 框架(项目没引入),保持中文内联字符串即可。
- 依赖引入:能复用现有库(
xterm、marked、dompurify、zustand、react-router-dom)就复用,不要未经确认引入新依赖(除非用户明确要求)。 - emoji 使用:用户/AI 全局规则允许且项目本身大量使用 emoji(🌱 ⚡ 🐚 📜 🛠️ 🐟 ✏️ 等),可以放心使用。
- commit message:Conventional Commits 格式(
feat:/fix:/refactor:/docs:等),一次提交只做一件事。 - InteractiveSession 设计:所有全屏 TUI 命令必须实现
interactive.ts的InteractiveSession接口(render/onKey/isActive/finalize)。禁止在普通CommandHandler里用ctx.print模拟全屏 — 那会与 prompt 输出混在一起,破坏 Terminal 模式切换。 - Terminal 模式切换:
modeRef === 'fullscreen'时handleKey早期 return 拦截所有键事件路由到sessionRef.current.onKey;不要在 fullscreen 模式里写 prompt 处理逻辑,反之亦然。xterm.onResize期间 session 需要重渲染时手动调sessionRef.current.render()。 - VirtualFS 写入副作用:
nano/vim通过fs.writeFile持久化编辑内容;改VirtualFS行为前先跑nano README.md+cat README.md烟测。 - 空文件 / 不存在文件:
nano/vim对不存在的文件会调fs.writeFile(path, '')先 touch 一份空文件再编辑;不要改成"只读"行为,否则 nano 会因readFile返回 null 而出错。 - 测试:只有
tests/VirtualFS.test.ts一个测试文件;新增 Vitest 测试时参考其结构(describe/it/beforeEach/expect)。需运行测试时用pnpm test。