node express mysql koa学习笔记

Enode.js 是基于v8 JavaScript引擎的 JavaScript运行时环境

任何可以使用 JavaScript来实现的应用都最终都会 使用 JavaScript 实现

1、全局对象和模块化开发

1.1 给node 程序传递参数

当我们 执行 node ./index.js 的时候其实,后面可以拼接参数,参数就是 node 的全局对象 process ,比如 node ./index.js ddg age=20

  argv: [
    'E:\\node\\node.exe',
    'F:\\web前端开发\\自学\\coderwhy-node\\code\\01_learn-node\\02_给node传递参数\\index.js',
    'ddg',
    'age=20'
  ],
// console.log(process);
// process 是 node 的 全局对象
// process 第一层节点,有一个 argv 节点, 它表示的就是参数
// argv : argument vector 的缩写,表示 传入的具体参数
console.log(process.argv);

process.argv.forEach(item => {
    console.log(item);
})
1.2 node 的输出方式
  • console.log() 最常用的输入内容的方式 console.log
  • 清空控制台 console.clear
  • console.trace() 查看函数的 调用栈
// console.log(process);
// process 是 node 的 全局对象
// process 第一层节点,有一个 argv 节点, 它表示的就是参数
// argv : argument vector 的缩写,表示 传入的具体参数
console.log(process.argv);

console.log(process.argv[2]);
console.log(process.argv[3]);

console.clear() // 清空 控制台

process.argv.forEach(item => {
    console.log(item);
})

function foo() {
    bar()
}
function bar() {
    console.trace()
    // trace 是可以打印 函数 的 调用栈的 

    // 输出结构如下
    // at bar 表示 这个输出 在 bar 函数 里面执行
    // at foo 表示  bar 函数 是在 foo 函数里面被调用
    // at Object 表示 foo 在全局调用,  node 会把他当成 匿名函数调用
    /* 
    Trace
    at bar (F:\web前端开发\自学\coderwhy-node\code\01_learn-node\02_给node传递参数\02_Node程序的输出.js:20:13)
    at foo (F:\web前端开发\自学\coderwhy-node\code\01_learn-node\02_给node传递参数\02_Node程序的输出.js:17:5)
    at Object. (F:\web前端开发\自学\coderwhy-node\code\01_learn-node\02_给node传递参数\02_Node程序的输出.js:36:1)
    at Module._compile (internal/modules/cjs/loader.js:1133:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1153:10)
    at Module.load (internal/modules/cjs/loader.js:977:32)
    at Function.Module._load (internal/modules/cjs/loader.js:877:14)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:74:12)
    at internal/main/run_main_module.js:18:47
    */
}
foo()

node的输出方式官方文档

1.3 node 全局对象
1.3.1 特殊的全局对象

这些全局对象实际上是模块中的变量,只是每个模块都有,看起来像是全局变量,在命令行交互中是不可以使用的

包括 __dirname、__filename、exports、module、require()

  • dirname: 获取当前文件所在的路径,不包括后面的文件名
  • filename 获取当前文件所在的路径和文件名称,包括文件名称
console.log(__dirname);
// F:\web前端开发\自学\coderwhy-node\code\01_learn-node\03_node中的全局变量
console.log(__filename);
// F:\web前端开发\自学\coderwhy-node\code\01_learn-node\03_node中的全局变量\01_特殊的全局对象.js
1.3.2 常见全局对象
process对象
  • process 提供了 node 进程中相关的信息
  • 后面在项目中,我也会讲解,如何将一些环境变量读取到 process 的 env 中
  • node 的运行环境、参数信息等等

console.log

可以看官方文档


定时器函数
  • setTimeout(callback,delay[,…args]):callback,在 delay毫秒后执行一次
  • setIneterval(callback,delay[,…args]):callback 每 delay 毫秒重复执行一次
  • setImmediate(callback[,…args]):callback I/O 事件后的回调的 “立即” 执行
  • process.nextTick(callback[,…args]):添加到下一次 tick 队列中

global 全局对象
console.log(global);
console.log(global.process);
// 定义变量
var name = "呆呆狗"
console.log(name);
console.log(global.name);// undefined
// node 的顶级对象 是 global  ,浏览器的顶级对象是 window,我们在浏览器运行的js文件中var定义的全局变量,是会被挂载到 window 上的,而 global 不会
// global 默认会挂载 process
// 再 node 中,其实每一个文件都是一个模块

2、js 的模块化

2.1 common.js 和 node

commonJS 是一个 规范,最初提出来是再浏览器以外的地方使用,并且当时被命名为 Server.js,后来为了体现他的广泛性,修改为 CommonJS , 平时我们也会简称为 CJS

  • node 是 commonJS 在服务器端 一个具有代表性的实现
  • Browserify 是 CommonJS 在浏览器中的一种实现
  • webpack 打包工具具备对 CommonJS 的支持和转换

node 中每一个 js文件都是一个单独的模块,这个模块包括 CommonJS 规范的核心变量 :export module.exports require

  • exports 和 module.exports 是负责对模块中的内容进行导出
  • require 函数 可以帮助我们导入 其他模块 (自定义模块、系统模块、第三方库模块) 中的内容

// bar.js
const name = "coderwhy"
const age =20

function sayHello(name){
  console.log('hello' + name);
}

exports.name = name
exports.age = age
exports.sayHello = sayHello
// main.js
const bar = require('./bar')
// 就相当于  bar = exports
// bar  也可以 换成 { name . age .sayHello } 解构赋值
console.log(bar);//{ name: 'coderwhy', age: 20, sayHello: [Function: sayHello] }

console.log(bar.name);
console.log(bar.age);

bar.sayHello('ddg')
2.2 module.exports
  • commonJS 中是没有 module.exports 的概念的
  • 为了实现模块的导出,node 中使用的是 module 的类,每一个模块都是 module 的一个实例 也就是 module
  • 在node 中真正用于导出的其实根本不是 exports 而是 module.exports
  • 因为 module 才是真正的实现者

其实就是导出的 module,源码中 module.exports = exports

2.3 require

查找规则

  1. require(传入的是核心模块 fs path 等等) ,则会直接返回,并且停止查找
  2. require(./或者…/ 或者 、/ 根目录) 开头的。没有后缀名则会 直接查找 这个文件 ;查找 文件.js文件 ; 查找 文件.json 文件;查找 文件.node 文件。 如果直接传一个目录,则会先找这个目录下的 index.js 文件 ; index.json 文件 ; index.node 文件。 如果 这两者都没找到 则报错
  3. 直接是一个字符串 比如 require(‘abc’) ,abc 不是核心模块,则会从当前目录下的 node_modules 文件夹下查找,没有找到 再去上一级目录下的 node_modules 下查找,直到 根目录下的 node_modules 根目录在没找到则会报错
    paths: [
      'F:\\web前端开发\\自学\\coderwhy-node\\code\\01_learn-node\\04_js-module\\02_commonjs\\node_modules',
      'F:\\web前端开发\\自学\\coderwhy-node\\code\\01_learn-node\\node_modules',
      'F:\\web前端开发\\自学\\coderwhy-node\\code\\node_modules',
      'F:\\web前端开发\\自学\\coderwhy-node\\node_modules',
      'F:\\web前端开发\\自学\\node_modules',
      'F:\\web前端开发\\node_modules',
      'F:\\node_modules'
    ]

// console.log(module)
2.4 模块加载的过程
  • 模块在被第一次引入的时候,模块中的js代码会被运行一次。是同步执行的
  • 模块被多次引入时,会缓存,最终只会被加载(运行)一次,因为每个模块对象module 都有一个属性:loaded ,为 false 的时候,表示没有被加载,true 表示已经被加载了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZKm8W7UQ-1625664086040)(F:\web前端开发\自学\coderwhy-node\coderwhy-node.assets\image-20210629185221783.png)]

此图中,就是循环引入, 就是 图 数据结构, 图结构在遍历的时候,node 采用了 深度优先搜索,所以 main => aaa => ccc => ddd => eee

走到 eee 里面没有引入了,会返回上一层,看看上一层还有没有引入其他的,没有在返回,直到 main,看到还引入了 bbb 则 引入 bbb => ccc

2.5 AMD 规范

AMD 主要是应用于浏览器的一种模块化规范

它采用的是 异步加载模块

事实上 AMD 的规范 还要早于 CommonJS,但是 CommonJS目前依然在被使用,而 AMD 使用的较少了

用起来 是 有点复杂了。需要引入一个 require.js

2.6 CMD 规范

也是一个异步加载模块

但是他将 commonJS 的 优点吸收过来了

seajs 需要用这个依赖文件

2.7 es module

他是 js 的一个模块化系统

  • 它采用了 import 和 export 关键字
  • 采用编译期的静态分析,并且也加入了动态引用的方式
  • export 负责将模块的内容导出
  • import 负责从其他模块导入内容

导出的方式

  • export const age = 20
  • export { } 这只是一个大括号,不是一个对象,这个大括号放置要导出的引用列表
  • export { name as myName } 可以给 name 起一个别名

导入的方式

  • import { name } from ‘路径’ 按需导入
  • import { name as heName } from ‘路径’ 也可以起别名
  • import * as foo from ‘路径’ 把导出的东西,都放到 foo 对象里面

在开发和封装功能的时候,通常希望将暴露 的所有接口放到一个文件中

export {  } from './bar.js'
// 这样表示的是,先导入,然后 再导出

默认导出

export default function (){}

import format from ‘./’

我们可以直接给这个默认导出的函数起一个名字


如果 把 import 加载一个模块,放到 逻辑代码中,是不可以的

因为 es module 在被 JS 引擎解析时,就必须要知道他的依赖关系,由于这个时候,无法执行,所以不能确定依赖关系,所有会报错

let flag = true
if (flag) {
  import('./bar.js').then(aaa => {
    console.log()
  })
}

3、常见内置模块

3.1 path
const path = require('path')

// 1、获取路径的信息
const filepath = '/User/ddg/abc.md'

console.log(path.dirname(filepath));
// /User/ddg
console.log(path.basename(filepath)); 
// abc.md
console.log(path.extname(filepath)); 
// .md

// 2、join 路径拼接
const basepath = "/User/ddg"
const filename= '/abc.md'
const joinUrl = path.join(basepath,filename)
console.log(joinUrl);//  \User\ddg\abc.md

// 3、resolve 路径拼接
// 它会解析第一个参数 的 / ./ ../ ,
// / 表示这个文件地址的根磁盘
// 如果第一个参数 没有加 / ./ ../ 那么,则会拼接上 绝对路径
// 如果第一个往后的参数有 / 则会 直接返回最后一个参数的路径
const joinUrl2 = path.resolve(basepath,filename)
console.log(joinUrl2);//  F:\User\ddg\abc.md

3.2 fs

读取文件信息的三种方式

const fs = require('fs')

// 读取文件的信息
const filepath = './abc.txt'

// 1、同步操作  读取文件信息
const info = fs.statSync(filepath)
// console.log('后续要执行的代码会被阻塞');
// console.log(info);

// 2、异步操作  读取文件信息
fs.stat(filepath, (err, info) => { 
  if(err){
    console.log(err);
    return
  }
  console.log(info);
})
// console.log('后续要执行的代码会被阻塞');

// 3、promise
fs.promises.stat(filepath).then(info=>{
  console.log(info);
}).catch((err)=>{{console.log(err);}})
console.log('后续要执行的代码会被阻塞');

文件描述符

const fs = require('fs')

fs.open('./abc.txt', (err, fd) => {
  if (err) {
    console.log(err);
    return
  }
  // 通过描述符获取信息
  fs.fstat(fd, (err, info) => { 
    if(err){
      console.log(err);
      return
    }
    console.log(info);
  })
})

文件的读写

const fs = require('fs')

// 文件写入
fs.writeFile('./abc.txt','呆呆狗2',{flag:"a"},err=>{
  console.log(err);
})
/* 
  1、 w 打开文件写入 默认值。 会覆盖掉原先的内容
  2、 w+ 打开文件进行读写,如果不存在则创建文件
  3、 r+ 打开文件进行读写,如果不存在那么抛出异常
  4、 r 打开文件读取,读取时的默认值
  5、 a 打开要写入的文件,将流放在文件末尾。如果不存在则创建文件
  6、 a+ 打开文件以进行读写,将流放在文件末尾,如果不存在则创建文件
*/

// 文件读取
fs.readFile('./abc.txt',{encoding:'utf-8'},(err,data)=>{
  console.log(data);
  // 不设置 encoding:'utf-8' 就显示二进制的编码
})

对文件夹的操作

const fs = require('fs')

// 1、创建文件夹
const dirname = './ddg'
if (!fs.existsSync(dirname)) {
  fs.mkdir(dirname,err=>{
    console.log(err);
  })
}

