一路繁花似锦绣前程
失败的越多,成功才越有价值

导航

 

十二、Koa2

1、简介
* koa:基于node.js平台的下一代web开发框架
* koa是有express原班人马打造的,致力于成为一个更小、更富有表现力、更健壮的web框架。使用koa编写
  web应用,通过组合不同的generator,可以免除重复繁琐的回调函数嵌套,并极大地提升错误处理的效率。
  koa不在内核方法中绑定任何中间件,它仅仅提供了一个轻量优雅的函数库,使得编写web应用变得得心应手。
2、快速开始
  • 安装koa2
# 初始化package.json
npm init

# 安装koa2
npm install koa
  • hello world 代码
const Koa = require('koa')
const app = new Koa()

app.use(async (ctx) => {
  ctx.body = 'hello koa2' //json数据
})

app.listen(3000)
* ctx.req:node的request对象
* ctx.res:node的response对象。绕过koa的response处理是不被支持的,
  应避免使用以下node属性:
    - res.statusCode
    - res.writeHead()
    - res.write()
    - res.end()
* ctx.request:koa的request对象
* ctx.response:koa的response对象
* 以下访问器和request别名等效
    - ctx.header
    - ctx.headers
    - ctx.method
    - ctx.method=
    - ctx.url
    - ctx.url=
    - ctx.originalUrl
    - ctx.origin
    - ctx.href
    - ctx.path
    - ctx.path=
    - ctx.query
    - ctx.query=
    - ctx.querystring
    - ctx.querystring=
    - ctx.host
    - ctx.hostname
* 以下访问器和response别名等效
    - ctx.body
    - ctx.body=
    - ctx.status
    - ctx.status=
    - ctx.message
    - ctx.message=
    - ctx.length=
    - ctx.length
    - ctx.type=
    - ctx.type
    - ctx.headerSent
    - ctx.redirect()
    - ctx.attachment()
    - ctx.set()
    - ctx.append()
    - ctx.remove()
    - ctx.lastModified=
    - ctx.etag=
  • 启动demo
node index.js
3、koa vs express
* 通常都会说Koa是洋葱模型,这重点在于中间件的设计。但是按照上面的分析,会发现Express也是类似的,
  不同的是Express中间件机制使用了Callback实现,这样如果出现异步则可能会使你在执行顺序上感到
  困惑,因此如果我们想做接口耗时统计、错误处理Koa的这种中间件模式处理起来更方便些。最后一点响
  应机制也很重要,Koa不是立即响应,是整个中间件处理完成在最外层进行了响应,而Express则是立即
  响应。
* 更轻量
    - koa不提供内置的中间件;
    - koa不提供路由,而是把路由这个库分离出来了(koa/router)
* Context对象
    - koa增加了一个Context的对象,作为这次请求的上下文对象(在koa2中作为中间件的第一个参
      数传入)。同时Context上也挂载了Request和Response两个对象。和Express类似,这两个
      对象都提供了大量的便捷方法辅助开发, 这样的话对于在保存一些公有的参数的话变得更加合情合理
* 异步流程控制
    - express采用callback来处理异步, koa v1采用generator,koa v2采用async/await
    - generator和async/await使用同步的写法来处理异步,明显好于callback和promise
* 中间件模型
    - express基于connect中间件,线性模型;
    - koa中间件采用洋葱模型(对于每个中间件,在完成了一些事情后,可以非常优雅的将控制权传递
      给下一个中间件,并能够等待它完成,当后续的中间件完成处理后,控制权又回到了自己)
    - request -> pylons app / routes middleware / session middleware / 
      cache middleware / error handler / status code redirect / 
      registry manager -> response
    - 中间件1 → 打印1 → 中间件2
                          ↓
      中间件1 ← 打印3 ← 中间件2
        ↓
      中间件1 → 打印4 → 出口
    - 中间件1   中间件2
      log(1)
        ↓
      next() → log(2)
        ↓         ↓
        ↓       异步()
        ↓         ↓ 异步后面输出
      log(4) ← log(3)
//同步
var express = require("express")
var app = express()

app.use((req, res, next) => {
  console.log(1)
  next()
  console.log(4)
  res.send("hello")
})
app.use(() => {
  console.log(3)
})

app.listen(3000)
//异步
var express = require("express")
var app = express()

