【実戦】6. ユーザーエクスペリエンスの最適化 - 読み込みとエラー状態の処理 (中) —— React17+React Hook+TS4 ベストプラクティス、Jira エンタープライズレベルプロジェクトを模倣 (9)


学習コンテンツのソース: React + React Hook + TS ベスト プラクティス - 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
クラコレス ^2.0.0
@クラコ/クラコ ^7.1.0
qs ^6.11.0
デイジェス ^1.11.7
反応ヘルメット ^6.1.0
@types/react-helmet ^6.1.6
反応クエリ ^6.1.0
@welldone-software/why-did-you-render ^7.0.1
@emotion/反応 & @emotion/styled ^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 - フロントエンドインタビュー集 - MOOC コラム)

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 種類のタスクがあるためです。

  • マクロタスク (マクロタスク): 同期スクリプト (コード全体)、setTimeout コールバック関数、setInterval コールバック関数、I/O、Ul レンダリング。
  • マイクロタスク: process.nextTick、Promise コールバック関数、Object.observe、MutationObserver

実行順序は次のとおりです。

  1. まず、JavaScript エンジンがマクロ タスクを実行します。このマクロ タスクは通常、現在の同期コードであるメイン コード自体を参照することに注意してください。
  2. 実行中にマイクロタスクが発生すると、マイクロタスクのタスク キューに追加されます。
  3. マクロタスクの実行が完了すると、現在のマイクロタスク キュー内のマイクロタスクは、マイクロタスク キューがクリアされるまで直ちに実行されます。
  4. マイクロタスクの実行が完了すると、次のマクロタスクの実行が開始されます。
  5. このサイクルは、マクロタスクとマイクロタスクがクリアされるまで繰り返されます。

一部の参考ノートはまだ草案段階にあるため、今後の内容に注目してください。

Guess you like

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