我正在使用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
答案 0 :(得分:1)
Knockout显然不会在角度服务器端渲染上工作。淘汰赛的理解是有一个DOM
,但没有这样的东西。
尊重Angular规则:永远不要直接使用特定于浏览器的API(如DOM)。如果这样做,您的模块将与Universal server rendering和其他Angular高级选项不兼容