构建一个即时消息应用(七):Access页面

[[345596]]

在潮阳等地区,都构建了全面的区域性战略布局,加强发展的系统性、市场前瞻性、产品创新能力,以专注、极致的服务理念,为客户提供成都网站设计、做网站 网站设计制作按需设计网站,公司网站建设,企业网站建设,高端网站设计,成都全网营销推广,成都外贸网站建设公司,潮阳网站建设费用合理。

本文是该系列的第七篇。

  • 第一篇: 模式
  • 第二篇: OAuth
  • 第三篇: 对话
  • 第四篇: 消息
  • 第五篇: 实时消息
  • 第六篇: 仅用于开发的登录

现在我们已经完成了后端,让我们转到前端。 我将采用单页应用程序方案。

首先,我们创建一个 static/index.html 文件,内容如下。

 
 
 
 
  1.  
  2.  
  3.  
  4.      
  5.      
  6.     Messenger 
  7.      
  8.      
  9.      
  10.  
  11.  
  12.  

这个 HTML 文件必须为每个 URL 提供服务,并且使用 JavaScript 负责呈现正确的页面。

因此,让我们将注意力转到 main.go 片刻,然后在 main() 函数中添加以下路由:

 
 
 
 
  1. router.Handle("GET", "/...", http.FileServer(SPAFileSystem{http.Dir("static")})) 
  2.  
  3. type SPAFileSystem struct { 
  4.     fs http.FileSystem 
  5.  
  6. func (spa SPAFileSystem) Open(name string) (http.File, error) { 
  7.     f, err := spa.fs.Open(name) 
  8.     if err != nil { 
  9.         return spa.fs.Open("index.html") 
  10.     } 
  11.     return f, nil 

我们使用一个自定义的文件系统,因此它不是为未知的 URL 返回 404 Not Found,而是转到 index.html

路由器

在 index.html 中我们加载了两个文件:styles.css 和 main.js。我把样式留给你自由发挥。

让我们移动到 main.js。 创建一个包含以下内容的 static/main.js 文件:

 
 
 
 
  1. import { guard } from './auth.js' 
  2. import Router from './router.js' 
  3.  
  4. let currentPage 
  5. const disconnect = new CustomEvent('disconnect') 
  6. const router = new Router() 
  7.  
  8. router.handle('/', guard(view('home'), view('access'))) 
  9. router.handle('/callback', view('callback')) 
  10. router.handle(/^\/conversations\/([^\/]+)$/, guard(view('conversation'), view('access'))) 
  11. router.handle(/^\//, view('not-found')) 
  12.  
  13. router.install(async result => { 
  14.     document.body.innerHTML = '' 
  15.     if (currentPage instanceof Node) { 
  16.         currentPage.dispatchEvent(disconnect) 
  17.     } 
  18.     currentPage = await result 
  19.     if (currentPage instanceof Node) { 
  20.         document.body.appendChild(currentPage) 
  21.     } 
  22. }) 
  23.  
  24. function view(pageName) { 
  25.     return (...args) => import(`/pages/${pageName}-page.js`) 
  26.         .then(m => m.default(...args)) 

如果你是这个博客的关注者,你已经知道它是如何工作的了。 该路由器就是在 这里 显示的那个。 只需从 @nicolasparada/router 下载并保存到 static/router.js 即可。

我们注册了四条路由。 在根路由 / 处,我们展示 home 或 access 页面,无论用户是否通过身份验证。 在 /callback 中,我们展示 callback 页面。 在 /conversations/{conversationID} 上,我们展示对话或 access 页面,无论用户是否通过验证,对于其他 URL,我们展示一个 not-found 页面。

我们告诉路由器将结果渲染为文档主体,并在离开之前向每个页面调度一个 disconnect 事件。

我们将每个页面放在不同的文件中,并使用新的动态 import() 函数导入它们。

身份验证

guard() 是一个函数,给它两个函数作为参数,如果用户通过了身份验证,则执行第一个函数,否则执行第二个。它来自 auth.js,所以我们创建一个包含以下内容的 static/auth.js 文件:

 
 
 
 
  1. export function isAuthenticated() { 
  2.     const token = localStorage.getItem('token') 
  3.     const expiresAtItem = localStorage.getItem('expires_at') 
  4.     if (token === null || expiresAtItem === null) { 
  5.         return false 
  6.     } 
  7.  
  8.     const expiresAt = new Date(expiresAtItem) 
  9.     if (isNaN(expiresAt.valueOf()) || expiresAt <= new Date()) { 
  10.         return false 
  11.     } 
  12.  
  13.     return true 
  14.  
  15. export function guard(fn1, fn2) { 
  16.     return (...args) => isAuthenticated() 
  17.         ? fn1(...args) 
  18.         : fn2(...args) 
  19.  
  20. export function getAuthUser() { 
  21.     if (!isAuthenticated()) { 
  22.         return null 
  23.     } 
  24.  
  25.     const authUser = localStorage.getItem('auth_user') 
  26.     if (authUser === null) { 
  27.         return null 
  28.     } 
  29.  
  30.     try { 
  31.         return JSON.parse(authUser) 
  32.     } catch (_) { 
  33.         return null 
  34.     } 

isAuthenticated() 检查 localStorage 中的 token 和 expires_at,以判断用户是否已通过身份验证。getAuthUser() 从 localStorage 中获取经过身份验证的用户。

当我们登录时,我们会将所有的数据保存到 localStorage,这样才有意义。

Access 页面

 

access page screenshot

让我们从 access 页面开始。 创建一个包含以下内容的文件 static/pages/access-page.js

 
 
 
 
  1. const template = document.createElement('template') 
  2. template.innerHTML = ` 
  3.     

    Messenger

     
  4.     Access with GitHub 
  5.  
  6. export default function accessPage() { 
  7.     return template.content 

因为路由器会拦截所有链接点击来进行导航,所以我们必须特别阻止此链接的事件传播。

单击该链接会将我们重定向到后端,然后重定向到 GitHub,再重定向到后端,然后再次重定向到前端; 到 callback 页面。

Callback 页面

创建包括以下内容的 static/pages/callback-page.js 文件:

 
 
 
 
  1. import http from '../http.js' 
  2. import { navigate } from '../router.js' 
  3.  
  4. export default async function callbackPage() { 
  5.     const url = new URL(location.toString()) 
  6.     const token = url.searchParams.get('token') 
  7.     const expiresAt = url.searchParams.get('expires_at') 
  8.  
  9.     try { 
  10.         if (token === null || expiresAt === null) { 
  11.             throw new Error('Invalid URL') 
  12.         } 
  13.  
  14.         const authUser = await getAuthUser(token) 
  15.  
  16.         localStorage.setItem('auth_user', JSON.stringify(authUser)) 
  17.         localStorage.setItem('token', token) 
  18.         localStorage.setItem('expires_at', expiresAt) 
  19.     } catch (err) { 
  20.         alert(err.message) 
  21.     } finally { 
  22.         navigate('/', true) 
  23.     } 
  24.  
  25. function getAuthUser(token) { 
  26.     return http.get('/api/auth_user', { authorization: `Bearer ${token}` }) 

callback 页面不呈现任何内容。这是一个异步函数,它使用 URL 查询字符串中的 token 向 /api/auth_user 发出 GET 请求,并将所有数据保存到 localStorage。 然后重定向到 /

HTTP

这里是一个 HTTP 模块。 创建一个包含以下内容的 static/http.js 文件:

 
 
 
 
  1. import { isAuthenticated } from './auth.js' 
  2.  
  3. async function handleResponse(res) { 
  4.     const body = await res.clone().json().catch(() => res.text()) 
  5.  
  6.     if (res.status === 401) { 
  7.         localStorage.removeItem('auth_user') 
  8.         localStorage.removeItem('token') 
  9.         localStorage.removeItem('expires_at') 
  10.     } 
  11.  
  12.     if (!res.ok) { 
  13.         const message = typeof body === 'object' && body !== null && 'message' in body 
  14.             ? body.message 
  15.             : typeof body === 'string' && body !== '' 
  16.                 ? body 
  17.                 : res.statusText 
  18.         throw Object.assign(new Error(message), { 
  19.             url: res.url, 
  20.             statusCode: res.status, 
  21.             statusText: res.statusText, 
  22.             headers: res.headers, 
  23.             body, 
  24.         }) 
  25.     } 
  26.  
  27.     return body 
  28.  
  29. function getAuthHeader() { 
  30.     return isAuthenticated() 
  31.         ? { authorization: `Bearer ${localStorage.getItem('token')}` } 
  32.         : {} 
  33.  
  34. export default { 
  35.     get(url, headers) { 
  36.         return fetch(url, { 
  37.             headers: Object.assign(getAuthHeader(), headers), 
  38.         }).then(handleResponse) 
  39.     }, 
  40.  
  41.     post(url, body, headers) { 
  42.         const init = { 
  43.             method: 'POST', 
  44.             headers: getAuthHeader(), 
  45.         } 
  46.         if (typeof body === 'object' && body !== null) { 
  47.             init.body = JSON.stringify(body) 
  48.             init.headers['content-type'] = 'application/json; charset=utf-8' 
  49.         } 
  50.         Object.assign(init.headers, headers) 
  51.         return fetch(url, init).then(handleResponse) 
  52.     }, 
  53.  
  54.     subscribe(url, callback) { 
  55.         const urlWithToken = new URL(url, location.origin) 
  56.         if (isAuthenticated()) { 
  57.             urlWithToken.searchParams.set('token', localStorage.getItem('token')) 
  58.         } 
  59.         const eventSource = new EventSource(urlWithToken.toString()) 
  60.         eventSource.onmessage = ev => { 
  61.             let data 
  62.             try { 
  63.                 data = JSON.parse(ev.data) 
  64.             } catch (err) { 
  65.                 console.error('could not parse message data as JSON:', err) 
  66.                 return 
  67.             } 
  68.             callback(data) 
  69.         } 
  70.         const unsubscribe = () => { 
  71.             eventSource.close() 
  72.         } 
  73.         return unsubscribe 
  74.     }, 

这个模块是 fetch 和 EventSource API 的包装器。最重要的部分是它将 JSON web 令牌添加到请求中。

Home 页面

 

home page screenshot

因此,当用户登录时,将显示 home 页。 创建一个具有以下内容的 static/pages/home-page.js 文件:

 
 
 
 
  1. import { getAuthUser } from '../auth.js' 
  2. import { avatar } from '../shared.js' 
  3.  
  4. export default function homePage() { 
  5.     const authUser = getAuthUser() 
  6.     const template = document.createElement('template') 
  7.     template.innerHTML = ` 
  8.         
     
  9.             
     
  10.                 ${avatar(authUser)} 
  11.                 ${authUser.username} 
  12.             
 
  •             Logout 
  •         
  •  
  •          
  •          
  •     ` 
  •     const page = template.content 
  •     page.getElementById('logout-button').onclick = onLogoutClick 
  •     return page 
  •  
  • function onLogoutClick() { 
  •     localStorage.clear() 
  •     location.reload() 
  • 对于这篇文章,这是我们在 home 页上呈现的唯一内容。我们显示当前经过身份验证的用户和注销按钮。

    当用户单击注销时,我们清除 localStorage 中的所有内容并重新加载页面。

    Avatar

    那个 avatar() 函数用于显示用户的头像。 由于已在多个地方使用,因此我将它移到 shared.js 文件中。 创建具有以下内容的文件 static/shared.js

     
     
     
     
    1. export function avatar(user) { 
    2.     return user.avatarUrl === null 
    3.         ? `
    4.         : `

    如果头像网址为 null,我们将使用用户的姓名首字母作为初始头像。

    你可以使用 attr() 函数显示带有少量 CSS 样式的首字母。

     
     
     
     
    1. .avatar[data-initial]::after { 
    2.     content: attr(data-initial); 

    仅开发使用的登录

     

    access page with login form screenshot

    在上一篇文章中,我们为编写了一个登录代码。让我们在 access 页面中为此添加一个表单。 进入 static/ages/access-page.js,稍微修改一下。

     
     
     
     
    1. import http from '../http.js' 
    2.  
    3. const template = document.createElement('template') 
    4. template.innerHTML = ` 
    5.     

      Messenger

       
    6.      
    7.          
    8.          
    9.      
    10.     Access with GitHub 
    11.  
    12. export default function accessPage() { 
    13.     const page = template.content.cloneNode(true) 
    14.     page.getElementById('login-form').onsubmit = onLoginSubmit 
    15.     return page 
    16.  
    17. async function onLoginSubmit(ev) { 
    18.     ev.preventDefault() 
    19.  
    20.     const form = ev.currentTarget 
    21.     const input = form.querySelector('input') 
    22.     const submitButton = form.querySelector('button') 
    23.  
    24.     input.disabled = true 
    25.     submitButton.disabled = true 
    26.  
    27.     try { 
    28.         const payload = await login(input.value) 
    29.         input.value = '' 
    30.  
    31.         localStorage.setItem('auth_user', JSON.stringify(payload.authUser)) 
    32.         localStorage.setItem('token', payload.token) 
    33.         localStorage.setItem('expires_at', payload.expiresAt) 
    34.  
    35.         location.reload() 
    36.     } catch (err) { 
    37.         alert(err.message) 
    38.         setTimeout(() => { 
    39.             input.focus() 
    40.         }, 0) 
    41.     } finally { 
    42.         input.disabled = false 
    43.         submitButton.disabled = false 
    44.     } 
    45.  
    46. function login(username) { 
    47.     return http.post('/api/login', { username }) 

    我添加了一个登录表单。当用户提交表单时。它使用用户名对 /api/login 进行 POST 请求。将所有数据保存到 localStorage 并重新加载页面。

    记住在前端完成后删除此表单。


    这就是这篇文章的全部内容。在下一篇文章中,我们将继续使用主页添加一个表单来开始对话,并显示包含最新对话的列表。

     

    当前名称:构建一个即时消息应用(七):Access页面
    网页URL:http://www.csdahua.cn/qtweb/news15/400215.html

    网站建设、网络推广公司-快上网,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

    广告

    声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 快上网

    成都快上网为您推荐相关内容

    静态网站知识

    分类信息网