@nguniversal/express-engine angular SSR 服务器端渲染 实践
最近开发的项目,需要seo优化,百度等搜索引擎可以搜索到,于是想到了angular的ssr。下面慢慢记录
一、universal教程
1、先敲一边angular文档的教程。
(1)添加 nguniversal
1ng add @nguniversal/express-engine
该命令会创建如下文件夹结构
标有 * 的文件都是新增的,不在原始的教程范例中。
(2)启动应用
1npm run dev:ssr
2、server.ts 修改
使用了路由,我们可以轻松的识别出这三类请求,并分别处理它们
路由请求类型 | 详情 |
---|---|
数据请求 | 请求的 URL 用 /api 开头。 |
应用导航 | 请求的 URL 不带扩展名 |
静态资产 | 所有其它请求。 |
目前我用的是 http-proxy-middleware
这个插件做代理。在数据请求中统一用 /api
,这样数据请求和静态页面区分开。也可以通过 nginx 做好代理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35import 'zone.js/node'; const TIME_OUT = environment.timeOut; const HOST = environment.proxyHost; const baseUrl = environment.baseUrl export function app(): express.Express { ... // 转发 const options = { target: HOST, // target host changeOrigin: true, // needed for virtual hosted sites ws: true, // proxy websockets pathRewrite: { ['^'+baseUrl]:'' }, router: { }, }; server.use(createProxyMiddleware([baseUrl], options)); ... server.get('*', (req, res) => { res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] }); }); ... }
3、在服务端使用绝对 URL 进行 HTTP(数据)请求
官网上并没有详细的描述,,在服务器端可以通过 @inject 注入获取路径绝对路径,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24app.get('/*', (req, res) => { let proto = req.protocol; if (req.headers && req.headers['x-forwarded-proto']) { proto = req.headers['x-forwarded-proto'].toString(); } const url= `${proto}://${req.get('host')}`; res.render(indexHtml, { req, providers: [ { provide: 'serverUrl', useValue: url, }, ], }); }); // 前端 constructor( @Optional() @Inject('serverUrl') private serverUrl: string, ){} let url = this.serverUrl + url
二、服务端到客户端的状态传输
解决调取两次接口的问题,服务端渲染页面的时候,会调取一次接口。客户端渲染的时候,又会调取一次接口。为了防止这种情况,可以使用 transferStatus
API,帮助解决这种情况。 它可以将数据从应用程序的服务器端传输到浏览器应用程序.
在 app.server.module.ts
导入 ServerTransferStateModule
在 app.module.ts
导入 BrowserTransferStateModule
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15constructor( private state: TransferState, ){} const key = makeStateKey(apiUrl) const a = this.state.get<any>(key, null) if(a){ ... return null } this.http.get().subscribe(res=>{ ... this.state.set(key, res) })
这样就可以在服务端获取数据,在浏览器端直接从 this.state
中获取。
在每个请求处都这样写,也太麻烦了。可以添加到 HttpInterceptor
中
1
2
3
4
5
6
7
8
9
10
11
12
13
14const key = makeStateKey(apiUrl) const a = this.state.get<any>(key, null) if(a){ this.state.set(key, null) return of(new HttpResponse({body: a.body})) } return next.handle(resetReq).pipe( tap(ev => { if ((ev instanceof HttpResponse) && this.serverUrl) { this.state.set(key, <any>ev) } }) );
三、遇到的问题
1、window、document等报错。需要添加判断
1
2
3
4
5
6
7
8
9
10constructor(@Inject(PLATFORM_ID) private platformId: Object, @Inject(APP_ID) private appId: string) { // 判断运行环境为客户端还是服务端 isPlatformBrowser(platformId) isPlatformServer(platformId) // 也可以直接判断 if(window){...}
2、尽量使用官方推荐的方式操作dom
Renderer2
1
2
3
4
5constructor( private el: ElementRef, private rd:Renderer2, ) {} this.rd.setAttribute(this.el.nativeElement, 'draggable', `${draggable}`);