http://www.liaoxuefeng.com/wiki/001434446689867b27157e896e74d51a89c25cc8b43bdb3000/001472794708264206fcf1589bb43caa0395752aa26538c000
views目录
base.html
{{ title }}
Getting started with WebSocket!
Learn JavaScript, Node.js, WebSocket, npm, koa2, nunjucks, babel, etc. at liaoxuefeng.com.
{% block main %} {% endblock %}
signin.html
{% extends "base.html" %} {% block main %}
WebSocket Chat
Please sign in before join the room.
Please sign in
Video training
{% endblock %}
room.html
{% extends "base.html" %} {% block main %}
{% endblock %}
controllers目录
index.js
// index:
module.exports = {
'GET /': async (ctx, next) => {
let user = ctx.state.user;
if (user) {
ctx.render('room.html', {
user: user
});
} else {
ctx.response.redirect('/signin');
}
}
};
signin.js
// sign in:
var index = 0;
module.exports = {
'GET /signin': async (ctx, next) => {
let names = '甲乙丙丁戊己庚辛壬癸';
let name = names[index % 10];
ctx.render('signin.html', {
name: `路人${name}`
});
},
'POST /signin': async (ctx, next) => {
index ++;
let name = ctx.request.body.name || '路人甲';
let user = {
id: index,
name: name,
image: index % 10
};
let value = Buffer.from(JSON.stringify(user)).toString('base64');
console.log(`Set cookie value: ${value}`);
ctx.cookies.set('name', value);
ctx.response.redirect('/');
},
'GET /signout': async (ctx, next) => {
ctx.cookies.set('name', '');
ctx.response.redirect('/signin');
}
};
app.js
const url = require('url');
const ws = require('ws');
const Cookies = require('cookies');
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const controller = require('./controller');
const templating = require('./templating');
const WebSocketServer = ws.Server;
const app = new Koa();
// log request URL:
app.use(async (ctx, next) => {
console.log(`Process ${ctx.request.method} ${ctx.request.url}...`);
await next();
});
// parse user from cookie:
app.use(async (ctx, next) => {
ctx.state.user = parseUser(ctx.cookies.get('name') || '');
await next();
});
// static file support:
let staticFiles = require('./static-files');
app.use(staticFiles('/static/', __dirname + '/static'));
// parse request body:
app.use(bodyParser());
// add nunjucks as view:
app.use(templating('views', {
noCache: true,
watch: true
}));
// add controller middleware:
app.use(controller());
let server = app.listen(3000);
function parseUser(obj) {
if (!obj) {
return;
}
console.log('try parse: ' + obj);
let s = '';
if (typeof obj === 'string') {
s = obj;
} else if (obj.headers) {
let cookies = new Cookies(obj, null);
s = cookies.get('name');
}
if (s) {
try {
let user = JSON.parse(Buffer.from(s, 'base64').toString());
console.log(`User: ${user.name}, ID: ${user.id}`);
return user;
} catch (e) {
// ignore
}
}
}
function createWebSocketServer(server, onConnection, onMessage, onClose, onError) {
let wss = new WebSocketServer({
server: server
});
wss.broadcast = function broadcast(data) {
wss.clients.forEach(function each(client) {
client.send(data);
});
};
onConnection = onConnection || function () {
console.log('[WebSocket] connected.');
};
onMessage = onMessage || function (msg) {
console.log('[WebSocket] message received: ' + msg);
};
onClose = onClose || function (code, message) {
console.log(`[WebSocket] closed: ${code} - ${message}`);
};
onError = onError || function (err) {
console.log('[WebSocket] error: ' + err);
};
wss.on('connection', function (ws) {
let location = url.parse(ws.upgradeReq.url, true);
console.log('[WebSocketServer] connection: ' + location.href);
ws.on('message', onMessage);
ws.on('close', onClose);
ws.on('error', onError);
if (location.pathname !== '/ws/chat') {
// close ws:
ws.close(4000, 'Invalid URL');
}
// check user:
let user = parseUser(ws.upgradeReq);
if (!user) {
ws.close(4001, 'Invalid user');
}
ws.user = user;
ws.wss = wss;
onConnection.apply(ws);
});
console.log('WebSocketServer was attached.');
return wss;
}
var messageIndex = 0;
function createMessage(type, user, data) {
messageIndex ++;
return JSON.stringify({
id: messageIndex,
type: type,
user: user,
data: data
});
}
function onConnect() {
let user = this.user;
let msg = createMessage('join', user, `${user.name} joined.`);
this.wss.broadcast(msg);
// build user list:
let users = this.wss.clients.map(function (client) {
return client.user;
});
this.send(createMessage('list', user, users));
}
function onMessage(message) {
console.log(message);
if (message && message.trim()) {
let msg = createMessage('chat', this.user, message.trim());
this.wss.broadcast(msg);
}
}
function onClose() {
let user = this.user;
let msg = createMessage('left', user, `${user.name} is left.`);
this.wss.broadcast(msg);
}
app.wss = createWebSocketServer(server, onConnect, onMessage, onClose);
console.log('app started at port 3000...');
controller.js
const fs = require('fs');
// add url-route in /controllers:
function addMapping(router, mapping) {
for (var url in mapping) {
if (url.startsWith('GET ')) {
var path = url.substring(4);
router.get(path, mapping[url]);
console.log(`register URL mapping: GET ${path}`);
} else if (url.startsWith('POST ')) {
var path = url.substring(5);
router.post(path, mapping[url]);
console.log(`register URL mapping: POST ${path}`);
} else if (url.startsWith('PUT ')) {
var path = url.substring(4);
router.put(path, mapping[url]);
console.log(`register URL mapping: PUT ${path}`);
} else if (url.startsWith('DELETE ')) {
var path = url.substring(7);
router.del(path, mapping[url]);
console.log(`register URL mapping: DELETE ${path}`);
} else {
console.log(`invalid URL: ${url}`);
}
}
}
function addControllers(router, dir) {
fs.readdirSync(__dirname + '/' + dir).filter((f) => {
return f.endsWith('.js');
}).forEach((f) => {
console.log(`process controller: ${f}...`);
let mapping = require(__dirname + '/' + dir + '/' + f);
addMapping(router, mapping);
});
}
module.exports = function (dir) {
let
controllers_dir = dir || 'controllers',
router = require('koa-router')();
addControllers(router, controllers_dir);
return router.routes();
};
static-files.js
const path = require('path');
const mime = require('mime');
const fs = require('mz/fs');
function staticFiles(url, dir) {
return async (ctx, next) => {
let rpath = ctx.request.path;
if (rpath.startsWith(url)) {
let fp = path.join(dir, rpath.substring(url.length));
if (await fs.exists(fp)) {
ctx.response.type = mime.lookup(rpath);
ctx.response.body = await fs.readFile(fp);
} else {
ctx.response.status = 404;
}
} else {
await next();
}
};
}
module.exports = staticFiles;
templating.js
const nunjucks = require('nunjucks');
function createEnv(path, opts) {
var
autoescape = opts.autoescape === undefined ? true : opts.autoescape,
noCache = opts.noCache || false,
watch = opts.watch || false,
throwOnUndefined = opts.throwOnUndefined || false,
env = new nunjucks.Environment(
new nunjucks.FileSystemLoader(path, {
noCache: noCache,
watch: watch,
}), {
autoescape: autoescape,
throwOnUndefined: throwOnUndefined
});
if (opts.filters) {
for (var f in opts.filters) {
env.addFilter(f, opts.filters[f]);
}
}
return env;
}
function templating(path, opts) {
var env = createEnv(path, opts);
return async (ctx, next) => {
ctx.render = function (view, model) {
ctx.response.body = env.render(view, Object.assign({}, ctx.state || {}, model || {}));
ctx.response.type = 'text/html';
};
await next();
};
}
module.exports = templating;