如何友好的启动Angular应用

前端之家收集整理的这篇文章主要介绍了如何友好的启动Angular应用前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

一、引言

一个单页应用第一次启动从文档的下载(包括各种资源)再到初始化至成功渲染这一过程基本上都是以秒为单位的。

Angular应用的 index.html 会在文档当中写入根组件,例如:

  1. <app-root>Loading...</app-root>

直到Angular初始化完成后 Loading... 字样才会从页面消失,并进入实际的应用。当然相比较一版空白着实还算优雅一点。

然而一个好的应用的体验怎能这样呢,有兴趣的可以先看一下 ng-alain 是如何友好的启动Angular的。

二、如何才算友好?

我们知道浏览器需要先接收一个HTML文档,然后解析文档并加载相应的样式及脚本文件,这里有很多优化相关的技术细节,但更多细节本文不作探讨。

对于Angular而言,真正开始渲染组件会在 platformBrowserDynamic().bootstrapModule 之后,因此若说友好,理应在此之前把那该死的 Loading... 换成一个动画或更友好的效果

所以,得出第一个要点:尽可能早显示启动动画,并尽可能在组件渲染之前关掉动画

然而,现实与想法的有点不同,那就是绝大部分启动过程中是需要依赖于远程数据,亦或者指引用户应该是进入登录页,还是控制页。

因此,第二个要点:启动前需要至少一次远程交互

三、如何做呢?

1、启动动画

HTML文档下载之后会立即显示,因此,可以利用这一点,把启动动画直接写在 index.html 页面当中。但,我们不应该像开头那样,而是一个复杂的CSS3动画,以下是一摘自 ng-alain

  1. <!doctype html>
  2. <html>
  3.  
  4. <head>
  5. <Meta charset="utf-8">
  6. <title>ngAlain</title>
  7. <base href="/">
  8.  
  9. <Meta name="viewport" content="width=device-width,initial-scale=1">
  10. <link rel="icon" type="image/x-icon" href="favicon.ico">
  11. <style type="text/css">
  12. .preloader {
  13. position: fixed;
  14. top: 0;
  15. left: 0;
  16. width: 100%;
  17. height: 100%;
  18. overflow: hidden;
  19. background: #49a9ee;
  20. z-index: 9999;
  21. transition: opacity .65s;
  22. }
  23.  
  24. .preloader-hidden-add {
  25. opacity: 1;
  26. display: block;
  27. }
  28.  
  29. .preloader-hidden-add-active {
  30. opacity: 0;
  31. }
  32.  
  33. .preloader-hidden {
  34. display: none;
  35. }
  36.  
  37. .cs-loader {
  38. position: absolute;
  39. top: 0;
  40. left: 0;
  41. height: 100%;
  42. width: 100%;
  43. }
  44.  
  45. .cs-loader-inner {
  46. -webkit-transform: translateY(-50%);
  47. transform: translateY(-50%);
  48. top: 50%;
  49. position: absolute;
  50. width: calc(100% - 200px);
  51. color: #FFF;
  52. padding: 0 100px;
  53. text-align: center;
  54. }
  55.  
  56. .cs-loader-inner label {
  57. font-size: 20px;
  58. opacity: 0;
  59. display: inline-block;
  60. }
  61.  
  62. @-webkit-keyframes lol {
  63. 0% {
  64. opacity: 0;
  65. -webkit-transform: translateX(-300px);
  66. transform: translateX(-300px);
  67. }
  68. 33% {
  69. opacity: 1;
  70. -webkit-transform: translateX(0px);
  71. transform: translateX(0px);
  72. }
  73. 66% {
  74. opacity: 1;
  75. -webkit-transform: translateX(0px);
  76. transform: translateX(0px);
  77. }
  78. 100% {
  79. opacity: 0;
  80. -webkit-transform: translateX(300px);
  81. transform: translateX(300px);
  82. }
  83. }
  84.  
  85. @keyframes lol {
  86. 0% {
  87. opacity: 0;
  88. -webkit-transform: translateX(-300px);
  89. transform: translateX(-300px);
  90. }
  91. 33% {
  92. opacity: 1;
  93. -webkit-transform: translateX(0px);
  94. transform: translateX(0px);
  95. }
  96. 66% {
  97. opacity: 1;
  98. -webkit-transform: translateX(0px);
  99. transform: translateX(0px);
  100. }
  101. 100% {
  102. opacity: 0;
  103. -webkit-transform: translateX(300px);
  104. transform: translateX(300px);
  105. }
  106. }
  107.  
  108. .cs-loader-inner label:nth-child(6) {
  109. -webkit-animation: lol 3s infinite ease-in-out;
  110. animation: lol 3s infinite ease-in-out;
  111. }
  112.  
  113. .cs-loader-inner label:nth-child(5) {
  114. -webkit-animation: lol 3s 100ms infinite ease-in-out;
  115. animation: lol 3s 100ms infinite ease-in-out;
  116. }
  117.  
  118. .cs-loader-inner label:nth-child(4) {
  119. -webkit-animation: lol 3s 200ms infinite ease-in-out;
  120. animation: lol 3s 200ms infinite ease-in-out;
  121. }
  122.  
  123. .cs-loader-inner label:nth-child(3) {
  124. -webkit-animation: lol 3s 300ms infinite ease-in-out;
  125. animation: lol 3s 300ms infinite ease-in-out;
  126. }
  127.  
  128. .cs-loader-inner label:nth-child(2) {
  129. -webkit-animation: lol 3s 400ms infinite ease-in-out;
  130. animation: lol 3s 400ms infinite ease-in-out;
  131. }
  132.  
  133. .cs-loader-inner label:nth-child(1) {
  134. -webkit-animation: lol 3s 500ms infinite ease-in-out;
  135. animation: lol 3s 500ms infinite ease-in-out;
  136. }
  137.  
  138. </style>
  139. </head>
  140.  
  141. <body>
  142. <app-root></app-root>
  143. <div class="preloader">
  144. <div class="cs-loader">
  145. <div class="cs-loader-inner">
  146. <label></label>
  147. <label></label>
  148. <label></label>
  149. <label></label>
  150. <label></label>
  151. <label></label>
  152. </div>
  153. </div>
  154. </div>
  155. </body>
  156.  
  157. </html>

