使用Firebase云功能在i18n上提供有角度的10 ssr通用应用程序

问题描述

我正在使用由10个应用程序(网站,应用程序和管理员)组成的angular 10和nx(托管在firebase上)进行monorepo项目。 该网站和应用使用内置的@ angular / localize软件包进行了国际化。

现在,我正在网站中实现角度通用性,但是每次尝试访问域中的任何URL时,我都会从https云功能获取超时。

这是我到目前为止所做的:

  • / apps / website / src
  • 添加了main.server.ts
import '@angular/localize/init';

import { enableProdMode } from '@angular/core';

import { environment } from './environments/environment';

if (environment.production) {
  enableProdMode();
}

export { AppServerModule } from './app/app.server.module';
export { renderModule,renderModuleFactory } from '@angular/platform-server';
  • apps / website / src / app
  • 添加了app.server.module.ts
import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';

import { AppModule } from './app.module';
import { AppComponent } from './app.component';

@NgModule({
  imports: [
    AppModule,ServerModule
  ],bootstrap: [AppComponent]
})
export class AppServerModule {}

  • / apps / website
  • 添加了tsconfig.server.json
    {
      "extends": "./tsconfig.app.json","compilerOptions": {
        "outDir": "../../dist/out-tsc-server","module": "commonjs","types": [
          "node"
        ]
      },"files": [
        "src/main.server.ts","server.ts"
      ],"angularCompilerOptions": {
        "entryModule": "./src/app/app.server.module#AppServerModule"
      }
    }
  • / apps / website
  • 添加了server.js
import 'zone.js/dist/zone-node';

import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import { join } from 'path';

import { AppServerModule } from './src/main.server';
import { APP_BASE_HREF } from '@angular/common';
import { LOCALE_ID } from '@angular/core';

// The Express app is exported so that it can be used by serverless Functions.
// I pass a locale argument to fetch the correct i18n app in the browser folder
export function app(locale: string): express.Express {
  const server = express();
  // get the correct locale client app path for the server
  const distFolder = join(process.cwd(),`apps/functions/dist/website/browser/${locale}`);
  
  // Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
  server.engine(
    'html',ngExpressEngine({
      bootstrap: AppServerModule,providers: [{provide: LOCALE_ID,useValue: locale}] // define locale_id for the server
    })
  );

  server.set('views',distFolder);
  server.set('view engine','html');
  // For static files
  server.get(
    '*.*',express.static(distFolder,{
      maxAge: '1y',})
  );


  // For route paths
  // All regular routes use the Universal engine
  server.get('*',(req,res) => {
    // this line always shows up in the cloud function logs
    console.log(`serving request,with locale ${locale},base url: ${req.baseUrl},accept-language: ${req.headers["accept-language"]}`);
    res.render('index.html',{
      req,providers: [{ provide: APP_BASE_HREF,useValue: req.baseUrl }]
    });
  });

  return server;
}

// only used for testing in dev mode
function run(): void {
  const port = process.env.PORT || 4000;

  // Start up the Node server
  const appFr = app('fr');
  const appEn = app('en');
  const server = express();
  server.use('/fr',appFr);
  server.use('/en',appEn);
  server.use('',appEn);

  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')) {
  console.log('running server');
  run();
}

export * from './src/main.server';
  • / apps / functions / src / app / ssr-website 添加index.ts来定义将要部署的云功能

    import * as functions from 'firebase-functions';
    const express = require("express");    
    const getTranslatedServer = (lang) => {
        const translatedServer = require(`../../../dist/website/server/${lang}/main`);
        return translatedServer.app(lang);
    };
        
    const appSsrEn = getTranslatedServer('en');
    const appSsrFr = getTranslatedServer('fr');
    
    // dispatch,as a proxy,the translated server app function to their coresponding url
    const server = express();
    server.use("/",appSsrEn); // use english version as default
    server.use("/fr",appSsrFr);
    server.use("/en",appSsrEn);
        
    export const globalSsr = functions.https.onRequest(server);

要构建我的ssr应用程序,请使用以下npm命令: npm run deploy:pp:functions来自我的package.json:

...
"build:ppasprod:all-locales:website": "npm run fb:env:pp && ng build website -c=prod-core-optim,prod-budgets,pp-file-replace,all-locales","build:ssr:website": "npm run build:ppasprod:all-locales:website && ng run website:server:production","predeploy:website:functions": "nx workspace-lint && ng lint functions && node apps/functions/src/app/cp-universal.ts && ng build functions -c=production","deploy:pp:functions": "npm run fb:env:pp && npm run build:ssr:website && npm run predeploy:website:functions && firebase deploy --only functions:universal-globalSsr"
...

基本上,它会构建ssr应用程序,将 dist / website 文件夹复制到 apps / functions 中,构建云功能,然后将其部署到firebase。

