如何在 Firebase 中部署 Angular Universal 渲染?

9
我正在使用Angular 5实现Angular Universal Rendering,当我在本地机器上部署和运行时,可以轻松地使用Angular Universal。只需按照此说明链接操作,它将仅在本地机器上工作https://github.com/angular/angular-cli/wiki/stories-universal-rendering。但是,当我开始在Firebase中部署Angular Universal时,出现了意外的问题。我遵循了这个链接,但信息很少。请帮助我如何在Firebase中部署Angular Universal Rendering。感谢您的帮助!谢谢!

如何将Angular 4 Universal应用程序部署到Firebase

https://www.youtube.com/watch?v=gxCu5TEmxXE

file structure like this:

src/app/app.module.ts:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule.withServerTransition({appId: 'something-unique'})
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

src/app/app.server.module.ts:

import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader';

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

@NgModule({
    imports: [
        AppModule,
        ServerModule,
        ModuleMapLoaderModule
    ],
    bootstrap: [AppComponent],
})

export class AppServerModule { }

src/main.server.ts:

export { AppServerModule } from './app/app.server.module';

src/tsconfig.server.json:

{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "outDir": "../out-tsc/app",
    "baseUrl": "./",
    "module": "commonjs",
    "types": []
  },
  "exclude": [
    "test.ts",
    "**/*.spec.ts"
  ],
  "angularCompilerOptions": {
    "entryModule": "app/app.server.module#AppServerModule"
  }
}

.angular-cli.json:

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "project": {
    "name": "ng-true-facts"
  },
  "apps": [
    {
      "root": "src",
      "outDir": "dist/browser",
      "assets": [
        "assets",
        "favicon.ico"
      ],
      "index": "index.html",
      "main": "main.ts",
      "polyfills": "polyfills.ts",
      "test": "test.ts",
      "tsconfig": "tsconfig.app.json",
      "testTsconfig": "tsconfig.spec.json",
      "prefix": "app",
      "styles": [
        "styles.css"
      ],
      "scripts": [],
      "environmentSource": "environments/environment.ts",
      "environments": {
        "dev": "environments/environment.ts",
        "prod": "environments/environment.prod.ts"
      }
    },
    {
      "platform": "server",
      "root": "src",
      "outDir": "dist/server",
      "assets": [
        "assets",
        "favicon.ico"
      ],
      "index": "index.html",
      "main": "main.server.ts",
      "test": "test.ts",
      "tsconfig": "tsconfig.server.json",
      "testTsconfig": "tsconfig.spec.json",
      "prefix": "app",
      "styles": [
        "styles.css"
      ],
      "scripts": [],
      "environmentSource": "environments/environment.ts",
      "environments": {
        "dev": "environments/environment.ts",
        "prod": "environments/environment.prod.ts"
      }
    }
  ],
  "e2e": {
    "protractor": {
      "config": "./protractor.conf.js"
    }
  },
  "lint": [
    {
      "project": "src/tsconfig.app.json",
      "exclude": "**/node_modules/**"
    },
    {
      "project": "src/tsconfig.spec.json",
      "exclude": "**/node_modules/**"
    },
    {
      "project": "e2e/tsconfig.e2e.json",
      "exclude": "**/node_modules/**"
    }
  ],
  "test": {
    "karma": {
      "config": "./karma.conf.js"
    }
  },
  "defaults": {
    "styleExt": "css",
    "component": {}
  }
}

./server.ts

import 'zone.js/dist/zone-node';
import 'reflect-metadata';

import { renderModuleFactory } from '@angular/platform-server';
import { enableProdMode } from '@angular/core';

import * as express from 'express';
import { join } from 'path';
import { readFileSync } from 'fs';

enableProdMode();

const app = express();

const PORT = process.env.PORT || 4000;
const DIST_FOLDER = join(process.cwd(), 'dist');

