مطالب
Angular CLI - قسمت هفتم - اجرای آزمون‌ها
پروژه‌های Angular CLI در حالت پیش فرض آن‌ها به همراه دو نوع آزمون واحد و آزمون end to end ایجاد می‌شوند. Angular CLI از Karma برای اجرای آزمون‌های واحد استفاده می‌کند و از Protractor برای اجرای آزمون‌های end to end. برای شروع می‌توان از راهنمای آن کمک گرفت:
 > ng test --help
زمانیکه دستور ng test را اجرا می‌کنیم، به صورت خودکار تمام فایل‌های spec.ts.* را یافته و آزمون‌های واحد موجود در آن‌ها را اجرا می‌کند. این نوع فایل‌های ویژه نیز به صورت خودکار، زمانیکه اجزای مختلف Angular را توسط Angular CLI ایجاد می‌کنیم، تولید می‌شوند. به علاوه دستور ng test تغییرات این فایل‌ها را تحت نظر قرار داده و در صورت نیاز، آزمون‌های واحد را مجددا و به صورت خودکار اجرا می‌کند.


یک مثال: بررسی اجرای دستور ng test

یکی از مثال‌های بررسی شده‌ی در این سری را انتخاب و یا حتی یک برنامه‌ی جدید را توسط Angular CLI ایجاد کرده و سپس دستور ng test را در ریشه‌ی این پروژه اجرا کنید. به این ترتیب برنامه به صورت خودکار کامپایل شده و سپس به صورت خودکار آزمون‌های واحد آن‌را که در فایل‌های spec.ts‌.* قرار دارند، اجرا می‌کند. در آخر نتیجه را در مرورگر گزارش می‌دهد:


همانطور که مشخص است، 3 specs, 3 failures داریم. در اینجا می‌توان بر روی لینک Spec List کلیک کرد و لیست آزمون‌های واحد موجود را مشاهده نمود:


هر کدام از عناوین ذکر شده نیز به جزئیات مشکلات آن‌ها، لینک شده‌اند. برای مثال اگر بر روی اولین مورد کلیک کنیم، خطایی مانند «'alert' is not a known element» قابل مشاهده‌است. به این معنا که برای نمونه در قسمت قبل کامپوننت alert را به صفحه اضافه کردیم:
 <alert type="success">Alert success!</alert>
اما اجرا کننده‌ی آزمون‌های واحد اطلاعاتی در مورد آن ندارد؛ از این جهت که آزمون‌های واحد به صورت ایزوله فقط همان کامپوننت خاص برنامه را آزمایش می‌کنند و کاری به وابستگی‌های آن ندارد. به همین جهت فایل src\app\app.component.spec.ts را گشوده و تغییرات ذیل را به آن اعمال کنید:
import { NO_ERRORS_SCHEMA } from '@angular/core';

describe('AppComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [
        AppComponent
      ],
  schemas: [NO_ERRORS_SCHEMA]
    }).compileComponents();
  }));
در اینجا ابتدا ماژول NO_ERRORS_SCHEMA معرفی شده و سپس به قسمت schemas معرفی گشته‌است.
پس از این تغییر، بلافاصله مجدد برنامه کامپایل شده و آزمون‌های واحد آن با موفقیت اجرا می‌شوند (با این فرض که هنوز پنجره‌ی اجرا کننده‌ی دستور ng test را باز نگه داشته‌اید):


تغییر افزودن schemas: [NO_ERRORS_SCHEMA] را باید در مورد تمام فایل‌های spec موجود تکرار کرد.


گزینه‌های مختلف دستور ng test

دستور ng test به همراه گزینه‌های متعددی است که شرح آن‌ها را در جدول ذیل مشاهده می‌کنید:

گزینه
 مخفف  توضیح
 code-coverage--  cc-   تولید گزارش code coverage که به صورت پیش فرض خاموش است. 
 colors--     به صورت پیش فرض فعال است و سبب نمایش رنگ‌های قرمز و سبز، برای آزمون‌های شکست خورده و یا موفق می‌شود. 
 single-run--  sr-   اجرای یکباره‌ی آزمون‌های واحد، بدون فعال سازی گزینه‌ی مشاهده‌ی مداوم تغییرات که به صورت پیش فرض خاموش است. 
 progress--     نمایش جزئیات کامپایل و اجرای آزمون‌های واحد که به صورت پیش فرض فعال است. 
 sourcemaps--  sm-   تولید فایل‌های سورس‌مپ که به صورت پیش فرض فعال است. 
 watch--
 w-   بررسی مداوم تغییرات فایل‌ها و اجرای آزمون‌های واحد به صورت خودکار که به صورت پیش فرض فعال است. 

بنابراین اجرا دستور ng test بدون ذکر هیچ گزینه‌ای به معنای اجرای مداوم آزمون‌های واحد، در صورت مشاهده‌ی تغییراتی در آن‌ها، به کمک Karma است.
همچنین دو دستور ذیل نیز به یک معنا هستند و هر دو سبب یکبار اجرای آزمون‌های واحد می‌شوند:
> ng test -sr
> ng test -w false


اجرای بررسی میزان پوشش آزمون‌های واحد

یکی از گزینه‌های ng test روشن کردن پرچم code-coverage است:
 > ng test --code-coverage
برای آزمایش آن دستور ذیل را در ریشه‌ی پروژه اجرا کنید (که سبب اجرای یکبار برررسی میزان پوشش آزمون‌های واحد می‌شود):
 > ng test -sr -cc
پس از اجرای این آزمون ویژه، پوشه‌ی جدیدی به نام coverage در ریشه‌ی پروژه‌ی جاری تشکیل می‌شود. فایل index.html آن‌را در مرورگر باز کنید تا بتوان گزارش تولید شده را مشاهده کرد:


کار این آزمون بررسی قسمت‌های مختلف برنامه و ارائه گزارشی است که مشخص می‌کند آیا آزمون‌های واحد نوشته شده تمام انشعابات برنامه را پوشش می‌دهند یا خیر؟ برای مثال اگر در متدی if/else دارید، آیا فقط قسمت if را پوشش داده‌اید و یا آیا قسمت else هم در آزمون‌های واحد، بررسی شده‌است.


اجرای آزمون‌های end to end

هدف از ساخت یک برنامه ... استفاده‌ی از آن توسط دیگران است؛ اینجا است که آزمون‌های end to end مفهوم پیدا می‌کنند. در آزمون‌های e2e رفتار برنامه همانند حالتی که یک کاربر از آن استفاده می‌کند، بررسی می‌شود (برای مثال باز کردن مرورگر، لاگین و مرور صفحات). برای این منظور، Angular CLI  در پشت صحنه از Protractor برای این نوع آزمون‌ها استفاده می‌کند.  
برای مشاهده‌ی راهنما و گزینه‌های مختلف مرتبط با آزمون‌های e2e، می‌توان دستور ذیل را صادر کرد:
 >ng e2e --help
البته با توجه به اینکه این دستور کار توزیع برنامه را نیز انجام می‌دهد، تمام گزینه‌های ng serve نیز در اینجا صادق هستند، به علاوه‌ی موارد ذیل:

 گزینه  مخفف توضیح
 config--  c-   به فایل کانفیگ آزمون‌های e2e اشاره می‌کند که به صورت پیش‌فرض همان protractor.conf.js واقع در ریشه‌ی پروژه‌است. 
 element-explorer--  ee-   بررسی و دیباگ protractor از طریق خط فرمان 
 serve--  s-   کامپایل و توزیع برنامه بر روی پورتی اتفاقی (حالت پیش فرض آن true است) 
 specs--  sp-   پیش فرض آن بررسی تمام specهای موجود در پروژ‌ه‌است. اگر نیاز به لغو آن باشد می‌توان از این گزینه استفاده کرد. 
 webdriver-update--  wu- به روز رسانی web driver که به صورت پیش فرض فعال است. 

بنابراین زمانیکه دستور ng e2e صادر می‌شود، به معنای کامپایل، توزیع برنامه بر روی پورتی اتفاقی و اجرای آزمون‌ها است.

از این جهت که این نوع آزمون‌ها، وابسته‌ی به جزئی خاص از برنامه نیستند، حالت عمومی داشته و فایل‌های spec آن‌ها را می‌توان در پوشه‌ی e2e واقع در ریشه‌ی پروژه، یافت. برای مثال در قسمتی از آن کار یافتن متن نمایش داده شده‌ی در صفحه‌ی اول سایت انجام می‌شود
getParagraphText() {
    return element(by.css('app-root h1')).getText();
}
و سپس در فایل spec آن بررسی می‌کند که آیا مساوی app works هست یا خیر؟
 expect(page.getParagraphText()).toEqual('app works!');

برای آزمایش آن دستور ng e2e را در ریشه‌ی پروژه صادر کنید. همچنین دقت داشته باشید که در این حالت نیاز است به اینترنت نیز متصل باشد؛ چون از chromedriver api گوگل نیز استفاده می‌کند. در غیراینصورت خطای ذیل را دریافت خواهید کرد:
 Error: getaddrinfo ENOTFOUND chromedriver.storage.googleapis.com chromedriver.storage.googleapis.com:443
مطالب
کار با SignalR Core از طریق یک کلاینت جاوا اسکریپتی
کلاینت جاوا اسکریپتی SignalR Core، بازنویسی کامل شده‌است و دیگر وابستگی به jQuery ندارد. این کلاینت از طریق npm توزیع می‌شود:
 npm install @aspnet/signalr-client --save
