Angular Universal übernimmt das Rendern auf dem Client nicht

Sep 08 2020

Ich versuche derzeit, meiner Winkel-App Server Side Rendering (SSR) hinzuzufügen, indem ich der offiziellen Winkelanleitung für Angular Universal folge . Ich konnte den Express-Server einrichten und die Antwort, die ich vom Server erhalte, enthält meine App, obwohl sie etwas eigenartig aussieht, als ob etwas CSS fehlen würde, was dazu führen würde, dass FontAwesome- Symbole zu groß gerendert werden usw.

Aus irgendeinem Grund übernimmt der Client / Browser niemals das (erneute) Rendern und die Website bleibt dieselbe wie die Antwort vom Server. Es ist nicht interaktiv und vollständig statisch. Wenn Sie auf Steuerelemente (außer Links) oder interaktive Elemente klicken, geschieht nichts.

Hier sehen Sie Teile der App Shell / HTML .

Hier ist der Netzwerkverkehr .

Die package.json :

    {
      "name": "my-app",
      "version": "0.0.0",
      "scripts": {
        "ng": "ng",
        "start": "ng serve",
        "build": "ng build",
        "test": "ng test",
        "lint": "ng lint",
        "e2e": "ng e2e",
        "dev:ssr": "ng run my-app:serve-ssr",
        "serve:ssr": "node dist/my-app/server/main.js",
        "build:ssr": "ng build --prod && ng run my-app:server:production",
        "prerender": "ng run my-app:prerender"
      },
      "private": true,
      "dependencies": {
        "@agm/core": "^1.1.0",
        "@angular/animations": "^10.0.14",
        "@angular/common": "^10.0.14",
        "@angular/compiler": "^10.0.14",
        "@angular/core": "^10.0.14",
        "@angular/elements": "^10.0.14",
        "@angular/forms": "^10.0.14",
        "@angular/localize": "~10.0.14",
        "@angular/platform-browser": "^10.0.14",
        "@angular/platform-browser-dynamic": "^10.0.14",
        "@angular/platform-server": "^10.0.14",
        "@angular/router": "^10.0.14",
        "@angular/service-worker": "^10.0.14",
        "@fortawesome/angular-fontawesome": "^0.7.0",
        "@fortawesome/fontawesome-svg-core": "^1.2.30",
        "@fortawesome/free-brands-svg-icons": "^5.14.0",
        "@fortawesome/free-regular-svg-icons": "^5.14.0",
        "@fortawesome/free-solid-svg-icons": "^5.14.0",
        "@ng-bootstrap/ng-bootstrap": "^7.0.0",
        "@ng-select/ng-select": "^4.0.4",
        "@nguniversal/express-engine": "^10.0.2",
        "@webcomponents/webcomponentsjs": "^2.2.10",
        "bootstrap": "^4.3.1",
        "bufferutil": "^4.0.1",
        "core-js": "^2.5.4",
        "document-register-element": "^1.13.2",
        "domino": "^2.1.6",
        "express": "^4.15.2",
        "html-webpack-plugin": "^2.30.1",
        "intersection-observer": "^0.7.0",
        "jquery": "^3.5.1",
        "ng-lazyload-image": "^5.1.2",
        "ng5-slider": "^1.1.14",
        "ngx-cookie-service": "^2.2.0",
        "ngx-infinite-scroll": "^7.1.0",
        "popper.js": "^1.14.7",
        "rxjs": "^6.6.2",
        "tslib": "^2.0.0",
        "utf-8-validate": "^5.0.2",
        "webpack": "^4.44.1",
        "xmlhttprequest": "^1.8.0",
        "zone.js": "~0.10.2"
      },
      "devDependencies": {
        "@angular-devkit/build-angular": "~0.1000.8",
        "@angular/cli": "^10.0.8",
        "@angular/compiler-cli": "^10.0.14",
        "@angular/language-service": "^10.0.14",
        "@nguniversal/builders": "^10.0.2",
        "@types/express": "^4.17.0",
        "@types/jasmine": "~2.8.8",
        "@types/jasminewd2": "~2.0.3",
        "@types/node": "^12.11.1",
        "codelyzer": "^6.0.0",
        "jasmine-core": "~3.5.0",
        "jasmine-spec-reporter": "~5.0.0",
        "karma": "~5.0.0",
        "karma-chrome-launcher": "~3.1.0",
        "karma-coverage-istanbul-reporter": "~3.0.2",
        "karma-jasmine": "~3.3.0",
        "karma-jasmine-html-reporter": "^1.5.0",
        "ngx-perfect-scrollbar": "^7.2.1",
        "node-sass": "^4.14.1",
        "protractor": "~7.0.0",
        "ts-node": "~7.0.0",
        "tslint": "~6.1.0",
        "typescript": "^3.9.7"
      }
    }

