服务器端渲染不适用于实时服务器上的模块延迟加载 angular 12
Server side rendering not working with modules lazy loading angular 12 on live server
在 localhost 模块上,延迟加载工作正常,在查看页面源代码中显示元标记和 HTML 中的 <app-root><app-root/>
内容,但在实时服务器上没有显示。在实时服务器上,我只能看到 AppModule
的直接子组件的元标记和 HTML 内容,但延迟加载的模块组件不显示元标记和 HTML。这种奇怪的行为只存在于实时服务器上。
app.server.module.ts
import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { AppComponent } from './app.component';
import { AppModule } from './app.module';
@NgModule({
imports: [AppModule, ServerModule],
bootstrap: [AppComponent]
})
export class AppServerModule { }
main.server.ts
import '@angular/localize/init'
import '@angular/platform-server/init';
import { enableProdMode } from '@angular/core';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
export { AppServerModule } from './app/app.server.module';
export { renderModule } from '@angular/platform-server';
server.ts
import '@angular/localize/init';
import 'zone.js/dist/zone-node';
import 'reflect-metadata';
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, readFileSync } from 'fs';
import { createWindow } from 'domino';
import 'localstorage-polyfill'
import 'localstorage-polyfill'
const scripts = readFileSync('dist/clientWeb/browser/index.html').toString();
const window = createWindow(scripts) as any;
(global as any).window = window;
(global as any).document = window.document;
(global as any).Event = window.Event;
(global as any).KeyboardEvent = window.KeyboardEvent;
(global as any).MouseEvent = window.MouseEvent;
// 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/clientWeb/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);
// Example Express Rest API endpoints
// server.get('/api/**', (req, res) => { });
// Serve static files from /browser
server.get('*.*', express.static(distFolder, {
maxAge: '1y'
}));
// All regular routes use the Universal engine
server.get('*', (req, res) => {
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}`);
});
}
const MockBrowser = require('mock-browser').mocks.MockBrowser;
const mock = new MockBrowser();
(global as any).localStorage = localStorage;
// 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';
app.modules.ts
import { BrowserModule, HammerGestureConfig, HAMMER_GESTURE_CONFIG, Title } from '@angular/platform-browser';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { AppLayoutComponent } from './shared/layout/app-layout/app-layout.component';
import { NgModule, CUSTOM_ELEMENTS_SCHEMA, ModuleWithProviders } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AppRoutingModule } from './app-routing.module';
import { HttpClientModule } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { Tradesmanapplayout2Component } from './shared/layout/tradesmanapplayout2/tradesmanapplayout2.component';
import { Supplierapplayout2Component } from './shared/layout/supplierapplayout2/supplierapplayout2.component';
import { Userapplayout2Component } from './shared/layout/userapplayout2/userapplayout2.component';
import { AppDasboardHeader2Component } from './shared/layout/app-dasboard-header2/app-dasboard-header2.component';
import { AppDasboardFooter2Component } from './shared/layout/app-dasboard-footer2/app-dasboard-footer2.component';
const routes: Routes = [
{
path: 'resetpassword',
component: AppHeaderLayoutComponent,
loadChildren: () => import('./common/resetPassword/resetpassword.module').then(m => m.ResetpasswordModule)
},
{
path: 'Supplier',
component: Userapplayout2Component,
loadChildren: () => import('./supplier/supplier.module').then(m => m.SupplierModule),
},
{
path: 'User',
component: Userapplayout2Component,
loadChildren: () => import('./user/user.module').then(m => m.UserModule),
},
{
path: 'MarketPlace',
//component: UsersApplayoutComponent,
//component: AppHeaderLayoutComponent,
component: Userapplayout2Component,
loadChildren: () => import('./marketplace/marketplace.module').then(m => m.MarketplaceModule),
},
{
path: 'User/Agrements',
//component: AppLayoutComponent,
component: AppHeaderLayoutComponent,
loadChildren: () => import('./agrements/agrements.module').then(m => m.AgrementsModule)
},
{
path: 'HWUser',
//component: AppLayoutComponent,
component: AppHeaderLayoutComponent,
loadChildren: () => import('./HelpAndFAQ/helpQuestion.module').then(m => m.HelpQuestionModule)
},
{
path: 'Tradesman',
component: Userapplayout2Component,
loadChildren: () => import('./trademan/trademan.module').then(m => m.TrademanModule),
canActivate: [AuthGuardTradesmanService],
canActivateChild: [AuthGuardTradesmanService]
},
{
path: 'ContactUs',
//component: AppLayoutComponent,
component: AppHeaderLayoutComponent,
loadChildren: () => import('./contactUs/contactUs.module').then(m => m.ContactUsModule)
},
{
path: 'landing-page/liveleads',
//component: AppLayoutComponent,
component: AppHeaderLayoutComponent,
loadChildren: () => import('./landing-page/liveleads/liveleads.module').then(m => m.LiveleadsModule)
},
{
path: 'landing-page',
component: AppHeaderLayoutComponent,
loadChildren: () => import('./landing-page/landing-page.module').then(m => m.LandingPageModule)
}
]
@NgModule({
declarations: [
AppComponent,
AppLayoutComponent,
AppDasboardHeaderComponent,
AppDasboardFooterComponent,
AppLeftmenuComponent,
AppcommonfooterComponent,
SupplierLayoutComponent,
TrademanLayoutComponent,
TrademenuLeftComponent,
SupplierLeftmenuComponent,
],
imports: [
BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }),
NgbModule,
RouterModule.forRoot(routes, { enableTracing: false }),
ModalModule.forRoot(),
HttpModule,
BrowserAnimationsModule,
NgxImageCompressService,
Events,
Title,
metaTagsService,
ShareService,
],
schemas: [
CUSTOM_ELEMENTS_SCHEMA
],
bootstrap: [AppComponent]
})
export class AppModule { }
已在 IISWeb.config 上部署
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<location path="." inheritInChildApplications="false">
<system.webServer>
<handlers>
<add name="iisnode" path="main.js" verb="*" modules="iisnode"/>
</handlers>
<aspNetCore processPath="dotnet" arguments=".\HW.Web2.dll" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" />
<rewrite>
<rules>
<rule name="LogFile" patternSyntax="ECMAScript" stopProcessing="true">
<match url="iisnode"/>
</rule>
<rule name="StaticContent">
<action type="Rewrite" url="{{REQUEST_URI}}"/>
</rule>
<rule name="DynamicContent">
<conditions>
<add input="{{REQUEST_FILENAME}}" matchType="IsFile" negate="True"/>
</conditions>
<action type="Rewrite" url="main.js"/>
</rule>
</rules>
</rewrite>
<iisnode devErrorsEnabled="false"
debuggingEnabled="false"
loggingEnabled="false"
nodeProcessCommandLine="C:\Program Files\nodejs\node.exe" />
</system.webServer>
</location>
</configuration>
angular 应用的 body 标记无法在服务器端呈现的原因有多种。这是一个清单:
- 首先确保你的直播环境支持NodeJS。服务端没有NodeJS,无法使用server-side渲染
- 在 Visual Studio 中,尝试将您的
ASPNETCORE_ENVIRONMENT
环境变量从 DEVELOPMENT
更改为 PRODUCTION
和 运行 您的应用程序。在某些情况下,应用程序在任一配置中的行为都不同(在 PRODUCTION
时在另一个位置查找 main.js
文件)。启动调试器并尝试预呈现视图后,您可能会在 Visual Studio 输出 window 中看到一些异常。
- 在我的例子中,
main.js
文件必须以 ClientApp/dist/main.js
结尾。所以我不得不修改 angular.json
将 projects:ClientApp:architect:build:options:outputPath
更改为 dist
(see also)
- 如果您在使用 Visual Studio 时遇到此问题,请务必查看 输出 window 中的错误,这将为您指明正确的方向。
- 如果您正在托管 PWA(例如通过
@angular/pwa
),那么在转到 查看源代码 时出现空白页面是完全正常的在浏览器中。如果你然后 ctrl + F5,你将绕过 angular 服务工作者,它向你显示预呈现的 html 来源。这是你不应该操心的事情。 Google、Bing、...实际上会获取并索引页面的 server-side 渲染版本。
- 如果您使用的是 ASP.NET Core 3.1 或更早版本,则您的
SupplyData
委托不能有 async
lambda。 SupplyData
不是 Func<Task>
而是 not being awaited in the source code of ASP.NET Core. I changed this in my port to .NET 6
options.SupplyData = async (context, data) => { ... };
- 一些实时环境会阻止您的 angular 应用程序在 SSR 期间发送回您的服务器的 Web 请求。在这种情况下,您将收到内部服务器错误 (500)。您需要使用
OnSupplyData
. 来解决这个问题
在 localhost 模块上,延迟加载工作正常,在查看页面源代码中显示元标记和 HTML 中的 <app-root><app-root/>
内容,但在实时服务器上没有显示。在实时服务器上,我只能看到 AppModule
的直接子组件的元标记和 HTML 内容,但延迟加载的模块组件不显示元标记和 HTML。这种奇怪的行为只存在于实时服务器上。
app.server.module.ts
import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { AppComponent } from './app.component';
import { AppModule } from './app.module';
@NgModule({
imports: [AppModule, ServerModule],
bootstrap: [AppComponent]
})
export class AppServerModule { }
main.server.ts
import '@angular/localize/init'
import '@angular/platform-server/init';
import { enableProdMode } from '@angular/core';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
export { AppServerModule } from './app/app.server.module';
export { renderModule } from '@angular/platform-server';
server.ts
import '@angular/localize/init';
import 'zone.js/dist/zone-node';
import 'reflect-metadata';
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, readFileSync } from 'fs';
import { createWindow } from 'domino';
import 'localstorage-polyfill'
import 'localstorage-polyfill'
const scripts = readFileSync('dist/clientWeb/browser/index.html').toString();
const window = createWindow(scripts) as any;
(global as any).window = window;
(global as any).document = window.document;
(global as any).Event = window.Event;
(global as any).KeyboardEvent = window.KeyboardEvent;
(global as any).MouseEvent = window.MouseEvent;
// 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/clientWeb/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);
// Example Express Rest API endpoints
// server.get('/api/**', (req, res) => { });
// Serve static files from /browser
server.get('*.*', express.static(distFolder, {
maxAge: '1y'
}));
// All regular routes use the Universal engine
server.get('*', (req, res) => {
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}`);
});
}
const MockBrowser = require('mock-browser').mocks.MockBrowser;
const mock = new MockBrowser();
(global as any).localStorage = localStorage;
// 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';
app.modules.ts
import { BrowserModule, HammerGestureConfig, HAMMER_GESTURE_CONFIG, Title } from '@angular/platform-browser';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { AppLayoutComponent } from './shared/layout/app-layout/app-layout.component';
import { NgModule, CUSTOM_ELEMENTS_SCHEMA, ModuleWithProviders } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AppRoutingModule } from './app-routing.module';
import { HttpClientModule } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { Tradesmanapplayout2Component } from './shared/layout/tradesmanapplayout2/tradesmanapplayout2.component';
import { Supplierapplayout2Component } from './shared/layout/supplierapplayout2/supplierapplayout2.component';
import { Userapplayout2Component } from './shared/layout/userapplayout2/userapplayout2.component';
import { AppDasboardHeader2Component } from './shared/layout/app-dasboard-header2/app-dasboard-header2.component';
import { AppDasboardFooter2Component } from './shared/layout/app-dasboard-footer2/app-dasboard-footer2.component';
const routes: Routes = [
{
path: 'resetpassword',
component: AppHeaderLayoutComponent,
loadChildren: () => import('./common/resetPassword/resetpassword.module').then(m => m.ResetpasswordModule)
},
{
path: 'Supplier',
component: Userapplayout2Component,
loadChildren: () => import('./supplier/supplier.module').then(m => m.SupplierModule),
},
{
path: 'User',
component: Userapplayout2Component,
loadChildren: () => import('./user/user.module').then(m => m.UserModule),
},
{
path: 'MarketPlace',
//component: UsersApplayoutComponent,
//component: AppHeaderLayoutComponent,
component: Userapplayout2Component,
loadChildren: () => import('./marketplace/marketplace.module').then(m => m.MarketplaceModule),
},
{
path: 'User/Agrements',
//component: AppLayoutComponent,
component: AppHeaderLayoutComponent,
loadChildren: () => import('./agrements/agrements.module').then(m => m.AgrementsModule)
},
{
path: 'HWUser',
//component: AppLayoutComponent,
component: AppHeaderLayoutComponent,
loadChildren: () => import('./HelpAndFAQ/helpQuestion.module').then(m => m.HelpQuestionModule)
},
{
path: 'Tradesman',
component: Userapplayout2Component,
loadChildren: () => import('./trademan/trademan.module').then(m => m.TrademanModule),
canActivate: [AuthGuardTradesmanService],
canActivateChild: [AuthGuardTradesmanService]
},
{
path: 'ContactUs',
//component: AppLayoutComponent,
component: AppHeaderLayoutComponent,
loadChildren: () => import('./contactUs/contactUs.module').then(m => m.ContactUsModule)
},
{
path: 'landing-page/liveleads',
//component: AppLayoutComponent,
component: AppHeaderLayoutComponent,
loadChildren: () => import('./landing-page/liveleads/liveleads.module').then(m => m.LiveleadsModule)
},
{
path: 'landing-page',
component: AppHeaderLayoutComponent,
loadChildren: () => import('./landing-page/landing-page.module').then(m => m.LandingPageModule)
}
]
@NgModule({
declarations: [
AppComponent,
AppLayoutComponent,
AppDasboardHeaderComponent,
AppDasboardFooterComponent,
AppLeftmenuComponent,
AppcommonfooterComponent,
SupplierLayoutComponent,
TrademanLayoutComponent,
TrademenuLeftComponent,
SupplierLeftmenuComponent,
],
imports: [
BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }),
NgbModule,
RouterModule.forRoot(routes, { enableTracing: false }),
ModalModule.forRoot(),
HttpModule,
BrowserAnimationsModule,
NgxImageCompressService,
Events,
Title,
metaTagsService,
ShareService,
],
schemas: [
CUSTOM_ELEMENTS_SCHEMA
],
bootstrap: [AppComponent]
})
export class AppModule { }
已在 IISWeb.config 上部署
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<location path="." inheritInChildApplications="false">
<system.webServer>
<handlers>
<add name="iisnode" path="main.js" verb="*" modules="iisnode"/>
</handlers>
<aspNetCore processPath="dotnet" arguments=".\HW.Web2.dll" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" />
<rewrite>
<rules>
<rule name="LogFile" patternSyntax="ECMAScript" stopProcessing="true">
<match url="iisnode"/>
</rule>
<rule name="StaticContent">
<action type="Rewrite" url="{{REQUEST_URI}}"/>
</rule>
<rule name="DynamicContent">
<conditions>
<add input="{{REQUEST_FILENAME}}" matchType="IsFile" negate="True"/>
</conditions>
<action type="Rewrite" url="main.js"/>
</rule>
</rules>
</rewrite>
<iisnode devErrorsEnabled="false"
debuggingEnabled="false"
loggingEnabled="false"
nodeProcessCommandLine="C:\Program Files\nodejs\node.exe" />
</system.webServer>
</location>
</configuration>
angular 应用的 body 标记无法在服务器端呈现的原因有多种。这是一个清单:
- 首先确保你的直播环境支持NodeJS。服务端没有NodeJS,无法使用server-side渲染
- 在 Visual Studio 中,尝试将您的
ASPNETCORE_ENVIRONMENT
环境变量从DEVELOPMENT
更改为PRODUCTION
和 运行 您的应用程序。在某些情况下,应用程序在任一配置中的行为都不同(在PRODUCTION
时在另一个位置查找main.js
文件)。启动调试器并尝试预呈现视图后,您可能会在 Visual Studio 输出 window 中看到一些异常。- 在我的例子中,
main.js
文件必须以ClientApp/dist/main.js
结尾。所以我不得不修改angular.json
将projects:ClientApp:architect:build:options:outputPath
更改为dist
(see also)
- 在我的例子中,
- 如果您在使用 Visual Studio 时遇到此问题,请务必查看 输出 window 中的错误,这将为您指明正确的方向。
- 如果您正在托管 PWA(例如通过
@angular/pwa
),那么在转到 查看源代码 时出现空白页面是完全正常的在浏览器中。如果你然后 ctrl + F5,你将绕过 angular 服务工作者,它向你显示预呈现的 html 来源。这是你不应该操心的事情。 Google、Bing、...实际上会获取并索引页面的 server-side 渲染版本。 - 如果您使用的是 ASP.NET Core 3.1 或更早版本,则您的
SupplyData
委托不能有async
lambda。SupplyData
不是Func<Task>
而是 not being awaited in the source code of ASP.NET Core. I changed this in my port to .NET 6
options.SupplyData = async (context, data) => { ... };
- 一些实时环境会阻止您的 angular 应用程序在 SSR 期间发送回您的服务器的 Web 请求。在这种情况下,您将收到内部服务器错误 (500)。您需要使用
OnSupplyData
. 来解决这个问题