Skip to content

第4章 技术栈入门

作者

谭策 — 独立开发者 | AIOps 领域探索者

IT Online 微信公众号

许可证

MPL-2.0 © 谭策

本章导读

欢迎来到技术栈入门章节!在前三章中,我们了解了项目是什么、搭建了开发环境、并体验了系统功能。本章将带你深入理解项目使用的核心技术,包括 TypeScript、React、Express、Tailwind CSS、Docker 等。

本章的定位是**"知其然,更知其所以然"。即使你已有一定基础,也建议阅读本章,因为我们将聚焦于"本项目如何使用这些技术"**,而非泛泛的概念介绍。对于零基础读者,我们提供了通俗易懂的类比和详细的代码示例。

学习目标

阅读完本章后,你将能够:

  • 理解 TypeScript 在项目中扮演的角色和核心价值
  • 掌握 React 组件化思想及常用 Hooks 的使用方式
  • 理解 Express 的中间件机制和路由设计模式
  • 掌握 Tailwind CSS 的原子化样式编写方式
  • 理解 Zustand 和 React Query 的状态管理策略
  • 掌握 Docker 容器化的核心概念和实操方法
  • 了解项目选用的每个第三方库的作用和使用场景

前置知识

  • 基本的编程概念(变量、函数、条件判断、循环)
  • 基本的 Web 概念(HTTP 协议、HTML/CSS/JavaScript)
  • 已按照第2章完成环境搭建

4.1 TypeScript:给 JavaScript 穿上铠甲

4.1.1 为什么选择 TypeScript?

JavaScript 是一种弱类型语言,这意味着你可以在运行时随意改变变量的类型。这在小型项目中很方便,但在大型项目中容易引发难以追踪的 Bug。

类比理解:JavaScript 就像一个没有安全检查清单的飞行员,可能会忘记关舱门、忘记加油;TypeScript 则像一位配备完整检查清单的机长,在起飞前就能发现所有潜在问题。

TypeScript 的核心优势:

  1. 编译时类型检查:在代码运行前就能发现类型错误,而不是等到线上出 Bug
  2. 智能提示:编辑器可以准确提示每个变量、函数的类型和方法
  3. 自文档化:类型定义本身就是最好的文档,阅读代码时一目了然
  4. 重构安全:修改函数签名时,编辑器会提示所有需要更新的地方

4.1.2 项目中的 TypeScript 配置

项目的 TypeScript 配置位于 tsconfig.json 文件中:

