آپلود فایل‌ها در یک برنامه‌ی Angular به کمک کامپوننت ng2-file-upload
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: ده دقیقه

در مطلب «بررسی روش آپلود فایل‌ها از طریق یک برنامه‌ی Angular به یک برنامه‌ی ASP.NET Core» روش عمومی آپلود فایل‌ها را بررسی کردیم. آن مطلب وابستگی به کامپوننت خاصی ندارد و عمومی است. در مطلب جاری می‌خواهیم روش دیگری را مبتنی بر کامپوننت ng2-file-upload بررسی کنیم که به همراه نمایش درصد پیشرفت ارسال فایل‌ها، امکان انتخاب بهتر نوع فایل‌های آپلودی و همچنین امکان مشاهده‌ی لیست کامل فایل‌های انتخاب شده و امکان حذف مواردی از آن، پیش از ارسال نهایی است.



پیشنیازهای کار با کامپوننت ng2-file-upload

برای شروع به کار با کامپوننت ng2-file-upload، ابتدا نیاز است بسته‌ی npm آن‌را نصب کرد:
 >npm install ng2-file-upload --save

همچنین یک کامپوننت آزمایشی را هم به برنامه (دقیقا همان مثال مطلب قبلی) جهت اعمال آن اضافه می‌کنیم:
 >ng g c UploadFile/ng2-file-upload-test

پس از آن نیاز است به ماژولی که این کامپوننت جدید در آن قرار دارد، مدخل FileUploadModule کامپوننت ng2-file-upload را افزود:
import { FileUploadModule } from "ng2-file-upload";

@NgModule({
  imports: [
    FileUploadModule
  ]
در غیراینصورت خطای شناخته نشدن خاصیت uploader را در حین اعمال این کامپوننت مشاهده خواهید کرد.


تکمیل Ng2FileUploadTestComponent جهت اعمال ng2-file-upload

اکنون به کلاس کامپوننت جدیدی که ایجاد کردیم، مراجعه کرده و تغییرات ذیل را اعمال می‌کنیم:
import { FileUploader, FileUploaderOptions } from "ng2-file-upload";
import { Ticket } from "./../ticket";

export class Ng2FileUploadTestComponent implements OnInit {
  fileUploader: FileUploader;
  model = new Ticket();
در اینجا یک خاصیت عمومی از نوع FileUploader تعریف شده‌است که در اختیار قالب این کامپوننت قرار خواهد گرفت. همچنین شیء مدل فرم نیز همانند مطلب «بررسی روش آپلود فایل‌ها از طریق یک برنامه‌ی Angular به یک برنامه‌ی ASP.NET Core» تهیه شده‌است. هدف این است که بررسی کنیم علاوه بر ارسال فایل‌ها، چگونه می‌توان اطلاعات یک فرم را نیز به سمت سرور ارسال کرد.


وهله سازی از کامپوننت ng2-file-upload و انجام تنظیمات اولیه‌ی آن

پس از تعریف خاصیت عمومی fileUploader، اکنون نوبت به وهله سازی آن است:
    this.fileUploader = new FileUploader(
      <FileUploaderOptions>{
        url: "api/SimpleUpload/SaveTicket",
        headers: [
          { name: "X-XSRF-TOKEN", value: this.getCookie("XSRF-TOKEN") },
          { name: "Accept", value: "application/json" }
        ],
        isHTML5: true,
        // allowedMimeType: ["image/jpeg", "image/png", "application/pdf", "application/msword", "application/zip"]
        allowedFileType: [
          "application",
          "image",
          "video",
          "audio",
          "pdf",
          "compress",
          "doc",
          "xls",
          "ppt"
        ],
        removeAfterUpload: true,
        autoUpload: false,
        maxFileSize: 10 * 1024 * 1024
      }
    );
- در اینجا url، مسیر اکشن متدی را در سمت سرور مشخص می‌کند که قرار است فایل‌های ارسالی را دریافت و ذخیره کند.
- اگر برنامه از نکات anti-forgery token استفاده می‌کند، این کامپوننت برخلاف روش مطرح شده‌ی در مطلب مشابه قبلی، هیچ هدری را به سمت سرور ارسال نمی‌کند. بنابراین نیاز است کوکی مرتبط را خودمان یافته و سپس به لیست هدرها اضافه کنیم. در اینجا روش استخراج یک کوکی را توسط کدهای جاوا اسکریپتی مشاهده می‌کنید:
  getCookie(name: string): string {
    const value = "; " + document.cookie;
    const parts = value.split("; " + name + "=");
    if (parts.length === 2) {
      return decodeURIComponent(parts.pop().split(";").shift());
    }
  }

- برای محدود سازی فایل‌های ارسالی توسط این کامپوننت، دو روش وجود دارد:
الف) مشخص سازی مقدار خاصیت allowedMimeType
همانطور که مشاهده می‌کنید، در اینجا باید mime type فایل‌های مجاز را مشخص کرد.
ب) مشخص سازی مقدار خاصیت allowedFileType
برخلاف تصور، در اینجا از پسوند فایل‌ها استفاده نمی‌کند و از یک لیست از پیش مشخص که نمونه‌ای از آن‌را در اینجا مشاهده می‌کنید، کمک گرفته می‌شود. بنابراین اگر برای مثال تنها نیاز به ارسال تصاویر بود، مقدار image را نگه داشته و مابقی را از لیست حذف کنید.

- removeAfterUpload به این معنا است که آیا لیست نهایی که نمایش داده می‌شود، پس از آپلود باقی بماند یا خیر؟
- توسط خاصیت maxFileSize می‌توان حداکثر اندازه‌ی قابل قبول فایل‌های ارسالی را مشخص کرد.


مدیریت رخ‌دادهای کامپوننت ng2-file-upload

اکنون که وهله‌ای از این کامپوننت ساخته شده‌است، می‌توان رخ‌دادهای آن‌را نیز مدیریت کرد. برای مثال:
الف) نحوه‌ی ارسال اطلاعات اضافی به همراه یک فایل به سمت سرور
    this.fileUploader.onBuildItemForm = (fileItem, form) => {
      for (const key in this.model) {
        if (this.model.hasOwnProperty(key)) {
          form.append(key, this.model[key]);
        }
      }
    };
