1. KOA2——基于 Node.js平台的Web服务器框架
由 Express原班人马打造
Express Koa , Koa2都是 Web服务器的框架,他们之间的差别和关系可以通过下面这个表格表示出
框架名 |
作用 |
异步处理 |
Express |
web框架 |
回调函数 |
Koa |
web框架 |
Generator+yield |
Koa2 |
web框架 |
async/await |
环境依赖 Node v7.6.0 及以上
由于 Koa2 它是支持 async和 await ,所以它对 Node 的版本是有要求的,它要求 Node 的版本至 少是在7.6级以上,因为语法糖 async和await 是在 Node7.6 版本之后出现才支持
1.1 洋葱模型的中间件
如下图所示, 对于服务器而言,它其实就是来处理一个又一个的请求, Web服务器接收由浏览器发 过来的一个又一个请求之后,它形成一个又一个的响应返回给浏览器. 而请求到达我们的服务器是 需要经过程序处理的,程序处理完之后才会形成响应,返回给浏览器,我们服务器处理请求的这一块程序,在 Koa2 的世界当中就把它称之为中间件
这种中间件可能还不仅仅只有一个,可能会存在多个,比如上图所示, 它就存在三层中间件,这三 层中间件在处理请求的过程以及它调用的顺序为:
当一个请求到达咱们的服务器,最先最先处理这个请求的是第一层中间件
第一层的中间件在处理这个请求之后,它会把这个请求给第二层的中间件
第二层的中间件在处理这个请求之后,它会把这个请求给第三层的中间件
第三层中间件内部并没有中间件了, 所以第三层中间件在处理完所有的代码之后,这个请求又
会到了第二层的中间件,所以第二层中间件对这个请求经过了两次的处处理
第二层的中间件在处理完这个请求之后,又到了第一层的中间件, 所以第一层的中间件也对这
个请求经过了两次的处理
这个调用顺序就是洋葱模型, 中间件对请求的处理有一种先进后出的感觉,请求最先到达第一层中 间件,而最后也是第一层中间件对请求再次处理了一下
2. KOA2 的快速上手
检查 Node 的版本
node -v 的命令可以帮助我们检查 Node 的版本, Koa2 的使用要求 Node版本在7.6及以上
安装 Koa2
这个命令可以快速的创建出 package.json 的文件, 这个文件可以维护项目中第三方包的信息
npm install koa
这个命令可以在线的联网下载最新版本 koa到当前项目中, 由于线上最新版本的 koa就是 koa2 , 所以我们并不需要执行 npm install koa2
编写入口文件 app.js, 创建 Koa 的实例对象
// 1.创建koa对象 const Koa = require('koa') // 导入构造方法 const app = new Koa() // 通过构造方法 , 创建实例对象 |
编写响应函数(中间件)
响应函数是通过use的方式才能产生效果, 这个函数有两个参数, 一个是 ctx ,一个是 next ctx :
上下文, 指的是请求所处于的Web容器,我们可以通过 ctx.request拿到请求对象, 也可 以通过 ctx.response拿到响应对象
next :内层中间件执行的入口
// 2.编写响应函数(中间件) app.use((ctx, next) => { console.log(ctx.request.url) ctx.response.body = 'hello world' }) |
指明端口号:通过 app.listen就可以指明一个端口号
// 3.绑定端口号 3000 app.listen(3000) |
启动服务器:通过 node app.js 就可以启动服务器了
随即打开浏览器, 在浏览器中输入 127.0.0.1:3000/ 你将会看到浏览器中出现 hello world 的字符 串, 并且在服务器的终端中, 也能看到请求的 url
3. KOA2 中间件的特点
Koa2 的实例对象通过 use方法加入一个中间件,一个中间件就是一个函数,这个函数具备两个参数,分别是 ctx和 next。中间件的执行符合洋葱模型,内层中间件能否执行取决于外层中间件的 next 函数是否调用。调用 next 函数得到的是 Promise对象, 如果想得到 Promise所包装的数据, 可以结合 await和async
app.use(async (ctx, next) => { // 刚进入中间件想做的事情 await next() // 内层所有中间件结束之后想做的事情 }) |
4.后台项目的开发
4.1.后台项目的目标
1.计算服务器处理请求的总耗时
计算出服务器对于这个请求它的所有中间件总耗时时长
2.在响应头上加上响应内容的 mime 类型
加入mime类型, 可以让浏览器更好的来处理由服务器返回的数据.
如果响应给前端浏览器是 json 格式的数据,这时候就需要在咱们的响应头当中增加Type 它的值就是application/json 就是 json 数据类型的 mime 类型
3.根据URL读取指定目录下的文件内容
为了简化后台服务器的代码,前端图表所要的数据, 并没有存在数据库当中,而是将存在文件当中的,这种操作只是为了简化咱们后台的代码. 所以咱们是需要去读取某一个目录下面的文件内容 的。
每一个目标就是一个中间件需要实现的功能, 所以后台项目中需要有三个中间件
4.2.后台项目的开发步骤
创建一个新的文件夹, 叫做 koa_server , 这个文件夹就是后台项目的文件夹
1.安装包
npm init -y
npm install koa
2.创建文件和目录结构
代码目录结构
app.js是后台服务器的入口文件
data 目录是用来存放所有模块的 json文件数据
middleware是用来存放所有的中间件代码
koa_response_data.js是业务逻辑中间件
koa_response_duration.js是计算服务器处理时长的中间件
koa_response_header.js是用来专门设置响应头的中间件
接着将各个模块的 json数据文件复制到 data 的目录之下, 接着在 app.js文件中写上代码如下:
// 服务器的入口文件 // 1.创建KOA的实例对象 const Koa = require('koa') const app = new Koa() // 2.绑定中间件 // 绑定第一层中间件 // 绑定第二层中间件 // 绑定第三层中间件 // 3.绑定端口号 8888 app.listen(8888) |
4.3.总耗时中间件
1.第1层中间件
总耗时中间件的功能就是计算出服务器所有中间件的总耗时,应该位于第一层,因为第一层 的中间件是最先处理请求的中间件,同时也是最后处理请求的中间件。第一次进入咱们中间件的时候,就记录一个开始的时间。当其他所有中间件都执行完之后,再记录下结束时间以后将两者相减就得出总耗时。
3.设置响应头
将计算出来的结果,设置到响应头的 X-Response-Time 中, 单位是毫秒 ms
具体代码如下:
app.js
// 绑定第一层中间件 const respDurationMiddleware = require('./middleware/koa_response_duration') app.use(respDurationMiddleware) |
koa_response_duration.js
// 计算服务器消耗时长的中间件 module.exports = async (ctx, next) => { // 记录开始时间 const start = Date.now() // 让内层中间件得到执行 await next() // 记录结束的时间 const end = Date.now() // 设置响应头 X-Response-Time const duration = end - start // ctx.set 设置响应头 ctx.set('X-Response-Time', duration + 'ms') } |
4.4 响应头中间件
1.第2层中间件
这个第2层中间件没有特定的要求
2.获取 mime类型
由于响应给前端浏览器当中的数据都是 json 格式的字符串,所以 mime 类型可以统一
的给它写成 application/json , 当然这一块也是简化的处理了,因为 mime 类型有几十几百 种,,所以这里简化处理一下
3.设置响应头
响应头的key是 Content-Type ,它的值是 application/json , 顺便加上 charset=utf-8
告诉浏览器,我这部分响应的数据,它的类型是 application/json ,同时它的编码是具体代码如下:
app.js
// 绑定第二层中间件
const respHeaderMiddleware = require('./middleware/koa_response_header')
app.use(respHeaderMiddleware)
koa_response_header.js
// 设置响应头的中间件
module.exports = async (ctx, next) => {
const contentType = 'application/json; charset=utf-8'
ctx.set('Content-Type', contentType)
await next()
}
4.5 业务逻辑中间件
1.第3层中间件
第三层中间件处理实际的业务逻辑,处理前端请求并返回
2.读取文件内容
获取 URL 请求路径
const url = ctx.request.url |
根据URL请求路径,拼接出文件的绝对路径
let filePath = url.replace('/api', '') filePath = '../data' + filePath + '.json' filePath = path.join(__dirname, filePath) |
这个 filePath就是需要读取文件的绝对路径
读取这个文件的内容
使用 fs模块中的 readFile方法进行实现
3.设置响应体
ctx.response.body
具体代码如下:
app.js
// 绑定第三层中间件 const respDataMiddleware = require('./middleware/koa_response_data') app.use(respDataMiddleware) |
koa_response_data.js
// 处理业务逻辑的中间件 ,读取某个json文件的数据
const path = require('path')
const fileUtils = require('../utils/file_utils')
module.exports = async (ctx, next) => {
// 根据url
const url = ctx.request.url // /api/seller ../data/seller.json
let filePath = url.replace('/api', '') // /seller
filePath = '../data' + filePath + '.json' // ../data/seller.json
filePath = path.join(__dirname, filePath)
try {
const ret = await fileUtils.getFileJsonData(filePath)
ctx.response.body = ret
} catch (error) {
const errorMsg = {
message: '读取文件内容失败 , 文件资源不存在', status: 404
}
ctx.response.body = JSON.stringify(errorMsg) }
console.log(filePath)
await next()
}
file_utils.js
// 读取文件的工具方法
const fs = require('fs')
module.exports.getFileJsonData = (filePath) => {
// 根据文件的路径, 读取文件的内容
return new Promise((resolve, reject) => {
fs.readFile(filePath, 'utf-8', (error, data) => {
if(error) {
// 读取文件失败
reject(error)
} else {
// 读取文件成功
resolve(data)
}
})
})
}
4.6 允许跨域
设置响应头koa_response_header.js 添加
// 设置响应头的中间件
module.exports = async (ctx, next) => {
const contentType = 'application/json; charset=utf-8'
ctx.set('Content-Type', contentType)
ctx.set("Access-Control-Allow-Origin", "*")
ctx.set("Access-Control-Allow-Methods", "OPTIONS, GET, PUT, POST, DELETE")
await next()
}
5.引入webScoket
WebSocket可以保持着浏览器和客户端之间的长连接, 通过 WebSocket可以实现数据由后端推送到前 端,保证了数据传输的实时性. WebSocket涉及到前端代码和后端代码的改造
5.1. WebSocket 的使用
安装 WebSocket包
npm i ws -S
创建 service\web_socket_service.js 文件
创建 WebSocket实例对象
const WebSocket = require('ws')
// 创建WebSocket服务端的对象, 绑定的端口号是9998
const wss = new WebSocket.Server({
port: 9998
})
wss.on("connection", client => {
console.log("有客户端连接 ...")
client.on("message", msg => {
console.log("客户端发送数据过来了")
// 发送数据给客户端
client.send('hello socket')
})
})
监听事件
在 app.js 中引入 web_socket_service.js 这个文件, 并调用 listen 方法
const webSocketService = require('./service/web_socket_service')
webSocketService.listen()
5.2 约定客户端之间数据交互格式
客户端和服务端之间的数据交互采用 JSON 格式
客户端发送数据给服务端的字段如下:
{
"action": "getData",
"socketType": "trendData",
"chartName": "trend",
"value": ""
}
或者
{
"action": "fullScreen",
"socketType": "fullScreen",
"chartName": "trend",
"value": true
}
或者
{
"action": "themeChange",
"socketType": "themeChange",
"chartName": "",
"value": "chalk"
}
其中:
action : 代表某项行为,可选值有
- getData 代表获取图表数据
- fullScreen 代表产生了全屏事件
- themeChange 代表产生了主题切换的事件
socketType : 代表业务模块类型, 这个值代表前端注册数据回调函数的标识, 可选值有:
- trendData
- sellerData
- mapData
- rankData
- hotData
- stockData
- fullScreen
- themeChange
chartName : 代表图表名称, 如果是主题切换事件, 可不传此值, 可选值有:
- trend
- seller
- rank
- stock
value : 代表 具体的数据值, 在获取图表数据时, 可不传此值, 可选值有
如果是全屏事件, true代表全屏, false代表非全屏
如果是主题切换事件, 可选值有 chalk或者 vintage
服务端发送给客户端的数据如下:
{
"action": "getData",
"socketType": "trendData",
"chartName": "trend",
"value": "",
"data": "从文件读取出来的json文件的内容"
}
或者
{
"action": "fullScreen",
"socketType": "fullScreen",
"chartName": "trend",
"value": true
}
或者
{
"action": "themeChange", "socketType": "themeChange", "chartName": "",
"value": "chalk"
}
注意, 除了 action为 getData 时, 服务器会在客户端发过来数据的基础之上, 增加 data字段, 其他的情况, 服务器会原封不动的将从某一个客户端发过来的数据转发给每一个处于连接状态 的客户端
5.3.代码实现 service\web_socket_service.js
const path = require('path')
const fileUtils = require('../utils/file_utils')
const WebSocket = require('ws')
// 创建WebSocket服务端的对象, 绑定的端口号是9998
const wss = new WebSocket.Server({
port: 9998
})
// 服务端开启了监听
module.exports.listen = () => {
// 对客户端的连接事件进行监听
// client:代表的是客户端的连接socket对象
wss.on('connection', client => {
console.log('有客户端连接成功了...')
// 对客户端的连接对象进行message事件的监听
// msg: 由客户端发给服务端的数据
client.on('message',async msg => {
console.log('客户端发送数据给服务端了: ' + msg)
let payload = JSON.parse(msg)
const action = payload.action
if (action === 'getData') {
let filePath = '../data/' + payload.chartName + '.json'
// payload.chartName // trend seller map rank hot stock
filePath = path.join(__dirname, filePath)
const ret = await fileUtils.getFileJsonData(filePath)
// 需要在服务端获取到数据的基础之上, 增加一个data的字段
// data所对应的值,就是某个json文件的内容
payload.data = ret
client.send(JSON.stringify(payload))
console.log(payload.data)
} else {
// 原封不动的将所接收到的数据转发给每一个处于连接状态的客户端
// wss.clients // 所有客户端的连接
wss.clients.forEach(client => {
console.log("客户端触发"+action+"事件")
// client.send(msg)
payload.data = JSON.parse(msg)
client.send(JSON.stringify(payload) )
})
}
// 由服务端往客户端发送数据
// client.send('hello socket from backend')
})
})
}