JQuery 1.3 ارائه شد
روز قبل، نگارش رسمی و نهایی jQuery 1.3 ارائه شد و بلافاصله هم فایل مخصوص آن جهت بکارگیری intellisense ویژوال استودیو، توسط علاقمندان تهیه و عرضه گردید.
تازههای این نگارش:
- Sizzle : یک موتور CSS selector قدرتمند (400 درصد بهبود سرعت نسبت به عملکرد کتابخانه قبلی در این مورد)
- بازنگری در نحوه مدیریت رخدادها
- موتور تزریق HTML بسیار سریع (تا 15 برابر سریعتر نسبت به کتابخانه قبلی)
- موتور بسیار سریع موقعیت یابی
- تشخیص نوع مرورگر در آن متوقف شده و بجای آن از تشخیص ویژگیها برای ماندگاری بیشتر این کتابخانه در سالهای آتی استفاده گردیده است. (بجای بررسی userAgent ، از باگهای شناخته شده یا ویژگیهای خاص، جهت تشخیص مرورگر استفاده میشود تا بازه وسیعی از محصولات مشابه را بتوانند پوشش دهند)
- جهت مشاهده و آزمایش بهبود کارآیی این کتابخانه لطفا به این صفحه مراجعه کنید. (لیست کامل این آزمایشات در انتهای صفحه لیست تغییرات، قابل دسترسی است)
مانند همیشه این کتابخانه در دو نگارش فشرده شده (جهت استفاده در سایتها) و نگارش عادی و حجیمتر (جهت برنامه نویسی و دیباگ کردن کدها) ارائه شده است. چون کدها در Google code هاست شده احتمالا دسترسی به آن مشکل خواهد بود. این مجموعه را از این آدرس میتوانید دریافت کنید.
توصیه میشود از سرورهای سریع گوگل بعنوان هاست کتابخانه جیکوئری سایتهای خود استفاده کنید. برای این منظور از آدرس زیر میتوان استفاده کرد:
CORS چیست؟
CORS و یا cross origin resource sharing، یک مکانیزم امنیتی است که در تمام مرورگرهای جدید جهت جلوگیری از ارسال اطلاعات یک وب سایت، به وب سایتی دیگر، درنظر گرفته شدهاست. درخواست دسترسی به یک منبع (Resource) مانند تصویر، داده و غیره، خارج از سرآغاز آن، یک درخواست cross-origin نامیده میشود. برای مدیریت یک چنین درخواستهایی، استانداردی به نام CORS طراحی شدهاست. میتوان به آن مانند نگهبانی یک ساختمان نگاه کرد که تا مجوز خاصی به آنها ارائه نشود، امکان دسترسی به منابع ساختمان را صادر نخواهند کرد.
Origin چیست؟
سرآغاز یک درخواست از سه قسمت تشکیل میشود:
- Protocol/Scheme مانند HTTP/HTTPS
- Host مانند نام دومین
- شماره پورت مانند 443 برای پروتکل HTTPS یا پورت 80 برای HTTP که عموما هر دو مورد به علت پیشفرض بودن، ذکر نمیشوند
بنابراین URLای مانند https://www.dntips.ir یک Origin را مشخص میکند. در اینجا به تمام منابعی که از این سرآغاز شروع میشوند و سه قسمت یاد شدهی آنها یکی است، same-origin گفته میشود.
در این حالت اگر منابعی به URLهایی مانند https://www.dntips.ir (پروتکل متفاوت) و یا https://github.com (با host متفاوت) اشاره کنند، به آنها Different-Origin گفته خواهد شد.
سیاست امنیتی Same-Origin چیست؟
سیاست امنیتی Same-Origin که به صورت توکار در تمام مرورگرهای امروزی تعبیه شدهاست، مانع تعامل سرآغازهایی متفاوت، جهت جلوگیری از حملات امنیتی مانند Cross-Site Request Forgery یا CSRF میشود. این سیاست امنیتی بسیار محدود کنندهاست و برای مثال مانع این میشود که بتوان توسط کدهای JavaScript ای، درخواستی را به سرآغاز دیگری ارسال کرد؛ حتی اگر بدانیم درخواست رسیده از منبعی مورد اطمینان صادر شدهاست. برای مثال اگر سایت جاری یک درخواست Ajax ای را به https://anotherwebsite.com/api/users ارسال کند، چون قسمت host مربوط به origin آنها یکی نیست، این عملیات توسط مرورگر غیرمجاز شمرده شده و مسدود میشود.
چگونه میتوان از تنظیمات CORS، برای رفع محدودیتهای سیاستهای دسترسی Same-Origin استفاده کرد؟
استاندارد CORS تعدادی header ویژه را جهت تبادل اطلاعات بین سرور و مرورگر مشخص کردهاست تا توسط آنها بتوان منابع را بین سرآغازهای متفاوتی به اشتراک گذاشت. در این حالت ابتدا کلاینت درخواستی را به سمت سرور ارسال میکند. سپس سرور پاسخی را به همراه هدرهای ویژهای که مشخص میکنند به چه منابعی و چگونه میتوان دسترسی یافت، به سمت کلاینت ارسال خواهد کرد. اکثر این هدرها با Access-Control-Allow شروع میشوند:
• Access-Control-Allow-Origin
در آن لیست سرآغازهایی را که سرور مایل است منابع خود را با آنها به اشتراک بگذارد، مشخص میشوند. در حالت توسعهی برنامه میتوان از مقدار * نیز برای آن استفاده کرد تا هر سرآغازی بتواند به منابع سرور دسترسی پیدا کند. اما از استفادهی این مقدار سراسری و کلی، در حالت انتشار برنامه خودداری کنید.
• Access-Control-Allow-Headers
• Access-Control-Allow-Methods
• Access-Control-Allow-Credentials
درخواستهای ویژهی Pre-Flight
در بسیاری از موارد، مرورگر زمانیکه تشخیص میدهد درخواست صادر شدهی از طرف برنامه، قرار است به یک Origin دیگر ارسال شود، خودش یک درخواست ویژه را به نام Pre-Flight و از نوع OPTIONS به سمت سرور ارسال میکند. علت آن این است که سرورهای قدیمی، مفهوم CORS را درک نمیکنند و در برابر درخواستهای ویژهای مانند Delete که از سرور جاری صادر نشده باشند، مقاومت میکنند (صرفا درخواستهای Same-Origin را پردازش میکنند). به همین جهت است که اگر به برگهی network ابزارهای توسعه دهندگان مرورگر خود دقت کنید، درخواستهای cross-origin به نظر دوبار ارسال شدهاند. بار اول درخواستی که method آن، options است و در خواست دوم که درخواست اصلی است و برای مثال method آن put است. این درخواست اول، Pre-Flight Request نام دارد. هدف آن این است که بررسی کند آیا سرور مقصد، استاندارد CORS را متوجه میشود یا خیر و آیا اینقدر قدیمی است که صرفا درخواستهای Same-Origin را پردازش میکند و مابقی را مسدود خواهد کرد. اگر سرور درخواست Pre-Flight را درک نکند، مرورگر درخواست اصلی را صادر نخواهد کرد.
البته درخواستهای pre-flight بیشتر در حالتهای زیر توسط مرورگر صادر میشوند:
- اگر متد اجرایی مدنظر، متدی بجز GET/POST/HEAD باشد.
- اگر content type درخواستهای از نوع POST، چیزی بجز application/x-www-form-urlencoded, multipart/form-data و یا text/plain باشند.
- اگر درخواست، به همراه یک هدر سفارشی باشد نیز سبب صدور یک pre-flight request خواهد شد.
بنابراین در برنامههای SPA خود که اطلاعات را با فرمت JSON به سمت یک Web API ارسال میکنند (و Origin آنها یکی نیست)، حتما شاهد درخواستهای Pre-Flight نیز خواهید بود.
تنظیمات CORS در ASP.NET Core
اکنون که با مفهوم CORS آشنا شدیم، در ادامه به نحوهی انجام تنظیمات آن در برنامههای ASP.NET Core خواهیم پرداخت.
تنظیمات ابتدایی CORS در فایل Startup.cs و متد ConfigureServices آن انجام میشوند:
public void ConfigureServices(IServiceCollection services) { // Add service and create Policy with options services.AddCors(options => { options.AddPolicy("CorsPolicy", builder => builder.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials() ); }); // Make sure MVC is added AFTER CORS services.AddMvc(); }
در اینجا CorsPolicy که به عنوان پارامتر مشخص شدهاست، نام این سیاست دسترسی سفارشی است و در قسمتهای مختلف برنامه میتوان ارجاعاتی را به این نام تعریف کرد.
و برای تعریف سرآغازی خاص و یا متدی مشخص، میتوان به صورت زیر عمل کرد (ذکر صریح WithOrigins برای حالت توزیع برنامه مناسب است و از دیدگاه امنیتی، استفادهی از AllowAnyOrigin کار صحیحی نیست ):
services.AddCors(options => { options.AddPolicy("AllowOrigin", builder => builder.WithOrigins("http://localhost:55294") .WithMethods("GET", "POST", "HEAD")); });
پس از تنظیم سیاست دسترسی مدنظر، اکنون نوبت به اعمال آن است. اینکار در متد Configure فایل Startup.cs انجام میشود:
public void Configure(IApplicationBuilder app) { // ... app.UseCors("CorsPolicy"); // ... app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); }
متد app.UseCors، یک متد سراسری است و کل سیستم را داخل این سیاست دسترسی CORS قرار میدهد. اگر میخواهید صرفا به کنترلر خاصی این تنظیمات اعمال شوند، میتوان از فیلتر EnableCors با ذکر نام سیاست دسترسی تعریف شده، استفاده کرد (در این حالت ذکر services.AddCors ضروری است و از ذکر app.UseCors صرفنظر میشود):
[EnableCors("CorsPolicy")] public class MyApiController : Controller
و یا اگر میخواهید از app.UseCors سراسری استفاده کنید، اما آنرا برای کنترلر و یا حتی اکشن متد خاصی غیرفعال نمائید، میتوان از فیلتر [DisableCors] استفاده کرد.
چند نکته:
- به نام رشتهای سیاست دسترسی تعریف شده، دقت داشته باشید. اگر این نام را درست ذکر نکنید، هدری تولید نخواهد شد.
- میانافزار CORS، برای درخواستهای same-origin نیز هدری را تولید نمیکند.
امکان تعریف سیاستهای دسترسی بدون نام نیز وجود دارد
در هر دو روش فوق که یکی بر اساس app.UseCors سراسری و دیگری بر اساس فیلتر EnableCors اختصاصی کار میکنند، ذکر نام سیاست دسترسی options.AddPolicy ضروری است. اگر علاقهای به ذکر این نام ندارید، میتوان به طور کامل از تنظیم services.AddCors صرفنظر کرد (قسمت پارامتر و تنظیمات آنرا ذکر نکرد) و متد app.UseCors را به صورت زیر تنظیم نمود که قابلیت داشتن تنظیمات خاص خود را نیز دارا است:
public void ConfigureServices(IServiceCollection services) { services.AddCors(); services.AddMvc(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseCors(policyBuilder => { string[] origins = new string[] { "http://localhost:2000", "http://localhost:2001" }; policyBuilder .AllowAnyHeader() .AllowAnyMethod() .WithOrigins(origins); }); app.UseMvc(); }
روش دیگر انجام اینکار، تعریف یک DefaultPolicy است:
public void ConfigureServices(IServiceCollection services) { services.AddCors(options => { options.AddDefaultPolicy( builder => { builder .AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader(); }); options.AddPolicy("MyCORSPolicy", builder => { builder.WithOrigins("http://localhost:49373") .AllowAnyHeader() .AllowAnyMethod(); }); }); services.AddMvc().AddNewtonsoftJson(); }
خطاهای متداول حین کار با CORS
خطای کار با SignalR و ارسال اطلاعات اعتبارسنجی کاربر
Access to XMLHttpRequest at 'http://localhost:22135/chat/negotiate?jwt=<removed the jwt key>' from origin 'http://localhost:53150' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.
services.AddCors(options => { options.AddPolicy("AllowAllOrigins", builder => { builder .AllowAnyOrigin() .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); }); });
تمام اعتبارسنجیها و اطلاعات کوکیها در سمت سرور قابل دسترسی نیستند
CORS به صورت پیشفرض اطلاعات کوکیها را به دومینی دیگر ارسال نمیکند. در این حالت اگر حتما نیاز به انجام اینکار است، باید در درخواست Ajax ای ارسالی، خاصیت withCredentials به true تنظیم شود. همچنین سمت سرور نیز هدر Access-Control-Allow-Credentials را تنظیم کند (همان متد AllowCredentials فوق). در اینجا Credentials به کوکیها، هدرهای اعتبارسنجی (مانند هدرهای JWT) کاربران و یا مجوزهای TLS کلاینتها اشاره میکند.
var xhr = new XMLHttpRequest(); xhr.open('get', 'http://www.example.com/api/test'); xhr.withCredentials = true; //In jQuery: $.ajax({ type: 'get', url: 'http://www.example.com/home', xhrFields: { withCredentials: true } //ES6 fetch API: fetch( 'http://remote-url.com/users', { 'PUT', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, mode: 'cors', credentials: 'include', body: JSON.stringify(body) } ) .then((response) => response.json()) .then((response) => console.log(response)) .catch((error) => console.error(error));
CORS حساس به حروف کوچک و بزرگ است
روش دیگر تنظیم فیلتر EnableCors را در اینجا مشاهده میکنید:
[EnableCors(origins: "http://MyWebsite.com", headers: "*", methods: "*")] //is not the same as this: [EnableCors(origins:"http://mywebsite.com", headers: "*", methods: "*")]
در حالت بروز خطای مدیریت نشدهای در سمت سرور، ASP.NET Core هدرهای CORS را ارسال نمیکند
اطلاعات بیشتر
- Chrome، امن ترین مرورگر | www.certcc.ir
- اتصال به TFS از طریق Linux | saeedzarinfam.blogfa.com
- اصل Single Responsibility چیست؟ | www.dotnetdev.info
- الکامپ٬ نمایشگاهی بدون جذابیت | navid.kashani.ir
- آموزش ساخت اشکال مختلف با استفاده از CSS | tortak.com
- Microsoft Ajax و استفاده از ScriptReferenceProfiler جهت Script Combining | mojtabasahraei.blogfa.com
- بسته نیوگت Roslyn | blogs.msdn.com
- تغییرات انجام شده در WCF RIA Services V1.0 SP2 | jeffhandley.com
- کمی در مورد سرنوشت Mono پس از ناول | gigaom.com
- نکاتی در مورد استفاده از LocalDB در ASP.NET قسمت اول | blogs.msdn.com
- نکاتی در مورد استفاده از LocalDB در ASP.NET قسمت دوم | blogs.msdn.com
تبدیل یک View به رشته و بازگشت آن به همراه نتایج JSON حاصل از یک عملیات Ajax ایی در ASP.NET MVC
ممکن است بخواهیم در پاسخ یک تقاضای Ajax ایی، اگر عملیات در سمت سرور با موفقیت انجام شد، خروجی یک Controller action را به کاربر نهایی نشان دهیم. در
چنین سناریویی لازم است که بتوانیم خروجی یک action را
بصورت رشته برگردانیم. در این مقاله به این مسئله خواهیم پرداخت .
فرض کنید در یک سیستم وبلاگ ساده قصد داریم امکان کامنت گذاشتن بصورت Ajax را پیاده سازی کنیم. یک ایده عملی و کارآ
این است: بعد از اینکه کاربر متن کامنت را وارد کرد و دکمهی ارسال کامنت را زد،
تقاضا به سمت سرور ارسال شود و اگر سرور پیغام موفقیت را صادر کرد، متن نوشته شده
توسط کاربر را به کمک کدهای JavaScript و در همان سمت کلاینت بصورت یک
کادر کامنت جدید به محتوای صفحه اضافه کنیم. بنده در اینجا برای اینکه بتوانم اصل
موضوع مورد بحث را توضیح دهم، از یک سناریوی جایگزین استفاده میکنم؛ کاربر موقعیکه دکمه ارسال را زد، تقاضا به سرور ارسال میشود. سرور بعد از انجام عملیات، تحت یک
شی JSON هم نتیجهی انجام عملیات و هم
محتوای HTML نمایش کامنت جدید در صفحه را به سمت کلاینت
ارسال خواهد کرد و کلاینت در صورت موفقیت آمیز بودن عملیات، آن محتوا را به صفحه
اضافه میکند.
با توجه به توضیحات داده شده، ابتدا یک شیء نیاز داریم تا بتوانیم توسط آن نتیجهی عملیات Ajax ایی را بصورت JSON به سمت کلاینت ارسال کنیم:
public class MyJsonResult { public bool success { set; get; } public bool HasWarning { set; get; } public string WarningMessage { set; get; } public int errorcode { set; get; }
public string message {set; get; } public object data { set; get; } }
سپس به متدی نیاز داریم که کار تبدیل نتیجهی action را به رشته، انجام دهد:
public static string RenderViewToString(ControllerContext context, string viewPath, object model = null, bool partial = false) { ViewEngineResult viewEngineResult = null; if (partial) viewEngineResult = ViewEngines.Engines.FindPartialView(context, viewPath); else viewEngineResult = ViewEngines.Engines.FindView(context, viewPath, null); if (viewEngineResult == null) throw new FileNotFoundException("View cannot be found."); var view = viewEngineResult.View; context.Controller.ViewData.Model = model; string result = null; using(var sw = new StringWriter()) { var ctx = new ViewContext(context, view, context.Controller.ViewData, context.Controller.TempData, sw); view.Render(ctx, sw); result = sw.ToString(); } return result; }
فرض کنیم در سمت Controller هم از کدی شبیه به این استفاده میکنیم:
public JsonResult AddComment(CommentViewModel model) { MyJsonResult result = new MyJsonResult() { success = false; }; if (!ModelState.IsValid) { result.success = false; result.message = "لطفاً اطلاعات فرم را کامل وارد کنید"; return Json(result); } try { Comment theComment = model.toCommentModel(); //EF service factory Factory.CommentService.Create(theComment); Factory.SaveChanges(); result.data = Tools.RenderViewToString(this.ControllerContext, "/views/posts/_AComment", model, true); result.success = true; } catch (Exception ex) { result.success = false; result.message = "اشکال زمان اجرا"; } return Json(result); }
و در سمت کلاینت برای ارسال Form به صورت Ajax ایی خواهیم داشت:
@using (Ajax.BeginForm("AddComment", "posts", new AjaxOptions() { HttpMethod = "Post", OnSuccess = "AddCommentSuccess", LoadingElementId = "AddCommentLoading" }, new { id = "frmAddComment", @class = "form-horizontal" })) { @Html.HiddenFor(m => m.PostId) <label for="fname">@Texts.ContactName</label> <input type="text" id="fname" name="FullName" class="form-control" placeholder="@Texts.ContactName "> <label for="email">@Texts.Email</label> <input type="email" id="InputEmail" name="email" class="form-control" placeholder="@Texts.Email"> <br><textarea name="C_Content" cols="60" rows="10" class="form-control"></textarea><br> <input type="submit" value="@Texts.SubmitComments" name="" class="btn btn-primary"> <div class="loading-mask" style="display:none">@Texts.LoadingMessage</div> }
باید توجه شود Texts در اینجا یک Resource هست که به منظور نگهداری کلمات استفاده شده در سایت، برای زبانهای مختلف استفاده میشود (رجوع شود به مفهوم بومی سازی در Asp.net) .
و در قسمت script ها داریم:
<script type="text/javascript"> function AddCommentSuccess(jsData) { if (jsData && jsData.message) alert(jsData.message); if (jsData && jsData.success) { document.getElementById("frmAddComment").reset(); //افزودن کامنت جدید ساخته شده توسط کاربر به لیست کامنتهای صفحه $("#divAllComments").html(jsData.data + $("#divAllComments").html()); } } </script>
بارگذاری یک یوزرکنترل با استفاده از جیکوئری
برای دات نت 2 کمی کار شما بیشتر میشود چون در VS2008 خیلی از موارد را خودکار اضافه میکند.
در این حالت یا باید ASP.NET AJAX Extensions روی سرور نصب باشد یا dll های آنرا به صورت دستی به دایرکتوری bin برنامه اضافه کنید و یک ارجاع از آنها را نیز به برنامه اضافه کنید.
همچنین وب کانفیگ را نیز باید ویرایش کنید تا این تغییر در آن لحاظ شود. (در غیر اینصورت کار نمیکند)
یک نکته دیگر هم اینکه بجای msg.d شما فقط msg را در asp.net 2 دارید و این d از 3.5 به بعد اضافه شده (در قسمت success و هنگام دریافت پاسخ از سرور توسط jQuery).
در ادامه روشهای مختلف ارسال درخواستهای Ajax را توسط jQuery و همچنین معادلهای XMLHttpRequest و Fetch API آنها بررسی میکنیم.
ارسال درخواستهای GET
توسط استاندارد جدید Fetch API | توسط XMLHttpRequest استاندارد |
توسط jQuery |
fetch('/my/name').then(function(response) { if (response.ok) { return response.text(); } else { throw new Error(); } }).then( function success(name) { console.log('my name is ' + name); }, function failure() { console.error('Name request failed!'); } ); | var xhr = new XMLHttpRequest(); xhr.open('GET', '/my/name'); xhr.onload = function() { if (xhr.status >= 400) { console.error('Name request failed!'); } else { console.log('my name is ' + xhr.responseText); } }; xhr.onerror = function() { console.error('Name request failed!'); }; xhr.send(); | $.get('/my/name').then( function success(name) { console.log('my name is ' + name); }, function failure() { console.error('Name request failed!'); } ); |
jQuery برای پشتیبانی از درخواستهای Ajax، متد ویژهای را به نام ()ajax ارائه میکند که برای ارسال درخواستهایی از هر نوع، مانندGET، POST و غیره کاربرد دارد. همچنین برای بعضی از نوعها، متدهای کوتاهتری را مانند ()get و ()post نیز در اختیار برنامه نویس قرار میدهد. جاوا اسکریپت خالص و Web API مرورگرها نیز دو شیء XMLHttpRequest و fetch را برای ارسال درخواستهای غیرهمزمان، ارائه میکند. XMLHttpRequest در تمام مرورگرهای قدیمی و جدید پشتیبانی میشود، اما fetch API مدتی است که در غالب مرورگرهای امروزی در دسترس است. در جدول فوق روش ارسال درخواستهای Ajax از نوع GET را توسط این سه روش مشاهده میکنید.
در این مثالها درخواستی به آدرس my/name سمت سرور ارسال شده و انتظار میرود که یک plaintext حاوی متن کاربر بازگشت داده شود که در نهایت در console لاگ میشود.
- در حالت استفادهی از jQuery در صورت بازگشت موفقیت آمیز پاسخی از طرف سرور، متد success و در غیراینصورت متد failure اجرا میشود. باید درنظر داشت که متد ajax جیکوئری، چیزی بیشتر از یک محصور کنندهی اشیاء XMLHttpRequest نیست.
- در حالت کار با XMLHttpRequest باید اندکی بیشتر تایپ کرد؛ اما اصول کار یکی است. در اینجا onload زمانی فراخوانی میشود که پاسخی از سرور دریافت شده و عملیات خاتمه یافتهاست؛ هرچند این پاسخ میتواند یک خطا نیز باشد. به همین جهت باید status آنرا بررسی کرد. اگر رخداد onerror فراخوانی شد، یعنی درخواست، در سطوح بسیار پایین آن مانند بروز یک خطای CORS با شکست مواجه شدهاست.
همانطور که مشاهده میکنید در حالت کار با XMLHttpRequest جاوا اسکریپت از اشیاء Promise پشتیبانی نمیکند که این کمبود با معرفی fetch API برطرف شدهاست که نمونهای از آنرا با متد then متصل به fetch مشاهده میکنید؛ دقیقا مشابه متد ajax جیکوئری که آن نیز یک Promise را بازگشت میدهد.
تفاوت Promise جیکوئری با fetch API در این است که جیکوئری در صورتیکه یک status code بیانگر خطا را دریافت کند، قسمت failure را اجرا میکند؛ اما fetch API مانند اشیاء XMLHttpRequest تنها در صورت بروز خطاهای سطح پایین درخواست، این متد را فراخوانی خواهد کرد. هرچند اگر در اینجا response.ok نبود، میتوان با صدور یک استثناء به رفتاری شبیه به jQuery رسید و قسمت then failure را به صورت خودکار اجرا کرد.
ارسال درخواستهای Ajax از نوع POST ، PUT و DELETE
توسط استاندارد جدید Fetch API | توسط XMLHttpRequest استاندارد | توسط jQuery |
fetch('/user/name', { method: 'POST', body: 'some data' }); | var xhr = new XMLHttpRequest(); xhr.open('POST', '/user/name'); xhr.send('some data'); | $.ajax({ method: 'POST', url: '/user/name', contentType: 'text/plain', data: 'some data' }); |
اگر از شیء XMLHttpRequest استفاده شود، Content-Type آن به صورت پیشفرض به text/plain تنظیم شدهاست. در اینجا بدنهی درخواست به متد send ارسال میشود.
متد fetch نیز همانند شیء XMLHttpRequest دارای Content-Type پیشفرض از نوع text/plain است. در اینجا بدنهی درخواست به خاصیت body انتساب داده میشود.
توسط استاندارد جدید Fetch API | توسط XMLHttpRequest استاندارد | توسط jQuery |
fetch('/user/1', { method: 'PUT', body: //record including new mobile number }); | var xhr = new XMLHttpRequest(); xhr.open('PUT', '/user/1'); xhr.send(/* record including new data */); | $.ajax({ method: 'PUT', url: '/user/1', contentType: 'text/plain', data: //record including new data }); |
توسط استاندارد جدید Fetch API | توسط XMLHttpRequest استاندارد | توسط jQuery |
fetch('/user/1', {method: 'DELETE'}); | var xhr = new XMLHttpRequest(); xhr.open('DELETE', '/user/1'); xhr.send(); | $.ajax('/user/1', {method: 'DELETE'}); |
مدیریت Encoding درخواستهای Ajax
در مثالهای قبل، اطلاعاتی از نوع text/plain را به سمت سرور ارسال کردیم که به آن encoding type نیز گفته میشود. برای تکمیل بحث میتوان حالتهای دیگری مانند application/x-www-form-urlencoded، application/json و یا multipart/form-data را که در برنامههای کاربردی زیاد مورد استفاده قرار میگیرند، بررسی کرد.
کار با URL Encoding
عموما URL encoding در دو قسمت آدرس درخواستی به سرور و یا حتی بدنهی درخواست ارسالی تنظیم میشود. MIME type آن نیز application/x-www-form-urlencoded است و اطلاعات آن شامل یکسری جفت کلید/مقدار است. برای متدهای ارسال از نوع GET و DELETE، اطلاعات آن در انتهای آدرس درخواستی و برای سایر حالات در بدنهی درخواست ذکر میشوند.
در جیکوئری با استفاده از متد param آن میتوان یک شیء جاوا اسکریپتی را به معادل URL-encoded string آنها تبدیل کرد:
$.param({ key1: 'some value', 'key 2': 'another value' });
key1=some+value&key+2=another+value
$.ajax({ method: 'POST', url: '/user', data: { name: 'VahidN', address: 'Address 1', phone: '555-555-5555' } });
برخلاف جیکوئری در حین کار با روشهای جاوا اسکریپتی خالص این encoding باید به صورت دستی و صریحی انجام شود. برای این منظور دو متد استاندارد encodeURI و encodeURIComponent در جاوا اسکریپت مورد استفاده قرار میگیرند. هدف متد encodeURI اعمال آن بر روی یک URL کامل است و یا کلید/مقدارهای جدا شدهی توسط & مانند first=Vahid&last=N. اما متد encodeURIComponent صرفا جهت اعمال بر روی یک تک مقدار طراحی شدهاست.
به این ترتیب معادل جاوا اسکریپتی قطعه کد جیکوئری فوق به صورت زیر است:
var xhr = new XMLHttpRequest(); var data = encodeURI('name=VahidN&address=Address 1&phone=555-555-5555'); xhr.open('POST', '/user'); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.send(data);
روش انجام اینکار توسط fetch API به صورت زیر است:
var data = encodeURI('name=VahidN&address=Address 1&phone=555-555-5555'); fetch('/user', { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: data });
function param(object) { var encodedString = ''; for (var prop in object) { if (object.hasOwnProperty(prop)) { if (encodedString.length > 0) { encodedString += '&'; } encodedString += encodeURI(prop + '=' + object[prop]); } } return encodedString; }
کار با JSON Encoding
در عمل JSON نمایش رشتهای یک شیء جاوا اسکریپتی است و هدف آن سهولت نقل و انتقالات این اشیاء به سرور و برعکس است. برخلاف حالت application/x-www-form-urlencoded که اطلاعات آن مسطح است، حالت application/json امکان ارسال اطلاعات سلسله مراتبی را نیز میسر میکند (مانند مثال زیر که phone آن دیگر مسطح نیست و خود آن نیز یک شیء جاوا اسکریپتی است).
در جیکوئری برای ارسال اشیاء جاوا اسکریپتی JSON Encoded به سمت سرور از روش زیر استفاده میشود:
$.ajax({ method: 'POST', url: '/user', contentType: 'application/json', data: JSON.stringify({ name: 'VahidN', address: 'Address 1', phone: { home: '555-555-5555', mobile: '444-444-4444' } }); });
پیاده سازی همین مثال با جاوا اسکریپت خالص و XMLHttpRequest استاندارد به صورت زیر است:
var xhr = new XMLHttpRequest(); var data = JSON.stringify({ name: 'VahidN', address: 'Address 1', phone: { home: '555-555-5555', mobile: '444-444-4444' } }); xhr.open('POST', '/user'); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.send(data);
در این حالت اگر خروجی سرور نیز JSON باشد، روش دریافت و پردازش آن به صورت زیر است:
var xhr = new XMLHttpRequest(); xhr.open('GET', '/user/1'); xhr.onload = function() { var user = JSON.parse(xhr.responseText); // do something with this user JavaScript object }; xhr.send();
اگر از مرورگر IE صرفنظر کنیم، تمام مرورگرهای دیگر دارای خاصیتی به نام xhr.response نیز هستند که نیاز به تبدیل و Parse دستی رشتهی دریافتی را حذف میکند؛ از این جهت که این خاصیت حاوی شیء جاوا اسکریپتی معادل بدنهی response دریافتی از سمت سرور است. البته با این شرط که سرور، Content-Type مساوی application/json را برای response تنظیم کرده باشد.
و روش انجام این عملیات توسط fetch API به صورت زیر است:
fetch('/user', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ name: 'VahidN', address: 'Address 1', phone: { home: '555-555-5555', mobile: '444-444-4444' } }); });
و یا اگر بخواهیم اطلاعات JSON دریافتی از سمت سرور را در اینجا پردازش کنیم، روش کار به صورت زیر است:
fetch('/user/1').then(function(response) { return response.json(); }).then(function(userRecord) { // do something with this user JavaScript object });
همین مثال را اگر بخواهیم توسط ECMAScript 2016 و Arrow functions آن بازنویسی کنیم، به قطعه کد زیر میرسیم:
fetch('/user/1') .then(response => response.json()) // Transform the data into json .then(userRecord => { // do something with this userRecord object });
کار با Multipart Encoding
نوع دیگری از encoding که بیشتر با فرمهای HTML بکار میرود، multipart/form-data نام دارد:
<form action="my/server" method="POST" enctype="multipart/form-data"> <label>First Name: <input name="first"> </label> <label>Last Name: <input name="last"> </label> <button>Submit</button> </form>
-----------------------------1686536745986416462127721994 Content-Disposition: form-data; name="first" Vahid -----------------------------1686536745986416462127721994 Content-Disposition: form-data; name="last" N -----------------------------1686536745986416462127721994--
روش ارسال اطلاعات با این نوع encoding خاص به سمت سرور توسط متد ajax جیکوئری به صورت زیر است:
var formData = new FormData(); formData.append('name', 'VahidN'); formData.append('address', 'Address 1'); formData.append('phone', '555-555-5555'); $.ajax({ method: 'POST', url: '/user', contentType: false, processData: false, data: formData });
در اینجا ذکر processData: false ضروری است. در غیراینصورت jQuery این اطلاعات را به یک URL-encoded string تبدیل میکند. همچنین با اعلام contentType: false، جیکوئری در کار مرورگر دخالت نمیکند. از این جهت که هدر ویژهی این نوع درخواستها توسط خود مرورگر تنظیم میشود و برای مثال یک چنین شکلی را دارد:
multipart/form-data; boundary=—————————1686536745986416462127721994
روش انجام اینکار با XMLHttpRequest و همچنین fetch API به صورت زیر است:
توسط استاندارد جدید Fetch API | توسط XMLHttpRequest استاندارد |
var formData = new FormData(); formData.append('name', 'VahidN'); formData.append('address', 'Address 1'); formData.append('phone', '555-555-5555'); fetch('/user', { method: 'POST', body: formData }); | var formData = new FormData(), xhr = new XMLHttpRequest(); formData.append('name', 'VahidN'); formData.append('address', 'Address 1'); formData.append('phone', '555-555-5555'); xhr.open('POST', '/user'); xhr.send(formData); |
آپلود فایلها توسط درخواستهای Ajax ایی
تنها راه آپلود فایلها در مرورگرهای قدیمی که شامل IE 9.0 هم میشود، تعریف المان <"input type="file> در داخل المان <form> و سپس submit مستقیم آن فرم است. برای رفع این مشکل در مرورگرهای پس از IE 9.0 و پشتیبانی از Ajax، جهت آپلود فایلها، استاندارد XMLHttpRequest Level 2 معرفی شدهاست. در این حالت اگر المان <input type=file> در صفحه وجود داشته باشد، روش ارسال Ajax ایی آن به سمت سرور به صورت زیر است:
توسط استاندارد جدید Fetch API | توسط XMLHttpRequest استاندارد | توسط jQuery |
var file = document.querySelector( 'INPUT[type="file"]').files[0]; fetch('/uploads', { method: 'POST', body: file }); } | var file = document.querySelector( 'INPUT[type="file"]').files[0], xhr = new XMLHttpRequest(); xhr.open('POST', '/uploads'); xhr.send(file); | var file = $('INPUT[type="file"]')[0].files[0]; $.ajax({ method: 'POST', url: '/uploads', contentType: false, processData: false, data: file }); |
یک نکته: اگر نیاز به آپلود بیش از یک فایل را داشتید و همچنین در اینجا نیاز به اطلاعات دیگری مانند سایر فیلدهای فرم نیز وجود داشت، از همان روش تعریف new FormData و افزودن اطلاعات مورد نیاز به آن استفاده کنید. امکان افزودن شیء file نیز به FormData پیش بینی شدهاست.
دانلود فایلها توسط درخواستهای Ajax ایی
پیشتر در حین بررسی JSON encoding توسط fetch API از متد ()json برای تبدیل اطلاعات دریافتی از سرور به json و بازگشت آن به صورت یک Promise استفاده کردیم:
fetch(url) .then((resp) => resp.json()) // Transform the data into json .then(function(data) { // use data object }) })
- ()clone یک کپی از response را تهیه میکند.
- ()redirect یک response جدید را با URL دیگری ایجاد میکند.
- ()arrayBuffer یک Promise را بازگشت میدهد که پس از پایان درخواست، response را به یک شیء ArrayBuffer تبدیل میکند.
- ()formData یک Promise را بازگشت میدهد که پس از پایان درخواست، response را به یک شیء FormData تبدیل میکند.
- ()blob یک Promise را بازگشت میدهد که پس از پایان درخواست، response را به یک شیء Blob تبدیل میکند.
- ()text یک Promise را بازگشت میدهد که پس از پایان درخواست، response را به string تبدیل میکند.
- ()json یک Promise را بازگشت میدهد که پس از پایان درخواست، response را به یک شیء جاوا اسکریپتی تبدیل میکند.
در اینجا متدی که میتواند برای تبدیل یک byte array بازگشتی از سرور به فایل قابل دریافت در سمت کلاینت مورد استفاده قرار گیرد، متد blob است:
function downloadBlob() { fetch('/Home/InMemoryReport') .then(function(response) { return response.blob(); }) .then(function(xlsxBlob) { var a = document.createElement("a"); document.body.appendChild(a); a.style = "display: none"; let url = window.URL.createObjectURL(xlsxBlob); a.href = url; a.download = "report.xlsx"; a.click(); window.URL.revokeObjectURL(url); }); }
ارسال درخواستهای Ajax به دومینهای دیگر (CORS)
گاهی از اوقات نیاز است اطلاعاتی را توسط درخواستهای Ajax، به سروری دیگر در دومینی دیگر ارسال و یا دریافت کرد. هرچند انجام اینکار به صورت مستقیم و خارج از مرورگر بدون مشکل قابل انجام است، اما مرورگرها برای درخواستهای جاوا اسکریپتی محدودیت «same-origin policy» را اعمال میکنند. به این معنا که XMLHttpRequest بین دومینها به صورت پیشفرض ممنوع است. برای ارسال درخواستهای مجاز و از پیش مشخص شدهی Ajax بین دومینها، تاکنون دو روش پیش بینی شدهاست:
الف) روش JSONP
«same-origin policy» از شروع ارسال درخواستی به خارج از دومین جاری، جلوگیری میکند. هرچند این مورد به درخواستهای XMLHttpRequest اعمال میشود، اما در مورد المانهایی از نوع <a>، <img> و <script> صادق نیست و آنها محدود به این سیاست امنیتی نیستند. روش «JavaScript Object Notation with Padding» و یا به اختصار JSONP از یکی از همین استثناءها جهت ارسال درخواستهایی به سایر دومینها استفاده میکند. البته نام این روش کمی غلط انداز است؛ از این جهت که در این فرآیند اصلا JSON ایی مورد استفاده قرار نمیگیرد؛ خروجی سرور در این حالت یک تابع جاوا اسکریپتی است و نه JSON.
روش انجام این نوع درخواستها را توسط جیکوئری در ذیل مشاهده میکنید:
$.ajax('http://jsonp-aware-endpoint.com/user/1', { jsonp: 'callback', dataType: 'jsonp' }).then(function(response) { // handle user info from server });
انجام اینکار بدون jQuery و در حقیقت کاری که jQuery در پشت صحنه برای ایجاد تگ script انجام میدهد، چنین چیزی است:
window.myJsonpCallback = function(data) { // handle user info from server }; var scriptEl = document.createElement('script'); scriptEl.setAttribute('src', 'http://jsonp-aware-endpoint.com/user/1?callback=myJsonpCallback'); document.body.appendChild(scriptEl);
ب) روش CORS
CORS و یا Cross Origin Resource Sharing روش مدرن و پذیرفته شدهی ارسال درخواستهای Ajax در بین دومینها است و دارای دو نوع ساده و غیرساده است. نوع سادهی آن به همراه هدر مخصوص Origin است که جهت بیان دومین ارسال کنندهی درخواست بکار میرود و تنها از encodingهای “text/plain” و “application/x-www-form-urlencoded” پشتیبانی میکند. نوع غیرسادهی آن که این روزها بیشتر بکار میرود، از نوع «preflight» است. Preflight در اینجا به این معنا است که زمانیکه درخواست Ajax ایی را به دومین دیگری ارسال کردید، پیش از ارسال، مرورگر یک درخواست از نوع OPTIONS را به سمت سرور مقصد ارسال میکند. در این حالت اگر سرور مجوز مناسبی را صادر کرد، آنگاه مرورگر اصل درخواست را به سمت آن سرور ارسال میکند. به همین جهت در این حالت به ازای هر درخواستی که در برنامه ارسال میشود، در برگهی network مرورگر، دو درخواست را مشاهده خواهید کرد. درخواست preflight از نوع OPTIONS به صورت خودکار توسط مرورگر مدیریت میشود و نیازی به کدنویسی خاصی ندارد.
مدیریت کوکیها در درخواستهای Ajax
اگر درخواست Ajax ایی را به دومین دیگری ارسال کنید، به صورت پیشفرض به همراه کوکیهای مرتبط نخواهد بود. برای رفع این مشکل نیاز است خاصیت withCredentials را به true تنظیم کنید:
توسط استاندارد جدید Fetch API | توسط XMLHttpRequest استاندارد | توسط jQuery |
fetch('http://someotherdomain.com', { method: 'POST', headers: { 'Content-Type': 'text/plain' }, credentials: 'include' }); | var xhr = new XMLHttpRequest(); xhr.open('POST', 'http://someotherdomain.com'); xhr.withCredentials = true; xhr.setRequestHeader('Content-Type', 'text/plain'); xhr.send('sometext'); | $.ajax('http://someotherdomain.com', { method: 'POST', contentType: 'text/plain', data: 'sometext', beforeSend: function(xmlHttpRequest) { xmlHttpRequest.withCredentials = true; } }); |
یک نکتهی مهم: در fetch API حتی برای درخواستهای ساده نیز کوکیها ارسال نمیشوند. در این حالت برای کار با دومین جاری و ارسال کوکیهای کاربر به سمت سرور، باید از تنظیم 'credentials: 'same-origin استفاده کرد؛ زیرا مقدار پیشفرض آن omit است.
$.get('http://site-url', function(data) { //این تابع پس از پایان کار عملیات ایجکسی در آینده فراخوانی خواهد شد });
$.get('http://site-url/0', function(data0) { // callback #1 $.get('http://site-url/1', function(data1) { // callback #2 $.post('http://site-url/2', function(data2) { // callback #3 }); }); });
روشهای زیادی برای حل این مساله ارائه شدهاست و در حال حاضر کار کردن با promiseها متداولترین روش حل مدیریت فراخوانی کدهای همزمان جاوا اسکریپتی است. برای نمونه اگر از AngularJS استفاده کنید، سرویسهای آن برای دریافت اطلاعات از سرور، از یک چنین مفهومی استفاده میکنند.
Promise در جاوا اسکریپت چیست؟
شیء Promise، نمایانگر قراردادی است که در آینده میتواند مورد قبول واقع شود، یا رد گردد. بررسی این قرارداد، تنها یکبار میتواند رخ دهد (پذیرش یا رد آن). هنگامیکه این بررسی صورت گرفت (رد یا پذیرش آن و نه هردو)، یک callback برای اطلاع رسانی فراخوانی میگردد. سپس این callback میتواند یک Promise دیگر را سبب شود. به این ترتیب میتوان Promiseها را زنجیر وار به یکدیگر متصل کرد. برای نمونه jQuery به صورت توکار از promises پشتیبانی میکند:
// returns a promise $.get('http://site-url/0') .then(function(data) { // callback 1 // returns a promise return $.get('http://site-url/1'); }) .then(function(data) { // callback 2 // returns a promise return $.post('http://site-url/2'); }) .then(function(data) { // callback 3 });
در این حالت، هر callback حداقل سه کار را میتواند انجام دهد:
الف) یک promise دیگر را بازگشت دهد. نمونه آنرا با return $.get در کدهای فوق ملاحظه میکنید.
ب) خاتمه عادی. همینجا کار promise با مقدار بازگشت داده شده، پایان مییابد.
ج) صدور یک استثناء. سبب برگشت خوردن و عدم پذیرش promise میشود.
استفاده از Promises در سایر کتابخانهها
jQuery پیاده سازی توکاری از promises دارد؛ اما سایر کتابخانهها، مانند AngularJS ایی که مثال زده شده چطور عمل میکنند؟
استانداردی به نام +Promises/A جهت یک دست سازی پیاده سازیهای promise در جاوا اسکریپت پیشنهاد شدهاست. jQuery نیمی از آنرا پیاده سازی کردهاست؛ اما کتابخانهی دیگری به نام Q Library، پیاده سازی نسبتا مفصلتری را از این استاندارد ارائه میدهد. فریم ورک AngularJS نیز در پشت صحنه از همین کتابخانه برای پیاده سازی promises استفاده میکند.
آشنایی با کتابخانه Q
استفاده مقدماتی از Q همانند مثالی است که از jQuery ملاحظه کردید.
Q.fcall(callback1) .then(callback2);
Q.fcall(function() { return $.get('http://my-url'); }) .then(callback3);
function waitForClick() { var deferred = Q.defer(); $('#okButton').click(function() { deferred.resolve(); }); $('#cancelButton').click(function() { deferred.reject(); }); return deferred.promise; } Q.fcall(waitForClick) .then(function() { // ok button was clicked }, function() { // cancel button was clicked });
در ادامه کار، اینبار متد then، دو callback را قبول میکند. Callback اول پس از پذیرش قرار داد و Callback دوم پس از رد قرار داد، فراخوانی خواهد گردید.
در رنجیره تعریف شده، اگر معادلی برای reject درنظر گرفته نشده باشد، مانند مثال ذیل:
Q.fcall(myFunction1) .then(success1) .then(success2, failure1);
همچنین اگر نتیجهی success1 با شکست مواجه شود نیز failure1 فراخوانی میگردد. اما باید درنظر داشت که شکست success2، توسط failure1 مدیریت نمیشود.
Promises در AngularJS
در AngularJS امکانات کتابخانه Q توسط پارامتری به نام q$ در اختیار سرویسهای برنامه قرار میگیرد (تزریق میشود):
var app = angular.module("myApp", []); app.factory('dataSvc', function($http, $q){ var basePath="api/books"; getAllBooks = function(){ var deferred = $q.defer(); $http.get(basePath).success(function(data){ deferred.resolve(data); }).error(function(err){ deferred.reject("service failed!"); }); return deferred.promise; }; return{ getAllBooks:getAllBooks }; }); app.controller('HomeController', function($scope, $window, dataSvc){ function initialize(){ dataSvc.getAllBooks().then(function(data){ $scope.books = data; }, function(msg){ $window.alert(msg); }); } initialize(); });
اکنون در کنترلری که قرار است از این سرویس استفاده کند، متد then کتابخانه Q را ملاحظه میکنید که دو Callback متناظر resolve و reject مدیریت promise بازگشت داده شده را به همراه دارد. اگر عملیات Ajaxایی موفقیت آمیز باشد، شیء books را مقدار دهی میکند و اگر خیر، پیامی را به کاربر نمایش خواهد داد.
پشتیبانی مرورگرهای جدید از استاندارد Promise
در حال حاضر کروم 32 و نگارشهای شبانه فایرفاکس، Promise را که جزئی از استاندارد JavaScript شدهاست، به صورت توکار و بدون نیاز به کتابخانههای جانبی، پشتیبانی میکنند.
if (window.Promise) { // Check if the browser supports Promises var promise = new Promise(function(resolve, reject) { //asynchronous code goes here }); }
if (window.Promise) { console.log('Promise found'); var promise = new Promise(function(resolve, reject) { // async if (result) { resolve(data); } else { reject('error'); } }); promise.then(function(data) { console.log('Promise fulfilled.'); }, function(error) { console.log('Promise rejected.'); }); } else { console.log('Promise not available'); }