Nim 2.2.10 · 2026

Nim 生成 JavaScript 完整教程

从零开始掌握 Nim 编译器的 JavaScript 后端,使用 dom 库操控浏览器,构建现代 Web 应用

关于本教程

本教程基于 Nim 2.2.10(2026 年 4 月发布),全面覆盖使用 Nim 编译器将代码编译为 JavaScript 的所有核心知识。所有代码示例均经过实际编译和运行测试,确保结果准确无误。

Nim 是一门静态类型、编译型的系统编程语言,语法受 Python 启发,同时具备 Lisp 级别的元编程能力。通过 js 命令,Nim 可以将代码编译为高效的 JavaScript,直接在浏览器或 Node.js 中运行。

高性能

Nim 编译为原生 JavaScript,无运行时开销,生成的代码紧凑高效

🔒

类型安全

静态类型系统在编译期捕获错误,减少运行时崩溃

🌐

完整 DOM 支持

内置 dom 模块提供完整的浏览器 DOM API 绑定

🔄

双向互操作

通过 importc/exportc 无缝调用 JavaScript 代码

章节导航

本教程分为 7 个章节,建议按顺序学习。点击侧边栏导航可跳转到对应章节。

适合人群

  • 有 JavaScript / TypeScript 基础,希望尝试静态类型语言的前端开发者
  • 对 Nim 语言感兴趣,想了解其 JavaScript 后端能力的开发者
  • 追求更高性能和类型安全的 Web 应用开发者
  • 希望用一门语言同时编写前端和后端的全栈开发者
测试环境

本教程所有代码均在 Nim 2.2.10 + Node.js v22.22.2 环境下编译并运行通过。DOM 相关代码在浏览器环境中测试编译。

快速入门

本章将带你从零开始:安装 Nim 编译器、编写第一个 JavaScript 程序、理解编译选项,并了解项目结构。

安装 Nim 编译器

推荐使用 choosenim 工具安装 Nim,它会自动下载并配置最新版本。

Linux / macOS

curl -sL https://nim-lang.org/choosenim/init.sh -o choosenim.sh
sh choosenim.sh -y

验证安装

nim --version
Nim Compiler Version 2.2.10 [Linux: amd64] Compiled at 2026-04-24 Copyright (c) 2006-2026 by Andreas Rumpf
环境变量

安装完成后,确保将 ~/.nimble/bin 添加到 PATH 环境变量中。

第一个程序 - Hello World

创建一个名为 hello.nim 的文件:

echo "Hello from Nim to JS!"

编译为 JavaScript

nim js -o:hello.js hello.nim
23051 lines; 0.107s; 20.047MiB peakmem; proj: hello.nim; out: hello.js [SuccessX]

运行生成的 JavaScript

node hello.js
Hello from Nim to JS!

编译命令详解

选项说明示例
js使用 JavaScript 后端编译nim js hello.nim
-o:file指定输出文件名nim js -o:app.js hello.nim
-r编译后自动运行(使用 Node.js)nim js -r hello.nim
-d:release发布模式,启用优化nim js -d:release hello.nim
--opt:size优化文件大小nim js --opt:size hello.nim

编译模式对比

以下示例展示不同编译模式下的差异:

Nim# compile_options.nim
proc calculateFactorial(n: int): int =
  if n <= 1: return 1
  result = n * calculateFactorial(n - 1)

proc calculateFibonacci(n: int): int =
  if n <= 2: return 1
  result = calculateFibonacci(n - 1) + calculateFibonacci(n - 2)

proc reverseString(s: string): string =
  result = newString(s.len)
  for i in 0..<s.len:
    result[s.len - 1 - i] = s[i]

type
  Stack[T] = object
    items: seq[T]
  Queue[T] = object
    items: seq[T]

proc push[T](s: var Stack[T], item: T) = s.items.add(item)
proc pop[T](s: var Stack[T]): T =
  if s.items.len > 0:
    result = s.items[s.items.len - 1]
    s.items.setLen(s.items.len - 1)

proc enqueue[T](q: var Queue[T], item: T) = q.items.add(item)
proc dequeue[T](q: var Queue[T]): T =
  if q.items.len > 0:
    result = q.items[0]
    q.items.delete(0)

echo "Factorial(5) = ", calculateFactorial(5)
echo "Fibonacci(10) = ", calculateFibonacci(10)
echo "Reverse 'hello' = ", reverseString("hello")

var stack: Stack[int]
stack.push(10)
stack.push(20)
stack.push(30)
echo "Stack pop = ", stack.pop()
echo "Stack pop = ", stack.pop()

var queue: Queue[string]
queue.enqueue("first")
queue.enqueue("second")
queue.enqueue("third")
echo "Queue dequeue = ", queue.dequeue()
echo "Queue dequeue = ", queue.dequeue()

Debug 模式(默认)

nim js -r -o:compile_debug.js compile_options.nim
Factorial(5) = 120 Fibonacci(10) = 55 Reverse 'hello' = olleh Stack pop = 30 Stack pop = 20 Queue dequeue = first Queue dequeue = second

文件大小:约 27 KB(包含调试信息)

Release 模式

nim js -d:release -r -o:compile_release.js compile_options.nim
Factorial(5) = 120 Fibonacci(10) = 55 Reverse 'hello' = olleh Stack pop = 30 Stack pop = 20 Queue dequeue = first Queue dequeue = second

文件大小:约 19 KB(优化后更小更快)

提示

Release 模式生成的文件比 Debug 模式小约 30%,且执行速度更快。生产环境部署时务必使用 -d:release

在浏览器中使用

<html>
<body>
  <script src="hello.js"></script>
</body>
</html>

Node.js 与浏览器目标

目标命令说明
Node.jsnim js -r hello.nim编译后直接用 Node.js 运行
浏览器nim js -o:app.js hello.nim生成 JS 文件引入 HTML
注意

Node.js 需要 12.0 以上版本。如果遇到 globalThis is not defined 错误,请升级 Node.js。

项目结构建议

my-project/
├── src/
│   ├── main.nim
│   └── utils.nim
├── public/
│   ├── index.html
│   └── app.js
└── nim.cfg

nim.cfg 配置文件

# nim.cfg
backend:js
-d:release
--out:public/app.js

配置后,只需运行 nim js src/main.nim 即可自动应用所有配置。

基础语法

本章介绍 Nim 的核心语法,这些语法在 JavaScript 后端中均可正常使用。所有代码均经过编译和运行测试。

变量声明

