使用 Firebase 云功能为带有 i18n 的 Angular 10 s-s-r 通用应用程序提供服务

Posted

技术标签:

【中文标题】使用 Firebase 云功能为带有 i18n 的 Angular 10 s-s-r 通用应用程序提供服务【英文标题】:Serving an angular 10 s-s-r universal app with i18n using firebase cloud functions 【发布时间】:2020-12-13 05:06:28 【问题描述】:

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

现在,我正在网站中实现 Angular Universal,但每次尝试从我的域访问任何 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/s-s-r-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);

为了构建我的 s-s-r 应用程序,我使用以下 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:s-s-r: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:s-s-r:website && npm run predeploy:website:functions && firebase deploy --only functions:universal-globalSsr"
...

基本上,它会构建 s-s-r 应用程序,复制 apps/functions 中的 dist/website 文件夹,构建云函数,然后将其部署到 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"],
                "stylePreprocessorOptions": 
                  "includePaths": ["libs/styles/src/lib/"]
                
              ,
              "configurations": 
                "production": 
                  "outputHashing": "media",
                  "fileReplacements": [
                    
                      "replace": "apps/website/src/environments/environment.ts",
                      "with": "apps/website/src/environments/environment.prod.ts"
                    
                  ],
                  "sourceMap": false,
                  "optimization": true,
                  "localize": ["en", "fr"]
                
              
            ,
            "serve-s-s-r": 
              "builder": "@nguniversal/builders:s-s-r-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'”,然后超时。

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

【问题讨论】:

这是一个相当不错的设置。功能是否正确完成?您是否检查过您的 App Engine 超时设置?我想说添加更多日志记录并简化您的应用程序以缩小问题范围。 函数执行仅在1分钟后超时完成。似乎 res.render 函数没有返回任何内容。顺便说一下,在 res.render 之前声明的 console.log 会打印在日志中。我已经使用通用和 firebase 制作了一个简单的 poc。该应用程序仅包含两条简单的路线。它按预期工作。服务器返回一个带有渲染 html 的页面。在这个项目中,还有 i18n。似乎问题来自本地化配置。我已经做了几处更改,看看是否有任何更改,但没有成功。 函数日志中还有其他内容吗? res.render 从不执行?如果你在console.log之后添加它会出现在日志中吗? @EmilGi 是的,放在后面的 console.log 也会出现在日志中。 我能问一下你有哪些包版本,你能得到firebase serve 来加载路由吗?您使用的是哪个版本的 firebase 和 angularfire? 【参考方案1】:

对于那些在使用带有 Firebase 的 Angular Universal 时服务器陷入无限加载状态的人,我的问题来自我的应用程序中的特定 Firestore 请求

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

 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 请求正在破坏 s-s-r(有关此事的更多信息):https://github.com/angular/angularfire/issues/2420

【讨论】:

以上是关于使用 Firebase 云功能为带有 i18n 的 Angular 10 s-s-r 通用应用程序提供服务的主要内容,如果未能解决你的问题,请参考以下文章

firebase 云功能中带有 typescript 的 firebase-admin

从一个信号触发的 Firebase 云功能发送 voip 推送通知

将 Firebase 托管根目录重定向到云功能不起作用

带有 Firebase 云消息传递的 Flutter 2.0:onMessage 未在 Android 上调用

如何在 Express 中使用带有 Firebase 功能的 webpack-hot-server-middleware

在 CMD(Firebase 云功能)中使用显示错误的 Razorpay 集成