// 2、读取文件夹中的所有文件
fs.readdir(dirname,(err,files)=>{
  console.log(files);// [ 'a.txt', 'b.txt' ]
})

// 3、重命名 文件
// 旧路径  新路径  回调函数
fs.rename('./ddg','./ddg2',err=>{
  console.log(err);
})
3.3 events 模块

node中的核心API 都是基于异步事件驱动的。

在这个体系中,某些对象(发射器(Emiteers))发出某一个事件

我们可以监听这个事件 (监听器 Listeners),并且传入的回调函数,这个回调函数会在监听到事件时调用

发出事件和监听事件 都是通过 EventEmitter 类 来完成的,他们都属于 events 对象

emitter.on(eventName,listener): // 监听事件,也可以使用 addListener
emitter.off(eventName,listener): // 移除事件,也可以使用 removeListener
emitter.emit(eventName[,...args]): // 发出事件,可以携带一些参数

4、包管理工具

4.1 配置文件

每一个项目都对应一个配置文件(package.json),包括项目名称、使用的插件、版本号、项目描述等等

npm init -y
4.2 配置文件常见的属性
  • scripts属性,用于配置一些脚本命令,以键值对的形式存在,配置后我们可以通过 npm run 命令的key 来执行这个命令
  • dependencies 属性 是无论开发环境还是生产环境都需要的依赖包
  • devDependencies属性 是 生产环境不需要的依赖包
4.3 版本管理的问题

比如版本 2.23.8

  • 第一个数字,表示可能不兼容之前的版本,这个版本号一般很少修改
  • 第二个数字,向下兼容的功能性新增,就是 新功能增加,但是兼容之前的版本
  • 第三个数字,前面两个没有任何修改,但是 修复了 之前的 bug

^2.23.8 表示 第一个数保持不变的, 第二个和第三个 永远安装最新的版本

~2.23.8 表示 第一个和第二个保持不变的,第三个永远安装最新的版本

5、buffer 和 浏览器的事件循环

5.1 数据的二进制

计算机追踪所有的内容:文字、数字、图片、音频、视频都终会使用二进制来表示

5.2 buffer 和二进制

对于前端来说,很少会和二进制打交道,但是对于服务器端为了做很多的功能,必须直接去操作二进制的数据

所以 node 为了 可以方便开发者完成更多功能,提供给了我们一个类 Buffer , 并且它是全局的

Buffer 看成是一个存储二进制的数组,数组中的每一项,可以保存 8位 二进制

5.3 Buffer 对字符串的存储
const message = "hello world"

// 第一种 1、创建 Buffer
// 打印出来的,是和 message 一一对应的,打印出来的 每一组数字,是 16进制的
//const buffer = new Buffer(message)
//console.log(buffer); //

// 第二种 2、
const buffer2 = Buffer.from(message)
console.log(buffer2);
5.4 Buffer 对文字的存储
const message = "呆呆狗"
// 其实 一个汉字 对应 三个字节码,
const buffer2 = Buffer.from(message)
console.log(buffer2);
// 

// 解码
console.log(buffer2.toString());// 呆呆狗
5.5 对 文件的处理
const fs = require('fs')

// 1、对文本文件的操作
fs.readFile("./foo.txt", { encoding: 'utf-8' }, (err, data) => {
  console.log(data);
  // 如果不传 utf-8 输出的,其实是  Buffer。本质上,我们读取到的东西都是二进制
})

// 2、对 图片的操作
fs.readFile('./one.png', (err, data) => {
  console.log(data); // 读取到的 也是 Buffer

  // 我们可以在写入 文件
  fs.writeFile('./one_copy.png', data, err => {
    // 这里的 data 表示要写入谁  就是 图片的 Buffer
    console.log(err);
  })
})

// npm i sharp 先安装依赖,然后 裁剪,然后输出
sharp('./one.png').resize(200,200).toFile('./baz.png')

6、事件循环

6.1 事件循环定义

事件循环 可以理解成 我们编写 js 代码 和 浏览器 或者 node 之间的一个桥梁

浏览器的事件循环是 一个我们编写的 js 代码和 浏览器 API 调用 的一个桥梁,桥梁之间他们通过回调函数进行沟通

node 的事件循环是一个我们编写 js 代码 和 系统调用 (file、system、network) 之间的一个桥梁,桥梁 之间他们通过回调函数进行沟通的

6.2 进程和线程

进程: 计算机已经运行的程序

线程:操作系统能够运行运算调度的最小单位

进程更像是线程的容器

操作系统就像是一个工厂,工厂里面有很多车间,这个车间就是进程,工人就是线程

6.3 多进程多线程开发
6.4 浏览器 事件循环 面试题

第一道

setTimeout(function () {
  console.log("set1");
  new Promise(function (resolve) {
    resolve();
  }).then(function () {
    new Promise(function (resolve) {
      resolve();
    }).then(function () {
      console.log("then4");
    });
    console.log("then2");
  });
});

new Promise(function (resolve) {
  console.log("pr1");
  resolve();
}).then(function () {
  console.log("then1");
});

setTimeout(function () {
  console.log("set2");
});

console.log(2);

queueMicrotask(() => {
  console.log("queueMicrotask1")
});

new Promise(function (resolve) {
  resolve();
}).then(function () {
  console.log("then3");
});

// pr1
// 2
// then1
// queuemicrotask1
// then3
// set1
// then2
// then4
// set2

分析:

  1. 先输出 pr1 , then1 进入 微任务队列
  2. 输出 2 , queueMicrotask 创建一个微任务
  3. then3 加入微任务
  4. 主线程 执行完毕,执行 第一波微任务 依次输出 then1,queueMicrotask,then3,此时已经没有微任务了,继续执行下一个宏任务
  5. 第一个定时器, 输出 set1 ,把 第一个定时器的 then 加入微任务队列
  6. 输出 then2
  7. 这一次的宏任务执行完毕,继续执行这一次的微任务,输出 then4
  8. 执行第二个定时器

第二道

async function async1 () {
  console.log('async1 start')
  await async2();
  console.log('async1 end')
}
 
async function async2 () {
  console.log('async2')
}

console.log('script start')

setTimeout(function () {
  console.log('setTimeout')
}, 0)
 
async1();
 
new Promise (function (resolve) {
  console.log('promise1')
  resolve();
}).then (function () {
  console.log('promise2')
})

console.log('script end')


// script start
// async1 start
// async2
// promise1
// script end
// aysnc1 end
// promise2
// setToueout

注意:await 下面的代码是微任务

6.5 阻塞IO 和 非阻塞 IO

如果我们 希望在程序中对一个文件进行操作,那么我们就需要打开这个文件内: 通过 文件描述符

js 可以对一个文件进行操作嘛?

看起来是可以的,但是事实上我们任何程序中的文件操作 都是 需要进行 系统调用

事实上,对文件的操作,是一个操作系统的系统调用 (IO系统,IO是输入、输出)

操作系统为我们提供了两种调用方式 :阻塞时调用 和 非阻塞式调用

阻塞式:调用结果返回之前,当前线程处于阻塞态(阻塞态CPU是不会分配时间片的),调用线程只有在得到调用结果之后,才会继续执行

非阻塞式:带哦用执行之后,当前线程不会停止执行,只需要过一段时间来检查有没有结果返回即可

所以在开发中的很多 耗时操作,都可以基于这样的 非阻塞式 调用

  • 比如 网络请求本身使用了 Socket通信,而 Socket 通信 本身提供了 select 模型,可以进行非阻塞方式的工作
  • 比如文件读写的 IO 操作,我们可以使用操作系统提供的基于事件的回调机制

非阻塞式调用 也存在一定问题,,我们不一定获取到 需要 读取的结果。不知道返回来的数据是否是完整的,所以为了知道是否读取完成,我们需要频繁的去确定读取到的数据是否是完整的,这个过程称之为 轮询操作

这个轮询工作有谁完整?

我们开发中不只是一个文件的读写,可能是多个文件,也可能是 网络的IO、数据库的IO、子线程调用

libuv 提供了一个 线程池

  • 线程池会负责所有相关的操作,并会通过轮询或者其他方式等待结果
  • 获取到结果后,就可以将对应的回调放到事件循环(某一个队列)中
  • 事件循环就可以负责接管后续的回调工作,告知 JS 应用程序执行对应的回调函数
6.6 node 的事件循环

一次事件循环,就是一次 tick

微任务队列

  • promcess.nextTick
  • promise 的 then 回调 queueMicrotask

事件循环 执行顺序就是

  1. ticks 队列 process.nextTick
  2. 其他微任务队列 promise 的then queueMicrotask
  3. timers setTimeout setInterval
  4. IO 队列
  5. check 队列 , setImmediate 队列
  6. close 队列 close 事件

面试题1

async function async1() {
  console.log('async1 start')
  await async2()
  console.log('async1 end')
}

async function async2() {
  console.log('async2')
}

console.log('script start')

setTimeout(function () {
  console.log('setTimeout0')
}, 0)

setTimeout(function () {
  console.log('setTimeout2')
}, 300)

setImmediate(() => console.log('setImmediate'));

process.nextTick(() => console.log('nextTick1'));

async1();

process.nextTick(() => console.log('nextTick2'));

new Promise(function (resolve) {
  console.log('promise1')
  resolve();
  console.log('promise2')
}).then(function () {
  console.log('promise3')
})

console.log('script end')


// script start
// async1 start
// async2
// promise1
// promise2
// script end
// nextTick1
// nextTick2
// async1 end
//  promise3
// setTimeout0
// setImmediate
// setTimeout2

第二道

setTimeout(() => {
  console.log("setTimeout");
}, 0);

setImmediate(() => {
  console.log("setImmediate");
});

// 问题: setTimeout setImmediate

不一定先输出谁

执行 main script 也需要事件,执行完以后 要 初始化事件循环 也需要事件

初始化事件循环 和 main script 是同时进行的,但事件循环初始化完毕后,就要执行事件循环了

  • 假如 我们 执行 main script 需要10ms 初始化事件循环 20ms ,然后执行 事件循环,但 第一个定时器是 0MS 执行,所以 他已经放在里面,可以拿出来立即执行。 因为 初始化事件循环完毕后,main script 已经执行完毕了
  • 假如我们 我们 执行 main script 需要10ms 初始化事件循环 5ms , 定时器又是宏任务,而 setImmediate

不好理解。。。。。

6.7 stream 流

读取文件

const fs = require('fs');

// 传统的方式
// fs.readFile('./foo.txt', (err, data) => {
//   console.log(data);
// });

// 流的方式读取
const reader = fs.createReadStream("./foo.txt", {
  start: 3,
  end: 10,
  // 每次读都少,默认是  64KB
  highWaterMark: 2
});

// 数据读取的过程
reader.on("data", (data) => {
  console.log(data);
 // 暂停
  reader.pause();

  setTimeout(() => {
    // 恢复
    reader.resume();
  }, 1000);
});

reader.on('open', () => {
  console.log("文件被打开");
})

reader.on('close', () => {
  console.log("文件被关闭");
})
// 先打开,然后  读取文件,读取完以后,关闭

写入文件

const fs = require('fs');

// 传统的写入方式
// fs.writeFile("./bar.txt", "Hello Stream", {flag: "a"}, (err) => {
//   console.log(err);
// });

// Stream的写入方式
const writer = fs.createWriteStream('./bar.txt', {
  flags: "r+",
  // 从哪里开始追加
  start: 4
});

writer.write("你好啊", (err) => {
  if (err) {
    console.log(err);
    return;
  }
  console.log("写入成功");
});

writer.write("李银河", (err) => {
  console.log("第二次写入");
})
// 不调用关闭,他会一直处于打开状态,可以一直写入内容

// writer.close();   很少用 close 调用
// write("Hello World");
// close();
writer.end("Hello World"); // end 表示  先写入,再结束

writer.on('close', () => {
  console.log("文件被关闭");
})

文件复制

const fs = require('fs');

// 传统的写法
// fs.readFile('./bar.txt', (err, data) => {
//   fs.writeFile('./baz.txt', data, (err) => {
//     console.log(err);
//   })
// })

// Stream的写法
const reader = fs.createReadStream("./foo.txt");
const writer = fs.createWriteStream('./foz.txt');

reader.pipe(writer); // 把读到的流放入 一个文件中
writer.close();

7、http 开发 web 服务器

7.1 http 内置模块 创建服务器
const http = require('http')

const server = http.createServer((req, res) => {
  res.end('hello world')
})

server.listen(8000, () => {
  // 端口 尽量不要写 1024 以下的
  console.log('启动成功');
})
7.2 创建 web 服务器的方式
// 第一种
const http = require('http')

const server = http.createServer((req, res) => {
  res.end('hello world')
})

server.listen(8000, () => {
  // 端口 尽量不要写 1024 以下的
  console.log('server1启动成功');
})

