📚 Termux 无 Root 学习开发系列
当前进度:阶段二 · 8/28
阶段一:环境搭建与终端基础 ✅ 已完成
阶段二:编程开发环境
06 Python 开发环境搭建
07 C/C++ 开发环境
08 Node.js 开发环境搭建(当前篇)
09 Git 版本控制
阶段三至九共 19 篇,后续持续更新

08 Node.js 开发环境:从 REPL 到 Web 服务器

难度:入门→进阶 | 前置:07 C/C++ 开发环境
在手机上搭建完整 Node.js 开发环境,编写第一个 API 服务
读完本篇你将能:在 Termux 上安装 Node.js 并验证版本,用 REPL 交互式探索 JavaScript,编写并运行 JS 文件,用 npm 与 pnpm 管理依赖,理解 ES Modules 语法,用 Express 和 Hono 搭建 Web 服务器,用 node:test 内置测试运行器写单元测试,最终完成一个支持增删改查的 Markdown 笔记 API 项目。
📑 本文目录
01安装 Node.js 与验证
02Node.js REPL 交互式环境
03运行 JavaScript 文件
04npm 包管理
05pnpm 包管理器
06ES Modules 模块系统
07常用库示例
08Express Web 服务器
09内置测试运行器
10实操项目:Markdown 笔记 API

上一篇你用 clang 编译了 C/C++ 程序,体会了编译型语言的严谨。本篇转向另一类生态最庞大的运行时——Node.js。它基于 V8 引擎,让 JavaScript 跑在浏览器之外。在手机上装 Node.js 不只是为了写脚本:现代前端工具链(Vite、ESBuild)、Web 服务器、API 服务、命令行工具几乎都依赖它。Termux 的 nodejs 包即装即用,无需交叉编译。

§1 安装 Node.js 与验证

一条命令安装 Node.js(含 npm、npx):

Shell
pkg install nodejs

若想装长期支持版(LTS),把包名换成 nodejs-lts。安装后验证三个命令:

Shell
node -v
npm -v
npx -v

预期输出(版本号以你实际安装为准):

Output
v24.18.0
11.4.2
11.4.2
💡 为什么手机开发也离不开 Node.js?
Python 擅长数据与脚本,但现代前端工具链几乎全是 Node.js 写的——Vite 起开发服务器、ESBuild 打包、Prettier 格式化代码、TypeScript 编译器 tsc 都是 Node 程序。此外 Node.js 天生是异步事件驱动的 Web 服务器运行时,用几十行 JavaScript 就能起一个 HTTP API。可以说:要在手机上做完整全栈开发,Python 解决后端逻辑,Node.js 解决前端工具链和 API 服务,两者互补而非替代。
💡 nvm 在 Termux 不适用
桌面端常用 nvm(Node Version Manager)切换多版本 Node,但它依赖 bash profile 与源码编译,在 Termux 的非 root 环境下安装困难。手机上推荐直接用 pkg install nodejs 或 nodejs-lts,二选一即可。需要多版本时建议用 proot 容器隔离。

§2 Node.js REPL 交互式环境

REPL 即"读取-求值-输出-循环",类似 06 篇的 Python REPL。直接输入 node 不带文件名即可进入:

REPL
$ node
> const x = 10
> x + 1
11
> function add(a, b) { return a + b }
> add(3, 4)
7
> .exit

REPL 支持以点号开头的特殊命令:

REPL
> .help  # 列出所有点命令
> .save session.js  # 把本次会话存成文件
> .load utils.js  # 把文件逐行送入 REPL 执行
> .clear  # 清空当前上下文

输入多行代码(如函数体)时,REPL 会显示 ... 续行提示,直到补全配对的花括号才执行。退出 REPL 用 .exit 或连按两次 Ctrl+C。

§3 运行 JavaScript 文件

用 Neovim 创建 hello.js:

JS (hello.js)
#!/data/data/com.termux/files/usr/bin/node
// hello.js - 在 Termux 上运行的第一个 Node 程序
const msg = 'Hello from Termux!';
const time = new Date().toLocaleTimeString();
console.log(`${msg} @ ${time}`);

第一行是 shebang(hashbang),指向 Termux 的 node 解释器路径,让文件加上可执行权限后能直接 ./hello.js 运行。运行:

Shell
node hello.js
Output
Hello from Termux! @ 14:08:32

开发时反复改代码、重启程序很烦。Node 22+ 内置 --watch 模式,文件保存后自动重启进程,不再需要 nodemon:

