[Practice] JWT, user authentication and asynchronous requests (1) —— React17+React Hook+TS4 best practice, imitating Jira enterprise-level projects (4)


Source of learning content: React + React Hook + TS Best Practice - MOOC


Compared with the original tutorial, I used the latest version at the beginning of my study (2023.03):

item Version
react & react-dom ^18.2.0
react-router & react-router-dom ^6.11.2
antd ^4.24.8
@commitlint/cli & @commitlint/config-conventional ^17.4.4
eslint-config-prettier ^8.6.0
husky ^8.0.3
lint-staged ^13.1.2
prettier 2.8.4
json-server 0.17.2
craco-less ^2.0.0
@craco/craco ^7.1.0
qs ^6.11.0
dayjs ^1.11.7
react-helmet ^6.1.0
@types/react-helmet ^6.1.6
react-query ^6.1.0
@welldone-software/why-did-you-render ^7.0.1
@emotion/react & @emotion/styled ^11.10.6

The specific configuration, operation and content will be different, and the "pit" will also be different. . .


1. Project launch: project initialization and configuration

2. React and Hook application: implement the project list

3. TS Application: JS God Assist - Strong Type

4. JWT, user authentication and asynchronous request

1.login

  • New file: src\screens\login\index.tsx:
import {
    
     FormEvent } from "react";

const apiUrl = process.env.REACT_APP_API_URL;

export const Login = () => {
    
    
  const login = (param: {
    
     username: string; password: string }) => {
    
    
    fetch(`${
      
      apiUrl}/login`, {
    
    
      method: "POST",
      headers: {
    
    
        "Content-Type": "application/json",
      },
      body: JSON.stringify(param),
    }).then(async (res) => {
    
    
      if (res.ok) {
    
    
      }
    });
  };

  // HTMLFormElement extends Element (子类型继承性兼容所有父类型)(鸭子类型:duck typing: 面向接口编程 而非 面向对象编程)
  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
    
    
    event.preventDefault();
    const username = (event.currentTarget.elements[0] as HTMLFormElement).value;
    const password = (event.currentTarget.elements[1] as HTMLFormElement).value;
    login({
    
     username, password });
  };

  return (
    <form onSubmit={
    
    handleSubmit}>
      <div>
        <label htmlFor="username">用户名</label>
        <input type="text" id="username" />
      </div>
      <div>
        <label htmlFor="password">密码</label>
        <input type="password" id="password" />
      </div>
      <button type="submit">登录</button>
    </form>
  );
};
  • src\App.tsxIntroduced in :
import "./App.css";
import {
    
     Login } from "screens/login";

function App() {
    
    
  return (
    <div className="App">
      <Login />
    </div>
  );
}

export default App;

Click on the current page to log in 404, the next step is to configure json-serverthe middleware so that it can simulate the non-restful interface

2.middleware of json-server

  • New file: __json_server_mock__\middleware.js:
module.exports = (req, res, next) => {
    
    
  if (req.method === "POST" && req.path === "/login") {
    
    
    if (req.body.username === "user" && req.body.password === "123") {
    
    
      return res.status(200).json({
    
    
        user: {
    
    
          token: "token123",
        },
      });
    } else {
    
    
      return res.status(400).json({
    
     message: "用户名或者密码错误" });
    }
  }
  next();
};
  • In package.jsonconfiguration :json-serverscript
"json-server": "json-server __json_server_mock__/db.json -w -p 3001 --middlewares ./__json_server_mock__/middleware.js"
  • Restart after configuration json-server, enter the middleware preset user name and password to access normally (200), otherwise (400: bad request)

3.jira-dev-tool(immooc-jira-tool)

jira-dev-tool - npm

Install

  • First confirm that the git workspace is clean and install jira-dev-tool (imooc-jira-tool)
npx imooc-jira-tool
  • introduced intosrc\index.tsx
import {
    
     loadDevTools } from "jira-dev-tool";

loadDevTools(() => {
    
    
  ReactDOM.render(
    <React.StrictMode>
      <AppProviders>
        <App />
      </AppProviders>
    </React.StrictMode>,
    document.getElementById("root")
  );
});