app.use(async (req, res, next) => {
  console.log(1)
  await next()
  console.log(4)
  res.send("hello")
})
app.use(async () => {
  console.log(2)
  await delay(1)
  console.log(3)
})

function delay(time) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, 1000)
  })
}
//同步
var koa = require("koa")
var app = new koa()

app.use((ctx, next) => {
  console.log(1)
  next()
  console.log(4)
  ctx.body = "hello"
})
app.use(() => {
  console.log(3)
})

app.listen(3000)

//异步
var koa = require("koa")
var app = new koa()

app.use(async (ctx, next) => {
  console.log(1)
  await next()
  console.log(4)
  ctx.body = "hello"
})
app.use(async () => {
  console.log(2)
  await delay(1)
  console.log(3)
})

function delay(time) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, 1000)
  })
}

app.listen(3000)
4、路由
  • 基本用法
var Koa = require("koa")
var Router = require("koa-router")

var app = new Koa()
var router = new Router()

router.post("/list", (ctx) => {
  ctx.body = ["111", "222", "333"]
})
app.use(router.routes()).use(router.allowedMethods())
app.listen(3000)
  • router.allowedMethods作用
* 浏览器-控制台-network-headers
    - general
        ~ status code: 405 method not allowed
    - response headers
        ~ allow: post
  • 请求方式
* Koa-router请求方式:get、put、post、patch、delete、del,而使用方法就是router.方式(),
  比如router.get()和router.post()。而router.all()会匹配所有的请求方法。
var Koa = require("koa")
var Router = require("koa-router")

var app = new Koa()
var router = new Router()

router.get("/user", (ctx) => {
  ctx.body = ["aaa", "bbb", "ccc"]
})
  .put("/user/:id", (ctx) => {
    ctx.body = {ok: 1, info: "user update"}
  })
  .post("/user", (ctx) => {
    ctx.body = {ok: 1, info: "user post"}
  })
  .del("/user/:id", (ctx) => {
    ctx.body = {ok: 1, info: "user del"}
  })

app.use(router.routes()).use(router.allowedMethods())
app.listen(3000)
  • 拆分路由
// list.js
var Router = require("koa-router")
var router = new Router()
router.get("/", (ctx) => {
  ctx.body = ["111", "222", "333"]
})
  .put("/:id", (ctx) => {
    ctx.body = {ok: 1, info: "list update"}
  })
  .post("/", (ctx) => {
    ctx.body = {ok: 1, info: "list post"}
  })
  .del("/:id", (ctx) => {
    ctx.body = {ok: 1, info: "list del"}
  })
module.exports = router
// index.js
var Router = require("koa-router")
var router = new Router()
var user = require("./user")
var list = require("./list")
router.use('/user', user.routes(), user.allowedMethods())
router.use('/list', list.routes(), list.allowedMethods())

module.exports = router
// entry入口
var Koa = require("koa")
var router = require("./router/index")

var app = new Koa()
app.use(router.routes()).use(router.allowedMethods())
app.listen(3000)
  • 路由前缀
router.prefix('/api')
  • 路由重定向
router.get("/home", (ctx) => {
  ctx.body = "home页面"
})
//写法1 
router.redirect('/', '/home');
//写法2
router.get("/", (ctx) => {
  ctx.redirect("/home")
})
5、静态资源
const Koa = require('koa')
const path = require('path')
const static = require('koa-static')

const app = new Koa()

app.use(static(path.join(__dirname, "public")))

app.use(async (ctx) => {
  ctx.body = 'hello world'
})

app.listen(3000, () => {
  console.log('[demo] static-use-middleware is starting at port 3000')
})
6、获取请求参数
  • get参数
* 在koa中,获取GET请求数据源头是koa中request对象中的query方法或querystring方法,
  query返回是格式化好的参数对象,querystring返回的是请求字符串,由于ctx对request
  的API有直接引用的方式,所以获取GET请求数据有两个途径。
    - 是从上下文中直接获取请求对象ctx.query,返回如{a:1,b:2}请求字符串ctx.querystring,
      返回如a=1&b=2
    - 是从上下文的request对象中获取请求对象ctx.request.query,返回如{a:1,b:2}请求字符串
      ctx.request.querystring,返回如a=1&b=2
  • post参数