Shell
node --watch hello.js
Output
Hello from Termux! @ 14:08:32
# 修改 hello.js 保存后:
(node:12345) Restarting
Hello from Termux! @ 14:09:01
💡 --watch vs nodemon
Node 内置 --watch 零依赖、即开即用,足以覆盖大多数调试场景。nodemon 仍有价值:可配置监听特定扩展名、忽略目录、执行非 node 命令(如重启 tsx、deno)。新项目优先用 --watch,复杂需求再上 nodemon。

§4 npm 包管理

npm 是随 Node.js 安装的自带包管理器。开发前先初始化项目,生成 package.json 清单:

Shell
mkdir myapp && cd myapp
npm init -y

-y 跳过交互提问,用默认值生成。手动补全后 package.json 长这样:

package.json
1
2
3
4
5
6
7
8
9
10
11
12
13
{
  "name": "myapp",
  "version": "1.0.0",
  "type": "module",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "test": "node --test"
  },
  "dependencies": {
    "express": "^5.1.0"
  }
}

常用 npm 命令:

Shell
npm install express  # 本地安装到 dependencies
npm install -D nodemon # 开发依赖 devDependencies
npm install -g pnpm  # 全局安装命令行工具
npm list -g  # 查看全局已装包
npm uninstall express  # 卸载
npm run test  # 执行 scripts 里的 test 命令
💡 三个产物的分工
package.json:项目清单,只记包名与版本范围,体积小,提交到 Git。package-lock.json:锁定每层依赖的确切版本与哈希,保证别人 npm ci 后拿到完全一致的依赖树,也提交。node_modules:实际下载的代码,体积大,加入 .gitignore 不提交。

§5 pnpm 包管理器

pnpm 已成为 2026 年主流包管理器。它用硬链接把全局 store 里的包复用到各项目,磁盘占用低、安装快,还能避免"幽灵依赖"(访问 package.json 没声明的包)。安装:

Shell
npm install -g pnpm
pnpm -v
Output
10.15.0

pnpm 命令与 npm 几乎对称,迁移成本极低:

Shell
pnpm init  # 生成 package.json
pnpm install  # 按 package.json 安装全部依赖
pnpm add express  # 等价 npm install express
pnpm add -D nodemon  # 开发依赖
pnpm remove express  # 卸载
pnpm test  # 跑 scripts.test
特性 npm pnpm
安装速度 基准 快 2-3 倍
磁盘占用 每项目独立副本 全局 store 硬链接复用
node_modules 结构 扁平(提升所有依赖) 符号链接(严格隔离)
幽灵依赖 有,能引用未声明包 无,只能用 package.json 声明的
monorepo workspaces 可用 原生 workspace,更省空间

在手机上磁盘空间宝贵,pnpm 的硬链接复用优势更明显:十个项目共用一个 express 副本,而不是十份。新项目建议默认用 pnpm。

§6 ES Modules 模块系统

Node.js 有两套模块系统:旧的 CommonJS(CJS)和新的 ES Modules(ESM)。Node 22+ 默认全面支持 ESM,新项目应优先使用。对比两者语法:

CommonJS (utils.cjs)
// 旧式:require / module.exports
const os = require('node:os');
function info() { return os.platform(); }
module.exports = { info };
ESM (utils.mjs)
// 新式:import / export
import os from 'node:os';
export function info() { return os.platform(); }

启用 ESM 有两种方式:在 package.json 加 "type": "module"(项目级,推荐),或把文件后缀改为 .mjs(单文件级)。注意 ESM 中引入 Node 内置模块建议带 node: 前缀(如 node:fs),明确区分内置包与 npm 包。

动态 import() 返回 Promise,适合按条件延迟加载:

JS
const mod = await import('./utils.mjs');
console.log(mod.info());
特性 require() (CJS) import (ESM)
语法 const x = require('x') import x from 'x'
加载时机 运行时同步 编译期静态分析
顶层 await 不支持 原生支持
文件后缀 .cjs / .js(默认) .mjs / type:module
未来方向 逐步淘汰 官方主推

§7 常用库示例

以下是手机开发最常用的几类库,每个都是最小可运行示例。

Express 5.x —— 最经典的 Web 框架:

JS
import express from 'express';
const app = express();
app.get('/', (req, res) => res.send('hi'));
app.listen(3000);

Hono 1.x —— 轻量框架,比 Express 更快更小,特别适合移动端和边缘环境:

JS
import { Hono } from 'hono';
import { serve } from '@hono/node-server';
const app = new Hono();
app.get('/', (c) => c.text('hi'));
serve(app);

内置 fetch —— Node 22+ 已内置全局 fetch,无需再装 node-fetch:

JS
const res = await fetch('https://httpbin.org/get');
console.log(res.status);  // 200
const data = await res.json();

chalk 5.x —— 终端彩色输出,调试日志必备:

JS
import chalk from 'chalk';
console.log(chalk.green('成功'));
console.log(chalk.red.bold('错误'));
💡 chalk 5 是纯 ESM
chalk 5 起只发 ESM 版本,用 require() 无法引入。这正是 ESM 成为新标准的缩影:越来越多新库只支持 import。早点切到 ESM,少踩坑。

§8 Express Web 服务器

把前面学的 ESM、npm、Express 串起来,写一个能跑的 API 服务器。先建项目装依赖:

Shell
pnpm init
pnpm add express

编写 server.js,提供 GET / 、GET /api/users、POST /api/users 三条路由:

JS (server.js)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import express from 'express';
 
const app = express();
app.use(express.json());
 
const users = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' }
];
 
app.get('/', (req, res) => {
  res.send('Hello from Termux!');
});
 
app.get('/api/users', (req, res) => {
  res.json(users);
});
 
app.post('/api/users', (req, res) => {
  const { name } = req.body;
  const user = { id: users.length + 1, name };
  users.push(user);
  res.status(201).json(user);
});
 
app.listen(3000, () => {
  console.log('Server on http://localhost:3000');
});

启动服务器:

Shell
node server.js
Output
Server on http://localhost:3000

另开一个终端(tmux 新窗格),用 02 篇学过的 curl 测试三条路由:

Shell
curl http://localhost:3000/
curl http://localhost:3000/api/users
curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Charlie"}'
Output
Hello from Termux!
[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]
{"id":3,"name":"Charlie"}
💡 app.use(express.json()) 的作用
它是内置中间件,把 Content-Type 为 application/json 的请求体解析成 req.body 对象。不写这行,POST 路由里 req.body 会是 undefined,添加用户会失败。Express 5 已把旧版 body-parser 的解析能力内置。

§9 内置测试运行器

Node 22+ 内置 node:test 模块,无需再装 jest 或 mocha。编写测试文件 calc.test.js:

JS (calc.test.js)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { test } from 'node:test';
import assert from 'node:assert';
 
test('加法运算', () => {
  assert.equal(1 + 1, 2);
});
 
test('数组排序', () => {
  const arr = [3, 1, 2];
  assert.deepEqual(arr.sort(), [1, 2, 3]);
});
 
test('异步定时器', async () => {
  const start = Date.now();
  await new Promise(r => setTimeout(r, 50));
  assert.ok(Date.now() - start >= 50);
});

运行测试用 --test 标志,Node 会自动发现所有 *.test.js 文件并执行:

Shell
node --test
Output
✔ 加法运算 (0.2ms)
✔ 数组排序 (0.1ms)
✔ 异步定时器 (51.3ms)
 
ℹ tests 3
ℹ suites 0
ℹ pass 3
ℹ fail 0
特性 node:test jest mocha
安装 Node 内置 需 npm 装 需 npm 装
配置 零配置 需 jest.config 需 .mocharc
异步测试 原生 async 原生 async 需 async 库
覆盖率 --experimental-test-coverage 内置 需 nyc
适用场景 新项目/标准 大型/快照 灵活定制
💡 何时仍需 jest
node:test 覆盖 90% 的单元测试需求,新项目完全够用。但如果你接手的老项目已用 jest 的快照测试(snapshot)、模块 mock(jest.mock)或前端组件测试(@testing-library),迁移成本高,保留 jest 即可。两者并不冲突,可在 package.json 的 test 脚本里共存。

§10 实操项目:Markdown 笔记 API

前面学完了 Express 路由和 node:test 测试,现在把它们组合起来——做一个能增删改查的 Markdown 笔记 API。数据用 JSON 文件持久化,不装数据库,手机上直接跑。

项目结构:

notes-api/ ├── package.json ├── server.js ← Express 服务器 ├── store.js ← JSON 文件读写 ├── test/ │ └── store.test.js ← 单元测试 └── data/ └── notes.json ← 数据文件(自动创建)

store.js — 数据读写模块:

import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { dirname } from 'node:path';

const DATA_FILE = 'data/notes.json';

