服务器端渲染不适用于实时服务器上的模块延迟加载 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.jsonprojects: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.
  • 来解决这个问题