نظرات مطالب
ساختار پروژه های Angular
خیر. می‌توانید فایل‌های مورد نیاز هر ماژول و area را در مسیرهای جداگانه مربوط به area قرار دهید. پوشه Scripts صرفا برای قرار گیری فایل‌های مورد نیاز کتابخانه هاست(نظیر Jqeury و angular و q و ...).
نظرات اشتراک‌ها
راهنمای نظم بخشیدن به کدهای CSS
کتاب کوچکی توسط آقای Snook (از طراحان UI جدید یاهومیل) در 112 صفخه منتشر شده که بسیار عالی،کوهی از تجربیاتشون رو به اشتراک گذاشتن با نام Scalable and Modular Architecture for CSS 
Scalable and Modular Architecture for CSS

Read more at: http://www.wowebook.com/book/scalable-and-modular-architecture-for-css/ 
مسیرراه‌ها
Blazor 5x

مبانی Blazor 

 احراز هویت و اعتبارسنجی کاربران Blazor Server

تهیه API مخصوص Blazor WASM
Blazor WASM 

احراز هویت و اعتبارسنجی کاربران Blazor WASM

توزیع برنامه 

مدیریت استثناءها

بررسی تغییرات Blazor 8x  

مطالب تکمیلی
مطالب دوره‌ها
بررسی کارآیی و ایندکس گذاری بر روی اسناد XML در SQL Server - قسمت دوم
تا اینجا ملاحظه کردید که XQuery ایندکس نشده چگونه بر روی Query Plan تاثیر دارد. در ادامه، مباحث ایندکس گذاری بر روی اسناد XML ایی را مرور خواهیم کرد.


ایندکس‌های XML ایی

ایندکس‌های XML ایی، ایندکس‌های خاصی هستند که بر روی ستون‌هایی از نوع XML تعریف می‌شوند. هدف از تعریف آن‌ها، بهینه سازی اعمال مبتنی بر XQuery، بر روی داده‌های این نوع ستون‌ها است. چهار نوع XML Index قابل تعریف هستند؛ اما primary xml index باید ابتدا ایجاد شود. در این حالت جدولی که دارای ستون XML ایی است نیز باید دارای یک clustered index باشد. هدف از primary XML indexها، ارائه‌ی تخمین‌های بهتری است به بهینه ساز کوئری‌ها در SQL Server.


جزئیات primary XML indexها

زمانیکه یک primary xml index را ایجاد می‌کنیم، node table یاد شده در قسمت قبل را، بر روی سخت دیسک ذخیره خواهیم کرد (بجای هربار محاسبه در زمان اجرا). متادیتای این اطلاعات ذخیره شده را در جداول سیستمی sys.indexes و sys.columns می‌توان مشاهده کرد. باید دقت داشت که تهیه‌ی این ایندکس‌ها، فضای قابل توجهی را از سخت دیسک به خود اختصاص خواهند داد؛ چیزی حدود 2 تا 5 برابر حجم اطلاعات اولیه. بدیهی است تهیه‌ی این ایندکس‌ها که نتیجه‌ی تجزیه‌ی اطلاعات XML ایی است، بر روی سرعت insert تاثیر خواهند گذاشت. Node table دارای ستون‌هایی مانند نام تگ، آدرس تگ، نوع داده آن، مسیر و امثال آن است.
زمانیکه یک Primary XML Index تعریف می‌شود، اگر به Query Plan حاصل دقت کنید، دیگر خبری از XML Readerها مانند قبل نخواهد بود. در اینجا Clustered index seek قابل مشاهده‌است.


ایجاد primary XML indexها

همان مثال قسمت قبل را که دو جدول از آن به نام‌های xmlInvoice و xmlInvoice2 ایجاد کردیم، درنظر بگیرید. اینبار یک xmlInvoice3 را با همان ساختار و همان 6 رکوردی که معرفی شدند، ایجاد می‌کنیم. بنابراین برای آزمایش جاری، در مثال قبل، هرجایی xmlInvoice مشاهده می‌کنید، آن‌را به xmlInvoice3 تغییر داده و مجددا جدول مربوطه و داده‌های آن‌را ایجاد کنید.
اکنون برای ایجاد primary XML index بر روی ستون invoice آن می‌توان نوشت:
 CREATE PRIMARY XML INDEX invoice_idx ON xmlInvoice3(invoice)
 SELECT * FROM sys.internal_tables
کوئری دومی که بر روی sys.internal_tables انجام شده، محل ذخیره سازی این ایندکس را نمایش می‌دهد که دارای نامی مانند xml_index_nodes_325576198_256000 خواهد بود. دو عدد پس از آن table object id و column object id هستند.
در ادامه علاقمند هستیم که بدانیم داخل آن چه چیزی ذخیره شده‌است:
 SELECT * FROM sys.xml_index_nodes_325576198_256000
اگر این کوئری را اجرا کنید احتمالا به خطای Invalid object name برخواهید خورد. علت اینجا است که برای مشاهده‌ی اطلاعات جداول داخلی مانند این، نیاز است حین اتصال به SQL Server، در قسمت server name نوشت admin:(local) و حالت authentication نیز باید بر روی Windows authentication باشد. به آن اصطلاحا Dedicated administrator connection نیز می‌گویند. برای این منظور حتما نیاز است از طریق منوی File -> New -> Database Engine Query شروع کنید در غیراینصورت پیام Dedicated administrator connections are not supported را دریافت خواهید کرد.
اگر به این جدول دقت کنید، 6 ردیف اطلاعات XML ایی، به حدود 100 ردیف اطلاعات ایندکس شده، تبدیل گردیده‌است. با استفاده از دستور ذیل می‌توان حجم ایندکس تهیه شده را نیز مشاهده کرد:
 sp_spaceused 'xmlInvoice3'
در صورت نیاز برای حذف ایندکس ایجاد شده می‌توان به نحو ذیل عمل کرد:
 --DROP INDEX invoice_idx ON xmlInvoice3


تاثیر primary XML indexها بر روی سرعت اجرای کوئری‌ها

همان 10 کوئری قسمت قبل را درنظر بگیرید. اینبار برای مقایسه می‌توان به نحو ذیل عمل کرد:
 SELECT * FROM xmlInvoice
WHERE invoice.exist('/Invoice[@InvoiceId = "1003"]') = 1

SELECT * FROM xmlInvoice3
WHERE invoice.exist('/Invoice[@InvoiceId = "1003"]') = 1
دو کوئری یکی هستند اما اولی بر روی xmlInvoice اجرا می‌شود و دومی بر روی xmlInvoice3. هر دو کوئری را انتخاب کرده و با استفاده از منوی Query، گزینه‌ی Include actual execution plan را نیز انتخاب کنید (یا فشردن دکمه‌های Ctrl+M) تا پس از اجرای کوئری، بتوان Query Plan نهایی را نیز مشاهده نمود.