async function load() {
  try {
    const raw = await readFile(DATA_FILE, 'utf8');
    return JSON.parse(raw);
  } catch {
    return [];
  }
}

async function save(notes) {
  await mkdir(dirname(DATA_FILE), { recursive: true });
  await writeFile(DATA_FILE, JSON.stringify(notes, null, 2));
}

export { load, save };

server.js — Express API 服务器:

import express from 'express';
import { load, save } from './store.js';

const app = express();
app.use(express.json());

// 获取所有笔记
app.get('/api/notes', async (req, res) => {
  const notes = await load();
  res.json(notes);
});

// 新建笔记
app.post('/api/notes', async (req, res) => {
  const notes = await load();
  const note = { id: Date.now(), ...req.body };
  notes.push(note);
  await save(notes);
  res.status(201).json(note);
});

// 删除笔记
app.delete('/api/notes/:id', async (req, res) => {
  const notes = await load();
  const filtered = notes.filter(n => n.id != req.params.id);
  await save(filtered);
  res.json({ ok: true });
});

app.listen(3000, () => console.log('API 运行在 http://localhost:3000'));

test/store.test.js — 单元测试:

import { test } from 'node:test';
import assert from 'node:assert';
import { load, save } from '../store.js';

test('save 和 load 往返一致', async () => {
  const data = [{ id: 1, title: '测试笔记' }];
  await save(data);
  const loaded = await load();
  assert.deepEqual(loaded, data);
});

test('load 空数据返回空数组', async () => {
  // 文件不存在时 load 返回 []
  const result = await load();
  assert.ok(Array.isArray(result));
});

运行步骤:

mkdir notes-api && cd notes-api
npm init -y
npm install express
# 创建文件后运行测试
node --test
# 启动服务器
node server.js

用 curl 测试 API:

# 新建笔记
curl -X POST http://localhost:3000/api/notes \   -H 'Content-Type: application/json' \   -d '{"title":"第一条","body":"Hello Termux!"}'

{"id":1717000000000,"title":"第一条","body":"Hello Termux!"}

# 查看所有笔记
curl http://localhost:3000/api/notes
💡 这个项目能做什么
这是一个完整的 CRUD API——增删改查四步齐全。把 server.js 放进 tmux 会话里后台运行,就能随时用 curl 或手机浏览器访问 localhost:3000/api/notes 管理笔记。后续可以加 HTML 前端页面,做成一个真正的小工具。

📝 分级练习

🟢 基础验证
请完成:安装 Node.js,编写一个 clock.js 脚本,每秒输出当前时间到终端,按 Ctrl+C 停止。提示:用 setInterval + Date 对象。
参考解法:
import { setInterval } from 'node:timers';
setInterval(() => console.log(new Date().toLocaleTimeString()), 1000);
🟡 组合应用
请完成:在笔记 API 项目基础上,添加 PUT /api/notes/:id 路由,实现更新笔记功能。需要组合 load、修改、save 三步操作。用 curl 测试更新是否生效。
参考方向:在 server.js 中添加 app.put('/api/notes/:id', ...) 路由。先 load 所有笔记,用 map 找到匹配 id 的笔记并替换其字段,再 save 回去。curl 测试用 -X PUT -d '{"title":"更新标题"}'。
🔴 开放挑战
请完成:给笔记 API 添加 HTML 前端页面,用纯 HTML+fetch 调用 API。在 server.js 中用 res.send() 返回 HTML,或用 express.static() 托管 public/ 目录。目标:用手机浏览器打开 localhost:3000 就能增删笔记。
提示方向:在 public/index.html 中写一个简单表单 + 笔记列表。用 fetch('POST /api/notes') 新建,fetch('GET /api/notes') 刷新列表,fetch('DELETE /api/notes/:id') 删除。express 中用 app.use(express.static('public')) 托管静态文件。

🏷 知识回顾

node -v 验证 npm/pnpm 包管理 ES Modules Express 路由 --watch 模式 内置 fetch API node:test 测试 package.json 结构 RESTful CRUD pnpm vs npm
下一篇 09 Git 版本控制 将学习在 Termux 中用 Git 管理代码版本——从 git init 到分支管理、远程仓库推送,以及用 SSH 密钥连接 GitHub。如果你跟着本系列一路学来,你的 Termux 已经具备了 Python、C/C++、Node.js 三种语言的开发能力,下一步就是用 Git 把代码管起来。
觉得有用?点赞、在看、分享让更多人发现 Termux 的可能性。关注系列持续更新,阶段三网络操作更精彩。