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

Nim 完全指南 / 23 JavaScript 后端

第 23 章:JavaScript 后端

23.1 基本编译

# 编译为 JavaScript
nim js -o:output.js main.nim

# 优化
nim js -d:release -o:output.js main.nim
# main.nim
echo "Hello from Nim (compiled to JS)!"

在浏览器中使用:

<!DOCTYPE html>
<html>
<head><title>Nim JS</title></head>
<body>
  <script src="output.js"></script>
</body>
</html>

在 Node.js 中使用:

node output.js

23.2 条件编译

# 根据后端选择不同实现
when defined(js):
  proc alert(msg: string) {.importjs: "alert(#)".}
  
  proc fetch(url: string): Future[JsObject] {.importjs: "fetch(#)".}
  
  echo "Running in JavaScript!"
else:
  echo "Running natively!"

# 检查运行环境
when defined(js):
  when defined(nodejs):
    echo "Running in Node.js"
  else:
    echo "Running in browser"

23.3 JavaScript 互操作

23.3.1 importjs

# 导入 JavaScript 函数
proc consoleLog(msg: cstring) {.importjs: "console.log(#)".}
proc setTimeout(callback: proc(), ms: int): int {.importjs: "setTimeout(@)".}
proc parseInt(s: cstring): int {.importjs: "parseInt(#)".}

consoleLog("Hello!")
discard setTimeout(proc() = echo "Delayed!", 1000)
echo parseInt("42")

23.3.2 JsObject

import std/jsffi

# 创建 JavaScript 对象
let obj = newJsObject()
obj["name"] = "Nim".toJs
obj["version"] = 2.toJs

# 访问属性
echo obj["name"].to(cstring)
echo obj["version"].to(int)

# 创建数组
let arr = newJsArray()
arr.push(1.toJs)
arr.push("hello".toJs)
echo arr.length

# JSON 操作
let json = JSON.parse("""{"key": "value"}""")
echo json["key"].to(cstring)

23.3.3 回调与异步

import std/[jsffi, asyncjs]

# 异步函数
proc fetchData(url: string): Future[JsObject] {.async.} =
  let response = await fetch(url.toJs)
  let data = await response.json()
  return data

# 使用
proc main() {.async.} =
  let data = await fetchData("https://api.example.com/data")
  console.log(data)

discard main()

23.4 DOM 操作

23.4.1 基本 DOM

import std/dom

# 获取元素
let el = document.getElementById("app")
let items = document.getElementsByClassName("item")
let first = document.querySelector(".container")

# 修改元素
el.innerHTML = "<h1>Hello from Nim!</h1>".cstring
el.setAttribute("class", "active".cstring)
el.style.color = "red".cstring

# 创建元素
let div = document.createElement("div")
div.innerHTML = "New element".cstring
document.body.appendChild(div)

# 事件处理
el.addEventListener("click", proc(event: Event) =
  echo "Clicked!"
)

23.4.2 事件处理

import std/dom

proc onButtonClick(event: Event) =
  let input = document.getElementById("name-input")
  let name = cast[InputElement](input).value
  let output = document.getElementById("output")
  output.innerHTML = ("Hello, " & $name & "!").cstring

# 绑定事件
let button = document.getElementById("submit-btn")
button.addEventListener("click", onButtonClick)

# 表单事件
let form = document.getElementById("my-form")
form.addEventListener("submit", proc(event: Event) =
  event.preventDefault()
  echo "Form submitted!"
)

23.5 Node.js 模块

23.5.1 文件系统

import std/jsffi

type FSModule = JsObject
let fs: FSModule = require("fs")

# 读取文件
proc readFileSync(path: cstring): cstring {.importjs: "fs.readFileSync(#, 'utf8')".}
proc writeFileSync(path, data: cstring) {.importjs: "fs.writeFileSync(#, #)".}

let content = readFileSync("data.txt".cstring)
echo content
writeFileSync("output.txt".cstring, "Hello from Nim!".cstring)

23.5.2 HTTP 服务器

