مطالب
CoffeeScript #11

کامپایل خودکار CoffeeScript

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

در قسمت اول گفته شد، برای کامپایل فایل CoffeeScript با استفاده از coffee به صورت زیر عمل می‌کردیم:

coffee --compile --output lib src
همانطور که در مثال بالا مشاهده می‌کنید، تمامی فایل‌های coffee. در داخل پوشه src را کامپایل می‌کنید و فایل‌های جاوااسکریپت تولید شده را در پوشه lib ذخیره می‌کنید.
حال به کامپایل خودکار CoffeeScript توجه کنید.

Cake

Cake یک سیستم فوق العاده ساده برای کامپایل خودکار است که مانند Make و Rake عمل می‌کند. این کتابخانه همراه پکیج coffee-script npm نصب می‌شود و برای استفاده با فراخوانی cake اجرا می‌شود.

برای ایجاد فایل tasks در cake که Cakefile نامیده می‌شود، می‌توان از خود CoffeeScript استفاده کرد. برای اجرای cake با استفاده از دستور [cake [task] [options می‌توان عمل کرد. برای اطلاع از لیست امکانات cake کافی است دستور cake را به تنهایی اجرا کنید.

وظایف را می‌توان با استفاده از تابع task، با ارسال نام و توضیحات (اختیاری) و تابع callback، تعریف کرد. به مثال زیر توجه کنید:

fs = require 'fs'

{print} = require 'sys'
{spawn} = require 'child_process'

build = (callback) ->
  coffee = spawn 'coffee', ['-c', '-o', 'lib', 'src']
  coffee.stderr.on 'data', (data) ->
    process.stderr.write data.toString()
  coffee.stdout.on 'data', (data) ->
    print data.toString()
  coffee.on 'exit', (code) ->
    callback?() if code is 0

task 'build', 'Build lib/ from src/', ->
  build()
همانطور که در مثال بالا مشاهده می‌کنید، تابع task را با نام build تعریف کردیم و با استفاده از دستور cake build می‌توان آن را اجرا نمود. پس از اجرا همانند مثال قبل تمامی فایل‌های CoffeeScript در پوشه‌ی src به فایل‌های جاوااسکریپت در پوشه lib تبدیل می‌شوند.
همان طور که مشاهده می‌کنید پس از تغییر در فایل CoffeeScript باید به صورت دستی cake build را فراخوانی کنیم که این دور از حالت ایده آل است.
خوشبختانه دستور coffee پارامتر دیگری به نام watch-- دارد که به وسیله آن می‌توان تمامی تغییرات یک پوشه را زیر نظر گرفت و در صورت نیاز دوباره کامپایل انجام شود. به مثال زیر توجه کنید:
 task 'watch', 'Watch src/ for changes', ->
    coffee = spawn 'coffee', ['-w', '-c', '-o', 'lib', 'src']
    coffee.stderr.on 'data', (data) ->
      process.stderr.write data.toString()
    coffee.stdout.on 'data', (data) ->
      print data.toString()
در صورتی که task ایی وابسته به task دیگری باشد، می‌توانید برای اجرای taskهای دیگر از دستور (invoke(name استفاده کنید. برای مثال یک task را به فایل Cakefile اضافه می‌کنیم که در آن ابتدا فایل index.html را باز کرده و سپس شروع به زیر نظر گرفتن پوشه src می‌کنیم.
task 'open', 'Open index.html', ->
  # First open, then watch
  spawn 'open', 'index.html'
  invoke 'watch'
همچنین می‌توانید با استفاده از تابع ()options ،option را برای taskها تعریف کنید.
option '-o', '--output [DIR]', 'output dir'

task 'build', 'Build lib/ from src/', ->
  # Now we have access to a `options` object
  coffee = spawn 'coffee', ['-c', '-o', options.output or 'lib', 'src']
  coffee.stderr.on 'data', (data) ->
    process.stderr.write data.toString()
  coffee.stdout.on 'data', (data) ->
    print data.toString()

Cake یک روش عالی برای انجام وظایف معمول به صورت خودکار است، مانند کامپایل فایل‌های CoffeeScript است. همچنین برای آشنایی بیشتر می‌توانید به سورس cake نگاهی کنید.

مسیرراه‌ها
ASP.NET MVC
              مطالب
              React 16x - قسمت 25 - ارتباط با سرور - بخش 4 - یک تمرین
              همان مثال backend قسمت 22 را با افزودن وب سرویس‌هایی برای قسمت‌های نمایش لیست فیلم‌ها، ژانرها و سایر صفحات اضافه شده‌ی به برنامه‌ی تکمیل شده‌ی تا قسمت 21، توسعه می‌دهیم. کدهای کامل آن، به علت شباهت و یکی بودن نکات آن با مطلب 22، در اینجا تکرار نخواهند شد و می‌توانید کل پروژه‌ی آن‌را از پیوست انتهای بحث دریافت کنید. سپس فایل dotnet_run.bat آن‌را اجرا کنید تا در آدرس https://localhost:5001 قابل دسترسی شود.


              افزودن سرویس httpService.js به برنامه

              تا این قسمت، تمام اطلاعات نمایش داده شده‌ی در لیست فیلم‌ها، از سرویس درون حافظه‌ای src\services\fakeMovieService.js و لیست ژانرها از سرویس src\services\fakeGenreService.js، تامین می‌شوند. اکنون در ادامه می‌خواهیم این سرویس‌ها را با سرویس backend یاد شده، جایگزین کنیم تا این برنامه، اطلاعات خودش را از سرور دریافت کند. به همین جهت قبل از هر کاری، سرویس عمومی src\services\httpService.js را که در قسمت قبل توسعه دادیم، به برنامه‌ی نمایش لیست فیلم‌ها نیز اضافه می‌کنیم (فایل آن‌را از پروژه‌ی قبلی کپی کرده و در اینجا paste می‌کنیم)، تا بتوانیم از امکانات آن در اینجا نیز استفاده کنیم. فایل httpService.js، دارای وابستگی‌های خارجی react-toastify و axios است. به همین جهت برای افزودن آن‌ها مراحل زیر را طی می‌کنیم:
              - نصب کتابخانه‌های react-toastify و axios از طریق خط فرمان (با فشردن دکمه‌های ctrl+back-tick در VSCode):
              > npm i axios --save
              > npm i react-toastify --save
              سپس به فایل app.js مراجعه کرده و importهای لازم آن‌را اضافه می‌کنیم:
              import { ToastContainer } from "react-toastify";
              import "react-toastify/dist/ReactToastify.css";
              همچنین نیاز است ToastContainer را به ابتدای متد render نیز اضافه کرد:
                render() {
                  return (
                    <React.Fragment>
                      <ToastContainer />


              دریافت اطلاعات لیست نمایش ژانرها از سرویس backend

              با فراخوانی آدرس https://localhost:5001/api/Genres، می‌توان لیست ژانرهای سینمایی تعریف شده‌ی در سرویس‌های backend را مشاهده کرد. اکنون قصد داریم از این اطلاعات، در برنامه استفاده کنیم. به همین جهت به فایل src\components\movies.jsx مراجعه کرده و تغییرات زیر را اعمال می‌کنیم:
              چون نمی‌خواهیم تغییراتی بسیار اساسی را در اینجا اعمال کنیم، قدم به قدم عمل کرده و سرویس قبلی fakeGenreService.js را با یک سرویس جدید که اطلاعات خودش را از سرور دریافت می‌کند، جایگزین می‌کنیم. بنابراین ابتدا فایل جدید src\services\genreService.js را ایجاد می‌کنیم. سپس آن‌را طوری تکمیل خواهیم کرد که اینترفیس آن، با اینترفیس fakeGenreService قبلی یکی باشد:
              import { apiUrl } from "../config.json";
              import http from "./httpService";
              
              export function getGenres() {
                return http.get(apiUrl + "/genres");
              }
              همچنین در اینجا import وابستگی config.json را نیز مشاهده می‌کنید که در قسمت قبل در مورد آن توضیح دادیم. به همین جهت برای تمیزتر شدن قسمت‌های مختلف برنامه، فایل config.json را در مسیر src\config.json ایجاد کرده و به صورت زیر تکمیل می‌کنیم:
              {
                 "apiUrl": "https://localhost:5001/api"
              }
              apiUrl به ریشه‌ی URLهای ارائه شده‌ی توسط backend service ما، اشاره می‌کند.

              پس از تکمیل سرویس جدید src\services\genreService.js، به فایل src\components\movies.jsx بازگشته و سطر قبلی
              import { getGenres } from "../services/fakeGenreService";
              را با سطر جدید زیر، جایگزین می‌کنیم:
              import { getGenres } from "../services/genreService";
              تا اینجا اگر برنامه را ذخیره کرده و اجرا کنید، خطای زیر را مشاهده خواهید کرد:
              Uncaught TypeError: Object is not a function or its return value is not iterable
              علت اینجا است که سرویس قبلی fakeGenreService، دارای متد export شده‌ای به نام getGenres بود که یک آرایه‌ی معمولی را بازگشت می‌داد. اکنون این سرویس جدید نیز همان ساختار را دارد، اما اینبار یک Promise را بازگشت می‌دهد. به همین جهت متد componentDidMount را به صورت زیر اصلاح می‌کنیم:
                async componentDidMount() {
                  const { data } = await getGenres();
                  const genres = [{ _id: "", name: "All Genres" }, ...data];
                  this.setState({ movies: getMovies(), genres });
                }
              متد getGenres باید await شود تا نتیجه‌ی آن توسط خاصیت data شیء بازگشتی از آن، قابل دسترسی شود. با این تغییر، نیاز است این متد را نیز به صورت async معرفی کرد.


              دریافت اطلاعات لیست فیلم‌ها از سرویس backend

              پس از دریافت لیست ژانرهای سینمایی از سرور، اکنون نوبت به جایگزینی src\services\fakeMovieService.js با یک نمونه‌ی متصل به backend است. به همین جهت ابتدا فایل جدید src\services\movieService.js را ایجاد کرده و سپس آن‌را به صورت زیر تکمیل می‌کنیم:
              import { apiUrl } from "../config.json";
              import http from "./httpService";
              
              const apiEndpoint = apiUrl + "/movies";
              
              function movieUrl(id) {
                return `${apiEndpoint}/${id}`;
              }
              
              export function getMovies() {
                return http.get(apiEndpoint);
              }
              
              export function getMovie(movieId) {
                return http.get(movieUrl(movieId));
              }
              
              export function saveMovie(movie) {
                if (movie.id) {
                  return http.put(movieUrl(movie.id), movie);
                }
              
                return http.post(apiEndpoint, movie);
              }
              
              export function deleteMovie(movieId) {
                return http.delete(movieUrl(movieId));
              }
              سپس شروع به اصلاح کامپوننت movies می‌کنیم.
              ابتدا دو متد دریافت لیست فیلم‌ها و حذف یک فیلم را که در این کامپوننت استفاده شده‌اند، import می‌کنیم:
              import { getMovies, deleteMovie } from "../services/movieService";
              بعد متد getMovies پیشین، که یک آرایه را بازگشت می‌داد، توسط متد جدیدی که یک Promise را بازگشت می‌دهد، جایگزین می‌شود:
                async componentDidMount() {
                  const { data } = await getGenres();
                  const genres = [{ id: "", name: "All Genres" }, ...data];
              
                  const { data: movies } = await getMovies();
                  this.setState({ movies, genres });
                }
              همچنین مدیریت حذف رکوردها را نیز به صورت زیر با پیاده سازی «به‌روز رسانی خوشبینانه UI» که در قسمت قبل در مورد آن بیشتر بحث شد، تغییر می‌دهیم. در این حالت فرض بر این است که به احتمال زیاد،  await deleteMovie با موفقیت به پایان می‌رسد. بنابراین بهتر است UI را ابتدا به روز رسانی کنیم تا کاربر حس کار کردن با یک برنامه‌ی سریع را داشته باشد:
                handleDelete = async movie => {
                  const originalMovies = this.state.movies;
              
                  const movies = originalMovies.filter(m => m.id !== movie.id);
                  this.setState({ movies });
              
                  try {
                    await deleteMovie(movie.id);
                  } catch (ex) {
                    if (ex.response && ex.response.status === 404) {
                      console.log(ex);
                      toast.error("This movie has already been deleted.");
                    }
              
                    this.setState({ movies: originalMovies }); //undo changes
                  }
                };
              ابتدا ارجاعی را از state قبلی ذخیره می‌کنیم تا در صوت بروز خطایی در سطر await deleteMovie، بتوانیم مجددا state را به حالت اول آن بازگردانیم. به همین منظور پیاده سازی «به‌روز رسانی خوشبینانه UI»، حتما نیاز به درج صریح try/catch را دارد. برای نمایش خطاهای ویژه‌ی 404 نیز از یک toast استفاده شده که نیاز به import زیر را دارد:
              import { toast } from "react-toastify";
              سایر خطاهای رخ داده، توسط interceptor درج شده‌ی در سرویس http، به صورت خودکار پردازش می‌شوند.


              اتصال فرم ثبت و ویرایش یک فیلم به backend server

              تا اینجا اگر برنامه را اجرا کنیم، با کلیک بر روی لینک هر فیلم نمایش داده شده‌ی در صفحه، به صفحه‌ی not-found هدایت می‌شویم. برای رفع این مشکل، به فایل src\components\movieForm.jsx مراجعه کرده و ابتدا
              import { getGenres } from "../services/fakeGenreService";
              import { getMovie, saveMovie } from "../services/fakeMovieService";
              قبلی را با نمونه‌ها‌ی جدیدی که با سرور کار می‌کنند، جایگزین می‌کنیم:
              import { getGenres } from "../services/genreService";
              import { getMovie, saveMovie } from "../services/movieService";
              سپس ارجاعات به این سه متد import شده را با await، همراه کرده و متد اصلی را به صورت async معرفی می‌کنیم:
                async componentDidMount() {
                  const { data: genres } = await getGenres();
                  this.setState({ genres });
              
                  const movieId = this.props.match.params.id;
                  if (movieId === "new") return;
              
                  const { data: movie } = await getMovie(movieId);
                  if (!movie) return this.props.history.replace("/not-found");
              
                  this.setState({ data: this.mapToViewModel(movie) });
                }
              البته می‌توان جهت بهبود کیفیت کدها، از متد componentDidMount، دو متد با مسئولیت‌های مجزای دریافت ژانرها (populateGenres) و سپس نمایش فرم اطلاعات فیلم (populateMovie) را استخراج کرد:
                async populateGenres() {
                  const { data: genres } = await getGenres();
                  this.setState({ genres });
                }
              
                async populateMovie() {
                  try {
                    const movieId = this.props.match.params.id;
                    if (movieId === "new") return;
              
                    const { data: movie } = await getMovie(movieId);
                    this.setState({ data: this.mapToViewModel(movie) });
                  } catch (ex) {
                    if (ex.response && ex.response.status === 404)
                      this.props.history.replace("/not-found");
                  }
                }
              
                async componentDidMount() {
                  await this.populateGenres();
                  await this.populateMovie();
                }
              در متد populateMovie، اگر movieId اشتباهی وارد شود و یا کلا عملیات دریافت اطلاعات، با شکست مواجه شود، کاربر را به صفحه‌ی not-found هدایت می‌کنیم. یعنی وجود try/catch در اینجا ضروری است. چون اگر movieId اشتباهی وارد شود، اینبار دیگر خطوط بعدی اجرا نمی‌شوند و در همان سطر await getMovie، یک استثناء صادر شده و کار خاتمه پیدا می‌کند. بنابراین نیاز داریم بتوانیم این استثنای احتمالی را مدیریت کرده و کاربر را به صفحه‌ی not-found هدایت کنیم.
              پیشتر زمانیکه متد getMovie، یک شیء ساده را از fake service، بازگشت می‌داد، چنین مشکلی را نداشتیم؛ به همین جهت در سطر بعدی آن، هدایت کاربر در صورت نال بودن نتیجه، با یک return صورت می‌گرفت. اما اینجا بجای نال، یک استثناء را ممکن است دریافت کنیم.

              مرحله‌ی آخر اصلاح این فرم، اتصال قسمت ثبت اطلاعات آن است که با قرار دادن یک await، پیش از متد saveMovie و async کردن متد آن، انجام می‌شود:
                doSubmit = async () => {
                  await saveMovie(this.state.data);
              
                  this.props.history.push("/movies");
                };


              کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-25-backend.zip و sample-25-frontend.zip
              مطالب
              اجبار به استفاده‌ی از HTTPS در حین توسعه‌ی برنامه‌های ASP.NET Core 2.1
              پس از نصب SDK جدید NET Core 2.1. و ایجاد یک برنامه‌ی جدید بر اساس آن توسط دستور«dotnet new mvc» و سپس اجرای آن به کمک دستور «dotnet run»، تصویر جدیدی مشاهده می‌شود:


              در نگارش‌های قبلی، پس از اجرای برنامه، صرفا یک سطر زیر نمایش داده می‌شد:
              Now listening on: http://localhost:5000
              اما اکنون تبدیل شده‌است به دو سطر که اولی HTTPS است و دومی HTTP معمولی:
              Now listening on: https://localhost:5001
              Now listening on: http://localhost:5000
              یعنی برنامه بر روی دو پورت 5000 و یا 5001 قابل دسترسی است. در این حال اگر سعی کنیم برنامه را بر روی پورت 5000 که HTTP معمولی است اجرا کنیم، بلافاصله به خطای امن نبودن دسترسی به سایت و اجرای خودکار برنامه بر روی پورت 5001 خواهیم رسید:


              علت هدایت خودکار به آدرس HTTPS، به تغییرات فایل آغازین برنامه بر می‌گردد:
              public void Configure(IApplicationBuilder app, IHostingEnvironment env)
              {
                  if (env.IsDevelopment())
                  {
                      app.UseDeveloperExceptionPage();
                  }
                  else
                  {
                      app.UseExceptionHandler("/Home/Error");
                      app.UseHsts();
                  }
              
                  app.UseHttpsRedirection();
              در اینجا علاوه بر UseHsts ، تنظیم UseHttpsRedirection نیز به صورت پیش‌فرض قرار داده شده‌اند که سبب ارتقاء و همچنین هدایت خودکار به آدرس HTTPS برنامه می‌شوند. توضیحات بیشتری در مورد Hsts: «فعال‌سازی HSTS در ASP.NET Core» که با میان افزار جدید و توکار Hsts جایگزین می‌شود.
              اگر خواستید این شماره‌ی پورت را تغییر دهید، می‌توانید به صورت زیر عمل کنید:
               services.AddHttpsRedirection(options => options.HttpsPort = 5002);
              میان افزار جدید UseHsts به مرورگرها فرمان می‌دهد که این سایت را در حالت HTTPS مرور کنند. البته همانطور که مشاهده می‌کنید این مورد فقط برای حالت ارائه‌ی نهایی تنظیم شده‌است و نه حالت استفاده‌ی از برنامه در حالت localhost. جزئیات این میان‌افزار را به صورت زیر نیز می‌توان تنظیم کرد و یا تغییر داد:
              services.AddHsts(options =>
              {
                  options.MaxAge = TimeSpan.FromDays(100);
                  options.IncludeSubDomains = true;
                  options.Preload = true;
              });


              اما چرا برنامه در حالت HTTPS قابل مشاهده نیست؟

              پس از نصب SDK نگارش جدید NET Core.، یک مجوز SSL توسعه نیز به سیستم عامل اضافه می‌شود:
              ASP.NET Core
              ------------
              Successfully installed the ASP.NET Core HTTPS Development Certificate.

              برای مشاهده‌ی این مجوز، دستور certmgr.msc را در قسمت run ویندوز وارد کرده و enter کنید:


              این مجوز پیش فرض در قسمت «Personal/Certificates» با نام «ASP.NET Core HTTPS development certificate» قابل مشاهده‌است که در حقیقت یک Self Signed Certificate است و به صورت پیش فرض توسط سیستم معتبر و قابل اطمینان شناخته نمی‌شود.
              برای اعلام قابل اطمینان بودن این مجوز به سیستم، در همین کنسول مدیریت مجوزها، بر روی این مجوز کلیک راست کرده و آن‌را کپی کنید. سپس آن‌را در مسیر «Trusted Root Certification Authorities/Certificates» قرار دهید (paste کنید).


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

              روش دوم انجام اینکار، استفاده از دستور زیر است:
              dotnet install tool dotnet-dev-certs -g --version 2.1.0-preview1-final
              dotnet dev-certs https --trust
              دستور اول برنامه‌ی dotnet-dev-certs را نصب می‌کند و دستور دوم آن‌را اجرا کرده و توسط پرچم trust، همان کار کپی و paste ذکر شده‌ی در قسمت قبلی را به صورت خودکار انجام خواهد داد. هرچند صفحه‌ی تائید این نقل و انتقال در اینجا نیز ظاهر می‌شود.


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



              تنظیمات مخصوص IIS Express برای اجرای برنامه‌های ASP.NET Core 2.1

              دستور «dotnet run» از IIS برای اجرا استفاده نمی‌کند و مبتنی بر وب سرور Kestrel است. تنظیمات IIS و IIS Express از فایل Properties\launchSettings.json خوانده می‌شوند که اینبار به صورت زیر تغییر کرده‌است:
              {
                "iisSettings": {
                  "windowsAuthentication": false,
                  "anonymousAuthentication": true,
                  "iisExpress": {
                    "applicationUrl": "http://localhost:4929",
                    "sslPort": 44313
                  }
                },
                "profiles": {
                  "IIS Express": {
                    "commandName": "IISExpress",
                    "launchBrowser": true,
                    "environmentVariables": {
                      "ASPNETCORE_ENVIRONMENT": "Development",
                      "ASPNETCORE_HTTPS_PORT": "44313"
                    }
                  },
                  "TestWebApp": {
                    "commandName": "Project",
                    "launchBrowser": true,
                    "environmentVariables": {
                      "ASPNETCORE_ENVIRONMENT": "Development",
                      "ASPNETCORE_URLS": "https://localhost:5001;http://localhost:5000"
                    }
                  }
                }
              }
              در اینجا تنظیمات مربوط به sslPort و همچنین ASPNETCORE_HTTPS_PORT نیز اضافه شده‌اند که IIS Express از آن‌ها استفاده می‌کند.
              مطالب
              ذخیره سازی اطلاعات در مرورگر توسط برنامه‌های Angular
              تمام برنامه‌های وب، از داده‌ها استفاده می‌کنند و امکان ذخیره سازی، به اشتراک گذاری و بازیابی آن‌ها حتی زمانیکه اتصال به شبکه برقرار نیست، بسیار حائز اهمیت است. به همین جهت مرورگرهای امروزی نیز به همراه قابلیت‌هایی هستند تا این امر را ساده‌تر کنند. این محل ذخیره سازی، درون مرورگر کاربر بوده و دسترسی به آن نیز بسیار سریع است. همچنین امکان دسترسی به آن در حالت آفلاین و بدون اتصال به شبکه نیز میسر است. البته باید دقت داشت که بسته به نوع ذخیره سازی اطلاعات محلی انتخاب شده، حداکثر 10 مگابایت بیشتر در اختیار برنامه قرار نمی‌گیرد. همچنین دسترسی این اطلاعات وابسته‌است به ماشین و وسیله‌ی مورد استفاده. برای مثال اگر کاربر از طریق سیستم و ماشین دیگری برنامه را مرور کند، دیگر دسترسی به اطلاعات محلی قبلی خود نخواهد داشت و یا اگر کاربر کش مرورگر را خالی کند، این اطلاعات نیز حذف می‌شوند.


              حالت‌های مختلف ذخیره سازی اطلاعات در مرورگر کاربر

              Web Storage و یا Client-side storage در دو حالت کلی session storage و local storage قابل دسترسی است:
              الف) session storage
              در این حالت اطلاعات ذخیره شده‌ی در session storage، پس از بسته شدن مرورگر، به صورت خودکار حذف خواهند شد.

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

              هر دو حالت فوق به صورت ایزوله ارائه می‌شوند؛ با محدودیت حجم 10 مگابایت (جمع حجم نهایی هر دو حالت با هم، محدود به 10 مگابایت است). به این معنا که برنامه‌های هر دومین، تنها به محل ذخیره سازی خاص همان دومین دسترسی خواهند داشت.
              همچنین API دسترسی به آن‌ها synchronous است و کار کردن با آن‌ها ساده‌است.

              البته Client-side storage به دو مورد فوق خلاصه نمی‌شود و شامل File Storage ،WebSQL ،IndexedDB و کوکی‌های مرورگر نیز هست.
              - File Storage هنوز مراحل آزمایشی خودش را طی می‌کند و مناسب برنامه‌های دنیای واقعی نیست.
              - WebSQL قرار بود بر اساس بانک اطلاعاتی معروف SQLite ارائه شود؛ اما W3C در سال 2010 این استاندارد را منسوخ شده اعلام کرد و با IndexedDB جایگزین شد. دسترسی به آن async است و می‌تواند موضوع بحثی مجزا باشد.
              - کوکی‌های مرورگرها نیز یکی دیگر از روش‌های ذخیره سازی اطلاعات در مرورگرها هستند و تنها به ذخیره سازی حداکثر 4096 بایت اطلاعات محدود هستند. کوکی‌ها نیز همانند local storage پس از بسته شدن مرورگر باقی می‌مانند؛ اما برخلاف آن، دارای تاریخ انقضاء و همچنین قابلیت ارسال بین دومین‌ها را نیز دارا می‌باشند. اگر تاریخ انقضای یک کوکی تعیین نشود، همانند session storage، در پایان کار مرورگر و بسته شدن آن، حذف خواهد شد.


              تهیه یک سرویس Angular برای کار با Web Storage

              جهت کپسوله سازی نحوه‌ی کار با session storage و local storage می‌توان سرویسی را برای این‌کار تهیه کرد:
              import { Injectable } from "@angular/core";
              
              @Injectable()
              export class BrowserStorageService {
              
                getSession(key: string): any {
                  const data = window.sessionStorage.getItem(key);
                  return JSON.parse(data);
                }
              
                setSession(key: string, value: any): void {
                  const data = value === undefined ? null : JSON.stringify(value);
                  window.sessionStorage.setItem(key, data);
                }
              
                removeSession(key: string): void {
                  window.sessionStorage.removeItem(key);
                }
              
                removeAllSessions(): void {
                  for (const key in window.sessionStorage) {
                    if (window.sessionStorage.hasOwnProperty(key)) {
                      this.removeSession(key);
                    }
                  }
                }
              
                getLocal(key: string): any {
                  const data = window.localStorage.getItem(key);
                  return JSON.parse(data);
                }
              
                setLocal(key: string, value: any): void {
                  const data = value === undefined ? null : JSON.stringify(value);
                  window.localStorage.setItem(key, data);
                }
              
                removeLocal(key: string): void {
                  window.localStorage.removeItem(key);
                }
              
                removeAllLocals(): void {
                  for (const key in window.localStorage) {
                    if (window.localStorage.hasOwnProperty(key)) {
                      this.removeLocal(key);
                    }
                  }
                }
              }
              دسترسی به local storage از طریق شیء window.localStorage انجام می‌شود و کار با آن در برنامه‌های Angular، نیاز به وابستگی خاص دیگری ندارد. این مورد برای کار با session storage از طریق شیء window.sessionStorage صورت می‌گیرد. هر دو حالت، دارای متدهای setItem برای ذخیره سازی اطلاعات، getItem برای دریافت اطلاعات، بر اساس کلیدی مشخص و removeItem برای حذف اطلاعات کلیدی معلوم، هستند.
              در حالت setItem اطلاعاتی را که مرورگرها ذخیره می‌کنند باید رشته‌ای باشد. به همین جهت توسط متد JSON.stringify می‌توان یک شیء را تبدیل به رشته کرد و ذخیره نمود و در حالت getItem توسط متد JSON.parse، می‌توان این رشته را مجددا به همان شیء پیشین خود تبدیل کرد و بازگشت داد.


              محل صحیح تعریف BrowserStorageService

              همانطور که در مطلب «سازماندهی برنامه‌های Angular توسط ماژول‌ها» بررسی شد، محل صحیح تعریف این سرویس سراسری مشترک در بین کامپوننت‌ها و ماژول‌های برنامه، در CoreModule و پوشه‌ی src\app\core\browser-storage.service.ts است:
              import { BrowserStorageService } from "./browser-storage.service";
              import { NgModule } from "@angular/core";
              import { CommonModule } from "@angular/common";
              import { RouterModule } from "@angular/router";
              
              @NgModule({
                imports: [CommonModule, RouterModule],
                exports: [], // components that are used in app.component.ts will be listed here.
                declarations: [], // components that are used in app.component.ts will be listed here.
                providers: [BrowserStorageService] // singleton services of the whole app will be listed here.
              })
              export class CoreModule { };

              و CoreModule نیز به AppModule اضافه می‌شود:
              import { CoreModule } from "./core/core.module";
              
              @NgModule({
                imports:      [
              //...
                  CoreModule,
              //...
                  RouterModule.forRoot(appRoutes)
                ],
              //...
              })
              export class AppModule { }

              بنابراین یکی دیگر از روش‌های به اشتراک گذاری اطلاعات در بین قسمت‌های مختلف برنامه، ذخیره سازی آن‌ها در session/local storage و سپس بازیابی آن‌ها بر اساس کلیدهای مشخص آن‌ها است.


              مثالی از نحوه‌ی کاربرد BrowserStorageService

              برای آزمایش سرویس تهیه شده، از کامپوننت و قالب ذیل استفاده خواهیم کرد. در اینجا سرویس BrowserStorageService به سازنده‌ی کلاس تزریق شده‌است و سپس دو حالت session storage و local storage مورد بررسی قرار گرفته‌اند:
              import { BrowserStorageService } from "./../../core/browser-storage.service";
              import { Component, OnInit } from "@angular/core";
              
              @Component({
                selector: "app-browser-storage-sample-test",
                templateUrl: "./browser-storage-sample-test.component.html",
                styleUrls: ["./browser-storage-sample-test.component.css"]
              })
              export class BrowserStorageSampleTestComponent implements OnInit {
              
                fromSessionStorage = "";
                fromLocalStorage = ""
              
                sessionStorageKey = "sessionStorageKey1";
                localStorageKey = "localStorageKey1"
              
                constructor(private browserStorage: BrowserStorageService) { }
              
                ngOnInit() {
                }
              
                sessionStorageSetItem() {
                  this.browserStorage.setSession(this.sessionStorageKey, "Val1");
                }
              
                sessionStorageGetItem() {
                  this.fromSessionStorage = this.browserStorage.getSession(this.sessionStorageKey);
                }
              
                localStorageSetItem() {
                  this.browserStorage.setLocal(this.localStorageKey, { key1: "val1", key2: 2 });
                }
              
                localStorageGetItem() {
                  this.fromLocalStorage = JSON.stringify(this.browserStorage.getLocal(this.localStorageKey));
                }
              }
              به همراه قالب:
              <h1>Browser storage sample</h1>
              <div class="panel">
                <button class="btn btn-primary" (click)="sessionStorageSetItem()" type="button">sessionStorage -> Set Item</button>
                <button class="btn btn-success" (click)="sessionStorageGetItem()" type="button">sessionStorage -> Get Item</button>
                <div class="alert alert-info" *ngIf="fromSessionStorage">
                  {{fromSessionStorage}}
                </div>
              </div>
              
              <div class="panel">
                <button class="btn btn-warning" (click)="localStorageSetItem()" type="button">localStorage -> Set Item</button>
                <button class="btn btn-success" (click)="localStorageGetItem()" type="button">localStorage -> Get Item</button>
                <div class="alert alert-info" *ngIf="fromLocalStorage">
                  {{fromLocalStorage}}
                </div>
              </div>

              در این حالت اگر برنامه را اجرا کنیم، یک چنین خروجی قابل مشاهده خواهد بود:


              و اگر به برگه‌ی Application کنسول ابزارهای توسعه دهنده‌های مرورگرها نیز مراجعه کنیم، این مقادیر ثبت شده را در دو حالت استفاده‌ی از session storage و local storage، می‌توان مشاهده کرد:



              کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
              نظرات مطالب
              آپلود فایل توسط فرم‌های پویای jqGrid
              اینم کد کامل Upload:
                  function doUpload(response, postdata, rowId) {
                      var result = $.parseJSON(response.responseText);
                      if (result.id < 1)
                          return [false, "عملیات ثبت موفقیت آمیز نبود", result.id];
              
                      var fileElementId = 'xThumbnail';
                      if (rowId) {
                          fileElementId = rowId + "_" + fileElementId;
                      }
              
                      var val = $("#" + fileElementId).val();
                      if (val == '' || val === undefined) {
                          return [false, "لطفا فایلی را انتخاب کنید", result.id];
                      }
              
                      alert(fileElementId);
                      $('#grid1').block({ message: '<h4>در حال ارسال فایل به سرور</h4>' });
                      $.ajaxFileUpload({
                          url: "@Url.Action("UploadFile", "ProductTypes", new { area = "Admin" })",
                          secureuri: false,
                          fileElementId: fileElementId, 
                          dataType: 'json',
                          data: { id: result.id }, 
                          complete: function () {
                              $('#grid1').unblock();
                          },
                          success: function (data, status) {
                              $("#grid").trigger("reloadGrid");
                          },
                          error: function (data, status, e) {
                              alert(e);
                          }
                      });
              
                      return [true, "با تشکر!", result.id];
                  }
              مطالب
              مدیریت پیشرفته‌ی حالت در React با Redux و Mobx - قسمت هشتم - تنظیمات پروژه‌های React برای کار با Mobx decorators
              تا اینجا دو مثالی را که از Mobx بررسی کردیم (مثال ورود متن و مثال کامپوننت شمارشگر)، به عمد به همراه decoratorهای @ دار آن نبودند. برای مثال در قسمت قبل، یک کلاس را با یک خاصیت ایجاد کردیم که روش مزین سازی خاصیت value آن را با  observable decorator، توسط متد decorate انجام دادیم و این هم یک روش کار با MobX است؛ بدون اینکه نیاز به تنظیمات خاصی را داشته باشد:
              import { decorate } from "mobx";
              
              class Count {
                value = 0;
              }
              
              decorate(Count, { value: observable });
              const count = new Count();
              اما اگر همان مثال بسیار ساده‌ی ورود متن را بخواهیم توسط decoratorهای @ دار MobX پیاده سازی کنیم ... پروژه‌ی استاندارد React ما کامپایل نخواهد شد که در این قسمت، روش رفع این مشکل را بررسی می‌کنیم.


              بازنویسی مثال ورود متن و نمایش آن با Mobx decorators

              در اینجا یک text-box، به همراه دو div در صفحه رندر خواهند شد که قرار است با ورود اطلاعاتی در text-box، یکی از آن‌ها (text-display) این اطلاعات را به صورت معمولی و دیگری (text-display-uppercase) آن‌را به صورت uppercase نمایش دهد. روش کار انجام شده هم مستقل از React است و به صورت مستقیم با استفاده از DOM API عمل شده‌است. این مثال را پیشتر در اولین قسمت بررسی MobX، ملاحظه کردید. اکنون اگر بخواهیم بجای شیءای که توسط متد observable کتابخانه‌ی MobX محصور شده‌است:
              const text = observable({
                value: "Hello world!",
                get uppercase() {
                  return this.value.toUpperCase();
                }
              });
              از یک کلاس ES6 به همراه Mobx decorators استفاده کنیم، به یک چنین پروژه‌ی جدیدی خواهیم رسید:
              ابتدا یک پروژه‌ی جدید React را ایجاد می‌کنیم:
              > create-react-app state-management-with-mobx-part3
              > cd state-management-with-mobx-part3
              > npm start
              در ادامه کتابخانه‌ی mobx را نیز نصب می‌کنیم. برای این منظور پس از باز کردن پوشه‌ی اصلی برنامه توسط VSCode، دکمه‌های ctrl+` را فشرده (ctrl+back-tick) و دستور زیر را در ترمینال ظاهر شده وارد کنید:
              > npm install --save mobx
              در ادامه، ابتدا فایل public\index.html را جهت نمایش دو div و یک text-box، ویرایش می‌کنیم:
              <!DOCTYPE html>
              <html lang="en">
                <head>
                  <title>MobX Basics, part 3</title>
                  <meta charset="UTF-8" />
                  <link href="src/styles.css" />
                </head>
              
                <body>
                  <main>
                    <input id="text-input" />
                    <p id="text-display"></p>
                    <p id="text-display-uppercase"></p>
                  </main>
              
                  <script src="src/index.js"></script>
                </body>
              </html>
              سپس محتویات فایل src\index.js را نیز به نحو زیر تغییر می‌دهیم:
              import { autorun, computed, observable } from "mobx";
              
              const input = document.getElementById("text-input");
              const textDisplay = document.getElementById("text-display");
              const loudDisplay = document.getElementById("text-display-uppercase");
              
              class Text {
                @observable value = "Hello World";
                @computed get uppercase() {
                  return this.value.toUpperCase();
                }
              }
              
              const text = new Text();
              
              input.addEventListener("keyup", event => {
                text.value = event.target.value;
              });
              
              autorun(() => {
                input.value = text.value;
                textDisplay.textContent = text.value;
                loudDisplay.textContent = text.uppercase;
              });
              تنها تفاوت این نگارش با نگارش قبلی آن، استفاده از کلاس Text که یک کلاس ES6 به همراه MobX Decorators است، بجای یک شیء ساده‌ی جاوا اسکریپتی می‌باشد. در اینجا خاصیت value به صورت observable@ تعریف شده و در نتیجه‌ی تغییر مقدار آن در کدهای برنامه، خاصیت محاسباتی وابسته‌ی به آن یا همان uppercase که با computed@ تزئین شده، به صورت خودکار به روز رسانی خواهد شد. متد autorun نیز به این تغییرات که حاصل فشرده شدن کلیدها هستند، واکنش نشان داده و متن دو div موجود در صفحه را به روز رسانی می‌کند.

              اکنون اگر در همین حال، برنامه را با دستور npm start اجرا کنیم، با خطای زیر متوقف خواهیم شد:
              ./src/index.js
              SyntaxError: \src\index.js: Support for the experimental syntax 'decorators-legacy' isn't currently enabled (8:3):
              
                 6 | 
                 7 | class Text {
              >  8 |   @observable value = "Hello World";
                   |   ^
                 9 |   @computed get uppercase() {
                10 |     return this.value.toUpperCase();
                11 |   }

              راه حل اول: از Decorators استفاده نکنیم!

              یک راه حل مشکل فوق این است که بدون هیچ تغییری در ساختار پروژه‌ی React خود، اصلا از decorator syntax استفاده نکنیم. برای مثال اگر یک کلاس متداول MobX ای چنین شکلی را دارد:
              import { observable, computed, action } from "mobx";
              
              class Timer {
                @observable start = Date.now();
                @observable current = Date.now();
              
                @computed
                get elapsedTime() {
                  return this.current - this.start + "milliseconds";
                }
              
                @action
                tick() {
                  this.current = Date.now();
                }
              }
              می‌توان آن‌را بدون استفاده از decorator syntax، به صورت زیر نیز تعریف کرد:
              import { observable, computed, action, decorate } from "mobx";
              
              class Timer {
                start = Date.now();
                current = Date.now();
              
                get elapsedTime() {
                  return this.current - this.start + "milliseconds";
                }
              
                tick() {
                  this.current = Date.now();
                }
              }
              
              decorate(Timer, {
                start: observable,
                current: observable,
                elapsedTime: computed,
                tick: action
              });
              نمونه‌ی این روش را در قسمت قبل با تعریف شیء شمارشگر مشاهده کرده‌اید. در اینجا با توجه به اینکه Decorators در جاوا اسکریپت چیزی نیستند بجز بیان زیبای higher-order functions و higher-order functions هم توابعی هستند که توابع دیگر را با ارائه‌ی قابلیت‌های بیشتری، محصور می‌کنند، به همین جهت هر کاری را که بتوان با تزئین کننده‌ها انجام داد، همان را با توابع معمولی جاوا اسکریپتی نیز می‌توان انجام داد. اینکار را در مثال فوق توسط متد decorate مشاهده می‌کنید. این متد ابتدا نوع کلاس خاصی را دریافت کرده و سپس در پارامتر دوم آن می‌توان شیءای را تعریف کرد که خواص آن، همان خواص کلاس پارامتر اول است و مقادیر این خواص، تزئین کننده‌هایی هستند که قرار است برای آن‌ها بکار گرفته شوند. مزیت این روش بدون تغییر باقی ماندن تعریف کلاس Timer در اینجا و همچنین انجام هیچگونه تغییری در ساختار پروژه‌ی React، بدون نیاز به نصب بسته‌های کمکی اضافی است.

              همچنین در این حالت بجای استفاده از کامپوننت‌های کلاسی، باید از روش بکارگیری متد observer برای محصور کردن کامپوننت تابعی تعریف شده استفاده کرد (تا دیگر نیازی به ذکر observer class@ نباشد):
              const Counter = observer(({ count }) => {
                return (
                 // ...
                );
              });


              راه حل دوم: از تایپ‌اسکریپت استفاده کنید!

              create-react-app امکان ایجاد پروژه‌های React تایپ‌اسکریپتی را با ذکر سوئیچ typescript نیز دارد:
              > create-react-app my-proj1 --typescript
              پس از ایجاد پروژه، فایل tsconfig.json آن‌را یافته و experimentalDecorators آن‌را به true تنظیم کنید:
              {
                "compilerOptions": {
                  // ...
                  "experimentalDecorators": true
                }
              }
              این تنها تغییری است که مورد نیاز می‌باشد و پس از آن برنامه‌ی React جاری، بدون مشکلی می‌تواند با decorators کار کند.


              فعالسازی MobX Decorators در پروژه‌های استاندارد React مبتنی بر ES6

              MobX از legacy" decorators spec" پشتیبانی می‌کند. یعنی اگر پروژه‌ای از spec جدید استفاده کند، دیگر نخواهد توانست با MobX فعلی کار کند. این هم مشکل MobX نیست. مشکل اینجا است که باید دانست کلا decorators در زبان جاوااسکریپت هنوز در مرحله‌ی آزمایشی قرار دارند و تکلیف spec نهایی و تائید شده‌ی آن مشخص نیست.
              برای فعالسازی decorators در یک پروژه‌ی React استاندارد مبتنی بر ES6، شاید کمی جستجو کنید و به نتایجی مانند افزودن فایل babelrc. به ریشه‌ی پروژه و نصب افزونه‌هایی مانند babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties@ برسید. اما ... این‌ها بدون اجرای دستور npm run eject کار نمی‌کنند و اگر این دستور را اجرا کنیم، در نهایت به یک فایل package.json بسیار شلوغ خواهیم رسید (اینبار ارجاعات به Babel، Webpack و تمام ابزارهای دیگر نیز ظاهر می‌شوند). همچنین این عملیات نیز یک طرفه‌است. یعنی از این پس قرار است کنترل تمام این پشت صحنه، در اختیار ما باشد و به روز رسانی‌های بعدی create-react-app را با مشکل مواجه می‌کند. این گزینه صرفا مختص توسعه دهندگان پیشرفته‌ی React است. به همین جهت نیاز به روشی را داریم تا بتوانیم تنظیمات Webpack و کامپایلر Babel را بدون اجرای دستور npm run eject، تغییر دهیم تا در نتیجه، decorators را در آن فعال کنیم و خوشبختانه پروژه‌ی react-app-rewired دقیقا برای همین منظور طراحی شده‌است.

              بنابراین ابتدا بسته‌های زیر را نصب می‌کنیم:
              > npm i --save-dev customize-cra react-app-rewired
              بسته‌ی react-app-rewired، امکان بازنویسی تنظیمات webpack پروژه‌ی react را بدون eject آن میسر می‌کند. customize-cra نیز با استفاده از امکانات همین بسته، نگارش‌های جدیدتر create-react-app را پشتیبانی می‌کند.

              پس از نصب این پیشنیازها، فایل جدید config-overrides.js را به ریشه‌ی پروژه، جائیکه فایل package.json قرار گرفته‌است، با محتوای زیر اضافه کنید تا پشتیبانی ازlegacy" decorators spec" فعال شوند:
              const {
                override,
                addDecoratorsLegacy,
                disableEsLint
              } = require("customize-cra");
              
              module.exports = override(
                // enable legacy decorators babel plugin
                addDecoratorsLegacy(),
              
                // disable eslint in webpack
                disableEsLint()
              );
              در ادامه فایل package.json را گشوده و قسمت scripts آن‌را برای استفاده‌ی از react-app-rewired، به صورت زیر بازنویسی کنید تا امکان تغییر تنظیمات webpack به صورت پویا در زمان اجرای برنامه، میسر شود:
                "scripts": {
                  "start": "react-app-rewired start",
                  "build": "react-app-rewired build",
                  "test": "react-app-rewired test",
                  "eject": "react-app-rewired eject"
                },
              پس از این تغییرات، نیاز است دستور npm start را یکبار دیگر از ابتدا اجرا کنید. اکنون برنامه بدون مشکل کامپایل شده و خروجی بدون خطایی در مرورگر نمایش داده خواهد شد.


              تنظیمات ESLint مخصوص کار با decorators

              فایل ویژه‌ی eslintrc.json. که در ریشه‌ی پروژه قرار می‌گیرد (این فایل بدون نام است و فقط از پسوند تشکیل شده)، برای پروژه‌های MobX، باید حداقل تنظیم زیر را داشته باشد تا ESLint بتواند legacyDecorators را نیز پردازش کند:
              {
                "extends": "react-app",
                "parserOptions": {
                  "ecmaFeatures": {
                    "legacyDecorators": true
                  }
                }
              }
              و یا یک نمونه‌ی غنی شده‌ی فایل eslintrc.json. مخصوص برنامه‌های React به صورت زیر است:
              {
                  "env": {
                      "node": true,
                      "commonjs": true,
                      "browser": true,
                      "es6": true,
                      "mocha": true
                  },
                  "settings": {
                      "react": {
                          "version": "detect"
                      }
                  },
                  "parserOptions": {
                      "ecmaFeatures": {
                          "jsx": true,
                          "legacyDecorators": true
                      },
                      "ecmaVersion": 2018,
                      "sourceType": "module"
                  },
                  "plugins": [
                      "babel",
                      "react",
                      "react-hooks",
                      "react-redux",
                      "no-async-without-await",
                      "css-modules",
                      "filenames",
                      "simple-import-sort"
                  ],
                  "rules": {
                      "no-const-assign": "warn",
                      "no-this-before-super": "warn",
                      "constructor-super": "warn",
                      "strict": [
                          "error",
                          "safe"
                      ],
                      "no-debugger": "error",
                      "brace-style": [
                          "error",
                          "1tbs",
                          {
                              "allowSingleLine": true
                          }
                      ],
                      "no-trailing-spaces": "error",
                      "keyword-spacing": "error",
                      "space-before-function-paren": [
                          "error",
                          "never"
                      ],
                      "spaced-comment": [
                          "error",
                          "always"
                      ],
                      "vars-on-top": "error",
                      "no-undef": "error",
                      "no-undefined": "warn",
                      "comma-dangle": [
                          "error",
                          "never"
                      ],
                      "quotes": [
                          "error",
                          "double"
                      ],
                      "semi": [
                          "error",
                          "always"
                      ],
                      "guard-for-in": "error",
                      "no-eval": "error",
                      "no-with": "error",
                      "valid-typeof": "error",
                      "no-unused-vars": "error",
                      "no-continue": "warn",
                      "no-extra-semi": "warn",
                      "no-unreachable": "warn",
                      "no-unused-expressions": "warn",
                      "max-len": [
                          "warn",
                          80,
                          4
                      ],
                      "react/prefer-es6-class": "warn",
                      "react/jsx-boolean-value": "warn",
                      "react-hooks/rules-of-hooks": "error",
                      "react-hooks/exhaustive-deps": "warn",
                      "react/prop-types": "off",
                      "react-redux/mapDispatchToProps-returns-object": "off",
                      "react-redux/prefer-separate-component-file": "off",
                      "no-async-without-await/no-async-without-await": "warn",
                      "css-modules/no-undef-class": "off",
                      "filenames/match-regex": [
                          "off",
                          "^[a-zA-Z]+\\.*\\b(typescript|module|locale|validate|test|action|api|reducer|saga)?\\b$",
                          true
                      ],
                      "filenames/match-exported": "off",
                      "filenames/no-index": "off",
                      "simple-import-sort/sort": "error"
                  },
                  "extends": [
                      "react-app",
                      "eslint:recommended",
                      "plugin:react/recommended",
                      "plugin:react-redux/recommended",
                      "plugin:css-modules/recommended"
                  ],
                  "globals": {
                      "Atomics": "readonly",
                      "SharedArrayBuffer": "readonly",
                      "process": true
                  }
              }
              البته برای اینکه این تنظیمات کار کند، باید افزونه‌های زیر را نیز به صورت محلی در ریشه‌ی پروژه‌ی جاری نصب کنید (این مورد از ESLint 6x به بعد اجباری است و از بسته‌های global استفاده نمی‌کند):
              >npm i --save-dev eslint babel-eslint eslint-config-react-app eslint-loader eslint-plugin-babel eslint-plugin-react eslint-plugin-css-modules eslint-plugin-filenames eslint-plugin-flowtype eslint-plugin-import eslint-plugin-no-async-without-await eslint-plugin-react-hooks eslint-plugin-react-redux eslint-plugin-redux-saga eslint-plugin-simple-import-sort eslint-loader typescript
              پس از آن می‌توان فایل config-overrides.js را به صورت زیر نیز بر اساس تنظیمات فوق، بهبود بخشید:
              const {
                override,
                addDecoratorsLegacy,
                useEslintRc
              } = require("customize-cra");
              
              module.exports = override(
                addDecoratorsLegacy(),
                useEslintRc(".eslintrc.json")
              );


              رفع اخطار مرتبط با decorators در VSCode

              تا اینجا کار تنظیم کامپایلر babel، جهت پردازش decorators انجام شد. اما خود VSCode نیز چنین اخطاری را در پروژه‌هایی که از decorates استفاده می‌کنند، نمایش می‌دهد:
              Experimental support for decorators is a feature that is subject to change in a future release.
              Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning.ts(1219)
              برای رفع آن، فایل جدید tsconfig.json را در ریشه‌ی پروژه ایجاد کرده و آن‌را به صورت زیر تکمیل کنید تا ادیتور تایپ‌اسکریپتی VSCode، دیگر خطاهای مرتبط با decorators را نمایش ندهد:
              {
                  "compilerOptions": {
                      "experimentalDecorators": true,
                      "allowJs": true
                  }
              }

              کدهای کامل این قسمت را می‌توانید از اینجا دریافت کنید: state-management-with-mobx-part3.zip
              مطالب
              طراحی گردش کاری با استفاده از State machines - قسمت اول
              State machine چیست؟

              State machine مدلی است بیانگر نحوه واکنش سیستم به وقایع مختلف. یک ماشین حالت وضعیت جاری قسمتی از سیستم را نگهداری کرده و به ورودی‌های مختلف پاسخ می‌دهد. این ورودی‌ها در نهایت وضعیت سیستم را تغییر خواهند داد.
              نحوه پاسخگویی یک ماشین حالت (State machine) را به رویدادی خاص، انتقال (Transition) می‌نامند. در یک انتقال مشخص می‌شود که ماشین حالت بر اساس وضعیت جاری خود، با دریافت یک رویداد، چه عکس العملی را باید بروز دهد. عموما (و نه همیشه) در حین پاسخگویی ماشین حالت به رویدادهای رسیده، وضعیت آن نیز تغییر خواهد کرد. در اینجا گاهی از اوقات پیش از انجام عملیاتی، نیاز است شرطی بررسی شده و سپس انتقالی رخ دهد. به این شرط، guard گفته می‌شود.
              بنابراین به صورت خلاصه، یک ماشین حالت، مدلی است از رفتاری خاص، تشکیل شده از حالات، رویدادها، انتقالات، اعمال (actions) و شرط‌ها (Guards). در اینجا:
              - یک حالت (State)، شرطی منحصربفرد در طول عمر ماشین حالت است. در هر زمان مشخصی، ماشین حالت در یکی از حالات از پیش تعریف شده خود قرار خواهد داشت.
              - یک رویداد (Event)، اتفاقی است که به ماشین حالت اعمال می‌شود؛ یا همان ورودی‌های سیستم.
              - یک انتقال (Transition)، بیانگر نحوه رفتار ماشین حالت جهت پاسخگویی به رویداد وارده بر اساس وضعیت جاری خود می‌باشد. در طی یک انتقال، سیستم از یک حالت به حالتی دیگر منتقل خواهد شد.
              - برای انجام یک انتقال، نیاز است یک شرط (Guard/Conditional Logic) بررسی شده و در صورت true بودن آن، انتقال صورت گیرد.
              - یک عمل (Action)، بیانگر نحوه پاسخگویی ماشین حالت در طول دوره انتقال است.


              چگونه می‌توان الگوی ماشین حالت را تشخیص داد؟

              اکثر برنامه‌های وب، متشکل از پیاده سازی چندین ماشین حالت می‌باشند؛ مانند ثبت نام در سایت، درخواست یک کتاب از کتابخانه، ارسال درخواست‌ها و پاسخگویی به آن‌ها و یا حتی ارسال یک مطلب در سایت، تائید و انتشار آن.
              البته عموما در حین طراحی برنامه‌ها، کمتر به این نوع مسایل به شکل یک ماشین حالت نگاه می‌شود. به همین جهت بهتر است معیارهایی را برای شناخت زود هنگام آن‌ها مدنظر داشته باشیم:
              - آیا در جدول بانک اطلاعاتی خود فیلدهایی مانند State (حالت) یا Status (وضعیت)دارید؟ اگر بله، به این معنا است که در حال کار با یک ماشین حالت هستید.
              - عموما فیلدهای Bit و Boolean، بیانگر حضور ماشین‌های حالت هستند. مانند IsPublished ، IsPaid و یا حتی داشتن یک فیلد timeStamp که می‌تواند NULL بپذیرد نیز بیانگر استفاده از ماشین حالت است؛ مانند فیلدهای published_at، paid_at و یا confirmed_at.
              - داشتن رکوردهایی که تنها در طول یک بازه زمانی خاص، معتبر هستند. برای مثال آبونه شدن در یک سایت در طول یک بازه زمانی مشخص.
              - اعمال چند مرحله‌ای؛ مانند ثبت نام در سایت و دریافت ایمیل فعال سازی. سپس فعال سازی اکانت از طریق ایمیل.


              مثالی ساده از یک ماشین حالت

              یک کلید برق را در نظر بگیرید. این کلید دارای دو حالت (states) روشن و خاموش است. زمانی که خاموش است، با دریافت رخدادی (event)، به وضعیت (state/status) روشن، منتقل خواهد شد (Transition) و برعکس.


              در اینجا حالات با مستطیل‌های گوشه گرد نمایش داده شده‌اند. انتقالات توسط فلش‌هایی انحناء دار که حالات را به یکدیگر متصل می‌کنند، مشخص گردیده‌اند. برچسب‌های هر فلش، مشخص کننده نام رویدادی است که سبب انتقال و تغییر حالت می‌گردد. با شروع یک ماشین حالت، این ماشین در یکی از وضعیت‌های از پیش تعیین شده‌اش قرار خواهد گرفت (initial state)؛ که در اینجا حالت خاموش است.
              این نوع نمودارها می‌توانند شامل جزئیات بیشتری نیز باشند؛ مانند برچسب‌هایی که نمایانگر اعمال قابل انجام در طی یک انتقال هستند.


              رسم ماشین‌های حالت در برنامه‌های وب، به کمک کتابخانه jsPlumb

              کتابخانه‌های زیادی برای رسم فلوچارت، گردش‌های کاری، ماشین‌های حالت و امثال آن جهت برنامه‌های وب وجود دارند و یکی از معروف‌ترین‌های آن‌ها کتابخانه jsPlumb است. این کتابخانه به صورت یک افزونه jQuery طراحی شده است؛ اما به عنوان افزونه‌ای برای کتابخانه‌های MooTools و یا YUI3/Yahoo User Interface 3 نیز قابل استفاده می‌باشد. کتابخانه jsPlumb در مرورگرهای جدید از امکانات ترسیم SVG و یا HTML5 Canvas استفاده می‌کند. برای سازگاری با مرورگرهای قدیمی‌تر مانند IE8 به صورت خودکار به VML سوئیچ خواهد کرد. همچنین این کتابخانه امکانات ترسیم تعاملی قطعات به هم متصل شونده را نیز دارا است (شبیه به طراح یک گردش کاری). البته برای اضافه شدن امکاناتی مانند کشیدن و رها کردن در آن نیاز به jQuery-UI نیز خواهد داشت.
              برای نمونه اگر بخواهیم مثال فوق را توسط jsPlumb ترسیم کنیم، روش کار به صورت زیر خواهد بود:
              <!doctype html>
              <html>
              <head>
                  <title>State Machine Demonstration</title>
                  <style type="text/css">
                      #opened
                      {
                          left: 10em;
                          top: 5em;
                      }
                      
                      #off
                      {
                          left: 12em;
                          top: 15em;
                      }
                      
                      #on
                      {
                          left: 28em;
                          top: 15em;
                      }
                      
                      .w
                      {
                          width: 5em;
                          padding: 1em;
                          position: absolute;
                          border: 1px solid black;
                          z-index: 4;
                          border-radius: 1em;
                          border: 1px solid #346789;
                          box-shadow: 2px 2px 19px #e0e0e0;
                          -o-box-shadow: 2px 2px 19px #e0e0e0;
                          -webkit-box-shadow: 2px 2px 19px #e0e0e0;
                          -moz-box-shadow: 2px 2px 19px #e0e0e0;
                          -moz-border-radius: 0.5em;
                          border-radius: 0.5em;
                          opacity: 0.8;
                          filter: alpha(opacity=80);
                          cursor: move;
                      }
                      
                      .ep
                      {
                          float: right;
                          width: 1em;
                          height: 1em;
                          background-color: #994466;
                          cursor: pointer;
                      }
                      
                      .labelClass
                      {
                          font-size: 20pt;
                      }
                  </style>
                  <script type="text/javascript" src="jquery.min.js"></script>
                  <script type="text/javascript" src="jquery-ui.min.js"></script>
                  <script type="text/javascript" src="jquery.jsPlumb-all-min.js"></script>
                  <script type="text/javascript">
                      $(document).ready(function () {
              
                          jsPlumb.importDefaults({
                              Endpoint: ["Dot", { radius: 5}],
                              HoverPaintStyle: { strokeStyle: "blue", lineWidth: 2 },
                              ConnectionOverlays: [
              ["Arrow", { location: 1, id: "arrow", length: 14, foldback: 0.8}]
              ]
                          });
              
                          jsPlumb.makeTarget($(".w"), {
                              dropOptions: { hoverClass: "dragHover" },
                              anchor: "Continuous"
                          });
              
                          $(".ep").each(function (i, e) {
                              var p = $(e).parent();
                              jsPlumb.makeSource($(e), {
                                  parent: p,
                                  anchor: "Continuous",
                                  connector: ["StateMachine", { curviness: 20}],
                                  connectorStyle: { strokeStyle: '#42a62c', lineWidth: 2 },
                                  maxConnections: 2,
                                  onMaxConnections: function (info, e) {
                                      alert("Maximum connections (" + info.maxConnections + ") reached");
                                  }
                              });
                          });
              
                          jsPlumb.bind("connection", function (info) {
                          });
              
                          jsPlumb.draggable($(".w"));
              
                          jsPlumb.connect({ source: "opened", target: "off" });
                          jsPlumb.connect({ source: "off", target: "on", label: "Turn On" });
                          jsPlumb.connect({ source: "on", target: "off", label: "Turn Off" });
                      });
                  </script>
              </head>
              <body>
                  <div class="w" id="opened">
                      Begin
                      <div class="ep">
                      </div>
                  </div>
                  <div class="w" id="off">
                      Off
                      <div class="ep">
                      </div>
                  </div>
                  <div class="w" id="on">
                      On
                      <div class="ep">
                      </div>
                  </div>
              </body>
              </html>
              مستندات کامل jsPlumb را در سایت آن می‌توان ملاحظه نمود.
              در مثال فوق، ابتدا css و فایل‌های js مورد نیاز ذکر شده‌اند. توسط css، مکان قرارگیری اولیه المان‌های متناظر با حالات، مشخص می‌شوند.
              سپس زمانیکه اشیاء صفحه در دسترس هستند، تنظیمات jsPlumb انجام خواهد شد. برای مثال در اینجا نوع نمایشی Endpoint‌ها به نقطه تنظیم شده است. موارد دیگری مانند مستطیل نیز قابل تنظیم است. سپس نیاز است منبع و مقصدها به کتابخانه jsPlumb معرفی شوند. به کمک متد jsPlumb.makeTarget، تمام المان‌های دارای کلاس w به عنوان منبع و با شمارش divهایی با class=ep، مقصدهای قابل اتصال تعیین شده‌اند (jsPlumb.makeSource). متد jsPlumb.bind یک callback function است و هربار که اتصالی برقرار می‌شود، فراخوانی خواهد شد. متد jsPlumb.draggable تمام عناصر دارای کلاس w را قابل کشیدن و رها کردن می‌کند و در آخر توسط متدهای jsPlumb.connect، مقصد و منبع‌های مشخصی را هم متصل خواهیم کرد. نمونه نهایی تهیه شده برای بررسی بیشتر.


              برای مطالعه بیشتر
              Finite-state machine
              UML state machine
              UML 2 State Machine Diagrams
              مثال‌هایی در این مورد

              نظرات مطالب
              ASP.NET MVC #17
              کلا یک آنتی فورجری توکن برای کل صفحه کافی است. روشی که آقای رضایی کمی بالاتر مطرح کردند (قرار دادن آن در فایل layout) هم جالب است. به این صورت در همه‌جا و در تمام صفحات حضور خواهد داشت؛ آن‌هم فقط یکبار و نه بیشتر. نحوه‌ی خواندن و اضافه کردن آن نیز به صورت زیر خواهد بود:
              function addToken(data) {
                 data.__RequestVerificationToken = $("input[name=__RequestVerificationToken]").val();
                 return data;
              }
               
              $.ajax({
                 // .....
                 data: addToken({ postId: postId }), // اضافه کردن توکن
                 dataType: "html", // نوع داده مهم است
                 // .....
              });
              سه نکته اینجا مهم است:
              - data type حتما باید در این حالت html باشد.
              - در قسمت data متد addToken کار افزودن خودکار آنتی فورجری‌توکن صفحه را انجام می‌دهد (محل آن مهم نیست که داخل فرم باشد یا خارج از آن).
              - سطر contentType نباید ذکر شود.  
              مطالب
              نمایش رکوردها به ترتیب اولویت به کمک jQuery UI sortable در ASP.NET MVC

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

              از توضیحاتی که قبلا  دادم مشخص است که این کار احتمالا در ASP.NET WebForm  کار سختی نیست ولی این کار باید در MVC  از ابتدا طراحی شود.

              طرح سوال : یک سری رکورد از یک Table داریم که می‌خواهیم به ترتیب وارد شدن رکورد‌ها نباشد و  ترتیبی که ما می‌خواهیم نمایش داده شود.

              پاسخ کوتاه : خب باید ابتدا یک فیلد (برای اولویت بندی)  به Table  اضافه کنیم  بعد اون فیلد رو بنا به ترتیبی که دوست داریم رکورد‌ها نمایش داده شود پر کنیم (Sort  می کنیم ) و در آخر هم هنگام نمایش در View رکورد‌ها را بر اساس این فیلد نمایش می‌دهیم.

              (این پست هم در ادامه پست قبلی در همان پروژه است و از همان Table  ها استفاده شده است)

              اضافه کردن فیلد :

              ابتدا یک فیلد به Table  مورد نظر اضافه می‌کنیم. من اسم این فیلد رو Priority گذاشتم. Table  من چنین وضعیتی دارد.

              افزودن فایل‌های jQuery UI :

              در این مرحله شما نیاز دارید فایل‌های مورد نیاز برای Sort  کردن رکورد‌ها را اضافه کنید. شما می‌توانید فقط فایل‌های مربوط به Sortable  را به صفحه خودتان اضافه کنید و یا مثل من فایل هایی که حاوی تمام قسمت‌های jQuery UI  هست را اضافه کنید.

              من برای این کار از Section  استفاده کردم ، ابتدا در Head  فایل Layout  دو Section  تعریف کردم برای CSS  و JavaScript . و فایل‌های مربوط به Sort کردن را در صفحه ای که باید عمل Sort انجام بشود در  این Section ها قرار دادم.

              فایل Layout

              <head>
                  <meta charset="utf-8" />
                  @RenderSection("meta", false)
                  <title>@ViewBag.Title</title>
                  <link href="@Url.Content("~/Content/~Site.css")" rel="stylesheet" type="text/css" />
                  <link href="@Url.Content("~/Content/redactor/css/redactor.css")" rel="stylesheet" type="text/css" />
                  <link href="@Url.Content("~/Content/css/bootstrap-rtl.min.css")" rel="stylesheet" type="text/css" />
                  @RenderSection("css", false)
                  <script src="@Url.Content("~/Scripts/jquery-1.8.2.min.js")" type="text/javascript"></script>
                  <script src="@Url.Content("~/Scripts/modernizr-1.7.min.js")" type="text/javascript"></script>
                  <script src="@Url.Content("~/Content/js/bootstrap-rtl.js")" type="text/javascript"></script>
                  <script src="@Url.Content("~/Content/redactor/redactor.min.js")" type="text/javascript"></script>
                  <script src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script>
                  @RenderSection("js", false)
              
              </head>

              فایل Index.chtml  در پوشه کنترلر Type


              @model IEnumerable<KhazarCo.Models.Type>
              @{
                  ViewBag.Title = "Index";
                  Layout = "~/Areas/Administrator/Views/Shared/_Layout.cshtml";
              }
              @section css
              {<link href="@Url.Content("~/Content/themes/base/jquery-ui.css")" rel="stylesheet" type="text/css" />
              }
              @section js
              {
                  <script src="@Url.Content("~/Scripts/jquery-ui-1.9.0.min.js")" type="text/javascript"></script>
              }


              در آخر فایل Index.chtml  به اینصورت شده است:

              <h2>
                  نوع ها</h2>
              <p>
                  @Html.ActionLink("ایجاد یک مورد جدید", "Create", null, new { @class = "btn btn-info" })
              </p>
              <table>
                  <thead>
                      <tr>
                          <th>
                              عنوان
                          </th>
                          <th>
                              توضیحات
                          </th>
                          <th>
                              فعال
                          </th>
                          <th>
                          </th>
                      </tr>
                  </thead>
                  <tbody>
                      @foreach (var item in Model.OrderBy(m => m.Priority))
                      {
                          <tr id="@item.Id">
                              <td>
                                  @Html.DisplayFor(modelItem => item.Title)
                              </td>
                              <td>
                                  @(new HtmlString(item.Description))
                              </td>
                              <td>
                                  @Html.DisplayFor(modelItem => item.IsActive)
                              </td>
                              <td>
                                  @Html.ActionLink("ویرایش", "Edit", new { id = item.Id }, new { @class = "btnEdit label label-warning" })
                                  |
                                  @Html.ActionLink("مشاهده", "Details", new { id = item.Id }, new { @class = "btnDetails label label-info" })
                                  |
                                  @Html.ActionLink("حذف", "Delete", new { id = item.Id }, new { @class = "btnDelete label label-important" })
                              </td>
                          </tr>
                      }
                  </tbody>
              </table>

              ** توجه داشته باشید که من به هر tr  یک id  اختصاص داده ام که این مقدار id  همان مقدار فیلد Id  همان رکورد هست ، ما برای مرتب کردن به این Id  نیاز داریم (خط 25).

              افزودن کد‌های کلاینت:

              حالا باید کدی بنویسم که دو کار را برای ما انجام دهد : اول حالت Sort  پذیری را به سطر‌های Table  بدهد و دوم اینکه هنگامی که ترتیب سطر‌های تغییر کرد ما را با خبر کند:


              <script type="text/javascript">
                  $(function () {
                      $("table tbody").sortable({
                          helper: fixHelper,
                          update: function (event, ui) {
                              jQuery.ajax('@Url.Action("Sort", "Type", new { area = "Administrator" })', {
                                  data: { s: $(this).sortable('toArray').toString() }
                              });
                          }
                      }).disableSelection();
                  });
                  var fixHelper = function (e, ui) {
                      ui.children().each(function () {
                          $(this).width($(this).width());
                      });
                      return ui;
                  };
              </script>


              توضیح کد :

              در این کد ما حالت ترتیب پذیری را به Table  می دهیم و هنگامی که عمل Update  در Table  انجام شد تابع مربوطه اجرا می‌شود. ما در این تایع، ترتیب جدید سطر‌ها را می‌گیریم ( ** به کمک مقدار Id  که به هر سطر دادیم ، این مقدار Id  برابر بود با Id خود رکورد در Database )  و به کمکjQuery.ajax  به تابع Sort  از کنترلر Type  در منطقه (area ) Administrator  ارسال می‌کنیم و در آنجا ادامه کار را انجام میدهیم.

              تابع fixHelper  هم به ما کمک می‌کند که هنگامی که سطر‌ها از جای خود جدا می‌شوند ، دارای عرض یکسانی باشند و عرض آن‌ها تغییری نکند.


              افزودن کد Server:

              حالا باید تابع Sort  که مقادیر را به آن ارسال کردیم بنویسم. من این تابع را بر اساس مقداری که از کلاینت ارسال می‌شود اینگونه طراحی کردم.


                      public EmptyResult Sort(string s)
                      {
                          if (s != null)
                          {
                              var ids = new List<int>();
                              foreach (var item in s.Split(','))
                              {
                                  ids.Add(int.Parse(item));
                              }
                              int intpriority = 0;
              
                              foreach (var item in ids)
                              {
                                  intpriority++;
                                  db.Types.Single(m => m.Id == item).Priority = intpriority;
                              }
                              db.SaveChanges();
                          }
                          return new EmptyResult();
                      }

              در ایتدا مقادیر Id  که از کلاینت  به صورت String  ارسال شده است را می‌گیریم و بعد به همان ترتیب ارسال در لیستی از int قرار می‌دهیم ids.

              سپس به اضای هر رکورد Type  مقدار اولویت را به فیلدی که برای همین مورد اضافه کردیم Priority اختصاص می‌دهیم. و در آخر هم تغییرات را ذخیره می‌کنیم. (خود کد کاملا واضح است و نیاری به توضیح بیشتر نیست )

              حالا باید هنگامی که لیست Type  ها نمایش داده می‌شود به ترتیب (OrderBy) فیلد Priority    نمایش داده شود پس تابع Index را اینطور تغییر می‌دهیم.

                      public ViewResult Index()
                      {
                          return View(db.Types.Where(m => m.IsDeleted == false).OrderBy(m => m.Priority));
                      }

              این هم خروجی کار من:

              این عکس مربوط به است به قسمت مدیریت پروژه شیرآلات مرجان خزر