// 第二种
const server2 = new http.Server((req, res) => {
  res.end('Server2')
})
server2.listen(8001, () => {
  console.log('server2启动成功');
  // 端口话如果不写,系统会默认分配一个,项目中一般都会写到环境变量中
  // 也可以通过  server2.address().port  来获取到系统分配的端口号
})

7.3 request 对象分析 和 request 的 url 分析
const http = require('http')
const url = require('url')
const qs = require('querystring')
const server = http.createServer((req, res) => {
  // request 对象中封装了客户端给我们服务器传递过来的 所有信息
  console.log(req.url);
  console.log(req.method);
  console.log(req.headers);// 请求头

  // if (req.url === '/login') {
  //   res.end('欢迎回来')
  // } else if (req.url === '/users') {
  //   res.end('用户列表')
  // } else {
  //   res.end('错误请求,检查~')
  // }
  // 如果 遇到/login?username=呆呆狗&password=123 呢
  // 最好的方式是用   内置模块 url
  const ressult = url.parse(req.url)
  console.log(ressult); // 有一个 pathname 这个表示基准路径,query表示?后面的东西
  // 如果我们想获取后面的查询参数,则可以根据内置模块 querystring
  const qsSecond = qs.parse(ressult.query) // 会反应一个对象,键值对形式
  console.log(qsSecond);
  res.end('hello world')
})

server.listen(8000, () => {
  // 端口 尽量不要写 1024 以下的
  console.log('启动成功');
})
7.4 request对象-method 的分析
const http = require('http')
const url = require('url')
const qs = require('querystring')
const server = http.createServer((req, res) => {
  // request 对象中封装了客户端给我们服务器传递过来的 所有信息
  const { pathname } = url.parse(req.url)
  if (req.method === 'POST') {
    // 拿到 body 的数据
    req.setEncoding('utf-8')
    req.on('data', (data) => {
      console.log(data); // 这里的 data 是字符串
      let dataNew = JSON.parse(data)
    })
    // 可以通过监听这个事件,获取post 的 body值,获取的是 Buffer

    // 1、data.toString()
    // 2、在监听事件前面声明, req.setEncoding('utf-8') utf-8 表示文件
    // 图片视频 等等,要设置成 req.setEncoding('binary')

  }
  res.end('hello world')
})

server.listen(8000, () => {
  // 端口 尽量不要写 1024 以下的
  console.log('启动成功');
})
7.5 request对象-headers

content-type : 是这次请求携带的数据的类型

  • application/json 表示一个json 类型
  • text/plain 表示是文本类型
  • application/xml 表示是 xml 类型文件
  • multipart/form-data 表示是上传文件

content-length :文件的大小和长度

connection : 的 keep-alive

http是基于 TCP 协议的,但是通常在进行一次请求和响应结束后会立刻中断

在 http1.0 中,如果想要继续保持链接

  • 浏览器要在请求头添加 connection : keep-alive
  • 服务器需要在响应头添加 connection:keep-alive
  • 客户端再次发起请求时,就会使用同一个链接,直接一方中断连接

在 http1.1中,所有连接默认是 connection : keep-alive 的

不同的 web 服务器会有不同的保持 keep-alive 的时间

node 默认是 5S

7.6 response 对象-响应结果
const http = require('http')

const server = http.createServer((req, res) => {

  // res.end('hello world')
  res.write("响应结果一")
  res.end()
  // res.end 其实是 执行两个东西,一个是写入结果,一个是 res.end()
})

server.listen(8000, () => {
  // 端口 尽量不要写 1024 以下的
  console.log('启动成功');
})
7.7 response 对象-响应码
const http = require('http')

const server = http.createServer((req, res) => {
  // 设置状态码
  // 方式一: 直接给属性赋值
  res.statusCode = 401
  // 方式二:和 head 一起设置
  res.writeHead(503, {

  })


  // res.end('hello world')
  res.write("响应结果一")
  res.end()
  // res.end 其实是 执行两个东西,一个是写入结果,一个是 res.end()
})

server.listen(8000, () => {
  // 端口 尽量不要写 1024 以下的
  console.log('启动成功');
})
7.8 response 对象-响应的header
const http = require('http')

const server = http.createServer((req, res) => {
  // 响应的hader
  // 设置方式 1
  res.setHeader("Content-Type", "text/plain;charset=utf8")
  // 设置方式2
  res.writeHead(200, {
    // 第一个参数是状态码,第二个 可以设置  响应的hader
    "Content-Type": 'text/plain;charset=utf8',
    // 如果我们要返回一个 '

你好

' 呢?
// 这就要设置成 text/html;charset=utf-8 了 }) // res.end('hello world') res.write("响应结果一") res.end() // res.end 其实是 执行两个东西,一个是写入结果,一个是 res.end() }) server.listen(8000, () => { // 端口 尽量不要写 1024 以下的 console.log('启动成功'); })
7.9 http模板发送 网络请求
const http = require('http')

http.get('http://localhost:8888', (res) => {
  res.on('data', (data) => {
    // 获取 请求的结果
    console.log(data.toString());
  })
  res.on('end', () => {
    // 监听是否获取到了所有的结果i
    console.log('获取到了所有的结果');
  })
})

// http 没有 http.post 方法
const result = http.request({
  method: 'POST',
  hostname: 'localhost',
  port: 8888,
}, (res) => {
  res.on('data', (data) => {
    // 获取 请求的结果
    console.log(data.toString());
  })
  res.on('end', () => {
    // 监听是否获取到了所有的结果i
    console.log('获取到了所有的结果');
  })
})
result.end()
7.10 http 实现文件上传
// 传递的是一个 xxxxxxxx.png
const http = require('http');
const fs = require('fs');
const qs = require('querystring');

const server = http.createServer((req, res) => {
  if (req.url === '/upload') {
    if (req.method === 'POST') {
      req.setEncoding('binary');

      let body = '';
      const totalBoundary = req.headers['content-type'].split(';')[1];
      const boundary = totalBoundary.split('=')[1];

      req.on('data', (data) => {
        body += data;
      });

      req.on('end', () => {
        console.log(body);
        // 处理body
        // 1.获取image/png的位置
        const payload = qs.parse(body, "\r\n", ": ");
        const type = payload["Content-Type"];

        // 2.开始在image/png的位置进行截取
        const typeIndex = body.indexOf(type);
        const typeLength = type.length;
        let imageData = body.substring(typeIndex + typeLength);

        // 3.将中间的两个空格去掉
        imageData = imageData.replace(/^\s\s*/, '');

        // 4.将最后的boundary去掉
        imageData = imageData.substring(0, imageData.indexOf(`--${boundary}--`));

        fs.writeFile('./foo.png', imageData, 'binary', (err) => {
          res.end("文件上传成功~");
        })
      })
    }
  }
});

server.listen(8000, () => {
  console.log("文件上传服务器开启成功~");
})

8、express 框架

8.1 express 的创建方式
  • 通过 express 提供的脚手架,直接创建一个应用的骨架
  • 从零搭建自己的 express 应用架构

方式一

// 安装脚手架
npm i express-generator -g
// 创建项目
express express-demo
// 安装依赖
npm i
// 启动项目
node bin/www
8.2 express 初体验
npm init -y
npm i express

const express = require('express')
// express 其实是一个函数
const app = express()

// 监听默认路径
app.get('/', (req, res, next) => {
  res.end('hello ')
})
app.post('/', (req, res, next) => {
  res.end('hello post')
})
app.post('/login', (req, res, next) => {
  res.end('login')
})

// 开启监听
app.listen(8000, () => {
  console.log('express 初体验启动成功~');
})
8.3 普通中间件的使用
const express = require('express')
const app = express()

// app.use 注册一个全局的中间价
app.use((req, res, next) => {
  console.log('01第一个普通的中间间');
  // 不论我们发起什么请求,都会执行 这个 console.log('01第一个普通的中间间')
  //res.end('hello wolrd')// end 不会妨碍 next 但每个中间件都有 res.end 会报错

  next() // 调用下一个中间件
})
app.use((req, res, next) => {
  console.log('02第2个普通的中间间');
  res.end('hello wolrd')
})
app.listen(8000, () => {
  console.log('服务器启动成功·');
})
8.4 路径中间件
const express = require('express')
const app = express()

// 路径匹配中间件  ,路径中间件也可以有 多个相同的,只需要写一个 next 就可以执行多个
// 没有 next 永远匹配到第一个 中间件
app.use('/home', (req, res, next) => {
  console.log('home 01');
  res.end('home 01')
})
app.listen(8000, () => {
  console.log('服务器启动成功·');
})
8.5 内置中间件 解析 json 和 urlencoded 数据
const express = require('express')
const app = express()
// 配置 解析json的中间件
app.use(express.json())
// 配置解析urlencoded 的中间件
app.use(express.urlencoded({
  extended: false
}))
app.post('/login', (req, res, next) => {
  console.log(req.body);
})
app.listen(8000, () => {
  console.log('服务器启动成功·');
})
  • extended: false:表示使用系统模块querystring来处理,也是官方推荐的
  • extended: true:表示使用第三方模块qs来处理
8.6 解析 form-data 的数据

非文件的键值对数据

const express = require('express')
// 1、导入
const multer = require('multer')
const app = express()
// 配置 解析json的中间件
app.use(express.json())
// 配置解析urlencoded 的中间件
app.use(express.urlencoded({
  extended: false
}))
// 2、创建
const upload = multer()
// 3、创建
// 注意  如果解析的 form-data 不是文件,可以用 any
app.use(upload.any())
// form-data 的 请求数据怎么解析? 这里要用到 express 推荐的一个库,multer
// form-data 的数据,目前大多都是 上传文件
app.post('/login', (req, res, next) => {
  console.log(req.body);
})
app.listen(8000, () => {
  console.log('服务器启动成功·');
})

实现multer 文件上传

注意 永远不要把 multer 作为全局中间件使用

const express = require('express')
// 1、导入
const multer = require('multer')
const app = express()
// 配置 解析json的中间件
app.use(express.json())
// 配置解析urlencoded 的中间件
app.use(express.urlencoded({ extended: false }))


// 2、创建
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, './uploads/')
  },// 目的地
  filename: (req, file, cb) => {
    cb(null, "foo.png")
    // 名字不会写死的
    // Date.now() 当前的时间戳
    // path.extname(file.originalname) file.originalname 是原始文件的名字
    // path.extname 取出后缀名
  }
})
const upload = multer({
  //dest: './uploads/',
  // 上传文件的存储路径

  storage
})
// 这个创建的配置,也可以自定义,因为,不定义的话 上传的文件,是没有后缀名的


// 3、创建
// 注意  如果解析的 form-data 不是文件,可以用 any

// 注意 永远不要把 multer 作为全局中间件使用
app.use(upload.any())
// form-data 的 请求数据怎么解析? 这里要用到 express 推荐的一个库,multer
// form-data 的数据,目前大多都是 上传文件
app.post('/login', (req, res, next) => {
  console.log(req.body);
})
app.post('/upload', upload.single('file'), (req, res, next) => {
  // 如果是上传单个文件,是 single ,多个文件是 array
  console.log('文件上传成功');
  console.log(req.files);// 上传文件后的信息
  res.end('上传成功')
})
app.listen(8000, () => {
  console.log('服务器启动成功·');
})
8.7 记录请求日志
// express 官方的 morgan 库
const loggerWriter = fs.createWriteStream('./log/access.log',{ flag: 'a+'})
app.use(morgan('combined', {stream : loggerWriter}))
8.8 客户端发送请求的方式
  • 通过 get 请求中的URL 的 params
  • 通过 get 请求中的URL 的 query
  • 通过post 请求中的body 的 json 格式
  • 通过post 请求中的body 的 x-www-form-urlencoded 格式
  • 通过post 请求中的 form-data 格式
8.9 url 的参数解析 params query
const express = require('express')
// express 其实是一个函数
const app = express()

// 监听默认路径
app.get('/home/:id/:name', (req, res, next) => {
  console.log(req.params);
  res.end('hello')
})
app.get('/home', (req, res, next) => {
  console.log(req.query);
  res.end('hello')
})


// 开启监听
app.listen(8000, () => {
  console.log('express 初体验启动成功~');
})
8.10 response 响应结果
const express = require('express')
// express 其实是一个函数
const app = express()

// 监听默认路径
app.get('/', (req, res, next) => {
  res.end('hello ')
})
app.post('/', (req, res, next) => {
  res.end('hello post')
})
app.post('/login', (req, res, next) => {
  res.end('login')
})

// 开启监听
app.listen(8000, () => {
  console.log('express 初体验启动成功~');
})
8.11 express 的路由

将路由抽离为单独模块的步骤如下:

  1. 创建路由模块对应的 .js 文件 (就是自定义模块)
  2. 调用 express.Router() 函数创建路由对象 (返回的是一个路由的实例对象)
  3. 向路由对象上挂载具体的路由
  4. 使用 module.exports 向外共享路由对象
  5. 在入口文件引入它,并使用 app.use()函数注册路由模块

