views目錄
base.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="description" content="learn javascript by www.liaoxuefeng.com">
<title>{{ title }}</title>
<link rel="stylesheet" href="/static/css/bootstrap.css">
<script src="/static/js/jquery.min.js"></script>
<script src="/static/js/vue.min.js"></script>
<script src="/static/js/bootstrap.js"></script>
</head>
<body>
<header class="navbar navbar-static-top">
<div class="container">
<div class="navbar-header">
<a href="/" class="navbar-brand">Learn JavaScript</a>
</div>
<nav class="collapse navbar-collapse" id="bs-navbar">
<ul class="nav navbar-nav">
<li><a target="_blank" >Get Courses</a></li>
<li><a target="_blank" >Source Code</a></li>
<li><a target="_blank" >Resource</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li><a href="/signout">Sign Out</a></li>
</ul>
</nav>
</div>
</header>
<div id="important" style="color:#cdbfe3; background-color:#6f5499; padding:30px 0; margin:-20px 0 20px 0;">
<div class="container">
<h1 style="color:#fff; font-size:60px">Getting started with WebSocket!</h1>
<p style="font-size:24px; line-height:48px">Learn JavaScript, Node.js, WebSocket, npm, koa2, nunjucks, babel, etc. at liaoxuefeng.com.</p>
</div>
</div>
{% block main %} {% endblock %}
<footer style="background-color:#ddd; padding: 20px 0;">
<div class="container">
<p>
<a target="_blank" >Website</a> -
<a target="_blank" >GitHub</a> -
<a target="_blank" >Weibo</a>
</p>
<p>This JavaScript course is created by <a target="_blank" >@廖雪峰</a>.</p>
<p>Code licensed <a target="_blank" >Apache</a>.</p>
</div>
</footer>
</body>
</html>
signin.html
{% extends "base.html" %} {% block main %}
<div class="container">
<div class="row">
<div class="col-md-12">
<h1>WebSocket Chat</h1>
<div class="alert alert-danger">Please sign in before join the room.
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><span class="glyphicon glyphicon-user"></span> Please sign in</h3>
</div>
<div class="panel-body">
<form action="/signin" method="post">
<div class="form-group">
<label>Name</label>
<input type="text" name="name" class="form-control" placeholder="你的昵稱" value="{{ name }}">
<p class="help-block">Your user name</p>
</div>
<button type="submit" class="btn btn-primary">Sign In</button>
</form>
</div>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><span class="glyphicon glyphicon-hd-video"></span> Video training</h3>
</div>
<div class="panel-body">
<video width="100%" controls="controls">
<source src="https://github.com/michaelliao/learn-javascript/raw/master/video/vscode-nodejs.mp4">
</video>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
room.html
{% extends "base.html" %} {% block main %}
<script>
function addToUserList(list, user) {
var i;
for (i=0; i<list.length; i++) {
if (list[i].id === user.id) {
return;
}
}
list.push(user);
}
function removeFromUserList(list, user) {
var i, target = -1;
for (i=0; i<list.length; i++) {
if (list[i].id === user.id) {
target = i;
break;
}
}
if (target >= 0) {
list.splice(target, 1);
}
}
function addMessage(list, msg) {
list.push(msg);
$('#message-list').parent().animate({
scrollTop: $('#message-list').height()
}, 1000);
}
$(function () {
var vmMessageList = new Vue({
el: '#message-list',
data: {
messages: []
}
});
var vmUserList = new Vue({
el: '#user-list',
data: {
users: []
}
});
var ws = new WebSocket('ws://localhost:3000/ws/chat');
ws.onmessage = function(event) {
var data = event.data;
console.log(data);
var msg = JSON.parse(data);
if (msg.type === 'list') {
vmUserList.users = msg.data;
} else if (msg.type === 'join') {
addToUserList(vmUserList.users, msg.user);
addMessage(vmMessageList.messages, msg);
} else if (msg.type === 'left') {
removeFromUserList(vmUserList.users, msg.user);
addMessage(vmMessageList.messages, msg);
} else if (msg.type === 'chat') {
addMessage(vmMessageList.messages, msg);
}
};
ws.onclose = function (evt) {
console.log('[closed] ' + evt.code);
var input = $('#form-chat').find('input[type=text]');
input.attr('placeholder', 'WebSocket disconnected.');
input.attr('disabled', 'disabled');
$('#form-chat').find('button').attr('disabled', 'disabled');
};
ws.onerror = function (code, msg) {
console.log('[ERROR] ' + code + ': ' + msg);
};
$('#form-chat').submit(function (e) {
e.preventDefault();
var input = $(this).find('input[type=text]');
var text = input.val().trim();
console.log('[chat] ' + text);
if (text) {
input.val('');
ws.send(text);
}
});
});
</script>
<div class="container">
<div class="row">
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><span class="glyphicon glyphicon-th-list"></span> Room</h3>
</div>
<div class="panel-body">
<div style="height:400px; overflow-x:hidden; overflow-y:scroll;">
<div id="message-list">
<div style="margin-bottom:25px;" v-for="msg in messages">
<div v-if="msg.type === 'join' || msg.type === 'left'">
<div class="media-left">

</div>
<div class="media-body">
<h4 class="media-heading" v-text="msg.data"></h4>
</div>
</div>
<div v-if="msg.type === 'chat'">
<div class="media">
<div class="media-left">

</div>
<div class="media-body">
<h4 class="media-heading" v-text="msg.user.name"></h4>
<span v-text="msg.data"></span>
</div>
</div>
</div>
</div>
</div>
</div>
<div>
<form id="form-chat" action="#0">
<div class="input-group">
<input type="text" class="form-control" placeholder="A good day, isn't it?">
<span class="input-group-btn"><button class="btn btn-default" type="submit">Go</button></span>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><span class="glyphicon glyphicon-user"></span> Users</h3>
</div>
<div class="panel-body">
<div style="height:434px; overflow-x:hidden; overflow-y:scroll;">
<div id="user-list">
<div class="media" v-for="user in users">
<div class="media-left">

</div>
<div class="media-body">
<h4 class="media-heading" v-text="user.name + ' (' + user.id + ')'"></h4>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h1>Get more courses...</h1>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">JavaScript</h3>
</div>
<div class="panel-body">
<p>full-stack JavaScript course</p>
<p><a target="_blank" >Read more</a></p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Python</h3>
</div>
<div class="panel-body">
<p>the latest Python course</p>
<p><a target="_blank" >Read more</a></p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">git</h3>
</div>
<div class="panel-body">
<p>A course about git version control</p>
<p><a target="_blank" >Read more</a></p>
</div>
</div>
</div>
</div>
</div>
{% 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;