const template = readFileSync(join(DIST_FOLDER, 'browser', 'index.html')).toString();

const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./dist/server/main.bundle');

const { provideModuleMap } = require('@nguniversal/module-map-ngfactory-loader');

app.engine('html', (_, options, callback) => {
    renderModuleFactory(AppServerModuleNgFactory, {
        document: template,
        url: options.req.url,
        extraProviders: [
            provideModuleMap(LAZY_MODULE_MAP)
        ]
    }).then(html => {
        callback(null, html);
    });
});

app.set('view engine', 'html');
app.set('views', join(DIST_FOLDER, 'browser'));

app.get('*.*', express.static(join(DIST_FOLDER, 'browser')));

app.get('*', (req, res) => {
    res.render(join(DIST_FOLDER, 'browser', 'index.html'), { req });
});

app.listen(PORT, () => {
    console.log(`Node server listening on http://localhost:${PORT}`);
});

./webpack.server.config.js (root project level)

const path = require('path');
const webpack = require('webpack');

module.exports = {
    entry: { server: './server.ts' },
    resolve: { extensions: ['.js', '.ts'] },
    target: 'node',
    externals: [/(node_modules|main\..*\.js)/],
    output: {
        path: path.join(__dirname, 'dist'),
        filename: '[name].js'
    },
    module: {
        rules: [
            { test: /\.ts$/, loader: 'ts-loader' }
        ]
    },
    plugins: [
        new webpack.ContextReplacementPlugin(
            /(.+)?angular(\\|\/)core(.+)?/,
            path.join(__dirname, 'src'),
            {}
        ),
        new webpack.ContextReplacementPlugin(
            /(.+)?express(\\|\/)(.+)?/,
            path.join(__dirname, 'src'),
            {}
        )
    ]
}

/dist/
   /browser/
   /server/

package.json

{
  "name": "ng-true-facts",
  "version": "0.0.0",
  "license": "MIT",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build --prod",
    "build:ssr": "npm run build:client-and-server-bundles && npm run webpack:server",
    "serve:ssr": "node dist/server.js",
    "build:client-and-server-bundles": "ng build --prod && ng build --prod --app 1 --output-hashing=false",
    "webpack:server": "webpack --config webpack.server.config.js --progress --colors",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "^5.2.0",
    "@angular/common": "^5.2.0",
    "@angular/compiler": "^5.2.0",
    "@angular/core": "^5.2.0",
    "@angular/forms": "^5.2.0",
    "@angular/http": "^5.2.0",
    "@angular/platform-browser": "^5.2.0",
    "@angular/platform-browser-dynamic": "^5.2.0",
    "@angular/router": "^5.2.0",
    "@nguniversal/module-map-ngfactory-loader": "^5.0.0-beta.5",
    "core-js": "^2.4.1",
    "firebase-functions": "^0.8.1",
    "rxjs": "^5.5.6",
    "ts-loader": "^3.5.0",
    "zone.js": "^0.8.19"
  },
  "devDependencies": {
    "@angular/cli": "~1.7.0",
    "@angular/compiler-cli": "^5.2.0",
    "@angular/language-service": "^5.2.0",
    "@angular/platform-server": "^5.2.6",
    "@types/jasmine": "~2.8.3",
    "@types/jasminewd2": "~2.0.2",
    "@types/node": "~6.0.60",
    "codelyzer": "^4.0.1",
    "jasmine-core": "~2.8.0",
    "jasmine-spec-reporter": "~4.2.1",
    "karma": "~2.0.0",
    "karma-chrome-launcher": "~2.2.0",
    "karma-coverage-istanbul-reporter": "^1.2.1",
    "karma-jasmine": "~1.1.0",
    "karma-jasmine-html-reporter": "^0.2.2",
    "protractor": "~5.1.2",
    "ts-node": "~4.1.0",
    "tslint": "~5.9.1",
    "typescript": "~2.5.3"
  }
}

