0x1 引言 0x1.1 目的与背景 当我刚进入大学时 ,我得到了我的第一台游戏本。当时,我经常需要在手机和电脑之间传输文件和文档。最初,我使用QQ和手机热点来进行传输,这显得相当繁琐。后来,我尝试了蓝牙传输,但速度过慢。尽管我使用了小米的热点文件传输功能,但仅为了传输简单的文本或图片,这样的操作还是过于复杂。
毕业后 ,我的生活出现了很大的改变。拥有了路由器后,内网传输变得相对简单。但当我获得了iPhone和MacBook,我发现跨操作系统传输文件并不简单。例如,从iPhone向Windows传输文本或链接时,我还需依赖QQ等软件。尝试使用Windows的SMB共享服务和MacBook的文件共享都有其局限性和问题。
后来 ,我转向开源的云盘解决方案,但传输速度的限制使我非常不满意。直到我获得了QNAP,大部分的文件传输问题得到了解决。然而,在局域网下保持Windows和MacBook数据的一致性,尤其是文本数据,依然是一个问题。显然,依赖QQ这样的方式效率低下。
因此 ,创建一个简单的网页成为了一种必要,让任何设备仅通过输入一个地址就能发送消息,避免了下载和设置各种软件的麻烦。
0x1.2 目标受众
大学生和职场新人 :他们经常在不同的设备之间传输文件和文档,尤其是在手机和电脑之间,需要一个高效的解决方案。
跨操作系统用户 :特别是那些使用iPhone和Windows或MacBook的人,他们经常面临操作系统之间的传输难题。
网络技术爱好者 :对于那些寻求局域网下文件和数据传输解决方案的人,他们可能已经尝试了各种方法,但仍然在寻找更加高效的方法。
对效率有追求的人 :不想依赖于多个应用程序和软件,而是寻求一个集中的、简化的解决方案来处理日常的文件和数据传输任务。
云存储和网络存储用户 :特别是那些使用开源云盘或QNAP这样的解决方案的人,他们在寻找更快、更直接的传输方法。
0x2 必要的理论知识解释 0x2.1 基础概念
文件传输 :
定义 :文件传输指的是从一个设备、系统或者位置将文件或数据移动到另一个设备、系统或位置的过程。
应用 :例如,通过USB、网络或云服务将文档从电脑传输到手机。
操作系统差异 :
定义 :不同的操作系统(如Windows、MacOS、Linux等)在架构、功能和界面上有所不同。
应用 :某些软件可能只在特定操作系统上运行,或在不同系统上有不同的表现。
局域网(LAN)原理 :
定义 :局域网是一个限定在较小地理范围内(如家庭、办公室)的计算机网络。
应用 :局域网使得同一位置的多台设备可以共享资源,如打印机或文件。
蓝牙和热点传输 :
定义 :蓝牙是一种无线技术标准,用于短距离数据交换,而热点传输则是通过Wi-Fi将文件从一个设备分享到另一个设备。
应用 :例如,用蓝牙耳机听音乐或使用手机热点给电脑上网。
云存储与网络存储 :
定义 :云存储是通过互联网存储数据在远程服务器上的服务;网络存储则是在局域网内提供集中化的数据存储。
应用 :如,使用Google Drive或Dropbox来存储文件或使用NAS来在家庭网络中共享文件。
跨平台兼容性 :
定义 :指的是软件或应用在多个操作系统或设备上的运行能力。
应用 :例如,一个应用既可以在Android上运行,也可以在iOS上运行。
Web技术基础 :
定义 :关于如何构建和呈现互联网内容的技术和原理。
应用 :例如,使用HTML、CSS和JavaScript来构建网页。
Node.js :
定义 :是一个允许在服务器上运行JavaScript的运行时环境。
应用 :例如,构建后端API或Web应用。
HTML :
定义 :是用来描述网页结构的标记语言。
应用 :例如,定义网页中的标题、段落和链接。
CSS :
定义 :是用于描述网页外观和格式的语言。
应用 :例如,设置字体、颜色和布局。
Socket :
定义 :是计算机之间进行通信的端点。
应用 :例如,实时聊天应用。
Server :
定义 :是为其他计算机或应用提供资源、服务或数据的系统或应用。
应用 :例如,Web服务器存储和提供网页内容。
Docker :
定义 :是一个平台,用于创建、运行和管理容器化的应用。
应用 :例如,为应用提供一致的运行环境。
IP地址 :
定义 :是分配给联网设备的数字地址,用于识别和定位。
应用 :例如,访问特定的网站或远程服务器。
User Agent :
定义 :是描述浏览器或其他客户端如何与服务器通信的字符串。
应用 :例如,网站可以识别用户的设备和浏览器,并据此提供优化的内容。
0x2.2 相关技术背景 随着技术的日益发展,文件传输和数据共享已经成为现代工作和生活中不可或缺的部分。无论是学生、企业家还是日常用户,我们都经常面临跨设备、跨操作系统的数据传输挑战。
文件传输 :这是一种基本的需求,涉及将文件从一个设备传送到另一个设备。尽管有许多可用的方法,例如USB、蓝牙和热点传输,但它们都有自己的局限性和速度问题。
操作系统差异 :由于Windows、MacOS和其他操作系统的内在差异,许多应用和服务在跨操作系统通信时面临挑战。
局域网原理 :局域网为设备提供了一种在有限的地理范围内共享资源的方式。例如,家庭或办公室内的设备可以通过局域网共享打印机或文件。
蓝牙与热点传输 :尽管这两种技术都提供了一种无线传输方式,但它们通常受限于传输距离和速度。
云存储与网络存储 :为了解决大文件传输的问题,许多用户转向了云存储或网络存储解决方案,如Google Drive或NAS。
跨平台兼容性 :这是当今技术世界中的一大挑战。由于多样性的增加,需要开发可以在多个平台上运行的应用。
Web技术基础 :HTML、CSS和JavaScript等技术为创建跨平台解决方案提供了基础,允许用户通过浏览器访问应用和服务。
服务器和后端技术 :Node.js提供了一个强大的平台,用于构建后端服务和API,而Socket技术使实时通信成为可能。这些技术经常被用于创建动态、实时的Web应用。
容器化技术 :Docker为开发者提供了一种方式,将其应用及其所有依赖性封装在一个容器中,确保其在任何环境中的一致性和可移植性。
网络识别 :IP地址和User Agent允许设备和网络服务识别请求的来源,从而为用户提供个性化的体验和内容。
0x3 Node.js原型开发 0x3.1 环境配置 初始化项目
1 2 mkdir realtime-text cd realtime-text
安装Node.js ,最好选择LTS版本,在项目根目录执行
1 2 3 npm init -y npm install express socket.io npm install multer
0x3.2 基本功能实现 创建服务器 (server.js)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 const express = require ('express' );const http = require ('http' );const socketIo = require ('socket.io' );const app = express();const server = http.createServer(app);const io = socketIo(server);app.use('/socket.io' , express.static('node_modules/socket.io/client-dist/' )); app.get('/' , (req, res ) => { res.sendFile(__dirname + '/index.html' ); }); io.on('connection' , (socket ) => { console .log('a user connected' ); socket.on('send text' , (text ) => { io.emit('receive text' , text); }); socket.on('disconnect' , () => { console .log('user disconnected' ); }); }); server.listen(3000 , () => { console .log('listening on *:3000' ); });
创建前端页面 (index.html)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <!doctype html > <html > <head > <title > Realtime Text</title > </head > <body > <textarea id ="textArea" rows ="10" cols ="50" > </textarea > <button id ="sendButton" > Send</button > <hr > <h3 > Received Text:</h3 > <div id ="receivedText" > </div > <script src ="/socket.io/socket.io.js" > </script > <script > var socket = io(); document .getElementById('sendButton' ).onclick = function ( ) { var text = document .getElementById('textArea' ).value; socket.emit('send text' , text); }; socket.on('receive text' , function (text ) { document .getElementById('receivedText' ).innerText = text; }); </script > </body > </html >
启动服务器
本地服务器默认地址:http://localhost:3000/
创建启动服务器脚本 (run.bat)
0x4 按需复制,无需深究学习 不想学习思路直接运行代码,按照以下步骤操作,如果需要上传NAS请继续阅读 0xA Docker封装与测试
创建对应的目录结构,并把index.html
和server.js
这两个的代码复制到你的网站根目录下
下载JetBrains Mono 字体,解压JetBrainsMono-Regular.ttf
、JetBrainsMono-Regular.woff2
到网站根目录
现在你的目录结构应该是这样的
1 2 3 4 5 6 7 8 D:\realtime_text 的目录 2023/08/27 04:51 11,332 index.html 2023/01/14 23:20 273,900 JetBrainsMono-Regular.ttf 2023/01/14 23:20 92,164 JetBrainsMono-Regular.woff2 2023/08/27 01:47 <DIR> node_modules 2023/08/27 01:12 52 run.bat 2023/08/27 01:49 1,942 server.js 2023/08/27 13:27 <DIR> uploads
0x4.1 index.html 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 <!doctype html > <html > <head > <title > Realtime Text</title > <style > [data-theme="dark" ] input , [data-theme="dark" ] textarea , [data-theme="dark" ] button { color : white; border-color : white; } :root { --text-color : black; --background-color : white; --input -bg: #fff ; --container-bg: #fff ; --header -bg: #fff ; --button -color : black; } [data-theme="dark" ] { --text-color : black; --background-color : white; } .bottom-controls { display : flex; justify-content : center; } .bottom-controls > div { display : flex; align-items : center; } div [style*="position: fixed; bottom: 0;" ] { padding-left : 10px ; padding-right : 10px ; } body { display : flex; justify-content : center; align-items : center; height : 100vh ; background-color : var (--background-color) !important ; color : var (--text-color); font-family : "JetBrainsMono" , Arial, sans-serif; } .container { border : 1px solid #ddd ; background-color : #fff ; } @media (max-width : 500px ) { .bottom-controls { justify-content : flex-start; } .bottom-controls > div { flex-direction : column; align-items : center; width : 100% ; } div [style*="position: fixed; bottom: 0;" ] { background-color : var (--container-bg); padding-left : 10px ; padding-right : 10px ; display : flex; flex-direction : column; } #deviceName , #textArea , #imageUpload , #sendButton { background-color : var (--container-bg); width : 100% ; box-sizing : border-box; margin-bottom : 5px ; } #deviceName , #textArea , #sendButton { height : 25px ; } #messages { padding-bottom : 70px ; } #deleteButton { width : 100% ; height : 25px ; } .container { max-height : 480px ; } #themeToggle { width : 100% ; height : 25px ; margin-bottom : 5px ; } } @media (min-width : 501px ) { #deviceName , #textArea , #imageUpload , #sendButton { background-color : var (--input-bg); height : 60px ; font-size : 18px ; padding : 10px ; margin-bottom : 10px ; } #textArea { width : 50% ; } #sendButton { padding : 10px 20px ; margin-left : 10px ; margin-right : 5px ; width : 100px ; } #deleteButton { margin-left : 5px ; width : 150px ; height : 50px ; } #themeToggle { margin-right : 10px ; width : 100px ; height : 50px ; } } body , button { background-color : var (--bg-color); color : var (--text-color); } button { color : var (--button-color); } .bottom-controls input , .bottom-controls textarea , .bottom-controls button { color : var (--text-color); border-color : var (--text-color); } .container { background-color : var (--container-bg); overflow-y : auto; min-width : 350px ; position : sticky; margin-bottom : 100px ; } body .dark-theme { --text-color : white; --background-color : #555 ; --input -bg: #555 ; --container-bg: #444 ; --header -bg: #444 ; --button -color : white; } #header { background-color : var (--header-bg); } @font-face { font-family : "JetBrainsMono" ; src : url ('JetBrainsMono-Regular.woff2' ) format ('woff2' ), /* 最优先 */ url ('JetBrainsMono-Regular.ttf' ) format ('truetype' ); font-weight : normal; font-style : normal; } </style > <meta name ="viewport" content ="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" > </head > <body > <div class ="container" > <div id ="header" style ="background-color: var(--container-bg);" > <ul id ="messages" style ="max-height: 70vh" > </ul > </div > </div > <div class ="bottom-controls" style ="position: fixed; bottom: 0; width: 100%; padding: 5px; box-shadow: 0 -2px 5px rgba(0,0,0,0.1); background-color: var(--container-bg);" > <div style ="display: flex; align-items: center;" > <button id ="themeToggle" > 切换主题</button > <input type ="text" id ="deviceName" placeholder ="Device Name" > <input type ="file" id ="imageUpload" accept ="image/*" > <textarea id ="textArea" placeholder ="写点什么?!" > </textarea > <button id ="sendButton" > 发送</button > <button id ="deleteButton" > 删除选中消息</button > </div > </div > <script src ="/socket.io/socket.io.js" > </script > <script > window .addEventListener('load' , function ( ) { setTimeout (function ( ) { window .scrollTo(0 , 1 ); }, 0 ); fetch('/messages' ) .then(response => response.json()) .then(data => { const messagesDiv = document .getElementById('messages' ); data.messages.forEach(message => { const p = document .createElement('p' ); p.textContent = message; messagesDiv.appendChild(p); }); }) .catch(error => { console .error('获取消息错误:' , error); }); }); document .getElementById('messages' ).scrollTop = document .getElementById('messages' ).scrollHeight; var socket = io(); document .getElementById('sendButton' ).onclick = function ( ) { var text = document .getElementById('textArea' ).value; var deviceName = document .getElementById('deviceName' ).value || "匿名设备" ; var imageFile = document .getElementById('imageUpload' ).files[0 ]; var listCheckBox = document .getElementById('listCheckBox' ); if (imageFile) { var formData = new FormData(); formData.append('image' , imageFile); formData.append('deviceName' , deviceName); formData.append('text' , text); formData.append('listCheckBox' , listCheckBox); fetch('/upload-image' , { method : 'POST' , body : formData }).then(response => response.json()).then(data => { socket.emit('send text' , data); }); } else { socket.emit('send text' , {text, deviceName }); } document .getElementById('textArea' ).value = '' ; document .getElementById('imageUpload' ).value = '' ; }; var messageId = 0 ; socket.on('receive text' , function (data ) { var li = document .createElement("li" ); var checkbox = document .createElement("input" ); checkbox.type = "checkbox" ; checkbox.className = "messageCheckbox" ; li.appendChild(checkbox); var time = data.timestamp || new Date ().toLocaleString(); var content = `[${data.deviceName} @ ${time} ]: ` ; if (data.text.startsWith("```" ) && data.text.endsWith("```" )) { content += `<pre>${data.text.slice(3 , -3 )} </pre>` ; } else { content += `${data.text} ` ; } if (data.imageUrl) { content += `<br><img src="${data.imageUrl} " alt="Uploaded image" style="max-width: 300px;">` ; } li.innerHTML += content; document .getElementById('messages' ).appendChild(li); li.setAttribute('data-message-id' , messageId); messageId++; console .log("设备名称和IP地址:" , data.deviceName); }); document .getElementById('deleteButton' ).onclick = function ( ) { var selectedMessages = document .querySelectorAll('.messageCheckbox:checked' ); var messageIdsToDelete = []; selectedMessages.forEach(function (checkbox ) { var li = checkbox.closest('li' ); messageIdsToDelete.push(li.getAttribute('data-message-id' )); li.remove(); }); socket.emit('delete messages' , messageIdsToDelete); messageIdsToDelete.forEach(id => { fetch(`/message/${id} ` , { method : 'DELETE' }).then(response => { if (!response.ok) { console .error('从服务器删除消息失败' ); } }); }); }; socket.on('delete messages' , function (messageIds ) { messageIds.forEach(function (messageId ) { var li = document .querySelector('li[data-message-id="' + messageId + '"]' ); if (li) li.remove(); }); io.emit('delete messages' , messageIds); }); document .addEventListener('click' , function (event ) { if (event.target.tagName === 'IMG' && event.target.closest('#messages' )) { var modal = document .getElementById('imageModal' ); var modalImage = document .getElementById('modalImage' ); modalImage.src = event.target.src; modal.style.display = 'flex' ; modal.addEventListener('click' , function ( ) { modal.style.display = 'none' ; }); } }); document .addEventListener('keydown' , function (event ) { if (event.key === 'Escape' ) { var modal = document .getElementById('imageModal' ); modal.style.display = 'none' ; } }); function getDeviceNameFromUserAgent ( ) { var userAgent = navigator.userAgent || navigator.vendor || window .opera; if (/windows phone/i .test(userAgent)) { return "Windows Phone" ; } if (/android/i .test(userAgent)) { return "Android Device" ; } if (/iPad|iPhone|iPod/ .test(userAgent) && !window .MSStream) { return "iOS Device" ; } if (/macintosh|mac os x/i .test(userAgent)) { return "Mac" ; } if (/windows/i .test(userAgent)) { return "Windows PC" ; } return "匿名设备" ; } document .getElementById('deviceName' ).value = getDeviceNameFromUserAgent(); document .getElementById('themeToggle' ).addEventListener('click' , function ( ) { document .body.classList.toggle('dark-theme' ); }); socket.on('load history' , function (history ) { history.forEach(data => { var li = document .createElement("li" ); var content = `[${data.deviceName} ]: ${data.text} ` ; if (data.imageUrl) { content += `<br><img src="${data.imageUrl} " alt="Uploaded image" style="max-width: 300px;">` ; } li.innerHTML = content; document .getElementById('messages' ).appendChild(li); }); }); </script > <div id ="imageModal" style ="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.7); z-index: 9999; align-items: center; justify-content: center;" > <img id ="modalImage" src ="" style ="max-width: 90%; max-height: 90%;" > </div > </body > </html >
0x4.2 server.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 const express = require ('express' );const http = require ('http' );const socketIo = require ('socket.io' );const multer = require ('multer' );const path = require ('path' );const sqlite3 = require ('sqlite3' ).verbose();const app = express();const server = http.createServer(app);const io = socketIo(server);const messageHistory = [];const db = new sqlite3.Database('./messages.db' );db.run("CREATE TABLE IF NOT EXISTS messages (id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT, deviceName TEXT, ipAddress TEXT, timestamp TEXT)" , (err ) => { if (err) { console .error("创建/检查表错误:" , err); return ; } console .log("表检查/创建成功" ); }); db.all("SELECT id, content, deviceName, ipAddress, timestamp FROM messages" , [], (err, rows ) => { if (err) { console .error("从数据库获取历史消息出错:" , err); return ; } rows.forEach(row => { const message = { id : row.id, text : row.content, deviceName : row.deviceName, ipAddress : row.ipAddress, timestamp : row.timestamp }; messageHistory.push(message); }); console .log("从数据库加载历史消息" ); }); app.use(express.json()); app.post('/send' , (req, res ) => { const messageContent = req.body.message; if (messageContent) { db.run("INSERT INTO messages (content, deviceName, ipAddress, timestamp) VALUES (?, ?, ?, ?)" , [data.text, data.deviceName, clientIpAddress, data.timestamp], (err ) => { if (err) { console .error("插入数据库出错:" , err); return ; } console .log('收到的信息:' , data.text || messageContent); }); } else { res.status(400 ).send({error : "消息内容为空" }); } }); app.use('/socket.io' , express.static('node_modules/socket.io/client-dist/' )); app.get('/' , (req, res ) => { res.sendFile(__dirname + '/index.html' ); }); io.on('connection' , (socket ) => { console .log('有一个用户连接' ); function formatMessageForEmission (originalMessage ) { return { ...originalMessage, deviceName : `${originalMessage.deviceName} (${originalMessage.ipAddress} )` }; } messageHistory.forEach((message ) => { socket.emit('receive text' , formatMessageForEmission(message)); }); socket.on('send text' , (data ) => { let clientIpAddress = socket.request.connection.remoteAddress.replace(/^::ffff:/ , '' ); if (clientIpAddress === '::1' || clientIpAddress === '127.0.0.1' ) { clientIpAddress = 'Localhost' ; } data.ipAddress = clientIpAddress; if (!data.deviceName || data.deviceName.trim() === "" ) { data.deviceName = "匿名设备" ; } data.timestamp = new Date ().toLocaleString(); db.run("INSERT INTO messages (content, deviceName, ipAddress, timestamp) VALUES (?, ?, ?, ?)" , [data.text, data.deviceName, clientIpAddress, data.timestamp], function (err ) { if (err) { console .error("插入数据库出错:" , err); return ; } data.id = this .lastID; messageHistory.push(data); console .log('通过套接字收到的消息:' , data.text); } ); io.emit('receive text' , formatMessageForEmission(data)); }); socket.on('disconnect' , () => { console .log('有一个用户断开连接' ); }); }); server.listen(3000 , () => { console .log('服务器地址:http://localhost:3000' ); }); const storage = multer.diskStorage({ destination : (req, file, cb ) => { cb(null , 'uploads/' ) }, filename : (req, file, cb ) => { cb(null , Date .now() + path.extname(file.originalname)) } }); const upload = multer({ storage : storage });app.post('/upload-image' , upload.single('image' ), (req, res ) => { let imageUrl = `/uploads/${req.file.filename} ` ; res.json({ text : req.body.text, deviceName : req.body.deviceName, imageUrl : imageUrl }); }); app.use('/uploads' , express.static(path.join(__dirname, 'uploads' ))); app.use('/css' , express.static(path.join(__dirname, 'css' ))); app.use('/js' , express.static(path.join(__dirname, 'js' ))); app.use('/' , express.static(path.join(__dirname, 'JetBrainsMono-Regular.ttf' ))); app.use('/' , express.static(path.join(__dirname, 'JetBrainsMono-Regular.woff2' ))); app.get('/' , (req, res ) => { res.sendFile(path.join(__dirname, 'index.html' )); }); app.delete('/message/:id' , (req, res ) => { const messageId = req.params.id; if (messageId) { db.run("DELETE FROM messages WHERE id=?" , [messageId], (err ) => { if (err) { console .error("从数据库删除消息出错:" , err); res.status(500 ).send({error : "删除消息失败" }); return ; } const index = messageHistory.findIndex(message => message.id === Number (messageId)); if (index !== -1 ) { messageHistory.splice(index, 1 ); } res.send({status : "消息已删除" }); }); } else { res.status(400 ).send({error : "无效的消息ID" }); } });
启动服务器
0x4.3 直接下载已打包好的 避免重复上传已打包文件参考:局域网文件和消息传输
请确保您具有吾爱破解 的账号,且用户组为锋芒初露 ,本帖的权限设置旨在防止第三方网站未经告知私自获取
要继续学习请往下看
0x5 界面图像优化与适配 0x5.1 消息展示与设备自适应 0x5.1.1 设备信息消息中心
美化布局 :使用简单的内联CSS让布局居中
显示多条消息 :而不是只覆盖显示一条
显示设备名称/IP :为简化起见,将让用户输入一个设备名称。关于IP地址,我们可以从请求头中获取
更新index.html
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 <!doctype html > <html > <head > <title > Realtime Text</title > <style > body { font-family : Arial, sans-serif; display : flex; justify-content : center; align-items : center; height : 100vh ; background-color : #f4f4f4 ; } .container { padding : 20px ; border : 1px solid #ddd ; background-color : #fff ; } </style > </head > <body > <div class ="container" > <input id ="deviceName" placeholder ="Enter Device Name" /> <textarea id ="textArea" rows ="4" cols ="50" placeholder ="Type your message..." > </textarea > <button id ="sendButton" > Send</button > <hr > <h3 > Received Messages:</h3 > <ul id ="messages" > </ul > </div > <script src ="/socket.io/socket.io.js" > </script > <script > var socket = io(); document .getElementById('sendButton' ).onclick = function ( ) { var text = document .getElementById('textArea' ).value; var deviceName = document .getElementById('deviceName' ).value || "Unknown" ; socket.emit('send text' , { text, deviceName }); }; socket.on('receive text' , function (data ) { var li = document .createElement("li" ); li.innerText = `[${data.deviceName} ]: ${data.text} ` ; document .getElementById('messages' ).appendChild(li); }); </script > </body > </html >
在server.js
中,我们也要做相应的修改以支持新的消息格式并发送设备的IP地址:
1 2 3 4 5 6 7 8 9 10 11 12 13 io.on('connection' , (socket ) => { console .log('a user connected' ); socket.on('send text' , (data ) => { let clientIpAddress = socket.request.headers['x-forwarded-for' ] || socket.request.connection.remoteAddress; data.ipAddress = clientIpAddress; io.emit('receive text' , data); }); socket.on('disconnect' , () => { console .log('user disconnected' ); }); });
这样,当用户发送消息时,它们将以[DeviceName]: Message
的格式显示,并在服务器端的console中记录IP地址。
我们可以在server.js
中获取连接的客户端的局域网IP地址,并将其与设备名称一同发送到前端。
首先,修改server.js
的消息处理部分以包含IP地址:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 io.on('connection' , (socket ) => { console .log('a user connected' ); socket.on('send text' , (data ) => { let clientIpAddress = socket.request.connection.remoteAddress.replace(/^::ffff:/ , '' ); if (clientIpAddress === '::1' || clientIpAddress === '127.0.0.1' ) { clientIpAddress = 'Localhost' ; } if (!data.deviceName || data.deviceName.trim() === "" ) { data.deviceName = "匿名设备" ; } data.deviceName = `${data.deviceName} (${clientIpAddress} )` ; io.emit('receive text' , data); }); socket.on('disconnect' , () => { console .log('user disconnected' ); }); });
更新index.html
如下:
1 2 3 4 5 6 7 var socket = io();document .getElementById('sendButton' ).onclick = function ( ) { var text = document .getElementById('textArea' ).value; var deviceName = document .getElementById('deviceName' ).value || "匿名设备" ; socket.emit('send text' , { text, deviceName }); };
现在,在发送消息时,如果用户没有输入设备名称,它将显示为“匿名设备”,并且每个消息都会在设备名称后面带有IP地址。
当用户发送消息时,它们将以 [DeviceName (IP Address)]: Message
的格式显示。
0x5.1.2 iPhone页面自适应 现在我希望在iPhone的浏览器中更好的显示,因为我在iPhone中需要先缩小页面,然后在移动到页面中间再放大才能继续输入消息
为了确保页面在移动设备上正常显示,需要添加一个viewport
元数据标签来控制页面的缩放。
在index.html
文件的<head>
部分,添加以下meta
标签:
1 <meta name ="viewport" content ="width=device-width, initial-scale=1.0" >
所以<head>
部分现在应该是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <head > <title > Realtime Text</title > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <style > body { font-family : Arial, sans-serif; display : flex; justify-content : center; align-items : center; height : 100vh ; background-color : #f4f4f4 ; } .container { padding : 20px ; border : 1px solid #ddd ; background-color : #fff ; } </style > </head >
添加此元数据标签后,页面应该会根据设备的屏幕大小自动调整缩放,并且在iPhone等移动设备上应该可以正常显示。
0x5.2 图像优化与传输 0x5.2.1 图像传输逻辑
在前端添加一个文件输入以选择图片
上传图片到服务器
服务器保存图片并返回一个可访问的URL
在客户端中显示图片
修改index.html
来添加文件输入,在<div class="container">
内添加一个文件输入:
1 <input type ="file" id ="imageUpload" accept ="image/*" >
并修改发送按钮的逻辑来检查是否选择了图片:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 document .getElementById('sendButton' ).onclick = function ( ) { var text = document .getElementById('textArea' ).value; var deviceName = document .getElementById('deviceName' ).value || "匿名设备" ; var imageFile = document .getElementById('imageUpload' ).files[0 ]; if (imageFile) { var formData = new FormData(); formData.append('image' , imageFile); formData.append('deviceName' , deviceName); formData.append('text' , text); fetch('/upload-image' , { method : 'POST' , body : formData }).then(response => response.json()).then(data => { socket.emit('send text' , data); }); } else { socket.emit('send text' , { text, deviceName }); } };
multer
在服务器上处理图像上传,它是一个node.js中间件,用于处理multipart/form-data
,主要用于上传文件
然后,在server.js
中添加以下代码来处理图片上传:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const multer = require ('multer' );const path = require ('path' );const storage = multer.diskStorage({ destination : (req, file, cb ) => { cb(null , 'uploads/' ) }, filename : (req, file, cb ) => { cb(null , Date .now() + path.extname(file.originalname)) } }); const upload = multer({ storage : storage });app.post('/upload-image' , upload.single('image' ), (req, res ) => { let imageUrl = `/uploads/${req.file.filename} ` ; res.json({ text : req.body.text, deviceName : req.body.deviceName, imageUrl : imageUrl }); }); app.use('/uploads' , express.static(path.join(__dirname, 'uploads' )));
确保在服务器目录中创建一个名为uploads
的文件夹来存储上传的图片
修改前端显示逻辑,修改socket.on('receive text', ...)
逻辑以处理可能的图片消息:
1 2 3 4 5 6 7 8 9 socket.on('receive text' , function (data ) { var li = document .createElement("li" ); var content = `[${data.deviceName} ]: ${data.text} ` ; if (data.imageUrl) { content += `<br><img src="${data.imageUrl} " alt="Uploaded image" style="max-width: 300px;">` ; } li.innerHTML = content; document .getElementById('messages' ).appendChild(li); });
现在,应该能够在前端选择图片,上传到服务器,并在接收消息时看到图片显示
0x5.2.2 图片显示与优化 现在我发现在发送多条消息时,发送消息的控件显示在屏幕之外时无法再继续输入消息了
这个问题出现的原因是,随着消息的增加,页面内容增长,但浏览器并没有自动滚动以保持输入控件在可见范围内。
为了解决这个问题,我们可以强制页面在添加新消息后滚动到底部。这样,输入框和按钮始终保持在屏幕上的可见范围内
首先更新HTML结构,我们需要对HTML的结构进行一些修改以分离消息列表和输入区域。将输入区域和按钮放入一个固定的底部工具栏中,使其始终位于屏幕底部。
在index.html
中<div class="container">
应该更改为以下结构:
1 2 3 4 5 6 7 8 9 10 <div class ="container" > <ul id ="messages" style ="max-height: 80vh; overflow-y: auto;" > </ul > <div style ="position: fixed; bottom: 0; width: 100%; background-color: white; padding: 10px;" > <input type ="text" id ="deviceName" placeholder ="Device Name" > <textarea id ="textArea" placeholder ="Type your message..." > </textarea > <input type ="file" id ="imageUpload" accept ="image/*" > <button id ="sendButton" > Send</button > </div > </div >
上述更改将消息列表的最大高度限制为视口的80%,并为其添加了滚动条。发送消息的工具栏被固定在底部。
添加自动滚动逻辑,在每次添加消息到列表后,执行滚动到底部的逻辑
在socket.on('receive text', ...)
的末尾添加以下代码:
1 document .getElementById('messages' ).scrollTop = document .getElementById('messages' ).scrollHeight;
这段代码会将消息列表滚动到最底部,确保新消息总是可见的。
这些更改应该确保无论发送多少消息,输入框和按钮始终保持在屏幕上的可见范围内。
消息多发导致标签被遮挡 我们需要对HTML和CSS进行调整以确保”Received Messages:”标签始终可见。
以下是如何修改的,修改HTML结构
在index.html
中,将”Received Messages:”标签从当前的位置移到工具栏上方,并为其添加一个包围元素:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <div class ="container" > <div id ="header" style ="background-color: white; padding: 10px;" > Received Messages: </div > <ul id ="messages" style ="max-height: 70vh; overflow-y: auto;" > </ul > <div style ="position: fixed; bottom: 0; width: 100%; background-color: white; padding: 10px;" > <input type ="text" id ="deviceName" placeholder ="Device Name" > <textarea id ="textArea" placeholder ="Type your message..." > </textarea > <input type ="file" id ="imageUpload" accept ="image/*" > <button id ="sendButton" > Send</button > </div > </div >
调整CSS样式,修改<ul id="messages">
的样式来考虑标题的高度。确保消息列表在标题和工具栏之间:
1 2 3 4 5 #messages { margin-top : 30px ; max-height : 70vh ; overflow-y : auto; }
这些更改应该确保”Received Messages:”标签不会被挡住,并且在添加消息时,列表仍然会正确滚动。
0x5.3 iPhone界面与交互优化 0x5.3.1 点击图片放大原尺寸 为了实现图片点击放大的效果,可以使用一个简单的模态窗口(modal)。当用户点击图片时,模态窗口会显示,并且在其中展示原始尺寸的图片。当用户点击模态窗口之外的任何位置或按下Esc键时,模态窗口会关闭。
在 index.html
的 <body>
末尾添加模态窗口的HTML结构:
1 2 3 4 <div id ="imageModal" style ="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.7); z-index: 9999; align-items: center; justify-content: center;" > <img id ="modalImage" src ="" style ="max-width: 90%; max-height: 90%;" > </div >
添加显示和关闭模态窗口的逻辑,在现有的JavaScript代码下添加以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 document .addEventListener('click' , function (event ) { if (event.target.tagName === 'IMG' && event.target.closest('#messages' )) { var modal = document .getElementById('imageModal' ); var modalImage = document .getElementById('modalImage' ); modalImage.src = event.target.src; modal.style.display = 'flex' ; modal.addEventListener('click' , function ( ) { modal.style.display = 'none' ; }); } }); document .addEventListener('keydown' , function (event ) { if (event.key === 'Escape' ) { var modal = document .getElementById('imageModal' ); modal.style.display = 'none' ; } });
这些代码首先会检查是否点击了消息列表中的图片。如果是,则显示模态窗口并设置图片的源。点击模态窗口的任何位置都会关闭它。如果按下’Escape’键,也会关闭模态窗口。
现在,当点击图片时,应该能够在模态窗口中放大显示图片,而点击模态窗口的任何位置或按下Esc键都会关闭它。
0x5.3.2 iPhone浏览器send按钮超屏 这个问题可能是由于固定底部工具栏的内容太多或样式设置不合适,在某些屏幕尺寸或设备上导致了溢出。为了解决这个问题,我们需要对工具栏的布局和样式进行调整,确保在较小的屏幕上也能适应。
我们可以采用以下策略:
将输入框和发送按钮放在一行。
调整宽度和样式以适应不同的屏幕尺寸。
使用flex布局简化样式和布局。
更新HTML结构,在index.html
中,将工具栏部分修改为:
1 2 3 4 5 6 7 8 <div style ="position: fixed; bottom: 0; width: 100%; background-color: white; padding: 5px; box-shadow: 0 -2px 5px rgba(0,0,0,0.1);" > <div style ="display: flex; align-items: center;" > <input type ="text" id ="deviceName" placeholder ="Device Name" style ="flex: 1; margin-right: 5px;" > <input type ="file" id ="imageUpload" accept ="image/*" style ="margin-right: 5px;" > <textarea id ="textArea" placeholder ="Type your message..." style ="flex: 2; margin-right: 5px;" > </textarea > <button id ="sendButton" > Send</button > </div > </div >
调整样式,确保每个元素都有合适的宽度和间距,使其适应小屏幕。这里,我们使用了flex
布局,让deviceName
和textArea
元素根据剩余空间动态调整其宽度。
现在,工具栏应该能够更好地适应较小的屏幕,如iPhone的浏览器,并且发送按钮应该始终可见和可用。可能需要根据实际设备和屏幕尺寸进行进一步的微调。
0x5.3.3 iPhone输入框超出屏幕 为了在iPhone的浏览器中获得更好的响应式布局,我们可以采用以下策略:
使用媒体查询来针对不同的屏幕尺寸进行样式调整。
在较小的屏幕上,考虑简化布局,可能需要将一些控件移至其他位置。
更新HTML结构,在index.html
中,继续使用上一个给出的工具栏HTML结构。
添加CSS媒体查询,我们将为小屏幕(如iPhone)添加特定的样式。为此,在的CSS中添加以下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #deviceName , #textArea { flex : 1 ; margin-right : 5px ; } @media (max-width : 500px ) { #deviceName { display : none; } #imageUpload { width : 40px ; margin-right : 5px ; } #textArea { flex : 3 ; } #sendButton { flex-shrink : 0 ; } }
这里的做法是,当屏幕宽度小于或等于500px时(可以根据需要调整此值),隐藏了deviceName
输入框以节省空间,并给予textArea
更多的空间。此外,还调整了上传按钮和发送按钮的尺寸和样式,以更好地适应小屏幕。
这些调整应该会改进iPhone浏览器上的布局,使其更加友好,可能需要根据实际设备和屏幕尺寸进行进一步的微调
0x5.3.4 iPhone按钮输入框重叠修正 为了实现这个目标,我们需要对较小屏幕的布局进行改进,使其变为多行排列。
更新HTML结构,在index.html
中,继续使用上一个给出的工具栏HTML结构。
调整CSS媒体查询以实现多行排列布局,我们将在小屏幕上采用多行排列布局。添加或修改以下CSS代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #deviceName , #textArea { flex : 1 ; margin-right : 5px ; } @media (max-width : 500px ) { div [style*="position: fixed; bottom: 0;" ] > div { flex-direction : column; } #deviceName { margin-bottom : 5px ; } #imageUpload { margin-right : 0 ; margin-bottom : 5px ; width : 100% ; } #textArea { margin-right : 0 ; margin-bottom : 5px ; flex : 3 ; } #sendButton { width : 100% ; margin-bottom : 5px ; } }
这样的改变会使得在小屏幕上,设备名称、图片上传、文本输入框和发送按钮都单独占据一行,而不会相互重叠。这应该会在iPhone上提供更好的用户体验
0x5.3.5 iPhone布局对齐与点击优化 更新媒体查询
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @media (max-width : 500px ) { div [style*="position: fixed; bottom: 0;" ] > div { flex-direction : column; } #deviceName , #textArea { margin-right : 0 ; margin-bottom : 5px ; width : 100% ; height : 50px ; } #imageUpload { margin-right : 0 ; margin-bottom : 5px ; width : 100% ; } #sendButton { width : 100% ; height : 50px ; margin-bottom : 5px ; } }
这些调整会使得在小屏幕上,每一个元素都占满了整个屏幕宽度,并且增大了输入框和按钮的高度,以便用户更容易地进行触摸操作。
这应该会提供更好的用户体验,特别是考虑到iPhone和其他移动设备上的拇指操作。可能还需要根据实际设备和需求进行进一步的微调。
0x5.3.6 控件边缘重叠与iPhone点击
控件与屏幕边缘的间距问题
iPhone上首次加载时的交互问题
设置控件与屏幕边缘的间距
为了增加边距,可以为工具栏内部的元素添加适当的间距。在的CSS中,可以添加或修改以下样式:
1 2 3 4 5 6 7 div [style*="position: fixed; bottom: 0;" ] { padding-left : 10px ; padding-right : 10px ; }
这会为底部工具栏的左右两侧增加10px的间距。可以根据需要调整这个值
iPhone上首次加载时的交互问题
iPhone上的Safari浏览器和其他移动浏览器有一个常见的问题,即当页面首次加载时,固定的元素可能不会响应触摸事件。这是因为浏览器的地址栏和底部工具栏可能会覆盖页面的部分内容。通常,通过滚动页面可以暂时解决这个问题。
为了解决这个问题,可以尝试在页面首次加载时自动滚动页面一小段距离,这样可以确保底部工具栏不会被浏览器的界面元素覆盖。以下是如何实现:
在index.html
的<script>
标签内部,添加以下JavaScript代码:
1 2 3 4 5 window .addEventListener('load' , function ( ) { setTimeout (function ( ) { window .scrollTo(0 , 1 ); }, 0 ); });
上述代码会在页面加载完成后,立即将页面向上滚动1px。这样可以确保底部的固定工具栏不会被浏览器的界面元素覆盖,并且应该可以响应用户的触摸事件
0x5.3.7 iPhone上两侧间距问题 对于在iPhone上的边距问题,我们需要更具体地处理与小屏幕或特定设备相关的CSS样式。
下面是如何更具体地针对较小屏幕(如iPhone)设置边距:
首先,确保已经使用了适当的元数据标签来控制视口。在的HTML文件的<head>
部分,添加以下标签:
1 <meta name ="viewport" content ="width=device-width, initial-scale=1.0" >
这会确保页面的宽度匹配设备的宽度,并且页面的初始缩放级别是1.0。
接下来,修改的CSS媒体查询以增加边距。将下面的代码添加或合并到的CSS中:
1 2 3 4 5 6 7 8 9 10 11 12 @media (max-width : 500px ) { div [style*="position: fixed; bottom: 0;" ] { padding : 0 10px ; } }
通过添加这些更改,iPhone上的边距应该会生效,并且页面内容会根据设备的屏幕宽度适当地调整。
0x5.3.8 iPhone输入框宽度异常 这个问题可能是由于padding
和width: 100%
的组合引起的。当我们为一个元素同时设置了padding
和width: 100%
时,该元素的总宽度会超过其父元素的宽度,因为这里的宽度是指内容宽度,而不包括边距、边框或内边距。
为了解决这个问题,可以使用box-sizing
属性,将其设置为border-box
,这样元素的总宽度将包括内容、内边距和边框(但不包括外边距)。
保证边距。
使用flexbox
来确保子元素在小屏幕上垂直排列。
在CSS中,找到与小屏幕相关的媒体查询,并进行如下修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @media (max-width : 500px ) { div [style*="position: fixed; bottom: 0;" ] { width : 100% ; padding : 5px ; box-sizing : border-box; } #deviceName , #textArea , #imageUpload , #sendButton { box-sizing : border-box; width : 100% ; margin-bottom : 5px ; } #deviceName , #textArea { height : 50px ; } #sendButton { height : 50px ; } }
这些样式应该确保了在小屏幕设备上,特别是iPhone上,所有的控件都正常排列,并且有足够的边距。
0x5.3.9 iPhone布局校验与滑动优化
控件应该按照一行行排列 : 这需要调整底部控件的样式。当前的代码中已经使用了flex-direction: column
,这是正确的,但的底部控件的子控件仍然在一个flex容器中,这使得它们在同一行上。需要分离它们以使它们在单独的行上。
两侧与屏幕保持10px的间距 : 可以在底部控件中设置padding-left
和padding-right
为10px
。
第一次打开页面需要滚动一小段距离 : 已经有了这个功能,使用window.scrollTo(0, 1)
在页面加载时向下滚动。这通常用于隐藏浏览器的地址栏,但在最新版本的iOS中可能不起作用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @media (max-width : 500px ) { div [style*="position: fixed; bottom: 0;" ] { padding-left : 10px ; padding-right : 10px ; display : flex; flex-direction : column; } #deviceName , #textArea , #imageUpload , #sendButton { width : 100% ; box-sizing : border-box; margin-bottom : 5px ; } #deviceName , #textArea , #sendButton { height : 50px ; } }
1 2 3 4 5 6 7 8 9 <div style ="position: fixed; bottom: 0; width: 100%; background-color: white; padding: 5px; box-shadow: 0 -2px 5px rgba(0,0,0,0.1);" > <input type ="text" id ="deviceName" placeholder ="Device Name" > <input type ="file" id ="imageUpload" accept ="image/*" > <textarea id ="textArea" placeholder ="Type your message..." > </textarea > <button id ="sendButton" > Send</button > </div >
0x5.3.A iPhone布局遮挡与间距修正
两侧控件的距离问题 : 如果右侧和屏幕之间的距离是10px,但左侧并非如此,可能是某些样式或其他浏览器默认样式干扰了。可以考虑为每个子控件设置margin-left
和margin-right
,以确保他们都有正确的边距。
图片消息被底部的输入框挡住 : 一个常见的方法是为消息列表添加底部内边距,该边距的高度与底部工具栏的高度相同,这样最后一条消息(无论是文本还是图片)都可以完全滚动到视图中而不被挡住。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @media (max-width : 500px ) { #deviceName , #textArea , #imageUpload , #sendButton { margin-left : 10px ; margin-right : 10px ; } #messages { padding-bottom : 70px ; } }
0x5.4 消息发送与设备名自动填充 0x5.4.1 发送后清除已选图像 为了在发送完消息后清除上次选择的图像,可以在发送消息的函数中清除 <input type="file">
的值。这将重置文件输入,并确保下次用户点击发送按钮时不会再次发送同一张图片。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 document .getElementById('sendButton' ).onclick = function ( ) { var text = document .getElementById('textArea' ).value; var deviceName = document .getElementById('deviceName' ).value || "匿名设备" ; var imageFile = document .getElementById('imageUpload' ).files[0 ]; if (imageFile) { var formData = new FormData(); formData.append('image' , imageFile); formData.append('deviceName' , deviceName); formData.append('text' , text); fetch('/upload-image' , { method : 'POST' , body : formData }).then(response => response.json()).then(data => { socket.emit('send text' , data); document .getElementById('imageUpload' ).value = "" ; }); } else { socket.emit('send text' , { text, deviceName }); } };
以上的修改确保了每次发送图像消息后,文件输入都会被清除。这样,用户在发送下一条消息时,之前选择的图片不会再次被发送
0x5.4.2 自动填充设备名称 为了自动获取设备名称,首先需要明确一个事实:Web 浏览器出于隐私和安全的考虑,并不允许直接获取设备的真实名称。但是,可以使用一些方法来近似识别或标识设备,例如使用浏览器的用户代理字符串。
用户代理 (User Agent) 字符串包含关于浏览器、操作系统和设备的信息,可以通过这些信息来推断或标识用户的设备类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 function getDeviceNameFromUserAgent ( ) { var userAgent = navigator.userAgent || navigator.vendor || window .opera; if (/windows phone/i .test(userAgent)) { return "Windows Phone" ; } if (/android/i .test(userAgent)) { return "Android Device" ; } if (/iPad|iPhone|iPod/ .test(userAgent) && !window .MSStream) { return "iOS Device" ; } if (/macintosh|mac os x/i .test(userAgent)) { return "Mac" ; } if (/windows/i .test(userAgent)) { return "Windows PC" ; } return "Unknown Device" ; } document .getElementById('deviceName' ).value = getDeviceNameFromUserAgent();
此函数根据用户代理字符串来近似地识别设备,并返回一个描述性名称。注意,这只是一个基本的示例,而真实的环境中可能需要更复杂的逻辑来准确识别设备。
这种方法只是提供了一个近似的设备名称,并不能真正的识别具体的设备型号或品牌
0x5.4.3 消息发送后自动清空输入框 为了在发送完消息后自动清除 textArea
(你之前的代码中使用这个ID代表消息输入框)中的值,你可以在发送消息的函数中添加一行代码来实现这个功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 document .getElementById('sendButton' ).onclick = function ( ) { var text = document .getElementById('textArea' ).value; var deviceName = document .getElementById('deviceName' ).value || "匿名设备" ; var imageFile = document .getElementById('imageUpload' ).files[0 ]; if (imageFile) { var formData = new FormData(); formData.append('image' , imageFile); formData.append('deviceName' , deviceName); formData.append('text' , text); fetch('/upload-image' , { method : 'POST' , body : formData }).then(response => response.json()).then(data => { socket.emit('send text' , data); }); } else { socket.emit('send text' , { text, deviceName }); } document .getElementById('textArea' ).value = '' ; document .getElementById('imageUpload' ).value = '' ; };
我在函数的底部添加了清除textArea
和图片选择器imageUpload
的代码。这样,无论是否选择了图片,都会在发送完消息后清除文本框和图片选择器的内容。
0x5.5 大屏适配与控件布局调整 0x5.5.1 Windows居中 iPhone逐行 修改CSS以在大屏幕上对底部控件进行水平居中:
1 2 3 4 5 6 7 8 9 10 11 12 .bottom-controls { display : flex; justify-content : center; } .bottom-controls > div { display : flex; align-items : center; }
使用媒体查询来特定地为小屏幕(例如iPhone)定义样式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @media (max-width : 500px ) { .bottom-controls { justify-content : flex-start; } .bottom-controls > div { flex-direction : column; align-items : center; width : 100% ; } #deviceName , #textArea , #imageUpload , #sendButton { display : block; width : 100% ; } }
这样,底部的控件在大屏幕上会水平居中,而在小屏幕(如iPhone)上,则会按照行排列。
0x5.5.2 扩大大屏幕底部控件尺寸 为了在大屏幕上使控件更大,可以使用媒体查询来区分大屏幕和小屏幕的样式。在大屏幕的样式中,可以增加控件的高度和字体大小。
1 2 3 4 5 6 7 8 9 10 11 @media (min-width : 501px ) { #deviceName , #textArea , #imageUpload , #sendButton { height : 60px ; font-size : 18px ; padding : 10px ; } #sendButton { padding : 10px 20px ; } }
这段代码将确保在宽度超过501px的屏幕上,控件的高度、字体大小和内部间距都增加了,从而更适合大屏幕使用。可以根据实际需要进一步调整这些值。
0x5.5.3 大屏幕输入框长度及控件间距 为了在大屏幕上调整输入框的长度并在输入框和按钮之间添加合适的距离,可以进行以下修改:
在大屏幕上为输入框和按钮添加合适的间距
为大屏幕上的输入框增加宽度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @media (min-width : 501px ) { #deviceName , #textArea , #imageUpload , #sendButton { height : 60px ; font-size : 18px ; padding : 10px ; margin-right : 10px ; } #sendButton { padding : 10px 20px ; } #textArea { width : 60% ; } div [style*="position: fixed; bottom: 0;" ] > div > *:last-child { margin-right : 0 ; } }
这段代码增加了#textArea
的宽度并在所有底部控件之间添加了margin-right
。为了确保最后一个元素(按钮)不与其父元素的右边界有太大的间隔,我添加了一个特定选择器来移除其右边距
0x5.6 增加深浅色主题
在HTML中添加一个切换按钮。
为深色模式定义CSS样式。
使用JavaScript监听按钮点击事件来切换主题。
更新index.html
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 [data-theme="dark" ] input , [data-theme="dark" ] textarea , [data-theme="dark" ] button { color : white; border-color : white; } :root { --text-color : black; --background-color : white; --input -bg: #fff ; --container-bg: #fff ; --header -bg: #fff ; --button -color : black; } [data-theme="dark" ] { --text-color : black; --background-color : white; } .bottom-controls { display : flex; justify-content : center; } .bottom-controls > div { display : flex; align-items : center; } div [style*="position: fixed; bottom: 0;" ] { padding-left : 10px ; padding-right : 10px ; } body { font-family : Arial, sans-serif; display : flex; justify-content : center; align-items : center; height : 100vh ; background-color : var (--background-color) !important ; color : var (--text-color); font-family : "JetBrainsMono" , Arial, sans-serif; } .element { color : var (--text-color); background-color : var (--background-color); } .container { padding : 20px ; border : 1px solid #ddd ; background-color : #fff ; } @media (max-width : 500px ) { .bottom-controls { justify-content : flex-start; } .bottom-controls > div { flex-direction : column; align-items : center; width : 100% ; } div [style*="position: fixed; bottom: 0;" ] { background-color : var (--container-bg); padding-left : 10px ; padding-right : 10px ; display : flex; flex-direction : column; } #deviceName , #textArea , #imageUpload , #sendButton { background-color : var (--container-bg); width : 100% ; box-sizing : border-box; margin-bottom : 5px ; } #deviceName , #textArea , #sendButton { height : 50px ; } #messages { padding-bottom : 70px ; } } @media (min-width : 501px ) { #deviceName , #textArea , #imageUpload , #sendButton { background-color : var (--input-bg); height : 60px ; font-size : 18px ; padding : 10px ; margin-bottom : 10px ; } #textArea { width : 50% ; } #sendButton { padding : 10px 20px ; margin-left : 10px ; } } body , button { background-color : var (--bg-color); color : var (--text-color); } button { color : var (--button-color); } .bottom-controls input , .bottom-controls textarea , .bottom-controls button { color : var (--text-color); border-color : var (--text-color); } .container { background-color : var (--container-bg); } body .dark-theme { --text-color : white; --background-color : #555 ; --input -bg: #555 ; --container-bg: #444 ; --header -bg: #444 ; --button -color : white; } #header { background-color : var (--header-bg); }
1 2 3 4 document .getElementById('themeToggle' ).addEventListener('click' , function ( ) { document .body.classList.toggle('dark-theme' ); });
现在,当点击”Toggle Theme”按钮,页面的主题应该会在深色和浅色模式之间切换,并且文本应该会根据相应的模式改变颜色。
0x6 导入自定义字体
选择字体 : 首先,需要一个字体文件。这可以是.ttf
、.woff
、.woff2
、.eot
等格式。确保拥有使用该字体的权利,特别是如果打算在商业项目中使用。
字体文件上传 : 将字体文件上传到的服务器或CDN,并确保知道文件的URL。
在CSS中导入字体 : 在CSS文件或<style>
标签中,使用@font-face
规则来定义和导入字体。
下载JetBrains Mono 字体,解压JetBrainsMono-Regular.ttf
、JetBrainsMono-Regular.woff2
到网站根目录
1 2 3 4 5 6 7 @font-face { font-family : "JetBrainsMono" ; src : url ('JetBrainsMono-Regular.woff2' ) format ('woff2' ), /* 最优先 */ url ('JetBrainsMono-Regular.ttf' ) format ('truetype' ); font-weight : normal; font-style : normal; }
在这里,path-to-your-font
应该是字体文件的URL。如果有多种格式的字体文件,列出它们都是一个好主意,因为这可以确保更好的兼容性
1 2 3 4 5 body { font-family : "JetBrainsMono" , Arial, sans-serif; }
在这里,"MyCustomFont"
是我们为自定义字体定义的名字,Arial, sans-serif
是后备字体,用于在某些情况下(例如,如果自定义字体加载失败)
考虑性能:字体文件可能很大,所以考虑只包括真正需要的字体权重和样式。例如,如果只需要常规和加粗,那么不要加载斜体或其他权重
0x7 添加自动加载历史记录功能 如果你想添加一个自动加载历史记录的功能,你需要在服务器端存储这些历史消息。这样,当新用户连接时,你就可以将这些历史消息发送给他们。
更新server.js
如下:
在服务器上添加一个变量来存储消息
1 const messageHistory = [];
当接收到新的文本消息时,将它添加到历史记录中
1 2 3 4 5 6 7 8 socket.on('send text' , (data ) => { messageHistory.push(data); io.emit('receive text' , data); });
当新用户连接时,发送所有历史消息给他们
1 2 3 4 5 6 7 8 9 10 io.on('connection' , (socket ) => { console .log('a user connected' ); messageHistory.forEach((message ) => { socket.emit('receive text' , message); }); });
index.html
在客户端的 JavaScript 里,你不需要做任何修改。因为客户端已经设置了一个监听函数来处理 ‘receive text’ 事件,所以当它接收到历史消息时,会自动将其添加到消息列表。
这样,当有新用户加入聊天时,服务器就会自动发送所有历史消息给他们
0x8 添加消息代码格式化和时间戳 为了在消息中添加发送时间,可以在服务器端获取当前时间,并将其与其他数据一起发送。同时,也需要在客户端的JavaScript中修改接收数据的部分,以显示这一时间。
首先,在 server.js
的 'send text'
事件处理程序中添加发送时间:
1 2 3 4 5 6 7 8 9 10 11 socket.on('send text' , (data ) => { data.timestamp = new Date ().toLocaleString(); messageHistory.push(data); io.emit('receive text' , data); });
修改 index.html
在接收消息部分,添加代码格式支持和时间戳
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 socket.on('receive text' , function (data ) { var li = document .createElement("li" ); var time = data.timestamp || new Date ().toLocaleString(); var content = `[${data.deviceName} @ ${time} ]: ` ; if (data.text.startsWith("```" ) && data.text.endsWith("```" )) { content += `<pre>${data.text.slice(3 , -3 )} </pre>` ; } else { content += `${data.text} ` ; } if (data.imageUrl) { content += `<br><img src="${data.imageUrl} " alt="Uploaded image" style="max-width: 300px;">` ; } li.innerHTML = content; document .getElementById('messages' ).appendChild(li); });
这样,如果用户在 textArea
输入的文本以 **”```”** 开头和结尾,这段文本将会被认为是代码,并用 <pre>
标签进行包裹,以维持其格式。同时,每条消息也会显示发送时间。
0x9 增加消息删除与数据库 0x9.1 增加消息删除 创建一个复选框要对应每条消息,所以必须设置每条消息和复选框的ID保持一致,在选择复选框时应该同时删除后面的消息
修改index.html
文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 var messageId = 0 ; socket.on('receive text' , function (data ) { var li = document .createElement("li" ); var checkbox = document .createElement("input" ); checkbox.type = "checkbox" ; checkbox.className = "messageCheckbox" ; li.appendChild(checkbox); var time = data.timestamp || new Date ().toLocaleString(); var content = `[${data.deviceName} @ ${time} ]: ` ; if (data.text.startsWith("```" ) && data.text.endsWith("```" )) { content += `<pre>${data.text.slice(3 , -3 )} </pre>` ; } else { content += `${data.text} ` ; } if (data.imageUrl) { content += `<br><img src="${data.imageUrl} " alt="Uploaded image" style="max-width: 300px;">` ; } li.innerHTML += content; document .getElementById('messages' ).appendChild(li); li.setAttribute('data-message-id' , messageId); messageId++; console .log("设备名称和IP地址:" , data.deviceName); });
向服务器发送删除请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 document .getElementById('deleteButton' ).onclick = function ( ) { var selectedMessages = document .querySelectorAll('.messageCheckbox:checked' ); var messageIdsToDelete = []; selectedMessages.forEach(function (checkbox ) { var li = checkbox.closest('li' ); messageIdsToDelete.push(li.getAttribute('data-message-id' )); li.remove(); }); socket.emit('delete messages' , messageIdsToDelete); messageIdsToDelete.forEach(id => { fetch(`/message/${id} ` , { method : 'DELETE' }).then(response => { if (!response.ok) { console .error('从服务器删除消息失败' ); } }); }); };
向所有客户端广播消息ID,以供删除
1 2 3 4 5 6 7 socket.on('delete messages' , function (messageIds ) { messageIds.forEach(function (messageId ) { var li = document .querySelector('li[data-message-id="' + messageId + '"]' ); if (li) li.remove(); }); io.emit('delete messages' , messageIds); });
0x9.2 前端数据库联动删消息 0x9.2.1 创建数据库 修改server.js
文件
1 const db = new sqlite3.Database('./messages.db' );
0x9.2.2 创建表 创建message表,如果存在则什么都不做,包含id、content、deviceName、 ipAddress、 timestamp字段
1 2 3 4 5 6 7 db.run("CREATE TABLE IF NOT EXISTS messages (id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT, deviceName TEXT, ipAddress TEXT, timestamp TEXT)" , (err ) => { if (err) { console .error("创建/检查表错误:" , err); return ; } console .log("表检查/创建成功" ); });
0x9.2.3 读取数据库所有消息 从数据库中查询所有的消息记录,并将每条消息存储到messageHistory
数组中,若查询过程中出现错误,会在控制台中打印错误信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 db.all("SELECT id, content, deviceName, ipAddress, timestamp FROM messages" , [], (err, rows ) => { if (err) { console .error("从数据库获取历史消息出错:" , err); return ; } rows.forEach(row => { const message = { id : row.id, text : row.content, deviceName : row.deviceName, ipAddress : row.ipAddress, timestamp : row.timestamp }; messageHistory.push(message); }); console .log("从数据库加载历史消息" ); });
0x9.2.4 保存前端消息到数据库 定义了一个HTTP POST路由/send
,用于从请求体中接收消息内容,并在消息内容存在时将其存储到数据库中,若消息内容为空,则返回一个错误响应
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 app.post('/send' , (req, res ) => { const messageContent = req.body.message; if (messageContent) { db.run("INSERT INTO messages (content, deviceName, ipAddress, timestamp) VALUES (?, ?, ?, ?)" , [data.text, data.deviceName, clientIpAddress, data.timestamp], (err ) => { if (err) { console .error("插入数据库出错:" , err); return ; } console .log('收到的信息:' , data.text || messageContent); }); } else { res.status(400 ).send({error : "消息内容为空" }); } });
0x9.2.5 广播所有消息给用户 当一个用户与服务器建立Socket连接时的操作:首先打印出用户已连接的消息,然后将存储在messageHistory
数组中的每条历史消息(经过修改以显示设备名和IP地址)发送给新连接的用户
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 io.on('connection' , (socket ) => { console .log('有一个用户连接' ); function formatMessageForEmission (originalMessage ) { return { ...originalMessage, deviceName : `${originalMessage.deviceName} (${originalMessage.ipAddress} )` }; } messageHistory.forEach((message ) => { socket.emit('receive text' , formatMessageForEmission(message)); });
0x9.2.6 广播Socket客户端消息 当服务器通过Socket接收到客户端发送的send text
事件时,首先获取和处理客户端的IP地址,为没有提供设备名称的消息设置默认名称,然后将消息及相关信息添加到数据库中。之后,会修改消息的设备名称,将新消息添加到历史记录数组,并通过Socket广播这条消息给所有连接的客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 socket.on('send text' , (data ) => { let clientIpAddress = socket.request.connection.remoteAddress.replace(/^::ffff:/ , '' ); if (clientIpAddress === '::1' || clientIpAddress === '127.0.0.1' ) { clientIpAddress = 'Localhost' ; } data.ipAddress = clientIpAddress; if (!data.deviceName || data.deviceName.trim() === "" ) { data.deviceName = "匿名设备" ; } data.timestamp = new Date ().toLocaleString(); db.run("INSERT INTO messages (content, deviceName, ipAddress, timestamp) VALUES (?, ?, ?, ?)" , [data.text, data.deviceName, clientIpAddress, data.timestamp], function (err ) { if (err) { console .error("插入数据库出错:" , err); return ; } data.id = this .lastID; messageHistory.push(data); console .log('通过套接字收到的消息:' , data.text); } ); io.emit('receive text' , formatMessageForEmission(data)); });
0x9.2.7 处理DELETE请求删数据库消息 当收到HTTP DELETE请求时,根据指定的消息ID从数据库中删除相应的消息记录;若删除过程中出现错误,则返回错误信息,否则通知请求者消息已被删除。如果未提供有效的消息ID,则返回“无效的消息ID”错误。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 app.delete('/message/:id' , (req, res ) => { const messageId = req.params.id; if (messageId) { db.run("DELETE FROM messages WHERE id=?" , [messageId], (err ) => { if (err) { console .error("从数据库删除消息出错:" , err); res.status(500 ).send({error : "删除消息失败" }); return ; } const index = messageHistory.findIndex(message => message.id === Number (messageId)); if (index !== -1 ) { messageHistory.splice(index, 1 ); } res.send({status : "消息已删除" }); }); } else { res.status(400 ).send({error : "无效的消息ID" }); } });
0xA Docker封装与测试 部署Node.js网站到QNAP NAS上的一个优雅的方式是使用Docker。这种方法提供了隔离,便于管理和升级,而不会影响主机系统
0xA.1 Docker环境准备 0xA.1.1 创建Dockerfile
打开终端或命令提示符 :导航到Node.js项目的根目录。
创建一个新的Dockerfile :
如果使用的是Linux或Mac,可以在终端中输入以下命令:
如果在Windows上,可以使用命令提示符或PowerShell并输入以下命令:
编辑Dockerfile :
使用喜欢的文本编辑器打开新创建的Dockerfile。例如,可以使用VS Code、Notepad++、vim等。
在Dockerfile中输入内容 :
复制上文给出的Dockerfile内容,并粘贴到的Dockerfile中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 FROM node:14 AS build-stageWORKDIR /usr/src/app COPY package*.json ./ RUN npm install COPY . . EXPOSE 3000 CMD ["npm" , "start" ]
保存并关闭文件
0xA.1.2 构建Docker镜像 在项目的目录中执行以下命令,以构建一个Docker镜像:
1 docker build -t realtimetext .
要确保的镜像已经正确构建,可以使用以下命令列出所有本地的Docker镜像:
0xA.1.3 运行Docker容器 确保Docker已安装并正在运行,然后运行以下命令以启动的Node.js应用:
1 docker run -p 4000:3000 realtime_text
0xA.2 导出容器上传并运行 0xA.2.1 导出Docker容器 首先,需要确定一个有效的路径来保存tar文件
查看当前运行的docker ID
将容器提交为新的Docker镜像
1 docker commit [CONTAINER_ID] realtime_text
这应该会将的realtime_text
镜像保存为realtime_text.tar
文件,位于docker-images/
目录下
1 docker save -o docker-images/realtime_text.tar realtime_text
0xA.2.2 上传Docker容器
需要确保在QNAP中的FileStation创建一个名为Container
的目录
在App Center中搜索并安装Container Station
,等待服务启动完成就可以打开了
打开映像 页面,点击右上角的导入 ,从本地计算机 选择刚导出的Docker镜像文件realtime_text ,再点击导入
0xA.2.3 运行Docker容器 在容器页面启动名为realtime_text 的容器,查看其详细信息及Web URL:http://192.168.50.145:32769,就是部署后的网页地址
0xA.3 维护容器占用空间 长期发送图片消息会导致upload目录占用较大空间,必要情况下可以在容器页面选择容器后的设置按钮,选择执行/bin/sh
控制台
0xB 常见问题与解决方案 0xB.1 构建失败
0xB.2 端口占用 在运行时报告了一个端口占用错误
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 “node:events:491 throw er; // Unhandled 'error' event ^ Error: listen EADDRINUSE: address already in use :::3000 at Server.setupListenHandle [as _listen2] (node:net:1485:16) at listenInCluster (node:net:1533:12) at Server.listen (node:net:1621:7) at Object.<anonymous> (D:\code\win-code\NodeJS\realtime_text\server.js:60:8) at Module._compile (node:internal/modules/cjs/loader:1159:14) at Module._extensions..js (node:internal/modules/cjs/loader:1213:10) at Module.load (node:internal/modules/cjs/loader:1037:32) at Module._load (node:internal/modules/cjs/loader:878:12) at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12) at node:internal/main/run_main_module:23:47 Emitted 'error' event on Server instance at: at emitErrorNT (node:net:1512:8) at process.processTicksAndRejections (node:internal/process/task_queues:82:21) { code: 'EADDRINUSE' , errno: -4091, syscall: 'listen' , address: '::' , port: 3000 } Node.js v18.12.1”
这个错误 Error: listen EADDRINUSE: address already in use :::3000
意味着端口 3000 已经被其他程序或者相同的程序的另一个实例使用了。你有几种方式来解决这个问题:
杀掉使用该端口的进程
在 Windows 上:
打开命令行窗口,然后运行 netstat -ano | findstr :3000
。
这会显示使用端口 3000 的所有进程。找到进程 ID(PID)。
执行 taskkill /PID <PID> /F
来杀掉进程。
在 macOS 或 Linux 上:
打开终端,然后运行 lsof -i :3000
。
这会显示使用端口 3000 的所有进程。找到进程 ID(PID)。
执行 kill -9 <PID>
来杀掉进程。
更改应用程序使用的端口
在你的 server.js
文件中,找到下面这行代码:
1 const port = process.env.PORT || 3000 ;
你可以更改 3000 到其他未被使用的端口号,例如:
1 const port = process.env.PORT || 4000 ;
然后重新运行你的应用程序。
重启计算机
如果上面的方法没有解决问题或者你不确定如何进行,重启计算机通常能够解决端口冲突的问题。
希望这能帮到你!
0xB.3 数据库自增ID列 DataGrip 是一个流行的数据库管理工具,允许你通过其图形界面来进行各种数据库操作。要在 DataGrip 中修改表的 id
列为自增,可以按照以下步骤进行:
打开 DataGrip 并连接到你的 SQLite 数据库。
在左侧的 数据库浏览器 中,找到你的表。
右键点击 你想要修改的表,然后选择 Modify Table 。
在打开的窗口中,找到 id
列。
选中 id
列,然后在右侧的属性面板中找到 Identity 或 Auto Increment 选项并勾选。
右键点击 该列名,并从上下文菜单中选择 Set Primary Key 。
最后,点击窗口下方的 提交 或 应用 按钮来保存更改。
请注意,不同的数据库系统和不同的版本可能会有些微差异。确保在进行此类操作之前备份你的数据。
如果 DataGrip 不直接支持 SQLite 的自增特性或有任何其他问题,你可能需要使用 SQL 语句来手动修改表结构,或者像之前所描述的那样,通过创建一个新表、复制数据、删除旧表、然后重命名新表的方式来达到目的。
0xB.4 确保前端页面与数据库内容同步 前端消息的ID与数据库ID对应。若前端删除消息失败,可能是ID不匹配或Docker中的sqlite3库问题。若需手动删除消息,以下是相关sql语句。不使用Docker容器可避免该问题,并注意Docker会将24小时制改为12小时制
0xB.4.1 连接数据库
0xB.4.2 查询表
0xB.4.3 查ID列 1 SELECT id FROM messages;
0xB.4.4 删除指定ID行 1 DELETE FROM messages WHERE id = 5;
0xB.4.5 删除指定多行ID 1 DELETE FROM messages WHERE id IN (3, 4, 5);
0xB.4.6 查询指定ID列的值 1 SELECT * FROM messages WHERE id = 5;
0xB.4.7 查询指定多行ID列的值 1 SELECT * FROM messages WHERE id IN (3, 4, 5);
0xB.4.8 修改指定ID 在这个语句中,18
替代了第一个 ?
,21
替代了第二个 ?
,而 21
替代了第三个 ?
。这样的话,当你执行这个SQL语句时,它会将 messages
表中 rowid
为 21,且 id
为 21 的行的 id
字段的值更新为 18。
1 UPDATE "messages" SET "id" = 18 WHERE "rowid" = 21 AND "id" = 21
0xB.4.9 SSH连接正在运行的docker 1 docker exec -it <container_id_or_name> /bin/bash
0xB.4.A docker中安装vim 1 2 apt-get update apt-get install -y vim
0xB.4.B 重置id列自增值 查询当前最大值
1 SELECT MAX(id) FROM messages;
将自增ID的SEQ设置为最大ID值: 使用以下SQL语句来设置自增ID列的序列(sequence)为当前最大ID值
1 UPDATE sqlite_sequence SET seq = 31 WHERE name = 'messages';
删除消息后需重启docker容器生效
0xC 运行效果 等一会,加载图片比较慢
0xC.1 大屏幕设备
0xC.2 小屏幕设备