发布于 2026-01-06 2 阅读
0

在我们的 Angular 应用中使用 Firebase Storage 实现文件上传 🔥:KittyGramAuth 的简单方法

在我们的 Angular 应用中使用 Firebase Storage 实现文件上传 🔥:简单易用

KittyGramAuth

前言:这是我和我亲爱的朋友 Siddharth(他也是 Angular 和 Web 技术方面的 GDE)共同创建KittyGram的系列文章中的第二篇:KittyGram 是一个超级简约的 Instagram 克隆版,只允许上传猫咪 🐱 照片。

有关项目概况以及我们目前已实施的内容,请参阅我们的第一篇文章:

在本文中,我们将介绍如何使用Firebase StorageAngular 中的响应式表单将文件上传到 Firebase Storage Bucket。

如果您对 Angular、Angular Material 和 Firebase 有基本的了解,那么阅读本文将获得最佳的学习体验。

如果你已经接触过 Angular 开发和 Angular Material,并且想了解更多相关内容,那么这篇文章绝对适合你。🙂

我还添加了文章摘要(TL;DR),如果您想直接跳转到文章的特定部分,可以点击查看🐾

总结:

太好了!我们接下来就开始实现上传可爱猫咪图片的功能吧。

使用 ReactiveFormsModule 😼

由于我们之前已经设置了Angular 应用程序,我们也已经创建CreateComponent并添加了所属/create路由以启用导航。

但是,我们该如何上传一张可爱的猫咪图片,并配上超级可爱的描述呢?我们可能还需要对上传的文件进行适当的验证,以确保文件格式确实是图片。

听起来我们需要考虑的事情很多,但让我们一步一步来。

我们先来创建整个用户界面CreateComponent,让它看起来像这样:

替代文字

AppMaterialModule向我们的💄添加所需的 AngularMaterialModules

由于我们将使用输入表单、小型进度条,并将所有内容封装在一个漂亮的显示卡片中,因此我们需要在我们的项目中导入以下 AngularMaterialModules AppMaterialModule

...
import { MatCardModule } from '@angular/material/card';
import { MaterialFileInputModule } from 'ngx-material-file-input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatProgressBarModule } from '@angular/material/progress-bar';
...

@NgModule({
  exports: [
    ...
    MatCardModule,
    MaterialFileInputModule,
    MatFormFieldModule,
    MatInputModule,
    MatProgressBarModule,
    ...
  ],
})
export class AppMaterialModule {}
Enter fullscreen mode Exit fullscreen mode

重要提示:MaterialFileInputModule您可能已经注意到,我们还从ngx-material-file-input导入了另一个模块。这对于在 Angular Material 中使用
输入至关重要type=filemat-form-field

使用响应式表单🤓

目前一切顺利,接下来我们需要采取的必要步骤是将ReactiveFormsModule我们的内部内容导入AppModule

...
import { ReactiveFormsModule } from '@angular/forms';

