【问题标题】:Angular Function getting called Twice and FileReader causing Infinite LoopAngular 函数被调用两次和 FileReader 导致无限循环
【发布时间】:2020-06-27 12:48:00
【问题描述】:

我正在编写一个简单的代码来接受一个表单中的几个值并在右侧显示表单值列表。我已经创建了表单字段的模态类,并在提交时将数据发送到服务。在另一个组件中,我订阅了数据。由于某些未知原因,我在第二个组件中的函数被调用了两次。此外,我接受表单中的图像文件或图像 URL,并生成在表单和列表中选择的文件的预览。要在表单中生成此文件的预览,它可以完美运行,但在第二个组件中,相同的代码会进入无限循环。任意

我的主要表单组件 html

<div class="row form">
  <div class="col-8">
    <form [formGroup]="songMetadata" (submit)="onSubmit(songMetadata)">
      <div class="row">
        <div class="col form-data">
          <mat-form-field>
            <mat-label>Song Name</mat-label>
            <input matInput formControlName="songName" />
          </mat-form-field>
          <br />
          <br />
          <mat-form-field>
            <mat-label>Artist Name</mat-label>
            <input matInput formControlName="artistName" />
          </mat-form-field>
          <br />
          <br />
          <mat-form-field>
            <mat-label>Album Name</mat-label>
            <input matInput formControlName="albumName" />
          </mat-form-field>
          <br />
          <br />
          <mat-form-field>
            <mat-label>Spotify URL</mat-label>
            <input matInput formControlName="url" />
          </mat-form-field>
          <br />
          <br />
          <mat-form-field>
            <mat-label>Other Description</mat-label>
            <textarea
              matInput
              formControlName="description"
              rows="4"
            ></textarea>
          </mat-form-field>
          <br />
          <br />
          <button
            type="submit"
            class="btn btn-primary"
            [disabled]="
              songMetadata.invalid ||
              (!isImageFileSelected && !isImageURLEntered)
            "
          >
            Submit
          </button>
        </div>
        <div class="col image-upload">
          <div
            dropZone
            class="text-center dropzone"
            (hovered)="changeIsHover($event)"
            (dropped)="fileDropped($event)"
            [class.hovering]="isHovering"
          >
            <img
              *ngIf="isImageFileSelected || isImageURLEntered"
              [src]="imagePreview"
              alt=""
              width="192"
              height="190"
            />
            <div
              class="drop-text"
              *ngIf="!isImageFileSelected && !isImageURLEntered"
            >
              Drag And Drop File Here
            </div>
          </div>
          <input
            type="text"
            placeHolder="Or Enter URL"
            (blur)="loadPreviewFromURL($event)"
          />
          <br />
          <label for="files" class="btn btn-primary">Or Select Image</label>
          <br />
          <input
            id="files"
            style="visibility:hidden;"
            type="file"
            (change)="fileSelected($event)"
          />
          <br />
          <!-- <input type="file" /> -->
        </div>
      </div>
    </form>
  </div>
  <!-- List View -->
  <div class="col-4">
    <list></list>
  </div>
</div>

主要组件 TS。

import { DataService } from "./data.service";
import { AngularFireStorageModule } from "@angular/fire/storage";
import { Component, OnInit } from "@angular/core";
import { FormGroup, FormBuilder, Validators } from "@angular/forms";
import { Observable } from "rxjs";
import { ISong } from "./song";
import { Data } from "@angular/router";

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.scss"]
})
export class AppComponent implements OnInit {
  songMetadata: FormGroup;
  selectedFile: File = null;
  isImageFileSelected: boolean;
  isImageURLEntered: boolean;
  isHovering: boolean;
  song: ISong;
  imagePreview: any;
  // // Upload Related Stuff
  // task: AngularFireUploadTask;
  // snapshot: Observable<any>;

  constructor(
    private formBuilder: FormBuilder,
    private service: DataService // private storage: AngularFireStorage
  ) {
    this.isImageFileSelected = false;
    this.isHovering = false;
    this.isImageURLEntered = false;
  }

  ngOnInit() {
    this.songMetadata = this.formBuilder.group({
      songName: ["xzcvzxv", Validators.required],
      artistName: ["xzcvcxzv", Validators.required],
      albumName: ["zxcvcxv", Validators.required],
      url: ["zxcvvc", Validators.required],
      description: ["zxcvzcxv", Validators.required]
    });
  }

  fileSelected(event: any) {
    this.isImageFileSelected = true;
    this.isImageURLEntered = false;
    this.selectedFile = event.target.files[0];
    this.loadPreview();
  }

  fileDropped(event: FileList) {
    this.isImageFileSelected = true;
    this.isImageURLEntered = false;
    this.selectedFile = event.item(0);
    this.loadPreview();
  }

  onSubmit(songForm: FormGroup) {
    event.preventDefault();
    if (this.isImageFileSelected) {
      this.song = {
        name: songForm.value.songName,
        artist: songForm.value.artistName,
        album: songForm.value.albumName,
        url: songForm.value.url,
        description: songForm.value.description,
        imageFile: this.selectedFile,
        imageURL: null
      };
    } else {
      this.song = {
        name: songForm.value.songName,
        artist: songForm.value.artistName,
        album: songForm.value.albumName,
        url: songForm.value.url,
        description: songForm.value.description,
        imageFile: null,
        imageURL: this.imagePreview
      };
    }
    this.service.addSong(this.song);
  }

  changeIsHover(isHovering: boolean) {
    this.isHovering = isHovering;
  }

