برپایی پیشنیازها
در اینجا برای بررسی مسیریابی، یک پروژهی جدید React را ایجاد میکنیم.
> create-react-app sample-15 > cd sample-15 > npm start
> npm install --save bootstrap
import "bootstrap/dist/css/bootstrap.css";
همچنین کتابخانهی ثالث بسیار معروف react-router-dom را نیز نصب میکنیم:
> npm i react-router-dom --save
افزودن مسیریابی به برنامه
پس از نصب کتابخانهی react-router-dom، برای افزودن آن به برنامه و فعالسازی مسیریابی، به فایل index.js مراجعه کرده و import آنرا به ابتدای فایل اضافه میکنیم:
import { BrowserRouter } from "react-router-dom";
ReactDOM.render( <BrowserRouter> <App /> </BrowserRouter>, document.getElementById("root") );
ثبت و معرفی مسیریابیها
در ادامه باید مسیریابیهای خود را ثبت کنیم؛ به این معنا که بر اساس URL خاصی، چه کامپوننتی باید رندر شود. به همین جهت پوشهی جدید src\components را ایجاد کرده و کامپوننت src\components\navbar.jsx را که یک کامپوننت تابعی بدون حالت است، در آن تعریف میکنیم:
import React from "react"; const NavBar = () => { return ( <nav className="navbar bg-dark navbar-dark navbar-expand-sm"> <div className="navbar-nav"> <a className="nav-item nav-link" href="/"> Home </a> <a className="nav-item nav-link" href="/products"> Products </a> <a className="nav-item nav-link" href="/posts/2018/06"> Posts </a> <a className="nav-item nav-link" href="/admin"> Admin </a> </div> </nav> ); }; export default NavBar;
سپس به فایل app.js مراجعه کرده و ساختار آنرا به صورت زیر، جهت درج این NavBar، ویرایش میکنیم تا سبب رندر و نمایش منوی راهبری در مرورگر شود:
import "./App.css"; import React, { Component } from "react"; import NavBar from "./components/navbar"; class App extends Component { render() { return ( <div> <NavBar /> </div> ); } } export default App;
import "./App.css"; import React, { Component } from "react"; import { Route } from "react-router-dom"; import Dashboard from "./components/admin/dashboard"; import Home from "./components/home"; import NavBar from "./components/navbar"; import Posts from "./components/posts"; import Products from "./components/products"; class App extends Component { render() { return ( <div> <NavBar /> <div className="container"> <Route path="/products" component={Products} /> <Route path="/posts" component={Posts} /> <Route path="/admin" component={Dashboard} /> <Route path="/" component={Home} /> </div> </div> ); } } export default App;
کامپوننت جدید src\components\products.jsx جهت رندر لیست آرایهی اشیاء product:
import React, { Component } from "react"; class Products extends Component { state = { products: [ { id: 1, name: "Product 1" }, { id: 2, name: "Product 2" }, { id: 3, name: "Product 3" } ] }; render() { return ( <div> <h1>Products</h1> <ul> {this.state.products.map(product => ( <li key={product.id}> <a href={`/products/${product.id}`}>{product.name}</a> </li> ))} </ul> </div> ); } } export default Products;
کامپوننت بدون حالت تابعی src\components\home.jsx با این محتوا:
import React from "react"; const Home = () => { return <h1>Home</h1>; }; export default Home;
کامپوننت بدون حالت تابعی src\components\posts.jsx با این محتوا:
import React from "react"; const Posts = () => { return ( <div> <h1>Posts</h1> Year: , Month: </div> ); }; export default Posts;
کامپوننت بدون حالت تابعی src\components\admin\dashboard.jsx در پوشهی جدید admin با این محتوا:
import React from "react"; const Dashboard = ({ match }) => { return ( <div> <h1>Admin Dashboard</h1> </div> ); }; export default Dashboard;
معرفی کامپوننت Switch
<div className="container"> <Route path="/products" component={Products} /> <Route path="/posts" component={Posts} /> <Route path="/admin" component={Dashboard} /> <Route path="/" component={Home} /> </div>
یک روش حل این مشکل، استفاده از ویژگی exact است:
<Route path="/" exact component={Home} />
راه دوم رفع این مشکل، استفاده از کامپوننت Switch است. به همین جهت ابتدا این کامپوننت را import میکنیم:
import { Route, Switch } from "react-router-dom";
class App extends Component { render() { return ( <div> <NavBar /> <div className="container"> <Switch> <Route path="/products" component={Products} /> <Route path="/posts" component={Posts} /> <Route path="/admin" component={Dashboard} /> <Route path="/" component={Home} /> </Switch> </div> </div> ); } }
بنابراین هنگام کار با Switch، ترتیب مسیریابیهای تعریف شده مهم است و باید از یک مسیریابی ویژه شروع شده و به یک مسیریابی عمومی مانند / ختم شود.
معرفی کامپوننت Link
تا اینجا اگر برنامه را اجرا کرده باشید و پیشتر سابقهی کار با برنامههای SPA یا Single page applications را داشته باشید، یک مشکل دیگر را نیز احساس کردهاید: سیستم مسیریابی که تا کنون تعریف کردهایم، به صورت SPA عمل نمیکند. یعنی به ازای هربار کلیک بر روی لینکهای منوی راهبری سایت، یکبار دیگر به طور کامل برنامه از صفر بارگذاری مجدد میشود و تمام اسکریپتهای آن مجددا از سرور دریافت شده و رندر خواهند شد. این مورد را در برگهی network ابزارهای توسعه دهندگان مرورگر خود بهتر میتوانید مشاهده کنید. به ازای هر درخواست نمایش کامپوننتی، تعدادی درخواست HTTP به سمت سرور ارسال میشوند که برای دریافت صفحهی index و bundle.js برنامه هستند. اما در برنامههای SPA، مانند جمیل، با هربار کلیک بر روی لینکی، شاهد ریفرش و بارگذاری مجدد کل آن صفحه نیستیم و تنها اطلاعات موجود در قسمت container به روز میشوند.
یک نکته: در اینجا ممکن است دو درخواست websocket و info را نیز مشاهده کنید. این دو مرتبط به hot module reloading هستند که با ذخیرهی برنامه در ادیتور VSCode، بلافاصله سبب به روز رسانی و ریفرش برنامه در مرورگر میشوند.
برای رفع مشکل SPA نبودن برنامه، باید به کامپوننت NavBar مراجعه کرده و تمام anchorهای استاندارد تعریف شدهی در آنرا با کامپوننت Link جایگزین کنیم:
import React from "react"; import { Link } from "react-router-dom"; const NavBar = () => { return ( <nav className="navbar bg-dark navbar-dark navbar-expand-sm"> <div className="navbar-nav"> <Link className="nav-item nav-link" to="/"> Home </Link> <Link className="nav-item nav-link" to="/products"> Products </Link> <Link className="nav-item nav-link" to="/posts/2018/06"> Posts </Link> <Link className="nav-item nav-link" to="/admin"> Admin </Link> </div> </nav> ); }; export default NavBar;
با این تغییرات اگر برنامه را اجرا کنیم، اینبار با کلیک بر روی هر لینک، دیگر شاهد بارگذاری کامل صفحه در مرورگر نخواهیم بود؛ بلکه تنها قسمت container ای که کامپوننت Route مسیریابی در آن درج شدهاست، به روز رسانی میشود و این عملیات نیز بسیار سریع است؛ از این جهت که محتوای این کامپوننتها از همان bundle.js حاوی تمام کدهای برنامه تامین میشود و این فایل تنها یکبار در آغاز برنامه از سرور خوانده شده و سپس توسط مرورگر پردازش میشود. بنابراین در برنامههای SPA، برخلاف برنامههای وب معمولی، هربار که کاربر آدرس متفاوتی را انتخاب میکند، بارگذاری مجدد برنامه و خوانده شدن محتوای متناظر از سرور صورت نمیگیرد؛ این محتوا هم اکنون در bundle.js برنامه مهیا است و قابلیت استفادهی آنی را دارد.
اما کامپوننت Link چگونه کار میکند؟
کامپوننت لینک در نهایت همان anchorهای استاندارد را رندر میکند؛ اما به هر کدام یک onClick را نیز اضافه میکند که سبب جلوگیری از رفتار پیشفرض anchor میشود. به همین جهت مرورگر درخواست اضافهای را به سمت سرور ارسال نمیکند. در اینجا مدیریت کنندهی onClick، تنها Url بالای صفحه را در مرورگر تغییر میدهد. اکنون که Url تغییر کردهاست، یکی از مسیریابیهای تعریف شده، با این Url تطابق یافته و سپس کامپوننت متناظر با آنرا رندر میکند.
بررسی Route props
اگر بر روی لینک نمایش products در منوی راهبری سایت کلیک کرده و سپس به خروجی افزونهی react developer tools دقت کنیم (تصویر فوق)، مشاهده میکنیم که این کامپوننت هم اکنون تعدادی خاصیت را به صورت props در اختیار دارد؛ مانند history (امکان هدایت کاربر را به صفحهای دیگر دارد)، location (آدرس جاری برنامه) و match (اطلاعاتی در مورد الگوریتم تطابق مسیر). کار تنظیم این props، توسط کامپوننت Route ای که کار ثبت مسیریابیها را انجام میدهد، صورت میگیرد. به عبارتی کامپوننت Route، محصور کنندهی کامپوننتی است که آنرا به عنوان پارامتر، دریافت و در صورت تطابق با مسیر جاری، آنرا رندر میکند. همچنین در این بین کار تزریق خواص props یاد شده را نیز انجام میدهد.
ارسال props سفارشی در حین مسیریابی به کامپوننتها
همانطور که بررسی کردیم، کامپوننت Route، حداقل سه خاصیت props را به کامپوننتهایی که رندر میکند، تزریق خواهد کرد. اما در اینجا برای تزریق خواص سفارشی چگونه باید عمل کرد؟
در حین کار با کامپوننت Route، برای ارسال props اضافی، بجای استفاده از ویژگی component آن، باید از ویژگی render استفاده کرد:
<Route path="/products" render={() => <Products param1="123" param2="456" />} />
البته اگر به تصویر فوق دقت کنید، سایر خواص پیشینی که تزریق شده بودند مانند history، location و match، دیگر در اینجا حضور ندارند. برای رفع این مشکل باید تعریف arrow function انجام شده را به صورت زیر تغییر داد:
<Route path="/products" render={props => ( <Products param1="123" param2="456" {...props} /> )} />
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-15-part-01.zip
حذف یک ردیف از اطلاعات به همراه پویانمایی محو شدن اطلاعات آن توسط jQuery در ASP.NET MVC
به نظرم جالب اومد.
ابتدا یک اکشن فیلتر تعریف شده و در ان هدر درخواست خوانده شده و با محتوی کوکی چک میشود در صورتی که برابر باشند عمل انجام خواهد شد با این تفاوت باید توکن در زمان ارسال در هدر درخواست قرار گیرد مانند زیر:
کد اکشن فیلتر:
public class ValidateJsonAntiForgeryTokenAttribute : ActionFilterAttribute { #region Methods (1) // Public Methods (1) /// <summary> /// Called when [action executing]. /// </summary> /// <param name="actionContext">The action context.</param> public void OnActionExecuting(HttpActionContext actionContext) { try { var cookieName = AntiForgeryConfig.CookieName; var headers = actionContext.Request.Headers; var cookie = headers .GetCookies() .Select(c => c[AntiForgeryConfig.CookieName]) .FirstOrDefault(); var rvt = headers.GetValues("__RequestVerificationToken").FirstOrDefault(); AntiForgery.Validate(cookie != null ? cookie.Value : null, rvt); } catch { actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Forbidden, "Unauthorized request."); } } #endregion Methods }
[HttpPost] //[ValidateAntiForgeryToken] [ValidateJsonAntiForgeryToken] [OutputCache(Location = OutputCacheLocation.None, NoStore = true)] public virtual ActionResult DeletePhoto(int? id) { string ret="error"; if (id != null && id > 0) { bool result = _imageGalleryService.Value.Photos.Value.Delete(id.ToInt32()); ret = result ? "ok" : "nok"; } return Content(ret); //return Json(new { ret }); }
$.ajax({ .... type: "POST", url: '@Url', data: JSON.stringify({ id: Id }), contentType: "application/json; charset=utf-8", // این قسمت اضافه شود headers: { __RequestVerificationToken: $("input[name=__RequestVerificationToken]").val() }, dataType: "json", ..........
برپایی پیشنیازها
در اینجا برای بررسی React Hooks، یک پروژهی جدید React را ایجاد میکنیم:
> npm i -g create-react-app > create-react-app sample-30 > cd sample-30 > npm start
> npm install --save bootstrap
import "bootstrap/dist/css/bootstrap.css";
همچنین اگر به فایل package.json موجود در ریشهی پروژه دقت کنیم، برای کار با React-hooks، نیاز است نگارش بستههای React و React-dom، حداقل مساوی 16.7 باشند که در زمان نگارش این مطلب، نگارش 16.12.0 آن به صورت خودکار نصب میشود. بنابراین بدون مشکلی میتوانیم شروع به کار با React hooks کنیم.
معرفی useState Hook
در اینجا قصد داریم یک شمارشگر را به همراه یک دکمه، در صفحه نمایش دهیم؛ بطوریکه این شمارشگر، تعداد بار کلیک بر روی دکمه را ردیابی میکند. از پیش میدانیم که برای ردیابی مقدار تعداد بار کلیک شدن، باید متغیر آنرا درون state یک class component قرار داد:
import "./App.css"; import React, { Component } from "react"; class App extends Component { state = { count: 0 }; incrementCount = () => { this.setState({ count: this.state.count + 1 }); }; render() { return ( <button onClick={this.incrementCount} className="btn btn-primary m-3"> I was clicked {this.state.count} times! </button> ); } } export default App;
اکنون میخواهیم همین کامپوننت را توسط React hooks بازنویسی کنیم. در ابتدا، فایل app.js را به AppClass.js، تغییر نام میدهیم تا نگارش قبلی class component را برای مقایسه، در اختیار داشته باشیم. در ادامه فایل جدید AppFunction.js را برای بازنویسی آن توسط یک کامپوننت تابعی، توسط میانبرهای imrc و سپس sfc در VSCode، ایجاد میکنیم. البته این تغییر نام فایلها، نیاز به تغییر نام ماژولهای import شدهی در فایل index.js را نیز به صورت زیر دارد:
//import App from "./AppClass"; import App from "./AppFunction";
اولین سؤالی که اینجا مطرح میشود، این است: در این کامپوننت تابعی جدید، state را از کجا بدست بیاوریم؟
با React Hooks، بجای تعریف یک state به صورت خاصیت، آنرا صرفا use میکنیم و این دقیقا نام اولین React Hooks ای است که بررسی میکنیم؛ یا همان useState. بنابراین ابتدا این شیء را import خواهیم کرد:
import React, { useState } from 'react';
const App = () => { const [count, setCount] = useState(0);
useState<number>(initialState: number | (() => number)): [number, React.Dispatch<React.SetStateAction<number>>]
import React, { useState } from "react"; const App = () => { const [count, setCount] = useState(0); const incrementCount = () => { setCount(count + 1); }; return ( <button onClick={incrementCount} className="btn btn-primary m-3"> I was clicked {count} times! </button> ); }; export default App;
- همچنین در اینجا (داخل این متد) دیگر خبری از thisها نیست؛ onClick، مستقیما به متغیر incrementCount اشاره میکند و {count} نیز مستقیما از خروجی useState، که به مقدار جاری count اشاره میکند، تامین میشود.
- اکنون با هربار کلیک بر روی این دکمه، متد منتسب به متغیر incrementCount فراخوانی شده و در داخل آن، همان متد setCount را جهت به روز رسانی state، فراخوانی میکنیم (بجای فراخوانی this.setState عمومی قبلی). در اینجا ابتدا مقدار جاری متغیر count در state، دریافت شده و سپس یک واحد به آن اضافه میشود. امضای متد جنریک setCount به صورت زیر است:
const setCount: (value: React.SetStateAction<number>) => void
استفاده از مقدار قبلی state توسط useState
زمانیکه متد this.setState فراخوانی میشود، اینکار سبب در صف قرار گرفتن رندر مجدد کامپوننت جاری خواهد شد. همچنین اعمال این متد نیز ممکن است در صف قرار گیرد. یعنی اگر پس از فراخوانی this.setState، سعی در خواندن state به روز شده را داشته باشیم، ممکن است مقدار اشتباهی را دریافت کنیم:
incrementCount = () => { this.setState({ count: this.state.count + 1 }); };
incrementCount = () => { this.setState(prevState => ({ count: prevState.count + 1 })); };
این نکته در مورد کامپوننتهای تابعی نیز وجود دارد:
const incrementCount = () => { setCount(count + 1); };
const incrementCount = () => { setCount(prevCount => prevCount + 1); };
به روز رسانی بیش از یک خاصیت در state
فرض کنید قصد داریم به مثال جاری، یک مربع را در صفحه اضافه کنیم که با کلیک بر روی آن، رنگش تغییر میکند (خاموش و روشن میشود):
در حالت AppClass یا کامپوننت کلاسی، کدهای برنامه به صورت زیر تغییر میکنند:
import "./App.css"; import React, { Component } from "react"; class App extends Component { state = { count: 0, isOn: false }; incrementCount = () => { this.setState(prevState => ({ count: prevState.count + 1 })); }; toggleLight = () => { this.setState(prevState => ({ isOn: !prevState.isOn })); }; render() { return ( <> <h1>App Class</h1> <h2>Counter</h2> <button onClick={this.incrementCount} className="btn btn-primary m-3"> I was clicked {this.state.count} times! </button> <h2>Toggle Light</h2> <div style={{ height: "50px", width: "50px", cursor: "pointer" }} className={ this.state.isOn ? "alert alert-info m-3" : "alert alert-warning m-3" } onClick={this.toggleLight} /> </> ); } } export default App;
- در متد رندر، نیاز است تا تنها یک child، بازگشت داده شود. به همین جهت میتوان از React.Fragment، برای محصور سازی المانهای تعریف شده، استفاده کرد. البته در React 16.7.0، دیگر نیازی به ذکر صریح React.Fragment نبوده و فقط میتوان نوشت </><> تا بیانگر یک فرگمنت باشد.
- سپس یک div تعریف شده که با استفاده از ویژگی style، یک سری شیوهنامهی ابتدایی، مانند طول و عرض و نوع اشارهگر ماوس آن، تعیین شدهاند.
- اکنون برای اینکه بتوان با کلیک بر روی این div، رنگ آنرا تغییر داد، نیاز است بتوان توسط متغیری، مقدار خاموش و روشن بودن را ردیابی کرد. به همین جهت خاصیت جدید isOn را به state اضافه میکنیم.
- در آخر، رویداد onClick این div را به متد رویدادگران toggleLight متصل میکنیم تا توسط آن و فراخوانی this.setState، بتوان مقدار قبلی isOn را در state، دریافت و سپس آنرا معکوس کرد و بجای مقدار جاری isOn در state درج کنیم. این فراخوانی، سبب رندر مجدد کامپوننت جاری شده و در نتیجهی آن، مقدار className را بر اساس مقدار this.state.isOn، به صورت پویا تغییر میدهد.
برای مشاهدهی خروجی برنامه در این حالت، نیاز است به index.js مراجعه و تغییر زیر را اعمال کرد تا کامپوننت App، از ماژول AppClass تامین شود:
import App from "./AppClass"; //import App from "./AppFunction";
اکنون قصد داریم دقیقا معادل همین قطعه کد را در کامپوننت تابعی خود پیاده سازی کنیم. به همین جهت به فایل src\AppFunction.js بازگشته و تغییرات زیر را اعمال میکنیم:
import React, { useState } from "react"; const App = () => { const [count, setCount] = useState(0); const [isOn, setIsOn] = useState(false); const incrementCount = () => { setCount(prevCount => prevCount + 1); }; const toggleLight = () => { setIsOn(prevIsOn => !prevIsOn); }; return ( <> <h1>App Function</h1> <h2>Counter</h2> <button onClick={incrementCount} className="btn btn-primary m-3"> I was clicked {count} times! </button> <h2>Toggle Light</h2> <div style={{ height: "50px", width: "50px", cursor: "pointer" }} className={isOn ? "alert alert-info m-3" : "alert alert-warning m-3"} onClick={toggleLight} /> </> ); }; export default App;
- اگر دقت کنید، کلیات این کامپوننت تابعی، با کامپوننت کلاسی، آنچنان متفاوت نیست. متد رندر آن دقیقا همان markup را بازگشت میدهد؛ با یک تفاوت مهم: در اینجا دیگر نیازی به ذکر thisها نیست، چون تمام ارجاعات، به متغیرهای داخل تابع App انجام شدهاست و نه به متدها و یا خاصیتهای یک کلاس.
- مرحلهی بعد تغییر، جایگزینی this.state.isOn قبلی، با یک متغیر درون تابع App است. به همین جهت در اینجا یک useState دیگر را تعریف میکنیم. هر useState، تنها به قسمتی از state اشاره میکند و مانند خاصیت کلی state مربوط به یک کلاس نیست. همچنین در خاصیت state یک کلاس، مقدار آن همواره به یک شیء اشاره میکند؛ اما با useState چنین اجباری وجود ندارد و میتواند هر نوع مجاز جاوا اسکریپتی باشد. برای مثال در اینجا مقدار اولیهی آن به false تنظیم شدهاست. پس از آن، خروجی این متد، قسمتی از state را که کنترل میکند، به نام متغیر isOn و تنظیم کنندهی آنرا به نام متد setIsOn، معرفی خواهد کرد. متد useState، یک متد جنریک است. یعنی نوع خروجیهای آن بر اساس نوع مقدار اولیهای که به آن انتساب داده میشود، تعیین میشود. برای مثال اگر نوع مقدار اولیهی آن، Boolean باشد، به صورت خودکار نوع متغیر و پارامتر متد خروجی از آن نیز Boolean خواهند بود.
- در آخر خاصیت toggleLight کلاس کامپوننت، تبدیل به یک متغیر یا ثابت، در تابع جاری App میشود و بجای this.setState کلی قبلی، از متد اختصاصیتر setIsOn، برای تغییر مقدار state متناظر، کمک گرفته خواهد شد. در اینجا با استفاده از prevIsOn، به مقدار دقیق پیشین متغیر isOn در state، دسترسی یافته و سپس آنرا معکوس میکنیم.
برای مشاهدهی خروجی برنامه در این حالت، نیاز است به index.js مراجعه و تغییر زیر را اعمال کرد تا کامپوننت App، از ماژول AppFunction تامین شود:
// import App from "./AppClass"; import App from "./AppFunction";
معرفی useEffect Hook
فرض کنید قصد داریم برچسب دکمهی «I was clicked {this.state.count} times» را در المان Title صفحه، نمایش دهیم. برای انجام چنین کاری نیاز است با DOM API تعامل داشته باشیم؛ اما پیشتر یک چنین کاری را تنها با class components میشد انجام داد. برای رفع این محدودیت در کامپوننتهای تابعی، hook جدیدی به نام useEffect، ارائه شدهاست که باید import شود:
import React, { useEffect, useState } from "react";
در اینجا effect به معنای side effect و یا اثرات جانبی است؛ مانند: تعامل با یک API خارجی، کار با API مرورگر و کلا هر جائیکه در برنامه با دنیای خارج تعاملی وجود دارد، به عنوان یک side effect شناخته میشود. بنابراین با استفاده متد useEffect، میتوان در کامپوننتهای تابعی نیز با دنیای خارج، تعامل برقرار کرد.
import React, { useEffect, useState } from "react"; const App = () => { const [count, setCount] = useState(0); useEffect(() => { document.title = `You have clicked ${count} times`; });
در اولین بار اجرای برنامه، عبارت «You have clicked 0 times»، در عنوان صفحهی جاری، ظاهر میشود که از مقدار پیشفرض count، استفاده کردهاست. اکنون اگر بر روی دکمه، کلیک کنیم، پس از تغییر برچسب دکمه (یا همان رندر کامپوننت، پس از تغییری در state)، آنگاه عنوان نمایش داده شدهی در مرورگر نیز تغییر میکند.
یک نکته: چون useEffect دارای همان scope متغیر count است، نیاز به API خاصی برای خواندن مقدار آن در اینجا نیست.
سؤال: برای پیاده سازی چنین قابلیتی در یک کامپوننت کلاسی چه باید کرد؟ در این مثال، در ابتدای کار باید مقدار پیشفرض موجود در state را در عنوان صفحه مشاهده کرد و پس از هربار به روز رسانی state نیز باید این عنوان، تغییر کند.
برای پیاده سازی معادل متد useEffect ای که در یک کامپوننت تابعی استفاده شد، در اینجا باید از دو life-cycle hook متفاوت، به نامهای componentDidMount و componentDidUpdate، استفاده کنیم:
class App extends Component { componentDidMount() { document.title = `You have been clicked ${this.state.count} times`; } componentDidUpdate() { document.title = `You have been clicked ${this.state.count} times`; }
- همچنین چون میخواهیم به ازای هر تغییری در state نیز این عنوان تغییر کند (با هر بار کلیک بر روی دکمه)، باید از متد componentDidUpdate هم استفاده کنیم.
پاکسازی اثرات جانبی در useEffect Hook
فرض کنید قصد داریم موقعیت فعلی کرسر ماوس را در مرورگر نمایش دهیم. برای انجام اینکار در کامپوننت کلاسی، میتوان از متد componentDidMount جهت دسترسی به DOM API و استفاده از متد addEventListener آن، برای گوش فرا دادن به حرکات کرسر ماوس، استفاده کرد:
class App extends Component { componentDidMount() { // ... window.addEventListener("mousemove", this.handleMouseMove); }
handleMouseMove = event => { this.setState({ x: event.pageX, y: event.pageY }); };
class App extends Component { state = { //... x: 0, y: 0 };
<h2>Mouse Position</h2> <p>X position: {this.state.x}</p> <p>Y position: {this.state.y}</p>
- تعدادی از آنها نیازی به پاکسازی و خارج شدن از حافظه را ندارند؛ مانند به روز رسانی عنوان صفحه در مرورگر. میتوان یک چنین side effect هایی را اجرا و سپس آنها را فراموش کرد.
- اما تعدادی از side effectها را حتما باید پاکسازی کرد؛ مانند mousemove listener تعریف شدهی در مثال فوق. در اینجا زمانیکه کامپوننت جاری mount میشود، این listener را تعریف میکنیم؛ اما با Unmount شدن آن، باید این listener را حذف کرد تا برنامه دچار نشتی حافظه نشود (اگر اینکار انجام نشود، در این مثال مرورگر هنگ خواهد کرد!). روش انجام اینکار در متد componentWillUnmount، به صورت زیر است:
componentWillUnmount() { window.removeEventListener("mousemove", this.handleMouseMove); }
در این حالت نیز میتوان از متد useEffect استفاده کرد. البته ابتدا باید state شیء ای را برای نگهداری اطلاعات به روز موقعیت مکانی کرسر ماوس، ایجاد کرد:
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
سپس side effect خود را در قسمت effect function متد useEffect قرار میدهیم که آن نیز به متغیر handleMouseMove اشاره میکند:
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }); useEffect(() => { // ... window.addEventListener("mousemove", handleMouseMove); }); const handleMouseMove = event => { setMousePosition({ x: event.pageX, y: event.pageY }); };
سپس برای نمایش x و y به روز رسانی شدهی در state، میتوان از markup زیر در متد رندر استفاده کرد.
<h2>Mouse Position</h2> {JSON.stringify(mousePosition, null, 2)} <br />
اگر effect function تعریف شده، دارای یک خروجی (از نوع تابع) باشد، به این معنا است که این side effect، نیاز به پاکسازی دارد و این متد را در زمان Unmount آن فراخوانی میکند:
useEffect(() => { // … // componentDidMount & componentDidUpdate window.addEventListener("mousemove", handleMouseMove); // componentWillUnmount return () => { window.removeEventListener("mousemove", handleMouseMove); }; });
سؤال: اگر بخواهیم از اجرای یک side effect، به ازای هر بار رندر جلوگیری کنیم، چه باید کرد؟
برای اینکار میتوان آرگومان دومی را به متد useEffect اضافه کرد که آرایهای از مقادیر است. توسط اعضای آن میتوان مقدار و یا مقادیری را مشخص کرد که side effect تعریف شده، به آن وابستهاست. اکنون اگر این مقدار تغییر کند، آنگاه side effect متناظر با آن نیز اجرا میشود:
useEffect(() => { document.title = `You have clicked ${count} times`; window.addEventListener("mousemove", handleMouseMove); return () => { window.removeEventListener("mousemove", handleMouseMove); }; },[]);
برای رفع این مشکل، باید به useEffect اعلام کنیم که side effect تعریف شدهی در آن، وابستهاست به مقدار count و با تغییرات آن در state، نیاز است مجددا اجرا شود:
useEffect(() => { // ... },[count]);
کار با چندین listener مختلف در متد useEffect
سؤال: آیا تنظیم یک وابستگی خاص در متد useEffect، امکان تنظیم event listenerهای دیگر را غیرممکن میکند؟
برای پاسخ به این سؤال، چند event listener دیگر را ثبت میکنیم. برای مثال یکی دیگر از APIهای مرورگر، navigator نام دارد که توسط آن میتوان وضعیت آنلاین و آفلاین بودن را به کمک خروجی خاصیت navigator.onLine آن، مشخص کرد. به کمک این API میخواهیم این وضعیت را نمایش دهیم. برای این منظور ابتدا state آنرا در کامپوننت تابعی، ایجاد میکنیم:
const [status, setStatus] = useState(navigator.onLine);
اکنون برای گوش فرا دادن به تغییرات این خاصیت (online و یا offline شدن کاربر)، نیاز است دو event listener را به کمک متد addEventListener ثبت کنیم و همچنین این متدها نیاز به پاکسازی هم دارند:
useEffect(() => { // ... window.addEventListener("online", handleOnline); window.addEventListener("offline", handleOffline); return () => { // ... window.removeEventListener("online", handleOnline); window.removeEventListener("offline", handleOffline); }; }, [count]);
const handleOnline = () => { setStatus(true); }; const handleOffline = () => { setStatus(false); };
<h2>Network Status</h2> <p> You are <strong>{status ? "online" : "offline"}</strong> </p>
برای آزمایش حالت offline آن، فقط کافی است به ابزار توسعه دهندگان مرورگر مراجعه کرده و در برگهی network آن، حالت online را offline کنید:
در این حالت هم نمایش وضعیت آنلاین بودن کاربر به درستی کار میکند و هم سایر قسمتهایی که تاکنون اضافه کردهایم. به این معنا که هر چند توسط ذکر پارامتر [count]، وابستگی خاصی برای side effect ویژهای، مشخص شدهاست، اما ما را از تنظیم event listenerهای دیگری در قسمتهای mount و unmount محروم نمیکند.
پاکسازی اثرات جانبی در useEffect Hook، زمانیکه روشی برای آن وجود ندارد
در مثالی دیگر میخواهیم از API موقعیت جغرافیایی کاربر در مرورگر یا navigator.geolocation استفاده کنیم. توسط این API هم میتوان طول و عرض جغرافیایی را به دست آورد و هم تغییرات آنرا تحت نظر قرار داد. برای مثال با بررسی این تغییرات میتوان سرعت را نیز به دست آورد.
در این حالت نیز ابتدا با تعریف state مختص به آن شروع میکنیم و اینبار به عنوان مثال، مقدار اولیهی آنرا خارج از تابع جاری تنظیم میکنیم (جهت نمایش یک گزینهی مهیای دیگر):
const initialLocationState = { latitude: null, longitude: null, speed: null }; const App = () => { // ... const [location, setLocation] = useState(initialLocationState);
useEffect(() => { // ... navigator.geolocation.getCurrentPosition(handleGeolocation); const watchId = navigator.geolocation.watchPosition(handleGeolocation); return () => { // ... navigator.geolocation.clearWatch(watchId); }; }, [count]);
یک روش برای حل این مشکل و غیرفعال کردن دستی listener متد getCurrentPosition پس از unmount، تعریف یک متغیر mounted پیش از متد useEffect است:
let mounted = true; useEffect(() => { // ... return () => { // ... mounted = false; }; }, [count]);
const handleGeolocation = event => { if (mounted) { setLocation({ latitude: event.coords.latitude, longitude: event.coords.longitude, speed: event.coords.speed }); } };
<h2>Geolocation</h2> <p>Latitude is {location.latitude}</p> <p>Longitude is {location.longitude}</p> <p>Your speed is {location.speed ? location.speed : "0"}</p>
سؤال: در اینجا شیء location چندین بار تکرار شدهاست. آیا میتوان مقادیر خواص آنرا توسط Object Destructuring نیز به دست آورد؟
پاسخ: بله. در اینجا هم Object Destructuring بر روی location state، کار میکند:
const [{ latitude, longitude, speed }, setLocation] = useState( initialLocationState );
<h2>Geolocation</h2> <p>Latitude is {latitude}</p> <p>Longitude is {longitude}</p> <p>Your speed is {speed ? speed : "0"}</p>
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-30-part-01.zip
در این مقاله مروری خواهیم داشت بر نحوهی انجام یک سری از اعمال متداول با استفاده از jQuery بر روی select استاندارد HTML. قبل از شروع به عنوان قالب کار، صفحه سادهی زیر را در نظر بگیرید که از یک DropDownList استاندارد ASP.Net تشکیل شده است:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="select.aspx.cs" Inherits="TestJQueryAjax.select" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
<script src="js/jquery.js" type="text/javascript"></script>
<script type="text/javascript">
//...
</script>
</head>
<body>
<form id="form1" runat="server">
<asp:DropDownList ID="ddlTest" runat="server">
<asp:ListItem Value="1">آیتم یک</asp:ListItem>
<asp:ListItem Value="2">آیتم دو</asp:ListItem>
<asp:ListItem Value="3">آیتم سه</asp:ListItem>
<asp:ListItem Value="4">آیتم چهار</asp:ListItem>
<asp:ListItem Value="5">آیتم پنج</asp:ListItem>
</asp:DropDownList>
</form>
</body>
</html>
$("#<%=ddlTest.ClientID %>").val()
$("#<%=ddlTest.ClientID %> option:selected").text()
<script type="text/javascript">
$(document).ready(function() {
$("#<%=ddlTest.ClientID %>").change(function() {
alert($("#<%=ddlTest.ClientID %>").val());
});
});
</script>
$("#<%=ddlTest.ClientID %>").val(2);
$("<option value='6'>آیتم شش</option>").appendTo("#<%=ddlTest.ClientID %>");
6) خالی و حذف کردن تمامی آیتمها
$("#<%=ddlTest.ClientID %>").html("");
و برای تکمیل بحث میتوان به این برگه مرجع مراجعه کرد:
jQuery – Select element cheat sheet
ترجیحا نوع Typescript را انتخاب کردم. البته در داخل فایل ts. امکان نوشتن جاوا اسکریپت هم هست. بعد از ایجاد پروژه اگر با تصویری شبیه به تصویر زیر روبرو شدید، در نتیجه تنظیمات نصب و راه اندازی به درستی صورت گرفته است.
اگر به قسمت solution explorer دقت کنید، فایلی به نام config.xml را مشاهده خواهید کرد. با کلیک بر روی این فایل، یک صفحهی گرافیکی باز خواهد شد که این امکان را به شما میدهد که پلاگینهای مورد نیاز خود، تنظیمات مربوط به نرم افزار تولیدی (مانند تنظیم ورژن ویندوزی که میخواهید app شما بر روی آن اجرا شود) و تنظیمات مربوط به هر یک از پلتفرمها را به صورت مجزا در اختیار داشته باشید.
یک فایل index.html هم در قالب پیشفرض قرار داده شده که بعدا میتوانید آن را تغییر دهید و یا صفحات دیگری را اضافه کنید. همان طور که در قسمتهای قبل گفته شد، قرار است ما یک وب اپلیکیشن طراحی کنیم و آن را درون Container بومی Cordova بسته بندی کنیم. لذا محدودیتی برای استفادهی از کتابخانههای مرتبط با CSS ، HTML و JavaScript نداریم و در ادامهی مقالات با مثالهای متعددی از آنها استفاده خواهیم کرد.
در فولدر scripts-->typeings-->cordova-->plugins اینترفیسهایی که برای دسترسی به امکانات بومی دستگاه تلفن فعلا در Cordova پشتیبانی میشوند، قرار گرفته است.
برای استفاده از تکنولوژیهای وب در محیط بومی دستگاه، در طی فرآیند کامپایل، Cordova یک اپلیکیشن را به وسیله دو چیز مهم که در زیر اشاره شده است، خواهد ساخت.
- یک اپلیکیشن با یک کامپوننت WebView که با مرورگر یکپارچه شده است.
- یه سری از منابعی که در داخل فایلهای اپلیکیشن وب ما قرار دارند.
برای یکپارچه شدن APIهای Cordova با وب پیج موجود، اندکی کد نیاز داریم که برای انکار لینکی شبیه لینک زیر را در فایل html خود استفاده میکنیم که فقط بعد از کامپایل وجود خارجی دارد؛ به صورت زیر:
<script src="cordova.js"></script>
در پایان هم برای فهمیدن اینکه APIهای Cordova در دسترس هستند، میتوانیم رخداد مربوط به devicerady را مدیریت کنیم؛ به صورت زیر:
document.addEventListener("deviceready", onDeviceReady, false); function onDeviceReady() { /* INIT */ }
برای مدیریت رخدادهای مربوط به pause و resume هم که نشان دهندهی ادامه برنامه (خارج شدن از حالت pause) و حالت تعلیق هستند، میتوان به شکل زیر عمل کرد:
function onDeviceReady() { // Handle the Cordova pause and resume events document.addEventListener('pause', onPause, false); document.addEventListener('resume', onResume, false); // TODO: Cordova has been loaded. Perform any initialization that requires Cordova here. } function onPause() { // TODO: This application has been suspended. Save application state here. } function onResume() { // TODO: This application has been reactivated. Restore application state here. }
حال قصد داریم پروژهی خود را که قرار است یک متن ساده را نشان دهد، با استفاده از شبیه ساز اجر ا کنیم. برای این منظور از قسمت toolbar ویژوال استودیو ، Solution Platform خود را انتخاب کنید و سپس میتوانید شبیه ساز مورد نظر خود را انتخاب کرده و برنامه را اجرا کنید. در اینجا محیط مورد نظر من اندروید است و برای این منظور هم میتوانم از شبیه ساز Android Emulator یا Ripple استفاده کنم. به دلیل سرعت کم شبیه ساز اندروید، میتوانید شبیه ساز YouWave را دانلود و اجرا کرده و در قسمتی که شبیه ساز را از toolbar ویژوال انتخاب میکردید، این بار گزینهی Device را انتخاب کنید. بعد از کامپایل برنامهی شما، فایل apk تولید شده بر روی شبیه ساز نصب خواهد شد و شما قادر خواهید بود آنرا اجرا کنید.
نتیجهی نهایی
با شبیه ساز Ripple
مطالعه بیشتر
https://msdn.microsoft.com/en-us/library/dn879821(v=vs.140).aspx
http://blog.falafel.com/getting-started-with-cordova-and-multi-device-hybrid-app-in-visual-studio/
http://www.codeproject.com/Articles/860150/Visual-Studio-and-Apache-Cordova
نکته : وقتی پروژه را برای اولین بار اجرا میکنید شاید کمی طول بکشد تا نتیجهی نهایی را ببنید و آن هم به دلیل این است که ویژوال استودیو باید مجموعهای از package های مورد نیاز Cordova را دانلود کند.
در مقاله بعد با jQuery Mobile آشنا خواهیم شد و یک مثال برای کار کردن با آن در نظر خواهم گرفت.
ادامه دارد ...
نکاتی که باید جهت بررسی یکسان بودن دو URL بررسی کرد
using System; using System.Web; namespace UrlNormalizationTest { public static class UrlNormalization { public static bool AreTheSameUrls(this string url1, string url2) { url1 = url1.NormalizeUrl(); url2 = url2.NormalizeUrl(); return url1.Equals(url2); } public static bool AreTheSameUrls(this Uri uri1, Uri uri2) { var url1 = uri1.NormalizeUrl(); var url2 = uri2.NormalizeUrl(); return url1.Equals(url2); } public static string[] DefaultDirectoryIndexes = new[] { "default.asp", "default.aspx", "index.htm", "index.html", "index.php" }; public static string NormalizeUrl(this Uri uri) { var url = urlToLower(uri); url = limitProtocols(url); url = removeDefaultDirectoryIndexes(url); url = removeTheFragment(url); url = removeDuplicateSlashes(url); url = addWww(url); url = removeFeedburnerPart(url); return removeTrailingSlashAndEmptyQuery(url); } public static string NormalizeUrl(this string url) { return NormalizeUrl(new Uri(url)); } private static string removeFeedburnerPart(string url) { var idx = url.IndexOf("utm_source=", StringComparison.Ordinal); return idx == -1 ? url : url.Substring(0, idx - 1); } private static string addWww(string url) { if (new Uri(url).Host.Split('.').Length == 2 && !url.Contains("://www.")) { return url.Replace("://", "://www."); } return url; } private static string removeDuplicateSlashes(string url) { var path = new Uri(url).AbsolutePath; return path.Contains("//") ? url.Replace(path, path.Replace("//", "/")) : url; } private static string limitProtocols(string url) { return new Uri(url).Scheme == "https" ? url.Replace("https://", "http://") : url; } private static string removeTheFragment(string url) { var fragment = new Uri(url).Fragment; return string.IsNullOrWhiteSpace(fragment) ? url : url.Replace(fragment, string.Empty); } private static string urlToLower(Uri uri) { return HttpUtility.UrlDecode(uri.AbsoluteUri.ToLowerInvariant()); } private static string removeTrailingSlashAndEmptyQuery(string url) { return url .TrimEnd(new[] { '?' }) .TrimEnd(new[] { '/' }); } private static string removeDefaultDirectoryIndexes(string url) { foreach (var index in DefaultDirectoryIndexes) { if (url.EndsWith(index)) { url = url.TrimEnd(index.ToCharArray()); break; } } return url; } } }
using NUnit.Framework; using UrlNormalizationTest; namespace UrlNormalization.Tests { [TestFixture] public class UnitTests { [Test] public void Test1ConvertingTheSchemeAndHostToLowercase() { var url1 = "HTTP://www.Example.com/".NormalizeUrl(); var url2 = "http://www.example.com/".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test2CapitalizingLettersInEscapeSequences() { var url1 = "http://www.example.com/a%c2%b1b".NormalizeUrl(); var url2 = "http://www.example.com/a%C2%B1b".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test3DecodingPercentEncodedOctetsOfUnreservedCharacters() { var url1 = "http://www.example.com/%7Eusername/".NormalizeUrl(); var url2 = "http://www.example.com/~username/".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test4RemovingTheDefaultPort() { var url1 = "http://www.example.com:80/bar.html".NormalizeUrl(); var url2 = "http://www.example.com/bar.html".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test5AddingTrailing() { var url1 = "http://www.example.com/alice".NormalizeUrl(); var url2 = "http://www.example.com/alice/?".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test6RemovingDotSegments() { var url1 = "http://www.example.com/../a/b/../c/./d.html".NormalizeUrl(); var url2 = "http://www.example.com/a/c/d.html".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test7RemovingDirectoryIndex1() { var url1 = "http://www.example.com/default.asp".NormalizeUrl(); var url2 = "http://www.example.com/".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test7RemovingDirectoryIndex2() { var url1 = "http://www.example.com/default.asp?id=1".NormalizeUrl(); var url2 = "http://www.example.com/default.asp?id=1".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test7RemovingDirectoryIndex3() { var url1 = "http://www.example.com/a/index.html".NormalizeUrl(); var url2 = "http://www.example.com/a/".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test8RemovingTheFragment() { var url1 = "http://www.example.com/bar.html#section1".NormalizeUrl(); var url2 = "http://www.example.com/bar.html".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test9LimitingProtocols() { var url1 = "https://www.example.com/".NormalizeUrl(); var url2 = "http://www.example.com/".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test10RemovingDuplicateSlashes() { var url1 = "http://www.example.com/foo//bar.html".NormalizeUrl(); var url2 = "http://www.example.com/foo/bar.html".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test11AddWww() { var url1 = "http://example.com/".NormalizeUrl(); var url2 = "http://www.example.com".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test12RemoveFeedburnerPart() { var url1 = "http://site.net/2013/02/firefox-19-released/?utm_source=rss&utm_medium=rss&utm_campaign=firefox-19-released".NormalizeUrl(); var url2 = "http://site.net/2013/02/firefox-19-released".NormalizeUrl(); Assert.AreEqual(url1, url2); } } }
9 پیش بینی برای آینده برنامه نویسی
«... انتخاب عدد با حرف در عدد نویسی متون فارسی تابع قاعده مطلقی نیست اما رعایت نکاتی برای نظم و راحت خوانده شدن متن توصیه میشود که تقریبا مورد توافق اغلب ویراستاران است. در متونی که پر رقم است بهتر است برای راحتی بیشتر از ابتدا از اعداد استفاده کرد. مثلا در یک متنی که توضیحات مفصلی درباره یک فرمول ریاضی دارد یا درباره مساحت متفاوت واحدهای مسکونی میگوید و قیمت آنها را بررسی میکند بهتر است صرفا از عدد استفاده شود. برخی از روانشناسان میگویند عدد بیش از حروف در ذهن خواننده میماند. مثلا وقتی بنویسد۱۲۰ تومان بهتر از صد و بیست تومان در یاد میماند. با این همه موضوع درس ما نوشتن رقم در متنی دانشگاهی٬ اداری٬ گزارشی است که حد متعارفی رقم در آن استفاده میشود. در چنین متونی بهتر است رقم۱ تا ۱۰ با عدد نوشته شود. نوشتن صفر به صورت ۰ میتواند خواندن آن را در نگاه اول با مشکل مواجه کند و به علت شباهت با نقطه اشتباه گرفته شود. عدد ۹ هم به صورت حروفی نه ممکن است به معنی خیر خوانده شود. عدد ۱۰ هم در صورتی که به صورت ده نوشته شود میتواند با تلفظهای متفاوتی خوانده و فهمیده شود. بنابر این برای یک دست شدن میتوان ۱ تا ۱۰ را با عدد نوشت....»
پلاگین DataTables کتابخانه jQuery - قسمت سوم
رندر کردن جدول حاوی دادهها باید به data tables سپرده بشه. بدین صورت که دادههای دریافتی از سرور به فرمت مناسبی تبدیل بشن و بعد به خصوصیت aaData نسبت داده بشن، البته به تبع اون و حتما باید خصوصیت aoColumns هم مقدار دهی بشه.
$(document).ready(function () { $.ajax({ url: "ِDefault.aspx/GetBrowsers", contentType: "application/json; charset=utf-8", dataType: "json", type: "POST", success: function (response) { if (response != "") { var data = eval("(" + response.d + ")"); $('#browsers-grid').dataTable({ "aaData": data, "bProcessing" : true, "aoColumns": [ { "mData": "Engine" }, { "mData": "Name" }, { "mData": "Platform" }, { "mData": "Version", "sClass": "center" }, { "mData": "Grade", "sClass": "center" } ] }); } }, }); });
کدهای سمت سرور:
مثلا فرض کنید ذر سمت سرور بخواهید لیستی از مرورگرها رو برگشت بدین. کلاس زیر رو در نظر بگیرید:
public class Browser { public int Id { get; set; } public string Engine { get; set; } public string Name { get; set; } public string Platform { get; set; } public float Version { get; set; } public string Grade { get; set; } }
برای برگشت دادن لیستی از مرورگرها به طرف کلاینت، متدی مثل زیر خواهید داشت:
[WebMethod] public static string GetBrowsers() { List<Browser> browsers = new List<Browser>() { new Browser { Id = 1, Engine = "Trident", Name = "Internet Explorer 4.0", Platform = "Win95+", Version = 4, Grade = "X" }, new Browser { Id = 2, Engine = "Trident", Name = "Internet Explorer 5.0", Platform = "Win95+", Version = 5, Grade = "C" }, }; return browsers.ToJson(); }
در متد بالا، لیستی از مرورگرها با استفاده از یک متد الحاقی تبدیل به فرمت json میشه و به طرف کاربر فرستاده میشه.
JQuery 1.3 ارائه شد
این Jquery انقدر جالبه که من که برنامه نویس نیستم، علاقه مند شدم... اولین بار هم از طریق وبلاگ شما آشنا شدم. خیلی دوست دارم که در موردش بیشتر بدونم... خیلی زیاد تو سایت jquery.com گشتم و چیز های جالبی هم پیدا کردم. اگر امکانش باشه، می خواستم خواهش کنم یه سری مطالب با تمرکز بیشتر در این مورد بنویسید. مثلا این توصیه هاست روی گوگل جالب بود. حالا نمی دونم اگر روی هاست داخل ایران باشه، آیا سرعت لودشدنش در ایران بیشتر میشه یا گوگل؟! فعلا دارم دنبال پلاگین های جالب Jquery می گردم... خیلی متشکرم از مطالب خیلی جالبتون.