📚 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):
若想装长期支持版(LTS),把包名换成 nodejs-lts。安装后验证三个命令:
预期输出(版本号以你实际安装为准):
💡 为什么手机开发也离不开 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 运行。运行:
Output
Hello from Termux! @ 14:08:32
开发时反复改代码、重启程序很烦。Node 22+ 内置 --watch 模式,文件保存后自动重启进程,不再需要 nodemon:
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
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');
});
|
启动服务器:
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 文件并执行:
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 的可能性。关注系列持续更新,阶段三网络操作更精彩。