json
{
  "compilerOptions": {
    "target": "ES2020",           // 编译目标为 ES2020
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",           // 使用 ES 模块规范
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",          // 使用 React 17+ JSX 转换
    "strict": true,              // 启用严格模式
    "noUnusedLocals": true,      // 禁止未使用的变量
    "noUnusedParameters": true,  // 禁止未使用的参数
    "noFallthroughCasesInSwitch": true  // switch 不能穿透
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

关键配置解读

配置项含义
stricttrue启用所有严格类型检查选项
noUnusedLocalstrue声明了但未使用的变量会报错
noUnusedParameterstrue函数参数未使用会报错
noEmittrueTypeScript 编译器不输出文件(由 Vite 处理编译)
jsxreact-jsx使用 React 的新 JSX 转换,无需 import React

4.1.3 类型定义实战

让我们看一个项目中的实际类型定义示例。在后端,数据库操作返回的数据需要明确定义类型:

typescript
// 用户信息接口
export interface UserInfo {
  id: string;          // 用户 ID,UUID 格式
  username: string;    // 用户名
  email: string;       // 邮箱
  role: 'admin' | 'user';  // 角色,只能是 admin 或 user
  password?: string;   // 密码,可选字段(返回时通常不包含)
  createdAt: string;   // 创建时间
  updatedAt: string;   // 更新时间
}

使用示例

typescript
// 正确:类型完全匹配
const user: UserInfo = {
  id: 'a1b2c3d4',
  username: 'admin',
  email: 'admin@example.com',
  role: 'admin',
  createdAt: '2026-05-27',
  updatedAt: '2026-05-27',
};

// 错误:role 只能是 'admin' 或 'user'
const user2: UserInfo = {
  // ...
  role: 'super-admin',  // TypeScript 会在这里报错!
};

// 错误:缺少必填字段
const user3: UserInfo = {
  id: 'e5f6g7h8',
  username: 'test',
  // 缺少 email、role 等必填字段,TypeScript 会报错
};

4.1.4 TypeScript 在前后端的使用差异

特性前端后端
编译工具Vite(内置 TypeScript 支持)tsc 编译器
运行时浏览器(JavaScript)Node.js(JavaScript)
类型定义@types/react手动定义或使用库自带类型
严格程度strict: truestrict: true

重要提醒:TypeScript 的类型只在编译时有效,运行时所有类型都会被擦除。这意味着你不能在运行时检查 TypeScript 类型,只能通过 JavaScript 的方式(如 typeof)检查。

4.1.5 动手练习

  1. 打开项目代码,找到 frontend/src/types/ 目录,阅读其中的类型定义文件
  2. 尝试修改一个类型定义,观察 TypeScript 编译器如何报错
  3. UserInfo 接口中添加一个 phone 可选字段,并更新相关代码

4.2 React:构建用户界面的乐高积木

4.2.1 React 是什么?

React 是一个用于构建用户界面的 JavaScript 库。它的核心理念是组件化:将 UI 拆分成独立的、可复用的组件,然后通过组合这些组件来构建完整的界面。

类比理解:React 就像乐高积木。每个组件就是一个积木块,你可以用小块拼成大块,最终搭建成城堡。积木之间通过**属性(props)**来传递信息,就像乐高积木的凸点和凹槽一样。

4.2.2 JSX:在 JavaScript 中写 HTML

JSX 是 React 的核心语法,它允许你在 JavaScript 代码中直接写类似 HTML 的代码:

tsx
// 这是一个简单的 React 组件
function WelcomeCard({ name, role }: { name: string; role: string }) {
  return (
    <div className="bg-white rounded-lg shadow p-4">
      <h2 className="text-lg font-bold">欢迎, {name}!</h2>
      <p className="text-gray-600">角色: {role === 'admin' ? '管理员' : '普通用户'}</p>
    </div>
  );
}

JSX 的关键规则

  1. JSX 必须有唯一的根元素(可以用 <>...</> 包裹多个元素)
  2. 使用 className 而不是 class(因为 class 是 JavaScript 保留字)
  3. 在 JSX 中嵌入 JavaScript 表达式需要用花括号 {}
  4. 自定义组件名必须大写(如 <WelcomeCard>),小写表示 HTML 元素(如 <div>

4.2.3 Hooks:让函数组件拥有状态

在 React 16.8 之前,只有类组件(Class Component)才能使用状态(state)。Hooks 的出现让函数组件也能拥有状态管理能力。项目中使用的主要 Hooks:

useState:管理组件内部状态

tsx
import { useState } from 'react';

function ServerForm() {
  // useState 返回一个数组:[当前值, 更新函数]
  const [name, setName] = useState('');
  const [host, setHost] = useState('');
  const [port, setPort] = useState(22);

  return (
    <form>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="服务器名称"
      />
      <input
        value={host}
        onChange={(e) => setHost(e.target.value)}
        placeholder="服务器地址"
      />
      <input
        type="number"
        value={port}
        onChange={(e) => setPort(Number(e.target.value))}
        placeholder="端口"
      />
    </form>
  );
}

useEffect:处理副作用

tsx
import { useState, useEffect } from 'react';

function DashboardStats() {
  const [stats, setStats] = useState(null);

  // useEffect 在组件挂载时执行
  useEffect(() => {
    fetch('/api/stats')
      .then(res => res.json())
      .then(data => setStats(data));
  }, []); // 空数组表示只在挂载时执行一次

  return <div>{stats ? JSON.stringify(stats) : '加载中...'}</div>;
}

useMemo:优化性能

tsx
import { useMemo } from 'react';

function TaskList({ tasks, filter }) {
  // 只有当 tasks 或 filter 变化时才重新计算
  const filteredTasks = useMemo(() => {
    return tasks.filter(task => {
      if (filter === 'all') return true;
      return task.status === filter;
    });
  }, [tasks, filter]);

  return (
    <ul>
      {filteredTasks.map(task => (
        <li key={task.id}>{task.name}</li>
      ))}
    </ul>
  );
}

4.2.4 项目中的组件模式

项目中的组件遵循以下模式:

1. 文件组织:每个组件一个文件,放在对应的功能目录中

src/
  components/
    Layout.tsx          # 布局组件
    AlertBadge.tsx      # 告警徽章
  pages/
    Dashboard.tsx       # 仪表盘页面
    Hosts.tsx           # 主机管理页面
  contexts/
    AuthContext.tsx     # 认证上下文

2. 组件导出:统一使用默认导出

tsx
// 正确的做法
export default function Dashboard() {
  // ...
}

// 项目也使用这种方式

3. Props 类型定义:使用 TypeScript 接口定义 Props

tsx
interface AlertCardProps {
  alert: Alert;
  onAcknowledge: (id: string) => void;
  onResolve: (id: string) => void;
}

export default function AlertCard({ alert, onAcknowledge, onResolve }: AlertCardProps) {
  // ...
}

4.2.5 React 生命周期回顾

虽然现代 React 主要使用函数组件和 Hooks,但理解生命周期概念仍然重要:

组件创建

useEffect(() => {
  // 相当于 componentDidMount
  console.log('组件已挂载');
  
  return () => {
    // 相当于 componentWillUnmount
    console.log('组件即将卸载');
  };
}, []);

依赖变化

useEffect(() => {
  // 相当于 componentDidUpdate
  console.log('依赖变化,重新执行');
}, [dep1, dep2]);

组件卸载

清理函数执行

4.2.6 动手练习

  1. 打开 frontend/src/components/Layout.tsx,找出其中使用了哪些 Hooks
  2. 尝试在 Dashboard.tsx 中添加一个 useState,记录页面访问次数
  3. 创建一个简单的 HelloWorld 组件,接收 name 属性并显示欢迎信息

4.3 Express:后端的路由调度器

4.3.1 Express 是什么?

Express 是一个基于 Node.js 的 Web 应用框架,它提供了简洁的 API 来处理 HTTP 请求、路由、中间件等。

类比理解:如果把后端比作一个餐厅,Express 就是前台服务员 + 点餐系统。它负责接待客人(接收请求)、点单(路由匹配)、把订单送到厨房(调用业务逻辑)、最后把菜端给客人(返回响应)。

4.3.2 Express 核心概念

路由(Routing)

路由定义了 URL 到处理函数的映射关系:

typescript
import express from 'express';

const app = express();

// GET 请求:获取所有用户
app.get('/api/users', (req, res) => {
  res.json({ users: [] });
});

// POST 请求:创建新用户
app.post('/api/users', (req, res) => {
  const { username, email } = req.body;
  res.status(201).json({ id: '1', username, email });
});

// PUT 请求:更新用户
app.put('/api/users/:id', (req, res) => {
  const { id } = req.params;  // 从 URL 参数中获取 ID
  const updates = req.body;
  res.json({ id, ...updates });
});

// DELETE 请求:删除用户
app.delete('/api/users/:id', (req, res) => {
  const { id } = req.params;
  res.status(204).send();
});

项目中路由的实际写法

typescript
// backend/src/app.ts 中的路由注册示例
app.use('/api/auth', authRoutes);
app.use('/api/users', userRoutes);
app.use('/api/servers', serverRoutes);
app.use('/api/agents', agentRoutes);
app.use('/api/workflows', workflowRoutes);
// ... 共 47 个路由组

中间件(Middleware)

中间件是 Express 的核心机制,它是一个在请求到达路由处理函数之前或之后执行的函数。

中间件的工作流程

请求进入

[中间件1: CORS] 处理跨域

[中间件2: JSON解析] 解析请求体

[中间件3: 日志] 记录请求信息

[中间件4: JWT验证] 验证用户身份

[路由处理函数] 执行业务逻辑

返回响应

自定义中间件示例

typescript
// JWT 验证中间件
function authMiddleware(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  
  if (!token) {
    return res.status(401).json({ error: '未提供认证令牌' });
  }
  
  try {
    const decoded = verifyToken(token);
    req.user = decoded;  // 将用户信息附加到请求对象
    next();  // 继续执行下一个中间件或路由
  } catch (error) {
    return res.status(403).json({ error: '令牌无效' });
  }
}

// 使用中间件
app.use('/api/protected', authMiddleware);

中间件的关键规则

  1. 中间件函数接收三个参数:reqresnext
  2. 必须调用 next() 将控制权传递给下一个中间件,否则请求会挂起
  3. 如果中间件发送了响应(如 res.json()),则不能再调用 next()
  4. 中间件的注册顺序很重要,先注册的先执行

4.3.3 Express 项目结构

项目中的后端采用分层架构:

backend/src/
  app.ts              # 应用入口,Express 初始化和路由注册
  routes/             # 路由层(URL 路由 -> 请求处理)
    authRoutes.ts
    serverRoutes.ts
    agentRoutes.ts
  services/           # 业务逻辑层
    llmService.ts
    sshService.ts
    workflowExecutor.ts
    alertService.ts
  models/             # 数据模型层
    database.ts
    migrations.ts
  middleware/         # 中间件
    auth.ts
    rateLimiter.ts
    errorHandler.ts
  websocket/          # WebSocket 处理
    handler.ts

这种分层的好处

  • 职责分离:路由只管 URL 匹配和请求分发,服务层只管业务逻辑
  • 易于测试:可以单独测试每个服务,不需要启动整个应用
  • 易于维护:修改业务逻辑只需要改服务层,不影响路由定义

4.3.4 错误处理

Express 提供了统一的错误处理中间件:

typescript
// 错误处理中间件
app.use((err, req, res, next) => {
  console.error('错误:', err.message);
  
  if (err.name === 'UnauthorizedError') {
    return res.status(401).json({ error: '认证失败' });
  }
  
  res.status(err.status || 500).json({
    error: err.message || '服务器内部错误',
  });
});

4.3.5 动手练习

  1. 打开 backend/src/app.ts,列出所有注册的路由组及其路径
  2. 创建一个简单的 Express 应用,包含 /api/hello 路由,返回 {"message": "Hello!"}
  3. 为上述应用添加一个请求日志中间件,记录每个请求的时间和 URL

4.4 Tailwind CSS:原子化的样式方案

4.4.1 Tailwind CSS 是什么?

Tailwind CSS 是一个原子化(Atomic)的 CSS 框架。与传统 CSS 框架(如 Bootstrap)提供预定义的组件不同,Tailwind 提供的是细粒度的工具类(Utility Classes),你可以像搭积木一样组合出任何样式。

类比理解:Bootstrap 像是提供了一整套预制房屋,你只能选择现有户型;Tailwind 像是提供了砖块、水泥、木材等基础建材,你可以自由设计任何建筑。

4.4.2 传统 CSS vs Tailwind CSS 对比

传统 CSS 写法

css
/* styles/button.css */
.btn-primary {
  background-color: #3b82f6;
  color: white;
  padding: 8px 16px;
  border-radius: 4px;
  font-weight: 600;
  transition: background-color 0.2s;
}

.btn-primary:hover {
  background-color: #2563eb;
}
tsx
import './styles/button.css';

function MyButton() {
  return <button className="btn-primary">点击</button>;
}

Tailwind CSS 写法

tsx
function MyButton() {
  return (
    <button className="bg-blue-500 text-white px-4 py-2 rounded font-semibold 
                       hover:bg-blue-600 transition-colors duration-200">
      点击
    </button>
  );
}

4.4.3 Tailwind 在项目中的配置

项目的 Tailwind 配置文件:

javascript
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

配置解读

  • content:指定 Tailwind 需要扫描的文件路径,它会从这些文件中提取使用的类名并生成对应的 CSS
  • theme.extend:可以扩展默认主题,添加自定义颜色、间距等
  • plugins:可以引入官方插件(如 @tailwindcss/forms@tailwindcss/typography

4.4.4 项目中的 Tailwind 使用示例

项目中的 Dashboard 组件使用了大量 Tailwind 类:

tsx
function Dashboard() {
  return (
    <div className="p-6 space-y-6">
      {/* 统计卡片 */}
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
        <div className="bg-white rounded-lg shadow p-6">
          <div className="text-sm font-medium text-gray-500">服务器总数</div>
          <div className="mt-2 text-3xl font-bold">{stats.serverCount}</div>
        </div>
        <div className="bg-white rounded-lg shadow p-6">
          <div className="text-sm font-medium text-gray-500">Agent 数量</div>
          <div className="mt-2 text-3xl font-bold">{stats.agentCount}</div>
        </div>
        {/* ... 更多卡片 */}
      </div>
    </div>
  );
}

常用 Tailwind 类速查

类别类名示例含义
布局flex, grid, block, inlineFlex/Grid/块级/行内布局
间距p-4, m-2, gap-6, space-y-4内边距/外边距/间距
尺寸w-full, h-64, max-w-4xl宽度/高度/最大宽度
颜色bg-blue-500, text-white, border-gray-200背景色/文字色/边框色
排版text-lg, font-bold, text-center字号/字重/对齐
圆角rounded, rounded-lg, rounded-full圆角大小
阴影shadow, shadow-lg, shadow-xl阴影大小
响应式md:, lg:, xl:断点前缀(中/大/超大屏幕)
状态hover:, focus:, active:交互状态前缀
过渡transition, duration-200, ease-in-out过渡动画

4.4.5 响应式设计

Tailwind 使用移动优先的响应式设计策略:

tsx
<div className="grid 
  grid-cols-1      {/* 默认(手机):1 列 */}
  sm:grid-cols-2   {/* >=640px:2 列 */}
  md:grid-cols-3   {/* >=768px:3 列 */}
  lg:grid-cols-4   {/* >=1024px:4 列 */}
  gap-4">
  {/* 内容 */}
</div>

断点速查表

断点前缀最小宽度对应设备
sm:640px大手机/小平板
md:768px平板
lg:1024px小桌面
xl:1280px桌面
2xl:1536px大桌面

4.4.6 动手练习

  1. 打开项目中的任何一个页面组件,找出使用的 Tailwind 类并解释其含义
  2. 使用 Tailwind 创建一个响应式的卡片布局,在手机上 1 列,平板上 2 列,桌面 3 列
  3. 为卡片添加 hover 效果:鼠标悬停时卡片上浮并加深阴影

4.5 Zustand:轻量级状态管理

4.5.1 状态管理是什么?

在前端应用中,**状态(State)**是随时间变化的数据。React 组件内部的状态(useState)只能在组件内部使用,当多个组件需要共享状态时,就需要状态管理方案。

类比理解:如果 React 组件是房间里的灯,useState 就是每个灯的独立开关。但如果整个楼层需要一个总控开关(比如关闭所有灯),就需要一个中央控制面板,这就是状态管理库的作用。

4.5.2 为什么选择 Zustand?

项目选择 Zustand 而非 Redux,原因如下:

特性ZustandRedux
学习曲线极低,5分钟上手较陡,需要理解 Action、Reducer、Store
代码量极少需要大量样板代码
性能优秀,自动优化重渲染需要手动优化
TypeScript 支持完美需要额外配置
中间件支持有(devtools、persist)丰富(thunk、saga)

4.5.3 Zustand 核心 API

Zustand 的核心 API 极其简洁:

typescript
import { create } from 'zustand';

// 创建 store
interface AppStore {
  // 状态
  servers: Server[];
  selectedServer: Server | null;
  
  // 操作
  setServers: (servers: Server[]) => void;
  selectServer: (server: Server) => void;
}

const useAppStore = create<AppStore>((set) => ({
  servers: [],
  selectedServer: null,
  
  setServers: (servers) => set({ servers }),
  selectServer: (server) => set({ selectedServer: server }),
}));

// 在组件中使用
function ServerList() {
  const { servers, selectServer } = useAppStore();
  
  return (
    <ul>
      {servers.map(server => (
        <li key={server.id} onClick={() => selectServer(server)}>
          {server.name}
        </li>
      ))}
    </ul>
  );
}

4.5.4 项目中的状态管理策略

项目采用混合状态管理策略

状态类型管理方式适用场景
全局 UI 状态Zustand主题、侧边栏展开状态
服务器数据React QueryAgent 列表、工作流列表
表单状态useState表单输入、验证
认证状态React Context用户信息、登录状态
工作流编辑器状态@xyflow/react 内部状态节点位置、连线关系

这种策略的优势

  • Zustand 处理简单的全局 UI 状态
  • React Query 处理需要从 API 获取的服务器状态
  • Context 处理认证这种特殊的跨组件状态
  • 每个工具做最擅长的事

4.5.5 动手练习

  1. 使用 Zustand 创建一个简单的计数器 Store
  2. 扩展 Store,添加 incrementdecrementreset 操作
  3. 在两个不同的组件中使用同一个 Store,验证状态共享

4.6 React Query:服务器状态管理

4.6.1 React Query 是什么?

React Query 是专门用于处理**服务器状态(Server State)**的库。它解决了传统数据获取中的许多痛点:缓存、自动重新请求、分页、乐观更新等。

类比理解:如果 API 调用是去图书馆借书,React Query 就是一个智能图书管理员。它会记住你借过的书(缓存),有人再借同一本书时直接给你(命中缓存),书过期了会自动去图书馆更新(后台重新请求)。

4.6.2 核心概念

tsx
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

// 查询数据
function ServerList() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['servers'],          // 查询的键,用于缓存
    queryFn: () => fetch('/api/servers').then(res => res.json()),
  });

  if (isLoading) return <div>加载中...</div>;
  if (error) return <div>加载失败: {error.message}</div>;

  return (
    <ul>
      {data.map(server => (
        <li key={server.id}>{server.name}</li>
      ))}
    </ul>
  );
}

// 修改数据
function AddServerForm() {
  const queryClient = useQueryClient();
  
  const mutation = useMutation({
    mutationFn: (newServer) => 
      fetch('/api/servers', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(newServer),
      }).then(res => res.json()),
    onSuccess: () => {
      // 请求成功后,使 servers 列表的缓存失效,触发重新请求
      queryClient.invalidateQueries({ queryKey: ['servers'] });
    },
  });

  const handleSubmit = (e) => {
    e.preventDefault();
    mutation.mutate({ name: 'New Server', host: '192.168.1.1' });
  };

  return <form onSubmit={handleSubmit}>...</form>;
}

