مطالب
Html Encoding

.
مقدمه 

در دنیای وب دو انکدینگ معروف داریم: Url Encoding و Html Encoding. در هر کدام از این انکدینگ‌ها یک عملیات کلی صورت می‌گیرد: تبدیل کاراکترهای غیرمجاز به عبارات معادل مجاز.

Url Encoding همان‌طور که از نامش پیداست روشی برای کدکردن Url هاست. مثل عبارت کدشده زیر:
Hello%20world%20,%20hi
درواقع کاراکتر مشخص‌کننده رشته‌ای که Url Encoding احتمالا در آن اعمال شده است، همان کاراکتر % است. بحث درباره این نوع انکدینگ کمی مفصل است که خود مطلب جداگانه‌ای می‌طلبد. (اطلاعات بیشتر)

Html Encoding نیز با توجه به نامش برای انکدینگ عبارات HTML استفاده می‌شود. مثلا عبارت زیر را درنظر بگیرید:
<html>encoding</html>
این عبارت پس از اعمال عملیات Html Encoding به صورت زیر در خواهد آمد:
&lt;html&gt;encoding&lt;/html&gt;
می‌بینید که در اینجا کاراکترهای > و < به صورت عبارات ;lt& و ;gt& در آمده‌اند. شرح کاملی درباره این عبارات معادل (که اصطلاحا به آن‌ها character entity می‌گویند) در اینجا آورده شده است.

در حالت کلی Html Encoding شامل کدکردن 5 کاراکتر زیر است:
.

کاراکتر  عبارت معادل  توضیحات
 >&gt; 
 <&lt;
 
"&quot;
 
'&#39;
یا ;apos& به غیر از IE
&&amp;
 

نکته: در برخی استانداردها (بیشتر برای XML) برای کاراکتر ' از عبارت ;apos& استفاده می‌شود. این عبارت جایگزین به غیر از IE در بقیه مرورگرها درست کار می‌کند.

این کاراکترها درواقع از عناصر اصلی تشکیل‌دهنده ساختار Html هستند، بنابراین وجود آن‌ها درون یک متن می‌تواند در روند رندر صفحات html اختلال ایجاد کند. بنابراین با استفاده از Html Encoding و تبدیل این کاراکترها به معادلشان (عباراتی که مرورگرها آن‌ها را می‌شناسند)، می‌توان از نمایش درست این کاراکترها مطمئن شد. البته یکی دیگر از دلایل مهم اعمال این انکدینگ، افزایش امنیت و جلوگیری از حملات XSS است.

فرمت این عبارات معادل به صورت ;entity_name& است. به کل این عبارت اصطلاحا Character Entity گفته می‌شود. این عبارات با کاراکتر & شروع شده و به یک کاراکتر ; ختم می‌شوند. کلمه میان این دو کاراکتر نیز عبارت جایگزین (یا همان entity name) هر یک از این کاراکترهاست که در لینک بالا به همراه بسیاری دیگر از کاراکترها اشاره شده است (^).
روش دیگری نیز برای کدکردن کاراکترها با فرمت ;entity_number#& وجود دارد. این entity_number درواقع کد کاراکتر مربوطه در جدول کاراکترست جاری مرورگر است. معمولا این کدها منطبق بر جدول ASCII هستند. برای کاراکترهای خارج از جدول اسکی هم از سایر جداول (مثلا یونیکد) استفاده می‌شود. عملیات انکدینگ برای کاراکترهای با کد 160 تا 255 (براساس استاندارد ISO-8859-1) با این روش انجام می‌شود (^). اطلاعات بیشتر راجع به این کدها در اینجا آورده شده است.

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

Html Encoding در دات‌نت

در دات‌نت متدهای متعددی برای اعمال Html Encoding وجود دارد. برخی از آن‌ها صرفا برای اسناد HTML طراحی شده‌اند و برخی دیگر یک پیاده‌سازی کلی دارند و بعضی نیز برای فایل‌های XML ارائه شده‌اند. این متدها عبارتند از:
  • متد System.Security.SecurityElement.Escape: این متد بیشتر برای اعمال این انکدینگ در XML به‌کار می‌رود. در این متد 5 کاراکتر اشاره شده در بالا به عبارات معادل انکد می‌شوند. البته برای کاراکتر ' از عبارت ;apos& استفاده می‌شود.

  • متدهای موجود در System.Net.WebUtility: متدهای HtmlEncode و HtmlDecode موجود در این کلاس عملیات انکدینگ را انجام می‌دهند. این کلاس از دات‌نت 4 اضافه شده است.

  • متدهای کلاس System.Web.HttpUtility: در این کلاس از متدهای موجود در کلاس System.Web.Util.HttpEncoder استفاده می‌شود. در پیاده‌سازی پیش‌فرض، متدهای این کلاس از متدهای موجود در کلاس WebUtility استفاده می‌کنند. البته می‌توان با فراهم کردن یک Encoder سفارشی و تنظیم آن در فایل کانفیگ (خاصیت encoderType در قسمت HttpRuntime) این رفتار را تغییر داد. دلیل اصلی جابجایی مکان پیاده‌سازی این متدها از دات نت 4 به بعد نیز به همین دلیل است. (اطلاعات بیشتر ^ و ^).

  • متدهای موجود در System.Web.HttpServerUtility: متدهای HtmlEncode و HtmlDecode موجود در این کلاس مستقیما از متدهای موجود در کلاس HttpUtility استفاده می‌کنند. خاصیت Server موجود در HttpContext یا در کلاس Page از نوع این کلاس است.

  • متدهای موجود در کلاس System.Web.Security.AntiXss.AntiXssEncoder: این کلاس از دات نت 4.5 اضافه شده است. همانطور که از نام این کلاس بر می‌آید، از HttpEncoder مشتق شده است که در متدهای مرتبط با html encoding تغییراتی در آن اعمال شده است. متدهای این کلاس برای امنیت بیشتر به جای استفاده از Black List از یک White List استفاده می‌کنند.

درحال حاضر بهترین گزینه موجود برای عملیات انکدینگ، متدهای موجود در کلاس WebUtility هستند. ازآنجاکه این کلاس در فضای System.Net و در کتابخانه System.dll قرار دارد (کتابخانه‌ای که معمولا برای تمام برنامه‌های دات‌نتی نیاز است)، بنابراین بارگذاری آن در برنامه نیز بار اضافی بر حافظه تحمیل نمی‌کند.
پیاده‌سازی عملیات HtmlEncode کار سختی نیست. مثلا می‌توان برای سادگی از متد Replace استفاده کرد. اما برای رشته‌های طولانی این متد کارایی مناسبی ندارد. به همین دلیل در تمام پیاده‌سازی‌ها، معمولا از یک حلقه بر روی تمام کاراکترهای رشته موردنظر برای یافتن کاراکترهای غیرمجاز استفاده می‌شود. در کدهای متدهای موجود، برای افزایش سرعت حتی از اشاره‌گر و کدهای unsafe نیز استفاده شده است.
برای افزایش کارایی در تولید رشته نهایی تبدیل‌شده، بهتر است از یک StringBuilder استفاده شود. در پیاده‌سازی‌های متدهای بالا برای اینکار معمولا از یک TextWriter استفاده می‌شود. TextWriterهای موجود از کلاس StrigBuilder برای دستکاری رشته‌ها استفاده می‌کنند.

صرفا جهت آشنایی بیشتر، پیاده‌سازی خلاصه‌شده متد HtmlEncode در کلاس WebUtility در زیر آورده شده است:
public static unsafe void HtmlEncode(string value, TextWriter output)
{
  int index = IndexOfHtmlEncodingChars(value, 0);
  if (index == -1)
  {
    output.Write(value);
    return;
  }
  int cch = value.Length - index;
  fixed (char* str = value)
  {
    char* pch = str;
    while (index-- > 0)
    {
      output.Write(*pch++);
    }
    while (cch-- > 0)
    {
      char ch = *pch++;
      if (ch <= '>')
      {
        switch (ch)
        {
          case '<':
            output.Write("&lt;");
            break;
          case '>':
            output.Write("&gt;");
            break;
          case '"':
            output.Write("&quot;");
            break;
          case '\'':
            output.Write("&#39;");
            break;
          case '&':
            output.Write("&amp;");
            break;
          default:
            output.Write(ch);
            break;
        }
      }
      else if (ch >= 160 && ch < 256)
      {
        // The seemingly arbitrary 160 comes from RFC 
        output.Write("&#");
        output.Write(((int)ch).ToString(NumberFormatInfo.InvariantInfo));
        output.Write(';');
      }
      else
      {
        output.Write(ch);
      }
    }
  }
}
private static unsafe int IndexOfHtmlEncodingChars(string s, int startPos)
{
  int cch = s.Length - startPos;
  fixed (char* str = s)
  {
    for (char* pch = &str[startPos]; cch > 0; pch++, cch--)
    {
      char ch = *pch;
      if (ch <= '>')
      {
        switch (ch)
        {
          case '<':
          case '>':
          case '"':
          case '\'':
          case '&':
            return s.Length - cch;
        }
      }
      else if (ch >= 160 && ch < 256)
      {
        return s.Length - cch;
      }
    }
  }
  return -1;
}
در ابتدا بررسی می‌شود که آیا اصلا متن ورودی حاوی کاراکترهای غیرمجاز است یا خیر. درصورت عدم وجود چنین کاراکترهایی، کار متد با برگشت خود متن ورودی پایان می‌یابد. درغیراینصورت عملیات انکدینگ آغاز می‌شود.
همان‌طور که می‌بینید عملیات انکدینگ برای 5 کاراکتر اشاره شده به صورت جداگانه انجام می‌شود و برای کاراکترهای با کد 160 تا 255 (با توجه به توضیحات موجود در مقدمه) نیز با استاندارد ;code#& عملیات تبدیل انجام می‌شود.
در سمت دیگر، پیاده‌سازی بهینه متد HtmlDecode چندان ساده نیست. چون به جای یافتن یک کاراکتر غیرمجاز باید به دنبال عبارات چند کاراکتری معادل گشت که کاری نسبتا پیچیده است.

اطلاعات و پیاده‌سازی نسبتا کاملی درباره Html Encoding در سمت سرور در اینجا قابل مشاهده است.

نکته: درصورت نیاز به کدکردن سایر کاراکترها (مثلا کاراکترهای یونیکد) پیاده‌سازی‌های موجود کارا نخواهند بود. بنابراین باید encoder سفارشی خود را تهیه کنید. مثلا می‌توانید شرط دوم در بررسی کد کاراکترها را بردارید (منظور قسمت ch < 256) که در این‌صورت متد شما محدوده وسیعی را پوشش می‌دهد. اما دقت کنید که با این تغییر متدی سفارشی برای عملیات decode نیز باید تهیه کنید!

Html Encoding در جاوا اسکریپت

برای انجام عملیات Url Encoding در جاوا اسکریپت چند متد توکار وجود دارد، که فرایند کلی عملیات همه آن‌ها تقریبا یکسان است. اما متاسفانه برای انجام عملیات Html Encoding متدی در جاوا اسکریپت وجود ندارد. بنابراین متدهای مربوطه باید توسط خود برنامه‌نویسان پیاده‌سازی شوند.

یک روش برای اینکار استفاده از لیست اشاره‌شده در بالا و انجام عملیات replace برای تمام این کاراکترهاست (5 کاراکتر اصلی و درصورت نیاز سایر کاراکترها). این کار می‌تواند کمی سخت باشد و درواقع پیاده‌سازی چنین متدی نسبتا مشکل نیز هست (مخصوصا عملیات decode).
اما خوشبختانه امکانی در اسناد html وجود دارد که این کار (مخصوصا Decode کردن) را آسان می‌کند.

این روش جالب برای انجام عملیات Html Encoding در جاوا اسکریپت، استفاده از یک قابلیت توکار در مرورگرهاست. عناصر DOM (مانند div) دو خاصیت innerText و innerHTML دارند که مرورگرها با توجه به مقادیر تنظیم‌شده برای هر یک، عملیات coding و decoding مربوطه را به صورت کاملا خودکار انجام داده و مقدار خاصیت دیگر را به‌روزرسانی می‌کنند (دقت کنید که در این دو پراپرتی، کلمه HTML کاملا با حروف بزرگ است، برخلاف Text که تنها حرف اول آن بزرگ است).

برای روشن‌تر شدن موضوع به مثال زیر برای عملیات encode توجه کنید:
<div id="log"></div>
<script type="text/javascript">
  var element = document.getElementById('log');
  element.innerText = '<html> encoding </html>';
  console.log(element.innerHTML);
</script>
که خروجی زیر را خواهد داشت:
&lt;html&gt; encoding &lt;/html&gt;
عکس این عملیات یعنی decoding نیز با استفاده از کدی مثل زیر امکان‌پذیر است:
<div id="log">
</div>
<script type="text/javascript">
  var element = document.getElementById('log');
  element.innerHTML = "&lt;html&gt; encoding &lt;/html&gt;";
  console.log(element.innerText);
</script>
خروجی کد بالا به صورت زیر است:
<html> encoding </html>
می‌بینید که با استفاده از این ویژگی جالب، می‌توان عملیات Html Encoding را انجام داد. در ادامه پیاده‌سازی مناسب این دو متد آورده شد است.
.
متد htmlEncode

برای پیاده‌سازی این متد برای حالت استفاده مستقیم داریم:
String.htmlEncode = function (s) {
  var el = document.createElement("div");
  el.innerText = s || '';
  return el.innerHTML;
};
در اینجا با استفاده از متد createElement شی document یک المان DOM (در اینجا div) ایجاد شده و سپس با توجه به توضیحات بالا خاصیت innerText آن به مقدار ورودی تنظیم می‌شود. استفاده از عبارت '' || s در اینجا برای جلوگیری از برگشت عبارات ناخواسته (مثل undefined یا null) برای ورودی‌های غیرمجاز است. درنهایت خاصیت innerHTML این المان به عنوان رشته انکدشده برگشت داده می‌شود.

نحوه استفاده از این متد به صورت زیر است:
console.log(String.htmlEncode("<html>"));
//result:   &lt;html&gt;
و برای حالت استفاده از خاصیت prototype داریم:
String.prototype.htmlEncode = function () {
  var el = document.createElement("div");
  el.innerText = this.toString();
  return el.innerHTML;
};
نحوه استفاده از این متد نیز به صورت زیر است:
console.log("<html>".htmlEncode());
//result:    &lt;html&gt;

متد htmlDecode

با استفاده از مطالب اشاره‌شده در بالا، پیاده‌سازی این متد به صورت زیر است:
String.htmlDecode = function (s) {
  var el = document.createElement("div");
  el.innerHTML = s || '';
  return el.innerText;
};
و به‌صورت خاصیتی از prototype شی String داریم:
String.prototype.htmlDecode = function () {
  var el = document.createElement("div");
  el.innerHTML = this.toString();
  return el.innerText;
};
نحوه استفاده از این متدها هم به صورت زیر است:
console.log(String.htmlDecode("&lt;html&gt;"));
console.log("&lt;html&gt;".htmlDecode());

پیاده‌سازی با استفاده از jQuery