入口文件

const express = require('express')
const userRouter = require('./routers/users')
// express 其实是一个函数
const app = express()

app.use(userRouter)
// app.use('/api',userRouter) ,则表示添加了路由前缀 localhost:8000/api/
// 开启监听
app.listen(8000, () => {
  console.log('express 初体验启动成功~');
})

路由文件

const express = require('express')

const router = express.Router()
router.get('/', (req, res, next) => {
  res.json({ name: '呆呆狗', age: 20 })
})
router.post('/', (req, res, next) => {
  res.end('hello post')
})
router.post('/login', (req, res, next) => {
  res.end('login')
})
module.exports = router
8.12 express 的静态资源服务器
const express = require('express')
// express 其实是一个函数
const app = express()

app.use(express.static('./build'))
// 开启监听
app.listen(8000, () => {
  console.log('express 初体验启动成功~');
})
8.13 错误的中间件
app.get('/',function(req,res){    // 1、路由
  throw new Error('服务器内部发生了错误!')// 2、抛出一个自定义的错误
  // 如果没有错误级别的中间件,代码到这里就结束了
  res.send('Home Page')
})
// 错误的中间件,必须要在所有路由之后
// 也可以用 next(new Error())   抛出错误
app.use(function(err,req,res,next){ // 1、错误级别的中间件
  console.log('发生了错误'+err.message)// 2、在服务器打印错误消息
  res.send('Error!'+err.message)// 3、向客户端响应错误相关的内容
  
  // 其实在真实开发中,这里用的是  switch 语句,判断响应的错误,然后 返回
})
const express = require('express');

const app = express();

const USERNAME_DOES_NOT_EXISTS = "USERNAME_DOES_NOT_EXISTS";
const USERNAME_ALREADY_EXISTS = "USERNAME_ALREADY_EXISTS";

app.post('/login', (req, res, next) => {
  // 加入在数据中查询用户名时, 发现不存在
  const isLogin = false;
  if (isLogin) {
    res.json("user login success~");
  } else {
    // res.type(400);
    // res.json("username does not exists~")
    next(new Error(USERNAME_DOES_NOT_EXISTS));
  }
})

app.post('/register', (req, res, next) => {
  // 加入在数据中查询用户名时, 发现不存在
  const isExists = true;
  if (!isExists) {
    res.json("user register success~");
  } else {
    // res.type(400);
    // res.json("username already exists~")
    next(new Error(USERNAME_ALREADY_EXISTS));
  }
});

app.use((err, req, res, next) => {
  let status = 400;
  let message = "";
  console.log(err.message);

  switch(err.message) {
    case USERNAME_DOES_NOT_EXISTS:
      message = "username does not exists~";
      break;
    case USERNAME_ALREADY_EXISTS:
      message = "USERNAME_ALREADY_EXISTS~"
      break;
    default: 
      message = "NOT FOUND~"
  }

  res.status(status);
  res.json({
    errCode: status,
    errMessage: message
  })
})

app.listen(8000, () => {
  console.log("路由服务器启动成功~");
});

8.14 express 源码
  1. 调用 express 函数到底发生了什么,就是调用了源码的 createApplication 函数

9、koa 框架

9.1 认识 koa

koa node.js的下一代 web 框架

事实上 express 和 koa 是同一个开发团队开发的

  • koa 更小、更丰富、更强大的能力
  • koa 比 express 具有更强的异步处理能力
  • koa 核心代码 只有 1600+ 行,更轻量级的框架
9.2 初体验
const Koa = require('koa')
const app = new Koa()

// koa 把所有的中间件都执行完以后,都没有返回结果的话,会返回 not found

app.use((ctx, next) => {
  // 第一个参数是上下文
  // 这个上下文 包含  request 和 response
  console.log(ctx.request);
  ctx.response.body = "hello world"
})
app.listen(8000, () => {
  console.log('http://localhost:8000');
})

9.3 koa 注册中间件

koa 没有提供 app.get/post ,没有提供 匹配路径的路由,没有提供连续注册

const Koa = require('koa')
const app = new Koa()

app.use((ctx, next) => {
  // 第一个参数是上下文
  // 这个上下文 包含  request 和 response\
  if (ctx.request.url === '/login') {
    if (ctx.request.method === 'GET') {
      console.log('来到了这里面');
      ctx.response.body = "Login Suceess"
    }
  }
  ctx.response.body = "请求成功~"
})
app.listen(8000, () => {
  console.log('http://localhost:8000');
})

9.4 koa 使用路由

koa 本身没有路由,需要借助第三方

npm i koa-router

路由文件

const Router = require('koa-router')

const router = new Router({
  prefix: "/users", // 路径前缀
})
router.put('/', (ctx, next) => {
  ctx.response.body = "put 成功~"
})
module.exports = router

入口文件

const Koa = require('koa')
const userRouter = require('./router/user')
const app = new Koa()

app.use(userRouter.routes())

app.use(userRouter.allowedMethods())
// allowedMethods 用于判断某一个 method 是否支持
// 如果没有实现某个请求,就会自动报错 405
app.listen(8000, () => {
  console.log('http://localhost:8000');
})

9.5 参数的处理 params query
const Koa = require('koa')
const Router = require('koa-router')
const app = new Koa()
const userRouter = new Router({
  prefix: "/users", // 路径前缀
})
userRouter.get('/:id', (ctx, next) => {
  console.log(ctx.request.params);
  console.log(ctx.request.query);
})

app.use(userRouter.routes())
app.listen(8000, () => {
  console.log('http://localhost:8000');
})

9.6 解析json urlencoded
npm i koa-bodyparser
const Koa = require('koa')
const bodyParser = require('koa-bodyparser')
const app = new Koa()
app.use(bodyParser())

app.use((ctx, next) => {
  console.log(ctx.request.body);
  ctx.response.body = "hello"
})
app.listen(8000, () => {
  console.log('http://localhost:8000');
})

9.7 解析 form-data 的数据

npm i koa-multer

const Koa = require('koa')
const multer = require('koa-multer')

const app = new Koa()
const upload = multer()

// 真实开发中,不建议 吧 multer 写入全局中间件
app.use(upload.any())
app.use((ctx, next) => {
  // 注意,解析 form-data 这里 用的是 req.body
  console.log(ctx.req.body);
  ctx.response.body = "hello"
})
app.listen(8000, () => {
  console.log('http://localhost:8000');
})

9.8 解析 form-data 的文件
const Koa = require('koa')
const Router = require('koa-router')
const multer = require('koa-multer')

const app = new Koa()
const uploadRouter = new Router({ prefix: '/upload' })
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, './uploades/')
  },// 目的地
  filename: (req, file, cb) => {
    cb(null, "foo.png")
    // 名字不会写死的
    // Date.now() 当前的时间戳
    // path.extname(file.originalname) file.originalname 是原始文件的名字
    // path.extname 取出后缀名
  }
})
const upload = multer({ storage })

uploadRouter.post('/avatar', upload.single('avatar'), (ctx, next) => {
  console.log(ctx.req.file);
  ctx.response.body = "上传头衔成功~"
  // 要是多个文件 就是  ctx.req.files
})

app.use(uploadRouter.routes())
app.listen(8000, () => {
  console.log('http://localhost:8000');
})

9.9 数据的响应

输出结果

  • string 字符串
  • Buffer Buffer 数据
  • Stream 流数据
  • Object | Array 对象或者数组
  • null 不输出任何内容
  • 如果 response.status 尚未设置,koa 会自动将状态设置为200 或 204
const Koa = require('koa')

const app = new Koa()

app.use((ctx, next) => {
  ctx.response.body = {
    ddg: '呆呆狗'
  }
  // 也可以 用 ctx.body = 返回数据
  // 本质上说,你写 ctx.body 其实 就是 ctx.response.body

  ctx.status = 203
  // 对象返回的是  json 格式
})
app.listen(8000, () => {
  console.log('http://localhost:8000');
})

9.10 静态资源服务器

npm i koa-static

const Koa = require('koa')
const static = require('koa-static')

const app = new Koa()

app.use(static('./build'))

app.listen(8000, () => {
  console.log('静态资源服务器启动成功~');
})
9.11 错误中间件
const Koa = require('koa')

const app = new Koa()

app.use((ctx, next) => {
  const isLogin = false
  if (!isLogin) {
    ctx.app.emit('error', new Error('你还没有登录~'), ctx)
  }
})

app.on('error', (err, ctx) => {
  // 一般来说也是先  switch 判断
  ctx.status = 200,
    ctx.body = "你还没有登录"
})
app.listen(8000, () => {
  console.log('服务器启动成功~');
})
9.12 koa洋葱模型 注意看第十节 express 和 koa的区别
  1. 中间件处理,查找匹配到的中间件,,假设有三个中间件
  2. 先执行第一个中间件, 第二个 第三个
  3. 如果 第二个 next 后面还有代码,则逆序回去,执行 next 后面的代码
  4. 第二个的next 后面执行完毕后,再去第一个的 next后面的代码
  5. 然后 返回值

10、express 和 koa 的区别

从架构上来说

  • express 是完善和强大的,帮助我们内置了很多功能
  • koa 是 自由和 简洁的,只包含最和核心的功能

需求: 假如有三个中间件,会在一次请求中匹配到,并且按照顺序执行

  1. 在middleware1 中,在 req.message 中添加一个字符串 aaa
  2. 在middleware2 中,在 req.message 中添加一个字符串 bbb
  3. 在middleware3 中,在 req.message 中添加一个字符串 ccc

当所有内容添加结束后,在 middleware1 中 通过 res 返回最终的结果

10.1 express 实现同步数据
const express = require('express')
const app = express()

const middleware1 = (req, res, next) => {
  req.message = "aaa"
  console.log(1);
  next()
  res.end(req.message)
  console.log(5);
}
const middleware2 = (req, res, next) => {
  req.message += "bbb"
  console.log(2);
  next()
  console.log(4);
}
const middleware3 = (req, res, next) => {
  req.message += "ccc"
  console.log(3);
}
app.use(middleware1, middleware2, middleware3)
app.listen(8000, (() => {
  console.log('启动成功');
}))

/*
* 1、先去 middleware1 赋值,然后 遇到 next 他会执行 middleware2 不会先执行 res.end()
* 2、执行完 middleware3 ,middleware3没有 next 就会回去,倒叙执行 middleware2 和 middleware1 剩下的代码
*/
//  输出的结果也是 12345

10.2 express 实现异步数据
const middleware3 = (req, res, next) => {
  // 假如,要在这个未知, 发起一个异步请求,然后,再拼接值,在返回出去,
  // 结果不会是我们想要的
  
  // 所以 express 处理这些异步数据,是有些乏力的
  
  // 也可以再外面定义一个函数,先请求,
  req.message += "ccc"
  console.log(3);
}
10.3 koa 实现同步数据
const Koa = require('koa');

const app = new Koa();

const middleware1 = (ctx, next) => {
  ctx.message = "aaa";
  next();
  ctx.body = ctx.message;
}

const middleware2 = (ctx, next) => {
  ctx.message += "bbb";
  next();
}

const middleware3 = (ctx, next) => {
  ctx.message += "ccc";
}

app.use(middleware1);
app.use(middleware2);
app.use(middleware3);

app.listen(8000, () => {
  console.log("服务器启动成功~");
})
10.4 koa 实现异步数据

koa中的next是借助promise,可以做一些异步的处理

const Koa = require('koa');
const axios = require('axios');

const app = new Koa();

const middleware1 = async (ctx, next) => {
  ctx.message = "aaa";
  await next();
  ctx.body = ctx.message;
}

const middleware2 = async (ctx, next) => {
  ctx.message += "bbb";
  await next();
}

const middleware3 = async (ctx, next) => {
  const result = await axios.get('http://123.207.32.32:9001/lyric?id=167876');
  ctx.message += result.data.lrc.lyric;
}

app.use(middleware1);
app.use(middleware2);
app.use(middleware3);

app.listen(8000, () => {
  console.log("服务器启动成功~");
})

11、MySQL

11.1 终端操作数据库

安装,微信搜索 软件管家 找到 MySQL 即可

11.1.1 连接数据库

首先要添加环境变量,找到 系统变量 => Path 双击进去,点击新建,C:\Program Files\MySQL\MySQL Server 8.0\bin 点击确定

打开 cmd 输入 mysql --version 即可查看版本号

mysql -uroot -p跟上密码

然后就可以连接上数据库了

11.1.2 显示数据库

show databases;

默认显示四个数据库

  • infomation_schema :信息数据库,其中包括 MySQL 再维护的其他数据库、表、列、访问权限等信息
  • performance_schema:性能数据库,记录着 MySQL Server 数据库引擎再运行过程中的一些资源消耗相关的信息
  • MySQL:用于存储数据库管理者的用户信息、权限信息以及一些日志信息等
  • sys 相当于是一个简易版的 performance_schema,将性能数据库的数据汇总成更容易理解的形式