* 对于POST请求的处理,koa-bodyparser中间件可以把koa2上下文的formData数据解析到
  ctx.request.body中
const bodyParser = require('koa-bodyparser')

// 使用ctx.body解析中间件
app.use(bodyParser())
7、ejs模板
  • 安装模块
# 安装koa模板使用中间件
npm install --save koa-views

# 安装ejs模板引擎
npm install --save ejs
  • 使用模板引擎
* 文件目录
########
├── package.json
├── index.js
└── view
    └── index.ejs
########
// ./index.js文件
const Koa = require('koa')
const views = require('koa-views')
const path = require('path')
const app = new Koa()

// 加载模板引擎
app.use(views(path.join(__dirname, './view'), {
  extension: 'ejs'
}))

app.use(async (ctx) => {
  let title = 'hello koa2'
  await ctx.render('index', {
    title,
  })
})

app.listen(3000)
// ./view/index.ejs 模板
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
</head>
<body>
<h1><%= title %></h1>
<p>EJS Welcome to <%= title %></p>
</body>
</html>
8、cookie&session
  • cookie
* koa提供了从上下文直接读取、写入cookie的方法
    - ctx.cookies.get(name, [options]) 读取上下文请求中的cookie
    - ctx.cookies.set(name, value, [options]) 在上下文中写入cookie
  • session
// koa-session-minimal适用于koa2的session中间件,提供存储介质的读写接口 。
const session = require('koa-session-minimal')
app.use(session({
  key: 'SESSION_ID', cookie: {
    maxAge: 1000 * 60
  }
}))
app.use(async (ctx, next) => {
  //排除login相关的路由和接口
  if (ctx.url.includes("login")) {
    await next()
    return
  }

  if (ctx.session.user) {
    //重新设置以下sesssion
    ctx.session.mydate = Date.now()
    await next()
  } else {
    ctx.redirect("/login")
  }
})
9、JWT
app.use(async (ctx, next) => {
  //排除login相关的路由和接口
  if (ctx.url.includes("login")) {
    await next()
    return
  }
  const token = ctx.headers["authorization"]?.split(" ")[1]
  // console.log(req.headers["authorization"])
  if (token) {
    const payload = JWT.verify(token)
    if (payload) {
      //重新计算token过期时间
      const newToken = JWT.generate({
        _id: payload._id, username: payload.username
      }, "10s")
      ctx.set("Authorization", newToken)
      await next()
    } else {
      ctx.status = 401
      ctx.body = {errCode: -1, errInfo: "token过期"}
    }
  } else {
    await next()
  }
})
10、上传文件
/**
 * 1、https://www.npmjs.com/package/@koa/multer
 * 2、npm install --save @koa/multer multer
 */
const multer = require('@koa/multer');
const upload = multer({dest: 'public/uploads/'})

router.post("/", upload.single('avatar'), (ctx, next) => {
  console.log(ctx.request.body, ctx.file)
  ctx.body = {
    ok: 1, info: "add user success"
  }
})
11、操作MongoDB
const mongoose = require("mongoose")

mongoose.connect("mongodb://127.0.0.1:27017/kerwin_project")
//插入集合和数据,数据库kerwin_project会自动创建
const mongoose = require("mongoose")
const Schema = mongoose.Schema
const UserType = {
  username: String, password: String, age: Number, avatar: String
}

const UserModel = mongoose.model("user", new Schema(UserType))
// 模型user 将会对应 users 集合, 
module.exports = UserModel

十三、mysql

1、介绍
* 付费的商用数据库:
    - Oracle:典型的高富帅
    - SQL Server:微软自家产品,Windows定制专款
    - DB2:IBM的产品,听起来挺高端
    - Sybase:曾经跟微软是好基友,后来关系破裂,现在家境惨淡
* 这些数据库都是不开源而且付费的,最大的好处是花了钱出了问题可以找厂家解决,
  不过在Web的世界里,常常需要部署成千上万的数据库服务器,当然不能把大把大把
  的银子扔给厂家,所以,无论是Google、Facebook,还是国内的BAT,无一例外
  都选择了免费的开源数据库:
    - MySQL:大家都在用,一般错不了
    - PostgreSQL:学术气息有点重,其实挺不错,但知名度没有MySQL高
    - sqlite:嵌入式数据库,适合桌面和移动应用
