Node.js

  • Node.js是一个开源的,跨平台的 JavaScript 运行环境。
  • Node.js是一个事件驱动I/O服务端JavaScript环境,基于Google的V8引擎,V8引擎执行Javascript的速度非常快,性能非常好。
  • Reference: 尚硅谷-NodeJS
  • Website: Node.js

1 NodeJS入门

1.1 简介

Node.js® is an open-source, cross-platform JavaScript runtime environment.

  • Node.js是一个开源的,跨平台的 JavaScript 运行环境。

1.2 作用

  • 开发服务器应用
  • 开发工具类应用
    • webpack、Vite、Babel
  • 开发桌面端应用
    • electron
      • VSCode、Figma、Postman

1.3 安装

  • 官网安装:Node.js

  • 安装验证

    • ```sh
      $ node -v
      $ npm -v # 新版的nodejs已经集成了npm
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11

      - ![image-20230726215614615](https://s2.loli.net/2023/09/29/RW79xjSgU4lq2Nm.png)

      > - Nodejs是一门计算机语言,运行在系统中的Chrome v8(jvm)引擎中。文件后缀是 `js` ;
      > - 运行的命令是:node ( js 是解释型语言,不需要编译,直接执行,所以不像 java 需要 java 和 javac)

      - 修改安装位置

      ```sh
      $ npm config set prefix "D:\Coding\Java\nodejs\npm_global"
      $ npm config set cache "D:\Coding\Java\nodejs\npm_cache"
  • 配置环境变量

    • NODE_PATH = D:\Coding\Java\nodejs\npm_global
  • npm root -g / npm config ls
    • image-20230727184140267

1.4 简单使用

1.4.1 命令行使用

image-20230929155252347

  • Node.js中不能使用BOM和DOM的API

1.4.2 API注意点

  • Web JS语法

    image-20230929155508741

  • NodeJS API

    image-20230929155533028

  • console定时器 是通用的

  • Node.js中的顶级对象为global,也可以用globalThis访问顶级对象

1.5 Buffer

1.5.1 概念

  • Buffer是一个类似于数组的对象,用于表示固定长度的字节序列
  • Buffer:本质是一段内存空间,专门用来处理二进制数据。

1.5.2 特点

  1. Buffer:大小固定且无法调整
  2. Buffer性能较好,可以直接对计算机内存进行操作
  3. 每个元素的大小为1字节(byte)

1.5.3 使用

1)创建

  • Buffer.alloc

    1
    2
    // 创建了一个长度为 10 字节的 Buffer,相当于申请了 10 字节的内存空间,每个字节的值为 0
    let buf_1 = Buffer.alloc(10); // 结果为 <Buffer 00 00 00 00 00 00 00 00 00 00>
  • Buffer.allocUnsafe

    1
    2
    3
    4
    // 创建了一个长度为 10 字节的 Buffer
    // buffer 中可能存在旧的数据(内存空间是可以复用的), 可能会影响执行结果,所以叫unsafe
    let buf_2 = Buffer.allocUnsafe(10);
    // 用这种方式创建速度比alloc快很多。
  • Buffer.from

    1
    2
    3
    4
    // 通过字符串创建 Buffer
    let buf_3 = Buffer.from('hello');
    // 通过数组创建 Buffer
    let buf_4 = Buffer.from([105, 108, 111, 118, 101, 121, 111, 117]);

2)Buffer 与字符串的转化

  • 我们可以借助 toString 方法将 Buffer 转为字符串。

  • toString 默认是按照 utf-8 编码方式进行转换的。

    1
    2
    let buf_4 = Buffer.from([105, 108, 111, 118, 101, 121, 111, 117]);
    console.log(buf_4.toString())

3)Buffer的读写

  • Buffer 可以直接通过 [] 的方式对数据进行处理。

    1
    2
    3
    4
    5
    6
    //读取
    console.log(buf_3[1]);
    //修改
    buf_3[1] = 97;
    //查看字符串结果
    console.log(buf_3.toString());
  1. 如果修改的数值超过 255 ,则超过 8 位数据会被舍弃。
  2. 一个utf-8 的字符一般占 3 个字节。

2 fs模块

  • fsfile system,文件系统。
  • fs模块可以实现与硬盘的交互,例如文件的创建、删除、重命名、移动、内容的写入读取以及文件夹的相关操作

2.1 文件写入

方法 说明
writeFile 异步写入
writeFileSync 同步写入
appendFile 异步追加
appendFileSync 同步追加
createWriteStream 流式写入=

2.1.1 writeFile 异步写入

  • 语法fs.writeFile(file,data[,options],callback)

  • 参数说明

    • file文件名
    • data 待写入的数据
    • options选项设置(可选)
      • {flag: 'a'}: 追加写入
    • callback写入回调
  • 返回值undefined

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 1. 导入 fs 模块
    const fs = require("fs");

    // 2. 调用 fs.writeFile() 方法写入文件
    // fs.writeFile(file, data[, options], callback)
    // fiel: 要写入的文件路径
    // data: 要写入的数据
    // options: 选项,可以对写入进行一些设置
    // callback: 回调函数,写入完成后执行

    fs.writeFile("./test.txt", "Hello world!", (err) => {
    if (err) {
    console.log("写入失败!");
    } else {
    console.log("写入成功!");
    }
    });
![image-20230929161007482](https://s2.loli.net/2023/09/29/HXqiYjKrkOCsPox.png)

  • 异步:如下,程序执行完毕! 与文件写入同时执行,谁先运行完谁先输出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const fs = require("fs");

    fs.writeFile("./test.txt", "Hello world!", (err) => {
    if (err) {
    console.log("写入失败!");
    } else {
    console.log("写入成功!");
    }
    });

    console.log("程序执行完毕1!");

    setTimeout(() => {
    console.log("程序执行完毕2!");
    }, 2000);
![image-20230929161304653](https://s2.loli.net/2023/09/29/yeR51cZwBrfuMYq.png)

2.1.2 writeFileSync 同步写入

  • 语法: fs.writeFileSync(file, data[, options])

  • 参数与 fs.writeFile 大体一致,只是==没有 callback 参数==

  • 返回值undefined

  • 同步:就是一条道,我走完你再走,从上到下顺序执行。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const fs = require("fs");

    try {
    fs.writeFileSync("test.txt", "Hello World!");
    console.log("文件写入成功!");
    } catch (e) {
    console.log(e);
    }

    console.log("后续代码...");
![image-20230929161545836](https://s2.loli.net/2023/09/29/pShry4MAsj8RlVg.png)

2.1.3 appendFile / appendFileSync 追加写入

  • appendFile 作用是在文件尾部追加内容,appendFile 语法与 writeFile 语法完全相同。

  • 语法:

    • fs.appendFile(file, data[, options], callback)
    • fs.appendFileSync(file, data[, options])
  • 返回值: 二者都为 undefined

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const fs = require("fs");

    fs.appendFile("./test.txt", "111", function (err) {
    if (err) {
    console.log("写入成功!");
    return;
    } else {
    console.log("写入失败!");
    }
    });

    console.log("后续代码...");

2.1.4 createWriteStream 流式写入

  • 语法fs.createWriteStream(path[, options])
  • 参数说明
    • path:文件路径
    • options:选项配置( 可选 )
      • {flags: 'a'} 追加写入
  • 返回值Object

    const fs = require("fs");
    
    const ws = fs.createWriteStream("./test.txt", { flags: 'a'});
    
    if (ws.writable) {
      ws.write("11111111111111");
      if (ws.write("写入文件内容")) {
        console.log("写入成功!");
      }
    }
    
    ws.close();
    
![image-20230929162425681](https://s2.loli.net/2023/09/29/Q7hWp6VxHwbvq2O.png)

程序打开一个文件是需要消耗资源的 ,流式写入可以减少打开关闭文件的次数

流式写入方式适用于大文件写入或者频繁写入的场景, writeFile 适合于写入频率较低的场景。

2.2 文件读取

方法 说明
readFile 异步读取
readFileSync 同步读取
createReadStream 流式读取

2.2.1 readFile 异步读取

  • 语法fs.readFile(path[, options], callback)

  • 参数说明

    • path:文件路径
    • options:选项配置
    • callback:回调函数
  • 返回值undefined

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const fs = require("fs");

    fs.readFile("./test.txt", "utf8", (err, data) => {
    if (err) {
    console.log("读取文件失败!");
    return;
    }
    console.log(data);
    });

    image-20230929162905230

2.2.2 readFileSync 同步读取

  • 语法fs.readFileSync(path[, options])

  • 参数说明

    • path:文件路径
    • options:选项配置
  • 返回值string | Buffer

    1
    2
    3
    4
    const fs = require("fs");

    const data = fs.readFileSync("./test.txt", "utf8");
    console.log(data.toString());

image-20230929163008471

2.2.3 createReadStream 流式读取

  • 语法fs.createReadStream(path[, options])

  • 参数说明

    • path:文件路径
    • options:选项配置( 可选 )
  • 返回值Object

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const fs = require('fs');

    const rs = fs.createReadStream("./test.txt");

    rs.on('data', chunk => {
    console.log(chunk.length); // 65536 字节 => 64KB,每次读取64KB的数据
    });

    rs.on('end', () => {
    console.log('读取完成');
    });

2.2.4 fs文件练习——文件复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const fs = require('fs');

//方式一 readFile
let data = fs.readFileSync('./test.txt');
fs.writeFileSync('./test2.txt', data);

//方式二 流式操作
const rs = fs.createReadStream('./test.txt');
const ws = fs.createWriteStream('./test3.txt');

//绑定 data 事件
rs.on('data', chunk => {
ws.write(chunk);
});

image-20230929163827628


  • 查看内存占用量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const fs = require("fs");
const process = require("process");

let data = fs.readFileSync("./test.txt", "utf8");
fs.writeFileSync("./test2.txt", data);
console.log("同步文件复制完成!");
console.log(process.memoryUsage());

const rs = fs.createReadStream("./test.txt");
const ws = fs.createWriteStream("./test3.txt");

rs.on("data", (chunk) => {
ws.write(chunk);
});
rs.on("end", () => {
console.log("异步文件复制完成!");
console.log(process.memoryUsage());
});


// 也可以借助管道完成读取
// rs.pipe(ws);

image-20230929164550651

  • 流式处理所占用的内存比直接读取更大是因为流式处理需要在处理数据时逐行或逐块读取数据,并将其存储在内存中以进行后续处理。这意味着在读取数据时,需要维护一个缓冲区来存储数据,因此在处理大量数据时,内存的使用率可能比直接读取更高。
  • 另外,流式处理还需要在处理后及时释放内存,否则可能会导致内存泄漏和程序崩溃等问题。因此,在设计流式处理程序时需要特别注意内存的使用和释放问题,以确保程序的稳定性和可靠性。

2.3 文件移动与重命名

方法 说明
rename 异步重命名/移动
renameSync 同步重命名/移动
  • 在 Node.js 中,我们可以使用 renamerenameSync 来移动或重命名 文件或文件夹
  • 语法
    • fs.rename(oldPath, newPath, callback)
    • fs.renameSync(oldPath, newPath)
  • 参数说明

    • oldPath:文件当前的路径
    • newPath:文件新的路径
    • callback:操作后的回调
1
2
3
4
5
6
7
8
9
10
const fs = require("fs");

fs.rename("./test.txt", "./newTest.txt", (err) => {
if (err) {
console.log("操作失败!");
return;
} else {
console.log("操作成功!");
}
});

2.4 文件删除

方法 说明
unlink / rm 异步删除
unlinkSync / rmSync 同步删除
  • 在 Node.js 中,我们可以使用unlinkunlinkSyncrmrmSync来删除文件。
  • 语法
    • fs.unlink(path, callback)
    • fs.unlinkSync(path)
    • fs.rm(path, callback)
    • fs.rmSync(path)
  • 参数说明

    • path:文件路径
    • callback:操作后的回调

2.5 文件夹操作

方法 说明
mkdir / mkdirSync 创建文件夹
readdir / readdirSync 读取文件夹
rmdir / rmdirSync 删除文件夹

1)mkdir 创建文件夹

  • 在 Node.js 中,我们可以使用 mkdirmkdirSync来创建文件夹
  • 语法
    • fs.mkdir(path[, options], callback)
    • fs.mkdirSync(path[, options])
  • 参数说明
    • path:文件夹路径
    • options:选项配置( 可选 )
      • recursive: true 递归创建
    • callback:操作后的回调
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const fs = require("fs");

console.log("当前文件的绝对路径:", __dirname);

// 1. 简单创建
fs.mkdir("./test", (err) => {
if (err) {
console.log("创建失败!");
return;
} else {
console.log("创建成功!");
}
});

// 2. 递归创建
fs.mkdir("./test/test1/test2", { recursive: true }, (err) => {
if (err) {
console.log("创建失败!");
return;
} else {
console.log("创建成功!");
}
});

2)readdir 读取文件夹

  • 在 Node.js 中,我们可以使用 readdirreaddirSyn 来创建文件夹
  • 语法
    • fs.readdir(path[, options], callback)
    • fs.readdirSync(path[, options])
  • 参数说明
    • path:文件夹路径
    • options:选项配置( 可选 )
    • callback:操作后的回调
1
2
3
4
5
6
7
8
fs.readdir("./", (err, files) => {
if (err) {
console.log("读取失败!");
return;
} else {
console.log(files);
}
});

image-20230929165852648

3)rmdir 删除文件夹

  • 在 Node.js 中,我们可以使用 rmdirrmdirSync来创建文件夹
  • 语法
    • fs.rmdir(path[, options], callback)
    • fs.rmdirSync(path[, options])
  • 参数说明
    • path:文件夹路径
    • options:选项配置( 可选 )
    • callback:操作后的回调
1
2
3
4
5
6
7
8
fs.rmdir("./test", { recursive: true }, (err) => {
if (err) {
console.log("删除失败!");
return;
} else {
console.log("删除成功!");
}
});

4)fs文件练习——批量重命名

  • 批量重命名,把1、2等变为01、02等,可应用于防止网盘文件错位。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const fs = require("fs");

    const files = fs.readdirSync('./test');

    files.forEach(item => {
    let [name, txt] = item.split('.');
    if (Number(name) < 10) {
    fs.rename(`./test/${item}`, `./test/0${name}.${txt}`, (err) => { });
    }
    })