چند نکته در این تصویر حائز اهمیت است:
- Query plan کوئری انجام شده بر روی جدول دارای primary XML index، مانند قسمت قبل، حاوی XML Readerها نیست.
- هزینه‌ی انجام کوئری بر روی جدول دارای XML ایندکس نسبت به حالت بدون ایندکس، تقریبا نزدیک به صفر است. (بهبود کارآیی فوق العاده)
اگر کوئری‌های دیگر را نیز با هم مقایسه کنید، تقریبا به نتیجه‌ی کمتر از یک سوم تا یک چهارم حالت بدون ایندکس خواهید رسید.
همچنین اگر برای حالت دارای Schema collection نیز ایندکس ایجاد کنید، اینبار کوئری پلن آن اندکی (چند درصد) بهبود خواهد یافت ولی نه آنچنان.


ایندکس‌های XML‌ایی ثانویه یا secondary XML indexes

سه نوع ایندکس XML ایی ثانویه نیز قابل تعریف هستند:
- VALUE : کار آن بهینه سازی کوئری‌های content و wildcard است.
- PATH : بهینه سازی انتخاب‌های مبتنی بر XPath را انجام می‌دهد.
- Property: برای بهینه سازی انتخاب خواص و ویژگی‌ها بکار می‌رود.

این ایندکس‌ها یک سری non-clustered indexes بر روی node tables هستند. برای ایجاد سه نوع ایندکس یاد شده به نحو ذیل می‌توان عمل کرد:
 CREATE XML INDEX invoice_path_idx ON xmlInvoice3(invoice)
 USING XML INDEX invoice_idx FOR PATH
در اینجا یک path index جدید ایجاد شده‌است. ایندکس‌های ثانویه نیاز به ذکر ایندکس اولیه نیز دارند.
پس از ایجاد ایندکس ثانویه بر روی مسیرها، اگر اینبار کوئری دوم را اجرا کنیم، به Query Plan ذیل خواهیم رسید:


همانطور که مشاهده می‌کنید، نسبت به حالت primary index، وضعیت clustered index seek به index seek تغییر کرده‌است و همچنین دقیقا مشخص است که از کدام ایندکس استفاده شده‌است.
در ادامه دو نوع ایندکس دیگر را نیز ایجاد می‌کنیم:
 CREATE XML INDEX invoice_value_idx ON xmlInvoice3(invoice)
 USING XML INDEX invoice_idx FOR VALUE
 
 CREATE XML INDEX invoice_prop_idx ON xmlInvoice3(invoice)
 USING XML INDEX invoice_idx FOR PROPERTY
 

سؤال: اکنون پس از تعریف 4 ایندکس یاد شده، کوئری دوم از کدام ایندکس استفاده خواهد کرد؟

در اینجا مجددا کوئری دوم را اجرا کرده و به قسمت Query Plan آن دقت خواهیم کرد:


برای مشاهده دقیق نام ایندکس مورد استفاده، کرسر ماوس را بر روی index seek قرار می‌دهیم. در اینجا اگر به قسمت object گزارش ارائه شده دقت کنیم، نام invoice_value_idx یا همان value index ایجاد شده، قابل مشاهده‌است؛ به این معنا که در کوئری دوم، اهمیت مقادیر بیشتر است از اهمیت مسیرها.

کوئری‌هایی مانند کوئری ذیل از property index استفاده می‌کنند:
 SELECT * FROM xmlInvoice3
WHERE invoice.exist('/Invoice//CustomerName[text() = "Vahid"]') = 1
در اینجا با بکارگیری // به دنبال CustomerName در تمام قسمت‌های سند Invoice خواهیم گشت. البته کوئری پلن آن نسبتا پیچیده‌است و شامل primary index اسکن و clusterd index اسکن نیز می‌شود. برای بهبود قابل ملاحظه‌ی آن می‌توان به نحو ذیل از عملگر self استفاده کرد:
 SELECT * FROM xmlInvoice3
WHERE invoice.exist('/Invoice//CustomerName[. = "Vahid"]') = 1


خلاصه نکات بهبود کارآیی برنامه‌های مبتنی بر فیلدهای XML

- در حین استفاده از XPath، ذکر  محور parent یا استفاده از .. (دو دات)، سبب ایجاد مراحل اضافه‌ای در Query Plan می‌شوند. تا حد امکان از آن اجتناب کنید و یا از روش‌هایی مانند cross apply و xml.nodes برای مدیریت اینگونه موارد تو در تو استفاده نمائید.
- ordinals را به انتهای Path منتقل کنید (مانند ذکر [1] جهت مشخص سازی نودی خاص).
- از ذکر predicates در وسط یک Path اجتناب کنید.
- اگر اسناد شما fragment با چند root elements نیستند، بهتر است document بودن آ‌ن‌ها را در حین ایجاد ستون XML مشخص کنید.
- xml.value را به xml.query ترجیح دهید.
- عملیات casting در XQuery سنگین بوده و استفاده از ایندکس‌ها را غیرممکن می‌کند. در اینجا استفاده از اسکیما می‌تواند مفید باشد.
- نوشتن sub queryها بهتر هستند از چندین XQuery در یک عبارت SQL.
- در ترکیب اطلاعات رابطه‌ای و XML، استفاده از متدهای xml.exist و sql:column نسبت به xml.value جهت استخراج و مقایسه اطلاعات، بهتر هستند.
- اگر قصد تهیه خروجی XML از جدولی رابطه‌ای را دارید، روش select for xml کارآیی بهتری را نسبت به روش FLOWR دارد. روش FLOWR برای کار با اسناد XML موجود طراحی و بهینه شده‌است؛ اما روش select for xml در اصل برای کار با اطلاعات رابطه‌ای بهینه سازی گردیده‌است.
مطالب
PowerShell 7.x - قسمت سیزدهم - ساخت یک Static Site Generator ساده توسط PowerShell و GitHub Actions
در این مطلب میخواهیم یک مثال دیگر از PowerShell را به همراه GitHub Actions را بررسی کنیم. هدف ایجاد یک Static Site Generator و در نهایت پابلیش خروجی استاتیک بر روی GitHub Pages است. روالی که در ادامه بررسی میکنیم صرفاً یک مثال از ترکیب این تکنولوژی‌ها است و قاعدتاً روش‌های ساده‌تر و سرراست‌تری نیز برای اینکار وجود دارد. به عنوان مثال میتوانید از Jekyll که یک SSG مبتنی بر Ruby است نیز استفاده کنید که GitHub Pages، به صورت پیش‌فرض از آن پشتیبانی میکند. در اینحالت به محض پوش کردن سایت بر روی ریپوزیتوری (با فرض اینکه این امکان را فعال کرده باشید) به صورت خودکار سایت بیلد شده و خروجی بر روی یک برنچ دیگر قرار خواهد گرفت و در نهایت برنچ بیلد شده توسط GitHub Pages میزبانی خواهد شد (البته امکان تغییر برنچ پیش‌فرض را نیز دارید). اما اگر بخواهیم کل فرآیند بیلد را به صورت سفارشی انجام دهیم، میتوانیم از GitHub Actions استفاده کنیم؛ یعنی مشابه کاری که Jekyll انجام میدهد. به محض پوش کردن محتوا، یک اسکریپت PowerShell برای اینکار فراخوانی شود و خروجی نهایی بر روی یک برنچ دیگر منتشر شود. خروجی نهایی این چنین قالبی خواهد داشت:

