مطالب
PowerShell 7.x - قسمت هشتم - ماژول‌ها
توسط ماژول‌ها میتوانیم یک مجموعه از دستورات را گروه‌بندی کنیم و تحت عنوان یک پکیج ارائه دهیم که برای دیگران نیز قابل استفاده باشند. برای ایجاد یک ماژول کافی است اسکریپت‌های خود را درون یک فایل با پسوند psm1 قرار دهیم؛ به این فایل اصطلاحاً root module گفته میشود. در واقع میتوان گفت ماژول‌ها یک روش مناسب برای به اشتراک‌گذاری اسکریپت‌ها میباشند. تا اینجا با کمک پروفایل‌ها توانستیم امکان استفاده مجدد از توابع و اسکریپت‌ها را داشته باشیم؛ ماژول‌ها نیز یک روش دیگر برای بارگذاری اسکریپت‌ها درون شل هستند. زمانیکه شل را باز میکنیم PowerShell به صورت خودکار یکسری مسیر را برای بارگذاری ماژول‌ها اسکن میکند. توسط متغیر env:PSModulePath$ میتوانیم لیست این مسیرها را ببینیم:  
PS /> $env:PSModulePath -Split ":"

/Users/sirwanafifi/.local/share/powershell/Modules
/usr/local/share/powershell/Modules
/usr/local/microsoft/powershell/7/Modules
همانطور که عنوان شد برای ایجاد یک ماژول کافی است اسکریپت‌های خود را داخل یک فایل با پسوند psm1 ذخیره کنیم. به عنوان مثال میتوانیم تابع Get-PingReply را درون یک فایل با نام PingModule.psm1 ذخیره و سپس توسط دستور Import-Module ماژول را ایمپورت کنیم:  
PS /> Import-Module ./PingModule.psm1
سپس توسط دستور Get-Module PingModule میتوانیم جزئیات ماژول ایمپورت شده را مشاهده نمائیم: 
PS /> Get-Module PingModule

ModuleType Version    PreRelease Name                                ExportedCommands
---------- -------    ---------- ----                                ----------------
Script     0.0                   PingModule                          Get-PingReply
به صورت پیش‌فرض تمام توابع درون اسکریپت export خواهند شد. اگر ExportedCommands خالی باشد به این معنا است که ماژول به درستی ایمپورت نشده‌است. به عنوان مثال اگر سعی کنید فایل قبل را با پسوند ps1 به عنوان ماژول ایمپورت کنید. خطایی هنگام ایمپورت کردن مشاهده نخواهید کرد و قسمت ExportedCommands خالی خواهد بود. در این‌حالت نیز امکان استفاده از تابع درون اسکریپت را خواهیم داشت؛ اما هیچ تضمینی نیست که به صورت یک ماژول به درستی عمل کند. بنابراین بهتر است ماژول‌هایی که ایجاد میکنیم حتماً پسوند psm1 داشته باشند.
PS /> Import-Module ./PingModule.ps1
PS /> Get-Module PingModule

ModuleType Version    PreRelease Name                                ExportedCommands
---------- -------    ---------- ----                                ----------------
Script     0.0                   PingModule
ممکن است بخواهیم یکسری توابع را به صورت private تعریف کنیم و فقط تعداد محدودی از توابع به صورت public باشند. در این حالت میتوانیم درون فایل psm1 با کمک دستور Export-ModuleMember اینکار را انجام دهیم: 
Function Get-PingReply {
    // as before
}

Function Get-PrivateFunction {
    Write-Debug 'This is a private function'
}

Export-ModuleMember -Function @(
    'Get-PingReply'
)
اضافه کردن متادیتا به ماژول‌ها
برای هر ماژول میتوانیم با کمک module manifest یکسری متادیتا به ماژول اضافه کنیم. این متادیتا درون یک فایل با پسوند psd1 که محتویات آن در واقع یک Hashtable است ذخیره میشود. برای ایجاد فایل manifest میتوانیم از دستور New-ModuleManifest استفاده کنیم. به عنوان مثال برای ایجاد فایل manifest برای ماژول PingModule.psm1 اینگونه عمل خواهیم کرد:  
$moduleSettings = @{
    Path                 = './PingModule.psd1'
    Description          = 'A module to ping a remote system'
    RootModule           = 'PingModule.psm1'
    ModuleVersion        = '1.0.0'
    FunctionsToExport    = @(
        'Get-PingReply'
    )
    PowerShellVersion    = '5.1'
    CompatiblePSEditions = @(
        'Core'
        'Desktop'
    )
}
New-ModuleManifest @moduleSettings
تنها پراپرتی موردنیاز برای ایجاد module manifest پراپرتی Path میباشد. این پراپرتی به فایلی که متادیتا قرار است درون آن ایجاد شود اشاره دارد. همانطور که مشاهده میکنید یکسری پراپرتی دیگر نیز اضافه کرده‌ایم و توسط splatted hash table (با کمک @) به دستور New-ModuleManifest ارسال کرده‌ایم. به این معنا که کلیدها و مقادیر درون hash table به جای اینکه یکجا به عنوان یک آبجکت به دستور موردنظر ارسال شوند، به صورت جدا پاس داده شده‌اند. اما اگر از $ استفاده میکردیم: 
$moduleSettings = @{
    Author = 'John Doe'
    Description = 'This is a sample module'
}

New-ModuleManifest $moduleSettings
با خطای زیر مواجه میشدیم: 
New-ModuleManifest : A parameter cannot be found that matches parameter name 'Author'.
در نهایت فایل manifest در مسیر تعیین شده ایجاد خواهد شد: 
#
# Module manifest for module 'PingModule'
#
# Generated by: sirwanafifi
#
# Generated on: 01/01/2023
#

@{

    # Script module or binary module file associated with this manifest.
    RootModule           = './PingModule.psm1'

    # Version number of this module.
    ModuleVersion        = '1.0.0'

    # Supported PSEditions
    CompatiblePSEditions = 'Core', 'Desktop'

    # ID used to uniquely identify this module
    GUID                 = '3f8561fc-c004-4c8e-b2fc-4a4191504131'

    # Author of this module
    Author               = 'sirwanafifi'

    # Company or vendor of this module
    CompanyName          = 'Unknown'

    # Copyright statement for this module
    Copyright            = '(c) sirwanafifi. All rights reserved.'

    # Description of the functionality provided by this module
    Description          = 'A module to ping a remote system'

    # Minimum version of the PowerShell engine required by this module
    PowerShellVersion    = '5.1'

    # Name of the PowerShell host required by this module
    # PowerShellHostName = ''

    # Minimum version of the PowerShell host required by this module
    # PowerShellHostVersion = ''

    # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
    # DotNetFrameworkVersion = ''

    # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
    # ClrVersion = ''

    # Processor architecture (None, X86, Amd64) required by this module
    # ProcessorArchitecture = ''

    # Modules that must be imported into the global environment prior to importing this module
    # RequiredModules = @()

    # Assemblies that must be loaded prior to importing this module
    # RequiredAssemblies = @()

    # Script files (.ps1) that are run in the caller's environment prior to importing this module.
    # ScriptsToProcess = @()

    # Type files (.ps1xml) to be loaded when importing this module
    # TypesToProcess = @()

    # Format files (.ps1xml) to be loaded when importing this module
    # FormatsToProcess = @()

    # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
    # NestedModules = @()

    # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
    FunctionsToExport    = 'Get-PingReply'

    # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
    CmdletsToExport      = '*'

    # Variables to export from this module
    VariablesToExport    = '*'

    # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
    AliasesToExport      = '*'

    # DSC resources to export from this module
    # DscResourcesToExport = @()

    # List of all modules packaged with this module
    # ModuleList = @()

    # List of all files packaged with this module
    # FileList = @()

    # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
    PrivateData          = @{

        PSData = @{

            # Tags applied to this module. These help with module discovery in online galleries.
            # Tags = @()

            # A URL to the license for this module.
            # LicenseUri = ''

            # A URL to the main website for this project.
            # ProjectUri = ''

            # A URL to an icon representing this module.
            # IconUri = ''

            # ReleaseNotes of this module
            # ReleaseNotes = ''

            # Prerelease string of this module
            # Prerelease = ''

            # Flag to indicate whether the module requires explicit user acceptance for install/update/save
            # RequireLicenseAcceptance = $false

            # External dependent modules of this module
            # ExternalModuleDependencies = @()

        } # End of PSData hashtable

    } # End of PrivateData hashtable

    # HelpInfo URI of this module
    # HelpInfoURI = ''

    # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix.
    # DefaultCommandPrefix = ''

}
هر ماژول باید یک آی‌دی منحصر به فرد داشته باشد که به صورت Guid توسط یک پراپرتی با همین نام تعیین میشود. برای هر پراپرتی درون این فایل توضیحی به صورت کامنت نوشته شده است؛ اما برای دیدن جزئیات کامل میتوانید به اینجا مراجعه نمائید. در اینجا RootModule به PingModule.psm1 تنظیم شده است. تنظیم این پراپرتی نحوه نمایش module type را در خروجی Get-Module مشخص میکند. این مقدار اگر به فایل ماژول (psm1) اشاره کند، نوع ماژول script در نظر گرفته میشود، اگر به یک DLL اشاره کند به binary و در نهایت اگر به یک فایلی با پسوند دیگری اشاره کند manifest در نظر گرفته میشود. همچنین درون فایل فوق یکسری پراپرتی مانند CmdletsToExport, VariablesToExport, AliasesToExport به صورت خودکار تنظیم شده‌اند. در نهایت برای تست صحت فایل میتوانیم از دستور Test-ModuleManifest استفاده کنیم: 
PS /> Test-ModuleManifest ./PingModule.psd1   

