强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 APIjester, json, tablesHTTP 路由、JSON、认证
系统工具os, terminal, times/proc 文件系统、终端控制

扩展建议

项目可扩展功能
CLI 搜索支持 .gitignore、多文件并行搜索、模糊匹配
Web API添加 SQLite 数据库、Swagger 文档、WebSocket
系统监控网络监控、告警系统、历史数据图表

全书总结

恭喜你完成了 Nim 完全指南 的学习!回顾一下我们涵盖的内容:

部分章节核心技能
基础入门1-6语法、类型、控制流
核心编程7-12函数、数据结构、OOP、泛型、元编程
工程实践13-17错误处理、模块、I/O、并发、FFI
应用开发18-21Web、测试、Docker、性能
进阶实战22-25C/JS 后端、最佳实践、项目实战

继续学习

祝你在 Nim 的世界里编程愉快!🚀


上一章:最佳实践