在我们的 Angular 应用中使用 Firebase Storage 实现文件上传 🔥:简单易用
KittyGramAuth
前言:这是我和我亲爱的朋友 Siddharth(他也是 Angular 和 Web 技术方面的 GDE)共同创建KittyGram 的系列文章中的第二篇 :KittyGram 是一个超级简约的 Instagram 克隆版,只允许上传猫咪 🐱 照片。
有关项目概况以及我们目前已实施的内容,请参阅我们的第一篇文章:
在本文中,我们将介绍如何使用 Firebase Storage 和 Angular 中的 响应式表单将文件上传到 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 播放列表 。
还可以看看这里:
VIDEO
启用 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()函数接受两个参数: filePath和 fileToUpload。
第一个参数代表文件在 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
搞定啦🎉 我们完成了😻。太棒了!💪💪💪 我们的应用程序已经准备就绪,可以上传任何类型的图片,还可以添加简短的描述🦄
您可以在这里找到该项目的源代码:
此仓库演示了如何在 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以执行单元测试 。
运行端到端测试
运行以通过 Protractor ng 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