Online Chat - Egg+Webpack+Socket.IO

Egg + Webpack + Socket.io Notes

See project PorYoung/allChat, an online chat web application based on egg.js and sockt.io.

Directory Structure

- app
  - controller
  - extend
  - middleware
  - model
  - service
  - public
    - dist  /* webpack output directory */
  - io  /* socket.io */
    - controller
    - middleware
  - view
  - router.js
- config
  - config.default.js
  - plugin.js
- build /* webpack */    
  - src
  - webpack.config.js
- ...

Egg

Quick Usage

npm i egg-init -g
egg-init egg-example --type=simple
cd egg-example
npm i
npm run dev

The server listens on 7001.

See egg for more detail.

Config

config/config.default.js default content

'use strict';
const path = require('path');
module.exports = appInfo => {
  const config = exports = {};

  // use for cookie sign key, should change to your own and keep security
  config.keys = appInfo.name + 'your_keys';

  // add your config here

  return config;
}

middleware

// add your middleware
config.middleware = ['permission','middleware2'];
// your middleware config, which will be the param 'options' you can access later
config.permission = {
  excludeUrl: {
    'ALL': ['/', '/login', '/register'],
    'POST': [],
    'GET': ['/register/checkUserid'],
  },
}

plugin

  • ejs
// egg-view-view plugin
config.view = {
  mapping: {
    '.html': 'ejs',
  },
  defaultViewEngine: 'ejs'
};
  • mongoose
// egg-mongoose plugin, [What is egg-mongoose](https://www.npmjs.com/package/egg-mongoose)
config.mongoose = {
  client: {
    url: 'mongodb://127.0.0.1/chat',
    options: {},
  },
};
  • egg security and session
// egg security solutions, see [egg Security](https://eggjs.org/en/core/security.html) for detail
// you have to send csrftoken before your request
config.security = {
  csrf: {
    headerName: 'x-csrf-token', // 通过 header 传递 CSRF token 的默认字段为 x-csrf-token
  },
};

config.session = {
  key: 'EGG_SESS',
  maxAge: 24 * 3600 * 1000, // 1 天
  httpOnly: true,
  encrypt: true,
  renew: true,
};

use csrfAjax.js to bind beforeSend event to ajax.

import Cookies from 'js-cookie'
const csrftoken = Cookies.get('csrfToken');

function csrfSafeMethod(method) {
  // these HTTP methods do not require CSRF protection
  return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
$.ajaxSetup({
  beforeSend: function (xhr, settings) {
    if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
      xhr.setRequestHeader('x-csrf-token', csrftoken);
    }
  },
});
  • socket.io
// see [egg Socket.IO](https://eggjs.org/en/tutorials/socketio.html) for detail
config.io = {
  namespace: {
    '/allChat': {
      connectionMiddleware: ['auth'],
      packetMiddleware: [],
    }
  }
};
  • global config
// define global configuration and variabels yourself
config.appConfig = {
  defaultAvatarArr: ['/public/image/default_avatar.jpg', '/public/image/default_avatar_1.jpg', '/public/image/default_avatar_2.jpg',],
  imagePublicPath: '/public/image',
  defaultChatRoom: 'default',
  defaultChatRoomMax: 999,
  messageSplitLimit: 8,
  allowReconnectionTime: 10 * 1000,
};

Upload File and Form

use Formidable in egg

// app/controller/xxx.js
const formidable = require('formidable');
const path = require('path');

// It's ok use formidable to wait the `form` end event to send response, but wrong in egg.
// You have to return a promise.

// handle function
async formParse(req, filename, config) {
  const form = new formidable.IncomingForm();
  return new Promise((resolve, reject) => {
    form.uploadDir = path.join(process.cwd(), 'app', config.appConfig.imagePublicPath);
    form.parse(req);
    form.on('fileBegin', (name, file) => {
      file.name = filename
      file.path = path.join(process.cwd(), 'app', config.appConfig.imagePublicPath, filename)
    })
    form.on('end', () => {
      resolve(path.join(config.appConfig.imagePublicPath, filename));
    })
    form.on('error', (err) => {
      reject('-1');
    })
  });
}

// usage
// const result = await this.formParse(ctx.req, filename, config);

mongoose

use mongoose in egg

// take login for example

