分享

AngularJS按需动态加载template和controller?

 someoneknow 2015-12-08
初次使用angularjs做项目,但是发现angularjs在路由配置后会一次性加载所有依赖文件,这对于大一点的项目来说是不可接受的,使用requirejs也不能阻止路由配置处angularjs自己去加载文件。

然后我找到了angularAMD,在angularjs和angular-ui-router的环境下是可以实现template、controller的动态加载(也就是进入哪个页面就加载改页面相关内容)。
但是由于不想自己折腾太多的UI,我使用了很火的框架ionic,他对路由模块可能坐了自己的封装,使用了<ion-nav-view>而不是<ui-view>,然后导致控制器能按需加载,模板却一次性加载。

我谷歌了这些内容,发现相关话题非常少,难道是我理解有误?angular不需要动态加载模块吗,但是对于有很多个页面的项目,这肯定是不合理的,譬如我们要做的是hybird app,页面量很大。不知道大家在使用angular时都是怎么对待这个需求的
按投票排序 按时间排序

10 个回答

占位 谢@张治中邀请

楼上的几个答案我都看了一下

1、 把单页面应用做成应用中所有页面都加载并且重新初始化Angularjs框架的行为在我个人的角度是无法接受的,不论是从交互体验的角度上还是从技术追求 的角度。当然,如果从公司的角度,加上其他因素的影响,最后根据各方面实际情况(开发周期、团队技术储备、产品经理交互需求等等)的妥协与折衷,这样做其 实也是可以的。

2、使用oclazyload的方案我调研了一下,是可行的,但是想说这个方案有些缺点,比如每次动态加载需要的脚本、模 版资源会有很多不必要的网络开销,路由定义比较复杂(多了一些配置项,其实不能算复杂,而是繁琐),对于大型复杂业务应用,路由众多,耗费的精力不可忽 视。在实际做对外开放的产品时,我们一般会把使用requirejs管理依赖关系的代码打包压缩,加版本号,同时根据项目情况决定要不要按照业务模块做拆 分打包&异步按需加载。

3、不知道楼主开发的产品是不是移动端的单页面应用,不知道楼主的应用是否业务复杂以及脚本文件的大小在 什么量级,所以我下面讲解的技术方案可能在某些地方并不适合楼主的场景,但是原理是相通的,楼主可以参考一下然后看看是否可以解决你的项目中面临的问题。 如果需要,可以随时找我一起讨论你所面临的问题以及采用哪种方式解决最好。

4、占位,晚上有空更新。

===========2015-06-08更新============

最近一直比较忙,今天抽空更新一部分。

看了楼主的评论回复,移动端和PC浏览器端,对于Angular本身而言没有区别,所以我说的这些应该也适用于移动端Angularjs应用。

首先说一下Angularjs的启动原理,就知道为什么很少有人做Angular代码的异步加载了。

好像知乎不支持markdown格式,写起来好难受,只能拿 “等号”玩了

========================Angular框架启动原理分析==============================

我们现在一般配合Requirejs 做代码依赖管理的情况下,都是在RequireJs的入口文件中,执行以下代码来启动Angular框架:

angular.element(document).ready(function() {
    angular.bootstrap(document, ['vpcConsole']);
  });
这种情况下,我们来扒代码看看 angular.bootstrap都干了什么(无关紧要的代码用省略号表示了):
function bootstrap(element, modules) {
  var doBootstrap = function() {
    
    //获取Jquery的Dom对象
    element = jqLite(element);

     ……

    //这句话是最关键的点,我们会继续深挖createInjector这个函数
    var injector = createInjector(modules);
    injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', '$animate',
       function(scope, element, compile, injector, animate) {
        scope.$apply(function() {
          element.data('$injector', injector);
          compile(element)(scope);
        });
      }]
    );
    return injector;
  };

   //以下代码应该是为了支持测试用的,不用关心,感兴趣可以看文档说明:http://docs./guide/bootstrap
  var NG_DEFER_BOOTSTRAP = /^NG_DEFER_BOOTSTRAP!/;

  if (window && !NG_DEFER_BOOTSTRAP.test(window.name)) {
    return doBootstrap();
  }

  window.name = window.name.replace(NG_DEFER_BOOTSTRAP, '');
  angular.resumeBootstrap = function(extraModules) {
    forEach(extraModules, function(module) {
      modules.push(module);
    });
    doBootstrap();
  };
}

在上面的bootstrap方法里面,我们找到了一行非常关键的代码,调用了createInjector方法,接下来我们看下createInjector方法里面干了什么事情(不重要的代码统统省略号)。
function createInjector(modulesToLoad) {
  /*
    开始部分申明了一堆变量,无视掉
   */
  ……

//重要的代码,loadModules方法需要深挖  
forEach(loadModules(modulesToLoad), function(fn) { instanceInjector.invoke(fn || noop); });

  return instanceInjector;

/*
源码里面这里写了一堆函数定义,由于function 声明会在js解析引擎里面被提前,所以前面有return语句也没关系
*/
……
}