The angle.json :

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "my-app": {
      "i18n": {
        "locales": {
          "de-AT": {
            "translation": "src/locale/messages.de-AT.xlf",
            "baseHref": ""
          }
        }
      },
      "root": "",
      "sourceRoot": "src",
      "projectType": "application",
      "prefix": "app",
      "schematics": {
        "@schematics/angular:component": {
          "style": "scss"
        }
      },
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "watch": false,
            "index": "src/index.html",
            "outputPath": "dist/my-app/browser",
            "main": "src/main.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "src/tsconfig.app.json",
            "assets": [
              "src/favicon.ico",
              "src/apple-touch-icon.png",
              "src/apple-touch-icon-precomposed.png",
              "src/robots.txt",
              "src/sitemap.xml",
              "src/assets",
              "src/manifest.json"
            ],
            "styles": [
              "src/styles.scss"
            ],
            "scripts": [
              {
                "input": "node_modules/document-register-element/build/document-register-element.js"
              }
            ]
          },
          "configurations": {
            "production": {
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ],
              "i18nMissingTranslation": "error",
              "localize": [
                "de-AT"
              ],
              "optimization": true,
              "outputHashing": "bundles",
              "sourceMap": false,
              "extractCss": true,
              "namedChunks": false,
              "aot": true,
              "extractLicenses": true,
              "vendorChunk": false,
              "buildOptimizer": true,
              "budgets": [
                {
                  "type": "initial",
                  "maximumWarning": "2mb",
                  "maximumError": "3mb"
                },
                {
                  "type": "anyComponentStyle",
                  "maximumWarning": "20kb"
                }
              ],
              "serviceWorker": true,
              "ngswConfigPath": "src/ngsw-config.json"
            },
            "dev": {
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.dev.ts"
                }
              ],
              "aot": false,
              "i18nMissingTranslation": "error",
              "localize": [
                "de-AT"
              ],
              "optimization": false,
              "outputHashing": "none",
              "extractCss": true,
              "namedChunks": true,
              "extractLicenses": true,
              "vendorChunk": false,
              "budgets": [
                {
                  "type": "initial",
                  "maximumWarning": "10mb",
                  "maximumError": "12mb"
                },
                {
                  "type": "anyComponentStyle",
                  "maximumWarning": "20kb"
                }
              ],
            }
          }
        },
        "serve": {
          "builder": "@angular-devkit/build-angular:dev-server",
          "options": {
            "browserTarget": "my-app:build"
          },
          "configurations": {
            "production": {
              "browserTarget": "my-app:build:production"
            }
          }
        },
        "extract-i18n": {
          "builder": "@angular-devkit/build-angular:extract-i18n",
          "options": {
            "browserTarget": "my-app:build"
          }
        },
        "test": {
          "builder": "@angular-devkit/build-angular:karma",
          "options": {
            "main": "src/test.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "src/tsconfig.spec.json",
            "karmaConfig": "src/karma.conf.js",
            "styles": [
              "src/styles.scss"
            ],
            "scripts": [],
            "assets": [
              "src/favicon.ico",
              "src/robots.txt",
              "src/sitemap.xml",
              "src/assets",
              "src/manifest.json"
            ]
          }
        },
        "lint": {
          "builder": "@angular-devkit/build-angular:tslint",
          "options": {
            "tsConfig": [
              "src/tsconfig.app.json",
              "src/tsconfig.spec.json",
              "src/tsconfig.server.json"
            ],
            "exclude": [
              "**/node_modules/**"
            ]
          }
        },
        "server": {
          "builder": "@angular-devkit/build-angular:server",
          "options": {
            "outputPath": "dist/my-app/server",
            "main": "server.ts",
            "tsConfig": "src/tsconfig.server.json"
          },
          "configurations": {
            "production": {
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ],
              "sourceMap": false,
              "optimization": true
            }
          }
        },
        "serve-ssr": {
          "builder": "@nguniversal/builders:ssr-dev-server",
          "options": {
            "browserTarget": "my-app:build",
            "serverTarget": "my-app:server"
          },
          "configurations": {
            "production": {
              "browserTarget": "my-app:build:production",
              "serverTarget": "my-app:server:production"
            }
          }
        },
        "prerender": {
          "builder": "@nguniversal/builders:prerender",
          "options": {
            "browserTarget": "my-app:build:production",
            "serverTarget": "my-app:server:production",
            "routes": [
              "/"
            ]
          },
          "configurations": {
            "production": {}
          }
        }
      }
    },
    "my-app-e2e": {
      "root": "e2e/",
      "projectType": "application",
      "prefix": "",
      "architect": {
        "e2e": {
          "builder": "@angular-devkit/build-angular:protractor",
          "options": {
            "protractorConfig": "e2e/protractor.conf.js",
            "devServerTarget": "my-app:serve"
          },
          "configurations": {
            "production": {
              "devServerTarget": "my-app:serve:production"
            }
          }
        },
        "lint": {
          "builder": "@angular-devkit/build-angular:tslint",
          "options": {
            "tsConfig": "e2e/tsconfig.e2e.json",
            "exclude": [
              "**/node_modules/**"
            ]
          }
        }
      }
    }
  },
  "defaultProject": "my-app"
}