4.6.3 项目中的 API 封装

项目将 React Query 与 Axios 结合使用,统一封装了 API 调用:

typescript
// frontend/src/lib/api.ts 中的模式
import axios from 'axios';

const api = axios.create({
  baseURL: '/api',
});

// 请求拦截器:自动附加 JWT Token
api.interceptors.request.use((config) => {
  const token = localStorage.getItem('token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// 响应拦截器:统一处理错误
api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      // Token 过期,跳转到登录页
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

4.6.4 缓存策略

React Query 的默认缓存策略:

配置默认值含义
staleTime0数据获取后立即变为"陈旧",下次聚焦窗口时重新请求
gcTime5分钟缓存的数据在无组件使用后 5 分钟被垃圾回收
retry3请求失败时重试次数
refetchOnWindowFocustrue窗口获得焦点时自动重新请求

项目中的推荐配置

typescript
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5,  // 5分钟内数据有效
      retry: 2,                   // 失败重试2次
    },
  },
});

4.6.5 动手练习

  1. 使用 useQuery 获取项目中的 Agent 列表数据
  2. 使用 useMutation 实现添加 Agent 功能
  3. 实现删除 Agent 后自动刷新列表的功能

4.7 Socket.io:实时通信基础

4.7.1 WebSocket 是什么?

