Angular 官方文档已经有 Angular Universal 那一章,专门用于讲解服务器端渲染。但是在使用服务器渲染的时候还是碰到不少的坑。
1. 文档中的坑
文档中已经大致介绍了服务器渲染的过程,以及需要的配置,但文档中还是有不少的问题。
1.1 index-aot.html
在使用 Express 路由的时候,有下面的代码,但是文档中并没有 index-aot.html 的说明。
server.get(['/', '/dashboard', '/heroes', '/detail/:id'], (req, res) => {
res.render('index-aot.html', {req});
});
可以使用下面的 index-aot.html,其中重要的是加入了3个 JS 文件。
- shim.min.js 是为浏览器提供一些还没有支持的新特性,比如 ES6 中的 promises, symbols, collections, iterators。
- zone.js 是 Angular 依赖的核心库。
- build.js 是 aot 编译后文件
<!DOCTYPE html>
<html>
<head>
<base href="/">
<title>Angular Tour of Heroes</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<!-- Polyfills -->
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
</head>
<body>
<my-app>Loading...</my-app>
</body>
<script src="dist/build.js"></script>
</html>
1.2 webpack output.path
webpack 新版本中 output.path 需要是绝对路径,按照文档中使用相对路径配置会报错
- configuration.output.path: The provided value "src/dist" is not an absolute path!
所以要将
output: {
path: 'src/dist',
filename: 'server.js'
}
改为
var path = require('path');
output: {
path: path.join(__dirname, 'src/dist'),
filename: 'server.js'
}
1.3 webpack tsConfigPath
文档中说 Webpack 配置需要改两个地方,但其实是三个地方
The Webpack configuration file for Universal is almost the same as for AOT. There are two differences:
1.The entry points
2.The output file name
AotPlugin 中的 tsConfigPath 需要改为 tsconfig-uni.json
plugins: [
new ngtools.AotPlugin({
tsConfigPath: './tsconfig-uni.json'
}),
new webpack.optimize.UglifyJsPlugin({ sourceMap: true })
],
1.4 @angular/animations
使用 npm 安装 @angular/platform-server 的时候会提示缺少 @angular/animations,如果不安装后面 build 的时候也会报错:
Could not resolve @angular/animations/browse ...
使用 npm 安装即可
npm i @angular/animations --save
2. 其他的问题
2.1 window is undefined
因为服务器渲染运行在 Node 环境中,因此是不存在 window 这个全局对象的,所以例如 LocalStorage,Cookie 等也是不能使用的。
所以在调用 window.localStorage 就会抛出 window is undefined 的错误。
最简单的方法就是判断如果没有 window 对象,就不使用 LocalStorage :
if (typeof window !== 'undefined') {
const token = localStorage.getItem('token');
}
更好一点的方法是在服务器端模拟一个 storage 对象,并使用 PLATFORM_ID 来判断当前平台是浏览器还是服务器
@Injectable()
export class LocalStorageService {
storage: {};
constructor(@Inject(PLATFORM_ID) private platformId: Object) {
if (isPlatformBrowser(this.platformId)) {
this.storage = window.localStorage;
}
if (isPlatformServer(this.platformId)) {
this.storage = {};
}
}
get(key: string) {
return this.storage[key];
}
set(key: string, value: string) {
this.storage[key] = value;
}
remove(key: string) {
delete this.storage[key];
}
}
2.2 setTimeout & setInterval
使用 setTimeout 会降低服务器端渲染的速度,而使用 setInterval 可能会导致渲染失败。
如果在 AppComponent 的构造函数中使用了 setInterval ,而且没有关掉则会导致在浏览器端没有响应。推测应该是在执行定时器的时候,程序会认为渲染还没有结束,所以导致失败。