* 作为一个JavaScript全栈工程师,选择哪个免费数据库呢?当然是MySQL。因为MySQL
  普及率最高,出了错,可以很容易找到解决方法。而且,围绕MySQL有一大堆监控和运维
  的工具,安装和使用很方便。
2、与非关系数据库区别
* 关系型和非关系型数据库的主要差异是数据存储的方式。关系型数据天然就是表格式的,
  因此存储在数据表的行和列中。数据表可以彼此关联协作存储,也很容易提取数据。
* 与其相反,非关系型数据不适合存储在数据表的行和列中,而是大块组合在一起。非关系
  型数据通常存储在数据集中,就像文档、键值对或者图结构。你的数据及其特性是选择数
  据存储和提取方式的首要影响因素。
* 关系型数据库最典型的数据结构是表,由二维表及其之间的联系所组成的一个数据组织
    - 优点:
        ~ 1、易于维护:都是使用表结构,格式一致;
        ~ 2、使用方便:SQL语言通用,可用于复杂查询;
        ~ 3、复杂操作:支持SQL,可用于一个表以及多个表之间非常复杂的查询。
    - 缺点:
        ~ 1、读写性能比较差,尤其是海量数据的高效率读写;
        ~ 2、固定的表结构,灵活度稍欠;
        ~ 3、高并发读写需求,传统关系型数据库来说,硬盘I/O是一个很大的瓶颈。
* 非关系型数据库严格上不是一种数据库,应该是一种数据结构化存储方法的集合,可以是文档或者键值对等。
    - 优点:
        ~ 1、格式灵活:存储数据的格式可以是key,value形式、文档形式、图片形式等等,文档形式、
          图片形式等等,使用灵活,应用场景广泛,而关系型数据库则只支持基础类型。
        ~ 2、速度快:nosql可以使用硬盘或者随机存储器作为载体,而关系型数据库只能使用硬盘;
        ~ 3、高扩展性;
        ~ 4、成本低:nosql数据库部署简单,基本都是开源软件。
    - 缺点:
        ~ 1、不提供sql支持;
        ~ 2、无事务处理;
        ~ 3、数据结构相对复杂,复杂查询方面稍欠。
3、sql语句
  • 插入
INSERT INTO `students`(`id`, `name`, `score`, `gender`) VALUES (null,'kerwin',100,1)
//可以不设置id(主键自增),create_time(设默认值为当前时间)
  • 更新
UPDATE `students` SET `name`='tiechui',`score`=20,`gender`=0 WHERE id=2;
  • 删除
DELETE FROM `students` WHERE id=2;
  • 查询
查所有的数据所有的字段
SELECT * FROM `students` WHERE 1;

查所有的数据某个字段
SELECT `id`, `name`, `score`, `gender` FROM `students` WHERE 1;

条件查询
SELECT * FROM `students` WHERE score>=80;
SELECT * FROM `students` where score>=80 AND gender=1

模糊查询
SELECT * FROM `students` where name like '%k%'

排序
SELECT id, name, gender, score FROM students ORDER BY score;
SELECT id, name, gender, score FROM students ORDER BY score DESC;

分页查询
SELECT id, name, gender, score FROM students LIMIT 50 OFFSET 0

记录条数
SELECT COUNT(*) FROM students;
SELECT COUNT(*) kerwinnum FROM students;

多表查询

SELECT * FROM students, classes;(这种多表查询又称笛卡尔查询,使用笛卡尔查询时要非常小心,由于结果集是目标表的行数乘积,对两个各自有100行记录的表进行笛卡尔查询将返回1万条记录,对两个各自有1万行记录的表进行笛卡尔查询将返回1亿条记录)
SELECT
    students.id sid,
    students.name,
    students.gender,
    students.score,
    classes.id cid,
    classes.name cname
FROM students, classes; (要使用表名.列名这样的方式来引用列和设置别名,这样就避免了结果集的列名重复问题。)

SELECT
    s.id sid,
    s.name,
    s.gender,
    s.score,
    c.id cid,
    c.name cname
FROM students s, classes c; (SQL还允许给表设置一个别名)


