Angular 2 + 折騰記 :(8) 動手寫一個不怎麼靠譜的上傳元件

CRPER發表於2019-03-01

前言

上傳功能在任何一個網站中的地位都是舉足輕重的,這篇文章主要扯下如何實現一個上傳元件


效果圖


所具有的功能

  1. 支援的圖片格式(不傳參則使用預設引數)
  2. 支援的圖片大小
  3. 圖片上傳之前會被壓縮(前端) — 非同步載入進來
  4. 上傳過程會顯示loading(loading元件)–就一些css3樣式
  5. 支援元件高度設定,寬度自適應
  6. 支援標題設定

元件以模組形式匯出

程式碼如下,相關邏輯請看註釋。

  • mit-upload.module.ts — 上傳模組
// 這三個就不用再解釋了
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from `@angular/core`;
import { CommonModule } from `@angular/common`;
import { FormsModule } from `@angular/forms`;


// 服務
import { MitImageUploadLoaderService } from `./services/mit-image-upload.loader.service`; // 非同步載入JS
import { MitImageUploadService } from `./services/mit-image-upload.service`;
const service = [
  MitImageUploadLoaderService,
  MitImageUploadService
];

// 頁面
import { MitUploadComponent } from `./mit-upload.component`;


// 元件 -- loading
import { MitLoadingModule } from `../mit-loading/mit-loading.module`;

const component = [
  MitUploadComponent
];