در اینجا شبیه به مطلب مشابه قبلی، مقادیر خواص شیء مدل، به صورت خودکار استخراج شده و به خاصیت form این کامپوننت که درحقیقت همان FormData ارسالی به سمت سرور است، اضافه می‌شوند.

ب) اطلاع یافتن از رخ‌داد خاتمه‌ی کار
رخ‌داد onCompleteAll پس از ارسال تمام فایل‌ها به سمت سرور فراخوانی می‌شود:
    this.fileUploader.onCompleteAll = () => {
      // clear the form
      // this.model = new Ticket();
    };

ج) در حین وهله سازی fileUploader، تعدادی محدودیت نیز قابل اعمال هستند. این محدودیت‌ها سبب نمایش هیچگونه پیام خطایی نمی‌شوند. فقط زمانیکه کاربر فایلی را انتخاب می‌کند، این فایل در لیست ظاهر نمی‌شود. اگر علاقمند به مدیریت این وضعیت باشید، می‌توان از رخ‌داد onWhenAddingFileFailed استفاده کرد:
    this.fileUploader.onWhenAddingFileFailed = (item, filter, options) => {
      // msg: `You can't select ${item.name} file because of the ${filter.name} filter.`
    };

د) اگر ارسال فایلی به سمت سرور با شکست مواجه شود، در ر‌خ‌دادگردان onErrorItem می‌توان به نام این فایل و اطلاعات بیشتری که از سمت سرور دریافت شده‌است، دسترسی یافت:
    this.fileUploader.onErrorItem = (fileItem, response, status, headers) => {
       //
    };

ه) اگر از سمت سرور اطلاعات JSON مانندی یا هر اطلاعات دیگری به سمت کلاینت پس از آپلود ارسال می‌شود، این اطلاعات را می‌توان در رخ‌دادگردان onSuccessItem دریافت کرد:
    this.fileUploader.onSuccessItem = (item, response, status, headers) => {
      if (response) {
        const ticket = JSON.parse(response);
        console.log(`ticket:`, ticket);
      }
    };


ارسال نهایی فرم و  فایل‌ها به سمت سرور

در پایان، با فراخوانی متد uploadAll شیء fileUploader جاری، می‌توان اطلاعات فرم و تمام فایل‌های آن‌را به سمت سرور ارسال کرد:
  submitForm(form: NgForm) {
    this.fileUploader.uploadAll();

    // NOTE: Upload multiple files in one request -> https://github.com/valor-software/ng2-file-upload/issues/671
  }
فقط باید دقت داشت که این کامپوننت هر فایل را جداگانه به سمت سرور ارسال می‌کند و برخلاف روش مطلب قبلی، همه را یکجا و در طی یک درخواست به سمت سرور ارسال نمی‌کند. اما کدهای سمت سرور آن با مطلب مشابه قبلی دقیقا یکی است و تفاوتی نمی‌کند (همان نکات قسمت «دریافت فرم درخواست پشتیبانی در سمت سرور و ذخیره‌ی فایل‌های آن‌» مطلب قبلی نیز در اینجا صادق است).