نکته: در اینجا از فونت آقای راستی‌کردار استفاده شده است؛ با آرزوی بهبودی و سلامتی ایشان.

ساختار پروژه
ساختاری که برای پروژه در نظر گرفته‌ام به صورت زیر است:
├── _layout
│   ├── _footer.html
│   ├── _header.html
│   ├── _nav.html
│   └── main.html
├── build
├── img
├── posts
└── set-posts.ps1
  • دایرکتوری layout_: درون این دایرکتوری، ساختار اصلی بلاگ را قرار داده‌ایم. در ادامه محتویات هر فایل را مشاهده خواهید کرد: 
<!--main.html-->
<!DOCTYPE html>
<html dir="rtl">

{{header}}

<body>
    {{nav}}
    <main>
        {{content}}
    </main>
    {{footer}}
</body>



<!--_header.html-->
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{title}}</title>
    <link href="https://cdn.jsdelivr.net/gh/rastikerdar/samim-font@v4.0.5/dist/font-face.css" rel="stylesheet"
        type="text/css" />
    <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.16/dist/tailwind.min.css" rel="stylesheet">
    <style>
        * {
        }
    </style>
</head>

<!--_nav.html-->
<header>
    <nav>
        <div>
            <div>
                <a href="#">بلاگ من</a>
            </div>
            <div>
                <ul>
                    {{nav}}
                </ul>
            </div>
        </div>
    </nav>
</header>


<!--_footer.html-->
<footer>
    <div>
        <div>
            <p>
                تمامی حقوق محفوظ است
            </p>
        </div>
    </div>
</footer>
  • دایرکتوری build: درون این دایرکتوری، خروجی‌های HTML که قرار است توسط اسکریپت PowerShell جنریت شوند، قرار خواهند گرفت. این پوشه در واقع قرار است توسط GitHub Pages میزبانی شود.
  • دایرکتوری img: درون این دایرکتوری، تصاویر مربوط به هر بلاگ‌پست را قرار خواهیم داد.
  • دایرکتوری posts: درون این دایرکتوری، مطالب‌مان را با فرمت Markdown، قرار خواهیم داد. به عنوان مثال در ادامه یک نمونه از آن را مشاهده خواهید کرد (در کد زیر از Front Matter برای اضافه کردن یکسری متادیتای موردنیاز که حین بیلد شدن ضروری هستند استفاده شده‌است) 
---
title: اولین پست من
slug: hello
date: 2023-04-26
author: سیروان عفیفی
tags: [tag1, tag2, tag3]
excerpt: این یک پست تستی است در مورد اینکه چطور میتوانیم از این قالب استفاده کنیم
---

# اولین پست من

## اولین پست من

### اولین پست من

#### اولین پست من

##### اولین پست من

###### اولین پست من

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

<img src="/img/graphql.jpg"/>
همانطور که مشاهده میکنید، مسیر تصویر استفاده شده در بلاگ‌پست، از دایرکتوری img خوانده شده‌است.
  • فایل set_post.ps1: موتور اصلی جنریت کردن صفحات HTML این فایل میباشد. در ادامه محتویات آن را مشاهده خواهید کرد. سپس هر کدام از توابع استفاده شده را یکی‌یکی توضیح خواهیم داد: 
Function Get-Layouts {
    $headerLayout = Get-Content -Path ./_layout/_header.html -Raw
    $homeLayout = Get-Content -Path ./_layout/main.html -Raw
    $footerLayout = Get-Content -Path ./_layout/_footer.html -Raw

    Return @{
        Header = $headerLayout
        Home   = $homeLayout
        Footer = $footerLayout
    }
}

Function Get-PostFrontMatter($postContent) {
    $frontMatter = [regex]::Match($postContent, "(---(?:\r?\n(?!--|\s*$).*)*)\s*((?:\r?\n(?!---).*)*\r?\n---)")
    Return $frontMatter
}

Function Set-Headings($postHtml) {
    Return $postHtml -Replace '<h(\d) id="(.*)">', {
        $level = $_.Groups[1].Value
        $id = $_.Groups[2].Value
        $class = Switch ($level) {
            '1' { 'text-4xl font-bold mb-2' }
            '2' { 'text-3xl font-bold mb-2' }
            '3' { 'text-2xl font-bold mb-2' }
            '4' { 'text-xl font-bold mb-2' }
            '5' { 'text-lg font-bold mb-2' }
            '6' { 'text-base font-bold mb-2' }
        }
        "<h$level class='$class' id='$id'>"
    }
}

Function ConvertTo-Slug {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]$String
    )
    process {
        $slug = $String -replace '[^\w\s-]', '' # remove non-word characters except hyphens
        $slug = $slug -replace '\s+', '-' # replace whitespace with a single hyphen
        $slug = $slug -replace '^-+|-+$', '' # remove leading/trailing hyphens
        $slug = $slug.ToLower() # convert to lowercase
        Write-Output $slug
    }
}

Function Get-Posts {
    $markdownPosts = Get-ChildItem -Path ./posts -Filter *.md
    $posts = @()
    Foreach ($post in $markdownPosts) {
        $postContent = Get-Content -Path $post.FullName -Raw
        $frontMatter = Get-PostFrontMatter $postContent
        $frontMatterObject = $frontMatter | ConvertFrom-Yaml

        $slug = $frontMatterObject.slug ?? (ConvertTo-Slug "$($frontMatterObject.date)-$($frontMatterObject.title)")
        $body = $postContent.Replace($frontMatter.Value, "") | ConvertFrom-Markdown
        $postHtml = $layouts.Home -replace '{{header}}', $layouts.Header `
            -replace '{{title}}', $frontMatterObject.title `
            -replace '{{nav}}', (Set-Navs) `
            -replace '{{content}}', $body.Html `
            -replace '{{footer}}', $layouts.Footer

        $postHtml = Set-Headings $postHtml
        $postHtml | Out-File -FilePath ./build/$slug.html

        $posts += @{
            title   = $frontMatterObject.title
            slug    = $slug
            excerpt = $frontMatterObject.excerpt
            date    = $frontMatterObject.date
            author  = $frontMatterObject.author
            body    = $body.Html
        }
    }
    Return $posts
}

Function Set-Archive {
    $posts = Get-Posts
    $archive = @()
    $archive = @"
        <ul>
            $($posts | ForEach-Object { "<li><a href='$($_.slug).html'>$($_.title)</a></li>" })
        </ul>
"@
    Return $archive -join "`r`n"
}

Function Copy-ToBuild {
    $layouts = Get-Layouts
    $latestPosts = Get-Posts | ForEach-Object { @"
    <div>
        <img src="https://via.placeholder.com/300x200" alt="$($_.title)">
        <h2>$($_.title)</h2>
        <p>$($_.excerpt)</p>
            <a href="$($_.slug).html">ادامه مطلب</a>
        </div>
"@
    }

    $homeLayout = $layouts.Home -replace '{{header}}', $layouts.Header `
        -replace '{{nav}}', (Set-Navs) `
        -replace '{{title}}', 'بلاگ من' `
        -replace '{{content}}', ('<div>' + $latestPosts + '</div>') `
        -replace '{{footer}}', $layouts.Footer

    $homeLayout | Out-File -FilePath ./build/index.html

    Copy-Item -Path ./img -Destination ./build -Recurse -Force

}

