一個單頁應用第一次啓動從文檔的下載(包括各類資源)再到初始化至成功渲染這一過程基本上都是以秒爲單位的。css
Angular應用的 index.html
會在文檔當中寫入根組件,例如:html
<app-root>Loading...</app-root>
直到Angular初始化完成後 Loading... 字樣纔會從頁面消失,並進入實際的應用。固然相比較一版空白着實還算優雅一點。git
然而一個好的應用的體驗怎能這樣呢,有興趣的能夠先看一下 ng-alain 是如何友好的啓動Angular的。github
咱們知道瀏覽器須要先接收一個HTML文檔,而後解析文檔並加載相應的樣式及腳本文件,這裏有不少優化相關的技術細節,但更多細節本文不做探討。web
對於Angular而言,真正開始渲染組件會在 platformBrowserDynamic().bootstrapModule
以後,所以若說友好,理應在此以前把那該死的 Loading... 換成一個動畫或更友好的效果。ajax
因此,得出第一個要點:儘量早顯示啓動動畫,並儘量在組件渲染以前關掉動畫。typescript
然而,現實與想法的有點不一樣,那就是絕大部分啓動過程當中是須要依賴於遠程數據,亦或者指引用戶應該是進入登陸頁,仍是控制頁。json
所以,第二個要點:啓動前須要至少一次遠程交互。bootstrap
HTML文檔下載以後會當即顯示,所以,能夠利用這一點,把啓動動畫直接寫在 index.html
頁面當中。但,咱們不該該像開頭那樣,而是一個複雜的CSS3動畫,如下是一摘自 ng-alain:promise
<!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'; });
此問題就這麼輕鬆的解決。
一種很是理所固然的想法即是在 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) => { }); } }
這裏有兩點須要注意:
load()
返回值必須是 Promise
類型。this.injector.get(Router)
方式來獲取路由實例,否則很容易引發循環依賴BUG。服務是須要註冊的,天然在根模塊中完成。
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!