Angular 通用 SSR API 从前端调用两次,从后端调用第二次

标签 angular angular-universal angular-ssr

我正在尝试使用 Angular Universal 实现 SSR,但面临两个问题

  1. 正如我在服务器日志中看到的那样,我的服务器上收到了两次点击,一次来自前端,第二次来自后端。

  2. 通用服务器调用api时,不向组件注入(inject)数据来绑定(bind)数据。

我的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"
  }
}

和 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调用并将响应注入(inject)到 Angular 组件以实现SSR。

最佳答案

出现这种情况的主要原因是 Angular Universal 的工作原理。它将在服务器上呈现您的页面并将其发送到浏览器。一旦到达浏览器, Angular 部分就会启动并开始运行。也就是说,您的 api 调用和其他 I/O 将再次发生。这就是它的工作原理。但可以使用 TransferState api 来避免。这可以通过多种方式来暗示。一种流行的方法是在拦截器中使用它。逻辑应如下所示。

您将在服务器上获取数据并使用transferstate api保存它,在浏览器拦截器中您可以检查transferstate api中的数据并将其作为响应返回,阻止浏览器进行任何调用。代码应如下工作

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

  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);
        }
    }
  }

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

关于Angular 通用 SSR API 从前端调用两次,从后端调用第二次,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/75356451/

相关文章:

express - Angular 2+ Universal是否可以从expressjs传递参数(不使用url查询字符串)?

node.js - 以 Angular 15 预加载 css

angular - 如何从一个组件调用另一个组件的方法 [Angular]

angular - 更新 cli 版本后创建新的 angular 项目时出错

reactjs - 为 React、Angular 等使用 HTTP 服务器

angular - 无法升级到 Angular 6,范围无效

javascript - 使用 jQuery 的 Angular 2+ 服务器端渲染

angular - 从 Angular 8 通用版迁移到 9 通用版失败 : require. 解析不是函数