NodeJS 与 Express
0x01 Node.js 基础
(1)概述
-
Node.js 官网:https://nodejs.org
-
Node.js 是一个基于 V8 引擎的 Javascript 运行环境
-
特性:
- 完全使用 Javascript 语法
- 具有超强的并发能力,实现高性能服务器
- 开发周期短、开发成本低、学习成本低
-
Node.js 可以解析 Javascript 代码,提供很多系统级别的 API
-
文件读写
const fs = require('fs') fs.readFile('./text.txt', 'utf-8', (err, content) => { console.log(content) })
-
进程管理
function main(argv) { console.log(argv) } main(process.argv.slice(2))
-
网络通信
const http = require('http') http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }) res.write("Hello, Node.js") res.end() }).listen(3000)
-
(2)环境搭建
-
在官网下载最新版 Node.js 并安装
-
使用命令
node --version
确认安装是否成功 -
创建一个目录,其中新建 index.js
console.log("Hello, world");
-
在该目录下,使用命令
node .\index.js
查看运行结果
(3)模块化
a. CommonJS 规范
-
Node.js 支持模块化开发,采用 CommonJS 规范
-
CommonJS 包括:modules、packages、system、filesystems、binary、console、cncodings、sockets 等
-
模块化是将公共的功能抽离成为一个单独 Javascript 文件作为一个模块,模块可以通过暴露其中的属性或方法,从而让外部进行访问
-
customModule.js
const name = "John" const getName = () => { console.log(name) } // 暴露方法一 module.exports = { getName: getName, } // 暴露方法二 exports.getName = getName
-
index.js
const customModule = require('./customModule') customModule.getName()
-
b. ES 模块化写法
-
在根目录创建目录 module,其中新建 mod.js
const module = {}; export default module;
-
修改 index.js,其中使用 ES 写法引入 module/mod.js
import module from "./module/mod.js"; console.log(module);
此时运行 index.js 后会报错,可以通过修改配置文件解决
-
修改 package.json
{ // ... "type": "module" }
此时可以正常运行 index.js
-
修改 mod.js,实现多个暴露
const module1 = { get() { return "module1"; }, }; const module2 = { get() { return "module2"; }, }; export { module1, module2, };
-
修改 index.js
import { module1 } from "./module/mod.js"; import { module2 } from "./module/mod.js"; console.log(module1.get(), module2.get());
(4)npm & yarn
-
npm
npm init
:初始化新项目,会生成一个 package.json 文件,其中包含项目信息- 使用命令
npm install
时,依赖项版本号,如"^1.1.1"
,其中的特殊符号含义如下:^
:安装同名包~
:安装同版本,即安装 1.1.**
:安装最新版
- 使用命令
npm [install/uninstall/update] [package_name] -g
:全局安装(或卸载、更新)依赖npm [install/uninstall/update] [package_name] --save-dev
:局部安装(或卸载、更新)依赖npm install [package_name]@[version/latest]
:安装指定版本(或最新版本)的依赖npm list (-g)
:列举当前目录(或全局)依赖npm info [package_name] (version)
:查看指定依赖的详细信息(以及版本)npm outdated
:检查依赖是否过时
-
nrm
NRM(Npm Registry Manager)是 npm 的镜像原管理工具
- 使用命令
npm install -g nrm
全局安装 nrm - 使用命令
nrm ls
查看可用源,*
表示当前使用的源 - 使用命令
nrm use xxx
切换到 xxx 源 - 使用命令
nrm test
测试源的响应时间
- 使用命令
-
yarn
- 相比 npm,yarn 具有安装速度快、保证安装包完整安全
- 使用命令
npm install -g yarn
全局安装 yarn
yarn init
:初始化新项目yarn add [package_name](@version) (--dev)
:安装指定依赖yarn upgrade [package_name]@[version]
:升级指定依赖yarn remove [package_name]
:移除指定依赖yarn install
:安装项目所有依赖
(5)内置模块
使用命令
npm install -g nodemon
全局安装 nodemon,用于热更新
a. url
-
parse
方法:将 URL 进行语法分析成对象// 导入 url 模块 const url = require("url"); // 声明字符串 const urlString = "http://localhost:8000/home/index.html?id=1&name=John#page=10"; // 处理字符串并输出 console.log(url.parse(urlString));
-
format
方法:将对象格式化成 URLconst url = require("url"); // 声明对象 const urlObject = { protocol: "http:", slashes: true, auth: null, host: "localhost:8000", port: "8000", hostname: "localhost", hash: "#page=10", search: "?id=1&name=John", query: "id=1&name=John", pathname: "/home/index.html", path: "/home/index.html?id=1&name=John", }; console.log(url.format(urlObject));
-
resolve
方法:const url = require("url"); // 声明并处理字符串 let a = url.resolve("/api/v1/users", "id"); // /api/v1/id let b = url.resolve("http://localhost:8000/", "/api"); // http://localhost:8000/api let c = url.resolve("http://localhost:8000/api", "/v1"); // http://localhost:8000/v1 console.log(`${a}\n${b}\n${c}`);
b. querystring
-
parse
方法:// 导入 querystring 模块 const querystring = require("querystring"); // 声明、处理字符串并输出处理结果 console.log(querystring.parse("name=John&age=18"));
-
stringify
方法:const querystring = require("querystring"); console.log( querystring.stringify({ name: "John", age: 18, }) );
-
escape
/unescape
方法:将特殊字符转义const querystring = require("querystring"); console.log(querystring.escape("http://localhost:8000/name=张三&age=18"));
const querystring = require("querystring"); console.log(querystring.unescape("http%3A%2F%2Flocalhost%3A8000%2Fname%3D%E5%BC%A0%E4%B8%89%26age%3D18"));
c. http
I. 基础
-
index.js
// 导入 http 模块 const http = require("http"); // 导入自定义模块 const moduleRenderHTML = require("./module/renderHTML"); const moduleRenderStatus = require("./module/renderStatus"); // 创建本地服务器 const server = http.createServer(); // 开启本地服务器 server.on("request", (req, res) => { // req 浏览器请求, res 服务器响应 // 添加响应头 res.writeHead(moduleRenderStatus.renderStatus(req.url), { "Content-Type": "text/html;charset=utf-8", }); // 根据 URL 返回不同的内容 res.write(moduleRenderHTML.renderHTML(req.url)); // 结束响应 res.end( JSON.stringify({ data: "Hello, world!", }) ); }); // 运行并监听端口 server.listen(8000, () => { console.log("Server is running on port 8000"); });
-
module/renderHTML.js
function renderHTML(url) { switch (url) { case "/": return ` <html> <h1>Node.js</h1> <p>URL: root</p> </html> `; case "/api": return ` <html> <h1>Node.js</h1> <p>URL: api</p> </html>`; default: return ` <html> <h1>Node.js</h1> <p>URL: 404 Not Found</p> </html>`; } } module.exports = { renderHTML };
-
module/renderStatus.js
function renderStatus(url) { const routes = ["/", "/api"]; return routes.includes(url) ? 200 : 404; } exports.renderStatus = renderStatus;
-
使用命令
nodemon .\index.js
运行,并访问 http://localhost:8000/ 或 http://localhost:8000/api
II. cors
-
修改 index.js,设置服务端允许跨域请求
// 导入内置模块 const http = require("http"); const qs = require("querystring"); const url = require("url"); const server = http.createServer(); server.on("request", (req, res) => { let data = ""; let urlObject = url.parse(req.url, true); res.writeHead(200, { "Content-Type": "application/json;charset=utf-8", "Access-Control-Allow-Origin": "*", }); req.on("data", (chunk) => { data += chunk; }); req.on("end", () => { responseResult(qs.parse(data)); }); function responseResult(data) { switch (urlObject.pathname) { case "/api/login": res.end(JSON.stringify({ msg: data })); break; default: res.end(JSON.stringify({ msg: "error" })); } } }); server.listen(8000, () => { console.log("Server is running on port 8000"); });
-
修改 index.html,设置客户端请求跨域数据
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" /> <title>Document</title> </head> <body> <script> fetch("http://localhost:8000/api/login") .then((res) => res.json()) .then((res) => console.log(res.msg)); </script> </body> </html>
III. https.get
JSONP
const http = require("http");
const https = require("https");
const server = http.createServer();
server.on("request", (req, res) => {
res.writeHead(200, {
"Content-Type": "application/json; charset=utf-8",
"Access-Control-Allow-Origin": "*",
});
let data = "";
// 使用 JSONP 方法利用 GET 请求进行跨域
https.get(`https://www.baidu.com/sugrec?...&wd=John`, (res) => {
res.on("data", (chunk) => {
data += chunk;
});
res.on("end", () => {
console.log(data);
});
});
});
server.listen(8000, () => {
console.log("Server is running on port 8000");
});
IV. https.post
-
修改 index.js
const http = require("http"); const https = require("https"); const server = http.createServer(); server.on("request", (req, res) => { res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Access-Control-Allow-Origin": "*", }); // POST 请求 let data = ""; let request = https.request( { hostname: "m.xiaomiyoupin.com", port: "443", path: "/mtop/market/search/placeHolder", method: "POST", headers: { "Content-Type": "application/json", }, }, (response) => { response.on("data", (chunk) => { data += chunk; }); response.on("end", () => { res.end(data); }); } ); request.write(JSON.stringify([{}, { baseParam: { ypClient: 1 } }])); request.end(); }); server.listen(8000, () => { console.log("Server is running on port 8000"); });
-
修改 index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" /> <title>Document</title> </head> <body> <script> fetch("http://localhost:8000/") .then((res) => res.json()) .then((res) => console.log(res)); </script> </body> </html>
d. event
// 导入 events 模块
const EventEmitter = require("events");
// 创建自定义事件类并继承内置模块 events
class MyEventEmitter extends EventEmitter {}
// 声明实例
const event = new MyEventEmitter();
// 监控 output 事件并执行回调函数
event.on("output", (name) => {
console.log(name);
});
// 触发事件
event.emit("output", "John");
event.emit("output", "Mary");
e. fs
导入 fs 模块:
const fs = require("fs");
I. 文件夹操作
-
创建
fs.mkdir("./newFolder", (err) => { if (err && err.code === "EEXIST") { console.log("The folder already exists"); } else { console.log("The folder has been created"); } });
-
重命名
fs.renamedir("./newFolder", "./folder", (err) => { if (err && err.code === "ENOENT") { console.log("The folder does not exist"); } else { console.log("The folder renamed successfully"); } });
-
删除
fs.rmdir("./folder", (err) => { if (err && err.code === "ENOENT") { console.log("The folder does not exist"); } else if (err && err.code === "ENOTEMPTY") { console.log("The folder is not empty"); } else { console.log("The folder deleted successfully"); } });
-
内容查看
fs.readdir("./folder", (err, data) => { if (err) { console.log(err); } else { console.log(data); } });
-
属性查看
fs.stat("./folder", (err, data) => { console.log(data) // 判定是否为文件类型 console.log(data.isFile()) // 判定是否为文件夹类型 console.log(data.isDirectoty()) })
II. 文件操作
-
创建与覆写
fs.writeFile("./folder/text.txt", "Hello, world!", (err) => { console.log(err); });
-
续写
fs.appendFile("./folder/text.txt", "Hello, world!", (err) => { console.log(err); });
-
读取
fs.readFile("./folder/text.txt", (err, data) => { if (err) { console.log(err); } else { console.log(data.toString("utf-8")); } });
-
删除
fs.unlink("./folder/text.txt", (err) => { console.log(err); });
III. 同步操作
-
上述文件夹操作与文件操作均采用异步方式操作
-
fs 模块也提供了同步操作方法,但是如果发生错误则会阻塞程序进行
-
举例:同步读取文件
console.log("Before reading"); console.log("Content: " + fs.readFileSync("./folder/text.txt", "utf-8")); console.log("After reading");
此时,当 folder/text.txt 不存在时,程序会在第三行停止并退出,无法继续执行
f. stream
-
在 Node.js 中,stream 是一个对象,使用时只需响应 stream 的事件即可,以下是常见事件:
data
:stream 的数据可读,其中每次传递的chunk
是 stream 的部分数据end
:stream 的数据到结尾error
:出错
-
举例:使用 stream 的事件读取文件
const fs = require("fs"); let stream = fs.createReadStream("./folder/text.txt", "utf-8"); stream.on("data", (chunk) => { console.log("Data: ", chunk); }); stream.on("end", () => { console.log("End of file"); }); stream.on("error", (err) => { console.log("Error: ", err); });
-
pipe
方法可以将两个 stream 串起来 -
举例:文件复制
const fs = require("fs"); let readStream = fs.createReadStream("./folder/text.txt", "utf-8"); let writeStream = fs.createWriteStream("./folder/text-copy.txt", "utf-8"); readStream.pipe(writeStream);
g. zlib
-
用于将静态资源文件压缩,从服务器传到浏览器,减少带宽使用
-
举例:文件复制并压缩,进行对比
const fs = require("fs"); const zlib = require("zlib"); let readStream = fs.createReadStream("./folder/text.txt", "utf-8"); let writeStream = fs.createWriteStream("./folder/text-copy.txt", "utf-8"); let gzip = zlib.createGzip(); readStream.pipe(gzip).pipe(writeStream);
h. crypto
-
提供通用的加密和哈希算法
-
举例:
-
MD5/SHA-1
// 导入 crypto 模块 const crypto = require("crypto"); // 设置加密算法 const hash = crypto.createHash("md5"); // SHA-1 // const hash = crypto.createHash("sha1"); // 将字符以 UTF-8 编码传入 Buffer hash.update("hello world"); // 计算并输出 console.log(hash.digest("hex"));
-
HMAC
const crypto = require("crypto"); // 设置加密算法以及密钥 const hmac = crypto.createHash("sha256", "secret-key"); hmac.update("hello world"); console.log(hmac.digest("hex"));
-
AES
const crypto = require("crypto"); function encrypt(key, iv, data) { // key 是加密密钥(256位) | iv 是初始化向量(16字节) | data 是明文(二进制字符串) // 创建一个AES-256-CBC模式的加密器 const decipher = crypto.createCipheriv("aes-256-cbc", key, iv); // 使用加密器对数据进行加密,返回加密结果 let encrypted = decipher.update(data, "binary", "hex"); // 添加加密器的最终加密结果 encrypted += decipher.final("hex"); // 返回密文(十六进制字符串) return encrypted; } function decrypt(key, iv, crypted) { // key 是解密密钥(长度32的Buffer对象) | iv 是初始化向量(长度16的Buffer对象) | crypted 是密文(16进制字符串) // 将加密数据从16进制字符串转换为二进制字符串 crypted = Buffer.from(crypted, "hex").toString("binary"); // 创建解密器,使用AES-256-CBC模式 const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv); // 初始化解密过程,将解密结果转换为UTF-8字符串 let decrypted = decipher.update(crypted, "binary", "utf8"); // 结束解密过程,将剩余的加密数据解密,并合并到解密结果中 decrypted += decipher.final("utf8"); // 返回明文(UTF-8字符串) return decrypted; }
-
(6)路由
a. 基础路由
目录结构:
graph TB ./-->static & router.js & index.js static-->index.html & 404.html
-
编写页面
-
编写路由:router.js
const fs = require("fs"); const path = require("path"); // 定义路由规则 const rules = { "/": (req, res) => { render(res, path.join(__dirname, "/static/index.html")); }, "/404": (req, res) => { render(res, path.join(__dirname, "/static/404.html")); }, }; // 渲染路由页面 function render(res, path) { res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.write(fs.readFileSync(path, "utf8")); res.end(); } // 完成路由规则 function routes(req, res, pathname) { if (pathname in rules) { rules[pathname](req, res); } else { rules["/404"](req, res); } } module.exports = { routes, };
path.join(__dirname, "/static/404.html")
:获取静态资源方法
-
编写服务端:index.js
const http = require("http"); const router = require("./router"); const server = http.createServer(); server.on("request", (req, res) => { const myURL = new URL(req.url, "http://127.0.0.1"); router.routes(req, res, myURL.pathname); res.end(); }); server.listen(8000, () => { console.log("Server is running on port 8000"); });
b. 获取参数
I. GET
-
在页面中创建表单并发送请求
<body> <form> <lable>Username: <input type="text" name="username" /></lable> <lable>Password: <input type="password" name="password" /></lable> <input type="button" onclick="submit()" value="submit" /> </form> <script> let form = document.querySelector("form"); form.submit = () => { let username = form.elements.username.value; let password = form.elements.password.value; fetch(`http://localhost:8000/?username=${username}&password=${password}`) .then((res) => res.json()) .then((res) => console.log(res)); }; </script> </body>
-
修改 router.js
const fs = require("fs"); const path = require("path"); const rules = { "/": (req, res) => { const myURL = new URL(req.url, "http://127.0.0.1"); const params = myURL.searchParams; // 获取参数 render(res, `{ "username": ${params.get("username")}, "password": ${params.get("password")}}`); }, "/404": (req, res) => { const data = fs.readFileSync(path.join(__dirname, "/static/404.html"), "utf8"); render(res, data, "text/html"); }, }; function render(res, data, type) { res.writeHead(200, { "Content-Type": `${type ? type : "application/json"}; charset=utf-8`, "Access-Control-Allow-Origin": "*", // 允许跨域请求 }); res.write(data); res.end(); } function routes(req, res, pathname) { if (pathname in rules) { rules[pathname](req, res); } else { rules["/404"](req, res); } } module.exports = { routes, };
II. POST
-
调整请求方法
<script> let form = document.querySelector("form"); form.submit = () => { let username = form.elements.username.value; let password = form.elements.password.value; fetch(`http://localhost:8000/`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ username, password, }), }) .then((res) => res.json()) .then((res) => console.log(res)); }; </script>
-
修改 router.js
"/": (req, res) => { let data = "" req.on("data", (chunk) => { data += chunk; }) req.on("end", () => { render(res, `{ "data": ${data} }`); }) },
0x02 Express
(1)概述
a. 简介
- Express 官网链接:https://www.expressjs.com.cn/
- Express 是基于 Node.js 平台的 Web 开发框架
- 特点:
- 极简、灵活地创建各种 Web 和移动应用
- 具有丰富的 HTTP 实用工具和中间件来创建强大的 API
- 在 Node.js 基础上扩展 Web 应用所需的基本功能
b. 第一个项目
-
使用命令
npm install express --save
安装 Express -
在根目录下创建 index.js
// 导入 express 模块 const express = require("express"); // 创建一个 express 应用 const app = express(); // 设置应用监听的端口 const port = 3000; // 处理根路径的GET请求 app.get("/", (req, res) => res.send("Hello World!")); // 监听端口并输出日志 app.listen(port, () => console.log(`Example app listening on port ${port}!`));
-
使用命令
node .\index.js
运行项目
c. 路由
-
基础路由
const express = require("express"); const app = express(); app.get("/", (req, res) => res.send("Hello World!")); app.get("/html", (req, res) => res.send(`<h1>Hello World!</h1>`)); app.get("/json", (req, res) => res.send({ msg: "Hello World!" }));
-
字符串模式
-
字符可选
app.get("/ab?cd", (req, res) => res.send("Hello World!"));
-
参数捕获
app.get("/abcd/:id", (req, res) => res.send("Hello World!"));
-
重复字符
app.get("/ab+cd", (req, res) => res.send("Hello World!"));
-
任意字符
app.get("/ab*cd", (req, res) => res.send("Hello World!"));
-
-
正则表达式
-
含指定字符
app.get(/a/, (req, res) => res.send("Hello World!"));
-
指定字符(串)结尾
app.get(/.*a$/, (req, res) => res.send("Hello World!"));
-
-
回调函数
-
多个回调函数
app.get( "/", (req, res, next) => { console.log("Next"); next(); }, (req, res) => { res.send("Hello World!"); } );
-
回调函数数组
let callback_1 = function (req, res, next) { console.log("Callback 1"); next(); }; let callback_2 = function (req, res, next) { console.log("Callback 2"); next(); }; let callback_3 = function (req, res) { res.send("Hello World!"); }; app.get("/", [callback_1, callback_2, callback_3]);
-
(2)中间件
- Express 是由路由和中间件构成的 Web 开发框架,本质上,Express 应用在调用各种中间件
- 中间件(Middleware)是一个函数,功能包括:
- 执行逻辑
- 修改请求和响应对象
- 终结请求-响应循环
- 调用堆栈中下一个中间件
- 如果当前中间件未终结请求-响应循环,则需要通过
next()
方法将控制权传递到下一个中间件,否则请求会被挂起
a. 应用级中间件
-
一般绑定到
express()
上,使用use()
和METHOD()
const express = require("express"); const app = express(); const port = 3000; // 中间件 app.use((req, res, next) => { req.text = "Hello, world!"; next(); }); app.get("/", (req, res) => { res.send(req.text); }); app.listen(port, () => console.log(`Example app listening on port ${port}!`));
-
中间件的注册位置需要注意
app.get("/p1", (req, res) => { res.send(req.text); }); app.use((req, res, next) => { req.text = "Hello, world!"; next(); }); app.get("/p2", (req, res) => { res.send(req.text); });
-
可以指定某一路由使用该中间件
app.use("/p2", (req, res, next) => { req.text = "Hello, world!"; next(); }); app.get("/p1", (req, res) => { res.send(req.text); }); app.get("/p2", (req, res) => { res.send(req.text); });
b. 路由级中间件
-
功能与应用级中间件类似,绑定到
express.Router()
上const express = require("express"); const app = express(); const router = express.Router(); const port = 3000; router.use((req, res, next) => { req.text = "Hello, world!"; next(); }) router.get("/", (req, res) => { res.send(req.text); }); app.use("/", router); app.listen(port, () => console.log(`Example app listening on port ${port}!`));
c. 错误处理中间件
-
需要引入
err
参数app.get('/', (req, res) => { throw new Error("Error") }) app.use((err, req, res, next) => { console.error(err.stack); res.status(500).send('Something broke!'); });
e. 内置中间件
-
express.static
是 Express 唯一内置的中间件,负责在 Express 应用中提供托管静态资源app.use(express.static('public'));
f. 第三方中间件
-
安装相应的模块并在 Express 应用中加载
const app = express(); const router = express.Router(); const xxx = require('xxx'); // 应用级加载 app.use(xxx); // 路由级加载 router.use(xxx);
(3)获取参数
-
GET
app.get("/", (req, res) => { console.log(req.query); res.send(req.query); });
-
POST
// 内置用于解析 POST 参数 app.use(express.urlencoded({ extended: false })); app.post("/", (req, res) => { console.log(req.body); res.send(req.body); });
- 使用 Postman 向 http://localhost:3000/ 发送 POST 请求,参数配置在 Body 中,使用 x-www-form-urlencoded
(4)服务端渲染
-
服务端渲染(SSR):前端发送请求,服务器端从数据库中拿出数据,通过渲染函数,把数据渲染在模板(ejs)里,产生了HTML代码,之后把渲染结果发给了前端,整个过程只有一次交互
- 客户端渲染(BSR/CSR):在服务端放了一个 HTML 页面,客户端发起请求,服务端把页面发送过去,客户端从上到下依次解析,如果在解析的过程中,发现 Ajax 请求,再次向服务器发送新的请求,客户端拿到 Ajax 响应的结果并渲染在页面上,这个过程中至少和服务端交互了两次
SSR 与 BSR 说明参考:《客户端渲染(BSR:Browser Side Render)、服务端渲染(SSR:Server Side Render)、搜索引擎优化、SEO(Search Engine Optimization) | CSDN-Ensoleile 2021》
-
设置使用 Express 渲染模板文件
-
使用命令
npm install ejs
安装模板引擎 -
在根目录创建目录 views
- views 中包含模板文件,如 index.ejs 等
-
修改 index.js
app.set("views", "./views"); app.set("view engine", "ejs");
-
修改
app.set("view engine", "ejs");
,使其可以直接渲染 HTML 文件app.set("view engine", "html"); app.engine("html", require("ejs").renderFile);
-
-
-
标签语法
<% %>
:流程控制标签<%= %>
:输出 HTML 标签<%- %>
:输出解析 HTML 标签<%# %>
:注释标签<%- include('/', {key: value}) %>
:导入公共模板
(5)生成器
-
使用命令
npm install -g express-generatore
全局安装 Express 生成器 -
使用命令
express myapp --view=ejs
生成 Express 项目- 上述命令不可用时,可以使用命令
npx express-generator --view=ejs myapp
- 上述命令不可用时,可以使用命令
-
使用命令
cd myapp
进入项目目录 -
使用命令
npm install
安装相关依赖 -
修改 package.json,使用热更新
{ "scripts": { "start": "nodemon ./bin/www" }, }
-
使用命令
npm start
启动项目
0x03 MongoDB
(1)概述
详见 《MongoDB | 博客园-SRIGT》
(2)使用 Node.js 操作
-
在根目录新建 config 目录,其中新建 db.config.js,用于连接数据库
const mongoose = require("mongoose"); mongoose.connect("mongodb://127.0.0.1:27017/node_project");
-
在 bin/www 中导入 db.config.js,用于导入数据库配置
require("../config/db.config");
-
在根目录新建 model 目录,其中新建 UserModel.js,用于创建 User 模型
const mongoose = require("mongoose"); const UserType = { name: String, username: String, password: String, }; const UserModel = mongoose.model("user", new mongoose.Schema(UserType)); module.exports = UserModel;
-
修改 routes/users.js,增加数据
const UserModel = require("../model/UserModel"); router.get("/", function (req, res, next) { const { name, username, password } = req.body; UserModel.create({ name, username, password }).then((data) => { console.log(data); }); res.send("respond with a resource"); });
-
查询数据
UserModel.find( { name: "John" }, ["username", "password"].sort({ _id: -1 }).skip(10).limit(10) );
-
更新数据(单条)
const { name, username, password } = req.body; UserModel.updateOne({ _id }, {username, password});
-
删除数据(单条)
UserModel.deleteOne({ _id });
0x04 接口规范与业务分层
(1)接口规范
-
采用 RESTful 接口规范
-
REST 风格 API 举例:
GET http://localhost:8080/api/user (查询用户) POST http://localhost:8080/api/user (新增用户) PUT http://localhost:8080/api/user/{id} (更新用户) DELETE http://localhost:8080/api/user (删除用户)
-
使用通用字段实现信息过滤
?limit=10
:指定返回记录的数量?offset=10
:指定返回记录的开始位置?page=1&per_page=10
:指定第几页以及每页的记录数?sortby=id&order=asc
:指定排序字段以及排序方式?state=close
:指定筛选条件
(2)业务分层
-
采用 MVC 架构
graph TB index.js-->router.js-->controller-->view & model- index.js 是服务器入口文件,负责接收客户端请求
- router.js 是路由,负责将入口文件的请求分发给控制层
- controller 是控制器(C),负责处理业务逻辑
- view 是视图(V),负责页面渲染与展示
- model 是模型(M),负责数据的增删改查
(3)实际应用
-
修改 myapp 项目
目录结构:
graph TB myapp-->config & controller & model & routes & services & ... config-->db.config.js controller-->UserController.js model-->UserModel.js routes-->index.js & users.js services-->UserService.js-
UserModel.js
const mongoose = require("mongoose"); const UserType = { name: String, username: String, password: String, }; const UserModel = mongoose.model("user", new mongoose.Schema(UserType)); module.exports = UserModel;
-
UserService.js
const UserModel = require("../models/UserModel"); const UserService = { addUser: (name, username, password) => { return UserModel.create({ name, username, password }).then((data) => { console.log(data); }); }, updateUser: (name, username, password) => { return UserModel.updateOne( { _id: req.params.id }, { name, username, password } ); }, deleteUser: (_id) => { return UserModel.deleteOne({ _id }); }, getUser: (page, limit) => { return UserModel.find({}, ["name", "username"]) .sort({ _id: -1 }) .skip((page - 1) * limit) .limit(limit); }, }; module.exports = UserService;
-
UserController.js
const UserController = { addUser: async (req, res) => { const { name, username, password } = req.body; await UserService.addUser(name, username, password); res.send({ message: "User created successfully", }); }, updateUser: async (req, res) => { const { name, username, password } = req.body; await UserService.updateUser(req.params.id, name, username, password); res.send({ message: "User updated successfully", }); }, deleteUser: async (req, res) => { await UserService.deleteUser(req.params.id); res.send({ message: "User deleted successfully", }); }, getUsers: async (req, res) => { const { page, limit } = req.query; const users = await UserService.getUsers(page, limit); res.send(users); }, }; module.exports = UserController;
-
users.js
router.post("/user", UserController.addUser); router.put("/user/:id", UserController.updateUser); router.delete("/user/:id", UserController.deleteUser); router.get("/user", UserController.getUsers);
-
0x05 登录鉴权
(1)Cookie 与 Session
-
时序图
sequenceDiagram 浏览器->>服务端:POST账号密码 服务端->>库(User):校验账号密码 库(User)-->>服务端:校验成功 服务端->>库(Session):存Session 服务端-->>浏览器:Set-Cookie:sessionId 浏览器->>服务端:请求接口(Cookie:sessionId) 服务端->>库(Session):查Session 库(Session)-->>服务端:校验成功 服务端->>服务端:接口处理 服务端-->>浏览器:接口返回
-
修改 app.js,其中引入 Session 与 Cookie 的配置
// 引入express-session中间件和connect-mongo存储模块 const session = require("express-session"); const MongoStore = require("connect-mongo"); // 配置session app.use( session({ secret: "secret-key", // session加密密钥 resave: true, // 是否在每次请求时重新保存session,即使它没有变化 saveUninitialized: true, // 是否保存未初始化的session(即仅设置了cookie但未设置session数据的情况) cookie: { maxAge: 1000 * 60 * 10, // cookie的过期时间(毫秒) secure: false, // 是否仅在https下发送cookie }, rolling: true, // 是否在每次请求时重置cookie的过期时间 store: MongoStore.create({ mongoUrl: "mongodb://127.0.0.1:27017/node_project_session", ttl: 1000 * 60 * 10, // session在数据库中的存活时间(毫秒) }), }) );
-
修改 UserController.js,其中设置 Session 对象
const UserController = { // ... login: async (req, res) => { const { username, password } = req.body; const data = await UserService.login(username, password); if (data.length === 0) { res.send({ message: "Invalid username or password", }); } else { req.session.user = data[0]; // 设置 Session 对象 res.send({ message: "Login successful", data, }); } } }; module.exports = UserController;
-
修改 routes/index.js,其中判断 Session
router.get("/", function (req, res, next) { if (req.session.user) { res.render("index", { title: "Express" }); } else { res.render("/login"); } });
此时,如果 Session 保存在内存中,当服务器重启后,Session 就会丢失;而保存在数据库中就不会丢失
-
修改 app.js,通过应用级中间件,将 Session 校验配置到全局
app.use((req, res, next) => { if (req.url.includes("login")) { next(); return; } if (req.session.user) { next(); } else { req.url.includes("api") ? res.send({ code: 401, message: "Please login first" }) : res.redirect("/login"); } });
-
修改 routes/users.js
router.post("/login", UserController.login); router.get("/logout", (req, res) => { req.session.destroy(() => { res.send({ message: "Logout successful", }); }); });
(2)JWT
a. 概述
-
JWT 全称 JSON Web Token
-
优势:
- 不需要保存 Session ID,节约存储空间
- 具有加密签名,校验结果高效准确
- 预防 CSRF 攻击
-
缺点:
- 带宽占用多,开销大
- 无法在服务端注销,有被劫持的问题
- 性能消耗大,不利于性能要求严格的 Web 应用
b. 实现
仅限于前后端分离项目使用
-
使用命令
npm install -S jsonwebtoken
安装 JWT -
根目录下新建 util 目录,其中新建 jwt.js,用于封装 JWT
const jsonwebtoken = require("jsonwebtoken"); const secret = "secret-key"; const JWT = { generate(value, exprires) { return jsonwebtoken.sign(value, secret, { expriresIn: exprires }); }, verify(token) { try { return jsonwebtoken.verify(token, secret); } catch (err) { return false; } }, }; module.exports = JWT;
-
修改 UserController.js,设置 JWT 对象
const JWT = require("../util/jwt"); const UserController = { // ... login: async (req, res) => { // ... if (data.length === 0) { res.send({ message: "Invalid username or password", }); } else { const token = JWT.generate( { _id: data._id, username: data[0].username, }, "1h" ); // 设置 JWT 对象 res.header("Authorization", token); // 将 token 放入响应头 // ... } }, }; module.exports = UserController;
-
在前端使用 Axios 的拦截器,将 token 保存在浏览器本地存储
-
修改 app.js,使用中间件校验
app.use((req, res, next) => { if (req.url.includes("login")) { next(); return; } const token = req.headers["authorization"]?.split(" ")[1]; if (token) { const payload = JWT.verify(token); if (payload) { // token 重新计时 const newToken = JWT.generate( { _id: payload._id, username: payload.username, }, "1h" ); res.headers("Authorization", newToken); next(); } else { res.send({ code: 401, message: "Token has expired" }); } } else { next(); } });
0x06 apiDoc
-
apiDoc 是一个简单的 RESTful API 文档生成工具
- 通过代码注释提取特定格式的内容生成文档
- 支持 Go、Java、C++、Rust 等大部分开发语言,可通过命令
apidoc lang
查看所有支持的列表
-
特点:
- 跨平台支持
- 多编程语言兼容
- 输出模板自定义
- 根据文档生成 mock 数据
-
使用命令
npm install -g apidoc
全局安装 apiDoc -
apiDoc 注释格式:
/** * @api {get} /user/:id Request User information * @apiName GetUser * @apiGroup User * * @apiParam {Number} id Users unique ID. * * @apiSuccess {String} firstname Firstname of the User. * @apiSuccess {String} lastname Lastname of the User. */
-
修改 routes\users.js
// ... /** * * @api {post} /api/user user * @apiName addUser * @apiGroup user * @apiVersion 1.0.0 * * * @apiParam {String} name 姓名 * @apiParam {String} username 用户名 * @apiParam {String} password 密码 * * @apiSuccess (200) {String} message 描述 * * @apiParamExample {application/json} Request-Example: * { * name : "张三", * username : "法外狂徒", * password : "333" * } * * * @apiSuccessExample {application/json} Success-Response: * { * message : "User created successfully" * } * * */ router.post("/user", UserController.addUser); // ...
-
-
使用命令
apidoc -i src/ -o doc/
从 src 目录生成 API 文档到 doc 目录- 在项目根目录下,使用命令
apidoc -i .\routes\ -o .\doc
- 在项目根目录下,使用命令
-
在 VSCode 中,使用 ApiDoc Snippets 插件可以辅助生成相应的注释
-End-