HTML 文档包括了动画需要的所有代码,因此可以完成尽可能早显示启动动画这一前提。而后者尽可能在组件渲染之前关掉动画又当如何处理呢?

组件树的渲染会在 bootstrapModule 之后,而其接口又是返回一个 Promise<NgModuleRef<AppModule>>,没错 Promise 意味者允许我们通过 then 来感受Angular启动后做点什么擦屁股的问题,例如去掉动画代码

  1. const bootstrap = () => {
  2. return platformBrowserDynamic().bootstrapModule(AppModule);
  3. };
  4.  
  5. bootstrap().then(() => {
  6. document.querySelector('.preloader').className += ' preloader-hidden-add preloader-hidden-add-active';
  7. });

此问题就这么轻松的解决

2、启动前加载数据

一种非常理所当然的想法便是在 bootstrapModule 之间发送AJAX请求不就可以了。话虽简单,那ajax代码怎么写?是不是还得考虑兼容性问题?远程数据加载后难道用 window.xxx 来存储吗?

若你这么做,那你太小看Angular,Angular是非常强大的。

Angular提供一个叫 APP_INITIALIZER 的 Token 值,用于在应用初始化时执行相应的函数

所以只需要像其它服务编码一样,写一个用于在启动应用时所需要的服务逻辑,以下是一摘自 ng-alain

  1. import { Router } from '@angular/router';
  2. import { Injectable,Injector } from '@angular/core';
  3. import { HttpClient } from '@angular/common/http';
  4. import { MenuService } from "../menu/menu.service";
  5. import { TranslatorService } from "../translator/translator.service";
  6. import { SettingsService } from "../settings/settings.service";
  7. import 'rxjs/add/operator/do';
  8. import 'rxjs/add/operator/toPromise';
  9. import 'rxjs/add/operator/catch';
  10. /**
  11. * 用于应用启动时
  12. * 一般用来获取应用所需要的基础数据等
  13. */
  14. @Injectable()
  15. export class StartupService {
  16. constructor(
  17. private menuService: MenuService,private tr: TranslatorService,private settingService: SettingsService,private httpClient: HttpClient,private injector: Injector) { }
  18.  
  19. load(): Promise<any> {
  20. // only works with promises
  21. // https://github.com/angular/angular/issues/15088
  22. let ret = this.httpClient
  23. .get('./assets/app-data.json')
  24. .toPromise()
  25. .then((res: any) => {
  26. // just only injector way if you need navigate to login page.
  27. // this.injector.get(Router).navigate([ '/login' ]);
  28.  
  29. this.settingService.setApp(res.app);
  30. this.settingService.setUser(res.user);
  31. // 初始化菜单
  32. this.menuService.add(res.menu);
  33. // 调整语言
  34. this.tr.use('en');
  35. })
  36. .catch((err: any) => {
  37. return Promise.resolve(null);
  38. });
  39.  
  40. return ret.then((res) => { });
  41. }
  42. }

这里有两点需要注意:

  • load() 返回值必须是 Promise 类型。@H_502_88@
  • 若需要路由跳转,尽可能采用 this.injector.get(Router) 方式来获取路由实例,不然很容易引起循环依赖BUG。@H_502_88@

服务是需要注册的,自然在根模块中完成。

  1. export function StartupServiceFactory(startupService: StartupService): Function {
  2. return () => { return startupService.load() };
  3. }
  4.  
  5. @NgModule({
  6. providers: [
  7. StartupService,{
  8. provide: APP_INITIALIZER,useFactory: StartupServiceFactory,deps: [StartupService],multi: true
  9. }
  10. ],bootstrap: [ AppComponent ]
  11. })
  12. export class AppModule { }

到此,两件事已经完成了。

四、结论

本文的想法还是来源里群里总有人在问一下问题,如何在Angular启用时先加载远程数据;其中 APP_INITIALIZER 算是很少有人提及的,其它的都是一些日常写法,了无新意。

希望此文能帮助各位。

Happy coding!

猜你在找的Angularjs相关文章