第4章 技术栈入门
作者
谭策 — 独立开发者 | AIOps 领域探索者
- 🌐 项目官网:ITOpsAgentinfo
- 📝 博客:zjzwfw.cloud
- 📧 邮箱:huawei_network@foxmail.com
- 💬 微信公众号: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 的核心优势:
- 编译时类型检查:在代码运行前就能发现类型错误,而不是等到线上出 Bug
- 智能提示:编辑器可以准确提示每个变量、函数的类型和方法
- 自文档化:类型定义本身就是最好的文档,阅读代码时一目了然
- 重构安全:修改函数签名时,编辑器会提示所有需要更新的地方
4.1.2 项目中的 TypeScript 配置
项目的 TypeScript 配置位于 tsconfig.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" }]
}关键配置解读:
| 配置项 | 值 | 含义 |
|---|---|---|
strict | true | 启用所有严格类型检查选项 |
noUnusedLocals | true | 声明了但未使用的变量会报错 |
noUnusedParameters | true | 函数参数未使用会报错 |
noEmit | true | TypeScript 编译器不输出文件(由 Vite 处理编译) |
jsx | react-jsx | 使用 React 的新 JSX 转换,无需 import React |
4.1.3 类型定义实战
让我们看一个项目中的实际类型定义示例。在后端,数据库操作返回的数据需要明确定义类型:
// 用户信息接口
export interface UserInfo {
id: string; // 用户 ID,UUID 格式
username: string; // 用户名
email: string; // 邮箱
role: 'admin' | 'user'; // 角色,只能是 admin 或 user
password?: string; // 密码,可选字段(返回时通常不包含)
createdAt: string; // 创建时间
updatedAt: string; // 更新时间
}使用示例:
// 正确:类型完全匹配
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: true | strict: true |
重要提醒:TypeScript 的类型只在编译时有效,运行时所有类型都会被擦除。这意味着你不能在运行时检查 TypeScript 类型,只能通过 JavaScript 的方式(如 typeof)检查。
4.1.5 动手练习
- 打开项目代码,找到
frontend/src/types/目录,阅读其中的类型定义文件 - 尝试修改一个类型定义,观察 TypeScript 编译器如何报错
- 在
UserInfo接口中添加一个phone可选字段,并更新相关代码
4.2 React:构建用户界面的乐高积木
4.2.1 React 是什么?
React 是一个用于构建用户界面的 JavaScript 库。它的核心理念是组件化:将 UI 拆分成独立的、可复用的组件,然后通过组合这些组件来构建完整的界面。
类比理解:React 就像乐高积木。每个组件就是一个积木块,你可以用小块拼成大块,最终搭建成城堡。积木之间通过**属性(props)**来传递信息,就像乐高积木的凸点和凹槽一样。
4.2.2 JSX:在 JavaScript 中写 HTML
JSX 是 React 的核心语法,它允许你在 JavaScript 代码中直接写类似 HTML 的代码:
// 这是一个简单的 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 的关键规则:
- JSX 必须有唯一的根元素(可以用
<>...</>包裹多个元素) - 使用
className而不是class(因为class是 JavaScript 保留字) - 在 JSX 中嵌入 JavaScript 表达式需要用花括号
{} - 自定义组件名必须大写(如
<WelcomeCard>),小写表示 HTML 元素(如<div>)
4.2.3 Hooks:让函数组件拥有状态
在 React 16.8 之前,只有类组件(Class Component)才能使用状态(state)。Hooks 的出现让函数组件也能拥有状态管理能力。项目中使用的主要 Hooks:
useState:管理组件内部状态
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:处理副作用
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:优化性能
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. 组件导出:统一使用默认导出
// 正确的做法
export default function Dashboard() {
// ...
}
// 项目也使用这种方式3. Props 类型定义:使用 TypeScript 接口定义 Props
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 动手练习
- 打开
frontend/src/components/Layout.tsx,找出其中使用了哪些 Hooks - 尝试在
Dashboard.tsx中添加一个useState,记录页面访问次数 - 创建一个简单的
HelloWorld组件,接收name属性并显示欢迎信息
4.3 Express:后端的路由调度器
4.3.1 Express 是什么?
Express 是一个基于 Node.js 的 Web 应用框架,它提供了简洁的 API 来处理 HTTP 请求、路由、中间件等。
类比理解:如果把后端比作一个餐厅,Express 就是前台服务员 + 点餐系统。它负责接待客人(接收请求)、点单(路由匹配)、把订单送到厨房(调用业务逻辑)、最后把菜端给客人(返回响应)。
4.3.2 Express 核心概念
路由(Routing)
路由定义了 URL 到处理函数的映射关系:
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();
});项目中路由的实际写法:
// 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验证] 验证用户身份
↓
[路由处理函数] 执行业务逻辑
↓
返回响应自定义中间件示例:
// 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);中间件的关键规则:
- 中间件函数接收三个参数:
req、res、next - 必须调用
next()将控制权传递给下一个中间件,否则请求会挂起 - 如果中间件发送了响应(如
res.json()),则不能再调用next() - 中间件的注册顺序很重要,先注册的先执行
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 提供了统一的错误处理中间件:
// 错误处理中间件
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 动手练习
- 打开
backend/src/app.ts,列出所有注册的路由组及其路径 - 创建一个简单的 Express 应用,包含
/api/hello路由,返回{"message": "Hello!"} - 为上述应用添加一个请求日志中间件,记录每个请求的时间和 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 写法:
/* 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;
}import './styles/button.css';
function MyButton() {
return <button className="btn-primary">点击</button>;
}Tailwind CSS 写法:
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 配置文件:
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}配置解读:
content:指定 Tailwind 需要扫描的文件路径,它会从这些文件中提取使用的类名并生成对应的 CSStheme.extend:可以扩展默认主题,添加自定义颜色、间距等plugins:可以引入官方插件(如@tailwindcss/forms、@tailwindcss/typography)
4.4.4 项目中的 Tailwind 使用示例
项目中的 Dashboard 组件使用了大量 Tailwind 类:
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, inline | Flex/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 使用移动优先的响应式设计策略:
<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 动手练习
- 打开项目中的任何一个页面组件,找出使用的 Tailwind 类并解释其含义
- 使用 Tailwind 创建一个响应式的卡片布局,在手机上 1 列,平板上 2 列,桌面 3 列
- 为卡片添加 hover 效果:鼠标悬停时卡片上浮并加深阴影
4.5 Zustand:轻量级状态管理
4.5.1 状态管理是什么?
在前端应用中,**状态(State)**是随时间变化的数据。React 组件内部的状态(useState)只能在组件内部使用,当多个组件需要共享状态时,就需要状态管理方案。
类比理解:如果 React 组件是房间里的灯,useState 就是每个灯的独立开关。但如果整个楼层需要一个总控开关(比如关闭所有灯),就需要一个中央控制面板,这就是状态管理库的作用。
4.5.2 为什么选择 Zustand?
项目选择 Zustand 而非 Redux,原因如下:
| 特性 | Zustand | Redux |
|---|---|---|
| 学习曲线 | 极低,5分钟上手 | 较陡,需要理解 Action、Reducer、Store |
| 代码量 | 极少 | 需要大量样板代码 |
| 性能 | 优秀,自动优化重渲染 | 需要手动优化 |
| TypeScript 支持 | 完美 | 需要额外配置 |
| 中间件支持 | 有(devtools、persist) | 丰富(thunk、saga) |
4.5.3 Zustand 核心 API
Zustand 的核心 API 极其简洁:
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 Query | Agent 列表、工作流列表 |
| 表单状态 | useState | 表单输入、验证 |
| 认证状态 | React Context | 用户信息、登录状态 |
| 工作流编辑器状态 | @xyflow/react 内部状态 | 节点位置、连线关系 |
这种策略的优势:
- Zustand 处理简单的全局 UI 状态
- React Query 处理需要从 API 获取的服务器状态
- Context 处理认证这种特殊的跨组件状态
- 每个工具做最擅长的事
4.5.5 动手练习
- 使用 Zustand 创建一个简单的计数器 Store
- 扩展 Store,添加
increment、decrement、reset操作 - 在两个不同的组件中使用同一个 Store,验证状态共享
4.6 React Query:服务器状态管理
4.6.1 React Query 是什么?
React Query 是专门用于处理**服务器状态(Server State)**的库。它解决了传统数据获取中的许多痛点:缓存、自动重新请求、分页、乐观更新等。
类比理解:如果 API 调用是去图书馆借书,React Query 就是一个智能图书管理员。它会记住你借过的书(缓存),有人再借同一本书时直接给你(命中缓存),书过期了会自动去图书馆更新(后台重新请求)。
4.6.2 核心概念
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 调用:
// 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 的默认缓存策略:
| 配置 | 默认值 | 含义 |
|---|---|---|
staleTime | 0 | 数据获取后立即变为"陈旧",下次聚焦窗口时重新请求 |
gcTime | 5分钟 | 缓存的数据在无组件使用后 5 分钟被垃圾回收 |
retry | 3 | 请求失败时重试次数 |
refetchOnWindowFocus | true | 窗口获得焦点时自动重新请求 |
项目中的推荐配置:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5分钟内数据有效
retry: 2, // 失败重试2次
},
},
});4.6.5 动手练习
- 使用
useQuery获取项目中的 Agent 列表数据 - 使用
useMutation实现添加 Agent 功能 - 实现删除 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 基本使用
服务端:
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('客户端已断开');
});
});客户端:
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 SSH | ssh:input / ssh:output | 双向 | 实时终端输入输出 |
| Agent 执行 | agent:progress | 服务器→客户端 | 实时进度推送 |
| 工作流执行 | workflow:progress | 服务器→客户端 | 工作流节点执行进度 |
| 告警通知 | alert:new | 服务器→客户端 | 新告警实时通知 |
4.7.5 动手练习
- 创建一个简单的 Socket.io 服务器,实现客户端连接日志记录
- 在前端实现连接并发送一个自定义事件
- 尝试实现一个简单的聊天室功能
4.8 Vite:新一代前端构建工具
4.8.1 Vite 是什么?
Vite 是一个现代化的前端构建工具,相比传统的 Webpack,它具有极快的开发服务器启动速度和高效的热模块替换(HMR)。
类比理解:Webpack 像一个全能但缓慢的建筑工人,每次都要把所有材料运到工地才能开工;Vite 像一个聪明的调度员,利用现代浏览器的原生模块支持,只运送当前需要的材料,所以开工快。
4.8.2 Vite vs Webpack
| 特性 | Vite | Webpack |
|---|---|---|
| 开发服务器启动 | 秒级(按需编译) | 分钟级(全量打包) |
| 热更新 | 极快(基于 ESM) | 较慢(需要重新打包) |
| 配置复杂度 | 简单 | 复杂 |
| 生产构建 | 使用 Rollup | 使用 Webpack |
| TypeScript 支持 | 内置 | 需要额外 loader |
4.8.3 项目中的 Vite 配置
// 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 转换和 HMRserver.port:开发服务器端口server.proxy:将/api请求代理到后端服务器,解决开发环境跨域问题
4.8.4 Vite 的工作原理
开发模式:
浏览器请求 /src/App.tsx
↓
Vite 拦截请求
↓
实时编译 App.tsx(利用浏览器 ESM)
↓
返回编译后的代码
↓
浏览器执行
生产构建:
运行 `vite build`
↓
使用 Rollup 打包所有文件
↓
代码压缩、Tree Shaking、代码分割
↓
输出到 dist/ 目录4.8.5 动手练习
- 修改
vite.config.ts中的端口号为 3001 - 添加一个环境变量,在不同环境下读取不同的 API 地址
- 运行
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:
# 多阶段构建:第一阶段,构建
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"]逐行解读:
FROM node:20-alpine AS builder- 基于
node:20-alpine镜像(Node.js 20 的 Alpine Linux 版本,体积小) AS builder给这个阶段命名,方便后续引用
WORKDIR /app
COPY package*.json ./
RUN npm ci- 设置工作目录为
/app - 只复制
package.json和package-lock.json - 安装依赖(
npm ci比npm install更快、更确定) - 为什么先复制 package.json? 因为依赖变化不频繁,Docker 可以缓存这一层
COPY . .
RUN npm run build- 复制所有源代码
- 执行构建(TypeScript 编译为 JavaScript)
FROM node:20-alpine- 开始第二阶段,使用新的基础镜像(不包含构建工具)
- 为什么用多阶段构建? 最终镜像不包含 TypeScript 编译器和开发依赖,体积更小
COPY --from=builder /app/dist ./dist- 从 builder 阶段复制编译后的代码
EXPOSE 3000
CMD ["node", "dist/app.js"]- 声明容器暴露的端口
- 设置默认启动命令
4.9.4 Docker Compose 解析
项目的 docker-compose.yml:
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 命令
# 构建并启动
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 --build4.9.6 动手练习
- 编写一个简单的 Dockerfile,运行一个 Node.js Hello World
- 使用
docker compose up启动项目,观察启动日志 - 修改
.env文件中的端口,重启并验证变化
4.10 其他关键依赖
4.10.1 前端关键依赖
| 库名 | 版本 | 用途 |
|---|---|---|
@xyflow/react | ^12.0.0 | 工作流编辑器(节点/连线/拖拽) |
xterm | ^5.3.0 | Web 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.0 | Markdown 渲染 |
date-fns | ^2.30.0 | 日期格式化 |
4.10.2 后端关键依赖
| 库名 | 版本 | 用途 |
|---|---|---|
express | ^4.18.2 | Web 框架 |
better-sqlite3 | ^9.2.2 | SQLite 数据库驱动 |
ssh2 | ^1.15.0 | SSH 连接管理 |
socket.io | ^4.7.2 | WebSocket 服务 |
jsonwebtoken | ^9.0.2 | JWT 认证 |
bcryptjs | ^2.4.3 | 密码哈希 |
helmet | ^7.1.0 | HTTP 安全头 |
cors | ^2.8.5 | 跨域资源共享 |
node-schedule | ^2.1.1 | 定时任务调度 |
4.10.3 @xyflow/react:工作流编辑器
@xyflow/react 是项目工作流编辑器的核心库,它提供了:
- 节点拖拽和放置
- 节点之间的连线
- 节点类型自定义
- 平移和缩放画布
- 撤销和重做
基础使用:
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 自动化平台。
本章练习
基础练习
概念理解:用自己的话解释以下概念的区别:
useStatevs Zustand Store- HTTP vs WebSocket
- 镜像 vs 容器
- 中间件 vs 路由处理函数
代码阅读:
- 打开
frontend/src/App.tsx,找出所有使用的 React Router API - 打开
backend/src/app.ts,列出所有使用的 Express 中间件 - 打开
docker-compose.yml,找出所有定义的服务
- 打开
环境验证:
- 运行
node -v确认 Node.js 版本 - 运行
docker --version确认 Docker 版本 - 运行
npm run dev确认前端开发服务器能正常启动
- 运行
进阶练习
TypeScript 实战:
- 为以下数据定义 TypeScript 接口:工作流节点、SSH 连接配置、告警信息
- 使用泛型定义一个通用的 API 响应类型:
ApiResponse<T>
React 实战:
- 创建一个包含表单验证的组件,使用
useState和useEffect - 实现一个自定义 Hook
useLocalStorage,用于读写 localStorage
- 创建一个包含表单验证的组件,使用
Docker 实战:
- 修改 Dockerfile,添加健康检查(HEALTHCHECK)
- 使用
docker compose exec进入容器执行命令 - 尝试导出和导入 Docker 镜像
思考题
为什么项目选择 Zustand 而非 Redux?在你的项目中你会做同样的选择吗?为什么?
项目的构建工具选择了 Vite 而非 Webpack,你认为 Vite 的核心优势是什么?在什么场景下你仍然会选择 Webpack?
Docker 的多阶段构建有什么优势?如果不用多阶段构建,最终镜像会有什么问题?
延伸阅读
- TypeScript:TypeScript 官方手册
- React:React 官方文档
- Express:Express 官方指南
- Tailwind CSS:Tailwind CSS 文档
- React Query:TanStack Query 文档
- Zustand:Zustand GitHub
- Socket.io:Socket.io 官方文档
- Vite:Vite 官方文档
- Docker:Docker 官方入门教程
- @xyflow/react:React Flow 文档
本章回顾:你已经学习了项目的所有核心技术栈!在下一章中,我们将深入解析项目的整体架构,理解各个模块如何协同工作。