فایل‌های آن نیز شامل فایل‌های جاوا اسکریپتی مرتبط و همچنین Typings مورد استفاده‌ی در TypeScript است که نمونه‌ای از نحوه‌ی استفاده از این Typings را در مطلب «کار با SignalR Core از طریق یک کلاینت Angular» مطالعه کردید.


بررسی محتوای پوشه‌ی node_modules\@aspnet\signalr-client

پس از نصب بسته‌ی «aspnet/signalr-client@»، در مسیر node_modules\@aspnet\signalr-client\dist دو پوشه‌ی src و browser را خواهید یافت. پوشه‌ی src حاوی منبع کامل این کلاینت و همچنین فایل‌های Typings مخصوص تایپ‌اسکریپت است.


و پوشه‌ی browser آن شامل دو گروه فایل است:


- در اینجا گروهی از فایل‌ها، حاوی عبارت ES5 هستند و تعدادی خیر. SignalR JavaScript بر اساس ES 6 یا EcmaScript 2015 تهیه شده‌است و از مفاهیمی مانند Promises  و  arrow functions استفاده می‌کند. باید دقت داشت که تعدادی از مرورگرها مانند IE از این قابلیت‌ها پیشتیبانی نمی‌کنند. در بین این فایل‌ها، آن‌هایی که حاوی عبارت ES5 نیستند، یعنی بر اساس ES 6 تهیه شده‌اند. سایر فایل‌ها توسط قابلیت Transpile مربوط به TypeScript به ES5 ترجمه شده‌اند. به علاوه حجم این فایل‌ها نیز بیشتر می‌باشد؛ چون حاوی تعاریف وابستگی‌هایی هستند که در ES 5 وجود خارجی ندارند. بنابراین بسته به نوع مرورگر مدنظر، یکی از این دو گروه را باید انتخاب کرد؛ ES 6 برای مرورگرهای جدید و ES 5 برای مرورگرهای قدیمی.
- به علاوه در اینجا تعدادی از فایل‌ها حاوی عبارت msgpackprotocol هستند. نگارش جدید SignalR از پروتکل‌های هاب سفارشی مانند پروتکل‌های باینری نیز پشتیبانی می‌کند. همچنین حاوی یک پیاده سازی توکار از پروتکل‌های باینری بر اساس MessagePack نیز هست. چون حجم کدهای پشتیبانی کننده‌ی از این پروتکل ویژه بالا است، آن‌را به یک فایل مجزا انتقال داده‌اند تا در صورت نیاز مورد استفاده قرار گیرد. بنابراین اگر از این پروتکل استفاده نمی‌کنید، نیازی هم به الحاق آن در صفحات خود نخواهید داشت. فایل third-party-notices.txt نیز مربوط است به یادآوری مجوز استفاده‌ی از MessagePack که MIT می‌باشد.
- در هر گروه نیز، دو فایل min و معمولی قابل مشاهده‌است. فایل‌های min برای توزیع نهایی مناسب هستند و فایل‌های غیرفشرده شده برای حالت دیباگ.


استفاده از کلاینت جاوا اسکریپتی SignalR Core

برای کار با کلاینت جاوا اسکریپتی SignalR Core از همان فایل‌های موجود در پوشه‌ی node_modules/@aspnet/signalr-client/dist/browser استفاده می‌کنیم. تفاوت این کلاینت با نگارش قبلی SignalR به صورت یک ذیل است:
1) ارجاع به فایل قدیمی signalR-2.2.1.min.js با فایل جدید signalR-client-1.0.0-alpha1.js جایگزین می‌شود. اگر می‌خواهید مرورگرهای قدیمی را پشتیبانی کنید، نگارش ES5 آن‌را لحاظ کنید.
2) پروکسی‌ها با new HubConnection جایگزین شده‌اند.
3) برای ثبت callbackهای سمت کلاینت، از متد جدید on استفاده می‌شود.
4) بجای متد done مربوط به jQuery، در اینجا از متد then مربوط به ES6 کمک گرفته شده‌است.
5) کار فراخوانی متدهای هاب توسط متد invoke انجام می‌شود.


یک مثال: بازنویسی قسمت سمت کلاینت مثال «کار با  SignalR Core از طریق یک کلاینت Angular» با jQuery

هرچند کلاینت جدید SignalR Core وابستگی به jQuery ندارد، اما جهت سهولت کار با DOM، کدهای سمت کلاینت مثال قبلی را با jQuery بازنویسی می‌کنیم. تمام کدهای سمت سرور این مثال با مطلب «کار با SignalR Core از طریق یک کلاینت Angular» یکی است؛ مانند ایجاد هاب، فعالسازی SiganlR در فایل آغازین برنامه و ثبت مسیرهاب. بنابراین در اینجا، این قسمت از کدهای سمت سرور را مجددا تکرار نمی‌کنیم و تمام نکات آن یکی هستند.

برای کار با کلاینت جاوا اسکریپتی SignalR Core، اینبار دستور ذیل را در ریشه‌ی پروژه‌ی وب اجرا می‌کنیم (یا هر پروژه‌ای که قرار است مدیریت فایل‌های سمت کلاینت و Viewهای برنامه را انجام دهد):
npm init
npm install @aspnet/signalr-client --save
bower install
دستور اول یک فایل package.json خالی را ایجاد می‌کند و دستور دوم بسته‌ی جاوا اسکریپتی SiganlR Core را نصب خواهد کرد. به علاوه این وابستگی را در فایل package.json نیز ثبت می‌کند. دستور سوم نیز وابستگی‌های قید شده‌ی در فایل bower.json را نصب می‌کند.

مرحله‌ی بعدی کار، تنظیمات فایل bundleconfig.json است؛ تا تمام اسکریپت‌های مورد نیاز جمع‌آوری و یکی شوند:
[
  {
    "outputFileName": "wwwroot/css/site.min.css",
    "inputFiles": [
      "wwwroot/lib/bootstrap/dist/css/bootstrap.min.css",
      "wwwroot/css/site.css"
    ]
  },
  {
    "outputFileName": "wwwroot/js/site.min.js",
    "inputFiles": [
      "wwwroot/lib/jquery/dist/jquery.min.js",
      "wwwroot/lib/bootstrap/dist/js/bootstrap.min.js",
      "node_modules/@aspnet/signalr-client/dist/browser/signalr-client-1.0.0-alpha1-final.min.js",
      "wwwroot/lib/jquery-validation/dist/jquery.validate.min.js",
      "wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js",
      "wwwroot/lib/jquery-ajax-unobtrusive/jquery.unobtrusive-ajax.min.js",
      "wwwroot/js/site.js"
    ],
    "minify": {
      "enabled": false,
      "renameLocals": false
    },
    "sourceMap": false
  }
]
در اینجا نحوه‌ی ثبت فایل signalr-client-1.0.0-alpha1-final.min.js مبتنی بر ES 6 را مشاهده می‌کنید. اگر می‌خواهید نگارش ES 5 آن‌را ذکر کنید، از فایل signalr-clientES5-1.0.0-alpha1-final.min.js استفاده نمائید.
با توجه به خروجی‌های نهایی فایل bundleconfig.json، تنها نیاز است مداخل ذیل را به فایل layout برنامه اضافه کرد:
<link href="~/css/site.min.css" rel="stylesheet" asp-append-version="true" />
<script src="~/js/site.min.js" type="text/javascript" asp-append-version="true"></script>

مرحله‌ی بعد، تغییر نام متد send قسمت قبل به broadcastMessage است:
public class MessageHub : Hub
{
   public Task Send(string message)
   {
     return Clients.All.InvokeAsync("broadcastMessage", message);
   }
}
به این ترتیب می‌توان به تمایز بهتری بین نام callback سمت کلاینت و متد Send سمت سرور رسید. بهتر است این‌دو هم‌نام نباشند.
در ادامه یک کنترلر ساده را به نام JsClientController با View ذیل ایجاد می‌کنیم:
<form method="post"
      asp-action="Index"
      asp-controller="Home"
      data-ajax="true"
      role="form">
  <div class="form-group">
     <label label-for="message">Message: </label>
     <input id="message" name="message" class="form-control"/>
  </div>
  <button class="btn btn-primary" type="submit">Send To Home/Index</button>
  <button class="btn btn-success" id="sendmessageDirect" type="button">Send To /message hub directly</button>
</form>

<div id="discussion">
</div>
کار آن نمایش فرم ذیل است:


از اولین دکمه برای ارسال یک پیام به کنترلر Home که در آن توسط <IHubContext<MessageHub پیامی به تمام کلاینت‌ها ارسال می‌شود، استفاده شده‌است. دومین دکمه متد Send هاب را مستقیما فراخوانی می‌کند؛ با این کدهای سمت کلاینت:
@section Scripts
{
   <script type="text/javascript" asp-append-version="true">
   $(function() {
      var connection = new signalR.HubConnection('/message');
      connection.on('broadcastMessage', function (message) {
          // Add the message to the page.
          var encodedMsg = $('<div />').text(message).html();
          $('#discussion').append('<li>' + encodedMsg + '</li>');
      });

      connection.start().then(function () {
          console.log('connected.');
          $('#sendmessageDirect').click(function () {
              // Call the Send method on the hub.
              connection.invoke('send', $('#message').val());
          });
      });
   });
   </script>
}
- ابتدا یک شیء جدید signalR.HubConnection ایجاد می‌شود. این شیء به آدرس Hub تعریف شده‌ی در فایل آغازین برنامه اشاره می‌کند.
- سپس در متد on هست که مشخص می‌کنیم متد سمت کلاینتی که قرار است از سمت سرور فراخوانی شود، چه نامی دارد. نام آن‌را در این مثال broadcastMessage درنظر گرفته‌ایم. در اینجا پارامتر message از سمت سرور دریافت شده و سپس در صفحه‌ی جاری نمایش داده می‌شود.
بدیهی است متد Send می‌تواند تعداد پارامترهای بیشتری را بپذیرد و همچنین متد broadcastMessage نیز محدودیتی از لحاظ تعداد پارامتر ندارد. اگر پارامترهای بیشتری را تعریف کردید، در همینجا باید قید شوند.
- در ادامه کار شروع این اتصال آغاز می‌شود. در متد then هست که باید کار اتصال دکمه‌ی sendmessageDirect صورت گیرد. چون عملیات اتصال ممکن است زمانبر باشد و connection ارسالی هنوز آغاز نشده باشد. در اینجا نحوه‌ی فراخوانی مستقیم متد Send سمت سرور را با یک پارامتر ملاحظه می‌کنید. این متد نیز می‌تواند بر اساس امضای متد Send سمت سرور، تعداد پارامترهای بیشتری را قبول کند.


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید: SignalRCore2WebApp02.zip
برای اجرا آن باید این دستورات را به ترتیب وارد کنید:
dotnet restore
npm install
npm install -g bower
bower install
dotnet watch run
مطالب
پیاده سازی PWA در Angular
امروزه طراحی اپلیکیشن‌های موبایل بخش زیادی از جامعه را در برگرفته است و روز به روز در حال توسعه میباشند. موازی با رشد روز افزون و نیاز بیشتر به این اپلیکیشن‌ها فریمورک‌های زیادی نیز  ابداع شده اند. از جمله این فریم ورک‌ها می‌توان به موارد زیر اشاره کرد:

Ionic  , react native , flutter , xamarin ….

دیگر لازم نیست برای طراحی اپلیکیشن خود حتما از زبان‌های native  استفاده کنید. بیشتر فریم ورک‌های معرفی شده جاوا اسکریپتی میباشند.

 
مزایای نوشتن یک اپلیکیشن با  فریم ورک‌ها:

1-  نوشتن کد به مراتب آسانتر است.
۲- چون اکثر فریم ورک‌ها، فریم ورک‌های جاوا اسکریپتی هستند، سرعت بالایی هنگام run کردن برنامه دارند ولی در build آخر و خروجی نهایی بر روی پلتفرم، این سرعت کندتر می‌شود.
۳- cossplatform بودن. با طراحی یک اپلیکیشن برای یک پلتفرم می‌توان همزمان برای پلتفرم‌های دیگر خروجی گرفت.


معایب:

۱- برنامه نویس را محدود  میکند.
۲- سرعت اجرای پایینی دارد.
۳- حجم برنامه به مراتب بالاتر میباشد.
۴- از منابع سخت افزاری بیشتری استفاده می‌کند.

اگر شما هم میخواهید از فریم ورک‌ها برای طراحی اپلیکیشن استفاده کنید در اینجا  می‌توانید اطلاعات بیشتری را درباره هر کدام از فریم ورک‌ها ببینید. هر کدام از این فریم ورک‌ها دارای مزایا معایب و همچنین رزومه کاری خوب می‌باشند. بیشتر فریم ورک‌های بالا در رندر آخر، همان کد native را تولید می‌کنند؛ مثلا اگر برنامه‌ای را با react native بنویسید، میتوانید همان برنامه را بر روی اندروید استودیو و کد native بالا بیارید. توضیحات  بالا مقایسه فریم ورک‌های Cross Platform با زبان‌های native بود. اما در این مقاله قصد آشنایی با تکنولوژی جدیدی را داریم که شما را قادر می‌سازد یک وب اپلیکیشن را با آن طراحی کنید.


 pwa چیست؟

pwa مخفف Progressive Web Apps است و یک تکنولوژی برای طراحی وب اپلیکیشن‌های تحت مرورگر میباشد. شما با pwa میتوانید اپلیکیشن خود را بر روی پلتفرم‌های مختلفی اجرا کنید، طوری که کاربران متوجه نشوند با یک صفحه‌ی وبی دارند کار میکنند. شاید شما هم فکر کنید این کار را هم میتوان با html ,css و responsive کردن صفحه انجام داد! ولی اگر بخواهید کاربر متوجه استفاده‌ی از یک صفحه‌ی وبی نشود، باید حتما از pwa استفاده کنید! برای مثال یک صفحه‌ی وبی معمولی حتما باید در بستر اینترنت اجرا شود، ولی با pwa با یکبار وصل شدن به اینترنت و کش کردن داده‌ها، برای بار دوم دیگر نیازی به اینترنت ندارد و میتواند به صورت offline  کار کند. شما حتی با pwa می‌توانید اپلیکیشن را در background اجرا کنید و notification ارسال کنید.


مزیت pwa

۱- یکی از مزیت‌های مهم pwa، حالت offline آن میباشد که حتی با قطع اینترنت، شما میتوانید همچنان با اپلیکیشن کار کنید.
۲- با توجه به اینکه شما در حقیقت با یک صفحه‌ی وبی کار میکنید، دیگر نیازی به دانلود و نصب ندارید.
۳- امکان به‌روز رسانی کردن، بدون اعلام کردن نسخه جدید.
۴- از سرعت بسیار زیادی برخوردار است.
۵- چون pwa از پروتکل https استفاده می‌کند، دارای امنیت بالایی میباشد.


معایب

۱- محدود به مرورگر می‌باشد.
۲- هرچند امروزه اکثر مرورگر‌ها pwa را پشتیبانی میکنند، ولی در بعضی از مرورگر‌ها و مرورگر‌های با ورژن پایین، pwa پشتیبانی نمیشود.
3- نمیتوان یک برنامه‌ی مبتنی بر os را نوشت و محدود به مرورگر میباشد


چند نکته درباره pwa

1- برای pwa  لزومی ندارد حتما از فریم ورک‌های spa استفاده کنید. شما از هر فریم ورک Client Side ای می‌توانید استفاده کنید.
۲- چون صفحات شما در پلتفرم‌های مختلف و با صفحه نمایش‌های مختلفی اجرا می‌شود، باید صفحات به صورت کاملا responsive شده طراحی شوند.
۳- باید از پروتکل https استفاده کنید.

ما در این مقاله از فریم ورک angular  استفاده  خواهیم کرد.

قبل از شروع، با شیوه کار pwa آشنا خواهیم شد. یکی از قسمت‌های مهم  Service Worker ،pwa میباشد که از جمله کش کردن، notification فرستادن و اجرای پردازش‌ها در پس زمینه را بر عهده دارد.


با توجه به شکل بالا، Service Worker مانند یک لایه‌ی نرم افزاری مابین کلاینت و سرور قرار دارد که درخواست‌های داده شده از کلاینت را در صورت اتصال به اینترنت، ارسال کرده و مجددا response را برگشت میدهد‌. اگر به هر دلیلی اینترنت قطع باشد، درخواست به صورت آفلاین به کش مرورگر که توسط proxy ساخته شده است، فرستاده میشود و response را برگشت میدهد.


چند نکته  در رابطه با Service Worker

- نباید برای  نگهداری داده global از Service Worker استفاد کرد. برای استفاده از داده‌های Global میتوان از Local Storage یا IndexedDB استفاده کرد.
- service worker  به dom دسترسی ندارند.


 سناریو

 قبل از شروع، سناریوی پروژه را تشریح خواهیم کرد. رکن اصلی یک برنامه‌ی وب، UI آن میباشد و قصد داریم کاربران را متوجه کار با یک صفحه‌ی وبی نکنیم. قالبی که ما در این مثال در نظر گرفتیم خیلی شبیه به یک اپلیکیشن پلتفرم اندرویدی میباشد. یک کاربر با کشیدن منوی کشویی میتواند گزینه‌های خود را انتخاب نمایند. اولین گزینه‌ای که قصد پیاده سازی آن را داریم، ثبت کاربران میباشد. بعد از ثبت کاربران در یک Component جدا، کاربران را در یک جدول نمایش خواهیم داد.


 قبل از شروع لازم است به چند نکته زیر توجه کرد

1-  سرویس crud را به صورت کامل در پروژه قرار خواهیم داد، ولی چون از حوصله‌ی مقاله خارج است، فقط ثبت کاربران و نمایش کاربران را پیاده سازی خواهیم کرد.
2 - هر اپلیکیشنی قطعا نیاز به یک وب سرویس دارد که واسط بین دیتابیس و اپلیکیشن باشد. ولی ما چون در این مثال به عنوان front end کار قصد طراحی  اپلیکیشن را داریم، برای حذف back end از firebase استفاده خواهیم کرد و مستقیما از انگولار به دیتابیس firebase کوئری خواهیم زد.


 شروع به کار

پیش نیاز‌های یک پروژه‌ی انگیولاری را بر روی سیستم خود فراهم کنید. ما در این مثال از یک template آماده انگیولاری استفاده خواهیم کرد. پس برای اینکه با جزئیات و طراحی ui درگیر نشویم، از لینک github پروژه را دریافت کنید.
سپس وارد root پروژه شوید و با دستور زیر پکیج‌های پروژه را نصب کنید:
 npm install
پس از نصب پکیج‌ها، با دستور ng serve، پروژه را اجرا کنید.

پس از اجرا باید خروجی بالا را داشته باشید. می‌خواهیم یکی از قابلیت‌های pwa را بررسی کنیم. بر روی صفحه کلیک راست کرده و وارد inspect شوید. وارد تب Application  شوید و Service Worker را انتخاب کنید.
 


