项目版本:
"@angular/core": "8.2.6"
"@angular/cli": "^8.3.3"
首先根据官方介绍配置相关文件:Server-side Rendering (SSR): An intro to Angular Universal
需要新增(用“*”标出)或修改的文件如下:
src/
// app web page
index.html
// bootstrapper for client app
main.ts
// * bootstrapper for server app
main.server.ts
// styles for the app
style.css
// application code
app/ ...
// * server-side application module
app.server.module.ts
// * express web server
server.ts
// TypeScript client configuration
tsconfig.json
// TypeScript client configuration
tsconfig.app.json
// * TypeScript server configuration
tsconfig.server.json
// TypeScript spec configuration
tsconfig.spec.json
// npm configuration
package.json
// * webpack server configuration
webpack.server.config.js
服务端没有浏览器API,因此如果直接使用,会在浏览器端报错。针对不同情况有以下几种解决方案:
- 通过PLATFORM_ID区分执行环境,进而执行特定代码:
import { PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
constructor(@Inject(PLATFORM_ID) private platformId: Object) {}
ngOnInit() {
if (isPlatformBrowser(this.platformId)) {
console.log('仅在客户端运行')
}
if (isPlatformServer(this.platformId)) {
console.log('仅在服务端运行')
}
}
- 很多时候使用的是navigator.userAgent,window.location.hostname,这些可以在node服务中拿到并注入到业务代码中:
// server.ts
app.get('*', (req, res) => {
res.render(
'index',
{
req,
res,
providers: [
{
provide: 'APP_HOSTNAME',
useFactory: () => req.hostname,
deps: []
},
// 注入cookies
{
provide: 'APP_COOKIES',
useFactory: () => req.get('Cookie'),
deps: []
},
// 注入ua
{
provide: 'APP_UA',
useFactory: () => req.get('User-Agent'),
deps: []
}
]
},
(err, html) => {
if (err) {
throw err
}
res.send(html)
}
)
})
// test.component.ts
import {
Inject,
Optional,
PLATFORM_ID
} from '@angular/core'
constructor(
@Inject(PLATFORM_ID) private platformId: Object,
@Inject('APP_HOSTNAME') @Optional() private readonly hostName: string
) {}
ngOnInit() {
if (isPlatformBrowser(this.platformId)) {
this.host = window.location.hostname
} else {
this.host = this.hostName
}
}
问题2、使用的第三方库不支持
- 有些第三方库并没有考虑到服务端执行,会报一些类似于
document is not defined
的错误,首先查看报错的库是否有更新修复,如果他们在后期支持了服务端的判断,则可以升级解决。 - 如果第三方库并没有解决,例如我们使用了countup.js,则可以将库移到本地修改以支持,缺点是后期不方便升级
- 动态引入。如果第三方库本来也不必在服务端执行,但是引入即报错,我们可以使用懒加载使得其在服务度不会被引入。 例如我们用到的库jsencrypt:
import { from, Observable, of } from 'rxjs'
@Injectable()
export class RsaService {
encrypt (data): Observable<string> {
if (isPlatformBrowser(this.platformId)) {
return from(import('jsencrypt')).pipe(
map(({ JSEncrypt }) => {
const encrypt = new JSEncrypt()
return encrypt.encrypt(data)
})
)
} else {
return of(data) as any
}
}
}
问题3、路由
如果项目原本有使用window.open或window.location.href等方式进行路由,统一都改为angular官方路由系统Router
import { Router } from '@angular/router'
constructor(
private router: Router
){}
goToTest (type, symbol) {
// 删除:window.location.href = `${this.url}/trade`
// 改为:
this.router.navigateByUrl(`${this.url}/trade`)
}
问题4、动画
如果你的项目有使用BrowserAnimationsModule添加动画,则需要在app.server.module.ts文件中引入NoopAnimationsModule模块,它模拟了真实的动画模块,但是实际上确什么都没有做。
import { NoopAnimationsModule } from '@angular/platform-browser/animations'
@NgModule({
imports: [
AppModule,
ServerModule,
NgbModule,
NoopAnimationsModule
],
bootstrap: [AppComponent]
})
export class AppServerModule {}
问题5、避免重复的HTTP请求
- 如果你的http请求使用了angular的HttpClient来实现,则可以通过引入两个模块来实现数据的同步。在app.module.ts中添加TransferHttpCacheModule,在app.server.module.ts中添加ServerTransferStateModule模块。这样可以轻松实现服务的请求结果的缓存,客户端会从缓存中拿到结果进而避免重复的请求。
- 如果你使用了其他http库,则需要在上面的基础上,再在app.module.ts引入BrowserTransferStateModule。
ps: 具体用法请参考Avoiding duplicate HTTP calls in Angular Universal
- 我们遇到了一个情况是5个请求中,只有2个请求被缓存了。后来通过查看TransferHttpCacheModule的逻辑发现,如果你的请求中有post请求,则会被停止进行缓存,剩下的请求也都不会被缓存,导致到了客户端依旧会触发二次请求。最终我们自己修改了TransferHttpCacheModule的逻辑来支持(被逼无奈),但是其实在初始化页面中通常是不会含有post请求的,可能他们认为如果有post请求,则可能会导致之前的get请求内容过期。如果你遇到同样的问题,查看一下你们的请求类型。
问题6、服务端使用模块映射而不是懒加载
- 服务端使用模块映射替代懒加载,在server module添加如下:
// app.server.module.ts
import {ModuleMapLoaderModule} from '@nguniversal/module-map-ngfactory-loader';
@NgModule({
imports: [
AppModule,
ServerModule,
ModuleMapLoaderModule
],
bootstrap: [AppComponent],
})
export class AppServerModule {}
// server.ts
const DIST_FOLDER = join(process.cwd(), 'dist')
const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require(join(
DIST_FOLDER,
'server',
'main'
))
app.engine('html', ngExpressEngine({
bootstrap: AppServerModuleNgFactory,
providers: [provideModuleMap(LAZY_MODULE_MAP)]
}))
问题7、setTimeout造成的问题
- setTimeout或类似逻辑会拖慢服务器的渲染过程,如果你的页面迟迟不返回,检查一下你的代码逻辑中是否有setTimeout,或者rxjs的timer。
private componentDestroyed$ = new Subject()
constructor (
@Inject(PLATFORM_ID) private platformId: object
) {}
ngOnInit () {
let timerTrigger = of(1)
if (isPlatformBrowser(this.platformId)) {
// 只在客户端启用timer,且在ngOnDestroy中删除他们。
timerTrigger = timer(0, 20000).pipe(takeUntil(this.componentDestroyed$))
}
}
ngOnDestroy () {
this.componentDestroyed$.next()
this.componentDestroyed$.complete()
}
问题8、使用Renderer2操作DOM
Renderer2可以帮助你在服务端操作DOM的属性内容等,甚至可以完成创建插入等操作,因为我没有遇到,所以请参考官方文档:Renderer2
问题9、require 引用静态资源
我们项目中有require引用静态资源的情况,但是生成的html中的引用并没有动态改变路径,且文件名没有哈希值。需要在angular.json中添加配置解决,添加outputHashing解决哈希问题,添加deployUrl解决路径匹配问题(我们的静态资源都放在assets目录中,所以我们设置了:”deployUrl”: “/assets/”)
// angular.json
"server": {
"builder": "@angular-devkit/build-angular:server",
"configurations": {
"production": {
// 可能会报Property deployUrl is not allowed.我这个版本的@angular/cli/lib/config/schema.json中server没有deployUrl的属性,所以会提示不合法,但是实际上装载的“@angular-devkit/build-angular:server”是包含这个属性的,所以可以正常执行。
"deployUrl": "/assets/",
"outputHashing": "media",
}
}
}
问题10、页面闪烁
Cannot find a way to achieve a smooth transition to client app 闪烁是很多人提到的一个问题, 我们的闪烁主要是路由动画造成的,当我们删除路由动画时,闪烁问题不再存在,且切换更快速(完全没必要弄个路由动画)
问题11、websocket处理
因为页面有websocket导致请求无返回,因为我们的websocket服务是自己写的,所以加了判断在服务端返回rxjs的empty来解决这个问题。
未解决问题
开发过程中每次改动都需要重新build整个项目,非常影响效率,有如下issues提到该问题,现在还未解决: