در
قسمت قبل، دکمهی new movie را برای کاربران وارد نشدهی به سیستم، از صفحهی نمایش لیست فیلمها، مخفی کردیم. اما ... اگر آدرس http://localhost:3000/movies/new مستقیما در مرورگر وارد شود، هنوز هم برای عموم کاربران قابل دسترسی است.
روش محافظت از مسیریابیهای تعریف شدهی در برنامه
شبیه به روشی را که در قسمت قبل، برای انتقال شیء user، به مسیریابی کامپوننت Movies استفاده کردیم:
<Route
path="/movies"
render={props => <Movies {...props} user={this.state.currentUser} />}
/>
در اینجا نیز میتوان برای محافظت از یک مسیریابی، استفاده کرد. به همین جهت به app.js مراجعه کرده و مسیریابی فعلی کامپوننت MovieForm را:
<Route path="/movies/:id" component={MovieForm} />
به صورت زیر تغییر میدهیم:
<Route
path="/movies/:id"
render={props => {
if (!this.state.currentUser) {
return <Redirect to="/login" />;
}
return <MovieForm {...props} />;
}}
/>
اینبار نیز بجای ویژگی component، از ویژگی render استفاده میکنیم تا بتوان در اینجا به صورت پویا، کدنویسی کرد. ابتدا بررسی میکنیم که آیا کاربر جاری تنظیم شدهاست؟ اگر خیر، او را به صفحهی لاگین هدایت میکنیم؛ در غیراینصورت، همان کامپوننت 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 بعنوان پارامتر این کامپوننت، از طریق Object Destructuring، خواصی را که قرار است به صورت props دریافت کنیم، مشخص کردهایم. مزیت اینکار، مشخص شدن اینترفیس این کامپوننت به نحو واضحی است. برای مثال بجای ذکر مقدار ویژگی path، به صورت یک رشتهی ثابت، آنرا از طریق یک متغیر دریافت میکنیم.
- در این کامپوننت نیاز است اطلاعات کاربر جاری وارد شدهی به سیستم در دسترس باشد. یا میتوان آنرا به عنوان یکی از خواص 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، همانند نحوهی تعریف کامپوننت Route استاندارد است؛ با این تفاوت که این کامپوننت در پشت صحنه، از وضعیت کاربر جاری سیستم مطلع است و بر اساس آن واکنش نشان میدهد.
مدیریت بازگشت کاربران، پس از لاگین به سیستم
پس از خروج از برنامه، اگر سعی در ویرایش یکی از فیلمهای موجود کنیم، به صفحهی لاگین هدایت خواهیم شد. پس از لاگین موفق، مجددا به ریشهی سایت بازگشت داده میشویم و نه به صفحهای که پیش از لاگین، مدنظر کاربر بودهاست. برای رفع این مشکل نیاز است بتوان به آدرس قبلی درخواستی، دسترسی یافت و این مورد توسط سیستم مسیریابی، به کامپوننتها به صورت خودکار تزریق میشود. برای مثال اگر در کامپوننت ProtectedRoute، مقدار شیء props دریافتی را لاگ کنیم:
return (
<Route
{...rest}
render={props => {
console.log(props);
و سپس بر روی لینک به مشاهدهی جزئیات و ویرایش یک فیلم کلیک کنیم، تصویر زیر حاصل میشود:
همانطور که مشخص است، شیء location دریافتی از props، به همراه اطلاعات آدرسی است که پیش از هدایت خودکار به صفحهی لاگین، درخواست کرده بودیم. به همین جهت یک چنین تنظیمی، در تعاریف کامپوننت ProtectedRoute درنظر گرفته شدهاند:
<Redirect
to={{
pathname: "/login",
state: { from: props.location }
}}
/>
در کامپوننت Redirect، مقدار to میتواند یک رشته و یا یک شیء باشد. اگر حالت انتساب یک شیء را انتخاب کردیم، خاصیت pathname آن مانند قبل است و مکان نهایی Redirect را مشخص میکند. اما کار خاصیت state آن، ارسال اطلاعاتی اضافی است به کامپوننتی که قرار است کار Redirect به آن صورت گیرد. برای مثال در تنظیم فوق، شیء ای که دارای خاصیت 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) {
//...
در اینجا خاصیت state، از شیء location تزریق شدهی به props این کامپوننت، استخراج میشود. سپس با مقدار دهی window.location به from.pathname آن، کار هدایت کاربر را پس از لاگین موفق، به آدرس قبلی مدنظر او، انجام میدهیم.
تا اینجا اگر برنامه را ذخیره کرده، از سیستم خارج شویم و سعی در ویرایش اولین رکورد موجود در لیست فیلمها کنیم، ابتدا به صفحهی لاگین هدایت میشویم. پس از لاگین موفق، اینبار بجای مشاهدهی ریشهی سایت که در اینجا به لیست فیلمها تنظیم شده، دقیقا صفحهی ویرایش جزئیات اولین فیلم را مشاهده خواهیم کرد.
عدم نمایش مجدد صفحهی لاگین، به کاربران وارد شدهی به سیستم
آخرین تغییری را که در اینجا اعمال خواهیم کرد، رفع مشکل امکان مشاهدهی مجدد صفحهی لاگین، با وارد کردن مستقیم آدرس آن در مرورگر، پس از ورود موفقیت آمیز به سیستم است. برای این منظور، ابتدای متد رندر کامپوننت فرم لاگین را به صورت زیر تغییر میدهیم تا اگر کاربر، پیشتر به سیستم وارد شده بود، به صورت خودکار به ریشهی سایت هدایت شده و مجددا فرم لاگین برای او رندر نشود:
import { Redirect } from "react-router-dom";
//...
render() {
if (auth.getCurrentUser()) return <Redirect to="/" />;
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید:
sample-29-backend.zip و
sample-29-frontend.zip