这是用于配置的angular.json:


    {
      "projects": {
        "website": {
          "i18n": {
            "locales": {
              "fr": "apps/website/src/locale/messages.fr.xlf","en": "apps/website/src/locale/messages.en.xlf"
            }
          },"projectType": "application","schematics": {
            "@nrwl/angular:component": {
              "style": "scss"
            }
          },"root": "apps/website","sourceRoot": "apps/website/src","prefix": "","architect": {
            "build": {
              "builder": "@angular-devkit/build-angular:browser","options": {
                "outputPath": "dist/website/browser","deleteOutputPath": false,"index": "apps/website/src/index.html","main": "apps/website/src/main.ts","polyfills": "apps/website/src/polyfills.ts","tsConfig": "apps/website/tsconfig.app.json","aot": true,"assets": [
                  "apps/website/src/assets",{
                    "input": "libs/assets/src/lib","glob": "**/*","output": "./assets"
                  }
                ],"styles": [
                  "apps/website/src/styles.scss","libs/styles/src/lib/styles.scss"
                ],"scripts": [],"stylePreprocessorOptions": {
                  "includePaths": ["libs/styles/src/lib/"]
                }
              },"configurations": {
                "devlocal": {
                  "budgets": [
                    {
                      "type": "anyComponentStyle","maximumWarning": "6kb"
                    }
                  ]
                },"all-locales": {
                  "localize": ["en","fr"]
                },"pp-core-optim": {
                  "optimization": false,"i18nMissingTranslation": "error","sourceMap": true,"statsJson": true
                },"pp-file-replace": {
                  "fileReplacements": [
                    {
                      "replace": "apps/website/src/environments/environment.ts","with": "apps/website/src/environments/environment.pp.ts"
                    }
                  ]
                },"prod-budgets": {
                  "budgets": [
                    {
                      "type": "initial","maximumWarning": "2mb","maximumError": "5mb"
                    },{
                      "type": "anyComponentStyle","maximumWarning": "6kb","maximumError": "10kb"
                    }
                  ]
                },"prod-core-optim": {
                  "i18nMissingTranslation": "error","optimization": true,"outputHashing": "all","sourceMap": false,"extractCss": true,"namedChunks": false,"extractLicenses": true,"vendorChunk": false,"buildOptimizer": true
                }
              }
            },"extract-i18n": {
              "builder": "@angular-devkit/build-angular:extract-i18n","options": {
                "browserTarget": "website:build"
              }
            },"server": {
              "builder": "@angular-devkit/build-angular:server","options": {
                "outputPath": "dist/website/server","main": "apps/website/server.ts","tsConfig": "apps/website/tsconfig.server.json","externalDependencies": ["@firebase/firestore"],"configurations": {
                "production": {
                  "outputHashing": "media","fileReplacements": [
                    {
                      "replace": "apps/website/src/environments/environment.ts","with": "apps/website/src/environments/environment.prod.ts"
                    }
                  ],"localize": ["en","fr"]
                }
              }
            },"serve-ssr": {
              "builder": "@nguniversal/builders:ssr-dev-server","options": {
                "browserTarget": "website:build","serverTarget": "website:server"
              },"configurations": {
                "production": {
                  "browserTarget": "website:build:production","serverTarget": "website:server:production"
                }
              }
            },"prerender": {
              "builder": "@nguniversal/builders:prerender","options": {
                "browserTarget": "website:build:production","serverTarget": "website:server:production","routes": ["/"]
              },"configurations": {
                "production": {}
              }
            }
          }
        }
      }
    }

构建完成后,将使用以下结构创建一个 / dist 文件夹:

dist/
└───website/
│   └───browser/
│   │   └───en/
│   │   └───fr/
│   └───server/
│       └───en/
│       └───fr/

在将dist / website / browser上传到主机之前,我先删除 / dist / website / browser / en / dist / website / browser / fr中的index.html文件以确保托管服务器提供https功能(而不是index.html文件)。

最后,这是我对firebase(firebase.json)的配置:

{
  ...
  "hosting": [
    ...
    {
      "target": "website","public": "dist/website/browser","ignore": ["firebase.json","**/.*","**/node_modules/**"],"rewrites": [
        {
          "source": "**","function": "universal-globalSsr"
        }
      ]
    },...
  ],...
}

如前所述,一切都按预期构建,打包和部署。尝试访问https://www.my-domaine.com/fr/后,我的函数将被执行,但是我的日志中会出现服务器超时,没有任何错误。 如果我尝试访问一个不存在的URL(例如:https://www.my-domaine.com/fr/foo),则会收到错误消息“无法匹配任何路由。URL段:'foo'”,然后发生超时。

目前,我不知道我的代码或项目配置有什么问题。
任何帮助将不胜感激。

解决方法

对于那些将Angular Universal与Firebase一起使用时服务器处于无限加载状态的用户,我的问题来自于我的应用中特定的Firestore请求

在我的项目中,我在Rxjs中使用@ angular / fire。在应用程序初始化时,我正在一项服务中发出请求以预缓存配置对象,类似于以下内容:

 this.afs
     .collection<MyObject>(this.cl.COLLECTION_NAME_OBJECT)
     .snapshotChanges()
     .pipe(
       map((actions) =>
         actions.map((a) => {
           const data = a.payload.doc.data() as MyObject;
           const id = a.payload.doc.ref;
           return { id,...data };
         })
       ),take(1)
     )
     .subscribe((objects: MyObjects[]) => {
       this.myObjects = objects;
     });

出于某种原因,管道中的take(1)运算符负责保留服务器端。删除take(1)解决了问题。

我发现了这个问题,其中某些特定类型的Firestore请求违反了ssr(有关此问题的更多信息):https://github.com/angular/angularfire/issues/2420