传统的 HTTP 通信是请求-响应模式:客户端发起请求,服务器返回响应,然后连接关闭。WebSocket 则建立了持久连接,服务器可以主动向客户端推送数据。

类比理解:HTTP 像是打电话,每次想说话都要拨号(请求),说完就挂断(关闭连接)。WebSocket 像是微信视频通话,接通后双方可以随时说话,不用每次重新拨号。

4.7.2 Socket.io 的优势

项目选择 Socket.io 而非原生 WebSocket:

特性Socket.io原生 WebSocket
浏览器兼容自动降级(轮询)需要浏览器支持
断线重连自动需要手动实现
事件系统内置命名事件只能发送字符串
房间机制内置需要手动实现

4.7.3 基本使用

服务端

typescript
import { Server } from 'socket.io';

const io = new Server(server, {
  cors: {
    origin: 'http://localhost:5173',
    credentials: true,
  },
});

io.on('connection', (socket) => {
  console.log('客户端已连接:', socket.id);

  // 监听客户端事件
  socket.on('ssh:input', (data) => {
    // 处理 SSH 输入并转发到服务器
    sshConnection.write(data.command);
  });

  // 向客户端发送事件
  socket.emit('ssh:output', { data: 'command result...' });

  // 断开连接
  socket.on('disconnect', () => {
    console.log('客户端已断开');
  });
});

