Nodejs & Webpack

163 阅读29分钟

一、Nodejs

1. nodejs 基础知识

1.1 nodejs 定义

Nodejs 是基于 V8 JavaScript 引擎的 JavaScript 运行时环境。

1.2 nodejs 架构图

  1. 我们编写的 JavaScript 代码会经过 V8 引擎,再通过 Nodejs 的 Bindings,将任务放到 Libuv 的事件循环中;
  2. libuv 是使用** C 语言**编写的库;
  3. libuv 提供了事件循环、文件系统读写、网络IO、线程池等等内容。

image.png

我们使用 JavaScript 编写的应用程序经过 v8 引擎的处理,通过 Nodejs Bindings(NODE API)的处理,将其交给 LIBUV,然后 LIBUV 将这些内容放到工作线程中(存放着一个个的事件处理程序),然后执行回调函数,再通过事件循环放到事件队列中依次调用这些函数,当函数出错时,会执行阻塞操作,再交由对应的程序处理,直到事件全部执行完成。

1.3 nodejs 的应用

  1. 目前前端开发的库都是以node包的形式进行管理;
  2. npm、yarn、pnpm 工具成为前端开发使用最多的工具;
  3. 越来越多的公司使用 Nodejs 作为 web 服务器开发、中间件、代理服务器;
  4. 大量项目需要借助 Nodejs 完成前后端渲染的同构应用;
  5. 编写脚本工具;
  6. 使用 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 可以导出呢?
      1. module 对象的 exports 属性是 exports 对象的一个引用;
      1. 也就是说 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) 时,查找规则如下:

  1. X 是一个 Node 核心模块,比如 path、http

    • 直接返回核心模块,并且停止查找
  2. X 是以 ./ 或 ../ 或 /(根目录)开头的

    • 将 X 当做一个文件在对应的目录下查找:

      • 如果有后缀名,按照后缀名的格式查找对应的文件
      • 如果没有后缀名,会按照如下顺序:
        • 直接查找文件 X
        • 查找 X.js 文件
        • 查找 X.json 文件
        • 查找 X.node 文件
    • 没有找到对应的文件,将 X 作为一个目录

      • 查找目录下面的 index 文件
        • 查找 X/index.js 文件
        • 查找 X/index.json 文件
        • 查找 X/index.node 文件
    • 如果没找到,报错:not found

  3. 直接是一个 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 的解析流程

  1. 构建(Constructor): 根据地址查找 js 文件,并且下载,将其解析成模块记录(Module Record)

image.png

  1. **实例化(Instantiation):**对模块记录进行实例化,并且分配内存空间,解析模块的导入和导出语句,把模块指向对应的内存地址。
  2. **运行(Evaluation):**运行代码,计算值,并且将值填充到内存地址中。

image.png

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 存储过程

image.png

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 模块或其他模块

  1. 使用 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()

  1. 使用 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 的使用过程有两种方式:

    1. 通过 express 提供的脚手架,直接创建一个应用的骨架;
    1. 从零搭建自己的 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() 将控制权传递给下一个中间件功能,否则,请求将被挂起。

image.png

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 应用中间件

  1. 自己编写
  • 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 服务器启动成功~')
})

  1. 使用一些插件
  • 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 客户端传递参数

  1. post => json
  2. post => x-www-form-urlencoded
  3. post => form-data 文件上传
  4. 其他方式
    • get => querystring
      • /home/list?offset=20&size=20
    • get => params
      • /users/:id
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 注册的中间件提供了两个参数:

    1. ctx:上下文(Context)对象
    • ctx 代表一次请求的上下文对象
    • ctx.request:获取请求对象
    • ctx.response:获取响应对象
    1. 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 同步执行

  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 执行异步

  1. 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服务器启动成功~')
})

  1. 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 执行

image.png

  • koa 和 express的同步执行代码都会有洋葱模型
  • 异步执行代码过程中,只有 koa 会有洋葱模型,而 express 没有