跳到主要内容

跑团辅助工具开发日志 2

· 阅读需 11 分钟

两个多星期过去了,这段时间一直在进行前端开发,在此记录一下开发进度。

已经开发的内容和值得记录的东西还挺多的,所以这篇可能会稍微有点长。

设计思路

边开发边进一步深化了一下整体的设计思路。目前计划的应用架构如下:

  1. 前后端分离,分开部署,前端为React SPA应用,后端为ASP.NET应用
  2. 我会在自己的阿里云上部署一份官方的前端和一份官方的后端服务器,但其他开发者可以通过开源代码自行部署后端服务器
  3. 要连接第三方后端,只需要在官方前端进行服务器地址的配置,这样可以支持灵活开发,当然想要重新开发部署第三方前端也是OK的
  4. 只要前后端api协议及版本兼容,可以任意前端与任意后端互联

开发进展

整个前端应用分为大三步:

玩家必须先登录服务器(用户体系由每个后端服务器运营者维护,例如不能用官方服务器的账密登录某个第三方服务器), 然后从服务器信息页面找到要加入的游戏,或者创建游戏。加入或创建游戏后跳转到游戏界面。

登录页面

登录页面是用于进入服务器之前,在这一步,除了会向指定的后端服务器请求服务器名和欢迎标语以外,均与具体后端服务器内容无关。

在这个页面,可以做三件事:登录注册配置后端服务器地址

登录

标准登录表单,没什么好多说的~

登录

其中“官方服务器”是后端返回的服务器名称,“欢迎来到JoySun的官方服务器”是欢迎标语。这些计划都是可以在后端进行配置的。

忘记密码功能目前还未实现,因为如果不接入一些第三方验证渠道(比如邮箱、手机等)的话,就只能使用密保问题来做忘记密码。 但是我并不是很想弄密保问题这些的,所以后续更新再说~😁

配置后端服务器地址

配置地址

配置的时候会对地址进行连接校验,如果成功连上了指定的服务器,才会保存更改。

我给所有的配置进行了一个抽象:

export type ConfigObject={
[key:string]:string|number|boolean
};

export type ConfigDescriptor={
type:'string'|'number'|'toggle'|'choice'|'range',
title:string,
extras?:{[key: string]:any},
rules?:Rule[]
}

type Config<T extends ConfigObject>={
defaultValue:T,
descriptors:Record<keyof T,ConfigDescriptor>,
storageKey:string
}

export default Config;

export function getConfigValue<T extends ConfigObject>(config:Config<T>){
const storedValue=localStorage.getItem(config.storageKey);
if(storedValue){
return JSON.parse(storedValue) as T;
}else{
return config.defaultValue;
}
}

export function setConfigValue<T extends ConfigObject>(config:Config<T>,value:ConfigObject){
localStorage.setItem(config.storageKey,JSON.stringify(value));
}

export function resetConfigValue<T extends ConfigObject>(config:Config<T>){
localStorage.removeItem(config.storageKey);
}

这样一来,可以方便得为所有配置类型设定默认值、localStorage的键,以及类型描述符。(其中Rule类型为antd表单的校验规则类型)

以服务器连接配置为例,导出一个ConnectionConfig变量:

import Config from './configs';

export type ConnectionConfigObject={
serverAddr:string
}

const ConnectionConfig: Config<ConnectionConfigObject>={
storageKey:"connConfig",
defaultValue:{
serverAddr:process.env.serverBaseUrl??"http://localhost:8000/api"
},
descriptors:{
serverAddr:{
type:'string',
title:"服务器地址",
rules:[
{required:true,message:"请输入服务器地址"}
],
extras:{
disabled: !(process.env.serverAddrAllowChange??true)
}
}
}
}

export default ConnectionConfig;

使用的时候也非常简单:

getConfigValue(ConnectionConfig); //获取当前配置值
setConfigValue(ConnectionConfig,{serverAddr:"xxxx"}); //设置配置值

除此之外,上面截图中的弹出窗口也是通用化配置的,只要传入一个Config<T>的实例,就能自动生成响应的antd对话框和表单。

仍然以上面的连接配置为例:

<ConfigModal title="连接设置" show={/*外部控制对话框开关*/} config={ConnectionConfig}
onFinish={(changed)=> {
//配置对话框关闭后动作,changed表明是否有变化
}}
onValidation={async (newConfig)=>{
//返回一个Promise<boolean>,来对配置内容的有效性进行校验
}}/>

简单明了。

注册

也是标准的注册表单,不必多言。

注册

当时开发到一半给注册加了一个功能,就是后端可以配置是否允许公开注册,这样可以给第三方服务器运营提供邀请制这样的效果。

当然,为了避免注册API被滥用,验证码也是有的。

验证码


登录态