let x = 42
var y = 10
const PI = 3.14159
echo "x = ", x, ", y = ", y
echo "PI = ", PI
x = 42, y = 10 PI = 3.14159

数据类型

类型说明示例
int整数类型let n = 42
float浮点数let f = 3.14
stringNim 字符串let s = "hello"
cstringC 风格字符串,JS 后端对应 JS 字符串let c: cstring = "hi"
bool布尔值let b = true
seq[T]动态数组let arr = @[1, 2, 3]

字符串操作

let name = "Nim"
echo "Hello, " & name & "!"
Hello, Nim!

数组与序列

var arr = [1, 2, 3, 4, 5]
echo "Array: ", arr

var seq1 = @[10, 20, 30]
seq1.add(40)
echo "Seq: ", seq1
Array: [1, 2, 3, 4, 5] Seq: @[10, 20, 30, 40]

元组

let person = (name: "Alice", age: 30)
echo "Person: ", person.name, " is ", person.age, " years old"
Person: Alice is 30 years old

控制流

if-elif-else

if x > 20:
  echo "x is greater than 20"
else:
  echo "x is 20 or less"
x is greater than 20

for 循环

for i in 1..5:
  echo "Iteration: ", i
Iteration: 1 Iteration: 2 Iteration: 3 Iteration: 4 Iteration: 5

while 循环

var count = 0
while count < 3:
  echo "Count: ", count
  inc(count)
Count: 0 Count: 1 Count: 2

过程(proc)

proc add(a, b: int): int =
  return a + b

echo "5 + 3 = ", add(5, 3)
5 + 3 = 8

字符串格式化

import std/strutils
let formatted = "Value: $1, Name: $2" % [$x, name]
echo formatted
Value: 42, Name: Nim

数学运算

import std/math
echo "sqrt(16) = ", sqrt(16.0)
echo "pow(2, 10) = ", pow(2.0, 10.0)
echo "sin(0) = ", sin(0.0)
echo "cos(0) = ", cos(0.0)
echo "floor(3.7) = ", floor(3.7)
echo "ceil(3.2) = ", ceil(3.2)
echo "round(3.5) = ", round(3.5)
echo "abs(-42) = ", abs(-42)
echo "max(10, 20) = ", max(10, 20)
echo "min(10, 20) = ", min(10, 20)
sqrt(16) = 4.0 pow(2, 10) = 1024.0 sin(0) = 0.0 cos(0) = 1.0 floor(3.7) = 3.0 ceil(3.2) = 4.0 round(3.5) = 4.0 abs(-42) = 42 max(10, 20) = 20 min(10, 20) = 10

字符串处理

import std/strutils
let s = "Hello, World!"
echo "Length: ", s.len
echo "Upper: ", s.toUpperAscii()
echo "Lower: ", s.toLowerAscii()
echo "Contains 'World': ", "World" in s
echo "Replace: ", s.replace("World", "Nim")
echo "Split: ", "a,b,c,d".split(",")
echo "Join: ", @["a", "b", "c"].join("-")
echo "Repeat: ", "Ab".repeat(3)
echo "Strip: ", "  hello  ".strip()
echo "Starts with 'Hello': ", s.startsWith("Hello")
Length: 13 Upper: HELLO, WORLD! Lower: hello, world! Contains 'World': true Replace: Hello, Nim! Split: @["a", "b", "c", "d"] Join: a-b-c Repeat: AbAbAb Strip: hello Starts with 'Hello': true

sequtils 高阶函数

import std/sequtils
let nums = [1, 2, 3, 4, 5]
echo "Map: ", nums.mapIt(it * 2)
echo "Filter: ", nums.filterIt(it > 2)
echo "Fold: ", nums.foldl(a + b)
Map: @[2, 4, 6, 8, 10] Filter: @[3, 4, 5] Fold: 15

JSON 处理

import std/json

let jsonObj = %*{
  "name": "Nim",
  "version": "2.2.10",
  "features": ["fast", "expressive", "safe"],
  "details": {
    "paradigm": "multi",
    "typing": "static"
  }
}

echo "JSON: ", jsonObj.pretty()
echo "Name: ", jsonObj["name"].getStr()
echo "Version: ", jsonObj["version"].getStr()
echo "First feature: ", jsonObj["features"][0].getStr()
echo "Paradigm: ", jsonObj["details"]["paradigm"].getStr()

let jsonStr = """{"city": "Beijing", "population": 21540000}"""
let parsed = parseJson(jsonStr)
echo "City: ", parsed["city"].getStr()
echo "Population: ", parsed["population"].getInt()

let arr = %*[1, 2, 3, 4, 5]
echo "Array: ", arr

var mutableObj = %*{"key": "value"}
mutableObj["newKey"] = %*"newValue"
echo "Modified: ", mutableObj
JSON: { "name": "Nim", "version": "2.2.10", "features": ["fast", "expressive", "safe"], "details": {"paradigm": "multi", "typing": "static"} } Name: Nim Version: 2.2.10 First feature: fast Paradigm: multi City: Beijing Population: 21540000 Array: [1,2,3,4,5] Modified: {"key":"value","newKey":"newValue"}

cstring 类型说明

重要区别

cstring 不能直接用 & 拼接。需要先用 $ 转换为 string 类型。

# 正确:用 $ 转换后再拼接
proc greet(name: cstring): cstring =
  return "Hello, " & $name & "!"

# 错误:cstring 不能直接用 & 拼接
# proc greet(name: cstring): cstring =
#   return "Hello, " & name  # 编译错误!

DOM 库详解

Nim 的 std/dom 模块为 JavaScript 浏览器环境提供了完整的 DOM API 绑定。本章将系统介绍如何使用 Nim 操控网页文档。

1. dom 模块简介

std/dom 是 Nim 标准库中专门为 JavaScript 后端提供的模块,通过 import std/dom 导入即可使用。核心类型包括:

  • Document - 表示整个网页文档,提供元素查询和创建的入口
  • Element - 表示 HTML 元素节点,提供属性和样式操作
  • Node - DOM 节点基类,所有节点类型的父类型
  • Event - 表示事件对象,包含事件相关信息

2. Document Ready - 文档就绪事件

import std/dom
proc example(e: Event) = echo "Document is ready"
document.addEventListener("DOMContentLoaded", example)

3. Document Unload - 文档卸载事件

import std/dom
proc example(e: Event) = echo "Document is unloaded"
document.addEventListener("unload", example)

4. 元素查询

import std/dom

