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

Nim 完全指南 / 18 Web 开发

第 18 章:Web 开发

18.1 Web 框架概览

框架特点适用场景
Jester轻量、Sinatra 风格小型 API、微服务
Prologue全功能、Django 风格中大型 Web 应用
HTTPbeast高性能 HTTP 服务器高并发服务
Mummy多线程 HTTP生产级服务

18.2 Jester 入门

安装

nimble install jester

基本应用

import jester

routes:
  get "/":
    resp "Hello, World!"
  
  get "/hello/@name":
    resp "Hello, " & @"name" & "!"
  
  post "/api/data":
    let body = request.body
    resp Http200, "Received: " & body, "text/plain"
  
  get "/json":
    resp %*{"message": "Hello", "status": "ok"}

路由与参数

import jester, json, strutils

routes:
  # 路径参数
  get "/user/@id":
    let userId = @"id"
    resp %*{"id": userId, "name": "User " & userId}
  
  # 查询参数
  get "/search":
    let q = request.params.getOrDefault("q", "")
    let limit = parseInt(request.params.getOrDefault("limit", "10"))
    resp %*{"query": q, "limit": limit}
  
  # 正则路由
  get re"/api/v(\d+)/users":
    let version = request.matches[0]
    resp %*{"version": version}
  
  # 多种 HTTP 方法
  get "/items":
    resp %*{"items": ["a", "b", "c"]}
  
  post "/items":
    let data = parseJson(request.body)
    resp Http201, %*{"created": data}
  
  put "/items/@id":
    resp %*{"updated": @"id"}
  
  delete "/items/@id":
    resp Http204
  
  # 静态文件
  get "/static/@path":
    sendStaticFile(@"path")
  
  # 通配符
  get "*":
    resp Http404, "Not Found"

中间件与错误处理

import jester, times

routes:
  before:
    echo &"[{now()}] {request.reqMethod} {request.path}"
  
  after:
    response.headers["X-Powered-By"] = "Nim+Jester"
  
  get "/":
    resp "OK"
  
  error Exception:
    resp Http500, "Internal Server Error"

18.3 Prologue 入门

安装

nimble install prologue

基本应用

import prologue
import prologue/middlewares

proc hello(ctx: Context) {.async.} =
  resp "<h1>Hello, Prologue!</h1>"

proc jsonApi(ctx: Context) {.async.} =
  resp jsonResponse(%*{"message": "Hello", "status": "ok"})

proc getUser(ctx: Context) {.async.} =
  let userId = ctx.getPathParams("id")
  resp jsonResponse(%*{"id": userId, "name": "User " & userId})

proc createUser(ctx: Context) {.async.} =
  let body = ctx.getRequestBody()
  let data = parseJson(body)
  resp Http201, jsonResponse(%*{"created": data})

let app = newApp()
app.addRoute("/", hello, HttpGet)
app.addRoute("/api/hello", jsonApi, HttpGet)
app.addRoute("/user/{id}", getUser, HttpGet)
app.addRoute("/user", createUser, HttpPost)
app.run()

路由组

import prologue

proc listUsers(ctx: Context) {.async.} =
  resp jsonResponse(%*{"users": []})

proc getUser(ctx: Context) {.async.} =
  resp jsonResponse(%*{"id": ctx.getPathParams("id")})

proc createUser(ctx: Context) {.async.} =
  resp Http201, jsonResponse(%*{"created": true})

let app = newApp()

# 路由组
let apiRouter = newRouter("/api")
apiRouter.addRoute("/users", listUsers, HttpGet)
apiRouter.addRoute("/users/{id}", getUser, HttpGet)
apiRouter.addRoute("/users", createUser, HttpPost)

app.addRouter(apiRouter)
app.run()

中间件

import prologue
import prologue/middlewares

proc loggingMiddleware(handler: HandlerAsync): HandlerAsync =
  result = proc(ctx: Context) {.async.} =
    echo &"[{now()}] {ctx.request.reqMethod} {ctx.request.path}"
    await handler(ctx)
    echo &"  Status: {ctx.response.code}"

proc corsMiddleware(handler: HandlerAsync): HandlerAsync =
  result = proc(ctx: Context) {.async.} =
    ctx.response.headers["Access-Control-Allow-Origin"] = "*"
    ctx.response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE"
    await handler(ctx)

