به روز رسانیهای خوشبینانهی UI
پیاده سازی اعمال CRUD توسط Axios در قسمت قبل، به همراه یک مشکل مهم است: اعمال کار با شبکه و سرور، زمانبر هستند و مدتی طول میکشد تا پاسخ عملیات از سمت سرور دریافت شود. در این بین اگر خطایی رخ دهد، مابقی کدهای نوشته شدهی در متدهایی مانند Update و Delete، اجرا نمیشوند. به این حالت «به روز رسانی بدبینانهی UI» گفته میشود. در حالت خوشبینانه، فرض بر این است که در اکثر موارد، فراخوانی سرور با موفقیت به پایان میرسد. در یک چنین حالتی، ابتدا UI به روز رسانی میشود و سپس فراخوانیهای سمت سرور صورت میگیرند. اگر این فراخوانی با شکست مواجه شد، مجددا UI را به حالت قبلی آن باز میگردانیم:
handleDelete = async post => { const posts = this.state.posts.filter(item => item.id !== post.id); this.setState({ posts }); await axios.delete(`${apiEndpoint}/${post.id}`); };
اما اگر در این بین خطایی رخ داد، چه باید کرد؟ باید آخرین تغییر انجام شده را به حالت اول باز گرداند. انجام یک چنین کاری در React سادهاست. چون ما state را به صورت مستقیم ویرایش نمیکنیم، همیشه میتوان ارجاعی را به state قبلی، ذخیره و سپس در صورت نیاز آنرا بازیابی کرد:
handleDelete = async post => { const originalPosts = this.state.posts; const posts = this.state.posts.filter(item => item.id !== post.id); this.setState({ posts }); // Optimistic Update try { await axios.delete(`${apiEndpoint}/${post.id}`); } catch (ex) { alert("An error occurred when deleting a post!"); this.setState({ posts: originalPosts }); // Undo changes } };
مدیریت خطاهای رخ دادهی در حین فراخوانی سرور
تا اینجا مشاهده کردیم که یک روش مدیریت خطاها در کدهای Axios، قرار دادن آنها در یک قطعه کد try/catch است. در اینجا نیز باید بتوان بین خطاهای پیش بینی شده و نشده، تفاوت قائل شد.
- خطاهای پیش بینی شده: برای مثال اگر درخواست حذف رکوردی را دادیم که در بانک اطلاعاتی موجود نیست، انتظار داریم سرور، خطای 404 یا return NotFound را بازگشت دهد و یا 400 که معادل bad request است و در حالت ارسال دادههایی غیرمعتبر، رخ میدهد. در این موارد بهتر است خطاهایی خاص را به کاربران نمایش داد؛ برای مثال رکورد درخواستی وجود ندارد یا پیشتر حذف شدهاست.
- خطاهای پیش بینی نشده: این نوع خطاها نباید و یا قرار نیست در شرایط عادی رخ دهند. برای مثال اگر شبکه در دسترس نیست، امکان ارتباط با سرور نیز میسر نخواهد بود و یا حتی ممکن است خطایی در کدهای سمت سرور، سبب بروز خطایی شده باشد. این نوع خطاها ابتدا باید لاگ شوند تا با بررسیهای آتی آنها، بتوان مشکلات پیش بینی نشده را بهتر برطرف کرد. همچنین در یک چنین مواردی، باید یک پیام خطای خیلی عمومی را به کاربر نمایش داد؛ برای مثال «یک خطای پیش بینی نشده رخ دادهاست.».
برای مدیریت این دو حالت باید به جزئیات شیء ex، در بدنهی catch، دقت کرد که دارای دو خاصیت request و response است. اگر ex.response تنظیم شده بود، یعنی دریافت خروجی از سرور موفقیت آمیز بودهاست. اگر سرور در دسترس نباشد و یا برنامهی سمت سرور کرش کرده باشد، ex.response نال خواهد بود. اگر ex.request نال نبود، یعنی ارسال درخواست به سمت سرور با موفقیت انجام شدهاست. برای مثال جهت بررسی خطای مورد انتظار 404، میتوان در قسمت catch(ex) به صورت زیر عمل کرد:
try { await axios.delete(`${apiEndpoint}/${post.id}`); } catch (ex) { if (ex.response && ex.response.status === 404) { alert("This post has already been deleted!"); } else { console.log("Error", ex); alert("An unexpected error occurred when deleting a post!"); } this.setState({ posts: originalPosts }); // Undo changes }
عموما خطاهای پیشبینی شده را لاگ نمیکنیم؛ چون ممکن است کاربر، یک صفحه را در چندین برگه باز کرده باشد و در یکی، رکوردی را حذف کند. در این حال، این رکورد هنوز در برگههای دیگر موجود است و اگر مجددا درخواست حذف آنرا صادر کند، مشکل خاصی از دیدگاه برنامه رخ ندادهاست و نیازی به پیگیریهای آتی را ندارد. یعنی صرفا یک client error است.
مدیریت سراسری خطاهای رخ دادهی در حین فراخوانی سرور
برای مدیریت خطاها، نیاز است یک چنین try/catchهایی را در تمام قسمتهای برنامه که با سرور کار میکنند، قرار دهیم. برای کاهش این کدهای تکراری، از interceptors کتابخانهی Axios استفاده میشود. در این کتابخانه میتوان در جاهائیکه درخواستی به سمت سرور ارسال میشود و یا پاسخی از سمت سرور دریافت میشود، قطعه کدهایی سراسری را قرار داد و بر روی درخواست و یا پاسخ، تغییراتی را اعمال کرد و یا حتی اطلاعات مربوطه را لاگ کرد؛ به این نوع قطعه کدها، interceptor گفته میشود و برای تعریف آنها میتوان از axios.interceptors.request و یا axios.interceptors.response، خارج از کلاس جاری استفاده کرد. برای مثال بر روی شیء axios.interceptors.response، میتوان متد use را فراخوانی کرد که دو پارامتر را که هر کدام یک callback function هستند، میپذیرد. اولی در صورت موفقیت آمیز بودن response فراخوانی میشود و دومی در صورت شکست آن. اگر نیازی به هر کدام نبود، میتوان آنرا به null مقدار دهی کرد. اگر مدیریت قسمت شکست علمیات مدنظر است، نیاز خواهد بود در پایان این callback function، یک Rejected Promise را بازگشت داد تا ادامهی برنامه، به درستی مدیریت شود. در این حالت اگر خطایی رخ دهد، ابتدا این interceptor فراخوانی میشود و سپس کنترل به بدنهی catch منتقل خواهد شد:
import "./App.css"; import axios from "axios"; import React, { Component } from "react"; axios.interceptors.response.use(null, error => { console.log("interceptor called."); return Promise.reject(error); }); const apiEndpoint = "https://localhost:5001/api/posts"; class App extends Component {
axios.interceptors.response.use(null, error => { const expectedError = error.response && error.response.status >= 400 && error.response.status < 500; if (!expectedError) { console.log("Error", error); alert("An unexpected error occurred when deleting a post!"); } return Promise.reject(error); });
یک نکته: استفاده از try/catchها فقط برای بازگشت UI به حالت قبلی و یا نمایش خطایی خاص به کاربر توصیه میشوند. اگر از روش «به روز رسانیهای خوشبینانهی UI» استفاده نمیکنید و همچنین خطاهای ویژهای بجز خطای عمومی لاگ شدهی در interceptor فوق مدنظر شما نیست، نیازی هم به try/catch نخواهد بود و پس از بروز خطا، قسمتهای بعدی کد اجرا نمیشوند؛ اما خطای عمومی فوق نمایش داده خواهد شد.
ایجاد یک HTTP Service با قابلیت استفادهی مجدد
تا اینجا تعریف interceptor را پیش از کلاس کامپوننت جاری قرار دادهایم که هم سبب شلوغی این ماژول شدهاست و هم در صورت نیاز به آن در سایر برنامهها، باید همین قطعه کد را مجددا در آنها کپی کرد. به همین جهت پوشهی جدید src\services را ایجاد کرده و سپس فایل src\services\httpService.js را در آن با محتوای زیر ایجاد میکنیم:
import axios from "axios"; axios.interceptors.response.use(null, error => { const expectedError = error.response && error.response.status >= 400 && error.response.status < 500; if (!expectedError) { console.log("Error", error); alert("An unexpected error occurred when deleting a post!"); } return Promise.reject(error); }); export default { get: axios.get, post: axios.post, put: axios.put, delete: axios.delete };
سپس به app.js مراجعه کرده و این ماژول را با یک نام دلخواه import میکنیم:
import http from "./services/httpService";
ایجاد یک ماژول Config
بهبود دیگری را که میتوانیم اعمال کنیم، انتقال const apiEndpoint تعریف شده، به یک ماژول مجزا است؛ تا اگر نیاز به استفادهی از آن در قسمتهای دیگری نیز وجود داشت، به سادگی بتوان آنرا مدیریت کرد. به همین جهت فایل جدید src\config.json را با محتوای زیر ایجاد میکنیم:
{ "apiEndpoint" : "https://localhost:5001/api/posts" }
import config from "./config.json";
نمایش بهتر خطاها به کاربر توسط کتابخانهی react-toastify
بجای alert توکار مرورگرها، میتوان یک صفحهی دیالوگ زیباتر را برای نمایش خطاها درنظر گرفت. به همین جهت ابتدا کتابخانهی react-toastify را نصب میکنیم:
> npm i react-toastify --save
import { ToastContainer } from "react-toastify"; import "react-toastify/dist/ReactToastify.css";
render() { return ( <React.Fragment> <ToastContainer />
import { toast } from "react-toastify"; // ... axios.interceptors.response.use(null, error => { // ... if (!expectedError) { // ... toast.error("An unexpected error occurrred."); }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-22-backend-part-03.zip و sample-22-frontend-part-03.zip