From 72677e270d293c110969b3f67e2ff31bcc4682c4 Mon Sep 17 00:00:00 2001 From: "meilin.huang" <954537473@qq.com> Date: Sat, 16 Sep 2023 17:07:48 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=89=8D=E7=AB=AF=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=E8=BF=81=E7=A7=BB=E8=87=B3localstorage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mayfly_go_web/package.json | 2 +- mayfly_go_web/src/common/request.ts | 10 +-- mayfly_go_web/src/common/sockets.ts | 4 +- mayfly_go_web/src/common/utils/storage.ts | 62 ++++++++++++++----- mayfly_go_web/src/common/utils/wartermark.ts | 6 +- mayfly_go_web/src/router/index.ts | 8 +-- mayfly_go_web/src/store/userInfo.ts | 4 +- .../views/layout/navBars/breadcrumb/user.vue | 2 +- .../views/login/component/AccountLogin.vue | 11 ++-- mayfly_go_web/src/views/ops/db/DbList.vue | 4 +- .../src/views/ops/db/component/tab/Query.vue | 4 +- .../src/views/ops/db/table/DbTableList.vue | 6 +- mayfly_go_web/src/views/ops/machine/api.ts | 4 +- .../views/ops/machine/file/MachineFile.vue | 4 +- mayfly_go_web/src/views/personal/index.vue | 4 +- mayfly_go_web/vite.config.ts | 1 + mayfly_go_web/yarn.lock | 8 +-- server/pkg/ws/client.go | 29 ++++++--- server/pkg/ws/client_manager.go | 6 +- server/pkg/ws/ws.go | 1 + 20 files changed, 110 insertions(+), 70 deletions(-) diff --git a/mayfly_go_web/package.json b/mayfly_go_web/package.json index 3d12337d..4dd85576 100644 --- a/mayfly_go_web/package.json +++ b/mayfly_go_web/package.json @@ -15,7 +15,7 @@ "countup.js": "^2.7.0", "cropperjs": "^1.5.11", "echarts": "^5.4.0", - "element-plus": "^2.3.12", + "element-plus": "^2.3.14", "jsencrypt": "^3.3.1", "lodash": "^4.17.21", "mitt": "^3.0.1", diff --git a/mayfly_go_web/src/common/request.ts b/mayfly_go_web/src/common/request.ts index ce540313..a644dbb2 100755 --- a/mayfly_go_web/src/common/request.ts +++ b/mayfly_go_web/src/common/request.ts @@ -1,7 +1,7 @@ import router from '../router'; import Axios from 'axios'; import config from './config'; -import { getSession } from './utils/storage'; +import { getToken } from './utils/storage'; import { templateResolve } from './utils/string'; import { ElMessage } from 'element-plus'; @@ -50,7 +50,7 @@ const service = Axios.create({ service.interceptors.request.use( (config: any) => { // do something before request is sent - const token = getSession('token'); + const token = getToken(); if (token) { // 设置token config.headers['Authorization'] = token; @@ -143,8 +143,8 @@ function request(method: string, url: string, params: any = null, headers: any = .request(query) .then((res) => res) .catch((e) => { - // 如果返回的code不为成功,则会返回对应的错误msg,则直接统一通知即可 - if (e.msg) { + // 如果返回的code不为成功,则会返回对应的错误msg,则直接统一通知即可。忽略登录超时或没有权限的提示(直接跳转至401页面) + if (e.msg && e?.code != ResultEnum.NO_PERMISSION) { notifyErrorMsg(e.msg); } return Promise.reject(e); @@ -176,7 +176,7 @@ function del(url: string, params: any = null, headers: any = null, options: any function getApiUrl(url: string) { // 只是返回api地址而不做请求,用在上传组件之类的 - return baseUrl + url + '?token=' + getSession('token'); + return baseUrl + url + '?token=' + getToken(); } export default { diff --git a/mayfly_go_web/src/common/sockets.ts b/mayfly_go_web/src/common/sockets.ts index 171360f5..cb678605 100644 --- a/mayfly_go_web/src/common/sockets.ts +++ b/mayfly_go_web/src/common/sockets.ts @@ -1,14 +1,14 @@ import Config from './config'; import { ElNotification } from 'element-plus'; import SocketBuilder from './SocketBuilder'; -import { getSession } from '@/common/utils/storage'; +import { getToken } from '@/common/utils/storage'; export default { /** * 全局系统消息websocket */ sysMsgSocket() { - const token = getSession('token'); + const token = getToken(); if (!token) { return null; } diff --git a/mayfly_go_web/src/common/utils/storage.ts b/mayfly_go_web/src/common/utils/storage.ts index 5ff91b4f..021a6f7b 100644 --- a/mayfly_go_web/src/common/utils/storage.ts +++ b/mayfly_go_web/src/common/utils/storage.ts @@ -1,17 +1,58 @@ +const TokenKey = 'token'; +const UserKey = 'user'; + +// 获取请求token +export function getToken(): string { + return getLocal(TokenKey); +} + +// 保存用户访问token +export function saveToken(token: string) { + setLocal(TokenKey, token); +} + +// 获取登录用户基础信息 +export function getUser() { + return getLocal(UserKey); +} + +// 保存用户信息 +export function saveUser(userinfo: any) { + setLocal(UserKey, userinfo); +} + +// 获取是否开启水印 +export function getUseWatermark() { + return getLocal('useWatermark'); +} + +export function saveUseWatermark(useWatermark: boolean) { + setLocal('useWatermark', useWatermark); +} + +// 清楚用户相关的用户信息 +export function clearUser() { + removeLocal(TokenKey); + removeLocal(UserKey); +} + // 1. localStorage // 设置永久缓存 export function setLocal(key: string, val: any) { window.localStorage.setItem(key, JSON.stringify(val)); } + // 获取永久缓存 export function getLocal(key: string) { let json: any = window.localStorage.getItem(key); return JSON.parse(json); } + // 移除永久缓存 export function removeLocal(key: string) { window.localStorage.removeItem(key); } + // 移除全部永久缓存 export function clearLocal() { window.localStorage.clear(); @@ -22,33 +63,20 @@ export function clearLocal() { export function setSession(key: string, val: any) { window.sessionStorage.setItem(key, JSON.stringify(val)); } + // 获取临时缓存 export function getSession(key: string) { let json: any = window.sessionStorage.getItem(key); return JSON.parse(json); } + // 移除临时缓存 export function removeSession(key: string) { window.sessionStorage.removeItem(key); } + // 移除全部临时缓存 export function clearSession() { + clearUser(); window.sessionStorage.clear(); } - -export function getUserInfo4Session() { - return getSession('userInfo'); -} - -export function setUserInfo2Session(userinfo: any) { - setSession('userInfo', userinfo); -} - -// 获取是否开启水印 -export function getUseWatermark4Session() { - return getSession('useWatermark'); -} - -export function setUseWatermark2Session(useWatermark: boolean) { - setSession('useWatermark', useWatermark); -} diff --git a/mayfly_go_web/src/common/utils/wartermark.ts b/mayfly_go_web/src/common/utils/wartermark.ts index e62bd40c..05db3b09 100644 --- a/mayfly_go_web/src/common/utils/wartermark.ts +++ b/mayfly_go_web/src/common/utils/wartermark.ts @@ -1,4 +1,4 @@ -import { getUseWatermark4Session, getUserInfo4Session } from '@/common/utils/storage'; +import { getUseWatermark, getUser } from '@/common/utils/storage'; import { dateFormat2 } from '@/common/utils/date'; // 页面添加水印效果 @@ -44,8 +44,8 @@ function del() { const watermark = { use: () => { setTimeout(() => { - const userinfo = getUserInfo4Session(); - if (userinfo && getUseWatermark4Session()) { + const userinfo = getUser(); + if (userinfo && getUseWatermark()) { set(`${userinfo.username} ${dateFormat2('yyyy-MM-dd HH:mm:ss', new Date())}`); } else { del(); diff --git a/mayfly_go_web/src/router/index.ts b/mayfly_go_web/src/router/index.ts index 40fe156a..dc3a04d8 100644 --- a/mayfly_go_web/src/router/index.ts +++ b/mayfly_go_web/src/router/index.ts @@ -1,7 +1,7 @@ import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'; import NProgress from 'nprogress'; import 'nprogress/nprogress.css'; -import { getSession, clearSession } from '@/common/utils/storage'; +import { clearSession, getToken } from '@/common/utils/storage'; import { templateResolve } from '@/common/utils/string'; import { NextLoading } from '@/common/utils/loading'; import { dynamicRoutes, staticRoutes, pathMatch } from './route'; @@ -30,7 +30,7 @@ const router = createRouter({ // 前端控制路由:初始化方法,防止刷新时丢失 export function initAllFun() { NextLoading.start(); // 界面 loading 动画开始执行 - const token = getSession('token'); // 获取浏览器缓存 token 值 + const token = getToken(); // 获取浏览器缓存 token 值 if (!token) { // 无 token 停止执行下一步 return false; @@ -49,7 +49,7 @@ export function initAllFun() { // 后端控制路由:执行路由数据初始化 export async function initBackEndControlRoutesFun() { NextLoading.start(); // 界面 loading 动画开始执行 - const token = getSession('token'); // 获取浏览器缓存 token 值 + const token = getToken(); // 获取浏览器缓存 token 值 if (!token) { // 无 token 停止执行下一步 return false; @@ -256,7 +256,7 @@ router.beforeEach(async (to, from, next) => { to.meta.title = templateResolve(to.meta.title as string, to.query); } - const token = getSession('token'); + const token = getToken(); if ((to.path === '/login' || to.path == '/oauth2/callback') && !token) { next(); NProgress.done(); diff --git a/mayfly_go_web/src/store/userInfo.ts b/mayfly_go_web/src/store/userInfo.ts index 451c4f18..957ed7ba 100644 --- a/mayfly_go_web/src/store/userInfo.ts +++ b/mayfly_go_web/src/store/userInfo.ts @@ -1,5 +1,5 @@ import { defineStore } from 'pinia'; -import { getSession } from '@/common/utils/storage'; +import { getUser } from '@/common/utils/storage'; export const useUserInfo = defineStore('userInfo', { state: (): UserInfoState => ({ @@ -8,7 +8,7 @@ export const useUserInfo = defineStore('userInfo', { actions: { // 设置用户信息 async setUserInfo(data: object) { - const ui = getSession('userInfo'); + const ui = getUser(); if (ui) { this.userInfo = ui; } else { diff --git a/mayfly_go_web/src/views/layout/navBars/breadcrumb/user.vue b/mayfly_go_web/src/views/layout/navBars/breadcrumb/user.vue index f4067305..47342c18 100644 --- a/mayfly_go_web/src/views/layout/navBars/breadcrumb/user.vue +++ b/mayfly_go_web/src/views/layout/navBars/breadcrumb/user.vue @@ -83,7 +83,7 @@ import { resetRoute } from '@/router/index'; import { storeToRefs } from 'pinia'; import { useUserInfo } from '@/store/userInfo'; import { useThemeConfig } from '@/store/themeConfig'; -import { clearSession, setLocal, getLocal, removeLocal } from '@/common/utils/storage'; +import { clearUser, clearSession, setLocal, getLocal, removeLocal } from '@/common/utils/storage'; import UserNews from '@/views/layout/navBars/breadcrumb/userNews.vue'; import SearchMenu from '@/views/layout/navBars/breadcrumb/search.vue'; import mittBus from '@/common/utils/mitt'; diff --git a/mayfly_go_web/src/views/login/component/AccountLogin.vue b/mayfly_go_web/src/views/login/component/AccountLogin.vue index 3c955fa6..88de413a 100644 --- a/mayfly_go_web/src/views/login/component/AccountLogin.vue +++ b/mayfly_go_web/src/views/login/component/AccountLogin.vue @@ -132,7 +132,7 @@ import { nextTick, onMounted, ref, toRefs, reactive, computed } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import { ElMessage } from 'element-plus'; import { initRouter } from '@/router/index'; -import { getSession, setSession, setUserInfo2Session, setUseWatermark2Session } from '@/common/utils/storage'; +import { saveToken, saveUser, saveUseWatermark } from '@/common/utils/storage'; import { formatAxis } from '@/common/utils/format'; import openApi from '@/common/openApi'; import { RsaEncrypt } from '@/common/rsa'; @@ -142,6 +142,7 @@ import { useUserInfo } from '@/store/userInfo'; import QrcodeVue from 'qrcode.vue'; import { personApi } from '@/views/personal/api'; import { AccountUsernamePattern } from '@/common/pattern'; +import { getToken } from '../../../common/utils/storage'; const rules = { username: [{ required: true, message: '请输入用户名', trigger: 'blur' }], @@ -354,7 +355,7 @@ const loginResDeal = (loginRes: any) => { }; // 存储用户信息到浏览器缓存 - setUserInfo2Session(userInfos); + saveUser(userInfos); // 1、请注意执行顺序(存储用户信息到vuex) useUserInfo().setUserInfo(userInfos); @@ -376,10 +377,10 @@ const loginResDeal = (loginRes: any) => { // 登录成功后的跳转 const signInSuccess = async (accessToken: string = '') => { if (!accessToken) { - accessToken = getSession('token'); + accessToken = getToken(); } // 存储 token 到浏览器缓存 - setSession('token', accessToken); + saveToken(accessToken); // 初始化路由 await initRouter(); @@ -405,7 +406,7 @@ const toIndex = async () => { state.loading.signIn = true; ElMessage.success(`${currentTimeInfo},欢迎回来!`); if (await useWartermark()) { - setUseWatermark2Session(true); + saveUseWatermark(true); } }, 300); }; diff --git a/mayfly_go_web/src/views/ops/db/DbList.vue b/mayfly_go_web/src/views/ops/db/DbList.vue index c11fb216..1de92282 100644 --- a/mayfly_go_web/src/views/ops/db/DbList.vue +++ b/mayfly_go_web/src/views/ops/db/DbList.vue @@ -172,7 +172,7 @@ import { ref, toRefs, reactive, onMounted, defineAsyncComponent } from 'vue'; import { ElMessage, ElMessageBox } from 'element-plus'; import { dbApi } from './api'; import config from '@/common/config'; -import { getSession } from '@/common/utils/storage'; +import { getToken } from '@/common/utils/storage'; import { isTrue } from '@/common/assert'; import { Search as SearchIcon } from '@element-plus/icons-vue'; import { dateFormat } from '@/common/utils/date'; @@ -406,7 +406,7 @@ const dumpDbs = () => { 'href', `${config.baseApiUrl}/dbs/${state.exportDialog.dbId}/dump?db=${state.exportDialog.value.join(',')}&type=${type}&extName=${ state.exportDialog.extName - }&token=${getSession('token')}` + }&token=${getToken()}` ); a.click(); state.exportDialog.visible = false; diff --git a/mayfly_go_web/src/views/ops/db/component/tab/Query.vue b/mayfly_go_web/src/views/ops/db/component/tab/Query.vue index c2e894f3..a8e30ada 100644 --- a/mayfly_go_web/src/views/ops/db/component/tab/Query.vue +++ b/mayfly_go_web/src/views/ops/db/component/tab/Query.vue @@ -92,7 +92,7 @@ import { nextTick, watch, onMounted, reactive, toRefs, ref, Ref } from 'vue'; import { storeToRefs } from 'pinia'; import { useThemeConfig } from '@/store/themeConfig'; -import { getSession } from '@/common/utils/storage'; +import { getToken } from '@/common/utils/storage'; import { isTrue, notBlank } from '@/common/assert'; import { format as sqlFormatter } from 'sql-formatter'; import config from '@/common/config'; @@ -148,7 +148,7 @@ const props = defineProps({ }); const { themeConfig } = storeToRefs(useThemeConfig()); -const token = getSession('token'); +const token = getToken(); let monacoEditor = {} as editor.IStandaloneCodeEditor; const dbTableRef = ref(null) as Ref; diff --git a/mayfly_go_web/src/views/ops/db/table/DbTableList.vue b/mayfly_go_web/src/views/ops/db/table/DbTableList.vue index 4ef09aaa..fa551036 100644 --- a/mayfly_go_web/src/views/ops/db/table/DbTableList.vue +++ b/mayfly_go_web/src/views/ops/db/table/DbTableList.vue @@ -124,7 +124,7 @@ import { formatByteSize } from '@/common/utils/format'; import { dbApi } from '../api'; import SqlExecBox from '../component/SqlExecBox'; import config from '@/common/config'; -import { getSession } from '@/common/utils/storage'; +import { getToken } from '@/common/utils/storage'; import { isTrue } from '@/common/assert'; const DbTableEdit = defineAsyncComponent(() => import('./DbTableEdit.vue')); @@ -259,9 +259,7 @@ const dump = (db: string) => { const a = document.createElement('a'); a.setAttribute( 'href', - `${config.baseApiUrl}/dbs/${props.dbId}/dump?db=${db}&type=${state.dumpInfo.type}&tables=${state.dumpInfo.tables.join(',')}&token=${getSession( - 'token' - )}` + `${config.baseApiUrl}/dbs/${props.dbId}/dump?db=${db}&type=${state.dumpInfo.type}&tables=${state.dumpInfo.tables.join(',')}&token=${getToken()}` ); a.click(); state.showDumpInfo = false; diff --git a/mayfly_go_web/src/views/ops/machine/api.ts b/mayfly_go_web/src/views/ops/machine/api.ts index 7f7a8d60..a33bde27 100644 --- a/mayfly_go_web/src/views/ops/machine/api.ts +++ b/mayfly_go_web/src/views/ops/machine/api.ts @@ -1,6 +1,6 @@ import Api from '@/common/Api'; import config from '@/common/config'; -import { getSession } from '@/common/utils/storage'; +import { getToken } from '@/common/utils/storage'; export const machineApi = { // 获取权限列表 @@ -63,5 +63,5 @@ export const cronJobApi = { }; export function getMachineTerminalSocketUrl(machineId: any) { - return `${config.baseWsUrl}/machines/${machineId}/terminal?token=${getSession('token')}`; + return `${config.baseWsUrl}/machines/${machineId}/terminal?token=${getToken()}`; } diff --git a/mayfly_go_web/src/views/ops/machine/file/MachineFile.vue b/mayfly_go_web/src/views/ops/machine/file/MachineFile.vue index 08740767..1921d811 100755 --- a/mayfly_go_web/src/views/ops/machine/file/MachineFile.vue +++ b/mayfly_go_web/src/views/ops/machine/file/MachineFile.vue @@ -272,7 +272,7 @@ import { ref, toRefs, reactive, onMounted, computed } from 'vue'; import { ElMessage, ElMessageBox, ElInput } from 'element-plus'; import { machineApi } from '../api'; -import { getSession } from '@/common/utils/storage'; +import { getToken } from '@/common/utils/storage'; import config from '@/common/config'; import { isTrue } from '@/common/assert'; import MachineFileContent from './MachineFileContent.vue'; @@ -285,7 +285,7 @@ const props = defineProps({ isFolder: { type: Boolean, default: true }, }); -const token = getSession('token'); +const token = getToken(); const folderUploadRef: any = ref(); const folderType = 'd'; diff --git a/mayfly_go_web/src/views/personal/index.vue b/mayfly_go_web/src/views/personal/index.vue index 97029232..e69ad2d4 100644 --- a/mayfly_go_web/src/views/personal/index.vue +++ b/mayfly_go_web/src/views/personal/index.vue @@ -179,7 +179,7 @@ import { dateFormat } from '@/common/utils/date'; import { storeToRefs } from 'pinia'; import { useUserInfo } from '@/store/userInfo'; import config from '@/common/config'; -import { getSession } from '@/common/utils/storage'; +import { getToken } from '@/common/utils/storage'; const { userInfo } = storeToRefs(useUserInfo()); const state = reactive({ @@ -248,7 +248,7 @@ const bindOAuth2 = () => { var iLeft = (window.screen.width - 10 - width) / 2; //获得窗口的水平位置; // 小窗口打开oauth2鉴权 let oauthWindow = window.open( - config.baseApiUrl + '/auth/oauth2/bind?token=' + getSession('token'), + config.baseApiUrl + '/auth/oauth2/bind?token=' + getToken(), 'oauth2', `height=${height},width=${width},top=${iTop},left=${iLeft},location=no` ); diff --git a/mayfly_go_web/vite.config.ts b/mayfly_go_web/vite.config.ts index f0eb2020..b026b42d 100644 --- a/mayfly_go_web/vite.config.ts +++ b/mayfly_go_web/vite.config.ts @@ -49,6 +49,7 @@ const viteConfig: UserConfig = { manualChunks: { vue: ['vue', 'vue-router', 'pinia'], echarts: ['echarts'], + monaco: ['monaco-editor'], }, }, }, diff --git a/mayfly_go_web/yarn.lock b/mayfly_go_web/yarn.lock index fbefdb36..5e92f7cb 100644 --- a/mayfly_go_web/yarn.lock +++ b/mayfly_go_web/yarn.lock @@ -804,10 +804,10 @@ echarts@^5.4.0: tslib "2.3.0" zrender "5.4.0" -element-plus@^2.3.12: - version "2.3.12" - resolved "https://registry.npmmirror.com/element-plus/-/element-plus-2.3.12.tgz#d3c91d0c701b2b3e67d06a351cb0c42dcc46460e" - integrity sha512-fAWpbKCyt+l1dsqSNPOs/F/dBN4Wp5CGAyxbiS5zqDwI4q3QPM+LxLU2h3GUHMIBtMGCvmsG98j5HPMkTKkvcA== +element-plus@^2.3.14: + version "2.3.14" + resolved "https://registry.npmmirror.com/element-plus/-/element-plus-2.3.14.tgz#302a23916b0c3375fcf4b927d7b94483dac13e1b" + integrity sha512-9yvxUaU4jXf2ZNPdmIxoj/f8BG8CDcGM6oHa9JIqxLjQlfY4bpzR1E5CjNimnOX3rxO93w1TQ0jTVt0RSxh9kA== dependencies: "@ctrl/tinycolor" "^3.4.1" "@element-plus/icons-vue" "^2.0.6" diff --git a/server/pkg/ws/client.go b/server/pkg/ws/client.go index c95a6b9e..69b61bc7 100644 --- a/server/pkg/ws/client.go +++ b/server/pkg/ws/client.go @@ -23,9 +23,28 @@ type Client struct { ReadMsgHander ReadMsgHandlerFunc // 读取消息处理函数 } +func NewClient(userId UserId, socket *websocket.Conn) *Client { + cli := &Client{ + ClientId: stringx.Rand(16), + UserId: userId, + WsConn: socket, + } + + return cli +} + +func (c *Client) WithReadHandlerFunc(readMsgHandlerFunc ReadMsgHandlerFunc) *Client { + c.ReadMsgHander = readMsgHandlerFunc + return c +} + +// 读取ws客户端消息 func (c *Client) Read() { go func() { for { + if c.WsConn == nil { + return + } messageType, data, err := c.WsConn.ReadMessage() if err != nil { if messageType == -1 && websocket.IsCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) { @@ -72,13 +91,3 @@ func (c *Client) WriteMsg(msg *Msg) error { func (c *Client) Ping() error { return c.WsConn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(time.Second)) } - -func NewClient(userId UserId, socket *websocket.Conn) *Client { - cli := &Client{ - ClientId: stringx.Rand(16), - UserId: userId, - WsConn: socket, - } - - return cli -} diff --git a/server/pkg/ws/client_manager.go b/server/pkg/ws/client_manager.go index fd9772d3..67863745 100644 --- a/server/pkg/ws/client_manager.go +++ b/server/pkg/ws/client_manager.go @@ -141,8 +141,10 @@ func (manager *ClientManager) doConnect(client *Client) { // 处理断开连接 func (manager *ClientManager) doDisconnect(client *Client) { //关闭连接 - _ = client.WsConn.Close() - client.WsConn = nil + if client.WsConn != nil { + _ = client.WsConn.Close() + client.WsConn = nil + } manager.delClient4Map(client) logx.Debugf("WS客户端已断开: uid=%d, count=%d", client.UserId, Manager.Count()) } diff --git a/server/pkg/ws/ws.go b/server/pkg/ws/ws.go index 39defecf..ddd298fd 100644 --- a/server/pkg/ws/ws.go +++ b/server/pkg/ws/ws.go @@ -23,6 +23,7 @@ func init() { // 添加ws客户端 func AddClient(userId uint64, conn *websocket.Conn) *Client { cli := NewClient(UserId(userId), conn) + cli.Read() Manager.AddClient(cli) return cli }