proc queryElements() =
  # 通过ID查询
  let el = document.getElementById("myElement")
  if el != nil:
    echo "Found element by ID"
  
  # 通过CSS选择器查询
  let first = document.querySelector(".item")
  if first != nil:
    echo "First item: ", first.textContent
  
  # 查询所有匹配元素
  let all = document.querySelectorAll(".item")
  echo "Found ", all.len, " items"
技巧

getElementByIdquerySelector 在找不到元素时返回 nil,使用前务必进行空值检查。

5. 创建和插入元素

import std/dom

proc createList(): Element =
  let ul = document.createElement("ul")
  ul.setAttribute("id", "dynamic-list")
  ul.setAttribute("class", "list-group")
  
  for i in 1..5:
    let li = document.createElement("li")
    li.textContent = "Item " & $i
    li.setAttribute("class", "list-item")
    ul.appendChild(li)
  
  return ul

proc insertList() =
  let container = document.getElementById("container")
  if container != nil:
    let list = createList()
    container.appendChild(list)

6. 属性操作

import std/dom

proc modifyElement() =
  let el = document.getElementById("myElement")
  if el != nil:
    el.textContent = "Updated text"
    el.setAttribute("style", "color: red;")
    el.setAttribute("data-id", "123")
    
    let id = el.getAttribute("data-id")
    echo "Data ID: ", id
    
    el.removeAttribute("data-id")

7. classList 操作

import std/dom

proc manipulateClasses() =
  let el = document.querySelector("#class-element")
  if el != nil:
    el.classList.add("new-class")
    el.classList.add("another-class")
    el.classList.remove("old-class")
    let hasClass = el.classList.contains("new-class")
    echo "Has new-class: ", hasClass
    el.classList.toggle("active")

8. 样式操作

import std/dom

proc applyStyles() =
  let el = document.querySelector("#styled-element")
  if el != nil:
    el.style.setProperty("color", "red")
    el.style.setProperty("font-size", "24px")
    el.style.setProperty("font-weight", "bold")
    el.style.setProperty("padding", "10px")
    el.style.setProperty("border", "1px solid #ccc")

9. DOM 遍历

import std/dom

proc traverseDOM() =
  let body = document.body
  if body != nil:
    var current = body.firstChild
    while current != nil:
      echo "Node: ", current.nodeName
      current = current.nextSibling

10. 元素操作 - 克隆、替换、删除

import std/dom

proc cloneElement() =
  let original = document.querySelector(".template")
  if original != nil:
    let clone = original.cloneNode(true)
    clone.setAttribute("class", "cloned")
    document.body.appendChild(clone)

proc replaceElement() =
  let oldEl = document.getElementById("old-element")
  if oldEl != nil:
    let newEl = document.createElement("div")
    newEl.textContent = "Replaced content"
    oldEl.parentNode.replaceChild(newEl, oldEl)

proc removeElement() =
  let el = document.getElementById("remove-me")
  if el != nil:
    el.parentNode.removeChild(el)
技巧

cloneNode(true) 执行深拷贝,会复制元素及其所有子节点;cloneNode(false) 仅复制元素本身。

11. 表单操作

import std/dom

proc handleForm() =
  let input = document.getElementById("myInput").InputElement
  if input != nil:
    let val = input.value
    echo "Input value: ", val
    input.value = "new value"
    
    # 表单验证
    if val.len == 0:
      input.setCustomValidity("This field is required")
    else:
      input.setCustomValidity("")

12. LocalStorage

import std/dom

proc storageExample() =
  # localStorage 是 window 对象的属性
  window.localStorage.setItem("username", "nim_user")
  let val = window.localStorage.getItem("username")
  echo "Stored: ", val
  window.localStorage.removeItem("username")

13. 窗口操作

import std/dom

proc windowInfo() =
  echo "Window inner width: ", window.innerWidth
  echo "Window inner height: ", window.innerHeight
  echo "Location: ", window.location.href
  
  # 页面刷新
  # window.location.reload()
  
  # 页面跳转
  # window.location.href = "https://nim-lang.org"

14. 定时器

import std/dom

proc setupTimers() =
  # setTimeout - 延迟执行一次
  proc onTimeout() = echo "Timeout fired!"
  discard setTimeout(onTimeout, 3000)
  
  # setInterval - 重复执行
  var count = 0
  proc tick() =
    inc(count)
    echo "Tick: ", count
  discard setInterval(tick, 1000)

15. Document Autorefresh

import std/dom
proc example() = window.location.reload()
discard setTimeout(example, 5_000)

16. dom 模块核心类型

类型说明主要字段 / 方法
Document网页文档根节点body, head, title, getElementById, querySelector, createElement
ElementHTML 元素节点id, className, textContent, innerHTML, style, classList, getAttribute
NodeDOM 节点基类nodeName, firstChild, nextSibling, parentNode
Event事件对象基类target, currentTarget, type, preventDefault
MouseEvent鼠标事件clientX, clientY, button, shiftKey, ctrlKey
KeyboardEvent键盘事件key, code, keyCode, altKey, ctrlKey, shiftKey
InputElementinput 输入框元素value, type, checked, disabled, setCustomValidity

17. 重要注意事项

cstring 类型

dom 模块中很多属性返回 cstring 类型(如 textContentgetAttribute 的返回值)。cstring 不能直接用 & 运算符拼接,需要先用 $ 转换为 string

e.target 类型转换

事件对象的 e.target 返回 Node 类型。如果需要访问 Element 特有的属性(如 getAttributeclassList),需要进行类型转换,例如 e.target.Element

localStorage 归属

localStoragewindow 对象的属性,不是全局变量。必须通过 window.localStorage 访问,直接使用 localStorage 会导致编译错误。

JavaScript 互操作

Nim 编译为 JavaScript 后,可以通过 importcimportjsexportc 等 pragma 实现与原生 JavaScript 代码的双向互操作。

1. importc - 从 JavaScript 导入函数

# 导入 JavaScript 全局函数
proc jsAlert(msg: cstring) {.importc: "alert", nodecl.}
proc jsConsoleLog(msg: cstring) {.importc: "console.log", nodecl.}
proc jsPrompt(msg, defaultVal: cstring): cstring {.importc: "prompt", nodecl.}

# 使用
jsConsoleLog("This is logged from Nim via console.log")

2. importjs pragma - 更灵活的导入

# 使用 importjs 导入
proc log(msg: cstring) {.importjs: "console.log(#)".}
proc jsParseInt(s: cstring): int {.importjs: "parseInt(#)".}
proc jsStringify(obj: JsObject): cstring {.importjs: "JSON.stringify(#)".}
技巧

