乐趣区

关于eggjs:实战搭建完整的IM即时通讯应用2

即时通讯应用服务,整套蕴含服务端、治理端和客户端,欢送 Star 反对和查看源码。

现已部署上线,欢送体验客户端和治理端

咱们书接上文,持续实现残缺的即时通讯服务,这篇着重讲下 Server 端我的项目中我认为几个重要的点,大部分内容须要去我的仓库源码和 egg 官网查看。

server 端具体阐明

应用脚手架 npm init egg --type=simple 初始化 server 我的项目,装置 mysql(我的是 8.0 版本),配置上 sequelize 所需的数据库链接明码等,就能够启动了
着重讲下 Server 端我的项目中我认为几个重要的点,大部分内容须要去 egg 官网查看。

// 目录构造阐明

├── package.json // 我的项目信息
├── app.js // 启动文件,其中有一些钩子函数
├── app
|   ├── router.js // 路由
│   ├── controller
│   ├── service
│   ├── middleware // 中间件
│   ├── model // 实体模型
│   └── io // socket.io 相干
│       ├── controller
│       └── middleware // io 独有的中间件
├── config // 配置文件
|   ├── plugin.js // 插件配置文件
|   └── config.default.js // 默认的配置文件
├── logs // server 运行期间产生的 log 文件
└── public // 动态文件和上传文件目录

路由

Router 次要用来形容申请 URL 和具体承当执行动作的 Controller 的对应关系,即 app/router

  1. 路由应用了版本号 v1,不便当前降级,个别的增删改查间接应用 restful 的形式比较简单
  2. 除了登录和注册的接口,在其余所有 http 接口增加了对 session 的查看,校验登录状态,地位在app/middleware/auth.js
  3. 在所有治理端的接口处增加了对 admin 权限的查看,地位在app/middleware/admin.js

对立鉴权

因为本零碎预设有管理员和个别通信用户的不同角色,所以须要针对治理和通信的接口路由做一下对立的鉴权解决。

比方治理端的路由/v1/admin/...,想在这个系列路由全都增加管理员的鉴权,这时候能够用中间件的形式进行鉴权,上面是在 admin router 中应用中间件的具体例子

// middware
module.exports = () => {return async function admin(ctx, next) {let { session} = ctx;

    // 判断 admin 权限
    if (session.user && session.user.rights.some(right => right.keyName === 'admin')) {await next();
    } else {ctx.redirect('/login');
    }
  };
};

// router
const admin = app.middleware.admin();
router.get('/api/v1/admin/rights', admin, controller.v1.admin.rightsIndex);

数据库相干

应用的 sequelize+mysql 组合,egg 也有 sequelize 的相干插件,sequelize 即是一款 Node 环境应用的 ORM,反对 Postgres, MySQL, MariaDB, SQLite 和 Microsoft SQL Server,应用起来还是挺不便的。须要先定义模型和模型间接的关系,有了关系之后便能够应用一些预设的办法了。

model 实体模型

模型的根底信息比拟容易解决,须要留神的就是实体之间的关系设计,即 associate,上面是 user 的关系形容

// User.js
module.exports = app => {const { STRING} = app.Sequelize;

  const User = app.model.define('user', {
    provider: {type: STRING},
    username: {
      type: STRING,
      unique: 'username'
    },
    password: {type: STRING}
  });

  User.associate = function() {
    // One-To-One associations
    app.model.User.hasOne(app.model.UserInfo);

    // One-To-Many associations
    app.model.User.hasMany(app.model.Apply);

    // Many-To-Many associations
    app.model.User.belongsToMany(app.model.Group, { through: 'user_group'});
    app.model.User.belongsToMany(app.model.Role, { through: 'user_role'});
  };

  return User;
};

一对一

例如 user 和 userInfo 的关系就是一对一的关系,定义好了之后,咱们在新建 user 的时候就能够应用 user.setUserInfo(userInfo)了,想获取此 user 的根底信息的时候也能够通过user.getUserInfo()

一对多

User 和 Apply(申请)的关系就是一对多,即一个用户能够对应多个本人的申请,目前只有好友申请和入群申请:

增加申请的时候能够user.addApply(apply),获取的时候能够这样获取:

const result = await ctx.model.Apply.findAndCountAll({
  where: {
    userId: ctx.session.user.id,
    hasHandled: false
  }
});

多对多

user 和 group 的关系就是多对多,即一个用户能够对应多个群组,一个群组也能够对应多个用户,这样 sequelize 会建设一个两头表 user_group 来实现这种关系。

个别我这么应用:

group.addUser(user); // 建设群组和用户的关系
user.getGroups(); // 获取用户的群组信息

须要留神的点

  1. sequelize 的所有操作都是基于 Promise 的,所有大多时候都应用 await 进行期待
  2. 批改了某个模型的实例的某个属性后,须要进行 save
  3. 当咱们须要把模型的数据进行组合后返回给前端的时候,须要通过 get({plain: true})这种形式,转化成数据,而后再拼接,例如获取会话列表的时候

socketio

egg 提供了 egg-socket.io 插件,须要在装置 egg-socket.io 后在 config/plugin.js 开启插件,io 有本人的中间件和 controller

socketio 的路由

io 的路由和个别的 http 申请的不太一样,留神这里的路由不能增加中间件解决(我没胜利),所以禁言解决我是在 controller 外面解决的

// 退出群
io.of('/').route('/v1/im/join', app.io.controller.im.join);
// 发送音讯
io.of('/').route('/v1/im/new-message', app.io.controller.im.newMessage);
// 查问音讯
io.of('/').route('/v1/im/get-messages', app.io.controller.im.getMessages);

留神:我把群组和好友关系都看做是一个 room(也就是一个会话),这样就是间接向这个 romm 外面发消息,外面的人都能够收到

socketio 的中间件

有两个默认的中间件,一个是连贯和断开时候调用的 connection Middleware,这里用来校验登录状态和解决业务逻辑了;另外一个是每次发消息时候调用的 packet Middleware,这里用来打印 log

因为预设了禁言权限,在 controller 外面进行解决

// 对用户发言的权限进行判断
if (!ctx.session.user.rights.some(right => right.keyName === 'speak')) {return;}

聊天

聊天分为单聊和群聊,聊天信息临时有个别的文字、图片、视频和定位音讯,能够依据业务扩大为订单或者商品等

音讯

message 的结构设计参考了几家第三方服务的设计,也联合本我的项目本身的状况做了调整,能够随便扩大,做如下阐明:

const Message = app.model.define('message', {
  /**
    * 音讯类型:* 0: 单聊
    * 1: 群聊
    */
  type: {type: STRING},
  // 音讯体
  body: {type: JSON},
  fromId: {type: INTEGER},
  toId: {type: INTEGER}
});

body 外面寄存的是音讯体,应用 json 用来寄存不同的音讯格局:

// 文本音讯
{
  "type": "txt",
  "msg":"哈哈哈" // 音讯内容
}
// 图片音讯
{
  "type": "img",
  "url": "http://nimtest.nos.netease.com/cbc500e8-e19c-4b0f-834b-c32d4dc1075e",
  "ext":"jpg",
  "w":360,    // 宽
  "h":480,    // 高
  "size": 388245
}
// 视频音讯
{
  "type": 'video',
  "url": "http://nimtest.nos.netease.com/cbc500e8-e19c-4b0f-834b-c32d4dc1075e",
  "ext":"mp4",
  "w":360,    // 宽
  "h":480,    // 高
  "size": 388245
}
// 地理位置音讯
{
  "type": "loc",
  "title":"中国 浙江省 杭州市 网商路 599 号",    // 地理位置 title
  "lng":120.1908686708565,        // 经度
  "lat":30.18704515647036            // 纬度
}

定时工作

以后只有一个,就是更新 baidu 的 token,这里还算简略,参考官网文档即可

机器人聊天

智能对话定制与服务平台 UNIT

这个还是挺有意思的,能够在 https://ai.baidu.com/ 新建机器人和增加对应的技能,我这里是闲聊,还有智能问答等能够抉择

  1. 新建机器人,治理机器人的技能,至多一个
  2. 返回百度云 ” 利用列表 ” 中创立、查看 API Key / Secret Key
  3. 在 config.default.js 中配置 baidu 相干参数,相干接口阐明在这里

如果不想启动能够在 app.js 和 app/schedule/baidu.js 中删除 ctx.service.baidu.getToken();

上传文件

首先须要在配置文件外面进行配置,我这里限度了文件大小,饼跨站了 ios 的视频文件格式:

config.multipart = {
  mode: 'file',
  fileSize: '3mb',
  fileExtensions: ['.mov']
};

应用了一个对立的接口来解决文件上传,外围问题是文件的写入,files 是前端传来的文件列表

