问题
With SpaPrerenderingExtensions
available in ASP.Net core 2.0 JavaScriptServices
, it seems that we can return the HTTP Status Code from the Angular (SSR) App which SpaPrerenderingExtensions
will use to return the HTTP Status Code to the client.
If you take a look at below method in SpaPrerenderingExtensions
private static async Task ServePrerenderResult(HttpContext context, RenderToStringResult renderResult)
Complete code of SpaPrerenderingExtensions
is available here: https://github.com/aspnet/JavaScriptServices/blob/8ded472fe93123079ab97c9332f24460340876d2/src/Microsoft.AspNetCore.SpaServices.Extensions/Prerendering/SpaPrerenderingExtensions.cs
You will notice that renderResult.Html
contains the html markup returned from Angular Server App whereas renderResult.StatusCode.Value
contains the status code.
The official documentation for enabling SSR (available at https://docs.microsoft.com/en-us/aspnet/core/spa/angular?view=aspnetcore-2.1&tabs=visual-studio#server-side-rendering) suggest adding below code to the main.server.ts
file.
import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import { renderModule, renderModuleFactory } from '@angular/platform-server';
import { APP_BASE_HREF } from '@angular/common';
import { enableProdMode } from '@angular/core';
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';
import { createServerRenderer } from 'aspnet-prerendering';
export { AppServerModule } from './app/app.server.module';
enableProdMode();
export default createServerRenderer(params => {
const { AppServerModule, AppServerModuleNgFactory, LAZY_MODULE_MAP } = (module as any).exports;
const options = {
document: params.data.originalHtml,
url: params.url,
extraProviders: [
provideModuleMap(LAZY_MODULE_MAP),
{ provide: APP_BASE_HREF, useValue: params.baseUrl },
{ provide: 'BASE_URL', useValue: params.origin + params.baseUrl }
]
};
const renderPromise = AppServerModuleNgFactory
? /* AoT */ renderModuleFactory(AppServerModuleNgFactory, options)
: /* dev */ renderModule(AppServerModule, options);
return renderPromise.then(html => ({ html }));
});
Can anyone please guide how do I return the status code from the Angular Server App so that its properly picked up by the ASP.Net Core 2.0 SpaPrerenderingExtensions
.
Any help would be highly appreciated. For example if I have a service which has value of status code, how do I pick value from that service and pass to server in main.server.ts
file.
Note: I am using ASP.Net Core 2.1 Angular CLI 6 based template.
回答1:
Ok, I tried below approach and it seems to work, any feedback and improvements would be highly welcomed. I am not sure if this approach is thread safe. What I mean is if concurrent requests are served by the server, is it possible that the status code of one request is assigned to other request since this approach is taking benefit of the fact that the sub properties of constant
objects are mutable.
I created a file globals.ts
with below code:
export const response = { statusCode: 200 };
Then, I created a service StatusCodeService
import { Injectable } from '@angular/core';
import * as myGlobals from './globals';
@Injectable()
export class StatusCodeService {
setStatusCode(statusCode: number) {
myGlobals.response.statusCode = statusCode;
}
}
Now whenever I have to set the status code, I use this service to set status code.
I have also updated the main.server.ts
file with below code to pass value to the server:
import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import { renderModule, renderModuleFactory } from '@angular/platform-server';
import { APP_BASE_HREF } from '@angular/common';
import { enableProdMode } from '@angular/core';
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';
import { createServerRenderer } from 'aspnet-prerendering';
export { AppServerModule } from './app/app.module.server';
import * as myGlobals from './app/globals';
enableProdMode();
export default createServerRenderer(params => {
const { AppServerModule, AppServerModuleNgFactory, LAZY_MODULE_MAP } = (module as any).exports;
const options = {
document: params.data.originalHtml,
url: params.url,
extraProviders: [
provideModuleMap(LAZY_MODULE_MAP),
{ provide: APP_BASE_HREF, useValue: params.baseUrl },
{ provide: 'BASE_URL', useValue: params.origin + params.baseUrl }
]
};
const renderPromise = AppServerModuleNgFactory
? /* AoT */ renderModuleFactory(AppServerModuleNgFactory, options)
: /* dev */ renderModule(AppServerModule, options);
return renderPromise.then((html) => {
let statusCode: number = 200;
if (myGlobals.response.statusCode == 404) {
statusCode = 404;
//reset status code to 200 for next request
myGlobals.response.statusCode = 200;
}
return {
html: statusCode == 404 ? 'Page not found!' : html, StatusCode: statusCode
}
});
});
I also tried the approach mentioned in Return 404 status code in aspnet core SPA Angular application
I updated the main.server.ts file for Angular 6.
import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import { APP_BASE_HREF } from '@angular/common';
import { first } from 'rxjs/operators';
import { enableProdMode, ApplicationRef, NgZone } from '@angular/core';
import { platformDynamicServer, PlatformState, INITIAL_CONFIG } from '@angular/platform-server';
import { createServerRenderer, RenderResult } from 'aspnet-prerendering';
import { AppServerModule } from './app/app.module.server';
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';
//ADD THIS LINE
import { HttpStatusCodeService } from './path/to/services/http-status-code.service';
enableProdMode();
export default createServerRenderer(params => {
const { AppServerModule, AppServerModuleNgFactory, LAZY_MODULE_MAP } = (module as any).exports;
const providers = [
provideModuleMap(LAZY_MODULE_MAP),
{ provide: INITIAL_CONFIG, useValue: { document: '<app-root></app-root>', url: params.url } },
{ provide: APP_BASE_HREF, useValue: params.baseUrl },
{ provide: 'BASE_URL', useValue: params.origin + params.baseUrl },
];
return platformDynamicServer(providers).bootstrapModule(AppServerModule).then(moduleRef => {
const appRef: ApplicationRef = moduleRef.injector.get(ApplicationRef);
const state = moduleRef.injector.get(PlatformState);
const zone = moduleRef.injector.get(NgZone);
//ADD THIS LINE: this will get the instance of the HttpStatusCodeService created for this request.
//const statusCodeService = moduleRef.injector.get(HttpStatusCodeService);
return new Promise<RenderResult>((resolve, reject) => {
zone.onError.subscribe((errorInfo: any) => reject(errorInfo));
appRef.isStable.pipe(first(isStable => isStable)).subscribe(() => {
// Because 'onStable' fires before 'onError', we have to delay slightly before
// completing the request in case there's an error to report
setImmediate(() => {
resolve({
html: state.renderToString()//,
//ADD THIS LINE: this will get the currently set status code and return it along with the prerendered html string
statusCode: statusCodeService.getStatusCode()
});
moduleRef.destroy();
});
});
});
});
});
But when I run the app, its almost broken and have below issues:
- There isn't any style sheet in the generated markup. Server Transfer State is missing. fxFlex layout doesn't work.
回答2:
I am posting this as an answer because I can format things better than in a comment, however this is all untested and based of looking at my outdated solution, your code snippets above, and the Angular6 docs (I don't have the latest VS templates so I can't test at the moment).
The options
parameter object that is given to both renderModule() and renderModuleFactory() has an extraProviders
array that, I believe, will let us add additional providers to the AppServerModule
module.
Here is my idea:
- Use my
HttpStatusCodeService
from my answer here: Return 404 status code in aspnet core SPA Angular application - Add it to the
providers
array in theAppBrowserModule
module (only the browser module, not the server module).- This will allow the service to be injected normally while the app is running in the browser.
- Instantiate
HttpStatusCodeService
inmain.server.ts
file and add that instance as a provider in theextraProviders
array.- This will allow the service to be injected while the app is running on the server while also giving us a reference to the injected service.
- Use the reference to the service to determine if the response needs to be a 404.
Here is my untested attempt to modify your main.server.ts
file snippet to work.
import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import { renderModule, renderModuleFactory } from '@angular/platform-server';
import { APP_BASE_HREF } from '@angular/common';
import { enableProdMode } from '@angular/core';
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';
import { createServerRenderer } from 'aspnet-prerendering';
export { AppServerModule } from './app/app.module.server';
//ADD THIS LINE
import { HttpStatusCodeService } from './path/to/services/http-status-code.service';
enableProdMode();
export default createServerRenderer(params => {
const { AppServerModule, AppServerModuleNgFactory, LAZY_MODULE_MAP } = (module as any).exports;
//ADD THIS LINE - We manually instantiate the service so we have a reference to it.
let statusCodeService = new HttpStatusCodeService();
const options = {
document: params.data.originalHtml,
url: params.url,
extraProviders: [
provideModuleMap(LAZY_MODULE_MAP),
{ provide: APP_BASE_HREF, useValue: params.baseUrl },
{ provide: 'BASE_URL', useValue: params.origin + params.baseUrl },
//ADD THIS LINE - We add a provider for the service that uses our instance.
{ provide: HttpStatusCodeService, useValue: statusCodeService },
]
};
const renderPromise = AppServerModuleNgFactory
? /* AoT */ renderModuleFactory(AppServerModuleNgFactory, options)
: /* dev */ renderModule(AppServerModule, options);
return renderPromise.then((html) => {
//ADD THIS LINE - get the status code from the service.
let statusCode: number = statusCodeService.getStatusCode();
return {
html: statusCode == 404 ? 'Page not found!' : html, StatusCode: statusCode
}
});
});
In my original solution, I rendered my 404 Not Found page using an Angular component, so I wouldn't override the returned HTML (html: html, statusCode: statusCode
).
And for the sake of completeness, here is the HttpStatusCodeService
from the answer linked to above:
import { Injectable } from '@angular/core';
@Injectable()
export class HttpStatusCodeService {
private statusCode: number;
constructor(){
this.statusCode = 200;
}
public setStatusCode(statusCode: number) {
this.statusCode = statusCode;
}
public getStatusCode(): number {
return this.statusCode;
}
}
Let me know if this works. If it doesn't, I can try to get a test environment set up to poke at this some more, but no promises.
来源:https://stackoverflow.com/questions/50934948/asp-net-core-2-1-angular-ssr-universal-returning-http-status-code-404