درصورت در دسترس بودن کتابخانه jQuery، کار پیاده‌سازی این متدها بسیار ساده‌تر خواهد شد. برای این‌کار می‌توان از متدهای زیر استفاده کرد:
.
- متد htmlEncode:
String.htmlEncode = function (s) {
  return $('<div/>').text(value).html();
};

String.prototype.htmlEncode = function () {
  return $('<div/>').text(this.toString()).html();
};
- متد htmlDecode:
String.htmlDecode = function (s) {
  return $('<div/>').html(s).text();
};

String.prototype.htmlDecode = function () {
  return $('<div/>').html(this.toString()).text();
};

نکات پایانی

1. با اینکه به نظر می‌رسد در متدهای ارائه شده در بالا، بین نسخه‌های معمولی و نسخه مخصوص jQuery تفاوتی وجود ندارد اما تست زیر نشان می‌دهد که نکات ریزی باعث به‌وجود آمدن برخی تفاوت‌ها می‌شود. رشته زیر را درنظر بگیرید:
var value = "a \n b";
با استفاده از متد htmlEncode معمولی نشان داده شده در بالا، عبارت انکد‌شده رشته فوق به صورت زیر خواهد بود: 
"a <br> b"
می‌بینید که به صورت هوشمندانه‌ای! مقدار n\ به تگ <br> انکد شده است. اما اگر با استفاده از متد نوشته شده با jQuery سعی به انکدکردن این رشته کنیم، می‌بینیم که مقدار n\ بدین صورت انکد نمی‌شود! حال کدام روش درست و استاندارد است؟

در ابتدای این مطلب هم اشاره شده بود که Html Encoding برای کدکردن یکسری کاراکتر غیرمجاز در متون موجود در صفحات HTML بکار می‌رود و معمولا همان 5 کاراکتر اشاره‌شده در بالا به عنوان کاراکترهای اصلی غیرمجاز به حساب می‌آیند. کاراکتر n\ از این نوع کاراکترها محسوب نمی‌شود. هم‌چنین ازآنجاکه عملیات عکس این تبدیل در Decode مربوطه صورت نمی‌گیرد، تبدیل این کاراکتر به معادلش در html اصلا کاری منطقی نیست و باعث خراب شدن متن موردنظر می‌شود.

با استفاده از متدهای HtmlEncode موجود در کلاس‌های دات نت (WebUtility و HtmlUtility که در بالا به آن‌ها اشاره شده بود) عملیات انکدینگ برای این رشته تکرار شد و نتیجه حاصله نشان داد که عبارت n\ در خروجی این متدها نیز انکد نمی‌شود. بنابراین متد نوشته شده با استفاده از jQuery خروجی‌های استانداردتری ارائه می‌دهد.

با کمی تحقیق و بررسی کدهای jQuery مشخص شد که دلیل این تفاوت، در استفاده از متد createTextNode از شی document در متد ()text است. بنابراین برای بهبود متد htmlEncode اولیه داریم:
String.htmlEncode = function (s) {
  var el = document.createElement("div");
  var txt = document.createTextNode(s);
  el.appendChild(txt);
  return el.innerHTML;
};
با استفاده از این متد نتایج مشابه متد نوشته شده با jQuery حاصل خواهد شد.
.
 
2. نکته مهم دیگری که باید بدان توجه داشت برقراری اصل مهم زیر در عملیات انکدینگ است:
String.htmlDecode(String.htmlEncode(myString)) === myString;
حال سعی می‌کنیم که برقراری این شرط را در یک مثال بررسی کنیم:
var myString = "<HTML>";
String.htmlDecode(String.htmlEncode(myString)) === myString;
// result:   true
// --------------------------------------------------------------------------
myString = "<اچ تی ام ال>";
String.htmlDecode(String.htmlEncode(myString)) === myString;
// result:   true
تا اینجا همه چیز ظاهرا درست پیش رفته است. اما حالا مثال زیر را درنظر بگیرید:
myString = "a \r b";
String.htmlDecode(String.htmlEncode(myString)) === myString;
// result:   false
می‌بینید که با وارد شدن کاراکتر r\ کار خراب می‌شود. این نتیجه برای تمامی متدهای جاوا اسکریپتی نشان داده شده صادق است. اما متدهای دات نتی اشاره شده در ابتدای این مطلب با این کاراکتر مشکلی ندارند و نتیجه درستی برمی‌گردانند. بنابراین یک جای کار می‌لنگد!
پس از کمی تحقیق و بررسی بیشتر مشخص شد که مرورگرها در تبدیل کاراکترها، کاراکتر carriage return (یا CR یا همان r\ با کد اسکی 13 یا 0D) را تبدیل به کاراکتر line feed (یا LF یا n\ با کد اسکی 10 یا 0A) می‌کنند. برای آزمایش این نکته می‌توانید از سه خط زیر استفاده کنید:
console.log(escape(String.htmlDecode('\r'))); // result:    %0A  :  it is url encode of character '\n'
console.log(escape(String.htmlDecode('\n'))); // result:    %0A
console.log(escape(String.htmlDecode('\r\n'))); // result:    %0A
با بررسی بیشتر مشخص شد که این تبدیل به محض مقداردهی به یکی از خاصیت‌های یک عنصر DOM صورت می‌گیرد. برای مثال کد زیر را در مرورگرهای مختلف امتحان کنید:
var el = document.createElement('div');
el.innerText = '\r';
console.log(escape(el.innerText)); // result:    %0A
el.innerHTML = '\r';
console.log(escape(el.innerHTML)); // result:    %0A
console.log(escape('\r')); // result:    %0D
با بررسی هایی که من کردم دلیل و یا راه‌حلی برای این مشکل پیدا نکردم!
بنابراین در استفاده از این متدها باید این نکته را مدنظر قرار داد. ازآنجاکه این مشکل حالتی به خصوص دارد نمی‌توان راه‌حلی کلی برای آن ارائه داد. پس برای موقعیت‌های گوناگون با توجه به زوایای روشن‌شده از این مشکل باید به دنبال راه‌حل مناسب بود.
البته ممکن است این اشکال درمورد کاراکترهای دیگری هم وجود داشته باشد که من به آن برخورد نکرده باشم (با درنظر گرفتن تفاوت میان مرورگرهای مختلف ممکن است پیچیده‌تر هم باشد).

نکته: ازآنجاکه برای رفع این مشکل، پیاده‌سازی متد htmlDecode به این کاملی، با عدم استفاده از ویژگی پراپرتی‌های innerHTML و innerText، کاری نسبتا سخت و پیچیده  و طولانی است، بنابراین در بیشتر حالات می‌توان از این مشکل صرف‌نظر کرد! به همین دلیل در اینجا نیز متد دیگری برای رفع این مشکل ارائه نمی‌شود!