@NgModule({
  imports: [
    FormsModule,
    CommonModule,
    MitLoadingModule
  ],
  declarations: [
    ...component
  ],
  exports: [
    ...component
  ],
  providers: [
    ...service
  ],
  schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class MitUploadModule { }複製程式碼
  • mit-upload.component.html — 元件的html結構
<div class="image-upload" [style.height]="height + `px`" [ngClass]="{`upload-fail`: uploadStatus}">
  <div class="upload-area">
    <input type="file" class="upload-input" (change)="selected($event)">
    <div class="icon-wrap">
      <i class="fpd fpd-upload"></i>
    </div>
    <p class="upload-tips">{{uploadDesrc}}</p>
    <div class="img-preview" *ngIf="preview">
      <img [src]="preview" class="res-img" alt="" onError="this.src=`assets/images/default_img/img_error_load.png`;">
    </div>
    <app-mit-loading [option]="`load1`" style="position:absolute;top:0;left:0;width:100%;height:100%;background: rgba(72, 72, 72, 0.65);"
      *ngIf="loadingStatus"></app-mit-loading>
  </div>
  <div class="upload-footer" *ngIf="uploadTitleName">
    <span>{{uploadTitleName}}</span><a href="javascript:;" (click)="delete($event)" *ngIf="preview">刪除</a>
  </div>
</div>複製程式碼

  • mit-upload.component.scss — 元件樣式(scss)

@charset `UTF-8`;
// 自定義的一些mixin什麼的。。
@import `../../../assets/scss_styles/custom_scss/_custom-export.scss`;
$iu-border:#e7e7e7 !default; // 邊框顏色
$iu-icon-color:#d5d4d4 !default;
.upload-fail {
  .upload-area {
    border: 2px solid #d9534f !important;
    .icon-wrap {
      i {
        color: #d9534f !important;
      }
    }
    .upload-tips {
      color: #d9534f;
    }
  }
}

.image-upload {
  width: 100%;
  height: 100%;
  position: relative;
  .upload-area {
    position: relative;
    width: 100%;
    height: 100%;
    border: 2px solid $iu-border;
    @include flex(center, center); // flex的mixin,就是水平垂直居中
    flex-wrap: wrap;
    .icon-wrap {
      width: 100%;
      @include flex(center, center);
      i {
        font-size: 64px;
        color: $iu-icon-color;
      }
    }
    p {
      margin: 0;
      padding: 0.5rem;
      font-size: 14px;
      color: #808080;
    }
    .upload-input {
      cursor:pointer;
      position: absolute;
      left: 0;
      top: 0;
      opacity: 0;
      height: 100%;
      width: 100%;
      background: transparent;
      z-index: $zindex-xs;
    }
    .img-preview {
      position: absolute;
      left: 0;
      top: 0;
      width: 100%;
      height: 100%;
      z-index: $zindex-md;
      img {
        width: 100%;
        height: 100%;
      }
    }
  }
  .upload-footer {
    padding: 0.5rem;
    @include flex(space-between, center);  // 兩邊對齊,垂直劇中
    span {
      font-size: 14px;
      color: #313131;
    }
    a {
      font-size: 14px;
      color: #37c2dd;
    }
  }
}

.res-img {
  width: 100%;
}複製程式碼

  • mit-image-upload.loader.service.ts — 非同步載入前端圖片壓縮的指令碼
    用到的是一個github上庫:localResizeIMG;
    我這裡下載了放在cdn上。。
import { Injectable } from `@angular/core`;

@Injectable()
export class MitImageUploadLoaderService {

  constructor() { }

  // 使用promise非同步獲取回撥後動態插入該指令碼
  load(): Promise<any> {
    const LRZ_URL = `http://xxxxxxx.bkt.clouddn.com/lrz.all.bundle.js`;// xxxx是我隨意打,我放在七牛上了
    const p = new Promise((resolve, reject) => {
        const script = document.createElement(`script`);
        script.type = `text/javascript`;
        script.setAttribute(`src`, LRZ_URL);
        script.onload = resolve;
        script.async = true;
        document.head.appendChild(script);
    });
    return p;
  }

}複製程式碼

  • mit-image-upload.service.ts — 處理圖片上傳(介面)
import { Injectable } from `@angular/core`; // 核心庫-注入服務
import { AuthService } from `../../../services/auth.service`;  // 鑑權
import { environment } from `../../../../environments/environment`; // 環境變數

// 圖片上傳介面
import { IMitImageUpload } from `../interface/mit-image-upload.model`;





@Injectable()
export class MitImageUploadService {

  constructor(private authHttp: AuthService) { }


  resize(e): Promise<any> {
    const lrz = (<any>window)[`lrz`];
    const p = new Promise((resolve, reject) => {
      lrz(e)
        .then(function (rst) {
          resolve(rst);
        })
        .catch(function (err) {
          // 處理失敗會執行
          reject(err);
        })
        .always(function () {
          // 不管是成功失敗,都會執行
        });
    });

    return p;
  }

  uploadImg(iMitImageUploadParam: IMitImageUpload) {
    // authHttp已經做了一些鑑權的封裝(對內建的http模組)
    return this.authHttp.upload(environment.baseUrl + `FileUpload/ImgUpload`, iMitImageUploadParam);
  }


}複製程式碼

  • environment.ts — 存放限制規格引數的。。

我們這裡是考慮environment這個來存放各種配置相關的資訊,所以就獨立出來了,正常邏輯是封裝到元件內的。

// 圖片上傳引數
export const uploadImgParam = {
  `fileType`: [`image/png`, `image/jpeg`, `image/jpg`],  // 圖片上傳格式
  `fileSize`: 3, // 圖片上傳大小限制(MB)
};複製程式碼
  • mit-upload.component.ts — 上傳邏輯的實現

import { Component, OnInit, Input, Output, EventEmitter, ElementRef, HostListener, AfterViewInit } from `@angular/core`;
import { MitImageUploadLoaderService } from `./services/mit-image-upload.loader.service`;
import { MitImageUploadService } from `./services/mit-image-upload.service`;

import { uploadImgParam } from `../../../environments/environment`;  // 上傳配置

@Component({
  selector: `app-mit-upload`,
  templateUrl: `./mit-upload.component.html`,
  styleUrls: [`./mit-upload.component.scss`]
})
export class MitUploadComponent implements OnInit, AfterViewInit {
  @Input() uploadTitleName: string; // 上傳標題字
  @Input() height: any; // 定製高度
  @Input() valiScope?: any; // 上傳限制條件
  @Input() uploadType: string; // 限定上型別
  @Output() uploadEvt = new EventEmitter();
  @Input() preview: any; // 圖片預覽
  public uploadDesrc = `請點選上傳`; // 點選上傳文字替換
  public loadingStatus = false; // loading

  public uploadStatus = false; // 上傳狀態樣式高亮

  constructor(
    private loader: MitImageUploadLoaderService,
    private mitImageUploadService: MitImageUploadService
  ) { }
  ngOnInit() {
    // console.log( this.uploadTitleName );
  }

  ngAfterViewInit() {
    const lrz = (<any>window)[`lrz`];

    if (!lrz) {
      this.loader.load().then(() => {
        // console.log( `lrz非同步載入成功` );
        // console.log(( <any>window )[ `lrz` ] );
      }).catch(() => { });
    } else {
      // console.log( `lrz無需非同步載入` );
    }
  }

  onerror(e) {
    // console.log( e );
  }

  // 選擇檔案
  selected(e) {
    this.fileValidator(e.target.files[0], this.uploadType);
  }



  // 上傳格式限制判斷
  fileValidator(e, uploadType?) {
    // console.log( e );
    // [valiScope]="{`imageType`:[`image/png`,`image/jpeg`,`image/jpg`],`imageSize`:3}"
    const scope = {
      type: [],
      size: 3
    };
    const filename = e.name;
    const filetype = e.type;
    const filesize = e.size;


    if (this.valiScope) {
      scope.type = this.valiScope.Type; // 限制的檔案格式
      scope.size = this.valiScope.Size * 1024 * 1024; // 限制的大小
    } else {
      scope.type = uploadImgParam.fileType;
      scope.size = uploadImgParam.fileSize * 1024 * 1024;
    }
    // console.log( scope.type );


    if (e && scope.type.indexOf(filetype) === -1) {
      this.uploadDesrc = `圖片格式不匹配`;
      this.uploadStatus = true;
    } else if (e && scope.size < filesize) {
      this.uploadDesrc = `圖片大小不匹配`;
      this.uploadStatus = true;
    } else {
      this.uploadStatus = false;
      this.loadingStatus = true;
      this.resize(e);
    }
    return null;
  }





  // reize
  resize(e) {
    if (e) {
      this.mitImageUploadService.resize(e).then((res) => {
        this.uploadIMG(res.formData); // 呼叫上傳介面
      });
    }
  }

  // 上傳圖片
  uploadIMG(data) {
    this.mitImageUploadService.uploadImg(data).subscribe((res) => {
      console.log(res);
      this.loadingStatus = false;
      if (res.State) {
        this.preview = res.Data;  // 回撥成功後渲染圖片
        this.uploadEvt.emit(res.Data);
      } else {
        this.uploadStatus = true;
        this.uploadDesrc = res.Message;
      }

    }, (error) => {
      this.loadingStatus = false;
      this.uploadStatus = true;
      this.uploadDesrc = `上傳失敗請重試`;
    });
  }


  // 刪除圖片預覽
  delete(e) {
    // console.log(e);
    this.preview = null;
  }

}複製程式碼

總結

  1. 我們的專案一些庫都使用cdn來存放一些靜態檔案,不僅有利於減少打包體積,也有利於載入速度!!

至此,一個不怎麼靠譜的上傳元件誕生了,你可以在這個基礎二次定製;
有更好的寫法或者建議的也可以留言指出,謝謝

相關文章