Angular是一个前端MVVM框架,其特点是通过前端的Javascript对DOM进行操作,从而实现各种交互逻辑。在运行过程中,Angular基本上是一次加载页面,然后通过JS调用Ajax加载后台数据,再显示到页面上。使用Angular开发网站,则网站要依据数据库显示的内容,也是需要调用JS才能得到。这对于某些搜索引擎来说极为不便。普通用户浏览页面时,页面的相应速度也与客户端的性能相关。
服务器端渲染的优点
为了提高SEO等性能,前端MVVM框架(如React, Vue)都有服务器端渲染解决方案。所谓服务器端渲染就是原来在客户端进行的数据加载移到服务器端来完成,加载数据以后,网页带数据(已经填充到DOM里)发送到客户端。Angular有一个官方的解决方案:Angular Universal。Angular Universal以一个插件的形式加载到工程中,只需要对源码做非常小的改动。
需要注意的是,Angular Universal并不是把一个前端javascript驱动的网站变成了一个类似Spring MVC那样的后端驱动的网站。服务器端渲染实际只在从服务器读取一个url的时候执行,浏览器加载服务器端渲染的页面以后,就会按原来的javascript驱动方式执行。Angular Universal只是执行到url对应的component的ngInit阶段,传到浏览器端的除了页面以外还包括完整生命周期的javascript。网站后续的执行,包括点击链接等,angular仍然按没有服务器端渲染的方式执行。
Angular Universal的官方文档见:https://angular.io/guide/universal
中文文档见: https://angular.cn/guide/universal
GitHub源码: https://github.com/angular/universal
在已经用angular-cli生成的angular项目文件夹中调用以下命令:
ng add @nguniversal/express-engine --clientProject <项目名称>
# + @nguniversal/[email protected]
# added 1 package and audited 43200 packages in 9.743s
# found 0 vulnerabilities
# Installed packages for tooling via npm.
# CREATE src/main.server.ts (220 bytes)
# CREATE src/app/app.server.module.ts (427 bytes)
# CREATE src/tsconfig.server.json (219 bytes)
# CREATE webpack.server.config.js (1360 bytes)
# CREATE server.ts (1472 bytes)
# UPDATE package.json (2074 bytes)
# UPDATE angular.json (4526 bytes)
# UPDATE src/main.ts (432 bytes)
# UPDATE src/app/app.module.ts (1390 bytes)
命令执行以后,会生成以下目录结构
+-- src/
| +-- index.html app web page
| +-- main.ts bootstrapper for client app
| +-- main.server.ts * bootstrapper for server app
| +-- style.css styles for the app
| +-- app/ ... application code
| | +-- app.server.module.ts * server-side application module
+-- server.ts * express web server
+-- tsconfig.json TypeScript client configuration
+-- tsconfig.app.json TypeScript client configuration
+-- tsconfig.server.json * TypeScript server configuration
+-- tsconfig.spec.json TypeScript spec configuration
+-- package.json npm configuration
+-- webpack.server.config.js * webpack server configuration
用 Universal 在本地系统中渲染应用可使用以下命令:
npm run build:ssr && npm run serve:ssr
前端编写代码时需要注意代码编写的规范性,否则编译会出现问题。
import { Statics } from 'src/app/service/static.service';
// 改为
import { Statics } from '../../../service/static.service';
由于 Universal 应用在服务器端渲染时并没有运行在浏览器中,因此该服务器上可能会缺少浏览器的某些 API 和其它能力。
比如,服务端应用不能引用浏览器独有的全局对象,比如 window、document、navigator 或 location。
一方面,我们需要避免直接使用这些全局对象,否则服务器渲染时,服务器端会报错,导致无法正常输出渲染的页面。
Angular 提供了一些这些对象的可注入的抽象层,比如 Location 或 DOCUMENT,它可以作为你所调用的 API 的等效替身。 如果 Angular 没有提供它,你也可以写一个自己的抽象层,当在浏览器中运行时,就把它委托给浏览器 API,当它在服务器中运行时,就提供一个符合要求的代用实现(也叫垫片 - shimming)。
另一个办法是通过判断是否在服务器端还是在浏览器端执行,在服务器端执行时跳过只能在客户端执行的代码,例如:
import { PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
constructor(@Inject(PLATFORM_ID) private platformId: Object) { ... }
ngOnInit() {
if (isPlatformBrowser(this.platformId)) {
// Client only code.
...
}
if (isPlatformServer(this.platformId)) {
// Server only code.
...
}
}
使用标准路由进行跳转
对于需要SEO的页面(即希望搜索引擎爬取的页面),应该有url入口,而不应该依赖用户点击某个按钮来显示。
因此应该尽量使用标准的A标签并使用routerLink属性,而不是使用click事件。
常规Angular项目只需要将build出来的文件copy到服务器,然后用nginx做静态的指向即可。
但Angular Universal则需要在服务器端执行js。
通常的做法是使用nodejs直接执行,或使用pm2命令工具。
执行npm run build:ssr后,dist文件夹下会生成以下文件夹
+-- dist/
| +-- browser
| | +-- ......
| +-- server
| | +-- ......
| +-- server.js
将server目录下的文件拷贝到browser目录
然后在服务器端执行node命令
node dist/server
如果使用pm2命令,可执行以下命令
# 安装pm2
npm install -g pm2
# 使用pm2启动项目
pm2 start dist/server
执行了服务器端渲染的程序,在客户端仍然需要调用Rest API接口。
如果API接口url域名不是在当前服务器,则调用时有可能会产生跨域问题。
在生产环境,这种情况可以通过nginx来解决。在调试环境,如果不做服务器端渲染直接调用ng命令,则可利用ng的proxy功能解决。
但如果调试状态下使用服务器端渲染,即使用node命令执行server.js时,由于所有访问都需经过server.js处理,这就需要server.js具备代理功能。
于是,我们使用express-http-proxy, 对server.ts进行一些改造,使其在接收到api调用时,转向指定的后台服务器。
首先,需要安装express-http-proxy依赖。
npm install express-http-proxy --save
在server.ts中加入以下语句
// ...
import * as proxy from 'express-http-proxy';
// ...
// Example Express Rest API endpoints
// app.get('/api/**', (req, res) => { });
let apiProxy = proxy('172.18.6.134:10000', {
proxyReqPathResolver: function(req) {
return req.originalUrl;
},
userResHeaderDecorator(headers, userReq, userRes, proxyReq, proxyRes) {
return headers;
}
});
app.use('/api/**', apiProxy);
以上代码会将所有访问 /api 路径的请求转向服务器 172.18.6.134:10000,并向客户端返回请求结果。