我正在尝试使用 Angular Universal 实现 SSR,但面临两个问题
正如我在服务器日志中看到的那样,我的服务器上收到了两次点击,一次来自前端,第二次来自后端。
通用服务器调用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/