问题具体是什么? - David
1
我不知道如何在Firebase Hosting上部署具有服务器端渲染的应用程序。请帮帮我的朋友。谢谢。 - Fred
你找到解决方案了吗? - Janco Boscan
@JancoBoscan no :( - Fred
2个回答

3

在查看了许多文章和视频后,我发现使用Angular 9可以通过几个命令轻松实现SSR,如下所示:

使用以下命令添加SSR:

ng add @nguniversal/express-engine

您需要添加Firebase软件包:

ng add @angular/fire

当检测到SSR时,您将被要求是否添加功能(选择“是”'y')。然后在Firebase中选择预先添加的项目。随后,只需使用以下命令进行部署:
ng deploy 

参考此文章


非常感谢您。在浏览了几个毫无必要的复杂教程后,这解决了问题。也适用于 nrwl nx! - Moo
这在更复杂的脚手架中并不实用,而这才是真正的场景。如果您有许多使用相同firebase.json的SSR云函数呢? - José Pulido

2
我将详细介绍实现在Firebase中部署Angular 5 Universal的主要10个步骤。您可以在这篇逐步文章中找到更多细节:https://blog.angularindepth.com/angular-5-universal-firebase-4c85a7d00862 让我们开始吧。
假设您知道如何在项目中初始化Firebase函数,您的构建结构可能如下所示:
- dist:Angular(浏览器)应用程序和静态文件 - dist/server:通用应用程序 - functions:Express服务器和依赖项
但是,您希望Express服务器从Firebase函数运行,并且它需要读取通用应用程序。此结构不能让它访问该文件夹。
我建议您改用以下结构:
- dist:Express服务器和依赖项 - browser:Angular(浏览器)应用程序和静态文件 - server:通用应用程序
为了实现这一点:
1. 创建一个新的空dist文件夹。
2. 将functions/package.json移动到dist/package.json
3. 删除functions文件夹
4. 更新firebase.json
Firebase现在查看dist文件夹而不是functions文件夹,在任何路由上调用ssr函数,并从dist/browser提供静态资产。

{
  "hosting": {
    "public": "dist/browser",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "**",
        "function": "ssr"
      }
    ]
  },
  "functions": {
    "source": "dist"
  }
}

ssr函数将是您导出的Express服务器中的函数名称,以便Firebase可以使用它。

5. 将src/index.html重命名为src/index-1.html

这样,当您调用基本路由时,Firebase无法从静态文件夹中提供空的<app-root></app-root>索引.html,而应该调用SSR函数。

6. 更新您的服务器index.ts如下:

// These are important and needed before anything else
import  'zone.js/dist/zone-node';
import  'reflect-metadata';

import { enableProdMode } from  '@angular/core';
import  *  as  express  from  'express';
import { join } from  'path';
  
// NOTE: leave this as require() since this file is built Dynamically from webpack
const { AppServerModuleNgFactory, LAZY_MODULE_MAP } =  require('./server/main.bundle');

// NgUniversalTools: Express Engine and moduleMap for lazy loading
import { ngExpressEngine } from  '@nguniversal/express-engine';
import { provideModuleMap } from  '@nguniversal/module-map-ngfactory-loader';

//firebase cloud functions
import * as firebaseFunctions from 'firebase-functions';


// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();

//check if Firebase functions is enabled or not
const DISABLE_FIREBASE = process.env.DISABLE_FIREBASE || false;

// Express server
const  app  =  express();
const  PORT  =  process.env.PORT  ||  4000;
const DIST_FOLDER = join(process.cwd(), DISABLE_FIREBASE ? 'dist' : './');

app.engine('html', ngExpressEngine({
bootstrap:  AppServerModuleNgFactory,
providers: [
    provideModuleMap(LAZY_MODULE_MAP)
]
}));

app.set('view engine', 'html');
app.set('views', join(DIST_FOLDER, 'browser'));