11.1.3 创建数据库 和 表

create database 数据库的名字; 创建一个数据库

select database(); 查看当前正在使用那个数据库

use 数据库名字; 选择要执行的数据库

show tables; 查看当前数据库的数据表

// 创建数据表
create table 表的名字(
	name varchar(10),
  age int,
  height double
);
// 在数据表中插入数据
insert into 表的名字 (要插入的键) values (要插入的值)

// 比如 给创建的 users 表  插入 name  age  height 字段
insert into users (name,age,height) values ('ddg',21,181);
11.2 GUI工具的介绍
  • 终端操作数据库 语句没有高亮,没有任何提示
  • 格式不美观
  • 等等~~

GUI工具就是 图形化操作工具

  • Navicat 首选,但收费,可各显神通~~
  • SQLYog:免费的SQL
  • TablePlus 常用功能都可以使用,但是会多一些限制
11.3 安装 navicat

各显神通 ~~

11.4 navicat 连接数据库
  1. 点击 左上角 连接 选择第一个 MySQL
  2. 连接名:随机即可
  3. 主机:默认 localhost
  4. 端口 3306
  5. 用户名 root
  6. 密码输入
  7. 点击左下角测试连接
  8. 没有问题点击确定
  9. 双击左边的 新建连接的连接名 ,就可以看到 我们所有的数据库了
11.5 SQL 语句
  • 关键字 通常是大写的
  • 每一句结束以后,都需要以分号 结尾
  • 如果遇到关键字作为表明 或者 字段名称,可以使用 `` 包裹

SQL 语句分类

  • DDL:数据定义语言,对数据库或者表进行:创建、删除、修改等操作
  • DML:数据操作语言:对表进行:添加、删除、修改等操作
  • DQL:数据查询语言:从数据库中查询记录
  • DCL:数据控制语言:对数据库、表格的权限进行相关访问控制操作
11.6 DDL :对数据库的操作

写sql语句,可以在 双击连接名,找到相应的数据库 双击,找到查询,右击新建查询语句

# 查看所有的数据库
SHOW DATABASES;

# 选择某一个数据库
USE studycoderwhynode;

# 查看当前正在使用的数据库
SELECT DATABASE();

# CREATE DATABASE 要创建数据库的名字
CREATE DATABASE studycoderwhynode-one
# 这种是不严谨的,如果已经有了这个数据库,会报错的

# 下面这行语句,表示,如果没有这个数据库则创建,有则不会新建
CREATE DATABASE IF NOT EXISTS studycoderwhynodeone
# 下面这行语句,新建这个数据库,并设置编码格式 和排序规则,一般不需要设置,默认也是这个
CREATE DATABASE IF NOT EXISTS studycoderwhynodeone DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;

# 删除数据库
DROP DATABASE IF EXISTS studycoderwhynodeone;

# 修改数据库,一般修改编码比较多
ALTER DATABASE studycoderwhynodeone CHARACTER SET = utf8 COLLATE = utf8_unicode_ci;
11.7 MySQL常见的数据类型

MySQL 支持的数据类型有: 数字类型、日期类型和时间类型,空间类型和 JSON 数据类型

最常见的是 前三种

数字类型

  • 数字类型有很多
  • 整数数字类型INT BIGINT SMALLINT TINYINT MEDIUMINT
  • 浮点数 类型:FLOAT DOUBLE
  • 精确数字类型:DECIMAL NUMERIC

日期类型 用的最多的还是 TIMESTAMP

  • YEAR以YYYY格式显示值
    • 范围1901到2155,和0000
  • DATE类型具有日期,但没有时间部分的值
    • DATE以格式 YYYY-MM-DD 显示值
    • 支持的范围是 1000-01-01 到 9999-12-31
  • DATETIME 类型用于包含日期和时间部分的值
    • DATETIME以格式 YYYY-MM-DD hh:mm:ss 显示值
    • 支持的范围是 1000-01-01 00:00:00 到 9999-12-31 23:59:59
  • TIMESTAMP 数据类型被用于同时包含日期和时间部分的值
    • TIMESTAMP 以格式 YYYY-MM-DD hh:mm:ss 显示
    • 但是他的范围是 UTC 时间范围 :1970-01-01 00:00:01 到 2308-01-19 03:14:07

DATETIME 或者 TIMESTAMP 值可以包括在高达微秒(6位)精度的后小数秒一部分

1000-01-01 00:00:00:000000 到 9999-12-31 23:59:59:999999

字符串类型

  • CHAR 类型在创建表时为固定长度,长度可以是0到255 之间的任何值
    • 在被查询时,会删除后面的空格
  • VARCHAR 类型的值,是可变长度的字符串,长度可以指定为0到65535之间的值
    • 在被查询时,不会删除后面的空格
  • BINARY 和 VARBINARY 类型用于存储二进制字符串,存储的是字节字符串
  • BLOB 用于存储大的二进制类型
  • TEXT 用于 存储大的字符串类型
11.8 常见的表约束

主键:PRIMARY KEY

一张表中,我们为了区分每一条记录的唯一性,必须有一个字段是永远不会重复。并且不会为空的,这个字段我们通常会将它设置为主键

  • 主键是表中唯一的索引
  • 并且必须是 NOT NULL 的,如果没有设置 NOT NULL ,那么 MySQL 也会隐式的设置为 NOT NULL
  • 主键也可以是多列索引,PRIMARY KEY (key_part,……),我们一般称之为联合主键
  • 建议:开发中主键字段应该是和业务无关的,尽量不要使用业务字段来作为主键

唯一:UNIQUE

某些字段在开发中我们希望是唯一的,不会重复的,比如手机号码、身份证号码等,这个字段我们可以使用 UNIQUE 来约束

使用 UNIQUE 约束的字段必须是在表中不同的

CREATE TABLE IF NOT EXISTS `students` (
	`name` VARCHAR(10),
  `phoneNum` VARCHAR(20) UNIQUE NOT NULL
)
# UNIQEU 索引允许 null 包含的列具有多个 null ,就是说 这个手机号可以设置为  null
# NOT NULL 表示 这个是必填的,不能设置为空

不能为空 NOT NULL

CREATE TABLE IF NOT EXISTS `students` (
	`name` VARCHAR(10) DEFAULT '' NOT NULL,
  `phoneNum` VARCHAR(20) UNIQUE NOT NULL
)
# 表示 name 不能为空,默认值是 ''

# UNIQEU 索引允许 null 包含的列具有多个 null ,就是说 这个手机号可以设置为  null
# NOT NULL 表示 这个是必填的,不能设置为空

某些字段,要求用户必须要插入,不能为空,则可以设置成 NOT NULL


默认值 DEFAULT

某些字段,我们希望在没有设置值时给予一个默认值,这个时候,我们可以使用 DEFAULT 来完成


自动递增 AUTO_INCREMENT

一般用在 int 类型中

11.9 创建一张完整的表
# 测试~~~~~~

# 查看所有的表
SHOW TABLES;

# 新建表
CREATE TABLE IF NOT EXISTS `students`(
	`name` VARCHAR(10),
	`age` INT,
	`score` INT
);

# 删除表
DROP TABLE IF EXISTS `users`;

# 查看表的结构
DESC students;

# 查看创建表的 SQL 语句
SHOW CREATE TABLE `students`
# 完整的创建表的语法
# 创建一个 users 表
# id 是 int 类型,作为主键,不为空,自动递增
# 名字,最大20个字符,不为空
# 年龄 int 默认值为 0
# 手机号 唯一的,不为空
# 创建的时间
CREATE TABLE IF NOT EXISTS `users`(
	id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
	name VARCHAR(20) NOT NULL,
	age INT DEFAULT 0,
	phoneNum VARCHAR(20) UNIQUE NOT NULL,
	createTime TIMESTAMP
);

# 修改表
# 1. 修改表名字
ALTER TABLE `users` RENAME TO `user`;
# 2. 添加列
ALTER TABLE `user` ADD `updateTime` TIMESTAMP ;
# 3. 修改某个字段的名称
ALTER TABLE `user` CHANGE `phoneNum` `telPhone` VARCHAR(20);
# 4. 修改字段的类型
ALTER TABLE `user` MODIFY `name` VARCHAR(30);
# 5. 删除字段
ALTER TABLE `user` DROP `age`;

# 根据一个表结果去创建另外一张表,注意它只会复制结构
# 根据 user 创建 user2
CREATE TABLE `user1` LIKE `user`;

# 根据另外一个表中的所有内容,创建另一个表 ,注意 它只会复制内容
# 根据 user 创建 user2 
CREATE TABLE `user2` `user`

11.10 DML 对数据库的增删改查

插入数据和时间默认数据

# 插入数据
INSERT INTO `user` VALUES (110,'ddg','110120130140','2021-7-4','2021-7-5');

INSERT INTO `user` (name,telphone,createTime,updateTime) VALUES ('lihei','1111','2020-7-8','2016-7-8')

INSERT INTO `user` (name,telphone) VALUES ('copy','1234')

# 需求:createTime , updateTime 自动设置
# DEFAULT CURRENT_TIMESTAMP 默认值是创建的时间
# ON UPDATE CURRENT_TIMESTAMP 更新的时候,赋值
ALTER TABLE `user` MODIFY `createTime` TIMESTAMP DEFAULT CURRENT_TIMESTAMP;
ALTER TABLE `user` MODIFY `updateTime` TIMESTAMP DEFAULT CURRENT_TIMESTAMP 
																			 ON UPDATE CURRENT_TIMESTAMP;

删除和更新数据

# 删除数据
# 删除所有数据
DELETE FROM `user`;
# 删除符合条件的 数据
DELETE FROM `user` WHERE id= 110;

# 更新所有数据
UPDATE `user` SET `name` = 'copy' , telPhone = '0537-7261266';
# 更新符合需求的 数据
UPDATE `user` SET `name` = 'copy' , telPhone = '0537-7261266' WHERE id =111;
11.11 DQL 从数据库查询
# 基本查询
# 1.基本查询,查询所有的字段以及所有的数据
SELECT * FROM `products`;
# 2.查询指定的字段
SELECT title,price FROM `products`;
# 3.对字段的结果起一个别名
SELECT title as phoneTitle,price as currentPrice FROM `products`;
# where 查询
# 查询 价格小于 1000 的字段
SELECT title,price FROM `products` WHERE price <1000
# 价格等于 999 的手机
SELECT * FROM `products` WHERE price =999
# 价格不等于999 的手机
SELECT * FROM `products` WHERE price !=999
# 查询所有 的 华为品牌 的手机
SELECT * FROM `products` WHERE brand = '华为'

# 逻辑运算语句
# 查询价格 在 1000 到 2000 的手机
# 这三个都可以,但是 between and 包括等于
SELECT * FROM `products` WHERE price > 1000 AND price <2000;
SELECT * FROM `products` WHERE price > 1000 && price <2000;
SELECT * FROM `products` WHERE price BETWEEN  1000 AND 2000;

# 查询 价格在5000以上或者是品牌是华为的手机
SELECT * FROM `products` WHERE price > 5000 || brand = '华为';

# 设置某些值为 null
UPDATE `products` SET url = NULL WHERE id >= 85 and id <= 88;
# 查询某一个值为NULL
SELECT * FROM `products` WHERE url IS NULL;
# 查询不为 null 的
SELECT * FROM `products` WHERE url IS NOT NULL;

  • % 表示可以匹配任意个 的 任意字符
  • _ 表示匹配一个的任意字符
# 模糊查询
# 2.3.模糊查询
# 查询 title 字段 中 只要包含 M 的, %表示 0 个 或者多个
SELECT * FROM `products` WHERE title LIKE '%M%';
SELECT * FROM `products` WHERE title LIKE '%P%';
# 查询 title 字段中,第二个字符是P 的
SELECT * FROM `products` WHERE title LIKE '_P%';

IN 的使用

# IN 表示多个值中取其中一个即可
SELECT * FROM `products` WHERE brand = '华为' || brand = '小米' || brand='苹果';
SELECT * FROM `products` WHERE brand IN('华为','小米','苹果');

对结果进行排序

# ASC 升序   DESC 降序
SELECT * FROM `products` WHERE brand IN ('华为', '小米', '苹果') 
												 ORDER BY price ASC, score DESC;

分页查询

# 4.分页查询
# LIMIT limit OFFSET offset;
# Limit offset, limit;
# limit 查多少条数据,  offset 从第几条后面开始查
SELECT * FROM `products` LIMIT 20 OFFSET 0;
SELECT * FROM `products` LIMIT 20 OFFSET 20;
# 偏移40条,查20条
SELECT * FROM `products` LIMIT 40, 20;

