最近开发的项目,需要seo优化,百度等搜索引擎可以搜索到,于是想到了angular的ssr。下面慢慢记录
一、universal教程
1、先敲一边angular文档的教程。
官方文档
(1)添加 nguniversal
1
ng add @nguniversal/express-engine
(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}`);