/

@nguniversal/express-engine angular SSR 服务器端渲染 实践

最近开发的项目,需要seo优化,百度等搜索引擎可以搜索到,于是想到了angular的ssr。下面慢慢记录

一、universal教程

1、先敲一边angular文档的教程。

官方文档

(1)添加 nguniversal

1
ng add @nguniversal/express-engine

该命令会创建如下文件夹结构 null

标有 * 的文件都是新增的,不在原始的教程范例中。

(2)启动应用

1
npm 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
35
import '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
24
app.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
15
constructor( 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
14
const 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
10
constructor(@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
5
constructor( private el: ElementRef, private rd:Renderer2, ) {} this.rd.setAttribute(this.el.nativeElement, 'draggable', `${draggable}`);
作者:liuk123标签:angular分类:angular

本文是原创文章,采用 CC BY-NC-ND 4.0 协议, 完整转载请注明来自 liuk123