我有一个Angular 10应用程序,该应用程序将Angular Universal用于SSR。此应用程序还发出http请求以获取一些html并动态呈现它。当我使用ng serve运行应用程序时,一切正常,动态内容将通过http请求获取,并正确呈现到页面中。这些是我的主要模块和组件
app.module.ts
export function createCompiler(compilerFactory: CompilerFactory) {
return compilerFactory.createCompiler();
}
@NgModule({
declarations: [
AppComponent,
],
imports: [
BrowserModule.withServerTransition({ appId: 'serverApp' }),
BrowserAnimationsModule,
HttpClientModule,
BrowserTransferStateModule,
CoreModule
],
providers: [
HttpClientModule,
{ provide: COMPILER_OPTIONS, useValue: {}, multi: true },
{ provide: CompilerFactory, useClass: JitCompilerFactory, deps: [COMPILER_OPTIONS] },
{ provide: Compiler, useFactory: createCompiler, deps: [CompilerFactory] }
],
bootstrap: [AppComponent]
})
export class AppModule { }
app.component.ts
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {
categories$: Observable<any[]>;
categories: any[]
store$: Observable<any>;
constructor() {}
ngOnInit() {
const cats = [
{ id: 1, name: 'XMAS' },
{ id: 2, name: 'KITCHEN' },
{ id: 3, name: 'DINNING' },
{ id: 4, name: 'BATHROOM' },
{ id: 5, name: 'BEDROOM' },
];
const store = {
countryCode : 'US',
}
this.store$ = of(store);
this.categories$ = of(cats);
this.categories = [...cats];
}
}
这是该组件的基本html
<p>TEST</p>
<div *ngIf="categories?.length">
<h2>CATEGORIES</h2>
<ul *ngFor="let cat of categories">
<li>{{ cat.name }}</li>
</ul>
</div>
<footer>
<app-dynamic-content name="Footer_Sections" [parentContext]="this"></app-dynamic-content>
</footer>
app-dynamic内容组件通过http请求获取内容,创建一个新组件,将获取的内容分配为模板,创建一个新模块,并将该组件添加到声明中以允许使用
dynamic-content.component.ts
@Component({
selector: 'app-dynamic-content',
templateUrl: './dynamic-content.component.html',
styleUrls: ['./dynamic-content.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DynamicContentComponent implements OnInit, OnDestroy, AfterViewInit {
@Input() name: string;
@Input() parentContext: any;
@ViewChild('container', { read: ViewContainerRef }) container: ViewContainerRef;
private componentRef: ComponentRef<any>;
private unsubscribe = new Subject<void>();
parsedContent: DynamicContent;
dynamicContent$: Observable<any>;
template: string;
constructor(
private dynamicContentsService: DynamicContentsService,
private compiler: Compiler,
private changeDetectorRef: ChangeDetectorRef
) { }
ngOnInit(): void {
this.dynamicContent$ = this.dynamicContentsService.getDynamicContent(this.name).pipe(filter(res => !!res));
}
ngAfterViewInit() {
this.dynamicContent$.pipe(take(1))
.subscribe((response) => {
if (response && response.dynamicContents.length) {
this.parsedContent = this.dynamicContentsService.parseDinamycContent(response.dynamicContents as DynamicContent[]);
this.template = this.parsedContent.value;
this.createDynamicComponent();
}
});
}
createDynamicComponent() {
const metadata = new Component({
selector: 'app-dynamic',
template: this.template
});
const factory = this.createComponentFactory(metadata);
this.componentRef = this.container.createComponent(factory);
this.changeDetectorRef.detectChanges();
}
createComponentFactory(metadata: Component): ComponentFactory<any> {
const self = this;
const componentClass = class DynamicComponent { context: any = self.parentContext; };
const moduleClass = class RuntimeComponentModule {};
const decoratedComponent = Component(metadata)(componentClass);
const modules = this.getModulesNeeded();
const decoratedNgModule = NgModule({ imports: modules, declarations: [decoratedComponent] })(moduleClass);
const module: ModuleWithComponentFactories<any> = this.compiler.compileModuleAndAllComponentsSync(decoratedNgModule);
return module.componentFactories.find(factory => factory.componentType === decoratedComponent);
}
getModulesNeeded() {
return [BrowserModule, CommonModule];
}
ngOnDestroy() {
this.unsubscribe.next();
this.unsubscribe.complete();
if (this.componentRef) {
this.componentRef.destroy();
}
}
}
dynamic-content.component.html
<ng-container>
<ng-template #container></ng-template>
</ng-container>
这是通过http请求获取的html内容:
<div class="social" *ngIf="context.store$ | async as store">
<div class="networks">
<h3>Follow US</h3>
</div>
</div>
正如我所说,当应用程序与ng serve一起运行时,此组件可以正常工作,呈现动态内容。问题是当我使用ng run app-name:serve-ssr使用ssr运行应用程序时,所有内容均来自服务器,除了此动态内容组件。该内容实际上是在服务器中获取的,因为我可以看到模板存储在脚本html标签中的transferState键中,但是我在服务器控制台中遇到了此错误
ERROR Error: The pipe 'async' could not be found!
at getPipeDef$1 (C:\app\dist\myapp\server\main.js:1:2321863)
at ɵɵpipe (C:\app\dist\myapp\server\main.js:1:2321915)
at template (ng:///.js:150:9)
at executeTemplate (C:\app\dist\myapp\server\main.js:1:2097798)
at renderView (C:\app\dist\myapp\server\main.js:1:2093138)
at renderComponent (C:\app\dist\myapp\server\main.js:1:2111597)
at renderChildComponents (C:\app\dist\myapp\server\main.js:1:2093514)
at renderView (C:\app\dist\myapp\server\main.js:1:2093555)
at ComponentFactory$1.create (C:\app\dist\myapp\server\main.js:1:2313775)
at R3ViewContainerRef.createComponent (C:\app\dist\myapp\server\main.js:1:2130074)
出现此错误后,浏览器似乎控制了控件从transferState键获取内容并呈现该内容,但ssr不适用于这种动态内容。我的通用服务器由ng add Universal命令组成,唯一的变化是代理中间件。
import 'zone.js/dist/zone-node';
import { createProxyMiddleware, Filter, Options, RequestHandler } from 'http-proxy-middleware';
import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import { join } from 'path';
import { AppServerModule } from './src/main.server';
import { APP_BASE_HREF } from '@angular/common';
import { existsSync } from 'fs';
import { environment } from 'src/environments/environment';
// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
const server = express();
const distFolder = join(process.cwd(), 'dist/myApp/browser');
const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
server.engine(
'html',
ngExpressEngine({
bootstrap: AppServerModule,
})
);
server.set('view engine', 'html');
server.set('views', distFolder);
server.use('/api', createProxyMiddleware({ target: environment?.host, changeOrigin: true }));
server.get(
'*.*',
express.static(distFolder, {
maxAge: '1y',
})
);
// All regular routes use the Universal engine
server.get('*', (req, res) => {
console.log(req.params);
res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
});
return server;
}
function run(): void {
const port = process.env.PORT || 4000;
// Start up the Node server
const server = app();
server.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
});
}
// Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle.
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = (mainModule && mainModule.filename) || '';
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
run();
}
export * from './src/main.server';
有什么办法可以使这两件事协同工作吗?