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
安装完成后,确保将 ~/.nimble/bin 添加到 PATH 环境变量中。
第一个程序 - Hello World
创建一个名为 hello.nim 的文件:
echo "Hello from Nim to JS!"
编译为 JavaScript
nim js -o:hello.js hello.nim
运行生成的 JavaScript
node hello.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
文件大小:约 27 KB(包含调试信息)
Release 模式
nim js -d:release -r -o:compile_release.js compile_options.nim
文件大小:约 19 KB(优化后更小更快)
Release 模式生成的文件比 Debug 模式小约 30%,且执行速度更快。生产环境部署时务必使用 -d:release。
在浏览器中使用
<html>
<body>
<script src="hello.js"></script>
</body>
</html>
Node.js 与浏览器目标
| 目标 | 命令 | 说明 |
|---|---|---|
| Node.js | nim 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
数据类型
| 类型 | 说明 | 示例 |
|---|---|---|
int | 整数类型 | let n = 42 |
float | 浮点数 | let f = 3.14 |
string | Nim 字符串 | let s = "hello" |
cstring | C 风格字符串,JS 后端对应 JS 字符串 | let c: cstring = "hi" |
bool | 布尔值 | let b = true |
seq[T] | 动态数组 | let arr = @[1, 2, 3] |
字符串操作
let name = "Nim"
echo "Hello, " & name & "!"
数组与序列
var arr = [1, 2, 3, 4, 5]
echo "Array: ", arr
var seq1 = @[10, 20, 30]
seq1.add(40)
echo "Seq: ", seq1
元组
let person = (name: "Alice", age: 30)
echo "Person: ", person.name, " is ", person.age, " years old"
控制流
if-elif-else
if x > 20:
echo "x is greater than 20"
else:
echo "x is 20 or less"
for 循环
for i in 1..5:
echo "Iteration: ", i
while 循环
var count = 0
while count < 3:
echo "Count: ", count
inc(count)
过程(proc)
proc add(a, b: int): int =
return a + b
echo "5 + 3 = ", add(5, 3)
字符串格式化
import std/strutils
let formatted = "Value: $1, Name: $2" % [$x, name]
echo formatted
数学运算
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)
字符串处理
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")
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)
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
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"
getElementById 和 querySelector 在找不到元素时返回 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 |
Element | HTML 元素节点 | id, className, textContent, innerHTML, style, classList, getAttribute |
Node | DOM 节点基类 | nodeName, firstChild, nextSibling, parentNode |
Event | 事件对象基类 | target, currentTarget, type, preventDefault |
MouseEvent | 鼠标事件 | clientX, clientY, button, shiftKey, ctrlKey |
KeyboardEvent | 键盘事件 | key, code, keyCode, altKey, ctrlKey, shiftKey |
InputElement | input 输入框元素 | value, type, checked, disabled, setCustomValidity |
17. 重要注意事项
dom 模块中很多属性返回 cstring 类型(如 textContent、getAttribute 的返回值)。cstring 不能直接用 & 运算符拼接,需要先用 $ 转换为 string。
事件对象的 e.target 返回 Node 类型。如果需要访问 Element 特有的属性(如 getAttribute、classList),需要进行类型转换,例如 e.target.Element。
localStorage 是 window 对象的属性,不是全局变量。必须通过 window.localStorage 访问,直接使用 localStorage 会导致编译错误。
JavaScript 互操作
Nim 编译为 JavaScript 后,可以通过 importc、importjs 和 exportc 等 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 & "!"
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)
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)
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)
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))
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()
三、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 类型,访问 classList、textContent 等 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 / clientY | int | 相对于视口的坐标 |
screenX / screenY | int | 相对于屏幕的坐标 |
button | int | 按下的鼠标按钮(0=左键,1=中键,2=右键) |
shiftKey / ctrlKey / altKey | bool | 修饰键是否按下 |
KeyboardEvent 主要字段
| 字段 | 类型 | 说明 |
|---|---|---|
key | cstring | 按下的键的值(如 "a"、"Enter") |
code | cstring | 键的物理代码(如 "KeyA"、"Enter") |
keyCode | int | 键码(已废弃,建议用 key) |
shiftKey / ctrlKey / altKey | bool | 修饰键是否按下 |
repeat | bool | 按键是否处于重复触发状态 |
十四、事件处理要点
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. 文件压缩:用 terser 或 esbuild 进一步压缩生成的 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