聚合查询

# 1.聚合函数的使用
# 求所有手机的价格的总和,并修改输出的字段名,as 可以省略
SELECT SUM(price) totalPrice FROM `products`;
# 求一下华为手机的价格的总和
SELECT SUM(price) FROM `products` WHERE brand = '华为';
# 求华为手机的平均价格
SELECT AVG(price) FROM `products` WHERE brand = '华为';
# 最高手机的价格和最低手机的价格
SELECT MAX(price) FROM `products`;
SELECT MIN(price) FROM `products`;

# 求华为手机的个数
# COUNT(*) 表示所有的
# COUNT(url) 表示查找 url 的个数,null 不会计算在内
SELECT COUNT(*) FROM `products` WHERE brand = '华为';
SELECT COUNT(*) FROM `products` WHERE brand = '苹果';
SELECT COUNT(url) FROM `products` WHERE brand = '苹果';

SELECT COUNT(price) FROM `products`;
# 去掉重复的价格,查询价格的 个数
SELECT COUNT(DISTINCT price) FROM `products`;

# 2.GROUP BY的使用
# 按照 brand 进行分组,
# brand, AVG(price), COUNT(*), AVG(score) 要显示的列
SELECT brand, AVG(price), COUNT(*), AVG(score) FROM `products` GROUP BY brand;


# 3.HAVING的使用
# HAVING的使用一定是在 GROUP BY 之后使用
# 先分组,然后在查找,查找平均价格大于2000SELECT brand, AVG(price) avgPrice, COUNT(*), AVG(score) FROM `products` GROUP BY brand HAVING avgPrice > 2000;


# 4.需求:求评分score > 7.5的手机的,平均价格是多少?
# 升级:平均分大于7.5的手机,按照品牌进行分类,求出平均价格。
SELECT brand, AVG(price) FROM `products` WHERE score > 7.5 GROUP BY brand;
11.12 多表的设计 外键约束

设置外键

# 1.创建brand的表和插入数据
CREATE TABLE IF NOT EXISTS `brand`(
	id INT PRIMARY KEY AUTO_INCREMENT,
	name VARCHAR(20) NOT NULL,
	website VARCHAR(100),
	phoneRank INT
);

INSERT INTO `brand` (name, website, phoneRank) VALUES ('华为', 'www.huawei.com', 2);
INSERT INTO `brand` (name, website, phoneRank) VALUES ('苹果', 'www.apple.com', 10);
INSERT INTO `brand` (name, website, phoneRank) VALUES ('小米', 'www.mi.com', 5);
INSERT INTO `brand` (name, website, phoneRank) VALUES ('oppo', 'www.oppo.com', 12);

INSERT INTO `brand` (name, website, phoneRank) VALUES ('京东', 'www.jd.com', 8);
INSERT INTO `brand` (name, website, phoneRank) VALUES ('Google', 'www.google.com', 9);


# 2.给brand_id设置引用brand中的id的外键约束
# 添加一个brand_id字段
ALTER TABLE `products` ADD `brand_id` INT;
-- ALTER TABLE `products` DROP `brand_id`;

# 修改brand_id为外键

ALTER TABLE `products` ADD FOREIGN KEY(brand_id) REFERENCES brand(id);

# 设置brand_id的值
UPDATE `products` SET `brand_id` = 1 WHERE `brand` = '华为';
UPDATE `products` SET `brand_id` = 2 WHERE `brand` = '苹果';
UPDATE `products` SET `brand_id` = 3 WHERE `brand` = '小米';
UPDATE `products` SET `brand_id` = 4 WHERE `brand` = 'oppo';

强制更新外键依赖的 字段

  • RESTRICT (默认属性) :当更新或删除某个记录时,会检查该记录是否有关键的外键记录,有的话,会报错,不允许更新或者删除
  • NO ACTION : 和 RESTRICT 是一致的,是在 sql 标准中定义的
  • CASCADE : 更新或删除某个记录时,会检查是否有关键的外键记录,有的话
    • 更新:会更新对应的记录
    • 删除:那么关联的记录会被一起删除掉
  • SET NULL :当更新或删除某个记录的时候,会检查该记录是否有关联的外键记录,有的话,将对应的值,设置为 null
# 3.修改和删除外键引用的id , 就是 brand 表的 id
# 在有外键约束的情况下, 如 id 被外面 约束着,则 id 不能修改和删除
UPDATE `brand` SET `id` = 100 WHERE `id` = 1;

# 4.修改brand_id关联外键时的action
# 4.1.获取到目前的外键的名称
SHOW CREATE TABLE `products`;


-- CREATE TABLE `products` (
--   `id` int NOT NULL AUTO_INCREMENT,
--   `brand` varchar(20) DEFAULT NULL,
--   `title` varchar(100) NOT NULL,
--   `price` double NOT NULL,
--   `score` decimal(2,1) DEFAULT NULL,
--   `voteCnt` int DEFAULT NULL,
--   `url` varchar(100) DEFAULT NULL,
--   `pid` int DEFAULT NULL,
--   `brand_id` int DEFAULT NULL,
--   PRIMARY KEY (`id`),
--   KEY `brand_id` (`brand_id`),
--   CONSTRAINT `products_ibfk_1` FOREIGN KEY (`brand_id`) REFERENCES `brand` (`id`)
-- ) ENGINE=InnoDB AUTO_INCREMENT=109 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

# 4.2.根据名称将外键删除掉
ALTER TABLE `products` DROP FOREIGN KEY products_ibfk_1;

# 4.2.重新添加外键约束
ALTER TABLE `products` ADD FOREIGN KEY (brand_id) REFERENCES brand(id)
																									ON UPDATE CASCADE
																									ON DELETE RESTRICT;


UPDATE `brand` SET `id` = 100 WHERE `id` = 1;

11.13 多表的查询
# 1.获取到的是笛卡尔乘积
# 这个会 获取到 两个表的数据个数的乘积 
SELECT * FROM `products`, `brand`;
# 获取到的是笛卡尔乘积进行筛选
SELECT * FROM `products`, `brand` WHERE products.brand_id = brand.id;

# 2.左连接
# 2.1. 查询所有的手机(包括没有品牌信息的手机)以及对应的品牌 null
# 以左边的表为主,     OUTER 可以加,可以不加
SELECT * FROM `products` LEFT OUTER JOIN `brand` ON products.brand_id = brand.id;

# 2.2. 查询没有对应品牌数据的手机
SELECT * FROM `products` LEFT JOIN `brand` ON products.brand_id = brand.id WHERE brand.id IS NULL;
-- SELECT * FROM `products` LEFT JOIN `brand` ON products.brand_id = brand.id WHERE brand_id IS NULL;


# 3.右连接
# 3.1. 查询所有的品牌(没有对应的手机数据,品牌也显示)以及对应的手机数据;
SELECT * FROM `products` RIGHT OUTER JOIN `brand` ON products.brand_id = brand.id;

# 3.2. 查询没有对应手机的品牌信息
SELECT * FROM `products` RIGHT JOIN `brand` ON products.brand_id = brand.id WHERE products.brand_id IS NULL;


# 4.内连接
SELECT * FROM `products` JOIN `brand` ON products.brand_id = brand.id;
SELECT * FROM `products` JOIN `brand` ON products.brand_id = brand.id WHERE price = 8699;

# 5.全连接
# mysql是不支持FULL OUTER JOIN
SELECT * FROM `products` FULL OUTER JOIN `brand` ON products.brand_id = brand.id;


(SELECT * FROM `products` LEFT OUTER JOIN `brand` ON products.brand_id = brand.id)
UNION
(SELECT * FROM `products` RIGHT OUTER JOIN `brand` ON products.brand_id = brand.id)

(SELECT * FROM `products` LEFT OUTER JOIN `brand` ON products.brand_id = brand.id WHERE brand.id IS NULL)
UNION
(SELECT * FROM `products` RIGHT OUTER JOIN `brand` ON products.brand_id = brand.id WHERE products.brand_id IS NULL)

11.14 多对多

一个学生可以选择多门课程,一个课程也可以被多个学生选择

# 1.基本数据的模拟
CREATE TABLE IF NOT EXISTS students(
	id INT PRIMARY KEY AUTO_INCREMENT,
	name VARCHAR(20) NOT NULL,
	age INT
);

CREATE TABLE IF NOT EXISTS courses(
	id INT PRIMARY KEY AUTO_INCREMENT,
	name VARCHAR(20) NOT NULL,
	price DOUBLE
);

INSERT INTO `students` (name, age) VALUES('why', 18);
INSERT INTO `students` (name, age) VALUES('tom', 22);
INSERT INTO `students` (name, age) VALUES('lilei', 25);
INSERT INTO `students` (name, age) VALUES('lucy', 16);
INSERT INTO `students` (name, age) VALUES('lily', 20);

INSERT INTO `courses` (name, price) VALUES ('英语', 100);
INSERT INTO `courses` (name, price) VALUES ('语文', 666);
INSERT INTO `courses` (name, price) VALUES ('数学', 888);
INSERT INTO `courses` (name, price) VALUES ('历史', 80);
INSERT INTO `courses` (name, price) VALUES ('物理', 888);
INSERT INTO `courses` (name, price) VALUES ('地理', 333);


# 多对多,我们一般都会在中间,建立一个关系表
# 2.建立关系表
CREATE TABLE IF NOT EXISTS `students_select_courses`(
	id INT PRIMARY KEY AUTO_INCREMENT,
	student_id INT NOT NULL,
	course_id INT NOT NULL,
	FOREIGN KEY (student_id) REFERENCES students(id) ON UPDATE CASCADE,
	FOREIGN KEY (course_id) REFERENCES courses(id) ON UPDATE CASCADE
);

# 3.学生选课
# why选择了英文、数学、历史
INSERT INTO `students_select_courses` (student_id, course_id) VALUES (1, 1);
INSERT INTO `students_select_courses` (student_id, course_id) VALUES (1, 3);
INSERT INTO `students_select_courses` (student_id, course_id) VALUES (1, 4);


INSERT INTO `students_select_courses` (student_id, course_id) VALUES (3, 2);
INSERT INTO `students_select_courses` (student_id, course_id) VALUES (3, 4);


INSERT INTO `students_select_courses` (student_id, course_id) VALUES (5, 2);
INSERT INTO `students_select_courses` (student_id, course_id) VALUES (5, 3);
INSERT INTO `students_select_courses` (student_id, course_id) VALUES (5, 4);


# 4.查询的需求
# 4.1. 查询所有有选课的学生,选择了哪些课程
SELECT stu.id id, stu.name stuName, stu.age stuAge, cs.id csId, cs.name csName, cs.price csPrice
FROM `students` stu
JOIN `students_select_courses` ssc ON stu.id = ssc.student_id
JOIN `courses` cs ON ssc.course_id = cs.id;


# 4.2. 查询所有的学生的选课情况 不关心学生到底有没有选择课程
SELECT stu.id id, stu.name stuName, stu.age stuAge, cs.id csId, cs.name csName, cs.price csPrice
FROM `students` stu
LEFT JOIN `students_select_courses` ssc ON stu.id = ssc.student_id
LEFT JOIN `courses` cs ON ssc.course_id = cs.id;

# 4.3. 哪些学生是没有选课
SELECT stu.id id, stu.name stuName, stu.age stuAge, cs.id csId, cs.name csName, cs.price csPrice
FROM `students` stu
LEFT JOIN `students_select_courses` ssc ON stu.id = ssc.student_id
LEFT JOIN `courses` cs ON ssc.course_id = cs.id
WHERE cs.id IS NULL;

# 4.4. 查询哪些课程是没有被选择的
SELECT stu.id id, stu.name stuName, stu.age stuAge, cs.id csId, cs.name csName, cs.price csPrice
FROM `students` stu
RIGHT JOIN `students_select_courses` ssc ON stu.id = ssc.student_id
RIGHT JOIN `courses` cs ON ssc.course_id = cs.id
WHERE stu.id IS NULL;

# 4.5. 某一个学生选了哪些课程(why)
SELECT stu.id id, stu.name stuName, stu.age stuAge, cs.id csId, cs.name csName, cs.price csPrice
FROM `students` stu
LEFT JOIN `students_select_courses` ssc ON stu.id = ssc.student_id
LEFT JOIN `courses` cs ON ssc.course_id = cs.id
WHERE stu.id = 2;
11.15 将结果转换成 数组或者对象
# 将联合查询到的数据转成对象(一对多)
# key 叫 id 值是 brand.id
# JSON_OBJECT() 后面写的 brand 表示这个键是 brand 值是 生成的对象
SELECT 
	products.id id, products.title title, products.price price,
	JSON_OBJECT('id', brand.id, 'name', brand.name, 'website', brand.website) brand
FROM `products`
LEFT JOIN `brand` ON products.brand_id = brand.id;

