using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.IO; using System.Linq; using System.Text; using System.Windows.Forms; namespace Image2Base64Converter { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void btnOpen_Click(object sender, EventArgs e) { OpenFileDialog ofd = new OpenFileDialog(); ofd.Multiselect = false; ofd.Filter = "Pictures |*.bmp;*.jpg;*.jpeg;*.png"; if (ofd.ShowDialog(this) == DialogResult.OK) { txtImagePath.Text = ofd.FileName; } } private void btnConvert_Click(object sender, EventArgs e) { if (!File.Exists(txtImagePath.Text)) { MessageBox.Show("File not found!"); return; } btnCopy.Enabled = false; btnConvert.Enabled = false; txtOutput.Enabled = false; BackgroundWorker bworker = new BackgroundWorker(); bworker.DoWork += (o, a) => { string header = "data:image/{0};base64,"; header = string.Format(header, txtImagePath.Text.GetExtension()); StringBuilder data = new StringBuilder(); byte[] buffer; BinaryReader head = new BinaryReader( new FileStream(txtImagePath.Text, FileMode.Open)); buffer = head.ReadBytes(6561); while (buffer.Length > 0) { data.Append(Convert.ToBase64String(buffer)); buffer = head.ReadBytes(6561); } head.Close(); head.Dispose(); this.Invoke(new Action(() => { txtOutput.ResetText(); txtOutput.AppendText(header); txtOutput.AppendText(data.ToString()); btnCopy.Enabled = true; btnConvert.Enabled = true; txtOutput.Enabled = true; })); }; bworker.RunWorkerAsync(); } private void btnCopy_Click(object sender, EventArgs e) { Clipboard.SetText(txtOutput.Text); } } public static class ExtensionMethods { public static string GetExtension(this string str) { string ext = string.Empty; for (int i = str.Length - 1; i >= 0; i--) { if (str[i] == '.') { break; } ext = str[i] + ext; } return ext; } } }
- ادغام چند فایل PDF به صورت فایل واحد (Merging)
- اضافه کردن یک فایل PDF به یک فایل موجود (Appending)
- جدا کردن صفحات دلخواه از یک فایل و ذخیره آنها در یک فایل جدا (Splitting)
- در صورت نصب امکانات پایتون برای ویژوال استودیو مثل سایر پروژههای دات نت قابل اجرا و دیباگ است.
- از طریق خط فرمان و دستور ipy که در ادامه مثالی میزنم.
- من خودم از طریق یک برنامه کنسول ساده پروژه را خارج از محیط توسعه اجرا میکنم که در ادامه مثالی میزنم.
var process = new Process(); var startInfo = new ProcessStartInfo { WindowStyle = ProcessWindowStyle.Hidden, FileName = "CMD.exe", Arguments = "/C ipy C:\\PdfTools\\PdfTools.py" }; process.StartInfo = startInfo; process.Start();
چه نوع اپلیکیشنهای را میتوان با Node.js توسعه داد؟
- سرور WebSocket جهت توسعهی اپلیکیشنهای بلادرنگ
- فایل آپلودر سریع در سمت کلاینت
- Ad Server
- و ...
var contents = fs.readFileSync('filePath'); console.log(content); console.log('Doing something else');
fs.readFile('filePath', function (err, contents) { console.log(contents); }); console.log('Doing something else');
برای شروع به کار با Node.js میتوانید با مراجعه به وبسایت رسمی آن، آنرا دانلود و بر روی سیستم خود نصب کنید. بعد از نصب Node میتوانیم از طریق command line وارد shell آن شوید و دستورات جاوا اسکریپتی خود را اجرا نمائید:
احتمالاً به این نوع استفادهی از Node.js که به REPL معروف است، نیازی نداشته باشید. در واقع هدف بررسی نصب بودن رانتایم بر روی سیستم است. با استفاده از فرمان node نیز میتوان یک فایل جاوا اسکریپتی را اجرا کرد. برای اینکار یک فایل با نام test.js را با محتویات زیر درون VS Code ایجاد کنید:
سپس دستور node test.js را وارد کنید:
همانطور که مشاهده میکنید نتیجهی فایل عنوان شده، در خروجی نمایش داده شده است. در حالت کلی تمام کاری که نود انجام میدهد، ارائه یک Execution engine برای جاوا اسکریپت میباشد.
استفاده از Node.js در ویژوال استودیو
برای کار با Node.js درون ویژوال استودیو باید ابتدا افزونهی Node.js Tools را برای ویژوال استودیو نصب کنید. بعد از نصب این افزونه، تمپلیت Node.js در زمان ایجاد یک پروژه برای شما نمایش داده خواهد شد:
برای شروع، تمپلیت Blank Node.js Console Application را انتخاب کرده و بر روی OK کلیک کنید. با اینکار یک پروژه با ساختار زیر برایمان ایجاد خواهد شد:
همانطور که ملاحظه میکنید، یک فایل با نام app.js درون تمپلیت ایجاد شده، موجود است. app.js در واقع نقطهی شروع برنامهمان خواهد بود. همچنین دو فایل دیگر نیز با نامهای README.md، جهت افزودن توضیحات و یک فایل با نام package.json، جهت مدیریت وابستگیهای برنامه به پروژه اضافه شدهاند. اکنون میتوانیم شروع به توسعهی برنامهی خود درون ویژوال استودیو کنیم. همچنین میتوانیم از قابلیتهای debugging ویژوال استودیو نیز بهره ببریم:
اگر مسیر پروژهی ایجاد شدهی فوق را درون windows explorer باز کنید خواهید دید که ساختار آن شبیه به یک پروژهی Node.js میباشد. با این تفاوت که دو آیتم دیگر همانند دیگر پروژههای ویژوال استودیو نیز به آن اضافه شده است که طبیعتاً میتوانید در حین کار با سورس کنترل، از انتشار آنها صرفنظر کنید.
لازم به ذکر است پروژهی ایجاد شدهی فوق را نیز میتوانید همانند حالت عادی، از طریق command line و همانند پروژههای Node.js اجرا کنید:
node app.js
در واقع از ویژوال استدیو میتوانیم به عنوان یک ابزار برای دیباگ پروژههای Node.js استفاده کنیم. لازم به ذکر است، Visual Studio Code نیز امکان دیباگ اپلیکیشنهای Node.js را در اختیارمان قرار میدهد. در نتیجه در مواقعیکه نسخهی کامل ویژوال استودیو در دسترس نیست نیز میتوانیم از VS Code برای دیباگ برنامههایمان استفاده کنیم:
روش بهینه نمایش عکس در Xamarin Forms
Constant Field : فیلد ثابتی که مستقیما در یک Class و یا Struct تعریف میشود.
Constant Local : ثابتی که در بلاکهای برنامه (بدنه یک تابع ، حلقه تکرار و ...) تعریف میشود.
جدول مقایسهای بین Const و ReadOnly
Constant | ReadOnly |
میتواند به Fieldها و همچنین localها اعمال شود. | تنها به Field ها اعمال میشود. |
مقدار دهی اولیه آن الزامی است. | مقدار دهی اولیه میتواند هنگام تعریف و یا در درون سازنده انجام شود (در هیچ متد دیگری امکان پذیر نیست). |
تخصیص حافظه انجام نمیشود و مقدار آن در کدهای IL گنجانده میشود (توضیح در ادامه مطلب). | تخصیص حافظه بصورت داینامیک انجام میشود و میتوانیم در زمان اجرا مقدار آن را بدست آوریم. |
ثابتها در #C بصورت پیش فرض از نوع static هستند. بدین معنا که از طریق نام کلاس قابل دسترسی هستند. | تنها از طریق وهله سازی از یک کلاس قابل دسترسی هستند. |
نوعهای درون ساز (built in) و Null Reference ها را میتوان بصورت const تعریف کرد. Boolean,Char, Byte, SByte, Int16, UInt16, Int32, UInt32, Int64, UInt64, Single, Double, Decimal , string. | مشابه Constant ها |
مقدار آن در طول عمر یک برنامه ثابت است. | مقدار آن میتواند در هنگام فراخوانی سازنده برای وهلههای مختلف متفاوت باشد. |
فیلدهای const را نمیتوان بصورت پارامترهای out و ref استفاده کرد. | فیلدهای ReadOnly را میتوان بصورت پارامترهای ref و out در درون سازنده استفاده کرد. |
نحوه تعریف یک constant :
همانطور که در تصویر مشاهده میکنید در کنار نماد انتخابی برای constها یک قفل کوچک (نشان از غیرقابل تغییر بودن) قرار گرفته است .
مثالی از تعریف و رفتار Constantها در #C :
const int field_constant = 10; //constant field static void Main(string[] args) { const int x = 10, y = 15; //constant local :correct const int z = x + y; //constant local : correct; const int a = x + GetVariableValue();//Error } public static int GetVariableValue() { const int localx = 10; return 10; }
فیلدهای فقط خواندنی ReadOnly
در #C فقط Fieldها را میتوان بصورت ReadOnly تعریف کرد. این فیلدها یا در زمان تعریف و یا از طریق سازنده مقدار دهی میشوند.
بررسی تفاوت readonly و const در سطح IL
برای مشاهده کدهای سطح میانی (IL Code) از ابزار خط فرمان Developer Command ویژوال استدیو 2017 و همچنین برنامه ILdasm استفاده شده است. همانطور که در جدول مقایسهای بیان شد، برای constant field ها تخصیص حافظهای صورت نمیگیرد و مقادیر مستقیما در کدهای IL گنجانده میشود.
مثال:
class Program { public const int numberOfDays = 7; public readonly double piValue = 3.14; static void Main(string[] args) { } }
ولی مقدار ذخیره شده در piValue در زمان اجرا قابل دسترسی میباشد.
مشکل Versioning فیلدهای const
public const int numberOfDays = 7; public readonly double piValue = 3.14;
کد برنامه اصلی که ارجاعی به اسمبلی جانبی دارد:
static void Main(string[] args) { var readEx = new MyLib.TestClass(); var readConstValue = MyLib.TestClass.numberOfDays; var readReadOnlyValue = readEx.piValue; }
.method private hidebysig static void Main(string[] args) cil managed { .entrypoint // Code size 17 (0x11) .maxstack 1 .locals init ([0] class [MyLib]MyLib.TestClass readEx, [1] int32 readConstValue, [2] float64 readOnlyValue) IL_0000: nop IL_0001: newobj instance void [MyLib]MyLib.TestClass::.ctor() IL_0006: stloc.0 //readEx IL_0007: ldc.i4.7 //ارزش ذخیره شده در کد IL_0008: stloc.1 //readConstValue IL_0009: ldloc.0 //readEg IL_000a: ldfld float64 [MyLib]MyLib.TestClass::piValue IL_000f: stloc.2 //readReadOnlyValue IL_0010: ret } // end of method Program::Main
اگر در کتابخانه جانبی ارزش فیلد const را تغییر دهید و آن را مجدد کامپایل کنید، تا زمانیکه اسمبلی برنامه اصلی را کامپایل نکردهاید، همان ارزش قبلی در برنامه نمایش داده میشود.
برای غلبه بر این مشکل از فیلدهای Static ReadOnly استفاده میکنیم.
مثال:
public class ReadonlyStatic { public static readonly string x = "Hi"; public static readonly string y; public ReadonlyStatic() { //y = "Hello"; This is wrong } static ReadonlyStatic() { y = "Hello"; } }
اولین مشکلی که با استفاده از فیلدهای Static ReadOnly حل میشود، مشکل Versioning فیلدهای Const است. بدین ترتیب دیگر نیازی به کامپایل مجدد برنامه مصرف کننده نیست .
نکته بعدی که در کد فوق نشان داده شدهاست، فیلدهای static readOnly در زمان تعریف و یا تنها از طریق سازندهی static میتوانند مقدار دهی شوند.
مقایسه ReadOnly و Static :
ReadOnly | Static |
هم در زمان تعریف و هم از طریق سازنده میتوان آن را مقدار دهی کرد. | در زمان تعریف و تنها از طریق سازنده static میتوان آن را مقدار دهی کرد. |
مقدار بر اساس مقادیری که در سازندهها تعیین میشود متفاوت است. | مقادیر بعد از مقدار دهی اولیه تغییر نمیکنند. |
چه زمانی از Const و چه زمانی از ReadOnly استفاده کنیم :
- زمانی باید از Const استفاده کرد که مطمئن هستیم ارزش ذخیره شده در آن در طول عمر یک برنامه تغییر نمیکند. بطور مثال ذخیره تعداد روز هفته در یک فیلد از نوع Constant. اگر شک داریم که ممکن است این ارزش تغییر کند، میتوانیم از حالت static readOnly برای غلبه بر مشکل Versioning استفاده کنیم.
- از آنجائیکه مقادیر constant در کدهای IL گنجانده میشوند، برای رسیدن به کارآیی بهتر، مقادیری را که در طول عمر یک برنامه تغییر نمیکنند، به صورت const تعریف میکنیم.
- هر زمان تصمیم داشتیم Constant هایی به ازای هر وهله از کلاس داشته باشیم از ReadOnly استفاده میکنیم.
این برنامه از چهار کامپوننت تشکیل شدهاست:
- کامپوننت App که در برگیرندهی سه کامپوننت زیر است:
- کامپوننت BasketItemsCounter: جمع تعداد آیتمهای انتخابی توسط کاربر را نمایش میدهد؛ به همراه دکمهای برای خالی کردن لیست انتخابی.
- کامپوننت ShopItemsList: لیست محصولات موجود در فروشگاه را نمایش میدهد. با کلیک بر روی هر آیتم آن، آیتم انتخابی به لیست انتخابهای او اضافه خواهد شد.
- کامپوننت BasketItemsList: لیستی را نمایش میدهد که حاصل انتخابهای کاربر در کامپوننت ShopItemsList است (یا همان سبد خرید). در ذیل این لیست، جمع نهایی قیمت قابل پرداخت نیز درج میشود. همچنین اگر کاربر بر روی دکمهی remove هر ردیف کلیک کند، یک واحد از چند واحد انتخابی، حذف خواهد شد.
بنابراین در اینجا سه کامپوننت مجزا را داریم که با هم تبادل اطلاعات میکنند. یکی جمع تعداد محصولات خریداری شده را، دیگری لیست محصولات موجود را و آخری لیست خرید نهایی را نمایش میدهد. همچنین این سه کامپوننت، فرزند یک دیگر هم محسوب نمیشوند و انتقال اطلاعات بین اینها نیاز به بالا بردن state هر کدام و قرار دادن آنها در کامپوننت App را دارد تا بتوان پس از آن از طریق props آنها را بین سه کامپوننت فوق که اکنون فرزند کامپوننت App محسوب میشوند، به اشتراک گذاشت. روش بهتر اینکار، استفاده از یک مخزن حالت سراسری است تا حالتهای این کامپوننتها را نگهداری کرده و دادهها را بین آنها به اشتراک بگذارد که در اینجا برای حل این مساله از کتابخانههای mobx و mobx-react استفاده خواهیم کرد.
برپایی پیشنیازها
برای پیاده سازی برنامهی فوق، یک پروژهی جدید React را ایجاد میکنیم:
> create-react-app state-management-with-mobx-part4 > cd state-management-with-mobx-part4
> npm install --save bootstrap mobx mobx-react mobx-react-devtools mobx-state-tree
- برای استفاده از شیوهنامههای بوت استرپ، بستهی bootstrap نیز در اینجا نصب میشود.
- اصل کار برنامه توسط دو کتابخانهی mobx و کتابخانهی متصل کنندهی آن به برنامههای react که mobx-react نام دارد، انجام خواهد شد.
- چون میخواهیم از افزونهی mobx-devtools نیز استفاده کنیم، نیاز است دو بستهی mobx-react-devtools و همچنین mobx-state-tree را که جزو وابستگیهای آن است، نصب کنیم.
سپس بستههای زیر را که در قسمت devDependencies فایل package.json درج خواهند شد، باید نصب شوند:
> npm install --save-dev babel-eslint customize-cra eslint eslint-config-react-app eslint-loader eslint-plugin-babel eslint-plugin-css-modules eslint-plugin-filenames eslint-plugin-flowtype eslint-plugin-import eslint-plugin-no-async-without-await eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-react-redux eslint-plugin-redux-saga eslint-plugin-simple-import-sort react-app-rewired typescript
- افزودن فایل جدید config-overrides.js به ریشهی پروژه، تا پشتیبانی ازlegacy" decorators spec" فعال شود.
- اصلاح فایل package.json و ویرایش قسمت scripts آن برای استفادهی از react-app-rewired، تا امکان تغییر تنظیمات webpack به صورت پویا در زمان اجرای برنامه، میسر شود.
- همچنین فایل غنی شدهی eslintrc.json. را نیز به ریشهی پروژه اضافه میکنیم.
تهیه سرویس لیست محصولات موجود در فروشگاه
این برنامه از یک لیست درون حافظهای، برای تهیهی لیست محصولات موجود در فروشگاه استفاده میکند. به همین جهت پوشهی service را افزوده و سپس فایل جدید src\services\productsService.js را با محتوای زیر، ایجاد میکنیم:
const products = [ { id: 1, name: "Item 1", price: 850 }, { id: 2, name: "Item 2", price: 900 }, { id: 3, name: "Item 3", price: 1500 }, { id: 4, name: "Item 4", price: 1000 } ]; export default products;
ایجاد کامپوننت نمایش لیست محصولات
پس از مشخص شدن لیست محصولات قابل فروش، کامپوننت جدید src\components\ShopItemsList.jsx را به صورت زیر ایجاد میکنیم:
import React from "react"; import products from "../services/productsService"; const ShopItemsList = ({ onAdd }) => { return ( <table className="table table-hover"> <thead className="thead-light"> <tr> <th>Name</th> <th>Price</th> <th>Action</th> </tr> </thead> <tbody> {products.map(product => ( <tr key={product.id}> <td>{product.name}</td> <td>{product.price}</td> <td> <button className="btn btn-sm btn-info" onClick={() => onAdd(product)} > Add </button> </td> </tr> ))} </tbody> </table> ); }; export default ShopItemsList;
- در اینجا همچنین هر ردیف، به همراه یک دکمهی Add نیز هست که قرار است با کلیک بر روی آن، متد رویدادگردان onAdd فراخوانی شود. این متد نیز از طریق props این کامپوننت دریافت میشود. کتابخانههای مدیریت حالت، تمام خواص و رویدادگردانهای مورد نیاز یک کامپوننت را از طریق props، تامین میکنند.
- فعلا این کامپوننت به هیچ مخزن دادهای متصل نیست و فقط طراحی ابتدایی آن آماده شدهاست.
ایجاد کامپوننت نمایش لیست خرید کاربر (سبد خرید)
اکنون که میتوان توسط کامپوننت لیست محصولات، تعدادی از آنها را خریداری کرد، کامپوننت جدید src\components\BasketItemsList.jsx را برای نمایش لیست نهایی خرید کاربر، به صورت زیر پیاده سازی میکنیم:
import React from "react"; const BasketItemsList = ({ items, totalPrice, onRemove }) => { return ( <> <table className="table table-hover"> <thead className="thead-light"> <tr> <th>Name</th> <th>Price</th> <th>Count</th> <th>Action</th> </tr> </thead> <tbody> {items.map(item => ( <tr key={item.id}> <td>{item.name}</td> <td>{item.price}</td> <td>{item.count}</td> <td> <button className="btn btn-sm btn-danger" onClick={() => onRemove(item.id)} > Remove </button> </td> </tr> ))} <tr> <td align="right"> <strong>Total: </strong> </td> <td> <strong>{totalPrice}</strong> </td> <td></td> <td></td> </tr> </tbody> </table> </> ); }; export default BasketItemsList;
const BasketItemsList = ({ items, totalPrice, onRemove }) => {
- همچنین هر ردیف نمایش داده شده، به همراه یک دکمهی Remove آیتم انتخابی نیز هست که به متد رویدادگردان onRemove متصل شدهاست.
- در ردیف انتهایی این لیست، مقدار totalPrice که یک خاصیت محاسباتی است، درج میشود.
- فعلا این کامپوننت نیز به هیچ مخزن دادهای متصل نیست و فقط طراحی ابتدایی آن آماده شدهاست.
ایجاد کامپوننت نمایش تعداد آیتمهای خریداری شده
کاربر اگر آیتمی را از لیست محصولات انتخاب کند و یا محصول انتخاب شده را از لیست خرید حذف کند، تعداد نهایی باقی مانده را میتوان در کامپوننت src\components\BasketItemsCounter.jsx مشاهده کرد:
import React, { Component } from "react"; class BasketItemsCounter extends Component { render() { const { count, onRemoveAll } = this.props; return ( <div> <h1>Total items: {count}</h1> <button type="button" className="btn btn-sm btn-danger" onClick={() => onRemoveAll()} > Empty Basket </button> </div> ); } } export default BasketItemsCounter;
- فعلا این کامپوننت نیز به هیچ مخزن دادهای متصل نیست و فقط طراحی ابتدایی آن آماده شدهاست.
نمایش ابتدایی سه کامپوننت توسط کامپوننت App
اکنون که این سه کامپوننت تکمیل شدهاند، میتوان المانهای آنها را در فایل src\App.js درج کرد تا در صفحه نمایش داده شوند:
import React, { Component } from "react"; import BasketItemsCounter from "./components/BasketItemsCounter"; import BasketItemsList from "./components/BasketItemsList"; import ShopItemsList from "./components/ShopItemsList"; class App extends Component { render() { return ( <main className="container"> <div className="row"> <BasketItemsCounter /> </div> <hr /> <div className="row"> <h2>Products</h2> <ShopItemsList /> </div> <div className="row"> <h2>Basket</h2> <BasketItemsList /> </div> </main> ); } } export default App;
میتوان همانند Redux کل state برنامه را داخل یک شیء store ذخیره کرد و یا چون در اینجا میتوان طراحی مخزن حالت MobX را به دلخواه انجام داد، میتوان چندین مخزن حالت را تهیه و به هم متصل کرد؛ مانند تصویری که مشاهده میکنید. در اینجا:
- src\stores\counter.js: مخزن دادهی حالت کامپوننت شمارشگر است.
- src\stores\market.js: مخزن دادهی کامپوننتهای لیست محصولات و سبد خرید است.
- src\stores\index.js: کار ترکیب دو مخزن قبل را انجام میدهد.
در ادامه کدهای کامل این مخازن را مشاهده میکنید:
مخزن حالت src\stores\counter.js
import { action, observable } from "mobx"; export default class CounterStore { @observable totalNumbersInBasket = 0; constructor(rootStore) { this.rootStore = rootStore; } @action increase = () => { this.totalNumbersInBasket++; }; @action decrease = () => { this.totalNumbersInBasket--; }; }
- در اینجا خاصیت totalNumbersInBasket به صورت observable تعریف شدهاست و با تغییر آن چه به صورت مستقیم، با مقدار دهی آن و یا توسط دو action تعریف شده، سبب به روز رسانی UI خواهد شد.
- میشد این مخزن را با مخزن src\stores\market.js یکی کرد؛ اما جهت ارائهی مثالی در مورد نحوهی تعریف چند مخزن و روش برقراری ارتباط بین آنها، به صورت مجزایی تعریف شد.
مخزن حالت src\stores\market.js
import { action, computed, observable } from "mobx"; export default class MarketStore { @observable basketItems = []; constructor(rootStore) { this.rootStore = rootStore; } @action add = product => { const selectedItem = this.basketItems.find(item => item.id === product.id); if (selectedItem) { selectedItem.count++; } else { this.basketItems.push({ ...product, count: 1 }); } this.rootStore.counterStore.increase(); }; @action remove = id => { const selectedItem = this.basketItems.find(item => item.id === id); selectedItem.count--; if (selectedItem.count === 0) { this.basketItems.remove(selectedItem); } this.rootStore.counterStore.decrease(); }; @action removeAll = () => { this.basketItems = []; this.rootStore.counterStore.totalNumbersInBasket = 0; }; @computed get totalPrice() { return this.basketItems.reduce((previous, current) => { return previous + current.price * current.count; }, 0); } }
- توسط متد add آن در کامپوننت نمایش لیست محصولات، میتوان آیتمی را به این آرایه اضافه کرد. در اینجا چون شیء product مورد استفاده دارای خاصیت count نیست، روش افزودن آنرا توسط spread operator برای درج خواص شیء product اصلی و سپس تعریف آنرا مشاهده میکنید. این فراخوانی، سبب افزایش یک واحد به عدد شمارشگر نیز میشود.
- متد remove آن در کامپوننت سبد خرید، مورد استفاده قرار میگیرد تا کاربر بتواند اطلاعاتی را از این لیست حذف کند. این فراخوانی، سبب کاهش یک واحد از عدد شمارشگر نیز میشود.
- متد removeAll آن در کامپوننت شمارشگر بالای صفحه استفاده میشود تا سبب خالی شدن آرایهی آیتمهای انتخابی گردد و همچنین عدد آنرا نیز صفر کند.
- خاصیت محاسباتی totalPrice آن در پایین جدول سبد خرید، جمع کل هزینهی قابل پرداخت را مشخص میکند.
مخزن حالت src\stores\index.js
در اینجا روش یکی کردن دو مخزن حالت یاد شده را به صورت خاصیتهای عمومی یک مخزن کد ریشه، مشاهده میکنید:
import CounterStore from "./counter"; import MarketStore from "./market"; class RootStore { counterStore = new CounterStore(this); marketStore = new MarketStore(this); } export default RootStore;
export default class MarketStore { @observable basketItems = []; constructor(rootStore) { this.rootStore = rootStore; } @action removeAll = () => { this.basketItems = []; this.rootStore.counterStore.totalNumbersInBasket = 0; }; }
پس از ایجاد مخازن حالت، اکنون نیاز است آنها را در اختیار سلسه مراتب کامپوننتهای برنامه قرار دهیم. به همین جهت به فایل src\index.js مراجعه کرده و آنرا به صورت زیر تغییر میدهیم:
import "./index.css"; import "bootstrap/dist/css/bootstrap.css"; import makeInspectable from "mobx-devtools-mst"; import { Provider } from "mobx-react"; import React from "react"; import ReactDOM from "react-dom"; import App from "./App"; import * as serviceWorker from "./serviceWorker"; import RootStore from "./stores"; const rootStore = new RootStore(); if (process.env.NODE_ENV === "development") { makeInspectable(rootStore); // https://github.com/mobxjs/mobx-devtools } ReactDOM.render( <Provider {...rootStore}> <App /> </Provider>, document.getElementById("root") ); serviceWorker.unregister();
- سپس یک وهلهی جدید از RootStore را که حاوی خاصیتهای عمومی counterStore و marketStore است، ایجاد میکنیم.
- اگر علاقمند باشید تا حین کار با MobX، جزئیات پشت صحنهی آنرا توسط افزونهی mobx-devtools ردیابی کنید، روش آنرا در اینجا با فراخوانی متد makeInspectable مشاهده میکنید. مقدار process.env.NODE_ENV نیز بر اساس پروسهی جاری node.js اجرا کنندهی برنامهی React تامین میشود. اطلاعات بیشتر
- قسمت آخر این تنظیمات، محصور کردن کامپوننت App که بالاترین کامپوننت در سلسله مراتب کامپوننتهای برنامه است، با شیء Provider میباشد. در این شیء توسط spread operator، سبب درج خواص عمومی rootStore، به عنوان مخازن قابل استفاده شدهایم. تنظیم {rootStore...} معادل عبارت زیر است:
<Provider counterStore={rootStore.counterStore} marketStore={rootStore.marketStore}>
اتصال کامپوننت ShopItemsList به مخزن حالت marketStore
پس از ایجاد rootStore و محصور کردن کامپوننت App توسط شیء Provider در فایل src\index.js، اکنون باید قسمت export default کامپوننتهای برنامه را جهت استفادهی از مخازن حالت، یکی یکی ویرایش کرد:
import { inject, observer } from "mobx-react"; import React from "react"; import products from "../services/productsService"; const ShopItemsList = ({ onAdd }) => { return ( // ... ); }; export default inject(({ marketStore }) => ({ onAdd: marketStore.add }))(observer(ShopItemsList));
اتصال کامپوننت BasketItemsList به مخزن حالت marketStore
در اینجا نیز سطر export default را جهت دریافت خاصیت marketStore، از شیء Provider تامین شدهی در فایل src\index.js، ویرایش میکنیم. به این ترتیب سه props مورد انتظار این کامپوننت، توسط خاصیتهای basketItems (آرایهی اشیاء انتخابی توسط کاربر)، totalPrice (خاصیت محاسباتی جمع کل هزینه) و متد رویدادگردان onRemove (برای حذف یک آیتم) تامین میشوند. در آخر کامپوننت را به صورت observer محصور کرده و بازگشت میدهیم تا تغییرات در مخزن حالت آن، سبب به روز رسانی UI آن شوند:
import { inject, observer } from "mobx-react"; import React from "react"; const BasketItemsList = ({ items, totalPrice, onRemove }) => { return ( // ... ); }; export default inject(({ marketStore }) => ({ items: marketStore.basketItems, totalPrice: marketStore.totalPrice, onRemove: marketStore.remove }))(observer(BasketItemsList));
اتصال کامپوننت BasketItemsCounter به دو مخزن حالت counterStore و marketStore
در اینجا روش استفادهی از decorator syntax کتابخانهی mobx-react را بر روی یک کامپوننت کلاسی مشاهده میکنید. تزئین کنندهی inject، امکان دسترسی به مخازن حالت تزریقی به شیء Provider را میسر کرده و سپس توسط آن میتوان props مورد انتظار کامپوننت را از مخازن متناظر استخراج کرده و در اختیار کامپوننت قرار داد. همچنین این کامپوننت توسط تزئین کنندهی observer نیز علامت گذاری شدهاست. در این حالت نیازی به تغییر سطر export default نیست.
import { inject, observer } from "mobx-react"; import React, { Component } from "react"; @inject(rootStore => ({ count: rootStore.counterStore.totalNumbersInBasket, onRemoveAll: rootStore.marketStore.removeAll })) @observer class BasketItemsCounter extends Component { render() { const { count, onRemoveAll } = this.props; return ( // ... ); } } export default BasketItemsCounter;
کدهای کامل این قسمت را میتوانید از اینجا دریافت کنید: state-management-with-mobx-part4.zip
<button onClick={this.handleReset} className="btn btn-primary btn-sm m-2" > Reset </button>
handleReset = () => { const counters = this.state.counters.map(counter => { counter.value = 0; return counter; }); this.setState({ counters }); // = this.setState({ counters: counters }); };
اکنون پس از ذخیره سازی فایل counters.jsx و بارگذاری مجدد برنامه در مرورگر، هرچقدر بر روی دکمهی Reset کلیک کنیم ... اتفاقی رخ نمیدهد! حتی اگر به افزونهی React developer tools نیز مراجعه کنیم، مشاهده خواهیم کرد که عمل تنظیم value به صفر، در تک تک کامپوننتهای شمارشگر، به درستی صورت گرفتهاست؛ اما تغییرات به DOM اصلی منعکس نشدهاند:
البته اگر به همین تصویر دقت کنید، هنوز مقدار count، در state آن 4 است. علت اینجا است که هر کدام از Counterها دارای local state خاص خودشان هستند و در آنها، مقدار count به صورت زیر مقدار دهی شدهاست که در آن تغییرات بعدی این this.props.value، متصل به count نیست و count، فقط یکبار مقدار دهی میشود:
class Counter extends Component { state = { count: this.props.counter.value };
حذف Local state
اکنون میخواهیم در کامپوننت Counter، قسمت local state آنرا به طور کامل حذف کرده و تنها از this.props جهت دریافت اطلاعاتی که نیاز دارد، استفاده کنیم. به این نوع کامپوننتها، «Controlled component» نیز میگویند. یک کامپوننت کنترل شده دارای local state خاص خودش نیست و تمام دادههای دریافتی را از طریق this.props دریافت میکند و هر زمانیکه قرار است دادهای تغییر کند، رخدادی را به والد خود صادر میکند. بنابراین این کامپوننت به طور کامل توسط والد آن کنترل میشود.
برای پیاده سازی این مفهوم، ابتدا خاصیت state کامپوننت Counter را حذف میکنیم. سپس تمام ارجاعات به this.state را در این کامپوننت یافته و آنها را تغییر میدهیم. اولین ارجاع، در متد handleIncrement به صورت this.state.count تعریف شدهاست:
handleIncrement = () => { this.setState({ count: this.state.count + 1 }); };
<button onClick={() => this.props.onIncrement(this.props.counter)} className="btn btn-secondary btn-sm" > Increment </button>
getBadgeClasses() { let classes = "badge m-2 badge-"; classes += this.props.counter.value === 0 ? "warning" : "primary"; return classes; } formatCount() { const { value } = this.props.counter; // Object Destructuring return value === 0 ? "Zero" : value; }
در ادامه به کامپوننت Counters مراجعه کرده و متد رویدادگردانی را جهت پاسخگویی به رخداد onIncrement رسیدهی از کامپوننتهای فرزند، تعریف میکنیم:
handleIncrement = counter => { console.log("handleIncrement", counter); };
<Counter key={counter.id} counter={counter} onDelete={this.handleDelete} onIncrement={this.handleIncrement} />
پیاده سازی کامل متد handleIncrement اینبار به صورت زیر است:
handleIncrement = counter => { console.log("handleIncrement", counter); const counters = [...this.state.counters]; // cloning an array const index = counters.indexOf(counter); counters[index] = { ...counter }; // cloning an object counters[index].value++; console.log("this.state.counters", this.state.counters[index]); this.setState({ counters }); };
تا اینجا اگر برنامه را ذخیره کرده و منتظر به روز رسانی آن در مرورگر شویم، با کلیک بر روی Reset، تمام کامپوننتها با هر وضعیتی که پیشتر داشته باشند، به حالت اول خود باز میگردند:
همگام سازی چندین کامپوننت با هم زمانیکه رابطهی والد و فرزندی بین آنها وجود ندارد
در ادامه میخواهیم یک منوی راهبری (یا همان NavBar در بوت استرپ) را به بالای صفحه اضافه کنیم و در آن جمع کل تعداد Counterهای رندر شده را نمایش دهیم؛ مانند نمایش تعداد آیتمهای انتخاب شدهی توسط یک کاربر، در یک سبد خرید. برای پیاده سازی آن، درخت کامپوننتهای React را مطابق شکل فوق تغییر میدهیم. یعنی مجددا کامپوننت App را در به عنوان کامپوننت ریشهای انتخاب کرده که سایر کامپوننتها از آن مشتق میشوند و همچنین کامپوننت مجزای NavBar را نیز اضافه خواهیم کرد.
برای این منظور به index.js مراجعه کرده و مجددا کامپوننت App را که غیرفعال کرده بودیم و بجای آن Counters را نمایش میدادیم، اضافه میکنیم:
import App from "./App"; ReactDOM.render(<App />, document.getElementById("root"));
سپس کامپوننت جدید NavBar را توسط فایل جدید src\components\navbar.jsx اضافه میکنیم تا منوی راهبری سایت را نمایش دهد:
import React, { Component } from "react"; class NavBar extends Component { render() { return ( <nav className="navbar navbar-light bg-light"> <a className="navbar-brand" href="#"> Navbar </a> </nav> ); } } export default NavBar;
اکنون به App.js مراجعه کرده و متد render آنرا جهت نمایش درخت کامپوننتهایی که مشاهده کردید، تکمیل میکنیم:
import "./App.css"; import React from "react"; import Counters from "./components/counters"; import NavBar from "./components/navbar"; function App() { return ( <React.Fragment> <NavBar /> <main className="container"> <Counters /> </main> </React.Fragment> ); } export default App;
تا اینجا اگر برنامه را ذخیره کنیم تا در مرورگر بارگذاری مجدد شود، چنین شکلی حاصل شدهاست:
اکنون میخواهیم تعداد کامپوننتهای شمارشگر را در navbar نمایش دهیم. پیشتر state کامپوننت Counters را توسط props، به کامپوننتهای Counter رندر شدهی توسط آن انتقال دادیم. استفادهی از این ویژگی به دلیل وجود رابطهی والد و فرزندی بین این کامپوننتها میسر شد. اما همانطور که در تصویر درخت کامپوننتهای جدید تشکیل شده مشاهده میکنید، رابطهی والد و فرزندی بین دو کامپوننت Counters و NavBar وجود ندارد. بنابراین اکنون این سؤال مطرح میشود که چگونه باید تعداد کل شمارشگرهای کامپوننت Counters را به کامپوننت NavBar، برای نمایش آنها انتقال داد؟ در یک چنین حالتهایی که رابطهی والد و فرزندی بین کامپوننتها وجود ندارد و میخواهیم آنها را همگام سازی کنیم و دادههایی را بین آنها به اشتراک بگذاریم، باید state را به یک سطح بالاتر انتقال داد. یعنی در این مثال باید state کامپوننت Counters را به والد آن که اکنون کامپوننت App است، منتقل کرد. پس از آن چون هر دو کامپوننت NavBar و Counters، از کامپوننت App مشتق میشوند، اکنون میتوان این state را به تمام فرزندان App توسط props منتقل کرد و به اشتراک گذاشت.
انتقال state به یک سطح بالاتر
برای انتقال state به یک سطح بالاتر، به کامپوننت Counters مراجعه کرده و خاصیت state آنرا به همراه تمامی متدهایی که آنرا تغییر میدهند و از آن استفاده میکنند، انتخاب و cut میکنیم. سپس به کامپوننت App مراجعه کرده و آنها را در اینجا paste میکنیم. یعنی خاصیت state و متدهای handleDelete، handleReset و handleIncrement را از کامپوننت Counters به کامپوننت App منتقل میکنیم. این مرحلهی اول است. سپس نیاز است به کامپوننت Counters مراجعه کرده و ارجاعات به state و متدهای یاد شده را توسط props اصلاح میکنیم. برای این منظور ابتدا باید این props را در کامپوننت App مقدار دهی کنیم تا بتوانیم آنها را در کامپوننت Counters بخوانیم؛ یعنی متد render کامپوننت App، تمام این خواص و متدها را باید به صورت ویژگیهایی به تعریف المان Counters اضافه کند تا خاصیت props آن بتواند به آنها دسترسی داشته باشد:
render() { return ( <React.Fragment> <NavBar /> <main className="container"> <Counters counters={this.state.counters} onReset={this.handleReset} onIncrement={this.handleIncrement} onDelete={this.handleDelete} /> </main> </React.Fragment> ); }
پس از این تعاریف میتوانیم به کامپوننت Counters بازگشته و ارجاعات فوق را توسط خاصیت props، در متد render آن اصلاح کنیم:
render() { return ( <div> <button onClick={this.props.onReset} className="btn btn-primary btn-sm m-2" > Reset </button> {this.props.counters.map(counter => ( <Counter key={counter.id} counter={counter} onDelete={this.props.onDelete} onIncrement={this.props.onIncrement} /> ))} </div> ); }
پس از این نقل و انتقالات، اکنون میتوانیم تعداد counters را در NavBar نمایش دهیم. برای این منظور ابتدا در کامپوننت App، به همان روشی که ویژگی counters={this.state.counters} را به تعریف المان Counters اضافه کردیم، شبیه به همین کار را برای کامپوننت NavBar نیز میتوانیم انجام دهیم تا از طریق خاصیت props آن قابل دسترسی شود و یا حتی میتوان به صورت زیر، تنها جمع کل را به آن کامپوننت ارسال کرد:
<NavBar totalCounters={this.state.counters.filter(c => c.value > 0).length} />
سپس در کامپوننت NavBar، عدد totalCounters فوق را که به تعداد کامپوننتهایی که مقدار value آنها بیشتر از صفر است، اشاره میکند، از طریق خاصیت props خوانده و نمایش میدهیم:
class NavBar extends Component { render() { return ( <nav className="navbar navbar-light bg-light"> <a className="navbar-brand" href="#"> Navbar{" "} <span className="badge badge-pill badge-secondary"> {this.props.totalCounters} </span> </a> </nav> ); } }
کامپوننتهای بدون حالت تابعی
اگر به کدهای کامپوننت NavBar دقت کنیم، تنها یک تک متد render در آن ذکر شدهاست و تمام اطلاعات مورد نیاز آن نیز از طریق props تامین میشود و دارای state و یا هیچ رویدادگردانی نیست. یک چنین کامپوننتی را میتوان به یک «Stateless Functional Component» تبدیل کرد؛ کامپوننتهای بدون حالت تابعی. در اینجا بجای اینکه از یک کلاس برای تعریف کامپوننت استفاده شود، میتوان از یک function استفاده کرد (به همین جهت به آن functional میگویند). احتمالا نمونهی آنرا با کامپوننت App پیشفرض قالب create-react-app نیز مشاهده کردهاید که در آن فقط یک ()function App وجود دارد. البته در کدهای فوق چون نیاز به ذکر state، در کامپوننت App وجود داشت، آنرا از حالت تابعی، به حالت کلاس استاندارد کامپوننت، تبدیل کردیم.
اگر بخواهیم کامپوننت بدون حالت NavBar را نیز تابعی کنیم، میتوان به صورت زیر عمل کرد:
import React from "react"; // Stateless Functional Component const NavBar = props => { return ( <nav className="navbar navbar-light bg-light"> <a className="navbar-brand" href="#"> Navbar{" "} <span className="badge badge-pill badge-secondary"> {props.totalCounters} </span> </a> </nav> ); }; export default NavBar;
پیشتر در کامپوننت NavBar از شیء this استفاده شده بود. این روش تنها با کلاسهای استاندارد کامپوننت کار میکند. در اینجا باید props را به عنوان پارامتر متد دریافت (همانند مثال فوق) و سپس از آن استفاده کرد.
البته لازم به ذکر است که انتخاب بین «کامپوننتهای بدون حالت تابعی» و یک کامپوننت معمولی تعریف شدهی توسط کلاسها، صرفا یک انتخاب شخصی است.
یک نکته: امکان Destructuring Arguments نیز در اینجا وجود دارد. یعنی بجای اینکه یکبار props را به عنوان پارامتر دریافت کرد و سپس توسط آن به خاصیت totalCounters دسترسی یافت، میتوان نوشت:
const NavBar = ({ totalCounters }) => {
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-08.zip
var prop = doc.Get("prop_name");
در این مجموعه مقالات، به بررسی و آموزش برنامه نویسی شیء گرا در جاوا اسکریپت میپردازیم. در طول آموزش، فرض را بر این قرار دادیم که شما به عنوان خوانندهی این مقاله، با مبانی جاوا اسکریپت آشنا میباشید و حداقل چند قطعه کد مفید را با جاوا اسکریپت نوشتهاید. همچنین کمی هم با مباحث شیء گرایی آشنا میباشید.
روال آموزش در این مجموعه به گونه است که در ابتدا به معرفی مباحث پیش نیاز جهت ورود به دنیای شیء گرایی در جاوا اسکریپت، پرداخته خواهد شد. سپس مباحث شیء گرایی را آغاز خواهیم نمود و تمامی نکات ریز و درشت آن را بررسی خواهیم کرد. پس از پایان این مقالات قادر خواهید بود تا تمامی کتابخانهها و Frameworkهای جاوا اسکریپتی را مطالعه نموده و به راحتی تکنیکهای کد نویسی آن را درک کنید. همچنین خود شما نیز برای نوشتن یک کتابخانه یا Framework جاوا اسکریپتی استاندارد و حرفهای، دست به کار شوید و یک کتابخانه سودمند را ارائه نمایید.
با چند مثال ساده و بصورت تقریبا مبتدی مسائل پایه را تشریح نموده و آرام آرام مباحث حرفه ای را آغاز خواهیم کرد. قرار است در پایان این آموزشها به بررسی الگوهای موجود در جاوا اسکریپت نیز بپردازم که مجموعه مقالات مربوط به الگوها را در قالب مجموعه مقالاتی مجزا ارائه مینمایم.
در تهیهی این مجموعه مقالات از منابع زیر استفاده شده است:
1. Pro JavaScript Techniques (John Resig ، خالق JQuery – فصول 2 و 3)
2. Professional JavaScript for Web Developers (Third Edition) (Nicholas C. Zakas – فصول 3، 4، 5، 6 و 7)
3. Object-Oriented JavaScript (Stoyan Stefanov – فصول 3، 4، 5، 6 و 8)
4. و تجربهی ناچیز اینجانب
توابع (Functions)
همیشه در برنامه، مجموعه دستوراتی وجود دارند که عمل خاصی را انجام میدهند و به دفعات نیز مورد استفاده قرار میگیرند. جهت جلوگیری از تکرار این دستورات، افزایش خوانایی و کاهش پیچیدگی برنامه، این دستورات را در بستهای به نام تابع قرار میدهیم تا در زمان نیاز، آن را فراخوانی یا اجرا نماییم. جهت تعریف تابع در جاوا اسکریپت به صورت زیر عمل مینماییم:
function <functionName> ([arg0, arg1, …, argN]) { <statements> }
به عنوان مثال:
function sayHello(name, message) { alert("Hello " + name + ", " + message); }
این تابع شامل دو آرگومان ورودی است که میتواند به صورت زیر فراخوانی شود:
sayHello("Meysam", "welcome to site");
خروجی این تابع “Hello Meysam, welcome to site” می باشد که به صورت یک پنجرهی پیغام، نمایش مییابد. تابع فوق هیچ مقداری را به عنوان خروجی به برنامهی اصلی یا محل فراخوانی خود بر نمیگرداند. اگر بخواهیم توسط تابع مقداری را برگردانیم میتوانیم از دستور return برای این منظور استفاده نماییم. به مثال زیر توجه کنید:
function sum(num1, num2) { return num1 + num2; }
تابع sum دو عدد را به عنوان آرگومان ورودی دریافت نموده و حاصل جمع آنها را توسط دستور return به عنوان خروجی بر میگرداند. تابع فوق را میتوان به صورت زیر فراخوانی نمود:
alert(sum(10, 20));
خروجی :
30
اینک به بررسی چند نکته در مورد دستور return میپردازیم. دستور return فقط میتواند یک مقدار را برگرداند و نمیتوان، چند مقدار را مقابل این دستور نوشت. همچنین روال اجرای تابع با رسیدن به دستور return خاتمه مییابد و دستورات بعد از آن اجرا نخواهند شد. به مثال زیر توجه کنید:
function sum(num1, num2) { return num1 + num2; alert("Hello"); }
در مثال فوق دستور alert به هیچ عنوان اجرا نخواهد شد؛ زیرا تابع با رسیدن به دستور return خاتمه مییابد. یک تابع میتواند شامل بیش از یک دستور return باشد.
function diff(num1,num2) { if (num1 > num2) return num1 - num2; else return num2 - num1; }
تابع فوق اختلاف دو عدد را بدست میآورد و اگر عدد اول بزرگتر باشد، عدد دوم را از عدد اول تفریق میکند؛ در غیر اینصورت عدد اول را از عدد دوم تفریق میکند و به عنوان خروجی بر میگرداند. نکتهی دیگری که لازم است بدانید این است که دستور return می تواند هیچ مقداری را بر نگرداند و به تنهایی بکار گرفته شود. در این صورت دقیقا بعد از دستور return سمی کالن (;) قرار میدهیم.
function checkNumber(num) { if (isNaN(num)) { alert("Not a number"); return; } alert(num+" is a number"); }
تابع فوق یک ورودی را دریافت مینماید و در صورتی که آرگومان ورودی عدد نباشد پیغام “Not a number” را نمایش میدهد و از تابع خارج میشود. در صورتی که آرگومان ورودی یک عدد باشد، پیغام دوم را نمایش میدهد.
توجه:
یک تابع بهتر است همیشه یک مقداری را به عنوان خروجی برگرداند و یا کلا هیچ مقداری را بر نگرداند. نوشتن توابعی که در برخی شرایط مقداری را به عنوان خروجی بر میگردانند و در برخی شرایط مقداری را برنمی گردانند موجب ایجاد پیچیدگی در اشکال زدایی میگردند. نوشتن تابعی به صورت زیر کمی گیج کننده و نامتعارف میباشد:
function sum(num1, num2) { if (isNaN(num1) || isNaN(num2)) return; // بهتر است حداقل مقدار 0 برگردانده شود return num1 + num2; }
کار با آرگومان ها
رفتار آرگومانها در جاوا اسکریپت نسبت به سایر زبانهای برنامه نویسی کاملا متفاوت میباشد. در جاوا اسکریپت تعداد و نوع آرگومانهای ارسالی بررسی نمیشوند و خطایی هم رخ نخواهد داد. به عنوان مثال اگر تابعی با 3 آرگومان ورودی دارید، میتوانید با 0 تا 3 آرگومان ورودی، آن تابع را فراخوانی نمایید. زیرا آرگومانها در سیستم داخلی جاوا اسکریپت به صورت یک آرایه ارسال میگردند و تابع توجه نمیکند که کدام آرگومانها به این آرایه ارسال شدهاند.
با توجه به قابلیتی که در مورد آرگومانها ذکر شد و به دلیل عدم مدیریت نوع و تعداد آرگومانهای ارسالی، مطمئنا جهت جلوگیری از بروز خطا در توابع، باید تعداد و نوع آرگومانهای ارسالی بررسی و مدیریت شوند. همچنین در هر تابع، آرایهای به نام arguments به صورت توکار تعبیه شده است که مدیریت آرگومانها را تسهیل میبخشد. به مثال زیر توجه کنید:
function sayHello() { alert("Hello " + arguments[0] + ", " + arguments[1]); } sayHello("Meysam", "welcome to site");
خروجی :
"Hello Meysam, welcome to site"
تابع فوق هیچ آرگومان ورودی ندارد ولی با دو آرگومان ورودی فراخوانی شده است. در داخل تابع توسط آرایه arguments به آرگومانهای ارسالی دسترسی پیدا کردیم. حال به مثال زیر توجه کنید:
function sum() { var s = 0; for (var i = 0; i < arguments.length; i++) s += arguments[i]; return s; } alert(sum()); alert(sum(10, 20, 30, 40, 50)); alert(sum(10));
خروجی :
0
150
10
در تابع فوق هیچ آرگومان ورودی وجود ندارد ولی این تابع را با 0، 5 و 1 آرگومان ورودی فراخوانی نمودیم. این تابع مجموع چند عدد را محاسبه و بر میگرداند و میتواند به تعداد نامحدودی عدد دریافت نماید. البته بهتر است نوع آرگومانهای ارسالی نیز بررسی شوند تا خطایی در محاسبات رخ ندهد. همچنین بجای حلقه for از حلقه for…in استفاده خواهم کرد.
function sum() { var s = 0; for (var i in arguments) { if (isNaN(arguments[i])) continue; s += arguments[i]; } return s; } alert(sum()); alert(sum(10, 20, "a", 40, 50)); alert(sum(10));
خروجی :
0
120
10
اگر دقت کرده باشید در فراخوانی دوم، رشته “a” به تابع ارسال شده است و چون آرگومانهای نامعتبر را مدیریت نمودهایم، خطایی در خروجی رخ نمیدهد. به مثال زیر نیز توجه نمایید:
function sum(a,b,c) { return a + b + c; } alert(sum(10, 20, 30)); alert(sum(10, 20)); alert(sum());
خروجی :
60
NaN
NaN
تابع فوق دارای 3 آرگومان ورودی میباشد؛ ولی ما این تابع را با 2 و 0 آرگومان ورودی فراخوانی نمودیم که خروجی نامناسبی را تولید نموده است. برای رفع این مشکل و معتبر سازی آرگومانهای ارسالی میتوانیم به صورت زیر عمل نماییم:
function sum(a, b, c) { if (isNaN(a)) a = 0; if (isNaN(b)) b = 0; if (isNaN(c)) c = 0; return a + b + c; } alert(sum(10, 20, 30)); alert(sum(10, 20)); alert(sum());
خروجی :
60
30
0
در تابع، قبل از انجام عمل محاسبه، بررسی کردیم که آرگومانهای ارسالی مقدار دهی شده باشند. در صورت عدم مقداردهی و یا مقداردهی نامناسب، آرگومان ارسالی را با صفر مقداردهی مینماید. اگر آرگومان مورد نظر به تابع ارسال نشود، مقدار پیش فرض آن undefined میباشد.
با توجه به مسائل مطرح شده در مورد توابع، این روش استفاده و کاربرد توابع، جزء معایب توابع در جاوا اسکریپت محسوب نمیشود. این تکنیک استفاده از توابع، موجب افزایش انعطاف پذیری توابع و آزادی عمل برنامه نویس میگردد که لذت بیشتری را به برنامه نویسی میدهد.
توجه:
به آرگومانهایی که در تابع دارای نام میباشند و یا به عبارتی، آرگومانهایی که نام آنها در تابع ذکر میشود، Named Arguments یا آرگومانهای نامی (اسمی و یا نامدار) میگویند. مثل آرگومان های a ، b وc در تابع sum .
عدم پشتیبانی از سربارگذاری یا Overloading
در زبانهای برنامه نویسی شیء گرا، امکان تعریف توابع هم نام وجود دارد؛ به شرطی که امضای این توابع با هم متفاوت باشند. منظور از امضاء، تعداد و نوع آرگومانهای ورودی میباشد. از آنجاییکه در سیستم داخلی جاوا اسکریپت، آرگومانها بصورت یک آرایه ارسال میشوند، بنابراین امضاء برای توابع مفهومی ندارد؛ پس نمیتوانیم توابع هم نام یا overloading داشته باشیم.
اگر دو تابع هم نام داشته باشیم، تابعی که دیرتر تعریف میشود، جایگزین تابع قبلی میگردد. به مثال زیر توجه کنید:
function calc(num1,num2) { return num1 + num2; } function calc(num1,num2) { return num1 - num2; } alert(calc(200,100));
خروجی :
100
همانطور که در مثال فوق مشاهده مینمایید، تابع دوم فراخوانی شدهاست و حاصل تفریق به عنوان خروجی نمایش یافته است.
یکی دیگر از ماژولهایی که امکان اتصال Node.js را به SQL Server ممکن میکند، Edge.js است. Edge.js یک ماژول Node.js است که امکان اجرای کدهای دات نت را در همان پروسه توسط Node.js فراهم میکند. این مسئله، توسعه دهندگان Node.js را قادر میسازد تا از فناوریهایی که به صورت سنتی استفادهی از آنها سخت یا غیر ممکن بوده است را به راحتی استفاده کنند. برای نمونه:
- SQL Server
- Active Directory
- Nuget packages
- استفاده از سخت افزار کامپیوتر (مانند وب کم، میکروفن و چاپگر)
نصب Node.js
اگر Node.js را بر روی سیستم خود نصب ندارید، میتوانید از اینجا آن را دانلود کنید. بعد از نصب برای اطمینان از کارکرد آن، command prompt را باز کرده و دستور زیر را تایپ کنید:
node -v
ایجاد پوشه پروژه
سپس پوشهای را برای پروژه Node.js خود ایجاد کنید. مثلا با استفاده از command prompt و دستور زیر:
md \projects\node-edge-test1 cd \projects\node-edge-test1
نصب Edge.js
Node با استفاده از package manager خود دانلود و نصب ماژولها را خیلی آسان کرده است. برای نصب، در command prompt عبارت زیر را تایپ کنید:
npm install edge npm install edge-sql
Hello World
ایجاد یک فایل متنی با نام server.js و نوشتن کد زیر در آن:var edge = require('edge'); // The text in edge.func() is C# code var helloWorld = edge.func('async (input) => { return input.ToString(); }'); helloWorld('Hello World!', function (error, result) { if (error) throw error; console.log(result); });
node server.js
ایجاد پایگاه داده تست
در مثالهای بعدی، نیاز به یک پایگاه داده داریم تا queryها را اجرا کنیم. در صورتی که SQL Server بر روی سیستم شما نصب نیست، میتوانید نسخهی رایگان آن را از اینجا دانلود و نصب کنید. همچنین SQL Management Studio Express را نیز نصب کنید.
- در SQL Management Studio، یک پایگاه داده را با نام node-test با تنظیمات پیش فرض ایجاد کنید.
- بر روی پایگاه داده node-test راست کلیک کرده و New Query را انتخاب کنید.
- اسکریپت زیر را copy کرده و در آنجا paste کنید، سپس بر روی Execute کلیک کنید.
IF EXISTS(SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('SampleUsers')) BEGIN; DROP TABLE SampleUsers; END; GO CREATE TABLE SampleUsers ( Id INTEGER NOT NULL IDENTITY(1, 1), FirstName VARCHAR(255) NOT NULL, LastName VARCHAR(255) NOT NULL, Email VARCHAR(255) NOT NULL, CreateDate DATETIME NOT NULL DEFAULT(getdate()), PRIMARY KEY (Id) ); GO INSERT INTO SampleUsers(FirstName,LastName,Email,CreateDate) VALUES('Orla','Sweeney','nunc@convallisincursus.ca','Apr 13, 2014'); INSERT INTO SampleUsers(FirstName,LastName,Email,CreateDate) VALUES('Zia','Pickett','porttitor.tellus.non@Duis.com','Aug 31, 2014'); INSERT INTO SampleUsers(FirstName,LastName,Email,CreateDate) VALUES('Justina','Ayala','neque.tellus.imperdiet@temporestac.com','Jul 28, 2014'); INSERT INTO SampleUsers(FirstName,LastName,Email,CreateDate) VALUES('Levi','Parrish','adipiscing.elit@velarcueu.com','Jun 21, 2014'); INSERT INTO SampleUsers(FirstName,LastName,Email,CreateDate) VALUES('Pearl','Warren','In@dignissimpharetra.org','Mar 3, 2014');
تنظیمات ConnectionString
قبل از استفاده از Edge.js با SQL Server، باید متغیر محیطی (environment variable) با نام EDGE_SQL_CONNECTION_STRING را تعریف کنید.
set EDGE_SQL_CONNECTION_STRING=Data Source=localhost;Initial Catalog=node-test;Integrated Security=True
SETX EDGE_SQL_CONNECTION_STRING "Data Source=localhost;Initial Catalog=node-test;Integrated Security=True"
روش اول: اجرای مستقیم SQL Server Query در Edge.js
فایلی با نام server-sql-query.js را ایجاد کرده و کد زیر را در آن وارد کنید:
var http = require('http'); var edge = require('edge'); var port = process.env.PORT || 8080; var getTopUsers = edge.func('sql', function () {/* SELECT TOP 3 * FROM SampleUsers ORDER BY CreateDate DESC */}); function logError(err, res) { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.write("Error: " + err); res.end(""); } http.createServer(function (req, res) { res.writeHead(200, { 'Content-Type': 'text/html' }); getTopUsers(null, function (error, result) { if (error) { logError(error, res); return; } if (result) { res.write("<ul>"); result.forEach(function(user) { res.write("<li>" + user.FirstName + " " + user.LastName + ": " + user.Email + "</li>"); }); res.end("</ul>"); } else { } }); }).listen(port); console.log("Node server listening on port " + port);
node server-sql-query.js
روش دوم: اجرای کد دات نت برای SQL Server Query
Edge.js تنها از دستورات Update، Insert، Select و Delete پشتیبانی میکند. در حال حاضر از store procedures و مجموعهای از کد SQL پشتیبانی نمیکند. بنابراین، اگر چیزی بیشتر از عملیات CRUD میخواهید انجام دهید، باید از دات نت برای این کار استفاده کنید.یادتان باشد، همیشه async
مدل اجرایی Node.js به صورت یک حلقهی رویداد تک نخی است. بنابراین این بسیار مهم است که کد دات نت شما به صورت async باشد. در غیر اینصورت یک فراخوانی به دات نت سبب مسدود شدن و ایجاد خرابی در Node.js میشود.
ایجاد یک Class Library
اولین قدم، ایجاد یک پروژه Class Library در Visual Studio که خروجی آن یک فایل DLL است و استفاده از آن در Edge.js است. پروژه Class Library با عنوان EdgeSampleLibrary ایجاد کرده و فایل کلاسی با نام Sample1 را به آن اضافه کنید و سپس کد زیر را در آن وارد کنید:
using System; using System.Collections.Generic; using System.Data; using System.Data.SqlClient; using System.Threading.Tasks; namespace EdgeSampleLibrary { public class Sample1 { public async Task<object> Invoke(object input) { // Edge marshalls data to .NET using an IDictionary<string, object> var payload = (IDictionary<string, object>) input; var pageNumber = (int) payload["pageNumber"]; var pageSize = (int) payload["pageSize"]; return await QueryUsers(pageNumber, pageSize); } public async Task<List<SampleUser>> QueryUsers(int pageNumber, int pageSize) { // Use the same connection string env variable var connectionString = Environment.GetEnvironmentVariable("EDGE_SQL_CONNECTION_STRING"); if (connectionString == null) throw new ArgumentException("You must set the EDGE_SQL_CONNECTION_STRING environment variable."); // Paging the result set using a common table expression (CTE). // You may rather do this in a stored procedure or use an // ORM that supports async. var sql = @" DECLARE @RowStart int, @RowEnd int; SET @RowStart = (@PageNumber - 1) * @PageSize + 1; SET @RowEnd = @PageNumber * @PageSize; WITH Paging AS ( SELECT ROW_NUMBER() OVER (ORDER BY CreateDate DESC) AS RowNum, Id, FirstName, LastName, Email, CreateDate FROM SampleUsers ) SELECT Id, FirstName, LastName, Email, CreateDate FROM Paging WHERE RowNum BETWEEN @RowStart AND @RowEnd ORDER BY RowNum; "; var users = new List<SampleUser>(); using (var cnx = new SqlConnection(connectionString)) { using (var cmd = new SqlCommand(sql, cnx)) { await cnx.OpenAsync(); cmd.Parameters.Add(new SqlParameter("@PageNumber", SqlDbType.Int) { Value = pageNumber }); cmd.Parameters.Add(new SqlParameter("@PageSize", SqlDbType.Int) { Value = pageSize }); using (var reader = await cmd.ExecuteReaderAsync(CommandBehavior.CloseConnection)) { while (await reader.ReadAsync()) { var user = new SampleUser { Id = reader.GetInt32(0), FirstName = reader.GetString(1), LastName = reader.GetString(2), Email = reader.GetString(3), CreateDate = reader.GetDateTime(4) }; users.Add(user); } } } } return users; } } public class SampleUser { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Email { get; set; } public DateTime CreateDate { get; set; } } }
[project]/bin/Debug/EdgeSampleLibrary.dll
var http = require('http'); var edge = require('edge'); var port = process.env.PORT || 8080; // Set up the assembly to call from Node.js var querySample = edge.func({ assemblyFile: 'EdgeSampleLibrary.dll', typeName: 'EdgeSampleLibrary.Sample1', methodName: 'Invoke' }); function logError(err, res) { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.write("Got error: " + err); res.end(""); } http.createServer(function (req, res) { res.writeHead(200, { 'Content-Type': 'text/html' }); // This is the data we will pass to .NET var data = { pageNumber: 1, pageSize: 3 }; // Invoke the .NET function querySample(data, function (error, result) { if (error) { logError(error, res); return; } if (result) { res.write("<ul>"); result.forEach(function(user) { res.write("<li>" + user.FirstName + " " + user.LastName + ": " + user.Email + "</li>"); }); res.end("</ul>"); } else { res.end("No results"); } }); }).listen(port); console.log("Node server listening on port " + port);
node server-dotnet-query.js
نکته: برای ایجاد pageNumber و pageSize داینامیک با استفاده از ارسال مقادیر توسط QueryString، میتوانید از ماژول connect استفاده کنید.