Enode.js 是基于v8 JavaScript引擎的 JavaScript运行时环境
任何可以使用 JavaScript来实现的应用都最终都会 使用 JavaScript 实现
当我们 执行 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);
})
// 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的输出方式官方文档
这些全局对象实际上是模块中的变量,只是每个模块都有,看起来像是全局变量,在命令行交互中是不可以使用的
包括 __dirname、__filename、exports、module、require()
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
可以看官方文档
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 中,其实每一个文件都是一个模块
commonJS 是一个 规范,最初提出来是再浏览器以外的地方使用,并且当时被命名为 Server.js,后来为了体现他的广泛性,修改为 CommonJS , 平时我们也会简称为 CJS
node 中每一个 js文件都是一个单独的模块,这个模块包括 CommonJS 规范的核心变量 :export 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')
其实就是导出的 module,源码中 module.exports = exports
查找规则
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)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZKm8W7UQ-1625664086040)(F:\web前端开发\自学\coderwhy-node\coderwhy-node.assets\image-20210629185221783.png)]
此图中,就是循环引入, 就是 图 数据结构, 图结构在遍历的时候,node 采用了 深度优先搜索,所以 main => aaa => ccc => ddd => eee
走到 eee 里面没有引入了,会返回上一层,看看上一层还有没有引入其他的,没有在返回,直到 main,看到还引入了 bbb 则 引入 bbb => ccc
AMD 主要是应用于浏览器的一种模块化规范
它采用的是 异步加载模块
事实上 AMD 的规范 还要早于 CommonJS,但是 CommonJS目前依然在被使用,而 AMD 使用的较少了
用起来 是 有点复杂了。需要引入一个 require.js
也是一个异步加载模块
但是他将 commonJS 的 优点吸收过来了
seajs 需要用这个依赖文件
他是 js 的一个模块化系统
导出的方式
导入的方式
在开发和封装功能的时候,通常希望将暴露 的所有接口放到一个文件中
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()
})
}
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
读取文件信息的三种方式
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);
})
node中的核心API 都是基于异步事件驱动的。
在这个体系中,某些对象(发射器(Emiteers))发出某一个事件
我们可以监听这个事件 (监听器 Listeners),并且传入的回调函数,这个回调函数会在监听到事件时调用
发出事件和监听事件 都是通过 EventEmitter 类 来完成的,他们都属于 events 对象
emitter.on(eventName,listener): // 监听事件,也可以使用 addListener
emitter.off(eventName,listener): // 移除事件,也可以使用 removeListener
emitter.emit(eventName[,...args]): // 发出事件,可以携带一些参数
每一个项目都对应一个配置文件(package.json),包括项目名称、使用的插件、版本号、项目描述等等
npm init -y
比如版本 2.23.8
^2.23.8 表示 第一个数保持不变的, 第二个和第三个 永远安装最新的版本
~2.23.8 表示 第一个和第二个保持不变的,第三个永远安装最新的版本
计算机追踪所有的内容:文字、数字、图片、音频、视频都终会使用二进制来表示
对于前端来说,很少会和二进制打交道,但是对于服务器端为了做很多的功能,必须直接去操作二进制的数据
所以 node 为了 可以方便开发者完成更多功能,提供给了我们一个类 Buffer , 并且它是全局的
Buffer 看成是一个存储二进制的数组,数组中的每一项,可以保存 8位 二进制
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);
const message = "呆呆狗"
// 其实 一个汉字 对应 三个字节码,
const buffer2 = Buffer.from(message)
console.log(buffer2);
//
// 解码
console.log(buffer2.toString());// 呆呆狗
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')
事件循环 可以理解成 我们编写 js 代码 和 浏览器 或者 node 之间的一个桥梁
浏览器的事件循环是 一个我们编写的 js 代码和 浏览器 API 调用 的一个桥梁,桥梁之间他们通过回调函数进行沟通
node 的事件循环是一个我们编写 js 代码 和 系统调用 (file、system、network) 之间的一个桥梁,桥梁 之间他们通过回调函数进行沟通的
进程: 计算机已经运行的程序
线程:操作系统能够运行运算调度的最小单位
进程更像是线程的容器
操作系统就像是一个工厂,工厂里面有很多车间,这个车间就是进程,工人就是线程
第一道
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
分析:
- 先输出 pr1 , then1 进入 微任务队列
- 输出 2 , queueMicrotask 创建一个微任务
- then3 加入微任务
- 主线程 执行完毕,执行 第一波微任务 依次输出 then1,queueMicrotask,then3,此时已经没有微任务了,继续执行下一个宏任务
- 第一个定时器, 输出 set1 ,把 第一个定时器的 then 加入微任务队列
- 输出 then2
- 这一次的宏任务执行完毕,继续执行这一次的微任务,输出 then4
- 执行第二个定时器
第二道
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 下面的代码是微任务
如果我们 希望在程序中对一个文件进行操作,那么我们就需要打开这个文件内: 通过 文件描述符
js 可以对一个文件进行操作嘛?
看起来是可以的,但是事实上我们任何程序中的文件操作 都是 需要进行 系统调用
事实上,对文件的操作,是一个操作系统的系统调用 (IO系统,IO是输入、输出)
操作系统为我们提供了两种调用方式 :阻塞时调用 和 非阻塞式调用
阻塞式:调用结果返回之前,当前线程处于阻塞态(阻塞态CPU是不会分配时间片的),调用线程只有在得到调用结果之后,才会继续执行
非阻塞式:带哦用执行之后,当前线程不会停止执行,只需要过一段时间来检查有没有结果返回即可
所以在开发中的很多 耗时操作,都可以基于这样的 非阻塞式 调用
非阻塞式调用 也存在一定问题,,我们不一定获取到 需要 读取的结果。不知道返回来的数据是否是完整的,所以为了知道是否读取完成,我们需要频繁的去确定读取到的数据是否是完整的,这个过程称之为 轮询操作
这个轮询工作有谁完整?
我们开发中不只是一个文件的读写,可能是多个文件,也可能是 网络的IO、数据库的IO、子线程调用
libuv 提供了一个 线程池
一次事件循环,就是一次 tick
微任务队列
事件循环 执行顺序就是
面试题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 是同时进行的,但事件循环初始化完毕后,就要执行事件循环了
不好理解。。。。。
读取文件
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();
const http = require('http')
const server = http.createServer((req, res) => {
res.end('hello world')
})
server.listen(8000, () => {
// 端口 尽量不要写 1024 以下的
console.log('启动成功');
})
// 第一种
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 来获取到系统分配的端口号
})
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('启动成功');
})
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('启动成功');
})
content-type : 是这次请求携带的数据的类型
content-length :文件的大小和长度
connection : 的 keep-alive
http是基于 TCP 协议的,但是通常在进行一次请求和响应结束后会立刻中断
在 http1.0 中,如果想要继续保持链接
在 http1.1中,所有连接默认是 connection : keep-alive 的
不同的 web 服务器会有不同的保持 keep-alive 的时间
node 默认是 5S
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('启动成功');
})
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('启动成功');
})
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('启动成功');
})
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()
// 传递的是一个 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("文件上传服务器开启成功~");
})
方式一
// 安装脚手架
npm i express-generator -g
// 创建项目
express express-demo
// 安装依赖
npm i
// 启动项目
node bin/www
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 初体验启动成功~');
})
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('服务器启动成功·');
})
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('服务器启动成功·');
})
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来处理非文件的键值对数据
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('服务器启动成功·');
})
// express 官方的 morgan 库
const loggerWriter = fs.createWriteStream('./log/access.log',{ flag: 'a+'})
app.use(morgan('combined', {stream : loggerWriter}))
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 初体验启动成功~');
})
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 初体验启动成功~');
})
将路由抽离为单独模块的步骤如下:
入口文件
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
const express = require('express')
// express 其实是一个函数
const app = express()
app.use(express.static('./build'))
// 开启监听
app.listen(8000, () => {
console.log('express 初体验启动成功~');
})
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("路由服务器启动成功~");
});
koa node.js的下一代 web 框架
事实上 express 和 koa 是同一个开发团队开发的
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');
})
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');
})
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');
})
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');
})
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');
})
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');
})
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');
})
输出结果
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');
})
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('静态资源服务器启动成功~');
})
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('服务器启动成功~');
})
从架构上来说
需求: 假如有三个中间件,会在一次请求中匹配到,并且按照顺序执行
- 在middleware1 中,在 req.message 中添加一个字符串 aaa
- 在middleware2 中,在 req.message 中添加一个字符串 bbb
- 在middleware3 中,在 req.message 中添加一个字符串 ccc
当所有内容添加结束后,在 middleware1 中 通过 res 返回最终的结果
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
const middleware3 = (req, res, next) => {
// 假如,要在这个未知, 发起一个异步请求,然后,再拼接值,在返回出去,
// 结果不会是我们想要的
// 所以 express 处理这些异步数据,是有些乏力的
// 也可以再外面定义一个函数,先请求,
req.message += "ccc"
console.log(3);
}
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("服务器启动成功~");
})
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("服务器启动成功~");
})
安装,微信搜索 软件管家 找到 MySQL 即可
首先要添加环境变量,找到 系统变量 => Path 双击进去,点击新建,C:\Program Files\MySQL\MySQL Server 8.0\bin
点击确定
打开 cmd 输入 mysql --version 即可查看版本号
mysql -uroot -p跟上密码
然后就可以连接上数据库了
show databases;
默认显示四个数据库
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);
GUI工具就是 图形化操作工具
- Navicat 首选,但收费,可各显神通~~
- SQLYog:免费的SQL
- TablePlus 常用功能都可以使用,但是会多一些限制
各显神通 ~~
SQL 语句分类
写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;
MySQL 支持的数据类型有: 数字类型、日期类型和时间类型,空间类型和 JSON 数据类型
最常见的是 前三种
数字类型
日期类型 用的最多的还是 TIMESTAMP
DATETIME 或者 TIMESTAMP 值可以包括在高达微秒(6位)精度的后小数秒一部分
1000-01-01 00:00:00:000000 到 9999-12-31 23:59:59:999999
字符串类型
主键: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 类型中
# 测试~~~~~~
# 查看所有的表
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`
插入数据和时间默认数据
# 插入数据
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;
# 基本查询
# 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 之后使用
# 先分组,然后在查找,查找平均价格大于2000的
SELECT 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;
设置外键
# 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';
强制更新外键依赖的 字段
# 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;
# 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)
一个学生可以选择多门课程,一个课程也可以被多个学生选择
# 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;
# 将联合查询到的数据转成对象(一对多)
# 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;
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);
});
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);
});
我们创建一个连接,但是如果我们有多个请求的话,比如 二十个用户正在请求这个接口,该连接很有可能正在被占用,
事实上,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);
});
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);
});
对象关系映射(简称ORM),是一种程序设计的方案
- 从效果上将,他提供了一个可在编程语言中,使用虚拟对象的效果
- Java开发中,尝试用的 ORM 包含 Hibernate 、MyBatis
简单点说,就是我们不需要再操作 sql 语句了,直接用他 提供的 类可以了
node 中 ORM 的库 sequelize,需要安装 mysql2 和 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);
});
跳过 。。。 。。 。 。 。。 。 。
完整的项目接口包括:
- 面向用户的业务接口
- 面向企业或者内部的后台管理接口
功能
目录结构划分
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');
})
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
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)
对密码进行加密,不能把密码明文传入到数据库中!!!
在注册密码的 路由中,在验证账号密码,后面加一个 处理密码的 中间件 handlePassword
在 utils 文件夹下,新建一个 password-handle.js 文件 来专门处理 密码
# 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
在 handlePassword 中间件中 引入,并处理一下
const handlePassword = async (ctx, next) => {
const { password } = ctx.request.body
ctx.request.body.password = md5password(password)
// 对密码进行加密
console.log(md5password);
await next()
}
登陆凭证
cookie 又称为 “小甜饼” 类型为 “小型文本文件”,某些网站为了辨别用户身份而存储在用户本地终端上的数据
浏览器会在特定的情况下携带上 cookie 来发送请求,我们可以通过 cookie 来获取一些信息
- cookie 总是保存在客户端中,按在客户端中的存储位置,cookie 可以分为内存 cookie 和 硬盘 cookie
- 内存 cookie 由浏览器保护,保护在内存中,浏览器关闭时 cookie 就会消失,其存在时间是短暂的
- 硬盘 cookie 保存在硬盘中,有一个过期时间,用户手动清理或者过期时间到时,才会被清理
- 如果判断一个 cookie 是内存 cookie 还是硬盘 cookie 呢?
- 没有设置过期时间,默认情况下 cookie 是 内存 cookie ,在关闭浏览器时,会自动删除
- 有设置过期时间,并且过期时间不为0 或者 负数的 cookie ,是硬盘 cookie 需要手动或者到期时,才会删除
cookie 生命周期
cookie 作用域 (允许 cookie 发送给那些 URL)
设置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 其实是基于 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("服务器启动成功~");
})
后面两个才是 最致命的缺点
在浏览器端,我们可以通过 js 设置cookie ,但是在其他端,cookie 需要你手动设置,每次发一个请求,你就需要设置一次。浏览器客户端和其他客户端,是不统一的
服务器集群,对于高并发的一些东西,比如有几万人访问这个接口,一个服务器是承受不了的,所以,需要多个服务器,这些服务器的代码可能一样,他们组成服务器集群,有 nginx来做一个反向代理,看看那个服务器空闲,就把这个请求分发给那个服务器。。。比如A服务器设置了sessionid,发过去了, 客户端请求的时候,被nginx分发到了B服务器,B服务器怎么解析。。
分布式,就是把系统分开,分成好多个子系统,session由用户管理系统,但我访问其他系统,其他系统怎么解析?
以上这两个缺点,都有解决方案,但是 使用 cookie+session 的越来越少
token可以翻译为令牌;
也就是在验证了用户账号和密码正确的情况,给用户颁发一个令牌;
这个令牌作为后续用户访问一些接口或者资源的凭证;
我们可以根据这个凭证来判断用户是否有权限来访问;
步骤:
JWT 生成 token 的组成
header
alg:采用的加密算法,默认时 HMAC SHA256(HS256),采用同一个密钥进行加密和解密
typ:JWT ,固定值
会通过 base64Url 算法进行编码
payload
signature (是签名的意思)
在真实开发中,我们可以直接使用一个库来完成: 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
# 打开 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("服务器启动成功~");
})
采用非对称加密
创建新的表 moment
定义发布动态内容的接口
定义路由接口
验证用户登录
Controller和Service中处理内容
定义修改动态内容的接口
定义路由接口
验证用户登录
验证用户的权限
Controller和Service中的处理
删除
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`)
})
}
// 客户端发起请求的时候,可以跟一个参数, 是 返回 小图、中图、大图?