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)环境搭建

  1. 官网下载最新版 Node.js 并安装

  2. 使用命令 node --version 确认安装是否成功

  3. 创建一个目录,其中新建 index.js

    console.log("Hello, world");
    
  4. 在该目录下,使用命令 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 模块化写法

  1. 在根目录创建目录 module,其中新建 mod.js

    const module = {};
    
    export default module;
    
  2. 修改 index.js,其中使用 ES 写法引入 module/mod.js

    import module from "./module/mod.js";
    
    console.log(module);
    

    此时运行 index.js 后会报错,可以通过修改配置文件解决

  3. 修改 package.json

    {
      // ...
      "type": "module"
    }
    

    此时可以正常运行 index.js

  4. 修改 mod.js,实现多个暴露

    const module1 = {
      get() {
        return "module1";
      },
    };
    const module2 = {
      get() {
        return "module2";
      },
    };
    
    export {
      module1,
      module2,
    };
    
  5. 修改 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 的镜像原管理工具

    1. 使用命令 npm install -g nrm 全局安装 nrm
    2. 使用命令 nrm ls 查看可用源,* 表示当前使用的源
    3. 使用命令 nrm use xxx 切换到 xxx 源
    4. 使用命令 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 方法:将对象格式化成 URL

    const 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. 基础

  1. 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");
    });
    
  2. 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 };
    
  3. module/renderStatus.js

    function renderStatus(url) {
      const routes = ["/", "/api"];
      return routes.includes(url) ? 200 : 404;
    }
    
    exports.renderStatus = renderStatus;
    
  4. 使用命令 nodemon .\index.js 运行,并访问 http://localhost:8000/http://localhost:8000/api

II. cors

  1. 修改 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");
    });
    
  2. 修改 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

  1. 修改 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");
    });
    
  2. 修改 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. 文件夹操作

  1. 创建

    fs.mkdir("./newFolder", (err) => {
      if (err && err.code === "EEXIST") {
        console.log("The folder already exists");
      } else {
        console.log("The folder has been created");
      }
    });
    
  2. 重命名

    fs.renamedir("./newFolder", "./folder", (err) => {
      if (err && err.code === "ENOENT") {
        console.log("The folder does not exist");
      } else {
        console.log("The folder renamed successfully");
      }
    });
    
  3. 删除

    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");
      }
    });
    
  4. 内容查看

    fs.readdir("./folder", (err, data) => {
      if (err) {
        console.log(err);
      } else {
        console.log(data);
      }
    });
    
  5. 属性查看

    fs.stat("./folder", (err, data) => {
      console.log(data)
    
      // 判定是否为文件类型
      console.log(data.isFile())
    
      // 判定是否为文件夹类型
      console.log(data.isDirectoty())
    })
    

II. 文件操作

  1. 创建与覆写

    fs.writeFile("./folder/text.txt", "Hello, world!", (err) => {
      console.log(err);
    });
    
  2. 续写

    fs.appendFile("./folder/text.txt", "Hello, world!", (err) => {
      console.log(err);
    });
    
  3. 读取

    fs.readFile("./folder/text.txt", (err, data) => {
      if (err) {
        console.log(err);
      } else {
        console.log(data.toString("utf-8"));
      }
    });
    
  4. 删除

    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
  1. 编写页面

  2. 编写路由: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"):获取静态资源方法
  3. 编写服务端: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

  1. 在页面中创建表单并发送请求

    <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>
    
  2. 修改 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

  1. 调整请求方法

    <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>
    
  2. 修改 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. 第一个项目

  1. 使用命令 npm install express --save 安装 Express

  2. 在根目录下创建 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}!`));
    
  3. 使用命令 node .\index.js 运行项目

  4. 访问 http://localhost:3000/

c. 路由

(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)获取参数

(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 渲染模板文件

    1. 使用命令 npm install ejs 安装模板引擎

    2. 在根目录创建目录 views

      • views 中包含模板文件,如 index.ejs 等
    3. 修改 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)生成器

  1. 使用命令 npm install -g express-generatore 全局安装 Express 生成器

  2. 使用命令 express myapp --view=ejs 生成 Express 项目

    • 上述命令不可用时,可以使用命令 npx express-generator --view=ejs myapp
  3. 使用命令 cd myapp 进入项目目录

  4. 使用命令 npm install 安装相关依赖

  5. 修改 package.json,使用热更新

    {
      "scripts": {
        "start": "nodemon ./bin/www"
      },
    }
    
  6. 使用命令 npm start 启动项目

  7. 访问 http://localhost:3000/

0x03 MongoDB

(1)概述

详见 《MongoDB | 博客园-SRIGT

(2)使用 Node.js 操作

  1. 在根目录新建 config 目录,其中新建 db.config.js,用于连接数据库

    const mongoose = require("mongoose");
    
    mongoose.connect("mongodb://127.0.0.1:27017/node_project");
    
  2. 在 bin/www 中导入 db.config.js,用于导入数据库配置

    require("../config/db.config");
    
  3. 在根目录新建 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;
    
  4. 修改 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");
    });
    
  5. 查询数据

    UserModel.find(
      { name: "John" },
      ["username", "password"].sort({ _id: -1 }).skip(10).limit(10)
    );
    
  6. 更新数据(单条)

    const { name, username, password } = req.body;
    UserModel.updateOne({ _id }, {username, password});
    
  7. 删除数据(单条)

    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
    1. 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;
      
    2. 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;
      
    3. 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;
      
    4. 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)-->>服务端:校验成功 服务端->>服务端:接口处理 服务端-->>浏览器:接口返回
  1. 修改 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在数据库中的存活时间(毫秒)
        }),
      })
    );
    
  2. 修改 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;
    
  3. 修改 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 就会丢失;而保存在数据库中就不会丢失

  4. 修改 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");
      }
    });
    
  5. 修改 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. 实现

仅限于前后端分离项目使用

  1. 使用命令 npm install -S jsonwebtoken 安装 JWT

  2. 根目录下新建 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;
    
  3. 修改 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;
    
  4. 在前端使用 Axios 的拦截器,将 token 保存在浏览器本地存储

  5. 修改 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-

posted @ 2024-06-20 00:41  SRIGT  阅读(31)  评论(0编辑  收藏  举报