/* TODO: implement data requests securely
// app.get('/api/*', (req, res) => {
// res.status(404).send('data requests are not supported');
// });
*/

// All regular routes use the Universal engine
app.get('*', (req, res) => {
    res.render(join(DIST_FOLDER, 'browser', 'index-1.html'), {req});
});

if(DISABLE_FIREBASE){
    // Server static files from express in case there's no firebase hosting
    app.get('*.*', express.static(join(DIST_FOLDER, 'browser')));

    // Start up the Node server if not using firebase cloud functions
    app.listen(PORT, () => {
        console.log(`Node server listening on http://localhost:${PORT}`);
    });
}

//server side rendering using frebase cloud functions
export let ssr = DISABLE_FIREBASE ? null : firebaseFunctions.https.onRequest(app);

7. 更新 .angular-cli.json

这样,它将适应新的结构:

{
//...some stuff...

"apps": [
    {
      "root": "src",
      "outDir": "dist/browser",
      "assets": [
        "assets",
        "favicon.ico"
      ],
      "index": "index-1.html",
      "main": "main.ts",
      "polyfills": "polyfills.ts",
      "test": "test.ts",
      "tsconfig": "tsconfig.app.json",
      "testTsconfig": "tsconfig.spec.json",
      "prefix": "app",
      "styles": [
        "styles.sass"
      ],
      "scripts": [],
      "environmentSource": "environments/environment.ts",
      "environments": {
        "dev": "environments/environment.ts",
        "prod": "environments/environment.prod.ts"
      }
    },
    {
      "root": "src",
      "outDir": "dist/server",
      "assets": [
        "assets",
        "favicon.ico"
      ],
      "index": "index-1.html",
      "main": "main.server.ts",
      "test": "test.ts",
      "tsconfig": "tsconfig.server.json",
      "testTsconfig": "tsconfig.spec.json",
      "prefix": "app",
      "styles": [
        "styles.sass"
      ],
      "scripts": [],
      "environmentSource": "environments/environment.ts",
      "environments": {
        "dev": "environments/environment.ts",
        "prod": "environments/environment.prod.ts"
      },
      "platform": "server"
    }
  ],

 //...more stuff...
}

8. 更新服务器构建位置

确保服务器文件的输出位于 dist 文件夹中。

9. 将Universal依赖项添加到Firebase package.json中

更新 dist/package.json 文件,使其如下所示:

{
//... some stuff...
"dependencies": {
      "@angular/animations": "^5.2.6",
      "@angular/common": "^5.2.6",
      "@angular/compiler": "^5.2.6",
      "@angular/core": "^5.2.6",
      "@angular/forms": "^5.2.6",
      "@angular/http": "^5.2.6",
      "@angular/platform-browser": "^5.2.6",
      "@angular/platform-browser-dynamic": "^5.2.6",
      "@angular/platform-server": "^5.2.6",
      "@angular/router": "^5.2.6",
      "@nguniversal/express-engine": "^5.0.0-beta.6",
      "@nguniversal/module-map-ngfactory-loader": "^5.0.0-beta.6",
      "express": "^4.16.2",
      "firebase-admin": "~5.9.0",
      "firebase-functions": "^0.8.1",
      "rxjs": "^5.5.6",
      "zone.js": "^0.8.20"
    },
//... more stuff...
}

10. 部署到 Firebase!

最后,使用 firebase deploy 命令将其部署到 Firebase。

故障排除

如果无法正常工作或您遗漏了某些细节,请查看我在回复开头链接的文章,因为从开始(创建 Angular 5 Universal 项目)到结束(使用 Firebase 函数部署)所有内容都有详细说明。

玩得愉快!


第五步是找到src文件夹的位置。我有dist->browser和dist->server文件夹。 - Jay
ng deploy https://github.com/angular/angularfire/blob/master/docs/deploy/getting-started.md - Jonathan

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