for (const file of ctx.request.files) {
  // 生成文件门路,留神 upload 文件门路须要存在
  const filePath = `./public/upload/${Date.now() + Math.floor(Math.random() * 100000).toString() + '.' + file.filename.split('.').pop()}`;
  const reader = fs.createReadStream(file.filepath); // 创立可读流
  const upStream = fs.createWriteStream(filePath); // 创立可写流
  reader.pipe(upStream); // 可读流通过管道写入可写流
  data.push({url: filePath.slice(1)
  });
}

我这里是存储到了 server 目录的/public/upload/,这个目录须要做一下动态文件的配置:

config.static = {
  prefix: '/public/',
  dir: path.join(appInfo.baseDir, 'public')
};

passport

这个章节的 egg 官网文档,要你的命,例子啥也没有,肯定要去看源码,太坑人了,我钻研了很久才弄明确是怎么回事。

因为我想更自在的管制账户明码登录,所以账号密码登录并没有应用 passport,应用的就是一般的接口认证配合 session。

上面具体说下应用第三方平台(我选用的是 GitHub)登录的过程:

  1. 在 GitHub OAuth Apps 新建你的利用,获取 key 和 secret
  2. 在我的项目装置 egg-passport 和 egg-passport-github

开启插件:

// config/plugin.js
module.exports.passport = {
  enable: true,
  package: 'egg-passport',
};

module.exports.passportGithub = {
  enable: true,
  package: 'egg-passport-github',
};
  1. 配置:
// config.default.js
config.passportGithub = {
  key: 'your_clientID',
  secret: 'your_clientSecret',
  callbackURL: 'http://localhost:3000/api/v1/passport/github/callback' // 留神这里十分的要害,这里须要和你在 github 下面设置的 Authorization callback URL 统一
};
  1. 在 app.js 中开启 passport
this.app.passport.verify(verify);
  1. 须要设置两个 passport 的 get 申请路由,第一个是咱们在 login 页面点击的申请,第二个是咱们在上一步设置的 callbackURL,这里次要是第三方平台会给咱们一个可用的 code,而后依据 OAuth2 受权规定去获取用户的详细信息
const github = app.passport.authenticate('github', { successRedirect: '/'}); // successRedirect 就是最初校验结束后前端会跳转的路由,我这里间接跳转到主页了
router.get('/v1/passport/github', github);
router.get('/v1/passport/github/callback', github);
  1. 这时候在前端点击 /v1/passport/github 会发动 github 对这个利用的受权,胜利后 github 会 302 到http://localhost:3000/v1/passport/github/callback?code=12313123123,咱们的 githubPassport 插件会去获取用户在 github 上的信息,获取到详细信息后,咱们须要在 app/passport/verify.js 去验证用户信息,并且和咱们本身平台的用户信息做关联,也要给 session 赋值
// verify.js
module.exports = async (ctx, githubUser) => {const { service} = ctx;
  const {provider, name, photo, displayName} = githubUser;
  ctx.logger.info('githubUser', { provider, name, photo, displayName});

  let user = await ctx.model.User.findOne({
    where: {username: name}
  });

  if (!user) {
    user = await ctx.model.User.create({
      provider,
      username: name
    });
    const userInfo = await ctx.model.UserInfo.create({
      nickname: displayName,
      photo
    });
    const role = await ctx.model.Role.findOne({
      where: {keyName: 'user'}
    });
    user.setUserInfo(userInfo);
    user.addRole(role);
    await user.save();}
  const {rights, roles} = await service.user.getUserAttribute(user.id);

  // 权限判断
  if (!rights.some(item => item.keyName === 'login')) {
    ctx.body = {
      statusCode: '1',
      errorMessage: '不具备登录权限'
    };
    return;
  }

  ctx.session.user = {
    id: user.id,
    roles,
    rights
  };

  return githubUser;
};

留神看下面的代码,如果是首次受权将会创立这个用户,如果是第二次受权,那么用户曾经被创立了

初始化

零碎部署或者运行的时候,须要预设一些数据和表,代码在app.jsapp/service/startup.js

逻辑就是我的项目启动结束后,利用 model 同步表构造到数据库中,而后开始新建一些根底数据:

  1. 新建角色和权限,并给角色调配权限
  2. 新建不同用户,调配角色
  3. 给一些用户建设好友关系
  4. 增加申请
  5. 创立群组,并增加一些人

做完以上这些就算是实现了初始数据了,能够进行失常的运行

部署

我是在腾讯云买的服务器 centos,在阿里云买的域名,装了 node(12.18.2)、nginx 和 mysql8.0,间接在 centos 下面启动,前端应用 nginx 进行反向代理。因为服务器资源无限,没有应用一些自动化工具 Jenkins 和 Docker,这就导致了我在更新的时候得有一些手动操作。

退出移动版