点击查看【processon】

Detect Login Status

It is recommended developing in a top-down approach, so we start with the app layer.
App layer should be the translator of the user case map, perhaps you chould face it when developing, any way, I also call this archtecture as “User Case Oriented Programming”.
TDD is also recommended, I use vitest this project.

  1. export default async function detectLoginStatus() {
  2. }
  3. if (import.meta.vitest) {
  4. const { describe, it, vi, afterEach, expect } = import.meta.vitest
  5. vi.mock('auth/adts/router')
  6. vi.mock('auth/adts/local_storge')
  7. vi.mock('auth/adts/api')
  8. const setCurrentUser = vi.spyOn(store, 'currentUser', 'set')
  9. describe('detectLoginStatus', () => {
  10. afterEach(() => {
  11. vi.resetAllMocks()
  12. })
  13. it('should redirect to login page if token is null', async () => {
  14. ;(localStorage.getToken as Mock).mockReturnValue(null)
  15. await detectLoginStatus()
  16. expect(router.goToLogin).toBeCalled()
  17. })
  18. it('should redirect to login page if token is expired', async () => {
  19. ;(localStorage.getToken as Mock).mockReturnValue('token')
  20. ;(api.detectLoginStatus as Mock).mockReturnValue(false)
  21. await detectLoginStatus()
  22. expect(api.detectLoginStatus).toBeCalledWith('token')
  23. expect(router.goToLogin).toBeCalled()
  24. })
  25. it('should set the current user and redirect to home page if token is avilable', async () => {
  26. ;(localStorage.getToken as Mock).mockReturnValue('token')
  27. ;(api.detectLoginStatus as Mock).mockReturnValue({ user: 'tom' })
  28. await detectLoginStatus()
  29. expect(api.detectLoginStatus).toBeCalledWith('token')
  30. expect(setCurrentUser).toBeCalledWith({ user: 'tom' })
  31. expect(router.goToHome).toBeCalled()
  32. })
  33. })
  34. }

Ports

Adapter pattern consists ports and implementers, it is responsable for isolate the technolgy form the domain logics. in well designed project, you cannot see which technolgies are used, even connot see it is a forentend or backend project. The core part of a project can always be assumed just run in internal storage.

  1. export interface API {
  2. detectLoginStatus(token: string): Promise<boolean | User>
  3. login(
  4. userName: string,
  5. password: string,
  6. ): Promise<{ token: string; user: User }>
  7. }
  8. export interface Router {
  9. goToHome(): void
  10. goToLogin(): void
  11. }
  12. export interface LocalStorege {
  13. setToken(token: string): void
  14. getToken(): string | null
  15. }
  16. export interface Store {
  17. currentUser?: User
  18. }

Adapters

Adapters are implementer of the ports in app layer, most the them are for side effects, such as api, frontend reactively store, router, forented events,and so on. As they isolate the real implemention, you can always replace one of them whitout any effects to the apps layer and modles layer.

  1. import { API } from 'auth/apps/ports'
  2. const user: User = { id: 1, name: 'Tom', role: 'user' }
  3. export const detectLoginStatus: API['detectLoginStatus'] = async (
  4. token: string,
  5. ) => {
  6. return user
  7. }
  8. export const login: API['login'] = async (
  9. username: string,
  10. password: string,
  11. ) => {
  12. return {
  13. token: 'token',
  14. user,
  15. }
  16. }

Router

  1. import { Router } from 'auth/apps/ports'
  2. export const goToHome: Router['goToHome'] = () => {
  3. if (location.pathname !== '/') location.pathname = '/'
  4. }
  5. export const goToLogin: Router['goToLogin'] = () => {
  6. if (location.pathname !== '/login') location.pathname = '/login'
  7. }

Local Storage

  1. import { LocalStorege } from 'auth/apps/ports'
  2. export const setToken: LocalStorege['setToken'] = (token: string) => {
  3. localStorage.setItem('token', token)
  4. }
  5. export const getToken: LocalStorege['getToken'] = () => {
  6. return localStorage.getItem('token')
  7. }

