Nim 完全指南 / 25 实战项目
第 25 章:实战项目
本章将三个完整的实战项目,涵盖 CLI 工具、Web API 和系统工具。
25.1 项目一:CLI 文件搜索工具
功能需求
- 支持按关键词搜索文件内容
- 支持正则表达式
- 支持递归搜索目录
- 彩色高亮输出
完整实现
# src/fuzzy.nim
import std/[
os, strutils, re, parseopt, terminal, strformat
]
type
SearchConfig = object
pattern: string
directory: string
recursive: bool
useRegex: bool
caseSensitive: bool
filePattern: string
SearchResult = object
file: string
lineNum: int
line: string
matches: seq[tuple[first, last: int]]
proc printUsage() =
echo """
用法: fuzzy [选项] <模式> [目录]
选项:
-r, --recursive 递归搜索子目录
-e, --regex 使用正则表达式
-i, --ignore-case 忽略大小写
-p, --pattern GLOB 文件名匹配模式 (如 "*.nim")
-h, --help 显示帮助信息
示例:
fuzzy "TODO" src/
fuzzy -r -e "proc \w+" src/
fuzzy -i -p "*.nim" "error" .
"""
proc searchFile(path: string, config: SearchConfig): seq[SearchResult] =
result = @[]
var lineNum = 0
try:
let content = readFile(path)
for line in content.splitLines():
inc lineNum
var matches: seq[tuple[first, last: int]] = @[]
if config.useRegex:
try:
let regex = re(config.pattern, {reIgnoreCase} *
(if not config.caseSensitive: {reIgnoreCase} else: {}))
let matchResult = line.findBounds(regex)
if matchResult.first >= 0:
matches.add(matchResult)
except RegexError:
discard
else:
let searchText = if config.caseSensitive: line else: line.toLower()
let searchPattern = if config.caseSensitive: config.pattern
else: config.pattern.toLower()
let pos = searchText.find(searchPattern)
if pos >= 0:
matches.add((pos, pos + searchPattern.len - 1))
if matches.len > 0:
result.add(SearchResult(
file: path,
lineNum: lineNum,
line: line,
matches: matches
))
except IOError:
discard
proc searchDirectory(dir: string, config: SearchConfig): seq[SearchResult] =
result = @[]
for entry in walkDir(dir):
case entry.kind
of pcFile, pcLinkToFile:
let filename = entry.path.extractFilename()
if config.filePattern.len == 0 or filename.matches(config.filePattern):
result.add(searchFile(entry.path, config))
of pcDir, pcLinkToDir:
if config.recursive:
result.add(searchDirectory(entry.path, config))
proc highlightMatch(line: string, first, last: int): string =
result = line[0..<first]
result.add("\e[1;31m") # 红色高亮
result.add(line[first..last])
result.add("\e[0m")
result.add(line[last+1..^1])
proc printResult(r: SearchResult) =
let fileColor = "\e[1;32m" # 绿色
let lineColor = "\e[1;33m" # 黄色
let resetColor = "\e[0m"
stdout.write &"{fileColor}{r.file}{resetColor}"
stdout.write &":{lineColor}{r.lineNum}{resetColor}: "
if r.matches.len > 0 and stdout.isatty():
let m = r.matches[0]
echo highlightMatch(r.line, m.first, m.last)
else:
echo r.line
proc parseArgs(): SearchConfig =
var config = SearchConfig(
directory: ".",
recursive: false,
useRegex: false,
caseSensitive: true,
filePattern: ""
)
var args: seq[string] = @[]
for kind, key, val in getopt():
case kind
of cmdArgument:
args.add(key)
of cmdLongOption, cmdShortOption:
case key
of "recursive", "r": config.recursive = true
of "regex", "e": config.useRegex = true
of "ignore-case", "i": config.caseSensitive = false
of "pattern", "p": config.filePattern = val
of "help", "h":
printUsage()
quit(0)
else:
echo &"未知选项: {key}"
quit(1)
of cmdEnd: discard
if args.len < 1:
printUsage()
quit(1)
config.pattern = args[0]
if args.len > 1:
config.directory = args[1]
config
proc main() =
let config = parseArgs()
if not dirExists(config.directory):
echo &"错误: 目录不存在: {config.directory}"
quit(1)
let results = searchDirectory(config.directory, config)
if results.len == 0:
echo &"未找到匹配项: {config.pattern}"
quit(1)
for r in results:
printResult(r)
echo &"\n共找到 {results.len} 个匹配项"
when isMainModule:
main()
编译和使用:
nim c -d:release -o:fuzzy src/fuzzy.nim
./fuzzy -r "proc" src/
./fuzzy -r -e "func \w+" src/
./fuzzy -i -p "*.nim" "todo" .
25.2 项目二:RESTful Web API
功能需求
- 用户管理 CRUD API
- JSON 数据库
- JWT 认证
- 中间件(日志、CORS)
完整实现
# src/apidemo.nim
import std/[
json, tables, times, strutils, strformat, os, sequtils
]
import jester
type
User = object
id: int
name: string
email: string
role: string
AppState = object
users: Table[int, User]
nextId: int
secret: string
var state = AppState(
users: initTable[int, User](),
nextId: 1,
secret: "nim-secret-key-2026"
)
# 初始数据
state.users[1] = User(id: 1, name: "管理员", email: "admin@example.com", role: "admin")
state.users[2] = User(id: 2, name: "张三", email: "zhangsan@example.com", role: "user")
proc toJson(user: User): JsonNode =
%*{"id": user.id, "name": user.name, "email": user.email, "role": user.role}
proc createToken(userId: int, role: string): string =
let header = encode($ %*{"alg": "HS256", "typ": "JWT"})
let payload = encode($ %*{
"sub": userId,
"role": role,
"iat": epochTime().int,
"exp": (epochTime() + 3600).int
})
header & "." & payload & ".sig"
proc verifyToken(token: string): JsonNode =
let parts = token.split(".")
if parts.len != 3:
return nil
try:
let payload = decode(parts[1])
let data = parseJson(payload)
if data["exp"].getInt() < epochTime().int:
return nil
return data
except:
return nil
routes:
# 健康检查
get "/health":
resp Http200, %*{"status": "ok", "timestamp": epochTime()}
# 登录
post "/api/login":
let body = parseJson(request.body)
let email = body["email"].getStr()
let password = body["password"].getStr()
var foundUser: User
var found = false
for u in state.users.values:
if u.email == email:
foundUser = u
found = true
break
if found and password == "password": # 简化演示
let token = createToken(foundUser.id, foundUser.role)
resp %*{"token": token, "user": foundUser.toJson()}
else:
resp Http401, %*{"error": "认证失败"}
# 用户列表
get "/api/users":
let users = toSeq(state.users.values).mapIt(it.toJson())
resp %*{"data": users, "total": users.len}
# 获取单个用户
get "/api/users/@id":
let id = parseInt(@"id")
if state.users.hasKey(id):
resp %*{"data": state.users[id].toJson()}
else:
resp Http404, %*{"error": "用户不存在"}
# 创建用户
post "/api/users":
let body = parseJson(request.body)
let user = User(
id: state.nextId,
name: body["name"].getStr(),
email: body["email"].getStr(),
role: body{"role"}.getStr("user")
)
state.users[user.id] = user
inc state.nextId
resp Http201, %*{"data": user.toJson()}
# 更新用户
put "/api/users/@id":
let id = parseInt(@"id")
if not state.users.hasKey(id):
resp Http404, %*{"error": "用户不存在"}
return
let body = parseJson(request.body)
var user = state.users[id]
if body.hasKey("name"): user.name = body["name"].getStr()
if body.hasKey("email"): user.email = body["email"].getStr()
if body.hasKey("role"): user.role = body["role"].getStr()
state.users[id] = user
resp %*{"data": user.toJson()}
# 删除用户
delete "/api/users/@id":
let id = parseInt(@"id")
if state.users.hasKey(id):
state.users.del(id)
resp Http204
else:
resp Http404, %*{"error": "用户不存在"}
# 404
resp Http404, %*{"error": "未找到"}
编译运行:
nimble install jester
nim c -d:release -o:api src/apidemo.nim
./api
API 测试:
# 获取用户列表
curl http://localhost:5000/api/users
# 创建用户
curl -X POST http://localhost:5000/api/users \
-H "Content-Type: application/json" \
-d '{"name": "新用户", "email": "new@example.com"}'
# 更新用户
curl -X PUT http://localhost:5000/api/users/3 \
-H "Content-Type: application/json" \
-d '{"name": "更新后的名字"}'
# 删除用户
curl -X DELETE http://localhost:5000/api/users/3
25.3 项目三:系统监控工具
功能需求
- 实时显示 CPU、内存、磁盘使用率
- 进程列表
- 彩色终端 UI
完整实现
# src/sysmon.nim
import std/[
os, strutils, strformat, times, terminal, sequtils, algorithm
]
type
SystemInfo = object
cpuUsage: float
memTotal, memUsed, memFree: int64
diskTotal, diskUsed, diskFree: int64
uptime: float
loadAvg: array[3, float]
ProcessInfo = object
pid: int
name: string
cpu: float
mem: float
state: string
proc readProcFile(path: string): string =
try: readFile(path)
except: ""
proc getCpuUsage(): float =
let stat = readProcFile("/proc/stat")
let firstLine = stat.splitLines()[0]
let values = firstLine.splitWhitespace()[1..^1].mapIt(parseBiggestInt(it))
let idle = values[3]
let total = values.foldl(a + b, 0.int64)
result = 100.0 * (1.0 - idle.float / total.float)
proc getMemoryInfo(): (int64, int64, int64) =
let meminfo = readProcFile("/proc/meminfo")
var total, free, available: int64
for line in meminfo.splitLines():
if line.startsWith("MemTotal:"):
total = parseInt(line.splitWhitespace()[1]) * 1024
elif line.startsWith("MemFree:"):
free = parseInt(line.splitWhitespace()[1]) * 1024
elif line.startsWith("MemAvailable:"):
available = parseInt(line.splitWhitespace()[1]) * 1024
(total, total - available, available)
proc getDiskInfo(): (int64, int64, int64) =
let (_, total, free) = getDiskUsage("/")
(total, total - free, free)
proc formatBytes(bytes: int64): string =
if bytes >= 1_073_741_824: &"{bytes.float / 1_073_741_824:.1f} GB"
elif bytes >= 1_048_576: &"{bytes.float / 1_048_576:.1f} MB"
elif bytes >= 1024: &"{bytes.float / 1024:.1f} KB"
else: &"{bytes} B"
proc drawProgressBar(width: int, percent: float,
color: string): string =
let filled = int(percent / 100.0 * width.float)
let empty = width - filled
result = color & "█".repeat(filled) & "\e[0m" & "░".repeat(empty)
proc getProcessList(): seq[ProcessInfo] =
result = @[]
for entry in walkDir("/proc"):
if entry.kind != pcDir: continue
let pidStr = entry.path.extractFilename()
var pid: int
try: pid = parseInt(pidStr)
except: continue
let comm = readProcFile(entry.path / "comm").strip()
let stat = readProcFile(entry.path / "stat").splitWhitespace()
if stat.len < 24: continue
let state = stat[2]
let utime = parseInt(stat[13])
let stime = parseInt(stat[14])
let starttime = parseInt(stat[21])
let statm = readProcFile(entry.path / "statm").splitWhitespace()
var mem = 0.0
if statm.len > 1:
mem = parseInt(statm[1]).float * 4096 / 1_073_741_824
result.add(ProcessInfo(
pid: pid,
name: comm,
cpu: (utime + stime).float / 100.0,
mem: mem,
state: state
))
result.sort(proc(a, b: ProcessInfo): int = cmp(b.cpu, a.cpu))
proc clearScreen() =
stdout.write "\e[2J\e[H"
proc moveCursor(row, col: int) =
stdout.write &"\e[{row};{col}H"
proc renderHeader() =
stdout.write "\e[1;36m" # 青色
echo "╔══════════════════════════════════════════════════════════════════╗"
echo "║ 系统监控工具 (sysmon) ║"
echo "║ 按 Ctrl+C 退出 ║"
echo "╚══════════════════════════════════════════════════════════════════╝"
stdout.write "\e[0m"
proc renderSystemInfo(info: SystemInfo) =
let cpuColor = if info.cpuUsage > 80: "\e[1;31m" # 红色
elif info.cpuUsage > 50: "\e[1;33m" # 黄色
else: "\e[1;32m" # 绿色
let memPercent = info.memUsed.float / info.memTotal.float * 100
let memColor = if memPercent > 80: "\e[1;31m"
elif memPercent > 50: "\e[1;33m"
else: "\e[1;32m"
let diskPercent = info.diskUsed.float / info.diskTotal.float * 100
let diskColor = if diskPercent > 90: "\e[1;31m"
elif diskPercent > 70: "\e[1;33m"
else: "\e[1;32m"
echo ""
echo &" CPU 使用率: {cpuColor}{info.cpuUsage:5.1f}%\e[0m ",
drawProgressBar(30, info.cpuUsage, cpuColor)
echo &" 内存使用: {memColor}{memPercent:5.1f}%\e[0m ",
drawProgressBar(30, memPercent, memColor),
&" {formatBytes(info.memUsed)} / {formatBytes(info.memTotal)}"
echo &" 磁盘使用: {diskColor}{diskPercent:5.1f}%\e[0m ",
drawProgressBar(30, diskPercent, diskColor),
&" {formatBytes(info.diskUsed)} / {formatBytes(info.diskTotal)}"
echo ""
proc renderProcessList(procs: seq[ProcessInfo], maxRows = 15) =
echo "\e[1;35m" # 紫色
echo &" {'PID':<8} {'状态':<4} {'CPU%':>6} {'内存':>8} {'名称'}"
echo " " & "─".repeat(60)
echo "\e[0m"
for i in 0..<min(procs.len, maxRows):
let p = procs[i]
let stateStr = case p.state
of "R": "运行"
of "S": "睡眠"
of "D": "等待"
of "Z": "僵尸"
else: "?"
let cpuColor = if p.cpu > 10: "\e[1;31m"
elif p.cpu > 5: "\e[1;33m"
else: "\e[0m"
echo &" {p.pid:<8} {stateStr:<4} {cpuColor}{p.cpu:>5.1f}%\e[0m ",
&"{p.mem:>7.1f}G {p.name}"
proc main() =
hideCursor()
while true:
clearScreen()
let cpu = getCpuUsage()
let (memTotal, memUsed, memFree) = getMemoryInfo()
let (diskTotal, diskUsed, diskFree) = getDiskInfo()
let procs = getProcessList()
let info = SystemInfo(
cpuUsage: cpu,
memTotal: memTotal, memUsed: memUsed, memFree: memFree,
diskTotal: diskTotal, diskUsed: diskUsed, diskFree: diskFree
)
renderHeader()
renderSystemInfo(info)
renderProcessList(procs)
echo &"\n 更新时间: {now().format(\"HH:mm:ss\")}"
sleep(2000)
when isMainModule:
try:
main()
except KeyboardInterrupt:
showCursor()
echo "\n再见!"
编译运行:
nim c -d:release -o:sysmon src/sysmon.nim
sudo ./sysmon # 需要 root 权限读取 /proc
25.4 项目总结
技术栈
| 项目 | 模块 | 技术 |
|---|---|---|
| CLI 工具 | os, strutils, re, parseopt, terminal | 文件 I/O、正则、命令行解析 |
| Web API | jester, json, tables | HTTP 路由、JSON、认证 |
| 系统工具 | os, terminal, times | /proc 文件系统、终端控制 |
扩展建议
| 项目 | 可扩展功能 |
|---|---|
| CLI 搜索 | 支持 .gitignore、多文件并行搜索、模糊匹配 |
| Web API | 添加 SQLite 数据库、Swagger 文档、WebSocket |
| 系统监控 | 网络监控、告警系统、历史数据图表 |
全书总结
恭喜你完成了 Nim 完全指南 的学习!回顾一下我们涵盖的内容:
| 部分 | 章节 | 核心技能 |
|---|---|---|
| 基础入门 | 1-6 | 语法、类型、控制流 |
| 核心编程 | 7-12 | 函数、数据结构、OOP、泛型、元编程 |
| 工程实践 | 13-17 | 错误处理、模块、I/O、并发、FFI |
| 应用开发 | 18-21 | Web、测试、Docker、性能 |
| 进阶实战 | 22-25 | C/JS 后端、最佳实践、项目实战 |
继续学习
祝你在 Nim 的世界里编程愉快!🚀
← 上一章:最佳实践