2.6 查看资源状态

  • 在 Node.js 中,我们可以使用 statstatSync来创建文件夹

  • 语法

    • fs.stat(path[, options], callback)
    • fs.statSync(path[, options])
  • 参数说明

    • path:文件夹路径
    • options:选项配置( 可选 )
    • callback:操作后的回调
1
2
3
4
5
6
7
8
9
10
11
12
const fs = require("fs");

fs.stat("./test", (err, stats) => {
if (err) {
console.log("获取文件信息失败!");
return;
} else {
console.log(stats);
console.log(stats.isFile());
console.log(stats.isDirectory());
}
});

image-20230929170910563

  • size 文件体积
  • birthtime 创建时间
  • mtime 最后修改时间
  • isFile 检测是否为文件
  • isDirectory 检测是否为文件夹

2.7 dirname全局变量

  • __dirnamerequire类似,都是 Node.js 环境中的全局变量

  • __dirname保存着当前文件所在目录的绝对路径 ,可以使用 __dirname与文件名拼接成绝对路径

    • ```js
      const fs = require(“fs”);

      let data = fs.readFileSync(__dirname+’/test/01.txt’, ‘utf-8’);
      console.log(data);

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40

      # 3 path模块



      > path 模块提供了 **操作路径** 的功能

      | API | 说明 |
      | --------------- | ------------------------ |
      | `path.resolve` | 拼接规范的绝对路径 |
      | `path.sep` | 获取操作系统的路径分隔符 |
      | `path.parse` | 解析路径并返回对象 |
      | `path.basename` | 获取路径的基础名称 |
      | `path.dirname` | 获取路径的目录名 |
      | `path.extname` | 获取路径的扩展名 |

      ```js
      const path = require("path");

      //resolve:拼接规范(分隔符统一)的绝对路径
      console.log(path.resolve(__dirname, './index.html'));
      //可以不写./也表示绝对路径
      console.log(path.resolve(__dirname, 'index.html'));

      //sep:获取操作系统的路径分隔符
      console.log(path.sep); // windows下是\,Linux下是/

      //parse解析路径并返回对象
      //console.log(__filename); //获取文件的绝对路径
      let str = "d:\\Coding\\test\\tempWebToolsNodejs\\3_path模块\\index.html ";
      console.log(path.parse(str));

      //basename:快速获取文件名
      console.log(path.basename(str));

      //dirname:获取路径的目录名
      console.log(path.dirname(str));

      //extname:获取路径的扩展名
      console.log(path.extname(str));

image-20230929172923143

4 HTTP协议

4.1 HTTP简介

  • HTTP(hypertext transport protocol) - 超文本传输协议
    • 是一种基于TCP/P的应用层通信协议
    • 这个协议详细规定了浏览器和万维网服务器之间互相通信的规则。
  • 主要规定了两个方面的内容
    • 客户端:用来向服务器发送数据,可以被称之为请求报文
    • 服务端:向客户端返回数据,可以被称之为响应报文
  • 报文:可以简单理解为就是一堆字符串

4.2 窥探HTTP报文 - Fiddler

Fiddler是一个http协议调试代理工具,它能够记录并检查所有你的电脑和互联网之间的http通讯,设置断点,查看所有的“进出”Fiddler的数据。

4.2.1 安装

4.2.2 配置

  • ToolOptions里 ,找到HTTPS勾选Decrypt..,然后同意接下来的弹窗。记得点OK!然后重启!!

    image-20230929182349512

  • 设置Web Browsers

    image-20230929182450547

4.2.3 使用

  • 双击某一条则会显示详细信息

    image-20230929182617620

  • 选择Raw会显示详细信息

    image-20230929182658004

4.3 请求报文的组成

image-20230929182906837

4.3.1 HTTP请求行

  • 请求方法

    image-20230929183009427

  • URLUniform Reaourse Locator,统一资源定位符,其本身也是一个字符串

    image-20230929183130120

  • 版本号

    image-20230929183210151

4.3.2 HTTP请求头

  • 格式:头名:头值

    image-20230929183240233

4.3.3 HTTP的请求体

  • 请求体内容的格式是非常灵活的,
  • (可以是空):GET请求,
  • (也可以是字符串,还可以是JSON):POST请求

4.4 响应报文的组成

image-20230929183523814

4.4.1 响应行

image-20230929183540909

image-20230929183551419

image-20230929183608322

image-20230929183636347

4.4.2 响应头和响应体

  • 响应体内容的类型是非常灵活的,常见的类型有 HTML、CSS、JS、图片、JSON。
  • 相应头请求头这些都是不需要记住的,在MDN可查。

5 http模块

5.1 node.js创建HTTP服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 1. 引入http模块
const http = require("http");

// 2. 创建http服务
// request 请求对象, 获取客户端传递过来的信息
// response 响应对象, 给客户端发送响应信息
const server = http.createServer((request, response) => {
response.end("Hello Server!"); // 响应信息
//解决响应内容中文乱码的问题
response.setHeader("content-type", "text/html;charset=utf-8");
response.end("你好, HTTP服务器!"); // 响应信息
});

// 3. 监听端口, 启动服务
server.listen(9000, () => {
console.log("服务器启动成功了, 请访问: http://localhost:9000");
});

http.createServer 的回调函数执行时机:收到HTTP请求时,执行

响应内容中文乱码的解决办法:response.setHeader('content-type','text/html;charset=utf-8');

5.2 获取请求报文

含义 语法
请求方法 reaquest.method
请求版本 request.httpVersion
请求路径 request.url 只能获取路径以及查询字符串,无法获取 URL 中的域名以及协议的内容
URL路径 require('url').parse(requet.url).pathname
URL查询字符串 require('url').parse(request.url).query
请求头 request.headers 将请求信息转化成一个对象,并将属性名都转化成了小写
请求体 request.on('data', function(chunk){})
request.on('end', function(){})

:memo:

  • 如果访问网站的时候,只填写了 IP 地址或者是域名信息,此时请求的路径为/

  • 关于 favicon.ico:这个请求是属于浏览器自动发送的请求——获取图标

5.2.1 提取报文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const http = require('http');

const server = http.createServer((request, response) => {
// 1. 获取请求方式
console.log(request.method);
// 2. 获取请求地址
console.log(request.url); // 只包含 url 中的路径与查询字符串
// 3. 获取HTTP协议的版本号
console.log(request.httpVersion);
// 4. 获取请求头信息
console.log(request.headers);

// 设置响应体
response.end("Hello Server!");
});

server.listen(9000, () => {
console.log("服务器启动成功了, 请访问: http://localhost:9000");
});

image-20230930154831775

5.2.2 请求体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const http = require("http");

const server = http.createServer((request, response) => {
let body = "";
request.on("data", (chunk) => {
body += chunk;
});
request.on("end", () => {
console.log(body);
response.setHeader("content-type", "text/html;charset=utf-8");
response.end("获取请求体!");
});
});

server.listen(9000, () => {
console.log("服务器启动成功了, 请访问: http://localhost:9000");
});

  • 借助form表单

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <title>03_请求体post</title>
    </head>
    <body>
    <form action="http://127.0.0.1:9000/index.html" method="post">
    <input type="text" name="username" />
    <input type="password" name="password" />
    <input type="submit" value="提交" />
    </form>
    </body>
    </html>