قبل از اینکه کاری را انجام دهید، چند بار صفحه را refresh کنید! صفحه بدون هیچ تغییری refresh میشود. اینبار گزینه‌ی offline را فعال کنید و مجددا صفحه را refresh کنید.
این بار صفحه No internet به معنی قطع بودن اینترنت نمایش داده میشود و شما دیگر نمی‌توانید بر روی اپلیکیشن خود فعالیتی داشته باشید! 


نصب pwa  بر روی پروژه

برای اضافه کردن pwa به پروژه وارد ریشه‌ی پروژه شوید و دستور زیر را وارد کنید:
 ng add @angular/PWA
پس از  اجرای دستور بالا، تغییراتی در پروژه لحاظ میشود از جمله در فایل‌هایindex.html,app-module,angular.json، همچنین یک پوشه‌ی جدید به نام icons در assets و دو فایل جیسونی زیر به روت پروژه اضافه شده‌اند:
 Manifest.json : اگر محتویات فایل را مشاهده کرده باشید، شامل تنظیمات فنی وب اپلیکیشن می‌باشد؛ از جمله Home Screen Icon و نام وب اپلیکیشن و سایر تنظیمات دیگر.
 Nsgw-config.json : این فایل نسبت به فایل manifest فنی‌تر میباشد و بیشتر به کانفیگ مد آفلاین و کش کردن مرتبط میشود. در ادامه با این فایل بیشتر کار داریم.

بعد از نصب pwa  بر روی پروژه، همه تنظیمات به صورت خودکار انجام می‌شود. البته می‌توانید تنظیمات مربوط به کش کردن داده‌ها را به صورت پیشرفته‌تر کانفیگ کنید.


  اجرا کردن وب اپلیکیشن

 برای اجرا کردن و نمایش خروجی از وب اپلیکیشن، ابتدا باید از پروژه build گرفت. با استفاده از دستور زیر از پروژه خود build بگیرید:
 ng build -- prod
سپس  با دستور زیر وارد پوشه‌ای که فایل‌های Build  شده در آن قرار دارند، شوید:
 cd dist/Web Application  Pwa
بعد با دستور زیر برنامه را اجرا کنید:
 Http-server -o
اگر چنانچه با دستور بالا به خطا برخوردید، با دستور زیر پکیج npm آن را نصب کنید:
 npm i http-server
اگر تا به اینجای کار درست پیش رفته باشید، پروژه را بدون هیچ تغییری مشاهده خواهیم کرد. inspect گرفته و وارد تب Application شوید. اینبار گزینه‌ی offline را فعال کنید و مجددا صفحه را refresh کنید! اینبار برنامه بدون هیچ مشکلی کار میکند. حتی شما میتوانید در کنسول، برنامه را به صورت کامل stop کنید.


برسی فایل Nsgw-config.json 

وارد فایل Nsgw-config.json شوید: 

"$schema": "./node_modules/@angular/service-worker/config/schema.json",
  "index": "/index.html",
  "assetGroups": [
    {
      "name": "app",
      "installMode": "prefetch",
      "resources": {
        "files": [
          "/favicon.ico",
          "/index.html",
          "/*.css",
          "/*.js"
        ]
      }
    }, {
      "name": "assets",
      "installMode": "lazy",
      "updateMode": "prefetch",
      "resources": {
        "files": [
          "/assets/**",
          "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"
        ]
      }

    }
  ],
  "dataGroups": [
    {
      "name": "api-performance",
      "urls": [
        "https://api/**"
      ],
      "cacheConfig": {
        "strategy": "performance",
        "maxSize": 100,
        "maxAge": "3d"
      }
    }
  ]
}
 این فایل مربوط به کش کردن داده‌ها میباشند و میتواند شامل چند object زیر باشد. مهمترین آنها را بررسی خواهیم کرد:
  ۱- assetGroups : کش کردن اطلاعات مربوط به اپلیکیشن
  ۲- Index : کش کردن  فایل مربوط به index.html
  ۳-  assetGroups : کش کردن فایل‌های مربوط به asset، شامل فایل‌های js، css  و  غیره
  ۴- dataGroups : این object مربوط به وقتی است که برنامه در حال اجرا است. میتوان داده‌های در حال اجرای اپلیکیشن را کش کرد. داده‌ی در حال اجرا میتواند شامل فراخوانی apiها باشد. برای مثال فرض کنید شما در حالت کار کردن online با اپلیکیشن، لیستی از اسامی کاربران را از api گرفته و نمایش میدهید. وقتی دفعه‌ی بعد در حالت offline  اپلیکیشن را باز کنیم، اگر api را کش کرده باشیم، اپلیکیشن داده‌ها را از کش فراخوانی میکند. این عمل درباره post کردن داده‌ها هم صدق میکند.

خود dataGroups شامل چند object زیر میباشد:
  ۱- name :  یک نام انتخابی برای Groups  میباشد.
 ۲- urls  : شامل آرایه‌ای از آدرس‌ها میباشد. میتوان آدرس یک دومین را همراه با کل apiها به صورت زیر کش کرد:
"https://api/**"
۳- cacheConfig  : که شامل
  ۱- maxSize  : حداکثر تعداد کش‌های مربوط به Groups .
  ۲- maxAge  : حداکثر  lifetime مربوط به کش.
  ۳- strategy  : که میتواند یکی از مقادیر freshness به معنی Network-First یا performance  به معنی Cache-First باشد.


 پیاده سازی پیغام نمایش به‌روزرسانی جدید وب اپلیکیشن

در اپلیکیشن‌های native  وقتی به‌روزرسانی جدیدی برای app اعلام میشود، در فروشگاهای اینترنتی پیغامی مبنی بر به‌روزرسانی جدید app برای کاربران ارسال میشود که کاربران میتوانند app خود را به‌روزرسانی کنند. ولی در pwa تنها با یک رفرش صفحه میتوان اپلیکیشن را به جدیدترین امکانات به‌روزرسانی کرد! برای اینکه بتوانیم با هر تغییر، پیغامی را جهت به‌روزرسانی نسخه یا هر  پیغامی دیگری را نمایش دهیم، از کد زیر استفاده میکنم:
if(this.swUpdate.isEnabled)
    {
      this.swUpdate.available.subscribe(()=> {

        if(confirm("New Version available.Load New Version?")){
          window.location.reload();
        }
      })
    }


وصل شدن به دیتابیس

برای اینکه بتوانیم یک وب اپلیکیشن کامل را طراحی کنیم، قطعا نیاز به دیتابیس داریم. برای دیتابیس از firebase استفاده میکنیم. قبل از شروع، وارد سایت firebase  شوید. پس از لاگین، بر روی Add Project کلیک کنید. بعد از انتخاب نام مناسب، create Project را انتخاب کنید. ساختار دیتابیس‌های firebase  شبیه nosqlها می‌باشد و بر پایه‌ی json کار میکنند. پس از ساخت پروژه و دریافت کد جاواسکریپتی زیر شروع به طراحی فیلد‌های دیتابیس خواهیم کرد.


در منوی سمت چپ، بر روی database کلیک کنید و یک دیتابیس را در حالت test mode  ایجاد نماید. سپس یک collection را به نام user ایجاد کرده و فیلد‌های زیر را به آن اضافه کنید:
Age :number
Fullname :string
Mobile : string
مرحله‌ی بعد، config پروژه‌ی انگیولاری میباشد. وارد  vscode شوید و با دستور زیر پکیج firebase را اضافه کنید:
  npm install --save firebase @angular/fire
سپس وارد appmodule شوید و آن‌را به صورت زیر کانفیگ کنید:

@NgModule({
  declarations: [AppComponent, RegisterComponent,AboutComponent, UserListComponent],
  imports: [
    BrowserModule,
    FormsModule,
    HttpClientModule,
    HttpClientModule,
    SharedModule,
    AppRoutingModule,
    AngularFireModule.initializeApp(environmentFirebase.firebase),
    AngularFireDatabaseModule,
    ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
  ],
  providers: [FirebaseService],
  bootstrap: [AppComponent]
})
export class AppModule {}
خط ۱۰ مربوط به pwa است که هنگام نصب pwa اضافه شده‌است و خط ۸ و مربوط به apiKey فایربیس میباشد. می‌شود گفت environmentFirebase.firebase شبیه  connection string می‌باشد که به صورت زیر کانفیگ شده است:
export const environment ={
   production: true,
   firebase: {
     apiKey: "AIz×××××××××××××××××××××××××××××××××8",
     authDomain: "pwaangular-6c041.firebaseapp.com",
     databaseURL: "https://pwaangular-6c041.firebaseio.com",
     projectId: "pwaangular-6c041",
     storageBucket: "pwaangular-6c041.appspot.com",
     messagingSenderId: "545522081966"
   }

}

FirebaseService در قسمت providers مربوط به سرویس crud میباشد. اگر وارد فایل مربوطه شوید، چند عمل اصلی به صورت زیر در آن پیاده سازی شده است:

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import * as firebase from 'firebase';
import { AngularFireDatabase } from '@angular/fire/database';
import { HttpClient } from '@angular/common/http';
import { map } from 'rxjs/operators';
import { ThrowStmt } from '@angular/compiler';
@Injectable({
  providedIn: 'root'
})
export class FirebaseService {

  ref = firebase.firestore().collection('users');
  constructor(public db: AngularFireDatabase,public _http:HttpClient) { 
 
  }

  getUsers(): Observable<any> {
    return new Observable((observer) => {
      this.ref.onSnapshot((querySnapshot) => {
        let User = [];
        querySnapshot.forEach((doc) => {
          let data = doc.data();
          User.push({
            key: doc.id,
            fullname: data.fullname,
            age: data.age,
            mobile: data.mobile
          });
        });
        observer.next(User);
      });
    });
  }