然后再来看 loadModules里面的关键语句(loadModules函数其实就定义在了 createInjector函数里面)
function loadModules(modulesToLoad){
    var runBlocks = [], moduleFn, invokeQueue, i, ii;
    forEach(modulesToLoad, function(module) {
      if (loadedModules.get(module)) return;
      loadedModules.put(module, true);

      try {
        if (isString(module)) {
          //得到angular模块
          moduleFn = angularModule(module);
          runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks);

          //这里使用for循环读取了modelFn的_invokeQueue,然后做遍历执行,这个_invokeQueue 是很关键的东西,等你知道了它的由来,就知道为什么Angularjs天然不支持异步加载了。
          for(invokeQueue = moduleFn._invokeQueue, i = 0, ii = invokeQueue.length; i < ii; i++) {
            var invokeArgs = invokeQueue[i],
                provider = providerInjector.get(invokeArgs[0]);

            provider[invokeArgs[1]].apply(provider, invokeArgs[2]);
          }
        } else if (isFunction(module)) {
            runBlocks.push(providerInjector.invoke(module));
        } else if (isArray(module)) {
            runBlocks.push(providerInjector.invoke(module));
        } else {
          assertArgFn(module, 'module');
        }
      } catch (e) {
      /*
       异常处理,不关心
      */      
        ……
      }
    });
    return runBlocks;
  }
上面的代码中已经发现_invokeQueue 是个很重要的东西,那么我们来看下它在什么地方生成的(这个地方代码比较长,但是非常非常关键,无用代码直接省略号)。
function setupModuleLoader(window) {

   ……

  function ensure(obj, name, factory) {
    return obj[name] || (obj[name] = factory());
  }
  
  //给window对象添加一个 angular 属性
  var angular = ensure(window, 'angular', Object);

  ……

  return ensure(angular, 'module', function() {
   
    var modules = {};

    return function module(name, requires, configFn) {
      ……
      return ensure(modules, name, function() {
       
        //这个就是我们上面提到的_invokeQueue;
        var invokeQueue = [];

        /** @type {!Array.<Function>} */
        var runBlocks = [];

        var config = invokeLater('$injector', 'invoke');

        //这个就是我们调用 angular.module()以后得到的实例,注意它对外暴露的接口都是用什么实现的(invokeLater做的实现)
        var moduleInstance = {
          //这个就是很重要的module的一个属性,_invokeQueue,前面的代码很清楚,它是一个数组,而且这个数组里面的东西,只有在angular.bootstrap的时候才会被执行,明白这一点非常关键非常重要。
          _invokeQueue: invokeQueue,
          _runBlocks: runBlocks,
          requires: requires,
          name: name,
          /*
            下面这些就是我们经常调用的各种接口,注意它的实现,都是使用invokeLater来做了一次封装,下面的代码注释里面说明了invokeLater的作用。
          */
          provider: invokeLater('$provide', 'provider'),
          factory: invokeLater('$provide', 'factory'),
          service: invokeLater('$provide', 'service'),
          value: invokeLater('$provide', 'value'),
          constant: invokeLater('$provide', 'constant', 'unshift'),
          animation: invokeLater('$animateProvider', 'register'),
          filter: invokeLater('$filterProvider', 'register'),
          controller: invokeLater('$controllerProvider', 'register'),
          directive: invokeLater('$compileProvider', 'directive'),
          config: config,
          run: function(block) {
            runBlocks.push(block);
            return this;
          }
        };

        if (configFn) {
          config(configFn);
        }

        return  moduleInstance;

         /*这里是InvokeLater的实现,我们调用 Angular的module实例所注册的factory、filter、controller、directive 等等都是通过这个方法放在了 invokeQueue里面,也就是说,当我们在代码里面执行 xxxModule.directive('mydirective',['xxx',function(xxx){}])
的时候,其实这个directive并没有真正被注册,而是放在了一个invokeQueue里面,知道这一点很重要。同理,我们定义一个controller的时候,也没有真正注册执行这个controller,而是这个controller被放在了一个数组里面,等着angular.bootstrap真正去执行并实例化这个函数
         */
        function invokeLater(provider, method, insertMethod) {
          return function() {
            invokeQueue[insertMethod || 'push']([provider, method, arguments]);
            return moduleInstance;
          };
        }
      });
    };
  });
}