کدهای کامل کامپوننت ng2-file-upload-test.component.ts را در اینجا می‌توانید مشاهده کنید.


تکمیل قالب کامپوننت Ng2FileUploadTestComponent

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

افزودن فیلد اضافی توضیحات به فرم

<div class="container">
  <h3>Support Form(ng2-file-upload)</h3>
  <form #form="ngForm" (submit)="submitForm(form)" novalidate>
    <div class="form-group" [class.has-error]="description.invalid && description.touched">
      <label class="control-label">Description</label>
      <input #description="ngModel" required type="text" class="form-control"
        name="description" [(ngModel)]="model.description">
      <div *ngIf="description.invalid && description.touched">
        <div class="alert alert-danger"  *ngIf="description.errors.required">
          description is required.
        </div>
      </div>
    </div>
هدف از این فیلد این است که شیء Ticket را وهله سازی و مقدار دهی کند. از مقدار آن در رخ‌دادگردان onBuildItemForm که پیشتر توضیح داده شد، استفاده می‌شود.

تعریف ویژه‌ی فیلد ارسال فایل‌ها به سمت سرور

    <div class="form-group">
      <label class="control-label">Screenshot(s)</label>
      <input required type="file" multiple ng2FileSelect [uploader]="fileUploader"
        class="form-control" name="screenshot">
    </div>
در اینجا ابتدا دایرکتیو ng2FileSelect ذکر می‌شود تا کامپوننت مرتبط فعالسازی شود. سپس خاصیت uploader این دایرکتیو توسط خاصیت fileUploader که پیشتر در کامپوننت، وهله سازی و تنظیم شد، مقدار دهی می‌شود.
ذکر ویژگی استاندارد multiple را نیز در اینجا مشاهده می‌کنید. وجود آن سبب خواهد شد تا کاربر بتواند چندین فایل را با هم انتخاب کند. اگر نیازی به ارسال چندین فایل نیست، این ویژگی را حذف کنید.

نمایش لیست فایل‌ها و نمایش درصد پیشرفت آپلود آن‌ها

جدولی را که در تصویر ابتدای بحث مشاهده کردید، به صورت ذیل شکل می‌گیرد (کدهای آن در همان صفحه‌ی توضیحات کامپوننت نیز موجود هستند):
    <div style="margin-bottom: 10px" *ngIf="fileUploader.queue.length">
      <h3>Upload queue</h3>
      <p>Queue length: {{ fileUploader?.queue?.length }}</p>
      <table class="table">
        <thead>
          <tr>
            <th width="50%">Name</th>
            <th>Size</th>
            <th>Progress</th>
            <th>Status</th>
            <th>Actions</th>
          </tr>
        </thead>
        <tbody>
          <tr *ngFor="let item of fileUploader.queue">
            <td><strong>{{ item?.file?.name }}</strong></td>
            <td nowrap>{{ item?.file?.size/1024/1024 | number:'.2' }} MB</td>
            <td>
              <div class="progress" style="margin-bottom: 0;">
                <div class="progress-bar" role="progressbar" [ngStyle]="{ 'width': item.progress + '%' }"></div>
              </div>
            </td>
            <td class="text-center">
              <span *ngIf="item.isError"><i class="glyphicon glyphicon-remove"></i></span>
            </td>
            <td nowrap>
              <button type="button" class="btn btn-danger btn-xs" (click)="item.remove()">
                <span class="glyphicon glyphicon-trash"></span> Remove
              </button>
            </td>
          </tr>
        </tbody>
      </table>

      <div>
        <div>
          Queue progress:
          <div class="progress">
            <div class="progress-bar" role="progressbar" [ngStyle]="{ 'width': fileUploader.progress + '%' }"></div>
          </div>
        </div>
        <button type="button" class="btn btn-danger btn-s" (click)="fileUploader.clearQueue()"
          [disabled]="!fileUploader.queue.length">
          <span class="glyphicon glyphicon-trash"></span> Remove all
        </button>
      </div>
    </div>
