最近經常碰到一種場景,手機複製粘貼一些文本等內容到電腦,或者電腦發到手機。
實現這個功能的方案很多,但是基本上要麼是手機連上電腦,要麼兩端各安裝一個什麼應用,侷限性還是很大。於是就有了一個想法,直接自己搞一個類似聊天室的網站不就好了,手機掃個碼就能打開,電腦上只要有瀏覽器就行。websocket把編輯的內容實時同步到服務器,多方便😁——emmm....啊這,這不就是谷歌文檔麼😅。。。那就當自己玩玩吧,技多不壓身嘛~而且自己搭的服務器,這些簡單的功能也不用搞什麼複雜的鑑權,用起來也方便~~
既然明確了目的就是玩,那就玩點新東西好了,首先嚐試下vite:
創建vite工程sync-site
npm init vite@latest sync-site --template vue
這裏我們並不需要vue,但是新的vite必須要指定一個框架,好在不會自動幫我們安裝依賴,我們可以進入sync-site目錄,手動去掉不必要的文件和代碼。
刪除vue相關的文件和代碼
①移除package.json裏vue相關的依賴
{
"name": "sync-site",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
},
"devDependencies": {
"vite": "^2.8.0"
}
}
②刪除components目錄和其他.vue後綴的文件
③vite.config.js移除vue相關代碼
import { defineConfig } from 'vite'
export default defineConfig({
})
④main.js裏的代碼改爲
document.getElementById('app').innerHTML = 'Hello Vite!'
⑤安裝依賴並啓動vite開發服務器
npm i
npm run dev
如果頁面上看到“Hello Vite!”,第一步配置vite就成功了
配置富文本編輯器Quill
①安裝最新版的quill
npm i -S quill
②main.js中引入並配置quill的主題樣式爲snow
當前quill版本是1.3.7,snow主題的樣式文件地址爲https://cdn.quilljs.com/1.3.7/quill.snow.css,我下載到了本地。修改main.js如下:
import Quill from 'quill'
import './quill.snow.css'
const toolbar = [
['bold', 'italic', 'underline', 'strike'],
[{'header': [1, 2, 3, 4, 5, 6, false]}],
[{'list': 'ordered'}, {'list': 'bullet'}],
[{'indent': '-1'}, {'indent': '+1'}],
[{'size': ['small', false, 'large', 'huge']}],
[{'color': []}, {'background': []}],
[{'font': []}],
[{'align': []}],
['clean'],
]
const $app= document.querySelector('#app')
const quill = new Quill($app, {
modules: {
toolbar,
},
theme: 'snow',
})
配置Express服務器
①安裝express依賴
②在src下創建server.js,使用static中間件來託管dist目錄下的靜態文件
③在3000端口啓動服務
const http = require('http')
const path = require('path')
const express = require('express')
const app = express()
const httpServer = http.createServer(app)
app.use(express.static(path.join(__dirname, '../dist')))
httpServer.listen(3000, function () {
console.log('\x1b[32m%s\x1b[0m', '服務啓動成功')
})
ps:
① 第五行使用node的http模塊創建http服務器,第九行的 server.listen(3000... 可以替換爲 app.listen(3000... ,如果看下express的listen函數源碼,會發現兩者並沒有什麼區別。之所以不直接使用express的listen函數是因爲後面我們的websocket也要使用到http server。
② 第十行的 \x1b[32m%s\x1b[0m 中,%s是佔位符,代表後面的字符串。\x1b[32m和\x1b[0m表示打印前景色是綠色的字符,具體原理見前文:控制檯與終端輸出帶樣式文本原理及實現。
修改vite配置文件vite.config.js監聽vite的build行爲
import {defineConfig} from 'vite'
export default defineConfig({
build: {
watch: {}
}
})
開啓node服務
node ./src/server.js
3000端口號上打開頁面,可以看到quill被成功引入。
配置socket.io
①安裝socket.io和socket.io-client依賴
index.html的app改爲editor,並修改main.js:
import Quill from 'quill'
import './quill.snow.css'
import io from 'socket.io-client'
const toolbar = [
['bold', 'italic', 'underline', 'strike'],
[{'header': [1, 2, 3, 4, 5, 6, false]}],
[{'list': 'ordered'}, {'list': 'bullet'}],
[{'indent': '-1'}, {'indent': '+1'}],
[{'size': ['small', false, 'large', 'huge']}],
[{'color': []}, {'background': []}],
[{'font': []}],
[{'align': []}],
['clean'],
]
const $editor = document.querySelector('#editor')
const quill = new Quill($editor, {
modules: {
toolbar,
},
theme: 'snow',
})
const socket = io()
socket.on('welcome', function (word) {
console.log('welcome:', word)
})
socket.on('broadcast', function (word) {
console.log('broadcast:', word)
})
// 接收遠程內容同步
socket.on('quill-sync', function (contentStr) {
contentStr && quill.setContents(JSON.parse(contentStr))
})
// 監聽編輯器內容更新,發送到服務器
quill.on('text-change', function (delta, oldDelta, source) {
if (source === 'user') {
const content = quill.getContents()
socket && socket.emit('quill-sync', JSON.stringify(content))
}
})
修改server.js:
const http = require('http')
const path = require('path')
const express = require('express')
const app = express()
const httpServer = http.createServer(app)
const {Server: SocketServer} = require('socket.io')
const io = new SocketServer(httpServer)
app.use(express.static(path.join(__dirname, '../dist')))
httpServer.listen(3000, function () {
console.log('\x1b[32m%s\x1b[0m', '服務啓動成功')
})
let _contentStr = ''
io.on('connection', (socket) => {
socket.emit('welcome', '你好,' + socket.id)
socket.broadcast.emit('broadcast', socket.id + '加入')
// 進入時初始化編輯器內容
socket.emit('quill-sync', _contentStr)
// 將收到的內容記錄到服務器,並廣播給其他用戶
socket.on('quill-sync', function (contentStr) {
_contentStr = contentStr
socket.broadcast.emit('quill-sync', contentStr)
})
})
這時候,當你第一次打開http://localhost:3000的時候,會在控制檯收到歡迎信息;新標籤頁再次打開該地址,原頁面會收到新加入用戶的信息;修改編輯器內容,打開的多個頁面會同時收到同步的內容。
增加房間號與接口鑑權
如果你想把這個頁面共享給其他夥伴,難免就涉及到socket連接的分配與鑑權,這裏爲了方便,直接使用http的Basic鑑權。
1. 安裝sass模塊(即dart sass)
2. 添加src目錄添加main.scss:
* {
padding: 0;
margin: 0;
}
html, body {
width: 100%;
height: 100%;
}
body {
display: flex;
flex-direction: column;
}
.ql-toolbar {
width: 100%;
height: auto;
z-index: 1;
flex: 0;
}
.ql-editor {
height: 100%;
box-sizing: border-box;
}
#status-bar {
flex: 0;
#btn-join {
display: inline-block;
}
#num-room {
display: none;
}
#btn-leave {
display: none;
}
&.joined {
#btn-join {
display: none;
}
#num-room {
display: inline-block;
}
#btn-leave {
display: inline-block;
}
}
}
#editor {
flex: 1;
}
3. 修改index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Vite App</title>
</head>
<body>
<div id="status-bar">
<button id="btn-join">進入房間</button>
<span id="num-room"></span>
<button id="btn-leave">離開房間</button>
</div>
<div id="editor"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
4. 修改main.js:
import Quill from 'quill'
import './quill.snow.css'
import './main.scss'
import io from 'socket.io-client'
const toolbar = [
['bold', 'italic', 'underline', 'strike'],
[{'header': [1, 2, 3, 4, 5, 6, false]}],
[{'list': 'ordered'}, {'list': 'bullet'}],
[{'indent': '-1'}, {'indent': '+1'}],
[{'size': ['small', false, 'large', 'huge']}],
[{'color': []}, {'background': []}],
[{'font': []}],
[{'align': []}],
['clean'],
]
const $editor = document.querySelector('#editor')
const $statusBar = document.getElementById('status-bar')
const $btnJoin = document.getElementById('btn-join')
const $numRoom = document.getElementById('num-room')
const $btnLeave = document.getElementById('btn-leave')
const quill = new Quill($editor, {
modules: {
toolbar,
},
theme: 'snow',
})
let socket = null
// 監聽編輯器內容更新,發送到服務器
quill.on('text-change', function (delta, oldDelta, source) {
if (source === 'user') {
const content = quill.getContents()
socket && socket.emit('quill-sync', JSON.stringify(content))
}
})
// 加入房間
$btnJoin.onclick = async function () {
const room = prompt('請輸入你要進入的房間:')
if (room) {
// 進入 basic 校驗
const response = await fetch(`/join?room=${room}`)
const token = await response.text()
$statusBar.classList.add('joined')
$numRoom.innerHTML = room
if (!socket) {
socket = io({auth: {token}})
}
socket.on('welcome', function (word) {
console.log('welcome:', word)
})
socket.on('broadcast', function (word) {
console.log('broadcast:', word)
})
// 接收遠程內容同步
socket.on('quill-sync', function (contentStr) {
contentStr && quill.setContents(JSON.parse(contentStr))
})
}
}
// 離開房間
$btnLeave.onclick = function () {
socket.disconnect()
socket = null
fetch('logout')
$statusBar.classList.remove('joined')
}
5. 修改server.js:
const http = require('http')
const path = require('path')
const express = require('express')
const app = express()
const httpServer = http.createServer(app)
const {Server: SocketServer} = require('socket.io')
const io = new SocketServer(httpServer)
app.use(express.static(path.join(__dirname, '../dist')))
httpServer.listen(3000, function () {
console.log('\x1b[32m%s\x1b[0m', '服務啓動成功')
})
// 每個房間對應的同步數據
const room2content = {}
io.on('connection', (socket) => {
// socket鑑權
const authorization = socket.handshake.auth?.token
const userInfo = authorization ? userDict[authorization] : null
if (userInfo) {
userInfo.socket = socket
socket.join(userInfo.room)
socket.emit('welcome', `歡迎[${userInfo.name}]加入房間[${userInfo.room}]`)
socket.broadcast.to(userInfo.room).emit('broadcast', `[${userInfo.name}]加入房間[${userInfo.room}]`)
// 進入時初始化編輯器內容
socket.emit('quill-sync', room2content[userInfo.room] || '')
// 將收到的內容記錄到服務器,並廣播給其他用戶
socket.on('quill-sync', function (contentStr) {
room2content[userInfo.room] = contentStr
socket.broadcast.to(userInfo.room).emit('quill-sync', contentStr)
})
socket.on('disconnecting', function () {
socket.broadcast.to(userInfo.room).emit('broadcast', `[${userInfo.name}]離開房間[${userInfo.room}]`)
userInfo.room = null
userInfo.socket = null
})
}
})
const userDict = {}
// 登錄/註冊
app.get('/join', function (req, res) {
const authorization = req.headers.authorization
const encodeAuthStr = authorization ? authorization.replace('Basic ', '') : ''
// 進入登錄/註冊
if (encodeAuthStr && encodeAuthStr !== 'logout') {
const name = Buffer.from(encodeAuthStr, 'base64').toString().split(':')[0]
let userInfo = userDict[authorization]
// 有用戶信息則更新房間號
if (userInfo) {
userInfo.room = req.query.room
}
// 沒有用戶信息就註冊一個
else {
userInfo = {
name: name,
room: req.query.room
}
userDict[authorization] = userInfo
}
res.send(authorization)
}
// 請求Basic鑑權
else {
res.set({'WWW-Authenticate': 'Basic'})
res.status(401)
res.end()
}
})
// 退出登錄(用於清除 base authorization)
app.get('/logout', function (req, res) {
res.status(401).end()
})
ps:
base鑑權退出登錄只要接口返回401即可