一、Nodejs
1. nodejs 基础知识
1.1 nodejs 定义
Nodejs 是基于 V8 JavaScript 引擎的 JavaScript 运行时环境。
1.2 nodejs 架构图
- 我们编写的 JavaScript 代码会经过 V8 引擎,再通过 Nodejs 的 Bindings,将任务放到 Libuv 的事件循环中;
- libuv 是使用** C 语言**编写的库;
- libuv 提供了事件循环、文件系统读写、网络IO、线程池等等内容。
我们使用 JavaScript 编写的应用程序经过 v8 引擎的处理,通过 Nodejs Bindings(NODE API)的处理,将其交给 LIBUV,然后 LIBUV 将这些内容放到工作线程中(存放着一个个的事件处理程序),然后执行回调函数,再通过事件循环放到事件队列中依次调用这些函数,当函数出错时,会执行阻塞操作,再交由对应的程序处理,直到事件全部执行完成。
1.3 nodejs 的应用
- 目前前端开发的库都是以node包的形式进行管理;
- npm、yarn、pnpm 工具成为前端开发使用最多的工具;
- 越来越多的公司使用 Nodejs 作为 web 服务器开发、中间件、代理服务器;
- 大量项目需要借助 Nodejs 完成前后端渲染的同构应用;
- 编写脚本工具;
- 使用 Electron 来开发桌面应用程序;
1.4 全局对象
__dirname: 获得当前文件所在的路径,不包括后面的文件名。
__filename: 获得当前文件所在的路径和文件名称,包括后面的文件名。
**process对象:**提供了 Node 进程中相关的信息:
- 比如 Node 的运行环境、参数信息等;
- 可以将一些环境变量读取到 process 的 env 中;
console对象: 详细信息
定时器函数:
- setTimeout(callback,delay, [...args]):callback 在 delay 毫秒后执行一次;
- setInterval(callback,delay, [...args]):callback 每 delay 毫秒重复执行一次;
- setImmediate(callback,delay, [...args]):callback I/0 事件后的回调的 “立即” 执行;
- process.nextTick(callback, [...args]):添加到下一次 tick 队列中;
global 和 window 的区别:
在浏览器中,全局变量都是在 window 上的,比如有 document、setInterval、setTimeout、alert、console 等;
在 Node 中,也有一个 global 属性,并且看起来它里面有很多其他对象。
但是在浏览器中执行的 JavaScript 代码,如果我们在顶级范围内通过 var 定义的一个属性,默认会被添加到 window 对象上:
var name = 'lwz';
console.log(window.name); // lwz
但是在 node 中,我们通过 var 定义一个变量,它只是在当前模块中有一个变量,不会放到全局变量中:
var name = 'lwz';
console.log(name); // undefined
2. JavaScript 模块化开发
2.1 模块化定义
-
什么是模块化、模块化开发
- 模块化开发的最终目的是将程序划分成一个个小的结构;
- 这个结构中编写属于自己的逻辑代码,有自己的作用域,定义变量名词时不会影响到其他的结构;
- 这个结构可以将自己希望暴露的变量、函数、对象等导出给其他结构使用;
- 也可以铜鼓某种方式,导入另外结构中的变量、函数、对象等;
上面提到的结构,就是模块;按照这种结构划分开发程序的过程,就是模块化开发的过程。
2.2 CommonJS 规范和 Node 关系
CommonJs 是一个规范,最初提出来是在浏览器之外的地方使用,在当时被命名为 ServerJS,后修改为 CommonJs,也简称CJS。
CommonJS规范的核心变量:exports、module.exports、require。
2.3 exports 导出与 module.export 关系
exports 是一个对象,我么你可以在这个对象中添加很多个属性,添加的属性会导出
// 导出文件
exports.name = name;
exports.age = age;
exports.sayHello = sayHello;
// 还可简写为:
exports {
name,
age,
sayHello
}
// 导入文件
// bar 变量等于 exports 对象
const bar = require('./filename')
CommonJs 中是没有 module.exports 概念的。但是为了实现模块的导出,Node 中使用的 Module 的类,每一个模块都是 Module 的一个实例,也就是 module,所以在 Node 中真正用于导出的其实根本不是 exports,而是 module.exports,所以 module 才是导出的真正实现者。
- 但是,为什么 exports 可以导出呢?
-
- module 对象的 exports 属性是 exports 对象的一个引用;
-
- 也就是说 module.exports = exports = bar
-
因为 module.exports 不再引用 exports 对象了,所以修改 exports 并不会修改 module.exports 导出对象中的值。
module.exports = exports = {}
// case 1
// a.js
module.exports = function f() { ... }
// b.js
var x = require('./a.js')() // this is ok, because you overwrite the module.exports
// case 2
// a.js
exports = function f() { ... }
// b.js
var x = require('./a.js')()
// this is error, because the real export is module.exports, but you only change exports,
// while module.exports doesn't change, as the exports is only the reference of the module.exports
2.4 require 细节
require 是一个函数,可以帮助我们引入一个文件(模块)中导出的对象。,导出格式为:require(X) 时,查找规则如下:
-
X 是一个 Node 核心模块,比如 path、http
- 直接返回核心模块,并且停止查找
-
X 是以 ./ 或 ../ 或 /(根目录)开头的
-
将 X 当做一个文件在对应的目录下查找:
- 如果有后缀名,按照后缀名的格式查找对应的文件
- 如果没有后缀名,会按照如下顺序:
- 直接查找文件 X
- 查找 X.js 文件
- 查找 X.json 文件
- 查找 X.node 文件
-
没有找到对应的文件,将 X 作为一个目录
- 查找目录下面的 index 文件
- 查找 X/index.js 文件
- 查找 X/index.json 文件
- 查找 X/index.node 文件
- 查找目录下面的 index 文件
-
如果没找到,报错:not found
-
-
直接是一个 X(没有路径),并且 X 不是一个核心模块
-
F:/foo/node/main.js 中编写 require('bar')
-
会在 path 产生的路径中一一查找:
- /foo/node_modules
- foo/node/node_modules
-
如果上面的路径中都没有找到,那么报错:not found
2.5 模块加载过程
- 模块在被第一次引入时,模块中的 js 代码会被执行一次
- 模块被多次引入时,会缓存,最终只加载(运行)一次
- 每个模块对象 module 都一个属性:loaded。为 false 表示还没有加载,为 true 表示已经加载完成。
- 如果有循环引入,加载顺序采用深度优先算法
2.6 CommonJs 规范缺点
-
CommonJS 加载模块是同步的:
- 同步表示只有等到对应的模块加载完成,当前模块中的内容才会执行;在服务器中不会有什么问题,因为服务器中加载的 js 文件都是本地文件,加载速度非常快;但是应用于浏览器时,浏览器加载 js 文件需要先从服务器将文件下载下来,之后再加载运行,采用同步就意味着后续的 js 代码都无法正常运行,即使是一些简单的 DOM 操作。
-
早期为了解决在浏览器中可以使用模块化,通常会使用 AMD 或 CMD。但是现在浏览器已经支持 ES Modules,另一方面借助 webpack 等工具可实现对 CommonJS 或者 ES Module 代码的转换;
-
AMD 和 CMD 已经很少使用。
2.7 ES Module
模块化导出:import & export。 如果需要动态导入某个文件,可以使用import 函数。此外,采用 ES Module 会开启严格模式。
2.8 ES Module 的解析流程
- 构建(Constructor): 根据地址查找 js 文件,并且下载,将其解析成模块记录(Module Record)
- **实例化(Instantiation):**对模块记录进行实例化,并且分配内存空间,解析模块的导入和导出语句,把模块指向对应的内存地址。
- **运行(Evaluation):**运行代码,计算值,并且将值填充到内存地址中。
1.3 包管理工具
1.3.1 semver 版本规范X.Y.Z
- X 主版本号(major): 做了不兼容的 API 修改(可能不兼容之前的版本);
- Y 次版本号(minor): 做了向下兼容的功能性修改(新功能增加,但兼容之前的版本);
- X 主版本号(major): 做了向下兼容的问题修改(没有新功能,修复了之前版本的 bug);
- x.y.z: 表示一个明确的版本号;
- ^x.y.z: x 是永远不变的,y 和 z 永远安装最新的版本;
- ~x.y.z: x 和 y 保持不变,z 永远安装最新的版本。
2. node 高级知识
2.1 文件系统(file system)
2.1.1 文件读取
- 同步读取
const fs = require("fs")
// 如果不指定编码方式,默认是 Buffer 二进制文件流
const res1 = fs.readFileSync("./aaa.txt", {
encoding: "utf-8"
})
console.log(res1)
console.log("后续的代码")
// res1 的值
// 后续的代码
- 异步读取-回调函数
const fs = require("fs")
const res2 = fs.readFile(
"./aaa.txt",
{
encoding: "utf-8"
},
(error, data) => {
if (error) {
return console.log("文件读取失败!", error)
} else {
console.log("读取文件结果: ", data)
}
}
)
console.log("先执行这里的代码~")
- 异步读取-promise
const fs = require("fs")
fs.promises
.readFile("./aaa.txt", {
encoding: "utf-8"
})
.then(res => {
console.log("读取文件结果: ", res)
})
.catch(err => {
console.log("读取文件失败: ", err)
})
console.log("先执行这里的代码~")
2.1.2 文件描述符
在常见的操作系统上,对于每个进程,内核都维护着一张当前打开着的文件和资源的表格。 每个打开的文件都分配了一个称为文件描述符的简单的数字标识符。 在系统层,所有文件系统都使用这些文件描述符来表示和跟踪每个特定的文件。 Windows 系统使用了一个虽然不同但概念上类似的机制来跟踪资源。
为了简化用户的工作,Nodejs 抽象出操作系统之间的特定差异,并为所有打开的文件分配一个数字类型的文件描述符。
fs.open() 方法用于分配新的文件描述符。 一旦被分配,则文件描述符可用于从文件读取数据、向文件写入数据、或请求关于文件的信息。
const fs = require("fs")
// 打开文件
fs.open("./bbb.txt", (error, fd) => {
if (error) {
return console.log("打开文件失败: ", error)
}
// 1. 获取文件描述符
console.log(fd)
// 2. 读取文件的信息
fs.fstat(fd, (error, stas) => {
if (error) return
console.log(stas)
// 3. 手动关闭文件
fs.close(fd)
})
})
2.1.3 文件读写操作
- fs.readFile(path, options?, callback): 读取文件的内容
- fs.writeFile(file, data, options?, callback): 在文件中写入内容
- 其中,options 中的 flag 选项可选值有很多,具体可见
const fs = require("fs")
// 1. 有一段内容(客户端传递过来 http/express/koa)
const content = "Hello World, my name is fogjoe!"
// 2. 文件的写入操作
fs.writeFile(
"./ccc.txt",
content,
{
// 可以不写,默认是 utf-8
encoding: "utf-8",
// 写入的方式
flag: "w"
},
err => {
if (err) {
console.log("文件写入失败:", err)
} else {
console.log("文件写入成功!")
}
}
)
2.1.4 文件夹操作
- 文件夹创建
const fs = require("fs")
// 1. 创建文件夹
fs.mkdir("./why", err => {
if (err) {
console.log("文件创建失败")
}
})
- 文件夹读取
const fs = require("fs")
// 读取文件夹
// 1. 读取文件夹,获取到文件夹中文件的字符串
// fs.readdir("./why", (err, files) => {
// console.log(files)
// })
// 2. 读取文件夹,获取到文件夹中文件的信息
// fs.readdir("./why", { withFileTypes: true }, (err, files) => {
// if (err) return console.log("文件夹获取失败")
// files.map(file => {
// if (file.isDirectory()) {
// console.log("file 是一个文件夹:", file.name)
// } else {
// console.log("file 是一个文件:", file.name)
// }
// })
// })
// 3. 递归的读取文件夹中所有文件的信息
function readDirectory(path) {
fs.readdir(path, { withFileTypes: true }, (err, files) => {
if (err) return console.log("文件夹读取失败")
files.map(file => {
if (file.isDirectory()) {
readDirectory(`${path}/${file.name}`)
} else {
console.log("获取文件名称: ", file.name)
}
})
})
}
readDirectory("./why")
- 文件(夹)重命名
const fs = require("fs")
// 1. 对文件夹重命名
// fs.rename("./kobe", "./why", err => {
// console.log("重命名结果:", err)
// })
// 2. 对文件重命名
fs.rename("./ccc.txt", "./ddd.txt", err => {
console.log("重命名结果:", err)
})
2.2 Events 模块
Node 中的核心 API 都是基于异步事件驱动的
- 在这个体系中,某些对象(发射器(Emitters))发出某一个事件;
- 我们可以监听这个事件(监听器(Listeners)),并且传入的回调函数,这个回调函数会在监听到事件时调用;
发出事件和监听事件都是通过 EventEmitter 类来完成的,它们都属于 events 对象。
- emitter.on(eventName, listener): 监听事件,也可以使用 addlistener;
- emitter.off(eventName, listener): 移除事件监听,也可以使用 removeListener;
- emitter.emit(eventName[,...args]): 发出事件,可以携带一些参数
2.2.1 events 模块的基本使用
// events 模块中的事件总线
const EventEmitter = require("events")
// 创建 EventEmitter 的实例
const emitter = new EventEmitter()
// 监听事件
emitter.on("why", () => {
console.log("监听 why 的事件")
})
// 发射事件
setTimeout(() => {
emitter.emit("why")
}, 2000)
2.2.2 events 的取消事件
// events 模块中的事件总线
const EventEmitter = require("events")
// 创建 EventEmitter 的实例
const emitter = new EventEmitter()
function handleWhyFn(name, age, height) {
console.log("监听 why 的事件", name, age, height)
}
// 监听事件
emitter.on("why", handleWhyFn)
// 发射事件
setTimeout(() => {
emitter.emit("why", "fog", 18, 1.88)
// 取消事件监听
emitter.off("why", handleWhyFn)
setTimeout(() => {
emitter.emit("why")
}, 1000)
}, 2000)
2.3 Buffer
计算机中所有的内容:文字、数字、图片、音频、视频最终都会使用二进制来表示。JavaScript 直接去处理非常直观的数据,而服务器处理的一般是字符流(二进制文件)。对于前端而言,很少会和二进制打交道,但是对于服务端,为了做更多的功能,我们必须直接去操作其二进制文件。 Node 为了方便开发者完成更多功能,提供了一个类 Buffer,它存储的是二进制数据:可以将 Buffer 看成是一个存储二进制的数组,这个数组的每一项,可以保存 8 位二进制。
Buffer 相当于一个字节的数组,数组中的每一项对应一个字节的大小。
2.3.1 Buffer 存储过程
2.3.2 Buffer 的创建过程
- 方式一
// 1. 创建 Buffer
// 此方法已弃置
const buf = new Buffer("hello")
console.log(buf)
// 2. 创建 Buffer
const buf2 = Buffer.from("world")
console.log(buf2)
// 3. 创建 Buffer(字符串中包含中文)
// 中文默认会使用三个字节来表示,如果比较复杂会使用四个字节
const buf3 = Buffer.from("你好呀")
console.log(buf3)
console.log(buf3.toString())
// 4. 手动指定的 Buffer 创建过程的编码
// 当编码方式与解码方式不一致,会出现乱码
// 编码操作
const buf4 = Buffer.from("哈哈哈", "utf16le")
console.log(buf4)
// 解码操作
console.log(buf4.toString("utf-8"))
- 方式二
const fs = require("fs")
// 1. 创建 Buffer:alloc
const buf = Buffer.alloc(8)
// 2. 手动访问 Buffer 的每个字节
console.log(buf[0])
console.log(buf[1])
// 3. 手动修改 Buffer 的每个字节
buf[0] = 100
buf[1] = 0x66
console.log(buf)
console.log(buf.toString())
buf[2] = "m".charCodeAt()
console.log(buf)
从文件读取 buffer
const fs = require("fs")
// 1. 从文件读取buffer
fs.readFile("./aaa.txt", { encoding: "utf-8" }, (err, data) => {
console.log(data)
})
fs.readFile("./aaa.txt", (err, data) => {
// toString 默认以 utf8 方式解码
console.log(data.toString())
})
fs.readFile("./aaa.txt", (err, data) => {
data[0] = 0x6d
console.log(data.toString())
fs.writeFile("./aaa.txt", data.toString(), err => {
if (err) {
console.log("文件写入失败")
} else {
console.log("文件写入成功")
}
})
})
// 2. 读取一个图片的二进制(node 中有一个库 sharp,可以对图片进行操作)
fs.readFile('./kobe.png', (err, data) => {
console.log(data)
})
2.4 Stream 流
连续字节的一种表现形式和抽象概念。 流应该是可读的,也是可写的。
已经有了 readFile 和 writeFile 方式读写文件,为什么还需要流?
- 直接读写文件的方式,虽然简单,但是无法控制一些细节的操作;
- 比如从什么位置开始读、读到什么位置、一次性读取多少个字节
- 读到某个位置后,暂停读取,某个时刻恢复继续读取等等
- 或者这个文件非常大,比如一个视频文件,一次性全部读取并不合适
2.4.1 文件读写的 Stream
事实上 Node 中很多模块的对象都是基于流实现的,比如,http 模块的 Request 和 Response 对象。此外,所有的流都是 EventEmitter 的实例。
Node 中有四种基本流类型
- Writable:可写数据流(fs.createWriteStream())
- Readable:可读数据流(fs.createReadStream())
- Duplex:可读可写数据流(net.Socket)
- Transform:在写入和读取数据时修改或转换数据的流(zlib.createDeflate())
2.4.2 Stream 的使用
- 可读流的基本使用
const fs = require("fs")
// 1. 一次性读取
fs.readFile("./aaa.txt", (err, data) => {
console.log(data)
})
// 2. 使用 Stream 流
// start: 开始位置
// end: 结束位置(包含 end 结束位置)
const readStream = fs.createReadStream("./aaa.txt", {
start: 8,
end: 22,
highWaterMark: 3
})
readStream.on("data", data => {
console.log(data.toString())
readStream.pause()
setTimeout(() => {
readStream.resume()
}, 2000)
})
- 可读流的其他操作
const fs = require("fs")
// 1. 一次性读取
fs.readFile("./aaa.txt", (err, data) => {
console.log(data)
})
// 2. 使用 Stream 流
// start: 开始位置
// end: 结束位置(包含 end 结束位置)
// highWaterMark:一次性读取字节的长度,默认是 64kb
const readStream = fs.createReadStream("./aaa.txt", {
start: 8,
end: 22,
highWaterMark: 3
})
readStream.on("data", data => {
console.log(data.toString())
})
// 3. 补充其他的事件监听
readStream.on("open", fd => {
console.log("通过流将文件打开", fd)
})
readStream.on("end", () => {
console.log("已经读取到 end 位置")
})
readStream.on("close", () => {
console.log("文件读取结束,并关闭文件")
})
- 可写流的使用过程
const fs = require("fs")
// 1. 一次性写入内容
// fs.writeFile(
// "./bbb.txt",
// "hello world",
// {
// encoding: "utf-8",
// flag: "a+"
// },
// err => {
// console.log("写入文件结果:", err)
// }
// )
// 2. 创建一个写入流
const writeStream = fs.createWriteStream("./ccc.txt", {
flags: "a",
start: 5
})
// 可以多次写入
writeStream.write("fogjoe")
writeStream.write("aaaa")
writeStream.write("bbbb", err => {
console.log("写入完成:", err)
})
// 我们会发现,并不能监听到 close 事件
// 这是因为写入流在打开后是不会自动关闭的
// 必须手动关闭,来告诉 Node 已经写入结束了
// 并且会发出一个 finish 事件的
// writeStream.on("close", () => {
// console.log("文件被关闭~")
// })
// 使用 finish 方法来监听 end 结束事件
writeStream.on("finish", () => {
console.log("写入完成了")
})
// 3. 写入完成时,需要手动去掉 用 close 方法
// writeStream.close()
// 4. end 方法:
// 1. 将最后的内容写入到文件中
// 2. 关闭文件
writeStream.end("hhhh")
- 文件的拷贝操作-pipe 通道
const fs = require("fs")
// 1. 方式一:一次性读取和写入文件
// fs.readFile("./foo.txt", (err, data) => {
// console.log(data)
// fs.writeFile("./foo_copy01.txt", data, err => {
// console.log("写入文件完成", err)
// })
// })
// 2. 方式二:创建可读流和可写流
// const readStream = fs.createReadStream("./foo.txt")
// const writeStream = fs.createWriteStream("./foo_copy02.txt")
// readStream.on("data", data => {
// writeStream.write(data)
// })
// readStream.on("end", () => {
// writeStream.close()
// })
// 3. 在可读流和可写流之间建立一个管道
const readStream = fs.createReadStream("./foo.txt")
const writeStream = fs.createWriteStream("./foo_copy03.txt")
readStream.pipe(writeStream)
2.5 http 模块
浏览器加载资源,大多是通过向 Web 服务器发送网络请求,拿到资源后再在页面上进行加载,一般情况下,可以使用很多方式发送请求,比如:Nginx、nodejs 等等。在 nodejs 中我们一般使用 http 模块来发送网络请求。
2.5.1 http 模块的基本使用
const http = require("http")
// 创建一个 http 对应的服务器
const server = http.createServer((request, response) => {
// request 对象中包含本次客户端请求的所有信息
// 请求的 url
// 请求的 method
// 请求的 headers
// 请求携带的数据
// response 对象用于给客户端返回结果的
response.end("Hello World")
})
// 开启对应的服务器,并且告知需要监听的端口
// 监听端口时,监听 1024 以上的端口,65535 以下的端口
// 1025~65535 之间的端口
// 2 个字节 => 256*256 => 65536 => 0~65535
server.listen(8000, () => {
console.log("服务器已经开启")
})
2.5.2 创建多个服务器
const http = require("http")
// 创建第一个服务器
const server1 = http.createServer((req, res) => {
res.end("2000服务器返回的结果~")
})
server1.listen(2000, () => {
console.log("2000服务器启动成功~")
})
// 创建第二个服务器
// 还可以使用 new http.Server 方式来创建
const server2 = http.createServer((req, res) => {
res.end("3000服务器创建成功")
})
server2.listen(3000, () => {
console.log("3000服务器启动成功~")
})
2.5.3 其他额外知识点补充
const http = require("http")
// 为避免每次更新代码后都需要重新启动
// npm i nodemon -g
const serve = http.createServer((req, res) => {
// 在浏览器中访问会执行两次
// 一次是基本的网络请求
// 另一次是访问图标时又会请求一次
// 解决方法是使用专业工具-postman
console.log("执行成功~")
res.end("Hello World")
})
serve.listen(8000, () => {
console.log("服务器开启成功~")
})
2.5.4 http 服务器--request 对象
在向服务器发送网络请求时,我们会携带很多信息,比如:
- 本次请求的 URL
- 本次请求的请求方式
- 请求的 headers 中也会携带一些信息,如客户端信息、接收数据的格式、支持的编码格式等
const http = require("http")
// 1. 创建 http server
const server = http.createServer((req, res) => {
// request 对象中包含哪些信息?
// 1. url信息
console.log(req.url)
// 2. method 信息(请求方式)
console.log(req.method)
// headers 信息(请求头信息)
console.log(req.headers)
res.end("Hello World")
})
// 2. 开启 server 服务器
server.listen(8000, () => {
console.log("服务器开启成功~")
})
2.5.5 http-区分不同 method
const http = require("http")
// 1. 创建 http server
const server = http.createServer((req, res) => {
const url = req.url
const method = req.method
if (url === "/login") {
if (method === "POST") {
res.end("login success~")
} else {
res.end("please check your request method")
}
} else if (url === "/products") {
res.end("product list~")
} else if (url === "/lyric") {
res.end("lyric list")
}
})
// 2. 开启 server 服务器
server.listen(8000, () => {
console.log("服务器开启成功~")
})
2.5.6 request参数解析-query参数
const http = require("http")
const url = require("url")
const qs = require("querystring")
// 1. 创建 http server
const server = http.createServer((req, res) => {
// 1. 参数一:query 类型参数
// home/list?offset=100&size=20
// 1.1 解析 url
const urlString = req.url
const urlInfo = url.parse(urlString)
console.log(urlInfo.query, urlInfo.pathname)
// 1.2 解析 query:offset=100&size=20
const queryString = urlInfo.query
// const queryInfo = qs.parse(queryString)
// console.log(queryInfo.offset, queryInfo.size)
const queryInfo = new URLSearchParams(queryString)
console.log(queryInfo.get("offset"), queryInfo.get("size"))
})
// 2. 开启 server 服务器
server.listen(8000, () => {
console.log("服务器开启成功~")
})
2.5.7 request参数解析-body参数
const http = require("http")
const url = require("url")
const qs = require("querystring")
// 1. 创建 http server
const server = http.createServer((req, res) => {
// 获取参数:body 参数
req.setEncoding("utf-8")
// request 对象本质上是一个 readable 可读流
let isLogin = false
req.on("data", data => {
const dataString = data
const loginInfo = JSON.parse(dataString)
if (loginInfo.name === "fogjoe" && loginInfo.password === "123456") {
isLogin = true
} else {
isLogin = false
}
})
req.on("end", () => {
if (isLogin) {
res.end("login success, welcome back~")
} else {
res.end("name or password is error, please checkout your login info~")
}
})
})
// 2. 开启 server 服务器
server.listen(8000, () => {
console.log("服务器开启成功~")
})
2.5.8 request参数解析-headers解析
const http = require("http")
const url = require("url")
const qs = require("querystring")
// 1. 创建 http server
const server = http.createServer((req, res) => {
console.log(req.headers)
console.log(req.headers["content-type"])
const token = req.headers["authorization"]
console.log(token)
res.end("checkout header info~")
})
// 2. 开启 server 服务器
server.listen(8000, () => {
console.log("服务器开启成功~")
})
2.5.9 response 返回响应结果
如果我们希望给客户端响应的结果数据,可以通过两种方式:
- Write 方式:这种方式是直接写出数据,但是并没有关闭流;
- end 方法:这种方式是写出最后的数据,并且写出后会关闭流;
如果我们没有调用 end 和 close,客户端将会一直等待结果:
- 所以客户端在发送网络请求时,都会设置超时时间。
const http = require("http")
const url = require("url")
const qs = require("querystring")
// 1. 创建 http server
const server = http.createServer((req, res) => {
// res: response对象 => Writable 可写流
// 1. 响应数据方式一:write
res.write("Hello World")
res.write("嘻嘻嘻嘻")
// 2. 响应数据方式二:end
// res.end("本次写出已经结束")
})
// 2. 开启 server 服务器
server.listen(8000, () => {
console.log("服务器开启成功~")
})
2.5.10 响应状态码
const http = require("http")
const url = require("url")
const qs = require("querystring")
// 1. 创建 http server
const server = http.createServer((req, res) => {
// 响应状态码
// 1. 方式一:statusCode
// res.statusCode = 401
// 2. 方式二:setHead 响应头
res.writeHead(401)
res.end("Hello World")
})
// 2. 开启 server 服务器
server.listen(8000, () => {
console.log("服务器开启成功~")
})
2.5.11 设置 header 信息
const http = require("http")
// 1. 创建 http server
const server = http.createServer((req, res) => {
// 设置 header 信息:数据的类型以及数据的编码格式
// 1. 单独设置某一个 header
// 一般使用 application/json 格式
// res.setHeader("Content-Type", "text/plain;charset=utf8;")
// res.end("服务器响应数据~")
// 2. 和 http status code 一起设置
res.writeHead(200, {
"Content-Type": "application/json;charset=utf8;"
})
const list = [
{ name: "fog", age: 18 },
{ name: "joe", age: 30 }
]
res.end(JSON.stringify(list))
})
// 2. 开启 server 服务器
server.listen(8000, () => {
console.log("服务器开启成功~")
})
2.5.12 在 node 中使用 http 模块或其他模块
- 使用 http 模块
- 使用 http 的 post 方法时,必须调用 end 表示获取结果结束
const http = require("http")
// 1. 使用 http 模块发送 get 请求
// http.get("http://localhost:8000", res => {
// // 从可读流中获取数据
// res.on("data", data => {
// const dataString = data.toString()
// const dataInfo = JSON.parse(dataString)
// console.log(dataInfo)
// })
// })
// 2. 使用 http 模块发送 post 请求
const req = http.request(
{
method: "POST",
hostname: "localhost",
port: 8000
},
res => {
res.on("data", data => {
const dataString = data.toString()
const dataInfo = JSON.parse(dataString)
console.log(dataInfo)
})
}
)
// 必须调用 end,表示写入内容完成
req.end()
- 使用 axios 模块
const axios = require("axios")
axios.get("http://localhost:8000").then(res => {
console.log(res.data)
})
2.5.13 http 文件上传
- 错误做法
不应该使用 writable 写入流,因为以这种方式写入文件后文件类型是错误的,无法读取。
const http = require('http')
const fs = require('fs')
// 1. 创建 http server
const server = http.createServer((req, res) => {
// 创建writable 的 stream
const writeStream = fs.createWriteStream('./foo.png', {
flags: 'a+'
})
// req.pipe(writeStream)
// 客户端传递的数据是表单数据(请求体)
req.on('data', data => {
console.log(data)
writeStream.write(data)
})
req.on('end', () => {
console.log('数据传输完成~')
writeStream.close()
res.end('文件上传成功~')
})
})
// 2. 开启 server 服务器
server.listen(8000, () => {
console.log('服务器开启成功~')
})
- 正确做法
这种做法其实是最底层的,实际开发过程中是不会采用这种方式的,基本是采用框架插件的方式。
const http = require('http')
const fs = require('fs')
// 1. 创建 http server
const server = http.createServer((req, res) => {
req.setEncoding('binary')
const boundary = req.headers['content-type'].split('; ')[1].replace('boundary=', '')
// 客户端传递的数据是表单数据(请求体)
let formData = ''
req.on('data', data => {
formData += data
})
req.on('end', () => {
// 1. 截图默认从 image/png 位置开始后面所有的数据
const imageType = 'image/png'
const imageTypePosition = formData.indexOf(imageType) + imageType.length
let imageData = formData.substring(imageTypePosition)
// 2. imageData 开始位置会有两个空格
imageData = imageData.replace(/^\s\s*/, '')
// 3. 替换最后的 boundary
imageData = imageData.substring(0, imageData.indexOf(`--${boundary}--`))
console.log(imageData)
// 4. 将 imageData 的数据存储到文件中
fs.writeFile('./bar.png', imageData, 'binary', () => {
console.log('文件存储成功~')
res.end('文件上传成功~')
})
})
})
// 2. 开启 server 服务器
server.listen(8000, () => {
console.log('服务器开启成功~')
})
- 浏览器上传文件代码
<body>
<input type="file" />
<button>上传</button>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
// 文件上传的逻辑
const btnEl = document.querySelector('button')
btnEl.onclick = function () {
// 1. 创建表单对象
const formData = new FormData()
// 2. 将选中的图标文件放入表单
const inputEl = document.querySelector('input')
formData.set('photo', inputEl.files[0])
// 3. 发送 post 请求,将表单数据携带到服务器(axios)
axios({
method: 'post',
url: 'http://localhost:8000',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
</script>
</body>
2.6 Node-Express 模块
2.6.1 express 的基本使用
express 的使用过程有两种方式:
-
- 通过 express 提供的脚手架,直接创建一个应用的骨架;
-
- 从零搭建自己的 express 应用架构;
方式一:安装 express-generator
安装脚手架
npm install -g express-generator
创建项目
express express-demo
安装依赖
npm install
启动项目
node bin/www
方式二:从零搭建
npm init
npm install express
const express = require('express')
// 创建服务器
const app = express()
// /home 的 get 请求处理
app.get('/home', (req, res) => {
res.end('Hello Home')
})
// /login 的 post 请求处理
app.post('/login', (req, res) => {
res.end('Hello Login')
})
// 开启监听
app.listen(9000, () => {
console.log('服务器启动成功~')
})
2.6.2 认识中间件
Express 是一个路由和中间件的 Web 框架,它本身的功能非常少:
- Express 应用程序本质上是一系列中间件函数的调用;
- 中间件的本质是传递给 express 的一个回调函数;
什么是中间件:
- 这个回调函数接受三个参数:
- 请求对象(request)
- 响应对象(response)
- next 函数(在 express 中定义的用于执行下一个中间件的函数)
- 中间件中可以执行的任务:
- 执行任何代码
- 更改请求(request)和(response)对象
- 结束请求-响应周期(返回周期)
- 调用栈中的下一个中间件
如果当前中间件功能中没有结束请求-响应周期,则必须调用 next() 将控制权传递给下一个中间件功能,否则,请求将被挂起。
const express = require('express')
// 1. 创建服务器
const app = express()
// 传入的这个回调函数就称之为是中间件(middleware)
// app.post('/login', 回调函数 => 中间件)
app.post('/login', (req, res, next) => {
// 1. 中间件中可以执行任意代码
// 打印
// 查询数据
// 判断逻辑
console.log('first middleware exec~')
// 2. 在中间件中响应 req/res 对象
req.age = 99
// 3. 可以在中间件中结束响应周期
// res.json({ message: '登录成功,欢迎回来', code: 0 })
// 4. 执行下一个中间件
next()
})
app.use((req, res, next) => {
console.log('second middleware exec~')
})
// 2. 监听服务器
app.listen(9000, () => {
console.log('服务器启动成功~')
})
2.6.3 应用中间件
- 自己编写
- express 主要提供了两种方式:
- app/router.use;
- app/router.methods;
- 可以是 app,也可以是 router
- methods 指的是常见的请求方式,比如:app.get、app.post等。
方式一:最普通的中间件
当发送网络请求时,永远只会执行第一个匹配到的中间件,后续的中间件是否会执行,取决于第一个中间件是否有执行next()
const express = require('express')
const app = express()
// 总结:当 express 接受到客户端发送的网络请求时,在所有中间件中开始进行匹配
// 当匹配到第一个符合要求的中间件时,那么就会执行这个中间件
// 后续的中间件是否会执行呢?取决于上一个中间件有没有执行 next
// 通过 use 方法注册的中间件是最普通的/简单的中间件
// 通过 use 注册的中间件,无论是什么请求方式都可以匹配上
// login/get
// login/post
//abc/patch
app.use((req, res, next) => {
console.log('normal middleware 01')
// res.end('already return result, response is over')
next()
})
app.use((req, res, next) => {
console.log('normal middleware 02')
})
app.listen(9000, () => {
console.log('express 服务器启动成功~')
})
方式二:path 匹配中间件
const express = require('express')
const app = express()
// 注册普通的中间件
// app.use((req, res, next) => {
// console.log('match normal middleware')
// res.end('-------------')
// })
// 注册路径匹配的中间件
// 路径匹配的中间件是不会对请求方式(method)进行限制
app.use('/home', (req, res, next) => {
console.log('match /home middleware')
res.end('home data')
})
app.listen(9000, () => {
console.log('express 服务器启动成功~')
})
方式三:path 和 method 匹配中间件
const express = require('express')
const app = express()
// 注册中间件:对 path/method 都有限制
// app.method(path, middleware)
app.get('/home', (req, res, next) => {
console.log('match /home get method middleware')
res.end('home data')
})
app.post('/users', (req, res, next) => {
console.log('match /home get method middleware')
res.end('create user success')
})
app.listen(9000, () => {
console.log('express 服务器启动成功~')
})
方式四:注册多个中间件
const express = require('express')
const app = express()
// app.get(路径, 中间件1, 中间件2,中间件3)
app.get(
'/home',
(req, res, next) => {
console.log('match /home get method middleware01')
// 后面的中间件是否执行取决于前一个是否执行 next 放行函数
next()
},
(req, res, next) => {
console.log('match /home get method middleware02')
next()
},
(req, res, next) => {
console.log('match /home get method middleware03')
next()
},
(req, res, next) => {
console.log('match /home get method middleware04')
next()
}
)
app.listen(9000, () => {
console.log('express 服务器启动成功~')
})
- 使用一些插件
- app.use(express.json()) // 对客户端传过来的 JSON 格式参数进行进行解析
- app.use(express.urlencoded({ extension: true })) // 解析 urlencoded 格式参数
- app.use(morgan()) // 请求日志记录 第三方插件
- multer() // 文件上传以及解析formdata 第三方插件
json 插件
const express = require('express')
const app = express()
// app.use((req, res, next) => {
// if (req.headers['content-type'] === 'application/json') {
// req.on('data', data => {
// const jsonInfo = JSON.parse(data.toString())
// req.body = jsonInfo
// })
// req.on('end', () => {
// next()
// })
// } else {
// next()
// }
// })
// 直接使用 express 提供给我们的中间件
app.use(express.json())
// 案例一:用户登录的请求处理 /login post => username/password
app.post('/login', (req, res, next) => {
console.log(req.body)
})
// 案例二:注册用户的请求处理 /register post => username/password
app.post('/register', (req, res, next) => {
console.log(req.body)
})
app.listen(9000, () => {
console.log('express 服务器启动成功~')
})
urlencoded 插件
const express = require('express')
// 创建 app 对象
const app = express()
// 应用一些中间件
app.use(express.json()) // 解析客户端传递过来的 JSON
// 解析传递过来 urlencoded 的时候,默认使用的 node 内置 querystring 模块
// { extended: true }: 不再使用内置的 querystring,而是使用 qs 第三方库
app.use(express.urlencoded({ extended: true })) // 解析客户端传递过来的 urlencoded
// 编写中间件
app.post('/login', (req, res, next) => {
console.log(req.body)
})
app.listen(9000, () => {
console.log('express 服务器启动成功~')
})
morgan 插件
const fs = require('fs')
const express = require('express')
const morgan = require('morgan')
// 创建 app 对象
const app = express()
// 创建第三方中间件
const writeStream = fs.createWriteStream('./logs/access.log')
app.use(morgan('combined', { stream: writeStream }))
// 编写中间件
app.post('/login', (req, res, next) => {
res.end('login success~')
})
app.listen(9000, () => {
console.log('express 服务器启动成功~')
})
multer 插件
- 单文件上传:multer.single()
const multer = require('multer')
const express = require('express')
// 创建 app 对象
const app = express()
// 应用一个 express 编写第三方的中间件
const upload = multer({
dest: './uploads'
})
// 编写中间件
app.post('/avatar', upload.single('avatar'), (req, res, next) => {
console.log(req.file)
res.end('文件上传成功~')
})
app.listen(9000, () => {
console.log('express 服务器启动成功~')
})
- 多文件上传:multer.array()
const multer = require('multer')
const express = require('express')
// 创建 app 对象
const app = express()
// 应用一个 express 编写第三方的中间件
const upload = multer({
// dest: './uploads'
storage: multer.diskStorage({
destination(req, file, callback) {
callback(null, './uploads/')
},
filename(req, file, callback) {
callback(null, Date.now() + '_' + file.originalname)
}
})
})
// 编写中间件
// 上传单个文件:single
app.post('/avatar', upload.single('avatar'), (req, res, next) => {
console.log(req.file)
res.end('文件上传成功~')
})
// 上传多文件:array
app.post('/photos', upload.array('photos'), (req, res, next) => {
console.log(req.files)
res.end('上传多张照片成功~')
})
app.listen(9000, () => {
console.log('express 服务器启动成功~')
})
- formdata 解析
const express = require('express')
const multer = require('multer')
// 创建 app 对象
const app = express()
// express 内置的插件
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
// 编写中间件
const formdata = multer()
app.post('/login', formdata.any(), (req, res, next) => {
console.log(req.body)
res.end('login success~')
})
app.listen(9000, () => {
console.log('express 服务器启动成功~')
})
2.6.4 客户端传递参数
- post => json
- post => x-www-form-urlencoded
- post => form-data 文件上传
- 其他方式
- get => querystring
- /home/list?offset=20&size=20
- get => params
- /users/:id
- get => querystring
const express = require('express')
// 创建 app 对象
const app = express()
// 编写中间件
// 1. 解析 querystring
app.get('/home/list', (req, res, next) => {
// offset/size
const queryInfo = req.query
console.log(queryInfo)
res.end('data list 数据')
})
// 2. 解析 params 参数
app.get('/users/:id', (req, res, next) => {
const id = req.params.id
res.end(`获取到${id}的数据~`)
})
app.listen(9000, () => {
console.log('express 服务器启动成功~')
})
2.6.5 服务器返回客户端数据方式
-
end 方式
- 类似于 http 中的 response.end 方法,用法相同
-
json 方法
- json 方法中可以传入很多的类型:Object、array、string、boolean、number、null 等,它们会被转化为 json 格式返回
-
status 方法
- 用于设置状态码
- 这是一个函数,而不是属性赋值
具体详看更多响应方式
const express = require('express')
// 创建 app 对象
const app = express()
// 编写中间件
app.post('/login', (req, res, next) => {
// 1. res.end方法(比较少)
// res.end('login success~')
// 2. res.json 方法
// res.json({
// code: 0,
// message: 'come back~',
// list: [
// { name: '查拉图斯特拉如是说', price: 30 },
// { name: '兄弟', price: 35 }
// ]
// })
// 3. res.status 方法
res.status(201)
res.json('create user success~')
})
app.listen(9000, () => {
console.log('express 服务器启动成功~')
})
2.6.6 路由的使用方式
如果我们把所有的代码逻辑都写在 app 中,那么 app 会变得越来越复杂,因此我们可以使用 express.Router 来创建一个路由处理程序:
- 一个 Router 实例拥有完整的中间件和路由系统;
- 因此,它也被称为 迷你应用程序(mini-app)
const express = require('express')
// 将步骤2中的代码抽到这个js文件中
const useRouter = require('./router/useRouter')
// 创建 app 对象
const app = express()
// 编写中间件
app.post('/login', (req, res, next) => {})
app.get('/home', (req, res, next) => {})
/** 用户接口 */
// 1. 将用户的几口直接定义在 app 中
// app.get('/users', (req, res, next) => {})
// app.get('/users/:id', (req, res, next) => {})
// app.post('/users', (req, res, next) => {})
// app.delete('/users/:id', (req, res, next) => {})
// app.patch('/users/:id', (req, res, next) => {})
// 2. 将用户的接口定义在单独的路由对象中
// const useRouter = express.Router()
// useRouter.get('/', (req, res, next) => {
// res.json('用户列表数据')
// })
// useRouter.get('/:id', (req, res, next) => {
// const id = req.params.id
// res.json('某一个用户的数据: ' + id)
// })
// useRouter.post('/', (req, res, next) => {
// res.json('创建用户成功')
// })
// useRouter.delete('/:id', (req, res, next) => {
// const id = req.params.id
// res.json('删除某一个用户的数据: ' + id)
// })
// useRouter.patch('/:id', (req, res, next) => {
// const id = req.params.id
// res.json('修改某一个用户的数据: ' + id)
// })
// 让路由生效
app.use('/users', useRouter)
app.listen(9000, () => {
console.log('express 服务器启动成功~')
})
2.6.7 express 部署静态资源服务器
- express.static('./build')
const express = require("express")
// 创建 app 对象
const app = express()
// 内置的中间件:直接将一个文件夹作为静态资源
app.use(express.static("./uploads"))
// 在浏览器中直接输入对应IP地址和端口号,会自动从本项目中解析 build/index.html
app.use(express.static("./build"))
app.post("/login", (req, res, next) => {
res.end("login success~")
})
app.listen(9000, () => {
console.log("express 服务器启动成功~")
})
2.6.8 错误处理方案
-
方案一
res.status(401)
res.json('未授权访问的信息')
-
方案二
-
http 状态码 200,信息中会包含错误 code/message
-
res.json({ code: 401, message: '未授权访问的信息,请检测 token' })
-
const express = require("express")
// 创建 app 对象
const app = express()
// 编写中间件
app.use(express.json())
app.post("/login", (req, res, next) => {
// 1. 获取登录传入的用户名和密码
const { username, password } = req.body
// 2. 对用户名和密码进行判断
if (!username || !password) {
next(-1001)
} else if (username !== "fogjoe" || password !== "123456") {
next(-1002)
} else {
res.json({
code: 0,
message: "login success~",
token: "Bear123415"
})
}
})
// 错误处理的中间件
app.use((errCode, req, res, next) => {
const code = errCode
let message = "未知的错误信息"
switch (code) {
case -1001:
message = "没有输入用户名和密码"
break
case -1002:
message = "输入用户名和密码错误"
break
}
res.json({ code, message })
})
app.listen(9000, () => {
console.log("express 服务器启动成功~")
})
2.7 Node-koa
2.7.1 koa 的基本使用
const Koa = require("koa")
// 创建 app 对象
const app = new Koa()
// 注册中间件(middleware)
// koa 的中间件有两个参数:ctx/next
app.use((ctx, next) => {
console.log("匹配到 koa 的中间件")
ctx.body = "Hello Koa"
})
app.listen(6000, () => {
console.log("koa 服务器启动成功~")
})
2.7.2 Koa 注册的中间件提供了两个参数:
-
- ctx:上下文(Context)对象
- ctx 代表一次请求的上下文对象
- ctx.request:获取请求对象
- ctx.response:获取响应对象
-
- next:本质上是一个 dispatch,类似于之前的 next
const Koa = require("koa")
const app = new Koa()
// 中间件
app.use((ctx, next) => {
// 1. 请求对象
console.log(ctx.request) // 请求对象:Koa 封装的请求对象
console.log(ctx.req) // 请求对象:Node 封装的请求对象
// 2. 响应对象
console.log(ctx.response) // 响应对象:Koa 封装的请求对象
console.log(ctx.res) // 响应对象:Node 封装的请求对象
// 3. 其他属性
console.log(ctx.query)
console.log(ctx.path)
next()
})
app.use((ctx, next) => {
console.log("second middleware~")
})
app.listen(6000, () => {
console.log("服务器访问成功~")
})
2.7.3 koa 区分路径和方法
- Koa 通过创建的 app 对象,注册中间件只能通过 use 方法:
- Koa 并没有提供 methods 的方式来注册中间件
- 也没有提供 path 中间件来匹配路径
- 真实开发中如何将路径和 method 分离呢:
- 方式一:根据 request 自己来判断
- 方式二:使用第三方路由中间件
const Koa = require("koa")
// 创建 app 对象
const app = new Koa()
// 中间件:path/method 实用路由
app.use((ctx, next) => {
if (ctx.path === "/users") {
if (ctx.method === "GET") {
ctx.body = "user data list"
} else if (ctx.method === "POST") {
ctx.body = "create user success~"
}
} else if (ctx.path === "/home") {
ctx.body = "home data list~"
} else if (ctx.path === "/login") {
ctx.body = "login success~"
}
})
app.listen(6000, () => {
console.log("koa 服务器启动成功~")
})
2.7.4 koa 中路由的使用方式
pnpm i @koa/router
const KoaRouter = require('@koa/router')
const userRouter = new KoaRouter({ prefix: '/users/ }) app.use(userRouter.routes())
const Koa = require("koa")
const KoaRouter = require("@koa/router")
// 创建 app 对象
const app = new Koa()
// 路由的使用
// 1. 创建路由对象
const userRouter = new KoaRouter({ prefix: "/users" })
// 2. 在路由中注册中间件:path/method
userRouter.get("/", (ctx, next) => {
ctx.body = "users list data~"
})
userRouter.get("/:id", (ctx, next) => {
const id = ctx.params.id
ctx.body = "获取某一个用户" + id
})
userRouter.post("/", (ctx, next) => {
ctx.body = "创建用户成功~"
})
userRouter.delete("/:id", (ctx, next) => {
const id = ctx.params.id
ctx.body = "删除某一个用户" + id
})
userRouter.patch("/:id", (ctx, next) => {
const id = ctx.params.id
ctx.body = "修改某一个用户" + id
})
// 3. 让路由的中间件生效
app.use(userRouter.routes())
app.use(userRouter.allowedMethods())
app.listen(6000, () => {
console.log("koa 服务器启动成功~")
})
2.7.5 koa 参数解析
const Koa = require("koa")
const KoaRouter = require("@koa/router")
const bodyParser = require("koa-bodyparser")
const multer = require("@koa/multer")
// 创建 app 对象
const app = new Koa()
// 使用第三方中间件解析body数据
app.use(bodyParser())
const formParser = multer()
// 注册路由对象
const userRouter = new KoaRouter({ prefix: "/users" })
/**
* 1. get: params 方式,例子:/:id
* 2. get:query 方式,例子:?name=why&age=18
* 3. post:json 方式,例子:{ "name": "why", "age": 18 }
* 4. post: x-www-form-urlencoded
* 5. post: form-data
*/
// 1. params
userRouter.get("/:id", (ctx, next) => {
const id = ctx.params.id
ctx.body = "user list data~" + id
})
// 2. query
userRouter.get("/", (ctx, next) => {
const query = ctx.query
console.log(query)
ctx.body = "用户的query信息" + JSON.stringify(query)
})
// 3. post/json(使用最多)
userRouter.post("/json", (ctx, next) => {
// 注意事项:不能从 ctx.body 中获取数据
// 需要安装第三方插件:koa-bodyparser
console.log(ctx.request.body, ctx.req.body)
// ctx.body 用于向客户端返回数据
ctx.body = "用户的json信息"
})
// 4. post/urlencoded
userRouter.post("/urlencoded", (ctx, next) => {
console.log(ctx.request.body)
ctx.body = "用户的urlencoded信息"
})
// 5. post/form-data
userRouter.post("/formdata", formParser.any(), (ctx, next) => {
console.log(ctx.request.body, ctx.req.body)
ctx.body = "用户的formdata信息"
})
app.use(userRouter.routes())
app.use(userRouter.allowedMethods())
// 启动服务器
app.listen(6000, () => {
console.log("koa 服务器启动成功~")
})
2.7.6 koa 文件上传
const Koa = require("koa")
const KoaRouter = require("@koa/router")
const multer = require("@koa/multer")
// 创建 app 对象
const app = new Koa()
// 注册路由对象
const uploadRouter = new KoaRouter({ prefix: "/upload" })
// const upload = multer({
// dest: "./uploads"
// })
const upload = multer({
storage: multer.diskStorage({
destination(req, file, cb) {
cb(null, "./uploads")
},
filename(req, file, cb) {
cb(null, Date.now() + "_" + file.originalname)
}
})
})
uploadRouter.post("/avatar", upload.single("avatar"), (ctx, next) => {
console.log(ctx.request.file)
ctx.body = "文件上传成功~"
})
uploadRouter.post("/photos", upload.array("photos"), (ctx, next) => {
console.log(ctx.request.files)
ctx.body = "文件上传成功~"
})
app.use(uploadRouter.routes())
app.use(uploadRouter.allowedMethods())
// 启动服务器
app.listen(6000, () => {
console.log("koa 服务器启动成功~")
})
2.7.7 koa 部署静态资源
需要安装第三方插件:koa-static
const Koa = require('koa')
const static = require('koa-static')
const app = new Koa()
app.use(static('./uploads'))
app.use(static('./build'))
app.listen(6000, () => {
console.log('koa服务器启动成功~')
})
2.7.8 koa 响应结果的方式
输出结果:body 将响应主体设置为一下方式之一:
- string:字符串数据
- Buffer:Buffer 数据
- Stream:流数据
- Object || Array:对象或者数组
- null:不输入任何内容,此时状态码会自动设置为 204
- 如果 response.status 尚未设置,Koa 会自动将状态设置为 200 或 204
const fs = require('fs')
const Koa = require('koa')
const KoaRouter = require('@koa/router')
// 创建app对象
const app = new Koa()
// 注册路由对象
const userRouter = new KoaRouter({ prefix: '/users' })
userRouter.get('/', (ctx, next) => {
// 1. body 的类型是 string
// ctx.body = 'user list data~'
// 2. body 的类型是 Buffer
// ctx.body = Buffer.from('你好啊~')
// 3. body 的类型是 Stream
// const readStream = fs.createReadStream('./uploads/01_name.jpeg')
// // 指定类型后返回结果就不会乱码
// ctx.type = 'image/jpeg'
// ctx.body = readStream
// 4. body 的类型是数据(array/object => 使用最多
// ctx.body = {
// code: 0,
// data: [
// { name: '111', age: 222 },
// { name: '222', age: 333 }
// ]
// }
// 5. body 的值是 null,自动设置 http status code 为 204
ctx.body = null
})
app.use(userRouter.routes())
app.use(userRouter.allowedMethods())
// 启动服务器
app.listen(6000, () => {
console.log('启动服务器成功~')
})
2.7.9 koa 错误处理方案
const Koa = require('koa')
const KoaRouter = require('@koa/router')
// 创建app对象
const app = new Koa()
// 注册路由对象
const userRouter = new KoaRouter({ prefix: '/users' })
userRouter.get('/', (ctx, next) => {
const isAuth = false
if (isAuth) {
ctx.body = 'user list data~'
} else {
// ctx.body = {
// code: -1003,
// message: '未授权的token,请检测你的token'
// }
// EventEmitter
ctx.app.emit('error', -1002, ctx)
}
})
// 独立的文件:error-handle.js
app.on('error', (code, ctx) => {
const errCode = code
let message = ''
switch (errCode) {
case -1001:
message = '账号或密码错误~'
break
case -1002:
message = '请求参数不正确~'
break
case -1003:
message = '未授权, 请检查你的token信息'
break
}
const body = {
code: errCode,
message
}
ctx.body = body
})
app.use(userRouter.routes())
app.use(userRouter.allowedMethods())
// 启动服务器
app.listen(6000, () => {
console.log('启动服务器成功~')
})
2.7.10 express 和 koa 区别
- 从架构设计上来说
- express 是完整和强大的,其中帮助我们内置了非常多好用的功能;
- koa是简洁和自由的,它只包含了最核心的功能,并不会对我们使用其他中间件进行任何的限制。
- 甚至是在app中连最基本的get、post都没有提供;
- 我们需要通过自己或者路由来判断请求方式或其他功能;
- 因为express和koa框架它们的核心其实都是中间件
- 但是它们的中间件执行机制其实是不同的,特别是针对某个中间件中包含异步操作时;
- express 只能同步执行代码,不能异步执行代码(即只能从上向下执行)
- koa 可以实现异步执行代码
- 但是它们的中间件执行机制其实是不同的,特别是针对某个中间件中包含异步操作时;
2.7.10.1 同步执行
- express
const express = require('express')
// 创建 app
const app = express()
// 编写中间件
app.use((req, res, next) => {
console.log('express middleware01')
req.msg = 'aaa'
next()
})
app.use((req, res, next) => {
console.log('express middleware02')
req.msg += 'bbb'
next()
res.json(req.msg)
})
app.use((req, res, next) => {
console.log('express middleware03')
req.msg += 'ccc'
})
// 启动服务器
app.listen(9000, () => {
console.log('express服务器启动成功~')
})
2. koa
const Koa = require('koa')
// 创建 app
const app = new Koa()
// 使用中间件
app.use((ctx, next) => {
console.log('koa middleware01')
ctx.msg = 'aaa'
next()
// 返回结果
ctx.body = ctx.msg
})
app.use((ctx, next) => {
console.log('koa middleware02')
ctx.msg += 'bbb'
next()
})
app.use((ctx, next) => {
console.log('koa middleware03')
ctx.msg += 'ccc'
})
// 启动服务器
app.listen(6000, () => {
console.log('koa服务器启动成功~')
})
2.7.10.2 执行异步
- express
const express = require('express')
const axios = require('axios')
// 创建 app
const app = express()
// 编写中间件
app.use(async (req, res, next) => {
console.log('express middleware01')
req.msg = 'aaa'
await next()
// 返回结果
// res.json(req.msg)
})
app.use(async (req, res, next) => {
console.log('express middleware02')
req.msg += 'bbb'
await next()
})
app.use(async (req, res, next) => {
console.log('express middleware03')
// 网络请求
const resData = await axios.get('http://123.207.32.32:8000/home/multidata')
req.msg += resData.data.data.banner.list[0].title
// 只能在这里返回结果
res.json(req.msg)
})
// 启动服务器
app.listen(9000, () => {
console.log('express服务器启动成功~')
})
- koa
const Koa = require('koa')
const axios = require('axios')
// 创建 app
const app = new Koa()
// 使用中间件
app.use(async (ctx, next) => {
console.log('koa middleware01')
ctx.msg = 'aaa'
await next()
// 返回结果
ctx.body = ctx.msg
})
app.use(async (ctx, next) => {
console.log('koa middleware02')
ctx.msg += 'bbb'
// 如果执行的下一个中间件是一个异步函数,那么next默认不会等到中间件的结果,就会执行下一步操作
// 如果我们希望等待下一个异步函数的执行结果,那么需要在next函数前面加上await
await next()
})
app.use(async (ctx, next) => {
console.log('koa middleware03')
// 网络请求
const res = await axios.get('http://123.207.32.32:8000/home/multidata')
ctx.msg += res.data.data.banner.list[0].title
})
// 启动服务器
app.listen(6000, () => {
console.log('koa服务器启动成功~')
})
2.7.11 Koa 洋葱模型
- 中间件处理代码的过程
- Response 返回 body 执行
- koa 和 express的同步执行代码都会有洋葱模型
- 异步执行代码过程中,只有 koa 会有洋葱模型,而 express 没有。