Store

  1. import { proxy } from 'valtio'
  2. import { Store } from 'auth/apps/ports'
  3. export default proxy<Store>({
  4. currentUser: undefined,
  5. })

Models

  1. export interface User {
  2. id: ID
  3. name: string
  4. role: 'user' | 'admin'
  5. }

This is the place you develop bussiness logic, bussiness are stable, rarely be modified after finished.
There is only an interface defination now. In most apps, bussiness logics are always in backend, so this layer will be very light in forentend. well in some specifc apps ,such as a drawing program, text editor, this layer well be very heavy.

App

at last, these is all codes for “detect login status” user case:

  1. import { Mock } from 'vitest'
  2. import * as router from 'auth/adts/router'
  3. import * as localStorage from 'auth/adts/local_storge'
  4. import * as api from 'auth/adts/api'
  5. import store from 'auth/adts/store'
  6. export async function detectLoginStatus() {
  7. const token = localStorage.getToken()
  8. if (!token) return router.goToLogin()
  9. const res = await api.detectLoginStatus(token)
  10. if (!res) return router.goToLogin()
  11. const user = res as User
  12. store.currentUser = user
  13. router.goToHome()
  14. }
  15. if (import.meta.vitest) {
  16. const { describe, it, vi, afterEach, expect } = import.meta.vitest
  17. vi.mock('auth/adts/router')
  18. vi.mock('auth/adts/local_storge')
  19. vi.mock('auth/adts/api')
  20. const setCurrentUser = vi.spyOn(store, 'currentUser', 'set')
  21. describe('detectLoginStatus', () => {
  22. afterEach(() => {
  23. vi.resetAllMocks()
  24. })
  25. it('should redirect to login page if token is null', async () => {
  26. ;(localStorage.getToken as Mock).mockReturnValue(null)
  27. await detectLoginStatus()
  28. expect(router.goToLogin).toBeCalled()
  29. })
  30. it('should redirect to login page if token is expired', async () => {
  31. ;(localStorage.getToken as Mock).mockReturnValue('token')
  32. ;(api.detectLoginStatus as Mock).mockReturnValue(false)
  33. await detectLoginStatus()
  34. expect(api.detectLoginStatus).toBeCalledWith('token')
  35. expect(router.goToLogin).toBeCalled()
  36. })
  37. it('should set the current user and redirect to home page if token is avilable', async () => {
  38. ;(localStorage.getToken as Mock).mockReturnValue('token')
  39. ;(api.detectLoginStatus as Mock).mockReturnValue({ user: 'tom' })
  40. await detectLoginStatus()
  41. expect(api.detectLoginStatus).toBeCalledWith('token')
  42. expect(setCurrentUser).toBeCalledWith({ user: 'tom' })
  43. expect(router.goToHome).toBeCalled()
  44. })
  45. })
  46. }

There is an very import conecpt in this layer: codes should express the bussineess.
How to express a “detect login status” user case in english ? I think it may be “Check is there a token, if token is null ,go to login page; If token is exsit, check if the token is expired or not by call a backend api, if token is expired , got to login to; If token is available ,set the current user and go to home page”.
And now look at the codes in detectLoginStauts, you will see that translate codes meaning to human language, it can be as the product documentation. This is what the “Code As Product” mean, the codes of this layer must be understandable to domain experts or product manager.
This is also mean: the codes of an user case should be the express of the sequence diagram of that user case. Perhaps you should face a sequence diagram when you develop an user case.

UI

  1. import { useState, FormEvent } from 'react'
  2. import login from 'auth/apps/login'
  3. export default function LoginForm() {
  4. const [password, setPassword] = useState<string>('')
  5. return (
  6. <div>
  7. <input type="text" value="tom" readOnly disabled />
  8. <input
  9. type="password"
  10. placeholder="anything"
  11. value={password}
  12. onInput={(e: FormEvent<HTMLInputElement>) =>
  13. setPassword(e.currentTarget.value)
  14. }
  15. />
  16. <button onClick={() => login('tom', password)}>login</button>
  17. </div>
  18. )
  19. }

Ui is very simple, it shouldn’t have any bussiness logic, just consumes the app functions and store data.