【실제 전투】6. 사용자 경험 최적화 - 로딩 및 오류 상태 처리(중간) —— React17+React Hook+TS4 모범 사례, Jira 엔터프라이즈 수준 프로젝트 모방(9)


학습 콘텐츠 소스: React + React Hook + TS Best Practice - MOOC


원래 튜토리얼과 비교하여 연구 초기(2023.03)에 최신 버전을 사용했습니다.

안건 버전
반응 및 반응 돔 ^18.2.0
반응 라우터 및 반응 라우터 돔 ^6.11.2
개미 ^4.24.8
@commitlint/cli & @commitlint/config-conventional ^17.4.4
eslint-config-prettier ^8.6.0
에스키모 개의 ^8.0.3
린트 스테이징 ^13.1.2
더 예쁘다 2.8.4
json 서버 0.17.2
craco-less ^2.0.0
@craco/craco ^7.1.0
질문 ^6.11.0
데이즈 ^1.11.7
반응 헬멧 ^6.1.0
@유형/반응 헬멧 ^6.1.6
반응 쿼리 ^6.1.0
@welldone-software/why-d-you-render ^7.0.1
@감정/반응 및 @감정/스타일 ^11.10.6

구체적인 구성, 작동 및 내용이 다르며 "피트"도 다릅니다. . .


1. 프로젝트 시작: 프로젝트 초기화 및 구성

2. React 및 Hook 애플리케이션: 프로젝트 목록 구현

3. TS 적용 : JS 갓 어시스트 - 강형

4. JWT, 사용자 인증 및 비동기 요청


5. CSS는 실제로 매우 간단합니다. CSS-in-JS로 스타일을 추가하세요.


6. 사용자 경험 최적화 - 로딩 및 오류 상태 처리

1~2

3. 로그인 및 등록 페이지 Loading 및 Error 상태 처리, Event Loop 상세 설명

목록 페이지의 비동기 상태가 완료되었으며 다음 단계는 등록 페이지에 로그인하는 것입니다.

수정 src\unauthenticated-app\index.tsx(새로운 상태 처리, j 모니터링 작업을 로그인 등록 페이지로 error넘기기 ):error

...
import {
    
     Card, Button, Divider, Typography } from "antd";
...

export const UnauthenticatedApp = () => {
    
    
  ...
  const [error, setError] = useState<Error | null>(null);
  return (
    <Container>
      ...
      <ShadowCard>
        <Title>{
    
    isRegister ? "请注册" : "请登录"}</Title>
        {
    
     error ? <Typography.Text type="danger">{
    
    error.message}</Typography.Text> : null }
        {
    
    isRegister ? <Register onError={
    
    setError}/> : <Login onError={
    
    setError}/>}
        <Divider />
        ...
      </ShadowCard>
    </Container>
  );
};
...

수정 ( 비동기 작업 후 src\unauthenticated-app\login.tsx전달 및 사용 ):onErrorcatch

...
export const Login = ({
    
    onError}: {
    
     onError: (error: Error) => void }) => {
    
    
  ...
  const handleSubmit = (values: {
     
      username: string; password: string }) => {
    
    
    login(values).catch(e => onError(e))
  };
  ...
};
...

같은 방식으로 수정 src\unauthenticated-app\register.tsx:

...
export const Register = ({
    
    onError}: {
    
     onError: (error: Error) => void }) => {
    
    
  ...
  const handleSubmit = (values: {
     
      username: string; password: string }) => {
    
    
    register(values).catch(e => onError(e))
  };
  ...
};
...

기본이 아닌 사용자 이름 및 암호 확인 사용: 응답 없음. . . 그러나 콘솔은 방금 입력한 사용자 이름과 비밀번호를 출력합니다. . .

이 문제의 원인은 로그인된 콜 체인을 통해 찾을 수 있습니다.src\auth-provider.ts

  • !res.ok, 반환했지만 Promise.reject(data)매개 data변수를 입력하도록 요청했는데 이는 분명히 예상되는 효과가 아닙니다 (등록은 동일). 이 부분을 다음과 같이 수정하십시오.Promise.reject(await res.json())

수정 후 다시 확인하면 완료!

Promise.catch사용하기 쉽지만 사고 방식을 바꾸고 사용 try..catch하고 이끌어냅니다 Event Loop.

src\unauthenticated-app\login.tsx물을 테스트하기 위해 먼저 수정하십시오 .

...
export const Login = ({
    
    onError}: {
    
     onError: (error: Error) => void }) => {
    
    
  ...
  const handleSubmit = (values: {
     
      username: string; password: string }) => {
    
    
    try {
    
    
      // login(values).catch(e => onError(e))
      login(values);
    } catch(e: Error | any) {
    
    
      onError(e)
    }
  };
  ...
};
...

