Vue+Koa2+mongoose寫(xiě)一個(gè)像素繪板的實(shí)現(xiàn)方法
前言
為什么是繪板:v2ex
作為一名前端,總會(huì)有意無(wú)意接觸到 NodeJS 、有意無(wú)意會(huì)去看文檔、有意無(wú)意會(huì)注意到框架,但真當(dāng)需要我們需要在工作中善用它時(shí),多半還是要感嘆一句“紙上得來(lái)終覺(jué)淺”。所以一周前我決定進(jìn)行一個(gè)實(shí)踐嘗試,希望能把以往無(wú)意中學(xué)到的知識(shí)融匯貫通,最終選擇把以前的一個(gè)畫(huà)板 Demo 重寫(xiě)并添加 server 端。
技術(shù)棧
- [vue + vuex + vue-router] 頁(yè)面渲染 + 數(shù)據(jù)共享 + 路由跳轉(zhuǎn)
- [axios] 以 Promise 的方式使用 HTTP 請(qǐng)求
- [stylus] CSS 預(yù)處理
- [element-ui] UI 庫(kù)
- [Webpack] 打包上面這些東西
- [koa 2 & koa-generator] NodeJS 框架和框架腳手架
- [mongodb & mongoose] 數(shù)據(jù)庫(kù)和操作數(shù)據(jù)庫(kù)的庫(kù)
- [node-canvas] 服務(wù)端數(shù)據(jù)副本記錄
- [Socket.io] 實(shí)時(shí)推送
- [pm2] Node 服務(wù)部署
- [nginx] 部署靜態(tài)資源訪問(wèn)服務(wù)(HTTPS),代理請(qǐng)求
- [letsencrypt] 生成免費(fèi)的 HTTPS 證書(shū)
Webpack 之所以也被列出來(lái),是因?yàn)楸卷?xiàng)目作為項(xiàng)目 luwuer.com 的一個(gè)模塊,需要 webpack 來(lái)實(shí)現(xiàn)獨(dú)立打包
node-canvas
安裝
node-canvas 是我目前遇到過(guò)最難安裝的依賴,以至于我根本不想在 Windows 下安裝他,它的功能依賴很多系統(tǒng)下默認(rèn)不存在的包,在 Github 上也能看到很多 issue 的標(biāo)簽是 installation help。以 CentOS 7 純凈版為例,在安裝它之前你需要安裝以下這些依賴,值得注意的是 npm 文檔上提供的命令沒(méi)有 cairo 。
# centos 前置條件 sudo yum install gcc-c++ cairo cairo-devel pango-devel libjpeg-turbo-devel giflib-devel # 安裝本體 yarn add canvas -D
還有一個(gè)不明所以的坑,如果前置條件準(zhǔn)備就緒后,安裝本體仍然一直卡取包這一步(不報(bào)錯(cuò)),此時(shí)需要單獨(dú)更新一下 npm
使用示例
參考文檔很容易就能掌握基本用法,下方例子中先取到像素點(diǎn)數(shù)據(jù)生成 ImageData ,然后通過(guò) putImageData 把歷史數(shù)據(jù)畫(huà)到 canvas 。
const {
createCanvas,
createImageData
} = require('canvas')
const canvas = createCanvas(canvasWidth, canvasHeight)
const ctx = canvas.getContext('2d')
// 初始化
const init = callback => {
Dot.queryDots().then(data => {
let imgData = new createImageData(
Uint8ClampedArray.from(data),
canvasWidth,
canvasHeight
)
// 移除 Smooth
ctx.mozImageSmoothingEnabled = false
ctx.webkitImageSmoothingEnabled = false
ctx.msImageSmoothingEnabled = false
ctx.imageSmoothingEnabled = false
ctx.putImageData(imgData, 0, 0, 0, 0, canvasWidth, canvasHeight)
successLog('canvas render complete !')
callback()
})
}
Socket.io
本項(xiàng)目在設(shè)計(jì)上有兩個(gè)必須用到推送的地方,一是其他用戶的建點(diǎn)信息,二是所有用戶發(fā)送的聊天消息。
client
// socket.io init
// transports: [ 'websocket' ]
window.socket = io.connect(window.location.origin.replace(/https/, 'wss'))
// 接收?qǐng)D片
window.socket.on('dataUrl', data => {
this.imageObject.src = data.url
this.loadInfo.push('渲染圖像...')
this.init()
})
// 接收其他用戶建點(diǎn)
window.socket.on('newDot', data => {
this.saveDot(
{
x: data.index % this.width,
y: Math.floor(data.index / this.width),
color: data.color
},
false
)
})
// 接收所有人的最新推送消息
window.socket.on('newChat', data => {
if (this.msgs.length === 50) {
this.msgs.shift()
}
this.msgs.push(data)
})
server /bin/www
let http = require('http');
let io = require('socket.io')
let server = http.createServer(app.callback())
let ws = io.listen(server)
server.listen(port)
ws.on('connection', socket => {
// 建立連接的 client 加入房間 chatroom ,為了下方可以廣播
socket.join('chatroom')
socket.emit('dataUrl', {
url: cv.getDataUrl()
})
socket.on('saveDot', async data => {
// 推送給其他用戶,即廣播
socket.broadcast.to('chatroom').emit('newDot', data)
saveDotHandle(data)
})
socket.on('newChat', async data => {
// 推送給所有用戶
ws.sockets.emit('newChat', data)
newChatHandle(data)
})
})
letsencrypt
申請(qǐng)證書(shū)
# 獲得程序
git clone https://github.com/letsencrypt/letsencrypt
cd letsencrypt
# 自動(dòng)生成證書(shū)(環(huán)境安裝完畢后會(huì)有兩次確認(rèn)),證書(shū)目錄 /etc/letsencrypt/live/{輸入的第一個(gè)域名} 我這里是 /etc/letsencrypt/live/www.luwuer.com/
./letsencrypt-auto certonly --standalone --email html6@foxmail.com -d www.luwuer.com -d luwuer.com
自動(dòng)續(xù)期
# 進(jìn)入定時(shí)任務(wù)編輯 crontab -e # 提交申請(qǐng),我這里設(shè)置每?jī)稍乱淮?,過(guò)期時(shí)間為三月 * * * */2 * cd /root/certificate/letsencrypt && ./letsencrypt-auto certonly --renew
nginx
yum install -y nginx
/etc/nginx/config.d/https.conf
server {
# 使用 HTTP/2,需要 Nginx1.9.7 以上版本
listen 443 ssl http2 default_server;
# 開(kāi)啟HSTS,并設(shè)置有效期為“6307200秒”(6個(gè)月),包括子域名(根據(jù)情況可刪掉),預(yù)加載到瀏覽器緩存(根據(jù)情況可刪掉)
add_header Strict-Transport-Security "max-age=6307200; preload";
# add_header Strict-Transport-Security "max-age=6307200; includeSubdomains; preload";
# 禁止被嵌入框架
add_header X-Frame-Options DENY;
# 防止在IE9、Chrome和Safari中的MIME類(lèi)型混淆攻擊
add_header X-Content-Type-Options nosniff;
# ssl 證書(shū)
ssl_certificate /etc/letsencrypt/live/www.luwuer.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/www.luwuer.com/privkey.pem;
# OCSP Stapling 證書(shū)
ssl_trusted_certificate /etc/letsencrypt/live/www.luwuer.com/chain.pem;
# OCSP Stapling 開(kāi)啟,OCSP是用于在線查詢證書(shū)吊銷(xiāo)情況的服務(wù),使用OCSP Stapling能將證書(shū)有效狀態(tài)的信息緩存到服務(wù)器,提高TLS握手速度
ssl_stapling_verify on;
#OCSP Stapling 驗(yàn)證開(kāi)啟
ssl_stapling on;
#用于查詢OCSP服務(wù)器的DNS
resolver 8.8.8.8 8.8.4.4 valid=300s;
# DH-Key交換密鑰文件位置
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# 指定協(xié)議 TLS
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
# 加密套件,這里用了CloudFlare's Internet facing SSL cipher configuration
ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
# 由服務(wù)器協(xié)商最佳的加密算法
ssl_prefer_server_ciphers on;
server_name ~^(\w+\.)?(luwuer\.com)$; # $1 = 'blog.' || 'img.' || '' || 'www.' ; $2 = 'luwuer.com'
set $pre $1;
if ($pre = 'www.') {
set $pre '';
}
set $next $2;
root /root/apps/$pre$next;
location / {
try_files $uri $uri/ /index.html;
index index.html;
}
location ^~ /api/ {
proxy_pass http://43.226.147.135:3000/;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# socket代理配置
location /socket.io/ {
proxy_pass http://43.226.147.135:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# location /weibo/ {
# proxy_pass https://api.weibo.com/;
# }
include /etc/nginx/utils/cache.conf;
}
server {
listen 80;
server_name www.luwuer.com;
rewrite ^(.*)$ https://$server_name$request_uri;
}
附錄
數(shù)據(jù)庫(kù)存儲(chǔ)結(jié)構(gòu)思考?xì)v程
首先需求是畫(huà)板可以作畫(huà)實(shí)際大小為 { width: 1024px, height: 512px } ,這就意味著有 1024 * 512 = 524,288 個(gè)像素點(diǎn),或則有 524,288 * 4 = 2,097,152 個(gè)表示顏色的數(shù)字,這些數(shù)據(jù)量在不做壓縮的情況下,最小存儲(chǔ)方式是后者剔除掉 rgba 中的 a ,也就是一個(gè)長(zhǎng)度為 524,288 * 3 = 1,572,864 的數(shù)組,如果賦值給變量占用內(nèi)存大概 1.5M (數(shù)據(jù)來(lái)源于 Chrome Memory)。為了存儲(chǔ)以上結(jié)構(gòu),我首先分了兩種類(lèi)型的存儲(chǔ)結(jié)構(gòu):
以點(diǎn)為對(duì)象存儲(chǔ),也就是說(shuō)會(huì)有 524,288 條數(shù)據(jù)
- 顏色 rbga 存儲(chǔ),后優(yōu)化為 rgb 存儲(chǔ)
- 顏色 16 進(jìn)制存儲(chǔ)
整個(gè)畫(huà)布數(shù)據(jù)當(dāng)作一條數(shù)據(jù)存儲(chǔ)
雖然看起來(lái)結(jié)構(gòu)2有點(diǎn)蠢,但起初我確實(shí)思考過(guò)這樣的結(jié)構(gòu),那時(shí)我還不清楚原來(lái)取數(shù)據(jù)最耗時(shí)的不是查詢而是 IO 。
后來(lái)我分別測(cè)試 1.1 和 1.2 這兩種結(jié)構(gòu),然后直接否定了結(jié)構(gòu) 2,因?yàn)樵跍y(cè)試中我發(fā)現(xiàn)了 IO 耗時(shí)占總耗時(shí)超過(guò) 98% ,而結(jié)構(gòu) 2 無(wú)疑不能因?yàn)閱螚l數(shù)據(jù)取得絕對(duì)的性能優(yōu)勢(shì)。
1.1
- 存儲(chǔ)大小 10M
- 取出全部數(shù)據(jù) 8000+ms
- 全表查詢 150ms (findOne 和 find 對(duì)比結(jié)果)
- 其余耗時(shí) 20ms (findOne 和 find 對(duì)比結(jié)果)
1.2
- 存儲(chǔ)大小 10M
- 取出全部數(shù)據(jù) 7500+ms
- 全表查詢
- 其余耗時(shí)
結(jié)構(gòu) 2 如果取數(shù)據(jù)不是毫秒級(jí),就是死刑,因?yàn)檫@種結(jié)構(gòu)下單個(gè)像素變動(dòng)就需要存儲(chǔ)整個(gè)圖片數(shù)據(jù)
老實(shí)講這個(gè)測(cè)試結(jié)果讓我有些難以接受,問(wèn)了好幾個(gè)認(rèn)識(shí)的后端為什么性能這么差、有沒(méi)有解決辦法,但都沒(méi)什么結(jié)果。更可怕的是,測(cè)試是在我 i7 CPU 的臺(tái)式電腦上進(jìn)行的,當(dāng)我把測(cè)試環(huán)境放到單核服務(wù)器上時(shí),取全表數(shù)據(jù)的耗時(shí)還要乘以 10 。好在只要想一個(gè)問(wèn)題久了,即使有時(shí)只是想著這個(gè)問(wèn)題發(fā)呆,也總能迸發(fā)出一些莫名的靈感。我想到了關(guān)鍵之一數(shù)據(jù)可以只在服務(wù)啟動(dòng)時(shí)取出放到內(nèi)存中,像素發(fā)生改變時(shí)數(shù)據(jù)庫(kù)和內(nèi)存數(shù)據(jù)副本同步修改,于是得以繼續(xù)開(kāi)發(fā)下去。最終我選擇了 1.1 的結(jié)構(gòu),選擇原因和下文的“數(shù)據(jù)傳輸”有關(guān)。
const mongoose = require('mongoose')
let schema = new mongoose.Schema({
index: {
type: Number,
index: true
},
r: Number,
g: Number,
b: Number
}, {
collection: 'dots'
})
index 代替 x & y 以及移除 rgba 中的 a 在代碼中再補(bǔ)上,都能顯著降低 collection 的實(shí)際存儲(chǔ)大小
在測(cè)試過(guò)程中其實(shí)還有個(gè)特別奇怪的問(wèn)題,就是單核小霸王服務(wù)器上,我如果一次性取出所有數(shù)據(jù)存儲(chǔ)到一個(gè) Array 中,程序會(huì)在中途奔潰,沒(méi)有任何報(bào)錯(cuò)信息。起初我以為是 CPU 滿荷載久了導(dǎo)致的奔潰(top 查看硬件使用信息),所以還特意新租了一個(gè)服務(wù)器,想用一個(gè)群里的朋友提醒的“分布式”。再后面一段時(shí)間,我通過(guò)分頁(yè)取數(shù)據(jù),發(fā)現(xiàn)程序總是在取第二十萬(wàn)零幾百條(一個(gè)固定數(shù)字)是陡然奔潰,所以為 CPU 證了清白。
PS:好在以前沒(méi)分布式經(jīng)驗(yàn),不然一條路走到黑,可能現(xiàn)在都還以為是 CPU 的問(wèn)題呢。
數(shù)據(jù)傳輸思考?xì)v程
上面有提到過(guò),長(zhǎng)度為 1,572,864 的顏色數(shù)組占用內(nèi)存為 1.5M ,我猜想數(shù)據(jù)傳輸時(shí)也是這個(gè)大小。起初我想,我得把這個(gè)數(shù)據(jù)壓縮壓縮(不是指 gzip ),但由于不會(huì),就想到了替代方案。前面已經(jīng)為了避免取數(shù)時(shí)高額的 IO 消耗,會(huì)在內(nèi)存中存儲(chǔ)一個(gè)數(shù)據(jù)副本,我想到這個(gè)數(shù)據(jù)我可以通過(guò)拼接(1.1 的結(jié)構(gòu)相對(duì)而言 CPU 消耗少得多)生成 ImageData 再通過(guò) ctx.putImageData 畫(huà)到 Canvas 上,這就是關(guān)鍵之二把數(shù)據(jù)副本畫(huà)在服務(wù)器上的一個(gè) canvas 上。
然后就好辦了,可以通過(guò) ctx.toDataURL || fs.writeFile('{path}', canvas.toBuffer('image/jpeg') 把數(shù)據(jù)以圖片的方式推送給客戶端,圖片本身的算法幫助我們壓縮了數(shù)據(jù),不用自己搗鼓。事實(shí)上壓縮率非??捎^,前期畫(huà)板上幾乎都是重復(fù)顏色時(shí),1.5M 數(shù)據(jù)甚至可以壓縮到小于 10k,后期估計(jì)應(yīng)該也在 300k 以內(nèi)。
鑒于 DataURL 更方便,這里我采用的 DataURL 的方式傳遞圖片數(shù)據(jù)。
工作記錄
- Day 1 把像素畫(huà)板前端內(nèi)容重構(gòu)一遍,解決圖像過(guò)大時(shí)放大視圖卡頓的問(wèn)題
- Day 2 處理后端邏輯,由于數(shù)據(jù)庫(kù)IO限制,嘗試不同的存儲(chǔ)結(jié)構(gòu),但性能都不理想
- Day 3 繼續(xù)問(wèn)題研究,最后決定在服務(wù)端也同步一份 canvas 操作,而不是只存在庫(kù)里,但流程還沒(méi)走通,因?yàn)橄挛缢艘挥X(jué)
- Day 4 1核1G服務(wù)器在訪問(wèn)數(shù)據(jù)庫(kù)取50w條數(shù)據(jù)時(shí)崩潰,后通過(guò)和朋友討論,在無(wú)意中發(fā)現(xiàn)了實(shí)際問(wèn)題,就有了解決方案(部分時(shí)間在新服務(wù)器配了套環(huán)境,不過(guò)由于問(wèn)題解決又棄用了)
- Day 5 增加公告、用戶、聊天、像素點(diǎn)歷史信息查詢功能
- Day 6/7 解決 socket.io https 問(wèn)題,通宵兩天最后發(fā)現(xiàn)是 CDN 加速問(wèn)題,差點(diǎn)螺旋升天
Day 4 說(shuō)的實(shí)際問(wèn)題,我只能大概定位在 NodeJS 變量大小限制或?qū)ο髠€(gè)數(shù)限制,因?yàn)樵谖覍?50w 長(zhǎng)度 Array[Object] 轉(zhuǎn)換為 200w 長(zhǎng)度 Array[Number] 后問(wèn)題消失了,知道具體原因的大佬望不吝賜教。
記錄是從日記里復(fù)制過(guò)來(lái)的,Day 6/7 確實(shí)是最艱難的兩天,其實(shí)代碼從一開(kāi)始就沒(méi)什么錯(cuò),有問(wèn)題的是又拍云的 CDN 加速,可怖的是我根本沒(méi)想到罪魁禍?zhǔn)资撬?。其?shí)在兩天的重復(fù)測(cè)試中,因?yàn)閷?shí)在是無(wú)計(jì)可施,我也有兩次懷疑 CDN 。第一次,我把域名解析到服務(wù)器 IP ,但測(cè)試結(jié)果仍然報(bào)錯(cuò),之后就又恢復(fù)了加速。第二次是在第七天的早上五點(diǎn),當(dāng)時(shí)頭很脹很難受就直接停了 CDN ,想著最后測(cè)試一下不行就去掉 CDN 的 https 證書(shū)用 http 訪問(wèn)。那時(shí)我才發(fā)現(xiàn),在我 ping 域名確定解析已經(jīng)改變后(修改解析后大概 10 分鐘),域名又會(huì)間隙性被重新解析到 CDN (這個(gè)反復(fù)原因不知道為什么,阿里云的域名解析服務(wù)),第一次測(cè)試不準(zhǔn)應(yīng)該就是這個(gè)原因,稍長(zhǎng)時(shí)間后就不再會(huì)了。解決后我有意恢復(fù) CDN 加速測(cè)試,但始終沒(méi)找出究竟是哪一個(gè)配置導(dǎo)致了問(wèn)題,所以最終我也沒(méi)能恢復(fù)加速。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- vue+koa2實(shí)現(xiàn)session、token登陸狀態(tài)驗(yàn)證的示例
- koa2+vue實(shí)現(xiàn)登陸及登錄狀態(tài)判斷
- Vue+Koa2 打包后進(jìn)行線上部署的教程詳解
- 詳解Vue SSR( Vue2 + Koa2 + Webpack4)配置指南
- vue2.0+koa2+mongodb實(shí)現(xiàn)注冊(cè)登錄
- 詳解vue+vuex+koa2開(kāi)發(fā)環(huán)境搭建及示例開(kāi)發(fā)
- 利用vue + koa2 + mockjs模擬數(shù)據(jù)的方法教程
- 詳解基于Vue+Koa的pm2配置
- 客戶端(vue框架)與服務(wù)器(koa框架)通信及服務(wù)器跨域配置詳解
相關(guān)文章
Vue?2?和?Vue?3?中?toRefs函數(shù)的不用用法
Vue?是一款流行的JavaScript?框架,用于構(gòu)建用戶界面,在Vue2和?Vue3中,都存在一個(gè)名為toRefs的函數(shù),但其行為在這兩個(gè)版本中有所不同,這篇文章主要介紹了Vue2和Vue3中toRefs的區(qū)別,需要的朋友可以參考下2023-08-08
Vue resource中的GET與POST請(qǐng)求的實(shí)例代碼
本篇文章主要介紹了Vue resource中的GET與POST請(qǐng)求的實(shí)例代碼,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2017-07-07
Vue.js中的extend綁定節(jié)點(diǎn)并顯示的方法
在本篇內(nèi)容里小編給大家整理了關(guān)于Vue.js中的extend綁定節(jié)點(diǎn)并顯示的方法以及相關(guān)知識(shí)點(diǎn),需要的朋友們學(xué)習(xí)下。2019-06-06

