《JavaScript全栈教程》13:Node.js后端开发——从环境搭建到Web框架与数据库ORM全解
从本篇开始,我们正式踏入JavaScript的后端开发领域。本篇将带你全面认识Node.js的诞生背景、核心优势,手把手完成本地环境搭建与IDE配置,深入理解CommonJS与ESM模块机制,掌握fs、stream、http、crypto等核心内置模块,并基于Koa框架从零构建Web应用,集成Nunjucks模板引擎实现MVC架构,设计REST API,最终通过SQLite和Sequelize完成数据库操作。学完本篇,你将具备独立开发Node.js后端服务的能力。
本篇核心收获
- 理解Node.js的诞生历程、核心优势及其与浏览器端JavaScript的区别
- 掌握Node.js与npm的安装、版本管理及命令行/交互模式的使用
- 学会使用VS Code搭建Node开发环境,实现代码编辑、运行与断点调试
- 深入理解CommonJS模块规范与ESM模块的
require/export/import用法及实现原理 - 熟练运用Node内置模块:global、process、fs(文件读写/流/stat)、stream、http(创建服务器/文件服务)、crypto(哈希/HMAC/AES/DH/RSA)
- 基于Koa框架和
@koa/router构建Web应用,理解中间件(middleware)的执行机制 - 集成Nunjucks模板引擎,实现MVC架构,分离控制器、视图与模型
- 设计符合REST规范的API接口,处理JSON请求与响应
- 使用SQLite数据库和Sequelize ORM框架完成数据持久化操作
Node.js:从浏览器战场走出的后端革命
诞生奇缘:浏览器大战催生的高性能引擎
众所周知,Netscape设计出JavaScript后的短短几个月,它便成为前端开发的唯一标准。后来微软通过IE击败Netscape一统桌面,结果几年时间浏览器毫无进步——微软认为IE6已经非常完善,甚至解散了开发团队。
然而Google却认为支持现代Web应用的新一代浏览器才刚刚起步,尤其是JavaScript引擎性能还可提升10倍。随后,Mozilla借助Netscape遗产在2002年推出Firefox;Apple于2003年基于开源KHTML推出WebKit内核的Safari(仅限Mac)。Google也基于WebKit推出了跨Windows和Mac的Chrome浏览器,并自主研发了高性能JavaScript引擎——V8,以BSD许可证开源。
现代浏览器大战让微软IE远远落后,因为其最有经验的团队已被解散。而支持HTML5的WebKit成为了手机端标准,IE从此与主流移动端绝缘。
浏览器大战与Node有何关系?
一位名叫Ryan Dahl的开发者,工作是用C/C++编写高性能Web服务。他深知异步IO、事件驱动是高性能的基本原则,但C/C++实现起来非常痛苦。于是他开始评估各种高级语言,发现很多语言虽同时提供同步和异步IO,但开发者一旦用了同步IO就懒得再写异步代码了。
最终,Ryan瞄准了JavaScript——因为JavaScript是单线程执行,根本不能进行同步IO操作,这一“缺陷”迫使它只能使用异步IO。选定了语言,还需要运行时引擎。他曾考虑自己写一个,但明智地放弃了,因为V8已经是开源的且由Google持续优化。于是在2009年,Ryan正式推出基于JavaScript语言和V8引擎的开源Web服务器项目,命名为Node.js。Node第一次将JavaScript带入后端开发,加上全球庞大的JavaScript开发者群体,迅速火爆起来。
Node.js的核心优势
相比其他后端语言,Node.js运行JavaScript的最大优势在于:
- 天生的事件驱动机制 + V8高性能引擎,让编写高性能Web服务轻而易举。
- JavaScript是完善的函数式语言,在Node环境下通过模块化、函数式编程,且无需考虑浏览器兼容性,可直接使用最新ECMAScript标准,完全满足工程需求。
安装Node.js
由于Node.js平台在后端运行JavaScript代码,必须首先在本机安装Node环境。
安装步骤
追求稳定性(如服务器长期运行)可选择LTS版本,本地开发和测试可选最新版本。
从Node.js官网下载对应平台的安装程序,初学者建议选择Prebuilt Installer,选择版本、操作系统、CPU类型后点击Download:

