背景

AngularJS 诞生于2009年,由Misko Hevery 等人创建,后为Google所收购。是一款优秀的前端JS框架,已经被用于Google的多款产品当中。

最近捣鼓一个前端项目,用的就是这个框架,其中有大量的在路由过程中使用 resolve 配置的代码,到底这个属性是什么作用,烦扰了好几天终于弄清楚了它的作用和流程。

reslove 配置项

resolve 在 state 配置参数中,是一个对象 (key-value) ,每一个 value 都是一个可以依赖注入的函数,并且返回一个对象。

配置项中的各个方法将在 Controller 控制器被实例化之前执行完成。接下来我们看看使用 resolve 的两种常见场景。

AngularJS路由语法

使用 AngularJS 实现简单页面导航,离不开 $stateProvider 的路由配置,而路由配置时,如果我们需要动态添加路由页面所依赖的控制器,或者控制器实例化时的一些参数需要注入的话,我们就可以使用 resolve 来完成。

首先,来看看动态加载依赖的用法,demo 的主页面 index.html 中的路由配置为:

"use strict";

//index.html页面的导航路由配置
//关注$stateProvider.state的resolve:添加了对控制器模块的依赖
define(['admin-app'], function (adminApp) {
    console.log('加载 adminAppRouter.js');

    /* 定义admin app 的路由 */
    adminApp.config(['$stateProvider', '$urlRouterProvider',
        function ($stateProvider, $urlRouterProvider) {
            console.log('adminAppRouter... 路由配置');

            //路由配置
            $urlRouterProvider.otherwise('/myhome');

            //1、ui-sref对应state的第一个参数即名称
            //2、请求URL路径,这里没有参数,如果有参数则依次用&连接;例如:/viewshome?id&name&age
            //3、templateUrl,与请求URL对应的需要加载的页面
            //4、resolve属性,用于延迟加载,它使用了$q服务,返回一个Promise对象,这里并没有使用
            //5、控制器代码在templateUrl页面加载时会执行,完成数据绑定过程
            $stateProvider
                .state('myhome',{
                    url: '/myhome',
                    templateUrl: 'page/my_home.tpl.html',
                    controller: 'MyHomeController',
                    controllerAs: 'ctrl',
                    
                    resolve: {
                        //添加动态依赖时,需要提供返回一个promise的对象,下面这里应该是通用的逻辑。
                        loadJs: ['$q', '$rootScope', function($q, $rootScope) {
                            console.log("resolve loadJs define.")
                            //创建一个deferred对象
                            var defer = $q.defer();

                            //调用定义好的viewsHomeController.js模块
                            console.log('adminAppRouter... 路由配置 resolve start.');
                            
                            require(['viewsHome-controller'], function() {
                                //scopt.$apply方法是在数据发生变化后更新到绑定的页面元素上
                                 $rootScope.$apply(function() {
                                    console.log('scope apply resolve start .');
                                    defer.resolve();
                                    console.log('scope apply resolve end .');
                                });
                                //即使注释掉上面的apply,但是放在这个异步回调里面,能够正确渲染页面
                                defer.resolve();
                                console.log('adminAppRouter... 路由配置 viewsHome-controller function');
                            });

                            //将defer.resolve();放在此处,能够加载页面,但是数据得不到渲染,异常报错Controller方法未定义
                            console.log('adminAppRouter... 路由配置 resolve finish.');

                            return defer.promise;
                        }]
                    }
                })
                .state('otherHome',{
                    url: '/otherHome',
                    templateUrl: 'views/other_home.html',
                    controller: 'OtherHomeController',
                    resolve: {
                        load: ['$q', '$rootScope', function($q, $rootScope) {
                            var defer = $q.defer();
                            require(['other-controller'], function() {
                                $rootScope.$apply(function() {
                                    defer.resolve();
                                });
                            });
                            return defer.promise;
                        }]
                    }
                })
        }
    ])
})

关注 resolve 的 load 定义,这里使用 require 动态加载了路由页面依赖的控制器定义文件,并在其回调函数中执行数据监控,该函数返回一个 Promise 对象。

那么这个 load 方法是怎么被使用的呢?

路由过程是这样的:

  • 1、执行 resolve 中的每个方法,并将各个方法的结果存储到集合中。
  • 2、resolve中 所有方法执行完成后,开始实例化 controller 对象
  • 3、实例化 controller 对象的时候会使用 resolve 的结果集作为参数来实例化。
    这里我们使用 resolve 只是希望在真正访问某个 html 页面时再加载其依赖,load 方法的返回值并不需要作为控制器的参数,所以这个方法名称可以随便定义。

解决控制器注入问题

如果一个控制器的参数数据需要外界注入,那么就可以通过 resolve 提供这些参数注入工厂完成。例如,我们定义一个 Controller 类,依赖一个 userData 参数和personInfo:

previewApp.controller('MyController', ['$scope', '$state', 'userData','personInfo',MyController]);

function MyController($scope, $state, $stateParams,userData,personInfo){

}

那么我们在使用 MyController 之前,必须解决这两个参数的注入问题:

resolve: {
	load: ['$q', '$rootScope', function($q, $rootScope) {
	    var defer = $q.defer();
	    require(['other-controller'], function() {
		$rootScope.$apply(function() {
		    defer.resolve();
		});
	    });
	    return defer.promise;
	}],

	userData:function(){
		return {name:'wang',age:10}
	},
	personInfo:function(){
		return "Hello";
	}
    }
})

resolve 配置中需要提供一个与控制器参数名称一样的属性,其值是一个工厂方法返回一个数据,那么最终该工厂数据会作为参数传递给 Controller。

这种需要注入控制器依赖数据的情况下提供的工厂方法名称必须跟参数名称一致,就不能瞎起名了。

启示录

resolve 的作用是数据预准备,只有数据准备好了之后才会触发 $stateChangeSuccess事件进行切换路由操作,接着实例化 Controller,然后更新模板内容。