之前有机会做技术分享,但怕内容枯燥乏味,于是就想在PPT上做一些花样,就有了下面的一些想法。
- 作为程序员,尤其是前端开发,自然不能用传统的PPT,网页PPT是不是更花哨一点,比如 slidev 和 reveal.js,考虑简便选了后者
- 弄一些特效什么的,各种滑动动画,背景渐变等,这些作为前端开发相信不难,比如弄上粒子背景动画,这里有粒子库particles.js
- 最重要的是讲完一部分,最好弄个抽奖什么的,和年会一样;于是就想到自己有几本前端开发的闲书,自己看过没有啥用了,索性送给大家
前面两个比较好弄,都有现成的模板,官方也有例子,很快就弄好了内容
现状和思考
下面重点想怎么弄现场抽奖,现状考虑:
- 参会的人很多不认识,也不确定是哪些,所以无法预先把名字确定
- 有远程通过直播听的,也要考虑在内
- 现在一般都有手机,能否弄个现场扫码,填姓名参与抽奖?
附带一些问题:
- 现场人可能乱填,需要有删除功能,或者批量清空
- 如果有人没有带手机,可以由主持手动添加
- 全部人都填完了,核验完再开始,需要开始和停止按钮
- 允许多次抽奖,需要重置按钮
粗略的做了一下大概就是上面的界面了,其中遇见几个重点问题和解决:
服务端
reveal.js仅提供PPT的一些效果和内容展示,都是静态的,所以需要起一个本地的server来支持扫码请求的数据收集
最简单的就是用Koa了,丰富的插件和中间件,这里只需要一个能支持静态文件目录的koa-static-resolver中间件,外加一个路由管理@koa/router,方便处理请求即可
const Koa = require('koa');
const KSR = require('koa-static-resolver');
const KoaRouter = require('@koa/router');
const app = new Koa();
app.use(KSR({
dirs: ['./']
}));
const router = new KoaRouter();
router.all('/:action', (ctx) => {
//...
});
app.use(router.routes()).use(router.allowedMethods());
二维码生成
浏览器端有现成的库qrcode直接通过canvas生成二维码
const $qrcodeCanvas = document.querySelector('.lucky-qrcode-canvas');
window.QRCode.toCanvas($qrcodeCanvas, url, {
width: 300
}, function(error) {
if (error) {
console.error(error);
}
console.log('qrcode success!');
});
现在大多主流手机app都有扫码功能,然后二维码内容指向一个url自动跳转即可;但现场扫码,跳转的url有这些问题:
- 如果url能提供公网域名的,那肯定没问题,但成本太高,为了一次分享弄个公网域名的url麻烦
- 前面不是启动了本地server,自然考虑直接连到演示的机器上,现场情况基本大家都是用的公司Wifi,也就是在一个局域网内,或者vpn的环境,也能连内网ip,所以需要把演示机器的内网IP给找到
const getInternalIp = () => {
const n = os.networkInterfaces();
//console.log(n);
const list = [];
for (const k in n) {
const inter = n[k];
for (const j in inter) {
const item = inter[j];
if (item.family === 'IPv4' && !item.internal) {
const a = item.address;
console.log(`Internal IP: ${a}`);
if (a.startsWith('192.') || a.startsWith('10.')) {
list.push(a);
}
}
}
}
return list.pop();
};
这里直接用的nodejs的os.networkInterfaces接口,过滤出来192.或10.开头的ip即可
输入页面
这里直接用bootstrap的样式,做了个最简单表单输入提交即可
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>抽奖</title>
<link href="../../node_modules/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body style="padding: 10%;">
<div class="mb-3">
<label for="exampleFormControlInput1" class="form-label">
<h5>姓名:</h5>
</label>
<input type="text" class="form-control name-value" id="exampleFormControlInput1" />
</div>
<div>
<button type="button" class="btn btn-primary name-submit">提交</button>
</div>
<div class="alert alert-success" style="margin-top: 20px;display: none;" role="alert">
添加成功
</div>
<div class="alert alert-danger" style="margin-top: 20px;display: none;" role="alert">
提交失败
</div>
<script>
const $name = document.querySelector('.name-value');
const $submit = document.querySelector('.name-submit');
const $danger = document.querySelector('.alert-danger');
const $success = document.querySelector('.alert-success');
$submit.addEventListener('click', function(e) {
$danger.style.display = 'none';
$success.style.display = 'none';
const v = $name.value;
if (!v) {
$name.focus();
return;
}
$name.setAttribute('disabled', 'disabled');
$submit.setAttribute('disabled', 'disabled');
console.log(v);
window.fetch(`/add?name=${v}`).then(function(response) {
return response.json();
}, function() {
$danger.style.display = 'block';
$name.removeAttribute('disabled');
$submit.removeAttribute('disabled');
}).then(function(data) {
console.log(data);
if (!data) {
return;
}
$success.style.display = 'block';
$submit.style.display = 'none';
});
});
</script>
</body>
</html>
名称管理和渲染
用户提交了名称,可以直接存内存,也不需要什么数据库,直接用一个自动去重的Set即可
const cacheList = new Set();
//增
cacheList.add(name);
//删
cacheList.delete(name);
//清空
cacheList.clear();
每次更新需要通知到浏览器端,可以使用websocket实时刷新,这里用的成熟的库socket.io,自带服务端和客户端,服务端也支持嵌入各种流行的web server框架,比如这里使用的Koa框架
const SocketIO = require('socket.io');
const app = new Koa();
const server = http.createServer(app.callback());
const socketIO = SocketIO(server, {
maxHttpBufferSize: 1e8
});
socketIO.on('connection', function(client) {
client.on('data', function(data) {
});
client.on('disconnect', function() {
});
//new user connected
});
const sockets = socketIO.sockets;
//发送名称列表
const nameList = Array.from(cacheList);
nameList.sort();
sockets.emit('data', {
action: 'list',
data: nameList
});
这样只要cacheList更新,通过sockets.emit方法就可以把最新数据发送到浏览器端,下面看浏览器端怎么接收:
- 首先是要在浏览器页面引入socket.io的客户端,在socket.io/client-dist目录下
<script src="node_modules/socket.io/client-dist/socket.io.min.js"></script>
- 然后初始化客户端接收器
const initSocket = function() {
const showLog = function(msg) {
console.log(`[socket] ${msg}`);
};
if (!window.io) {
showLog('not found io');
return;
}
const socket = window.io.connect('/');
lucky.socket = socket;
let server_connected = false;
let has_error = false;
let reconnect_times = 0;
const reload = function() {
socket.close();
window.location.reload();
};
socket.on('data', function(data) {
if (server_connected) {
if (data.action === 'list' && data.data) {
initList(data.data);
}
}
});
socket.on('connect', function(data) {
showLog('socket connected');
if (server_connected) {
if (has_error) {
reload();
}
}
server_connected = true;
has_error = false;
reconnect_times = 0;
});
socket.on('connect_error', function(data) {
showLog('socket connection error');
has_error = true;
});
socket.on('connect_timeout', function(data) {
showLog('socket connection timeout');
});
socket.on('reconnecting', function(data) {
reconnect_times += 1;
showLog(`socket reconnecting ... ${reconnect_times}`);
if (reconnect_times > 20) {
socket.close();
showLog(`socket closed after retry ${reconnect_times} times`);
}
});
socket.on('reconnect_error', function(data) {
showLog('socket reconnection error');
has_error = true;
});
socket.on('reconnect_failed', function(data) {
showLog('socket reconnection failed');
has_error = true;
});
};
如果顺利,服务端和浏览器就实时连接起来了,可以实时相互传输数据了
抽取动画和随机抽取结果
- 动画其实这里用了一个流行缓动类tween.js
<script src="node_modules/@tweenjs/tween.js/dist/tween.umd.js"></script>
一开始有考虑过three.js做成3D的,但时间有限,大家可以试试
- 然后就是随机抽取结果,图简单就是直接math random到索引,然后最终就是索引所在的人中奖,将动画最终定位到这里即可
const index = Math.floor(lucky.list.length * Math.random());
完整的代码
git clone lucky-turntable (github.com)