代码扒到这里就差不多了。如果你看明白了整个angular.bootstrap的时候的来龙去脉,就会 发现,假设我当前页面已经加载了相关资源,Angular框架已经运行起来(执行了angualr.bootstrap方法),那么我后续通过按需加载引 入的javascript脚本文件中的那些
xxxModule.controller("xxx",['yyy',function(yyy){}])、
xxxModule.directive("zzz",['www',function(www){}])

代 码都虽然会在requirejs引入的时候被执行一遍,但是执行的结果仅仅是把这些controller和directive和factory等等函数放 在了一个invokeLater的数组里面,我们的前端路由激活的时候,去通过angular寻找对应路由(视图)的controller的时候,发现根 本没有这个controller,原因就是在这里:Angular框架启动以后(执行了bootstrap方法之后),它读取controller构造函 数、directive构造函数等的地方和我们执行 controller方法、directive方法所注册进去的代码不是一个地方(或者说不是缓存在一个变量里面),所以,在按需加载的情况下,虽然我们 的代码看起来执行了,但是真正Angular的部分却并没有真正的执行,而是仅仅被放在一个地方等待着“invokeLater”(其实再等多久也没用, 因为不能再执行angular.bootstrap方法了)。

=======================异步按需加载方案分析======================

AngularJs框架启动原理分析完以后,就可以分析异步按需加载方案了。首先我们来看一下我们做异步按需加载方案需要解决哪些问题:
1、angularjs框架启动后,调用 各种api无法真正注册相关构造函数的问题
2、路由激活时,加载当前路由需要的脚本资源(考虑防止重复加载、考虑最大化利用客户端缓存等等问题)
3、当前单页面应用模块划分和打包(当上面两个技术问题解决以后,就要考虑这个偏向业务的问题了)

我们先来看第一个问题怎么解决:
1、解决异步按需加载代码后Angular原生代码无法真正注册各种构造函数的问题

经 过上面的加载原理分析以后,我们就知道该怎么办了:把angular.module("xxx")的实例的factory、controller、 directive、value、filter 等等方法都“变换”掉,让我们的代码执行这些方法的时候,直接把我们的函数放在运行期的对应的缓存的对象里面,这样一来异步加载的代码就会在执行的时候真 正被注册到Angular运行时可以读取的地方(Angular运行时具体缓存各个构造函数的地方自己扒源码吧,懒得贴出来了),这样在路由激活的时候, 就可以找到对应的controller,然后执行。

这个办法可以参考下面的代码(可以参考前面同学回答的oclazyload这个框架里面的代码,但是这个框架作了很多其他的事情,导致最核心的思想没有很清晰的体现,不过也可以读一读,挺有意思):
(function()
{
    var app = angular.module('app', []);
 
    app.config(function($routeProvider, $controllerProvider, $compileProvider, $filterProvider, $provide)
    {
        app.controllerProvider = $controllerProvider;
        app.compileProvider    = $compileProvider;
        app.routeProvider      = $routeProvider;
        app.filterProvider     = $filterProvider;
        app.provide            = $provide;
 
        // Register routes with the $routeProvider
    });
})();

解决这个问题以后,我们来看第2个按路由加载代码的问题如何解决:(精力有限,暂时更新到这里,预计本周 还会更新一次,先给个提示:按照路由去加载所需的代码方案有很多种,常见的是就在定义路由的时候定义一个 resolve,在里面做资源的加载,但是这种方法需要在路由定义的敌方写比较多的东西,不是很喜欢这种方式。题主你的手机端应用建议你做好合理的模块或 者说页面的划分,我初步的建议是你每个页面内部的资源全部都打包成一个脚本包括视图模板也打包进去,最终看文件的大小是不是在移动端单次web请求允许的 范围之内,然后angularjs相关的和公共代码打包成一个,应用启动时加载这个公共的脚本,切换到其他页面或者路由的时候公共的资源脚本已经运行起 来,不需要再加载,而只需加载对应页面或者路由的脚本就可以了。初步可以这样,后续再详细更新说明具体的方案和原因。)


===========2015-06-21更新============
抱歉最近非常忙,更新时间略长


之 前的内容讲解了Angularjs支持动态加载脚本相关的局限性(动态引入到浏览器里面的代码中,没有经过处理的话,所有的 xxxModule.directive()、xxxModule.controller() 执行过后并没有真正立刻被实例化,而是放在了一个缓存中等待app bootstrap的时候去执行,可是由于这些代码是动态引入的,在它们引入之前,app 已经bootstrap过了。),现在开始讲如何配合 requirejs 做相关的处理来支持动态加载:

先看下有几个问题需要解决:

1、确定按需加载的静态资源引入页面的时机。
2、按需加载的静态资源打包方式。
3、其他一些问题的整体考虑。

依次回答上面三个问题,加上之前更新的内容,然后关于AngularJs动态加载脚本的问题和相关的原理基本上可以说明白了。