  getUser(id: string): Observable<any> {
    return new Observable((observer) => {
      this.ref.doc(id).get().then((doc) => {
        let data = doc.data();
        observer.next({
          key: doc.id,
          title: data.title,
          description: data.description,
          author: data.author
        });
      });
    });
  }

  postUser(user): Observable<any> {
    return new Observable((observer) => {
      
      this.ref.add(user).then((doc) => {
        observer.next({
          key: doc.id,
        });
      });
    });
  }

  updateUser(id: string, data): Observable<any> {
    return new Observable((observer) => {
      this.ref.doc(id).set(data).then(() => {
        observer.next();
      });
    });
  }

  deleteUser(id: string): Observable<{}> {
    return new Observable((observer) => {
      this.ref.doc(id).delete().then(() => {
        observer.next();
      });
    });
  }

getDataOnApi(){
  return this._http.get('https://site.com/api/General/Getprovince')
  .pipe(
      map((res: Response) => {
return res;

      })
  );
}

getOnApi(){
  return  this._http.get("https://site.com/api/General/Getprovince",).pipe(
    map((response:any) => {
       
return  response
    } )
    );
}
}
اگر دقت کرده باشید، خیلی شبیه به کد نویسی سمت سرور میباشد.

با دستورات زیر میتوانید مجددا پروژه را اجرا کنید:
ng build --prod
cd dist/Pwa-WepApp
Http-server -o


تست وب اپلیکیشن بر روی پلتفرم‌های مختلف

برای اینکه بتوانیم خروجی وب اپلیکیشن را بر روی پلتفرم‌های مختلفی تست کنیم، میتوانیم آن را آپلود کرده و مثل یک سایت اینترنتی، با وارد کردن دومین، وارده پروژه شد. ولی کم هزینه‌ترین راه، استفاده از ابزار ngrok میباشد. میتوانید توسط این مقاله پروژه خودتان در لوکال بر روی https سوار کنید.

نکته! توجه کنید apiهای مربوط به firebase را نمیتوان کش کرد.


کد‌های مربوط به این قسمت را میتوانید از این repository دریافت کنید .
اشتراک‌ها
کتابخانه perfect-scrollbar

Minimalistic but perfect custom scrollbar plugin  Demo

perfect means...

  • There should be no css change on any original element.
  • The scrollbar should not affect the original design layout.
  • The design of the scrollbar should be (nearly) fully customizable.
  • If the size of the container or the content changes, the scrollbar size and position should be able to change.
  • New! It should work with vanilla JavaScript and major tools like NPM or Browserify.
کتابخانه perfect-scrollbar
مطالب
Ef 6 و Ngen : شروعی سریعتر برای برنامه های مبتنی بر Entity Framework
تولید کد Native زمانی اتفاق می‌افتد که کامپایلر JIT، کد اسمبلی‌های MSIL را به کدهای Native در ماشین محلی کامپایل می‌کند و این عمل بلافاصله قبل از اجرای متد برای اولین بار اتفاق می‌افتد. این کد به صورت موقتی بوده و در حافظه‌ای که برای پردازش در نظر گرفته شده ذخیره می‌شود و در پایان هر پردازش توسط سیستم عامل ویرایش می‌شود. کد Native به ازای هر بار شروع یک پردازش تولید می‌شود. ابزار Native Image Generator یا همان Ngen اقدام به تولید کد Native با استفاده از کامپایلر JIT نموده و آن را در هارد دیسک ذخیره می‌نماید. زمانیکه برنامه نیازمند یک اسمبلی CLR است، به جای بارگذاری خود اسمبلی، ایمیج کد Native آن بارگذاری می‌شود. به این نکته نیز توجه داشته باشید که CLR اطلاعاتی در مورد اینکه کدام اسمبلی، ایمیج کد Native است و این ایمیج در کجا و در چه زمانی تهیه شده است، دارد. کد Native باعث بهبود استفاده از حافظه می‌شود، زمانیکه یک اسمبلی بین پروسس‌ها به اشتراک گذاشته شده‌است. تا قبل از EF6 کتابخانه‌های هسته‌ای EF در زمان اجرا جزئی از دات نت فریمورک بودند و تولید کد Native آنها به صورت اتوماتیک انجام می‌شد. اما از نسخه 6، تمامی این کتابخانه‌ها در داخل پکیج Nuget آن ترکیب شده‌اند . پس برای تولید کد Native مربوط به فایل EntityFramework.dll نیازمند ابزار Ngen هستیم.
1- ابتدا یک برنامه‌ی ساده کنسول ویندوز ساخته و از Package Manager Console دستور Install-package entityframework را اجرا نموده تا پکیج Ef به برنامه اضافه گردد.
using System;
using System.Data.Entity;

namespace UsingNgen
{

    public class NgenDbContex : DbContext
    { }

    class Program
    {
        static void Main()
        {
            var nGenCtx = new NgenDbContex();
            Console.WriteLine("Press a key to exit...");
            Console.ReadKey();
        }
    }


}
حال کد ساده بالا را به برنامه اضافه می‌کنیم و برنامه را Build میکنیم.
2- برای ثبت جزئیات اجرای برنامه از ابزار Windows Performance Recorder که جزئی از ویندوز می‌باشد، استفاده می‌کنیم. کافیست عبارت WPR را در نوار جستجوی ویندوز تایپ کنید تا این ابزار در دسترس قرار گیرد. 



برای ضبط جزئیات، روی دکمه‌ی Start کلیک کنید و به محل ذخیره‌ی فایل اجرایی حاصل از Build ویژوال استودیو رفته و آن را اجرا کنید. بعد از اتمام اجرا، جزئیات را ذخیره نمایید.

بعد از ذخیره فایل، در پنجره بالا دکمه‌ای به نام Open in WPA ظاهر می‌شود. WPA مخفف Windows Performance Analyzer می‌باشد. آن را کلیک کنید تا محیط آنالایزر باز شود.

حال در سمت چپ این پنجره انواع آنالایزرها را مشاهده می‌کنید. روی آنالایزر Computation کلیک کنید و از زیرمجموعه‌ی آن، CPU Usage را انتخاب کنید. آمار مربوط به برنامه خودمان را در تصویر بالا مشاهده می‌کنید. کل برنامه 164 میلی ثانیه زمان برده و فایل Clr.dll حدود 47 میلی ثانیه و یک فایل clrjit.dll نیز برای تولید کد JIT وجود دارد. حال برای تسریع در عمل شروع، از تکنیک Ngen به صورت زیر استفاده می‌کنیم.

3- دوباره به نوار جستجوی ویندوز رفته و ابزار Developer Command Prompt for VsXXXX را با امتیاز دسترسی از نوع Admin اجرا کنید. XXXX نسخه‌ی ویژوال استودیو می‌باشد.

حال به محل ذخیره فایل اجرایی برنامه رفته و دستور Ngen Install EntityFramework.dll را تایپ کنید تا یک ایمیج کد Native از entityframework.dll ساخته شود. دوباره ابزار Windows Performance Recorder را لود کرده و روی دکمه Start کلیک کنید و فایل اجرایی برنامه را اجرا نمایید. پس از اتمام عملیات ثبت جزئیات، آن را در Windows Performance Analyzer باز نمایید.

همانطور که مشاهده می‌کنید کل برنامه ما 89 میلی ثانیه زمان برده و Clr.dll 29 ثانیه و به جای clrjit.dll فایل EntityFramework به صورت native تولید شده است.

مطالب
تهیه بک آپ‌های خودکار از SQL Server Express

SQL Server express edition نگارش مجانی و ساده شده‌ی اس کیوال سرور است. این نگارش مجانی فاقد SQL Server agent برای زمان بندی انجام امور تکراری، برای مثال تهیه بک آپ‌های خودکار است. این مورد در کل ایرادی محسوب نمی‌شود زیرا می‌توان این عملیات را با استفاده از سیستم استاندارد scheduled tasks ویندوز نیز پیاده سازی کرد.
برنامه خط فرمان سورس بازی به نام ExpressMaint موجود است که می‌تواند از دیتابیس‌های اس کیوال سرور اکسپرس (و غیر اکسپرس) بک آپ تهیه کند. فقط کافی است این برنامه را به عنوان یک scheduled task ویندوز معرفی کنیم تا در زمان‌های تعیین شده در مکان‌هایی مشخص، بک آپ تهیه کند. همچنین این برنامه فایل‌های بک آپ تهیه شده را نیز تعیین اعتبار می‌کند.
با پارامترهای خط فرمان آن در این‌جا می‌توانید آشنا شوید. خلاصه کاربردی آن را به صورت چند دستور در ادامه مرور خواهیم کرد.

الف) یک فایل bat را با محتوای زیر درست کنید :

C:\backup\expressmaint.exe -S (local)\sqlexpress -D ALL_USER -T DB -R C:\backup -RU WEEKS -RV 2 -B C:\backup -BU DAYS -BV 2 -V -C
توضیحات: در این فایل bat ، مسیر فایل اجرایی برنامه حتما باید دقیقا ذکر شود و گرنه scheduled task ویندوز درست کار نخواهد کرد. همچنین instance اس کیوال سرور اکسپرس در اینجا (local)\sqlexpress فرض شده است. این دستور از تمامی دیتابیس‌های غیرسیستمی در مسیر C:\backup بک آپ می‌گیرد (به ازای هر دیتابیس یک پوشه مجزا درست خواهد کرد و هر فایل را بر اساس تاریخ و ساعت مشخص می‌سازد). همچنین لاگ عملیات را نیز در همان پوشه تهیه می‌کند (نتیجه اعتبار سنجی صورت گرفته بر روی بک آپ‌های تهیه شده در این فایل‌ها ثبت می‌شود). مطابق پارامترهای بکار گرفته شده، بک آپ‌های قدیمی‌تر از دو روز به صورت خودکار حذف شده و لاگ فایل‌ها به مدت 2 هفته نگهداری می‌شوند.