![image-20230930155641216](https://s2.loli.net/2023/09/30/5YxnFA8eTyIcGop.png)

5.2.3 URL路径与查询字符串

1)方法一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const http = require("http");
// 1. 导入 url 模块
const url = require("url");

const server = http.createServer((request, response) => {
console.log(request.url);
let res = url.parse(request.url, true); // true 表示把查询字符串转换为对象
// url.parse() 方法会把 url 路径与查询字符串分开
console.log("url:\n", res);
// url 路径
console.log("url路径:\n", res.pathname);
// 查询字符串
console.log("查询字符串:\n", res.query.keyword);
response.setHeader("content-type", "text/html;charset=utf-8");
response.end("url路径与查询字符串!");
});

server.listen(9000, () => {
console.log("服务器启动成功了, 请访问: http://localhost:9000");
});

image-20230930161207658

2)方法二 ==推荐==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const http = require("http");

const server = http.createServer((request, response) => {
let url = new URL(request.url, "http://localhost:9000");
// url.pathname 表示 url 路径
console.log(url.pathname);
// url.searchParams.get() 方法可以获取查询字符串
console.log(url.searchParams.get("keyword"));

response.setHeader("content-type", "text/html;charset=utf-8");
response.end("url路径与查询字符串!");
});

server.listen(9000, () => {
console.log("服务器启动成功了, 请访问: http://localhost:9000");
});

在这里插入图片描述

5.2.4 HTTP请求练习

请求类型(方法) 请求地址 响应体结果
get /login 登录页面
get /reg 注册页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const http = require("http");

const server = http.createServer((request, response) => {
const request_method = request.method;
const request_pathname = new URL(request.url, "http://localhost:9000")
.pathname;
// console.log(request_method, request_pathname);
response.setHeader("content-type", "text/html;charset=utf-8"); // 设置中文
if (request_method === "GET") {
if (request_pathname === "/login") {
response.end("登录页面");
} else if (request_pathname === "/reg") {
response.end("注册页面");
} else {
response.end("404");
}
} else {
response.end("404");
}
});

server.listen(9000, () => {
console.log("服务器启动成功了, 请访问: http://localhost:9000");
});

5.3 设置响应报文

5.3.1 设置HTTP响应报文

语法 说明
response.statusCode 设置响应状态码
response.statusMessage 设置响应状态描述
response.setHeader('header', 'value') 设置响应头信息
response.write('xx')
response.end('xxx')
设置响应体
end只能写一次,write可以写多次
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const http = require("http");

const server = http.createServer((request, response) => {
response.statusCode = 404; // 设置响应状态码
response.statusMessage = "Not Found"; // 设置响应状态信息
response.setHeader("a", "1"); // 设置响应头
response.setHeader("content-type", "text/html;charset=utf-8");
response.write("hello ");
response.write("world");
response.write("!");
response.end("设置响应报文!");
});

server.listen(9000, () => {
console.log("服务器启动成功了, 请访问: http://localhost:9000");
});

image-20230930162607644

5.3.2 HTTP响应练习

  • 采用html和js分离+导入实现

  • html完成表格样式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
    td {
    padding: 20px 40px;
    }
    /* odd even交替实现隔行换色 */
    table tr:nth-child(odd) {
    background: rgb(202, 219, 255);
    }

    table tr:nth-child(even) {
    background: #fcb;
    }

    table,
    td {
    border-collapse: collapse;
    }
    </style>
    </head>

    <body>
    <table border="1">
    <tr>
    <td></td>
    <td></td>
    <td></td>
    </tr>
    <tr>
    <td></td>
    <td></td>
    <td></td>
    </tr>
    <tr>
    <td></td>
    <td></td>
    <td></td>
    </tr>
    <tr>
    <td></td>
    <td></td>
    <td></td>
    </tr>
    </table>
    <script>
    //实现点击换色
    //获取所有的 td
    let tds = document.querySelectorAll("td");
    //遍历
    tds.forEach((item) => {
    item.onclick = function () {
    this.style.background = "yellow";
    };
    });
    </script>
    </body>
    </html>
  • js设置响应

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const http = require("http");
    const fs = require("fs");

    const server = http.createServer((request, response) => {
    response.setHeader("content-type", "text/html;charset=utf-8");
    let data = fs.readFileSync("./08_http响应练习.html");
    response.end(data);
    });

    server.listen(9000, () => {
    console.log("服务器启动成功了, 请访问: http://localhost:9000");
    });

5.4 资源加载过程

  • 网页资源的加载都是循序渐进的,首先获取 HTML 的内容, 然后解析 HTML 在发送其他资源的请求,如CSS,Javascript,图片等。

5.4.1 静态资源服务

  • 静态资源是指内容长时间不发生改变的资源 ,例如图片,视频,CSS 文件,JS文件,HTML文件,字体文件等。
  • 动态资源是指内容经常更新的资源 ,例如百度首页,网易首页,京东搜索列表页面等。
  • HTTP 服务在哪个文件夹中寻找静态资源,那个文件夹就是静态资源目录,也称之为网站根目录

5.4.2 网页中的URL

  • 绝对路径

    • 绝对路径可靠性强,而且相对容易理解,在项目中运用较多

    • | 形式 | 特点 |
      | ————————————- | —————————————————————————————— |
      | http://www.baidu.com/test | 直接向目标资源发送请求,容易理解。网站的外链会用到此形式 |
      | //www.baidu.com/test | 与页面UL的协议拼接形成完整URL再发送请求。大型网站用的比较多 |
      | /test | 与页面URL的协议、主机名、端口拼接形成完整UL再发送请求。中小型网站==避免更换域名等问题== |

  • 相对路径

5.4.3 资源类型(mime类型)

  • 媒体类型(通常称为 Multipurpose Internet Mail ExtensionsMIME 类型)是一种标准,用来表示文档、文件或字节流的性质类型。

    • ```md
      mime 类型结果: [type]/[subType]
      例如:text/html, text/css, image/jpeg, image/png, application/json
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83

      - HTTP服务可以设置响应头 `Content-Type` 来表明响应体的MIME类型,浏览器会根据该类型觉得如何处理资源

      - 常见文件对应的mime类型

      - html `text/html`
      - css `text/css`
      - js `text/javascript`
      - png `image/png`
      - jpg `image/jpeg`
      - gif `image/gif`
      - mp4 `video/mp4`
      - mp3 `audio/mpeg`
      - json `application/json`

      - 对于未知的资源类型,可以选择 `application/octet-stream` 类型,浏览器在遇到该类型的相应时,会对响应体内容进行独立存储,也就是常见的 `下载` 效果

      ```js
      /**
      * 创建一个 HTTP 服务,端口为 9000,满足如下需求
      * GET /index.html 响应 page/index.html 的文件内容
      * GET /css/app.css 响应 page/css/app.css 的文件内容
      * GET /images/logo.png 响应 page/images/logo.png 的文件内容
      */
      const http = require("http");
      const fs = require("fs");
      const path = require("path");

      let mimes = {
      html: "text/html",
      css: "text/css",
      js: "text/javascript",
      png: "image/png",
      jpg: "image/jpeg",
      gif: "image/gif",
      mp4: "video/mp4",
      mp3: "audio/mp3",
      json: "application/json",
      };

      const server = http.createServer((request, response) => {
      // 获取请求的路径
      let { pathname } = new URL(request.url, "http://127.0.0.1");
      // 获取文件的后缀名
      let filePath = path.join(__dirname, "public", pathname);

      // 读取文件
      fs.readFile(filePath, (err, data) => {
      if (err) {
      switch (err.code) {
      case "ENOENT":
      response.statusCode = 404;
      response.end("404, 您请求的资源不存在");
      break;
      case "EPERM":
      response.statusCode = 403;
      response.end("403, 您没有权限访问该资源");
      default:
      response.statusCode = 500;
      response.end("500, 服务器内部错误");
      }
      return;
      }
      let ext = path.extname(pathname).slice(1);
      let type = mimes[ext];
      if (type) {
      if (type === "text/html") {
      response.setHeader("Content-Type", type + ";charset=utf-8");
      }
      else {
      response.setHeader("Content-Type", type);
      }
      }
      else {
      response.setHeader("Content-Type", "application/octet-stream");
      }
      response.end(data);
      });
      });

      server.listen(9000, () => {
      console.log("服务器启动成功了, 请访问: http://localhost:9000");
      });

5.4.4 GET和POST请求场景小结

  • GET请求的情况:
    • 在地址栏直接输入url访问
    • 点击a链接
    • link标签引入css
    • script标签引入js
    • img标签引入图片
    • form标签中的methodget(不区分大小写)
    • ajax中的get请求
  • POST请求的情况:
    • form标签中的method为post(不区分大小写)
    • AJAX的post请求

5.4.5 GET和POST请求的区别

  • GETPOST 是 HTTP 协议请求的两种方式。
    • GET主要用来获取数据,POST主要用来提交数据
    • GET 带参数请求是将参数缀到 URL 之后,在地址栏中输入 url 访问网站就是 GET 请求,
    • POST 带参数请求是将参数放到请求体中。
    • POST 请求相对 GET安全一些,因为在浏览器中参数会暴露在地址栏。
    • GET请求大小有限制,一般为 2K,而POST请求则没有大小限制。

6 Node.js模块化

6.1 模块化简介

  • 模块化
    • 将一个复杂的程序文件依据一定规则(规范)拆分成多个文件的过程
    • 其中拆分出的每个文件就是一个模块,模块的内部数据是私有的,不过模块可以暴露内部数据以便其他模块使用
  • 模块化项目

    • 编码时是按照模块一个一个编码的,整个项目就是一个模块化的项目
  • 优点

    • 防止命名冲突
    • 高复用性
    • 高维护性

6.2 模块暴露

6.2.1 基本使用

  • 创建me.js

    1
    2
    3
    4
    5
    6
    7
    // 声明函数
    function hello() {
    console.log("hello");
    }

    // 暴露模块
    module.exports = hello;
  • 创建index.js

    1
    2
    3
    4
    5
    // 导入模块
    const hello = require('./me.js');

    // 调用模块
    hello(); // hello

6.2.2 暴露数据

  • 模块暴露数据的方式有两种:

    • module.exports = value
    • exports.name = value
  • module.exports可以暴露任意数据。

    1
    2
    //me.js
    module.exports = 'hello world!';
1
2
3
4
//导入模块
const me = require('./me.js');
//输出 me
console.log(me);

  • 不能使用exports = value的形式暴露数据,模块内部 module 与 exports 的隐式关系exports = module.exports = {} ,require 返回的是目标模块中module.exports的值

    image-20230930190114841

