很多开发社区都在讨论 Angular 6(详见下方链接)的话题,在社区还能找到诸如如何构建客户端应用,或如何创建对 SEO 友好的用户界面等问题的答案。不过很多人没想到的是,最近最引人注目的新事物是在服务器端诞生的。
Angular Universal就是这样的一项革命性技术。它的宗旨是帮助开发者开发全新一代 Web 应用和 Angular 移动应用(详见下方链接)。本文将重点介绍它的各项功能!
相关链接:
Angular 6: https://2muchcoffee.com/blog/angular-6-features/
Angular 移动应用: https://2muchcoffee.com/mobile-app-development
在 IT 行业,Angular 是广为人知的框架技术,而单页应用也一样很出名。其实 Angular 单页应用有很多潜在优势,诸如:
更流畅、速度更快的 UI,因为 Angular 单页应用中大多数 HTML+CSS+Script 资源都是一次性加载完的。之后,只有在渲染页面部分必要数据时才会被载入,这具体取决于用户的操作。
Angular 的 SPA 是作为客户端技术运行的,HTML 页面是静态的,而所有动态修改都发生在浏览器中。在早期的 PHP 中,JSP 和 HTML 都与服务器端逻辑混在了一起,它是在服务器上生成的,所以服务器需要处理更多负载。
总之,我们可以很容易地证明单页应用(SPA)提供了很出色的用户界面性能,并带来了很好的用户体验!
既然 SPA 有这么多好处,提供了这么高质量的用户体验,你肯定会问为什么它没有广泛普及呢?下面就是它的一些局限。
首先,搜索引擎无法确定网页到底是已准备好渲染呢,还是还没有渲染完毕。例如,SPA 还没加载完或者渲染完的时候搜索引擎就没法获取整个 HTML。只有在 MVC 都开始工作后页面才准备好让搜索引擎渲染其数据。
这里的麻烦在于搜索引擎得挑对时间开始扫描,或者知道什么时候渲染工作结束;否则引擎很可能会索引一些不完整的内容。
此外,SPA 深度链接索引起来比较复杂,这也是是 SPA 与搜索引擎相性不佳的另一大因素。
SPA 还缺少对浏览器中 HTML5 历史记录的支持,只能用其他方法替代,例如用来在 URL 之间跳转的 HTML 书签锚点 (/main#section2)。尽管搜索引擎很难分开索引页面,但想要做到这一点也有一些办法。只是相比这些绕弯子的办法来说,纯 HTML 还是更省事。
此外,SPA 的性能表现还是个问题,例如它的初始加载就很慢。众所周知,HTML 解决方案在好些指标上都比 SPA 强,速度就是其中之一(移动平台尤其明显);因为 SPA 要处理大量 JS 数据,所以启动很慢。
一个好消息是谷歌改进了索引单页应用的方法,所以上面提到的两个缺陷已经不复存在了。此外,在微软官方带头下,人们开始抛弃 IE9,这也让大多数平台上的 HTML5 历史可用性得到了提升。
现在还可以应用”/main/section2“这类简单 URL,所以不用强制实现 URL 锚点了。
当然,这对 SPA 的发展有很大助力。但我们还应该考虑其他主流搜索引擎的情况,例如在中国流行的百度,或者美国人喜爱的必应、雅虎等。
的确这些改进看起来并不足以为 SPA 吸引太多忠实粉丝,但也不用那么失望。对于单页应用来说,仍然有一种方法可以在浏览、优化和 SEO 性能之间取得平衡,它就是 Angular Universal。
简而言之,Angular Universal 让开发者可以创建速度快、互动内容丰富且对 SEO 友好的网站,同时充分利用单页应用的所有优势。
Angular Universal 不仅能在服务器端提供标记渲染,还能提供一个简化版的 Angular 在前端生成所需的 HTML。这样一来你就能获得一个 Angular SEO 友好的单页应用,而且它会从服务器获取主要的 HTML 负载,缩短启动时间。
从渲染初始 HTML 到 Angular 作为 SPA 开始工作之间有一段延迟。如果用户在 Angular 接管之前就触发了什么事件的话,现在 Angular Universal 也有办法应对了:它会记录下服务器渲染的事件,然后等客户端 SPA 就绪后在客户端执行事件。
想要入门 Angular Universal 的话,最好的途径就是使用官方的 universal-starter(新手包)。它带有一些现成的应用,其中包括支持服务器端渲染的 express 服务器。
有一些问题是开发者使用 Angular Universal 开发第一个项目时经常遇到的。虽说“它完全是开箱即用的”,但你还是要注意一些要点。下面列举几个 Angular Universal 的实例。
一般来说我们希望应用在客户端和服务器端执行的结果是一致的,而且不需要依赖任何 API。
但现实情况略有不同,有时候很难让代码实现上述目标。此外,我们可能希望有些操作在服务器端和客户端上有不一样的执行效果。
例如,当需要使用调用 DOM 元素的外部库时,服务器端进程无法访问浏览器内部对象,结果就会出错。比如说用 Svg.js 这个第三方库时,在 SSR(服务器端渲染)模式下构建项目时会出现一些预料之内的错误。
为了解决这个问题,我们为这个库添加了一个包装器,它检查到代码是在客户端执行时就会允许访问库方法。这样我们就可以避免在服务器端调用库方法导致的错误了。
import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
@Injectable()
export class SvgService {
private _svg: any;
constructor(@Inject(PLATFORM_ID) private platformId: Object) {
if (isPlatformBrowser(this.platformId)) {
this._svg = require('svg.js');
}
}
get(element) {
if (isPlatformBrowser(this.platformId)) {
return this._svg.get(element);
}
}
}
SPA 只有一个索引文件,当你需要为不同的路由添加不同的标题和元数据标签时它就会碍事。它跟社交媒体嵌入功能也有关系,你在 Facebook 或 Twitter 分享页面后想要生成预览时它也会出点问题。
为了解决这个问题,我们创建了一个服务,让它为每个页面动态添加必要的元数据标记。
服务示例:
import { Injectable } from '@angular/core';
import { Title, Meta } from '@angular/platform-browser';
import * as seoConfig from '../../../assets/config/seo-config.json';
@Injectable()
export class SeoService {
constructor(private titleService: Title, private meta: Meta) {}
setMeta(page: string) {
this.setTitle(seoConfig[page].title);
this.setNameAttribute('description', seoConfig[page].description);
this.setNameAttribute('keywords', seoConfig[page].keywords);
this.setNameAttribute('twitter:title', seoConfig[page].title);
this.setNameAttribute('twitter:description', seoConfig[page].description);
this.setNameAttribute('twitter:image', seoConfig[page].image);
this.setPropertyAttribute('og:title', seoConfig[page].title);
this.setPropertyAttribute('og:description', seoConfig[page].description);
this.setPropertyAttribute('og:url', seoConfig[page].url);
this.setPropertyAttribute('og:image', seoConfig[page].image);
}
private setTitle(title: string) {
return this.titleService.setTitle(title);
}
private setNameAttribute(attribute: string, value: string) {
if (this.checkAttributeExist(attribute, 'name')) {
this.meta.updateTag({name: attribute, content: value});
} else {
this.meta.addTag({name: attribute, content: value});
}
}
private setPropertyAttribute(attribute: string, value: string) {
if (this.checkAttributeExist(attribute, 'property')) {
this.meta.updateTag({property: attribute, content: value});
} else {
this.meta.addTag({property: attribute, content: value});
}
}
private checkAttributeExist(attribute: string, type: string) {
return this.meta.getTag(`${type}="${attribute}"`);
}
}
然后这个组件会变成:
import { SeoService } from '../core/services/seo.service';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss']
})
export class HomeComponent {
constructor(private seoService: SeoService) {
this.seoService.setMeta('home');
}
使用 seoconfig.json 文件时的情况:
{
"home": {
"title": "2muchcoffee | Web and Mobile Application Development Company",
"description": "2muchcoffee is top full-stack web and mobile app development company specializing in frontend and backend JS frameworks. Building cross-platform web, hybrid and native mobile applications for established businesses and MVP's for startups.",
"keywords": "2muchcoffee, Angular, frontend, backend",
"url": "https://2muchcoffee.com",
"image": "/assets/img/content/opengraph/main.png"
}
}
开发者经常会使用基于 Angular 功能的第三方服务,如自定义指令和组件等。这里用 Angular Flex layout 来举例。
它可能会导致一些影响用户体验的意外问题。在服务器端渲染之后,客户端收到的文档已经包含了带有样式的 style 标签。但是 @angular/flex-layout 只在 Angular 库完全加载后才开始工作。
Angular 库加载完毕后才能正确操作上述指令。根据网络情况,从下载初始文档到 Angular 接管之间可能需要几秒钟的延迟。
在此期间,用户可能会看到不包含 flex 标记的页面。等 Angular 启动后所有内容都会归位,但这时页面就会闪烁。
为了解决这个用户体验问题,我们决定拒绝在主页上使用 @angular/flex-layout 指令,并在 CSS 文件中指定 flex 标记属性。