联表查询
SELECT s.id, s.name, s.class_id, c.name class_name, s.gender, s.score
FROM students s
INNER JOIN classes c
ON s.class_id = c.id; (连接查询对多个表进行JOIN运算,简单地说,就是先确定一个主表作为结果集,然后,把其他表的行有选择性地“连接”在主表结果集上。)
* 我们把tableA([1,2])看作左表,把tableB([2,3])看成右表,那么inner join是选出两张表都存在的记录:
    - [2]
* left outer join是选出左表存在的记录:
    - [1,2]
* right outer join是选出右表存在的记录:
    - [2,3]
* full outer join则是选出左右表都存在的记录:
    - [1,2,3]
  • 注意
* InnoDB支持事务,MyISAM不支持事务。这是MySQL将默认存储引擎从MyISAM变成InnoDB的重要原因之一
* InnoDB支持外键,而MyISAM不支持。对一个包含外键的InnoDB表转为MYISAM会失败
  • 外键约束
* CASCADE:在父表上update/delete记录时,同步update/delete掉子表的匹配记录
* SET NULL:在父表上update/delete记录时,将子表上匹配记录的列设为null(要注意子表的外键列不能为not null)
* NO ACTION:如果子表中有匹配的记录,则不允许对父表对应候选键进行update/delete操作
* RESTRICT:同no action,都是立即检查外键约束
4、nodejs操作数据库
const express = require('express')
const app = express()
const mysql2 = require('mysql2')
const port = 9000

app.get('/', async (req, res) => {
  const config = getDBConfig()
  const promisePool = mysql2.createPool(config).promise();
  // console.log(promisePool)
  let user = await promisePool.query('select * from students');
  console.log(user)
  if (user[0].length) {
    //存在用户
    res.send(user[0])
  } else {
    //不存在
    res.send({
      code: -2, msg: 'user not exsit',
    })
  }
})

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
})

function getDBConfig() {
  return {
    host: '127.0.0.1', user: 'root', port: 3306, password: '', database: 'kerwin_test', connectionLimit: 1 //创建一个连接池
  }
}
查询:
promisePool.query('select * from users');

插入:
promisePool.query('INSERT INTO `users`(`id`,`name`,`age`, `password`) VALUES (?,?,?,?)',[null,"kerwin",100,"123456"]);

更新:
promisePool.query(`UPDATE users SET name = ? ,age=? WHERE id = ?`,["xiaoming2",20,1])

删除:
promisePool.query(`delete from users where id=?`,[1])

十四、Socket编程

1、websocket介绍
* http(||-connection lifecycle)
    - client               server
        |                    |
        |------request------→||  
        |                    ||  
        |←----response-------||  
        |                    |
        |------request------→||  
        |                    ||  
        |←----response-------||  
        ↓                    ↓ 
* websocket(||-connection lifecycle)
    - client                      server
        |                           |
        |---------handshake--------→||  
        |                           ||  
        |←------acknowledgement-----||  
        |                           || 
        |←-bi-directional messages-→||  
        |                           ||  
        |←-bi-directional messages-→||  
        |                           ||  
        |←-----connection end------→||  
        ↓                           ↓   
* 应用场景
    - 弹幕
    - 媒体聊天
    - 协同编辑
    - 基于位置的应用
    - 体育实况更新
    - 股票基金报价实时更新
* WebSocket并不是全新的协议,而是利用了HTTP协议来建立连接。我们来看看WebSocket连接是如何创建的。
* 首先,WebSocket连接必须由浏览器发起,因为请求协议是一个标准的HTTP请求,格式如下:
########
GET ws://localhost:3000/ws/chat HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Origin: http://localhost:3000
Sec-WebSocket-Key: client-random-string
Sec-WebSocket-Version: 13
########
* 该请求和普通的HTTP请求有几点不同:
    - GET请求的地址不是类似/path/,而是以ws://开头的地址;
    - 请求头Upgrade: websocket和Connection: Upgrade表示这个连接将要被转换为WebSocket连接;
    - Sec-WebSocket-Key是用于标识这个连接,并非用于加密数据;
    - Sec-WebSocket-Version指定了WebSocket的协议版本。