客户端

typescript
import { io } from 'socket.io-client';

const socket = io('http://localhost:3001');

// 连接成功
socket.on('connect', () => {
  console.log('已连接服务器');
});

// 监听服务器事件
socket.on('ssh:output', (data) => {
  console.log('收到输出:', data);
  terminal.write(data.data);
});

// 向服务器发送事件
function sendCommand(cmd) {
  socket.emit('ssh:input', { command: cmd });
}

4.7.4 项目中的 Socket.io 应用

项目中的主要 Socket.io 应用场景:

场景事件名方向用途
Web SSHssh:input / ssh:output双向实时终端输入输出
Agent 执行agent:progress服务器→客户端实时进度推送
工作流执行workflow:progress服务器→客户端工作流节点执行进度
告警通知alert:new服务器→客户端新告警实时通知

4.7.5 动手练习

  1. 创建一个简单的 Socket.io 服务器,实现客户端连接日志记录
  2. 在前端实现连接并发送一个自定义事件
  3. 尝试实现一个简单的聊天室功能

4.8 Vite:新一代前端构建工具

4.8.1 Vite 是什么?

Vite 是一个现代化的前端构建工具,相比传统的 Webpack,它具有极快的开发服务器启动速度高效的热模块替换(HMR)

类比理解:Webpack 像一个全能但缓慢的建筑工人,每次都要把所有材料运到工地才能开工;Vite 像一个聪明的调度员,利用现代浏览器的原生模块支持,只运送当前需要的材料,所以开工快。

