Angular Universal does not render all in server side

I am building a solution with Angular 4 + Angular Universal + Knockout.

The idea is that the angular application will render html in server side for SEO purposes, but as part of that rendering process it must be able to use knockoutJs to bind some html text with a view model in order to render first the html bound to knockout and then finalize server side rendering and send the result html to the browser.

The reason to mix angular with knockout is because we have some html (eg: '<span data-bind="text: test"></span>' ) as a string coming from an existing third party that contains knockout markup.

I am able to use knockout inside my Angular module and apply the bindings. The html text is able to display the content of the view model variable... but when executing the Angular app in server side this part is not rendered in server side , it works but it is rendered in client side and it's not acceptable as a solution for us.

It is as if the server side javascript rendering engine didn't wait for an asynchronous call to apply the knockout bindings before sending to the client the finalized html rendered.

I have followed these steps to run Angular 4 with Universal in back end.

This is my simple knockout view model mysample.ts :

import * as ko from 'knockout';

export interface IMySample {
    test: string;
}

export class MySample implements IMySample {
    public test: any = ko.observable("Hi, I'm a property from knockout");

    constructor(){
    }
}

And this is my main component home.component.ts where I have some html as a text that contains knockout binding and I want to render in server side.

import { Component, OnInit } from '@angular/core';
import { MySample } from '../ko/mysample';
import { KoRendererService } from '../ko-renderer.service';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css'],
  providers: [KoRendererService]
})
export class HomeComponent implements OnInit {

  htmlWithKnockout: string = '<span data-bind="text: test"></span>';
  htmlAfterBinding: string = null;
  constructor(private koRendererService: KoRendererService) 
  { 
  }

  ngOnInit() {
    var mySample = new MySample();
    this.koRendererService.getHtmlRendered(this.htmlWithKnockout, mySample).then((htmlRendered: string) => {
      this.htmlAfterBinding = htmlRendered;
    });
  }
}

with its view home.component.html that should display the same html with knockout binding but already rendered in the server (it's working only on client side):

<p>This content should be part of the index page DOM</p>
<div id="ko-result" [innerHTML]="htmlAfterBinding"></div>

And this is the service ko-rendered.service.ts I have created to apply the knockout bindings. I have made it asynchronous because I read here that Angular Unviersal should await for asynchronous calls to end before rendering the html in server side)

import * as ko from 'knockout';

interface IKoRendererService {
    getHtmlRendered(htmlAsText: string, viewModel: any): Promise<string>;
}

export class KoRendererService implements IKoRendererService {
    constructor(){

    }

    getHtmlRendered(htmlAsText: string, viewModel: any): Promise<string> {
        return new Promise<string>((resolve, reject) => {
            var htmlDivElement: HTMLDivElement = document.createElement('div');
            htmlDivElement.innerHTML = htmlAsText;
            ko.applyBindings(viewModel, htmlDivElement.firstChild);
            var result = htmlDivElement.innerHTML;
            resolve(result);
        });
    }
}

This is the response to the index.html page at the browser. We can see here that the angular content has been rendered in server side properly, however the part with the knockout binding is not in the DOM, it is retrieved at client side.

<!DOCTYPE html><html lang="en"><head>
  <meta charset="utf-8">
  <title>Sample</title>
  <base href="/">

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="styles.d41d8cd98f00b204e980.bundle.css" rel="stylesheet"><style ng-transition="carama"></style></head>
<body>
  <app-root _nghost-c0="" ng-version="4.1.3"><ul _ngcontent-c0="">
  <li _ngcontent-c0=""><a _ngcontent-c0="" routerLink="/" href="/">Home</a></li>
  <li _ngcontent-c0=""><a _ngcontent-c0="" routerLink="about" href="/about">About Us</a></li>
</ul>
<hr _ngcontent-c0="">

<h1 _ngcontent-c0="">Welcome to the server side rendering test with Angular Universal</h1>
<router-outlet _ngcontent-c0=""></router-outlet><app-home _nghost-c1=""><p _ngcontent-c1="">This content should be part of the index page DOM</p>
<div _ngcontent-c1="" id="ko-result"></div></app-home>
</app-root>
<script type="text/javascript" src="inline.77dfeeb563e4dcc7a506.bundle.js"></script><script type="text/javascript" src="polyfills.d90888e283bda7f009a0.bundle.js"></script><script type="text/javascript" src="vendor.451987311459166e7919.bundle.js"></script><script type="text/javascript" src="main.af6e993f16ecd4063c3b.bundle.js"></script>

</body></html>

