试用前排查:WebSocket 心跳与断线重连、登录失败按账号限制、旧浏览器提示
- 服务器 30 秒发一次 ping:Nginx Proxy Manager 默认 60 秒没数据就断开,窗体开着不操作程序会被结束 - 连接意外断开时提示、清掉失效的窗体、自动重连,连上后提示重新运行 - 登录失败:同一账号 10 次只锁这个账号,同一 IP 300 次,登录成功不再清零 IP 计数 - index.html:浏览器太旧时说明原因和解决办法,加载慢时提示稍等 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
982b1297aa
commit
27fdc3615d
8 files changed
+137
-23
No files matched your search
@@ -8,5 +8,35 @@
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
<!--
|
||||
浏览器太旧时(机房的旧版 Chrome、360 浏览器的兼容模式),网页程序运行不起来,页面一片空白。
|
||||
这段用最老的写法,旧浏览器也能执行:网页程序出了语法错误时说明原因;15 秒后还没显示出来(网络慢),
|
||||
先告诉学生在加载。网页程序启动后会替换掉这些文字。
|
||||
-->
|
||||
<script>
|
||||
;(function () {
|
||||
var tooOld = false
|
||||
function show(title, text) {
|
||||
var app = document.getElementById('app')
|
||||
// Vue 启动后会在容器上加 data-v-app,那时不再显示
|
||||
if (!app || app.hasAttribute('data-v-app')) return
|
||||
app.innerHTML =
|
||||
'<div style="max-width:560px;margin:80px auto;padding:0 16px;font:16px/1.8 sans-serif;color:#333">' +
|
||||
'<h2 style="margin:0 0 12px">' + title + '</h2><p>' + text + '</p>' +
|
||||
'<p>用 360 浏览器、QQ 浏览器时,请点地址栏右边的闪电图标,切换到<b>极速模式</b>;或者换用新版的 <b>Chrome</b>、<b>Edge</b>。' +
|
||||
'还是打不开的话,请把下面这行文字告诉老师:</p>' +
|
||||
'<p style="font-size:13px;color:#888;word-break:break-all">' + navigator.userAgent.replace(/</g, '<') + '</p></div>'
|
||||
}
|
||||
window.addEventListener('error', function (event) {
|
||||
if (event && event.error instanceof SyntaxError) {
|
||||
tooOld = true
|
||||
show('网页没能打开', '浏览器版本太旧,运行不了本网站。')
|
||||
}
|
||||
})
|
||||
setTimeout(function () {
|
||||
if (!tooOld) show('网页正在加载…', '第一次打开要下载约 1.5 MB,全班同时打开时会慢一些,请稍等。如果一直停在这里,可能是浏览器版本太旧。')
|
||||
}, 15000)
|
||||
})()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -79,7 +79,11 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKey))
|
||||
:box="box"
|
||||
@reply="session.replyMessageBox(box.requestId, $event)"
|
||||
/>
|
||||
<div v-if="state.forms.length === 0" class="desktop-hint">点击"运行"后,学生程序的窗体会显示在这里</div>
|
||||
<div v-if="state.connection === 'lost'" class="desktop-hint lost">与服务器的连接断开了,正在重新连接…</div>
|
||||
<div v-else-if="state.connection === 'restored' && state.forms.length === 0" class="desktop-hint lost">
|
||||
连接已恢复。刚才的程序随连接断开已经结束,请重新运行
|
||||
</div>
|
||||
<div v-else-if="state.forms.length === 0" class="desktop-hint">点击"运行"后,学生程序的窗体会显示在这里</div>
|
||||
<div class="desktop-tools">
|
||||
<span v-if="scale < 1" class="desktop-scale" title="窗体比运行区大,按比例缩小显示;点"放大"可以看得更清楚">
|
||||
缩放 {{ Math.round(scale * 100) }}%
|
||||
@@ -219,6 +223,10 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKey))
|
||||
justify-content: center;
|
||||
color: rgb(255 255 255 / 0.7);
|
||||
}
|
||||
.desktop-hint.lost {
|
||||
color: #ffd591;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.photos {
|
||||
font-size: 12px;
|
||||
|
||||
+34
-1
@@ -45,6 +45,11 @@ export function createRunSession() {
|
||||
/** 正在播放的声音(媒体播放器控件,实训台上的广播喇叭随之亮起) */
|
||||
playing: null as { name: string } | null,
|
||||
metrics: { compileMs: 0, startMs: 0, lastRoundTripMs: 0 },
|
||||
/**
|
||||
* 和服务器的连接:lost 表示意外断开(网络不稳、服务器在更新),正在自动重连;
|
||||
* restored 表示断开后又连上了(提示学生刚才的程序已经结束),下次运行时变回 open
|
||||
*/
|
||||
connection: 'connecting' as 'connecting' | 'open' | 'lost' | 'restored',
|
||||
})
|
||||
/** id → 控件节点,patch 按 id 修改节点属性 */
|
||||
const nodes = shallowReactive(new Map<string, ControlNode>())
|
||||
@@ -54,6 +59,12 @@ export function createRunSession() {
|
||||
/** 正在建立或已建立的连接;同时多处调用 connect() 时共用同一个 */
|
||||
let connecting: Promise<void> | undefined
|
||||
let actionSentAt = 0
|
||||
/** 离开页面时主动断开,不再重连 */
|
||||
let closed = false
|
||||
let reconnectTimer: ReturnType<typeof setTimeout> | undefined
|
||||
/** 连续重连失败的次数,等待时间随之加长(最长 RECONNECT_MAX_MS) */
|
||||
let reconnectFailures = 0
|
||||
const RECONNECT_MAX_MS = 10_000
|
||||
/** 每个媒体播放器控件一个 <audio> */
|
||||
const players = new Map<string, HTMLAudioElement>()
|
||||
|
||||
@@ -92,12 +103,27 @@ export function createRunSession() {
|
||||
const ws = new WebSocket(`${protocol}://${location.host}/ws?token=${encodeURIComponent(token ?? '')}`)
|
||||
socket = ws
|
||||
ws.onmessage = (event) => handle(JSON.parse(event.data) as ServerMessage)
|
||||
ws.onopen = () => resolve()
|
||||
ws.onopen = () => {
|
||||
state.connection = state.connection === 'lost' ? 'restored' : 'open'
|
||||
reconnectFailures = 0
|
||||
resolve()
|
||||
}
|
||||
ws.onclose = () => {
|
||||
// 服务器那边的程序随连接一起结束了;窗体留在页面上只会"点了没反应"
|
||||
if (!closed && (state.forms.length > 0 || state.status === 'compiling' || state.status === 'running')) {
|
||||
state.errors.push('与服务器的连接断开了(网络不稳或服务器正在更新),程序已经结束。连上之后请重新运行。')
|
||||
}
|
||||
stopAudio()
|
||||
state.forms = []
|
||||
state.messageBoxes = []
|
||||
state.status = 'stopped'
|
||||
state.board = null
|
||||
socket = undefined
|
||||
connecting = undefined
|
||||
if (!closed) {
|
||||
state.connection = 'lost'
|
||||
reconnectTimer = setTimeout(() => void connect(), Math.min(1000 * 2 ** reconnectFailures++, RECONNECT_MAX_MS))
|
||||
}
|
||||
resolve()
|
||||
}
|
||||
}),
|
||||
@@ -112,6 +138,11 @@ export function createRunSession() {
|
||||
/** sampleId:当前实验(服务器据此在第一次运行时建好实验预置的数据库) */
|
||||
async function run(files: SourceFile[], sampleId?: string) {
|
||||
await connect()
|
||||
if (socket?.readyState !== WebSocket.OPEN) {
|
||||
state.errors = ['连不上服务器,请检查网络,稍后再点"运行"。']
|
||||
return
|
||||
}
|
||||
state.connection = 'open'
|
||||
state.diagnostics = []
|
||||
state.console = ''
|
||||
state.errors = []
|
||||
@@ -251,6 +282,8 @@ export function createRunSession() {
|
||||
|
||||
/** 离开页面时断开连接:服务器随即结束正在运行的程序 */
|
||||
function close() {
|
||||
closed = true
|
||||
clearTimeout(reconnectTimer)
|
||||
socket?.close()
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user