4.8.2 Vite vs Webpack

特性ViteWebpack
开发服务器启动秒级(按需编译)分钟级(全量打包)
热更新极快(基于 ESM)较慢(需要重新打包)
配置复杂度简单复杂
生产构建使用 Rollup使用 Webpack
TypeScript 支持内置需要额外 loader

4.8.3 项目中的 Vite 配置

typescript
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  server: {
    port: 5173,
    proxy: {
      '/api': {
        target: 'http://localhost:3001',
        changeOrigin: true,
      },
    },
  },
});

配置解读

  • plugins:使用 React 插件支持 JSX 转换和 HMR
  • server.port:开发服务器端口
  • server.proxy:将 /api 请求代理到后端服务器,解决开发环境跨域问题

4.8.4 Vite 的工作原理

开发模式:
  浏览器请求 /src/App.tsx

  Vite 拦截请求

  实时编译 App.tsx(利用浏览器 ESM)

  返回编译后的代码

  浏览器执行
  
生产构建:
  运行 `vite build`

  使用 Rollup 打包所有文件

  代码压缩、Tree Shaking、代码分割

  输出到 dist/ 目录

4.8.5 动手练习

  1. 修改 vite.config.ts 中的端口号为 3001
  2. 添加一个环境变量,在不同环境下读取不同的 API 地址
  3. 运行 vite build,查看输出目录结构和文件大小

