上节实现了注册的接口。
这节在前端代码里集成一下。
创建 src/api/register.js
/**
* 用户注册 API。开发环境通过 Vite 代理访问 localhost:3000;生产可设 VITE_API_BASE。
*/
function getApiBase() {
const base = import.meta.env.VITE_API_BASE;
if (base) {
return String(base).replace(/\/$/, '');
}
return '/api';
}
/**
* POST /user/register
* @returns {Promise<{ id: number, username: string, createdAt: string }>}
*/
export async function registerUser(username, password) {
const url = `${getApiBase()}/user/register`;
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
let data = {};
try {
data = await res.json();
} catch {
/* ignore */
}
if (!res.ok) {
const msg =
data.message ||
data.error ||
(typeof data === 'string' ? data : null) ||
`请求失败 (${res.status})`;
throw new Error(msg);
}
return data;
}
这里的 base url 在 vite.config.js 里配置:
import { defineConfig } from 'vite'
export default defineConfig({
// base: '/threejs-open-world/',
server: {
proxy: {
// 开发时前端请求 /api/* 转发到后端,路径去掉 /api 前缀 → 与 curl localhost:3000/user/register 一致
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
build: {
outDir: 'dist',
assetsDir: 'assets',
minify: false,
}
})
然后在界面加一下这部分 html:

<!-- 注册(结构在 HTML;显示/隐藏仅用 hidden 属性 + CSS,不由 JS 创建节点) -->
<div id="registerPanel" class="register-panel" hidden>
<div class="settings-content register-content">
<div class="settings-header">
<h2>注册</h2>
<button id="closeRegisterBtn" type="button" class="close-btn">关闭 (ESC)</button>
</div>
<div class="settings-body">
<form id="registerForm" class="register-form">
<div class="settings-item register-field">
<label for="registerUsername">用户名</label>
<input id="registerUsername" name="username" type="text" autocomplete="username" required minlength="1" />
</div>
<div class="settings-item register-field">
<label for="registerPassword">密码</label>
<input id="registerPassword" name="password" type="password" autocomplete="new-password" required minlength="1" />
</div>
<div class="register-actions">
<button type="submit" id="registerSubmitBtn" class="save-btn">注册</button>
</div>
<p id="registerMessage" class="register-message" role="status" hidden></p>
</form>
</div>
</div>
</div>
<!-- 设置按钮 -->
<button id="registerBtn" class="register-btn" type="button" title="注册">注册</button>
在 main.js 写一下这部分逻辑:
代码比较多,你可以直接从 github 仓库复制代码:

import { registerUser } from './api/register.js';

export let isRegisterOpen = false;

if (isRegisterOpen) {
isRegisterOpen = false;
const registerPanel = document.getElementById('registerPanel');
if (registerPanel) registerPanel.hidden = true;
const registerMsg = document.getElementById('registerMessage');
if (registerMsg) {
registerMsg.hidden = true;
registerMsg.textContent = '';
registerMsg.className = 'register-message';
}
}

function toggleRegister() {
const registerPanel = document.getElementById('registerPanel');
if (!registerPanel) return;
const willOpen = !isRegisterOpen;
if (willOpen) {
if (isSettingsOpen) {
isSettingsOpen = false;
const settingsPanel = document.getElementById('settingsPanel');
if (settingsPanel) settingsPanel.style.display = 'none';
}
if (isManualOpen) {
isManualOpen = false;
const manualPanel = document.getElementById('manualPanel');
if (manualPanel) manualPanel.style.display = 'none';
}
}
isRegisterOpen = !isRegisterOpen;
registerPanel.hidden = !isRegisterOpen;
if (isRegisterOpen && document.pointerLockElement) {
document.exitPointerLock();
}
const registerMsg = document.getElementById('registerMessage');
if (!isRegisterOpen && registerMsg) {
registerMsg.hidden = true;
registerMsg.textContent = '';
registerMsg.className = 'register-message';
}
}

const registerPanel = document.getElementById('registerPanel');
const registerBtn = document.getElementById('registerBtn');
const closeRegisterBtn = document.getElementById('closeRegisterBtn');
const registerForm = document.getElementById('registerForm');
if (registerPanel) {
registerPanel.addEventListener('mousedown', (e) => e.stopPropagation());
registerPanel.addEventListener('click', (e) => e.stopPropagation());
}
if (registerBtn) {
registerBtn.addEventListener('click', (e) => {
e.stopPropagation();
toggleRegister();
});
}
if (closeRegisterBtn) {
closeRegisterBtn.addEventListener('click', (e) => {
e.stopPropagation();
toggleRegister();
});
}
if (registerForm) {
registerForm.addEventListener('submit', async (e) => {
e.preventDefault();
const usernameEl = document.getElementById('registerUsername');
const passwordEl = document.getElementById('registerPassword');
const msgEl = document.getElementById('registerMessage');
const submitBtn = document.getElementById('registerSubmitBtn');
const username = usernameEl ? usernameEl.value.trim() : '';
const password = passwordEl ? passwordEl.value : '';
if (!username || !password) {
if (msgEl) {
msgEl.hidden = false;
msgEl.textContent = '请填写用户名和密码';
msgEl.className = 'register-message register-error';
}
return;
}
if (msgEl) {
msgEl.hidden = true;
msgEl.textContent = '';
msgEl.className = 'register-message';
}
if (submitBtn) submitBtn.disabled = true;
try {
const data = await registerUser(username, password);
if (msgEl) {
msgEl.hidden = false;
const t = data.createdAt
? new Date(data.createdAt).toLocaleString('zh-CN')
: '';
msgEl.textContent = t
? `注册成功:${data.username}(id: ${data.id})· ${t}`
: `注册成功:${data.username}(id: ${data.id})`;
msgEl.className = 'register-message register-success';
}
} catch (err) {
if (msgEl) {
msgEl.hidden = false;
msgEl.textContent = err instanceof Error ? err.message : '注册失败';
msgEl.className = 'register-message register-error';
}
} finally {
if (submitBtn) submitBtn.disabled = false;
}
});
}
试下效果:

注册成功!
去数据库看一下:

这样,注册功能就完成了。
总结
这节我们加上了注册的前端界面。
代码比较多,可以直接从仓库复制。
现在可以注册账号,下节来做登录的功能。