Die server.ts :

import 'zone.js/dist/zone-node';

import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import { join } from 'path';

const domino = require('domino');
const fs = require('fs');
const path = require('path');

const template = fs
  .readFileSync(path.join('dist/my-app/browser/de-AT', 'index.html'))
  .toString();
const window = domino.createWindow(template);

// Ignite UI browser objects abstractions
(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;
(global as any).FocusEvent = window.FocusEvent;
(global as any).PointerEvent = window.PointerEvent;
(global as any).HTMLElement = window.HTMLElement;
(global as any).HTMLElement.prototype.getBoundingClientRect = () => {
    return {
      left: '',
      right: '',
      top: '',
      bottom: ''
  };
};

(global as any).XMLHttpRequest = require('xmlhttprequest').XMLHttpRequest;

// Other optional depending on your application configuration
(global as any).object = window.object;
(global as any).navigator = window.navigator;
(global as any).localStorage = window.localStorage;
(global as any).DOMTokenList = window.DOMTokenList;

Object.defineProperty(window.document.body.style, 'transform', {
  value: () => {
    return {
      enumerable: true,
      configurable: true,
    };
  },
});
(global as any).CSS = null;
(global as any).Prism = null;

import { AppServerModule } from './src/main.server';
import { APP_BASE_HREF } from '@angular/common';
import { existsSync } from 'fs';

// 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/my-app/browser/de-AT');
  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}`);
  });
}

// 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';

Die tsconfig.base.json :

{
  "compileOnSave": false,
  "compilerOptions": {
    "baseUrl": "./",
    "outDir": "./dist/out-tsc",
    "sourceMap": true,
    "declaration": false,
    "module": "es2020",
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "importHelpers": true,
    "target": "es2015",
    "downlevelIteration": true,
    "typeRoots": [
      "node_modules/@types",
      "typings.d.ts"
    ],
    "lib": [
      "es2018",
      "dom"
    ]
  }
}

Die tsconfig.server.json :

{
  "extends": "./tsconfig.app.json",
  "compilerOptions": {
    "outDir": "../out-tsc/server",
    "target": "es2016",
    "types": [
      "node"
    ]
  },
  "files": [
    "main.server.ts",
    "../server.ts"
  ],
  "angularCompilerOptions": {
    "entryModule": "./app/app.server.module#AppServerModule"
  }
}

Die app.server.module.ts :

    import { NgModule } from '@angular/core';
    import { ServerModule } from '@angular/platform-server';
    import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
    import { UniversalInterceptor } from './universal-interceptor';
    import { AppModule } from './app.module';
    import { AppComponent } from './app.component';

    @NgModule({
      imports: [
        AppModule,
        ServerModule
      ],
      providers: [
        {
          provide: HTTP_INTERCEPTORS,
          useClass: UniversalInterceptor,
          multi: true
        }
      ],
      bootstrap: [AppComponent]
    })
    export class AppServerModule {}

Ich weiß wirklich nicht, was ich falsch mache. Irgendwelche Ideen?

Antworten

1 SkWiirrel Sep 15 2020 at 13:50

Nach langen Nachforschungen habe ich den Täter gefunden. Es scheint, als hätte die CLI beim Hinzufügen von Angular Universal den DOMContentLoadedEreignis-Listener umbrochen, um die App um einen anderen DOMContentLoadedEreignis-Listener in der Datei main.ts zu booten . So sah es aus:

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
  enableProdMode();
}

document.addEventListener('DOMContentLoaded', () => { // Remove this
  document.addEventListener('DOMContentLoaded', () => {
    platformBrowserDynamic().bootstrapModule(AppModule)
      .catch(err => console.error(err));
  });
}); // Remove this

Durch das Entfernen der zweiten Ereignis-Listener wurde der App-Bootstrap erneut ausgeführt.