Deploy Angular 8 Universal (SSR) application on Fi

2020-02-11 08:44发布

问题:

I recently upgraded my Angular 8 application to be an Universal application (SSR). I had it deployed to Firebase before it was a SSR application, but then I figured out that deploying a SSR application on Firebase doesn't work by using regular Firebase Hosting. I did some research and discovered that I have to use Firebase Cloud Functions.

I created a SSR application using:

ng add @nguniversal/express-engine --clientProject PROJECT_NAME

PROJECT_NAME can be found in angular.json file below "projects" section.

Can anyone help me with this?

回答1:

Important note: This solution was taken from Q&A section of the Angular course on Udemy (here). I tried it out and with some modifications managed to make it work.


So, first make sure SSR actually works by running npm run build:ssr and npm run serve:ssr.

Then install Firebase Tools and initialize the project:

  • If you didn't install the Firebase Command Line Tools already before, run npm install -g firebase-tools
  • Run firebase login, providing your Firebase credentials (email/password) if requested.
  • Run firebase init

Answer some questions ...

  • "Are you ready to proceed?"

    Type y and hit ENTER.

  • "Which firebase CLI features do you want to setup?"

    Choose ...

    (*) Functions
    
    (*) Hosting
    

    ... , selecting both with the SPACE key, and hitting ENTER.

  • "Select a default Firebase project for this directory?"

    Select one with the ARROW keys and hit ENTER.

  • "What language would you like to use to write Cloud Functions?"

    Select TypeScript with the ARROW keys and hit ENTER.

  • "Do you want to use TSLint?"

    Type y and hit ENTER.

  • "Do you want to install dependencies with npm now?"

    Type y and hit ENTER.

  • "What do you want to use as your public directory?"

    Type dist/browser and hit ENTER (Please note: this is different from deploying an app without Universal!).

  • "Configure as a single page app?"

    Type y and hit ENTER.

  • File index.html already exists. Overwrite?

    Type n (important!) and hit ENTER.

Modify some files ...

  • In firebase.json replace "destination": "/index.html" by "function": "ssr"

    (ssr points to this export const ssr = functions.https.onRequest(universal); variable, you will find it below).

  • In server.ts add export to the app initialization: export const app = express(); instead of const app = express();

  • In server.ts either comment out the last three lines (app.listen(...)) or replace them with these:

    // If we're not in the Cloud Functions environment, spin up a Node server
    if (!process.env.FUNCTION_NAME) {
        // Start up the Node server
        app.listen(PORT, () => {
            console.log(`Node Express server listening on http://localhost:${PORT}`);
        });
    }

  You can remove them when deploying to Firebase but you need them when running npm run serve:ssr to be able to host your app on localhost.

  • In webpack.server.config.js modify the output like this:
    output: {
        // Puts the output at the root of the dist folder
        path: path.join(__dirname, 'dist'),
        // Export a UMD of the webpacked server.ts & dependencies for rendering in Cloud Functions
        library: 'app',
        libraryTarget: 'umd',
        filename: '[name].js',
    },

  and modify externals like this:

    externals: [
        // Firebase has some troubles being webpacked when it's in the Node environment, so we will skip it.
        /^firebase/
    ],

  This will fix an error:

Cannot find module 'require("./server/main")'

  when running npm run serve:ssr or firebase serve commands.

  • Rebuild your app by running npm run build:ssr.

  • Using the terminal move to the functions folder: cd functions

  • Install a npm package for filesystem access: npm i fs-extra

  • Inside the functions folder create a new file named copy-angular-app.js, with this content:

    const fs = require('fs-extra');
    fs.copy('../dist', './dist').then(() => {
        // We should remove the original "index.html" since Firebase will use it when SSR is enabled (instead of calling SSR functions),
        // and because of that, SSR won't work for the initial page.
        fs.remove('../dist/browser/index.html').catch(e => console.error('REMOVE ERROR: ', e));
    }).catch(err => {
        console.error('COPY ERROR: ', err)
    });

  This fixes initial page not loaded as a SSR (instead of showing content for the initial page it's still showing <app-root></app-root>).

  Note: Since we removed index.html file, running npm run serve:ssr won't work unless you first rebuild your app (by running npm run build:ssr -> this will recreate index.html file).

  • In functions/package.json (not in the project's package.json!) change the build entry like this:

"build": "node copy-angular-app && tsc",

  • In functions/src/index.ts replace the content with this:
    import * as functions from 'firebase-functions';

    const universal = require(`${process.cwd()}/dist/server`).app;
    export const ssr = functions.https.onRequest(universal);
  • In the terminal make sure that you are in the functions directory, and there run npm run build to copy the dist folder into the functions folder.

Additional note: To make building for Firebase easier you can create a script in the main project's package.json file:

    "build:ssr": "npm run build:client-and-server-bundles && npm run compile:server", // this one should already exist
    "build:ssr-firebase": "npm run build:ssr && npm --prefix functions/ run build",

This script will first build your Angular SSR application (npm run build:ssr) and then it will run npm run build inside your functions folder (that way it will copy the project's dist folder to your functions dist folder and will remove the project's index.html file).

Deploy your app ...

  • You can serve your app locally before deployment, on localhost:5000, by running firebase serve (if you want).

  • Stop the server (Ctrl + C).

  • Then you can deploy the app by running firebase deploy and visit it via the url which is displayed in the terminal.

This way I managed to deploy my Angular SSR app on Firebase.

Hope this helps...