4.9 Docker:容器化部署基础

4.9.1 Docker 是什么?

Docker 是一个容器化平台,它允许你将应用及其所有依赖打包成一个独立的、可移植的镜像(Image),然后在任何支持 Docker 的环境中运行。

类比理解:如果应用是一道菜,传统部署像是去不同的厨房(服务器)现场做菜,每个厨房的调料和设备可能不同;Docker 则像是预制菜(镜像),在任何微波炉(Docker 引擎)里加热就能得到相同的味道。

4.9.2 核心概念

概念说明类比
镜像(Image)只读模板,包含应用和依赖预制菜的配方和食材包
容器(Container)镜像的运行实例加热后的预制菜
Dockerfile构建镜像的指令文件菜谱
Volume(卷)持久化数据存储冰箱(独立于厨房的存在)
Docker Compose多容器编排工具厨房管理调度员

4.9.3 Dockerfile 解析

项目的后端 Dockerfile:

dockerfile
# 多阶段构建:第一阶段,构建
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# 多阶段构建:第二阶段,运行
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
EXPOSE 3000
CMD ["node", "dist/app.js"]

逐行解读

dockerfile
FROM node:20-alpine AS builder
  • 基于 node:20-alpine 镜像(Node.js 20 的 Alpine Linux 版本,体积小)
  • AS builder 给这个阶段命名,方便后续引用
dockerfile
WORKDIR /app
COPY package*.json ./
RUN npm ci
  • 设置工作目录为 /app
  • 只复制 package.jsonpackage-lock.json
  • 安装依赖(npm cinpm install 更快、更确定)
  • 为什么先复制 package.json? 因为依赖变化不频繁,Docker 可以缓存这一层
dockerfile
COPY . .
RUN npm run build
  • 复制所有源代码
  • 执行构建(TypeScript 编译为 JavaScript)
dockerfile
FROM node:20-alpine
  • 开始第二阶段,使用新的基础镜像(不包含构建工具)
  • 为什么用多阶段构建? 最终镜像不包含 TypeScript 编译器和开发依赖,体积更小
dockerfile
COPY --from=builder /app/dist ./dist
  • 从 builder 阶段复制编译后的代码
dockerfile
EXPOSE 3000
CMD ["node", "dist/app.js"]
  • 声明容器暴露的端口
  • 设置默认启动命令

4.9.4 Docker Compose 解析

项目的 docker-compose.yml

yaml
version: '3.8'

services:
  frontend:
    build:
      context: .
      dockerfile: docker/Dockerfile.frontend
    ports:
      - "80:80"
    depends_on:
      - backend
    networks:
      - itops-net

  backend:
    build:
      context: .
      dockerfile: docker/Dockerfile.backend
    ports:
      - "3000:3000"
    volumes:
      - ./data:/app/data
    env_file:
      - .env
    networks:
      - itops-net

networks:
  itops-net:
    driver: bridge

关键配置解读

配置项含义
services定义服务(容器),每个服务对应一个容器
build指定构建上下文和 Dockerfile 路径
ports端口映射:主机端口:容器端口
depends_on依赖关系,确保后端先启动
volumes数据卷挂载:主机路径:容器路径
env_file环境变量文件
networks自定义网络,服务间可通过服务名通信

4.9.5 常用 Docker 命令

bash
# 构建并启动
docker compose up -d

# 查看日志
docker compose logs -f backend

# 停止服务
docker compose stop

# 停止并删除容器
docker compose down

# 进入容器
docker compose exec backend sh

# 查看容器状态
docker compose ps

# 重新构建并启动
docker compose up -d --build

4.9.6 动手练习

  1. 编写一个简单的 Dockerfile,运行一个 Node.js Hello World
  2. 使用 docker compose up 启动项目,观察启动日志
  3. 修改 .env 文件中的端口,重启并验证变化