6.3 导入文件模块

  • 在模块中使用require传入文件路径即可引入文件

  • require使用的一些注意事项:

    • 对于自己创建的模块,导入时路径建议写相对路径 ,且不能省略 ./ 和 …/
    • js 和 json 文件导入时可以不用写后缀,c/c++编写的 node 扩展文件也可以不写后缀,但是一般用不到
    • 如果导入其他类型的文件,会以 js 文件进行处理
    • 导入node.js内置模块时,直接require模块的名字即可,无需加 ./ 和 …/

6.4 导入文件夹的情况

  • 如果导入的路径是个文件夹,则会
    • 首先检测该文件夹下package.json文件中 main 属性对应的文件
    • 如果存在则导入,反之如果文件不存在会报错。
    • 如果 main 属性不存在,或者 package.json不存在,则会尝试导入文件夹下的index.jsindex.json,如果还是没找到,就会报错。

6.5 导入模块的基本流程

  • require导入自定义模块的基本流程:

    • 将相对路径转为绝对路径,定位目标文件。
    • 缓存检测。
    • 读取目标文件代码。
    • 包裹为一个函数并执行(自执行函数)。通过arguments.callee.toString()查看自执行函数。
    • 缓存模块的值。
    • 返回module.exports的值。

在这里插入图片描述

6.6 CommonJS规范

  • module.exportsexports 以及 require 这些都是 CommonJS 模块化规范中的内容。
  • 而 Node.js 是实现了 CommonJS 模块化规范,二者关系有点像 JavaScript 与 ECMAScript

7 包管理工具

7.1 概念介绍

    • package
    • 代表了一组特定功能的源码集合
  • 包管理工具
    • 管理的应用软件,可以对进行 下载安装、更新、删除、上传 等操作
    • 借助包管理工具,可以快速开发项目,提升开发效率
  • 前端常用的包管理工具
    • npm
    • yarn
    • cnpm

7.2 npm

指令 说明
npm init 初始化
npm install
npm install xxx@x.x.x
安装模块(指定版本)
npm uninstall 卸载模块
npm update 更新模块
npm prefix 查看工作空间
npm ls 查看已安装模块
npm outdated 查看过时的已安装模块
npm help 查看命令的详情帮助
npm config 管理npm的配置路径
npm cache 管理模块的缓存
npm root 查看包的安装路径
npm version 查看模块版本号
npm view 查看模块的注册信息
npm adduser 用户登录
npm logout 退出登录
npm publish 发布模块
npm docs 说明文档
npm remove xxx
npm remove -g xxx
移除依赖(全局移除)

7.2.1 简介

  • Node Package Manager
  • node.js 官方内置的包管理工具
    • npm -v

7.2.2 基本使用

1)创建

  • 空目录,以此作为工作目录,执行npm init

    image-20230930212155568

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    {
    "name": "test", // 包名
    "version": "1.0.0", // 版本号
    "description": "学习npm", // 描述
    "main": "index.js", // 入口文件
    "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
    },
    "author": "bayyy", // 作者
    "license": "ISC" // 开源证书
    }

:warning:

  • package name (包名)不能使用中文、大写,默认值是文件夹的名称,所以文件夹名称也不能使用中文和大写
  • version(版本号)要求x.x.x的形式定义,x必须是数字,默认值为1.0.0
  • ISC证书与MIT证书功能上是相同的
  • package.json 可以手动创建与修改
  • 使用 npm init -y 或者 npm init --yes 急速创建 package.json

2)搜索包

3)下载

  • npm install xxx / npm i xxx
    • node_modules: 存放下载的包
    • package-lock.json: 包的锁文件,锁定包的版本

4)require导入npm包基本流程

1
2
3
const uniq = require("uniq");
const uniq = require("./node_modules/uniq")
const uniq = require("./node_modules/uniq/uniq.js")
  1. 在当前文件夹下node_modules中寻找同名的文件夹
  2. 在上级目录中下的node_modules中寻找同名的文件夹,直至找到磁盘根目录

7.2.3 生产与开发

1)环境

  • 开发环境是程序员专门用来写代码的环境,一般是指程序员的电脑,开发环境的项目只能程序员自己访问
  • 生产环境是项目代码正式运行的环境,一般是指正式的服务器电脑,生产环境的项目每个客户都可以访问

2)依赖

  • 我们可以在安装时设置选项来区分依赖的类型,目前分为两类:
类型 命令 补充
生产依赖 npm i -S uniq
npm i--save uniq
-S等效于-save-S是默认选项
包信息保存在package.jsondependencies属性
开发依赖 npm i -D less
npm i --save-dev less
-D等效于-save-dev
包信息保存在package.jsondevDependencies属性

7.2.4 全局安装

  • npm install -g xxx

:key:

  • 全局安装的命令不受工作目录位置影响
  • 可以通过 npm root -g 查看全局安装包的位置
  • 只有全局类工具才适合全局安装,可以通过查看包的官方文档来确定安装方式

7.2.5 修改windows执行策略

  • 管理员身份打开 powershell 命令行
  • 键入命令 set-ExecutionPolicy remoteSigned

7.2.5 配置别名

  • 配置 package.json 中的 scripts 属性

    1
    2
    3
    4
    "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node index.js"
    }
  • start 别名比较特殊,可以直接运行 npm start

  • npm start 一般作为项目启动命令
  • npm run 有自动向上级目录查找的特性,与 require 函数类似
  • 对于模式的项目,可以查看 scripts 属性来参考项目的一些操作

7.2.6 更换镜像源

1)直接配置

淘宝 NPM 镜像是一个完整 npmjs.com 镜像,同步频率目前为 10分钟一次,以保证尽量与官方服务同步。

1
2
3
4
5
$ npm config set registry https://registry.npm.taobao.org
$ npm config set registry https://registry.npmmirror.com/
#查看npm配置信息
$ npm config list
$ npm config ls

2)工具配置 nrm

  • 使用 nrm 配置 npm 的镜像地址 npm registry manager

  1. 安装 npm i -g nrm

  2. 修改镜像 nrm use taobao

    image-20230930221336128

  3. 列举所有镜像 nrm list/ls

    image-20230930221417328

  4. 检查是否配置成功 npm config list


:key: 补充说明

  1. ==建议==使用第二种方式进行镜像配置,后续修改较为方便
  2. npm 使用率高于 cnpm

7.3 cnpm

7.3.1 cnpm简介

  • cnpm是一个淘宝构建的npmjs.com的完整镜像,也称为 淘宝镜像
  • cnpm服务部署在国内阿里云服务器上,可以提高包的下载速度
  • 官方也提供了一个全局工具包 cnpm,操作命令与npm大体相同

7.3.2 cnpm安装

npm install -g cnpm --registry=https://registry.npmmirror.com

7.4 yarn

7.4.1 yarn 简介

  • yarn是由Facebook在20l6年推出的新的Javascript包管理工具,官方网址:Yarn | enYarn | cn
  • 特点 (官方宣称的一些特点)
    • 速度超快:yarn缓存了每个下载过的包,所以再次使用时无需重复下载。同时利用并行下载以最大化资源利用率,因此安装速度更快
    • 超级安全:在执行代码之前,yarn会通过算法校验每个安装包的完整性
    • 超级可靠:使用详细、简洁的锁文件格式和明确的安装算法,yarn能够保证在不同系统上无差异的工作

7.4.2 yarn 使用

  • npm i -g yarn
    • npm 的锁文件为 - package-lock.json
    • yarn 的锁文件为 - yarn.lock

7.4.3 yarn常用命令

功能 命令
初始化 yarn init / yarn init -y
安装包 yarn add xxx 生产依赖
yarn add xxx -dev 开发依赖
yarn global addd xxx 全局安装
删除包 yarn remove xxx 删除项目依赖包
yarn global remove xxx 全局删除包
安装项目依赖 yarn
运行命令别名 yarn <别名> # 不需要添加run

:warning: yarn 全局安装包不可用 -> 需要手动配置环境变量

  • 可通过 yarn global bin 查看全局安装包的位置

7.4.4 配置淘宝镜像

  • yarn config set registry https://registry.npmmirror.com/

  • 查看 yarn 的配置项 yarn config list

    image-20230930222820574

7.5 管理发布包

7.5.1 创建与发布

  • 我们可以将自己开发的工具包发布到npm服务上,方便自己和其他开发者使用,操作步骤如下:
  1. 创建文件夹,并创建文件 index.js,在文件中声明函数,使用module.exports暴露

  2. npm 初始化工具包,package.json 填写包的信息(==包的名字是唯一的==)

  3. 注册账号https://www.npmjs.com/signup

  4. 激活账号

  5. 修改为官方的官方镜像(命令行中运行nrm use npm)

  6. 命令行下npm login填写相关用户信息

    image-20230930223829691

  7. 命令行下npm publish提交包

    image-20230930224135017

  • 测试结果:

    image-20230930224216893

    image-20230930224755369

7.5.2 更新/删除包

  • 更新

    1. 更新包中的代码
    2. 测试代码是否可用
    3. 修改package.json中的版本号
    4. 发布更新
    5. npm publish
  • 删除

    • npm unpublish

    :key: 删除包的条件

    • 是包的作者
    • 发布小于24小时
    • 大于24小时,且没有其他包依赖,且每周小于300下载量,且只有一个维护者

7.6 扩展内容

7.6.1 编程语言的包管理工具

语言 包管理工具
PHP composer
Python pip
Java maven
Go go mod
JavaScript npm/yarn/cnpm/other
Ruby rubyGems

7.6.2 操作系统的包管理工具

操作系统 包管理工具
Centos yum
Ubuntu apt
MacOS homebrew
Windows chocolatey

7.7 NVM

7.7.1 nvm介绍

  • NVM
    • Node Version Manager node版本管理工具
    • 方便切换不同版本的Node.js

7.7.2 nvm使用


常用操作 说明
nvm list available 显示所有可以下载的 Node.js 版本
nvm list 显示已安装的版本
nvm current 显示当前使用的 Node.js 版本
nvm install xx.xx.xx 安装 xx.xx.xx 版本的 Node.js
nvm install latest 安装最新版本的 Node.js
nvm uninstall xx.xx.xx 删除某个版本的 Node.js
nvm use xx.xx.xx 切换到 xx.xx.xx 版本的Node.js
nvm alias <name> <vesion> 为指定的版本设置一个别名
nvm unalias <name> 删除别名

8 express框架

8.1 介绍

  • 基于 Node.js平台,快速、开放、极简的 Web 开发框架 Express
    • 简单来说,express是一个封装好的工具包,封装了很多功能,便于我们开发WEB应用(HTTP服务)

8.2 express使用

8.2.1 安装

  • npm i express

8.2.2 基本使用

  • 创建 JS 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1. 导入 express
const express = require("express");

// 2. 创建应用对象
const app = express();