# 将查询到的多条数据,组织成对象,放入到一个数组中(多对多)
# GROUP BY stu.id; 通过 ID 合并
SELECT 
	stu.id, stu.name, stu.age,
	JSON_ARRAYAGG(JSON_OBJECT('id', cs.id, 'name', cs.name, 'price', cs.price))
FROM `students` stu
JOIN `students_select_courses` ssc ON stu.id = ssc.student_id
JOIN `courses` cs ON ssc.course_id = cs.id
GROUP BY stu.id;

SELECT * FROM products WHERE price > 6000;

12、npm i mysql2

12.1 安装 基本使用
const mysql = require('mysql2');

// 1.创建数据库连接
const connection = mysql.createConnection({
  host: 'localhost',
  port: 3306,
  // 和那个数据库建立连接
  database: 'studycoderwhynode',
  // 数据库用户名字
  user: 'root',
  // 密码
  password: '数据库的密码'
});

// 2.执行SQL语句
const statement = `
  SELECT * FROM products WHERE price > 6000;
`
// 执行语句
connection.query(statement, (err, results, fields) => {
  console.log(results);
});

12.2 预处理
const mysql = require('mysql2');

// 1.创建数据库连接
const connection = mysql.createConnection({
  host: 'localhost',
  port: 3306,
  // 和那个数据库建立连接
  database: 'studycoderwhynode',
  // 数据库用户名字
  user: 'root',
  // 密码
  password: '476182'
});

// 2.执行SQL语句
const statement = `
  SELECT * FROM products WHERE price > ? AND score > ?;
`
// 问号,表示先空着,固定了,
// excute 在内部会 先进性  prepare 然后再 query
// prepare 的作用
//  1、可以 提高性能,将创建的语句发给 MySQL,MySQL编译,并存储,但是不会执行。我们要提供实际的参数才会执行,并且就算执行多次,也只会编译一次
//  2、防止 sql注入 :之后传入的值,不会像模块引擎那样就编译,一些sql注入的内容不会被执行,,,比如你在后面加了个 or 1 =1 ,则不会执行 or 1=1 
connection.execute(statement, [6000, 7], (err, results, fields) => {
  console.log(results);
  console.log(err);
  console.log(fields);
});
12.3 连接池

我们创建一个连接,但是如果我们有多个请求的话,比如 二十个用户正在请求这个接口,该连接很有可能正在被占用,

事实上,mysql2 给我们提供了连接池 ( connection pools )

连接池可以再需要的时候,自动创建连接,并且创建的连接不会被销毁,会放到连接池中,后续可以继续使用

我们可以再连接池的时候设置 LIMT,最大创建个数

const mysql = require('mysql2');

// 1.创建连接池
const connections = mysql.createPool({
  host: 'localhost',
  port: 3306,
  // 和那个数据库建立连接
  database: 'studycoderwhynode',
  // 数据库用户名字
  user: 'root',
  // 密码
  password: '476182',
  connectionLimit: 10
});

// 2.使用连接池
const statement = `
  SELECT * FROM products WHERE price > ? AND score > ?;
`
connections.execute(statement, [6000, 7], (err, results) => {
  console.log(results);
});


12.4 promise 方式
const mysql = require('mysql2');

// 1.创建连接池
const connections = mysql.createPool({
  host: 'localhost',
  port: 3306,
  // 和那个数据库建立连接
  database: 'studycoderwhynode',
  // 数据库用户名字
  user: 'root',
  // 密码
  password: '476182',
  connectionLimit: 10
});

// 2.使用连接池
const statement = `
  SELECT * FROM products WHERE price > ? AND score > ?;
`
connections.promise().execute(statement, [6000, 7]).then(([results]) => {
  console.log(results);
}).catch(err => {
  console.log(err);
});
12.5 认识 ORM

对象关系映射(简称ORM),是一种程序设计的方案

  • 从效果上将,他提供了一个可在编程语言中,使用虚拟对象的效果
  • Java开发中,尝试用的 ORM 包含 Hibernate 、MyBatis

简单点说,就是我们不需要再操作 sql 语句了,直接用他 提供的 类可以了

node 中 ORM 的库 sequelize,需要安装 mysql2 和 sequelize

12.6 node 中 ORM 的库 sequelize

基本使用

const { Sequelize } = require('sequelize');

const sequelize = new Sequelize('coderhub', 'root', 'Coderwhy888.', {
  host: 'localhost',
  // 要连接的数据库
  dialect: 'mysql'
});

sequelize.authenticate().then(() => {
  console.log("连接数据库成功~");
}).catch(err => {
  console.log("连接数据库失败~", err);
});

跳过 。。。 。。 。 。 。。 。 。

13、项目实战

13.1 功能说明

完整的项目接口包括:

  • 面向用户的业务接口
  • 面向企业或者内部的后台管理接口

功能

  1. 用户管理系统
  2. 内容管理系统
  3. 内容评论管理
  4. 内容标签管理
  5. 文件管理系统
13.2 项目搭建

目录结构划分

13.3 配置信息写入环境变量

npm i dotenv

.env 文件

APP_HOST=http://localhost
APP_PORT=8000

MYSQL_HOST=localhost
MYSQL_PORT=3306
MYSQL_DATABASE=coderhub
MYSQL_USER=root
MYSQL_PASSWORD=.

src=>app=>config.js

const dotenv = require('dotenv');

// 加载环境变量
dotenv.config();
// process.env.APP_PORT

module.exports = {
  APP_HOST,
  APP_PORT,
  MYSQL_HOST,
  MYSQL_PORT,
  MYSQL_DATABASE,
  MYSQL_USER,
  MYSQL_PASSWORD,
} = process.env;

src=>main.js

const app = require('./app/index')
const config = require('./app/config');
app.listen(8000, () => {
  console.log('http:localhost:8000');
})
13.4 配置 postman 和 路由

postman ,可以 点击 collections 创建一个集合,集合右边 有三个点,点击,然后选择 Add Folder ,表示创建一个 文件夹, 再创建的文件夹的后面三个点,点击选择 Add Request 表示 新建一个请求

继续,点击 左上角 的 +New 选择 Environment , 添加环境变量

比如 添加 baseURL 值为 localhost:8000

我们这请求的时候 就可以 先右上角选择 某个环境,然后再 地址栏中,写{{baseURL}} 后面跟上要写的路径即可


配置路由

app=>index

这个文件主要负责把路由注册上

const Koa = require('koa')
const app = new Koa()
const userRouter = require('../router/user.router')


app.use(userRouter.routes())

app.use(userRouter.allowedMethods())
// allowedMethods 用于判断某一个 method 是否支持
// 如果没有实现某个请求,就会自动报错 405


module.exports = app;

router=>user.router.js

这哥文件主要负责,写路由地址

const Router = require('koa-router')
// 导入逻辑
const { create } = require('../controller/user.controller')
const userRouter = new Router({
  prefix: "/users", // 路径前缀
})
// 具体的 逻辑 。不会放在这里,会放到 controller 目录下的  单独抽取出去
// userRouter.get('/', (ctx, next) => {
//   ctx.response.body = "put 成功~"
// })
userRouter.post('/', create)

module.exports = userRouter

controller=>user.controller.js

这个文件主要负责,每个匹配到的路由,处理逻辑

const service = require('../service/user.service')
class UserController {
  async create(ctx, next) {
    // 获取用户请求传递的参数
    const user = ctx.request.body
    // 查询数据
    const result = await service.create(user)

    // 返回数据
    ctx.response.body = "注册t 成功~"
  }
}

module.exports = new UserController()

service=>user.service

这个文件负责,把数据写到 数据库中

class UserService {
  async create(user) {
    // 将 user 存储到 数据库中
  }
}
module.exports = new UserService()

解析数据

npm i koa-bodyparser 解析json urlencoded

13.5 往数据库插入数据 以注册为例

app=>database

const mysql = require('mysql2');

const config = require('./config');

const connections = mysql.createPool({
  host: config.MYSQL_HOST,
  port: config.MYSQL_PORT,
  database: config.MYSQL_DATABASE,
  user: config.MYSQL_USER,
  password: config.MYSQL_PASSWORD
});

// 测试连接是否成功
connections.getConnection((err, conn) => {
  console.log(conn);
  conn.connect((err) => {
    if (err) {
      console.log("连接失败:", err);
    } else {
      console.log("数据库连接成功~");
    }
  })
});

module.exports = connections.promise();

service=>user.service.js

// 导出的 时候,是 connections.promise()  所以下面不需要写 .promise().execute()
const connection = require('../app/database')
class UserService {
  async create(user) {
    const { name, password } = user
    // 将 user 存储到 数据库中
    const statement = `INSERT INTO user (name,password) VALUES (?,?);`
    const result = await connection.execute(statement, [name, password])
    console.log('用户注册到数据库中~');
    return result
  }
}
module.exports = new UserService()

还有判断 客户端 传过来的数据,是否正确

src=>middleware=>user.middleware.js

const verifyUser = async (ctx, next) => {
  // 1.获取用户名和密码
  const { name, password } = ctx.request.body;

  // 2.判断用户名或者密码不能空
  if (!name || !password) {
    const error = new Error(errorTypes.NAME_OR_PASSWORD_IS_REQUIRED);
    return ctx.app.emit('error', error, ctx);
  }

  // 3.判断这次注册的用户名是没有被注册过
  const result = await service.getUserByName(name);
  if (result.length) {
    const error = new Error(errorTypes.USER_ALREADY_EXISTS);
    // 发射一个错误信息
    // 在 app文件夹下新建 一个 error-handle.js
    return ctx.app.emit('error', error, ctx);
  }

  // 为什么加  await 
  // 因为,下一个中间件 可能有异步操作,这样保证,后面的操作全部做完,在 执行 返回
  await next()
}

module.exports = {
  verifyUser
}

在 user.middleware.js 中导入 上面那个文件

const {
  verifyUser,
} = require('../middleware/user.middleware')
// verifyUser 判断 客户端发过来的数据是否正确
userRouter.post('/', verifyUser, create)

然后 还需要处理错误,这个 在 error-handler.js 中 处理

ctx.app.emit(‘error’,error,ctx) 是抛出错误,需要在 接手 ,在 app=>index.js 导入这个文件,放到最后

app.on(‘error’, errorHandler)

13.6 对密码进行加密

对密码进行加密,不能把密码明文传入到数据库中!!!

  1. 在注册密码的 路由中,在验证账号密码,后面加一个 处理密码的 中间件 handlePassword

  2. 在 utils 文件夹下,新建一个 password-handle.js 文件 来专门处理 密码

  3. # utils => password-handle.js
    // 引入 node 自带的一个库
    const crypto = require('crypto')
    const md5password = (password) => {
      const md5 = crypto.createHash('md5') // 采用md5加密算法
      const result = md5.update(password).digest('hex')
      // md5.update(password) 他返回的是一个对象,
      // digest 把他转成 16 进制。这个函数不传递参数的时候,拿到的是一个 Buffer 二进制
      return result
    }
    module.exports = md5password
    
  4. 在 handlePassword 中间件中 引入,并处理一下

    const handlePassword = async (ctx, next) => {
      const { password } = ctx.request.body
      ctx.request.body.password = md5password(password)
      // 对密码进行加密
      console.log(md5password);
      await next()
    }
    
13.7 登陆凭证 cookie session token
  1. 注册路由
  2. 注册路由的处理函数
  3. 注册验证密码的 中间件,需要验证,账号密码是否为空,判断用户是否存在,判断密码是否匹配
  4. 登录成功返回凭证
    1. cookie+session
    2. token 令牌

登陆凭证

  • cookie + session 逐渐被淘汰
  • Token 令牌 才是主流

cookie

cookie 又称为 “小甜饼” 类型为 “小型文本文件”,某些网站为了辨别用户身份而存储在用户本地终端上的数据

浏览器会在特定的情况下携带上 cookie 来发送请求,我们可以通过 cookie 来获取一些信息

  • cookie 总是保存在客户端中,按在客户端中的存储位置,cookie 可以分为内存 cookie 和 硬盘 cookie
  • 内存 cookie 由浏览器保护,保护在内存中,浏览器关闭时 cookie 就会消失,其存在时间是短暂的
  • 硬盘 cookie 保存在硬盘中,有一个过期时间,用户手动清理或者过期时间到时,才会被清理
  • 如果判断一个 cookie 是内存 cookie 还是硬盘 cookie 呢?
  • 没有设置过期时间,默认情况下 cookie 是 内存 cookie ,在关闭浏览器时,会自动删除
  • 有设置过期时间,并且过期时间不为0 或者 负数的 cookie ,是硬盘 cookie 需要手动或者到期时,才会删除

