Skip to content

《JavaScript全栈教程》13:Node.js后端开发——从环境搭建到Web框架与数据库ORM全解

约 9040 字大约 30 分钟

JavaScript全栈教程JavaScript

2026-04-13

从本篇开始,我们正式踏入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的最大优势在于:

  1. 天生的事件驱动机制 + V8高性能引擎,让编写高性能Web服务轻而易举。
  2. 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环境,确保nodenpm能正常运行。

第一个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)

模块的好处

  1. 提高代码可维护性:将函数分组放到不同文件。
  2. 复用代码:模块编写完毕可被其他地方引用,包括Node内置模块和第三方模块。
  3. 避免命名冲突:相同名字的函数和变量可以分别存在不同模块中。

编写并导出模块

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.jshello.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.exportsexports,两者配合使用能满足不同的导出需求,但也需要注意它们的区别,避免踩坑。

两种输出方式:

方法一:对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, ...]
    }
});

BufferString转换:

// 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.'));

模块小结:流处理大文件时内存效率高,通过事件监听数据块,pipepipeline简化了流之间的数据传输。

http——创建Web服务器

Node.js开发的目的就是用JavaScript编写Web服务器程序。

HTTP协议基础

要理解Web服务器工作原理,需对HTTP协议有基本了解(可参考MDN的HTTP协议简介: https://developer.mozilla.org/zh-CN/docs/Web/HTTP)。

最简单的HTTP服务器

http模块封装了底层TCP连接和HTTP解析,应用程序操作requestresponse对象:

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服务器,结合fspath可实现静态文件服务器。响应对象是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),传入ctxnext参数。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/bodyparser
import { 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 :&lt;script&gt;alert(&quot;小明&quot;)&lt;/script&gt;</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-mountkoa-static

npm install koa-mount koa-static
import 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-Typeapplication/json,body为JSON数据
  • 响应的Content-Type也是application/json

资源URL示例

  • GET /api/products:获取所有Product
  • GET /api/products/123:获取id为123的Product
  • POST /api/products:新建Product
  • PUT /api/products/123:更新Product
  • DELETE /api/products/123:删除Product
  • GET /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会自动添加createdAtupdatedAt字段。

模块小结:Sequelize提供完整的ORM能力,通过模型定义映射表结构,支持findOnecreatesavedestroy等操作,代码更接近业务逻辑。

本篇核心知识点速记

  • Node.js:基于V8引擎的JavaScript后端运行时,利用单线程+异步IO实现高性能。
  • 模块系统:CommonJS(module.exports/require)与ESM(export/import),ESM默认严格模式,文件扩展名.mjs
  • 全局对象global(Node)、process(进程信息、nextTickexit事件)。
  • fs模块:异步/同步读写文件、stat获取信息、Buffer处理二进制、Promise版本(fs/promises)。
  • stream模块Readable/Writable流,data/end/error事件,pipepipeline传输数据。
  • 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操作。