Function Set-Navs {
    $navs = @(
        @{
            title = "صفحه اصلی"
            url   = "/sample"
        },
        @{
            title = "درباره ما"
            url   = "/sample/about.html"
        },
        @{
            title = "تماس با ما"
            url   = "/sample/contact.html"
        }
    )
    $navLayout = Get-Content -Path ./_layout/_nav.html -Raw
    $navLayout -replace '{{nav}}', ($navs | ForEach-Object { "<li><a href=""$($_.url)""text-gray-700 hover:text-gray-800 m-2"">$($_.title)</a></li>" })
}


Copy-ToBuild
همانطور که در کد فوق مشاهده میکنید، تابع Copy-ToBuild فراخوانی شده است. درون این تابع، ابتدا لی‌اوت‌های موردنیاز برای تولید صفحات HTML را درون یک متغییر با نام layouts قرار داده‌ایم. درون لی‌اوت‌ها یکسری placeholder برای قرارگیری قسمت‌های مختلف سایت تعریف کرده‌ایم که قرار است توسط تابع عنوان شده، جایگزین شوند. در ادامه توسط تابع Get-Posts، تمامی مطالب درون دایرکتوری posts را واکشی کرده و برای هر کدام، صفحه‌ی HTML معادل آن را تولید کرده‌ایم. برای پارز کردن قسمت Front Matter هر بلاگ‌پست نیز از پکیج powershell-yaml استفاده شده‌است. درون تابع Get-Posts باید هر مطلب را همراه با لی‌اوت اصلی سایت، به HTML تبدیل کنیم. بنابراین یکسری عملیات string replacement درون تابع عنوان شده انجام گرفته است. در نهایت، مطالب همراه با لی‌اوت مناسبی درون دایرکتوری build ذخیره خواهند شد و سپس یک لیست از مطالب پارز شده به عنوان خروجی تابع برگردانده خواهد شد. در نهایت درون تابع Copy-ToBuild، یکسری card برای نمایش آخرین مطالب بلاگ تهیه شده، سپس خروجی نهایی درون فایل index.html همراه با قالب اصلی ذخیره خواهد شد.

ایجاد GitHub Actions Workflow
در ادامه برای ساختن workflow نهایی باید با کمک GitHub Actions، اسکریپت PowerShellی را که ساختیم، اجرا کنیم. این اسکریپت ابتدا پروژه را clone کرده، سپس وابستگی موردنیاز را نصب کرده و در نهایت اسکریپت را اجرا خواهد کرد: 
name: Deploy static content to Pages

on:
  push:
    branches:
      - main
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: "pages"
  cancel-in-progress: false

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v2

      - name: Install powershell-yaml module
        shell: pwsh
        run: |
          Set-PSRepository PSGallery -InstallationPolicy Trusted
          Install-Module powershell-yaml -ErrorAction Stop

      - name: Setup Pages
        uses: actions/configure-pages@v3

      - name: Build Static Site
        shell: pwsh
        run: |
          . ./set-posts.ps1

      - name: Upload Static Site Artifact
        uses: actions/upload-pages-artifact@v1
        with:
          path: build

      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v2

نحوه انتشار یک مطلب جدید
  • درون دایرکتوری posts، مطلب موردنظر را به همراه Front Matter زیر ایجاد کرده و سپس محتویات مطلب را بعد از آن وارد کنید:
---
title: اولین پست من
slug: hello
date: 2023-04-26
author: سیروان عفیفی
tags: [tag1, tag2, tag3]
excerpt: این یک پست تستی است در مورد اینکه چطور میتوانیم از این قالب استفاده کنیم
---

content
  • تغییرات ایجاد شده را کامیت و سپس پوش کنید. به محض پوش کردن تغییرات، GitHub Actions پروسه بیلد را انجام خواهد داد و بلافاصله میتوانید تغییرات را مشاهده نمائید.
نکته: قابلیت GitHub Pages به صورت پیش‌فرض فعال نیست. برای فعال کردن آن ابتدا باید به قسمت تنظیمات ریپوزیتوری GitHubتان مراجعه کرده و سپس از تب Pages، فیلد Source را بر روی GitHub Actions قرار دهید:


بنابراین بعد از پوش کردن تغییرات workflowایی که ایجاد کردیم توسط Source شناسایی خواهد شد و سپس وب‌سایت از طریق آدرس https://<username>.github.io/<repository> در دسترس قرار خواهد گرفت:

کدهای این مطلب را میتوانید از اینجا دریافت کنید.

مطالب
Angular Material 6x - قسمت چهارم - نمایش پویای اطلاعات تماس‌ها
در قسمت قبل، یک لیست ثابت item 1/item 2/… را در sidenav نمایش دادیم. در این قسمت می‌خواهیم این لیست را با اطلاعات دریافت شده‌ی از سرور، پویا کنیم و همچنین با کلیک بر روی هر کدام، جزئیات آن‌ها را نیز در قسمت main-content نمایش دهیم.



تهیه سرویس اطلاعات پویای برنامه

سرویس Web API ارائه شده‌ی توسط ASP.NET Core در این برنامه، لیست کاربران را به همراه یادداشت‌های آن‌ها به سمت کلاینت باز می‌گرداند و ساختار موجودیت‌های آن‌ها به صورت زیر است:

موجودیت کاربر که یک رابطه‌ی one-to-many را با UserNotes دارد:
using System;
using System.Collections.Generic;

namespace MaterialAspNetCoreBackend.DomainClasses
{
    public class User
    {
        public User()
        {
            UserNotes = new HashSet<UserNote>();
        }

        public int Id { set; get; }
        public DateTimeOffset BirthDate { set; get; }
        public string Name { set; get; }
        public string Avatar { set; get; }
        public string Bio { set; get; }

        public ICollection<UserNote> UserNotes { set; get; }
    }
}
و موجودیت یادداشت‌های کاربر که سر دیگر رابطه را تشکیل می‌دهد:
using System;

namespace MaterialAspNetCoreBackend.DomainClasses
{
    public class UserNote
    {
        public int Id { set; get; }
        public DateTimeOffset Date { set; get; }
        public string Title { set; get; }

        public User User { set; get; }
        public int UserId { set; get; }
    }
}
در نهایت اطلاعات ذخیره شده‌ی در بانک اطلاعاتی توسط سرویس کاربران:
    public interface IUsersService
    {
        Task<List<User>> GetAllUsersIncludeNotesAsync();
        Task<User> GetUserIncludeNotesAsync(int id);
    }
در اختیار کنترلر Web API برنامه، برای ارائه‌ی به سمت کلاینت، قرار می‌گیرد:
namespace MaterialAspNetCoreBackend.WebApp.Controllers
{
    [Route("api/[controller]")]
    public class UsersController : Controller
    {
        private readonly IUsersService _usersService;