目前设计是基于自定义会话的登录态,会话token保存在sessionStorage中。

在登录页面触发登录或注册后,Api.Auth.loginApi.Auth.register接口成功返回将携带session token,写入sessionStorage, 并跳转到服务器信息页面。整个服务器信息页面及其子路由都有一个AuthWrapper包裹,用来校验登录态。如果发现未登录,则跳转到登录页面。

由于sessionStorage的储存本身并没有超时之类的,因此计划采用服务端超时。当需要登录态校验的Api返回Auth.E002状态码时,认为是会话token超时, 自动跳转到登录页。

这里插空感谢一下ahooks,它提供的钩子真是太好用了

对于组件中使用登录态,我封装了一个hooks:

import Api from "@/api/api";
import React, {useCallback} from "react";
import {useSessionStorageState} from "ahooks";

type SessionContextType={
token:string,
loginAsync:(username:string,password:string)=>Promise<void>,
logoutAsync:()=>Promise<void>,
setToken:(newToken:string)=>void,
verifyLoginAsync:()=>Promise<boolean>;
}

const TOKEN_KEY="ses_token";

const SessionContext=React.createContext<SessionContextType>({
token:"",
loginAsync:async ()=>{},
logoutAsync:async ()=>{},
setToken:()=>{},
verifyLoginAsync:async ()=>{return false},
});

export default function Session(props:{children:React.ReactNode}){
const [localToken,setLocalToken]=useSessionStorageState(TOKEN_KEY,{
defaultValue:"",
serializer:(value)=>value,
deserializer:(value)=>value
});

const login=useCallback(async (username:string,password:string)=>{
const sessionToken=await Api.Auth.login(username,password);
setLocalToken(sessionToken);
},[]);

const logout=useCallback(async ()=>{
await Api.Auth.logout();
setLocalToken("");
},[]);

const verifyToken=useCallback(async ()=>{
if(!localToken){
return false;
}
return await Api.Auth.checkLogin();
},[localToken]);

return React.createElement(SessionContext.Provider, {
value: {
token: localToken,
setToken: setLocalToken,
loginAsync: login,
logoutAsync: logout,
verifyLoginAsync: verifyToken
}
},props.children);
}

Session.useSession=function(){
return React.useContext(SessionContext);
}

Session.getLocalToken=function(){
return sessionStorage.getItem(TOKEN_KEY)??"";
}

使用时可以直接通过Session.useSession()引入React组件:

const {token,loginAsync}=Session.useSession();
//...
if(!token){
loginAsync(/*...*/);
}

这块也是做改造的时候现学的哈哈,之前也不懂很多React的概念。每次开发新项目都是对自己知识面的扩充呀~😏


服务器信息页面

从这里开始,大部分的信息都是从服务端获取的了。

对于普通玩家来说,服务器信息页面共有4个子页面:游戏大厅社群好友创作中心个人设置。对于管理员用户,还有一个管理中心

游戏大厅

游戏大厅主要展示服务器名、轮播当前的服务器公告、展示游戏房间列表、提供创建和加入游戏的入口。

游戏大厅

这个子页面目前功能差不多实现了80%左右,主要是剩一个创建房间的流程没做。当然,进入游戏界面的跳转也还没做,毕竟整个游戏界面都还没整哈哈😁

玩家可以根据房间是否需要邀请码(叫密码也行)、是否仅限好友、是否满员、进行状态对所有房间进行筛选。目前所有房间是一次性加载出来的(mock), 没有做分页。因为这种随时可能变动的列表做分页……感觉不是很妥当。这个后面再斟酌。

个人设置

这个子页面可以对用户昵称和头像进行修改(头像只能用内置的,并不打算做上传自定义头像功能),可以修改密码(如果后面加入邮箱或手机验证之类的也放到这), 可以查看缓存占用情况,以及应用版本号等。

缓存的内容目前有两部分:

  1. 一些认为不经常变动的信息(例如服务器公告等这些详情),降低API请求量。如果要强制刷新那直接从浏览器刷新页面即可
  2. 静态文件。例如轮播图,以及之后会有的一些规则描述文件、玩家上传的文件等,降低OSS流量消耗。

个人设置

至于使用的开源软件清单,之后差不多完工了会单独在仓库外层放一个说明文档,这里到时候直接外链跳转过去。

当然,之后随着配置项的增多,这里可能还会新增内容,那就到时候再说吧~


那么这次开发日志就更新到这了,草草结束,我实在是懒(逃跑

这两周半开发了这么些内容,接下来再看看是先做社群好友还是先做游戏界面。

做游戏界面的话,存在一个问题,就是umi.js框架自带的mock并不支持websocket,只支持http,所以不得不自己写一个后端来mock……有点烦躁哈哈

那下次开发日志再见吧!🎈