当前位置: 首页 > 科技观察

构建一个即时通讯应用(七):访问页面

时间:2023-03-17 13:27:22 科技观察

本文为系列文章的第七篇。第1部分:模式第2部分:OAuth第3部分:对话第4部分:消息传递第5部分:实时消息传递第6部分:仅用于开发的登录现在我们已经完成了后端,让我们转到前端。我将使用单页应用程序场景。首先,我们创建一个包含以下内容的static/index.html文件。信使此HTML文件必须提供每个URL,JavaScript负责呈现正确的页面。因此,让我们暂时关注main.go,并在main()函数中添加以下路由:router.Handle("GET","/...",http.FileServer(SPAFileSystem{http.Dir("static")}))typeSPAFileSystemstruct{fshttp.FileSystem}func(spaSPAFileSystem)Open(namestring)(http.File,error){f,err:=spa.fs.Open(name)iferr!=nil{returnspa.fs.Open("index.html")}returnf,nil}我们使用自定义文件系统,因此它不会为未知URL返回404NotFound,而是转到index.html。路由器在index.html中,我们加载了两个文件:styles.css和main.js。我把造型留给你的自由意志。让我们转到main.js。创建一个静态/main.js文件,内容如下:'/',guard(view('home'),view('access')))router.handle('/callback',view('callback'))router.handle(/^\/conversations\/([^\/]+)$/,guard(view('conversation'),view('access')))router.handle(/^\//,view('not-found'))router.install(asyncresult=>{document.body.innerHTML=''if(currentPageinstanceofNode){currentPage.dispatchEvent(disconnect)}currentPage=awaitresultif(currentPageinstanceofNode){document.body.appendChild(currentPage)}})functionview(pageName){return(...args)=>import(`/pages/${pageName}-page.js`).then(m=>m.default(...args))}如果你是这个博客的粉丝,你已经知道了这是它的工作原理。该路由器就是此处显示的路由器。只需从@nicolasparada/router下载并保存到static/router.js。我们注册了四条路线。在根路由/处,我们显示主页或访问页面,无论用户是否通过身份验证。在/callback中,我们显示回调页面。在/conversations/{conversationID}上,无论用户是否经过身份验证,我们都会显示对话或访问页面,对于其他URL,我们会显示未找到的页面。我们告诉路由器将结果呈现到文档主体中,并在离开之前向每个页面发送一个断开连接事件。我们将每个页面放在不同的文件中,并使用新的动态import()函数导入它们。Authenticationguard()是一个函数,给它两个函数作为参数,如果用户通过了身份验证,则执行第一个函数,否则执行第二个。它来自auth.js,所以我们创建一个包含以下内容的static/auth.js文件:==null||expiresAtItem===null){returnfalse}constexpiresAt=newDate(expiresAtItem)if(isNaN(expiresAt.valueOf())||expiresAt<=newDate()){returnfalse}returntrue}exportfunctionguard(fn1,fn2){return(...args)=>isAuthenticated()?fn1(...args):fn2(...args)}exportfunctiongetAuthUser(){if(!isAuthenticated()){returnnull}constauthUser=localStorage。getItem('auth_user')if(authUser===null){returnnull}try{returnJSON.parse(authUser)}catch(_){returnnull}}isAuthenticated()检查localStorage中的token和expires_at判断用户是否有通过了认证。getAuthUser()从localStorage获取经过身份验证的用户。当我们登录时,我们将所有数据保存到localStorage,所以这是有意义的。访问页面访问页面截图让我们从访问页面开始。使用以下内容创建文件static/pages/access-page.js:consttemplate=document.createElement('template')template.innerHTML=`

Messenger

AccesswithGitHub`exportdefaultfunctionaccessPage(){returntemplate.content}因为路由器拦截了所有的链接点击导航,所以我们必须专门阻止这个链接的事件传播。单击该链接会将我们重定向到后端,然后重定向到GitHub,再重定向到后端,然后再次重定向到前端;到回调页面。回调页面创建一个包含以下内容的static/pages/callback-page.js文件:location.toString())consttoken=url.searchParams.get('token')constexpiresAt=url.searchParams.get('expires_at')try{if(token===null||expiresAt===null){thrownewError('InvalidURL')}constauthUser=awaitgetAuthUser(token)localStorage.setItem('auth_user',JSON.stringify(authUser))localStorage.setItem('token',token)localStorage.setItem('expires_at',expiresAt)}catch(err){alert(err.message)}finally{navigate('/',true)}}functiongetAuthUser(token){returnhttp.get('/api/auth_user',{authorization:`Bearer${token}`})}回调页面没有渲染。这是一个异步函数,它使用URL查询字符串中的令牌向/api/auth_user发出GET请求,并将所有数据保存到localStorage。然后重定向到/。这里的HTTP是一个HTTP模块。创建一个包含以下内容的static/http.js文件:import{isAuthenticated}from'./auth.js'asyncfunctionhandleResponse(res){constbody=awaitres.clone().json().catch(()=>res.text())if(res.status===401){localStorage.removeItem('auth_user')localStorage.removeItem('token')localStorage.removeItem('expires_at')}if(!res.ok){constmessage=typeofbody==='object'&&body!==null&&'message'inbody?body.message:typeofbody==='string'&&body!==''?body:res.statusTextthrowObject.assign(newError(message),{url:res.url,statusCode:res.status,statusText:res.statusText,headers:res.headers,body,})}returnbody}functiongetAuthHeader(){returnisAuthenticated()?{authorization:`Bearer${localStorage.getItem('token')}`}:{}}exportdefault{get(url,headers){returnfetch(url,{headers:Object.assign(getAuthHeader(),headers),}).then(handleResponse)},post(url,body,headers){constinit={method:'POST',headers:getAuthHeader(),}if(typeofbody==='object'&&body!==null){init.body=JSON.stringify(body)init.headers['content-type']='application/json;charset=utf-8'}Object.assign(init.headers,headers)returnfetch(url,init).then(handleResponse)},订阅(url,callback){consturlWithToken=newURL(url,location.origin)if(isAuthenticated()){urlWithToken.searchParams.set('token',localStorage.getItem('token'))}consteventSource=newEventSource(urlWithToken.toString())eventSource.onmessage=ev=>{letdatatry{data=JSON.parse(ev.data)}catch(err){console.error('couldnotparsemessagedataasJSON:',err)return}callback(data)}constunsubscribe=()=>{eventSource.close()}returnunsubscribe},}该模块是对fetch和EventSourceAPI的包装,最重要的部分是它将JSON网络令牌添加到请求中。主页主页截图所以当用户登录时,会显示主页。使用以下内容创建一个static/pages/home-page.js文件:import{getAuthUser}from'../auth.js'import{avatar}from'../shared.js'exportdefaultfunctionhomePage(){constauthUser=getAuthUser()consttemplate=document.createElement('template')template.innerHTML=`
${avatar(authUser)}${authUser.username}
注销
`constpage=template.contentpage.getElementById('logout-button').onclick=onLogoutClickreturnpage}functiononLogoutClick(){localStorage.clear()location.reload()}对于本文,这是我们在主页上呈现的唯一内容。我们显示当前经过身份验证的用户和注销按钮。当用户点击注销时,我们清除localStorage中的所有内容并重新加载页面。Avataravatar()函数用于显示用户的头像。由于它在多个地方使用,我将它移到了shared.js文件中。创建一个包含以下内容的文件static/shared.js:exportfunctionavatar(user){returnuser.avatarUrl===null?``:``}如果头像URL为空,我们将使用用户的首字母作为初始头像。您可以使用attr()函数显示带有少量CSS样式的首字母。.avatar[data-initial]::after{content:attr(data-initial);}开发专用登录访问页面登录表单截图在上一篇文章中,我们写了一个.avatar的登录代码。让我们在访问页面中为此添加一个表单。进入static/ages/access-page.js并稍微修改它。importhttpfrom'../http.js'consttemplate=document.createElement('template')template.innerHTML=`

Messenger

AccesswithGitHub`exportdefaultfunctionaccessPage(){constpage=template.content.cloneNode(true)page.getElementById('login-form').onsubmit=onLoginSubmitreturnpage}asyncfunctiononLoginSubmit(ev){ev.preventDefault()constform=ev.currentTargetconstinput=form.querySelector('input')constsubmitButton=form。querySelector('button')input.disabled=truesubmitButton.disabled=truetry{constpayload=awaitlogin(input.value)input.value=''localStorage.setItem('auth_user',JSON.stringify(payload.authUser))localStorage.setItem('token',payload.token)localStorage.setItem('expires_at',payload.expiresAt)location.reload()}catch(err){alert(err.message)setTimeout(()=>{input.focus()},0)}finally{input.disabled=falsesubmitButton.disabled=false}}functionlogin(用户名){returnhttp.post('/api/login',{username})}我在用户提交表单时添加了一个登录表单。它使用用户名向/api/login发出POST请求。将所有数据保存到localStorage并重新加载页面。请记住在前端完成后删除此表单。这就是本文的全部内容。在下一篇文章中,我们将继续使用主页添加表单以开始对话并显示包含最新对话的列表。源代码