如何友好的启动Angular应用

作者: cipchk | 来源:发表于2017-09-07 21:06 被阅读118次

    一、引言

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

    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!

    相关文章

      网友评论

        本文标题:如何友好的启动Angular应用

        本文链接:https://www.haomeiwen.com/subject/wbrwjxtx.html