importjs 中的 # 会被替换为实际参数。如果有多个参数,按顺序依次替换。

3. exportc - 导出函数给 JavaScript

# 导出函数给 JavaScript 调用
proc multiply(a, b: cint): cint {.exportc.} =
  return a * b

proc fibonacci(n: cint): cint {.exportc.} =
  if n <= 2:
    return 1
  else:
    return fibonacci(n - 1) + fibonacci(n - 2)

proc greet(name: cstring): cstring {.exportc.} =
  return "Hello, " & $name & "!"
fib(10) = 55 multiply(6, 7) = 42 greet(Nim) = Hello, Nim!

4. 在 HTML 中调用导出的函数

<html>
<body>
  <script src="app.js"></script>
  <script>
    // 直接调用 Nim 导出的函数
    console.log("fib(10) = " + fibonacci(10));
    console.log("6 * 7 = " + multiply(6, 7));
    console.log(greet("Nim"));
  </script>
</body>
</html>

5. jsffi 模块 - JsObject 操作

import std/jsffi

proc log(msg: cstring) {.importjs: "console.log(#)".}

# 创建 JS 对象
proc testJsObject() =
  let obj = newJsObject()
  obj["name"] = "Nim".toJs
  obj["version"] = 2.2.toJs
  obj["features"] = @[1, 2, 3].toJs
  
  # 读取属性
  let name = obj["name"].to(cstring)
  let version = obj["version"].to(float)
  log("Name: " & name)
  log("Version: " & $version)
Name: Nim Version: 2.2

6. JS 数组操作

import std/jsffi

proc testJsArray() =
  let arr = @[10, 20, 30].toJs
  
  let length = arr["length"].to(int)
  log("Array length: " & $length)
  
  for i in 0..<3:
    let val = arr[i].to(int)
    log("Item " & $i & ": " & $val)
Array length: 3 Item 0: 10 Item 1: 20 Item 2: 30

7. JSON 操作

import std/jsffi

proc jsParseJson(s: cstring): JsObject {.importjs: "JSON.parse(#)".}
proc jsStringify(o: JsObject): cstring {.importjs: "JSON.stringify(#)".}

proc testJsonOps() =
  let obj = newJsObject()
  obj["a"] = 1.toJs
  obj["b"] = "hello".toJs
  
  let jsonStr = jsStringify(obj)
  log("JSON: " & jsonStr)
  
  let parsed = jsParseJson("""{"x": 42, "y": "world"}""")
  let x = parsed["x"].to(int)
  let y = parsed["y"].to(cstring)
  log("x = " & $x & ", y = " & y)
JSON: {"a":1,"b":"hello"} x = 42, y = world

8. 类型转换

import std/jsffi

proc testConversions() =
  let n = 42.toJs
  let s = "hello".toJs
  let f = 3.14.toJs
  let b = true.toJs
  
  log("int: " & $n.to(int))
  log("string: " & s.to(cstring))
  log("float: " & $f.to(float))
  log("bool: " & $b.to(bool))
int: 42 string: hello float: 3.14 bool: true

9. importjs 高级用法 - 方法调用

import std/jsffi

# 导入对象方法
proc push(arr: JsObject, item: JsObject) {.importjs: "#.push(#)".}
proc pop(arr: JsObject): JsObject {.importjs: "#.pop()".}
proc join(arr: JsObject, sep: cstring): cstring {.importjs: "#.join(#)".}

# 使用
let arr = @[1, 2, 3].toJs
push(arr, 4.toJs)
echo join(arr, ", ")  # "1, 2, 3, 4"

10. 调用 JavaScript 构造函数

import std/jsffi

# 导入 Date 构造函数
proc newDate(): JsObject {.importjs: "new Date()".}
proc newDateStr(s: cstring): JsObject {.importjs: "new Date(#)".}

# 导入 Date 方法
proc getFullYear(d: JsObject): int {.importjs: "#.getFullYear()".}
proc getMonth(d: JsObject): int {.importjs: "#.getMonth()".}
proc getDate(d: JsObject): int {.importjs: "#.getDate()".}

# 使用
let now = newDate()
echo "Year: ", getFullYear(now)
echo "Month: ", getMonth(now) + 1  # 月份从0开始
echo "Day: ", getDate(now)

11. cstring 注意事项

注意

cstring 不能直接用 & 运算符拼接,需要先用 $ 转换为 string。正确做法:"Hello, " & $name & "!"

12. 完整互操作示例

import std/jsffi

# 导入 JavaScript 函数
proc log(msg: cstring) {.importjs: "console.log(#)".}
proc jsAlert(msg: cstring) {.importjs: "alert(#)".}

# 导出函数给 JavaScript
proc calculate(a, b: cint): cint {.exportc.} =
  return a * b + 42

# 使用 JsObject 与 JavaScript 交互
proc processJsData(data: JsObject): JsObject {.exportc.} =
  let name = data["name"].to(cstring)
  let age = data["age"].to(int)
  
  let result = newJsObject()
  result["greeting"] = ("Hello, " & $name & "! You are " & $age & " years old.").toJs
  result["nextAge"] = (age + 1).toJs
  return result

异步编程与 AJAX

使用 Nim 的 std/asyncjs 模块编写异步代码,并通过 Fetch API 完成 AJAX 请求。

一、asyncjs 模块简介

Nim 的 std/asyncjs 模块为 JavaScript 后端提供了完整的异步编程支持,它将 Nim 的 async/await 语法编译为 JavaScript 原生的 Promise

  • 引入方式import std/asyncjs
  • async/await 语法:通过 {.async.} pragma 标记异步过程,使用 await 等待结果
  • Future[T] 类型:表示一个异步计算的结果,对应 JavaScript 的 Promise

二、基本异步 proc

Nimimport std/asyncjs
import std/jsffi

proc setTimeout(cb: proc(), ms: int): int {.importjs: "setTimeout(#, #)".}
proc log(msg: cstring) {.importjs: "console.log(#)".}

# 异步 proc - 使用 {.async.} pragma
proc asyncAdd(a, b: int): Future[int] {.async.} =
  return a + b

proc asyncMultiply(a, b: int): Future[int] {.async.} =
  return a * b

# 异步链式调用
proc calculate(a, b: int): Future[int] {.async.} =
  let sum = await asyncAdd(a, b)
  let product = await asyncMultiply(sum, 2)
  return product