* 随后,服务器如果接受该请求,就会返回如下响应:
########
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: server-random-string
########
* 该响应代码101表示本次连接的HTTP协议即将被更改,更改后的协议就是Upgrade: websocket指定的WebSocket协议。
* 版本号和子协议规定了双方能理解的数据格式,以及是否支持压缩等等。如果仅使用WebSocket的API,就不需要关心这些。
* 现在,一个WebSocket连接就建立成功,浏览器和服务器就可以随时主动发送消息给对方。消息有两种,一种是文本,
  一种是二进制数据。通常,我们可以发送JSON格式的文本,这样,在浏览器处理起来就十分容易。
* 为什么WebSocket连接可以实现全双工通信而HTTP连接不行呢?实际上HTTP协议是建立在TCP协议之上的,
  TCP协议本身就实现了全双工通信,但是HTTP协议的请求-应答机制限制了全双工通信。WebSocket连接
  建立以后,其实只是简单规定了一下:接下来,咱们通信就不使用HTTP协议了,直接互相发数据吧。
* 安全的WebSocket连接机制和HTTPS类似。首先,浏览器用wss://xxx创建WebSocket连接时,
  会先通过HTTPS创建安全的连接,然后,该HTTPS连接升级为WebSocket连接,底层通信走的仍然
  是安全的SSL/TLS协议。
* 浏览器支持:很显然,要支持WebSocket通信,浏览器得支持这个协议,这样才能发出ws://xxx的请求。
  目前,支持WebSocket的主流浏览器如下:
    - Chrome
    - Firefox
    - IE >= 10
    - Sarafi >= 6
    - Android >= 4.4
    - iOS >= 8
* 服务器支持
    - 由于WebSocket是一个协议,服务器具体怎么实现,取决于所用编程语言和框架本身。
      Node.js本身支持的协议包括TCP协议和HTTP协议,要支持WebSocket协议,需要对
      Node.js提供的HTTPServer做额外的开发。已经有若干基于Node.js的稳定可靠的
      WebSocket实现,我们直接用npm安装使用即可。
2、ws模块
  • 服务器:
const WebSocket = require("ws")

WebSocketServer = WebSocket.WebSocketServer
const wss = new WebSocketServer({port: 8080});
wss.on('connection', function connection(ws) {
  ws.on('message', function message(data, isBinary) {
    wss.clients.forEach(function each(client) {
      if (client !== ws && client.readyState === WebSocket.OPEN) {
        client.send(data, {binary: isBinary});
      }
    });
  });
  ws.send('欢迎加入聊天室');
});
  • 客户端:
var ws = new WebSocket("ws://localhost:8080")
ws.onopen = () => {
  console.log("open")
}
ws.onmessage = (evt) => {
  console.log(evt.data)
}
  • 授权验证:
//前端
var ws = new WebSocket(`ws://localhost:8080?token=${localStorage.getItem("token")}`)
ws.onopen = () => {
  console.log("open")
  ws.send(JSON.stringify({
    type: WebSocketType.GroupList
  }))
}
ws.onmessage = (evt) => {
  console.log(evt.data)
}
//后端
const WebSocket = require("ws");
const JWT = require('../util/JWT');
WebSocketServer = WebSocket.WebSocketServer
const wss = new WebSocketServer({port: 8080});
wss.on('connection', function connection(ws, req) {
  const myURL = new URL(req.url, 'http://127.0.0.1:3000');
  const payload = JWT.verify(myURL.searchParams.get("token"))
  if (payload) {
    ws.user = payload
    ws.send(createMessage(WebSocketType.GroupChat, ws.user, "欢迎来到聊天室"))

    sendBroadList() //发送好友列表
  } else {
    ws.send(createMessage(WebSocketType.Error, null, "token过期"))
  }
  // console.log(3333,url)
  ws.on('message', function message(data, isBinary) {
    const messageObj = JSON.parse(data)
    switch (messageObj.type) {
      case WebSocketType.GroupList:
        ws.send(createMessage(WebSocketType.GroupList, ws.user, JSON.stringify(Array.from(wss.clients).map(item => item.user))))
        break;
      case WebSocketType.GroupChat:
        wss.clients.forEach(function each(client) {
          if (client !== ws && client.readyState === WebSocket.OPEN) {
            client.send(createMessage(WebSocketType.GroupChat, ws.user, messageObj.data));
          }
        });
        break;
      case WebSocketType.SingleChat:
        wss.clients.forEach(function each(client) {
          if (client.user.username === messageObj.to && client.readyState === WebSocket.OPEN) {
            client.send(createMessage(WebSocketType.SingleChat, ws.user, messageObj.data));
          }
        });
        break;
    }

    ws.on("close", function () {
      //删除当前用户
      wss.clients.delete(ws.user)
      sendBroadList() //发送好用列表
    })
  });

});
const WebSocketType = {
  Error: 0, //错误
  GroupList: 1,//群列表
  GroupChat: 2,//群聊
  SingleChat: 3//私聊
}

function createMessage(type, user, data) {
  return JSON.stringify({
    type: type, user: user, data: data
  });
}

function sendBroadList() {
  wss.clients.forEach(function each(client) {
    if (client.readyState === WebSocket.OPEN) {
      client.send(createMessage(WebSocketType.GroupList, client.user, JSON.stringify(Array.from(wss.clients).map(item => item.user))))
    }
  });
}
3、socket.io模块
  • 服务端:
const io = require('socket.io')(server);

io.on('connection', (socket) => {
  const payload = JWT.verify(socket.handshake.query.token)
  if (payload) {
    socket.user = payload
    socket.emit(WebSocketType.GroupChat, createMessage(socket.user, "欢迎来到聊天室"))
    sendBroadList() //发送好友列表
  } else {
    socket.emit(WebSocketType.Error, createMessage(null, "token过期"))
  }

  socket.on(WebSocketType.GroupList, () => {
    socket.emit(WebSocketType.GroupList, createMessage(null, Array.from(io.sockets.sockets).map(item => item[1].user).filter(item => item)));
  })

  socket.on(WebSocketType.GroupChat, (messageObj) => {
    socket.broadcast.emit(WebSocketType.GroupChat, createMessage(socket.user, messageObj.data));
  })

  socket.on(WebSocketType.SingleChat, (messageObj) => {
    Array.from(io.sockets.sockets).forEach(function (socket) {
      if (socket[1].user.username === messageObj.to) {
        socket[1].emit(WebSocketType.SingleChat, createMessage(socket[1].user, messageObj.data));
      }
    })
  })

  socket.on('disconnect', reason => {
    sendBroadList() //发送好用列表
  });
});

function sendBroadList() {
  io.sockets.emit(WebSocketType.GroupList, createMessage(null, Array.from(io.sockets.sockets).map(item => item[1].user).filter(item => item)))
}

//最后filter,是因为 有可能存在null的值
  • 客户端:
const WebSocketType = {
  Error: 0, //错误
  GroupList: 1, //群列表
  GroupChat: 2, //群聊
  SingleChat: 3 //私聊
}

const socket = io(`ws://localhost:3000?token=${localStorage.getItem("token")}`);
socket.on("connect", () => {
  socket.emit(WebSocketType.GroupList)
})
socket.on(WebSocketType.GroupList, (messageObj) => {
  select.innerHTML = ""
  select.innerHTML = `<option value="all">all</option>` + messageObj.data.map(item => `
    <option value="${item.username}">${item.username}</option>`).join("")
})

socket.on(WebSocketType.GroupChat, (msg) => {
  console.log(msg)
})

socket.on(WebSocketType.SingleChat, (msg) => {
  console.log(msg)
})

socket.on(WebSocketType.Error, (msg) => {
  localStorage.removeItem("token")
  location.href = "/login"
})

send.onclick = () => {
  if (select.value === "all") {
    socket.emit(WebSocketType.GroupChat, {
      data: text.value
    })
  } else {
    socket.emit(WebSocketType.SingleChat, {
      data: text.value, to: select.value
    })
  }
}

十五、mocha

1、概念
* 单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。
* 比如对函数abs(),我们可以编写出以下几个测试用例:
    - 输入正数,比如1、1.2、0.99,期待返回值与输入相同;
    - 输入负数,比如-1、-1.2、-0.99,期待返回值与输入相反;
    - 输入0,期待返回0;
    - 输入非数值类型,比如null、[]、{},期待抛出Error。
* 把上面的测试用例放到一个测试模块里,就是一个完整的单元测试。
* 如果单元测试通过,说明我们测试的这个函数能够正常工作。如果单元测试不通过,要么函数有bug,
  要么测试条件输入不正确,总之,需要修复使单元测试能够通过。
