在Vue2+Echarts+koa2+websocket电商平台数据可视化实时检测系统(一)文中,介绍了前端项目搭建过程,本文将继续描述后端使用koa2搭建后台服务系统流程。 本文将带你体验使用javascript写后端应用。
思考几个问题
单线程的javascript怎么写后端服务
JavaScript 是一种单线程语言,这意味着它在执行代码时只能做一件事情。然而,这并不意味着 JavaScript 不能用于后端服务的开发。事实上,有很多 JavaScript 运行时环境,如 Node.js,可以让 JavaScript 用于后端服务的开发。
在 Node.js 中,JavaScript 可以用于开发高性能的后端服务,包括 Web 服务器、API 服务器、数据库服务器等等。在这种情况下,JavaScript 的单线程特性并不是一个问题,因为 Node.js 运行时环境本身是多线程的,并且可以使用多线程技术来处理并发请求。
在开发后端服务时的JavaScript 与前端 JavaScript 有一些区别,主要是以下几点:
- 运行环境: 前端 JavaScript 运行在浏览器中,而后端 JavaScript 运行在 Node.js 运行时环境中。
- API 支持: 前端 JavaScript 可以使用浏览器提供的 API,如 DOM API、BOM API、Web API 等等,而后端 JavaScript 可以使用 Node.js 提供的 API,如文件系统 API、网络 API、数据库 API 等等。
- 安全性: 由于前端 JavaScript 运行在浏览器中,因此它的安全性受到限制,而后端 JavaScript 运行在服务器端,因此它的安全性可以得到更好的控制。
- 框架和库: 在前端 JavaScript 中,有很多流行的框架和库,如 React、Angular、Vue.js 等等,而在后端 JavaScript 中,也有很多流行的框架和库,如 Express、Koa、Nest.js 等等。
总的来说,在开发后端服务时,JavaScript 与前端 JavaScript 有一些区别,但是它们的基本语法和语义是相同的,因此可以相互转换。
node.js是什么
Node.js 是一个开源的 JavaScript 运行时环境(也称为 JavaScript 引擎),它允许开发者使用 JavaScript 语言编写服务器端应用程序。Node.js 的核心是一个基于 Chrome V8 引擎的事件驱动、非阻塞 I/O 的平台,这使得它非常适合处理大量并发连接和 I/O 密集型任务,如实时通信、Web 服务器、API 服务等。
Node.js 为 JavaScript 做了以下几个关键贡献:
- 扩展性: 通过非阻塞 I/O 和事件驱动模型,Node.js 可以轻松处理高并发,使得 JavaScript 可以用于后端开发,而不仅仅是前端浏览器环境。
- 实时性: Node.js 适用于实时通信,如实时聊天应用、WebSocket 服务等,因为它的事件循环机制可以确保数据的即时发送和接收。
- 模块化: Node.js 提供了模块系统,使得代码易于组织和复用,开发者可以使用 npm(Node Package Manager)来管理和安装第三方模块。
- 生态系统丰富: Node.js 拥有丰富的第三方库支持,如 Express(Web 框架)、Socket.io(实时通信库)、Mongoose(MongoDB ORM)等,极大地扩展了其功能和适用范围。
- 易于学习: 对于熟悉 JavaScript 的开发者来说,学习 Node.js 的门槛相对较低,因为它使用的是同一种语言。
Node.js 使得 JavaScript 不再局限于浏览器环境,而是成为了一种多用途的编程语言,不仅可用于构建前端应用,也能用于构建高性能的后端服务
1. KOA2——基于 Node.js平台的Web服务器框架
Koa是一个基于Node.js的Web框架,它提供了一组强大的工具和函数,帮助开发者构建高效、可靠的后端应用程序。Koa的设计理念是中间件(Middleware)的洋葱模型,通过一系列的中间件来处理HTTP请求和响应,使得开发者可以更灵活地控制应用程序的流程。
在Node.js中,Koa与Express等框架类似,都是用来简化和加速后端开发的工具。Koa相对于Express更加轻量级,它采用了ES6的语法特性,支持异步函数和Promise,使得编写异步代码更加简洁和优雅。
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')
})
})
}