# 主异步函数
proc main() {.async.} =
  log("Starting async operations...")
  let result1 = await asyncAdd(10, 20)
  log("10 + 20 = " & $result1)
  let result2 = await asyncMultiply(5, 6)
  log("5 * 6 = " & $result2)
  let result3 = await calculate(3, 4)
  log("(3 + 4) * 2 = " & $result3)
  log("All async operations completed!")

discard main()
Starting async operations... 10 + 20 = 30 5 * 6 = 30 (3 + 4) * 2 = 14 All async operations completed!

三、Promise 包装

Nimimport std/asyncjs
import std/jsffi

proc setTimeout(cb: proc(), ms: int): int {.importjs: "setTimeout(#, #)".}

# 将回调函数包装为 Promise
proc delay(ms: int): Future[void] =
  return newPromise() do (resolve: proc()):
    discard setTimeout(resolve, ms)

proc main() {.async.} =
  echo "Waiting..."
  await delay(2000)
  echo "2 seconds passed!"

discard main()

四、Fetch API - GET 请求

Nimimport std/asyncjs
import std/jsffi

# 导入 fetch API
proc fetch(url: cstring): Future[JsObject] {.importjs: "fetch(#)".}
proc fetch(url: cstring, options: JsObject): Future[JsObject] {.importjs: "fetch(#, #)".}

# Response 方法
proc json(response: JsObject): Future[JsObject] {.importjs: "#.json()".}
proc text(response: JsObject): Future[cstring] {.importjs: "#.text()".}

proc log(msg: cstring) {.importjs: "console.log(#)".}

# 获取 JSON 数据
proc fetchJson(url: string): Future[JsObject] {.async.} =
  let response = await fetch(url.cstring)
  let data = await json(response)
  return data

# 获取文本数据
proc fetchText(url: string): Future[cstring] {.async.} =
  let response = await fetch(url.cstring)
  let txt = await text(response)
  return txt

proc main() {.async.} =
  log("Fetching data...")
  try:
    let data = await fetchJson("https://api.github.com/repos/nim-lang/Nim")
    log("Fetch completed!")
  except:
    log("Fetch failed!")

discard main()

五、Fetch API - POST 请求

Nimimport std/asyncjs
import std/jsffi

proc fetch(url: cstring, options: JsObject): Future[JsObject] {.importjs: "fetch(#, #)".}
proc json(response: JsObject): Future[JsObject] {.importjs: "#.json()".}

proc postData(url: string, body: string): Future[JsObject] {.async.} =
  let options = newJsObject()
  options["method"] = "POST".toJs
  options["body"] = body.cstring.toJs
  let headers = newJsObject()
  headers["Content-Type"] = "application/json".toJs
  options["headers"] = headers
  let response = await fetch(url.cstring, options)
  let data = await json(response)
  return data

六、错误处理

Nimimport std/asyncjs
import std/jsffi

proc log(msg: cstring) {.importjs: "console.log(#)".}

proc riskyOperation(n: int): Future[int] {.async.} =
  if n >= 7:
    raise newException(ValueError, "Number too large: " & $n)
  return n * 2

proc main() {.async.} =
  try:
    let result = await riskyOperation(5)
    log("Result: " & $result)
  except ValueError as e:
    log("Error: " & e.msg.cstring)
  
  # 使用 catch 方法
  var reason: Error
  await riskyOperation(10).catch((r: Error) => (reason = r))
  if reason != nil:
    log("Caught error: " & reason.message.cstring)

discard main()

七、then 链式调用

Nimimport std/asyncjs
from std/sugar import `=>`

proc fn(n: int): Future[int] {.async.} =
  if n >= 7: raise newException(ValueError, "foobar: " & $n)
  else: result = n * 2

proc asyncFact(n: int): Future[int] {.async.} =
  if n > 0: result = n * await asyncFact(n-1)
  else: result = 1

proc main() {.async.} =
  assert asyncFact(3).await == 6
  assert asyncFact(3).then(asyncFact).await == 720
  let x = await fn(3)
  assert x == 6

discard main()

八、并行异步

Nimimport std/asyncjs
import std/jsffi

proc asyncTask(id: int, delay: int): Future[int] {.async.} =
  proc setTimeout(cb: proc(), ms: int): int {.importjs: "setTimeout(#, #)".}
  let promise = newPromise() do (resolve: proc(val: int)):
    proc cb() = resolve(id * 100)
    discard setTimeout(cb, delay)
  return await promise

proc main() {.async.} =
  # 并行执行多个异步任务
  let f1 = asyncTask(1, 1000)
  let f2 = asyncTask(2, 2000)
  let f3 = asyncTask(3, 1500)
  
  let r1 = await f1
  let r2 = await f2
  let r3 = await f3
  
  echo "Results: ", r1, ", ", r2, ", ", r3

discard main()
技巧

上面的三个任务会同时开始计时,总耗时约为最慢任务的 2000ms,而非三者之和的 4500ms。这就是并行执行的关键:先创建 Future,后 await。

九、asyncjs 要点总结

特性 / API说明
{.async.}将 proc 标记为异步过程,返回值自动包装为 Future[T]
await暂停当前异步过程,等待 Future 完成并获取结果
Future[T]表示异步计算结果的类型,对应 JavaScript 的 Promise
newPromise将基于回调的 API 包装为 Future / Promise
.then(fn)对 Future 结果进行链式处理,返回新的 Future
.catch(fn)捕获 Future 的拒绝状态,处理异步错误
并行执行先创建所有 Future 再分别 await,实现任务并行
fetch浏览器原生 AJAX API,返回 Promise,可直接 await

事件处理与 Canvas 绘图

学习在 Nim 中监听各类 DOM 事件,并使用 Canvas API 进行图形绘制。

一、事件监听基础

在 Nim 中为元素绑定事件,需要通过 addEventListener 注册回调。事件回调必须使用 closure 调用约定,最简单的做法是用带类型标注的匿名 proc

Nimimport std/dom
import std/jsffi

proc log(msg: cstring) {.importjs: "console.log(#)".}

# 事件回调需要使用 closure 调用约定
proc setupClickEvent() =
  let btn = document.getElementById("myButton")
  if btn != nil:
    let onClick: proc(e: Event) = proc(e: Event) =
      log("Button clicked!")
    btn.addEventListener("click", onClick)
注意

回调 proc 必须显式声明参数类型 proc(e: Event),否则 Nim 无法推断 closure 调用约定。

二、鼠标事件

Nimimport std/dom
import std/jsffi

proc log(msg: cstring) {.importjs: "console.log(#)".}