@NgModule({
  ...
  imports: [
    ...
    ReactiveFormsModule,
  ],
  ...
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

太好了,这样我们就可以在组件中使用响应式表单了。
开始吧!💪 让我们来实现上传图片的表单:

create.component.ts

import { Component, OnDestroy, OnInit } from '@angular/core';
import {
  AbstractControl,
  FormBuilder,
  FormGroup,
  Validators,
} from '@angular/forms';
import { Observable, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

import { AuthService } from '../../services/auth/auth.service';
import { UtilService } from '../../services/util/util.service';

@Component({
  selector: 'app-create',
  templateUrl: './create.component.html',
  styleUrls: ['./create.component.scss'],
})
export class CreateComponent implements OnInit, OnDestroy {
  destroy$: Subject<null> = new Subject();
  fileToUpload: File;
  kittyImagePreview: string | ArrayBuffer;
  pictureForm: FormGroup;
  user: firebase.User;

  constructor(
    private readonly authService: AuthService,
    private readonly formBuilder: FormBuilder,
    private readonly utilService: UtilService,
    ...
  ) {}

  ngOnInit() {
    this.pictureForm = this.formBuilder.group({
      photo: [null, Validators.required],
      description: [null, Validators.required],
    });

    this.authService.user$
      .pipe(takeUntil(this.destroy$))
      .subscribe((user: firebase.User) => (this.user = user));
}

  ngOnDestroy() {
    this.destroy$.next(null);
  }
}
Enter fullscreen mode Exit fullscreen mode

首先,我们来注入元素FormBuilder。它能帮助我们创建一个FormGroup结构化表单的框架。由于我们只需要照片和简短的描述,所以我们只需FromControls.group({[..],[..]})函数中添加两个元素即可。

也就是说,我们还在内部传递一个默认值FormControls(在我们的例子中是null),以及一个或多个表单验证器,它们可以帮助我们验证用户输入。

通过这种方式,我们可以传递模块提供的内置验证器@angular/forms(例如我们在这里使用的 Required 验证器),或者实现自定义验证器。

由于我们需要确保上传的文件确实是图像类型,因此我们需要将其实现为自定义验证器。

我们把这个验证器称为image

  private image(
    photoControl: AbstractControl,
  ): { [key: string]: boolean } | null {
    if (photoControl.value) {
      const [kittyImage] = photoControl.value.files;
      return this.utilService.validateFile(kittyImage)
        ? null
        : {
            image: true,
          };
    }
    return;
  }
Enter fullscreen mode Exit fullscreen mode

并将其添加到FormControl命名列表中photo

this.pictureForm = this.formBuilder.group({
      photo: [
        null,
        [Validators.required, this.image.bind(this)],
      ],
      ...
    });
Enter fullscreen mode Exit fullscreen mode

验证器调用UtilService并检查上传的文件类型是否为图像:

util.service.ts

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class UtilService {
  private imageFileTypes = [
    ...
    'image/apng',
    'image/bmp',
    'image/gif',
    'image/jpeg',
    'image/png',
    'image/svg+xml',
    ...
  ];

  validateFile(file: File): boolean {
    return this.imageOrVideoFileTypes.includes(file.type);
  }
}
Enter fullscreen mode Exit fullscreen mode

如果我们的某个验证器对用户输入的评估失败,整个表单(当然也包括其赋值部分FormControl)将立即进入一个invalid状态,因此我们可以根据抛出的错误做出相应的反应。稍后我们将在模板代码中再次讨论这一点。

除了表单验证之外,我们还subscribe需要authService获取所有用户数据,例如用户名displayName或密码userAvatar

最后一步,在ngOninit函数内部,我们还需要subscribe获取valueChanges Observable每个函数提供的值FormControl

ngOnInit() {
    ...
    this.pictureForm
      .get('photo')
      .valueChanges.pipe(takeUntil(this.destroy$))
      .subscribe((newValue) => {
        this.handleFileChange(newValue.files);
      });
}
Enter fullscreen mode Exit fullscreen mode

用户每次更改输入值时,都会通过此信号发出Observable

图片上传后我们想做什么?
我们想看到预览图,对吧?那么,让我们来实现这个handleFileChange功能:

  handleFileChange([ kittyImage ]) {
    this.fileToUpload = kittyImage;
    const reader = new FileReader();
    reader.onload = (loadEvent) => (this.kittyImagePreview = 
    loadEvent.target.result);
    reader.readAsDataURL(kittyImage);
  }
Enter fullscreen mode Exit fullscreen mode

我们还使用了官方的FileReader函数来获取图片 URL,以便在image标签内显示。该readAsDataURL函数可以实现此目的,详情请参阅文档:

read操作完成后,`<object>`readyState变为`<object> DONE`,并触发 `loadend` 事件。
此时,` result<object>` 属性包含数据,数据格式为 `data: URL`,表示文件数据的 base64 编码字符串。

太好了,这正是我们需要的😊

别忘了:
既然我们订阅了所有这些可观察对象,我们也需要unsubscribe从中获取它们。

按照Jan-Niklas Wortmann这篇文章takeUntil中描述的模式,我们可以 像躲避🦊一样避免内存泄漏。

太棒了!
既然我们已经在文件中完成了几个重要的步骤,接下来create.component.ts就应该转到create.component.html. 文件了。加油!💪💪💪

首先,我们将添加所有需要的材质组件:

create.component.html

<form
  *ngIf="user"
  class="form" 
  [formGroup]="pictureForm">
  <mat-card>
    <mat-card-header>
      <div mat-card-avatar>
        <img class="avatar" [src]="user.photoURL" />
      </div>
      <mat-card-title>Post a cute Kitty 😻</mat-card-title>
      <mat-card-subtitle>{{ user.displayName }}</mat-card-subtitle>
    </mat-card-header>
    <img
      *ngIf="kittyImagePreview"
      class="preview-image"
      [src]="kittyImagePreview"
      alt="Cute Kitty Picture"
    />
    <mat-card-content>
      <mat-form-field appearance="outline" class="full-width">
         ...
      </mat-form-field>
      <mat-form-field appearance="outline" class="full-width">
         ...
      </mat-form-field>
    </mat-card-content>
    <mat-card-actions>
      ...
    </mat-card-actions>
  </mat-card>
</form>
Enter fullscreen mode Exit fullscreen mode

如您所见,我们创建了一个表单,并将其MatCardComponent作为子组件插入其中。此表单具有一个属性绑定到相关组件pictureForm,该相关组件是FormGroup我们已在create.component.ts文件夹中创建的。

接下来,我们看到在内部显示用户的姓名和头像MatCardHeaderComponent

这里是image标签页,我们会在这里看到上传的猫咪图片的预览图。

mat-card-content现在,我们将在标签内添加两个MatFormFieldComponents元素:一个用于文件输入,一个用于图像描述的文本字段。

我们先来看第一个:

<mat-form-field appearance="outline" class="full-width">
  <mat-label>Photo of your cute Kitty</mat-label>
  <ngx-mat-file-input
       accept="image/*"
       formControlName="photo"
       placeholder="Basic outline placeholder"
      >
  </ngx-mat-file-input>
  <mat-icon matSuffix>folder</mat-icon>
</mat-form-field>
Enter fullscreen mode Exit fullscreen mode

你还记得我们添加了吗MaterialFileInputModule?我们需要它与 Material Design 的外观和感觉input相符。type=file

这个模块导出ngx-mat-file-input组件。而这正是我们在这里使用的组件。

accept="image/*"属性有助于预先筛选可从对话框中选择的文件。

现在,我们只需要textarea为第二个元素添加一个 HTML 标签FormControl

<mat-form-field appearance="outline" class="full-width">
   <mat-label>Describe your Kitty</mat-label>
   <textarea
        formControlName="description"
        matInput
        placeholder="Describe your cute Kitty to us 😻"
       >
   </textarea>
</mat-form-field>
Enter fullscreen mode Exit fullscreen mode

photo要创建单个 FormControl与descriptions相应 HTML 标签之间的绑定,我们只需要formControlName相应地设置属性即可。

Angular 响应式表单为我们提供了一种非常简单的方法,可以在关联的表单下方显示错误消息FormControl

通过电话,pictureForm.controls['photo'].hasError(‘..’)如果我们添加的验证器之一由于无效的用户输入而抛出错误,我们将立即收到通知。

这样我们就可以将其放入*ngIf=".."指令中,并将其包裹在 ` MatErrorComponent<div>` 标签内,该标签已经具有用于显示错误消息的现成样式:

<-- Error messages for image FormControl -->
<mat-error *ngIf="pictureForm.controls['photo'].hasError('required')">
           Please select a cute Kitty Image 🐱
</mat-error>
<mat-error *ngIf="pictureForm.controls['photo'].hasError('image')">
          That doesn't look like a Kitty Image to me 😿
</mat-error>


<-- Error messages for description FormControl -->
<mat-error *ngIf="pictureForm.controls['description'].hasError('required')">
          You <strong>SHOULD</strong> describe your Kitty 😿
</mat-error>
Enter fullscreen mode Exit fullscreen mode

为了确保用户无法使用无效表单点击提交按钮,我们还需要将该disabled属性绑定到invalid整个表单的状态。也就是说,只要我们的任何评估Validators都会返回错误,该按钮就会被禁用。

<mat-card-actions>
   <button
        mat-raised-button
        color="primary"
        [disabled]="pictureForm.invalid || submitted"
        (click)="postKitty()"
      >
        Post Kitty
   </button>
</mat-card-actions>
Enter fullscreen mode Exit fullscreen mode

我知道您已经认出了按钮点击事件处理程序中的函数postKitty()。而且我确信您一定很想知道我们究竟是如何将可爱的猫咪图片上传到 Firebase Storage 的。

那么,我们就一起来想想该如何实现这个目标吧?

设置 Angularfire Storage 🅰️🔥

在第一篇文章中,我们已经搭建好了 Firebase 项目。如果您还没有创建 Firebase 项目,请随时返回查看。我在这里等您 🙂

此外,如果您是 Firebase 的新手,不妨看看这个很棒的YouTube 播放列表

还可以看看这里:

启用 Firebase Storage 🔥

要启用 Firebase Storage,我们需要使用设置 Firebase 项目时使用的同一个 Google 帐户返回
Firebase 控制台。

在左侧导航栏中,点击菜单项,菜单将展开,并显示Develop
更多菜单项。 点击该菜单项,您将看到类似这样的内容:Storage

替代文字

点击按钮后Get started,系统会引导您完成一个简短的向导,询问您一些读写权限限制方面的信息。但现在我们无需考虑这些,所以可以保留默认值。

点击按钮关闭向导done后,稍等片刻,您应该会看到类似这样的内容:

替代文字

做得好!您现在已经设置好 Firebase Storage 存储桶,用来存放可爱的猫咪图片了🎉。

那很简单,不是吗?

当然,现在里面什么都没有。但我保证,一旦我们上传第一批可爱的猫咪图片,文件和文件夹就会自动在这个 Firebase Storage 存储桶中创建。

在我们的应用程序内部创建StorageService📚

最后一步就是建立 Firebase Storage 与表单提交之间的实际连接。

我们还需要一种方法,通过进度条告知用户文件上传的进度。

我们可以把所有这些业务逻辑封装到一个服务中,我们称之为 `Service` StorageService。让我们通过调用以下命令来创建它:

ng g s services/storage/storage

你可能觉得这会很棘手,但相信我,并非如此。
大部分繁重的工作已经完成,并以AngularFireStorage我们从软件包导入的服务的形式呈现@angular/fire/storage

storage.service.ts

import {
  AngularFireStorage,
  AngularFireUploadTask,
} from '@angular/fire/storage';
import { from, Observable } from 'rxjs';
import { Injectable } from '@angular/core';
import { switchMap } from 'rxjs/operators';

export interface FilesUploadMetadata {
  uploadProgress$: Observable<number>;
  downloadUrl$: Observable<string>;
}

@Injectable({
  providedIn: 'root',
})
export class StorageService {
  constructor(private readonly storage: AngularFireStorage) {}

  uploadFileAndGetMetadata(
    mediaFolderPath: string,
    fileToUpload: File,
  ): FilesUploadMetadata {
    const { name } = fileToUpload;
    const filePath = `${mediaFolderPath}/${new Date().getTime()}_${name}`;
    const uploadTask: AngularFireUploadTask = this.storage.upload(
      filePath,
      fileToUpload,
    );
    return {
      uploadProgress$: uploadTask.percentageChanges(),
      downloadUrl$: this.getDownloadUrl$(uploadTask, filePath),
    };
  }

  private getDownloadUrl$(
    uploadTask: AngularFireUploadTask,
    path: string,
  ): Observable<string> {
    return from(uploadTask).pipe(
      switchMap((_) => this.storage.ref(path).getDownloadURL()),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

因此,我们创建了一个返回两个 Observable 的函数,并将它们暴露给我们的CreateComponent对象subscribe

仔细观察,你会发现我们是AngularFireUploadTask通过调用注入到依赖项中的服务upload()上的函数来获取的。AngularFireStorage

它通过调用返回一个 Observable 对象,percentageChanges()该对象会发出数字。正如你已经正确猜到的那样,我们可以使用这些数字在进度条上显示进度。

upload()函数接受两个参数:filePathfileToUpload

第一个参数代表文件在 Firebase Storage 中的路径,第二个参数当然就是我们要存储在这个路径下的图片本身。由于我们需要一个唯一的文件路径,所以我们也可以使用最近的时间戳。

返回值是一个 Promise,但由于我们想要使用 Observables,因此需要调用RxJS 操作符from来创建它。该操作符可以将各种其他对象(例如数组和 Promise)转换为 Observables。

由于我们只需要等待这个 Observable 被解析,而我们更感兴趣的是通过调用它而发出的内部 Observable getDownloadURL,因此我们需要使用RxJS 操作符switchMap切换到所谓的内部 Observable并返回它。

通过调用我们注入ref的函数AngularFireStorage,我们创建了一个 AngularFire 包装的 Storage Reference。该对象可以从基于 Promise 的方法(例如)创建 Observables 方法getDownloadURL

目前一切顺利。现在让我们将此服务作为依赖项注入到我们的代码中create.component.ts,并实现该postKitty()功能。

  constructor(
    ...
    private readonly snackBar: MatSnackBar,
    private readonly storageService: StorageService,
    ...
  ) {}
Enter fullscreen mode Exit fullscreen mode

我们还MatSnackBar需要添加一个很棒的功能,用于向用户显示成功或错误信息。

现在,最后一部分缺失的代码也需要完成了:

  postKitty() {
    this.submitted = true;
    const mediaFolderPath = `${ MEDIA_STORAGE_PATH }/${ this.user.email }/media/`;

    const { downloadUrl$, uploadProgress$ } = this.storageService.uploadFileAndGetMetadata(
      mediaFolderPath,
      this.fileToUpload,
    );

    this.uploadProgress$ = uploadProgress$;

    downloadUrl$
      .pipe(
        takeUntil(this.destroy$),
        catchError((error) => {
          this.snackBar.open(`${ error.message } 😢`, 'Close', {
            duration: 4000,
          });
          return EMPTY;
        }),
      )
      .subscribe((downloadUrl) => {
        this.submitted = false;
        this.router.navigate([ `/${ FEED }` ]);
      });
  }
Enter fullscreen mode Exit fullscreen mode

我们只需要对调用该函数subscribe后得到的两个 Observable 进行操作即可StorageServiceuploadFileAndGetMetadata

如前所述,uploadProgress$Observable 只会发出数字。
所以,让我们把它添加MatProgressbarComponent到我们的create.component.html
模板中,然后在模板内部,我们可以subscribe使用async管道来调用这个 Observable,如下所示:

...
<mat-progress-bar *ngIf="submitted" [value]="uploadProgress$ | async" mode="determinate">
</mat-progress-bar>
...
Enter fullscreen mode Exit fullscreen mode

如果上传成功,我们希望返回到原始页面FeedComponent。如果出现错误,我们将借助RxJS 操作符catchError捕获错误。这样处理错误(而不是在.subscribe()回调函数中处理)使我们能够在不取消整个流的情况下处理错误。

在我们的例子中,我们将使用我们的snackBar服务向用户发送错误消息作为一条简短的提示信息(提供反馈总是很重要的😊),并返回EMPTY,这将立即发出完整的通知。

如你所料,我们需要mediaFolderPath在这里定义我们的常量。
让我们创建一个storage.const.ts文件来定义这个常量:

export const MEDIA_STORAGE_PATH = `kittygram/media/`;
Enter fullscreen mode Exit fullscreen mode

搞定啦🎉
我们完成了😻。太棒了!💪💪💪
我们的应用程序已经准备就绪,可以上传任何类型的图片,还可以添加简短的描述🦄

您可以在这里找到该项目的源代码:

GitHub 标志 martinakraus / KittyGramUpload

此仓库演示了如何在 KittyGram 中上传图片并将其存储在 Firebase Storage 中。

KittyGramAuth

本项目使用Angular CLI版本 9.0.5生成。

开发服务器

ng serve在开发服务器上运行。导航至[此处http://localhost:4200/应填写路径]。如果您更改任何源文件,应用程序将自动重新加载。

代码脚手架

运行ng generate component component-name以生成新组件。您也可以使用ng generate directive|pipe|service|class|guard|interface|enum|module.

建造

运行ng build此命令构建项目。构建产物将存储在指定dist/目录中。使用--prod标志进行生产环境构建。

运行单元测试

通过Karma运行ng test以执行单元测试

运行端到端测试

运行以通过Protractorng e2e执行端到端测试

更多帮助

要获取有关 Angular CLI 的更多帮助,请使用ng help或查看Angular CLI README






未完待续👣

图片上传是KittyGram的一项关键功能。但这仅仅是个开始。我们现在希望将下载链接以及这篇文章的其他一些详细信息存储到某种数据库中,以便我们能够用它来填充我们的信息流。

我们的推送内容还将包含一些特色功能,例如无限滚动浏览我们数据库中存储的所有精美猫咪图片😼。而这正是我们将在下一篇文章中详细介绍的内容。

敬请期待,一旦Siddharth完成写作,我将在这篇文章中更新链接。

最后想说几句🧡

非常感谢您一直陪伴我到最后,并阅读完整篇文章。

我非常感谢Siddharth Ajmera为本文进行校对,并与我合作完成这个项目。

希望您喜欢这篇文章。如果喜欢,请点赞♥️或点赞🦄。也请将其添加到您的阅读列表🔖,以便日后查阅代码。

如果您有任何不明白的地方,欢迎在下方留言,我非常乐意为您解答。💪

最后还有一件事,别忘了在这里关注Siddharth:

希望很快能见到大家👋👋👋

图标来源:AngularIO 新闻资料包|文件上传者:LAFS,来自 Noun Project

文章来源:https://dev.to/angular/implement-file-upload-with-firebase-storage-in-our-angular-app-the-simple-way-1ofi