Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Server side rendering with Angular5 (migrating from AngularJS)

We are converting our app from AngularJS to Angular5. I am trying to figure out how to replicate some behavior using Angular5 - namely using server-side rendering to create injectable values.

In our current Angular1.6 app, we have this index.hbs file:

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Collaborative Tool</title>
  <link href="favicon.ico" rel="shortcut icon" type="image/x-icon">
</head>

<body class="aui content" ng-app="app">

  <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.5/angular.js"></script>

  <script>

    /* globals angular */
    angular.module('app')
      .value('USER', JSON.parse('{{{user}}}'))
      .value('WORKSTREAM_ENUM', JSON.parse('{{{workStreamEnum}}}'))
      .value('CATEGORY_ENUM', JSON.parse('{{{categoryEnum}}}'))
      .value('ROLES_ENUM', JSON.parse('{{{roles}}}'))
      .value('FUNCTIONAL_TEAM_ENUM', JSON.parse('{{{functionalTeams}}}'))
      .value('CDT_ENV', '{{CDT_ENV}}')
      .value('CDT_HOST', '{{CDT_HOST}}')
      .value('CDT_LOGOUT_URL', '{{CDT_LOGOUT_URL}}');


  </script>

</body>
</html>

so what we do is load angular in the first script tag and then we create some values/enums/constants using the second script tag. Essentially using server-side rendering (handlebars) to send data to the front end.

My question: Is there some way to do something very similar with Angular5? How can we use-server side rendering to create injectable modules/values in Angular5?

like image 928
Alexander Mills Avatar asked Apr 23 '18 08:04

Alexander Mills


2 Answers

My team had the same problem when transitioning from AngularJS to Angular (early release candidates of v2). We came up with a solution that we still use and I'm not aware of any updates to make it easier (at least when not using Angular Universal - if you are using that then there is stuff built in to bootstrap initial data). We pass data to our Angular app by serializing the JSON object and setting it up as an attribute on the app root Angular component in our HTML:

<app-root [configuration]="JSON_SERIALIZED_OBJECT"></app-root>

where JSON_SERIALIZED_OBJECT is the actual serialized object. We use .NET (non-Core, so Angular Universal isn't really an option) to render our page (doing [configuration]="@JsonConvert.SerializeObject(Model.Context)") so don't know what you need to do, but it looks like you should be able to do the same thing that you've done previously to serialize it.

Once that is setup, we have to manually JSON.parse(...) that object in our main app component, but we treat it just like an Angular input. This is what our component looks like to grab that:

import { Component, ElementRef } from '@angular/core';
import { ConfigurationService } from 'app/core';

@Component(...)
export class AppComponent {
    constructor(private element: ElementRef, private configurationService: ConfigurationService) {
        this.setupConfiguration();
    }

    private setupConfiguration() {
        const value = this.getAttributeValue('[configuration]');
        const configuration = value ? JSON.parse(value) : {};

        this.configurationService.setConfiguration(configuration);
    }

    private getAttributeValue(attribute: string) {
        const element = this.element.nativeElement;

        return element.hasAttribute(attribute) ? element.getAttribute(attribute) : null;
    }
}

As shown, we use a service to share the data around the system. It can be something as simple as this:

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';

import { Configuration } from './configuration.model';

@Injectable()
export class ConfigurationService {
    private readonly configurationSubject$ = new BehaviorSubject<Configuration>(null);
    readonly configuration$ = this.configurationSubject$.asObservable();

    setConfiguration(configuration: Configuration) {
        this.configurationSubject$.next(configuration);
    }
}

Then in our components that need data from the configuration, we inject this service and watch for changes.

import { Component, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/takeUntil';

import { ConfigurationService } from 'app/core';

@Component(...)
export class ExampleThemedComponent implements OnDestroy {
    private readonly destroy$ = new Subject<boolean>();

    readonly theme$: Observable<string> = this.configurationService.configuration$
        .takeUntil(this.destroy$.asObservable())
        .map(c => c.theme);

    constructor(private configurationService: ConfigurationService) {
    }

    ngOnDestroy() {
        this.destroy$.next(true);
    }
}

Note: we make changes to our configuration sometimes while running so that is why we use a subject and observables. If your configuration won't change, then you can skip all of that portion of these examples.

like image 28
Daniel W Strimpel Avatar answered Mar 16 '23 07:03

Daniel W Strimpel


Dependency Injection still can be used inside your components when rendering it on the server side.

If you're planning to use server-side rendering with Angular 5 you should consider looking into Angular Universal it provides the building blocks for having Angular single page apps rendered in the server-side (for SEO-friendly indexable content).

There are many good angular universal starter projects out there. A good example is [universal-starter][2] . It uses ngExpressEngine to render your application on the fly at the requested url. It uses a webpack project configuration which contains a prerender task that compiles your application and prerenders your applications files. This task looks like this:

// Load zone.js for the server.
import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import {readFileSync, writeFileSync, existsSync, mkdirSync} from 'fs';
import {join} from 'path';

import {enableProdMode} from '@angular/core';
// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();

// Import module map for lazy loading
import {provideModuleMap} from '@nguniversal/module-map-ngfactory-loader';
import {renderModuleFactory} from '@angular/platform-server';
import {ROUTES} from './static.paths';

// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./dist/server/main.bundle');

const BROWSER_FOLDER = join(process.cwd(), 'browser');

// Load the index.html file containing referances to your application bundle.
const index = readFileSync(join('browser', 'index.html'), 'utf8');

let previousRender = Promise.resolve();

// Iterate each route path
ROUTES.forEach(route => {
  var fullPath = join(BROWSER_FOLDER, route);

  // Make sure the directory structure is there
  if(!existsSync(fullPath)){
    mkdirSync(fullPath);
  }

  // Writes rendered HTML to index.html, replacing the file if it already exists.
  previousRender = previousRender.then(_ => renderModuleFactory(AppServerModuleNgFactory, {
    document: index,
    url: route,
    extraProviders: [
      provideModuleMap(LAZY_MODULE_MAP)
    ]
  })).then(html => writeFileSync(join(fullPath, 'index.html'), html));
});

Later on you can run an express server which renders your apps generated HTMLs:

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

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

// Server static files from /browser
app.get('*.*', express.static(join(DIST_FOLDER, 'browser'), {
  maxAge: '1y'
}));

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

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

You can run server-side specific code such as:

import { PLATFORM_ID } from '@angular/core';
 import { isPlatformBrowser, isPlatformServer } from '@angular/common';

 constructor(@Inject(PLATFORM_ID) private platformId: Object) { ... }

 ngOnInit() {
   if (isPlatformBrowser(this.platformId)) {
      // Client only code.
      ...
   }
   if (isPlatformServer(this.platformId)) {
     // Server only code.
     ...
   }
 }

but beware that window, document, navigator, and other browser types - do not exist on the server. So any library that might use these might not work.

like image 188
guilhebl Avatar answered Mar 16 '23 07:03

guilhebl