3. یک مشکل دیگر که این متدها دارند این است که متاسفانه در متد htmlEncode، از 5 کاراکتر معروف بالا، کاراکترهای ' و " در این متدها اصلا تبدیل نمی‌شوند. همچنین سایر کاراکترهای عنوان‌دار یا کاراکترهای خارج از جدول ASCII (مثلا کاراکترهای با کد 160 تا 255 یا کاراکترهای یونیکد) نیز که معمولا انکد می‌شوند در این متد تغییری نمی‌کنند و به همان صورت برگشت داده می‌شوند.
هرچند متد htmlDecode نشان داده شده در این مطلب، به‌درستی تمامی عبارات معادل (حتی عبارات معادل غیر از 5 کاراکتر نشان داده شده در بالا با هر دو استاندارد ;character-entity&  و  ;code#&) را تبدیل کرده و کاراکتر درست را برمی‌گرداند.

برای اصلاح این مشکل می‌توان متد htmlEncode را کاملا به صورت دستی و مستقیم نوشت و اعمال انکدینگ‌های موردنیاز را با استفاده یک حلقه روی تمام کاراکترها متن موردنظر انجام داد. چیزی شبیه به کد زیر:
String.htmlEncode = function (text) {
  text = text || '';
  var encoded = '';
  for (var i = 0; i < text.length; i++) {
    var c = text[i];
    switch (c) {
      case '<':
        encoded += '&lt;';
        break;
      case '>':
        encoded += '&gt;';
        break;
      case '&':
        encoded += '&amp;';
        break;
      case '"':
        encoded += '&quot;';
        break;
      case "'":
        encoded += '&#39;';
        break;
      default:
        // the upper limit can be removed to support more chars...
        var code = c.charCodeAt();
        if (code >= 160 & code < 256)
          encoded += '&#' + code + ';';
        else
          encoded += c;
    }
  }
  return encoded;
};
روش استفاده شده در متد بالا همانند متد HtmlEncode در کلاس WebUtility است.


کتابخانه‌های موجود

هرچند توضیحات ارائه شده در این مطلب کافی هستند، اما صرفا برای آشنایی با سایر کتابخانه‌های موجود، روش‌های استفاده‌شده در آن‌ها و نقایص و مزایای آن‌ها این قسمت اضافه شده است.

Prototype: این کتابخانه شامل مجموعه‌ای از متدهای کمکی برای راحتی کار در سمت کلاینت است. برای عملیات html encoding دو متد escapeHTML و unescapeHTML دارد که به صورت زیر پیاده شده‌اند:
function escapeHTML() {
  return this.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}

function unescapeHTML() {
  return this.stripTags().replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&');
}
همان‌طور که می‌بینید در این متدها از replace استفاده شده است که برای متن‌های طولانی کندتر از روش‌های نشان داده‌شده در این مطلب است. هم‌چنین عملیات انکد و دیکد را تنها برای 3 کاراکتر < و > و & انجام می‌دهد که نقص بزرگی محسوب می‌شود.

jQuery.string: این پلاگین حاوی چند متد برای کار با رشته‌هاست که یکی از این متدها با نام htmlspecialchars مخصوص عملیات انکدینگ است. در این متد تنها همان 5 کاراکتر اصلی تبدیل می‌شوند. متاسفانه متدی برای decode در این پلاگین وجود ندارد. پیاده‌سازی خلاصه‌شده این کتابخانه تنها برای نمایش نحوه عملکرد متد فوق به صورت زیر است:
var andExp = /&/g,
    htmlExp = [/(<|>|")/g, /(<|>|')/g, /(<|>|'|")/g],
    htmlCharMap = { '<': '&lt;', '>': '&gt;', "'": '&#039;', '"': '&quot;' },
    htmlReplace = function (all, $1) {
  return htmlCharMap[$1];
};
$.extend({
  // convert special html characters
  htmlspecialchars: function (string, quot) {
    return string.replace(andExp, '&amp;').replace(htmlExp[quot || 0], htmlReplace);
  }
});
نحوه استفاده از این متد هم به صورت زیر است:
$.htmlspecialchars("<div>");

string.$: پلاگین دیگری برای jQuery که عملیات مربوط به رشته‌ها را دربر دارد. در این پلاگین برای عملیات انکدینگ دو متد escapeHTML و unescapeHTML به صورت زیر تعریف شده‌اند:
this.escapeHTML = function (s) {
  this.str = this.s(s)
      .split('&').join('&amp;')
      .split('<').join('&lt;')
      .split('>').join('&gt;');
  return this;
};

this.unescapeHTML = function (s) {
  this.str = this.stripTags(this.s(s)).str.replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>');
  return this;
};
همان‌طور که می‌بنید در متد encode این پلاگین از یک روش جالب اما به نسبت ناکارآمد در رشته‌های طولانی، برای استخراج کاراکترهای غیرمجاز استفاده شده است. در این متدها هم تنها 3 کاراکتر & و < و > انکد و دیکد می‌شوند.

encoder.js: کتابخانه نسبتا کاملی برای عملیات انکدینگ رشته‌ها در سمت کلاینت. این کتابخانه علاوه بر encode و decode رشته‌ها متدهایی برای تبدیل html entityها به فرمت عددی‌شان و برعکس، حذف کاراکترهای یونیکد، بررسی اینکه رشته ورودی شامل کاراکترهای انکد شده است، جلوگیری از انکدینک مجدد یک رشته و ... نیز دارد.

htmlEncode: این متد پیاده‌سازی کاملی برای اجرای عملیات Html Encode دارد و محدوده وسیعی از کاراکترها را نیز تبدیل می‌کند. مشاهده عملیات موجود در این متد برای آشنایی با مطالب ظریف‌تر پیشنهاد می‌شود.


مطالب
یافتن خطاهای متداول کدهای جاوا اسکریپتی با غنی سازی تنظیمات کامپایلر TypeScript
یکی از اهداف مهم استفاده‌ی از TypeScript، یافتن خطاهای متداول کدهای جاواسکریپتی، پیش از اجرای آن‌ها در مرورگر است. برای مثال، قطعه کد زیر:
  defaultChecks() {
    const author = { firstName: "Vahid", lastName: "N" };
    console.log(author.lastname);
    author.lastName.trimStart();
    author.firstName.charCodeAt("1");
  }
دارای سه مشکل است که سریعا توسط TypeScript شناسایی می‌شود:


- خاصیت lastname در شیء author وجود خارجی ندارد.
- نوع رشته‌ای، به همراه متد trimStart نیست.
- متد charCodeAt یک عدد را به عنوان پارامتر قبول می‌کند.

اما باید درنظر داشت که بسیاری از قابلیت‌های بررسی کد TypeScript، به صورت پیش‌فرض فعال نیستند که در ادامه آن‌ها را برای یافتن پیش از موعود بسیاری از مشکلات، فعالسازی خواهیم کرد.


نصب افزونه‌ی TSLint در VSCode

جهت مشاهده‌ی بهتر خطاهای کامپایلر TypeScript، پیش از کامپایل نهایی کدها، می‌توان از افزونه‌ی TSLint استفاده کرد. برای نصب آن، ابتدا باید بسته‌ی ذیل را نصب کرد:
  > npm install -g tslint typescript
سپس نیاز است افزونه‌ی آن‌را نیز نصب کنید: https://marketplace.visualstudio.com/items?itemName=eg2.tslint
کار TSLint انجام static code analysis است؛ چیزی شبیه به افزونه‌هایی مانند ری‌شارپر در ویژوال استودیو که راهنماهایی را در مورد بهتر کردن کیفیت کدهای نوشته شده ارائه می‌دهد.


فعالسازی بررسی نال و نوع‌های نال پذیر

 strictNullChecks یکی از مهم‌ترین پرچم‌های تنظیمات کامپایلر تایپ‌اسکریپت است. برای افزودن آن، به فایل tsconfig.json مراجعه کرده و پرچم آن‌را به true تنظیم کنید:
{
    "compilerOptions": {
        "strictNullChecks": true
    }
}
به این ترتیب کامپایلر تایپ‌اسکریپت، بین null ،undefined و سایر نوع‌ها، تفاوت قائل خواهد شد و اگر نوعی نال‌پذیر است، باید آن‌را به صورت صریح type | null تعریف کنید.
برای مثال، متد ذیل را در نظر بگیرید:
  getSessionItem(key: string): any {
    const data = window.sessionStorage.getItem(key);
    return JSON.parse(data);
  }
زمانیکه پرچم strictNullChecks فعال شود، قطعه کد فوق با خطای ذیل کامپایل نخواهد شد:
[ts]
Argument of type 'string | null' is not assignable to parameter of type 'string'.
Type 'null' is not assignable to type 'string'.
const data: string | null
از این جهت که خروجی متد getItem به صورت string | null تعریف شده‌است؛ اما پارامتر متد JSON.parse فقط string را قبول می‌کند. یعنی ممکن است data دریافتی نال باشد که متد JSON.parse قادر به پردازش آن نیست.
برای رفع این مشکل تنها کافی است بررسی کنیم که آیا data نال است یا خیر؟ و اگر خیر، آنگاه آن‌را به متد JSON.parse ارسال کنیم:
  getSessionItem(key: string): any {
    const data = window.sessionStorage.getItem(key);
    if (data) {
      return JSON.parse(data);
    } else {
      return null;
    }
  }
همانطور که ملاحظه می‌کنید، فعالسازی strictNullChecks می‌تواند از بروز بسیاری از خطاهای در زمان اجرای برنامه، جلوگیری کند.


گزارش return‌های فراموش شده

در متد ذیل، یک return فراموش شده وجود دارد و تمام شرط‌های برنامه به یک خروجی مشخص، منتهی نمی‌شوند:
  noImplicitReturns(a: number) {
    if (a > 10) {
      return a;
    }
    // No return in this branch
  }
برای تشخیص زود هنگام یک چنین مشکلاتی می‌توان پرچم noImplicitReturns را در فایل tsconfig.json به true تنظیم کرد:
{
    "compilerOptions": {
        "noImplicitReturns": true
    }
}
پس از این تنظیم، کامپایلر در مورد متد noImplicitReturns، خطای ذیل را صادر می‌کند:
 [ts] Not all code paths return a value.


تشخیص کدهای مرده

قطعه کدی که پس از یک return قرار بگیرد، یک کد مرده نامیده می‌شود. با تنظیم پرچم allowUnreachableCode در فایل tsconfig.json به false، می‌توان کامپایلر TypeScript را وادار کرد تا اینگونه موارد را به عنوان خطا گزارش کند:
{
    "compilerOptions": {
        "allowUnreachableCode": false
    }
}
پس از این فعالسازی، کامپایلر TypeScript دو خطای [ts] Unreachable code detected را در مورد قطعه کد ذیل صادر می‌کند:
    allowUnreachableCode() {
      if (false) {
        console.log("Unreachable code");
      }
      const a = 1;
      if (a > 0) {
        return 10; // reachable code
      }
      return 0;
      console.log("Unreachable code");
    }
مورد اول مربوط به بدنه‌ی if ایی است که شرط آن false است و این بدنه هیچگاه فراخوانی نمی‌شود و مورد دوم، به سطر پس از return آخر مرتبط است که آن مورد نیز یک کد مرده محسوب می‌شود.


تشخیص پارامترها و متغیرهای استفاده نشده

دو متد ذیل را درنظر بگیرید:
  unusedLocals() {
    const a = "foo"; // Error: 'a' is declared but its value is never read
    return "bar";
  }

  unusedParameters(n: number) {
    n = 0; // Never read
  }
در اولی متغیر a تعریف شده‌است، اما هیچگاه استفاده نشده‌است. در متد دوم، پارامتر n نیز هیچگاه خوانده نشده‌است و وجود آن بی‌مصرف است؛ حتی ممکن است نشان فراموش شدن استفاده‌ی از آن و مقدار دهی اشتباه آن باشد.
برای فعالسازی بررسی یک چنین مواردی باید دو پرچم ذیل را در فایل tsconfig.json به true تنظیم کرد:
{
    "compilerOptions": {
        "noUnusedLocals": true,
        "noUnusedParameters": true
    }
}
پس از آن، کامپایلر دو خطای ذیل را در مورد متدهای فوق، گزارش می‌کند:
[ts] 'a' is declared but its value is never read.
[ts] 'n' is declared but its value is never read.


یافتن خواصی که نباید در یک شیء وجود داشته باشند

در مثال ذیل، خاصیت baz در تعاریف اصلی نوع‌های x و y وجود ندارد:
  excessPropertyForObjectLiterals() {
    let x: { foo: number };
    x = { foo: 1, baz: 2 };  // Error, excess property 'baz'
    let y: { foo: number, bar?: number };
    y = { foo: 1, baz: 2 };  // Error, excess property 'baz'
  }
برای فعالسازی یافتن یک چنین مواردی که در بسیاری از حالات ممکن است ناشی از اشتباهات تایپی نیز باشند، می‌توان پرچم suppressExcessPropertyErrors را در فایل tsconfig.json به false تنظیم کرد:
{
    "compilerOptions": {
        "suppressExcessPropertyErrors": false
    }
}
پس از آن برای نمونه در مورد انتساب اول، یک چنین پیام خطایی از طرف کامپایلر TypeScript صادر خواهد شد:
[ts]
Type '{ foo: number; baz: number; }' is not assignable to type '{ foo: number; }'.
Object literal may only specify known properties, and 'baz' does not exist in type '{ foo: number; }'.
(property) baz: number


یافتن breakهای فراموش شده در عبارات switch

در مثال زیر، یک break فراموش شده‌است:
  fallthroughCasesInSwitchStatement(a: number) {
    switch (a) {
      case 0:
        break;

      case 1:
        a += 1;

      case 2:
        a += 2;
        break;
    }
  }
برای تشخیص آن توسط کامپایلر TypeScript باید پرچم noFallthroughCasesInSwitch را در فایل tsconfig.json به true تنظیم کرد:
{
    "compilerOptions": {
        "noFallthroughCasesInSwitch": true
    }
}
پس از آن کامپایلر TypeScript خطای «[ts] Fallthrough case in switch» را در مورد متد فوق صادر می‌کند.


یافتن ایندکس‌های تعریف نشده‌ی در اشیاء

در مثال زیر، شیء x دارای خاصیت b نیست؛ اما دقیقا با این ایندکس مورد استفاده قرار گرفته‌است:
  indexingObjectsLackingIndexSignatures() {
    const x = { a: 0 };
    x["a"] = 1; // ok
    x["b"] = 1; // Error, type '{ a: number; }' has no index signature.
  }
برای تشخیص یک چنین خطاهایی می‌توان پرچم suppressImplicitAnyIndexErrors را در فایل tsconfig.json به false تنظیم کرد:
{
    "compilerOptions": {
        "suppressImplicitAnyIndexErrors": false
    }
}
پس از آن کامپایلر TypeScript خطای ذیل را در مورد دسترسی به ایندکسی که با امضای شیء سازگاری ندارد، صادر می‌کند:
[ts] Element implicitly has an 'any' type because type '{ a: number; }' has no index signature.


اجبار به تعریف صریح نوع‌ها در TypeScript

عمده‌ی قابلیت TypeScript در یافتن خطاها به تعاریف نوع‌ها و راهنمایی کامپایلر آن در این زمینه بر می‌گردد. اما چون این زبان سازگاری کاملی را با JavaScript دارد، تعریف نوع‌ها در آن اجباری نیست و در این حالت اگر نوعی تعریف نشده باشد، به any تفسیر می‌شود. جهت اجبار به تعریف نوع‌ها در TypeScript می‌توان پرچم noImplicitAny را در فایل tsconfig.json به true تنظیم کرد:
{
    "compilerOptions": {
        "noImplicitAny": true
    }
}
در این حالت دیگر قطعه کد ذیل کامپایل نخواهد شد:
  noImplicitAny(args) { // Error: Parameter 'args' implicitly has an 'any' type.
    console.log(args);
  }
برای رفع این مشکل می‌توان نوع args را به صورت صریحی مشخص کرد:
  noImplicitAnyArgs(args: string[]) { // ok with the type information
    console.log(args);
  }


یک نکته‌ی تکمیلی
اگر از دستور ng build --watch برای ساخت برنامه‌های Angular استفاده می‌کنید، تغییرات فوق زمانی تاثیر داده خواهند شد که یکبار این برنامه را بسته و مجددا اجرا کنید.
مطالب
ارتقاء به Angular 6: بررسی تغییرات Angular CLI
اولین مرحله‌ی ارتقاء به Angular 6، به روز رسانی Angular CLI 1.x به نگارش 6 آن است. این شماره نگارش نیز با شماره نگارش Angular یکی شده‌است و دیگر 1x نیست. CLI 6.0 فقط پروژه‌های Angular 5.x و 6x را پشتیبانی می‌کند و برای نصب آن نیاز به حداقل NodeJS 8.9 و NPM 5.5 را خواهید داشت. بنابراین ابتدا دستورات زیر را صادر کرده و اگر هنوز از نگارش‌های قدیمی این ابزارها استفاده می‌کنید، قبل از هر کاری باید آن‌ها را به روز رسانی کنید:
node -v
npm -v
این نگارش از CLI در پشت صحنه از Webpack 4 استفاده می‌کند که نسبت به نگارش‌های پیشین آن بسیار سریعتر است، tree-shaking بهتری را انجام می‌دهد و در نهایت سبب تولید برنامه‌هایی با حجم کمتر و با سرعت build بیشتری خواهد شد.


فایل پیشین angular-cli.json حذف و فایل جدید angular.json بجای آن معرفی شده‌است

یکی از مهم‌ترین تغییرات CLI 6.0 نسبت به نگارش‌های قبلی آن، پشتیبانی از چندین پروژه است و به همین منظور ساختار فایل تنظیمات آن‌را به طور کامل تغییر داده‌اند و اگر دستور ng new project1 را صادر کنید، دیگر از فایل پیشین angular-cli.json خبری نبوده و بجای آن فایل جدید angular.json قابل مشاهده‌است:
{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "project1": {
در اینجا ساختار ابتدای فایل angular.json را مشاهده می‌کنید که در آن مفهوم projects (و یا workspace در اینجا) به همراه project1 جدیدی که ایجاد کردیم، به عنوان زیر مجموعه‌ی آن قابل مشاهده‌است.
مزیت مهم این قابلیت، امکان ایجاد libraries است که به صورت توکار از این نگارش پشتیبانی می‌شود و می‌توان اجزایی مانند components ،directives ،pipes و services اشتراکی را در یک یا چندین کتابخانه قرار داد و سپس از آن‌ها در پروژه‌ی اصلی و یا چندین پروژه‌ی متصل استفاده کرد.
نگارش 6 در پشت صحنه، پروژه‌ی موفق ng-packagr را به مجموعه‌ی CLI اضافه کرده‌است و از این پس توسط خود CLI می‌توان کتابخانه‌های استاندارد Angular را تولید کرد. این مورد، مزیت استاندارد سازی کتابخانه‌های npm حاصل را نیز به همراه دارد؛ مشکلی که گاهی از اوقات به علت عدم رعایت این ساختار، با بسته‌های فعلی npm مخصوص Angular وجود دارند. برای مثال بدون استفاده‌ی از این ابزار، نیاز است مستندات 13 صفحه‌ای ساخت کتابخانه‌های Angular را سطر به سطر پیاده سازی کنید که توسط CLI 6.0، به صورت خودکار ایجاد و مدیریت می‌شود.

بنابراین اکنون سؤال مهمی که مطرح می‌شود این است: آیا باید فایل angular-cli.json پیشین را به صورت دستی به این فایل جدید به روز رسانی کرد و چگونه؟


به روز رسانی تمام بسته‌های سراسری سیستم

در ادامه پیش از هر کاری نیاز است تمام بسته‌های سراسری npm ایی را که هر از چندگاهی به سیستم خود اضافه کرده‌اید، به روز رسانی کنید. برای مشاهده‌ی لیست موارد تاریخ مصرف گذشته‌ی آن‌ها، دستور زیر را صادر کنید:
 npm outdated -g --depth=0
و برای به روز رسانی یکجای آن‌ها نیاز است دستور زیر اجرا شود:
 npm update -g


به روز رسانی خودکار ساختار فایل angular-cli.json

این به روز رسانی توسط CLI 6.0 به صورت خودکار پشتیبانی می‌شود و شامل این مراحل است:
ابتدا نیاز است بسته‌ی سراسری آن‌را به روز رسانی کرد:
 npm i -g @angular/cli
سپس وارد پوشه‌ی اصلی پروژه‌ی خود شده و این دستور را به صورت محلی نیز وارد کنید:
 npm install --save-dev @angular/cli@latest
با اینکار بسته‌ی CLI محلی پروژه به روز شده و اکنون می‌توانیم از قابلیت جدید آن که ng update نام دارد، استفاده کنیم. برای این منظور دستورات ذیل را به ترتیب اجرا کنید:
ng update @angular/cli
ng update @angular/core
ng update rxjs
دستور اول کار تبدیل خودکار فایل angular-cli.json قدیمی را به ساختار جدید آن انجام میدهد؛ با این لاگ:
DELETE .angular-cli.json
CREATE angular.json (4273 bytes)
UPDATE karma.conf.js (1008 bytes)
UPDATE src/tsconfig.spec.json (322 bytes)
UPDATE package.json (2076 bytes)
UPDATE tslint.json (3217 bytes)
دستور دوم بسته‌های هسته‌ی angular را به روز رسانی می‌کند و دستور سوم کار به روز رسانی کتابخانه‌ی rxjs را انجام می‌دهد.
لیست سایر بسته‌هایی را که می‌توان توسط این دستور به روز رسانی کرد، با اجرا دستور ng update می‌توانید مشاهده کنید. برای مثال اگر از Angular Material نیز استفاده می‌کنید، دستور به روز رسانی آن به صورت زیر است:
 ng update @angular/material


مشکل! دستور ng update کار نمی‌کند!

اگر پروژه‌ی شما صرفا مبتنی بر بسته‌های اصلی Angular باشد، مراحل یاد شده‌ی فوق را با موفقیت به پایان خواهید رساند. اما اگر از کتابخانه‌های ثالثی استفاده کرده باشید، منهای دستور «ng update @angular/cli» که کار تولید فایل جدید angular.json را انجام می‌دهد، مابقی با خطاهایی مانند «incompatible peer dependency» و یا «Invalid range:>=2.3.1 <3.0.0||>=4.0.0» متوقف می‌شوند.
در یک چنین حالتی نیاز است ابتدا وابستگی‌های محلی پروژه را به روز کرد و سپس دستورات ng update را تکرار نمود. برای این منظور ابتدا بسته‌ی npm-check-updates را نصب کنید:
 npm install npm-check-updates -g
کار آن به روز رسانی خودکار بسته‌های npm یک پروژه است. پس از آن دستورات زیر را صادر نمائید:
ncu -u
npm install
دستور اول تمام شماره نگارش‌های بسته‌های موجود در فایل package.json را به صورت خودکار به آخرین نگارش آن‌ها روز رسانی می‌کند و دستور دوم این بسته‌های جدید را دریافت و نصب خواهد کرد.
اکنون تمام وابستگی‌های محلی پروژه‌ی شما به صورت خودکار به آخرین نگارش آن‌ها به روز رسانی شده‌اند و اینبار اگر دستورات ذیل را اجرا کنید، با خطاهای یاد شده مواجه نخواهید شد:
ng update @angular/core
ng update rxjs

البته اگر در این حالت برنامه را کامپایل کنید، کار نخواهد کرد. علت اصلی آن به به‌روز رسانی rxjs به نگارش 6 آن مرتبط می‌شود که در مطلب بعدی پیگیری خواهد شد و این نگارش شامل حذفیات بسیاری است در جهت کاهش حجم آن، یکپارچکی و یک دست شدن syntax آن و همچنین بهبود قابل ملاحظه‌ی کارآیی آن. البته پیشنیاز الزامی آن آشنایی با pipe-able operators است. علت دیگر کامپایل نشدن برنامه هم می‌تواند عدم استفاده از HttpClient نگارش 4.3 به بعد باشد.


خاموش کردن اخطار «TypeScript version mismatch»

اگر نگارش TypeScript نصب شده‌ی در سیستم به صورت سراسری، با نگارش محلی پروژه‌ی شما یکی نباشد، اخطار «TypeScript version mismatch» را دریافت می‌کنید. روش خاموش کردن آن در CLI جدید با اجرای دستور زیر است:
 ng config cli.warnings.typescriptMismatch false
البته نگارش 6 نیاز به TypeScript 2.7.2 ذکر شده‌ی در package.json پروژه‌ی محلی را دارد؛ وگرنه اصلا شروع به کامپایل برنامه نمی‌کند:
{
  "devDependencies": {
    "typescript": "~2.7.2"
  }
}
بهترین راه برای یافتن این شماره، اجرای دستور ng new projet1 در یک پوشه‌ی خالی است و سپس مقایسه‌ی محتوای فایل package.json آن با فایل پروژه‌ی موجود.


 تغییرات ng build در نگارش 6

در نگارش 6، مفهوم پیشین environments به configurations تغییر یافته‌است و اینبار در فایل جدید angular.json تنظیم می‌شوند:
{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "angular-template-driven-forms-lab": {
          "configurations": {
            "production": {
              "optimization": true,
              "outputHashing": "all",
              "sourceMap": false,
              "extractCss": true,
              "namedChunks": false,
              "aot": true,
              "extractLicenses": true,
              "vendorChunk": false,
              "buildOptimizer": true,
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ]
            }
          }
        },
و پیشتر اگر برای تنظیم محیط build از سوئیچ env استفاده می‌شد:
 ng build --env staging
اکنون این سوئیچ به configuration تغییر یافته‌است و نام آن از مداخل configurations فایل angular.json، مانند مثال فوق که «production» نام دارد، باید دریافت و تنظیم شود:
 ng build --configuration staging
و یا برای مثال دستور «ng build --env=prod» دیگر اجرا نمی‌شود و env=prod حذف شده‌است و اکنون اجرای  ng build --prod مانند اجرای دستور ng build --configuration=prod است.


تغییرات نام‌های نهایی تولیدی

در CLI 6.0، نام‌های نهایی تولیدی دیگر به همراه bundle  و یا chunk نیستند. برای مثال دستور ng build یک چنین خروجی را تولید می‌کند:
 >ng build --watch

Date: 2018-05-05T09:10:50.158Z
Hash: a43eab94ff01539b8592
Time: 31733ms
chunk {main} main.js, main.js.map (main) 9.38 kB [initial] [rendered]
chunk {polyfills} polyfills.js, polyfills.js.map (polyfills) 226 kB [initial] [rendered]
chunk {runtime} runtime.js, runtime.js.map (runtime) 5.4 kB [entry] [rendered]
chunk {styles} styles.js, styles.js.map (styles) 15.6 kB [initial] [rendered]
chunk {vendor} vendor.js, vendor.js.map (vendor) 3.05 MB [initial] [rendered]

و هش‌های حالت prod به صورت زیر تولید و به نام فایل اضافه می‌شوند:
>ng build --prod --watch

Date: 2018-05-05T09:17:01.803Z
Hash: f25fd6788a4969c52b70
Time: 73279ms
chunk {0} runtime.6afe30102d8fe7337431.js (runtime) 1.05 kB [entry] [rendered]
chunk {1} styles.34c57ab7888ec1573f9c.css (styles) 0 bytes [initial] [rendered]
chunk {2} polyfills.6c08419970f9e4781b69.js (polyfills) 59.4 kB [initial] [rendered]


ساده شدن افزودن وابستگی‌های ثالث به پروژه‌های CLI

برای نصب یک کتابخانه‌ی ثالث، پیشتر می‌بایستی ابتدا بسته‌ی npm آن جداگانه نصب و سپس فایل config برنامه، جهت معرفی مداخل آن، ویرایش می‌شد. اکنون دستور جدید ng add تمام این مراحل را به صورت خودکار انجام می‌دهد:
 ng add @angular/material
برای نمونه دستور فوق نه تنها فایل‌های Angular Material را دریافت می‌کند، بلکه فایل‌های CSS و ماژول‌های مرتبط با آن‌را نیز import خواهد کرد. البته این مورد از کلید ng-add فایل package.json بسته‌ی در حال نصب دریافت و تنظیم می‌شود و کتابخانه‌های جدید باید خود را بر این اساس وفق دهند.
مطالب
Angular CLI - قسمت پنجم - ساخت و توزیع برنامه
ساخت و توزیع برنامه‌های Angular یکی از مهم‌ترین و بحث برانگیزترین قسمت‌های نگارش‌های جدید آن است و به ازای هر پروژه و قالبی که برای آن توسط گروه‌های مختلف ارائه شده‌است، روش‌های متفاوتی را شاهد خواهید بود. در ادامه روش توصیه شده‌ی توسط تیم Angular را که مبتنی است بر webpack و به صورت خودکار توسط Angular CLI مدیریت می‌شود، بررسی خواهیم کرد.


ساخت (Build) برنامه‌های Angular

Angular CLI کار ساخت و کامپایل برنامه را به صورت خودکار انجام داده و خروجی را در مسیری مشخص درج می‌کند. در اینجا می‌توان گزینه‌هایی را بر اساس نوع کامپایل مدنظر مانند کامپایل برای حالت توسعه و یا کامپایل برای حالت توزیع نهایی، انتخاب کرد. همچنین مباحث bundling و یکی کردن تعداد بالای ماژول‌های برنامه در آن لحاظ می‌شوند تا برنامه در حالت توزیع نهایی، سبب 100ها رفت و برگشت به سرور برای دریافت ماژول‌های مختلف آن نشود. به علاوه مباحث uglification (به نوعی obfuscation کدهای جاوا اسکریپتی نهایی) و tree-shaking (حذف کدهایی که در برنامه استفاده نشده‌اند؛ یا کدهای مرده) نیز پیاده سازی می‌شوند. با انجام tree-shaking‌، نه تنها اندازه‌ی توزیع نهایی به کاربر کاهش پیدا می‌کند، بلکه مرورگر نیز حجم کمتری از کدهای جاوااسکریپتی را باید تفسیر کند.
برای شروع می‌توان از دستور ذیل برای مشاهده‌ی تمام گزینه‌های مهیای ساخت برنامه استفاده کرد:
> ng build --help
ذکر تنهای دستور ng build‌، بدون هیچ گزینه‌ای، برای حالت «توسعه‌ی» برنامه بسیار ایده‌آل است (و دقیقا به معنای صدور دستور ng build --dev است). در این حالت خروجی کامپایل شده‌ی برنامه در پوشه‌ی dist تولید می‌شود. اگر از قسمت دوم این سری به خاطر داشته باشید، نام این پوشه‌ی خروجی، جزئی از تنظیمات فایل angular-cli.json. است:
"apps": [
{
   "outDir": "dist",
زمانیکه دستور ng build‌  صادر شود، این فایل‌ها را در پوشه‌ی dist خواهید یافت:

فایل 
توضیح 
 inline.bundle.js   WebPack runtime
از آن برای بارگذاری ماژول‌های برنامه و چسباندن قسمت‌های مختلف به یکدیگر استفاده می‌شود. 
 main.bundle.js   شامل تمام کدهای ما است. 
 polyfills.bundle.js   Polyfills - جهت پشتیبانی از مرورگرهای مختلف.
 styles.bundle.js    شامل بسته بندی تمام شیوه نامه‌های برنامه است 
vendor.bundle.js  کدهای کتابخانه‌های ثالث مورد استفاده و همچنین خود Angular، در اینجا بسته بندی می‌شوند. 
 

روشی برای بررسی محتوای bundleهای تولید شده

تولید bundleها در جهت کاهش رفت و برگشت‌های به سرور و بالا بردن کارآیی برنامه ضروری هستند؛ اما دقیقا این بسته بندی‌ها شامل چه اطلاعاتی می‌شوند؟ این اطلاعات را می‌توان از فایل‌های source map تولیدی استخراج کرد و برای این منظور می‌توان از برنامه‌ی source-map-explorer استفاده کرد.

روش نصب عمومی آن:
 > npm install -g source-map-explorer
روش اجرا:
 > source-map-explorer dist/main.bundle.js
پس از آن یک گزارش HTML ایی از محتوای bundle مدنظر تولید می‌شود.


یک مثال: ساخت برنامه‌ی مثال قسمت چهارم - تنظیمات مسیریابی در حالت dev

در ادامه، کار Build همان مثالی را که در قسمت قبل توضیح داده شد، بررسی می‌کنیم. برای این منظور از طریق خط فرمان به ریشه‌ی پوشه‌ی اصلی پروژه وارد شده و دستور ng build را صادر کنید. یک چنین خروجی را مشاهده خواهید کرد:
 D:\Prog\angular-routing>ng build
Hash: 123cae8bd8e571f44c31
Time: 33862ms
chunk {0} polyfills.bundle.js, polyfills.bundle.js.map (polyfills) 158 kB {4} [initial] [rendered]
chunk {1} main.bundle.js, main.bundle.js.map (main) 14.7 kB {3} [initial] [rendered]
chunk {2} styles.bundle.js, styles.bundle.js.map (styles) 9.77 kB {4} [initial] [rendered]
chunk {3} vendor.bundle.js, vendor.bundle.js.map (vendor) 2.34 MB [initial] [rendered]
chunk {4} inline.bundle.js, inline.bundle.js.map (inline) 0 bytes [entry] [rendered]
و اگر فایل index.html تولیدی آن‌را بررسی کنید، تنها الحاق همین 4 فایل js تولیدی را مشاهده می‌نمائید:
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>AngularRouting</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <app-root>Loading...</app-root>
<script type="text/javascript" src="inline.bundle.js">
</script><script type="text/javascript" src="polyfills.bundle.js">
</script><script type="text/javascript" src="styles.bundle.js">
</script><script type="text/javascript" src="vendor.bundle.js">
</script><script type="text/javascript" src="main.bundle.js"></script>
</body>
</html>

یک نکته: زمانیکه دستور ng serve -o صادر می‌شود، در پشت صحنه دقیقا همین دستور ng build صادر شده و اطلاعات را درون حافظه تشکیل می‌دهد. اما اگر کار ng build را دستی انجام دهیم، اینبار ng serve -o اطلاعات را از پوشه‌ی dist دریافت می‌کند. بنابراین در حین کار با ng serve -o نیازی به build دستی پروژه نیست.

سؤال: چرا حجم فایل endor.bundle.js اینقدر بالا است و شامل چه اجزایی می‌شود؟
نکته‌ای که در اینجا وجود دارد، حجم بالای فایل vendor.bundle.js آن است که 2.34 MB می‌باشد:


چون دستور ng build بدون پارامتری ذکر شده‌است، برنامه را برای حالت توسعه Build می‌کند و به همین جهت هیچگونه بهینه سازی در این مرحله صورت نخواهد گرفت. برای بررسی محتوای این فایل می‌توان دستور ذیل را در ریشه‌ی اصلی پروژه صادر کرد:
 > source-map-explorer dist/vendor.bundle.js
پس از اجرای این دستور، بلافاصله مرورگر پیش فرض سیستم اجرا شده و گزارشی را ارائه می‌دهد.


همانطور که مشاهده می‌کنید، در حالت بهینه سازی نشده و Build برای توسعه، کامپایلر Angular حدود 41 درصد حجم فایل vendor.bundle.js را تشکیل می‌دهد. به علاوه ماژول‌ها و قسمت‌هایی را ملاحظه می‌کنید که اساسا برنامه‌ی فعلی مثال ما از آن‌ها استفاده نمی‌کند؛ مانند http، فرم‌ها و غیره.


سفارشی سازی Build برای محیط‌های مختلف

اگر به پروژه‌ی تولید شده‌ی توسط Angular CLI دقت کنید، حاوی پوشه‌ای است به نام src\environments


هدف از فایل‌های environment برای نمونه تغییر آدرس توزیع برنامه در حالت توسعه و ارائه نهایی است.
همچنین در اینجا می‌توان نحوه‌ی بهینه سازی فایل‌های تولیدی را توسط Build Targets مشخص کرد و اینکار توسط ذکر پرچم prod-- (مخفف production) صورت می‌گیرد.
در ادامه، تفاوت‌های دستورهای ng build و ng build --prod را ملاحظه می‌کنید:
- با اجرای ng build، از فایل environment.ts استفاده می‌شود؛ برخلاف حالت اجرای ng build --prod که از فایل environment.prod.ts استفاده می‌کند.
- Cache-busting در حالت ارائه‌ی نهایی، به تمام اجزای پروژه اعمال می‌شود؛ اما در حالت توسعه فقط برای تصاویر قید شده‌ی در فایل‌های css.
- فایل‌های source map فقط برای حالت توسعه تولید می‌شوند.
- در حالت توسعه، cssها داخل فایل‌های js تولیدی قرار می‌گیرند؛ اما در حالت ارائه‌ی نهایی به صورت فایل‌های css بسته بندی می‌شوند.
- در حالت توسعه برخلاف حالت ارائه‌ی نهایی، کار uglification انجام نمی‌شود.
- در حالت توسعه برخلاف حالت ارائه‌ی نهایی، کار tree-shaking یا حذف کدهای مرده و بدون ارجاع، انجام نمی‌شود.
- در حالت توسعه برخلاف حالت ارائه‌ی نهایی، کار AOT انجام نمی‌شود. در اینجا AOT به معنای Ahead of time compilation است.
- در هر دو حالت توسعه و ارائه‌ی نهایی کار bundling و دسته بندی فایل‌ها انجام خواهد شد.

به همین جهت است که ng build سریع است؛ اما حجم بالاتری را هم تولید می‌کند. چون بسیاری از بهینه سازی‌های حالت ارائه‌ی نهایی را به همراه ندارد.


دستورات build برای حالت توسعه و ارائه‌ی نهایی

برای حالت توسعه، هر 4 دستور ذیل یک مفهوم را دارند و به همین جهت مورد ng build متداول‌تر است:
>ng build --target=development --environment=dev
>ng build --dev -e=dev
>ng build --dev
>ng build

برای حالت ارائه‌ی نهایی، هر 3 دستور ذیل یک مفهوم را دارند و به همین جهت مورد ng build --prod متداول‌تر است:
>ng build --target=production --environment=prod
>ng build --prod -e=prod
>ng build --prod

همچنین هر کدام از این دستورات را توسط پرچم‌های ذیل نیز می‌توان سفارشی سازی کرد:

 پرچم  مخفف  توضیح
 sourcemap--  sm-  تولید سورس‌مپ
aot--    Ahead of Time compilation 
watch--  w-  تحت نظر قرار دادن فایل‌ها و ساخت مجدد
environment--  e-  محیط ساخت
 target--  t-  نوع ساخت
 dev--    مخفف نوع ساخت جهت توسعه
 prod--     مخفف نوع ساخت جهت ارائه نهایی

برای مثال در حالت prod، سورس‌مپ‌ها تولید نخواهند شد. اگر علاقمندید تا این فایل‌ها نیز تولید شوند، پرچم souremap را نیز ذکر کنید.
و یا اگر برای حالت dev می‌خواهید AOT را فعالسازی کنید، پرچم aot-- را در آنجا قید کنید.


یک مثال: ساخت برنامه‌ی مثال قسمت چهارم - تنظیمات مسیریابی در حالت prod

تا اینجا خروجی حالت dev ساخت برنامه‌ی قسمت چهارم را بررسی کردیم. در ادامه دستور ng build --prod را در ریشه‌ی پروژه صادر می‌کنیم:
 D:\Prog\angular-routing>ng build --prod
Hash: f5bd7fd555a85af8a86f
Time: 39932ms
chunk {0} polyfills.18173234f9641113b9fe.bundle.js (polyfills) 158 kB {4} [initial] [rendered]
chunk {1} main.c6958def7c5f51c45261.bundle.js (main) 50.3 kB {3} [initial] [rendered]
chunk {2} styles.d41d8cd98f00b204e980.bundle.css (styles) 69 bytes {4} [initial] [rendered]
chunk {3} vendor.b426ba6883193375121e.bundle.js (vendor) 1.37 MB [initial] [rendered]
chunk {4} inline.8cec210370dd3af5f1a0.bundle.js (inline) 0 bytes [entry] [rendered]


همانطور که ملاحظه می‌کنید، اینبار نه تنها حجم فایل‌ها به میزان قابل ملاحظه‌ای کاهش پیدا کرده‌اند، بلکه این نام‌ها به همراه یک سری hash هم هستند که کار cache-busting (منقضی کردن کش مرورگر، با ارائه‌ی نگارشی جدید) را انجام می‌دهند.

در ادامه اگر بخواهیم مجددا برنامه‌ی source-map-explorer را جهت بررسی محتوای فایل‌های js اجرا کنیم، به خطای عدم وجود sourcemapها خواهیم رسید (چون در حالت prod، به صورت پیش فرض غیرفعال هستند). به همین‌جهت برای این مقصود خاص نیاز است از پرچم فعالسازی موقت آن استفاده کرد:
> ng build --prod --sourcemap
> source-map-explorer dist/vendor.b426ba6883193375121e.bundle.js


همانطور که در تصویر نیز مشخص است، اینبار کامپایلر Angular به همراه تمام ماژول‌هایی که در برنامه ارجاعی به آن‌ها وجود نداشته‌است، حذف شده‌اند و کل حجم بسته‌ی Angular به 366 KB کاهش یافته‌است.


بررسی دستور ng serve

تا اینجا برای اجرای برنامه در حالت dev از دستور ng serve -o استفاده کرده‌ایم. کار ارائه‌ی برنامه توسط این دستور، از محتوای کامپایل شده‌ی درون حافظه با مدیریت webpack انجام می‌شود. به همین جهت بسیار سریع بوده و قابلیت live reload را ارائه می‌دهد (نمایش آنی تغییرات در مرورگر، با تغییر فایل‌ها).
همانند تمام دستورات دیگر، اطلاعات بیشتری را در مورد این دستور، از طریق راهنمای آن می‌توان به دست آورد:
 > ng serve --help

که شامل این موارد هستند (علاوه بر تمام مواردی را که در حالت ng build می‌توان مشخص کرد؛ مثلا ng serve --prod -o):

 پرچم مخفف
توضیح
 open-- o-
بازکردن خودکار مرورگر پیش فرض.
حالت پیش فرض آن گشودن مرورگر توسط خودتان است و سپس مراجعه‌ی دستی به آدرس برنامه. 
 port--  p-  تغییر پورت پیش فرض مانند ng server -p 8626 
 live-reload--  lr-   فعال است مگر اینکه آن‌را با false مقدار دهی کنید.
 ssl--    ارائه به صورت HTTPS
 proxy-config--  pc-  Proxy configuration file 


استخراج فایل تنظیمات webpack از Angular CLI

Angular CLI برای مدیریت build، در پشت صحنه از webpack استفاده می‌کند. فایل تنظیمات آن نیز جزئی از فایل‌های توکار این ابزار است و قرار نیست به صورت پیش فرض و مستقیم توسط پروژه‌ی جاری ویرایش شود. به همین جهت آن‌را در ساختار پروژه‌ی تولید شده، مشاهده نمی‌کنید.
اگر علاقمند به سفارشی سازی بیشتر این تنظیمات پیش فرض باشید، ابتدا باید آن‌را اصطلاحا eject کنید و سپس می‌توان آن‌را ویرایش کرد:
 > ng eject
Ejection was successful.

To run your builds, you now need to do the following commands:
- "npm run build" to build.
- "npm run test" to run unit tests.
- "npm start" to serve the app using webpack-dev-server.
- "npm run e2e" to run protractor.

Running the equivalent CLI commands will result in an error.
============================================
Some packages were added. Please run "npm install".
همانطور که مشاهده می‌کنید عنوان کرده‌است که از این پس خودتان باید بسیاری از مسایل را به صورت دستی مدیریت کنید و Angular CLI دیگر آن‌ها را به صورت خودکار مدیریت نمی‌کند و دیگر دستورات ng build و ng serve کار نخواهند کرد (این تغییرات در فایل package.json درج می‌شوند).
در این حالت است که فایل webpack.config.js به ریشه‌ی پروژه جهت سفارشی سازی شما اضافه خواهد شد. همچنین فایل‌های .angular-cli.json، package.json نیز جهت درج این تغییرات ویرایش می‌شوند.

و اگر در این لحظه پشیمان شده‌اید (!) فقط کافی است تا این مرحله‌ی جدید commit شده‌ی به مخزن کد را لغو کنید و باز هم به همان Angular CLI قبلی می‌رسید.
مطالب
بررسی مشکلات AngularJS 1.x
از اولین مقاله‌ای که در مورد AngularJS در این سایت منتشر کردم، بیش از دو سال می‌گذرد. در آن زمان فقط از این فریمورک تعریف و تمجید کردم؛ اما بد نیست بعد از چند تجربه‌ی کاری دلایل تنفری را که نسبت به آن پیدا کرده ام، نیز بیان کنم.
اگر عبارت why I hate angularjs را در گوگل جستجو کنید، می‌بینید که فقط من این عقیده را پیدا نکرده‌ام و افراد دیگری نیز هستند که مثل من فکر می‌کنند و حتی از لحاظ فنی AngularJS را به چالش کشیده‌اند. برای مثال سایت I hate angular بیشتر مقالاتی را که ضد AngularJS هستند، گردآوری کرده است و برای بررسی مشکلات Angular می‌تواند شروع خوبی باشد.
البته قصد ندارم که از نظر فنی Angular را نقد کنم؛ فقط قصد به اشتراک گذاری یک سری از مشکلات توسعه‌ی Single Page Application‌ها را با استفاده از فریمورک Angular، دارم و این را در نظر داشته باشید که بعضی از این مشکلات در هنگام توسعه SPA‌ها با فریمورک‌هایی از این دست، گریبان‌گیر شما می‌شوند و الزاما ربطی به AngularJS ندارند.

سازگار نبودن افزونه‌های jQuery با Angular

برنامه‌های واقعی فقط از تعدادی ng-repeat تشکیل نشده‌اند که ما از دیدن آن‌ها ذوق زده شویم. خواسته یا ناخواسته مجبوریم در برنامه‌های وب خودمان از افزونه‌های محبوب جی‌کوئری نیز استفاده کنیم. خوب، خیلی هم خوب! چندین راه حل پیش روی ماست:

روش اول - نادیده گرفتن angular
انگار نه انگار که از angular استفاده می‌کنیم و افزونه مورد نظر را بدون در نظر گرفتن وجود angular، کاملا عادی فراخوانی کنیم.
نتیجه: ممکن است بعضی  وقت‌ها جواب بدهد، ولی اکثر مواقع، نتیجه عجیب غریب است و خطا‌ها قابل فهم نیستند و توانایی اشکال زدایی آن‌ها را نخواهید داشت. دلیلش هم مشخص است؛ چون Angular فازی به نام کامپایل و اصطلاحا context مربوط به خودش را دارد و فراخوانی افزونه مورد نظر، خارج از context انگولار رخ می‌دهد و انگولار از وجود این افزونه بی خبر است. حال ممکن است به طور اتفاقی، فراخوانی افزونه قبل، مابین و یا حتی بعد از فاز کامپایل انگولار رخ دهد. باز هم فرض کنید که بر حسب اتفاق همه چیز خوب پیش رفت، اما اکنون سایر قابلیت‌های خوب انگولار مثل ng- model و model binding آن در دسترس نیستند و در آخر به این نتیجه می‌رسید که پس چرا دارم از انگولار استفاده می‌کنم.

روش دوم - استفاده از directive‌های محصور کننده
راه اصولی برای استفاده از افزونه‌های جی‌کوئری در AngularJS، استفاده از directiveهای تهیه شده برای آن افزونه است. اگر خوش شانس باشید، معمولا برای افرونه‌های معروف، directive انگولاری آن نیز تهیه شده است. اما این همه‌ی داستان نیست؛ فرض کنید که از کتابخانه jQuery file upload، برای آپلود فایل می‌خواهید استفاده کنید. خوشبختانه directive  انگولاری نیز برای آن تهیه شده است و مستندات استفاده از آن هم، تنها مثالی هست که برای آن فراهم شده است. اما فرض کنید که می‌خواهید مانند مثال استفاده از آن در jQuery، یک file input که کاربر تنها  بتواند یک فایل را از طریق کشیدن و رها کردن آپلود کند، با استفاده از Directive انگولاری آن پیاده سازی کنید. اما کار با این directive، به آسانی مثال جی‌کوئری آن نیست. یک‌کم که جلوتر بروید می‌بینید که این directive گنگ طراحی شده است.  البته بیشتر directive هایی که اصطلاحا wrapper برای افزونه‌های جی‌کوئری هستند این مشکل را دارند و کار با آن‌ها چندان لذت بخش نیست و باید ساعت‌ها با آنها کلنجار رفت تا به نتیجه‌ی دلخواه رسید و همه‌ی این‌ها را در نظر بگیرید که اگر با api‌های jQuery آن کار می‌کردید، دیگر این مشکلات را نداشتید. قبلا نیز یک نمونه‌ی دیگر از مشکلات استفاده از این گونه directive‌های محصور کننده را تحت مقاله ای با عنوان استفاده از افزونه isotope در انگولار به اشتراک گذاشتم.

روش سوم - استفاده از directive هایی که به صورت native با انگولار نوشته شده‌اند
اما چرا به هنگام استفاده از directive‌های محصور کننده افرونه‌های جی‌کوئری، با مشکلات زیادی روبرو می‌شویم؟ دلیلش این است که انگولار می‌گوید بهتر است این افزونه‌ها با استفاده از خود angular بازنویسی شوند. برای مثال برای آپلود فایل می‌توان از کتابخانه‌ی با کیفیت ng-file-upload که هیچ وابستگی به jQuery ندارد استفاده کرد. اما آیا واقعا برای تمامی افزونه‌های جی‌کوئری معادلی برای AngularJs آن با همان کیفیت تهیه شده است؟ جواب مطمئنا خیر است. برای مثال در حالی که برای datagrid افزونه‌های بی شماری برای جی‌کوئری تهیه شده است، اما برای angular تنها یکی دو تا directive با کیفیت تهیه شده‌است که نه تنها قابلیت رقابت با معادل‌های jQuery شان را ندارند، آنچنان نیز stable نیستند و در مستندات خودشان هشدار می‌دهند که فلان ویژگی در حال تست هست و هنوز پایدار نیست.

روش چهارم – نوشتن directive توسط خودتان
به عنوان آخرین راه حل باید خودتان دست به کار شده و برای افزونه مورد نظرتان directive بنویسید. اما نوشتن directive برای افزونه‌های پیچیده‌ی جی‌کوئری به سادگی مثال‌های آموزشی AngularJS همانند چگونگی نوشتن directive برای jQueryUI Datepicker  نیست. اگر کدهای directive‌های نوشته شده برای افزونه‌های پیچیده را بررسی کنید، کدهایی را می‌بینید که برای شما منطقی نیست. برای مثال ممکن است با تعداد زیادی setTimeOut مواجه شوید که احتمالا با نحوه‌ی کامپایل HTML توسط انگولار مرتبط است. در کل باید بدانید که نوشتن directive برای تعداد زیادی از افزونه‌ها کار راحتی نیست و احتمالش هست که قید این را کار نیز بزنید.

پس اگر قصد توسعه SPA با هر فریمورکی مثل angular را داشته باشید، این را در نظر داشته باشید که دیر یا زود هنگام استفاده از افزونه‌های جی‌کوئری به مشکل برخواهید خورد. 

 

بیشتر امکانات تو کار ASP.NET MVC را از دست خواهید داد

به هنگام توسعه‌ی برنامه با استفاده از فریم ورک‌های SPA، امکانات توکار ASP.NET MVC مثل اعتبارسنجی یکپارچه و strongly typed view‌ها را از دست خواهید داد. شاید یک سری پروژه در Github پیدا کنید که سعی کرده‌اند این‌ها را با یکدیگر سازگار کنند. اما به محض استفاده متوجه می‌شوید که اگر همه‌ی کارها را خودتان با Angular انجام بدهید راحت‌تر هستید تا استفاده از کتابخانه‌های آزمایشی و ناقص.

البته باز هم نمی‌گویم که این‌ها تقصیر AngularJS است. ذات توسعه‌ی SPA‌ها، این گونه است و در توسعه‌ی SPA با هر فریمورکی به این مشکلات برخواهید خورد.

حال که یکسری مشکلات عمومی را بررسی کردیم، بدنیست نگاهی اختصاصی به خود AngularJS بیندازیم.

ضعف طراحی 

اگر به تعدای از لینک‌های سایت ihateangular مراجعه کنید می‌بینید که هر کسی نظری دارد: یکی می‌گوید به هیچ وجه Directive ننویسید، یکی دیگر می‌گوید کنترلر ننویسید و تمامی کارها را در directive‌های سفارشی نوشته شده توسط خودتان انجام بدهید، کلا همه جا علیه performance این فریمورک صحبت می‌کنند و همگی به پیچیده بودن آن اذعان دارند.

 اما نمی‌شود با چند مقاله‌ی موجود در اینترنت، یک فریمورک با این محبوبیت را زیر سوال برد. اما واقعا فکر می‌کنید که چرا نسخه‌ی 2 انگولار یک بازنویسی کامل است؟ دلیلش واضح است؛ این فریمورک از پایه اشکال دار بوده است و باید از اساس اصلاح شود. پس می‌توان نتیجه گرفت که اشکالات وارد شده به این فریمورک صحیح هستند. 

AngularJs 2 یک بازنویسی کامل است
 
قبلا این موضوع در این نظرسنجی مطرح شده است. بازنویسی کامل یعنی این که خیلی چیزها به کل تغییر کرده‌اند و کدهای قبلی شما با نسخه‌ی جدید سازگار نیستند. بیشتر مطالبی که فراگرفته بودید دیگر کاربردی ندارد و دوباره مطالب جدیدی را باید یاد بگیرید. این را هم در نظر بگیرید که توسعه دهندگانی که در حال نوشتن directive هستند، احتمالا با آمدن نسخه 2 انگولار، مجبورند directive خود را بازنویسی کنند. آیا خودتان بودید، دیگر دل به کار می‌دادید؟!

نتیجه گیری

AngularJS فریمورک خیلی خوبی برای نوشتن برنامه‌های تست پذیر است و کسی منکر قابلیت‌های آن نیست. ولی این را نیز در نظر بگیرید که برای تست پذیر بودن، خیلی چیز‌ها از جمله سادگی کار را از دست می‌دهید. معمولا می‌گویند که AngularJS کارهای مشکل را ساده می‌کند و کارهای ساده را مشکل.

پیشنهاد من این است که اگر هنوز AngularJS را فرا نگرفته‌اید، حداقل یادگیری آن را تا انتشار نسخه‌ی 2 آن به تعویق بیندازید. اگر AngularJS را بلد هستید، دیگر آن را در پروژه‌ای استفاده نکنید؛ چون دیگر کدهای شما در نسخه‌ی 2 کار نخواهد کرد و احتیاج به انجام تغییرات گسترده‌ای در کدهای نوشته شده قبلی پیدا می‌کنید.

مطالب
یکپارچه سازی Angular CLI و ASP.NET Core در VS 2017
در این مطلب مثالی را در مورد نحوه‌ی تنظیمات یک پروژه‌ی خالی ASP.NET Core، جهت استفاده‌ی از یک پروژه‌ی Angular CLI قرار گرفته‌ی در پوشه‌ی آن‌را بررسی خواهیم کرد.

پیشنیازها

 - مطالعه‌ی سری کار با Angular CLI خصوصا قسمت نصب و قسمت ساخت برنامه‌های آن، پیش از مطالعه‌ی این مطلب ضروری است.
 - همچنین فرض بر این است که سری ASP.NET Core را نیز یکبار مرور کرده‌اید و با نحوه‌ی برپایی یک برنامه‌ی MVC آن و ارائه‌ی فایل‌های استاتیک توسط یک پروژه‌ی ASP.NET Core آشنایی دارید.


ایجاد یک پروژه‌ی جدید ASP.NET Core در VS 2017

در ابتدا یک پروژه‌ی خالی ASP.NET Core را در VS 2017 ایجاد خواهیم کرد. برای این منظور:
 - ابتدا از طریق منوی File -> New -> Project (Ctrl+Shift+N) گزینه‌ی ایجاد یک ASP.NET Core Web application را انتخاب کنید.
 - در صفحه‌ی بعدی آن هم گزینه‌ی «empty template» را انتخاب نمائید.


تنظیمات یک برنامه‌ی ASP.NET Core خالی برای اجرای یک برنامه‌ی Angular CLI

برای اجرای یک برنامه‌ی مبتنی بر Angular CLI، نیاز است بر روی فایل csproj برنامه‌ی ASP.NET Core کلیک راست کرده و گزینه‌ی Edit آن‌را انتخاب کنید.
سپس محتوای این فایل را به نحو ذیل تکمیل نمائید:

الف) درخواست عدم کامپایل فایل‌های TypeScript
  <PropertyGroup>
    <TargetFramework>netcoreapp1.1</TargetFramework>
    <TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
  </PropertyGroup>
چون نحوه‌ی کامپایل پروژه‌های Angular CLI صرفا مبتنی بر کامپایل مستقیم فایل‌های TypeScript آن نیست و در اینجا از یک گردش کاری توکار مبتنی بر webpack، به صورت خودکار استفاده می‌کند، کامپایل فایل‌های TypeScript توسط ویژوال استودیو، مفید نبوده و صرفا سبب دریافت گزارش‌های خطای بیشماری به همراه کند کردن پروسه‌ی Build آن خواهد شد. بنابراین با افزودن تنظیم TypeScriptCompileBlocked به true، از VS 2017 خواهیم خواست تا در این زمینه دخالت نکند.

ب) مشخص کردن پوشه‌هایی که باید الحاق و یا حذف شوند
  <ItemGroup>
    <Folder Include="Controllers\" />
    <Folder Include="wwwroot\" />
  </ItemGroup>
 
  <ItemGroup>
    <Compile Remove="node_modules\**" />
    <Content Remove="node_modules\**" />
    <EmbeddedResource Remove="node_modules\**" />
    <None Remove="node_modules\**" />
  </ItemGroup>
 
  <ItemGroup>
    <Compile Remove="src\**" />
    <Content Remove="src\**" />
    <EmbeddedResource Remove="src\**" />    
  </ItemGroup>
در اینجا پوشه‌های کنترلرها و wwwroot به پروژه الحاق شده‌اند. پوشه‌ی wwwroot جایی است که فایل‌هایی خروجی را Angular CLI ارائه خواهد کرد.
سپس دو پوشه‌ی node_modules و src واقع در ریشه‌ی پروژه را نیز به طور کامل از سیستم ساخت و توزیع VS 2017 حذف کرده‌ایم. پوشه‌ی node_modules وابستگی‌های Angular را به همراه دارد و src همان پوشه‌ی برنامه‌ی Angular ما خواهد بود.

ج) افزودن وابستگی‌های سمت سرور مورد نیاز
  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore" Version="1.1.1" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.1.2" />
    <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="1.1.1" />
  </ItemGroup>
 
  <ItemGroup>
    <DotNetCliToolReference Include="Microsoft.DotNet.Watcher.Tools" Version="1.0.0" />
  </ItemGroup>
 
  <ItemGroup>
    <!-- extends watching group to include *.js files -->
    <Watch Include="**\*.js" Exclude="node_modules\**\*;**\*.js.map;obj\**\*;bin\**\*" />
  </ItemGroup>
برای اجرای یک برنامه‌ی تک صفحه‌ای وب Angular، صرفا به وابستگی MVC و StaticFiles آن نیاز است.
در اینجا Watcher.Tools هم به همراه تنظیمات آن اضافه شده‌اند که در ادامه‌ی بحث به آن اشاره خواهد شد.


افزودن یک کنترلر Web API جدید

با توجه به اینکه دیگر در اینجا قرار نیست با فایل‌های cshtml و razor کار کنیم، کنترلرهای ما نیز از نوع Web API خواهند بود. البته در ASP.NET Core، کنترلرهای معمولی آن، توانایی ارائه‌ی Web API و همچنین فایل‌های Razor را دارند و از این لحاظ تفاوتی بین این دو نیست و یکپارچگی کاملی صورت گرفته‌است.
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
 
namespace ASPNETCoreIntegrationWithAngularCLI.Controllers
{
  [Route("api/[controller]")]
  public class ValuesController : Controller
  {
    // GET: api/values
    [HttpGet]
    [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
    public IEnumerable<string> Get()
    {
      return new string[] { "Hello", "DNT" };
    }
  }
}
در اینجا کدهای یک کنترلر نمونه را جهت بازگشت یک خروجی JSON ساده مشاهده می‌کنید که در ادامه، در برنامه‌ی Angular CLI تهیه شده از آن استفاده خواهیم کرد.


تنظیمات فایل آغازین یک برنامه‌ی ASP.NET Core جهت ارائه‌ی برنامه‌های Angular

در ادامه به فایل Startup.cs برنامه‌ی خالی جاری، مراجعه کرده و آن‌را به نحو ذیل تغییر دهید:
using System;
using System.IO;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
 
namespace ASPNETCoreIntegrationWithAngularCLI
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }
 
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole();
 
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
 
            app.Use(async (context, next) => {
                await next();
                if (context.Response.StatusCode == 404 &&
                    !Path.HasExtension(context.Request.Path.Value) &&
                    !context.Request.Path.Value.StartsWith("/api/", StringComparison.OrdinalIgnoreCase))
                {
                    context.Request.Path = "/index.html";
                    await next();
                }
            });
 
            app.UseMvcWithDefaultRoute();
            app.UseDefaultFiles();
            app.UseStaticFiles();
        }
    }
}
در اینجا برای ارائه‌ی کنترلر Web API، نیاز به ثبت سرویس‌های MVC است. همچنین ارائه‌ی فایل‌های پیش فرض و فایل‌های استاتیک (همان پوشه‌ی wwwroot برنامه) نیز فعال شده‌اند.
در قسمت app.Use آن، تنظیمات URL Rewriting مورد نیاز جهت کار با مسیریابی برنامه‌های Angular را مشاهده می‌کنید. برای نمونه اگر کاربری در ابتدای کار آدرس /products را درخواست کند، این درخواست به سمت سرور ارسال می‌شود و چون چنین صفحه‌ای در سمت سرور وجود ندارد، خطای 404 بازگشت داده می‌شود و کار به پردازش برنامه‌ی Angular نخواهد رسید. اینجا است که تنظیم میان‌افزار فوق، کار مدیریت خروجی‌های 404 را بر عهده گرفته و کاربر را به فایل index.html برنامه‌ی تک صفحه‌ای وب، هدایت می‌کند. به علاوه در اینجا اگر درخواست وارد شده، دارای پسوند باشد (یک فایل باشد) و یا با api/ شروع شود (اشاره کننده‌ی به کنترلرهای Web API برنامه)، از این پردازش و هدایت به صفحه‌ی index.html معاف خواهد شد.


