如何友好的启动Angular应用

一、引言

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

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) => { });
    }
}

这里有两点需要注意:

  • 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!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容