ModuleType Version    PreRelease Name                                ExportedCommands
---------- -------    ---------- ----                                ----------------
Script     1.0.0                 PingModule                          Get-PingReply
پابلیش ماژول
در نهایت توسط Publish-Module میتوانیم ماژول موردنظرمان را درون یک مخزن PowerShell پابلیش کنیم. رایج‌ترین مخزن برای انتشار پکیج‌های PowerShell یک فید نیوگت (مانند PowerShell Gallery) است. همچنین میتوانیم یک مخزن لوکال نیز برای پابلیش پکیج‌ها نیز ایجاد کنیم و خروجی نهایی را به صورت یک فایل با پسوند nupkg با دیگران به اشتراک بگذاریم. برای اینکار ابتدا نیاز است یک مخزن لوکال را به PowerShell معرفی کنیم: 
PS /> Register-PSRepository -Name 'PSLocal' `                       
>>     -SourceLocation "$(Resolve-Path $RepoPath)" `
>>     -PublishLocation "$(Resolve-Path $RepoPath)" `
>>     -InstallationPolicy 'Trusted'
در کد فوق میتوانید مقدار متغیر RepoPath را به محل مدنظر روی سیستم‌تان تنظیم کنید. ساختار ماژولی که میخواهیم پابلیش کنیم نیز اینچنین خواهد بود: 
ProjectRoot 
| -- PingModule
     | -- PingModule.psd1 
     | -- PingModule.psm1
در ادامه برای پابلیش ماژول فوق درون ProjectRoot دستور Publish-Module را اینگونه اجرا خواهیم کرد: 
PS /> Publish-Module -Path ./PingModule/ -Repository PSLocal
خروجی دستور فوق یک فایل با پسوند nupkg در مسیر مخزنی است که معرفی کردیم. همچنین با کمک دستور Find-Module میتوانیم ماژول موردنظر را لیست کنیم: 
PS /> Find-Module -Name PingModule -Repository PSLocal

Version              Name                                Repository           Description
-------              ----                                ----------           -----------
0.0.1                PingModule                          PSLocal              Get-PingReply is a.
برای نصب ماژول روی یک سیستم دیگر نیز ابتدا باید مطمئن شوید که سیستم مقصد، مخزن لوکال را تعریف کرده باشد و در نهایت توسط دستور Install-Module میتوانیم ماژول موردنظر را نصب کنیم: 
PS /> Install-Module -Name PingModule -Repository PSLocal -Scope CurrentUser
همچنین امکان پابلیش کردن ورژن‌های متفاوت را نیز خواهیم داشت: 
ProjectRoot 
| -- PingModule
     | -- 1.0.0
          | -- PingModule.psd1 
          | -- PingModule.psm1
     | -- 1.0.1
          | -- PingModule.psd1 
          | -- PingModule.psm1
برای پابلیش کردن هر کدام از ماژول‌های فوق باید به ازای هر کدام یکبار دستور Publish-Module را اجرا کنیم: 
PS /> Publish-Module -Path ./PingModule/1.0.0 -Repository PSLocal
PS /> Publish-Module -Path ./PingModule/1.0.1 -Repository PSLocal
اکنون حین نصب ماژول میتوانیم ورژن را نیز تعیین کنیم؛ در غیراینصورت آخرین ورژن یعنی 1.0.1 نصب خواهد شد: 
PS /> Install-Module -Name PingModule -Repository PSLocal -Scope CurrentUser
PS /> Get-InstalledModule -Name PingModule                                                                            
Version              Name                                Repository           Description
-------              ----                                ----------           -----------
1.0.1                PingModule                          PSLocal              Get-PingReply is a.
ساختار مناسب برای ایجاد ماژول‌ها
برای توسعه ماژول‌های PowerShell توصیه میشود که از ساختار Multi-file layout استفاده شود به این معنا که بخش‌های مختلف پروژه به قسمت‌های کوچک‌تر و قابل‌نگهداری تقسیم شوند: 
ProjectRoot 
| -- PingModule
     | -- 1.0.0
          | -- Public/
          | -- Private/
          | -- PingModule.psd1
          | -- PingModule.psm1
در اینحالت باید اسکریپت‌ها و فایل‌های موردنیاز را توسط dot sourcing درون فایل ماژول بارگذاری کنیم: 
$ScriptList = Get-ChildItem -Path $PSScriptRoot/Public/*.ps1 -Filter *.ps1

foreach ($Script in $ScriptList) {
    . $Script.FullName
}

$ScriptList = Get-ChildItem -Path $PSScriptRoot/Private/*.ps1 -Filter *.ps1

foreach ($Script in $ScriptList) {
    . $Script.FullName
}
یک مثال عملی
در ادامه میخواهیم یک ماژول تهیه کنیم که قابلیت امضاء روی PDF را با کمک کتابخانه iTextSharp.LGPLv2.Core انجام دهیم و به شکل زیر برای کاربر قابل استفاده باشد: 
PS /> Set-PDFSingature -PdfToSign "./sample_invoice.pdf" -SignatureImage "./sample_signature.jpg"
بنابراین ساختار پروژه را اینگونه ایجاد خواهیم کرد: 
ProjectRoot 
| -- SignPdf
     | -- 1.0.0
          | -- Public/
               | -- dependencies/
                    | -- BouncyCastle.Crypto.dll
                    | -- System.Drawing.Common.dll
                    | -- Microsoft.Win32.SystemEvents.dll
                    | -- iTextSharp.LGPLv2.Core.dll
               | -- Set-PDFSingature.ps1
          | -- SignPdf.psd1
          | -- SignPdf.psm1
برای استفاده از پکیج ذکر شده نیاز خواهد بود که Dllهای موردنیاز را نیز به عنوان وابستگی به پروژه اضافه کنیم (محتویات پوشه dependencies) سپس درون فایل ماژول (SignPdf.psm1) همانطور که عنوان شد میبایست اسکریپت‌ها درون پوشه Public را بارگذاری کنیم و همچنین درون تابعی که قرار است Export شود را نیز تعیین کرده‌ایم (Set-PDFSingature) 
$ScriptList = Get-ChildItem -Path $PSScriptRoot/Public/*.ps1 -Filter *.ps1

foreach ($Script in $ScriptList) {
    . $Script.FullName
}

Export-ModuleMember -Function Set-PDFSingature
در ادامه درون فایل Set-PDFSignature پیاده‌سازی را انجام خواهیم داد: 
using namespace iTextSharp.text
using namespace iTextSharp.text.pdf
using namespace System.IO

Function Set-PDFSingature {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateScript({
                if (Test-Path ([Path]::Join($(Get-Location), $_))) {
                    return $true
                }
                else {
                    throw "Signature image not found"
                }
                if ($_.EndsWith('.pdf')) {
                    return $true
                }
                else {
                    throw "File extension must be .pdf"
                }
            })]
        [string]$PdfToSign,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateScript({
                if (Test-Path ([Path]::Join($(Get-Location), $_))) {
                    return $true
                }
                else {
                    throw "Signature image not found"
                }
                if ($_.EndsWith('.png') -or $_.EndsWith('.jpg')) {
                    return $true
                }
                else {
                    throw "File extension must be .png or .jpg"
                }
            })]
        [string]$SignatureImage,
        [Parameter(Mandatory = $false, ValueFromPipeline = $true)]
        [int]$XPos = 130,
        [Parameter(Mandatory = $false, ValueFromPipeline = $true)]
        [int]$YPos = 50
    )
    Try {
        Add-Type -Path "$PSScriptRoot/dependencies/*.dll"
        $pdf = [PdfReader]::new("$(Get-Location)/$PdfToSign")
        $fs = [FileStream]::new("$(Get-Location)/$PdfToSign-signed.pdf", 
            [FileMode]::Create)
        $stamper = [PdfStamper]::new($pdf, $fs)
        $stamper.AcroFields.AddSubstitutionFont([BaseFont]::CreateFont())
        $content = $stamper.GetOverContent(1)
        $width = $pdf.GetPageSize(1).Width
        $image = [Image]::GetInstance("$(Get-Location)/$SignatureImage")
        $image.SetAbsolutePosition($width - $XPos, $YPos)
        $image.ScaleAbsolute(100, 30)
        $content.AddImage($image)
        $stamper.Close()
        $pdf.Close()
        $fs.Dispose()
    }
    Catch {
        Write-Host "Error: $($_.Exception.Message)"
    }
}
روال قرار دادن یک امضاء بر روی یک فایل PDF قبلاً در سایت توضیح داده شده است. کدهای فوق در واقع معادل PowerShell همان کدهای موجود در سایت هستند و نکته خاصی ندارند. در نهایت میتوانیم ماژول تهیه شده را روی مخزن موردنظر پابلیش کنیم: 
PS /> Publish-Module -Path ./SignPdf/1.0.0 -Repository PSLocal
برای نصب ماژول میتوانیم از دستور Install-Module استفاده کنیم: 
PS /> Install-Module -Name SignPdf -Repository PSLocal -Scope CurrentUser
در نهایت برای استفاده از ماژول ایجاد شده میتوانیم اینگونه عمل کنیم: 
PS /> Set-PDFSingature -PdfToSign "./sample_invoice.pdf" -SignatureImage "./sample_signature.jpg"
خروجی نیز فایل امضاء شده خواهد بود: 

کدهای ماژول را میتوانید از اینجا دانلود کنید. 

مطالب
چگونه پروژه‌های Angular ی سبکی داشته باشیم - قسمت اول
یکی از عیب‌هایی که برنامه نویسان front-end و گاها بعضی از مدیران از Angular می‌گیرند، حجم زیاد صفحاتی است که با Angular کار می‌شود. در نتیجه‌ی جستجوی مشکل ذکر شده، با تعدادی پاسخ مشابه ^ و ^ روبرو می‌شویم که هیچ کدام روش صحیحی را برای رفع مشکل ذکر شده عنوان نکرده‌اند. در ادامه پروژه‌ی Angular ای را شروع می‌کنیم و حجم صفحات خروجی را مورد بررسی قرار می‌دهیم. سپس نحوه‌ی بهینه سازی و کم کردن حجم صفحات خروجی را بررسی می‌کنیم. 

برای شروع، CMD را در مسیر دلخواهی برای ساخت پروژه باز کرده  و ابتدا Angular CLI را توسط دستور زیر به آخرین نسخه‌ی موجود به‌روز رسانی می‌کنیم:
npm install -g @angular/cli
و سپس پروژه‌ی انگیولاری را  با دستور زیر می‌سازیم:
ng new HowToKeepAngularSizeSmall
پاسخ سوال ? Would you like to add Angular routing? (y/N)  را  y و scss را به عنوان فرمت stylesheet انتخاب می‌کنیم.
در ادامه پروژه را با استفاده از دستور زیر
ng serve --open --prod
اجرا می‌کنیم . سپس با استفاده از ابزار DevTools console از تب network، حجم فایل‌های لود شده را بررسی می‌کنیم:

حجم خروجی پروژه بعد از ساخت 222KB است.
حال برای آنکه پروژه‌ی جاری را به پروژه‌های واقعی نزدیک‌تر کنیم، بسته‌های npm زیر را به فایل package.json اضافه کرده و با دستور npm i بسته‌ها را نصب می‌کنیم.
"@agm/core": "^1.0.0-beta.5",
"@angular/flex-layout": "^7.0.0-beta.23",
"@angular/material": "^7.3.3",
"@angular/platform-browser": "~7.2.0",
"@asymmetrik/ngx-leaflet": "^5.0.1",
"@ngx-loading-bar/router": "1.3.1",
"@ngx-translate/core": "^11.0.1",
"@ngx-translate/http-loader": "^4.0.0",
"@progress/kendo-angular-buttons": "^4.3.3",
"@progress/kendo-angular-dateinputs": "^3.6.0",
"@progress/kendo-angular-dialog": "^3.10.1",
"@progress/kendo-angular-dropdowns": "^3.5.1",
"@progress/kendo-angular-excel-export": "^2.3.0",
"@progress/kendo-angular-grid": "^3.13.0",
"@progress/kendo-angular-inputs": "^4.2.0",
"@progress/kendo-angular-intl": "^1.7.0",
"@progress/kendo-angular-l10n": "^1.3.0",
"@progress/kendo-angular-layout": "^3.2.0",
"@progress/kendo-data-query": "^1.5.0",
"@progress/kendo-drawing": "^1.5.8",
"@progress/kendo-theme-default": "^3.3.1",
"@swimlane/ngx-datatable": "^14.0.0",
"angular-calendar": "0.23.7",
"angular-tree-component": "7.0.1",
"bootstrap": "^3.4.0",
"chart.js": "2.7.2",
"d3": "4.13.0",
"dragula": "3.7.2",
"hammerjs": "2.0.8",
"intl": "1.2.5",
"leaflet": "1.3.1",
"moment": "2.21.0",
"ng2-charts": "1.6.0",
"ng2-dragula": "1.5.0",
"ng2-file-upload": "1.3.0",
"ng2-validation": "4.2.0",
"ngx-perfect-scrollbar": "5.3.5",
"ngx-quill": "2.2.0",
"screenfull": "3.3.2",
"font-awesome": "^4.7.0",
"jalali-moment": "^3.3.1",
"jquery": "^3.3.1",
"ng-snotify": "^4.3.1",
"normalize.css": "^8.0.1",
و خطوط زیر را به styles.scss اضافه می‌کنیم:
@import "~bootstrap/dist/css/bootstrap.css";
@import "~@progress/kendo-theme-default/scss/all";
@import '@angular/material/prebuilt-themes/pink-bluegrey.css';
@import '~perfect-scrollbar/css/perfect-scrollbar.css';
@import "~ng-snotify/styles/material";
و قسمت scripts زیر را به فایل angular.json اضافه می‌کنیم:
"scripts": [
              "node_modules/jquery/dist/jquery.js",
              "node_modules/bootstrap/dist/js/bootstrap.min.js",
              "node_modules/hammerjs/hammer.min.js"
            ],
و فایل app.module.ts را نیز به صورت زیر تغییر می‌دهیم:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';


// Import Material
import {
  MatFormFieldModule, MatInputModule,
  MatButtonModule, MatButtonToggleModule,
  MatDialogModule, MatIconModule,
  MatSelectModule, MatToolbarModule,
  MatDatepickerModule,
  DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE, MatTableModule, MatCheckboxModule, MatRadioModule, MatCardModule, fadeInContent,
  MatListModule, MatProgressBarModule, MatTabsModule,
  MatSidenavModule,
  MatSlideToggleModule,
  MatMenuModule
} from '@angular/material';



// Import kendo angular ui
import { ButtonsModule } from '@progress/kendo-angular-buttons';
import { GridModule, ExcelModule } from '@progress/kendo-angular-grid';
import { DropDownsModule } from '@progress/kendo-angular-dropdowns';
import { InputsModule } from '@progress/kendo-angular-inputs';
import { DateInputsModule } from '@progress/kendo-angular-dateinputs';
import { DialogsModule } from '@progress/kendo-angular-dialog';
import { RTL } from '@progress/kendo-angular-l10n';
import { LayoutModule } from '@progress/kendo-angular-layout';
import { WindowService, WindowRef, WindowCloseResult, DialogService, DialogRef, DialogCloseResult } from '@progress/kendo-angular-dialog';

import { SnotifyModule, SnotifyService, SnotifyPosition, SnotifyToastConfig, ToastDefaults } from 'ng-snotify';


import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,

    // material 
    MatSidenavModule,
    MatSlideToggleModule,
    MatInputModule,
    MatFormFieldModule,
    MatButtonModule, MatButtonToggleModule,
    MatDialogModule, MatIconModule,
    MatSelectModule, MatToolbarModule,
    MatDatepickerModule,
    MatCheckboxModule,
    MatRadioModule,
    MatCardModule,
    MatMenuModule,
    MatListModule,
    MatProgressBarModule,
    MatTabsModule,


    // kendo-angular
    ButtonsModule,
    GridModule,
    ExcelModule,
    DropDownsModule,
    InputsModule,
    DateInputsModule,
    DialogsModule,
    LayoutModule,

    SnotifyModule,


  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

بدون اینکه component جدیدی را به پروژه‌ی جاری اضافه کنیم، پروژه را با دستور ng serve --open --prod  اجرا کرده و خروجی را بررسی می‌کنیم:

همانطور که می‌بینید بدون افزودن کامپوننت جدیدی، حجم خروجی از 222KB به 582KB رسیده‌است. معمولا در هر پروژه نیاز به تعدادی دایرکتیو و سرویس پایه نیز می‌باشد که کم کم به حجم خروجی صفحات می‌افزاید. در نظر بگیرید که هنوز هیچ قالب خاصی برای صفحه مورد نظرمان استفاده نشده و به حجم 582KB رسیده‌ایم. برای نمونه می‌توانیم سری به سایت madewithangular.com بزنیم و حجم خروجی تعدادی از سایت‌های نوشته شده‌ی با انگیولار را بررسی کنیم. سایت‌های زیر خروجی بالای 1.5MB دارند. همچنین سایتی را که خودم تقریبا یک سال پیش شروع کرده بودم، حجم خروجی آن  2.7MB است:

دلیل بالا رفتن حجم خروجی، اضافه شدن فایل‌های JavaScript و style-sheet به bundle اصلی پروژه است. برای مثال حجم فایل main.js را در نمونه‌های ذکر شده بررسی کنید. 

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

مطالب
بررسی angular.bootstrap
در پست‌های قبلی با مفهوم ng-app آشنا شدید. دایرکتیو ng-app برای استفاده از راه انداز خودکار فریم ورک Angular (معروف به auto-bootstrap) استفاده می‌شود. در حالت پیش فرض، به ازای هر سند Html فقط می‌توان یک ماژول در Angular تعریف کرد. در سند مربوطه اولین المانی که دارای دایرکتیو ng-app باشد به عنوان عنصر ریشه در نظر گرفته می‌شود و تمام عناصر تعریف شده در محدوده این دایرکتیو قایل استفاده برای ماژول مورد نظر خواهد بود. سایر عناصر حتی اگر ng-app یکسان داشته باشند نادیده گرفته می‌شوند.
ابتدا یک مثال زیر را به روش auto-bootstrap بررسی  می‌کنیم:
<div ng-app="myApp">
    <div ng-controller="myController as ctrl">
       <span>ng-app #1</span> {{ctrl.firstName}} {{ctrl.lastName}}
    </div>
</div>

<div ng-app="myApp">
    <div ng-controller="myController as ctrl">
        <span>ng-app #2</span> {{ctrl.firstName}} {{ctrl.lastName}}
    </div>
</div>

@section scripts
{
    <script type="text/javascript" src="~/scripts/Modules/module8.js"></script>
}
در کنترلر مورد نظر نیز تعاریف به صورت زیر خواهد بود:
var app = angular.module('myApp', []);

app.controller('myController', function ()
{
    this.firstName = "Masoud";
    this.lastName = "Pakdel";
});
در مثال بالا دو تگ div وجود دارد که به صورت مشترک با استفاده از دایرکتیو ng-app به یک ماژول اشاره می‌کنند. طبق گفته‌ها بالا در روش auto-bootstrap اولین عنصری که دارای دایرکتیو ng-app باشد به عنوان محدوده ماژول مورد استفاده قرار خواهد گرفت در نتیجه سایر المان‌ها (در اینجا منظور تگ div دوم است)نادیده گرفته خواهند شد.  پس خروجی به صورت زیر می‌شود:

اما اگر قصد داشته باشیم که در یک سند html دو نقطه شروع تعریف کنیم در حالی که هر کدام از یک منبع داده استفاده نمایند باید bootstrap برنامه را به صورت دستی تعیین کرد. برای این کار کافیست از دستور angular.bootstrap به صورت زیر استفاده نماییم:
پیاده سازی مثال بالا
<div id="myAppContainer1">
    <div ng-controller="myController as ctrl">
        <span>ng-app #1</span> {{ctrl.firstName}} {{ctrl.lastName}}
    </div>
</div>

<div id="myAppContainer2">
    <div ng-controller="myController as ctrl">
        <span>ng-app #2</span> {{ctrl.firstName}} {{ctrl.lastName}}
    </div>
</div>

@section scripts
{
    <script type="text/javascript" src="~/scripts/Modules/module8.js"></script>
}
اولین تغییر مورد نظر این است که، دایرکتیو ng-app  حذف شد و به جای آن id برای تگ div تعیین کردیم. در فایل کنترلر مورد نظر نیز تغییر زیر را اعمال می‌کنیم:
var app = angular.module('myApp', []);

app.controller('myController', function ()
{
    this.firstName = "Masoud";
    this.lastName = "Pakdel";
});

angular.bootstrap(document.getElementById("myAppContainer1"), ["myApp"]);

angular.bootstrap(document.getElementById("myAppContainer2"), ["myApp"]);
با استفاده از دستور angular.bootstrap می‌توان بر اساس id تعیین شده تگ مورد نظر در سند را به دست آورد و ماژول مورد نظر را به آن نسبت داد.

خروجی مثال بالا:


برخلاف حالت قبل هر دو نقطه شروع به یک منبع داده اشاره می‌کنند و محدودیت حالت قبل برطرف می‌شود.
مطالب
توسعه برنامه های Cross Platform با Xamarin Forms & Bit Framework - قسمت چهارم
تا قسمت سوم توانستیم Xamarin را نصب و پروژه‌ی اولیه آن را بیلد کنیم. سپس کد مشترک بین سه پلتفرم را بر روی Windows اجرا و Edit & continue آن را هم تست کردیم که هم برای UI ای که با Xaml نوشته می‌شود و هم برای منطقی که با CSharp نوشته می‌شود، کار می‌کند.
همانطور که گفتیم، کد UI و Logic برای هر سه پلتفرم مشترک است؛ منتهی به علت امکانات دیباگ فوق العاده و سرعت بیشتر ویندوز، ابتدا آن را بر روی ویندوز تست کردیم و بعد برای تکمیل UI، آن را بر روی Android اجرا می‌کنیم. این بار می‌توانید دو پروژه UWP و iOS را Unload کنید و سپس پروژه Android ای را در صورت Unload بودن Load کنید. (با راست کلیک نمودن روی پروژه). این کار باعث می‌شود Visual Studio بیهوده کند نشود؛ مخصوصا اگر سیستم شما ضعیف است.
ابتدا با موبایل یا تبلت اندرویدی شروع می‌کنیم. اگر چه Xamarin از نسخه‌ی 4.0.3 اندروید به بالا را پشتیبانی می‌کند، ولی توصیه می‌کنم وقتتان را بر روی گوشی‌های اندرویدی کمتر از 4.4 تلف نکنید. دستگاه را می‌توانید، هم به صورت USB و هم به صورت Wifi استفاده کنید. ابتدا باید دستگاه اندرویدی خود را آماده‌ی برای دیباگ کنید. برای این منظور مقاله‌های فارسی و انگلیسی زیادی وجود دارند که می‌توانید از آن‌ها استفاده کنید. من عبارت "اندروید debug" را جستجو کردم و به این مقاله رسیدم. همچنین Android SDK شما باید USB debugging اش نصب شده باشد که البته حجم زیادی ندارد. برای بررسی این مورد ابتدا از وجود فولدر extras\google\usb\_driver درAndroid SDK خود مطمئن شوید. حال سؤال این است که ویژوال استودیو، Android SDK را کجا نصب کرده‌است که خیلی ساده در این لینک توضیح داده شده‌است.
اگر فولدر extras\google\usb\_driver وجود نداشت، باید آن را نصب کنید که خیلی ساده توسط Android SDK Manager امکان پذیر است؛ ولی نه! امکان پذیر نیست!
دلیل: در Xamarin شما همیشه بر روی آخرین SDK‌ها حرکت می‌کنید. این شامل Windows SDK 17134 و Android SDK 27 و iOS SDK 11 است. وقتی از نسخه‌ی فعلی ویژوال استودیو، یعنی 15.8 به نسخه‌ی بعدی ویژوال استودیو که الان Preview است بروید، یعنی 15.9، عملا به این معنا است که به Windows SDK 17763 و Android SDK 28 و iOS SDK 12 می‌روید. این بزرگترین مزیت Xamarin است و این یعنی شما همیشه به صد در صد امکانات هر پلتفرم در زبان CSharp دسترسی دارید و همیشه آخرین SDK هر سیستم عامل در اختیار شماست و اگر دوستی از طریق Swift توانست مثالی از ARKit 2.0 را در iOS 12 پیاده سازی کند، قطعا شما هم می‌توانید. همچنین تیم Xamarin نه تنها این امکانات را بلکه Documentation لازم را نیز در اختیار شما قرار می‌دهد. چون در همین مثال، مستندات Apple به زبان Swift / Objective-C بوده و مستندات Xamarin به زبان CSharp.
حال اگر سری به فولدر Android SDK نصب شده‌ی توسط Visual Studio بزنید، مشاهده می‌کنید که خبری از Android SDK Manager نیست! به صورت رسمی، مدتی است که گوگل در نسخه‌های اخیر Android SDK، دیگر Android SDK Manager را ارائه نمی‌کند و همانطور که گفتم شما الان بر روی آخرین نسخه‌ی Android SDK هستید. هر چند ترفندهایی وجود دارد که این Manager را باز می‌گردانند، ولی لزومی به انجام این کار در Xamarin نیست و شما می‌توانید از Android SDK Manager ای که تیم Xamarin ارائه داده‌است، استفاده کنید. همین مسئله در مورد Android Virtual Device Manager که برای مدیریت Emulator‌ها بود نیز صدق می‌کند.
برای استفاده از این دو، ضمن استفاده از ابزارهای دور زدن تحریم، در ویژوال استودیو، در منوی Tools، به قسمت Android رفته و Android SDK Manager را باز کنید. Android Emulator Manager نیز جایگزین Android Virtual Device Manager ای است که قبلا توسط گوگل ارائه می‌شد. حال بعد از باز کردن Android SDK Manager ارائه شده توسط Xamarin، به برگه‌ی Tools آن بروید و از  قسمت extras مطمئن شوید که Google USB driver تیک خورده باشد.
حال پس از وصل کردن گوشی یا تبلت اندرویدی به سیستم توسط کابل USB و Set as startup project نمودن پروژه‌ی XamApp.Android که در قسمت قبل آن را Clone کرده بودید، می‌توانید پروژه را بر روی گوشی خود اجرا کنید. اگر نام گوشی خود را در کنار دکمه‌ی سبز اجرای پروژه (F5) نمی‌بینید، بستن و باز کردن Visual Studio را امتحان کنید. 

پروژه را که اجرا کنید، اولین بیلد کمی طول می‌کشد (اولین بار دو برنامه بر روی گوشی شما نصب می‌شوند که برای کار دیباگ در Xamarin لازم هستند) و اساسا بیلد یک پروژه‌ی اندرویدی کند است. خوشبختانه به واسطه وجود Xaml edit and continue احتیاجی به Stop - Start کردن پروژه و بیلد کردن برای اعمال تغییرات UI نیست و به محض تغییر Xaml، می‌توانید تاثیر آن را در گوشی خود ببینید. ولی برای هر تغییر CSharp باید Stop - Start و Build کنید که زمان بر است و به همین علت تست بر روی پروژه ویندوزی را برای پیاده سازی منطق برنامه پیشنهاد می‌کنیم. البته در نسخه‌ی 15.9 ویژوال استودیو، سرعت بیلد تا 40% بهبود یافته است.
ممکن است شما گوشی اندرویدی یا تبلت نداشته باشید که بخواهید بر روی آن تست کنید و یا مثلا گوشی شما Android 7 هست و می‌خواهید بر روی Android 8 تست بگیرید. در این جا شما احتیاج به استفاده از Emulator را خواهید داشت.
توجه داشته باشید که Emulator شما ترجیحا نباید ARM باشد و بهتر است یا X86 یا X64 باشد، وگرنه ممکن است خیلی کند شود. همچنین بهتر است Google Play Services داشته باشد. همچنین ترجیحا دنبال گزینه‌ی اجرا کردن Emulator نروید؛ اگر خود ویندوز شما درون یک Virtual Machine در حال اجراست.

ابتدا ضمن جستجو کردن "فعال سازی intel virtualization"، اقدام به فعال سازی این امکان در سیستم خود کنید. این آموزش را مناسب دیدم.
گزینه‌های مطرح: [Google Android Emulator] - [Genymotion] - [Microsoft Hyper-V Android Emulator] که فقط یکی از آنها را لازم دارید.

Google Android Emulator توسط خود Google ارائه می‌شود و دارای Google Play Services نیز هست. بر اساس این آموزش به صفحه Workloads در Visual Studio Installer بروید و از قسمت Xamarin دو مورد "Google Android Emulator API Level 27" و "Intel Hardware Accelerated Execution Manager (HAXM) global install" را نصب کنید. توجه داشته باشید که بدین منظور احتیاج به ابزارهای دور زدن تحریم دارید؛ زیرا نیاز به دسترسی به سرورهای گوگل هست. این Emulator آماده برای دیباگ هست و نیازی به اقدام خاصی نیست.

Genymotion حجم کمتری دارد و برای دانلود احتیاج به ابزارهای دور زدن تحریم را ندارد و اساسا نسبت به بقیه بر روی سیستم‌های ضعیف‌تر، بهتر کار می‌کند. فقط Emulator ای که با آن می‌سازید، به صورت پیش فرض Google Play Services را ندارد که در آخرین نسخه‌های آن گزینه Open  GApps به toolbar اضافه شده که Google Play Services را اضافه می‌کند. (از انجام هر گونه عملیات پیچیده بر اساس آموزش‌هایی که برای نسخه‌های قدیمی‌تر Genymotion هستند، پرهیز کنید). مطابق با ابتدای همین آموزش برای دستگاه‌های اندرویدی، Emulator خود را آماده برای دیباگ کنید.

Microsoft Hyper-V android emulators. مایکروسافت قبلا اقدام به ارائه یک Android Emulator کرده بود که برای نسخه 4 و 5 اندروید بودند و بزرگ‌ترین ضعف آنها عدم پشتیبانی از Google Play Services بود که ادامه داده نشدند. ولی سری جدید ارائه شده توسط مایکروسافت چنین مشکلی را ندارد. اگر CPU شما AMD بوده و روش‌های قبلی برای شما کند هستند یا اساسا کار نمی‌کنند، یا در حال حاضر در حال استفاده از Docker for Windows هستید که از Hyper-V استفاده می‌کنید و قصد استفاده مجدد از منابع موجود را دارید، این نیز گزینه خوبی است که جزئیات آن را می‌توانید در  اینجا  دنبال کنید. این Emulator آماده برای دیباگ هست و نیازی به اقدام خاصی نیست. 

پس از اینکه Emulator خود را ساختید، آن را اجرا کنید. سپس برنامه را از درون ویژوال استدیو اجرا کنید. مطابق نسخه ویندوزی، دوباره یک دکمه دارید و یک Label، عدد بر روی Label، با هر بار کلیک کردن بر روی دکمه، افزایش می‌یابد.
سرعت اجرای این برنامه در Emulator یا گوشی شما برای دیباگ است و در حالت Release، سرعت چندین برابر بهتر خواهد شد و به هیچ وجه تست‌های Performance را بر روی Debug mode انجام ندهید.

حال نوبت به پابلیش پروژه می‌رسد. در این قسمت باید توجه کنید که حجم Apk شما برای پروژه‌ی XamApp مثال ما به 7 مگ می‌رسد که برای یک فرم ساده خیلی زیاد به نظر می‌رسد. ولی اگر شما بجای یک فرم ساده، صد فرم پیچیده نیز داشته باشید، باز هم این حجم به 8 مگ نخواهد رسید. حجم Apk خیلی متاثر از کدهای شما نیست، بلکه شامل موارد زیر است:
1- NET. که خود شامل CLR  & BCL است. (BCL (Base Class Library  مثل کلاس‌های string - Stream - List - File و (CLR (Common language runtime که شامل موارد لازم برای اجرای کدها است. این پیاده سازی بر اساس NET Standard 2.0. بوده که عملا اجازه استفاده از تعداد خیلی زیادی از کتابخانه‌های موجود را می‌دهد، حتی Entity framework core! البته هر کتابخانه حجم DLL‌های خودش را اضافه می‌کند.
2- Android Support libraries که به شما اجازه می‌دهد از تعداد زیادی (و البته نه همه) امکانات نسخه‌های جدید اندروید در پروژه‌تان استفاده کنید که بر روی نسخ قدیمی‌تر Android نیز کار کنند. همچنین با یکپارچگی با Google Play Services عملا خیلی از کارها ساده‌تر و با Performance بهتری انجام می‌شود، مانند گرفتن موقعیت کاربر جاری.
3-  Xamarin essentials . اگر چه در CSharp شما به صد در صد امکانات هر سیستم عامل دسترسی دارید و می‌توانید مثلا مقدار درصد شارژ باطری را بخوانید، ولی اینکار مستلزم نوشتن سه کد CSharp ای برای Android - iOS - Windows است که طبیعتا کار را سخت می‌کند. اما Xamarin Essentials به شما اجازه می‌دهد با یک کد CSharp واحد برای هر سه پلتفرم، با باطری، کلیپ‌بورد، قطب نما و خیلی موارد دیگر کار کنید.
4- Xamarin.Forms. اگر Button و Label ای که در مثال برنامه داشتیم، با یکبار نوشتن بر روی هر سه پلتفرم دارند کار می‌کنند، در حالی که هر پلتفرم، Button مخصوص به خود را دارد؛ این را Xamarin Forms مدیریت می‌کند. علاوه بر این، Binding نیز به عهده‌ی Xamarin Forms است.
5- Prism Autofac Bit Framework: درک آن‌ها نیاز به دنبال کردن آموزش‌های این دوره را دارد؛ ولی به صورت کلی معماری پروژه شما بسیار کارآمد و حرفه‌ای خواهد شد و به کدی با قابلیت نگهداری بالا خواهید رسید. 
6-  Rg Plugins Popup  و  Xamanimation  نیز دو کتابخانه‌ی UI بسیار کاربردی و جالب هستند که در طول این آموزش از آنها استفاده خواهد شد.
حجم 7 مگ برای این تعداد کتابخانه و امکان، خیلی زیاد نیست و شما عملا تعداد زیادی از پروژه‌های خود را می‌توانید با همین حجم ببندید و اگر مثلا به پروژه‌ی Humanizer خیلی علاقه داشته باشید (که در این صورت حق هم دارید!) می‌توانید با اضافه شدن چند کیلوبایت (!) به پروژه آن را داشته باشید. اکثر کتابخانه‌های NET. ای سبک هستند. همچنین موقع قرار گرفتن در پروژه، فشرده سازی نیز می‌شوند و قسمت‌های استفاده نشده‌ی آن‌ها نیز توسط Linker حذف می‌شوند.
علاوه بر این، اجرای برنامه بر روی گوشی‌های ضعیف و قدیمی کمی طول می‌کشد. این مربوط به اجرای برنامه است؛ نه باز شدن فرم مثال ما که دارای Button و Label بود و اگر مثال ما دو فرم داشته باشد (که در آموزش‌های بعدی به آن می‌رسیم) می‌بینید که چرخش بین فرم‌ها بسیار سریع است.

مواردی مهم در زمینه‌ی بهبود عملکرد پروژه‌های Xamarin در Android
در ابتدا باید بدانید Apk شما شامل دو قسمت است؛ یکی کدهای CSharp ای شما که DotNet ای بوده و در کنار کدهای کتابخانه‌هایی چون Json.NET بر روی DotNet اجرا می‌شوند. دیگری کتابخانه‌ای است که مثلا با Java نوشته شده و بعد برای استفاده در CSharp بر روی آن یک Wrapper یا پوشاننده توسعه داده شده‌است. عموما توسعه دهندگان چنین پروژه‌هایی، ابتدا پروژه را به Java می‌نویسند و بعد برای JavaScript - CSharp و ... Wrapper ارائه می‌دهند.
برای بهبود اینها ابزارهایی چون AOT-NDK-LLVM-ProGurad-Linker و ... وجود دارند که سعی می‌کنم به صورت ساده آنها را توضیح دهم.

وظیفه ProGurad این است که از قسمتی از پروژه‌ی شما که بخاطر کتابخانه‌های Java ای، عملا DotNet ای نیست، کدهای اضافه و استفاده نشده را حذف کند.
ممکن است استفاده از ProGurad باعث شود کلاسی که داینامیک استفاده شده است، به اشتباه حذف شود. پروژه XamApp دارای یک ProGuard configuration file است که جلوی چنین اشتباهاتی را حتی الامکان می‌گیرد.
همچنین ProGurad که در داخل Android SDK قرار دارد، به Space در طول مسیر حساس است (!) و با توجه به اینکه مسیر پیش فرض Android SDK نصب شده‌ی توسط ویژوال استودیو دارای Space است (C:\Program Files (x86)\Android\android-sdk)  شما در همان ابتدا دچار مشکل می‌شوید! برای حل این مشکل ابدا فولدر Android SDK را جا به جا نکنید؛ بلکه از امکانی در ویندوز به نام Junction folder یا فولدر جانشین استفاده کنید. بدین منظور دستور زیر را وارد کنید:
mklink /j C:\android-sdk "C:\Program Files (x86)\Android\android-sdk"
این مورد باعث می‌شود که مسیر C:\android-sdk نیز به همان مسیر پیش فرض اشاره کند و این دو مسیر در واقع یکی هستند. امیدوارم این امکان را با قابلیت Shortcut سازی در ویندوز اشتباه نگیرید! حال از منوی Tools > Options > Xamarin > Android Settings مسیر Android SDK را به C:\android-sdk تغییر دهید که فاقد Space است و ویژوال استودیو را ببندید و باز کنید.

NDK که در ادامه SDK برای Android قرار می‌گیرد، Native Development Kit است و باعث می‌شود هم DLL‌های DotNet ای و هم Jar‌های Java ای به فایل‌های so تبدیل شوند. so همان DLL ویندوز است، البته برای Linux و همانطور که احتمالا می‌دانید، پایه Android بر روی Linux است. طبیعتا کامپایل شدن کدها به so، بر روی بهبود سرعت برنامه تاثیر گذار است.

Linker نیز مشابه با ProGuard کمک می‌کند، ولی اینبار حجم DLL‌های DotNet ای مانند Json.NET را کم می‌کند. بالاخره شما از صد در صد کلاس‌های یک DLL استفاده نمی‌کنید و موارد اضافی نیز باید حذف شوند. البته این وسط، امکان حذف اشتباه کلاس‌هایی که به صورت داینامیک فراخوانی شده باشند وجود دارد که LinkerConfig موجود در پروژه XamApp حتی الامکان جلوی این مشکل را می‌گیرد.

Release mode  مثل هر پروژه CSharp ای دیگری، بهتر است پروژه در حالت Release mode پابلیش شود. در پروژه XamApp در حالت Release mode، موارد بالا یعنی Linker-NDK-ProGuard نیز درخواست می‌شوند.

جزئیات این موارد در مستندات Xamarin وجود دارد و در پایان این دوره یک Project Builder سورس باز نیز به شما ارائه می‌شود که ساختار اولیه پروژه‌ها را بر اساس نیازهای شما و با بهترین تنظیمات ممکن می‌سازد.

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

کدهای‌های DotNet ای به سه شکل می‌توانند اجرا شوند:
JIT - AOT - Interpreter
یک برنامه DotNet ای برای اجرا می‌تواند از ترکیب اینها استفاده کند. حالت Interpreter که خیلی جدید معرفی شده و الآن موضوع بحث نیست؛ می‌ماند JIT و AOT
کد CSharp در هنگام کامپایل به IL تبدیل و سپس در زمان اجرا توسط Just in time compiler به زبان ماشین تبدیل می‌شود. اگر قبلا پروژه‌ی ASP.NET یا ASP.NET Core نوشته باشید، چنین رفتاری را در پشت صحنه خواهد داشت. خود JIT که در هر بار اجرای برنامه انجام می‌شود، عملا زمان بر هست. ولی کد زبان ماشین حاصل از آن خیلی Optimize شده برای دقیقا همان ماشین هست؛ با در نظر گرفتن خیلی فاکتورها. در پروژه‌های سمت سرور مثل ASP.NET که پروژه وقتی یک بار اجرا می‌شود، مثلا روی IIS، ممکن است صدها هزار دستور را اجرا کند، در طول چندین روز یا ماه، این عمل JIT خیلی مفید هست. البته همان سربار اولیه‌ی JIT هم توسط چیزی به عنوان Tiered JIT می‌تواند کمتر شود.
اما در پروژه‌ی موبایل که برنامه ممکن است بعد از باز شدن، مثلا ده دقیقه باز باشد و بعد بسته شود، انجام شدن JIT با هر بار باز شدن برنامه خیلی مفید به فایده نیست. بنا به برخی مسائل که واقعا سطح این آموزش را خیلی پیچیده می‌کند، نتیجه کار JIT قابلیت Cache شدن آن چنانی ندارد و عملا باید هر بار اجرا شود.
در پروژه‌های موبایل، گزینه دیگری بر روی میز هست به نام Ahead of time یا AOT که کار تبدیل IL به زبان ماشین را در زمان کامپایل و پابلیش پروژه انجام دهد. طبیعتا این باعث می‌شود سرعت برنامه موبایل در عمل خیلی بالاتر رود، چون سربار JIT در هر بار اجرای برنامه حذف می‌شود. همچنین روال AOT می‌تواند از LLVM یا Low level virtual machine استفاده کند که منجر به تبدیل شدن کد زبان ماشینی می‌شود که بر روی LLVM کار می‌کند. LLVM خودش یک Runtime با سرعت خیلی بالاست که بر روی تمامی سیستم عامل‌ها کار می‌کند.
بر روی Android - iOS - Windows می‌شود از AOT استفاده کرد. در iOS و ویندوز، استفاده‌ی از AOT منجر به افزایش سایز برنامه نمی‌شود، چون قبلا برنامه یک سری کد IL بوده که زمان اجرا توسط JIT به کد ماشین تبدیل می‌شده و الان بجای آن IL، یک سری کد زبان ماشین مبتنی بر LLVM هست. اما بر روی Android، پیاده سازی AOT ناقص هست و البته که با فعال کردن‌اش، سرعت برنامه بسیار بیشتر می‌شود، ولی کماکان نیاز به JIT و IL هم برای برخی از سناریوها هست. این مورد یعنی اینکه فعال سازی AOT+LLVM بر روی اندروید تا مادامی که AOT در Android به صورت آزمایشی هست، باعث افزایش حجم Apk ما از 7 به 13 مگ می‌شود. البته این مورد در نسخه‌های بعدی رفع خواهد شد و رفتار Android مشابه با iOS-Windows خواهد بود؛ یعنی حجم نسبتا کم و سرعت خیلی بالا.
برای فعال سازی AOT+LLVM در csproj پروژه اندرویدی، دو مقدار AotAssemblies و EnableLLVM را از false به true تغییر دهید:
 <AotAssemblies>true</AotAssemblies> 
 <EnableLLVM>true</EnableLLVM>
با این تنظیمات، بیلد شما طولانی‌تر و در عوض سرعت اجرای برنامه بیشتر خواهد شد.

نحوه انجام دادن پابلیش 
برای انجام دادن پابلیش، بر روی پروژه XamApp.Android در هنگامیکه بر روی Release mode هستید، راست کلیک کنید و Archive را بزنید. سپس فایل Archive شده را انتخاب و Distribute را بزنید که به شما Apk مناسب برای انتشار توسط خودتان یا Google Play می‌دهد.
نکات مهم:
1- فایل Apk حاصل از Archive را بدون Distribute کردن، در اختیار کسی قرار ندهید. فقط پیام Corrupt و خراب بودن فایل، حاصل کارتان خواهد بود!
2- اولین باری که Distribute می‌کنید، Wizard مربوطه کمک می‌کند تا یک فایل Certificate را برای Apk اتان بسازید. آن فایل را گم نکنید! در پابلیش‌های بعدی دیگر نباید Certificate جدیدی بسازید؛ بلکه فایل قبلی را باید به آن معرفی کنید و فقط رمز آن Certificate را دوباره بزنید.
3- به برنامه آیکون بدهید. برای آن Splash Screen خوبی بگذارید. در هر بار پابلیش، ورژن برنامه را افزایش دهید. اینها همگی توضیحات اش بر روی بستر وب موجود است. سؤالی بود، همینجا هم می‌توانید بپرسید.
فایل‌های Apk این مثال را می‌توانید از اینجا دانلود کنید.

در قسمت بعدی آموزش، دیباگ و پابلیش گرفتن پروژه بر روی iOS را خواهیم داشت که البته مقداری از مطالب اش با مطالب این آموزش مشترک هست. بعد دست به کد شده و آموزش CSharp و Xaml را خواهیم داشت تا پروژه‌ای با کیفیت، کارآمد و عالی از هر جهت بنویسید.
همچنین تعدادی از نکات مربوط به Performance که مربوط به ظاهر برنامه و نحوه چیدمان صفحات و کنترل‌ها هستند و بر روی Performance هر سه پلتفرم تاثیر گذار هستند (و نه فقط Android‌) نیز در ادامه بحث خواهند شد.
مطالب
روش Controller as در AngularJs
در پست‌های قبلی بیان شد که برای پیاده سازی عملیات مقید سازی عناصر View به مدل در کنترلر باید scope$ را به تابع سازنده کنترلر تزریق کرد. برای مثال:
var app = angular.module('myApp', []);

app.controller('myController', function ($scope) {
    $scope.name = 'Masoud';
    $scope.family = 'Pakdel';
})
View متناظر نیز به صورت می‌باشد:
<div ng-app="myApp">
    <div ng-controller="myController">       
        <div>
            {{name}} {{family}}
        </div>
    </div>
</div>
در Angular 1.2 روشی به نام controller as معرفی شده است که با توجه به نوع پیاده سازی آن نیازی به تزریق scope$ در توابع سازنده نیست. فقط در کنترلر به جای وابستگی مستقیم به scope$ ا زکلمه کلیدی this و در هنگام عملیات مقید سازی باید از نام مستعار تعیین شده برای کنترلر استفاده نمایید. برای مثال
var app = angular.module('myApp', []);

app.controller('myController', function () {
    this.name = 'Masoud';
    this.family = 'Pakdel';
})
و استفاده آن در View
<div ng-app="myApp">
    <div ng-controller="myController as myCtrl">       
        <div>
            {{myCtrl.name}} {{myCtrl.family}}
        </div>
    </div>
</div>
در هنگام عملیات routing نیز می‌توان این عناوین مستعار را برای کنترلر با استفاده از controllerAs مشخص نمود. به صورت زیر:
app.config(function($routeProvider){
    $routeProvider.when('/first', { 
            templateUrl: 'first.html', 
            controller: 'FirstCtrl', 
            controllerAs:'fc' })
        .when('/second', { 
            templateUrl: 'second.html' , 
            controller: 'ُSecondCtrl', 
            controllerAs:'sc' })
        .otherwise({ 
            redirectTo: '/first' 
       });
});
مطالب
SEO در AngularJS بدون نیاز به Server Side Rendering
همه ما میدانیم برای اینکه محتوای ما به وسیله Google و سایر موتورهای جستجو index شود باید این محتوا در سمت سرور ایجاد و به کلاینت ارسال شود. مدتی بود با مقالاتی مواجه میشدم که نیازی به این کار نیست و گوگل این قابلیت را دارد تا اطلاعاتی را که سمت کلاینت پردازش و Render می‌شوند نیز index کند. تا این که خودم تصمیم گرفتم این مورد را تست کنم.
چند روز پیش شروع به بررسی SEO در AngularJS 1.x کردم. صورت مسئله‌ی من به این صورت بود که نام تعدادی شهر را با AngularJS در صفحه Render کنم، طوریکه در DOM اولیه که از سرور هدایت می‌شود، نام شهرها موجود نباشند. کد زیر را مشاهده کنید.
<html dir="rtl">
<head>
    <title>وب سایت</title>
    <script src="angular.min.js">        
    </script>    
</head>
<body ng-app="app">
    <ul ng-controller="ctrl">
        <li ng-repeat="item in list">{{item}}</li>
    </ul>
</body>
    <script>
        var app=angular.module('app',[]);
        app.controller('ctrl',function($scope,$timeout){
            $scope.list=[
                'اردبیل',
                'تهران',
                'شیراز',
                'قزوین',
            ]           
        });
    </script>
</html>
این فایل را به صورت آزمایشی در Host خودم آپلود کردم؛ با مسیر.
سپس در وب مستر گوگل، مسیر را تعریف کردم و به crawl گوگل اعلام کردم که این مسیر را index کند. بعد از مدتی متوجه شدم این صفحه با تمام نام‌های شهر‌ها index شده‌اند!
مسئله را سخت‌تر کردم و این بار به صورت مسئله اولیه این مورد را هم اضافه کردم که بعد از اینکه صفحه بارگذاری شد، بعد از 5 ثانیه، نام شهر مشهد هم به لیست DOM‌ها اضافه شود و به کد بالا این کد را هم اضافه کردم (این کار را برای شبیه سازی درخواست AJAX انجام دادم):
$timeout(function(){
     $scope.list.push('مشهد')
},5000);
بعد از مدت کوتاهی متوجه شدم نام شهر مشهد هم در گوگل index شده است.این لینک را مشاهده کنید.
البته نیاز به بررسی دقیقتر این مسئله هست و باید در پروژه‌های واقعی این مورد را بررسی کرد تا safe بودن این قابلیت گوگل مورد تایید قرار بگیرد. در حال حاضر برای SEO در ReactJS و AngualrJS و VueJS از Render سمت Server استفاده میکنم. اگر این قابلیت به طور 100% جوابگوی SEO باشد، دیگر نیازی نیست Developer‌ها سمت سرور و کلاینت، کارهای تکراری برای SEO انجام دهند.
مطالب
HTML5 Offline Web Applications
وب به سمتی پیش رفته که کاربران زیادی از تلفن همراه ، تبلت‌ها و دیگر عامل ها(Agent) جهت مرور صفحات وب استفاده می‌کنند. در نتیجه تعداد کاربرانی که مدام در حال حرکت به مرور صفحات وب و استفاده از سرویس‌های برخط می‌پردازند رو به افزایش است. برنامه‌های خارج از شبکه‌ی HTML 5 یا به عبارتی HTML5 Offline Web Applications توسعه دهنگان را قادر می‌سازد تا نرم افزار‌های تحت وبی ارائه دهند که در حالت قطع بودن اینترنت و یا شبکه همچنان به سرویس دادن به کاربران ادامه دهد. دیگر اینگونه نیست که وب تنها در حالت برخط بودن معنی پیدا کند. یک نرم افزار مدیریت هزینه‌ی تحت وب را بررسی کنید که روی تلفن همراه شما اجرا شده ، در محلی که دسترسی به اینترنت نیست قصد استفاده از آن را دارید. چه قدر خوب می‌شود که این نرم افزار به گونه ای پیاده سازی شده باشد که بتواند در حالت برون خطی (Offline) به سرویس دهی ادامه دهد به طور مثال قادر به ذخیره‌ی داده‌های شما به صورت برون خط و همزمان سازی آنها پس از اتصال به اینترنت باشد.
برنامه‌های تحت وب برون خط با کمک قابلیتی به نام نهانگاه برنامه (Application Cache) کار می‌کنند. این قابلیت می‌تواند تمامی بخش‌های سایت را به شکل برون خط و خارج از شبکه، ذخیره کند. با به کار گیری این ویژگی می‌توان تمامی فایل‌های ایستا (JavaScript , HTML , CSS , Image) بر روی ابزار کاربر ذخیره نمود.

 نهانگاه برنامه چه تفاوتی با نهانگاه مرورگر (Browser cache) دارد ؟

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

برای استفاده از این ویژگی اولین باید فایلی به نام cache.manifest ایجاد کرد. این فایل باید با نوع محتوای (Mime type) مناسب برای کاربر ارسال شود. این فایل یک فایل متنی می‌باشد که لیست فایل‌های مورد نظر ما با قواعد خاصی در آن قرار می‌گیرد.
همیشه اولین خط این فایل عبارت CACHE MANIFEST قرار دارد ، پس از این خط عبارت CACHE: وارد می‌شود و فایل‌های مد نظر ما لیست می‌شود.
CACHE MANIFEST
 
#Cache Section
CACHE:
/Content/Images/icons-18-white.png
/Content/Images/icons-36-white.png
/Content/Images/ajax-loader.png
/Content/css
/Scripts/js
ذخیره‌ی برخی فایل‌ها روی سیستم کاربر ضرورتی ندارد ، برای مثال اسکریپتی که از یک وب سرویس برخط اطلاعات آب و هوا را دریافت می‌کند در حالت Offline کاربردی ندارد. برای مشخص کردن این فایل‌ها یک لیست سفید آماده می‌کنیم.
NETWORK
webService.Js
در بخش معرفی فایل‌های آفلاین بازید همه‌ی فایل‌های مد نظر ما معرفی شوند اما در لیست سفید با گذاشتن ستاره به مرورگر اعلام می‌کنیم که هر فایل و مسیری که در بخش قبلی (CACHE) نیامده را همواره از سرور درخواست کن. درج ستاره در بخش NETWORK ضروری است زیرا همه‌ی آدرس‌های سایت باید در یکی از بخش‌های cache.manifest قرار بگیرد ، اگر آدرسی در cache.manifest قرار نگیرد هیچگاه بارگذاری نخواهد شد.
در فایل cache.manifest می‌توان یادداشت هم اضافه کرد ، تمامی کاراکتر هایی که بعد از # قرار گیرند پردازش نمی‌شوند. یادداشت‌ها مفید هستند ، می‌تونید اطلاعات نسخه‌ی فعلی فایل cache.manifest را در آنها قرار دهید.
مثال :
CACHE MANIFEST
# 2010-06-18:v2

# Explicitly cached 'master entries'.
CACHE:
/favicon.ico
index.html
css
images/
stylesheet
.logo.png
scripts/main.js

# Resources that require the user to be online.
NETWORK:
login.php
/myapi
http://api.twitter.com
گفته شد فایل Cache.manifest باید با نوع محتوای مناسب ارسال شود ، برای تعیین نوع محتوا در وب سرور apache باید خط زیر به فایل .htaaccess اضافه شود :
AddType text/cache-manifest .appcache
در ASP.NET Web Forms می‌توان از یک Generic Handler برای ارسال فایل با نوع محتوای مناسب استفاده کرد : 
using System.Web;
 
namespace JavaScriptReference {
 
    public class Manifest : IHttpHandler {
 
        public void ProcessRequest(HttpContext context) {
            context.Response.ContentType = "text/cache-manifest";
            context.Response.WriteFile(context.Server.MapPath("Manifest.txt"));
        }
 
        public bool IsReusable {
            get {
                return false;
            }
        }
    }
}
یا می‌توان در یک فایل ASPX به صورتی دستی نوع محتوا را مشخص کرد : 
CACHE MANIFEST

# Version Jesus 3!

CACHE:

index.html
js/Custom.js
js/Utility.js
styles/index.css
styles/kendo.common.min.css
styles/BlueOpal/loading.gif
styles/BlueOpal/slider-h.gif
styles/BlueOpal/slider-v.gif

NETWORK:
*

<%@ Page Language="VB" ContentType="text/cache-manifest"  ResponseEncoding="utf-8" AutoEventWireup="true" CodeFile="manifest.aspx.vb" Inherits="Configuration_manifest" %>
در ASP.NET MVC علاوه بر اینکه می‌توان دستی نوع محتوای Response را مشخص کرد، می‌توان یک ActionResult منحصر به فرد ایجاد کرد. یک نمونه پیاده سازی شده را اینجا مشاهده کنید.
پس از انجام همه‌ی این پیش نیاز‌ها باید فایل cache manifest را به خصیصه‌ی manifest برچسب html ارجاع دهیم. 
<!DOCTYPE html>
<html manifest="/manifest.aspx">
اکنون قسمت‌های مد نظر سایت ما در حالت عدم دسترسی به شبکه نیز قابل استفاده می‌باشد. حتی این ویژگی در حالت برخط صفحات ما با سرعت بالاتری بارگذاری شوند.
خصیصه‌ی manifest تمامی فایل‌های HTML باید مقدار گیرد در غیر این صورت ممکن است صفحات درون نهانگاه قرار نگیرند.  
برای بررسی مرورگرهایی که این ویژگی را پشتیبانی می‌کنند این لینک  را مشاهده کنید.
مطالب
یافتن اکشن متد‌های به اشتباه کش شده در ASP.NET MVC
مرسوم است برای کش کردن خروجی یک اکشن متد در ASP.NET MVC از ویژگی OutputCache استفاده شود. نکته‌ی مهمی که در مورد نحوه پیاده سازی آن وجود دارد، استفاده از OutputCacheModule استاندارد ASP.NET است. در این حالت پس از فراخوانی ابتدایی اکشن متد و کش شدن محتوای حاصل از آن، در دفعه‌ی بعد فراخوانی این آدرس خاص، اصلا چرخه کاری یک کنترلر روی نداده و تمام مسایل توسط OutputCacheModule به صورت مستقل و پیش از رسیدن آن به کنترلر، مدیریت می‌شوند.
خوب، تا اینجا ممکن است مشکلی به نظر نرسد و هدف از کش کردن اطلاعات یک اکشن متد نیز همین مورد است. اما اگر این اکشن متد کش شده، به اشتباه در یک کنترلر مزین شده با ویژگی Authorize قرار گیرد، چه خواهد شد؟ مثلا این کنترلر امن، برای ارائه فایل‌ها یا حتی نمایش قسمتی از صفحه یا کل صفحه، از کش استفاده کرده است. در بار اول دریافت فایل، بدیهی است که تمام مسایل اعتبارسنجی باید مطابق طول عمر یک کنترلر روی دهند. اما در بار دوم فراخوانی آدرس دریافت صفحه یا فایل، اصلا کار به فراخوانی کنترلر نمی‌رسد. به عبارتی کلیه کاربران سایت (اعم از لاگین شده، نشده، دارای دسترسی مشاهده صفحه یا آدرس امن و یا بدون دسترسی)، به این محتوای خاص بدون مشکلی دسترسی خواهند داشت (فقط کافی است که از آدرس نهایی به نحوی مطلع شوند).

سؤال: چگونه می‌توان کلیه اکشن متدهای یک پروژه ASP.NET MVC را که دارای ویژگی OutputCache در یک کنترلر امن هستند، یافت؟

using System;
using System.Linq;
using System.Reflection;
// Add a ref. to \Program Files\Microsoft ASP.NET\ASP.NET MVC 4\Assemblies\System.Web.Mvc.dll
using System.Web.Mvc;
// Add a ref. to System.Web
using System.Web.UI;

namespace FindOutputCaches
{
    class Program
    {
        static void Main(string[] args)
        {
            var path = @"D:\site\bin\Web.dll";
            var asmTarget = Assembly.LoadFrom(path);

            checkSecuredControllers(asmTarget);

            Console.WriteLine("Press a key...");
            Console.Read();
        }

        private static void checkSecuredControllers(Assembly asmTarget)
        {
            // یافتن کلیه کنترلرهایی که فیلتر اوتورایز دارند
            var securedControllers = asmTarget.GetTypes()
                                              .Where(type => typeof(IController).IsAssignableFrom(type) &&
                                                             Attribute.IsDefined(type, typeof(AuthorizeAttribute)) &&
                                                             !type.Name.StartsWith("T4MVC"))
                                              .ToList();

            foreach (var controller in securedControllers)
            {
                // یافتن کلیه اکشن متدهای کنترلر جاری
                var actionMethods = controller.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
                                              .Where(method => typeof(ActionResult).IsAssignableFrom(method.ReturnType))
                                              .ToList();

                foreach (var method in actionMethods)
                {
                    // یافتن متدهایی که دارای آوت پوت کش هستند
                    var attributes = method.GetCustomAttributes(typeof(OutputCacheAttribute), true);
                    if (attributes == null || !attributes.Any())
                        continue;

                    var outputCache = (OutputCacheAttribute)attributes[0]; // AllowMultiple = false
                    if (outputCache.Location == OutputCacheLocation.None)
                        continue; //سبب عدم کش شدن شده است؛ مثلا برای کارهای ای‌جکسی

                    Console.WriteLine("Detected incorrect usage of OutputCache in:\n {0}-->{1}",
                                           controller.FullName, method.Name);
                }
            }
        }
    }
}
کدهای کامل این بررسی را در اینجا ملاحظه می‌کنید.
ابتدا مسیر اسمبلی کامپایل شده پروژه ASP.NET MVC که حاوی کنترلرهای برنامه است، باید مشخص گردد.
سپس در این اسمبلی کلیه نوع‌های تعریف شده، یافت گردیده و آن‌هایی که پیاده سازی کننده IController هستند (یعنی کلاس‌های کنترلر واقعی برنامه) و همچنین دارای ویژگی AuthorizeAttribute نیز می‌باشند، جدا خواهند شد.
در ادامه، در هر کنترلر امن یافت شده، متدهایی را بررسی خواهیم کرد که دارای خروجی از نوع ActionResult باشند (فقط اکشن متدها مدنظر هستند). اگر این اکشن متد یافت شده دارای ویژگی OutputCacheAttribute بود و همچنین Location آن به None تنظیم نشده بود ... یعنی مشکل امنیتی وجود دارد که باید برطرف شود.

البته برای تکمیل این مطلب باید دو حالت زیر هم پیاده سازی و بررسی شوند:
- کلیه Viewهای برنامه بررسی شوند. اگر در View خاصی که متعلق است به یک کنترلر یا حتی اکشن متد امن، ارجاعی به اکشن متدی کش شده در کنترلری دیگر وجود داشت، این مورد هم یک باگ امنیتی است.
- کلیه کنترلرهای عمومی که دارای اکشن متدی امن هستند نیز باید جهت یافتن OutputCache بررسی شوند.
مطالب
روش کار با فایل‌های ایستا در برنامه‌های React
روش ذکر فایل‌های ایستا در کامپوننت‌های جاوا اسکریپتی برنامه‌های React

React، برای مدیریت پروژه‌ی خود، از Webpack استفاده می‌کند و در این حالت، کار با فایل‌های ایستا مانند تصاویر و قلم‌های وب، شبیه به کار با فایل‌های CSS خواهد بود؛ یعنی این نوع فایل‌ها را باید در فایل‌های جاوا اسکریپتی برنامه، import کرد. به این ترتیب Webpack کار یکی سازی این فایل‌ها را با bundle نهایی تولید شده، انجام می‌دهد.

یک مثال: فرض کنید فایل button.css به صورت زیر تعریف شده‌است:
.Button {
    padding: 20px;
}

برای استفاده‌ی از این فایل css در یک کامپوننت، ابتدا آن‌را import کرده و سپس از classNameهای تعریف شده‌ی در آن استفاده می‌کنیم:
import React, { Component } from 'react';
import './Button.css'; 

class Button extends Component {
  render() {
    return <div className="Button" />;
  }
}
در حالت توسعه، با هر تغییری در این فایل css، بارگذاری مجدد برنامه به صورت خودکار صورت گرفته و نتیجه‌ی نهایی رندر خواهد شد. در حالت نهایی ارائه‌ی برنامه، تمام فایل‌های css، با هم یکی شده و نگارش minified آن‌ها، به bundle نهایی برنامه اضافه می‌شوند. توصیه شده‌است یک فایل css را در چندین کامپوننت برنامه import نکنید. بهتر است یک کامپوننت دکمه را ایجاد کنید که فایل css مشخصی را import می‌کند و سپس از آن کامپوننت در قسمت‌های مختلف برنامه استفاده کنید.

البته برخلاف حالت کار با CSS imports، با import یک فایل ایستا، یک رشته در اختیار ما قرار می‌گیرد که از آن می‌توان در کدهای خود استفاده کرد. برای کاهش تعداد رفت و برگشت‌های به سرور، اگر فایل‌های تصویری با فرمت‌های bmp, gif, jpg, jpeg و  png، کمتر از 10,000 بایت باشند، از data URI آن‌ها بجای مسیر نهایی استفاده خواهد شد. این مورد شامل فایل‌های svg نمی‌شود.

یک مثال:
import React from 'react';
import logo from './logo.svg'; 

console.log(logo);

function Header() {
  return <img src={logo} alt="Logo" />;
}

export default Header;
در اینجا ابتدا یک فایل تصویری، import شده‌است. سپس می‌توان از رشته‌ی متناظر با آن، به عنوان src المان تصویر استفاده کرد.

این مورد برای تصاویر ذکر شده‌ی در فایل‌های CSS نیز صادق است:
.Logo {
    background-image: url(./logo.png);
}
Webpack در فایل‌های CSS به دنبال مسیرهای فایل‌های ثابت شروع شده‌ی با /. می‌گردد و مسیرهای این نوع فایل‌ها را با مسیر نهایی ارائه‌ی برنامه، به صورت خودکار جایگزین و اصلاح می‌کند. همچنین نیازی هم به نگرانی در مورد کش شدن این فایل‌های ثابت وجود ندارد؛ چون webpack از نام‌های خاصی به همراه hash این نوع فایل‌ها، برای جایگزینی نهایی استفاده خواهد کرد و اگر محتوای فایل‌های ثابت تغییر کنند، این هش نیز تغییر کره و مرورگر نگارش جدید آن‌ها را دریافت می‌کند. مزیت دیگر کار با webpack، ارائه‌ی خطاهای زمان کامپایل برنامه است. برای مثال اگر فایل‌های ثابت مورد استفاده‌ی در برنامه به درستی مسیر دهی نشده باشند، یک خطای زمان کامپایل صادر می‌شود.

یک مثال: فرض کنید در برنامه‌ی ASP.NET Core خود که با React یکی شده‌است، فایل project_folder/ClientApp/src/images/progress_bar.gif را قرار داده‌اید. روش import آن با توجه به مسیرهای نسبی برنامه به صورت زیر است:
import progressBar from '../images/progress_bar.gif';
و روش فراخوانی آن در کدهای یک کامپوننت به نحو زیر خواهد بود:
<img alt="loading..." src={progressBar} />


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

اگر از تایپ‌اسکریپت استفاده می‌کنید، چنین importهایی سبب بروز خطای «'Cannot find module './logo.png» می‌شوند. برای رفع این مشکل، فایلی را به نام assets.d.ts به پروژه‌ی خود اضافه کرده و آن‌را به صورت زیر تکمیل کنید:
declare module "*.gif";
declare module "*.jpg";
declare module "*.jpeg";
declare module "*.png";
declare module "*.svg";
به این ترتیب پسوند‌های فایل‌های مختلف ایستا، به عنوان فرمت‌های مجاز ماژول‌ها، قابل استفاده می‌شوند.


نحوه‌ی پردازش پوشه‌ی ویژه‌ی public در برنامه‌های React

اگر فایلی در پوشه‌ی ویژه‌ی public برنامه‌های react قرار گیرد، توسط webpack پردازش نخواهد شد. در این حالت این نوع فایل‌ها بدون هیچ نوع تغییری به پوشه‌ی build نهایی کپی می‌شوند. بنابراین برای کار با فایل‌های ایستای قرار گرفته‌ی در پوشه‌ی public باید از متغیر خاصی به نام PUBLIC_URL استفاده کرد. برای مثال درون فایل index.html، چنین تعریفی را می‌توان مشاهده کرد:
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
اگر نیاز به استفاده‌ی از فایلی درون پوشه‌ی src و یا node_modules دارید، باید آن‌ها را به این پوشه کپی کنید تا جزئی از پروسه‌ی build شوند. متغیر PUBLIC_URL در زمان اجرای دستور npm run build، با مسیر صحیحی جایگزین خواهد شد.
برای دسترسی به این مسیر در کامپوننت‌های برنامه نیز می‌توان از متغیر محیطی process.env.PUBLIC_URL استفاده کرد:
render() {
    return <img src={process.env.PUBLIC_URL + '/img/logo.png'} />;
}
البته روش توصیه شده، همان ذکر importهای فایل‌های ایستا است و بهتر است از این متغیر محیطی زیاد استفاده نکنید؛ چون پردازش ثانویه‌ای بر روی آن‌ها صورت نمی‌گیرد و یا minified نمی‌شوند. نبود آن‌ها سبب بروز خطای زمان کامپایل نخواهد شد و همچنین هیچ hash ای به نام نهایی آن‌ها به صورت خودکار اضافه نمی‌گردد که ممکن است سبب بروز مشکلات کش شدن طولانی مدت این فایل‌های ایستا شود.

بنابراین اکنون این سؤال مطرح می‌شود که چه زمانی بهتر است از پوشه‌ی public استفاده شود؟
- اگر می‌خواهید نام فایل نهایی ایستای مدنظر مانند manifest.json، بدون تغییر باقی بماند.
- هزاران فایل ایستا را دارید و می‌خواهید این مسیرها را به صورت پویا در برنامه فراخوانی کنید (و قرار نیست جزئی از bundle نهایی شوند).
- می‌خواهید فایل‌های js خاصی را خارج از سیستم bundle اصلی قرار دهید؛ چون به هر دلیلی این نوع فایل‌ها با سیستم webpack سازگاری ندارند و نباید توسط آن پردازش شوند. در این حالت باید این نوع فایل‌ها را با تگ script به فایل index.html به صورت دستی معرفی کنید.
نظرات مطالب
VS Code برای توسعه دهندگان ASP.NET Core - قسمت سوم - گردش کاری‌های متداول
گردش کاری ایجاد یک solution جدید به کمک NET CLI.

فرض کنید می‌خواهیم ساختار فوق را که شبیه به پروژه‌های ایجاد شده‌ی توسط Visual Studio است، توسط NET CLI. ایجاد کنیم. روش اینکار به صورت ذیل است:

1) ایجاد پوشه‌ی اصلی و زیر پوشه‌های src و test و سپس ایجاد یک فایل sln خالی

mkdir CSharpWithVSCode
cd CSharpWithVSCode
mkdir src
mkdir test
dotnet new sln
2) ایجاد پوشه‌ی برنامه‌ی کنسول درون پوشه‌ی src و سپس ایجاد ساختار پروژه برنامه‌ی کنسول
cd src
mkdir CSharpWithVSCode.ConsoleApp
cd CSharpWithVSCode.ConsoleApp
dotnet new console
3) ایجاد پوشه‌ی یک پروژه‌ی کتابخانه‌ای درون پوشه‌ی src و سپس ایجاد ساختار آن
cd..
cd..
cd src
mkdir CSharpWithVSCode.ClassLib
cd CSharpWithVSCode.ClassLib
dotnet new classlib
4) افزودن دو پروژه‌ی جدید ایجاد شده به فایل sln
cd..
cd..
dotnet sln add .\src\CSharpWithVSCode.ConsoleApp\CSharpWithVSCode.ConsoleApp.csproj
dotnet sln add .\src\CSharpWithVSCode.ClassLib\CSharpWithVSCode.ClassLib.csproj
5) افزودن ارجاعی از classlib به پروژه‌ی کنسول
cd .\src\CSharpWithVSCode.ConsoleApp\
dotnet add reference ..\CSharpWithVSCode.ClassLib\CSharpWithVSCode.ClassLib.csproj