ب) برای اجرای زمان بندی شده‌ی این فایل bat تهیه شده، دستورات زیر را در خط فرمان اجرا کنید (فرض بر این است که فایل bat تهیه شده در مسیر مشخص شده C:\backup\backup.bat قرار دارد) :

AT 23:30 /EVERY:m,t,w,th,f,s,su C:\backup\backup.bat
AT 11:30 /EVERY:m,t,w,th,f,s,su C:\backup\backup.bat
به این صورت ویندوز هر روز، دوبار در طول روز از کلیه دیتابیس‌ها به صورت خودکار بک آپ تهیه می‌کند.

روشی که در این‌جا ذکر شد منحصر به نگارش express نیست و با کلیه نگارش‌های SQL Server سازگار است.


مطالب
React 16x - قسمت 32 - React Hooks - بخش 3 - نکات ویژه‌ی برقراری ارتباط با سرور
در قسمت‌های 22 تا 25 این سری، روش برقراری ارتباط با سرور را در برنامه‌های React، توسط کتابخانه‌ی معروف Axios، بررسی کردیم. در این قسمت می‌خواهیم همان نکات را زمانیکه قرار است از کامپوننت‌های تابعی، به همراه useState hook و useEffect hook استفاده کنیم، مرور نمائیم.


برپایی پیش‌نیازها

در اینجا نیز از همان برنامه‌ای که در قسمت 30، برای بررسی مثال‌های React hooks ایجاد کردیم، استفاده خواهیم کرد. فقط در آن، کتابخانه‌ی Axios را نیز نصب می‌کنید. به همین جهت در ریشه‌ی پروژه‌ی React این قسمت، دستور زیر را در خط فرمان صادر کنید:
> npm install --save axios
برنامه‌ی backend مورد استفاده هم همان برنامه‌ای است که از قسمت 22 شروع به توسعه‌ی آن کردیم و کدهای کامل آن‌را از پیوست‌های انتهای بحث، می‌توانید دریافت کنید. این برنامه که در مسیر شروع شده‌ی با https://localhost:5001/api قرار می‌گیرد، جهت پشتیبانی از افعال مختلف HTTP مانند Get/Post/Delete/Update طراحی شده‌است. برای راه اندازی آن، به پوشه‌ی این برنامه، مراجعه کرده و فایل dotnet_run.bat آن‌را اجرا کنید، تا endpointهای REST Api آن قابل دسترسی شوند. برای مثال باید بتوان به مسیر https://localhost:5001/api/posts آن در مرورگر دسترسی یافت.
در ادامه می‌خواهیم در برنامه‌ی React خود، لیست مطالب برنامه‌ی backend را از سرور دریافت کرده و نمایش دهیم. همچنین یک search box را به همراه دکمه‌های search و clear نیز به آن اضافه کنیم.


دریافت اطلاعات اولیه از سرور، درون useEffect Hook

پس از نصب پیش‌نیازها و راه اندازی برنامه‌ی backend، در ابتدا فایل src\config.json را جهت درج مشخصات آدرس REST Api آن، ایجاد می‌کنیم:
{
   "apiUrl": "https://localhost:5001/api"
}
سپس فایل جدید src\components\part03\Search.jsx را جهت توسعه‌ی کامپوننت جستجوی این قسمت ایجاد می‌کنیم و ساختار ابتدایی آن‌را با import وابستگی‌های React و مسیر فوق، به صورت یک function که در همان محل قابل export است، ایجاد می‌کنیم، تا فعلا یک React.Fragment را بازگشت دهد:
import React from "react";
import {apiUrl} from "../../config.json";

export default function App() {
  return <></>;
}
بر این اساس، کامپوننت App فایل index.js را به صورت زیر از کامپوننت App فوق، تامین خواهیم کرد:
import App from "./components/part03/Search";
اکنون می‌خواهیم اولین درخواست خود را به سمت backend server ارسال کنیم. برای این منظور در کامپوننت‌های تابعی، از useEffect Hook استفاده می‌شود؛ چون کار با یک API خارجی نیز یک side effect محسوب می‌گردد. بنابراین متد useEffect را import کرده و سپس آن‌را بالای return، فراخوانی می‌کنیم. درون آن نیاز است اطلاعات را از سرور دریافت کنیم و برای اینکار از کتابخانه‌ی axios که آن‌را در قسمت 23 معرفی کردیم، استفاده خواهیم کرد. به همین جهت import آن‌را نیز در این ماژول خواهیم داشت:
import axios from "axios";
import React, { useEffect, useState } from "react";

import { apiUrl } from "../../config.json";

export default function App() {
  useEffect(() => {
    axios
      .get(apiUrl + "/posts/search?query=")
      .then(response => console.log(response.data));
  });
  return <></>;
}
در اینجا با استفاده از متد get کتابخانه‌ی axios، درخواستی را به آدرس https://localhost:5001/api/posts/search، با یک کوئری استرینگ خالی، ارسال کرده‌ایم تا تمام داده‌ها را بازگشت دهد. روش قدیمی استفاده‌ی از axios را که با استفاده از Promiseها و متد then آن است، در اینجا ملاحظه می‌کنید که خروجی خاصیت data شیء response دریافتی را لاگ کرده‌است:


اکنون می‌خواهیم این اطلاعات دریافتی را در برنامه‌ی خود نیز نمایش دهیم. به همین جهت نیاز است تا response.data را درون state کامپوننت جاری قرار داده و در حین رندر کامپوننت، با تشکیل حلقه‌ای بر روی آن، اطلاعات نهایی را نمایش دهیم. بنابراین نیاز به useState Hook خواهیم داشت که ابتدا آن‌را import کرده و سپس آن‌را تعریف و در قسمت then، فراخوانی می‌کنیم:
import axios from "axios";
import React, { useEffect, useState } from "react";

import { apiUrl } from "../../config.json";

