Angular Universal, istemcide oluşturmayı devralmaz
Şu anda açısal evrensel için resmi açısal kılavuzu izleyerek açısal uygulamama Sunucu Tarafı Oluşturma (SSR) eklemeye çalışıyorum . Ekspres sunucuyu kurabildim ve sunucudan aldığım yanıt uygulamamı içeriyor, ancak bazı CSS'ler eksikmiş gibi biraz tuhaf görünmesine rağmen FontAwesome simgelerinin çok büyük görüntülenmesi vb.
Yine de bazı nedenlerden dolayı, istemci / tarayıcı asla (yeniden) oluşturmayı üstlenmez ve web sitesi sunucudan yanıt olarak geldiği gibi kalır. Etkileşimli ve tamamen statik değildir. Kontrollere (bağlantılar dışında) veya etkileşimli öğelere tıklandığında hiçbir şey olmuyor.
Burada App Shell / HTML'nin bölümlerini görebilirsiniz .
İşte Ağ Trafiği .
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"
}
}
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"
}
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';
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"
]
}
}
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"
}
}
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 {}
Neyi yanlış yaptığımı gerçekten bilmiyorum. Herhangi bir fikir?
Yanıtlar
Uzun bir araştırmadan sonra suçluyu buldum. Görünüşe göre Angular Universal eklenirken, CLI DOMContentLoadedolay dinleyicisini uygulamayı main.ts dosyasındaki başka bir DOMContentLoadedolay dinleyicisinin etrafında önyüklemek için sarmaladı . Öyle görünüyordu:
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
Bu nedenle, ikinci olay dinleyicilerini kaldırmak, uygulamanın yeniden başlatılmasını sağladı.