شیء fileUploader وهله سازی شده‌ی در کامپوننت این قالب، دارای خاصیت queue است. در این خاصیت، لیست فایل‌های انتخابی توسط کاربر درج می‌شوند. برای مثال مقدار fileUploader?.queue?.length مساوی تعداد فایل‌های انتخابی توسط کاربر است. بنابراین می‌توان حلقه‌ای را بر روی آن تشکیل داد و مشخصات این فایل‌ها را در صفحه نمایش داد. همچنین هر آیتم آن دارای متد remove نیز هست. کار این متد، حذف این آیتم از لیست queue است و یا اگر متد fileUploader.clearQueue فراخوانی شود، تمام آیتم‌های این لیست را حذف می‌کند.
در اینجا از progress-bar بوت استرپ برای نمایش درصد آپلود فایل‌ها استفاده شده‌است:
              <div class="progress" style="margin-bottom: 0;">
                <div class="progress-bar" role="progressbar" [ngStyle]="{ 'width': item.progress + '%' }"></div>
              </div>
این کامپوننت ارسال فایل، خاصیت item.progress هر فایل موجود در queue را مدام به روز رسانی می‌کند. به همین جهت می‌توان از آن جهت تغییر عرض پیشرفت progress-bar بوت استرپ استفاده کرد.

غیرفعال کردن دکمه‌ی ارسال، در صورت عدم انتخاب یک فایل

    <button class="btn btn-primary" [disabled]="form.invalid || !fileUploader.queue.length"
      type="submit">Submit</button>
  </form>
اگر بخواهیم انتخاب حداقل یک فایل را توسط کاربر اجباری کنیم، می‌توان خاصیت disabled دکمه‌ی ارسال را به طول صف یا fileUploader.queue.length نیز متصل کرد.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
  • #
    ‫۶ سال و ۱۰ ماه قبل، چهارشنبه ۲۴ آبان ۱۳۹۶، ساعت ۱۹:۴۹
    از آنجاکه ajax از طریق خود کامپوننت ارسال می‌شود ، چگونه می‌توان به response سرور دسترسی پیدا کرد ؟
    • #
      ‫۶ سال و ۱۰ ماه قبل، چهارشنبه ۲۴ آبان ۱۳۹۶، ساعت ۲۲:۴۳
      در قسمت this.fileUploader.onSuccessItem این مورد بررسی شده‌است.
  • #
    ‫۶ سال و ۱۰ ماه قبل، سه‌شنبه ۳۰ آبان ۱۳۹۶، ساعت ۱۴:۴۷
    سلام؛ متاسفانه داخل یک فرم اگر فایل انتخاب نشود هیچ خطایی ارسال نمیشود و اگر نیازی به آپلود فایل نباشد (ویرایش فرم) مجدد به مشکل میخوریم.
    • #
      ‫۶ سال و ۱۰ ماه قبل، سه‌شنبه ۳۰ آبان ۱۳۹۶، ساعت ۱۴:۵۸
      پیش از submit خاصیت this.fileUploader?.queue?.length را بررسی کنید. اگر مقداری داشت یعنی فایلی انتخاب شده‌است و اگر خیر، پیامی را نمایش دهید.
  • #
    ‫۶ سال و ۳ ماه قبل، چهارشنبه ۹ خرداد ۱۳۹۷، ساعت ۱۳:۴۷
    با سلام و احترام
    متاسفانه این کامپوننت در سیستم‌های مختلف کار نمیکند!
    در یک سیستم با مرورگر کروم ورژن 66 (بروزترین ورژن) کار میکند و در سیستمی دیگر با همین مرورگر کار نمیکند!
    هیچ اروری هم داخل console نمایش نمیدهد و کلا فایل را انتخاب نمیکند.
  • #
    ‫۴ سال و ۲ ماه قبل، یکشنبه ۲۵ خرداد ۱۳۹۹، ساعت ۱۵:۴۰
    با سلام؛ بعد از پیاده سازی همین موضوع خطای Incorrect Content-Type دریافت می‌کنم در کنسول بخش Network در  ResponsePayload
    و این خود خطا هستش :
    System.InvalidOperationException: Incorrect Content-Type: image/jpeg
    • #
      ‫۴ سال و ۲ ماه قبل، یکشنبه ۲۵ خرداد ۱۳۹۹، ساعت ۱۸:۵۸
      این خطا یعنی فایل ارسالی قرار است به پارامتر دیگری که از جنس فایل نیست، انتساب داده شود؛ مانند id در اینجا:
      public async Task<IActionResult> UploadImage(IFormFile file, Guid id)
      برای رفع این مشکل، تمام این پارامترها را تبدیل به یک ViewModel کنید و کل ViewModel را به صورت پارامتر اکشن متد استفاده کنید:
      public class UploadImageViewModel
      {
          public IFormFile file {get;set;}
          public Guid id {get;set;}
      }
      
      public async Task<IActionResult> UploadImage(UploadImageViewModel model)
      {