ایجاد ساختار اولیه‌ی برنامه‌ی Angular CLI در داخل پروژه‌ی جاری

اکنون از طریق خط فرمان به پوشه‌ی ریشه‌ی برنامه‌ی ASP.NET Core‌، جائیکه فایل Startup.cs قرار دارد، وارد شده و دستور ذیل را اجرا کنید:
 >ng new ClientApp --routing --skip-install --skip-git --skip-commit
به این ترتیب پوشه‌ی جدید ClientApp، در داخل پوشه‌ی برنامه اضافه خواهد شد که در آن تنظیمات اولیه‌ی مسیریابی Angular نیز انجام شده‌است؛ از دریافت وابستگی‌های npm آن صرفنظر شده و همچنین کار تنظیمات git آن نیز صورت نگرفته‌است (تا از تنظیمات git پروژه‌ی اصلی استفاده شود).
پس از تولید ساختار برنامه‌ی Angular CLI، به پوشه‌ی آن وارد شده و تمام فایل‌های آن را Cut کنید. سپس به پوشه‌ی ریشه‌ی برنامه‌ی ASP.NET Core جاری، وارد شده و این فایل‌ها را در آنجا paste نمائید. به این ترتیب به حداکثر سازگاری ساختار پروژه‌های Angular CLI و VS 2017 خواهیم رسید. زیرا اکثر فایل‌های تنظیمات آن‌را می‌شناسد و قابلیت پردازش آن‌ها را دارد.
پس از این cut/paste، پوشه‌ی خالی ClientApp را نیز حذف کنید.