export default function App() {
  const [results, setResults] = useState([]);

  useEffect(() => {
    axios.get(apiUrl + "/posts/search?query=").then(response => {
      console.log(response.data);
      setResults(response.data);
    });
  });
چون اطلاعات بازگشتی به صورت یک آرایه‌است، مقدار اولیه‌ی متد useState را با یک آرایه‌ی خالی مقدار دهی کرده‌ایم. سپس برای مقدار دهی متغیر results موجود در state، به متد setResults تعریف شده‌ی توسط useState، مقدار response.data را ارسال می‌کنیم. در این حالت اگر برنامه را ذخیره کرده و اجرا کنید .... برنامه و همچنین مرورگر، هنگ می‌کنند!


همانطور که مشاهده می‌کنید، یک حلقه‌ی بی پایان در اینجا رخ داده‌است! برای پایان آن، مجبور خواهیم شد ابتدا کنسول اجرایی برنامه‌ی React را به صورت دستی خاتمه داده و سپس مرورگر را نیز refresh کنیم تا این حلقه، خاتمه پیدا کند.
علت این مشکل را در قسمت 30 بررسی کردیم؛ effect method تابع useEffect (همان متد در برگیرنده‌ی قطعه کدهای axios.get در اینجا)، پس از هربار رندر کامپوننت، یکبار دیگر نیز اجرا می‌شود. یعنی این متد، هر دو حالت componentDidMount و componentDidUpdate کامپوننت‌های کلاسی را با هم پوشش می‌دهد و چون در اینجا setState را با فراخوانی متد setResults داریم، یعنی درخواست رندر مجدد کامپوننت انجام شده‌است و پس از آن، مجددا effect method فراخوانی می‌شود و ... این حلقه هیچ‌گاه خاتمه نخواهد یافت. به همین جهت مرورگر و برنامه، هر دو با هم هنگ می‌کنند!

در این برنامه فعلا می‌خواهیم که فقط در حالت componentDidMount، کار درخواست اطلاعات از backend صورت گیرد. به همین جهت پارامتر دوم متد useEffect را با یک آرایه‌ی خالی مقدار دهی می‌کنیم:
  useEffect(() => {
   // ...
  }, []);
تا اینجا موفق شدیم متد setResults را تنها در اولین بار نمایش کامپوننت، فراخوانی کنیم که در نتیجه‌ی آن، متغیر results موجود در state، مقدار دهی شده و همچنین کار رندر مجدد کامپوننت در صف قرار می‌گیرد. بنابراین مرحله‌ی بعد، تکمیل قسمت return کامپوننت تابعی است تا آرایه‌ی results را نمایش دهد:
//...

export default function App() {
  // ...
  return (
    <>
      <table className="table">
        <thead>
          <tr>
            <th>Title</th>
          </tr>
        </thead>
        <tbody>
          {results.map(post => (
            <tr key={post.id}>
              <td>{post.title}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </>
  );
}
در اینجا ابتدا یک فرگمنت را توسط </><> تعریف کرده‌ایم و سپس در داخل آن می‌توان المان‌های فرزند را قرار داد. سپس برای ایجاد trهای جدول، یک حلقه را توسط results.map، بر روی عناصر دریافتی از آرایه‌ی مطالب، تشکیل داده‌ایم. چون این حلقه بر روی trهای پویا تشکیل می‌شود، هر tr، نیاز به یک key دارد، تا در DOM مجازی React قابل شناسایی و ردیابی شود که در آخر یک چنین شکلی را ایجاد می‌کند:



استفاده ازAsync/Await  برای دریافت اطلاعات، درون یک  useEffect Hook

اکنون می‌خواهیم درون effect method یک useEffect Hook، روش قدیمی استفاده‌ی از callbackها و متد then را برای دریافت اطلاعات، با روش جدیدتر async/await که در قسمت 23 آن‌را بیشتر بررسی کردیم، جایگزین کنیم.
  useEffect(async () => {
    const { data } = await axios.get(apiUrl + "/posts/search?query=");
    console.log(data);
    setResults(data);
  }, []);
خروجی متد axios.get، یک شیء Promise است که نتیجه‌ی عملیات async را بازگشت می‌دهد. در جاوا اسکریپت مدرن، می‌توان از واژه‌ی کلیدی await برای دسترسی به شیء response دریافتی از آن، استفاده کرد. سپس هر جائیکه از واژه‌ی کلیدی await استفاده می‌شود، متد جاری را باید با واژه‌ی کلیدی async نیز مزین کرد. با انجام اینکار و اجرای برنامه، اخطار زیر در کنسول توسعه دهندگان مرورگر ظاهر می‌شود؛ هرچند نتیجه نهایی هم هنوز نمایش داده می‌شود:
Warning: An effect function must not return anything besides a function, which is used for clean-up.
It looks like you wrote useEffect(async () => ...) or returned a Promise.
این اخطار به این معنا است که effect function تعریف شده را نمی‌توان به صورت async تعریف کرد و از چنین قابلیتی پشتیبانی نمی‌شود. یک effect function حداکثر می‌تواند یک متد دیگر را بازگشت دهد (و یا هیچ چیزی را بازگشت ندهد) که نمونه‌ی آن‌را در قسمت 30، با متدهایی که کار پاکسازی منابع را انجام می‌دادند، بررسی کردیم. اگر متدی را مزین به واژه‌ی کلیدی async کردیم، یعنی این متد در اصل یک Promise را بازگشت می‌دهد؛ اما یک effect function، حداکثر یک تابع دیگر را می‌تواند بازگشت دهد تا componentWillUnmount را پیاده سازی کند.

برای رفع این مشکل، روش توصیه شده، ایجاد یک تابع مجزای async و سپس فراخوانی آن درون effect function است:
  useEffect(() => {
    getResults();
  }, []);

  const getResults = async () => {
    const { data } = await axios.get(apiUrl + "/posts/search?query=");
    console.log(data);
    setResults(data);
  };
مشکل یا محدودیتی برای ایجاد متدهای async، در خارج از یک effect function وجود ندارد. به همین جهت اعمالی را که نیاز به Async/Await دارند، در این متدهای مجزا انجام داده و سپس می‌توان آن‌ها را درون effect function، به نحوی که ملاحظه می‌کنید، فراخوانی کرد. با این تغییر، هنوز هم اطلاعات نهایی، بدون مشکل دریافت می‌شوند، اما دیگر اخطاری در کنسول توسعه دهندگان مرورگر درج نخواهد شد.


پیاده سازی componentDidUpdate با یک useEffect Hook، جهت انجام جستجوهای پویا

تا اینجا با اضافه کردن پارامتر دومی به متد useEffect، رویداد componentDidUpdate آن‌را از کار انداختیم، تا برنامه با هربار فراخوانی setState و اجرای مجدد effect function، در یک حلقه‌ی بی‌نهایت وارد نشود. اکنون این سؤال مطرح می‌شود که اگر یک textbox را برای جستجوی در عناوین نمایش داده شده، در بالای جدول آن قرار دهیم، نیاز است با هربار تغییر ورودی آن، کار فراخوانی مجدد effect function صورت گیرد، تا بتوان نتایج جدیدتری را از سرور دریافت و به کاربر نشان داد؛ این مشکل را چگونه باید حل کرد؟
برای دریافت عبارت وارد شده‌ی توسط کاربر و جستجو بر اساس آن، ابتدا متغیر state و متد تنظیم آن‌را با استفاده از useState Hook و یک مقدار اولیه‌ی دلخواه تنظیم می‌کنیم:
export default function App() {
  // ...
  const [query, setQuery] = useState("Title");
سپس المان textbox زیر را هم به بالای المان جدول، اضافه می‌کنیم:
<input
  type="text"
  name="query"
  className="form-control my-3"
  placeholder="Search..."
  onChange={event => setQuery(event.target.value)}
  value={query}
/>
این کنترل توسط رویداد onChange، عبارت تایپ شده را به متد setQuery ارسال کرده و در نتیجه‌ی آن، کار تنظیم متغیر query در state کامپوننت جاری، صورت می‌گیرد. همچنین با تنظیم value={query}، سبب خواهیم شد تا این کنترل، به یک المان کنترل شده‌ی توسط state تبدیل شود و در ابتدای نمایش فرم، مقدار ابتدایی useState را نمایش دهد.
اکنون که متغیر query دارای مقدار شده‌است، می‌توان از آن در متد axios.get، به نحو زیر و با ارسال یک کوئری استرینگ به سمت سرور، استفاده کرد:
const { data } = await axios.get(
   `${apiUrl}/posts/search?query=${encodeURIComponent(query)}`
);
استفاده از تابع encodeURIComponent، سبب می‌شود تا اگر کاربر برای مثال "Text 1" را وارد کرد، فاصله‌ی بین دو عبارت، به درستی encode شده و یک کوئری مانند https://localhost:5001/api/posts/search?query=Title%201 به سمت سرور ارسال گردد.

تا اینجا اگر برنامه را ذخیره کرده و اجرا کنید، با تایپ در textbox جستجو، تغییری در نتایج حاصل نمی‌شود؛ چون effect function تعریف شده که سبب اجرای مجدد axios.get می‌شود، طوری تنظیم شده‌است که فقط یکبار، آن‌هم پس از رندر اولیه‌ی کامپوننت، اجرا شود. برای رفع این مشکل، با مقدار دهی آرایه‌ای که به عنوان پارامتر دوم متد useEffect تعریف شده، می‌توان اجرای مجدد effect function آن‌را وابسته‌ی به تغییرات متغیر query در state کامپوننت کرد:
  useEffect(() => {
    getResults();
  }, [query]);
اکنون اگر برنامه را ذخیره کرده و اجرا کنید، با هربار ورود اطلاعات درون textbox جستجو، یک کوئری جدید به سمت سرور ارسال شده و نتیجه‌ی جستجوی انجام شده، به صورت یک جدول رندر می‌شود:



دریافت اطلاعات جستجو، تنها با ارسال اطلاعات یک فرم به سمت سرور


تا اینجا کاربر با هر حرفی که درون textbox جستجو وارد می‌کند، یک کوئری، به سمت سرور ارسال خواهد شد. برای کاهش آن می‌توان یک دکمه‌ی جستجو را در کنار این textbox قرار داد تا تنها پس از کلیک بر روی آن، این جستجو صورت گیرد.
برای پیاده سازی این قابلیت، ابتدا وابستگی به query را از متد useEffect حذف می‌کنیم، تا دیگر با تغییر اطلاعات textbox، متد callback آن اجرا نشود (پارامتر دوم آن‌را مجددا به یک آرایه‌ی خالی تنظیم می‌کنیم). سپس یک دکمه را که از نوع button است و رویداد onClick آن به getResults اشاره می‌کند، در بالای جدول نتایج مطالب، قرار می‌دهیم:
<button
  className="btn btn-primary"
  type="button"
  onClick={getResults}
>
  Search
</button>
تا اینجا اگر کاربر اطلاعاتی را وارد کرده و سپس بر روی دکمه‌ی Search فوق کلیک کند، نتایج جستجوی خودش را در جدول ذیل آن مشاهده می‌کند. اکنون می‌خواهیم این امکان را به کاربران بدهیم که با فشردن دکمه‌ی enter درون textbox جستجو، همین قابلیت جستجو را در اختیار داشته باشند؛ تا دیگر الزامی به کلیک بر روی دکمه‌ی Search، نباشد. برای اینکار تنها کافی است، کل مجموعه‌ی textbox و دکمه را درون یک المان form قرار دهیم و نوع button را نیز به submit تغییر دهیم. سپس onClick دکمه را حذف کرده و بجای آن رویداد onSubmit فرم را پیاده سازی می‌کنیم:
<form onSubmit={handleSearch}>
  <div className="input-group my-3">
    <label htmlFor="query" className="form-control-label sr-only"></label>
    <input
type="text"
id="query"
name="query"
className="form-control"
placeholder="Search ..."
onChange={event => setQuery(event.target.value)}
value={query}
    />
    <div className="input-group-append">
<button className="btn btn-primary" type="submit">
  Search
</button>
    </div>
  </div>
</form>
در اینجا یک المان فرم، به همراه یک textbox و button از نوع submit تعریف شده‌اند. رویداد onSubmit نیز به متد منتسب به متغیر handleSearch، متصل شده‌است تا با فشردن دکمه‌ی enter توسط کاربر در این textbox، کار جستجوی مجدد، صورت گیرد:
  const handleSearch = event => {
    event.preventDefault();
    getResults();
  };
تا اینجا اگر برنامه را ذخیره کرده و "Text 1" را در textbox جستجو، وارد کرده و enter کنیم، همانند تصویر فوق، رکورد متناظری از سرور دریافت و نمایش داده می‌شود.


افزودن قابلیت پاک کردن textbox جستجو و معرفی useRef Hook

در ادامه می‌خواهیم یک دکمه‌ی جدید را در کنار دکمه‌ی Search، اضافه کنیم تا با کلیک کاربر بر روی آن، نه فقط محتوای وارد شده‌ی در textbox پاک شود، بلکه focus نیز به آن منتقل گردد. برای پاک کردن textbox، فقط کافی است متد setQuery را با یک رشته‌ی خالی ارسالی به آن فراخوانی کنیم. اما برای انتقال focus به textbox، نیاز به داشتن ارجاع مستقیمی به آن المان وجود دارد که با مفهوم آن در قسمت 18 آشنا شدیم: «برای دسترسی به یک المان DOM در React، باید یک reference را به آن نسبت داد. برای این منظور یک خاصیت جدید را در سطح کلاس کامپوننت ایجاد کرده و آن‌را با React.RefObject مقدار دهی اولیه کرده و سپس ویژگی ref المان مدنظر را به این RefObject تنظیم می‌کنیم». برای انجام یک چنین کاری در اینجا، Hook ویژه‌ای به نام useRef معرفی شده‌است. بنابراین برای پیاده سازی این نیازمند‌ی‌ها، ابتدا دکمه‌ی Clear را در کنار دکمه‌ی Search قرار می‌دهیم:
<button
  type="button"
  onClick={handleClearSearch}
  className="btn btn-info"
>
  Clear
</button>
سپس رویداد onClick آن‌را به متد منتسب به متغیر handleClearSearch، مرتبط می‌کنیم:
import React, { useEffect, useRef, useState } from "react";
// ...

export default function App() {
  // ...
  const searchInputRef = useRef();


  const handleClearSearch = () => {
    setQuery("");
    searchInputRef.current.focus();
  };
در اینجا ابتدا useRef را import کرده‌ایم، تا توسط آن بتوان یک متغیر از نوع React.MutableRefObject را ایجاد کرد. سپس در متد منتسب به handleClearSearch، ابتدا با فراخوانی setQuery، مقدار query را در state کامپوننت، پاک کرده و سپس به کمک این شیء Ref، دسترسی مستقیمی به شیء textbox یافته و متد focus آن‌را فراخوانی می‌کنیم (شیء current آن، معادل DOM Element متناظر است).
البته این searchInputRef برای اینکه دقیقا به textbox تعریف شده اشاره کند، باید آن‌را به ویژگی ref المان، انتساب داد:
<input
  type="text"
  id="query"
  name="query"
  className="form-control"
  placeholder="Search ..."
  onChange={event => setQuery(event.target.value)}
  value={query}
  ref={searchInputRef}
/>
تا اینجا اگر برنامه را ذخیره کرده و اجرا کنیم، با کلیک بر روی دکمه‌ی Clear، متن textbox جستجو حذف شده و سپس کرسر مجددا به همان textbox برای ورود اطلاعات، منتقل می‌شود.


نمایش «لطفا منتظر بمانید» در حین دریافت اطلاعات از سرور


البته در اینجا با هر بار کلیک بر روی دکمه‌ی جستجو، نتیجه‌ی نهایی به سرعت نمایش داده می‌شود؛ اما اگر سرعت اتصال کاربر کمتر باشد، با یک وقفه این امر رخ می‌دهد. به همین جهت بهتر است یک پیام «لطفا منتظر بمانید» را در این حین به او نمایش دهیم. به همین جهت در ابتدا state مرتبطی را به کامپوننت اضافه می‌کنیم:
const [loading, setLoading] = useState(false);
تا با فراخوانی متد setLoading آن بتوان سبب رندر مجدد UI شد و پیامی را نمایش داد و یا مخفی کرد:
  const getResults = async () => {
    setLoading(true);
    const { data } = await axios.get(
      `${apiUrl}/posts/search?query=${encodeURIComponent(query)}`
    );
    console.log(data);
    setResults(data);
    setLoading(false);
  };
متد setLoading در ابتدای متد منتسب به متغیر getResults، مقدار متغیر loading را در state به true تنظیم می‌کند و در پایان عملیات، به false. اکنون بر این اساس می‌توان UI متناظری را نمایش داد:
      {loading ? (
        <div className="alert alert-info">Loading results...</div>
      ) : (
        <table className="table">
          <thead>
            <tr>
              <th>Title</th>
            </tr>
          </thead>
          <tbody>
            {results.map(post => (
              <tr key={post.id}>
                <td>{post.title}</td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
در اینجا با استفاده از یک ternary operator، اگر loading به true تنظیم شده باشد، یک div به همراه عبارت Loading results، نمایش داده می‌شود؛ در غیراینصورت، جدول اطلاعات مطالب، نمایش داده خواهد شد.

برای آزمایش آن می‌توان سرعت اتصال را در برگه‌ی شبکه‌ی ابزارهای توسعه دهندگان مرورگر، تغییر داد:



مدیریت خطاها در حین اعمال async

آخرین امکانی را که به این مطلب اضافه خواهیم کرد، مدیریت خطاهای اعمال async است که با try/catch صورت می‌گیرد:
// ...

export default function App() {
  // ...
  const [error, setError] = useState(null);

  // ...

  const getResults = async () => {
    setLoading(true);

    try {
      const { data } = await axios.get(
        `${apiUrl}/posts/search?query=${encodeURIComponent(query)}`
      );
      console.log(data);
      setResults(data);
    } catch (err) {
      setError(err);
    }

    setLoading(false);
  };
در حین فراخوانی await axios.get، اگر خطایی رخ دهد، این کتابخانه استثنایی را صادر خواهد کرد که می‌توان به جزئیات آن در بدنه‌ی catch نوشته شده دسترسی یافت و برای مثال آن‌را به کاربر نمایش داد. برای این منظور ابتدا state مخصوص آن‌را ایجاد می‌کنیم و سپس توسط فراخوانی متد setError آن، کار رندر مجدد کامپوننت را در صف انجام قرار خواهیم داد.در نهایت برای نمایش آن می‌توان یک div را به پایین جدول اضافه نمود:
{error && <div className="alert alert-warning">{error.message}</div>}
برای آزمایش آن، برنامه‌ی backend را که در حال اجرا است، خاتمه دهید و سپس در برنامه سعی کنید به آن متصل شوید:




کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-30-part-03-frontend.zip و sample-30-part-03-backend.zip
نظرات مطالب
Blazor 5x - قسمت 27 - برنامه‌ی Blazor WASM - کار با سرویس‌های Web API
این پروژه برای اجرا، نیاز به اجرای همزمان دو پروژه را دارد که بهتر است ابتدا Web API اجرا شود و سپس کلاینتی که قرار است با آن کار کند:
الف) پروژه‌ی Web API
در این پروژه دو تغییر زیر انجام شده:
- در فایل HotelManagement\BlazorWasm\BlazorWasm.WebApi\Properties\launchSettings.json شماره پورت‌ها به 5001 و 5000 تنظیم شدند که البته چون بر روی SSL اجرا می‌شود، مقدار 5001 آن مهم است:
https://localhost:5001;http://localhost:5000
- در فایل HotelManagement\BlazorWasm\BlazorWasm.WebApi\appsettings.json شماره پورت کلاینت مشخص شده که در قسمت ب تنظیم می‌شود:
"Client_URL": "https://localhost:5002/"

ب) پروژه‌ی کلاینت
در این پروژه هم دو تغییر زیر انجام شده:
- در فایل HotelManagement\BlazorWasm\BlazorWasm.Client\Properties\launchSettings.json  شماره پورت‌ها به 5002 و 5003 تنظیم شدند که البته چون بر روی SSL اجرا می‌شود، مقدار 5002 آن مهم است:
https://localhost:5002;http://localhost:5003
- در فایل HotelManagement\BlazorWasm\BlazorWasm.Client\wwwroot\appsettings.json شماره پورت اتصال به Web API مشخص شده که همان 5001 قسمت الف است:
"BaseAPIUrl": "https://localhost:5001/"

این نکات را برای کار با IIS Express هم باید رعایت کنید و در فایل‌های launchSettings.json آن‌ها تنظیم کنید (و اگر شماره پورت ssl آن‌ها یکی نیست، یعنی تنظیم خوبی دارند). بعد از این تنظیمات، فایل‌های appsettings.json پروژه‌ها را هم یافته و شماره پورت‌های کلاینت و Web API را مانند مثال فوق و بر اساس شماره‌هایی که تنظیم کردید، دقیق تنظیم کنید تا کلاینت بداند که قرار است درخواست‌ها را دقیقا به چه آدرسی ارسال کند.
نظرات مطالب
نحوه‌ی مشارکت در پروژه‌های GitHub به کمک Visual Studio
یک نکته‌ی تکمیلی: چگونه یک Release خودکار را در GitHub ایجاد کنیم؟
اگر پروژه‌ی خود را به GitHub ارسال کرده‌اید و پس از آن یک بسته‌ی نیوگت را هم بر اساس این نگارش تهیه و منتشر کرده‌اید، می‌توانید یک release خودکار را به صورت زیر در GitHub ایجاد کنید:
git tag -a 1.6.7 -m "Published 1.6.7 to nuget.org"
git push --follow-tags
دستور اول بر اساس نگارش مدنظر شما (برای مثال 1.6.7 در اینجا)، یک Tag را ایجاد می‌کند. دستور دوم آن‌را به GitHub ارسال می‌کند که سبب تولید خودکار یک release جدید در برگه‌ی releases پروژه نیز می‌شوند. توضیحاتی که پس از m- ذکر شده‌اند، در برگه‌ی releases به عنوان توضیح آن نگارش خاص قابل مشاهده خواهند بود.