// 3. 创建路由规则
app.get("/home", (req, res) => {
res.end("hello express");
});

// 4. 监听端口启动服务
app.listen(8000, () => {
console.log(
"服务已经启动(8000端口监听中...), 请访问: http://localhost:8000/home"
);
});
  • 执行脚本命令 node 01.js

8.3 express路由

8.3.1 定义

  • 路由确定了应用程序如何响应客户端对特定端点的请求

8.3.2 路由的使用

  • 一个路由的组成有请求方法,路径和回调函数组成
  • app.<method>(path, callback)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const express = require("express");
const app = express();

app.get("/home", (req, res) => {
res.end("<h1>HOME</h1>");
});

app.get("/", (req, res) => {
res.end("<h1>ROOT</h1>");
});

app.post("/post", (req, res) => {
res.end("<h1>Post Method</h1>");
});

app.all("/all", (req, res) => {
res.end("<h1>ALL</h1>");
});

app.all('*', (req, res) => {
res.end("<h1>404 Not Found</h1>");
});

app.listen(8000, () => {
console.log(
"服务已经启动(8000端口监听中...), 请访问: http://localhost:8000/"
);
});

8.4 参数获取

8.4.1 获取请求参数

方法 说明
method 请求方法
url 请求地址
httpVersion http版本
headers 请求头
path 请求路径(不包含请求参数)
query 请求参数
ip 请求ip
get() 获取请求头的某个字段
params 路由参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 原生方法
console.log("method: ", req.method); // 请求方法
console.log("url: ", req.url); // 请求地址
console.log("httpVersion: ", req.httpVersion) // http版本
console.log("headers: ", req.headers); // 请求头
// express 封装的方法
console.log("path: ", req.path); // 请求路径(不包含请求参数)
console.log("query: ", req.query); // 请求参数
console.log("params: ", req.params); // 路由参数
console.log("ip: ", req.ip); // 请求ip
console.log("get(): ", req.get("host")); // 获取请求头中的某个字段
console.log("hostname: ", req.hostname); // 请求主机名
console.log("protocol: ", req.protocol); // 请求协议
console.log("secure: ", req.secure); // 请求协议是否是https
  • 测试结果:image-20231001002335521

8.4.2 获取路由参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const express = require("express");
const app = express();

app.get("/:id.html", (req, res) => { // :id 占位符, 表示一个变量
console.log(req.params.id); // { id: '123' }
res.setHeader("Content-Type", "text/html;charset=utf-8"); // 设置中文编码
res.end(`<h1>请求参数为: ${req.params.id}</h1>`);
});

app.listen(8000, () => {
console.log(
"服务已经启动(8000端口监听中...), 请访问: http://localhost:8000/request"
);
});
  • 测试结果:image-20231001002842008

8.4.3 路由参数练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const express = require("express");
const app = express();

// 导入 json 数据
const { singers } = require("./singers.json");