4.10 其他关键依赖

4.10.1 前端关键依赖

库名版本用途
@xyflow/react^12.0.0工作流编辑器(节点/连线/拖拽)
xterm^5.3.0Web SSH 终端渲染
xterm-addon-fit^0.8.0终端自适应调整大小
xterm-addon-web-links^0.9.0终端链接识别
lucide-react^0.294.0图标库
react-router-dom^6.20.1路由管理
markdown-it^14.0.0Markdown 渲染
date-fns^2.30.0日期格式化

4.10.2 后端关键依赖

库名版本用途
express^4.18.2Web 框架
better-sqlite3^9.2.2SQLite 数据库驱动
ssh2^1.15.0SSH 连接管理
socket.io^4.7.2WebSocket 服务
jsonwebtoken^9.0.2JWT 认证
bcryptjs^2.4.3密码哈希
helmet^7.1.0HTTP 安全头
cors^2.8.5跨域资源共享
node-schedule^2.1.1定时任务调度

4.10.3 @xyflow/react:工作流编辑器

@xyflow/react 是项目工作流编辑器的核心库,它提供了:

  • 节点拖拽和放置
  • 节点之间的连线
  • 节点类型自定义
  • 平移和缩放画布
  • 撤销和重做

基础使用

tsx
import { ReactFlow, Controls, Background, useNodesState, useEdgesState } from '@xyflow/react';

function WorkflowEditor() {
  const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
  const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);

  return (
    <ReactFlow
      nodes={nodes}
      edges={edges}
      onNodesChange={onNodesChange}
      onEdgesChange={onEdgesChange}
    >
      <Controls />
      <Background />
    </ReactFlow>
  );
}

本章小结

本章系统介绍了项目使用的核心技术栈:

技术角色一句话总结
TypeScript类型系统给 JavaScript 增加编译时类型检查,减少运行时错误
React前端框架组件化构建 UI,通过 Hooks 管理状态和副作用
Express后端框架提供路由、中间件机制,构建 RESTful API
Tailwind CSS样式方案原子化 CSS,通过组合工具类快速构建样式
Zustand全局状态轻量级状态管理,比 Redux 更简洁易用
React Query服务器状态专门处理 API 数据的获取、缓存和同步
Socket.io实时通信基于 WebSocket 的双向实时通信
Vite构建工具新一代前端构建工具,开发服务器启动极快
Docker容器化将应用和依赖打包为可移植镜像
@xyflow/react工作流编辑器实现可视化工作流拖拽编辑
xterm.js终端渲染在浏览器中渲染 SSH 终端

这些技术各司其职,组合起来构建了一个完整的企业级 IT 运维多 Agent 自动化平台。


本章练习

基础练习

  1. 概念理解:用自己的话解释以下概念的区别:

    • useState vs Zustand Store
    • HTTP vs WebSocket
    • 镜像 vs 容器
    • 中间件 vs 路由处理函数
  2. 代码阅读

    • 打开 frontend/src/App.tsx,找出所有使用的 React Router API
    • 打开 backend/src/app.ts,列出所有使用的 Express 中间件
    • 打开 docker-compose.yml,找出所有定义的服务
  3. 环境验证

    • 运行 node -v 确认 Node.js 版本
    • 运行 docker --version 确认 Docker 版本
    • 运行 npm run dev 确认前端开发服务器能正常启动

进阶练习

  1. TypeScript 实战

    • 为以下数据定义 TypeScript 接口:工作流节点、SSH 连接配置、告警信息
    • 使用泛型定义一个通用的 API 响应类型:ApiResponse<T>
  2. React 实战

    • 创建一个包含表单验证的组件,使用 useStateuseEffect
    • 实现一个自定义 Hook useLocalStorage,用于读写 localStorage
  3. Docker 实战

    • 修改 Dockerfile,添加健康检查(HEALTHCHECK)
    • 使用 docker compose exec 进入容器执行命令
    • 尝试导出和导入 Docker 镜像

思考题

  1. 为什么项目选择 Zustand 而非 Redux?在你的项目中你会做同样的选择吗?为什么?

  2. 项目的构建工具选择了 Vite 而非 Webpack,你认为 Vite 的核心优势是什么?在什么场景下你仍然会选择 Webpack?

  3. Docker 的多阶段构建有什么优势?如果不用多阶段构建,最终镜像会有什么问题?


延伸阅读


本章回顾:你已经学习了项目的所有核心技术栈!在下一章中,我们将深入解析项目的整体架构,理解各个模块如何协同工作。

基于 MPL-2.0 许可证发布