diff --git a/sources/tech/20180716 Building a Messenger App- Access Page.md b/sources/tech/20180716 Building a Messenger App- Access Page.md new file mode 100644 index 0000000000..21671b92f6 --- /dev/null +++ b/sources/tech/20180716 Building a Messenger App- Access Page.md @@ -0,0 +1,459 @@ +[#]: collector: (lujun9972) +[#]: translator: ( ) +[#]: reviewer: ( ) +[#]: publisher: ( ) +[#]: url: ( ) +[#]: subject: (Building a Messenger App: Access Page) +[#]: via: (https://nicolasparada.netlify.com/posts/go-messenger-access-page/) +[#]: author: (Nicolás Parada https://nicolasparada.netlify.com/) + +Building a Messenger App: Access Page +====== + +This post is the 7th on a series: + + * [Part 1: Schema][1] + * [Part 2: OAuth][2] + * [Part 3: Conversations][3] + * [Part 4: Messages][4] + * [Part 5: Realtime Messages][5] + * [Part 6: Development Login][6] + + + +Now that we’re done with the backend, lets move to the frontend. I will go with a single-page application. + +Lets start by creating a file `static/index.html` with the following content. + +``` + + + + + + Messenger + + + + + + +``` + +This HTML file must be server for every URL and JavaScript will take care of rendering the correct page. + +So lets go the the `main.go` for a moment and in the `main()` function add the following route: + +``` +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 +} +``` + +We use a custom file system so instead of returning `404 Not Found` for unknown URLs, it serves the `index.html`. + +### Router + +In the `index.html` we loaded two files: `styles.css` and `main.js`. I leave styling to your taste. + +Lets move to `main.js`. Create a `static/main.js` file with the following content: + +``` +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)) +} +``` + +If you are follower of this blog, you already know how this works. That router is the one showed [here][7]. Just download it from [@nicolasparada/router][8] and save it to `static/router.js`. + +We registered four routes. At the root `/` we show the home or access page whether the user is authenticated. At `/callback` we show the callback page. On `/conversations/{conversationID}` we show the conversation or access page whether the user is authenticated and for every other URL, we show a not found page. + +We tell the router to render the result to the document body and dispatch a `disconnect` event to each page before leaving. + +We have each page in a different file and we import them with the new dynamic `import()`. + +### Auth + +`guard()` is a function that given two functions, executes the first one if the user is authenticated, or the sencond one if not. It comes from `auth.js` so lets create a `static/auth.js` file with the following content: + +``` +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()` checks for `token` and `expires_at` from localStorage to tell if the user is authenticated. `getAuthUser()` gets the authenticated user from localStorage. + +When we login, we’ll save all the data to localStorage so it will make sense. + +### Access Page + +![access page screenshot][9] + +Lets start with the access page. Create a file `static/pages/access-page.js` with the following content: + +``` +const template = document.createElement('template') +template.innerHTML = ` +

Messenger

+ Access with GitHub +` + +export default function accessPage() { + return template.content +} +``` + +Because the router intercepts all the link clicks to do its navigation, we must prevent the event propagation for this link in particular. + +Clicking on that link will redirect us to the backend, then to GitHub, then to the backend and then to the frontend again; to the callback page. + +### Callback Page + +Create the file `static/pages/callback-page.js` with the following content: + +``` +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}` }) +} +``` + +The callback page doesn’t render anything. It’s an async function that does a GET request to `/api/auth_user` using the token from the URL query string and saves all the data to localStorage. Then it redirects to `/`. + +### HTTP + +There is an HTTP module. Create a `static/http.js` file with the following content: + +``` +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 + }, +} +``` + +This module is a wrapper around the [fetch][10] and [EventSource][11] APIs. The most important part is that it adds the JSON web token to the requests. + +### Home Page + +![home page screenshot][12] + +So, when the user login, the home page will be shown. Create a `static/pages/home-page.js` file with the following content: + +``` +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() +} +``` + +For this post, this is the only content we render on the home page. We show the current authenticated user and a logout button. + +When the user clicks to logout, we clear all inside localStorage and do a reload of the page. + +### Avatar + +That `avatar()` function is to show the user’s avatar. Because it’s used in more than one place, I moved it to a `shared.js` file. Create the file `static/shared.js` with the following content: + +``` +export function avatar(user) { + return user.avatarUrl === null + ? `
` + : `${user.username}'s avatar` +} +``` + +We use a small figure with the user’s initial in case the avatar URL is null. + +You can show the initial with a little of CSS using the `attr()` function. + +``` +.avatar[data-initial]::after { + content: attr(data-initial); +} +``` + +### Development Login + +![access page with login form screenshot][13] + +In the previous post we coded a login for development. Lets add a form for that in the access page. Go to `static/pages/access-page.js` and modify it a little. + +``` +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 }) +} +``` + +I added a login form. When the user submits the form. It does a POST requets to `/api/login` with the username. Saves all the data to localStorage and reloads the page. + +Remember to remove this form once you are done with the frontend. + +* * * + +That’s all for this post. In the next one, we’ll continue with the home page to add a form to start conversations and display a list with the latest ones. + +[Souce Code][14] + +-------------------------------------------------------------------------------- + +via: https://nicolasparada.netlify.com/posts/go-messenger-access-page/ + +作者:[Nicolás Parada][a] +选题:[lujun9972][b] +译者:[译者ID](https://github.com/译者ID) +校对:[校对者ID](https://github.com/校对者ID) + +本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出 + +[a]: https://nicolasparada.netlify.com/ +[b]: https://github.com/lujun9972 +[1]: https://nicolasparada.netlify.com/posts/go-messenger-schema/ +[2]: https://nicolasparada.netlify.com/posts/go-messenger-oauth/ +[3]: https://nicolasparada.netlify.com/posts/go-messenger-conversations/ +[4]: https://nicolasparada.netlify.com/posts/go-messenger-messages/ +[5]: https://nicolasparada.netlify.com/posts/go-messenger-realtime-messages/ +[6]: https://nicolasparada.netlify.com/posts/go-messenger-dev-login/ +[7]: https://nicolasparada.netlify.com/posts/js-router/ +[8]: https://unpkg.com/@nicolasparada/router +[9]: https://nicolasparada.netlify.com/img/go-messenger-access-page/access-page.png +[10]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API +[11]: https://developer.mozilla.org/en-US/docs/Web/API/EventSource +[12]: https://nicolasparada.netlify.com/img/go-messenger-access-page/home-page.png +[13]: https://nicolasparada.netlify.com/img/go-messenger-access-page/access-page-v2.png +[14]: https://github.com/nicolasparada/go-messenger-demo