proc setupMouseEvents() =
  let el = document.getElementById("target")
  if el != nil:
    let onMouseMove: proc(e: Event) = proc(e: Event) =
      let me = e.MouseEvent
      log("Mouse at: " & $me.clientX & ", " & $me.clientY)
    el.addEventListener("mousemove", onMouseMove)
    
    let onClick: proc(e: Event) = proc(e: Event) =
      let me = e.MouseEvent
      log("Clicked! Button: " & $me.button)
    el.addEventListener("click", onClick)

三、键盘事件

Nimimport std/dom
import std/jsffi

proc log(msg: cstring) {.importjs: "console.log(#)".}

proc setupKeyboardEvents() =
  let onKeyDown: proc(e: Event) = proc(e: Event) =
    let ke = e.KeyboardEvent
    log("Key pressed: " & ke.key)
    log("Code: " & ke.code)
    log("Ctrl: " & $ke.ctrlKey & ", Shift: " & $ke.shiftKey)
  document.addEventListener("keydown", onKeyDown)
  
  let onKeyUp: proc(e: Event) = proc(e: Event) =
    let ke = e.KeyboardEvent
    log("Key released: " & ke.key)
  document.addEventListener("keyup", onKeyUp)

四、表单事件

Nimimport std/dom
import std/jsffi

proc log(msg: cstring) {.importjs: "console.log(#)".}

proc setupFormEvents() =
  let input = document.getElementById("myInput")
  if input != nil:
    let onInput: proc(e: Event) = proc(e: Event) =
      let target = e.target
      if target != nil:
        log("Input value: " & target.value)
    input.addEventListener("input", onInput)
    
    let onFocus: proc(e: Event) = proc(e: Event) =
      log("Input focused")
    input.addEventListener("focus", onFocus)
    
    let onBlur: proc(e: Event) = proc(e: Event) =
      log("Input blurred")
    input.addEventListener("blur", onBlur)

五、事件委托

Nimimport std/dom
import std/jsffi

proc log(msg: cstring) {.importjs: "console.log(#)".}

proc setupEventDelegation() =
  let container = document.getElementById("list-container")
  if container != nil:
    let onContainerClick: proc(e: Event) = proc(e: Event) =
      let target = e.target
      if target != nil:
        let el = target.Element  # 转换为 Element 类型
        if el.classList.contains("item"):
          log("Clicked item: " & el.textContent)
    container.addEventListener("click", onContainerClick)
技巧

e.target 返回的是 Node 类型,访问 classListtextContent 等 Element 属性前,需要用 target.Element 进行类型转换。

六、窗口事件

Nimimport std/dom
import std/jsffi

proc log(msg: cstring) {.importjs: "console.log(#)".}

proc setupWindowEvents() =
  let onResize: proc(e: Event) = proc(e: Event) =
    log("Window resized: " & $window.innerWidth & "x" & $window.innerHeight)
  window.addEventListener("resize", onResize)
  
  let onScroll: proc(e: Event) = proc(e: Event) =
    log("Scrolled: " & $window.scrollY)
  window.addEventListener("scroll", onScroll)

七、拖拽事件

Nimimport std/dom
import std/jsffi

proc log(msg: cstring) {.importjs: "console.log(#)".}

proc setupDragEvents() =
  let dragEl = document.getElementById("draggable")
  if dragEl != nil:
    let onDragStart: proc(e: Event) = proc(e: Event) =
      log("Drag started")
    dragEl.addEventListener("dragstart", onDragStart)
    
    let onDragEnd: proc(e: Event) = proc(e: Event) =
      log("Drag ended")
    dragEl.addEventListener("dragend", onDragEnd)

八、Canvas 绘图基础

Nimimport std/dom
import std/jsffi

# 获取 canvas 2D 上下文
proc getCtx(canvas: Element): JsObject {.importjs: "#.getContext('2d')".}

# Canvas 方法
proc fillRect(ctx: JsObject, x, y, w, h: float) {.importjs: "#.fillRect(#, #, #, #)".}
proc strokeRect(ctx: JsObject, x, y, w, h: float) {.importjs: "#.strokeRect(#, #, #, #)".}
proc clearRect(ctx: JsObject, x, y, w, h: float) {.importjs: "#.clearRect(#, #, #, #)".}
proc beginPath(ctx: JsObject) {.importjs: "#.beginPath()".}
proc moveTo(ctx: JsObject, x, y: float) {.importjs: "#.moveTo(#, #)".}
proc lineTo(ctx: JsObject, x, y: float) {.importjs: "#.lineTo(#, #)".}
proc closePath(ctx: JsObject) {.importjs: "#.closePath()".}
proc stroke(ctx: JsObject) {.importjs: "#.stroke()".}
proc fill(ctx: JsObject) {.importjs: "#.fill()".}
proc arc(ctx: JsObject, x, y, r, start, endAngle: float) {.importjs: "#.arc(#, #, #, #, #)".}
proc fillText(ctx: JsObject, text: cstring, x, y: float) {.importjs: "#.fillText(#, #, #)".}
proc strokeText(ctx: JsObject, text: cstring, x, y: float) {.importjs: "#.strokeText(#, #, #)".}

# 设置属性
proc setFillStyle(ctx: JsObject, color: cstring) {.importjs: "#.fillStyle = #".}
proc setStrokeStyle(ctx: JsObject, color: cstring) {.importjs: "#.strokeStyle = #".}
proc setLineWidth(ctx: JsObject, width: float) {.importjs: "#.lineWidth = #".}
proc setFont(ctx: JsObject, font: cstring) {.importjs: "#.font = #".}

九、绘制矩形

Nimproc drawRect() =
  let canvas = document.getElementById("myCanvas")
  if canvas != nil:
    let ctx = getCtx(canvas)
    setFillStyle(ctx, "red")
    fillRect(ctx, 10, 10, 100, 50)
    setStrokeStyle(ctx, "blue")
    setLineWidth(ctx, 3)
    strokeRect(ctx, 150, 10, 100, 50)
    clearRect(ctx, 20, 20, 30, 30)

十、绘制路径和三角形

Nimproc drawPath() =
  let canvas = document.getElementById("myCanvas")
  if canvas != nil:
    let ctx = getCtx(canvas)
    beginPath(ctx)
    moveTo(ctx, 50, 50)
    lineTo(ctx, 100, 100)
    lineTo(ctx, 150, 50)
    closePath(ctx)
    setStrokeStyle(ctx, "green")
    setLineWidth(ctx, 2)
    stroke(ctx)

