一、引言

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

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应用的更多相关文章

  1. HTML实现代码雨源码及效果示例

    这篇文章主要介绍了HTML实现代码雨源码及效果示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

  2. 吃透移动端 1px的具体用法

    这篇文章主要介绍了吃透移动端 1px的具体用法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

  3. html5开发三八女王节表白神器

    一年一度的三八女王节马上来临,今天小编基于html5给大家开发一个表白神器,做一个 浪漫的程序猿,具体代码大家参考下本文

  4. CSS中实现动画效果-附案例

    这篇文章主要介绍了 CSS中实现动画效果并附上案例代码及实现效果,就是CSS动画样式处理,动画声明需要使用@keyframes name,后面的name是人为定义的动画名称,下面我们来看看文章的具体实现内容吧,需要的小伙伴可以参考一下

  5. 基于HTML5+Webkit实现树叶飘落动画

    本文给大家分享一段实例代码给大家介绍基于HTML5+Webkit实现树叶飘落动画效果,需要的朋友参考下吧

  6. html5实现图片转圈的动画效果——让页面动起来

    这篇文章主要介绍了html5实现图片转圈的动画效果——让页面动起来的相关资料,需要的朋友可以参考下

  7. HTML5 3D书本翻页动画的实现示例

    这是一款十分炫酷的HTML5 3D书本翻页动画,效果相对比较简单,拖拽鼠标模拟用手翻页,需要的朋友们下面随着小编来一起学习学习吧

  8. html5简介及新增功能介绍

    这篇文章主要介绍了html5简介及新增功能介绍,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  9. HTML5 图片悬停放大的实现代码示例

    这篇文章主要介绍了HTML5 图片悬停放大的实现代码示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

  10. ios – 围绕其中心点旋转UIImageView?

    我在UIImageView中有一个透明的png,我想围绕它的中心点旋转.代码应该非常简单:图像以正确的速度/时间和直角旋转,但其位置会发生偏移.这是一个正在发生的事情的例子:灰色方块只是为了在屏幕上显示位置.透明的png是另一个图.白色虚线显示UIImageView的中心.图像的左侧显示图像的原始位置,右侧显示使用上述代码旋转后的图像.黑色和白色圆圈位于图像文件的中心.有什么东西我不见了吗?

随机推荐

  1. Angular2 innerHtml删除样式

    我正在使用innerHtml并在我的cms中设置html,响应似乎没问题,如果我这样打印:{{poi.content}}它给了我正确的内容:``但是当我使用[innerHtml]=“poi.content”时,它会给我这个html:当我使用[innerHtml]时,有谁知道为什么它会剥离我的样式Angular2清理动态添加的HTML,样式,……

  2. 为Angular根组件/模块指定@Input()参数

    我有3个根组件,由根AppModule引导.你如何为其中一个组件指定@input()参数?也不由AppModalComponent获取:它是未定义的.据我所知,你不能将@input()传递给bootstraped组件.但您可以使用其他方法来做到这一点–将值作为属性传递.index.html:app.component.ts:

  3. angular-ui-bootstrap – 如何为angular ui-bootstrap tabs指令指定href参数

    我正在使用角度ui-bootstrap库,但我不知道如何为每个选项卡指定自定义href.在角度ui-bootstrap文档中,指定了一个可选参数select(),但我不知道如何使用它来自定义每个选项卡的链接另一种重新定义问题的方法是如何使用带有角度ui-bootstrap选项卡的路由我希望现在还不算太晚,但我今天遇到了同样的问题.你可以通过以下方式实现:1)在控制器中定义选项卡href:2)声明一个函数来改变控制器中的散列:3)使用以下标记:我不确定这是否是最好的方法,我很乐意听取别人的意见.

  4. 离子框架 – 标签内部的ng-click不起作用

    >为什么标签标签内的按钮不起作用?>但是标签外的按钮(登陆)工作正常,为什么?>请帮我解决这个问题.我需要在点击时做出回复按钮workingdemo解决方案就是不要为物品使用标签.而只是使用divHTML

  5. Angular 2:将值传递给路由数据解析

    我正在尝试编写一个DataResolver服务,允许Angular2路由器在初始化组件之前预加载数据.解析器需要调用不同的API端点来获取适合于正在加载的路由的数据.我正在构建一个通用解析器,而不是为我的许多组件中的每个组件设置一个解析器.因此,我想在路由定义中传递指向正确端点的自定义输入.例如,考虑以下路线:app.routes.ts在第一个实例中,解析器需要调用/path/to/resourc

  6. angularjs – 解释ngModel管道,解析器,格式化程序,viewChangeListeners和$watchers的顺序

    换句话说:如果在模型更新之前触发了“ng-change”,我可以理解,但是我很难理解在更新模型之后以及在完成填充更改之前触发函数绑定属性.如果您读到这里:祝贺并感谢您的耐心等待!

  7. 角度5模板形式检测形式有效性状态的变化

    为了拥有一个可以监听其包含的表单的有效性状态的变化的组件并执行某些组件的方法,是reactiveforms的方法吗?

  8. Angular 2 CSV文件下载

    我在springboot应用程序中有我的后端,从那里我返回一个.csv文件WheniamhittingtheURLinbrowsercsvfileisgettingdownloaded.现在我试图从我的角度2应用程序中点击此URL,代码是这样的:零件:服务:我正在下载文件,但它像ActuallyitshouldbeBook.csv请指导我缺少的东西.有一种解决方法,但您需要创建一个页面上的元

  9. angularjs – Angular UI-Grid:过滤后如何获取总项数

    提前致谢:)你应该避免使用jQuery并与API进行交互.首先需要在网格创建事件中保存对API的引用.您应该已经知道总行数.您可以使用以下命令获取可见/已过滤行数:要么您可以使用以下命令获取所选行的数量:

  10. angularjs – 迁移gulp进程以包含typescript

    或者我应该使用tsc作为我的主要构建工具,让它解决依赖关系,创建映射文件并制作捆绑包?

返回
顶部