app.get("/singer/:id.html", (req, res) => {
let id = req.params.id;
// let {id} = req.params; // 结构赋值
// 在 singers 数组中找到 id 对应的歌手信息
let singer = singers.find((item) => item.id == id);
if (!singer) {
res.statusCode = 404;
res.end("<h1>404 Not Found</h1>");
return;
}

res.end(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
</head>
<body>
<h1>歌手: ${singer.name}</h1>
<h2>性别: ${singer.sex}</h2>
</body>
<script></script>
</html>
`);
});

app.listen(8000, () => {
console.log(
"服务已经启动(8000端口监听中...), 请访问: http://localhost:8000/singer"
);
});

8.4.4 响应设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 原生响应
res.statusCode = 200; // 设置响应状态码
res.statusMessage = "OK"; // 设置响应状态信息
res.setHeader("Content-Type", "text/html;charset=utf-8"); // 设置中文编码
res.write("<h1>你好, 世界!</h1>"); // 设置响应体(写入响应体)
res.end("response"); // 设置响应体(结束响应)

// express 响应
res.status(200) // 设置响应状态码
res.set('a', '1') // 设置响应头
res.send('<h1>你好, 世界!</h1>') // 设置响应体(express 会自动设置中文编码)

// 链式调用
res.status(200).set('a', '1').send('<h1>你好, 世界!</h1>')

:key: express 会自动根据需要设置中文编码响应

:key: 可以链式调用

  • 其他响应
1
2
3
4
res.redirect('http://www.baidu.com') // 重定向(url, 默认状态码 302)
res.download('./package.json') // 下载文件(Content-Disposition: attachment; filename=package.json)
res.sendFile(__dirname + '/post.html') // 发送文件(文件路径, 回调函数)
res.json({ name: 'zs' }) // 发送 json 数据(Content-Type: application/json)

8.5 express中间件

  • 中间件(Middle ware)本质是一个回调函数
    • 中间件函数可以像路由回调一样访问请求对象(request)+响应对象(response)
  • 作用:中间件的作用就是使用函数封装公共操作简化代码
  • 类型
    • 全局中间件
    • 路由中间件

8.5.1 全局中间件

  • function recordMiddleware(req, res, next) {...}

  • 每一个请求到达服务端之后都会执行全局中间件函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    const express = require("express");
    const fs = require("fs");
    const path = require("path");

    const ws = fs.createWriteStream(path.resolve(__dirname, "./log.txt"), {
    flags: "a",
    });

    // 创建应用对象
    const app = express();

    // 声明中间件函数
    function recordMiddleware(req, res, next) {
    let { url, ip } = req;
    let time = new Date().toLocaleString();
    let log = `url: ${url}, ip: ${ip}, time: ${time}\n`;
    ws.write(log);
    next(); // 调用下一个中间件函数
    }

    // 使用中间件函数
    app.use(recordMiddleware);


    app.get("/home", (req, res) => {
    res.send("<h1>首页</h1>");
    });

    app.get("/admin", (req, res) => {
    res.send("<h1>后台管理</h1>");
    });

    app.all("*", (req, res) => {
    res.status(404).send("<h1>404 Not Found</h1>");
    });

    app.listen(8000, () => {
    console.log(
    "服务已经启动(8000端口监听中...), 请访问: http://localhost:8000/"
    );
    });
  • :key: 注意要使用 next() 进行放行

8.5.2 路由中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const express = require("express");

const app = express();

app.get("/home", (req, res) => {
res.send("<h1>首页</h1>");
});

let checkCodeMiddlerware = (req, res, next) => {
if (req.query.code === "521") {
next();
} else {
res.send("<h1>请先登录</h1>");
}
};

app.get("/admin", checkCodeMiddlerware ,(req, res) => {
res.send("<h1>后台管理</h1>");
});

app.all("*", (req, res) => {
res.status(404).send("<h1>404 Not Found</h1>");
});

app.listen(8000, () => {
console.log(
"服务已经启动(8000端口监听中...), 请访问: http://localhost:8000/"
);
});
  • :key: 路由中间件 添加在需要添加此中间件路由处理函数之前

8.5.3 静态资源中间件

1
app.use(express.static(__dirname + '/public'));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const express = require("express");
const path = require("path");

const app = express();

app.use(express.static(path.resolve(__dirname, "./public")));

app.get("/home", (req, res) => {
res.send("<h1>首页</h1>");
});

app.get("/admin", (req, res) => {
res.send("<h1>后台管理</h1>");
});

app.all("*", (req, res) => {
res.status(404).send("<h1>404 Not Found</h1>");
});

app.listen(8000, () => {
console.log(
"服务已经启动(8000端口监听中...), 请访问: http://localhost:8000/"
);
});

:key: 会自动设置静态资源类型

  • index.html文件为默认打开的资源 -> http://localhost:8000/
  • 如果静态资源与路由规则同时匹配,==谁先匹配谁就响应==
  • 路由响应动态资源静态资源中间件响应静态资源

8.5.4 请求体数据 body-parser

body-parser - npm

  • 安装 npm i body-parser

  • 导包 const bodyParser = require('body-parser');

  • 获取中间件函数

    1
    2
    3
    4
    5
    // 创建 json 解析器
    const jsonParser = bodyParser.json();

    // 创建 querystring 解析器
    const urlencodedParser = bodyParser.urlencoded({ extended: false });
  • 设置路由中间件,使用 requese.body 来获取请求体数据

    1
    2
    3
    4
    app.post("/login", urlencodedParser, (req, res) => {
    console.log(req.body); // 中间件函数会将解析后的数据放入 req.body 中 - [Object: null prototype] { username: '123', password: '123' }
    res.send("登录成功");
    });

8.5.5 防盗链

禁止设置域名之外其他网站访问服务资源

  • 请求区别在于 请求头 referer 包含所在域名

8.5.6 路由模块化

image-20231001160628241

  • xxxRouter.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 1. 导入 express
const express = require("express");

// 2. 创建路由对象
const router = express.Router();

// 3. 创建路由规则

router.get("/home", (req, res) => {
res.send("<h1>首页</h1>");
});

router.get("/search", (req, res) => {
res.send("<h1>内容搜索</h1>");
});

// 4. 暴露路由对象
module.exports = router;
  • 路由模块引入
1
2
3
4
5
6
// 导入路由模块
const homeRouter = require("./routes/homeRouter");
const adminRouter = require("./routes/adminRouter");
...
// 使用路由中间件
app.use(homeRouter, adminRouter);

:key: 可以设置路由前缀

1
2
app.use('/', indexRouter);
app.use('/users', usersRouter);

8.6 EJS 模板引擎

8.6.1 简介

  • 模板引擎

    • 模板引擎是分离用户界面和业务数据的一种技术

      “E” 代表什么?可以表示 “可嵌入(Embedded)”,也可以是“高效(Effective)”、“优雅(Elegant)”或者是“简单(Easy)”。EJS 是一套简单的模板语言,帮你利用普通的 JavaScript 代码生成 HTML 页面。EJS 没有如何组织内容的教条;也没有再造一套迭代和控制流语法;有的只是普通的 JavaScript 代码而已。

  • EJS

  • EJS特性

    • 快速编译与绘制输出
    • 简洁的模板标签:<% %>
    • 自定义分割符(例如:用 <? ?> 替换 <% %>
    • 引入模板片段
    • 同时支持服务器端和浏览器 JS 环境
    • JavaScript 中间结果静态缓存
    • 模板静态缓存
    • 兼容 Express 视图系统

8.6.2 EJS使用

1)基本使用

  • 安装: npm i ejs --save

  • 简单使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    const fs = require("fs");
    // 1. 导入 ejs
    const ejs = require("ejs");

    // 字符串
    let china = "中国";
    let str = `我爱你 ${china}`;
    console.log(str); // 我爱你 中国

    // 使用 ejs 渲染 - 1. 字符串
    let str2 = "我爱你 <%= china %>";
    let result2 = ejs.render(str2, { china: china });
    console.log(result2); // 我爱你 中国

    // 使用 ejs 渲染 - 2. 文件
    let str3 = fs.readFileSync(__dirname + "/01_html.html").toString();
    let nowTime = new Date().toLocaleString();
    let result3 = ejs.render(str3, { china, nowTime });
    console.log(result3); // 我爱你 中国

    可以在html或其他文件中设置坑位,由js文件引入时进行填充

2)列表渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 列表
const games = ["王者荣耀", "英雄联盟", "绝地求生", "穿越火线", "地下城与勇士"];

const ejs = require("ejs");
const fs = require("fs");

let result1 = ejs.render(
`<ul>
<% games.forEach((item) => { %>
<li><%= item %></li>
<% }) %>
</ul>`,
{ games }
);

const str = fs.readFileSync(__dirname + "/02_列表.html").toString();
const result2 = ejs.render(str, { games });

console.log(result1);
console.log(result2);

3)条件渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let isLogin = false;

const ejs = require("ejs");
const fs = require("fs");

let result1 = ejs.render(
`<% if(isLogin) { %>
<a href="#">退出</a>
<% } else { %>
<button>登录</button>
<button>注册</button>
<% } %>`,
{ isLogin }
);

const str = fs.readFileSync(__dirname + "/03_if渲染.html").toString();
const result2 = ejs.render(str, { isLogin });

console.log(result1);

8.6.3 express配合ejs

  • 创建 xxx.ejs 模板文件

image-20231001164204957

1
2
3
4
5
6
7
8
9
10
// 1. 设置模板引擎
app.set("view engine", "ejs");
// 2. 设置模板目录
app.set('views', path.join(__dirname, 'views'));

app.get("/login", (req, res) => {
let username = "bay";
// 3. render方法渲染模板
res.render("login", { username });
});

模板名会自动寻找此名称的 ejs 文件

8.6.4 express-generator

express-generator - npm

  • 安装 npm i -g express-generator

    image-20231001164543967

  • 使用 express -e <name>

    • 进入文件安装依赖 npm i

      • image-20231001164816933

        • ```md
          bin:可执行文件。里面包含了一个启动文件 www 默认监听端口是 3000。
          public:静态文件。用来存放项目静态文件目录如js,css以及图片。
          routes:路由文件。路由主要定义 url 和 资源 的映射关系 ( 一一对应关系 ),
          主要用来接收前端发送的请求 响应数据给前端
          views:后端模版文件。
          app.js:入口文件。
          package.json:工程信息和模块依赖。
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          21
          22
          23
          24
          25
          26
          27
          28
          29
          30
          31
          32
          33
          34
          35
          36
          37
          38
          39
          40
          41
          42
          43
          44
          45
          46
          47

          - 运行 `node ./bin/www`

          - 自动生成的 `app.js`

          ```js
          var createError = require('http-errors'); // 错误处理模块(http-errors)
          var express = require('express');
          var path = require('path');
          var cookieParser = require('cookie-parser'); // cookie解析中间件(cookie-parser)
          var logger = require('morgan'); // 日志中间件(morgan)

          var indexRouter = require('./routes/index'); // 路由
          var usersRouter = require('./routes/users'); // 路由

          var app = express();

          // 设置模板引擎
          app.set('views', path.join(__dirname, 'views')); // 模板文件存放的目录
          app.set('view engine', 'ejs'); // 模板引擎

          app.use(logger('dev')); // 用来记录日志的
          app.use(express.json()); // 用来解析json格式的请求体
          app.use(express.urlencoded({ extended: false })); // 用来解析urlencoded格式的请求体
          app.use(cookieParser()); // 用来解析cookie
          app.use(express.static(path.join(__dirname, 'public'))); // 静态文件服务

          app.use('/', indexRouter);
          app.use('/users', usersRouter);

          // 捕获404错误并转发到错误处理器
          app.use(function(req, res, next) {
          next(createError(404));
          });

          // 错误处理器
          app.use(function(err, req, res, next) {
          // 设置本地变量,只提供开发中的错误
          res.locals.message = err.message; // 错误信息
          res.locals.error = req.app.get('env') === 'development' ? err : {}; // 错误对象

          // 渲染错误页面
          res.status(err.status || 500); // 设置状态码
          res.render('error'); // 渲染模板
          });

          module.exports = app;

8.7 文件上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 显示上传表单
router.get("/portrait", function (req, res, next) {
res.render("portrait"); // 渲染模板 portrait.ejs
});

// 处理文件上传
router.post("/portrait", function (req, res, next) {
const form = formidable({
multiples: true, // 多文件上传
uploadDir: path.join(__dirname, "../public/images"), // 上传目录
keepExtensions: true, // 保留后缀
});

form.parse(req, (err, fields, files) => {
if (err) {
next(err);
return;
}
res.send("<h1>上传成功</h1>");
return;
});
res.send("<h1>上传失败</h1>");
});

8.8 记账本案例

8.8.1 注意点

  • 静态资源最好使用 绝对地址

    • 避免加 ./ -> 这会导致指向根目录
    • image-20231001192741266
  • express-generator 已经自动应用了 解析部分数据的中间件,故可直接进行 请求体数据的获取

    • image-20231001192704335

8.8.2 logdb

lowdb - npm (npmjs.com)

1)简介

  • 简介:lowdb是一个本地的json文件数据库,简单来说就是用一个json文件来充当数据库,来实现增删改查这些数据库的基本的功能。
  • 优点
    • 轻量化
    • 纯js实现
    • 更好的typescript支持
    • 原子化写入:简单理解就是不会错误写入,你指定A,就会写入A,这对数据库十分重要

2)基本使用

  • 安装 npm i lowdb
    • npm i lowdb@1.0.0 => 最新版为ES6语法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 导入 lowdb
const low = require("lowdb");
const FileSync = require("lowdb/adapters/FileSync"); // 用于读写文件
const adapter = new FileSync("db.json"); // 申明一个适配器
const db = low(adapter); // 获取数据库实例

// 初始化数据库
db.defaults({ posts: [], user: {} }).write();

// 添加一些数据
db.get("posts").push({ id: 1, title: "lowdb is awesome" }).write();

// db.set() 方法用于设置数据(如果数据不存在则创建, 如果存在则更新)
db.set("user.name", "typicode").write();

// 获取数据
console.log(db.get("posts").value());
const res1 = db.get("posts").find({ id: 1 }).value(); // 根据 id 查找数据

// 删除数据
db.get("posts").remove({ id: 1 }).write();
const res2 = db.get("posts").remove({ id: 3 }).write();
console.log("res", res2); // 返回被删除的数据

// 更新数据
db.get("posts").find({ id: 2 }).assign({ title: "lowdb is awesome" }).write();

3)shortid

  • 安装 npm i shortid

  • 使用 let id = shortid.generate();

9 MongoDB

MongoDB:应用程序数据平台

9.1 简介

  • MongoDB是一个基于分布式文件存储的数据库

  • 特性

    • 面向文档存储,基于JSON/BSON 可表示灵活的数据结构
    • 动态 DDL能力,没有强Schema约束,支持快速迭代
    • 高性能计算,提供基于内存的快速数据查询
    • 容易扩展,利用数据分片可以支持海量数据存储
    • 丰富的功能集,支持二级索引、强大的聚合管道功能,为开发者量身定做的功能,如数据自动老化、固定集合等等。
    • 跨平台版本、支持多语言SDK…

9.2 相关概念

image-20231001200456875

:key: 一般情况下

  • 一个项目使用一个数据库
  • 一个集合会存储同一种类型的数据
  • img

    • database 数据库,与SQL的数据库(database)概念相同,一个数据库包含多个集合(表)
    • collection 集合,相当于SQL中的表(table),一个集合可以存放多个文档(行)。 不同之处就在于集合的结构(schema)是动态的,不需要预先声明一个严格的表结构。更重要的是,默认情况下 MongoDB 并不会对写入的数据做任何schema的校验。
    • document 文档,相当于SQL中的行(row),一个文档由多个字段(列)组成,并采用bson(json)格式表示。
    • field 字段,相当于SQL中的列(column),相比普通column的差别在于field的类型可以更加灵活,比如支持嵌套的文档、数组。
    • 此外,MongoDB中字段的类型是固定的、区分大小写、并且文档中的字段也是有序的
  • img

    • _id 主键,MongoDB 默认使用一个_id 字段来保证文档的唯一性。
    • reference 引用,勉强可以对应于 外键(foreign key) 的概念,之所以是勉强是因为 reference 并没有实现任何外键的约束,而只是由客户端(driver)自动进行关联查询、转换的一个特殊类型。
    • view 视图,MongoDB 3.4 开始支持视图,和 SQL 的视图没有什么差异,视图是基于表/集合之上进行动态查询的一层对象,可以是虚拟的,也可以是物理的(物化视图)。
    • index 索引,与SQL 的索引相同。
    • $lookup,这是一个聚合操作符,可以用于实现类似 SQL-join 连接的功能
    • transaction 事务,从 MongoDB 4.0 版本开始,提供了对于事务的支持
    • aggregation 聚合,MongoDB 提供了强大的聚合计算框架,group by 是其中的一类聚合操作。

9.3 基本使用

9.3.1 安装

  • 下载地址:Download MongoDB Community Server | MongoDB

  • 建议选择zip类型,通用性更强

  • 配置步骤如下:

    1. 将压缩包移动到 C:\Program Files 下,然后解压
    2. 创建 C:\data\db 目录,mongodb会将数据默认保存在这个文件夹
    3. 以mongodb中 bin 目录作为工作目录,启动命令行
    4. 运行命令 mongod,如图证明启动成功

      • image-20231001201212956
    5. 运行命令 mongo

      • image-20231001201501363
  • 配置环境变量

    • image-20231001201744422

9.3.2 命令行交互

1)数据库命令

命令 说明
show dbs 显示所有数据库
db 显示当前所在的数据库
show collections 查询库中的连接
use xxx 创建数据库/切换数据库
db.dropDatabase() 删除当前的数据库
db.getName() 获取数据库名称
db.stats() 获取数据库状态
db.version() 获取数据库版本
db.getMongo() 查看当前数据库的链接机器地址
db.cloneDatabase("127.0.0.1") 从指定主机上克隆数据库
db.copyDatabase("yhb", "test1", "127.0.0.1") 从指定的机器上复制指定数据库数据到某个数据库

2)集合命令

命令 说明
db.createCollection("human" [,{"size":1024,capped:true,max:100}]) 创建一个集合
// 指定数据库大小size,满了,会删除旧文档
show collections 获取当前db中的所有集合
db.getCollection("human") 获取指定集合
db.<name>.renameCollection('xxx') 重命名集合
db.xxx.stats() 查看集合状态
db.xxx.drop() 删除某个集合
db.printCollectionStats() 显示当前db所有聚集索引的状态

3)文档命令

命令 说明
db.xxx.find() 查询所有记录
db.xxx.distinct("xxx") 查询去掉后的当前聚集集合中的某列的重复数据
db.xxx.find(查询条件) 查询文档
db.xxx.insert(xxx)
db.people.save(xxx)
插入文档
db.xxx.updata(查询条件, 新文档)
db.xxx.updata({name: xxx}, {$set: {age: 24}})
更新文档
db.xxx.remove(查询条件) 删除文档

9.4 Mongoose

  • Mongoose是一个对象文档模型库
    • Mongoose为模型提供了一种直接的,基于scheme结构去定义你的数据模型。它内置数据验证, 查询构建,业务逻辑钩子等,开箱即用
  • 方便使用代码操作mongodb数据库

9.4.1 安装

  • 安装 npm i mongoose

9.4.2 连接数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 1. 导入 mongoose 第三方模块
const mongoose = require("mongoose");

// 2. 连接 MongoDB 数据库
mongoose.connect("mongodb://127.0.0.1:27017/users");

// 3. 设置回调
// 语法: mongoose.connection.on("事件名称", 回调函数)
// 或者: mongoose.connection.once("事件名称", 回调函数) => 只执行一次, 适合写连接成功的回调
// 事件名称:
// 1. open => 连接成功; 2. error => 连接失败; 3. close => 断开连接
mongoose.connection.once("open", () => {
console.log("数据库连接成功");
});
mongoose.connection.on("error", () => {
console.log("数据库连接失败");
});
mongoose.connection.once("close", () => {
console.log("数据库断开连接");
});

// 4. 关闭数据库连接
setTimeout(() => {
mongoose.disconnect();
}, 2000);

9.4.3 文档操作

1)构建

1
2
3
4
5
6
7
8
9
10
11
const Book = mongoose.model('Book', yourSchema);

const small = new Tank({ name: '历险记' });
await small.save();

// or

await Tank.create({ name: '历险记' });

// or, 批量插入
await Tank.insertMany([{ name: '历险记' }]);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
mongoose.connection.once("open", () => {
// console.log("数据库连接成功");
// 1. 创建文档的结构对象(schema)
// 设置约束条件: 约束文档中的属性名和属性值的类型
const BookSchema = new mongoose.Schema({
name: String,
author: String,
price: Number,
});
// 2. 创建模型对象, 对文档操作的封装对象
const BookModel = mongoose.model("book", BookSchema);
// 3. 新增
BookModel.create({
name: "西游记",
author: "吴承恩",
price: 100,
})
.then((data) => {
console.log(data);
mongoose.disconnect(); // 关闭数据库连接
})
.catch((err) => {
console.log(err);
});
});

2)查询

可以使用 model 的 find、findById、findOne 或 where 静态函数检索文档

1
await Book.find({ name: '历险记').where('createdDate').gt(oneYearAgo).exec();
1
2
3
4
// 查询所有
Book.find().then((data) => {
console.log(data);
});

3)删除

模型具有静态 deleteOne()deleteMany() 函数,用于删除与给定 filter 匹配的所有文档

1
Book.deleteOne({ name: '历险记' });

4)更新

  • 修改不返回
1
await Book.updateOne({ name: '历险记' }, { name: '历险记II' });
  • 修改并返回
1
2
3
A.findOneAndUpdate(conditions, update, options)  // returns Query
A.findOneAndUpdate(conditions, update) // returns Query
A.findOneAndUpdate() // returns Query

5)条件控制

1
2
3
4
5
6
7
8
9
10
11
12
Book.find({ price: { $lt: 100 } }).then((data) => {
console.log("price search", data);
});
Book.find({ $or: [{ price: 100 }, { name: "三国演义" }] }).then((data) => {
console.log("or search", data);
});
Book.find({ name: /三/ }).then((data) => {
console.log("re search", data);
});
Book.find({ name: new RegExp("红") }).then((data) => {
console.log("RegExp search", data);
});

9.4.4 字段类型

类型 描述
String 字符串
Number 数字
Boolean 布尔值
Array 数组,也可以使用 [] 来标识
Date 日期
Buffer Buffer 对象
Mixed 任意类型,需要使用 mongoose.Schema.Types.Mixed 指定
ObjectID 对象ID,需要使用 mongoose.Schema.Types.ObjectID 指定
Decimal128 高精度数字,需要使用 mongoose.Schema.Types.Decimal128 指定
Map 集合,键值必须是字符串
UUID 通用唯一标识符,要创建 UUID,建议使用 Node 的内置 UUIDv4 生成器
BigInt 大整型

  • 字段项验证
验证值 说明
retuired: true 必填项
default: 'xxx' 默认值
enum: ['111', '222'] 枚举值
unique: true 唯一值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const BookSchema = new mongoose.Schema({
name: {
type: String,
required: true, // 必要的
},
author: {
type: String,
default: "匿名", // 默认值
},
price: {
type: Number,
enum: [20, 30, 40], // 枚举
},
boolID: {
type: Number,
unique: true, // 唯一值
},
});

9.4.5 个性化读取

1
2
3
4
5
6
7
8
Book.find({ price: { $gt: 0 } })
.select({ name: 1, price: 1, _id: 0 })
.sort({ price: 1 }) // 1: 升序; -1: 降序
.skip(1) // 跳过条数 (1条)
.limit(2) // 限制显示条数 (3条)
.then((data) => {
console.log("price search", data);
});

9.4.6 代码模块化

image-20231006181808839

  • index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 导入 db 文件
const db = require("./db/db");
const BookModel = require("./models/BookModel");
const mongoose = require("mongoose");

// 调用 db 函数
db(
() => {
// 具体操作内容, 如:
BookModel.find({ price: { $gt: 0 } })
.select({ name: 1, price: 1, _id: 0 })
.sort({ price: 1 }) // 1: 升序; -1: 降序
.skip(1) // 跳过条数 (1条)
.limit(2) // 限制显示条数 (3条)
.then((data) => {
console.log("price search", data);
});
},
// 可以省略
() => {
console.log("数据库连接失败....");
}
);
  • db.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
*
* @param {*} success 数据库连接成功的回调函数
* @param {*} error 数据库连接失败的回调函数
*/
module.exports = function (success, error) {
// 如果没有传递回调函数, 则使用默认的回调函数
if (typeof error !== "function") {
error = () => {
console.log("数据库连接失败");
};
}
// 1. 导入 mongoose 第三方模块
const mongoose = require("mongoose");
const { DBHOST, DBPORT, DBNAME } = require("../config/config");

// 2. 连接 MongoDB 数据库
mongoose.connect(`mongodb://${DBHOST}:${DBPORT}/${DBNAME}`);

// 3. 设置回调
// 1. open => 连接成功; 2. error => 连接失败; 3. close => 断开连接
mongoose.connection.once("open", () => {
success();
});
mongoose.connection.on("error", () => {
error();
});
mongoose.connection.once("close", () => {
console.log("数据库断开连接");
});
};
  • BookModel.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const mongoose = require("mongoose");

// 创建文档的结构对象
const BookSchema = new mongoose.Schema({
name: String,
author: String,
price: Number,
});

// 创建 model 对象
const Book = mongoose.model("book", BookSchema);

// 暴露模型对象
module.exports = Book;
  • config.js
1
2
3
4
5
module.exports = {
DBHOST: "127.0.0.1",
DBPORT: 27017,
DBNAME: "users",
};

9.5 图形化工具

9.6 优化记账本联系

9.6.1 日期对象转换 moment

  • 需求:2023-10-06 -> Object -> new Date()
  • Moment.js
  • 将时间字符串转换为时间对象,
    • moment('yyyy-mm-dd').toDate()
  • 格式化字符串
    • moment(time).format('YYYY-MM-DD')

10 API接口

10.1 简介

  • API(Application Program Interface),是前后端通信的桥梁

    • 简单理解:一个接口就是服务中的一个路由规则,根据请求响应结果,==一般为JSON格式==
    • 这里的接口指的是“数据接口”
  • 开发与调用

    • 后端开发
    • 一般情况下,前端调用
  • 组成

    • 请求方法,接口地址(URL)、请求参数、接口结果
  • 示例

    • image-20231006194245333

10.2 RESTful API

  • RESTful(Representational State Transfer) API是一种特殊风格的接口,主要特点有如下几个:
    • URL中的路径表示资源,路径中不能有动词,例如create,delete,update等这些都不能有
    • 操作资源要与HTTP请求方法对应
    • 操作结果要与HTTP响应状态码对应
  • 特点
    • REST 是 Representational State Transfer 的缩写,如果一个架构符合 REST 原则,就称它为 RESTful 架构
    • RESTful 架构可以充分的利用 HTTP 协议的各种功能,是 HTTP 协议的最佳实践
    • RESTful API 是一种软件架构风格、设计风格,可以让软件更加清晰,更简洁,更有层次,可维护性更好
  • 服务端回应数据
    • 客户端请求时,要明确告诉服务器,接受 JSON 格式,请求的 HTTP 头的 ACCEPT 属性要设成 application/json
    • 服务端返回的数据,不应该是纯文本,而应该是一个 JSON 对象。服务器回应的 HTTP 头的 Content-Type 属性要设为 application/json
    • 错误处理 如果状态码是4xx,就应该向用户返回出错信息。一般来说,返回的信息中将 error 作为键名,出错信息作为键值即可。 {error: “Invalid API key”}
    • 认证 RESTful API 应该是无状态,每个请求应该带有一些认证凭证。推荐使用 JWT 认证,并且使用 SSL
    • Hypermedia 即返回结果中提供链接,连向其他API方法,使得用户不查文档,也知道下一步应该做什么
  • image-20231006194616526

10.3 json-server

  • json-server

  • 操作步骤:

    1. 全局安装 npm i -g json-server

    2. 创建JS0N文件(db.json),编写基本结构

      1
      2
      3
      4
      5
      6
      7
      8
      {
      "song": [
      { "id": 1, "name": "青花瓷", "singer": "周杰伦" },
      { "id": 2, "name": "干杯", "singer": "五月天" },
      { "id": 3, "name": "当", "singer": "动力火车" },
      { "id": 4, "name": "不能说的秘密", "singer": "周杰伦" }
      ]
      }
    3. 以JS0文件所在文件夹作为工作目录,执行命令 json-server --watch db.json

      • 默认监听端口为3000
      • image-20231006195432404
  • 参数说明

参数 简写 默认值 说明
—config -c 指定配置文件 默认值 ‘json-server.json’
—port -p 设置端口 [默认值: 3000] Number
—host -H 设置域 [默认值: “0.0.0.0”] String
—watch -w 监听文件 是否监听
—routes -r 指定自定义路由
—middlewares -m 指定中间件 文件 [数组]
—static -s 设置静态文件路径 静态目录

10.4 接口测试工具 apipost

ApipostApifoxPostman

10.4.1 基本使用

image-20231006201828035

10.4.2 设置默认前缀

image-20231006201937664

image-20231006202010435

10.4.3 请求体

image-20231006202224663

10.4.4 公共参数与文档功能

  • 公共参数

    • 新建目录

    • 公共内容

      image-20231006202655163


  • 文档功能

10.5 优化记账本-API

  • ./routes/api/account.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
var express = require("express");
var router = express.Router();
const path = require("path");
const moment = require("moment"); // 用于格式化时间
const AccountModel = require("../../models/AccountModel");

// 记账本列表
router.get("/account", function (req, res, next) {
AccountModel.find()
.sort({ time: -1 })
.then((data) => {
// json格式返回
res.json({
code: "0000", // 状态码: 一般以20000或者0000表示成功
msg: "读取成功", // 提示信息
data: data, // 返回的数据
});
})
.catch((err) => {
res.json({
code: "1001",
msg: "读取失败",
data: null,
});
});
});

// 新增记录
router.post("/account", (req, res) => {
console.log(req.body);
AccountModel.create({
...req.body,
time: moment(req.body.time).toDate(), // 将时间字符串转换为时间对象, 解构赋值后新增同属性会覆盖
})
.then((data) => {
res.json({
code: "0000",
msg: "添加成功",
data: data,
});
})
.catch((err) => {
res.json({
code: "1002",
msg: "添加失败",
data: null,
});
});
});

// 删除账单
router.delete("/account/:id", function (req, res, next) {
AccountModel.deleteOne({ _id: req.params.id })
.then((data) => {
res.json({
code: "0000",
msg: "删除成功",
data: data,
});
})
.catch((err) => {
res.json({
code: "1003",
msg: "删除失败",
data: null,
});
});
});

// 获取单个账单信息
router.get("/account/:id", function (req, res, next) {
AccountModel.findById(req.params.id)
.then((data) => {
res.json({
code: "0000",
msg: "读取成功",
data: data,
});
})
.catch((err) => {
res.json({
code: "1004",
msg: "读取失败",
data: null,
});
});
});

// 更新账单信息
router.patch("/account/:id", function (req, res, next) {
AccountModel.updateOne({ _id: req.params.id }, req.body)
.then((data) => {
res.json({
code: "0000",
msg: "更新成功",
data: data,
});
})
.catch((err) => {
res.json({
code: "1005",
msg: "更新失败",
data: null,
});
});
});

module.exports = router;

11 会话控制

11.1 简介

  • 会话控制就是对会话进行控制
    • HTTP是一种无状态的协议,它没有办法区分多次的请求是否来自于同一个客户端,无法区分用户
    • 而产品中又大量存在的这样的需求,所以我们需要通过会话控制来解决该问题
  • 常见的会话控制技术有三种
    • cookie
    • session
    • token

11.2.1 简介

  • cookie是HTTP服务器发送到用户浏览器并保存在本地的一小块数据

    • cookie是保存在浏览器端的一小块数据
    • cookie是按照域名划分保存的
  • 特点:浏览器向服务器发送请求时,会自动将当前域名下可用的cookie设置在请求头中,然后传递给服务器

  • cookie设置响应报文 set-cookie:name=xxx

11.2.2 express操作cookie

1)增加

  • res.cookie('key', 'value', )
1
2
3
4
5
6
7
8
9
// 设置cookie
app.get("/set", (req, res) => {
res.cookie("username", "zhangsan");
res.cookie("pwd", "123", {
maxAge: 1000 * 60 * 60 * 24, // cookie存在时长, 1天
path: "/", // 默认值
});
res.send("设置cookie成功");
});
  • 测试结果:image-20231006212219191

2)删除