Notice how the div with id="ko-result" is empty. Later on in client side this div is properly modified in the DOM and looks like:

<div _ngcontent-c1="" id="ko-result"><span>Hi, I'm a property from knockout</span></div>

But I need that rendering at server side ...

Any help would be much appreciated. Thanks!

UPDATE 1 : This is my package.json with my dependencies:

{
  "name": "server-side-rendering",
  "version": "0.0.0",
  "license": "MIT",
  "scripts": {
    "prestart": "ng build --prod && ngc",
    "start": "ts-node src/server.ts"
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "^4.1.3",
    "@angular/common": "^4.0.0",
    "@angular/compiler": "^4.0.0",
    "@angular/core": "^4.0.0",
    "@angular/forms": "^4.0.0",
    "@angular/http": "^4.0.0",
    "@angular/platform-browser": "^4.0.0",
    "@angular/platform-browser-dynamic": "^4.0.0",
    "@angular/platform-server": "^4.1.3",
    "@angular/router": "^4.0.0",
    "core-js": "^2.4.1",
    "rxjs": "^5.1.0",
    "zone.js": "^0.8.4",
    "knockout": "^3.4.2"
  },
  "devDependencies": {
    "@angular/cli": "1.0.6",
    "@angular/compiler-cli": "^4.0.0",
    "@types/jasmine": "2.5.38",
    "@types/node": "~6.0.60",
    "codelyzer": "~2.0.0",
    "jasmine-core": "~2.5.2",
    "jasmine-spec-reporter": "~3.2.0",
    "karma": "~1.4.1",
    "karma-chrome-launcher": "~2.1.1",
    "karma-cli": "~1.0.1",
    "karma-jasmine": "~1.1.0",
    "karma-jasmine-html-reporter": "^0.2.2",
    "karma-coverage-istanbul-reporter": "^0.2.0",
    "protractor": "~5.1.0",
    "ts-node": "~2.0.0",
    "tslint": "~4.5.0",
    "typescript": "~2.2.0"
  }
}

UPDATE 2 : At client side rendering it also works, I can see the final rendering on the browser, but as expected all the content is missing from the index.html request and it's javascript who fetches the content later on. This is the response when running the same app with ng-serve (client-side rendering):

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Sample</title>
  <base href="/">

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <app-root>Loading sample, you should not see this in client side...</app-root>
<script type="text/javascript" src="inline.bundle.js"></script><script type="text/javascript" src="polyfills.bundle.js"></script><script type="text/javascript" src="styles.bundle.js"></script><script type="text/javascript" src="vendor.bundle.js"></script><script type="text/javascript" src="main.bundle.js"></script></body>
</html>

UPDATE 3 : I read here that to have Angular app universal compatible we should not manipulate the DOM directly. I was wondering whether the fact that I am using document to create the HtmlElement that knockout applyBindings needs in my ko-rendered.service.ts was somehow making angular universal to ignore this rendering at server side, but I have tried to create the DOM element with Renderer2 (eg: import { Component, OnInit, Renderer2 } from '@angular/core'; ) instead and that did not resolve the issue either:

var htmlDivElement: any = renderer2.createElement('div')
            // var htmlDivElement: HTMLDivElement = document.createElement('div');

UPDATE 4 : I have seen the following errors in server-side node environment during the ko.applyBindings call, so I suspect the whole approach is problematic because knockoutJs is not really designed to be executed in server-side in an environment without a browser. It relies too much on the DOM and as The Angular Universal good practices say:

Don't use any of the browser types provided in the global namespace such as navigator or document. Anything outside of Angular will not be detected when serializing your application into html

These are the errors that must be causing that Angular Universal stops rendering at server side and simply passes it on to the browser:

listening on http://localhost:4000!
ERROR { Error: Uncaught (in promise): TypeError: Cannot read property 'body' of undefined
TypeError: Cannot read property 'body' of undefined
    at Object.ko.applyBindings 

(C:Devnode_modulesknockoutbuildoutputknockout-latest.debug.js:3442:47)
..
ERROR { Error: Uncaught (in promise): TypeError: this.html.charCodeAt is not a function
TypeError: this.html.charCodeAt is not a function

Knockout obviously doesn't work on angular server side rendering. Knockout is of the understanding that there is a DOM , but there is no such thing.

Respect the Angular rule: never use browser-specific APIs (like the DOM) directly. If you do so, your module won't be compatible with Universal server rendering and other Angular advanced options

链接地址: http://www.djcxy.com/p/5300.html

上一篇: 使用Angular4(Angular Universal)进行服务器端渲染

下一篇: Angular Universal不会在服务器端渲染所有内容