تنظیم محل خروجی نهایی Angular CLI به پوشه‌ی wwwroot

برای اینکه سیستم Build پروژه‌ی Angular CLI جاری، خروجی خود را در پوشه‌ی wwwroot قرار دهد، تنها کافی است فایل .angular-cli.json را گشوده و outDir آن‌را به wwwroot تنظیم کنیم:
"apps": [
    {
      "root": "src",
      "outDir": "wwwroot",
به این ترتیب پس از هر بار build آن، فایل‌های index.html و تمام فایل‌های js نهایی، در پوشه‌ی wwwroot که در فایل Startup.cs‌، کار عمومی کردن آن انجام شد، تولید می‌شوند.


فراخوانی کنترلر Web API برنامه در برنامه‌ی Angular CLI

در ادامه صرفا جهت آزمایش برنامه، فایل src\app\app.component.ts را گشوده و به نحو ذیل تکمیل کنید:
import { Component, OnInit } from '@angular/core';
import { Http } from '@angular/http'; 
 
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  constructor(private _httpService: Http) { }
 
  apiValues: string[] = [];
 
  ngOnInit() {
    this._httpService.get('/api/values').subscribe(values => {
      this.apiValues = values.json() as string[];
    });
  }
}
در اینجا خروجی JSON کنترلر Web API برنامه دریافت شده و به آرایه‌ی apiValues انتساب داده می‌شود.

سپس این آرایه را در فایل قالب این کامپوننت (src\app\app.component.html) استفاده خواهیم کرد:
<h1>Application says:</h1>
<ul *ngFor="let value of apiValues">
  <li>{{value}}</li>
</ul>
<router-outlet></router-outlet>
در اینجا یک حلقه ایجاد شده و عناصر آرایه‌ی apiValues به صورت یک لیست نمایش داده می‌شوند.


نصب وابستگی‌های برنامه‌ی Angular CLI

در ابتدای ایجاد پوشه‌ی ClientApp، از پرچم skip-install استفاده شد، تا صرفا ساختار پروژه، جهت cut/paste آن با سرعت هر چه تمام‌تر، ایجاد شود. اکنون برای نصب وابستگی‌های آن یا می‌توان در solution explorer به گره dependencies مراجعه کرده و npm را انتخاب کرد. در ادامه با کلیک راست بر روی آن، گزینه‌ی restore packages ظاهر می‌شود. و یا می‌توان به روش متداول این نوع پروژه‌ها، از طریق خط فرمان به پوشه‌ی ریشه‌ی پروژه وارد شد و دستور npm install را صادر کرد. بهتر است اینکار را از طریق خط فرمان انجام دهید تا مطمئن شوید که از آخرین نگارش‌های این ابزار که بر روی سیستم نصب شده‌است، استفاده می‌کنید.


روش اول اجرای برنامه‌های مبتنی بر ASP.NET Core و Angular CLI

تا اینجا اگر برنامه را از طریق VS 2017 اجرا کنید، خروجی را مشاهده نخواهید کرد. چون هنوز فایل index.html آن تولید نشده‌است.
بنابراین روش اول اجرای این نوع برنامه‌ها، شامل مراحل ذیل است:
الف) ساخت پروژه‌ی Angular CLI در حالت watch
 > ng build --watch