After this step, there will be an extra "gear" on the page, click to use jira-dev-tool

question

Problems that may occur when starting project joint debugging after installing jira-dev-tool (imooc-jira-tool)

Question one
  • Error:
request (TypeError: Failed to fetch). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.
  • solve:
npx msw init ./public/ --save
question two

Since jira-dev-tool has not been updated for two years and relies on react@“^16.0.0”, if you want to continue using it, the following error will be reported when npm i:

npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR! 
npm ERR! While resolving: [email protected]
npm ERR! Found: [email protected]
npm ERR! node_modules/react
npm ERR!   react@"^18.2.0" from the root project
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer react@"^16.0.0" from [email protected]       
npm ERR! node_modules/jira-dev-tool
npm ERR!   jira-dev-tool@"^1.7.61" from the root project      
npm ERR!
npm ERR! Fix the upstream dependency conflict, or retry       
npm ERR! this command with --force or --legacy-peer-deps      
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.
npm ERR!
npm ERR!
npm ERR! For a full report see:
npm ERR! C:\...\npm-cache\_logs\2023-03-08T09_11_24_998Z-eresolve-report.txt

npm ERR! A complete log of this run can be found in:
npm ERR! C:\...\npm-cache\_logs\2023-03-08T09_11_24_998Z-debug-0.log

Solution one:

  • Delete the file yarn.lock and "jira-dev-tool": "^1.7.61",the part , and install it manually with jira-dev-tool

Solution 2 (recommended)

  • use yarninsteadnpm i

use

  • The developer tool uses MSW to realize the "distributed backend" based on the principle of Service Worker
  • After the back-end logic processing, use localStorage as the database to perform addition, deletion, modification and query operations
  • An independent back-end service and database is installed on the browser, which will no longer be affected by any centralized service. Click 'Clear Database' to reset the back-end service
  • It can precisely control the time, failure probability and failure rules of HTTP requests
  • Although Service Worker + localStorage is essentially different from traditional back-end services, it will not affect front-end development at all

Other specific operations can be found in the documentation and the following operations: jira-dev-tool - npm

After installation /login, enter and request the login interface. You can see that the status code is followed by the words (from service worker) and the connection is successful:

insert image description here

The first tab page of the development tool console sets the minimum request time and request failure ratio:

insert image description here

The development tool console will /loginbe added to the asynchronous request failure setting, the status code 400 will change to 500, prompting: "The request failed, please check the settings of jira-dev-tool":
insert image description here

4. JWT principle and auth-provider implementation

register a new user

  • Modify: src\screens\login\index.tsx:
    • Call interface loginchanged to register;
    • Button Login Change to Register

Register a new user jira (password: jira), the interface returns:

{
    
    
  "user": {
    
    
    "id": 2087569429,
    "name": "jira",
    "token": "MjA4NzU2OTQyOQ=="
  }
}

token is the product of JWT (JSON Web Tokens)

auth-provider

Modify src\screens\ProjectList\components\SearchPanel.tsxand Useradd token:

export interface User {
    
    
  id: string;
  name: string;
  email: string;
  title: string;
  organization: string;
  token: string;
}
...

New src\auth-provider.ts:

Mock third-party services

// 在真实环境中,如果使用了 firebase 这种第三方 auth 服务的话,本文件不需要开发者开发

import {
    
     User } from "screens/ProjectList/components/SearchPanel"

const localStorageKey = '__auth_provider_token__'
const apiUrl = process.env.REACT_APP_API_URL;

export const getToken = () => window.localStorage.getItem(localStorageKey)

export const handleUserResponse = ({
    
    user} : {
    
     user: User }) => {
    
    
  window.localStorage.setItem(localStorageKey, user.token || '')
  return user
}

export const login = (data: {
    
     username: string, password: string }) => {
    
    
  return fetch(`${
      
      apiUrl}/login`, {
    
    
    method: "POST",
    headers: {
    
    
      "Content-Type": "application/json",
    },
    body: JSON.stringify(data),
  }).then(async (res) => {
    
    
    if (res.ok) {
    
    
      return handleUserResponse(await res.json())
    } else {
    
    
      return Promise.reject(data)
    }
  });
}