cookie 生命周期

  • 默认情况下 cookie 是内存 cookie 也称之为 会话 cookie 也就是在浏览器关闭时会自动删除
  • 我们可以通过 expires 或者 max-age 来设置过期的时间
    • expires:设置的是 Date.toUTCString(),设置格式是 expires=data-in-GMTString-format
    • max-age:设置过期的秒钟,max-age=max-age-in-seconds (例如一年为60X60X24X365)

cookie 作用域 (允许 cookie 发送给那些 URL)

  • Domain:指定那些域名可以接受 cookie
    • 如果不指定,那些默认是 origin 不包括子域名 ,比如 域名 是 ww.ddg.com,那么 music.ddg.com、video.ddg.com 就是它的子域名,但 ddg.com/ xx 下的,都可以携带
    • 如果指定,则包含子域名。如,设置 Domain=ddg.com 则 cookie 也包含在 子域名中 (如music.ddg.com、video.ddg.com)
  • Path:指定主机下那些路径可以接受 cookie
    • 例如,设置 Path=/docs,则以下地址都会匹配
      • /docs
      • /docs/web/
      • /docs/web/http

设置cookie

一般来说,是服务器设置 cookie,,客户端删除 cookie。但 客户端也可以设置 cookie

浏览器端设置cookie


<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Documenttitle>
head>
<body>
  <button id="btn">添加cookiebutton>
  <script>
    document.getElementById('btn').onclick = function () {
      // document.cookie = 'name=kobe';
      // 通过 js 设置 cookie
      // age值为18 , 过期时间为 5秒
      document.cookie = 'age=18;max-age=5;';
    }
  script>
body>
html>

服务器端设置 cookie

const Koa = require('koa')
const Router = require('koa-router')

const app = new Koa()
const testRouter = new Router()
app.use(testRouter.routes())
testRouter.get('/test', (ctx, next) => {
  ctx.cookies.set("name", "lilei", {
    // maxAge: 是毫秒
    maxAge: 500 * 1000,

  })
  ctx.body = "牛逼"
})
testRouter.get('/demo', (ctx, next) => {
  // 读取 cookie
  const value = ctx.cookies.get('name')
  ctx.body = "你的cookie是" + value
})

app.listen(4000, () => {
  console.log('4000');
})
session

session 其实是基于 cookies 的,,但是一般不会 cookie 或者 session 单独来使用,一般是 cookie+session 结合使用

const Koa = require('koa');
const Router = require('koa-router');
// 安装  koa-session
const Session = require('koa-session');
const app = new Koa();
const testRouter = new Router();
// 创建Session的配置
const session = Session({
  // 起个名 叫 sessionid
  key: 'sessionid',
  maxAge: 10 * 1000, // 1单位也是毫秒
  signed: true, // 是否使用加密签名,防止客户端伪造
}, app);

// 设置签名以后,在浏览器 cookies 里面查看 ,会有一个 sessionid.sig 的一个属性,这个就是签名

// 设置签名, 通过 "aaaa" 来 加密
app.keys = ["aaaa"];
app.use(session);

// 登录接口
testRouter.get('/test', (ctx, next) => {
  // name/password -> id/name
  const id = 110;
  const name = "coderwhy";

  // sessionid = { id,name} 会把这个对象转换成 base64
  ctx.session.user = { id, name };
  ctx.body = "test";
});

testRouter.get('/demo', (ctx, next) => {
  console.log(ctx.session.user);
  ctx.body = 'demo';
});
app.use(testRouter.routes());
app.use(testRouter.allowedMethods());
app.listen(8080, () => {
  console.log("服务器启动成功~");
})
cookie 和 session 的缺点
  1. cookie 会被附加在每个http 请求中,所以 无形中增加了流量(事实上某些请求是不需要的)
  2. cookie 是明文传递的,所以存在安全问题
  3. cookie 的大小限制是 4KB 有的还会有个数限制,对于复杂的需求来说是不够的
  4. 对于浏览器外的其他客户端(比如 ios 、Android),必须手动的设置 cookie 和 session
  5. 对于分布式系统和服务器集群中如何可以保证其他系统也可以正确的解析 session?

后面两个才是 最致命的缺点

在浏览器端,我们可以通过 js 设置cookie ,但是在其他端,cookie 需要你手动设置,每次发一个请求,你就需要设置一次。浏览器客户端和其他客户端,是不统一的

服务器集群,对于高并发的一些东西,比如有几万人访问这个接口,一个服务器是承受不了的,所以,需要多个服务器,这些服务器的代码可能一样,他们组成服务器集群,有 nginx来做一个反向代理,看看那个服务器空闲,就把这个请求分发给那个服务器。。。比如A服务器设置了sessionid,发过去了, 客户端请求的时候,被nginx分发到了B服务器,B服务器怎么解析。。

分布式,就是把系统分开,分成好多个子系统,session由用户管理系统,但我访问其他系统,其他系统怎么解析?

以上这两个缺点,都有解决方案,但是 使用 cookie+session 的越来越少

token
  • token可以翻译为令牌;

  • 也就是在验证了用户账号和密码正确的情况,给用户颁发一个令牌;

  • 这个令牌作为后续用户访问一些接口或者资源的凭证;

  • 我们可以根据这个凭证来判断用户是否有权限来访问;

步骤:

  1. 生成 token 登录的时候,颁发token
  2. 验证 token 访问某些资源或者接口时,验证token
JWT 实现 token 机制

JWT 生成 token 的组成

  • header

    • alg:采用的加密算法,默认时 HMAC SHA256(HS256),采用同一个密钥进行加密和解密

    • typ:JWT ,固定值

    • 会通过 base64Url 算法进行编码

  • payload

    • 携带的数据,比如,我们可以将用户的 id 和 name 放到 payload 中
    • 默认也会携带 iat (issued at),令牌的签发时间
    • 我们也可以设置过期时间:exp
    • 会通过 base64Url 算法进行编码
  • signature (是签名的意思)

    • 设置一个 secretKey(密钥) 通过将 前两个的结果合并后进行 HMAC SHA256(HS256) 的算法
    • HMAC SHA256(base64Url(header)+.+base64Url(payload),secretKey )
    • 但是如果 secretKey 暴露是一件非常危险的事情,因为之后就可以模拟颁发 token,也可以解密 token

在真实开发中,我们可以直接使用一个库来完成: jsonwebtoken;

一般来说,都会在请求头设置, 一个 Authorization 字段, 属性值是 :Bearer token……

const Koa = require('koa');
const Router = require('koa-router');
const jwt = require('jsonwebtoken');
const app = new Koa()
const testRouter = new Router()
// 设置一个签名
const SERCET_KEY = 'daidaigou'
// 登录接口
testRouter.get('/test', (ctx, next) => {
  // 登陆成功以后
  // 令牌 包括 id  name

  // 设置 payload  
  const user = { id: 123, name: "呆呆狗" }
  // 第一个参数,就是你要传递什么数据
  // 第二个参数,密钥
  // 第三个参数,额外的参数
  const token = jwt.sign(user, SERCET_KEY, {
    // 设置过期时间  单位秒
    expiresIn: 50 * 1000,
  })
  ctx.body = token;
})
// 验证接口
testRouter.get('/demo', (ctx, next) => {
  // 一般来说, token都是设置在请求头中, 名为:Authorization,值为 Bearer token
  const authorization = ctx.headers.authorization;
  const token = authorization.replace("Bearer ", "");

  // JWT 验证失败,会直接抛出异常的 可以用 try catch 捕获
  try {
    // 验证 token  
    // 第一个参数 token
    // 第二个颁发的签名
    const result = jwt.verify(token, SERCET_KEY);
    ctx.body = result;
  } catch (error) {
    console.log(error.message);
    ctx.body = "token是无效的~";
  }
  // 解析成功则会返回
  /* 
  {
    "id": 123,
    "name": "呆呆狗",
    "iat": 1625559910,  颁发的时间
    "exp": 1625609910   过期的时间
  }
  */
})
app.use(testRouter.routes())
app.listen(4000, () => {
  console.log('服务器开启 4000');
})
非对称加密

HS256 加密算法一旦密钥暴露,就会非常危险,

这个时候,我们可以使用非对称加密 RS256

  • 私钥(private key)用户发布令牌
  • 公钥(public key)用于验证令牌
# 打开 git bash
// 进入 openssl
openssl
 // 生成私钥 
 genrsa -out private.key 1024  
 // genrsa 表示生成    -out 表示输出   1024 表示位数 

 // 生成公钥
 rsa -in private.key -pubout -out public.key
 // rsa -in private.key 把 private.key 作为一个输入,要依靠 私钥生成公钥
 // -pubout 表示生成公钥
 // -out public.key 输出位置
const Koa = require('koa');
const Router = require('koa-router');
const jwt = require('jsonwebtoken');
const fs = require('fs');
const { pathToFileURL } = require('url');

// fs 读取文件的时候,有时候可以使用相对路径,有时候不可以
// 我们可以在 这个项目的 上一个 文件夹,来启动这个项目,这个时候,使用相对路径就会有问题
// 在项目中的任何一个地方的相对路径,都是相对于 process.cwd
// 就是你在那个文件夹下启动这个项目 process.cwd 就是那个文件夹

// 解决方案
// 1. 使用 path.resolve(__dirname,拼接路径)
// 2. 

const app = new Koa();
const testRouter = new Router();

// 在项目中的任何一个地方的相对路径, 都是相对于process.cwd()
console.log(process.cwd());

const PRIVATE_KEY = fs.readFileSync('./keys/private.key');
const PUBLIC_KEY = fs.readFileSync('./keys/public.key');

// 登录接口
testRouter.get('/test', (ctx, next) => {
  // 登陆成功以后
  // 令牌 包括 id  name
  const user = { id: 110, name: 'why' };
  // 第一个参数,就是你要传递什么数据
  // 第二个参数,密钥
  // 第三个参数,额外的参数
  const token = jwt.sign(user, PRIVATE_KEY, {
    // 设置过期时间  单位秒
    expiresIn: 50 * 1000,
    // 设置 非对称 加密 算法
    algorithm: "RS256"
  });

  ctx.body = token;
});

// 验证接口
testRouter.get('/demo', (ctx, next) => {
  const authorization = ctx.headers.authorization;
  const token = authorization.replace("Bearer ", "");

  // JWT 验证失败,会直接抛出异常的 可以用 try catch 捕获
  try {
    // 验证 token  
    // 第一个参数 token
    // 第二个颁发的签名
    // 第三个参数 设置其他参数
    const result = jwt.verify(token, PUBLIC_KEY, {
      // 设置 验证方式,是一个数组
      algorithms: ["RS256"]
    });
    ctx.body = result;
  } catch (error) {
    console.log(error.message);
    ctx.body = "token是无效的~";
  }
});

app.use(testRouter.routes());
app.use(testRouter.allowedMethods());

app.listen(8080, () => {
  console.log("服务器启动成功~");
})
13.8 处理项目的登录问题

采用非对称加密

  1. 注册路由
  2. 处理 路由逻辑
  3. 添加验证中间件
  4. 给验证中间件 ctx 添加 user 属性,值是 通过 数据查询到的对象
  5. 颁发 token
  6. 在 auth中间件中,添加一个处理用户 token 是否有效的函数
13.9 发布和修改动态

创建新的表 moment

定义发布动态内容的接口

定义路由接口

验证用户登录

Controller和Service中处理内容

定义修改动态内容的接口

定义路由接口

验证用户登录

验证用户的权限

Controller和Service中的处理

13.10 删除和查询、添加动态内容

删除

  1. 定义路由接口
  2. 验证用户登录
  3. 验证用户权限
  4. Controller 和 Service 的处理
13.11 发表和修改评论
13.12 上传和 查看头像
CREATE TABLE IF NOT EXISTS `avatar`(
	id INT PRIMARY KEY AUTO_INCREMENT,
	filename VARCHAR(255) NOT NULL UNIQUE,
	mimetype VARCHAR(30),
	size INT,
	user_id INT,
	createAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
	updateAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
	FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE ON UPDATE CASCADE
);

客户端请求某个地址

然后,我们读取出来,这个信息,

ctx.responese.set('content-type',sql查询到的对象的mimetype属性 就是, 'image/jpeg')
ctx.body = fs.createReadStream('路径+查询到的对象的 filename 属性')

文章的图片

还做了一个处理,大图、小图、中图

用 jimp 插件

const file = ctx.req.files
const destPath = path.join(file.destination,file.filename)

for( let file of files){
  Jimp.read(file.path).then(image=>{
	// 第一个参数是大小,第二个是宽度自适应
  image.resize(1280,Jimp.AUTO).write(`${destPath}-small`)
  image.resize(1280,Jimp.AUTO).write(`${destPath}-middle`)
   image.resize(1280,Jimp.AUTO).write(`${destPath}-big`)
})
}

// 客户端发起请求的时候,可以跟一个参数,  是 返回 小图、中图、大图?

你可能感兴趣的:(前端学习笔记,node,vue,node,node.js,vue.js)