برای اجرای آن از طریق خط فرمان، به پوشه‌ی ریشه‌ی پروژه وارد شده و دستور فوق را وارد کنید. به این ترتیب کار build پروژه انجام شده و همچنین فایل‌های نهایی آن در پوشه‌ی wwwroot قرار می‌گیرند. به علاوه چون از پرچم watch استفاده شده‌است، با هر تغییری در پوشه‌ی src برنامه، این فایل‌ها به صورت خودکار به روز رسانی می‌شوند. بنابراین این پنجره‌ی خط فرمان را باید باز نگه داشت تا watcher آن بتواند کارش را به صورت مداوم انجام دهد.

ب) اجرای برنامه از طریق ویژوال استودیو
اکنون که کار ایجاد محتوای پوشه‌ی wwwroot برنامه انجام شده‌است، می‌توان برنامه را از طریق VS 2017 به روش متداولی اجرا کرد:


یک نکته: می‌توان قسمت الف را تبدیل به یک Post Build Event هم کرد. برای این منظور باید فایل csproj را به نحو ذیل تکمیل کرد:
<Target Name="AngularBuild" AfterTargets="Build">
    <Exec Command="ng build" />
</Target>
به این ترتیب با هربار Build پروژه در VS 2017، کار تولید مجدد محتوای پوشه‌ی wwwroot نیز انجام خواهد شد.
تنها مشکل روش Post Build Event، کند بودن آن است. زمانیکه از روش ng build --watch به صورت مستقل استفاده می‌شود، برای بار اول اجرا، اندکی زمان خواهد برد؛ اما اعمال تغییرات بعدی به آن بسیار سریع هستند. چون صرفا نیاز دارد این تغییرات اندک و تدریجی را کامپایل کند و نه کامپایل کل پروژه را از ابتدا.


