Angular Universal不会在服务器端

时间:2017-05-30 12:26:32

标签: angular knockout.js serverside-rendering angular-universal

我正在使用Angular 4 + Angular Universal + Knockout构建解决方案。

这个想法是角度应用程序将在服务器端渲染html用于SEO目的,但作为渲染过程的一部分,它必须能够使用knockoutJs将一些html文本与视图模型绑定,以便首先渲染html绑定,然后完成服务器端渲染并将结果html发送到浏览器。

将角度与淘汰赛混合的原因是因为我们有一些html(例如:'<span data-bind="text: test"></span>')作为来自包含淘汰标记的现有第三方的字符串。

我可以在Angular模块中使用knockout并应用绑定。 html文本能够显示视图模型变量的内容... 但是当在服务器端执行Angular应用程序时,此部分不在服务器端呈现,它可以工作,但它在客户端呈现一方面,这对我们来说是不可接受的。

就好像服务器端javascript呈现引擎在向客户端发送最终确定的html之前没有等待异步调用来应用knockout绑定。

我在后端跟着these steps to run Angular 4 with Universal

这是我简单的淘汰视图模型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(){
    }
}

这是我的主要组件home.component.ts,其中我有一些html作为包含敲除绑定的文本,我想在服务器端呈现。

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;
    });
  }
}

其视图home.component.html应显示与敲除绑定相同的html但已在服务器中呈现(它仅在客户端工作):

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

这是我创建的应用敲除绑定的服务ko-rendered.service.ts。我已经使它异步,因为I read here Angular Unviersal应该在服务器端渲染html之前等待异步调用结束)

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);
        });
    }
}

这是对浏览器的index.html页面的响应。我们可以在这里看到角度内容已经在服务器端正确呈现,但是具有knockout绑定的部分不在DOM中,它在客户端检索。

<!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>

注意id="ko-result"的div是如何为空。稍后在客户端,这个div在DOM中被正确修改,看起来像:

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

但我需要在服务器端进行渲染 ...

非常感谢任何帮助。谢谢!

UPDATE 1 :这是我的package.json和我的依赖项:

{
  "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"
  }
}

更新2 :在客户端呈现它也可以工作,我可以看到浏览器上的最终呈现,但正如预期的那样,index.html请求中缺少所有内容,而且它是javascript谁获取的以后的内容。这是使用ng-serve(客户端呈现)运行相同应用时的响应:

<!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>

更新3 :我读了here要使Angular app兼容,我们不应该直接操作DOM。我想知道我是否正在使用document来创建我的applyBindings中需要淘汰ko-rendered.service.ts需要的HtmlElement的事实是在某种程度上使角度通用在服务器端忽略了这种渲染,但我有尝试用Renderer2创建DOM元素(例如:import { Component, OnInit, Renderer2 } from '@angular/core';)而不是解决问题:

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

更新4 :我在ko.applyBindings调用期间在服务器端节点环境中看到了以下错误,因此我怀疑整个方法存在问题,因为 knockoutJs并不是真的旨在在没有浏览器的环境中在服务器端执行。它过分依赖于DOM ,而Angular Universal good practices则说:

  

请勿使用全局命名空间中提供的任何浏览器类型   例如导航器或文档。 Angular以外的任何东西都不会   将您的应用程序序列化为html时检测到

这些是必须导致Angular Universal在服务器端停止渲染并将其直接传递给浏览器的错误:

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:\Dev\node_modules\knockout\build\output\knockout-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

1 个答案:

答案 0 :(得分:1)

Knockout显然不会在角度服务器端渲染上工作。淘汰赛的理解是有一个DOM,但没有这样的东西。

  

尊重Angular规则:永远不要直接使用特定于浏览器的API(如DOM)。如果这样做,您的模块将与Universal server rendering和其他Angular高级选项不兼容