1
2
3
4
5
// 删除cookie
app.get("/del", (req, res) => {
res.clearCookie("username");
res.send("删除cookie成功");
});

cookie-parser

  • 安装 npm i cookie-parser

  • 插件导入

    1
    2
    const cookieParser = require("cookie-parser");
    app.use(cookieParser());
  • 读取 req.cookies

11.3 session

11.3.1 简介

  • session是保存在服务器端的一块儿数据,保存当前访问用户的相关信息
  • 作用:实现会话控制,可以识别用户的身份,快速获取当前用户的相关信息
  • 运行流程
    1. 填写账号和密码校验身份,校验通过后创建session信息,然后将session_id的值通过响应头返回给浏览器
    2. 有了cookie,下次发送请求时会自动携带cookie,服务器通过cookie中的session_id的值确定用户的身份

11.3.2 express-session

1)安装+配置

  • 安装 npm i express-session connect-mongo

    • express-session
    • connect-mongo
  • 简单配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    const express = require("express");
    const session = require("express-session");
    const MongoStore = require("connect-mongo");

    const app = express();

    app.use(
    session({
    name: "sid", // 设置cookie的name,默认值是:connect.sid
    secret: "bayyy", // 用来对session id相关的cookie进行签名(加盐)
    saveUninitialized: false, // 是否自动保存未初始化的会话,建议false
    resave: true, // 是否每次都重新保存会话
    store: MongoStore.create({
    mongoUrl: "mongodb://localhost:27017/users", // mongodb链接地址
    }),
    cookie: {
    httpOnly: true, // 开启后前端无法通过 JS 操作cookie
    maxAge: 1000 * 60 * 60 * 24, // 设置cookie过期时间(毫秒) -> 1天
    },
    })
    );

    app.get("/", (req, res) => {
    res.send("<h1>HOME</h1>");
    });

    app.listen(8000, () => {
    console.log("http://localhost:8000");
    });