        public UsersController(IUsersService usersService)
        {
            _usersService = usersService ?? throw new ArgumentNullException(nameof(usersService));
        }

        [HttpGet]
        public async Task<IActionResult> Get()
        {
            return Ok(await _usersService.GetAllUsersIncludeNotesAsync());
        }

        [HttpGet("{id:int}")]
        public async Task<IActionResult> Get(int id)
        {
            return Ok(await _usersService.GetUserIncludeNotesAsync(id));
        }
    }
}
کدهای کامل لایه سرویس، تنظیمات EF Core و تنظیمات ASP.NET Core این قسمت را از پروژه‌ی پیوستی انتهای بحث می‌توانید دریافت کنید.
در این حالت اگر برنامه را اجرا کنیم، در مسیر زیر
 https://localhost:5001/api/users
یک چنین خروجی قابل مشاهده خواهد بود:


و آدرس https://localhost:5001/api/users/1 صرفا مشخصات اولین کاربر را بازگشت می‌دهد.


تنظیم محل تولید خروجی Angular CLI

ساختار پوشه بندی پروژه‌ی جاری به صورت زیر است:


همانطور که ملاحظه می‌کنید، کلاینت Angular در یک پوشه‌است و برنامه‌ی سمت سرور ASP.NET Core در پوشه‌ای دیگر. برای اینکه خروجی نهایی Angular CLI را به پوشه‌ی wwwroot پروژه‌ی وب کپی کنیم، فایل angular.json کلاینت Angular را به صورت زیر ویرایش می‌کنیم:
"build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "../MaterialAspNetCoreBackend/MaterialAspNetCoreBackend.WebApp/wwwroot",
تنظیم این outputPath به wwwroot پروژه‌ی وب سبب خواهد شد تا با صدور فرمان زیر:
 ng build --no-delete-output-path --watch
برنامه‌ی Angular در حالت watch (گوش فرا دادان به تغییرات فایل‌ها) کامپایل شده و سپس به صورت خودکار در پوشه‌ی MaterialAspNetCoreBackend.WebApp/wwwroot کپی شود. به این ترتیب پس از اجرای برنامه‌ی ASP.NET Core توسط دستور زیر:
 dotnet watch run
 این برنامه‌ی سمت سرور، در همان لحظه هم API خود را ارائه خواهد داد و هم هاست برنامه‌ی Angular می‌شود.
بنابراین دو صفحه‌ی کنسول مجزا را باز کنید. در اولی ng build (را با پارامترهای یاد شده در پوشه‌ی MaterialAngularClient) و در دومی dotnet watch run را در پوشه‌ی MaterialAspNetCoreBackend.WebApp اجرا نمائید.
هر دو دستور در حالت watch اجرا می‌شوند. مزیت مهم آن این است که اگر تغییر کوچکی را در هر کدام از پروژه‌ها ایجاد کردید، صرفا همان قسمت کامپایل می‌شود و در نهایت سرعت کامپایل نهایی برنامه به شدت افزایش خواهد یافت.


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

در ادامه پیش از تکمیل سرویس دریافت اطلاعات از سرور، نیاز است معادل‌های کلاس‌های موجودیت‌های سمت سرور خود را به صورت اینترفیس‌هایی تایپ‌اسکریپتی تعریف کنیم:
ng g i contact-manager/models/user
ng g i contact-manager/models/user-note
این دستورات دو اینترفیس خالی کاربر و یادداشت‌های او را در پوشه‌ی جدید models ایجاد می‌کنند. سپس آن‌ها را به صورت زیر و بر اساس تعاریف سمت سرور آن‌ها، تکمیل می‌کنیم:
محتویات فایل contact-manager\models\user-note.ts :
export interface UserNote {
  id: number;
  title: string;
  date: Date;
  userId: number;
}
محتویات فایل contact-manager\models\user.ts :
import { UserNote } from "./user-note";

export interface User {
  id: number;
  birthDate: Date;
  name: string;
  avatar: string;
  bio: string;

  userNotes: UserNote[];
}


ایجاد سرویس Angular دریافت اطلاعات از سرور

ساختار ابتدایی سرویس دریافت اطلاعات از سرور را توسط دستور زیر ایجاد می‌کنیم:
 ng g s contact-manager/services/user --no-spec
که سبب ایجاد فایل user.service.ts در پوشه‌ی جدید services خواهد شد:
import { Injectable } from "@angular/core";

@Injectable({
  providedIn: "root"
})
export class UserService {

  constructor() { }
}
قسمت providedIn آن مخصوص Angular 6x است و هدف از آن کم حجم‌تر کردن خروجی نهایی برنامه‌است؛ اگر از سرویسی که تعریف شده، در برنامه جائی استفاده نشده‌است. به این ترتیب دیگر نیازی نیست تا آن‌را به صورت دستی در قسمت providers ماژول جاری ثبت و معرفی کرد.
کدهای تکمیل شده‌ی UserService را در ذیل مشاهده می‌کنید:
import { HttpClient, HttpErrorResponse } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable, throwError } from "rxjs";
import { catchError, map } from "rxjs/operators";

import { User } from "../models/user";

@Injectable({
  providedIn: "root"
})
export class UserService {

  constructor(private http: HttpClient) { }

  getAllUsersIncludeNotes(): Observable<User[]> {
    return this.http
      .get<User[]>("/api/users").pipe(
        map(response => response || []),
        catchError((error: HttpErrorResponse) => throwError(error))
      );
  }

  getUserIncludeNotes(id: number): Observable<User> {
    return this.http
      .get<User>(`/api/users/${id}`).pipe(
        map(response => response || {} as User),
        catchError((error: HttpErrorResponse) => throwError(error))
      );
  }
}
در اینجا از pipe-able operators مخصوص RxJS 6x استفاده شده که در مطلب «ارتقاء به Angular 6: بررسی تغییرات RxJS» بیشتر در مورد آن‌ها بحث شده‌است.
- متد getAllUsersIncludeNotes، لیست تمام کاربران را به همراه یادداشت‌های آن‌ها از سرور واکشی می‌کند.
- متد getUserIncludeNotes صرفا اطلاعات یک کاربر را به همراه یادداشت‌های او از سرور دریافت می‌کند.


بارگذاری و معرفی فایل svg نمایش avatars کاربران به Angular Material

بسته‌ی Angular Material و کامپوننت mat-icon آن به همراه یک MatIconRegistry نیز هست که قصد داریم از آن برای نمایش avatars کاربران استفاده کنیم.
در قسمت اول، نحوه‌ی «افزودن آیکن‌های متریال به برنامه» را بررسی کردیم که در آنجا آیکن‌های مرتبط، از فایل‌های قلم، دریافت و نمایش داده می‌شوند. این کامپوننت، علاوه بر قلم آیکن‌ها، از فایل‌های svg حاوی آیکن‌ها نیز پشتیبانی می‌کند که یک نمونه از این فایل‌ها در مسیر wwwroot\assets\avatars.svg فایل پیوستی انتهای مطلب کپی شده‌است (چون برنامه‌ی وب ASP.NET Core، هاست برنامه است، این فایل را در آنجا کپی کردیم).
ساختار این فایل svg نیز به صورت زیر است:
<?xml version="1.0" encoding="utf-8"?>
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"
     xmlns:xlink="http://www.w3.org/1999/xlink">
    <defs>
    <svg viewBox="0 0 128 128" height="100%" width="100%" 
             pointer-events="none" display="block" id="user1" >
