ساخت ربات تلگرامی با #C
فزض کنید من 10 شماره دارم میخوام براشون مثل تبلیغات اس ام اسی پیام بدم .. چجوری این امکان هست؟
فکر کنم از متودهای خود تلگرام باید بشه
کوکیهایی که باید HTTPS only شوند
کوکیهای پیشفرض برنامههای ASP.NET به صورت HTTP Only به سمت کلاینت ارسال میشوند. این کوکیها توسط اسکریپتها قابل خوانده شدن نیستند و به همین جهت یکی از راههای مقاومت بیشتر در برابر حملات XSS به شمار میروند. پس از ارتقاء به HTTPS، این کوکیها را هم میتوان HTTPs Only کرد تا فقط به کلاینتهایی که از طریق آدرس HTTPS سایت به آن وارد شدهاند، ارائه شود:
1) کوکی آنتیفورجری توکن
AntiForgeryConfig.RequireSsl = true;
<configuration> <system.web> <authentication mode="Forms"> <forms requireSSL="true" cookieless="UseCookies"/> </authentication> </system.web> </configuration>
3) تمام httpCookies
<configuration> <system.web> <httpCookies httpOnlyCookies="true" requireSSL="true" /> </system.web> </configuration>
4) کوکیهای Role manager
<configuration> <system.web> <roleManager cookieRequireSSL="true" /> </system.web> </configuration>
5) کوکیهای OWIN Authentication و ASP.NET Identity 2.x
var options = new CookieAuthenticationOptions() { CookieHttpOnly = true, CookieSecure = CookieSecureOption.Always, ExpireTimeSpan = TimeSpan.FromMinutes(10) };
فعالسازی اجبار به استفادهی از HTTPS
با استفاده از فیلتر RequireHttps، دسترسی به تمام اکشن متدهای برنامه تنها به صورت HTTPS میسر خواهد شد:
filters.Add(new RequireHttpsAttribute(permanent: true));
using System.Web.Mvc; namespace MyWebsite { internal static class FilterConfig { internal static void RegisterGlobalFilters(GlobalFilterCollection filters) { filters.Add(new RequireHttpsAttribute(permanent: true)); } } }
همچنین در متد Application_BeginRequest نیز میتوان بررسی کرد که آیا درخواست ارسالی یک درخواست HTTPS است یا خیر؟ اگر خیر، میتوان کاربر را به صورت خودکار به نگارش HTTPS آن هدایت دائم کرد:
protected void Application_BeginRequest(Object sender, EventArgs e) { if (!HttpContext.Current.Request.IsSecureConnection) { var builder = new UriBuilder { Scheme = "https", Host = Request.Url.Host, // use the RawUrl since it works with URL Rewriting Path = Request.RawUrl }; Response.Status = "301 Moved Permanently"; Response.AddHeader("Location", builder.ToString()); } }
فعالسازی HSTS جهت اطلاع به مرورگر که این سایت تنها از طریق HTTPS قابل دسترسی است و تمام درخواستهای HTTP را به صورت خودکار از طریق HTTPS انجام بده:
<httpProtocol> <customHeaders> <add name="Strict-Transport-Security" value="max-age=16070400; includeSubDomains" />
<rewrite> <rules> <rule name="Redirect to HTTPS" stopProcessing="true"> <match url="(.*)" /> <conditions> <add input="{HTTPS}" pattern="off" ignoreCase="true" /> <add input="{HTTP_HOST}" negate="true" pattern="localhost" /> </conditions> <action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Permanent" /> </rule>
اصلاح تمام آدرسهای مطلقی که توسط برنامه تولید میشوند
اگر در برنامهی خود از Url.Action برای تولید آدرسها استفاده میکنید، با ذکر پارامتر protocol آن، آدرس تولیدی آن بجای یک مسیر نسبی، یک مسیر مطلق خواهد بود. اگر پیشتر این پروتکل را به صورت دستی به http تنظیم کردهاید، روش صحیح آن به صورت زیر است که با آدرس جدید HTTPS سایت هم سازگار است:
var fullBaseUrl = Url.Action(result: MVC.Home.Index(), protocol: this.Request.Url.Scheme);
اصلاح تمام آدرسهایی که پیشتر توسط برنامه تولید شدهاند
یک نمونه آن در مطلب «به روز رسانی تمام فیلدهای رشتهای تمام جداول بانک اطلاعاتی توسط Entity framework 6.x» بحث شدهاست.
اصلاح فایل robots.txt و درج آدرس HTTPS جدید
اگر در فایل robots.txt سایت، آدرس مطلق Sitemap را به صورت HTTP درج کرده بودید، آنرا به HTTPS تغییر دهید:
User-agent: * Sitemap: https://www.dntips.ir/Sitemap
-به ازای هر درخواست به بانک اطلاعاتی رجوع میکند؟
برای سناریو هایی که تعداد کاربران زیاد، به همراه تعداد درخواستهای زیاد وسرعت بالا مورد نیاز است، این روش بهینه میباشد؟(البته قبول دارم که باید جهت این نیاز سرورهای اختصاصی تهیه کرد)
-سرعت کاهش پیدا نمیکند؟
بهتر نیست از FileSystem بهره برد؟
A connection was successfully established with the server, but then an error occurred during the login process. (provider: SSL Provider, error: 0 - The certificate chain was issued by an authority that is not trusted.)
اگر نیاز به استفادهی از پرچم Encrypt=True وجود داشته باشد:
- باید سرور اس کیوال با یک certificate معتبر تنظیم شود.
- کلاینت باید این certificate را تائید کند. اگر کلاینت چنین قصدی را ندارد، میتواند تنظیم TrustServerCertificate=True را به رشتهی اتصالی اضافه کند.
در غیراینصورت، خطای ذکر شده را دریافت خواهیم کرد.
برای رفع این مشکل یا باید رمزنگاری اتصالات را در SQL Server پیاده سازی کرد و یا از این پس، ذکر پرچم Encrypt=False در انتهای رشتهی اتصالی به آن، ضروری است. برای مثال یک نمونه رشتهی اتصالی تغییر یافتهی بر اساس این تنظیم، به صورت زیر است:
Data Source=.\\SQLEXPRESS;Initial Catalog=MyTestDb;Integrated Security=true;Encrypt=False;
بدیهی است این تغییر مختص به EF 7.0 نیست و تمام برنامههایی که از ADO.NET و نگارش جدید Microsoft.Data.SqlClient استفاده میکنند، باید به آن توجه داشته باشند.
تهیه یک binder provider پردازش رشتهها
کار model binding، تطابق اطلاعات رسیدهی از درخواست جاری، با پارامترهای اکشن متد یک کنترلر است. هر مقدار رسیده، به یک binder متناسب ارسال میشود تا پردازش آن مدیریت گردد. به صورت پیش فرض در ASP.NET Core، تعدد 14 عدد binder providers که اینترفیس IModelBinderProvider را پیاده سازی میکنند، در این بین جهت یافتن یک binder مناسب، بررسی خواهند شد. برای مثال کار یک binder، پردازش نوعهای پیچیدهاست (complex types) و دیگری نوعهای ساده (simple types) مانند int و string را پردازش میکند.
public class CustomStringModelBinderProvider : IModelBinderProvider { public IModelBinder GetBinder(ModelBinderProviderContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } if (context.Metadata.IsComplexType) { return null; } var fallbackBinder = new SimpleTypeModelBinder(context.Metadata.ModelType); if (context.Metadata.ModelType == typeof(string)) { return new CustomStringModelBinder(fallbackBinder); } return fallbackBinder; } }
سپس اگر نوع مدل جاری رشتهای بود، وهلهای از CustomStringModelBinder را بازگشت میدهیم (کلاسی است که آنرا در ادامه تهیه خواهیم کرد). درغیراینصورت همان SimpleTypeModelBinder توکار این فریمورک را بازگشت خواهیم داد.
تهیهی یک model binder سفارشی پردازش رشتهها
تا اینجا تامین کنندهای را که مشخص میکند چه model binder ایی قرار است بازگشت داده شود، تهیه کردیم. مرحلهی بعد، پیاده سازی CustomStringModelBinder با پیاده سازی اینترفیس IModelBinder است:
public class CustomStringModelBinder : IModelBinder { private readonly IModelBinder _fallbackBinder; public CustomStringModelBinder(IModelBinder fallbackBinder) { if (fallbackBinder == null) { throw new ArgumentNullException(nameof(fallbackBinder)); } _fallbackBinder = fallbackBinder; } public Task BindModelAsync(ModelBindingContext bindingContext) { if (bindingContext == null) { throw new ArgumentNullException(nameof(bindingContext)); } var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); if (valueProviderResult != ValueProviderResult.None) { bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult); var valueAsString = valueProviderResult.FirstValue; if (string.IsNullOrWhiteSpace(valueAsString)) { return _fallbackBinder.BindModelAsync(bindingContext); } var model = valueAsString.Replace((char)1610, (char)1740).Replace((char)1603, (char)1705); bindingContext.Result = ModelBindingResult.Success(model); return Task.CompletedTask; } return _fallbackBinder.BindModelAsync(bindingContext); } }
در اینجا تمام مواردی را که نمیخواهیم پردازش کنیم، به همان SimpleTypeModelBinder که از طریق سازندهی کلاس دریافت میکنیم، واگذار خواهیم کرد.
معرفی به binder provider سفارشی به سیستم
مرحلهی آخر این عملیات، معرفی binder تهیه شده به سیستم است که روش آن را در ذیل مشاهده میکنید:
public static class CustomStringModelBinderExtensions { public static MvcOptions UseCustomStringModelBinder(this MvcOptions options) { if (options == null) { throw new ArgumentNullException(nameof(options)); } var simpleTypeModelBinder = options.ModelBinderProviders.FirstOrDefault(x => x.GetType() == typeof(SimpleTypeModelBinderProvider)); if (simpleTypeModelBinder == null) { return options; } var simpleTypeModelBinderIndex = options.ModelBinderProviders.IndexOf(simpleTypeModelBinder); options.ModelBinderProviders.Insert(simpleTypeModelBinderIndex, new CustomStringModelBinderProvider()); return options; } }
در آخر تنها کافی است در کلاس آغازین برنامه، متد الحاقی UseCustomStringModelBinder فوق را به تنظیمات Mvc اضافه کنیم:
public void ConfigureServices(IServiceCollection services) { services.AddMvc(options => { options.UseCustomStringModelBinder(); });
<edmx:Edmx xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx" Version="4.0"> <edmx:DataServices> <Schema xmlns="http://docs.oasis-open.org/odata/ns/edm" Namespace="OwinAspNetCore.Models"> <EntityType Name="Product"> <Key> <PropertyRef Name="Id"/> </Key> <Property Name="Id" Type="Edm.Int32" Nullable="false"/> <Property Name="Name" Type="Edm.String"/> <Property Name="Price" Type="Edm.Decimal" Nullable="false"/> </EntityType> </Schema> <Schema xmlns="http://docs.oasis-open.org/odata/ns/edm" Namespace="Default"> <Function Name="TestFunction" IsBound="true"> <Parameter Name="bindingParameter" Type="Collection(OwinAspNetCore.Models.Product)"/> <Parameter Name="Val" Type="Edm.Int32" Nullable="false"/> <Parameter Name="Name" Type="Edm.String"/> <ReturnType Type="Edm.Int32" Nullable="false"/> </Function> <EntityContainer Name="Container"> <EntitySet Name="Products" EntityType="OwinAspNetCore.Models.Product"/> </EntityContainer> </Schema> </edmx:DataServices> </edmx:Edmx>
string postUrl = "http://localhost:port/...."; HttpClient client = new HttpClient(); var response = client.PostAsync(postUrl, new StringContent(JsonConvert.SerializeObject(new { Rating = 5 }), Encoding.UTF8, "application/json")).Result;
آن را نصب نموده و بعد از تکمیل شدن، visual studio را restart کنید.
پروژهی console خود را باز کرده و از طریق Add -> new item، آیتم OData client را جستجو کرده و با نام ProductClient.tt آن را تولید نمایید (نام آن اختیاری است):
فایل ProductClient.tt را که یک T4 code generator میباشد، باز کرده و مقدار ثابت MetadataDocumentUri را به آدرس سرویس odata خود تغییر دهید:
public const string MetadataDocumentUri = "http://localhost:port/odata/";
روی این آیتم کلیک راست و گزینهی Run Custom tool را انتخاب نمایید. این تمام کاری است که نیاز به انجام دادن دارید.
حال فایل Program.cs را باز کرده و آنرا اینگونه تغییر دهید:
using ConsoleApplication1.OwinAspNetCore.Models; using System; using System.Linq; namespace ConsoleApplication1 { public class Program { static void Main(string[] args) { Uri uri = new Uri("http://localhost:24977/odata"); //var context = new Default.Container(uri); var context = new TestNameSpace.TestNameSpace(uri); //get var products = context.Products.Where(pr => pr.Name.Contains("a")) .Take(1).Select(pr => new { Firstname = pr.Name, PriceValue = pr.Price }).ToList(); //add context.AddToProducts(new Product() { Name = "Name1", Price = 123 }); //update Product p = context.Products.First(); p.Name = "changed"; context.UpdateObject(p); //delete context.DeleteObject(context.Products.Last()); //commit context.SaveChanges(); } } }
مشاهده میفرمایید که همهی عملیاتهای لازم برای CRUD، به شرط اینکه در سمت سرور طراحی شده باشند، به راحتی از سمت کلاینت قابل فراخوانی خواهند بود.
از این ویژگی فوق العاده میتوان حتی در کلاینتها جاوااسکریپتی نیز استفاده کرد. فرض کنید نرم افزار تحت وبی را با استفاده از jquery یا angularjs طراحی کردهاید. قاعدتا فراخوانی درخواستهای شما به سمت سرور، چیزی شبیه به این خواهد بود:
//angularjs $http.get("/products/get", {Name: "Test", Company: "Test"}) .then(function(response) { console.log(response.data); }); //jquery $.get("/products/get", {Name: "Test", Company: "Test"}, function(data, status){ console.log("Data: " + data); });
با استفاده از odata و typescript و یک library مربوط به odata client در سمت کلاینت، نرم افزار شما بجای موارد، بالا چیزی شبیه به مثال زیر خواهد بود (با همراه داشتن strongly typed و intellisense کامل)
let product1 = await context.products.filter(c => c.Name.contains("Ali")).toArray(); let product2 = await context.products.getSomeFunction(1, 'Test'); context.product.add({Name: 'Test'} as Product); await context.saveChanges()
در مقالههای آتی به ویژگیهای بیشتری از Odata خواهیم پرداخت.
دریافت پارامترهای مسیریابی
گاهی از اوقات نیاز به ارسال پارامترهایی به مسیریابیهای تعریف شدهاست. برای مثال در لیست محصولات تعریف شده، بسته به انتخاب هر کدام، باید id متناظری نیز در URL نهایی ظاهر شود. به این Id، یک Route parameter گفته میشود. برای پیاده سازی این نیازمندی به صورت زیر عمل میکنیم:
در فایل app.js، یک مسیریابی جدید را برای نمایش جزئیات یک محصول اضافه میکنیم:
import ProductDetails from "./components/productDetails"; // ... class App extends Component { render() { return ( <div> <NavBar /> <div className="container"> <Switch> <Route path="/products/:id" component={ProductDetails} /> <Route path="/products" render={props => ( <Products param1="123" param2="456" {...props} /> )} /> <Route path="/posts/:year/:month" component={Posts} /> <Route path="/admin" component={Dashboard} /> <Route path="/" component={Home} /> </Switch> </div> </div> ); } }
کامپوننت جدید src\components\productDetails.jsx نیز به صورت زیر تعریف شدهاست:
import React, { Component } from "react"; class ProductDetails extends Component { handleSave = () => { // Navigate to /products }; render() { return ( <div> <h1>Product Details - </h1> <button className="btn btn-primary" onClick={this.handleSave}> </div> ); } } export default ProductDetails;
در اینجا با گشودن اطلاعات خاصیت match تزریق شدهی به کامپوننت ProductDetails، میتوان اطلاعاتی مانند پارامترهای دریافتی مسیریابی را دقیقا مشاهده کرد. برای مثال در این تصویر id=1 از URL بالای صفحه که به http://localhost:3000/products/1 تنظیم شدهاست، دریافت میشود.
بنابراین امکان خواندن اطلاعات پارامترهای مسیریابی، توسط شیء match تزریق شدهی به یک کامپوننت وجود دارد. به همین جهت کامپوننت ProductDetails را ویرایش کرده و المان h1 آنرا جهت نمایش id محصول به صورت زیر تغییر میدهیم که در آن شیء match.params، از props کامپوننت تامین میشود:
<h1>Product Details - {this.props.match.params.id} </h1>
برای آزمایش آن مجددا از صفحهی products شروع کرده و بر روی لینک یکی از محصولات، کلیک کنید. در اینجا هرچند id محصول به درستی نمایش داده میشود، اما ... نمایش جزئیات آن به همراه بارگذاری کامل و مجدد صفحهی آن است که از حالت SPA خارج شدهاست. برای رفع این مشکل به کامپوننت products مراجعه کرده و anchorهای تعریف شده را همانطور که در قسمت قبل نیز بررسی کردیم، تبدیل به کامپوننت Link میکنیم.
از حالت قبلی:
{this.state.products.map(product => ( <li key={product.id}> <a href={`/products/${product.id}`}>{product.name}</a> </li> ))}
import { Link } from "react-router-dom"; // ... <Link to={`/products/${product.id}`}>{product.name}</Link>
پارامترهای اختیاری مسیریابی
به تعریف مسیریابی زیر، دو پارامتر سال و ماه، اضافه شدهاند:
<Route path="/posts/:year/:month" component={Posts} />
برای رفع این مشکل میتوان با اضافه کردن یک ? به هر پارامتر، پارامترهای تعریف شده را اختیاری کرد:
<Route path="/posts/:year?/:month?" component={Posts} />
با این تغییرات اگر مجددا آدرس http://localhost:3000/posts/2018 را درخواست کنیم، کامپوننت Posts بجای کامپوننت Home نمایش داده میشود.
اکنون کامپوننت Posts را به صورت زیر تغییر میدهیم تا پارامترهای مسیریابی را نیز درج کند:
import React from "react"; const Posts = ({ match }) => { return ( <div> <h1>Posts</h1> Year: {match.params.year} , Month: {match.params.month} </div> ); }; export default Posts;
پس از ذخیره سازی این تغییرات و بارگذاری مجدد برنامه در مرورگر، اگر آدرس http://localhost:3000/posts/2018/1 را وارد کنیم، خروجی زیر حاصل میشود:
کار با پارامترهای کوئری استرینگهای مسیریابی
پارامترهای اختیاری، جزو قابلیتهایی هستند که باید تا حد ممکن از بکارگیری آنها اجتناب و آنها را با کوئری استرینگها تعریف کرد. کوئری استرینگها با یک ? در انتهای URL شروع میشوند و میتوانند چندین پارامتر را داشته باشند؛ مانند: http://localhost:3000/posts?sortBy=newest&approved=true و یا حتی میتوان آنها را با پارامترهای اختیاری نیز ترکیب کرد مانند: http://localhost:3000/posts/2018/05?sortBy=newest&approved=true
برای استخراج کوئری استرینگها در برنامههای React باید از شیء location استفاده کرد:
در اینجا مقدار خاصیت search، کل قسمت کوئری استرینگها را به همراه دارد. البته ما قصد پردازش آنرا به صورت دستی نداریم. به همین جهت از کتابخانهی زیر برای انجام اینکار استفاده خواهیم کرد:
> npm i query-string --save
import queryString from "query-string"; const Posts = ({ match, location }) => { const result = queryString.parse(location.search); console.log(result); // ...
- سپس شیء queryString را از ماژول مرتبط، import میکنیم. در ادامه به کمک متد parse آن، میتوان location.search را آنالیز کرد که خروجی آن، یک شیء جاوا اسکریپتی به صورت زیر است:
{approved: "true", sortBy: "newest"}
const { approved, sortBy } = queryString.parse(location.search);
یک نکته: باید دقت داشت که کتابخانهی query-string، همیشه مقادیر خواص را رشتهای بازگشت میدهد؛ حتی اگر عدد باشند.
مدیریت مسیرهای نامعتبر درخواستی
فرض کنید کاربری آدرس غیرمعتبر http://localhost:3000/xyz را که هیچ نوع مسیریابی را برای آن تعریف نکردهایم، درخواست میکند. در این حالت برنامه کامپوننت home را رندر میکند که مرتبط است با تعاریف مسیریابی برنامه در فایل app.js. تنها path تعریف شدهای که با این آدرس تطابق پیدا میکند، / متناظر با کامپوننت home است.
بجای این رفتار پیشفرض، مایل هستیم که کاربر به یک صفحهی سفارشی «پیدا نشد» هدایت شود. به همین جهت ابتدا کامپوننت جدید تابعی بدون حالت src\components\notFound.jsx را با محتوای زیر ایجاد میکنیم:
import React from "react"; const NotFound = () => { return <h1>Not Found</h1>; }; export default NotFound;
<Route path="/" exact component={Home} />
- ابتدا شیء Redirect را از react-router-dom باید import کنیم.
- همچنین import کامپوننت NotFound نیز باید ذکر شود.
- سپس پیش از مسیریابی کلی /، مسیریابی جدید not-found را که به کامپوننت NotFound اشاره میکند، اضافه میکنیم.
- اکنون در انتهای Switch تعریف شده (جائی که دیگر هیچ مسیریابی تعریف شدهای، با مسیر درخواستی کاربر، تطابق نداشته)، باید کامپوننت Redirect را جهت هدایت به این مسیریابی جدید، تعریف کرد:
import { Redirect, Route, Switch } from "react-router-dom"; //... import NotFound from "./components/notFound"; //... class App extends Component { render() { return ( <div> <NavBar /> <div className="container"> <Switch> //... <Route path="/not-found" component={NotFound} /> <Route path="/" exact component={Home} /> <Redirect to="/not-found" /> </Switch> </div> </div> ); } }
کاربرد دیگر کامپوننت Redirect، هدایت کاربران از یک آدرس قدیمی، به یک آدرس جدید است که نحوهی تعریف آن به صورت زیر میباشد:
<Redirect from="/messages" to="/posts" />
هدایت کاربران به آدرسهای مختلف با برنامه نویسی
گاهی از اوقات پس از تکمیل فرمی و یا کلیک بر روی دکمهای، میخواهیم کاربر را به آدرس خاصی هدایت کنیم. برای مثال در برنامهی جاری میخواهیم زمانیکه کاربری صفحهی جزئیات یک محصول را مشاهده و بر روی دکمهی فرضی Save کلیک کرد، دوباره به همان صفحهی لیست محصولات هدایت شود؛ برای این منظور از شیء history استفاده خواهیم کرد:
در اینجا متدها و خواص شیء history را مشاهده میکنید. برای نمونه توسط متد push آن میتوان آدرس جدیدی را به تاریخچهی آدرسهای مرور شدهی توسط کاربر، اضافه کرد و کاربر را با برنامه نویسی، به صفحهی جدیدی هدایت نمود:
class ProductDetails extends Component { handleSave = () => { // Navigate to /products this.props.history.push("/products"); };
یک نکته: اگر به تصویر دقت کنید، متد replace هم در اینجا قابل استفاده است. متد push با افزودن رکوردی به تاریخچهی آدرسهای مرور شدهی در مرورگر، امکان بازگشت به محل قبلی را با کلیک بر روی دکمهی back مرورگر، فراهم میکند؛ اما replace تنها رکورد آدرس جاری را در تاریخچهی مرورگر به روز رسانی میکند. یعنی از داشتن تاریخچه محروم خواهیم شد. عمدهی کاربرد این متد، در صفحات لاگین است؛ زمانیکه کاربر به سیستم وارد میشود و به صفحهی جدیدی مراجعه میکند، با کلیک بر روی دکمهی back، دوباره نمیخواهیم او را به صفحهی لاگین هدایت کنیم.
تعریف مسیریابیهای تو در تو
قصد داریم به صفحهی admin، دو لینک جدید به مطالب و کاربران ادمین را اضافه کنیم، به نحوی که با کلیک بر روی هر کدام، محتوای هر صفحهی متناظر، در همینجا نمایش داده شود (تصویر فوق). به عبارتی در حال حاضر، مسیریابی تعریف شده، جهت مدیریت لینکهای NavBar بالای صفحه کار میکند. اکنون میخواهیم مسیریابی دیگری را داخل آن برای پوشش منوی کنار صفحهی ادمین اضافه کنیم. به اینکار، تعریف مسیریابیهای تو در تو گفته میشود و پیاده سازی آن توسط کامپوننت react-router-dom بسیار سادهاست و شامل این موارد میشود:
- ابتدا مسیریابیهای جدید را همینجا داخل کامپوننت src\components\admin\dashboard.jsx تعریف میکنیم:
const Dashboard = ({ match }) => { return ( <div> <h1>Admin Dashboard</h1> <div className="row"> <div className="col-3"> <SideBar /> </div> <div className="col"> <Route path="/admin/users" component={Users} /> <Route path="/admin/posts" component={Posts} /> </div> </div> </div> ); };
تنها نکتهی جدید آن، امکان درج کامپوننت Route در قسمتهای مختلف برنامه، خارج از app.js میباشد و این امکان محدود به app.js نیست. در این حالت اگر مسیر /admin/posts توسط کاربر وارد شد، دقیقا در همان محلی که کامپوننت Route درج شدهاست، کامپوننت متناظر با این مسیر یعنی کامپوننت Posts، رندر میشود.
در ادامه محتوای این کامپوننتهای جدید را نیز ملاحظه میکنید:
محتوای کامپوننت src\components\admin\sidebar.jsx
import React from "react"; import { Link } from "react-router-dom"; const SideBar = () => { return ( <ul className="list-group"> <li className="list-group-item"> <Link to="/admin/posts">Posts</Link> </li> <li className="list-group-item"> <Link to="/admin/users">Users</Link> </li> </ul> ); }; export default SideBar;
محتوای کامپوننت src\components\admin\users.jsx
import React from "react"; const Users = () => { return <h1>Admin Users</h1>; }; export default Users;
محتوای کامپوننت src\components\admin\posts.jsx
import React from "react"; const Posts = () => { return ( <div> <h1>Admin Posts</h1> </div> ); }; export default Posts;
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-15-part-02.zip
ابتدا شیء user، در بالاترین سطح، دریافت شده و به صفحهای خاص از طریق ویژگیهای props ارسال میشود:
<Page user={user} />
<PageLayout user={user} />
<NavigationBar user={user} />
ایجاد شیء Context در برنامههای React
React Context، راه حلی است جهت به اشتراک گذاری دادهها، در بین انواع و اقسام کامپوننتهای یک برنامه، بدون اینکه نیازی باشد این اطلاعات را توسط props، از یک سطح، به سطحی دیگر، به صورت دستی انتقال داد. برای ایجاد یک نمونهی از آن، ابتدا پوشهی جدید src\contexts را افزوده و سپس فایل src\contexts\userContext.js را درون آن، با محتوای زیر ایجاد میکنیم:
import React from "react"; export const UserContext = React.createContext({ user: {} }); export const UserProvider = UserContext.Provider; export const UserConsumer = UserContext.Consumer;
تامین یک شیء Context در برنامه، در یک کامپوننت کلاسی و یا تابعی
تا اینجا یک شیء Context را به همراه اجزای export شدهی Provider و Consumer آن ایجاد کردیم. اکنون نوبت به پیاده سازی قسمت Provider آن است:
import "../../App.css"; import React, { Component } from "react"; import { UserProvider } from "../../contexts/userContext"; import Main from "./Main"; class App extends Component { state = { user: { name: "User 1" } }; componentDidMount() { // get user from the server or local storage and then set the currently logged in user to the this.state } render() { return ( <> <h1>App Class</h1> <UserProvider value={this.state.user}> <Main /> </UserProvider> </> ); } } export default App;
در ادامه قصد داریم اطلاعات این شیء user موجود در state را با تمام کامپوننتهایی که در درخت رندر کامپوننت جاری قرار میگیرند و با کامپوننت Main شروع میشوند، به اشتراک بگذاریم. این به اشتراک گذاری با import شیء UserProvider از ماژول contexts/userContext به نحوی که مشاهده میکنید، انجام میشود. شیء UserProvider، کار محصور سازی کامپوننت Main را انجام میدهد. سپس این Provider میتواند مقداری را توسط ویژگی value خود دریافت کند که برای مثال در اینجا شیء user است. اکنون این value تا n سطح بعدی که از کامپوننت Main مشتق میشوند نیز در دسترس خواهد بود.
یک نکته: متد React.createContext به همراه یک آرگومان defaultValue اختیاری است که در اختیار Consumerهای آن قرار داده میشود؛ اگر Provider متناظر با آن، در درخت کامپوننتهای برنامه، یافت نشود. یعنی تعریف Provider الزامی نیست. اگر نیاز است مقدار ثابتی را بین چندین کامپوننت به اشتراک بگذارید، فقط کافی است آنها را توسط React.createContext مقدار دهی اولیه کرده و ... استفاده کنید:
export const DefaultRouteContext = React.createContext({ path: '/welcome' });
خواندن شیء Context در کامپوننتی دیگر
اکنون که یک تامین کنندهی Context را ایجاد کردیم، برای خواندن اطلاعات آن در درخت کامپوننتهای محصور شدهی توسط UserProvider، میتوان به صورت زیر عمل کرد:
import React from "react"; import { UserConsumer } from "../../contexts/userContext"; export default function Main(props) { return ( <> <UserConsumer> {value => <div>User name: {value.name}.</div>} </UserConsumer> </> ); }
خروجی برنامه پس از این تغییرات به صورت زیر است:
ساده سازی دسترسی به UserConsumer توسط useContext Hook
نحوهی تعریف یک Provider و محصور سازی فرزندانی که باید از آن ارثبری کنند، در بین کامپوننتهای کلاسی و تابعی، یکی است. اما در کامپوننتهای تابعی حداقل میتوان نحوهی دسترسی به UserConsumer را به نحو زیر توسط useContext Hook ساده کرد:
import React, { useContext } from "react"; import { UserContext } from "../../contexts/userContext"; export default function Main() { const value = useContext(UserContext); return ( <> <div>User name: {value.name}.</div> </> ); }
مزیت دیگر این روش، ساده سازی کار با چندین شیء Context است. برای مثال اگر دو شیء Context را تعریف کرده باشید، خواندن دو مقدار از آنها، پیشتر چنین شکل تو در تویی را توسط دو Consumer پیدا میکرد:
function HeaderBar() { return ( <CurrentUser.Consumer> {user => <Notifications.Consumer> {notifications => <header> Welcome back, {user.name}! You have {notifications.length} notifications. </header> } } </CurrentUser.Consumer> ); }
function HeaderBar() { const user = useContext(CurrentUser); const notifications = useContext(Notifications); return ( <header> Welcome back, {user.name}! You have {notifications.length} notifications. </header> ); }
ارسال اطلاعات به کامپوننت Context Provider، از طریق کامپوننتهای فرزند
تا اینجا با استفاده از React Context، اطلاعات یک Provider را با فرزندان آن به اشتراک گذاشتیم؛ عکس این عمل نیز میسر است. برای اینکار، همانند تمام کامپوننتهای دیگری که برای ارسال اطلاعات به فراخوان خود از طریق رخدادها عمل میکنند، میتوان یک متد رویدادگردان را در کامپوننت والد، به استفاده کنندهی از Context ارسال کرد:
import "../../App.css"; import React, { Component } from "react"; import { UserProvider } from "../../contexts/userContext"; import Main from "./Main2"; class App extends Component { state = { user: { name: "User 1" } }; componentDidMount() { // get user from the server or local storage and then set the currently logged in user to the this.state } logout = () => { console.log("logout"); this.setState({ user: {} }); }; render() { const contextValue = { user: this.state.user, logoutUser: this.logout }; return ( <> <h1>App Class</h1> <UserProvider value={contextValue}> <Main /> </UserProvider> </> ); } } export default App;
اکنون تمام استفاده کنندههای از این UserProvider میتوانند با فراخوانی متد منتسب به logout، سبب پاک شدن اطلاعات کاربر موجود در state کامپوننت App، به روز رسانی state و در نتیجهی آن، رندر مجدد کامپوننت و ارائهی یک UserProvider جدید، با اطلاعاتی جدید به فرزندان آن شوند:
import React, { useContext } from "react"; import { UserContext } from "../../contexts/userContext"; export default function Main() { const { user, logoutUser } = useContext(UserContext); return ( <> <div>User name: {user.name}.</div> <button type="button" className="btn btn-primary" onClick={logoutUser}> Logout user </button> </> ); }
روش انجام اینکار بدون استفاده از useContext را نیز در ادامه مشاهده میکنید که در ابتدا نیاز به تعریف تابعی را دارد که همان خواص استخراجی را دریافت میکند. سپس باید بر اساس آنها، المانهای مدنظر نمایش نام کاربر و دکمهی خروج او را بازگشت داد:
import React from "react"; import { UserConsumer } from "../../contexts/userContext"; export default function Main(props) { return ( <> <UserConsumer> {({ user, logoutUser }) => ( <> <div>User name: {user.name}.</div> <button type="button" className="btn btn-primary" onClick={logoutUser} > Logout user </button> </> )} </UserConsumer> </> ); }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-30-part-04.zip