let app = newApp()
app.use(loggingMiddleware)
app.use(corsMiddleware)
app.run()

18.4 HTTPbeast

安装

nimble install httpbeast

高性能 HTTP 服务

import httpbeast, json

proc onRequest(req: Request) {.gcsafe.} =
  case req.path.get()
  of "/":
    req.send(Http200, "Hello from HTTPbeast!")
  of "/json":
    req.send(Http200, $(%*{"message": "OK"}), "application/json")
  of "/echo":
    if req.httpMethod.get() == HttpPost:
      req.send(Http200, req.body.get())
    else:
      req.send(Http405, "Method Not Allowed")
  else:
    req.send(Http404, "Not Found")

let settings = initSettings(port = Port(8080), numThreads = 4)
run(onRequest, settings)

18.5 JSON API 实战

RESTful API

import jester, json, sequtils, tables, strutils

type
  Todo = object
    id: int
    title: string
    completed: bool

var todos = initTable[int, Todo]()
var nextId = 1

routes:
  get "/api/todos":
    let items = toSeq(todos.values).mapIt(%*{
      "id": it.id,
      "title": it.title,
      "completed": it.completed
    })
    resp %*{"data": items, "total": items.len}
  
  post "/api/todos":
    let body = parseJson(request.body)
    let todo = Todo(
      id: nextId,
      title: body["title"].getStr(),
      completed: false
    )
    todos[todo.id] = todo
    inc nextId
    resp Http201, %*{"data": todo}
  
  get "/api/todos/@id":
    let id = parseInt(@"id")
    if todos.hasKey(id):
      let todo = todos[id]
      resp %*{"data": todo}
    else:
      resp Http404, %*{"error": "Not found"}
  
  put "/api/todos/@id":
    let id = parseInt(@"id")
    if todos.hasKey(id):
      let body = parseJson(request.body)
      todos[id] = Todo(
        id: id,
        title: body{"title"}.getStr(todos[id].title),
        completed: body{"completed"}.getBool(todos[id].completed)
      )
      resp %*{"data": todos[id]}
    else:
      resp Http404, %*{"error": "Not found"}
  
  delete "/api/todos/@id":
    let id = parseInt(@"id")
    if todos.hasKey(id):
      todos.del(id)
      resp Http204
    else:
      resp Http404, %*{"error": "Not found"}

18.6 实战示例

🏢 场景:JWT 认证

import jester, json, times, strutils

const SECRET = "my-secret-key"

proc base64Encode(s: string): string =
  # 简化的 Base64 编码
  import std/base64
  encode(s)

proc createToken(userId: string, expiresInHours = 24): string =
  let header = base64Encode($ %*{"alg": "HS256", "typ": "JWT"})
  let payload = base64Encode($ %*{
    "sub": userId,
    "iat": epochTime().int,
    "exp": (epochTime() + expiresInHours.float * 3600).int
  })
  header & "." & payload & ".signature"

proc authMiddleware(request: Request): string =
  let auth = request.headers.getOrDefault("Authorization", "")
  if not auth.startsWith("Bearer "):
    return ""
  auth[7..^1]

routes:
  post "/api/login":
    let body = parseJson(request.body)
    let username = body["username"].getStr()
    let password = body["password"].getStr()
    
    if username == "admin" and password == "password":
      let token = createToken(username)
      resp %*{"token": token}
    else:
      resp Http401, %*{"error": "Invalid credentials"}
  
  get "/api/protected":
    let token = authMiddleware(request)
    if token.len == 0:
      resp Http401, %*{"error": "Unauthorized"}
    else:
      resp %*{"message": "Access granted", "token": token}

本章小结

框架安装特点
Jesternimble install jester轻量、Sinatra 风格
Prologuenimble install prologue全功能、中间件
HTTPbeastnimble install httpbeast高性能
Mummynimble install mummy多线程

练习

  1. 使用 Jester 实现一个 CRUD API
  2. 使用 Prologue 创建带中间件的 Web 应用
  3. 实现 JWT 认证系统

扩展阅读


上一章:外部函数接口 | 下一章:测试与质量