如何友好的啓動Angular應用

1、引言

一個單頁應用第一次啓動從文檔的下載(包括各類資源)再到初始化至成功渲染這一過程基本上都是以秒爲單位的。css

Angular應用的 index.html 會在文檔當中寫入根組件,例如:html

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

直到Angular初始化完成後 Loading... 字樣纔會從頁面消失,並進入實際的應用。固然相比較一版空白着實還算優雅一點。git

然而一個好的應用的體驗怎能這樣呢,有興趣的能夠先看一下 ng-alain 是如何友好的啓動Angular的。github

2、如何纔算友好?

咱們知道瀏覽器須要先接收一個HTML文檔,而後解析文檔並加載相應的樣式及腳本文件,這裏有不少優化相關的技術細節,但更多細節本文不做探討。web

對於Angular而言,真正開始渲染組件會在 platformBrowserDynamic().bootstrapModule 以後,所以若說友好,理應在此以前把那該死的 Loading... 換成一個動畫或更友好的效果。ajax

因此,得出第一個要點:儘量早顯示啓動動畫,並儘量在組件渲染以前關掉動畫typescript

然而,現實與想法的有點不一樣,那就是絕大部分啓動過程當中是須要依賴於遠程數據,亦或者指引用戶應該是進入登陸頁,仍是控制頁。json

所以,第二個要點:啓動前須要至少一次遠程交互bootstrap

3、如何作呢?

一、啓動動畫

HTML文檔下載以後會當即顯示,所以,能夠利用這一點,把啓動動畫直接寫在 index.html 頁面當中。但,咱們不該該像開頭那樣,而是一個複雜的CSS3動畫,如下是一摘自 ng-alainpromise

<!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 { }

到此,兩件事已經完成了。

4、結論

本文的想法仍是來源裏羣裏總有人在問一下問題,如何在Angular啓用時先加載遠程數據;其中 APP_INITIALIZER 算是不多有人說起的,其它的都是一些平常寫法,了無新意。

但願此文能幫助各位。

Happy coding!

相關文章
相關標籤/搜索