هر svg تعریف شده‌ی در آن دارای یک id است. از این id به عنوان نام avatar کاربرها استفاده خواهیم کرد. نحوه‌ی فعالسازی آن نیز به صورت زیر است:
ابتدا به فایل contact-manager-app.component.ts مراجعه و سپس این کامپوننت آغازین ماژول مدیریت تماس‌ها را با صورت زیر تکمیل می‌کنیم:
import { Component } from "@angular/core";
import { MatIconRegistry } from "@angular/material";
import { DomSanitizer } from "@angular/platform-browser";

@Component()
export class ContactManagerAppComponent {

  constructor(iconRegistry: MatIconRegistry, sanitizer: DomSanitizer) {
    iconRegistry.addSvgIconSet(sanitizer.bypassSecurityTrustResourceUrl("assets/avatars.svg"));
  }
  
}
MatIconRegistry جزئی از بسته‌ی angular/material است که در ابتدای کار import شده‌است. متد addSvgIconSet آن، مسیر یک فایل svg حاوی آیکن‌های مختلف را دریافت می‌کند. این مسیر نیز باید توسط سرویس DomSanitizer در اختیار آن قرار گیرد که در کدهای فوق روش انجام آن‌را ملاحظه می‌کنید. در مورد سرویس DomSanitizer در مطلب «نمایش HTML در برنامه‌های Angular» بیشتر بحث شده‌است.
در اینجا در صورتیکه فایل svg شما دارای یک تک آیکن است، روش ثبت آن به صورت زیر است:
iconRegistry.addSvgIcon(
      "unicorn",
      this.domSanitizer.bypassSecurityTrustResourceUrl("assets/unicorn_icon.svg")
    );
که در نهایت کامپوننت mat-icon، این آیکن را به صورت زیر می‌تواند نمایش دهد:
 <mat-icon svgIcon="unicorn"></mat-icon>

یک نکته: پوشه‌ی node_modules\material-design-icons به همراه تعداد قابل ملاحظه‌ای فایل svg نیز هست.


نمایش لیست کاربران در sidenav

در ادامه به فایل sidenav\sidenav.component.ts مراجعه کرده و سرویس فوق را به آن جهت دریافت لیست کاربران، تزریق می‌کنیم:
import { User } from "../../models/user";
import { UserService } from "../../services/user.service";

@Component()
export class SidenavComponent implements OnInit {

  users: User[] = [];

  constructor(private userService: UserService) {  }

  ngOnInit() {
    this.userService.getAllUsersIncludeNotes()
      .subscribe(data => this.users = data);
  }
}
به این ترتیب با اجرای برنامه و بارگذاری sidenav، در رخ‌داد OnInit آن، کار دریافت اطلاعات کاربران و انتساب آن به خاصیت عمومی users صورت می‌گیرد.

اکنون می‌خواهیم از این اطلاعات جهت نمایش پویای آن‌ها در sidenav استفاده کنیم. در قسمت قبل، جای آن‌ها را در منوی سمت چپ صفحه به صورت زیر با اطلاعات ایستا مشخص کردیم:
    <mat-list>
      <mat-list-item>Item 1</mat-list-item>
      <mat-list-item>Item 2</mat-list-item>
      <mat-list-item>Item 3</mat-list-item>
    </mat-list>
اگر به مستندات mat-list مراجعه کنیم، در میانه‌ی صفحه، navigation lists نیز ذکر شده‌است که می‌تواند لیستی پویا را به همراه لینک به آیتم‌های آن نمایش دهد و این مورد دقیقا کامپوننتی است که در اینجا به آن نیاز داریم. بنابراین فایل sidenav\sidenav.component.html را گشوده و mat-list فوق را با mat-nav-list تعویض می‌کنیم:
    <mat-nav-list>
      <mat-list-item *ngFor="let user of users">
        <a matLine href="#">
          <mat-icon svgIcon="{{user.avatar}}"></mat-icon> {{ user.name }}
        </a>
      </mat-list-item>
    </mat-nav-list>
اکنون اگر برنامه را اجرا کنیم، یک چنین شکلی قابل مشاهده است:


که در اینجا علاوه بر لیست کاربران که از سرویس Users دریافت شده، آیکن avatar آن‌ها که از فایل assets/avatars.svg بارگذاری شده نیز قابل مشاهده است.


اتصال کاربران به صفحه‌ی نمایش جزئیات آن‌ها

در mat-nav-list فوق، فعلا هر کاربر به آدرس # لینک شده‌است. در ادامه می‌خواهیم با کمک سیستم مسیریابی، با کلیک بر روی نام هر کاربر، در سمت راست صفحه جزئیات او نیز نمایش داده شود:
    <mat-nav-list>
      <mat-list-item *ngFor="let user of users">
        <a matLine [routerLink]="['/contactmanager', user.id]">
          <mat-icon svgIcon="{{user.avatar}}"></mat-icon> {{ user.name }}
        </a>
      </mat-list-item>
    </mat-nav-list>
در اینجا با استفاده از routerLink، هر کاربر را بر اساس Id او، به صفحه‌ی جزئیات آن شخص، متصل کرده‌ایم. البته این مسیریابی برای اینکه کار کند باید به صورت زیر به فایل contact-manager-routing.module.ts اضافه شود:
const routes: Routes = [
  {
    path: "", component: ContactManagerAppComponent,
    children: [
      { path: ":id", component: MainContentComponent },
      { path: "", component: MainContentComponent }
    ]
  },
  { path: "**", redirectTo: "" }
];
البته اگر تا اینجا برنامه را اجرا کنید، با نزدیک کردن اشاره‌گر ماوس به نام هر کاربر، آدرسی مانند https://localhost:5001/contactmanager/1 در status bar مرورگر ظاهر خواهد شد، اما با کلیک بر روی آن، اتفاقی رخ نمی‌دهد.
این مشکل دو علت دارد:
الف) چون ContactManagerModule را به صورت lazy load تعریف کرده‌ایم، دیگر نباید در لیست imports فایل AppModule ظاهر شود. بنابراین فایل app.module.ts را گشوده و سپس تعریف ContactManagerModule را هم از قسمت imports بالای صفحه و هم از قسمت imports ماژول حذف کنید؛ چون نیازی به آن نیست.
ب) برای مدیریت خواندن id کاربر، فایل main-content\main-content.component.ts را گشوده و به صورت زیر تکمیل می‌کنیم:
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";

import { User } from "../../models/user";
import { UserService } from "../../services/user.service";

@Component({
  selector: "app-main-content",
  templateUrl: "./main-content.component.html",
  styleUrls: ["./main-content.component.css"]
})
export class MainContentComponent implements OnInit {