콘솔 출력은 양호하지만 인터페이스는 효과가 없습니다. . .

문제는 로그인이 비동기 작업이라는 점인데, 프로그램에서 동기 작업이 먼저 실행된 다음 비동기 작업이 실행되므로 onError가 먼저 실행되고 백엔드에서 반환된 오류 메시지가 수신되지 않습니다.

다시 수정합니다 src\unauthenticated-app\login.tsx(비동기 작업을 처리하려면 async await 사용).

...
export const Login = ({
    
    onError}: {
    
     onError: (error: Error) => void }) => {
    
    
  ...
  const handleSubmit = async (values: {
     
      username: string; password: string }) => {
    
    
    try {
    
    
      // login(values).catch(e => onError(e))
      await login(values);
    } catch(e: Error | any) {
    
    
      onError(e)
    }
  };
  ...
};
...

이것은 정상입니다!

다음으로 등록 페이지에 비밀번호 확인 기능을 추가합니다.

수정 src\unauthenticated-app\register.tsx(암호 확인 Form.Item및 관련 처리 로직 추가):

...
export const Register = ({
    
    onError}: {
    
     onError: (error: Error) => void }) => {
    
    
  const {
    
     register, user } = useAuth();
  const handleSubmit = ({
     
      cpassword, ...values }: {
     
      username: string, password: string, cpassword: string }) => {
    
    
    if (cpassword === values.password) {
    
    
      register(values).catch(e => onError(e));
    } else {
    
    
      onError(new Error('请确认两次的输入密码相同'))
      return
    }
  };
  return (
    <Form onFinish={
    
    handleSubmit}>
      <Form.Item
        name="username"
        rules={
    
    [{
    
     required: true, message: "请输入用户名" }]}
      >
        <Input placeholder="用户名" type="text" id="username" />
      </Form.Item>
      <Form.Item
        name="password"
        rules={
    
    [{
    
     required: true, message: "请输入密码" }]}
      >
        <Input placeholder="密码" type="password" id="password" />
      </Form.Item>
      <Form.Item
        name="cpassword"
        rules={
    
    [{
    
     required: true, message: "请确认密码" }]}
      >
        <Input placeholder="确认密码" type="password" id="cpassword" />
      </Form.Item>
      <Form.Item>
        <LongButton htmlType="submit" type="primary">
          注册
        </LongButton>
      </Form.Item>
    </Form>
  );
};

Loading그런 다음 로그인 등록 페이지에 대한 비동기 상태 처리를 추가합니다 .

...
import {
    
     useAsync } from "utils/use-async";

export const Login = ({
    
    onError}: {
    
     onError: (error: Error) => void }) => {
    
    
  const {
    
     login, user } = useAuth();
  const {
    
     run, isLoading } = useAsync()

  const handleSubmit = async (values: {
     
      username: string; password: string }) => {
    
    
    try {
    
    
      // login(values).catch(e => onError(e))
      await run(login(values))
    } catch(e: Error | any) {
    
    
      onError(e)
    }
  };
  return (
    <Form onFinish={
    
    handleSubmit}>
      ...
      <Form.Item>
        <LongButton loading={
    
    isLoading} htmlType="submit" type="primary">
          登录
        </LongButton>
      </Form.Item>
    </Form>
  );
};
...

확인해보세요. 아무런 효과가 없지만 콘솔에서 400 오류가 발생합니다. 확인하세요.

  • try..catchin이 onError수신되지 않았습니다. 유일한 변수는 runthis 입니다.
  • run확인해보세요. 오류가 내부적으로 소화되어 정상적으로 폐기되지 않은 것으로 나타났습니다 ( catch수신 error throw또는 패키지를 Promise.reject반환할 수 있으며 후자가 권장됨).

수정 src\utils\use-async.ts:

...
export const useAsync = <D>(initialState?: State<D>) => {
    
    
  ...
  // run 来触发异步请求
  const run = (promise: Promise<D>) => {
    
    
    ...
    return promise
      .then(...)
      .catch((error) => {
    
    
        // catch 会消化异常,如果不主动抛出,外面是接收不到异常的
        setError(error);
        // return error; // 原代码
        // throw error;
        return Promise.reject(error);
      });
  };
  ...
};

확인하십시오. 정상 catch이며 오류 메시지가 표시됩니다.

  • try...catch는 런타임 오류에만 작동합니다(try...catch는 유효한 코드의 예외만 처리할 수 있음).
  • try...catch는 동기식으로 작동합니다(try...catch는 동기 코드의 예외만 처리할 수 있음).

문제는 해결되었지만 이 try...catch는 여전히 약간 엉성한 느낌이 들므로 계속해서 최적화합니다.