有经验的开发者可选择Package Manager,它允许本地安装多个不同版本的Node并随时切换。
Windows安装注意事项:务必选择全部组件,包括勾选Add to Path。
安装完成后,打开命令提示符,输入node -v验证安装:
C:\Users\IEUser> node -v
v22.3.0继续输入node,将进入Node.js的交互环境,可直接输入JavaScript语句并立即执行,例如100 + 200回车后输出结果。连按两次Ctrl+C可退出交互环境。
npm——包管理工具
npm是Node.js的包管理工具(package manager)。为什么需要它?因为开发时会用到大量别人写的JavaScript代码。如果每次都手动搜索、下载、解压、使用,非常繁琐。npm集中管理所有模块:开发者将模块打包上传到npm官网,使用者通过npm install直接安装,npm还会根据依赖关系自动下载所有依赖包。
npm已在Node.js安装时顺带装好。输入npm -v验证:
C:\>npm -v
10.8.0直接输入npm会显示所有可用命令。后续会逐步学习。
模块小结:请在本机安装Node.js环境,确保
node和npm能正常运行。
第一个Node程序
从本篇开始,JavaScript代码将不再在浏览器中执行,而是在Node环境中以命令行方式运行。因此需要选择一个文本编辑器编写代码并保存到本地。
编辑器避坑指南
- 绝对不能用Word和写字板:它们保存的不是纯文本文件。
- 记事本:以UTF-8格式保存时会自作聪明地在文件开头添加UTF-8 BOM,导致程序运行出现莫名其妙错误。若用记事本,保存时请使用ANSI编码,且暂时不要输入中文。
- 推荐Visual Studio Code:注意用UTF-8格式保存。
输入以下代码:
'use strict';
console.log('Hello, world.');第一行'use strict';表示以严格模式运行JavaScript代码,避免各种潜在陷阱。
选择一个目录(例如C:\Workspace),保存为hello.js。打开命令行窗口,切换到该目录,运行:
C:\Workspace> node hello.js
Hello, world.文件名必须以.js结尾,只能包含英文字母、数字和下划线。如果当前目录下没有该文件,会报错:Error: Cannot find module。
命令行模式 vs Node交互模式
- 命令行模式:提示符类似
PS C:\>,在此模式下可以执行node进入交互环境,或执行node hello.js运行.js文件。 - Node交互模式:提示符为
>,在此模式下可直接输入JavaScript代码并立即执行,每一行代码的结果会自动打印出来。
区别示例:在交互模式下输入100 + 200 + 300;会直接输出600。但写一个calc.js文件内容为100 + 200 + 300;,然后执行node calc.js会发现什么输出都没有——因为直接运行.js文件不会自动打印结果,必须用console.log()显式输出。
console.log(100 + 200 + 300);模块小结:用文本编辑器写JavaScript程序保存为
.js文件,用node直接运行。交互模式适合验证代码片段,运行.js文件适合执行完整程序。
搭建Node开发环境
使用文本编辑器开发Node程序效率太低,运行还需单独敲命令,调试更麻烦。我们需要IDE集成开发环境,能在一个环境里编码、运行、调试。
Visual Studio Code
微软出品的VS Code,是一个精简版迷你Visual Studio,且支持跨平台(Windows、Mac、Linux)。
安装VS Code:从官网下载安装。安装过程中务必勾选 “将‘通过Code打开’操作添加到Windows资源管理器目录上下文菜单”:

运行和调试JavaScript
VS Code以文件夹作为工程目录(Workspace Dir),所有JavaScript文件存放在该目录下。例如在C:\Work\下创建hello目录作为工程目录,结构如下:
hello/ -- workspace dir
└── hello.js -- JavaScript file启动VS Code,选择菜单File - Open Folder...,选择hello目录,即可编辑hello.js:

运行JS代码:确保当前编辑器正在编辑hello.js,选择左侧调试按钮,点击Run And Debug,若弹出环境选项则选择Node,在右下侧DEBUG CONSOLE即可看到运行结果:

调试JS代码:在编辑器中打一个断点(鼠标点击行号左侧出现小红点),点击Run And Debug,程序会在断点处暂停,左侧窗口可查看变量,顶部按钮可选择单步执行或继续:

模块小结:使用VS Code可极大提升Node.js开发效率,支持代码编辑、运行、断点调试一体化。
使用模块
随着程序代码增多,在一个文件里会越来越难维护。在Node环境中,一个.js文件就称之为一个模块(module)。
模块的好处
- 提高代码可维护性:将函数分组放到不同文件。
- 复用代码:模块编写完毕可被其他地方引用,包括Node内置模块和第三方模块。
- 避免命名冲突:相同名字的函数和变量可以分别存在不同模块中。
编写并导出模块
将hello.js改造,创建一个函数并导出:
'use strict';
const s = 'Hello';
function greet(name) {
console.log(s + ', ' + name + '!');
}
module.exports = greet;module.exports = greet;把函数greet作为模块的输出暴露出去,这样其他模块就可以使用它。
引入模块
编写main.js,引入hello模块:
'use strict';
// 引入 hello 模块:
const greet = require('./hello');
let s = 'Michael';
greet(s); // Hello, Michael!注意相对路径:因为main.js和hello.js位于同一目录,必须写'./hello'。如果只写'hello',Node会依次在内置模块、全局模块和当前模块下查找,很可能报错Cannot find module 'hello'。
CommonJS规范
CommonJS 是 Node.js 原生遵循的模块化标准,定义了一套完整的 JavaScript 模块加载与导出机制。 在这套规范中,每个 .js 文件都是一个独立模块,模块内部的变量、函数均拥有私有作用域,相互隔离、互不冲突,不会污染全局环境。 模块如需对外暴露成员(变量、函数均可),可使用 module.exports = variable 语法;如需引入其他模块暴露的成员,则通过 const ref = require('module_name') 语法完成模块加载与引用。
结论:
- 对外输出:
module.exports = variable;(variable可以是任意对象、函数、数组等) - 引入模块:
const foo = require('other_module');
深入了解模块原理
JavaScript语言本身在ESM(ES6模块)标准之前并没有模块机制。Node.js利用函数式编程的闭包特性实现了模块隔离。
当Node.js加载hello.js时,会把代码包装一下:
(function () {
// 读取的 hello.js 代码:
let s = 'Hello';
let name = 'world';
console.log(s + ': ' + name + ':');
// hello.js 代码结束
})();原来的全局变量s变成了匿名函数内部的局部变量,不同模块的同名变量互不干扰。
模块输出module.exports的实现:Node先准备一个对象module,然后传入加载函数:
let module = {
id: 'hello',
exports: {}
};
let load = function (module) {
// 读取的 hello.js 代码:
function greet(name) {
console.log('Hello, ' + name + '!');
}
module.exports = greet;
// hello.js 代码结束
return module.exports;
};
let exported = load(module);变量module是Node在加载js文件前准备的,并作为参数传入,因此我们可以在hello.js中直接使用module。
module.exports vs exports
在了解了 CommonJS 规范的基本加载与导出逻辑后,我们来深入看模块导出的具体实现方式。Node.js 为模块导出提供了两个相关变量:module.exports和exports,两者配合使用能满足不同的导出需求,但也需要注意它们的区别,避免踩坑。
两种输出方式:
方法一:对module.exports赋值
function hello() { console.log('Hello, world!'); }
function greet(name) { console.log('Hello, ' + name + '!'); }
module.exports = { hello: hello, greet: greet };方法二:直接使用exports
exports.hello = hello;
exports.greet = greet;注意:不能直接对exports赋值:
// 无效!模块并没有输出任何变量
exports = { hello: hello, greet: greet };原理:Node准备exports变量和module.exports变量实际上是同一个变量,初始化为空对象{}。直接对exports赋值会切断引用,导致module.exports仍然是空对象。
结论:
- 输出键值对象
{},可以利用exports添加键值。 - 输出函数或数组,必须直接对
module.exports赋值。 - 强烈建议统一使用
module.exports = xxx,只需记忆一种方法。
模块小结:CommonJS规范通过
module.exports导出、require导入,利用闭包实现模块隔离。推荐统一使用module.exports赋值。
使用ESM模块
随着ES6标准推出,JavaScript迎来了原生模块支持——ECMAScript Modules(简称ESM),既可在浏览器中使用,也可在Node.js中使用。
不使用ESM时(CommonJS)
'use strict';
let s = 'Hello';
function out(prompt, name) { console.log(`${prompt}, ${name}!`); }
function greet(name) { out(s, name); }
function hi(name) { out('Hi', name); }
module.exports = { greet: greet, hi: hi };使用ESM模块
使用export关键字标识需要导出的函数,文件扩展名改为.mjs:
let s = 'Hello';
// out 是模块内部函数,模块外部不可见
function out(prompt, name) { console.log(`${prompt}, ${name}!`); }
// greet 是导出函数
export function greet(name) { out(s, name); }
// hi 是导出函数
export function hi(name) { out('Hi', name); }编写main.mjs引入:
import { greet, hi } from './hello.mjs';
let name = 'Bob';
greet(name);
hi(name);单个函数导出(默认导出)
CommonJS写法:module.exports = greet;
ESM写法:
export default function greet(name) { // ... }导入时:
import greet from './hello.mjs';ESM模块的严格模式
ESM模块默认启用严格模式,无需手动声明'use strict'。
在浏览器中加载ESM
使用type="module":
<script type="module" src="/example.js"></script>
<script type="module">
import { greet } from "/example.js";
greet('Bob');
</script>模块小结:ESM使用
export/import关键字,默认严格模式,文件扩展名.mjs。与CommonJS相比,它是JavaScript语言原生的模块系统。
基本模块
Node.js运行在服务器端,没有浏览器的安全限制,必须能接收网络请求、读写文件、处理二进制内容。内置的常用模块底层用C/C++实现。
global
浏览器中的全局对象是window,Node.js中的全局对象是global。
> global.console
Object [console] { log: [Function: log], warn: [Function: warn], ... }process
process对象代表当前Node.js进程,可拿到许多有用信息:
> process === global.process;
true
> process.version; // 'v22.3.0'
> process.platform; // 'darwin'
> process.arch; // 'x64'
> process.cwd(); // 返回当前工作目录
> process.chdir('/private/tmp'); // 切换当前工作目录事件驱动与nextTick:Node.js不断执行响应事件的JavaScript函数,直到没有可执行函数时退出。process.nextTick()将在下一轮事件循环中调用:
process.nextTick(function () {
console.log('nextTick callback!');
});
console.log('nextTick was set!');
// 输出:
// nextTick was set!
// nextTick callback!exit事件:程序即将退出时执行回调:
process.on('exit', function (code) {
console.log('about to exit with code: ' + code);
});判断JavaScript执行环境
根据全局变量名称判断:
if (typeof window === 'undefined') {
console.log('node.js');
} else {
console.log('browser');
}导入Node模块
方法一(传统require):
const { randomInt } = require('node:crypto');
const n = randomInt(0, 100);方法二(ESM import):
import { randomInt } from 'node:crypto';
const n = randomInt(0, 100);fs——文件系统模块
fs模块负责读写文件,同时提供了异步和同步方法。
异步读文件
// read-text-file-async.mjs
import { readFile } from 'node:fs';
console.log('BEGIN');
readFile('sample.txt', 'utf-8', function (err, data) {
if (err) {
console.log(err);
} else {
console.log(data);
}
});
console.log('END');回调函数标准形式:第一个参数代表错误信息,第二个参数代表结果。判断逻辑:
if (err) {
// 出错了
} else {
// 正常
}执行结果(先打印END,后打印文件内容):
BEGIN
END
Sample file content...读取二进制文件
不传入文件编码,回调函数的data参数返回一个Buffer对象(包含零个或任意个字节的数组):
readFile('sample.png', function (err, data) {
if (!err) {
console.log(data instanceof Buffer); // true
console.log(data); // Buffer(12451) [137, 80, 78, 71, ...]
}
});Buffer与String转换:
// Buffer -> String
let text = data.toString('utf-8');
// String -> Buffer
let buf = Buffer.from(text, 'utf-8');同步读文件
函数名加Sync后缀,不接收回调,直接返回结果,需用try...catch捕获错误:
import { readFileSync } from 'node:fs';
try {
let s = readFileSync('sample.txt', 'utf-8');
console.log(s);
} catch (err) {
console.log(err);
}写文件
fs.writeFile():
import { writeFile } from 'node:fs';
let data = 'Hello, Node.js';
writeFile('output.txt', data, function (err) {
if (err) console.log(err);
});- 如果
data是String,默认按UTF-8编码写入文本文件。 - 如果
data是Buffer,写入二进制文件。 - 同步方法:
writeFileSync()。
stat
获取文件大小、创建时间等信息:
import { stat } from 'node:fs';
stat('sample.png', function (err, st) {
if (!err) {
console.log('isFile: ' + st.isFile()); // 是否是文件
console.log('isDirectory: ' + st.isDirectory()); // 是否是目录
if (st.isFile()) {
console.log('size: ' + st.size); // 文件大小
console.log('birth time: ' + st.birthtime); // 创建时间
console.log('modified time: ' + st.mtime); // 修改时间
}
}
});同步方法:statSync()。
使用Promise版本的fs
Node还提供Promise版本的fs,可从node:fs/promises导入:
import { readFile } from 'node:fs/promises';
async function readTextFile(path) {
return await readFile(path, 'utf-8');
}
readTextFile('sample.txt').then(s => console.log(s));异步还是同步?
- 绝大部分需要在服务器运行期反复执行的代码,必须使用异步代码,否则同步代码执行期间服务器将停止响应(JavaScript只有一个执行线程)。
- 服务器启动时读取配置文件、结束时写入状态文件可使用同步代码(只执行一次)。
- 大量
async函数时,使用await调用fs/promises更便捷。
模块小结:
fs模块提供异步/同步文件读写、stat获取信息、Buffer处理二进制数据。推荐优先使用异步或Promise方式。
stream——流
stream是Node.js提供的服务器端模块,支持流这种数据结构。
什么是流?
流是一种抽象的数据结构,数据有序且必须依次读取/写入,不能像Array那样随机定位。例如:键盘输入对应标准输入流(stdin),显示器输出对应标准输出流(stdout)。

在Node.js中,流是一个对象,我们响应流的事件:
data事件:流的数据已可以读取end事件:流已到末尾,无更多数据error事件:出错
从文件流读取
import { createReadStream } from 'node:fs';
let rs = createReadStream('sample.txt', 'utf-8');
rs.on('data', (chunk) => { console.log('----- chunk ----'); console.log(chunk); });
rs.on('end', () => { console.log('---- end ----'); });
rs.on('error', err => { console.log(err); });data事件可能有多次,每次传递的chunk是流的一部分数据。
以流形式写入文件
import { createWriteStream } from 'node:fs';
let ws = createWriteStream('output.txt', 'utf-8');
ws.write('使用 Stream 写入文本数据...\n');
ws.write('继续写入...\n');
ws.write('DONE.\n');
ws.end(); // 结束写入
// 写入二进制数据
let ws2 = createWriteStream('output.png');
let buf = Buffer.from(b64, 'base64');
ws2.write(buf);
ws2.end();所有可读流继承自stream.Readable,所有可写流继承自stream.Writable。
pipe
两个流可以串起来:Readable流有一个pipe()方法,将数据自动从Readable流导入Writable流:
rs.pipe(ws);使用pipeline(支持添加转换器,如gzip压缩):
import { createReadStream, createWriteStream } from "node:fs";
import { pipeline } from 'node:stream/promises';
async function copy(src, dest) {
let rs = createReadStream(src);
let ws = createWriteStream(dest);
await pipeline(rs, ws);
}
copy('sample.txt', 'output.txt').then(() => console.log('copied.'));模块小结:流处理大文件时内存效率高,通过事件监听数据块,
pipe和pipeline简化了流之间的数据传输。
http——创建Web服务器
Node.js开发的目的就是用JavaScript编写Web服务器程序。
HTTP协议基础
要理解Web服务器工作原理,需对HTTP协议有基本了解(可参考MDN的HTTP协议简介: https://developer.mozilla.org/zh-CN/docs/Web/HTTP)。
最简单的HTTP服务器
http模块封装了底层TCP连接和HTTP解析,应用程序操作request和response对象:
import http from 'node:http';
const server = http.createServer((request, response) => {
console.log(request.method + ': ' + request.url);
response.writeHead(200, { 'Content-Type': 'text/html' });
response.end('<h1>Hello world!</h1>');
});
server.on('clientError', (err, socket) => {
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});
server.listen(8080);
console.log('Server is running at http://127.0.0.1:8080/');运行后浏览器访问http://localhost:8080,看到响应内容:

控制台打印请求信息:GET: / 和 GET: /favicon.ico。
文件服务器
设定一个目录,解析request.url中的路径,找到本地文件并发送。
解析path部分使用URL对象:
let url = new URL('http://localhost' + '/index.html?v=1');
let pathname = url.pathname; // index.html使用path模块处理文件路径(跨平台):
import path from 'node:path';
let workDir = path.resolve('.'); // 当前目录
let filePath = path.join(workDir, 'pub', 'index.html');完整文件服务器实现(simple-file-server.js):
import http from 'node:http';
import path from 'node:path';
import { createReadStream } from 'node:fs';
import { stat } from 'node:fs/promises';
const wwwRoot = path.resolve('.');
function guessMime(pathname) { return 'text/html'; } // 简化,实际需映射
const server = http.createServer((request, response) => {
if (request.method != 'GET') {
response.writeHead(400);
response.end('<h1>400 Bad Request</h1>');
} else {
let url = new URL('http://localhost' + request.url);
let filePath = path.join(wwwRoot, url.pathname);
stat(filePath).then(st => {
if (st.isFile()) {
response.writeHead(200, { 'Content-Type': guessMime(url.pathname) });
createReadStream(filePath).pipe(response);
} else {
response.writeHead(404);
response.end('<h1>404 Not Found</h1>');
}
}).catch(() => {
response.writeHead(404);
response.end('<h1>404 Not Found</h1>');
});
}
});
server.listen(8080);因为response对象本身是一个Writable Stream,直接用pipe()实现自动读取文件内容并输出到HTTP响应。
浏览器访问http://localhost:8080/index.html:

模块小结:
http模块可快速创建Web服务器,结合fs和path可实现静态文件服务器。响应对象是Writable Stream,可直接pipe文件流。
crypto——加密与哈希
crypto模块提供通用的加密和哈希算法,由C/C++实现,通过JavaScript接口暴露。
MD5和SHA1
import crypto from 'node:crypto';
const hash = crypto.createHash('md5');
hash.update('Hello, world!');
hash.update('Hello, nodejs!');
console.log(hash.digest('hex')); // 7e1977739c748beac0c0fd14fd26a544将'md5'换成'sha1'、'sha256'、'sha512'即可使用对应算法。
HMAC
需要密钥的哈希算法:
const hmac = crypto.createHmac('sha256', 'secret-key');
hmac.update('Hello, world!');
hmac.update('Hello, nodejs!');
console.log(hmac.digest('hex'));AES对称加密
function aesEncrypt(key, iv, msg) {
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
let encrypted = cipher.update(msg, 'utf8', 'hex');
encrypted += cipher.final('hex');
return encrypted;
}
function aesDecrypt(key, iv, encrypted) {
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
const key = 'Passw0rdPassw0rdPassw0rdPassw0rd'; // 32 bytes
const iv = 'a1b2c3d4e5f6g7h8'; // 16 bytes
const encrypted = aesEncrypt(key, iv, 'Hello, world!');
const decrypted = aesDecrypt(key, iv, encrypted);注意:密钥长度必须为32字节(aes-256-cbc),IV长度16字节。不同系统(Node.js、Java、PHP)需确保算法、密钥、IV、输出格式(hex/base64)一致。
Diffie-Hellman密钥交换
// xiaoming's keys:
let ming = crypto.createDiffieHellman(512);
let mingKeys = ming.generateKeys();
let prime = ming.getPrime();
let generator = ming.getGenerator();
// xiaohong's keys:
let hong = crypto.createDiffieHellman(prime, generator);
let hongKeys = hong.generateKeys();
// exchange and generate secret:
let mingSecret = ming.computeSecret(hongKeys);
let hongSecret = hong.computeSecret(mingKeys);RSA非对称加密
生成密钥对(需安装OpenSSL):
# 生成加密的RSA密钥对
openssl genrsa -aes256 -out rsa-key.pem 2048
# 导出原始私钥
openssl rsa -in rsa-key.pem -outform PEM -out rsa-prv.pem
# 导出原始公钥
openssl rsa -in rsa-key.pem -outform PEM -pubout -out rsa-pub.pem使用私钥加密、公钥解密:
import fs from 'node:fs';
import crypto from 'node:crypto';
let prvKey = fs.readFileSync('./rsa-prv.pem', 'utf8');
let pubKey = fs.readFileSync('./rsa-pub.pem', 'utf8');
let message = 'Hello, world!';
let encByPrv = crypto.privateEncrypt(prvKey, Buffer.from(message));
let decByPub = crypto.publicDecrypt(pubKey, encByPrv);
console.log(decByPub.toString('utf8'));使用公钥加密、私钥解密:
let encByPub = crypto.publicEncrypt(pubKey, Buffer.from(message));
let decByPrv = crypto.privateDecrypt(prvKey, encByPub);注意:RSA加密的原始信息必须小于Key的长度。通常做法:生成随机AES密码,用AES加密大数据,再用RSA加密AES口令。
模块小结:
crypto模块提供哈希(MD5/SHA/HMAC)、对称加密(AES)、密钥交换(DH)、非对称加密(RSA)等算法,所有底层由C/C++实现,性能优异。
Web开发与koa框架
Web开发演进
从静态Web页面到CGI,再到ASP/JSP/PHP,最后到MVC模式。Node.js将JavaScript引入服务器端,前后端语言统一,开发效率高。
koa简介
koa是Express的下一代基于Node.js的Web框架。
Express(第一代):基于回调,异步嵌套层次多时代码难看。
koa 1.x:基于ES6的generator实现异步,代码像同步。
koa 2.x:完全使用Promise并配合async/await,是当前推荐版本。
koa入门
创建工程:
mkdir hello-koa
cd hello-koa
npm init -y # 生成package.json
npm install koa手动添加"type": "module"到package.json以支持ESM。
编写app.mjs:
import Koa from 'koa';
const app = new Koa();
app.use(async (ctx, next) => {
await next();
ctx.response.type = 'text/html';
ctx.response.body = '<h1>Hello Koa!</h1>';
});
app.listen(3000);
console.log('app started at port 3000...');运行node app.mjs,浏览器访问http://localhost:3000:

koa middleware(中间件)
每收到一个HTTP请求,koa调用通过app.use()注册的async函数(称为middleware),传入ctx和next参数。await next()用于调用下一个middleware。
示例:三个middleware组成处理链(打印日志、记录时间、输出HTML):
app.use(async (ctx, next) => {
console.log(`${ctx.request.method} ${ctx.request.url}`);
await next();
});
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`Time: ${ms}ms`);
});
app.use(async (ctx, next) => {
await next();
ctx.response.type = 'text/html';
ctx.response.body = '<h1>Hello Koa!</h1>';
});middleware顺序很重要(即app.use()的注册顺序)。如果一个middleware没有调用await next(),后续middleware将不再执行(如权限检查失败直接返回403)。
模块小结:koa通过middleware链处理请求,每个middleware可执行前置逻辑、调用下一个、执行后置逻辑。
ctx封装了request和response。
处理URL与路由
@koa/router
安装:npm install @koa/router
import Koa from 'koa';
import Router from '@koa/router';
const app = new Koa();
const router = new Router();
router.get('/', async (ctx, next) => {
ctx.response.type = 'text/html';
ctx.response.body = '<h1>Index Page</h1>';
});
router.get('/hello/:name', async (ctx, next) => {
let s = ctx.params.name;
ctx.response.type = 'text/html';
ctx.response.body = `<h1>Hello, ${s}</h1>`;
});
app.use(router.routes());
app.listen(3000);访问http://localhost:3000/和http://localhost:3000/hello/Bob:


