因为我已经使用Angular Universal试用了几个月,所以我要写下我的学习心得
这个内容是将在公司内部LT中使用的内容写成文字形式并加以扩展。
因为写得比较慢,结果Angular 4 RC已经发布了。。。
我使用Angular Universal在空闲时间开发了一些公司内部的服务。在这个过程中,我将记录下我尝试中遇到的困难和问题。
首先
请您阅读Quramy在这篇关于Angular2的服务器端渲染的文章来了解Angular Universal的基本概念。如标题所示,Angular Universal是一组用于在Angular中进行服务器端渲染(SSR)的模块集合。
Angular Universal的模块本身还处于RC1阶段,目前在npm上发布的版本无法跟随Angular 2的升级(2.4)。
据说正式版的发布时间与Angular 4相同,预计在3月,所以请稍稍等待。
如果你还想像我一样试试看的话,请克隆并运行Angular 2 Universal Starter。你会知道它是什么样的。
我们这次的服务也是基于Angular 2 Universal Starter构建的。
请您留言纠正,以下是有关现有Angular Universal的要点:
重要点1:SSR失败不会导致错误。
有一次我认为「虽然已经进行了SSR,但为什么加载这么慢呢?是因为Angular太重了吗?」可事实上,后来才发现并没有成功进行SSR。
原因是由于在Node端的主模块node.module.ts中存在错别字。
尽管SSR在中途失败,但express本身没有崩溃,仍然作为服务器运行,
因此在客户端进行渲染。
注意
这次因为没有写函数的返回类型而绕过了编译错误,而且如果一直在 watch 模式下运行 tslint 的话就能发现这个问题。
由于要说反了,不只是 Lint,还有像 TypeScript 这样有类型的话也可以减少错误,所以大家都应该写 TypeScript。
点2.1:浏览器与Node之间的差异。
在编写通用代码时,需要注意的是浏览器和Node之间的差异。我认为经常会有特定对象只存在于其中一个平台的情况。而且我们应该尽可能地(组件)写得清晰、优雅。
在这次开发过程中,我们需要使用Notification和SessionStorage。
注意
也许在这种情况下,作为Angular,我认为创建一个相对整洁的组件,同时创建一个服务是正确的做法,并且我已经实施了这个方案。
通过这种方式,组件就不再需要担心浏览器和Node的差异了。
然而,最终结果是服务仍然需要关注浏览器和Node的差异。
我认为在如何处理这种差异的问题上,结果是一样的,但有两种方式。
不依赖于Angular Universal进行判断。
你可以用以下常見的條件式 if (typeof window === ‘undefined’) { } 等來判斷是在瀏覽器還是在 Node 環境。
在這種情況下,你可以將 Service 包括在內,從目前的階段開始,將其作為 NgModule 公開給外部使用,只要 Angular 的 API 不改變,就可以保持向後兼容。
请给出一个原生中文的选项。
代码示例:
@Injectable
class NotificationService {
constructor() {}
requestPermission() {
if (this.isBrowser && this.isPermitted) {
window.Notification && Notification.requestPermission();
}
}
get isBrowser(): boolean {
return typeof window !== 'undefined';
}
get isPermitted(): boolean {
return window.Notification && Notification.permission === 'granted';
}
}
以Angular Universal作为依据进行判断。
只需以下方式导入Angular Universal即可,因为它包含了一个用于判断的值。
-
- import {isBrowser, isNode} from ‘angular2-universal/browser’
- import {isBrowser, isNode} from ‘angular2-universal/node’
你可以通过查看Angular 2 Universal Starter中的src/{browser.module.ts, node.module.ts}来了解应该从哪个位置进行import。你需要在浏览器和Node分别创建MainModule并在那里进行依赖注入。
然后,在服务等部分中,你可以在构造函数中进行依赖注入并使用值。
在这种情况下,将NgModule用作应用程序的一部分并且不对外部公开是有效的。
(如果未来Angular的核心中包含了Universal,那么即使在这里进行描述也可以进行外部公开,但是可能无法实现向后兼容。)
Code example:
代码示例:
@Injectable
class NotificationService {
constructor(@Inject('isBrowser') private isBrowser) {}
requestPermission() {
if (this.isBrowser && this.isPermitted) {
window.Notification && Notification.requestPermission();
}
}
get isPermitted(): boolean {
return window.Notification && Notification.permission === 'granted';
}
}
2.2点:Angular 2模块的处理方式
这个提及的是与2.1点相似的情况,但这里是关于使用npm提供的模块的讨论。
npm提供了许多模块,它们使用angular2-或ng2-作为前缀。
但是由于大多数模块没有考虑到Universal,因此在实际使用时经常会遇到以下错误:
ReferenceError: window is not defined
注意
可以通过在2.1部分中提到的isBrowser和isNode的DI值,并在模板内或组件中重复使用,来解决这个问题。然而,并不是所有情况都可以避免。因此,在某些情况下,可能需要自行修复并提交PR。
然而,由于增加了进行判断和分类的操作,将无法进行SSR,这会导致初始加载变得缓慢。虽然我们这次决定不予考虑这个问题,但我认为在实际应用中肯定会出现需要解决的情况。
要点3:处理CSS
Angular 2 Universal Starter中的webpack.config.ts文件可见,使用raw-loader将CSS同时输出到浏览器和Node中。
因此,在加载时会导致CSS重复输出,如果使用CSS框架或在服务中使用大量CSS,则会增加额外的通信量。
我认为在Angular 4中已经进行了修正,但是当我们在AppComponent中使用encapsulation: ViewEncapsulation.None来应用全局样式时,在开发时没有问题,但是在进行AOT构建时,虽然构建能够通过,但在运行时会出现错误。
注意
在处理这个问题时,我们选择使用Webpack的extract-text-webpack-plugin来合并CSS文件,以达到全局统一的样式。同时,我们将页面相关的CSS使用raw-loader单独输出。
您可以在以下提交中查看实施示例:
https://github.com/nana4gonta/universal-starter/commit/7fc3fdd3fdc5ee041e5535af2863c4442804a955
要点4:Express侧的异步处理
在这段代码中,Express不仅负责Angular的SSR,还扮演了一个API服务器的角色,用于返回从MySQL和其他API获取的数据,因此需要进行异步处理。
由于这是在Angular 4发布之前,而TypeScript版本为2.0系列,所以以下几种方法可以考虑作为异步处理的方式。
-
- Promise
-
- co + Generator
- (Angularの依存関係にある)RxJS
注意
因为只在Express端使用,为了避免增加依赖,我们选择使用RxJS进行实现,但这是一个错误的决策。
无法充分享受到Rx的好处,无论是从MySQL获取数据还是插入数据,反而由于不熟悉而导致代码变得混乱。
关于这一点,我认为Promise和co + Generator以及Angular 4的async/await应该会有相似的效果,从而使代码变得非常方便和可读。适当的运用是非常重要的。
总结
由于我自己尝试的时候是在Angular 2.0系列 + Angular Universal RC的环境下的。
-
- TypeScript 2.1で追加された機能の恩恵にあやかれない
- Angular 2.2, 2.3, 2.4の恩恵にあやかれない
尽管遇到了所谓的‘双重困境’和之前提到的四个问题,但我们还是能进行有趣的开发。我认为在开始适应Angular 4之前,源代码将会被公开,为了下一步,我们不仅要适应Angular 4,还要努力实现可复用的优雅代码。