import std/jsffi

type HttpModule = JsObject
let http: HttpModule = require("http")

proc createServer(callback: proc(req, res: JsObject)): JsObject =
  http.createServer(callback)

let server = createServer(proc(req, res: JsObject) =
  res.writeHead(200.toJs, %*{"Content-Type": "text/plain"})
  res.end("Hello from Nim Node.js server!".toJs)
)

server.listen(3000.toJs, proc() =
  echo "Server running on port 3000"
)

23.6 前端框架集成

23.6.1 Karax(Nim 的 Virtual DOM 库)

# nimble install karax
import karax / [karaxdsl, vdom, kdom]

var count = 0

proc render(): VNode =
  buildHtml(tdiv):
    h1: text "Counter: " & $count
    button(onclick = proc(ev: Event, tgt: VNode) =
      inc count
    ):
      text "Increment"
    button(onclick = proc(ev: Event, tgt: VNode) =
      count = 0
    ):
      text "Reset"

setRenderer render

编译:

nim js -d:release -o:app.js frontend.nim
<!DOCTYPE html>
<html>
<head>
  <script src="app.js"></script>
</head>
<body id="ROOT">
</body>
</html>

23.6.2 Karax 路由

import karax / [karaxdsl, vdom, kdom, kajax]

var currentPage = ""

proc renderHome(): VNode =
  buildHtml(tdiv):
    h1: text "Home Page"
    a(href = "#/about"): text "About"

proc renderAbout(): VNode =
  buildHtml(tdiv):
    h1: text "About Page"
    a(href = "#/"): text "Home"

proc render(): VNode =
  let hash = $window.location.hash
  case hash
  of "#/about": renderAbout()
  else: renderHome()

setRenderer render

23.7 实战示例

🏢 场景:全栈 Nim 应用

后端(Jester):

# backend.nim - 编译为本地二进制
import jester, json

var todos: seq[JsonNode] = @[]

routes:
  get "/api/todos":
    resp %*{"data": todos}
  
  post "/api/todos":
    let todo = parseJson(request.body)
    todos.add(todo)
    resp Http201, %*{"created": todo}

前端(Karax):

# frontend.nim - 编译为 JavaScript
import karax / [karaxdsl, vdom, kdom, kajax]
import std/jsffi

var todos: seq[cstring] = @[]
var newTodo = ""

proc addTodo() =
  if newTodo.len > 0:
    todos.add(newTodo)
    newTodo = ""

proc render(): VNode =
  buildHtml(tdiv):
    h1: text "Todo App"
    input(`type` = "text",
          value = newTodo,
          oninput = proc(ev: Event, tgt: VNode) =
            newTodo = $tgt.value
          )
    button(onclick = proc(ev: Event, tgt: VNode) =
      addTodo()
    ):
      text "Add"
    
    ul:
      for todo in todos:
        li: text todo

setRenderer render

🏢 场景:数据可视化

import std/dom

type ChartConfig = JsObject

proc createChart(canvas: Element, config: ChartConfig) {.importjs: "new Chart(#, #)".}

# 初始化图表
let canvas = document.getElementById("myChart")
let config = %*{
  "type": "bar",
  "data": {
    "labels": ["Jan", "Feb", "Mar", "Apr", "May"],
    "datasets": [{
      "label": "Sales",
      "data": [12, 19, 3, 5, 2],
      "backgroundColor": "rgba(54, 162, 235, 0.5)"
    }]
  }
}
createChart(canvas, config)

本章小结

功能语法模块
编译 JSnim js
JS 导入{.importjs.}
DOM 操作dom 模块std/dom
异步asyncjsstd/asyncjs
JS 对象JsObjectstd/jsffi
虚拟 DOMKarax第三方

练习

  1. 使用 Nim + Karax 创建一个单页应用
  2. 为 Node.js 编写一个命令行工具
  3. 创建一个前后端都用 Nim 编写的应用

扩展阅读


上一章:C 后端深入 | 下一章:最佳实践