Angular Universal non si occupa del rendering sul client

Sep 08 2020

Attualmente sto cercando di aggiungere Server Side Rendering (SSR) alla mia app angolare seguendo la guida angolare ufficiale per angular universal . Sono stato in grado di configurare il server espresso e la risposta che ricevo dal server contiene la mia app, anche se sembra un po' bizzarra come se mancasse del CSS con il risultato che le icone di FontAwesome vengono rese troppo grandi, ecc.

Per qualche motivo, tuttavia, il client/browser non assume mai il (ri)rendering e il sito Web rimane lo stesso come è arrivato come risposta dal server. Non è interattivo e completamente statico. Quando si fa clic sui controlli (ad eccezione dei collegamenti) o sugli elementi interattivi, non accade nulla.

Qui puoi vedere parti dell'App Shell/HTML .

Ecco il traffico di rete .

Il pacchetto.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"
      }
    }

Angular.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"
}

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

Il 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"
    ]
  }
}

Il 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"
  }
}

L' 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 {}

Non so davvero cosa sto sbagliando. Qualche idea?

Risposte

1 SkWiirrel Sep 15 2020 at 13:50

Dopo lunghe indagini ho trovato il colpevole. Sembra che quando si aggiunge Angular Universal, la CLI abbia avvolto il DOMContentLoadedlistener di eventi per eseguire il bootstrap dell'app attorno a un altro DOMContentLoadedlistener di eventi nel file main.ts. Ecco come appariva:

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

Quindi la rimozione del secondo listener di eventi ha ripristinato il bootstrap dell'app.