احراز هویت و اعتبارسنجی کاربران در برنامههای Angular - قسمت اول - معرفی و ایجاد ساختار برنامه
ASP.NET MVC #21
افزودن سرویس httpService.js به برنامه
تا این قسمت، تمام اطلاعات نمایش داده شدهی در لیست فیلمها، از سرویس درون حافظهای src\services\fakeMovieService.js و لیست ژانرها از سرویس src\services\fakeGenreService.js، تامین میشوند. اکنون در ادامه میخواهیم این سرویسها را با سرویس backend یاد شده، جایگزین کنیم تا این برنامه، اطلاعات خودش را از سرور دریافت کند. به همین جهت قبل از هر کاری، سرویس عمومی src\services\httpService.js را که در قسمت قبل توسعه دادیم، به برنامهی نمایش لیست فیلمها نیز اضافه میکنیم (فایل آنرا از پروژهی قبلی کپی کرده و در اینجا paste میکنیم)، تا بتوانیم از امکانات آن در اینجا نیز استفاده کنیم. فایل httpService.js، دارای وابستگیهای خارجی react-toastify و axios است. به همین جهت برای افزودن آنها مراحل زیر را طی میکنیم:
- نصب کتابخانههای react-toastify و axios از طریق خط فرمان (با فشردن دکمههای ctrl+back-tick در VSCode):
> npm i axios --save > npm i react-toastify --save
import { ToastContainer } from "react-toastify"; import "react-toastify/dist/ReactToastify.css";
render() { return ( <React.Fragment> <ToastContainer />
دریافت اطلاعات لیست نمایش ژانرها از سرویس backend
با فراخوانی آدرس https://localhost:5001/api/Genres، میتوان لیست ژانرهای سینمایی تعریف شدهی در سرویسهای backend را مشاهده کرد. اکنون قصد داریم از این اطلاعات، در برنامه استفاده کنیم. به همین جهت به فایل src\components\movies.jsx مراجعه کرده و تغییرات زیر را اعمال میکنیم:
چون نمیخواهیم تغییراتی بسیار اساسی را در اینجا اعمال کنیم، قدم به قدم عمل کرده و سرویس قبلی fakeGenreService.js را با یک سرویس جدید که اطلاعات خودش را از سرور دریافت میکند، جایگزین میکنیم. بنابراین ابتدا فایل جدید src\services\genreService.js را ایجاد میکنیم. سپس آنرا طوری تکمیل خواهیم کرد که اینترفیس آن، با اینترفیس fakeGenreService قبلی یکی باشد:
import { apiUrl } from "../config.json"; import http from "./httpService"; export function getGenres() { return http.get(apiUrl + "/genres"); }
{ "apiUrl": "https://localhost:5001/api" }
پس از تکمیل سرویس جدید src\services\genreService.js، به فایل src\components\movies.jsx بازگشته و سطر قبلی
import { getGenres } from "../services/fakeGenreService";
import { getGenres } from "../services/genreService";
Uncaught TypeError: Object is not a function or its return value is not iterable
async componentDidMount() { const { data } = await getGenres(); const genres = [{ _id: "", name: "All Genres" }, ...data]; this.setState({ movies: getMovies(), genres }); }
دریافت اطلاعات لیست فیلمها از سرویس backend
پس از دریافت لیست ژانرهای سینمایی از سرور، اکنون نوبت به جایگزینی src\services\fakeMovieService.js با یک نمونهی متصل به backend است. به همین جهت ابتدا فایل جدید src\services\movieService.js را ایجاد کرده و سپس آنرا به صورت زیر تکمیل میکنیم:
import { apiUrl } from "../config.json"; import http from "./httpService"; const apiEndpoint = apiUrl + "/movies"; function movieUrl(id) { return `${apiEndpoint}/${id}`; } export function getMovies() { return http.get(apiEndpoint); } export function getMovie(movieId) { return http.get(movieUrl(movieId)); } export function saveMovie(movie) { if (movie.id) { return http.put(movieUrl(movie.id), movie); } return http.post(apiEndpoint, movie); } export function deleteMovie(movieId) { return http.delete(movieUrl(movieId)); }
ابتدا دو متد دریافت لیست فیلمها و حذف یک فیلم را که در این کامپوننت استفاده شدهاند، import میکنیم:
import { getMovies, deleteMovie } from "../services/movieService";
async componentDidMount() { const { data } = await getGenres(); const genres = [{ id: "", name: "All Genres" }, ...data]; const { data: movies } = await getMovies(); this.setState({ movies, genres }); }
handleDelete = async movie => { const originalMovies = this.state.movies; const movies = originalMovies.filter(m => m.id !== movie.id); this.setState({ movies }); try { await deleteMovie(movie.id); } catch (ex) { if (ex.response && ex.response.status === 404) { console.log(ex); toast.error("This movie has already been deleted."); } this.setState({ movies: originalMovies }); //undo changes } };
import { toast } from "react-toastify";
اتصال فرم ثبت و ویرایش یک فیلم به backend server
تا اینجا اگر برنامه را اجرا کنیم، با کلیک بر روی لینک هر فیلم نمایش داده شدهی در صفحه، به صفحهی not-found هدایت میشویم. برای رفع این مشکل، به فایل src\components\movieForm.jsx مراجعه کرده و ابتدا
import { getGenres } from "../services/fakeGenreService"; import { getMovie, saveMovie } from "../services/fakeMovieService";
import { getGenres } from "../services/genreService"; import { getMovie, saveMovie } from "../services/movieService";
async componentDidMount() { const { data: genres } = await getGenres(); this.setState({ genres }); const movieId = this.props.match.params.id; if (movieId === "new") return; const { data: movie } = await getMovie(movieId); if (!movie) return this.props.history.replace("/not-found"); this.setState({ data: this.mapToViewModel(movie) }); }
async populateGenres() { const { data: genres } = await getGenres(); this.setState({ genres }); } async populateMovie() { try { const movieId = this.props.match.params.id; if (movieId === "new") return; const { data: movie } = await getMovie(movieId); this.setState({ data: this.mapToViewModel(movie) }); } catch (ex) { if (ex.response && ex.response.status === 404) this.props.history.replace("/not-found"); } } async componentDidMount() { await this.populateGenres(); await this.populateMovie(); }
پیشتر زمانیکه متد getMovie، یک شیء ساده را از fake service، بازگشت میداد، چنین مشکلی را نداشتیم؛ به همین جهت در سطر بعدی آن، هدایت کاربر در صورت نال بودن نتیجه، با یک return صورت میگرفت. اما اینجا بجای نال، یک استثناء را ممکن است دریافت کنیم.
مرحلهی آخر اصلاح این فرم، اتصال قسمت ثبت اطلاعات آن است که با قرار دادن یک await، پیش از متد saveMovie و async کردن متد آن، انجام میشود:
doSubmit = async () => { await saveMovie(this.state.data); this.props.history.push("/movies"); };
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-25-backend.zip و sample-25-frontend.zip
الگوی استراتژی (Strategy) اجازه میدهد که یک الگوریتم در یک کلاس بسته بندی شود و در زمان اجرا برای تغییر رفتار یک شیئ تعویض شود.برای مثال فرض کنید که ما در حال طراحی یک برنامه مسیریابی برای یک شبکه هستیم. همانطوریکه میدانیم برای مسیر یابی الگوریتمهای مختلفی وجود دارد که هر کدام دارای مزایا و معایبی هستند. و با توجه به وضعیت موجود شبکه یا عملی که قرار است انجام پذیرد باید الگوریتمی را که دارای بالاترین کارائی است انتخاب کنیم. همچنین این برنامه باید امکانی را به کاربر بدهد که کارائی الگوریتمهای مختلف را در یک شبکه فرضی بررسی کنید. حالا طراحی پیشنهادی شما برای این مسئله چست؟
دوباره فرض کنید که در مثال بالا در بعضی از الگوریتمها نیاز داریم که گرههای شبکه را بر اساس فاصلهی آنها از گره مبداء مرتب کنیم. دوباره برای مرتب سازی الگوریتمهای مختلف وجود دارد و هر کدام در شرایط خاص، کارائی بهتری نسبت به الگوریتمهای دیگر دارد. مسئله دقیقا شبیه مسئله بالا است و این مسله میتوانند دارای طراحی شبیه مسله بالا باشد. پس اگر ما بتوانیم یک طراحی خوب برای این مسئله ارائه دهیم میتوانیم این طراحی را برای مسائل مشابه به کار ببریم.
هر کدام از ما میتوانیم نسبت به درک خود از مسئله و سلیقه کاری، طراحهای مختلفی برای این مسئله ارائه دهیم. اما یک طراحی که میتواند یک جواب خوب و عالی باشد، الگوی استراتژی است که توانسته است بارها و بارها به این مسئله پاسخ بدهد.
الگوی استراتژی گزینه مناسبی برای مسائلی است که میتوانند از چندین الگوریتم مختلف به مقصود خود برسند.
نمودار UML الگوی استراتژی به صورت زیر است :
اجازه بدهید، شیوه کار این الگو را با مثال مربوط به مرتب سازی بررسی کنیم. فرض کنید که ما تصمیم گرفتیم که از سه الگویتم زیر برای مرتب سازی استفاده کنیم.
3 - الگوریتم مرتب سازی Merge Sort
ما برای مرتب سازی در این برنامه دارای سه استراتژی هستیم. که هر کدام را به عنوان یک کلاس جداگانه در نظر میگیریم (همان کلاسهای ConcreteStrategy ). برای اینکه کلاس Client بتواند به سادگی یک از استراتژیها را انتخاب کنید بهتر است که تمام کلاسهای استراتزی دارای اینترفیس مشترک باشند. برای این کار میتوانیم یک کلاس abstract تعریف کنیم و ویژگیهای مشترک کلاسهای استراتژی را در آن قرار دهیم و کلاسهای استراتژی آنها را به ارث ببرند(همان کلاس Strategy ) و پیاده سازی کنند.
در زیل کلاس Abstract که کل کلاسهای استراتژی از آن ارث میبرند را مشاهده میکنید :
abstract class SortStrategy { public abstract void Sort(ArrayList list); }
class QuickSort : SortStrategy { public override void Sort(ArrayList list) { // الگوریتم مربوطه }
}
کلاس مربوط به ShellSort
class ShellSort : SortStrategy { public override void Sort(ArrayList list) { // الگوریتم مربوطه } }
class MergeSort : SortStrategy { public override void Sort(ArrayList list) { // الگوریتم مربوطه } }
class SortedList { private ArrayList list = new ArrayList(); private SortStrategy sortstrategy; public void SetSortStrategy(SortStrategy sortstrategy) { this.sortstrategy = sortstrategy; } public void Add(string name) { list.Add(name); } public void Sort() { sortstrategy.Sort(list); } }
روش محافظت از مسیریابیهای تعریف شدهی در برنامه
شبیه به روشی را که در قسمت قبل، برای انتقال شیء user، به مسیریابی کامپوننت Movies استفاده کردیم:
<Route path="/movies" render={props => <Movies {...props} user={this.state.currentUser} />} />
<Route path="/movies/:id" component={MovieForm} />
<Route path="/movies/:id" render={props => { if (!this.state.currentUser) { return <Redirect to="/login" />; } return <MovieForm {...props} />; }} />
اکنون اگر این تغییرات را ذخیره کرده و در حالت Logout، مسیر http://localhost:3000/movies/new را مستقیما درخواست دهیم، به صفحهی لاگین هدایت خواهیم شد.
ایجاد کامپوننتی با قابلیت استفادهی مجدد، برای محافظت از مسیریابیها
هرچند روشی که تا اینجا برای محافظت از مسیریابیها معرفی شد، بدون مشکل کار میکند، اما اگر قرار باشد برای تمام مسیریابیهای اینگونه، استفاده شود، به تکرار بیش از اندازهی کدهای یکسانی خواهیم رسید. به همین جهت میتوان این منطق را تبدیل به یک کامپوننت با قابلیت استفادهی مجدد کرد؛ تا دیگر نیازی به تکرار این if/elseها نباشد. برای این منظور، فایل جدید src\components\common\protectedRoute.jsx را ایجاد میکنیم. کامپوننت جدید protectedRoute را هم در پوشهی common قرار دادهایم؛ چون وابستگی به دومین این برنامه نداشته و میتواند در سایر برنامه نیز مورد استفاده قرار گیرد. سپس با استفاده از میانبرهای imrc و sfc، یک کامپوننت تابعی بدون حالت را به نام ProtectedRoute ایجاد کرده و در آن، همان کامپوننت اصلی Route را بازگشت میدهیم. بنابراین هر زمانیکه از ProtectedRoute استفاده شود، خروجی آن، همان کامپوننت استاندارد Route خواهد بود که اینبار قرار است از وضعیت کاربر جاری وارد شدهی به سیستم، مطلع باشد. به همین جهت در اولین قدم، همان قطعه کد Route فوق را که به همراه if/else نوشتیم، از فایل app.js کپی کرده و به اینجا، داخل متد رندر کامپوننت، منتقل میکنیم. سپس شروع میکنیم به متغیر کردن عباراتی که در آن به صورت صریح و ثابت، مقدار دهی شدهاند تا به یک کامپوننت با قابلیت استفادهی مجدد برسیم:
import React from "react"; import { Route, Redirect } from "react-router-dom"; import * as auth from "../../services/authService"; const ProtectedRoute = ({ path, component: Component, render, ...rest }) => { return ( <Route {...rest} render={props => { if (!auth.getCurrentUser()) return ( <Redirect to={{ pathname: "/login", state: { from: props.location } }} /> ); return Component ? <Component {...props} /> : render(props); }} /> ); }; export default ProtectedRoute;
- در این کامپوننت نیاز است اطلاعات کاربر جاری وارد شدهی به سیستم در دسترس باشد. یا میتوان آنرا به عنوان یکی از خواص props دریافت کرد و یا همانند این مثال، امکان دریافت مستقیم آن از authService نیز وجود دارد.
- در ادامه اگر CurrentUser مقدار دهی نشده باشد، کامپوننت Redirect را که کاربر را به صفحهی لاگین هدایت میکند، بازگشت میدهیم. در غیراینصورت نیاز است یک کامپوننت را بجای برای مثال MovieForm، بازگشت دهیم. علت استفادهی از component: Component این است که React انتظار دارد، کامپوننتها با نام بزرگ شروع شوند. به همین جهت خاصیت component را از props دریافت کرده و آنرا به Component تغییر نام میدهیم.
- زمانیکه از کامپوننت Route استاندارد استفاده میشود، یا از ویژگی component آن استفاده میشود و یا از ویژگی render آن که یک تابع است، تا بتوان داخل آن، کدهای پویایی را درج کرد. به همین جهت ممکن است که مقدار متغیر کامپوننت دریافت شده، نال باشد. بنابراین در اینجا بررسی میشود که آیا Component، مقدار دهی شدهاست یا خیر؟ اگر بله، همان کامپوننت را به همراه props آن بازگشت میدهیم. در غیراینصورت، متد render مقدار دهی شده را به همراه props ارسالی به آن، بازگشت خواهیم داد.
- علت وجود پارامتر rest نیز این است که این کامپوننت علاوه بر ویژگیهایی که تاکنون پیش بینی کردهایم، ممکن است در آینده ویژگیهای دیگری را نیز نیاز داشته باشد. به همین جهت مابقی آنها را توسط {rest...}، به صورت خودکار در اینجا درج میکنیم. برای نمونه در اینجا ذکر path={path} را مشاهده نمیکنید؛ چون توسط همان {rest...} به صورت خودکار تامین میشود.
اکنون به app.js بازگشته و کدهای قبلی را با این کامپوننت جدید ProtectedRoute، جایگزین میکنیم:
import ProtectedRoute from "./components/common/protectedRoute"; // ... <ProtectedRoute path="/movies/:id" component={MovieForm} />
مدیریت بازگشت کاربران، پس از لاگین به سیستم
پس از خروج از برنامه، اگر سعی در ویرایش یکی از فیلمهای موجود کنیم، به صفحهی لاگین هدایت خواهیم شد. پس از لاگین موفق، مجددا به ریشهی سایت بازگشت داده میشویم و نه به صفحهای که پیش از لاگین، مدنظر کاربر بودهاست. برای رفع این مشکل نیاز است بتوان به آدرس قبلی درخواستی، دسترسی یافت و این مورد توسط سیستم مسیریابی، به کامپوننتها به صورت خودکار تزریق میشود. برای مثال اگر در کامپوننت ProtectedRoute، مقدار شیء props دریافتی را لاگ کنیم:
return ( <Route {...rest} render={props => { console.log(props);
همانطور که مشخص است، شیء location دریافتی از props، به همراه اطلاعات آدرسی است که پیش از هدایت خودکار به صفحهی لاگین، درخواست کرده بودیم. به همین جهت یک چنین تنظیمی، در تعاریف کامپوننت ProtectedRoute درنظر گرفته شدهاند:
<Redirect to={{ pathname: "/login", state: { from: props.location } }} />
اکنون که این شیء، به کامپوننت لاگین، پس از Redirect خودکار ارسال میشود، نیاز است به src\components\loginForm.jsx مراجعه کرده و تغییرات زیر را اعمال کنیم:
doSubmit = async () => { try { const { data } = this.state; await auth.login(data.username, data.password); const { state } = this.props.location; window.location = state ? state.from.pathname : "/"; } catch (ex) { //...
تا اینجا اگر برنامه را ذخیره کرده، از سیستم خارج شویم و سعی در ویرایش اولین رکورد موجود در لیست فیلمها کنیم، ابتدا به صفحهی لاگین هدایت میشویم. پس از لاگین موفق، اینبار بجای مشاهدهی ریشهی سایت که در اینجا به لیست فیلمها تنظیم شده، دقیقا صفحهی ویرایش جزئیات اولین فیلم را مشاهده خواهیم کرد.
عدم نمایش مجدد صفحهی لاگین، به کاربران وارد شدهی به سیستم
آخرین تغییری را که در اینجا اعمال خواهیم کرد، رفع مشکل امکان مشاهدهی مجدد صفحهی لاگین، با وارد کردن مستقیم آدرس آن در مرورگر، پس از ورود موفقیت آمیز به سیستم است. برای این منظور، ابتدای متد رندر کامپوننت فرم لاگین را به صورت زیر تغییر میدهیم تا اگر کاربر، پیشتر به سیستم وارد شده بود، به صورت خودکار به ریشهی سایت هدایت شده و مجددا فرم لاگین برای او رندر نشود:
import { Redirect } from "react-router-dom"; //... render() { if (auth.getCurrentUser()) return <Redirect to="/" />;
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-29-backend.zip و sample-29-frontend.zip