  user: User;
  constructor(private route: ActivatedRoute, private userService: UserService) { }

  ngOnInit() {
    this.route.params.subscribe(params => {
      this.user = null;

      const id = params["id"];
      if (!id) {
        return;
      }

      this.userService.getUserIncludeNotes(id)
        .subscribe(data => this.user = data);
    });
  }
}
در اینجا به کمک سرویس ActivatedRoute و گوش فرادادن به تغییرات params آن در ngOnInit، مقدار id مسیر دریافت می‌شود. سپس بر اساس این id، با کمک سرویس کاربران، اطلاعات این تک کاربر از سرور دریافت و به خاصیت عمومی user نسبت داده خواهد شد.
اکنون می‌توان از اطلاعات این user دریافتی، در قالب این کامپوننت و یا همان فایل main-content.component.html استفاده کرد:
<div *ngIf="!user">
  <mat-spinner></mat-spinner>
</div>
<div *ngIf="user">
  <mat-card>
    <mat-card-header>
      <mat-icon mat-card-avatar svgIcon="{{user.avatar}}"></mat-icon>
      <mat-card-title>
        <h2>{{ user.name }}</h2>
      </mat-card-title>
      <mat-card-subtitle>
        Birthday {{ user.birthDate | date:'d LLLL' }}
      </mat-card-subtitle>
    </mat-card-header>
    <mat-card-content>
      <mat-tab-group>
        <mat-tab label="Bio">
          <p>
            {{user.bio}}
          </p>
        </mat-tab>
        <!-- <mat-tab label="Notes"></mat-tab> -->
      </mat-tab-group>
    </mat-card-content>
  </mat-card>
</div>
در اینجا از کامپوننت mat-spinner برای نمایش حالت منتظر بمانید استفاده کرده‌ایم. اگر user نال باشد، این spinner نمایش داده می‌شود و برعکس.


همچنین mat-card را هم بر اساس مثال مستندات آن، ابتدا کپی و سپس سفارشی سازی کرده‌ایم (اگر دقت کنید، هر کامپوننت آن سه برگه‌ی overview، سپس API و در آخر Example را به همراه دارد). این روشی است که همواره می‌توان با کامپوننت‌های این مجموعه انجام داد. ابتدا مثالی را در مستندات آن پیدا می‌کنیم که مناسب کار ما باشد. سپس سورس آن‌را از همانجا کپی و در برنامه قرار می‌دهیم و در آخر آن‌را بر اساس اطلاعات خود سفارشی سازی می‌کنیم.



نمایش جزئیات اولین کاربر در حین بارگذاری اولیه‌ی برنامه

تا اینجای کار اگر برنامه را از ابتدا بارگذاری کنیم، mat-spinner قسمت نمایش جزئیات تماس‌ها ظاهر می‌شود و همانطور باقی می‌ماند، با اینکه هنوز موردی انتخاب نشده‌است. برای رفع آن به کامپوننت sidnav مراجعه کرده و در لحظه‌ی بارگذاری اطلاعات، اولین مورد را به صورت دستی نمایش می‌دهیم:
import { Router } from "@angular/router";

@Component()
export class SidenavComponent implements OnInit, OnDestroy {

  users: User[] = [];
  
  constructor(private userService: UserService, private router: Router) {
  }

  ngOnInit() {
    this.userService.getAllUsersIncludeNotes()
      .subscribe(data => {
        this.users = data;
        if (data && data.length > 0 && !this.router.navigated) {
          this.router.navigate(["/contactmanager", data[0].id]);
        }
      });
  }
}
در اینجا ابتدا سرویس Router به سازنده‌ی کلاس تزریق شده‌است و سپس زمانیکه کار دریافت اطلاعات تماس‌ها پایان یافت و this.router.navigated نبود (یعنی پیشتر هدایت به آدرسی صورت نگرفته بود؛ برای مثال کاربر آدرس id داری را ریفرش نکرده بود)، اولین مورد را توسط متد this.router.navigate فعال می‌کنیم که سبب تغییر آدرس صفحه از https://localhost:5001/contactmanager به https://localhost:5001/contactmanager/1 و باعث نمایش جزئیات آن می‌شود.

البته روش دیگر مدیریت این حالت، حذف کدهای فوق و تبدیل کدهای کامپوننت main-content به صورت زیر است:
let id = params['id'];
if (!id) id = 1;
در اینجا اگر id انتخاب نشده باشد، یعنی اولین بار نمایش برنامه است و خودمان id مساوی 1 را برای آن در نظر می‌گیریم.


بستن خودکار sidenav در حالت نمایش موبایل

اگر اندازه‌ی صفحه‌ی نمایشی را کوچکتر کنیم، قسمت sidenav در حالت over نمایان خواهد شد. در این حالت اگر آیتم‌های آن‌را انتخاب کنیم، هرچند آن‌ها نمایش داده می‌شوند، اما زیر این sidenav مخفی باقی خواهند ماند:


بنابراین در جهت بهبود کاربری این قسمت بهتر است با کلیک کاربر بر روی sidenav و گزینه‌های آن، این قسمت بسته شده و ناحیه‌ی زیر آن نمایش داده شود.
در کدهای قالب sidenav، یک template reference variable برای آن به نام sidenav درنظر گرفته شده‌است:
<mat-sidenav #sidenav
برای دسترسی به آن در کدهای کامپوننت خواهیم داشت:
import { MatSidenav } from "@angular/material";

@Component()
export class SidenavComponent implements OnInit, OnDestroy {

  @ViewChild(MatSidenav) sidenav: MatSidenav;
اکنون که به این ViewChild دسترسی داریم، می‌توانیم در حالت نمایشی موبایل، متد close آن‌را فراخوانی کنیم:
  ngOnInit() {
    this.router.events.subscribe(() => {
      if (this.isScreenSmall) {
        this.sidenav.close();
      }
    });
  }
در اینجا با مشترک this.router.events شدن، متوجه‌ی کلیک کاربر و نمایش صفحه‌ی جزئیات آن می‌شویم. در قسمت سوم این مجموعه نیز خاصیت isScreenSmall را بر اساس ObservableMedia مقدار دهی کردیم. بنابراین اگر کاربر بر روی گزینه‌ای کلیک کرده بود و همچنین اندازه‌ی صفحه در حالت موبایل قرار داشت، sidenav را خواهیم بست تا بتوان محتوای زیر آن‌را مشاهده کرد:



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: MaterialAngularClient-03.zip
برای اجرای آن:
الف) ابتدا به پوشه‌ی src\MaterialAngularClient وارد شده و فایل‌های restore.bat و ng-build-dev.bat را اجرا کنید.
ب) سپس به پوشه‌ی src\MaterialAspNetCoreBackend\MaterialAspNetCoreBackend.WebApp وارد شده و فایل‌های restore.bat و dotnet_run.bat را اجرا کنید.
اکنون برنامه در آدرس https://localhost:5001 قابل دسترسی است.
مطالب
آشنایی با CLR: قسمت ششم
در مقاله قبلی مبحث کامپایلر JIT را آغاز کردیم. در این قسمت قصد داریم مبحث کارآیی CLR و مباحث دیباگینگ را پیش بکشیم.
از آنجا که یک کد مدیریت نشده، مبحث کارهای JIT را ندارد، ولی CLR مجبور است وقتی را برای آن بگذارد، به نظر می‌رسد ما با یک نقص کوچک در کارآیی روبرو هستیم. گفتیم که جیت کدها را در حافظه‌ی پویا ذخیره می‌کند. به همین خاطر با terminate شدن یا خاتمه دادن به برنامه، این کدها از بین می‌روند یا اینکه اگر دو نمونه از برنامه را اجرا کنیم، هر کدام جداگانه کد را تولید می‌کنند و هر کدام برای خودشان حافظه‌ای بر خواهند داشت و اگر مقایسه‌ای با کدهای مدیریت نشده داشته باشید، در مورد مصرف حافظه یک مشکل ایجاد می‌کند. همچنین JIT در حین تبدیل به کدهای بومی یک بهینه سازی روی کد هم انجام میدهد که این بهینه سازی وقتی را به خود اختصاص می‌دهد ولی همین بهینه سازی کد موجب کارآیی بهتر برنامه می‌گردد.
در زبان سی شارپ دو سوئیچ وجود دارند که بر بهینه سازی کد تاثیر گذار هستند؛ سوئیچ‌های debug و optimize. در جدول زیر تاثیر هر یک از سوئیچ‌ها را بر کیفیت کد IL و JIT در تبدیل به کد بومی را نشان میدهد.

