新聞中心
[[345596]]

在潮陽等地區(qū),都構(gòu)建了全面的區(qū)域性戰(zhàn)略布局,加強發(fā)展的系統(tǒng)性、市場前瞻性、產(chǎn)品創(chuàng)新能力,以專注、極致的服務(wù)理念,為客戶提供成都網(wǎng)站設(shè)計、做網(wǎng)站 網(wǎng)站設(shè)計制作按需設(shè)計網(wǎng)站,公司網(wǎng)站建設(shè),企業(yè)網(wǎng)站建設(shè),高端網(wǎng)站設(shè)計,成都全網(wǎng)營銷推廣,成都外貿(mào)網(wǎng)站建設(shè)公司,潮陽網(wǎng)站建設(shè)費用合理。
本文是該系列的第七篇。
- 第一篇: 模式
- 第二篇: OAuth
- 第三篇: 對話
- 第四篇: 消息
- 第五篇: 實時消息
- 第六篇: 僅用于開發(fā)的登錄
現(xiàn)在我們已經(jīng)完成了后端,讓我們轉(zhuǎn)到前端。 我將采用單頁應(yīng)用程序方案。
首先,我們創(chuàng)建一個 static/index.html 文件,內(nèi)容如下。
Messenger
這個 HTML 文件必須為每個 URL 提供服務(wù),并且使用 JavaScript 負(fù)責(zé)呈現(xiàn)正確的頁面。
因此,讓我們將注意力轉(zhuǎn)到 main.go 片刻,然后在 main() 函數(shù)中添加以下路由:
- router.Handle("GET", "/...", http.FileServer(SPAFileSystem{http.Dir("static")}))
- type SPAFileSystem struct {
- fs http.FileSystem
- }
- func (spa SPAFileSystem) Open(name string) (http.File, error) {
- f, err := spa.fs.Open(name)
- if err != nil {
- return spa.fs.Open("index.html")
- }
- return f, nil
- }
我們使用一個自定義的文件系統(tǒng),因此它不是為未知的 URL 返回 404 Not Found,而是轉(zhuǎn)到 index.html。
路由器
在 index.html 中我們加載了兩個文件:styles.css 和 main.js。我把樣式留給你自由發(fā)揮。
讓我們移動到 main.js。 創(chuàng)建一個包含以下內(nèi)容的 static/main.js 文件:
- import { guard } from './auth.js'
- import Router from './router.js'
- let currentPage
- const disconnect = new CustomEvent('disconnect')
- const router = new Router()
- router.handle('/', 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(async result => {
- document.body.innerHTML = ''
- if (currentPage instanceof Node) {
- currentPage.dispatchEvent(disconnect)
- }
- currentPage = await result
- if (currentPage instanceof Node) {
- document.body.appendChild(currentPage)
- }
- })
- function view(pageName) {
- return (...args) => import(`/pages/${pageName}-page.js`)
- .then(m => m.default(...args))
- }
如果你是這個博客的關(guān)注者,你已經(jīng)知道它是如何工作的了。 該路由器就是在 這里 顯示的那個。 只需從 @nicolasparada/router 下載并保存到 static/router.js 即可。
我們注冊了四條路由。 在根路由 / 處,我們展示 home 或 access 頁面,無論用戶是否通過身份驗證。 在 /callback 中,我們展示 callback 頁面。 在 /conversations/{conversationID} 上,我們展示對話或 access 頁面,無論用戶是否通過驗證,對于其他 URL,我們展示一個 not-found 頁面。
我們告訴路由器將結(jié)果渲染為文檔主體,并在離開之前向每個頁面調(diào)度一個 disconnect 事件。
我們將每個頁面放在不同的文件中,并使用新的動態(tài) import() 函數(shù)導(dǎo)入它們。
身份驗證
guard() 是一個函數(shù),給它兩個函數(shù)作為參數(shù),如果用戶通過了身份驗證,則執(zhí)行第一個函數(shù),否則執(zhí)行第二個。它來自 auth.js,所以我們創(chuàng)建一個包含以下內(nèi)容的 static/auth.js 文件:
- export function isAuthenticated() {
- const token = localStorage.getItem('token')
- const expiresAtItem = localStorage.getItem('expires_at')
- if (token === null || expiresAtItem === null) {
- return false
- }
- const expiresAt = new Date(expiresAtItem)
- if (isNaN(expiresAt.valueOf()) || expiresAt <= new Date()) {
- return false
- }
- return true
- }
- export function guard(fn1, fn2) {
- return (...args) => isAuthenticated()
- ? fn1(...args)
- : fn2(...args)
- }
- export function getAuthUser() {
- if (!isAuthenticated()) {
- return null
- }
- const authUser = localStorage.getItem('auth_user')
- if (authUser === null) {
- return null
- }
- try {
- return JSON.parse(authUser)
- } catch (_) {
- return null
- }
- }
isAuthenticated() 檢查 localStorage 中的 token 和 expires_at,以判斷用戶是否已通過身份驗證。getAuthUser() 從 localStorage 中獲取經(jīng)過身份驗證的用戶。
當(dāng)我們登錄時,我們會將所有的數(shù)據(jù)保存到 localStorage,這樣才有意義。
Access 頁面
access page screenshot
讓我們從 access 頁面開始。 創(chuàng)建一個包含以下內(nèi)容的文件 static/pages/access-page.js:
- const template = document.createElement('template')
- template.innerHTML = `
Messenger
- Access with GitHub
- `
- export default function accessPage() {
- return template.content
- }
因為路由器會攔截所有鏈接點擊來進(jìn)行導(dǎo)航,所以我們必須特別阻止此鏈接的事件傳播。
單擊該鏈接會將我們重定向到后端,然后重定向到 GitHub,再重定向到后端,然后再次重定向到前端; 到 callback 頁面。
Callback 頁面
創(chuàng)建包括以下內(nèi)容的 static/pages/callback-page.js 文件:
- import http from '../http.js'
- import { navigate } from '../router.js'
- export default async function callbackPage() {
- const url = new URL(location.toString())
- const token = url.searchParams.get('token')
- const expiresAt = url.searchParams.get('expires_at')
- try {
- if (token === null || expiresAt === null) {
- throw new Error('Invalid URL')
- }
- const authUser = await getAuthUser(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)
- }
- }
- function getAuthUser(token) {
- return http.get('/api/auth_user', { authorization: `Bearer ${token}` })
- }
callback 頁面不呈現(xiàn)任何內(nèi)容。這是一個異步函數(shù),它使用 URL 查詢字符串中的 token 向 /api/auth_user 發(fā)出 GET 請求,并將所有數(shù)據(jù)保存到 localStorage。 然后重定向到 /。
HTTP
這里是一個 HTTP 模塊。 創(chuàng)建一個包含以下內(nèi)容的 static/http.js 文件:
- import { isAuthenticated } from './auth.js'
- async function handleResponse(res) {
- const body = await res.clone().json().catch(() => res.text())
- if (res.status === 401) {
- localStorage.removeItem('auth_user')
- localStorage.removeItem('token')
- localStorage.removeItem('expires_at')
- }
- if (!res.ok) {
- const message = typeof body === 'object' && body !== null && 'message' in body
- ? body.message
- : typeof body === 'string' && body !== ''
- ? body
- : res.statusText
- throw Object.assign(new Error(message), {
- url: res.url,
- statusCode: res.status,
- statusText: res.statusText,
- headers: res.headers,
- body,
- })
- }
- return body
- }
- function getAuthHeader() {
- return isAuthenticated()
- ? { authorization: `Bearer ${localStorage.getItem('token')}` }
- : {}
- }
- export default {
- get(url, headers) {
- return fetch(url, {
- headers: Object.assign(getAuthHeader(), headers),
- }).then(handleResponse)
- },
- post(url, body, headers) {
- const init = {
- method: 'POST',
- headers: getAuthHeader(),
- }
- if (typeof body === 'object' && body !== null) {
- init.body = JSON.stringify(body)
- init.headers['content-type'] = 'application/json; charset=utf-8'
- }
- Object.assign(init.headers, headers)
- return fetch(url, init).then(handleResponse)
- },
- subscribe(url, callback) {
- const urlWithToken = new URL(url, location.origin)
- if (isAuthenticated()) {
- urlWithToken.searchParams.set('token', localStorage.getItem('token'))
- }
- const eventSource = new EventSource(urlWithToken.toString())
- eventSource.onmessage = ev => {
- let data
- try {
- data = JSON.parse(ev.data)
- } catch (err) {
- console.error('could not parse message data as JSON:', err)
- return
- }
- callback(data)
- }
- const unsubscribe = () => {
- eventSource.close()
- }
- return unsubscribe
- },
- }
這個模塊是 fetch 和 EventSource API 的包裝器。最重要的部分是它將 JSON web 令牌添加到請求中。
Home 頁面
home page screenshot
因此,當(dāng)用戶登錄時,將顯示 home 頁。 創(chuàng)建一個具有以下內(nèi)容的 static/pages/home-page.js 文件:
- import { getAuthUser } from '../auth.js'
- import { avatar } from '../shared.js'
- export default function homePage() {
- const authUser = getAuthUser()
- const template = document.createElement('template')
- template.innerHTML = `
- ${avatar(authUser)}
- ${authUser.username}
- `
- const page = template.content
- page.getElementById('logout-button').onclick = onLogoutClick
- return page
- }
- function onLogoutClick() {
- localStorage.clear()
- location.reload()
- }
對于這篇文章,這是我們在 home 頁上呈現(xiàn)的唯一內(nèi)容。我們顯示當(dāng)前經(jīng)過身份驗證的用戶和注銷按鈕。
當(dāng)用戶單擊注銷時,我們清除 localStorage 中的所有內(nèi)容并重新加載頁面。
Avatar
那個 avatar() 函數(shù)用于顯示用戶的頭像。 由于已在多個地方使用,因此我將它移到 shared.js 文件中。 創(chuàng)建具有以下內(nèi)容的文件 static/shared.js:
- export function avatar(user) {
- return user.avatarUrl === null
- ? `
` - : `
`
- }
如果頭像網(wǎng)址為 null,我們將使用用戶的姓名首字母作為初始頭像。
你可以使用 attr() 函數(shù)顯示帶有少量 CSS 樣式的首字母。
- .avatar[data-initial]::after {
- content: attr(data-initial);
- }
僅開發(fā)使用的登錄
access page with login form screenshot
在上一篇文章中,我們?yōu)榫帉懥艘粋€登錄代碼。讓我們在 access 頁面中為此添加一個表單。 進(jìn)入 static/ages/access-page.js,稍微修改一下。
- import http from '../http.js'
- const template = document.createElement('template')
- template.innerHTML = `
Messenger
- Access with GitHub
- `
- export default function accessPage() {
- const page = template.content.cloneNode(true)
- page.getElementById('login-form').onsubmit = onLoginSubmit
- return page
- }
- async function onLoginSubmit(ev) {
- ev.preventDefault()
- const form = ev.currentTarget
- const input = form.querySelector('input')
- const submitButton = form.querySelector('button')
- input.disabled = true
- submitButton.disabled = true
- try {
- const payload = await login(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 = false
- submitButton.disabled = false
- }
- }
- function login(username) {
- return http.post('/api/login', { username })
- }
我添加了一個登錄表單。當(dāng)用戶提交表單時。它使用用戶名對 /api/login 進(jìn)行 POST 請求。將所有數(shù)據(jù)保存到 localStorage 并重新加載頁面。
記住在前端完成后刪除此表單。
這就是這篇文章的全部內(nèi)容。在下一篇文章中,我們將繼續(xù)使用主頁添加一個表單來開始對話,并顯示包含最新對話的列表。
- 源代碼
網(wǎng)站名稱:構(gòu)建一個即時消息應(yīng)用(七):Access頁面
文章轉(zhuǎn)載:http://m.5511xx.com/article/codiodg.html


咨詢
建站咨詢