十一、绘制圆形

Nimproc drawCircle() =
  let canvas = document.getElementById("myCanvas")
  if canvas != nil:
    let ctx = getCtx(canvas)
    beginPath(ctx)
    arc(ctx, 200, 200, 50, 0, 3.14159 * 2)
    setFillStyle(ctx, "yellow")
    fill(ctx)
    setStrokeStyle(ctx, "orange")
    setLineWidth(ctx, 3)
    stroke(ctx)

十二、绘制文本

Nimproc drawText() =
  let canvas = document.getElementById("myCanvas")
  if canvas != nil:
    let ctx = getCtx(canvas)
    setFont(ctx, "20px Arial")
    setFillStyle(ctx, "black")
    fillText(ctx, "Hello Canvas!", 50, 250)
    setStrokeStyle(ctx, "purple")
    strokeText(ctx, "Nim Canvas", 50, 280)

十三、事件类型参考

事件名称触发时机
click元素被点击
mousedown / mouseup鼠标按下 / 释放
mousemove鼠标在元素上移动
keydown / keyup键盘按下 / 释放
input / change输入框内容变化 / 值改变
focus / blur元素获得 / 失去焦点
submit表单提交
resize / scroll窗口大小改变 / 页面滚动
dragstart / dragend拖拽开始 / 结束
load资源加载完成

MouseEvent 主要字段

字段类型说明
clientX / clientYint相对于视口的坐标
screenX / screenYint相对于屏幕的坐标
buttonint按下的鼠标按钮(0=左键,1=中键,2=右键)
shiftKey / ctrlKey / altKeybool修饰键是否按下

KeyboardEvent 主要字段

字段类型说明
keycstring按下的键的值(如 "a"、"Enter")
codecstring键的物理代码(如 "KeyA"、"Enter")
keyCodeint键码(已废弃,建议用 key)
shiftKey / ctrlKey / altKeybool修饰键是否按下
repeatbool按键是否处于重复触发状态

十四、事件处理要点

核心要点

1. closure 调用约定:事件回调必须使用 closure 调用约定,通过显式声明参数类型 proc(e: Event) 实现。

2. target 类型转换e.target 返回 Node,访问 Element 属性需要转换:let el = target.Element

3. 事件子类型转换:访问 MouseEvent / KeyboardEvent 字段需从 Event 转换:let me = e.MouseEvent

4. 事件委托:在父容器统一监听,减少监听器数量,对动态元素尤其有效。

实战项目

通过三个完整项目,综合运用 DOM 操作、事件处理与异步 AJAX,掌握 Nim 前端开发的完整流程。

项目一:计数器应用

这是一个最基础的交互应用。通过按钮控制计数器的增减与重置,并根据数值正负切换样式。

Nim 代码

Nimimport std/dom

# 全局状态
var count = 0

# 更新显示
proc updateDisplay() =
  let display = document.getElementById("counter-display")
  if display != nil:
    display.textContent = $count
    
  let counter = document.getElementById("counter")
  if counter != nil:
    if count > 0:
      counter.classList.add("positive")
      counter.classList.remove("negative")
    elif count < 0:
      counter.classList.add("negative")
      counter.classList.remove("positive")
    else:
      counter.classList.remove("positive")
      counter.classList.remove("negative")

proc increment(e: Event) =
  inc(count)
  updateDisplay()

proc decrement(e: Event) =
  dec(count)
  updateDisplay()

proc reset(e: Event) =
  count = 0
  updateDisplay()

proc initCounter() =
  let incBtn = document.getElementById("increment-btn")
  let decBtn = document.getElementById("decrement-btn")
  let resetBtn = document.getElementById("reset-btn")
  
  if incBtn != nil: incBtn.addEventListener("click", increment)
  if decBtn != nil: decBtn.addEventListener("click", decrement)
  if resetBtn != nil: resetBtn.addEventListener("click", reset)
  
  updateDisplay()

proc onReady(e: Event) = initCounter()
document.addEventListener("DOMContentLoaded", onReady)

配套 HTML

HTML<!DOCTYPE html>
<html>
<head>
  <title>Nim Counter</title>
  <style>
    body { font-family: sans-serif; display: flex; justify-content: center; padding: 50px; }
    #counter { font-size: 48px; margin: 20px; }
    .positive { color: green; }
    .negative { color: red; }
    button { font-size: 18px; padding: 10px 20px; margin: 5px; cursor: pointer; }
  </style>
</head>
<body>
  <div>
    <h1>计数器</h1>
    <div id="counter"><span id="counter-display">0</span></div>
    <button id="increment-btn">+ 增加</button>
    <button id="decrement-btn">- 减少</button>
    <button id="reset-btn">重置</button>
  </div>
  <script src="counter_app.js"></script>
</body>
</html>

编译命令

Shellnim js -o:counter_app.js counter_app.nim

项目二:Todo List 应用

Todo List 是经典的 CRUD 应用。本项目演示了动态创建 DOM 元素、事件委托、列表渲染等核心技巧。

Nim 代码

Nimimport std/dom
import std/jsffi
import std/strutils

proc log(msg: cstring) {.importjs: "console.log(#)".}

type
  TodoItem = object
    id: int
    text: string
    done: bool

var
  todos: seq[TodoItem] = @[]
  nextId = 1

proc renderTodos() =
  let list = document.getElementById("todo-list")
  if list == nil: return
  list.innerHTML = ""
  
  for todo in todos:
    let li = document.createElement("li")
    li.setAttribute("data-id", $todo.id)
    let cls = if todo.done: "todo-item done" else: "todo-item"
    li.setAttribute("class", cls.cstring)
    
    let checkbox = document.createElement("input").InputElement
    checkbox.`type` = "checkbox"
    checkbox.checked = todo.done
    checkbox.setAttribute("class", "todo-checkbox")
    
    let span = document.createElement("span")
    span.textContent = todo.text.cstring
    span.setAttribute("class", "todo-text")
    
    let deleteBtn = document.createElement("button")
    deleteBtn.textContent = "Delete"
    deleteBtn.setAttribute("class", "todo-delete")
    
    li.appendChild(checkbox)
    li.appendChild(span)
    li.appendChild(deleteBtn)
    list.appendChild(li)

proc addTodo(text: string) =
  if text.len == 0: return
  todos.add(TodoItem(id: nextId, text: text, done: false))
  inc(nextId)
  renderTodos()

