Angular Universal SSR API 调用两次,一次来自前端,一次来自后端。

huangapple go评论53阅读模式
英文:

Angular Universal SSR API Calling two time one from Frontend and Second fro backend

问题

I am trying to achieve SSR using Angular Universal, but facing two problems.

  1. 我正试图使用Angular Universal实现SSR,但面临两个问题:

I getting two hits on my server as I can see in server logs, one from frontend and the second from the backend.

  1. 在Universal服务器调用API时,没有将数据注入到组件中以绑定数据。

my package.json

  1. 我的package.json文件:
{
  "name": "web-client",
  "version": "0.0.0",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build --prod",
    "watch": "ng build --watch --configuration development",
    "test": "ng test",
    "dev:ssr": "ng run web-client:serve-ssr",
    "serve:ssr": "node dist/web-client/server/main.js",
    "build:ssr": "ng build && ng run web-client:server",
    "prerender": "ng run web-client:prerender",
    "build:stats": "ng build --stats-json",
    "analyze": "webpack-bundle-analyzer dist/web-client/browser/stats.json"
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "^15.0.0",
    "@angular/cdk": "^15.0.3",
    "@angular/common": "^15.0.0",
    "@angular/compiler": "^15.0.0",
    "@angular/core": "^15.0.0",
    "@angular/forms": "^15.0.0",
    "@angular/material": "^15.0.3",
    "@angular/platform-browser": "^15.0.0",
    "@angular/platform-browser-dynamic": "^15.0.0",
    "@angular/platform-server": "^15.0.0",
    "@angular/router": "^15.0.0",
    "@auth0/auth0-angular": "^2.0.1",
    "@nestjs/common": "^9.3.2",
    "@nestjs/core": "^9.3.2",
    "@nguniversal/express-engine": "^15.0.0",
    "express": "^4.15.2",
    "http-proxy-middleware": "^2.0.6",
    "ngx-spinner": "^15.0.1",
    "rxjs": "~7.5.0",
    "tslib": "^2.3.0",
    "zone.js": "~0.12.0"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "^15.0.4",
    "@angular/cli": "~15.0.4",
    "@angular/compiler-cli": "^15.0.0",
    "@nguniversal/builders": "^15.0.0",
    "@types/express": "^4.17.0",
    "@types/jasmine": "~4.3.0",
    "@types/node": "^14.15.0",
    "jasmine-core": "~4.5.0",
    "karma": "~6.4.0",
    "karma-chrome-launcher": "~3.1.0",
    "karma-coverage": "~2.2.0",
    "karma-jasmine": "~5.1.0",
    "karma-jasmine-html-reporter": "~2.0.0",
    "typescript": "~4.8.2",
    "webpack-bundle-analyzer": "^4.7.0"
  }
}

and server.ts file

  1. 以及server.ts文件:
import 'zone.js/node';

import { APP_BASE_HREF } from '@angular/common';
import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import { existsSync } from 'fs';
import { join } from 'path';
const { createProxyMiddleware } = require('http-proxy-middleware');
import { AppServerModule } from './src/main.server';

// 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/b-jobz-web-client/browser');
  const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';

  const options = {
    target: 'http://localhost:8080', // target host
    pathRewrite: {
      '^/api': ''
    },
    logLevel: 'debug',
  };

  server.use(
    '/staging',
    createProxyMiddleware({
      target: 'http://localhost:3002',
      changeOrigin: true,
      pathRewrite: {
        '^/api': ''
      },
      logLevel: 'debug',
    })
  );

  // Our Universal express-engine (found @ https://github.com/angular/universal/tree/main/modules/express-engine)
  server.engine('html', ngExpressEngine({
    bootstrap: AppServerModule,
  }));

  server.set('view engine', 'html');
  server.set('views', distFolder);

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

API call should be made from the backend and inject the response into the Angular component to achieve SSR.

  1. API调用应该从后端进行,并将响应注入到Angular组件以实现SSR。
英文:

I am trying to achieve SSR using Angular Universal, but facing two problem

  1. I getting two hit on my server as i can see in server logs, one from frontend and second from backend.

  2. when universal sever is calling the api, not injecting data to component to bind the data.

my package.json

{
"name": "web-client",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build --prod",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"dev:ssr": "ng run web-client:serve-ssr",
"serve:ssr": "node dist/web-client/server/main.js",
"build:ssr": "ng build && ng run web-client:server",
"prerender": "ng run web-client:prerender",
"build:stats": "ng build --stats-json",
"analyze": "webpack-bundle-analyzer dist/web-client/browser/stats.json"
},
"private": true,
"dependencies": {
"@angular/animations": "^15.0.0",
"@angular/cdk": "^15.0.3",
"@angular/common": "^15.0.0",
"@angular/compiler": "^15.0.0",
"@angular/core": "^15.0.0",
"@angular/forms": "^15.0.0",
"@angular/material": "^15.0.3",
"@angular/platform-browser": "^15.0.0",
"@angular/platform-browser-dynamic": "^15.0.0",
"@angular/platform-server": "^15.0.0",
"@angular/router": "^15.0.0",
"@auth0/auth0-angular": "^2.0.1",
"@nestjs/common": "^9.3.2",
"@nestjs/core": "^9.3.2",
"@nguniversal/express-engine": "^15.0.0",
"express": "^4.15.2",
"http-proxy-middleware": "^2.0.6",
"ngx-spinner": "^15.0.1",
"rxjs": "~7.5.0",
"tslib": "^2.3.0",
"zone.js": "~0.12.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^15.0.4",
"@angular/cli": "~15.0.4",
"@angular/compiler-cli": "^15.0.0",
"@nguniversal/builders": "^15.0.0",
"@types/express": "^4.17.0",
"@types/jasmine": "~4.3.0",
"@types/node": "^14.15.0",
"jasmine-core": "~4.5.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.0.0",
"typescript": "~4.8.2",
"webpack-bundle-analyzer": "^4.7.0"
}
}

and server.ts file


import 'zone.js/node';
import { APP_BASE_HREF } from '@angular/common';
import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import { existsSync } from 'fs';
import { join } from 'path';
const { createProxyMiddleware } = require('http-proxy-middleware');
import { AppServerModule } from './src/main.server';
// 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/b-jobz-web-client/browser');
const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';
const options = {
target: 'http://localhost:8080', // target host
pathRewrite: {
'^/api': ''
},
logLevel: 'debug',
};
server.use(
'/staging',
createProxyMiddleware({
target: 'http://localhost:3002',
changeOrigin: true,
pathRewrite: {
'^/api': ''
},
logLevel: 'debug',
})
);
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/main/modules/express-engine)
server.engine('html', ngExpressEngine({
bootstrap: AppServerModule,
}));
server.set('view engine', 'html');
server.set('views', distFolder);
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';

API call should be called from backend and inject the response to angular component to achieve SSR.

答案1

得分: 2

以下是您要翻译的内容的中文翻译:

"这种情况的主要原因是Angular Universal的工作原理。它将在服务器上渲染页面并将其发送到浏览器。一旦到达浏览器,Angular部分开始运行。这包括API调用和其他I/O操作。这就是它的工作方式。但可以使用TransferState API来避免这种情况。这可以有多种方式来实现。一种常见的方式是在拦截器中使用它。逻辑应该如下所示。

在应用程序服务器模块的TypeScript中添加:

providers: [
   {
     provide: HTTP_INTERCEPTORS,
     useClass: ServerStateInterceptor,
     multi: true
   },
],

在应用程序模块中添加:

providers: [
   {
     provide: HTTP_INTERCEPTORS,
     useClass: BrowserStateInterceptor,
     multi: true
   },
]

现在,在服务器拦截器中:

intercept(req: HttpRequest<any>, next: HttpHandler) {
    return next.handle(req).pipe(
      tap(event => {
        if (req.method === 'POST' || req.method === 'GET') {
          if (event instanceof HttpResponse && (event.status === 200 || event.status === 202)) {
            let key: any = "";
            if (req.url !== null) {
              key = req.url;
            }
            this.transferState.set(makeStateKey(key), event.body);
          }
        }
      }),
    );
  }

现在,您只需在浏览器拦截器中从传输状态获取数据:

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    if (req.method === 'POST' || req.method === 'GET') {
        let postKey: any = "";
        if (req.url !== null) {
          postKey = req.url as string;
        }
        const key = makeStateKey(postKey);
        const storedResponse = this.transferState.get(key, null);
        if (storedResponse) {
          const response = new HttpResponse({ body: storedResponse, status: 200 });
          return of(response);
        }
    }
  }

这将阻止您的应用程序两次调用服务器。您可以实现其他类型的解决方案,例如使用解析器,这取决于您的架构。但多多少少,这将解决您的问题。"

英文:

The main reason for such case is how angular universal works. It will render your page on the server and send it to browser. Once it reaches the browser, the angular portion kicks in and starts running. That is you api calls and other i/o will take place again. That is how it works. But it can be avoided using TransferState api. This can be implied in many ways. A popular way can be using it in interceptors. The logic shall work like following.

You will fetch the data on the server and save it using transferstate api and in browser interceptor you can check the transferstate api for the data and return that as response stopping the browser from making any call. The code shall work like following

In you app server modules ts add

  providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: ServerStateInterceptor,
multi: true
},
],

In your app module add

  providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: BrowserStateInterceptor,
multi: true
},
]

