一、引言
一个单页应用第一次启动从文档的下载(包括各种资源)再到初始化至成功渲染这一过程基本上都是以秒为单位的。
Angular应用的 index.html
会在文档当中写入根组件,例如:
<app-root>Loading...</app-root>
直到Angular初始化完成后 Loading... 字样才会从页面消失,并进入实际的应用。当然相比较一版空白着实还算优雅一点。
然而一个好的应用的体验怎能这样呢,有兴趣的可以先看一下 ng-alain 是如何友好的启动Angular的。
二、如何才算友好?
我们知道浏览器需要先接收一个HTML文档,然后解析文档并加载相应的样式及脚本文件,这里有很多优化相关的技术细节,但更多细节本文不作探讨。
对于Angular而言,真正开始渲染组件会在 platformBrowserDynamic().bootstrapModule
之后,因此若说友好,理应在此之前把那该死的 Loading... 换成一个动画或更友好的效果。
所以,得出第一个要点:尽可能早显示启动动画,并尽可能在组件渲染之前关掉动画。
然而,现实与想法的有点不同,那就是绝大部分启动过程中是需要依赖于远程数据,亦或者指引用户应该是进入登录页,还是控制页。
因此,第二个要点:启动前需要至少一次远程交互。
三、如何做呢?
1、启动动画
HTML文档下载之后会立即显示,因此,可以利用这一点,把启动动画直接写在 index.html
页面当中。但,我们不应该像开头那样,而是一个复杂的CSS3动画,以下是一摘自 ng-alain:
<!doctype html> <html> <head> <Meta charset="utf-8"> <title>ngAlain</title> <base href="/"> <Meta name="viewport" content="width=device-width,initial-scale=1"> <link rel="icon" type="image/x-icon" href="favicon.ico"> <style type="text/css"> .preloader { position: fixed; top: 0; left: 0; width: 100%; height: 100%; overflow: hidden; background: #49a9ee; z-index: 9999; transition: opacity .65s; } .preloader-hidden-add { opacity: 1; display: block; } .preloader-hidden-add-active { opacity: 0; } .preloader-hidden { display: none; } .cs-loader { position: absolute; top: 0; left: 0; height: 100%; width: 100%; } .cs-loader-inner { -webkit-transform: translateY(-50%); transform: translateY(-50%); top: 50%; position: absolute; width: calc(100% - 200px); color: #FFF; padding: 0 100px; text-align: center; } .cs-loader-inner label { font-size: 20px; opacity: 0; display: inline-block; } @-webkit-keyframes lol { 0% { opacity: 0; -webkit-transform: translateX(-300px); transform: translateX(-300px); } 33% { opacity: 1; -webkit-transform: translateX(0px); transform: translateX(0px); } 66% { opacity: 1; -webkit-transform: translateX(0px); transform: translateX(0px); } 100% { opacity: 0; -webkit-transform: translateX(300px); transform: translateX(300px); } } @keyframes lol { 0% { opacity: 0; -webkit-transform: translateX(-300px); transform: translateX(-300px); } 33% { opacity: 1; -webkit-transform: translateX(0px); transform: translateX(0px); } 66% { opacity: 1; -webkit-transform: translateX(0px); transform: translateX(0px); } 100% { opacity: 0; -webkit-transform: translateX(300px); transform: translateX(300px); } } .cs-loader-inner label:nth-child(6) { -webkit-animation: lol 3s infinite ease-in-out; animation: lol 3s infinite ease-in-out; } .cs-loader-inner label:nth-child(5) { -webkit-animation: lol 3s 100ms infinite ease-in-out; animation: lol 3s 100ms infinite ease-in-out; } .cs-loader-inner label:nth-child(4) { -webkit-animation: lol 3s 200ms infinite ease-in-out; animation: lol 3s 200ms infinite ease-in-out; } .cs-loader-inner label:nth-child(3) { -webkit-animation: lol 3s 300ms infinite ease-in-out; animation: lol 3s 300ms infinite ease-in-out; } .cs-loader-inner label:nth-child(2) { -webkit-animation: lol 3s 400ms infinite ease-in-out; animation: lol 3s 400ms infinite ease-in-out; } .cs-loader-inner label:nth-child(1) { -webkit-animation: lol 3s 500ms infinite ease-in-out; animation: lol 3s 500ms infinite ease-in-out; } </style> </head> <body> <app-root></app-root> <div class="preloader"> <div class="cs-loader"> <div class="cs-loader-inner"> <label> ●</label> <label> ●</label> <label> ●</label> <label> ●</label> <label> ●</label> <label> ●</label> </div> </div> </div> </body> </html>
HTML 文档包括了动画需要的所有代码,因此可以完成尽可能早显示启动动画这一前提。而后者尽可能在组件渲染之前关掉动画又当如何处理呢?
组件树的渲染会在 bootstrapModule
之后,而其接口又是返回一个 Promise<NgModuleRef<AppModule>>
,没错 Promise
意味者允许我们通过 then
来感受Angular启动后做点什么擦屁股的问题,例如去掉动画代码。
const bootstrap = () => { return platformBrowserDynamic().bootstrapModule(AppModule); }; bootstrap().then(() => { document.querySelector('.preloader').className += ' preloader-hidden-add preloader-hidden-add-active'; });
此问题就这么轻松的解决。
2、启动前加载数据
一种非常理所当然的想法便是在 bootstrapModule
之间发送AJAX请求不就可以了。话虽简单,那ajax代码怎么写?是不是还得考虑兼容性问题?远程数据加载后难道用 window.xxx
来存储吗?
若你这么做,那你太小看Angular,Angular是非常强大的。
Angular提供一个叫 APP_INITIALIZER
的 Token 值,用于在应用初始化时执行相应的函数。
所以只需要像其它服务编码一样,写一个用于在启动应用时所需要的服务逻辑,以下是一摘自 ng-alain:
import { Router } from '@angular/router'; import { Injectable,Injector } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { MenuService } from "../menu/menu.service"; import { TranslatorService } from "../translator/translator.service"; import { SettingsService } from "../settings/settings.service"; import 'rxjs/add/operator/do'; import 'rxjs/add/operator/toPromise'; import 'rxjs/add/operator/catch'; /** * 用于应用启动时 * 一般用来获取应用所需要的基础数据等 */ @Injectable() export class StartupService { constructor( private menuService: MenuService,private tr: TranslatorService,private settingService: SettingsService,private httpClient: HttpClient,private injector: Injector) { } load(): Promise<any> { // only works with promises // https://github.com/angular/angular/issues/15088 let ret = this.httpClient .get('./assets/app-data.json') .toPromise() .then((res: any) => { // just only injector way if you need navigate to login page. // this.injector.get(Router).navigate([ '/login' ]); this.settingService.setApp(res.app); this.settingService.setUser(res.user); // 初始化菜单 this.menuService.add(res.menu); // 调整语言 this.tr.use('en'); }) .catch((err: any) => { return Promise.resolve(null); }); return ret.then((res) => { }); } }
这里有两点需要注意:
服务是需要注册的,自然在根模块中完成。
export function StartupServiceFactory(startupService: StartupService): Function { return () => { return startupService.load() }; } @NgModule({ providers: [ StartupService,{ provide: APP_INITIALIZER,useFactory: StartupServiceFactory,deps: [StartupService],multi: true } ],bootstrap: [ AppComponent ] }) export class AppModule { }
到此,两件事已经完成了。
四、结论
本文的想法还是来源里群里总有人在问一下问题,如何在Angular启用时先加载远程数据;其中 APP_INITIALIZER
算是很少有人提及的,其它的都是一些日常写法,了无新意。
希望此文能帮助各位。
Happy coding!