* 单元测试通过后有什么意义呢?如果我们对abs()函数代码做了修改,只需要再跑一遍单元测试,
  如果通过,说明我们的修改不会对abs()函数原有的行为造成影响,如果测试不通过,说明我们的
  修改与原有行为不一致,要么修改代码,要么修改测试。
* 这种以测试为驱动的开发模式最大的好处就是确保一个程序模块的行为符合我们设计的测试用例。
  在将来修改的时候,可以极大程度地保证该模块行为仍然是正确的。
* mocha是JavaScript的一种单元测试框架,既可以在浏览器环境下运行,也可以在Node.js环境下运行。
* 使用mocha,我们就只需要专注于编写单元测试本身,然后,让mocha去自动运行所有的测试,并给出测试结果。
* mocha的特点主要有:
    - 既可以测试简单的JavaScript函数,又可以测试异步代码,因为异步是JavaScript的特性之一;
    - 可以自动运行所有测试,也可以只运行特定的测试;
    - 可以支持before、after、beforeEach和afterEach来编写初始化代码。
2、编写测试
const assert = require('assert');
const sum = require('../test');
describe('#hello.js', () => {
  describe('#sum()', () => {
    it('sum() should return 0', () => {
      assert.strictEqual(sum(), 0);
    });

    it('sum(1) should return 1', () => {
      assert.strictEqual(sum(1), 1);
    });

    it('sum(1, 2) should return 3', () => {
      assert.strictEqual(sum(1, 2), 3);
    });

    it('sum(1, 2, 3) should return 6', () => {
      assert.strictEqual(sum(1, 2, 3), 6);
    });
  });
});
3、chai断言库
* 断言:mocha允许你使用任意你喜欢的断言库,在上面的例子中,我们使用了Node.js的内置的断言模块作为
  断言。如果能够抛出一个错误,它就能够运行。这意味着你能使用下面的这些仓库,比如:
    - should.js:BDD风格贯穿始终
    - expcet.js:expect()样式断言
    - chai:expect()、assert()和should风格的断言
    - better-assert:C风格的自文档化的assert()
    - unexcepted:“可扩展的BDD断言工具”
var chai = require('chai')
var assert = chai.assert;

describe('assert Demo', function () {
  it('use assert lib', function () {
    var value = "hello";
    assert.typeOf(value, 'string')
    assert.equal(value, 'hello')
    assert.lengthOf(value, 5)
  })
})
var chai = require('chai');
chai.should();

describe('should Demo', function () {
  it('use should lib', function () {
    var value = 'hello'
    value.should.exist.and.equal('hello').and.have.length(5).and.be.a('string')
    // value.should.be.a('string')
    // value.should.equal('hello')
    // value.should.not.equal('hello2')
    // value.should.have.length(5);
  })
});
var chai = require('chai');
var expect = chai.expect;

describe('expect Demo', function () {
  it('use expect lib', function () {
    var value = 'hello'
    var number = 3

    expect(number).to.be.at.most(5)
    expect(number).to.be.at.least(3)
    expect(number).to.be.within(1, 4)

    expect(value).to.exist
    expect(value).to.be.a('string')
    expect(value).to.equal('hello')
    expect(value).to.not.equal('您好')
    expect(value).to.have.length(5)
  })
});
4、异步测试
var fs = require("fs").promises
var chai = require('chai');
var expect = chai.expect;
it('test async function', async function () {
  const data = await fs.readFile('./1.txt', "utf8");
  expect(data).to.equal('hello')
});
5、http测试
const request = require('supertest')
const app = require('../app');

describe('#test koa app', () => {
  let server = app.listen(3000);
  describe('#test server', () => {
    it('#test GET /', async () => {
      await request(server)
        .get('/')
        .expect('Content-Type', /text\/html/)
        .expect(200, '<h1>hello world</h1>');
    });

    after(function () {
      server.close()
    });
  });
});
6、钩子函数
describe('#hello.js', () => {
  describe('#sum()', () => {
    before(function () {
      console.log('before:');
    });

    after(function () {
      console.log('after.');
    });

    beforeEach(function () {
      console.log('  beforeEach:');
    });

    afterEach(function () {
      console.log('  afterEach.');
    });
  });
});
posted on 2023-03-10 11:02  一路繁花似锦绣前程  阅读(51)  评论(0编辑  收藏  举报