// app/model/user.js
module.exports = app => {
const mongoose = app.mongoose;
const Schema = mongoose.Schema;

const UserSchema = new Schema({
  _id: {
    type: String,
    unique: true,
    // mongoose aliases
    alias: 'userid',
  },
  username: String,
  password: String,
};
};

// app/service/user.js
const Service = require('egg').Service;
class UserService extends Service {
  async findOneByUserid(userid) {
    let docs = await this.ctx.model.User.findOne({
      _id: userid,
    });
    if (docs) {
      // mongoose virtuals
      return docs.toObject({
        virtuals: true
      });
    }
    return docs;
  }
}

// app/controller/user.js
const Controller = require('egg').Controller;
class UserController extends Controller {
  async login() {
    const {
      ctx,
      service
    } = this;
    let {
      userid,
      password,
      rememberMe
    } = ctx.request.body;
    let userinfo = await service.user.findOneByUserid(userid);
    if (userinfo && userinfo.password == password) {
      ctx.session.user = Object.assign(userinfo, {
        ipAddress: ctx.request.ip
      });
      if (rememberMe) ctx.session.maxAge = ms('30d');
      return ctx.body = '1';
    }
    return ctx.body = '-1';
  }
}
module.exports = UserController;

$or, $and

model.find({
  $or:[{
    criterion_1: 1
  },{
    $and: [{
      criterion_2: 2
    },{
      criterion_3: 3
    }]
  }]
});

alias and virtuals

Aliaes and Virtuals for more detail.

const UserSchema = new Schema({
    _id: {
      type: String,
      unique: true,
      alias: 'userid',
    },
});
// you can use _id or userid in getter and setter
// assume `doc` is the query doc
console.log(doc.toObject({ virtuals: true })); // { _id: 'xxx', userid: 'xxx' }
console.log(doc.userid); // "xxx"

Populate

Populate for more detail.
You must set ref to _id, other fields are not avalible;

Note: ObjectId, Number, String, and Buffer are valid for use as refs. However, you should use ObjectId unless you are an advanced user and have a good reason for doing so.

// take `User` model and `Message` model for example,
// which user have more than one messages,
// `Message` have attributes that ref to `_id` alias `userid` here.

// app/model/message.js
module.exports = app => {
  const mongoose = app.mongoose;
  const Schema = mongoose.Schema;

  const MessageSchema = new Schema({
    from: {
      type: String,
      ref: 'User',
    },
    to: {
      type: String,
      ref: 'User',
    },
  });
  return mongoose.model('Message', MessageSchema);

  // now you can access user infomation while query message
  // Message.find(citerion).populate('from','userid username').populate('to','userid username');
}

Find by pagination

// in app/service/message.js
async findByPagination(criterion, limit, page) {
  const total = await this.ctx.model.Message.count(criterion);
  const totalPageNum = parseInt(total / limit);
  if (page > totalPageNum) {
    return null;
  }
  const start = limit * page;
  const queryArr = await this.ctx.model.Message
    .where(criterion)
    // sort by date desc
    .sort({
      date: -1
    })
    .limit(limit)
    .skip(start)
    .populate('from','userid username avatar')
    .populate('to','userid username avatar');
  let resArr = [];
  queryArr.forEach((doc)=>{
    resArr.push(doc.toObject({virtuals: true}));
  });
  return resArr;
}

Helper

get remote IP

// ctx.request.ip

// use socket.io to get ip address
// socket.handshake.address
// maybe you need to parse the IP address
parseIPAddress(ip) {
  if (ip.indexOf('::ffff:') !== -1) {
    ip = ip.substring(7);
  }
  return ip;
}

Socket.IO

use socket.io in egg

  1. controller
    extends app.controller
  2. middleware
// app/io/middleware/auth.js
module.exports = () => {
  return async (ctx, next) => {
    const { app, socket, logger, helper, service } = ctx;
    const sid = socket.id;
    const nsp = app.io.of('/allChat');
    const query = socket.handshake.query;
  }
}

get socketid

// by the socket object, get the connector id
const sid = socket.id;

// get online user socketid in room by the namespace adapter, except current connector id
nsp.adapter.clients(rooms, (err, clients) => {
  //clients is an socketid arrs
  logger.info('#online', clients);
}

disconnect or refresh event

When user refresh current page in browser, it will trigger disconnection and leave event, then the join event. Try to use a timer(setTimeout function) to solve this problem, but it might not be a good solution.

See auth.js.

send message to users in room

socket.to(room).emit('room_message', message);

send private message to user

// sender
let userinfo = await service.user.findOneByUserid(ctx.session.user.userid);
message.from = {
  userid: userinfo.userid,
  username: userinfo.username,
};
// receiver
let toUserinfo = await service.user.findOneByUserid(message.to);
if (!toUserinfo) {
  socket.emit(socket.id, helper.parseMsg('warning', {
    type: 'warning',
    content: '该用户不见了=_=!',
  }));
} else {
  message.to = {
    userid: toUserinfo.userid,
    username: toUserinfo.username,
    socketid: toUserinfo.socketid,
  };
  let messageParsed = helper.parseMsg('private_message', message);
  socket.to(message.to.socketid).emit(message.to.socketid, messageParsed);
}

use socket.io in front end

instead of

你可能感兴趣的:(notes,NodeJS)