Now in your server interceptor

 intercept(req: HttpRequest&lt;any&gt;, next: HttpHandler) {
return next.handle(req).pipe(
tap(event =&gt; {
if(req.method===&#39;POST&#39; || req.method===&#39;GET&#39;){
if ((event instanceof HttpResponse &amp;&amp; (event.status === 200 || event.status === 202))) {
let key: any=&quot;&quot;;
if(req.url!==null){
key=req.url
}
this.transferState.set(makeStateKey(key), event.body);
}
}
}),
);
}

Now you just get the data from transfer state in your browser interceptor

 intercept(req: HttpRequest&lt;any&gt;, next: HttpHandler): Observable&lt;HttpEvent&lt;any&gt;&gt; {
if (req.method === &#39;POST&#39;|| req.method === &#39;GET&#39;) {
let postKey: any=&quot;&quot;;
if(req.url!==null){
postKey = req.url as string;
}
const key = makeStateKey(postKey);
const storedResponse = this.transferState.get(key, null);
if (storedResponse) {
const response = new HttpResponse({body: storedResponse, status: 200});
return of(response);
}
}
}

This will stop your app from calling the server twice. You may implement other types of implementation such as using resolvers. It depends on your architecture. But more or less this will solve your case.

答案2

得分: 0

Browser interceptor:

import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { TransferState, makeStateKey } from "@angular/platform-browser";
import { Observable, of } from "rxjs";

@Injectable()
export class BrowserStateInterceptor implements HttpInterceptor {

    constructor(private transferState: TransferState) { 
    }
      
    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        if (req.method === 'POST' || req.method === 'GET') {
            let postKey: any = "";
            if (req.url !== null) {
                postKey = req.url as string;
            }
            const key = makeStateKey(postKey);
            const storedResponse = this.transferState.get(key, null);
            if (storedResponse) {
                const response = new HttpResponse({ body: storedResponse, status: 200 });
                return of(response);
            }
        }
        return next.handle(req);
    }
}

Server interceptor:

import { HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { TransferState, makeStateKey } from "@angular/platform-browser";
import { tap } from "rxjs";

@Injectable()
export class ServerStateInterceptor implements HttpInterceptor {

    constructor(private transferState: TransferState) { 
    }

    intercept(req: HttpRequest<any>, next: HttpHandler) {
        return next.handle(req).pipe(
            tap(event => {
                if (req.method === 'POST' || req.method === 'GET') {

                    if ((event instanceof HttpResponse && (event.status === 200 || event.status === 202))) {
                        let key: any = "";
                        if (req.url !== null) {
                            key = req.url;
                        }
                        this.transferState.set(makeStateKey(key), event.body);
                    }
                }
            }),
        );
    }
}
英文:

Browser interceptor:

        import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from &quot;@angular/common/http&quot;;
import { Injectable } from &quot;@angular/core&quot;;
import { TransferState, makeStateKey } from &quot;@angular/platform-browser&quot;;
import { Observable, of } from &quot;rxjs&quot;;
@Injectable()
export class BrowserStateInterceptor implements HttpInterceptor {
constructor(private transferState: TransferState) { 
}
intercept(req: HttpRequest&lt;any&gt;, next: HttpHandler): Observable&lt;HttpEvent&lt;any&gt;&gt;{
if (req.method === &#39;POST&#39;|| req.method === &#39;GET&#39;) {
let postKey: any=&quot;&quot;;
if(req.url!==null){
postKey = req.url as string;
}
const key = makeStateKey(postKey);
const storedResponse = this.transferState.get(key, null);
if (storedResponse) {
const response = new HttpResponse({body: storedResponse, status: 200});
return of(response);
}
}
return next.handle(req);
}
}

Server interceptor:

import { HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from &quot;@angular/common/http&quot;;
import { Injectable } from &quot;@angular/core&quot;;
import { TransferState, makeStateKey } from &quot;@angular/platform-browser&quot;;
import { tap } from &quot;rxjs&quot;;
@Injectable()
export class ServerStateInterceptor implements HttpInterceptor {
constructor(private transferState: TransferState) { 
}
intercept(req: HttpRequest&lt;any&gt;, next: HttpHandler) {
return next.handle(req).pipe(
tap(event =&gt; {
if(req.method===&#39;POST&#39; || req.method===&#39;GET&#39;){
if ((event instanceof HttpResponse &amp;&amp; (event.status === 200 || event.status === 202))) {
let key: any=&quot;&quot;;
if(req.url!==null){
key=req.url
}
this.transferState.set(makeStateKey(key), event.body);
}
}
}),
);
}
}

huangapple
  • 本文由 发表于 2023年2月6日 08:29:58
  • 转载请务必保留本文链接:https://go.coder-hub.com/75356451.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定