proc toggleTodo(id: int) =
  for i in 0..<todos.len:
    if todos[i].id == id:
      todos[i].done = not todos[i].done
      break
  renderTodos()

proc deleteTodo(id: int) =
  var newTodos: seq[TodoItem] = @[]
  for todo in todos:
    if todo.id != id: newTodos.add(todo)
  todos = newTodos
  renderTodos()

proc setupTodoApp() =
  let input = document.getElementById("todo-input").InputElement
  let addBtn = document.getElementById("todo-add")
  let list = document.getElementById("todo-list")
  if input == nil or addBtn == nil or list == nil: return
  
  let onAdd: proc(e: Event) = proc(e: Event) =
    addTodo($input.value)
    input.value = ""
  addBtn.addEventListener("click", onAdd)
  
  let onKeyPress: proc(e: Event) = proc(e: Event) =
    let ke = e.KeyboardEvent
    if ke.key == "Enter":
      addTodo($input.value)
      input.value = ""
  input.addEventListener("keypress", onKeyPress)
  
  let onListClick: proc(e: Event) = proc(e: Event) =
    let target = e.target
    if target == nil: return
    let el = target.Element
    
    if el.nodeName == "INPUT" and el.InputElement.`type` == "checkbox":
      let li = el.parentNode
      let idStr = li.getAttribute("data-id")
      if idStr.len > 0:
        toggleTodo(parseInt($idStr))
    
    elif el.nodeName == "BUTTON" and el.classList.contains("todo-delete"):
      let li = el.parentNode
      let idStr = li.getAttribute("data-id")
      if idStr.len > 0:
        deleteTodo(parseInt($idStr))
  
  list.addEventListener("click", onListClick)
  renderTodos()
  log("Todo app initialized!")

proc onReady(e: Event) = setupTodoApp()
document.addEventListener("DOMContentLoaded", onReady)

配套 HTML

HTML<!DOCTYPE html>
<html>
<head>
  <title>Nim Todo List</title>
  <style>
    body { font-family: sans-serif; max-width: 500px; margin: 50px auto; }
    #todo-input { padding: 8px; width: 300px; }
    #todo-add { padding: 8px 16px; }
    #todo-list { list-style: none; padding: 0; }
    .todo-item { display: flex; align-items: center; padding: 8px; border-bottom: 1px solid #eee; }
    .todo-item.done .todo-text { text-decoration: line-through; color: #999; }
    .todo-text { flex: 1; margin: 0 10px; }
    .todo-delete { background: #e74c3c; color: white; border: none; padding: 4px 8px; cursor: pointer; }
  </style>
</head>
<body>
  <h1>Todo List</h1>
  <div>
    <input type="text" id="todo-input" placeholder="输入待办事项...">
    <button id="todo-add">添加</button>
  </div>
  <ul id="todo-list"></ul>
  <script src="todo_app.js"></script>
</body>
</html>

编译命令

Shellnim js -o:todo_app.js todo_app.nim
说明

本项目使用事件委托处理列表项的点击:在 <ul> 上统一监听 click,通过 e.target 判断点击的是复选框还是删除按钮。

项目三:API 数据展示应用

本项目结合异步编程与 AJAX,通过 fetch 请求远程 API 数据,并将结果动态渲染到页面上。

Nimimport std/dom
import std/asyncjs
import std/jsffi

proc fetch(url: cstring): Future[JsObject] {.importjs: "fetch(#)".}
proc json(response: JsObject): Future[JsObject] {.importjs: "#.json()".}
proc log(msg: cstring) {.importjs: "console.log(#)".}

proc displayUser(user: JsObject) =
  let container = document.getElementById("user-info")
  if container == nil: return
  container.innerHTML = ""
  
  let name = user["name"].to(cstring)
  let email = user["email"].to(cstring)
  let phone = user["phone"].to(cstring)
  
  let nameEl = document.createElement("h2")
  nameEl.textContent = name
  container.appendChild(nameEl)
  
  let emailEl = document.createElement("p")
  emailEl.textContent = "Email: " & email
  container.appendChild(emailEl)
  
  let phoneEl = document.createElement("p")
  phoneEl.textContent = "Phone: " & phone
  container.appendChild(phoneEl)

proc loadUser(userId: int) {.async.} =
  log("Loading user...")
  try:
    let response = await fetch(("https://jsonplaceholder.typicode.com/users/" & $userId).cstring)
    let user = await json(response)
    displayUser(user)
    log("User loaded!")
  except:
    log("Failed to load user!")

proc setupApp() =
  let btn = document.getElementById("load-btn")
  if btn != nil:
    let onLoad: proc(e: Event) = proc(e: Event) =
      let input = document.getElementById("user-id").InputElement
      if input != nil:
        let id = parseInt($input.value)
        discard loadUser(id)
    btn.addEventListener("click", onLoad)

proc onReady(e: Event) =
  setupApp()
  log("App ready!")

document.addEventListener("DOMContentLoaded", onReady)
注意

由于 loadUser 是异步过程,事件回调中调用它时需要用 discard loadUser(id) 丢弃返回的 Future。同时务必用 try/except 包裹网络请求。

项目结构最佳实践

Textmy-nim-app/
├── src/
│   ├── counter_app.nim
│   ├── todo_app.nim
│   └── api_app.nim
├── public/
│   ├── index.html
│   ├── counter.html
│   └── todo.html
├── nim.cfg
└── README.md
nim.cfg# 编译输出目录与文件名
--out:app.js
# 启用 release 优化
-d:release
# 优化生成代码体积
--opt:size
# 使用 ORC 内存管理
--gc:orc

部署到生产环境

优化建议

1. 使用 -d:release:启用编译器优化,生成更小更快的代码。

2. 文件压缩:用 terseresbuild 进一步压缩生成的 JS 文件。

3. CDN 部署:将静态资源上传至 CDN(如 Vercel、Netlify、Cloudflare Pages)。

4. 工具集成:与 Vite、Webpack 等前端工具集成,实现热更新与资源打包。

Shell# 1. 编译优化版本
nim js -d:release --opt:size -o:dist/app.js src/app.nim

# 2. 压缩 JS(需安装 terser)
npx terser dist/app.js -o dist/app.min.js --compress --mangle

# 3. 复制 HTML 到发布目录
cp public/index.html dist/
部署流程

完整的部署流程为:编写 Nim 代码 → nim js 编译生成 JS → 压缩混淆 → 连同 HTML 一起部署到静态服务器或 CDN。

— 本教程结束 —
Nim 2.2.10 · 所有代码均经过编译测试 · 2026