export const register = (data: {
    
     username: string, password: string }) => {
    
    
  return fetch(`${
      
      apiUrl}/register`, {
    
    
    method: "POST",
    headers: {
    
    
      "Content-Type": "application/json",
    },
    body: JSON.stringify(data),
  }).then(async (res) => {
    
    
    if (res.ok) {
    
    
      return handleUserResponse(await res.json())
    } else {
    
    
      return Promise.reject(data)
    }
  });
}

export const logout = async () => window.localStorage.removeItem(localStorageKey)

Details:

  • When the function is defined, add async before the value to make it return a Promise object
  • The callback function input parameter and the callback function have one and only one function call and its input is consistent with the callback function input parameter. The callback function can be directly abbreviated as its internal function call without parameters (this is functional programming - PointFree one application):
    • const login = (form: AuthForm) => auth.login(form).then(user => setUser(user))
    • const login = (form: AuthForm) => auth.login(form).then(setUser)

[Notes] Functional Programming - PointFree

5.useContext(user,login,register,logout)

New src\context\auth-context.tsx:

import React, {
    
     ReactNode, useState } from "react"
import * as auth from 'auth-provider'
import {
    
     User } from "screens/ProjectList/components/SearchPanel"

interface AuthForm {
    
    
  username: string,
  password: string
}

const AuthContext = React.createContext<{
    
    
  user: User | null,
  login: (form : AuthForm) => Promise<void>,
  register: (form : AuthForm) => Promise<void>,
  logout: () => Promise<void>,
} | undefined>(undefined)

AuthContext.displayName = 'AuthContext'

export const AuthProvider = ({
    
    children}:{
    
    children: ReactNode}) => {
    
    
  // 这里要考虑到初始值的类型与后续值类型,取并组成一个泛型
  const [user, setUser] = useState<User | null>(null)

  const login = (form: AuthForm) => auth.login(form).then(user => setUser(user))
  const register = (form: AuthForm) => auth.register(form).then(user => setUser(user))
  const logout = () => auth.logout().then(() => setUser(null))

  return <AuthContext.Provider children={
    
    children} value={
    
    {
    
     user, login, register, logout }}/>
}

export const useAuth = () => {
    
    
  const context = React.useContext(AuthContext)
  if (!context) {
    
    
    throw new Error('useAuth 必须在 AuthProvider 中使用')
  }
  return context
}

New src\context\index.tsx:

import {
    
     ReactNode } from "react";
import {
    
     AuthProvider } from "./auth-context";

export const AppProvider = ({
    
    children}:{
    
    children: ReactNode}) => {
    
    
  return <AuthProvider>
    {
    
    children}
  </AuthProvider>
}

Used in the project AppProvider, modify src\index.tsx:

import {
    
     AppProvider } from "context";
...
loadDevTools(() => {
    
    
  root.render(
    // <React.StrictMode>
    <AppProvider>
      <App />
    </AppProvider>
    // </React.StrictMode>
  );
});
...

Modify src\screens\login\index.tsx, call useAuthin login, and use the previously registered account jira(jira)to verify :

import {
    
     useAuth } from "context/auth-context";
import {
    
     FormEvent } from "react";

export const Login = () => {
    
    
  const {
    
    login, user} = useAuth()
  // HTMLFormElement extends Element (子类型继承性兼容所有父类型)(鸭子类型:duck typing: 面向接口编程 而非 面向对象编程)
  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
    
    ...};
  return (
    <form onSubmit={
    
    handleSubmit}>
      <div>
        {
    
    
          user ? <div>
            登录成功,用户名{
    
    user?.name}
          </div> : null
        }
      </div>
      <div>
        <label htmlFor="username">用户名</label>
        <input type="text" id="username" />
      </div>
      <div>
        <label htmlFor="password">密码</label>
        <input type="password" id="password" />
      </div>
      <button type="submit">登录</button>
    </form>
  );
};


Some reference notes are still in draft stage, so stay tuned. . .

Guess you like

Origin blog.csdn.net/qq_32682301/article/details/131363494