Angular 8 - 在前端更改检测问题上调整图像大小
Angular 8 - Resizing image on the front-end change-detection issue
我正在尝试根据我在网上找到的几个教程构建一个图像压缩器服务。 service 本身按预期工作,它接收一个图像作为文件,然后压缩它并 returns 一个 可观察。
一切都很好,只是我想在将压缩图像上传到服务器之前在我的组件中使用它。
该组件不会检测新的压缩图像何时通过 async 管道到达。如果我手动订阅 Observable,我会按预期获得图像数据,但如果我尝试用它更新组件 属性,它不会立即更改视图,而是用旧的 'image data' 如果我尝试压缩新图像。
我发现如果部分代码在 ngZone 之外解析,则可能会出现此问题,因此我找到了一种解决方法(请参阅下面的代码)注入 ApplicationRef 并使用 .tick()
这实际上很好用,但使我的服务难以重用。
我的问题是:
服务代码的哪一部分在 ngZone 之外运行以及可能的修复或解决方法是什么,以便服务可以在其他组件中重用,而不必在每次服务发出数据时都注入 ApplicationRef 和 .tick()。
这是我的服务代码:
import { Observable , Subscriber } from 'rxjs';
import { Injectable } from '@angular/core';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
@Injectable({
providedIn: 'root'
})
export class ImageCompressorService {
// globals
private _currentFile : File ;
private _currentImage : ICompressedImage = {} ;
// Constructor
constructor( private sanitizer : DomSanitizer) {}
// FileReader Onload callback
readerOnload(observer : Subscriber<ICompressedImage>) {
return (progressEvent : ProgressEvent) => {
const img = new Image();
img.src = (progressEvent.target as any).result;
img.onload = this.imageOnload(img , observer);
}
}
// Image Onload callback
imageOnload(image : HTMLImageElement , observer : Subscriber<ICompressedImage>) {
return () => {
const canvas = document.createElement('canvas');
canvas.width = 100;
canvas.height = 100;
const context = <CanvasRenderingContext2D>canvas.getContext('2d');
context.drawImage(image , 0 , 0 , 100 , 100);
this.toICompressedImage(context , observer);
}}
// Emit CompressedImage
toICompressedImage(context : CanvasRenderingContext2D , observer : Subscriber<ICompressedImage> ) {
context.canvas.toBlob(
(blob) => {
this._currentImage.blob = blob ;
this._currentImage.image = new File([blob] , this._currentFile.name , {type : 'image/jpeg', lastModified : Date.now()} );
this._currentImage.imgUrl = this.sanitizer.bypassSecurityTrustUrl(URL.createObjectURL(blob));
this._currentImage.name = this._currentFile.name ;
observer.next(this._currentImage);
observer.complete();
} ,
'image/jpeg' ,
1
);
}
// Compress function
compress(file : File) : Observable<ICompressedImage> {
this._currentFile = file ;
return new Observable<ICompressedImage>(
observer => {
const currentFile = file;
const reader = new FileReader();
reader.readAsDataURL(currentFile);
reader.onload = this.readerOnload(observer);
}
);
}
}
// Image Data Interface
export interface ICompressedImage {
name? : string;
image? : File ;
blob? : Blob ;
imgUrl? : SafeUrl ;
}
这是我的 component.ts :
import { Component, OnInit, ApplicationRef } from '@angular/core';
import { ImageCompressorService, ICompressedImage } from 'src/app/shared/services/image-compressor.service';
@Component({
selector: 'app-new-project',
templateUrl: './new-project.component.html',
styleUrls: ['./new-project.component.css']
})
export class NewProjectComponent implements OnInit {
// globals
private selectedImage ;
compressedImage : ICompressedImage = {name : 'No file selected'};
// Constructor
constructor( private compressor : ImageCompressorService,
private ar : ApplicationRef
) {}
// OnInit implementation
ngOnInit(): void {}
// Compress method
compress(fl : FileList) {
if (fl.length>0) {
this.selectedImage = fl.item(0);
this.compressor
.compress(this.selectedImage)
.subscribe(data => {
this.compressedImage = data ;
this.ar.tick();
});
} else {
console.error('No file/s selected');
}
}
}
这是我的 HTML 模板 组件:
<div style='border : 1px solid green;'>
<input type='file' #SelectedFile (change)="compress($event.target.files)" accept='image/*' >
</div>
<div
style = 'border : 1px solid blue ; height : 200px;'
*ngIf="compressedImage " >
<strong>File Name : </strong>{{ compressedImage?.name }}
<img *ngIf="compressedImage?.imgUrl as src"
[src]= 'src' >
</div>
我展示代码的方式非常完美。尝试注释掉
this.ar.tick(); 在 Compress Method 中 component.ts 文件并查看更改 .
经过几个小时的挖掘,我找到了一个可行的解决方案。我在我的服务中注入了 NgZone 包装器。之后,在我的压缩方法中,我使用 运行 所有文件处理代码 zone.runOutsideAngular() ,从而故意防止 ChangeDetection ,一旦调整大小操作完成并且新的压缩图像可用,我运行 观察者(订阅者)的下一个方法 zone.Run() ,它实际上在 Angular 的区域内运行代码,强制 ChangeDetection 。
我已经测试了在我的组件中手动订阅生成的可观察对象,以及通过异步管道进行的订阅。两者都很有魅力。使用异步管道发布代码。
service.ts :
import { Observable , Subscriber } from 'rxjs';
import { Injectable, NgZone } from '@angular/core';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
@Injectable({
providedIn: 'root'
})
export class ImageCompressorService {
// globals
private _currentFile : File ;
private _currentImage : ICompressedImage = {} ;
// Constructor
constructor( private sanitizer : DomSanitizer , private _zone : NgZone) {
}
// FileReader Onload callback
readerOnload(observer : Subscriber<ICompressedImage>) {
return (progressEvent : ProgressEvent) => {
const img = new Image();
img.src = (progressEvent.target as any).result;
img.onload = this.imageOnload(img , observer);
}
}
// Image Onload callback
imageOnload(image : HTMLImageElement , observer : Subscriber<ICompressedImage>) {
return () => {
const canvas = document.createElement('canvas');
canvas.width = 100;
canvas.height = 100;
const context = <CanvasRenderingContext2D>canvas.getContext('2d');
context.drawImage(image , 0 , 0 , 100 , 100);
this.toICompressedImage(context , observer);
}}
// Emit CompressedImage
toICompressedImage(context : CanvasRenderingContext2D , observer : Subscriber<ICompressedImage> ) {
context.canvas.toBlob(
(blob) => {
this._currentImage.blob = blob ;
this._currentImage.image = new File([blob] , this._currentFile.name , {type : 'image/jpeg', lastModified : Date.now()} );
this._currentImage.imgUrl = this.sanitizer.bypassSecurityTrustUrl(URL.createObjectURL(blob));
this._currentImage.name = this._currentFile.name ;
this._zone.run(() => {
observer.next(this._currentImage);
observer.complete();
})
} ,
'image/jpeg' ,
1
);
}
// Compress function
compress(file : File) : Observable<ICompressedImage> {
this._currentFile = file ;
return new Observable<ICompressedImage>(
observer => {
this._zone.runOutsideAngular(() => {
const currentFile = file;
const reader = new FileReader();
reader.readAsDataURL(currentFile);
reader.onload = this.readerOnload(observer);
})
}
);
}
}
// Image Data Interface
export interface ICompressedImage {
name? : string;
image? : File ;
blob? : Blob ;
imgUrl? : SafeUrl ;
}
component.ts :
import { Component, OnInit } from '@angular/core';
import { ImageCompressorService, ICompressedImage } from 'src/app/shared/services/image-compressor.service';
import { Observable } from 'rxjs';
@Component({
selector: 'app-new-project',
templateUrl: './new-project.component.html',
styleUrls: ['./new-project.component.css']
})
export class NewProjectComponent implements OnInit {
// globals
private selectedImage ;
compressedImage : Observable<ICompressedImage>;
// Constructor
constructor( private compressor : ImageCompressorService) {}
// OnInit implementation
ngOnInit(): void {}
// Compress method
compress(fl : FileList) {
if (fl.length>0) {
this.selectedImage = fl.item(0);
this.compressedImage = this.compressor.compress(this.selectedImage)
} else {
console.error('No file/s selected');
}
}
}
component.html :
<div style='border : 1px solid green;'>
<input type='file' #SelectedFile (change)="compress($event.target.files)" accept='image/*' >
</div>
<div
style = 'border : 1px solid blue ; height : 200px;'
*ngIf="compressedImage | async as image" >
<strong>File Name : </strong>{{ image.name }}
<img *ngIf="image.imgUrl as src"
[src]= 'src' >
</div>
我正在尝试根据我在网上找到的几个教程构建一个图像压缩器服务。 service 本身按预期工作,它接收一个图像作为文件,然后压缩它并 returns 一个 可观察。 一切都很好,只是我想在将压缩图像上传到服务器之前在我的组件中使用它。
该组件不会检测新的压缩图像何时通过 async 管道到达。如果我手动订阅 Observable,我会按预期获得图像数据,但如果我尝试用它更新组件 属性,它不会立即更改视图,而是用旧的 'image data' 如果我尝试压缩新图像。
我发现如果部分代码在 ngZone 之外解析,则可能会出现此问题,因此我找到了一种解决方法(请参阅下面的代码)注入 ApplicationRef 并使用 .tick() 这实际上很好用,但使我的服务难以重用。
我的问题是: 服务代码的哪一部分在 ngZone 之外运行以及可能的修复或解决方法是什么,以便服务可以在其他组件中重用,而不必在每次服务发出数据时都注入 ApplicationRef 和 .tick()。
这是我的服务代码:
import { Observable , Subscriber } from 'rxjs';
import { Injectable } from '@angular/core';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
@Injectable({
providedIn: 'root'
})
export class ImageCompressorService {
// globals
private _currentFile : File ;
private _currentImage : ICompressedImage = {} ;
// Constructor
constructor( private sanitizer : DomSanitizer) {}
// FileReader Onload callback
readerOnload(observer : Subscriber<ICompressedImage>) {
return (progressEvent : ProgressEvent) => {
const img = new Image();
img.src = (progressEvent.target as any).result;
img.onload = this.imageOnload(img , observer);
}
}
// Image Onload callback
imageOnload(image : HTMLImageElement , observer : Subscriber<ICompressedImage>) {
return () => {
const canvas = document.createElement('canvas');
canvas.width = 100;
canvas.height = 100;
const context = <CanvasRenderingContext2D>canvas.getContext('2d');
context.drawImage(image , 0 , 0 , 100 , 100);
this.toICompressedImage(context , observer);
}}
// Emit CompressedImage
toICompressedImage(context : CanvasRenderingContext2D , observer : Subscriber<ICompressedImage> ) {
context.canvas.toBlob(
(blob) => {
this._currentImage.blob = blob ;
this._currentImage.image = new File([blob] , this._currentFile.name , {type : 'image/jpeg', lastModified : Date.now()} );
this._currentImage.imgUrl = this.sanitizer.bypassSecurityTrustUrl(URL.createObjectURL(blob));
this._currentImage.name = this._currentFile.name ;
observer.next(this._currentImage);
observer.complete();
} ,
'image/jpeg' ,
1
);
}
// Compress function
compress(file : File) : Observable<ICompressedImage> {
this._currentFile = file ;
return new Observable<ICompressedImage>(
observer => {
const currentFile = file;
const reader = new FileReader();
reader.readAsDataURL(currentFile);
reader.onload = this.readerOnload(observer);
}
);
}
}
// Image Data Interface
export interface ICompressedImage {
name? : string;
image? : File ;
blob? : Blob ;
imgUrl? : SafeUrl ;
}
这是我的 component.ts :
import { Component, OnInit, ApplicationRef } from '@angular/core';
import { ImageCompressorService, ICompressedImage } from 'src/app/shared/services/image-compressor.service';
@Component({
selector: 'app-new-project',
templateUrl: './new-project.component.html',
styleUrls: ['./new-project.component.css']
})
export class NewProjectComponent implements OnInit {
// globals
private selectedImage ;
compressedImage : ICompressedImage = {name : 'No file selected'};
// Constructor
constructor( private compressor : ImageCompressorService,
private ar : ApplicationRef
) {}
// OnInit implementation
ngOnInit(): void {}
// Compress method
compress(fl : FileList) {
if (fl.length>0) {
this.selectedImage = fl.item(0);
this.compressor
.compress(this.selectedImage)
.subscribe(data => {
this.compressedImage = data ;
this.ar.tick();
});
} else {
console.error('No file/s selected');
}
}
}
这是我的 HTML 模板 组件:
<div style='border : 1px solid green;'>
<input type='file' #SelectedFile (change)="compress($event.target.files)" accept='image/*' >
</div>
<div
style = 'border : 1px solid blue ; height : 200px;'
*ngIf="compressedImage " >
<strong>File Name : </strong>{{ compressedImage?.name }}
<img *ngIf="compressedImage?.imgUrl as src"
[src]= 'src' >
</div>
我展示代码的方式非常完美。尝试注释掉 this.ar.tick(); 在 Compress Method 中 component.ts 文件并查看更改 .
经过几个小时的挖掘,我找到了一个可行的解决方案。我在我的服务中注入了 NgZone 包装器。之后,在我的压缩方法中,我使用 运行 所有文件处理代码 zone.runOutsideAngular() ,从而故意防止 ChangeDetection ,一旦调整大小操作完成并且新的压缩图像可用,我运行 观察者(订阅者)的下一个方法 zone.Run() ,它实际上在 Angular 的区域内运行代码,强制 ChangeDetection 。 我已经测试了在我的组件中手动订阅生成的可观察对象,以及通过异步管道进行的订阅。两者都很有魅力。使用异步管道发布代码。
service.ts :
import { Observable , Subscriber } from 'rxjs';
import { Injectable, NgZone } from '@angular/core';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
@Injectable({
providedIn: 'root'
})
export class ImageCompressorService {
// globals
private _currentFile : File ;
private _currentImage : ICompressedImage = {} ;
// Constructor
constructor( private sanitizer : DomSanitizer , private _zone : NgZone) {
}
// FileReader Onload callback
readerOnload(observer : Subscriber<ICompressedImage>) {
return (progressEvent : ProgressEvent) => {
const img = new Image();
img.src = (progressEvent.target as any).result;
img.onload = this.imageOnload(img , observer);
}
}
// Image Onload callback
imageOnload(image : HTMLImageElement , observer : Subscriber<ICompressedImage>) {
return () => {
const canvas = document.createElement('canvas');
canvas.width = 100;
canvas.height = 100;
const context = <CanvasRenderingContext2D>canvas.getContext('2d');
context.drawImage(image , 0 , 0 , 100 , 100);
this.toICompressedImage(context , observer);
}}
// Emit CompressedImage
toICompressedImage(context : CanvasRenderingContext2D , observer : Subscriber<ICompressedImage> ) {
context.canvas.toBlob(
(blob) => {
this._currentImage.blob = blob ;
this._currentImage.image = new File([blob] , this._currentFile.name , {type : 'image/jpeg', lastModified : Date.now()} );
this._currentImage.imgUrl = this.sanitizer.bypassSecurityTrustUrl(URL.createObjectURL(blob));
this._currentImage.name = this._currentFile.name ;
this._zone.run(() => {
observer.next(this._currentImage);
observer.complete();
})
} ,
'image/jpeg' ,
1
);
}
// Compress function
compress(file : File) : Observable<ICompressedImage> {
this._currentFile = file ;
return new Observable<ICompressedImage>(
observer => {
this._zone.runOutsideAngular(() => {
const currentFile = file;
const reader = new FileReader();
reader.readAsDataURL(currentFile);
reader.onload = this.readerOnload(observer);
})
}
);
}
}
// Image Data Interface
export interface ICompressedImage {
name? : string;
image? : File ;
blob? : Blob ;
imgUrl? : SafeUrl ;
}
component.ts :
import { Component, OnInit } from '@angular/core';
import { ImageCompressorService, ICompressedImage } from 'src/app/shared/services/image-compressor.service';
import { Observable } from 'rxjs';
@Component({
selector: 'app-new-project',
templateUrl: './new-project.component.html',
styleUrls: ['./new-project.component.css']
})
export class NewProjectComponent implements OnInit {
// globals
private selectedImage ;
compressedImage : Observable<ICompressedImage>;
// Constructor
constructor( private compressor : ImageCompressorService) {}
// OnInit implementation
ngOnInit(): void {}
// Compress method
compress(fl : FileList) {
if (fl.length>0) {
this.selectedImage = fl.item(0);
this.compressedImage = this.compressor.compress(this.selectedImage)
} else {
console.error('No file/s selected');
}
}
}
component.html :
<div style='border : 1px solid green;'>
<input type='file' #SelectedFile (change)="compress($event.target.files)" accept='image/*' >
</div>
<div
style = 'border : 1px solid blue ; height : 200px;'
*ngIf="compressedImage | async as image" >
<strong>File Name : </strong>{{ image.name }}
<img *ngIf="image.imgUrl as src"
[src]= 'src' >
</div>