【NodeJS】从入门到进阶

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/loginhttp://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.htmlhttp://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.1 区别与适用场景

对比项 Cookie Session
存储位置 浏览器,可被查看与修改 服务端,仅通过 Cookie 传递 sessionId
容量与安全 容量小,不宜存敏感信息 可存较多数据,适合登录态
典型依赖 cookie-parser express-session

Session 依赖 Cookie 存 sessionId,二者常配合使用。

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.htmlejs-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.jsnpm 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 等库解析,得到 fieldsfiles

npm i express formidable,新建 upload.htmlupload-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,选文件并提交,在控制台查看 fieldsfiles 结构。

相关推荐
张3蜂2 小时前
Node.js 安装与配置完全指南:从零开始搭建开发环境
node.js
礼拜天没时间.1 天前
Node.js运维部署实战:从0到1开始搭建Node.js运行环境
linux·运维·后端·centos·node.js·sre
等什么君!1 天前
如何正确使用nvm工具管理 node.js
node.js
受打击无法动弹2 天前
Window 10部署openclaw报错node.exe : npm error code 128
npm·node.js·openclaw
全马必破三3 天前
Webpack知识点汇总
前端·webpack·node.js
NEXT063 天前
CommonJS 与 ES Modules的区别
前端·面试·node.js
YuMiao3 天前
把 WebSocket 服务迁移到 Cloudflare Durable Objects —— 以一次协同编辑实战为例
javascript·node.js
belldeep4 天前
nodejs:如何使用 express markdown-it 实现指定目录下 Md 文件的渲染
node.js·express·markdown
未名编程4 天前
Linux / macOS / Windows 一条命令安装 Node.js + npm(极限一行版大全)
linux·macos·node.js