موقعیکه از دستور -optimize استفاده می‌شود، کد IL تولید شده شامل تعداد زیادی از دستورات بدون دستورالعمل No Operation یا به اختصار NOP و پرش‌های شاخه‌ای به خط کد بعدی می‌باشد. این دستور العمل‌ها ما را قادر میسازند تا ویژگی edit & Continue را برای دیباگ کردن و یک سری دستورالعمل‌ها را برای کدنویسی راحت‌تر برای دیباگ کردن و ایجاد break point‌ها داشته باشیم.

موقعی که کد IL بهینه شده تولید شود، این خصوصیات اضافه حذف خواهند شد و دنبال کردن خط به خط کد، کار سختی می‌شود. ولی در عوض فایل نهایی exe یا dll، کوچکتر خواهد شد. بهینه سازی IL توسط JIT حذف خواهد شد و برای کسانی که دوست دارند کدهای IL را تحلیل و آنالیز کنند، خواندنش ساده‌تر و آسان‌تر خواهد بود.

نکته‌ی بعدی اینکه موقعیکه شما از سوئیچ (/debug(+/full/pdbonly استفاده می‌کنید، یک فایل PDB  یا Program Database ایجاد می‌شود. این فایل به دیباگرها کمک می‌کند تا متغیرهای محلی را شناسایی و به کدهای IL متصل شوند. کلمه‌ی full بدین معنی است که JIT می‌تواند دستورات بومی را ردیابی کند تا مبداء آن کد را پیدا کند. سبب می‌شود که ویژوال استودیو به یک دیباگر متصل شده تا در حین اجرای پروسه، آن را دیباگ کند. در صورتی که این سوئیچ را استفاده نکنید، به طور پیش فرض پروسه اجرا و مصرف حافظه کمتر می‌شود. اگر شما پروسه‌ای را اجرا کنید که دیباگر به آن متصل شود، به طور اجباری JIT مجبور به انجام عملیات ردیابی خواهد شد؛ مگر اینکه گزینه‌ی suppress jit  optimization on module load را غیرفعال کرده باشید.
موقعیکه در ویژوال استودیو دو حالت دیباگ و ریلیز را انتخاب می‌کنید، در واقع تنظیمات زیر را اجرا می‌کنید:

//debug

/optimize­ 
/debug:full

//=======================

//Release

/optimize+
/debug:pdbonly
احتمالا موارد بالا به شما می‌گویند که یک سیستم مبتنی بر CLR مشکلات زیادی دارد که یکی از آن‌ها، زمان‌بر بودن انجام عملیات فرآیند پردازش است و دیگری مصرف زیاد حافظه و عدم اشترک حافظه که در مورد کامپایل جیت به آن اشاره کردیم. ولی در بند بعدی قصد داریم نظرتان را عوض کنم.

اگر خیلی شک دارید که واقعا یک برنامه‌ی CLR کارآیی یک برنامه را پایین می‌آورد، بهتر هست به بررسی کارآیی چند برنامه غیر آزمایشی noTrial که حتی خود مایکروسافت آن برنامه‌ها را ایجاد کرده است بپردازید و آن‌ها را با یک برنامه‌ی unmanaged مقایسه کنید. قطعا باعث تعجب شما خواهد شد. این نکته دلایل زیادی دارد که در زیر تعدادی از آن‌ها را بررسی می‌کنیم.
اینکه CLR در محیط اجرا قصد کمپایل دارد، باعث آشنایی کامپایلر با محیط اجرا می‌گردد. از این رو تصمیماتی را که می‌گیرد، می‌تواند به کارآیی یک برنامه کمک کند. در صورتیکه یک برنامه‌ی unmanaged که قبلا کمپایل شده و با محیط‌های متفاوتی که روی آن‌ها اجرا میشود، هیچ آشنایی ندارد و نمیتواند از آن محیط‌ها حداکثر بهره‌وری لازم را به عمل آورد.
برای آشنایی با این ویژگی‌ها توجه شما را به نکات ذیل جلب می‌کنم:

یک.  JIT می‌تواند با نوع پردازنده آشنا شود که آیا این پردازنده از نسل پنتیوم 4 است یا نسل Core i. به همین علت می‌تواند از این مزیت استفاده کرده و دستورات اختصاصی آن‌ها را به کار گیرد، تا برنامه با performance بالاتری اجرا گردد. در صورتی که unmanaged باید حتما دستورات را در پایین‌ترین سطح ممکن و عمومی اجرا کند؛ در صورتیکه شاید یک دستور اختصاصی در یک سی پی یو خاص، در یک عملیات موجب 4 برابر، اجرای سریعتر شود.

دو.  JIT میتواند بررسی هایی را که برابر false هستند، تشخیص دهد. برای فهم بهتر، کد زیر را در نظر بگیرید:
if (numberOfCPUs > 1) {
...
}

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

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

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

system.runtime.profileoptimization
کلاس بالا سبب می‌شود که CLR در یک فایل ثبت کند که چه متدهایی در حین اجرای برنامه کمپایل شوند تا در آینده در حین آغاز اجرای برنامه کامپایلر JIT بتواند همزمان این متدها را در ترد دیگری کامپایل کند. اگر برنامه‌ی شما روی یک پردازنده‌ی چند هسته‌ای اجرا می‌شود، در نتیجه اجرای سریعتری خواهید داشت. به این دلیل که چندین متد به طور همزمان در حال کمپایل شدن هستند و همزمان با آماده سازی برنامه برای اجرا اتفاق می‌افتد؛ به جای اینکه عمل کمپایل همزمان با تعامل کاربر با برنامه باشد.