Online Chat-Egg+Webpack+Sockett.IO
Egg+Webpack+Socket.io Notes
See project PorYoung/all Chat、an online chat web appplication based on egg.js and sockt.io.
Directory Structure
Quick Usage
See egg for more detail.
Config ejs モングース egg security and session sockete.io global config
use
use
Poputlate for more detail.You must set
Note:ObjectId,Number,String,and Buffer are valid for use.However,you shound use Object Id unless you are and have a good reason for dong so.
get remote IP
use sockete.io in egg controller extens middleware
When user refresh current page in browser,it will trigger disconnect and leave event,the n the join event.Try to use a
See auth.js.
send message to users in room
instead of the get room the current connector jined、 Webpack 4.0
you can use
see webpack.co.nfig.js.
spilt Chunks
トsupport webpack 4.0,install
タッチイベント
See project PorYoung/all Chat、an online chat web appplication 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
- ...
EggQuick 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// egg-view-view plugin
config.view = {
mapping: {
'.html': 'ejs',
},
defaultViewEngine: 'ejs'
};
// 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 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 before Send 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);
}
},
});
// see [egg Socket.IO](https://eggjs.org/en/tutorials/socketio.html) for detail
config.io = {
namespace: {
'/allChat': {
connectionMiddleware: ['auth'],
packetMiddleware: [],
}
}
};
// 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 Formuse
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);
モングース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 Virtual 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"
ポプラテPoputlate for more detail.You must set
ref
to _id
、other fields are not avalible;Note:ObjectId,Number,String,and Buffer are valid for use.However,you shound use Object Id unless you are and have a good reason for dong 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;
}
ヘルパー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;
}
Sockett.IOuse sockete.io in egg
app.controller
// 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イベントWhen user refresh current page in browser,it will trigger disconnect and leave event,the n the join event.Try to use a
timer
(setTimeout function)to solive is proble,but might not be good solution.See auth.js.
send message to users in room
socket.to(room).emit('room_message', message);
send prvate 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 sockete.io in front endinstead of
const socketClient = require('socket.io-client');
const allChat = socketClient(config.host.concat('/allChat'), {
query: {
room: config.socket.room,
},
transports: ['websocket'],
});
allChat.on("connect", () => {
const sid = allChat.id;
console.log('#connected', sid, allChat);
// id p2p
allChat.on(sid, msg => {
console.log('#receive,', msg);
switch (msg.data.action) {
case 'deny':
case 'welcome':
case 'warning':
case 'private_message':
}
});
});
//
allChat.on('disconnect', msg => {
console.log('#disconnect', msg);
});
allChat.on('disconnecting', () => {
console.log('#disconnecting');
});
allChat.on('error', () => {
console.log('#error');
});
Tipssocktid
get from Namespace.adapter.clients
contain the room #room
at the head.Object.keys(socket.rooms)[0];
you can use
egg-webpack
in egg or use webpack-cli
.see webpack.co.nfig.js.
spilt Chunks
optimization: {
splitChunks: {
cacheGroups: {
common: {
name: "common",
chunks: "all",
minSize: 1,
priority: 0,
minChunks: 2,
},
vendors: {
name: "vendors",
test: /[\\/]node_modules[\\/]/,
chunks: "all",
priority: 10
}
}
},
},
extract-text-webpack-pluginトsupport webpack 4.0,install
extract-text-webpack-plugin@next
//module rules
{
test: /\.css/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader']
}),
},
{
test: /\.(less)/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [{
loader: 'css-loader',
options: {
alias: {
'@': path.resolve(staticPath, 'image') // '~@/logo.png' , src/img/logo.png
}
},
},
'postcss-loader',
{
loader: 'less-loader',
options: {
lessPlugins: [
new LessPluginCleanCSS({
advanced: true,
}),
],
},
}
]
}),
},
new ExtractTextPlugin({
filename: 'css/[name].bundle-[hash].css',
}),
import jquerynew webpack.ProvidePlugin({
"$": "jquery",
"jQuery": "jquery",
"window.jQuery": "jquery",
// underscore
"_": 'underscore'
}),
Utilsタッチイベント
var myTouchEvent = function () {
var swip_time = 300,
swip_dis = 30,
point_start,
point_end,
time_start,
time_end,
//1 2 3 4
result;
if ("ontouchstart" in window) {
var startEvt = "touchstart",
moveEvt = "touchmove",
endEvt = "touchend";
} else {
var startEvt = "mousedown",
moveEvt = "mousemove",
endEvt = "mouseup";
}
var getPos = function (e) {
var touches = e.touches;
if (touches && touches[0]) {
return {
x: touches[0].clientX,
y: touches[0].clientY
};
}
return {
x: e.clientX,
y: e.clientY
};
}
var getDistance = function (p1, p2) {
return parseInt(Math.sqrt(Math.pow(Math.abs(p1.x - p2.x), 2) + Math.pow(Math.abs(p1.y - p2.y), 2)));
}
var getDirection = function (p1, p2) {
var angle = Math.atan2(p1.y - p2.y, p2.x - p1.x) * 180 / Math.PI;
if (angle <= 45 && angle >= -45) return "right";
if (angle >= 45 && angle <= 135) return "up";
if (angle >= 135 || angle <= -135) return "left";
if (angle <= -45 && angle >= -135) return "down";
}
var startEvtHandle = function (e) {
var pos = getPos(e);
var touches = e.touches;
if (!touches || touches.length == 1) {
point_start = getPos(e);
time_start = new Date().getTime();
}
//
$("#notification").css({
height: 0,
overflow: "hidden"
}).html(" ").show();
point_end = pos;
}
var transformYPre = 0;
var moveEvtHandle = function (e) {
point_end = getPos(e);
var y = point_end.y - point_start.y;
if (y > 0 && y > 80) {
y = 80;
} else if (y < 0) {
y = 0;
}
transformYPre += y - transformYPre;
$("#listPanel").css({
transition: "all 1s",
transform: "translate3d(0," + transformYPre + "px,0)"
})
$("#notification").css({
transition: "all 1s",
height: transformYPre + "px",
lineHeight: transformYPre + "px"
})
e.preventDefault();
}
var endEvtHandle = function (e) {
time_end = new Date().getTime();
var dis = getDistance(point_start, point_end);
var time = time_end - time_start;
//
if (dis >= swip_dis && time >= swip_time) {
var dir = getDirection(point_start, point_end),
disY = point_end.y - point_start.y,
disX = point_end.x - point_start.x;
if (disY >= 80 && dir == "down") {
result = 3;
//
loadMessage(++page);
console.log(' ');
// 30s
var timer = setInterval(function () {
if (loadMessageFlag) {
$("#listPanel").css({
transition: "all 1s",
transform: "translate3d(0,0,0)"
})
//
if (loadMessageFlag == 1) $("#notification").html("Success");
else if (loadMessageFlag == 2) $("#notification").html(" =_=");
loadMessageFlag = 0;
setTimeout(function () {
$("#notification").css({
height: "30px",
lineHeight: "30px"
}).html("").hide();
clearInterval(timer);
}, 300);
}
});
//30s
setTimeout(function () {
clearInterval(timer);
//
$("#notification").html("Failed");
loadMessageFlag = false;
setTimeout(function () {
$("#notification").css({
height: "30px",
lineHeight: "30px"
}).html("").hide();
}, 300);
}, 31000);
} else if (disX >= 80 && dir == "right") {
result = 2;
} else if (disX < -30 && dir == "left") {
result = 4;
} else if (disY < -30 && dir == "up") {
$("#listPanel").scrollTop(parseInt(Math.abs(point_end.y - point_start.y)));
result = 1;
}
} else {
$("#listPanel").css({
transition: "all 1s",
transform: "translate3d(0,0,0)"
}).animate({
scrollTop: '30px'
}, 300);
$("#notification").css({
height: "30px",
lineHeight: "30px"
}).html("").hide();
}
}
$("#listPanel").on(startEvt, function (e) {
if ($(this).scrollTop() <= 0) {
startEvtHandle(e);
$(this).on(moveEvt, moveEvtHandle);
$(this).on(endEvt, function (e) {
endEvtHandle(e);
$(this).off(moveEvt).off(endEvt);
});
}
})
}
Sroll To Bottomconst scrollToBottom = () => {
let scrollHeight = $("#listPanel")[0].scrollHeight - $("#listPanel")[0].clientHeight;
$("#listPanel").animate({
scrollTop: scrollHeight
}, 300);
};