处理POST请求
需要解析request.body,使用@koa/bodyparser:
npm install @koa/bodyparserimport { bodyParser } from '@koa/bodyparser';
app.use(bodyParser()); // 必须在router之前注册
router.post('/signin', async (ctx, next) => {
let name = ctx.request.body.name || '';
let password = ctx.request.body.password || '';
if (name === 'koa' && password === '12345') {
ctx.response.body = `<h1>Welcome, ${name}!</h1>`;
} else {
ctx.response.body = `<h1>Signin failed</h1><p><a href="/">Retry</a></p>`;
}
});重构:自动扫描Controller
将URL处理函数按功能分组放到controller目录下,每个文件导出映射对象。
controller/signin.mjs:
async function index(ctx, next) { /* 渲染表单 */ }
async function signin(ctx, next) { /* 处理登录 */ }
export default {
'GET /': index,
'POST /signin': signin
};controller/hello.mjs:
async function hello(ctx, next) {
let s = ctx.params.name;
ctx.response.body = `<h1>Hello, ${s}</h1>`;
}
export default { 'GET /hello/:name': hello };app.mjs中自动扫描并注册:
import { readdirSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const dirname = path.dirname(fileURLToPath(import.meta.url));
let files = readdirSync(path.join(dirname, 'controller')).filter(f => f.endsWith('.mjs'));
for (let file of files) {
let { default: mapping } = await import(`./controller/${file}`);
for (let url in mapping) {
if (url.startsWith('GET ')) {
router.get(url.substring(4), mapping[url]);
} else if (url.startsWith('POST ')) {
router.post(url.substring(5), mapping[url]);
}
}
}进一步封装为controller.mjs middleware,最终app.mjs只需:
import controller from './controller.mjs';
app.use(await controller());HTTP请求处理流程:

模块小结:
@koa/router提供RESTful风格路由,@koa/bodyparser解析POST请求体。通过自动扫描controller目录可实现模块化路由管理。
使用Nunjucks模板引擎
模板引擎基于模板配合数据构造字符串输出。需要考虑转义(防XSS)、格式化(货币、日期)、简单逻辑(条件、循环)。
Nunjucks简介
Mozilla开发的纯JavaScript模板引擎,语法类似Python的jinja2。
安装:npm install nunjucks
基本使用
import nunjucks from 'nunjucks';
function createEnv(path, { autoescape = true, noCache = false, watch = false, throwOnUndefined = false }, filters = {}) {
const loader = new nunjucks.FileSystemLoader(path, { noCache, watch });
const env = new nunjucks.Environment(loader, { autoescape, throwOnUndefined });
for (let name in filters) env.addFilter(name, filters[name]);
return env;
}
const env = createEnv('view', { noCache: true }, {
hex: function (n) { return '0x' + n.toString(16); }
});渲染模板:
const s = env.render('hello.html', { name: '小明' });
console.log(s); // <h1>Hello 小明</h1>自动转义:
env.render('hello.html', { name: ':<script>alert("小明")</script>' });
// 输出:<h1>Hello :<script>alert("小明")</script></h1>模板语法示例
循环:
<body>
<h3>Fruits List</h3>
{% for f in fruits %}
<p>{{ f }}</p>
{% endfor %}
</body>模板继承:
base.html定义可编辑块:
<html>
<body>
{% block header %}<h3>Unnamed</h3>{% endblock %}
{% block body %}<div>No body</div>{% endblock %}
{% block footer %}<div>copyright</div>{% endblock %}
</body>
</html>子模板extend.html:
{% extends 'base.html' %}
{% block header %}<h1>{{ header }}</h1>{% endblock %}
{% block body %}<p>{{ body }}</p>{% endblock %}渲染:
console.log(env.render('extend.html', { header: 'Hello', body: 'bla bla bla...' }));性能
Nunjucks会缓存已读取的文件内容(noCache: false)。开发环境关闭缓存(noCache: true),生产环境务必开启缓存。
模块小结:Nunjucks提供自动转义、过滤器、循环/条件、模板继承等功能。通过
env.render(view, model)生成HTML。
使用MVC架构
将koa与Nunjucks结合,形成 MVC(Model-View-Controller) 模式:
- Controller:异步函数,负责业务逻辑
- View:Nunjucks模板,负责显示逻辑
- Model:传递给View的JavaScript对象

集成Nunjucks到koa
创建view.mjs:
import nunjucks from 'nunjucks';
function createEnv(path, { autoescape = true, noCache = false, watch = false, throwOnUndefined = false }, filters = {}) {
const loader = new nunjucks.FileSystemLoader(path, { noCache, watch });
const env = new nunjucks.Environment(loader, { autoescape, throwOnUndefined });
for (let name in filters) env.addFilter(name, filters[name]);
return env;
}
const env = createEnv('view', { noCache: process.env.NODE_ENV != 'production' });
export default env;在app.mjs中绑定ctx.render方法:
import templateEngine from './view.mjs';
app.context.render = function (view, model) {
this.response.type = 'text/html; charset=utf-8';
this.response.body = templateEngine.render(view, Object.assign({}, this.state || {}, model || {}));
};处理静态文件
使用koa-mount和koa-static:
npm install koa-mount koa-staticimport mount from 'koa-mount';
import serve from 'koa-static';
if (!isProduction) {
app.use(mount('/static', serve('static')));
}生产环境下静态文件由反向代理服务器(如Nginx)处理,开发环境下由koa处理。
编写Controller
controller/index.mjs:
async function index(ctx, next) {
ctx.render('index.html', { title: 'Welcome' });
}
export default { 'GET /': index };controller/signin.mjs:
async function signin(ctx, next) {
let email = ctx.request.body.email || '';
let password = ctx.request.body.password || '';
if (email === 'admin@example.com' && password === '123456') {
ctx.render('signin-ok.html', { title: 'Sign In OK', name: 'Mr Bob' });
} else {
ctx.render('signin-failed.html', { title: 'Sign In Failed' });
}
}
export default { 'POST /signin': signin };运行效果
首页:

登录成功:

登录失败:

扩展:公共变量
ctx.render内部合并了ctx.state,可在middleware中设置公共变量(如当前登录用户):
app.use(async (ctx, next) => {
var user = tryGetUserFromCookie(ctx.request);
if (user) {
ctx.state.user = user;
await next();
} else {
ctx.response.status = 403;
}
});模块小结:MVC架构分离业务逻辑、显示逻辑和数据模型。通过
ctx.render方法在Controller中渲染View,结合静态文件中间件,可快速构建Web应用。
使用REST API
REST(Representational State Transfer) 是一种设计API的模式,常用数据格式为JSON。
REST API规范
- 请求的
Content-Type为application/json,body为JSON数据 - 响应的
Content-Type也是application/json
资源URL示例:
GET /api/products:获取所有ProductGET /api/products/123:获取id为123的ProductPOST /api/products:新建ProductPUT /api/products/123:更新ProductDELETE /api/products/123:删除ProductGET /api/products/123/reviews?page=2&size=10:分页获取评论
koa中处理REST
将JavaScript对象赋值给ctx.body,koa自动转为JSON字符串输出。
示例:添加REST API到signin.mjs
// GET /api/users/:id
async function user_info(ctx, next) {
let id = ctx.params.id;
if (id === '12345') {
ctx.body = { id: 12345, email: 'admin@example.com', name: 'Bob' };
} else {
ctx.body = { error: 'USER_NOT_FOUND' };
}
}
// POST /api/signin
async function signin_api(ctx, next) {
let email = ctx.request.body.email || '';
let password = ctx.request.body.password || '';
if (email === 'admin@example.com' && password === '123456') {
ctx.body = { id: 12345, email: email, name: 'Bob' };
} else {
ctx.body = { error: 'SIGNIN_FAILED' };
}
}
export default {
'POST /api/signin': signin_api,
'GET /api/users/:id': user_info
};测试GET请求(浏览器直接访问):

无效ID返回错误:

测试POST请求(使用curl):
curl -H 'Content-Type: application/json' \
-d '{"email":"admin@example.com","password":"123456"}' \
http://localhost:3000/api/signin
# 返回 {"id":12345,"email":"admin@example.com","name":"Bob"}前端调用REST API
修改HTML表单,使用fetch发送JSON请求:
<form id="signin-form" onsubmit="return signin()">
<!-- 表单字段 -->
</form>
<script>
function signin() {
let email = document.querySelector('input[name=email]').value;
let password = document.querySelector('input[name=password]').value;
fetch('/api/signin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
}).then(response => response.json()).then(result => {
if (result.error) alert('Sign in failed: ' + result.error);
else alert('Welcome, ' + result.name + '!');
});
return false;
}
</script>模块小结:REST API使用JSON格式,koa中通过
ctx.body赋值对象自动输出JSON,结合@koa/bodyparser解析请求体,可轻松实现RESTful服务。
数据库操作
关系数据库基础
数据库用于集中存储和查询数据。关系数据库基于表(Table),表之间通过外键关联。例如年级表(Grade)和班级表(Class)通过Grade_ID关联:

班级表和学生表关联:

常用SQL语句示例:SELECT * FROM classes WHERE grade_id = '1';
选择SQLite
我们使用嵌入式数据库SQLite(无需安装),生产环境可换MySQL或PostgreSQL。
安装驱动:npm install sqlite3
封装Promise接口
sqlite3原生使用回调,封装后支持await:
// db.mjs
import sqlite3 from 'sqlite3';
export function createDatabase(file) {
const db = new sqlite3.Database(file, sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE | sqlite3.OPEN_FULLMUTEX);
const wrapper = { db };
wrapper.update = async function (strs, ...params) {
return new Promise((resolve, reject) => {
let sql = strs.join('?');
db.run(sql, ...params, function (err) {
err ? reject(err) : resolve(this.changes);
});
});
};
wrapper.insert = async function (strs, ...params) {
return new Promise((resolve, reject) => {
let sql = strs.join('?');
db.run(sql, ...params, function (err) {
err ? reject(err) : resolve(this.lastID);
});
});
};
wrapper.select = async function (strs, ...params) {
return new Promise((resolve, reject) => {
let sql = strs.join('?');
db.all(sql, ...params, function (err, rows) {
err ? reject(err) : resolve(rows);
});
});
};
wrapper.fetch = async function (strs, ...params) {
return new Promise((resolve, reject) => {
let sql = strs.join('?');
db.get(sql, ...params, function (err, row) {
err ? reject(err) : resolve(row);
});
});
};
return wrapper;
}在应用中初始化数据库
import { createDatabase } from './db.mjs';
async function initDb() {
const db = createDatabase('test.db');
await db.update`CREATE TABLE IF NOT EXISTS users (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
password TEXT NOT NULL
)`;
let user = await db.fetch`SELECT * FROM users WHERE email = ${'admin@example.com'}`;
if (user === null) {
await db.insert`INSERT INTO users (email, name, password) VALUES (${'admin@example.com'}, ${'Bob'}, ${'123456'})`;
}
return db;
}
app.context.db = await initDb();使用数据库替换登录逻辑
async function signin(ctx, next) {
let email = ctx.request.body.email || '';
let password = ctx.request.body.password || '';
let user = await ctx.db.fetch`SELECT * FROM users WHERE email = ${email}`;
if (user && user.password === password) {
ctx.render('signin-ok.html', { title: 'Sign In OK', name: user.name });
} else {
ctx.render('signin-failed.html', { title: 'Sign In Failed' });
}
}模块小结:通过封装sqlite3的Promise接口,可使用标签函数(自动参数化)安全执行SQL,避免注入。数据库操作完全异步。
使用ORM——Sequelize
ORM(Object-Relational Mapping) 将数据库表映射到JavaScript对象,操作对象即操作数据库。
安装与定义模型
npm install sequelize sqlite3// orm.mjs
import { Sequelize, DataTypes } from 'sequelize';
export const sequelize = new Sequelize('sqlite:test.db');
export const User = sequelize.define('User', {
id: { primaryKey: true, autoIncrement: true, type: DataTypes.INTEGER, allowNull: false },
email: { unique: true, type: DataTypes.STRING, allowNull: false },
name: { type: DataTypes.STRING, allowNull: false },
password: { type: DataTypes.STRING, allowNull: false }
}, { tableName: 'users' });初始化数据库
import { sequelize, User } from './orm.mjs';
async function initDb() {
await sequelize.sync(); // 自动创建表
const email = 'admin@example.com';
let user = await User.findOne({ where: { email } });
if (!user) {
await User.create({ email, name: 'Bob', password: '123456' });
}
}查询与登录
import { User } from './orm.mjs';
async function signin(ctx, next) {
let email = ctx.request.body.email || '';
let password = ctx.request.body.password || '';
let user = await User.findOne({ where: { email } });
if (user && user.password === password) {
ctx.render('signin-ok.html', { title: 'Sign In OK', name: user.name });
} else {
ctx.render('signin-failed.html', { title: 'Sign In Failed' });
}
}常用操作
- 查询多行:
await User.findAll() - 查询单行:
await User.findOne({ where: { email } }) - 创建:
await User.create({ email, name, password }) - 更新:
user.name = 'New Name'; await user.save(); - 删除:
await user.destroy();
Sequelize会自动添加createdAt和updatedAt字段。
模块小结:Sequelize提供完整的ORM能力,通过模型定义映射表结构,支持
findOne、create、save、destroy等操作,代码更接近业务逻辑。
本篇核心知识点速记
- Node.js:基于V8引擎的JavaScript后端运行时,利用单线程+异步IO实现高性能。
- 模块系统:CommonJS(
module.exports/require)与ESM(export/import),ESM默认严格模式,文件扩展名.mjs。 - 全局对象:
global(Node)、process(进程信息、nextTick、exit事件)。 - fs模块:异步/同步读写文件、
stat获取信息、Buffer处理二进制、Promise版本(fs/promises)。 - stream模块:
Readable/Writable流,data/end/error事件,pipe和pipeline传输数据。 - http模块:
createServer创建Web服务器,request/response对象,结合fs实现文件服务器。 - crypto模块:哈希(MD5/SHA/HMAC)、对称加密(AES)、非对称加密(RSA)、密钥交换(DH)。
- koa框架:基于
async/await的middleware链,@koa/router处理路由,@koa/bodyparser解析POST请求体。 - Nunjucks模板:自动转义、模板继承、过滤器,通过
env.render渲染HTML。 - MVC架构:Controller(业务逻辑)、View(模板)、Model(数据),
ctx.render整合。 - REST API:JSON格式请求/响应,
ctx.body赋值对象自动序列化,使用fetch调用。 - 数据库:SQLite嵌入式数据库,封装Promise接口,使用标签函数防SQL注入。
- Sequelize ORM:定义模型(
define),sync建表,findOne/create/save/destroy操作。