2)session操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// session 创建
app.get("/login", (req, res) => {
// 当且仅当 username=admin&password=123 时, 登录成功, 并且设置session
if (req.query.username === "admin" && req.query.password === "123") {
// 设置session信息
req.session.username = "admin";
req.session.uid = "131";
res.send("<h1>登录成功</h1>");
} else {
res.send("<h1>登录失败</h1>");
}
});

// session 读取
app.get("/cart", (req, res) => {
if (req.session.username) {
res.send("<h1>购物车</h1>");
} else {
res.send("<h1>请登录</h1>");
}
});

// session 销毁
app.get("/logout", (req, res) => {
req.session.destroy((err) => {
if (err) {
console.log(err);
} else {
res.send("<h1>退出登录</h1>");
}
});
});

11.4 session和cookie读取

  • 存在的位置
    • cookie 浏览器端
    • session 服务端
  • 安全性
    • cookie是以明文的方式存放在客户端的,安全性相对较低
    • session存放于服务器中,所以安全性相对较好
  • 网络传输量
    • cookie设置内容过多会增大报文体积,会影响传输效率
    • session数据存储在服务器,只是通过cookie传递id,所以不影响传输效率
  • 存储限制
    • 浏览器限制单个cookie保存的数据不能超过4K,且单个域名下的存储数量也有限制
    • session数据存储在服务器中,所以没有这些限制

11.5 优化记账本-注册功能

11.5.1 密码加密 MD5

  • 安装 npm i md5

  • 使用

    1
    2
    const md5 = require("md5");
    let pwd = md5(xxx)

11.5.2 CSRF跨站请求伪造

  • link href 等进行对其他(跨域)网站直接操作,会导致携带其他网站的cookie

  • 解决 操作使用post请求

    • ```html
      <button href="/logout">退出登录</button>
      
      </form>
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16

      ### 11.5.3 公益404

      - 404.html

      ```html
      <!DOCTYPE html>
      <html lang="en">
      <head>
      <meta charset="UTF-8" />
      <title>error</title>
      </head>
      <body>
      <script src="//volunteer.cdn-go.cn/404/latest/404.js"></script>
      </body>
      </html>

11.6 token

11.6.1 简介

  • token是服务端生成返回给HTTP客户端的一串加密字符串,token中保存着用户信息
  • 作用:实现会话控制,可以识别用户的身份,主要用于移动端APP
  • 工作流程

    • 填写账号和密码校验身份,校验通过后响应token,token一般是在响应体中返回给客户端的
    • 后续发送请求时,需要手动将token添加在请求报文中,一般是放在请求头
  • 特点

    • 服务端压力更小
      • 数据存储在客户端
    • 相对更安全
      • 数据加密
      • 可以避免CSRF(跨站请求伪造)
    • 扩展性更强
      • 服务间可以共享
      • 增加服务节点更简单

11.6.2 JWT

  • JWT(JSON Web Token)是目前最流行的跨域认证解决方案,可用于基于token的身份验证

    • JWT使token的生成与校验更规范
    • 我们可以使用jsonwebtoken包来操作token
    • jsonwebtoken - npm
  • 安装 npm i jsonwebtoken

  • 使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 导入 jwt
    const jwt = require("jsonwebtoken");

    // 创建(生成) token
    // let token = jwt.sign(用户数据, 加密字符串, 配置对象);
    const token = jwt.sign(
    {
    username: "zhangsan",
    },
    "bayyy",
    {
    expiresIn: 3, // 过期时间(单位: 秒)
    }
    );

    // 校验 token
    jwt.verify(token, "bayyy", (err, data) => {
    if (err) {
    console.log(err);
    }
    console.log(data);
    });

11.7 记账本-token

11.7.1 本地域名

  • 所谓本地域名就是只能在本机使用的域名,一般在开发阶段使用
  • 操作流程
    • 编辑文件 C:\Windows\System32\drivers\etc\hosts
    • 127.0.0.1 www.bayyy.com
      • 如果修改失败,需要修改该文件的权限
  • 端口号 监听 80 默认端口,无需在域名后标注

11.7.2 HTTPS

  • https本意是http+SSL(Secure Sockets Layer安全套接层)

    • https可以加密HTTP报文,所以也可以理解为是安全的HTTP
  • 工具官网:Certbot (eff.org)

  • 操作流程

    1. 下载工具 https://github.com/certbot/certbot/releases/latest/download/certbot-beta-installer-win_amd64_signed.exe

    2. 安装工具

    3. 管理员运行命令 certbot certonly-standalone

    4. 代码配置如下

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      const https = require("https");
      https
      .createServer(
      {
      key: fs.readFileSync("/etc/letsencrypt/path/to/privkey.pem"),
      cert: fs.readFileSync("/etc/letsencrypt/path/to/cert.pem"),
      ca: fs.readFileSync("/etc/letsencrypt/path/to/chain.pem"),
      },
      app
      )
      .listen(443, () => {
      console.log("HTTPS Server is running on: https://localhost:%s", 443);
      });
    5. 证书更新,有效期为3个月

      1
      2
      3
      4
      ## 一般更新
      certbot renew
      ## 强制更新
      certbot --force-renewal

11.8 前后端开发扩展介绍

image-20231007121204854