Angular 9:如何在服务器端渲染(Angular Universal)中使用Angular i18n

7
我正在将一个使用服务器端渲染(Angular Universal)和 Angular i18n 用于 2 种语言(法语和英语)的 Angular 7 应用迁移到 Angular 9。在旧的 Angular 7 进程中,由于我使用了 AOT,我必须为生产做 5 次构建:
- 2 次客户端构建(分别为法语和英语) - 2 次服务器构建(分别为法语和英语) - server.ts 的构建
然后,在 `server.ts` 文件中,我会动态加载正确的服务器 bundle。 旧的 server.ts:
app.engine('html', (_, options: any, callback) => {

  const isFR= options.req.url.indexOf('site-fr') >= 0 ;
  const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = isFR ? require('./dist/server/fr/main') : require('./dist/server/en/main');

// Our index.html we'll use as our template
  const template = readFileSync(join(DIST_FOLDER, 'browser', isFR ? 'fr' : 'en', 'index.html')).toString();
  renderModuleFactory(AppServerModuleNgFactory, {
    // Our index.html
    document: template,

我将应用迁移到了Angular 9,根据文档的理解,现在只需要一个客户端构建就可以了。

您还可以使用现有的生产配置向ng build命令提供--localize选项。在这种情况下,CLI会按照项目配置中i18n下定义的所有区域设置来构建。

对于客户端构建,这似乎是有效的,因为会生成两个文件夹(fren)。
然而,在任何地方都没有提到如何使用i18n和SSR。所以我最终只得到了一个server.ts
以下是我用来构建和运行项目的脚本: angular.json
"serve:ssr": "node dist/myproject/server/main.js",
"build:ssr": "ng build -c production --localize && ng run myproject:server:production"

新的server.ts文件

// The Express app is exported so that it can be used by serverless Functions.
export function app(port) {
  const server = express();
  const distFolder = join(process.cwd(), 'dist/myproject/browser/fr');
  const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';

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

Dist文件夹结构

- dist
  - myproject
    - browser
        - fr
          - index.html
          - *.js
        - en
          - index.html
          - *.js      
    - server
        - main.js

注意:我确实看到了一篇关闭的Github问题描述这个问题,但解决方案基本上是回到以前的方式,即每种语言有两个版本构建,也有两个server.ts的版本构建。

肯定还有其他方法吧?


ngx-translate是这方面的更好解决方案。我在我的AppBrowserModule中使用了TranslateHttpLoader。 我在我的AppServerModule中使用了TranslateJsonLoader。无需重新加载页面! - Pieterjan
2个回答

5

我找到了一个只需要2个构建的解决方案。但是现在需要运行2个服务器进程的实例。

步骤#1:angular.json

确保您在angular.json中正确定义了您的区域设置,并在my-project:server选项中添加一个新的allLocales目标。

我创建了一个新的allLocales目标,因为我不知道如何将production目标与enfr配置结合起来。这样做的目的只是为了拥有一个包含所有语言服务器生成的服务器构建。

对于前端捆绑包,使用ng build(即ng build --configuration=production,fr,en或ng build --configuration=production --localize)可以直接实现。

angular.json

 "projects": {
    "my-project": {
      "i18n": {
        "locales": {
          "en": {
            "translation": "src/locale/messages.en.xlf",
            "baseHref": ""
          },
          "fr": {
            "translation": "src/locale/messages.fr.xlf",
            "baseHref": ""
          }
        }
      },
      "root": "",
      "sourceRoot": "src",
      "projectType": "application",
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            //...
          },
          "configurations": {
            "production": {
             //...
            },

            "en": {
              "localize": [
                "en"
              ]
            },


            "fr": {
              "localize": [
                "fr"
              ]
            }

          }
        },

        "server": {
          "builder": "@angular-devkit/build-angular:server",
          "options": {
            "outputPath": "dist/my-project/server",
            "main": "server.ts",
            "tsConfig": "tsconfig.server.json"
          },

          "configurations": {
            "production": {
              //...
            },

            "allLocales": {
              "outputHashing": "none",
              "optimization": false,
              "sourceMap": false,
              "namedChunks": false,
              "extractLicenses": true,
              "vendorChunk": false,
              "localize": [
                "en", "fr"
              ],
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ]
            }
          }
        },

步骤 #2: server.ts

修改 server.ts 文件以接受语言参数。每个运行的生成服务器捆绑包 main.js 实例将拥有自己的端口和语言。

server.ts

//...
export function app(language) { //add language here
  const server = express();
  const distFolder = join(process.cwd(), 'dist/my-project/browser', language); //Use language here


//...

function run() {
  const port = process.env.PORT || 5006;
  const language = process.env.LANGUAGE || 'fr';

  // Start up the Node server
  const server = app(language); //Use the language here
  server.listen(port, () => {
    console.log(`Node Express server listening on http://localhost:${port} for language ${language}`);
    });

步骤三:修改 package.json 文件

package.json

"build:ssr": "ng build -c production --localize && ng run my-project:server:allLocales"
"serve:ssr-en": "env PORT=5006 LANGUAGE=en node dist/my-project/server/en/main.js",
"serve:ssr-fr": "env PORT=5007 LANGUAGE=fr node dist/my-project/server/fr/main.js",

build:ssr 命令将构建所有语言的客户端捆绑包,然后再构建所有语言的服务器捆绑包。 server:ssr-XX 命令将启动与语言 XX 相关联的端口和语言的 Node.js 服务器。

这是结构:

- dist
  - myproject
    - browser
        - fr
          - index.html
          - *.js
        - en
          - index.html
          - *.js      
    - server
        - fr
            - main.js
        - en
            - main.js

步骤4:反向代理

如果您正在使用反向代理,请不要忘记重定向所有请求到正确的main.js实例。

注意 使用Angular 9构建过程现在更快,因为只有2个构建。


1
如何在单个端口上运行两种语言而无需使用反向代理? - baj9032
1
理论上,您需要编写另一个服务器脚本,手动“require”与语言对应的main.js文件。或者查看其他提议的答案。 - David

4
这是我们在Angular 9 (universal & i18n)中解决问题的方法: 我们使用旧的webpack配置,并为ssr构建进行本地化。 在单个构建中,生成每种语言的浏览器和服务器配置。
注意:因为在Angular 8中已经有了universal和i18n,所以您可能需要检查Webpack服务器配置的文档。 package.json:
  • We used the old webpack server config which was still in Angular 8 Universal documents.
  • Build production enviroment with localize flag.
  • Build was fine but docker had problems with memory heap size so node space is increased

    ...
    "webpack:server": "webpack --config webpack.server.config.js --progress --colors",
    "build:prod": "ng build --configuration=production --localize",
    "build:server:prod": "node --max_old_space_size=8048 ./node_modules/@angular/cli/bin/ng run APP-NAME:server:production",
    "build:client-and-server-bundles": "npm run build:prod && npm run build:server:prod",
    "build:ssr": "npm run build:client-and-server-bundles && npm run webpack:server",
    "serve:ssr": "node dist/server.js"
    

angular.json(在项目设置中声明i18n语言环境):

服务器选项:

    ...
      "options": {
        "main": "src/main.server.ts",
        "tsConfig": "src/tsconfig.server.json",
        "localize": ["fi", "en", "sv"]
      }

server.ts

在服务器的配置中,我们声明了所有捆绑包的路由。

const routes = [
  {path: '/en/*', view: 'en/index', bundle: require('./dist/server/en/main')},
  {path: '/sv/*', view: 'sv/index', bundle: require('./dist/server/sv/main')},
  {path: '/*', view: 'fi/index', bundle: require('./dist/server/fi/main')}
];

然后就有了一个hacky的解决方法: 为每个locale声明require方法,使用AppServerModule、Lazy module map、express engine和module map provider构建服务器配置。
const {AppServerModule: AppServerModuleFi, LAZY_MODULE_MAP: LAZY_MODULE_MAP_FI, ngExpressEngine: ngExpressEngineFi, provideModuleMap: provideModuleMapFi} = require('./dist/server/fi/main');
const {AppServerModule: AppServerModuleEn, LAZY_MODULE_MAP: LAZY_MODULE_MAP_EN, ngExpressEngine: ngExpressEngineEn, provideModuleMap: provideModuleMapEn} = require('./dist/server/en/main');
const {AppServerModule: AppServerModuleSv, LAZY_MODULE_MAP: LAZY_MODULE_MAP_SV, ngExpressEngine: ngExpressEngineSv, provideModuleMap: provideModuleMapSv} = require('./dist/server/sv/main');

然后对于每个路由,我们将使用之前声明的具有“专用”要求配置的服务器配置文件。英文版构建示例:

routes.forEach((route) => {
  if (route.path.startsWith('/en')) { // Check against path
  // EN routes
  app.get(route.path, (req, res) => {

    app.engine('html', ngExpressEngineEn({
      bootstrap: AppServerModuleEn,
      providers: [
        provideModuleMapEn(LAZY_MODULE_MAP_EN)
      ]
    }));
    app.set('view engine', 'html');
    app.set('views', join(DIST_FOLDER, 'browser'));

    res.render(route.view, {
      req,
      res,
      engine: ngExpressEngineEn({
        bootstrap: AppServerModuleEn,
        providers: [provideModuleMapEn(LAZY_MODULE_MAP_EN),
        { req, res }]
      })
    });
  });

非常好的答案,感谢您的分享,您可能想向开发团队展示它以给他们指导方向,他们有一个未完成的任务,但自2月份以来没有进展。 - Remy
这样做会为每个语言环境创建3个服务器吗?当您运行构建时,所有的main.js文件都将包含server.ts代码吗? - Spock
@Spock,我刚刚仔细检查了一下,每个语言环境都有一个服务器。我还检查了代码,发现在main.js文件中没有重复的代码。 - sauli purhonen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接