1: 毫无疑问,在用户浏览某个“页面”的时候,应用会激活对应的路由,然后去加载对应的资源脚本。在我们实际开发过程中,一个业务模块下面会有n个相关的子页 面来共同服务于某个业务需求,这种情况下,我们会给这n个子页面定义一个最基本的父路由,在进入该父路由的时候,去判断当前路由相关的资源脚本是否加载, 如果没有加载的话,则利用requirejs去 获取资源脚本文件,等拿到并执行以后,再执行相关的业务逻辑(比如激活某个页面的路由、实例化对应的controller函数);如果已经加载过了,则和 正常的路由解析过程一致,直接依次执行其子路由直到用户想要访问的页面对应的路由激活。所以说,在不引入其他第三方库的情况下(或者已经引入了某个库,不 再适合引入其他库的情况下),我们可能需要在路由定义的地方做手脚,加入自己的静态资源加载服务。

2:由于每个业务模块有n个页面,所以 一定有大量的其他代码一起配合完成相关的业务逻辑,比如有很多directive,有很多filter,这些脚本有的是我们单页面应用里面其他业务模块用 不到的,有的是其他业务模块可以共用的,所以我们在开发过程中,除了以业务模块为维度统一划分开发文件结构以外,还会多出一个 common 或者 base 这样类型的文件夹,专门负责放置应用内部各个业务模块都使用的公共代码或者公共组件等等。这种情况下,我们的项目中,app文件夹下面的一级文件夹就是 common + n 个业务模块文件夹。在这种项目文件结构下,我们会把 common 和所有用到的第三方组件资源脚本打包、压缩在一个文件中,在用户打开浏览器加载整个单页面应用的时候最先加载并执行,其他n个业务模块,每一个文件夹下的 脚本资源单独打包压缩成一个js文件,每个业务模块文件夹下需要一个种子文件去引用依赖当前项目文件夹中的其他资源脚本,这样在使用r.js做打包的时 候,就可以以这个文件为种子文件将其依赖的脚本打包到一个文件里面。在打包业务模块的脚本的时候,一定会有某些脚本引用了 common 里的文件或者第三方组件中的文件,所以需要在打包配置项中,做一些配置,告诉打包工具不把声明的这些文件打包到当前文件中。具体的配置项可以参考 r.js 中的 path配置项,其中某个子项如果使用 xxx:“empty:” 这样的写法的话,则该声明意为不把 xxx 打包到当前打包的文件中。可以参考r.js的示例配置文件:github.com/jrburke/r.js

这样,打包部分的方式和方法也说明了,具体相关配置不太熟悉的同学可以看requirejs的官方文档或者直接看 r.js 的github项目r.js/example.build.js at master · jrburke/r.js · GitHub

大部分同学应该是使用 grunt-requirejs做打包的,其实和r.js是一样的,所以看上面的链接地址的代码和注释就可以了。

3: 在做这样的技术改造或者技术升级的时候,需要考虑很多很多因素,而不是仅仅去找几个相同的例子然后把自己的代码改成那样就好了,而是需要考虑已有代码做升 级的时候,如何才能改动最小,对已有代码侵入最少,还要考虑到对开发人员的友好性,考虑到测试和线上调试的方便程度,还要考虑到开发团队内部其他成员的接 受程度等等。以题主目前的处境来看,其实你的业务代码的按需加载已经由你采用的框架帮你实现,现在你的困惑主要集中在模板部分不能按需加载,所以,我们可 以把目光直接聚焦到如何解决 模板不能按需加载的问题上,而不用再去考虑如何做业务脚本的按需加载的整体实现。所以,针对题主你的问题,我建议如下(假设你的HTML模板都已经打包到 了一个文件中):1、使用 grunt-html2js 打包转换HTML模板为js文件 2、打包后的模板缓存文件和业务模块代码打包到一起 3、业务脚本动态引入的时候,自动引入HTML模板的js文件,这样就实现了HTML模板的动态引入。原理:Angularjs 的指令、State 等等在处理 templateUrl 的时候,会去服务端按照templateUrl的地址发起请求,但是在请求真正发起之前会使用这个Url作为key去 $templateCache 中查找有没有该key对应的缓存,如果有的话,则不会真的发起请求,而是直接采用缓存中的HTML字符串。所以我们的 grunt-html2js会生成js文件,这个文件里面,每个HTML模板都会以某个Url(这个Url取决于自己的配置)作为自己的key,然后把模 板内容转义为字符串,存入$templateCache中。所以,题主,你可以按照这个思路重新调整一下模板相关的部分,多做debug,观察每个视图激 活的过程,看下templateUrl的处理,就知道该怎么做了。




http://www.zhihu.com/question/30624377

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多