수정 src\utils\use-async.ts(로직을 합리화하기 위해 예외를 발생시킬지 여부의 구성 증가):

...
const defaultConfig = {
    
    
  throwOnError: false
}

export const useAsync = <D>(initialState?: State<D>, initialConfig?: typeof defaultConfig) => {
    
    
  const config = {
    
    ...defaultConfig, ...initialConfig}
  ...

  // run 来触发异步请求
  const run = (promise: Promise<D>) => {
    
    
    ...
    return promise
      .then((data) => {
    
    
        setData(data);
        return data;
      })
      .catch((error) => {
    
    
        // catch 会消化异常,如果不主动抛出,外面是接收不到异常的
        setError(error);
        return config.throwOnError ? Promise.reject(error) : error;
      });
  };
  ...
};

수정 src\unauthenticated-app\login.tsx( 전달 { throwOnError: true }):

...
export const Login = ({
    
    onError}: {
    
     onError: (error: Error) => void }) => {
    
    
  ...
  const {
    
     run, isLoading } = useAsync(undefined, {
    
     throwOnError: true })
  ...
};
...

같은 방식으로 수정 src\unauthenticated-app\register.tsx:

...
export const Register = ({
    
    onError}: {
    
     onError: (error: Error) => void }) => {
    
    
  ...
  const {
    
     run, isLoading } = useAsync(undefined, {
    
     throwOnError: true })

  const handleSubmit = async ({
     
      cpassword, ...values }: {
     
      username: string, password: string, cpassword: string }) => {
    
    
    if (cpassword === values.password) {
    
    
      try {
    
    
        await run(register(values))
      } catch (e: Error | any) {
    
    
        onError(e)
      }
    } else {
    
    
      onError(new Error('请确认两次的输入密码相同'))
      return
    }
  };
  return (
    <Form onFinish={
    
    handleSubmit}>
      ...
      <Form.Item>
        <LongButton loading={
    
    isLoading} htmlType="submit" type="primary">
          注册
        </LongButton>
      </Form.Item>
    </Form>
  );
};

마지막으로 다음을 수정합니다 ( src\unauthenticated-app\index.tsx로그인과 등록 사이를 전환할 때 error지우기 ).

...
export const UnauthenticatedApp = () => {
    
    
  const [isRegister, setIsRegister] = useState(false);
  const [error, setError] = useState<Error | null>(null);
  return (
    <Container>
      ...
      <ShadowCard>
        ...
        <Button type="link" onClick={
    
    () => {
    
     setIsRegister(!isRegister); setError(null) }}>
          切换到{
    
    isRegister ? "已经有账号了?直接登录" : "没有账号?注册新账号"}
        </Button>
      </ShadowCard>
    </Container>
  );
};
...

테스트 효과는 완벽합니다!

Extended Learning (출처: High Salary Road - Front-end Interview Collection - MOOC Column )

js는 단일 스레드이고 비동기는 js에서 직관적이지 않습니다.

인쇄 순서 결정:

console.log('script start')
setTimeout(function(){
    
    
  console.log('setTimeout');
},0);
new Promise(function(resolve){
    
    
  console.log('promise1');
  resolve();
  console.log('promise2');
}).then(function(){
    
    
  console.log('promise then');
});
console log('script end');

인쇄 주문:

script start
promise1
promise2
script end
promise then
setTimeout

JavaScript에는 2가지 종류의 작업이 있기 때문입니다.

  • 매크로 태스크(macro-task): 동기식 스크립트(전체 코드), setTimeout 콜백 함수, setlninterval 콜백 함수, l/O, Ul 렌더링;
  • 마이크로 태스크: process.nextTick, Promise 콜백 함수, Object.observe, MutationObserver

실행 순서는 다음과 같습니다.

  1. 첫째, JavaScript 엔진은 매크로 작업을 실행합니다.이 매크로 작업은 일반적으로 현재 동기 코드인 기본 코드 자체를 참조합니다.
  2. 실행 중에 마이크로태스크가 발견되면 마이크로태스크 태스크 대기열에 추가됩니다.
  3. 매크로태스크 실행이 완료된 후 현재 마이크로태스크 큐의 마이크로태스크는 마이크로태스크 큐가 지워질 때까지 즉시 실행됩니다.
  4. 마이크로태스크의 실행이 완료되면 다음 매크로태스크의 실행이 시작됩니다.
  5. 매크로태스크와 마이크로태스크가 지워질 때까지 이 주기가 반복됩니다.

일부 참조 노트는 아직 초안 단계에 있으므로 계속 지켜봐 주시기 바랍니다. . .

추천

출처blog.csdn.net/qq_32682301/article/details/131587312