روش دوم اجرای برنامه‌های مبتنی بر ASP.NET Core و Angular CLI

روش دومی که در اینجا بررسی خواهد شد، مستقل است از قسمت «ب» روش اول که توضیح داده شد. برنامه‌های NET Core. نیز به همراه CLI خاص خودشان هستند و نیازی نیست تا حتما از VS 2017 برای اجرای آن‌ها استفاده کرد. به همین جهت وابستگی Microsoft.DotNet.Watcher.Tools را نیز در ابتدای کار به وابستگی‌های برنامه اضافه کردیم.

الف) در ادامه، VS 2017 را به طور کامل ببندید؛ چون نیازی به آن نیست. سپس دستور ذیل را در خط فرمان، در ریشه‌ی پروژه‌، صادر کنید:
> dotnet watch run
این دستور پروژه‌ی ASP.NET Core را کامپایل کرده و بر روی پورت 5000 ارائه می‌دهد:
>dotnet watch run
[90mwatch : [39mStarted
Hosting environment: Production
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.
به علاوه پارامتر watch آن سبب خواهد شد تا هر تغییری که در کدهای پروژه‌ی ASP.NET Core صورت گیرند، بلافاصله کامپایل شده و قابل استفاده شوند.

ب) در اینجا چون برنامه بر روی پورت 5000 ارائه شده‌است، بهتر است دستور ng serve -o را صادر کرد تا بتوان به نحو ساده‌تری از سرور وب ASP.NET Core استفاده نمود. در این حالت برنامه‌ی Angular CLI بر روی پورت 4200 ارائه شده و بلافاصله در مرورگر نیز نمایش داده می‌شود.
مشکل! سرور وب ما بر روی پورت 5000 است و سرور آزمایشی Angular CLI بر روی پورت 4200. اکنون برنامه‌ی Angular ما، یک چنین درخواست‌هایی را به سمت سرور، جهت دریافت اطلاعات ارسال می‌کند: localhost:4200/api
برای رفع این مشکل می‌توان فایلی را به نام proxy.config.json با محتویات ذیل ایجاد کرد:
{
  "/api": {
    "target": "http://localhost:5000",
    "secure": false
  }
}
سپس دستور ng server صادر شده، اندکی متفاوت خواهد شد:
 >ng serve --proxy-config proxy.config.json -o
در اینجا به ng serve اعلام کرده‌ایم که تمامی درخواست‌های ارسالی به مسیر api/  (و یا همان localhost:4200/api جاری) را به سرور وب ASP.NET Core، بر روی پورت 5000 ارسال کن و نتیجه را در همینجا بازگشت بده. به این ترتیب مشکل عدم دسترسی به سرور وب، جهت تامین اطلاعات برطرف خواهد شد.
مزیت این روش، به روز رسانی خودکار مرورگر با انجام هر تغییری در کدهای قسمت Angular برنامه است.

نکته 1: بدیهی است می‌توان قسمت «ب» روش دوم را با قسمت «الف» روش اول نیز جایگزین کرد (ساخت پروژه‌ی Angular CLI در حالت watch). اینبار گشودن مرورگر بر روی پورت 5000 (و یا آدرس http://localhost:5000) را باید به صورت دستی انجام دهید. همچنین هربار تغییر در کدهای Angular، سبب refresh خودکار مرورگر نیز نمی‌شود که آن‌را نیز باید خودتان به صورت دستی انجام دهید (کلیک بر روی دکمه‌ی refresh پس از هر بار پایان کار ng build).
نکته 2: می‌توان قسمت «الف» روش دوم را حذف کرد (حذف dotnet run در حالت watch). یعنی می‌خواهیم هنوز هم ویژوال استودیو کار آغاز IIS Express را انجام دهد. به علاوه می‌خواهیم برنامه را توسط ng serve مشاهده کنیم (با همان پارامترهای قسمت «ب» روش دوم). در این حالت تنها موردی را که باید تغییر دهید، پورتی است که برای IIS Express تنظیم شده‌است. عدد این پورت را می‌توان در فایل Properties\launchSettings.json مشاهده کرد و سپس به تنظیمات فایل proxy.config.json اعمال نمود.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: ASPNETCoreIntegrationWithAngularCLI.zip
به همراه این کدها تعدادی فایل bat نیز وجود دارند که جهت ساده سازی عملیات یاد شده‌ی در این مطلب، می‌توان از آن‌ها استفاده کرد:
 - فایل restore.bat کار بازیابی و نصب وابستگی‌های پروژه‌ی دات نتی و همچنین Angular CLI را انجام می‌دهد.
 - دو فایل ng-build-dev.bat و ng-build-prod.bat بیانگر قسمت «الف» روش اول هستند. فایل dev مخصوص حالت توسعه است و فایل prod مخصوص ارائه‌ی نهایی.
 - دو فایل dotnet_run.bat و ng-serve-proxy.bat خلاصه کننده‌ی قسمت‌های «الف» و «ب» روش دوم هستند.
مطالب
ساخت قالب‌های نمایشی و ادیتور دکمه سه وضعیتی سازگار با Twitter bootstrap در ASP.NET MVC
گروه بندی دکمه‌ها در Twitter bootstrap

    <div class="btn-group" data-toggle="buttons-radio">
        <button class="btn" type="button">بلی</button>
        <button class="btn" type="button">خیر</button>
    </div>
در این مثال دو دکمه را ملاحظه می‌کنید که در یک div با کلاس btn-group محصور شده‌اند. به این ترتیب این دو دکمه در کنار هم، همانند دکمه‌های یک toolbar قرار خواهند گرفت. همچنین در بوت‌استرپ امکان انتساب ویژگی data-toggle=buttons-radio نیز به این div وجود دارد. در این حالت، این دکمه‌ها همانند دکمه‌های رادیویی رفتار خواهند کرد:

در ادامه قصد داریم یک Editor template و یک Display template مخصوص را جهت تدارک یک چنین دکمه‌هایی، برای مدیریت خواص Boolean ایجاد کنیم. به عبارتی اگر مدل برنامه چنین تعاریفی را داشت:
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace Mvc4TwitterBootStrapTest.Models
{
    public class User
    {
        public int Id { set; get; }

        [DisplayName("نام")]
        [Required(ErrorMessage="لطفا نام را تکمیل کنید")]
        public string Name { set; get; }

        [DisplayName("نام خانوادگی")]
        [Required(ErrorMessage = "لطفا نام خانوادگی را تکمیل کنید")]
        public string LastName { set; get; }

        [DisplayName("فعال است؟")]
        [UIHint("BootstrapBoolean")]
        public bool? IsActive { set; get; }
    }
}
فیلد nullable bool آن به صورت خودکار به شکل زیر رندر شود:


تهیه قالب ادیتور Views\Shared\EditorTemplates\BootstrapBoolean.cshtml

@model bool?
@{  
    var yesIsSelected = Model.HasValue && Model.Value ? "active" : null;
    var noIsSelected = Model.HasValue && !Model.Value ? "active" : null;
    var isIndeterminate = !Model.HasValue ? "active" : null;
    var htmlField = ViewData.TemplateInfo.HtmlFieldPrefix;   
}
@Html.HiddenFor(model => model)
<div class="btn-group" data-toggle="buttons-radio">
    <button type="button" class="btn btn-info @yesIsSelected bool-@htmlField" onclick="$('#@htmlField').val(true);">
        بلی</button>
    <button type="button" class="btn btn-info @noIsSelected bool-@htmlField" onclick="$('#@htmlField').val(false);">
        خیر</button>
    @if (ViewData.ModelMetadata.IsNullableValueType)
    {
        <button type="button" class="btn btn-info @isIndeterminate bool-@htmlField" onclick="$('#@htmlField').val('');">
            نامشخص</button> 
    }
</div>
سورس کامل فایل BootstrapBoolean.cshtml را که در مسیر Views\Shared\EditorTemplates باید کپی شود، در اینجا ملاحظه می‌کنید.
نوع اطلاعاتی که این قالب ادیتور پردازش خواهد کرد از نوع nullable bool است. البته مشکلی هم با نوع‌های bool معمولی ندارد. در حالت nullable، دکمه سومی را به نام «نامشخص» به مجموعه دکمه‌های «بلی» و «خیر» اضافه می‌کند. گاهی از اوقات در فرم‌های دریافت اطلاعات نیاز است بررسی کنیم آیا واقعا کاربر اطلاعاتی را انتخاب کرده یا اینکه بدون توجه به فیلدها، بر روی دکمه ارسال کلیک کرده است. در یک چنین حالتی تعریف دکمه‌های سه وضعیتی Boolean می‌تواند مفهوم پیدا کند.
در مورد اصول تهیه این قالب در ابتدای مطلب، با کلاس‌های btn-group و ویژگی data-toggle آشنا شدید. دقیقا این سه دکمه نیز در اینجا به همین نحو تعریف شده‌اند.
در ابتدای نمایش یک View، خصوصا در حالت ویرایش اطلاعات، نیاز است اطلاعات موجود، به دکمه‌های تعریف شده اعمال شوند. در اینجا برای انتخاب یک دکمه، باید کلاس active به آن نسبت داده شود، که نحوه تدارک آن‌را در سه متغیر yesIsSelected، noIsSelected و isIndeterminate ابتدای تعاریف قالب مشاهده می‌کنید.
سپس یک فیلد مخفی به صفحه اضافه شده است. از این جهت که به کمک jQuery، در حین کلیک بر روی یکی از دکمه‌ها، مقدار آن‌را به این فیلد که نهایتا به سرور ارسال خواهد شد، اعمال خواهیم کرد.


تهیه قالب نمایشی Views\Shared\DisplayTemplates\BootstrapBoolean.cshtml

@model bool?
@if (Model.HasValue)
{
    if (Model.Value)
    {
    <span class="label label-success">بلی</span> 
    }
    else
    {
    <span class="label label-important">خیر</span> 
    }
}
else
{ 
    <span class="label label-inverse">نامشخص</span> 
}
در حالت صرفا نمایشی، فایل قالب BootstrapBoolean.cshtml قرار گرفته در مسیر Views\Shared\DisplayTemplates، یک چنین تعاریفی را خواهد داشت.

و نهایتا برای استفاده از آن تنها کافی است توسط ویژگی UIHint، نام این قالب، به خاصیت Boolean مدنظر اعمال شود:
[UIHint("BootstrapBoolean")]
public bool? IsActive { set; get; }

نظرات مطالب
AngularJS #4
 با سلام. من طبق آموزش‌های شما که واقعا عالیه پیش رفتم و توی یه کار عملی به یه مشکلی خوردم.

فایل اسکریپت من اینه :
var saman = angular.module('SamanApplication', []);

saman.service('loginService', ['$http', function (http) {
    var loginData = [];
    this.login = function () {
        
        http.get('Saman/LogOn/IsLogedIn').success(function (data, status, headers, config) {
            loginData = data;
        });
        return loginData;
    };
}]);

        saman.controller('loginController', function ($scope, loginService) {
            $scope.response = [];
            $scope.click = function () { $scope.response = loginService.login() };
        });

فایل html  هم :
<body ng-app="SamanApplication">
    <div ng-controller="loginController">
        <button ng-click="click()">Test</button>
        {{data}}
    </div>
</body>

اما مشکل اینجاست که بار اول که کلیک میکنم اطلاعات از سرور میاد ولی در {{ِdata}} نمایش داده نمیشه. اما بار دوم که کلیک میکنم نمایش داده میشه !
امکانش هست راهنمایی کنید ؟
نظرات مطالب
احراز هویت و اعتبارسنجی کاربران در برنامه‌های Angular - قسمت ششم - کار با منابع محافظت شده‌ی سمت سرور
یک نکته‌ی تکمیلی: روش دیگری برای بهبود کنترل نمایش و مخفی سازی قسمت‌های مختلف صفحه

کدهای یک دایرکتیو سفارشی نمایش و یا مخفی سازی قسمت‌های مختلف صفحه را بر اساس سطوح دسترسی کاربر جاری، در IsVisibleForAuthUserDirective مشاهده کردید. روش دیگر انجام اینکار، نوشتن یک دایرکتیو ساختاری شبیه به ngIf توکار خود Angular است. کاری که ngIf انجام می‌دهد، مخفی کردن یک المان در صفحه نیست؛ بلکه کل آن‌را از DOM حذف می‌کند.

نکته‌ی اصلی پیاده سازی یک دایرکتیو ساختاری

اگر به سازنده‌ی IsVisibleForAuthUserDirective دقت کنید، تزریق وابستگی ElementRef را داریم:
@Directive({
  selector: "[isVisibleForAuthUser]"
})
export class IsVisibleForAuthUserDirective implements OnInit, OnDestroy {
  constructor(private elem: ElementRef, private authService: AuthService) { }
به این ترتیب Angular به صورت خودکار امکان دسترسی به المان جاری را با تزریق آن در سازنده‌ی کلاس، میسر می‌کند.
برای ایجاد یک دایرکتیور ساختاری، نیاز است از تزریق TemplateRef و ViewContainerRef استفاده کرد:
@Directive({
selector: "[hasAuthUserViewPermission]"
})
export class HasAuthUserViewPermissionDirective implements OnInit, OnDestroy {

  constructor(
  private templateRef: TemplateRef<any>,
  private viewContainer: ViewContainerRef,
  private authService: AuthService
) { }
در این حالت Angular تنها زمانی این وابستگی‌ها را به سازنده‌ی کلاس تزریق می‌کند که پیش از نام این دایرکتیو، یک * قرار دهید (مانند ngIf توکار آن):
<div *hasAuthUserViewPermission="['Admin','User']">
    *hasAuthUserViewPermission="['Admin','User']"
</div>
پس از آن می‌توان از viewContainer، برای نمایش المان و یا قالب مدنظر:
 this.viewContainer.createEmbeddedView(this.templateRef);
و یا حذف کامل آن المان از DOM کمک گرفت:
 this.viewContainer.clear();

اگر این نکات را کنار هم قرار دهیم و کدهای IsVisibleForAuthUserDirective فوق را بر این اساس اصلاح کنیم، به قطعه کد زیر می‌رسیم:
import { Directive, Input, OnDestroy, OnInit, TemplateRef, ViewContainerRef } from "@angular/core";
import { Subscription } from "rxjs/Subscription";

import { AuthService } from "./../../core/services/auth.service";

@Directive({
  selector: "[hasAuthUserViewPermission]"
})
export class HasAuthUserViewPermissionDirective implements OnInit, OnDestroy {
  private isVisible = false;
  private requiredRoles: string[] | null = null;
  private subscription: Subscription | null = null;

  @Input()
  set hasAuthUserViewPermission(roles: string[] | null) {
    this.requiredRoles = roles;
  }

  // Note, if you don't place the * in front, you won't be able to inject the TemplateRef<any> or ViewContainerRef into your directive.
  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef,
    private authService: AuthService
  ) { }

  ngOnInit() {
    this.subscription = this.authService.authStatus$.subscribe(status => this.changeVisibility(status));
    this.changeVisibility(this.authService.isAuthUserLoggedIn());
  }

  ngOnDestroy(): void {
    if (this.subscription) {
      this.subscription.unsubscribe();
    }
  }

  private changeVisibility(status: boolean) {
    const isInRoles = !this.requiredRoles ? true : this.authService.isAuthUserInRoles(this.requiredRoles);
    if (isInRoles && status) {
      if (!this.isVisible) {
        this.viewContainer.createEmbeddedView(this.templateRef);
        this.isVisible = true;
      }
    } else {
      this.isVisible = false;
      this.viewContainer.clear();
    }
  }
}

سپس تعریف آن‌را به قسمت‌های declarations و exports مربوط به SharedModule اضافه می‌کنیم:
import { HasAuthUserViewPermissionDirective } from "./directives/has-auth-user-view-permission.directive";

@NgModule({
  declarations: [
    HasAuthUserViewPermissionDirective
  ],
  exports: [
    HasAuthUserViewPermissionDirective
  ]
})
export class SharedModule {}

اکنون ماژول Dashboard برای استفاده‌ی از این امکانات تنها کافی است SharedModule را دریافت کند (یا هر ماژول دیگری نیز به همین ترتیب است):
import { SharedModule } from "../shared/shared.module";
@NgModule({
  imports: [
    SharedModule
  ]
})
export class DashboardModule { }

پس از آن برای مخفی سازی یک المان از دید کاربران وارد نشده‌ی به سیستم، فقط کافی است دایرکتیو ساختاری hasAuthUserViewPermission را به المان اعمال کنیم:
<div *hasAuthUserViewPermission="">
    *hasAuthUserViewPermission=""
</div>
و یا اگر نیاز به اعمال نقش‌ها نیز وجود داشت می‌توان به صورت زیر عمل کرد:
<div *hasAuthUserViewPermission="['Admin','User']">
    *hasAuthUserViewPermission="['Admin','User']"
</div>

خلاصه‌ی این تغییرات به کدهای نهایی این سری اعمال شده‌اند.
مطالب
پیاده سازی Unobtrusive Ajax در ASP.NET Core 1.0
پیاده سازی Unobtrusive Ajax را در ASP.NET MVC 5.x، می‌توانید در مطلب «ASP.NET MVC #21» مطالعه کنید. HTML Helpers مرتبط با Ajax، به طور کامل از ASP.NET Core 1.0 حذف شده‌اند. اما این مورد به این معنا نیست که نمی‌توان Unobtrusive Ajax را در ASP.NET Core که تمرکزش بیشتر بر روی Tag Helpers جدید هست تا HTML Helpers قدیمی، پیاده سازی کرد.


Unobtrusive Ajax چیست؟

در حالت معمولی، با استفاده از متد ajax جی‌کوئری، کار ارسال غیرهمزمان اطلاعات، به سمت سرور صورت می‌گیرد. چون در این روش کدهای جی‌کوئری داخل صفحات برنامه‌های ما قرار می‌گیرند، به این روش، «روش چسبنده» می‌گویند. اما با استفاده از افزونه‌ی «jquery.unobtrusive-ajax.min.js» مایکروسافت، می‌توان این کدهای چسبنده را تبدیل به کدهای غیرچسنبده یا Unobtrusive کرد. در این حالت، پارامترهای متد ajax، به صورت ویژگی‌ها (attributes) به شکل data-ajax به المان‌های مختلف صفحه اضافه می‌شوند و به این ترتیب، افزونه‌ی یاد شده به صورت خودکار با یافتن مقادیر ویژگی‌های data-ajax، این المان‌ها را تبدیل به المان‌های ای‌جکسی می‌کند. در این حالت به کدهایی تمیزتر و عاری از متدهای چسبنده‌ی ajax قرار گرفته‌ی در داخل صفحات وب خواهیم رسید.
روش طراحی Unobtrusive را در کتابخانه‌های معروفی مانند بوت استرپ هم می‌توان مشاهده کرد.


پیشنیازهای فعال سازی Unobtrusive Ajax در ASP.NET Core 1.0

توزیع افزونه‌ی «jquery.unobtrusive-ajax.min.js» مایکروسافت، از طریق bower صورت می‌گیرد که پیشتر در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 14 - فعال سازی اعتبارسنجی ورودی‌های کاربران» با آن آشنا شدیم. در اینجا نیز برای دریافت آن، تنها کافی است فایل bower.json را به نحو ذیل تکمیل کرد:
{
  "name": "asp.net",
  "private": true,
  "dependencies": {
   "bootstrap": "3.3.6",
   "jquery": "2.2.0",
   "jquery-validation": "1.14.0",
   "jquery-validation-unobtrusive": "3.2.6",
   "jquery-ajax-unobtrusive": "3.2.4"
  }
}
و پس از آن فایل bundleconfig.json مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 21 - بررسی تغییرات Bundling و Minification» یک چنین شکلی را پیدا می‌کند:
[
  {
   "outputFileName": "wwwroot/css/site.min.css",
   "inputFiles": [
    "bower_components/bootstrap/dist/css/bootstrap.min.css",
    "content/site.css"
   ]
  },
  {
   "outputFileName": "wwwroot/js/site.min.js",
   "inputFiles": [
    "bower_components/jquery/dist/jquery.min.js",
    "bower_components/jquery-validation/dist/jquery.validate.min.js",
    "bower_components/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js",
    "bower_components/jquery-ajax-unobtrusive/jquery.unobtrusive-ajax.min.js",
    "bower_components/bootstrap/dist/js/bootstrap.min.js"
   ],
   "minify": {
    "enabled": true,
    "renameLocals": true
   },
   "sourceMap": false
  }
]
در اینجا فایل‌های css و اسکریپت مورد نیاز برنامه، به ترتیب اضافه شده و یکی خواهند شد. خروجی نهایی آن‌ها به شکل زیر در صفحات وب مورد استفاده قرار می‌گیرند:
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - My ASP.NET Application</title>
    <link href="~/css/site.min.css" rel="stylesheet" />
</head>
<body>
    <div>
        <div>
                @RenderBody()
        </div>
    </div>

    <script src="~/js/site.min.js" type="text/javascript" asp-append-version="true"></script>
    @RenderSection("Scripts", required: false)
</body>
</html>
در اینجا تنها دو فایل نهایی این عملیات، یعنی css/site.min.css و js/site.min.js به صفحه الحاق شده‌اند که حاوی تمام پیشنیازهای اسکریپتی و شیوه‌نامه‌های برنامه هستند و در این حالت دیگر نیاز به افزودن آن‌ها به دیگر صفحات سایت نیست.


استفاده از معادل‌های واقعی Unobtrusive Ajax در ASP.NET Core 1.0

واقعیت این است که HTML Helper ای‌جکسی حذف شده‌ی از ASP.NET Core 1.0، کاری بجز افزودن ویژگی‌های data-ajax را که توسط افزونه‌ی jquery.unobtrusive-ajax.min.js پردازش می‌شوند، انجام نمی‌دهد و این افزونه مستقل است از مباحث سمت سرور و به نگارش خاصی از ASP.NET گره نخورده است. بنابراین در اینجا تنها کاری را که باید انجام داد، استفاده از همان ویژگی‌های اصلی است که این افزونه قادر به شناسایی آن‌ها است.
خلاصه‌ی آن‌ها را جهت انتقال کدهای قدیمی و یا تهیه‌ی کدهای جدید، در جدول ذیل می‌توانید مشاهده کنید:

 HTML attribute   AjaxOptions 
 data-ajax-confirm   Confirm 
 data-ajax-method   HttpMethod 
 data-ajax-mode   InsertionMode 
 data-ajax-loading-duration   LoadingElementDuration 
 data-ajax-loading   LoadingElementId 
 data-ajax-begin   OnBegin 
 data-ajax-complete   OnComplete 
 data-ajax-failure   OnFailure 
 data-ajax-success   OnSuccess 
 data-ajax-update   UpdateTargetId 
 data-ajax-url   Url 
   
در ASP.NET Core 1.0، به علت حذف متدهای کمکی Ajax دیگر خبری از AjaxOptions نیست. اما اگر علاقمند به انتقال کدهای قدیمی به ASP.NET Core 1.0 هستید، معادل‌های اصلی این پارامترها را می‌توانید در ستون HTML attribute مشاهده کنید.

چند نکته:
- اگر قصد استفاده‌ی از این ویژگی‌ها را دارید، باید ویژگی "data-ajax="true را نیز حتما قید کنید تا سیستم Unobtrusive Ajax فعال شود.
- ویژگی data-ajax-mode تنها با ذکر data-ajax-update (و یا همان UpdateTargetId پیشین) معنا پیدا می‌کند.
- ویژگی data-ajax-loading-duration نیاز به ذکر data-ajax-loading (و یا همان LoadingElementId پیشین) را دارد.
- ویژگی data-ajax-mode مقادیر before، after و replace-with را می‌پذیرد. اگر قید نشود، کل المان با data دریافتی جایگزین می‌شود.
- سه callback قابل تعریف data-ajax-complete، data-ajax-failure و data-ajax-success، یک چنین پارامترهایی را از سمت سرور در اختیار کلاینت قرار می‌دهند:

parameters  
 Callback  
 xhr, status   data-ajax-complete 
 data, status, xhr   data-ajax-success 
 xhr, status, error   data-ajax-failure 

برای مثال می‌توان ویژگی data-ajax-success را به نحو ذیل در سمت کلاینت مقدار دهی کرد:
 data-ajax-success = "myJsMethod"
این متد جاوا اسکریپتی یک چنین امضایی را دارد:
  function myJsMethod(data, status, xhr) {
}
در این حالت در سمت سرور، پارامتر data در یک اکشن متد، به صورت ذیل مقدار دهی می‌شود:
 return Json(new { param1 = 1, param2 = 2, ... });
و در سمت کلاینت در متد myJsMethod این پارامترها را به صورت data.param1 می‌توان دریافت کرد.


مثال‌هایی از افزودن ویژگی‌های data-ajax به المان‌های مختلف

 در حالت استفاده از Form Tag Helpers که در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 12 - معرفی Tag Helpers» بررسی شدند، یک فرم ای‌جکسی، چنین تعاریفی را پیدا خواهد کرد:
با این ViewModel فرضی
using System.ComponentModel.DataAnnotations;
 
namespace Core1RtmEmptyTest.ViewModels.Account
{
    public class RegisterViewModel
    {
        [Required]
        [EmailAddress]
        [Display(Name = "Email")]
        public string Email { get; set; }
    }
}
که در View متناظر Ajax ایی ذیل استفاده شده‌است:
@using Core1RtmEmptyTest.ViewModels.Account
@model RegisterViewModel
@{
}
 
<form method="post"
      asp-controller="TestAjax"
      asp-action="Index"
      asp-route-returnurl="@ViewBag.ReturnUrl"
      class="form-horizontal"
      role="form"
      data-ajax="true"
      data-ajax-loading="#Progress"
      data-ajax-success="myJsMethod">
 
    <input asp-for="Email" class="form-control" />
    <span asp-validation-for="Email" class="text-danger"></span>
 
    <button type="submit">ارسال</button>
 
    <div id="Progress" style="display: none">
        <img src="images/loading.gif" alt="loading..." />
    </div>
</form>
 
@section scripts{
    <script type="text/javascript">
        function myJsMethod(data, status, xhr) {
            alert(data.param1);
        }
    </script>
}
در اینجا تمام تعاریف مانند قبل است؛ تنها سه ویژگی data-ajax جهت فعال سازی jquery-ajax-unobtrusive به فرم اضافه شده‌اند. همچنین یک callback دریافت پیام موفقیت آمیز بودن عملیات Ajax ایی نیز تعریف شده‌است.

این View از کنترلر ذیل استفاده می‌کند:
using Core1RtmEmptyTest.ViewModels.Account;
using Microsoft.AspNetCore.Mvc;
 
namespace Core1RtmEmptyTest.Controllers
{
    public class TestAjaxController : Controller
    {
 
        public IActionResult Index()
        {
            return View();
        }
 
        [HttpPost]
        public IActionResult Index([FromForm]RegisterViewModel vm)
        {
            var ajax = isAjax();
            if (ajax)
            {
                // it's an ajax post
            }
 
 
            if (ModelState.IsValid)
            {
                //todo: save data
 
                return Json(new { param1 = 1, param2 = 2 });
            }
            return View();
        }
 
        private bool isAjax()
        {
            return Request?.Headers != null && Request.Headers["X-Requested-With"] == "XMLHttpRequest";
        }
    }
}
به ASP.NET Core 1.0، متد کمکی IsAjax اضافه نشده‌است؛ اما تعریف آن‌را در این کنترلر مشاهده می‌کنید. در مورد قید FromForm در ادامه توضیح داده خواهد شد (هرچند در این مورد خاص، حالت پیش فرض است و الزامی به قید آن نیست).

و یا Action Link ای‌جکسی نیز به صورت خلاصه به این نحو قابل تعریف است:
<div id="EmployeeInfo">
<a 
 asp-controller="MyController" asp-action="MyAction"
 data-ajax="true" 
 data-ajax-loading="#Progress" 
 data-ajax-method="POST" 
 data-ajax-mode="replace" 
 data-ajax-update="#EmployeeInfo">
 Get Employee-1 info
</a>

  <div id="Progress" style="display: none">
    <img src="images/loading.gif" alt="loading..."  />
  </div>
</div>


نکته‌ای در مورد اکشن متدهای ای‌جکسی در ASP.NET Core 1.0

همانطور که در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 18 - کار با ASP.NET Web API»، قسمت «تغییرات Model binding پیش فرض، برای پشتیبانی از ASP.NET MVC و ASP.NET Web API» نیز ذکر شد:
public IActionResult Index([FromBody] MyViewModel vm)
{
   return View();
}
ذکر ویژگی FromBody در اینجا الزامی است. از این جهت که اطلاعات با فرمت JSON، از قسمت body درخواست استخراج و به MyViewModel بایند خواهند شد (در حالت dataType: json). و اگر dataType : application/x-www-form-urlencoded; charset=utf-8 بود (مانند حالت پیش فرض Unobtrusive Ajax)، باید از ویژگی FromForm استفاده شود. در غیر اینصورت در سمت سرور نال دریافت خواهیم کرد.