Node.js 从入门到进阶
- [1. Node.js 基础概念](#1. Node.js 基础概念)
-
- [1.1 事件驱动与单线程](#1.1 事件驱动与单线程)
- [1.2 模块化:CommonJS](#1.2 模块化:CommonJS)
- [2. 文件系统 fs](#2. 文件系统 fs)
-
- [2.1 同步、异步与流](#2.1 同步、异步与流)
- [2.2 写文件与追加](#2.2 写文件与追加)
- [2.3 读文件:异步、同步与流](#2.3 读文件:异步、同步与流)
- [2.4 目录、重命名与删除](#2.4 目录、重命名与删除)
- [3. 原生 HTTP 服务](#3. 原生 HTTP 服务)
-
- [3.1 请求与响应](#3.1 请求与响应)
- [3.2 带路由的 HTTP 服务示例](#3.2 带路由的 HTTP 服务示例)
- [4. Express 入门](#4. Express 入门)
-
- [4.1 中间件与路由](#4.1 中间件与路由)
- [4.2 最小 Express 服务](#4.2 最小 Express 服务)
- [4.3 静态资源与自定义中间件](#4.3 静态资源与自定义中间件)
- [4.4 常用响应方法](#4.4 常用响应方法)
- [5. Cookie 与 Session](#5. Cookie 与 Session)
-
- [5.1 区别与适用场景](#5.1 区别与适用场景)
- [5.2 Cookie 示例](#5.2 Cookie 示例)
- [5.3 Session 示例](#5.3 Session 示例)
- [6. 模板引擎 EJS](#6. 模板引擎 EJS)
-
- [6.1 语法](#6.1 语法)
- [6.2 原生 Node 中渲染 EJS](#6.2 原生 Node 中渲染 EJS)
- [6.3 Express 中集成 EJS](#6.3 Express 中集成 EJS)
- [7. REST API 设计](#7. REST API 设计)
-
- [7.1 HTTP 方法语义](#7.1 HTTP 方法语义)
- [7.2 用户 CRUD 示例](#7.2 用户 CRUD 示例)
- [8. 进阶](#8. 进阶)
-
- [8.1 防盗链(Referer)](#8.1 防盗链(Referer))
- [8.2 路由模块化](#8.2 路由模块化)
- [8.3 文件上传(Formidable)](#8.3 文件上传(Formidable))
技术栈:Node.js 内置模块(fs、http、path)+ Express + EJS + Cookie/Session + Formidable
1. Node.js 基础概念
1.1 事件驱动与单线程
Node.js 基于事件循环,主线程单线程执行 JavaScript。I/O 等耗时操作通过异步回调或 Promise 交给底层线程池,不阻塞主线程。写文件、读网络等 API 多为回调或 Promise,重 CPU 计算不宜放在回调中执行。
1.2 模块化:CommonJS
Node 默认采用 CommonJS:require() 引入,module.exports 导出,每个文件一个模块,独立作用域。
| 用法 | 说明 |
|---|---|
| require('fs') | 引入内置或 node_modules 模块 |
| require('./utils') | 引入当前目录模块(可省略 .js) |
| module.exports = fn | 导出一个函数或对象 |
| exports.xxx = fn | 等价于 module.exports.xxx |
2. 文件系统 fs
2.1 同步、异步与流
fs.writeFile(path, data, callback) 为异步,不阻塞;fs.writeFileSync(path, data) 为同步,会阻塞,适合脚本或启动时读配置。大文件用 createReadStream / createWriteStream,避免一次性读入内存。
2.2 写文件与追加
新建 write-demo.js,写入下列代码后执行 node write-demo.js。同目录下会生成 output.txt。
javascript
const fs = require("fs");
fs.writeFile("./output.txt", "第一行内容", (err) => {
if (err) return console.error(err);
console.log("写入成功");
});
fs.appendFile("./output.txt", "\r\n追加的第二行", (err) => {
if (err) return console.error(err);
console.log("追加成功");
});
2.3 读文件:异步、同步与流
javascript
const fs = require("fs");
fs.readFile("./output.txt", (err, data) => {
if (err) return console.error(err);
console.log("异步:", data.toString());
});
const content = fs.readFileSync("./output.txt", "utf-8");
console.log("同步:", content);
const rs = fs.createReadStream("./output.txt");
rs.on("data", (chunk) => console.log("流:", chunk.toString()));
rs.on("end", () => console.log("流结束"));
2.4 目录、重命名与删除
javascript
const fs = require("fs");
fs.mkdir("./a/b/c", { recursive: true }, (err) => {
if (err) return console.error(err);
});
fs.rename("./output.txt", "./output-renamed.txt", (err) => {
if (err) return console.error(err);
});
fs.rm("./output-renamed.txt", (err) => {
if (err) return console.error(err);
});
3. 原生 HTTP 服务
3.1 请求与响应
req.url 为路径与查询串,req.method 为 GET/POST 等。用 new URL(req.url, base) 可解析出 pathname、searchParams。响应时先 res.setHeader() 设置头,再 res.end(data) 或 res.write(data) 后 res.end()。
3.2 带路由的 HTTP 服务示例
新建 http-server.js,运行 node http-server.js,浏览器访问 http://localhost:9000/login、http://localhost:9000/ 及带查询参数的 URL 做对比。
javascript
const http = require("http");
const server = http.createServer((req, res) => {
const url = new URL(req.url || "/", "http://localhost:9000");
const pathname = url.pathname;
res.setHeader("content-type", "text/html;charset=utf-8");
if (req.method === "GET") {
if (pathname === "/login") {
const keyword = url.searchParams.get("keyword");
res.end("<h1>登录页</h1><p>keyword: " + (keyword || "") + "</p>");
return;
}
if (pathname === "/reg") {
res.end("<h1>注册页</h1>");
return;
}
}
res.end("<h1>默认页</h1>");
});
server.listen(9000, () => console.log("http://localhost:9000"));
4. Express 入门
4.1 中间件与路由
中间件为函数 (req, res, next),用于统一解析 body、打日志、鉴权等;不调用 next() 则不会进入后续中间件或路由。路由按 method + path 匹配,先匹配先执行。解析 JSON/表单的中间件须在使用 req.body 的路由之前挂载。
4.2 最小 Express 服务
npm init -y 后执行 npm i express,新建 app.js:
javascript
const express = require("express");
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.get("/", (req, res) => res.send("Hello Express"));
app.get("/login", (req, res) => res.send("<h1>登录页</h1>"));
app.post("/login", (req, res) => {
console.log("body:", req.body);
res.send("body 已打印到控制台");
});
app.listen(3000, () => console.log("http://localhost:3000"));
运行后访问 GET /、/login;用 Postman 发 POST /login,Body 选 raw JSON 或 x-www-form-urlencoded,查看控制台 req.body。
4.3 静态资源与自定义中间件
express.static 将指定目录映射为根路径下的静态文件。中间件在路由之前执行,可打印 method 与 url 再放行。
javascript
const path = require("path");
const express = require("express");
const app = express();
app.use(express.static(path.join(__dirname, "public")));
app.use((req, res, next) => {
console.log(req.method, req.url);
next();
});
app.get("/api/hello", (req, res) => res.json({ msg: "hello" }));
app.listen(3000, () => console.log("http://localhost:3000"));
新建 public 目录并放入 index.html,访问 http://localhost:3000/index.html 与 http://localhost:3000/api/hello,对照控制台输出。
4.4 常用响应方法
| 方法 | 说明 |
|---|---|
| res.send(str | Buffer | object) | 自动设 Content-Type,对象以 JSON 输出 |
| res.json(obj) | application/json 并发送对象 |
| res.sendFile(path) | 发送文件 |
| res.redirect(url) | 302 跳转 |
| res.status(code) | 设置状态码,可链式 .send() |
| res.setHeader(name, value) | 设置响应头 |
5. Cookie 与 Session
5.1 区别与适用场景
| 对比项 | Cookie | Session |
|---|---|---|
| 存储位置 | 浏览器,可被查看与修改 | 服务端,仅通过 Cookie 传递 sessionId |
| 容量与安全 | 容量小,不宜存敏感信息 | 可存较多数据,适合登录态 |
| 典型依赖 | cookie-parser | express-session |
Session 依赖 Cookie 存 sessionId,二者常配合使用。
5.2 Cookie 示例
npm i express cookie-parser,新建 cookie-demo.js:
javascript
const express = require("express");
const cookieParser = require("cookie-parser");
const app = express();
app.use(cookieParser());
app.get("/set", (req, res) => {
res.cookie("name", "tom", { maxAge: 60 * 1000, httpOnly: true });
res.send("Cookie 已设置");
});
app.get("/get", (req, res) => {
res.send("name = " + (req.cookies.name || "未设置"));
});
app.get("/clear", (req, res) => {
res.clearCookie("name");
res.send("Cookie 已清除");
});
app.listen(3000, () => console.log("http://localhost:3000"));
依次访问 /set、/get、/clear、再 /get,观察返回值。
5.3 Session 示例
npm i express express-session,新建 session-demo.js:
javascript
const express = require("express");
const session = require("express-session");
const app = express();
app.use(
session({
secret: "your-secret-key",
resave: false,
saveUninitialized: true,
})
);
app.get("/", (req, res) => {
req.session.username = "john";
res.send("已写入 session.username");
});
app.get("/user", (req, res) => {
res.send("session.username = " + (req.session.username || "未设置"));
});
app.listen(3000, () => console.log("http://localhost:3000"));
同一浏览器先访问 / 再访问 /user 可见 john;无痕或另一浏览器访问 /user 为「未设置」。
6. 模板引擎 EJS
6.1 语法
服务端用「模板字符串 + 数据」渲染成 HTML 再返回。EJS 常用标签:
| 标签 | 含义 |
|---|---|
<% code %> |
执行 JS,不输出 |
<%= value %> |
输出转义后的值 |
<%- html %> |
输出原始 HTML,慎用(防 XSS) |
6.2 原生 Node 中渲染 EJS
npm i ejs,新建 template.html 与 ejs-demo.js。
template.html:
html
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>EJS</title></head>
<body>
<% if (isLogin) { %>
<p>欢迎,<%= username %></p>
<% } else { %>
<p>请登录</p>
<% } %>
</body>
</html>
ejs-demo.js:
javascript
const fs = require("fs");
const ejs = require("ejs");
const http = require("http");
const server = http.createServer((req, res) => {
const html = fs.readFileSync("./template.html", "utf-8");
const rendered = ejs.render(html, { isLogin: true, username: "张三" });
res.setHeader("content-type", "text/html;charset=utf-8");
res.end(rendered);
});
server.listen(3000, () => console.log("http://localhost:3000"));
将 isLogin 改为 false 后重启,页面变为「请登录」。
6.3 Express 中集成 EJS
npm i express ejs,新建 views 目录及 views/page.ejs(内容同 template,扩展名改为 .ejs)。主文件:
javascript
const path = require("path");
const express = require("express");
const app = express();
app.set("view engine", "ejs");
app.set("views", path.join(__dirname, "views"));
app.get("/", (req, res) => {
res.render("page", { isLogin: true, username: "李四" });
});
app.listen(3000, () => console.log("http://localhost:3000"));
访问 / 得到与上节一致的渲染结果。
7. REST API 设计
7.1 HTTP 方法语义
| 方法 | 常见语义 | 示例 |
|---|---|---|
| GET | 查询资源 | 用户列表、单用户 |
| POST | 创建资源 | 新增用户 |
| PUT | 全量更新 | 按 id 更新整条 |
| PATCH | 部分更新 | 只改若干字段 |
| DELETE | 删除资源 | 按 id 删除 |
路径参数用 req.params(如 /users/:id 的 id),请求体用 req.body;返回 JSON 用 res.json()。
7.2 用户 CRUD 示例
新建 user-api.js,npm i express 后运行:
javascript
const express = require("express");
const app = express();
app.use(express.json());
let users = [{ id: 1, name: "张三", email: "zhangsan@example.com" }];
app.get("/users", (req, res) => res.json({ users }));
app.get("/users/:id", (req, res) => {
const user = users.find((u) => u.id === Number(req.params.id));
if (!user) return res.status(404).json({ error: "用户不存在" });
res.json({ user });
});
app.post("/users", (req, res) => {
const { name, email } = req.body || {};
const id = Date.now();
users.push({ id, name, email });
res.status(201).json({ message: "创建成功", user: { id, name, email } });
});
app.put("/users/:id", (req, res) => {
const index = users.findIndex((u) => u.id === Number(req.params.id));
if (index === -1) return res.status(404).json({ error: "用户不存在" });
const { name, email } = req.body || {};
users[index] = { id: users[index].id, name, email };
res.json({ user: users[index] });
});
app.delete("/users/:id", (req, res) => {
const len = users.length;
users = users.filter((u) => u.id !== Number(req.params.id));
if (users.length === len) return res.status(404).json({ error: "用户不存在" });
res.json({ message: "删除成功" });
});
app.listen(3000, () => console.log("http://localhost:3000"));
用 Postman 或 curl 调用 GET /users、POST /users(body:name、email)、GET /users/1、PUT /users/1、DELETE /users/1 做验证。
8. 进阶
8.1 防盗链(Referer)
根据请求头 Referer 判断来源域名,仅放行白名单,避免外站直接引用静态资源消耗带宽。在提供静态资源的 Express 前增加中间件:
javascript
const express = require("express");
const app = express();
app.use((req, res, next) => {
const referer = req.get("referer");
if (referer) {
const url = new URL(referer);
if (url.hostname !== "localhost" && url.hostname !== "127.0.0.1") {
return res.status(403).end("<h1>禁止访问</h1>");
}
}
next();
});
app.use(express.static("public"));
app.listen(3000, () => console.log("http://localhost:3000"));
本机直接访问正常;在其他域页面用 <img src="http://localhost:3000/xxx"> 时,根据该页 Referer 可能返回 403。
8.2 路由模块化
按业务拆成独立 Router,主文件只做挂载。新建 routes/products.js:
javascript
const express = require("express");
const router = express.Router();
router.get("/", (req, res) => res.json({ list: ["商品A", "商品B"] }));
router.get("/:id", (req, res) => res.json({ id: req.params.id, name: "商品" + req.params.id }));
module.exports = router;
主文件:
javascript
const express = require("express");
const productRouter = require("./routes/products");
const app = express();
app.use("/api/products", productRouter);
app.listen(3000, () => console.log("http://localhost:3000"));
访问 /api/products、/api/products/1 验证。
8.3 文件上传(Formidable)
表单上传文件须设 enctype="multipart/form-data",body 非 JSON 也非简单表单,需用 formidable 等库解析,得到 fields 与 files。
npm i express formidable,新建 upload.html 与 upload-server.js。
upload.html:
html
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>上传</title></head>
<body>
<form action="http://localhost:3000/upload" method="post" enctype="multipart/form-data">
<input type="text" name="title" placeholder="标题" />
<input type="file" name="file" />
<button type="submit">提交</button>
</form>
</body>
</html>
upload-server.js:
javascript
const express = require("express");
const formidable = require("formidable");
const path = require("path");
const fs = require("fs");
const app = express();
app.get("/", (req, res) => res.sendFile(path.join(__dirname, "upload.html")));
app.post("/upload", (req, res, next) => {
const form = formidable({
uploadDir: path.join(__dirname, "uploads"),
keepExtensions: true,
});
form.parse(req, (err, fields, files) => {
if (err) {
next(err);
return;
}
console.log("fields:", fields, "files:", files);
res.json({ message: "上传成功", fields, files });
});
});
fs.mkdirSync(path.join(__dirname, "uploads"), { recursive: true });
app.listen(3000, () => console.log("http://localhost:3000"));
运行后打开 http://localhost:3000,选文件并提交,在控制台查看 fields、files 结构。