  loadPreview = () => {
    let reader = new FileReader();
    reader.readAsDataURL(this.selectedFile);
    reader.onload = event => {
      this.imagePreview = reader.result;
    };
    console.log("PREVIEW HIT");
  };

  loadPreviewFromURL(event: any) {
    console.log(event.target.value);
    this.isImageURLEntered = true;
    this.isImageFileSelected = false;
    this.imagePreview = event.target.value;
  }
}

列表组件 HTML

<div *ngFor="let s of songs; let i = index">
  <div class="col-4">
    <img
      *ngIf="s?.imageURL"
      [src]="livePrvw(s?.imageURL)"
      width="100"
      height="100"
      alt="Image URL Preview"
    />
    <img
      *ngIf="s?.imageFile"
      [src]="livePreview(s?.imageFile)"
      width="100"
      height="100"
      alt="Image FILE Preview"
    />
  </div>
  <div class="col-8">
    {{ s.name }}
  </div>
</div>

listComponent TS

import { DataService } from "./../data.service";
import { ISong } from "./../song";
import { Component, OnInit, OnChanges } from "@angular/core";

@Component({
  selector: "list",
  templateUrl: "./list.component.html",
  styleUrls: ["./list.component.scss"]
})
export class ListComponent implements OnInit {
  songs: ISong[];
  // song: ISong;

  constructor(private service: DataService) {}

  ngOnInit() {
    this.service.songsAsObservable.subscribe(data => {
      this.songs = data;
    });
  }

  //  also getting called twice
  livePreview(file: File) {
    // Going into infinite loop

    // let newReader = new FileReader();
    // let imagePreview: any;
    // newReader.readAsDataURL(file);

    // newReader.onload = event => {
    //   imagePreview = newReader.result;
    // };

    console.log(file);
  }

  // getting called twice somehow
  livePrvw(imageURL: string) {
    console.log(imageURL);
    return imageURL;
  }
}

重复 ListComponent 中的方法无故被调用两次,并且在 Main Component 中工作的 FileReader 代码在 ListComponent 中不起作用(进入无限循环)

非常感谢任何帮助

【问题讨论】:

  • 我建议不要绑定到执行任何繁重处理的函数。在您的情况下,我将创建一个视图模型并预先加载所有数据。然后绑定到歌曲视图模型
  • 你现在受制于变化检测周期
  • 顺便说一句,最好不要在公共 github 存储库中存储任何敏感信息,例如 firebase 密钥。
  • 感谢 Firebase 密钥失误。但其他一切都应该工作。我尝试将 ngOnChanges 放入列表组件中,以查看 onChanges 被触发了多少次,但据我所知,这也被触发了一次,我无法找到对这两个函数的两次调用。还有关于列表组件中无限循环 FileReader 的任何想法?你能在你的机器上运行一次代码,看看它的行为是否不同吗??
  • 我认为你最好的选择是在 stackblitz 中使用你目前拥有的抽象代码,用最少的代码重新创建问题

标签: angular typescript angular-services angular-file-upload


【解决方案1】:

你的问题

您有一个允许用户上传图片或指定图片 URL 的表单。

提交表单时,您会渲染上传的图片。

返回图像 URL 的函数要么运行多次,要么不起作用。

我的诊断

您当前的解决方案存在一些问题。

  1. 多个函数执行

您正在将 &lt;img src /&gt; 绑定到解析 URL 的函数。每次更改检测运行时,都会查询您的函数。您应该在变更检测周期中做尽可能少的处理。

一般而言,通过将组件 changeDetectionStrategy 设置为 OnPush、注入 ChangeDetectorRef 并在您想要触发更改检测时手动调用 detectChanges() 来解决此问题。但是,我认为在这种情况下这会掩盖裂缝,所以不是我建议的解决方案。

  1. 无法解析上传文件的 URL

您正在尝试将&lt;img src /&gt; 绑定到通过FileReader 获取数据URL 的函数。文件读取器异步执行,因此您需要将其包装在某种 promise / observable 中并使用 async 管道来调用它。

但由于第 1 点所述的原因,您不应该这样做。

我的方法

我将重点关注如何解决图像 URL 的具体问题,因为您有一个复杂的表单,大多数情况下超出了这个问题的范围。

我在这里的主要重构是将 URL 解析排除在更改检测周期之外。我会先解析 URL,然后使用解析的 URL 将项目添加到数组中。

我的实现

我会将您的问题抽象为一个简单的应用程序,您可以在其中上传图像和/或指定图像 URL。提交表单后会显示解析后的图片。

该组件相当琐碎,所以我在这里只展示服务:

image.service.ts

export class ImageService {
  private images$ = new BehaviorSubject<{ url: string, type: string }[]>([]);
  private images: { url: string, type: string }[] = [];

  addFromFile(file: File) {
    const reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = event => {
      this.addImage({
        url: reader.result.toString(),
        type: 'fromFile'
      });
    };    
  }

  addFromUrl(url: string) {
    this.addImage({
      url: url,
      type: 'fromUrl'
    });
  }

  getImages(): Observable<{ url: string, type: string }[]> {
    return this.images$.asObservable();
  }

  private addImage(image: { url: string, type: string }) {
    this.images.push(image);
    this.images$.next(this.images);
  }
}

请注意,只有在解析 URL 后,图像才会被推送到数组和主题中。

与您的表单相比,我确实简化了很多,但我认为您可以从这种方法中获得一些灵感来解决您的问题。

演示:https://stackblitz.com/edit/angular-yhnnwv

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-11-01
    • 2021-07-14
    • 1970-01-01
    • 2018-05-06
    • 2021-06-12
    • 2011-08-23
    • 2018-03-12
    • 2019-11-07
    相关资源
    最近更新 更多