探索angularjs+requirejs全面实现按需加载的套路


Posted in Javascript onFebruary 26, 2016

在进行有一定规模的项目时,通常希望实现以下目标:1、支持复杂的页面逻辑(根据业务规则动态展现内容,例如:权限,数据状态等);2、坚持前后端分离的基本原则(不分离的时候,可以在后端用模版引擎直接生成好页面);3、页面加载时间短(业务逻辑复杂就需要引用第三方的库,但很可能加载的库和用户本次操作没关系);4,还要代码好维护(加入新的逻辑时,影响的文件尽量少)。

想同时实现这些目标,就必须有一套按需加载的机制,页面上展现的内容和所有需要依赖的文件,都可以根据业务逻辑需要按需加载。最近都是基于angularjs做开发,所以本文主要围绕angularjs提供的各种机制,探索全面实现按需加载的套路。

一、一步一步实现
基本思路:1、先开发一个框架页面,它可以完成一些基本的业务逻辑,并且支持扩展的机制;2、业务逻辑变复杂,需要把部分逻辑拆分到子页面中,子页面按需加载;3、子页面中的展现内容也变了复杂,又需要进行拆分,按需加载;4、子页面的内容复杂到依赖外部模块,需要按需加载angular模块。

1、框架页
提到前端的按需加载,就会想到AMD( Asynchronous Module Definition),现在用requirejs的非常多,所以首先考虑引入requires。

index.html

<script src="static/js/require.js" defer async data-main="/test/lazyspa/spa-loader.js"></script>

注意:采用手动启动angular的方式,因此html中没有ng-app。

spa-loader.js

require.config({
  paths: {
    "domReady": '/static/js/domReady',
    "angular": "//cdn.bootcss.com/angular.js/1.4.8/angular.min",
    "angular-route": "//cdn.bootcss.com/angular.js/1.4.8/angular-route.min",
  },
  shim: {
    "angular": {
      exports: "angular"
    },
    "angular-route": {
      deps: ["angular"]
    },
  },
  deps: ['/test/lazyspa/spa.js'],
  urlArgs: "bust=" + (new Date()).getTime()
});

spa.js

define(["require", "angular", "angular-route"], function(require, angular) {
  var app = angular.module('app', ['ngRoute']);
  require(['domReady!'], function(document) {
    angular.bootstrap(document, ["app"]); /*手工启动angular*/
    window.loading.finish();
  });
});

2、按需加载子页面
angular的routeProvider+ng-view已经提供完整的子页面加载的方法,直接用。
注意必须设置html5Mode,否则url变化以后,routeProvider不截获。

index.html

<div>
  <a href="/test/lazyspa/page1">page1</a>
  <a href="/test/lazyspa/page2">page2</a>
  <a href="/test/lazyspa/">main</a>
</div>
<div ng-view></div>

spa.js

app.config(['$locationProvider', '$routeProvider', function($locationProvider, $routeProvider) {
  /* 必须设置生效,否则下面的设置不生效 */
  $locationProvider.html5Mode(true);
  /* 根据url的变化加载内容 */
  $routeProvider.when('/test/lazyspa/page1', {
    template: '<div>page1</div>',
  }).when('/test/lazyspa/page2', {
    template: '<div>page2</div>',
  }).otherwise({
    template: '<div>main</div>',
  });
}]);

3、按需加载子页面中的内容
用routeProvider的前提是url要发生变化,但是有的时候只是子页面中的局部要发生变化。如果这些变化主要是和绑定的数据相关,不影响页面布局,或者影响很小,那么通过ng-if一类的标签基本就解决了。但是有的时候要根据页面状态,完全改变局部的内容,例如:用户登录前和登录后局部要发生的变化等,这就意味着局部的布局可能也挺复杂,需要作为独立的单元来对待。

利用ng-include可以解决页面局部内容加载的问题。但是,我们可以再考虑更复杂一些的情况。这个页面片段对应的代码是后端动态生成的,而且不仅仅有html还有js,js中定义了代码片段对应的controller。这种情况下,不仅仅要考虑动态加载html的问题,还要考虑动态定义controller的问题。controller是通过angular的controllerProvider的register方法注册,因此需要获得controllerProvider的实例。

spa.js

app.config(['$locationProvider', '$routeProvider', '$controllerProvider', function($locationProvider, $routeProvider, $controllerProvider) {
  app.providers = {
    $controllerProvider: $controllerProvider //注意这里!!!
  };
  /* 必须设置生效,否则下面的设置不生效 */
  $locationProvider.html5Mode(true);
  /* 根据url的变化加载内容 */
  $routeProvider.when('/test/lazyspa/page1', {
    /*!!!页面中引入动态内容!!!*/
    template: '<div>page1</div><div ng-include="\'page1.html\'"></div>',
    controller: 'ctrlPage1'
  }).when('/test/lazyspa/page2', {
    template: '<div>page2</div>',
  }).otherwise({
    template: '<div>main</div>',
  });
  app.controller('ctrlPage1', ['$scope', '$templateCache', function($scope, $templateCache) {
    /* 用这种方式,ng-include配合,根据业务逻辑动态获取页面内容 */
    /* !!!动态的定义controller!!! */
    app.providers.$controllerProvider.register('ctrlPage1Dyna', ['$scope', function($scope) {
      $scope.openAlert = function() {
        alert('page1 alert');
      };
    }]);
    /* !!!动态定义页面的内容!!! */
    $templateCache.put('page1.html', '<div ng-controller="ctrlPage1Dyna"><button ng-click="openAlert()">alert</button></div>');
  }]);
}]);

4、动态加载模块
采用上面子页面片段的加载方式存在一个局限,就是各种逻辑(js)要加入到启动模块中,这样还是限制子页面片段的独立封装。特别是,如果子页面片段需要使用第三方模块,且这个模块在启动模块中没有事先加载时,就没有办法了。所以,必须要能够实现模块的动态加载。实现模块的动态加载就是把angular启动过程中加载模块的方式提取出来,再处理一些特殊情况。

但是,实际跑起来发现文章中的代码有问题,就是“$injector”到底是什么?研究了angular的源代码injector.js才大概搞明白是怎么回事。

一个应用有两个$injector,providerInjector和instanceInjector。invokeQueue和用providerInjector,runBlocks用instanceProvider。如果$injector用错了,就会找到需要的服务。

routeProvider中动态加载模块文件。

template: '<div ng-controller="ctrlModule1"><div>page2</div><div><button ng-click="openDialog()">open dialog</button></div></div>',
resolve: {
  load: ['$q', function($q) {
    var defer = $q.defer();
    /* 动态加载angular模块 */
    require(['/test/lazyspa/module1.js'], function(loader) {
      loader.onload && loader.onload(function() {
        defer.resolve();
      });
    });
    return defer.promise;
  }]
}

动态加载angular模块

angular._lazyLoadModule = function(moduleName) {
  var m = angular.module(moduleName);
  console.log('register module:' + moduleName);
  /* 应用的injector,和config中的injector不是同一个,是instanceInject,返回的是通过provider.$get创建的实例 */
  var $injector = angular.element(document).injector();
  /* 递归加载依赖的模块 */
  angular.forEach(m.requires, function(r) {
    angular._lazyLoadModule(r);
  });
  /* 用provider的injector运行模块的controller,directive等等 */
  angular.forEach(m._invokeQueue, function(invokeArgs) {
    try {
      var provider = providers.$injector.get(invokeArgs[0]);
      provider[invokeArgs[1]].apply(provider, invokeArgs[2]);
    } catch (e) {
      console.error('load module invokeQueue failed:' + e.message, invokeArgs);
    }
  });
  /* 用provider的injector运行模块的config */
  angular.forEach(m._configBlocks, function(invokeArgs) {
    try {
      providers.$injector.invoke.apply(providers.$injector, invokeArgs[2]);
    } catch (e) {
      console.error('load module configBlocks failed:' + e.message, invokeArgs);
    }
  });
  /* 用应用的injector运行模块的run */
  angular.forEach(m._runBlocks, function(fn) {
    $injector.invoke(fn);
  });
};

定义模块
module1.js

define(["angular"], function(angular) {
  var onloads = [];
  var loadCss = function(url) {
    var link, head;
    link = document.createElement('link');
    link.href = url;
    link.rel = 'stylesheet';
    head = document.querySelector('head');
    head.appendChild(link);
  };
  loadCss('//cdn.bootcss.com/bootstrap/3.3.6/css/bootstrap.min.css');
  /* !!! 动态定义requirejs !!!*/
  require.config({
    paths: {
      'ui-bootstrap-tpls': '//cdn.bootcss.com/angular-ui-bootstrap/1.1.2/ui-bootstrap-tpls.min'
    },
    shim: {
      "ui-bootstrap-tpls": {
        deps: ['angular']
      }
    }
  });
  /*!!! 模块中需要引用第三方的库,加载模块依赖的模块 !!!*/
  require(['ui-bootstrap-tpls'], function() {
    var m1 = angular.module('module1', ['ui.bootstrap']);
    m1.config(['$controllerProvider', function($controllerProvider) {
      console.log('module1 - config begin');
    }]);
    m1.controller('ctrlModule1', ['$scope', '$uibModal', function($scope, $uibModal) {
      console.log('module1 - ctrl begin');
      /*!!! 打开angular ui的对话框 !!!*/
      var dlg = '<div class="modal-header">';
      dlg += '<h3 class="modal-title">I\'m a modal!</h3>';
      dlg += '</div>';
      dlg += '<div class="modal-body">content</div>';
      dlg += '<div class="modal-footer">';
      dlg += '<button class="btn btn-primary" type="button" ng-click="ok()">OK</button>';
      dlg += '<button class="btn btn-warning" type="button" ng-click="cancel()">Cancel</button>';
      dlg += '</div>';
      $scope.openDialog = function() {
        $uibModal.open({
          template: dlg,
          controller: ['$scope', '$uibModalInstance', function($scope, $mi) {
            $scope.cancel = function() {
              $mi.dismiss();
            };
            $scope.ok = function() {
              $mi.close();
            };
          }],
          backdrop: 'static'
        });
      };
    }]);
    /* !!!动态加载模块!!! */
    angular._lazyLoadModule('module1');
    console.log('module1 loaded');
    angular.forEach(onloads, function(onload) {
      angular.isFunction(onload) && onload();
    });
  });
  return {
    onload: function(callback) {
      onloads.push(callback);
    }
  };
});

二、完整的代码
index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta content="width=device-width,user-scalable=no,initial-scale=1.0" name="viewport">
    <base href='/'>
    <title>SPA</title>
  </head>
  <body>
    <div ng-controller='ctrlMain'>
      <div>
        <a href="/test/lazyspa/page1">page1</a>
        <a href="/test/lazyspa/page2">page2</a>
        <a href="/test/lazyspa/">main</a>
      </div>
      <div ng-view></div>
    </div>
    <div class="loading"><div class='loading-indicator'><i></i></div></div>
    <script src="static/js/require.js" defer async data-main="/test/lazyspa/spa-loader.js?_=3"></script>
  </body>
</html>

spa-loader.js

window.loading = {
  finish: function() {
    /* 保留个方法做一些加载完成后的处理,我实际的项目中会在这里结束加载动画 */
  },
  load: function() {
    require.config({
      paths: {
        "domReady": '/static/js/domReady',
        "angular": "//cdn.bootcss.com/angular.js/1.4.8/angular.min",
        "angular-route": "//cdn.bootcss.com/angular.js/1.4.8/angular-route.min",
      },
      shim: {
        "angular": {
          exports: "angular"
        },
        "angular-route": {
          deps: ["angular"]
        },
      },
      deps: ['/test/lazyspa/spa.js'],
      urlArgs: "bust=" + (new Date()).getTime()
    });
  }
};
window.loading.load();

spa.js

'use strict';
define(["require", "angular", "angular-route"], function(require, angular) {
  var app = angular.module('app', ['ngRoute']);
  /* 延迟加载模块 */
  angular._lazyLoadModule = function(moduleName) {
    var m = angular.module(moduleName);
    console.log('register module:' + moduleName);
    /* 应用的injector,和config中的injector不是同一个,是instanceInject,返回的是通过provider.$get创建的实例 */
    var $injector = angular.element(document).injector();
    /* 递归加载依赖的模块 */
    angular.forEach(m.requires, function(r) {
      angular._lazyLoadModule(r);
    });
    /* 用provider的injector运行模块的controller,directive等等 */
    angular.forEach(m._invokeQueue, function(invokeArgs) {
      try {
        var provider = providers.$injector.get(invokeArgs[0]);
        provider[invokeArgs[1]].apply(provider, invokeArgs[2]);
      } catch (e) {
        console.error('load module invokeQueue failed:' + e.message, invokeArgs);
      }
    });
    /* 用provider的injector运行模块的config */
    angular.forEach(m._configBlocks, function(invokeArgs) {
      try {
        providers.$injector.invoke.apply(providers.$injector, invokeArgs[2]);
      } catch (e) {
        console.error('load module configBlocks failed:' + e.message, invokeArgs);
      }
    });
    /* 用应用的injector运行模块的run */
    angular.forEach(m._runBlocks, function(fn) {
      $injector.invoke(fn);
    });
  };
  app.config(['$injector', '$locationProvider', '$routeProvider', '$controllerProvider', function($injector, $locationProvider, $routeProvider, $controllerProvider) {
    /**
     * config中的injector和应用的injector不是同一个,是providerInjector,获得的是provider,而不是通过provider创建的实例
     * 这个injector通过angular无法获得,所以在执行config的时候把它保存下来
    */
    app.providers = {
      $injector: $injector,
      $controllerProvider: $controllerProvider
    };
    /* 必须设置生效,否则下面的设置不生效 */
    $locationProvider.html5Mode(true);
    /* 根据url的变化加载内容 */
    $routeProvider.when('/test/lazyspa/page1', {
      template: '<div>page1</div><div ng-include="\'page1.html\'"></div>',
      controller: 'ctrlPage1'
    }).when('/test/lazyspa/page2', {
      template: '<div ng-controller="ctrlModule1"><div>page2</div><div><button ng-click="openDialog()">open dialog</button></div></div>',
      resolve: {
        load: ['$q', function($q) {
          var defer = $q.defer();
          /* 动态加载angular模块 */
          require(['/test/lazyspa/module1.js'], function(loader) {
            loader.onload && loader.onload(function() {
              defer.resolve();
            });
          });
          return defer.promise;
        }]
      }
    }).otherwise({
      template: '<div>main</div>',
    });
  }]);
  app.controller('ctrlMain', ['$scope', '$location', function($scope, $location) {
    console.log('main controller');
    /* 根据业务逻辑自动到缺省的视图 */
    $location.url('/test/lazyspa/page1');
  }]);
  app.controller('ctrlPage1', ['$scope', '$templateCache', function($scope, $templateCache) {
    /* 用这种方式,ng-include配合,根据业务逻辑动态获取页面内容 */
    /* 动态的定义controller */
    app.providers.$controllerProvider.register('ctrlPage1Dyna', ['$scope', function($scope) {
      $scope.openAlert = function() {
        alert('page1 alert');
      };
    }]);
    /* 动态定义页面内容 */
    $templateCache.put('page1.html', '<div ng-controller="ctrlPage1Dyna"><button ng-click="openAlert()">alert</button></div>');
  }]);
  require(['domReady!'], function(document) {
    angular.bootstrap(document, ["app"]);
  });
});

module1.js

'use strict';
define(["angular"], function(angular) {
  var onloads = [];
  var loadCss = function(url) {
    var link, head;
    link = document.createElement('link');
    link.href = url;
    link.rel = 'stylesheet';
    head = document.querySelector('head');
    head.appendChild(link);
  };
  loadCss('//cdn.bootcss.com/bootstrap/3.3.6/css/bootstrap.min.css');
  require.config({
    paths: {
      'ui-bootstrap-tpls': '//cdn.bootcss.com/angular-ui-bootstrap/1.1.2/ui-bootstrap-tpls.min'
    },
    shim: {
      "ui-bootstrap-tpls": {
        deps: ['angular']
      }
    }
  });
  require(['ui-bootstrap-tpls'], function() {
    var m1 = angular.module('module1', ['ui.bootstrap']);
    m1.config(['$controllerProvider', function($controllerProvider) {
      console.log('module1 - config begin');
    }]);
    m1.controller('ctrlModule1', ['$scope', '$uibModal', function($scope, $uibModal) {
      console.log('module1 - ctrl begin');
      var dlg = '<div class="modal-header">';
      dlg += '<h3 class="modal-title">I\'m a modal!</h3>';
      dlg += '</div>';
      dlg += '<div class="modal-body">content</div>';
      dlg += '<div class="modal-footer">';
      dlg += '<button class="btn btn-primary" type="button" ng-click="ok()">OK</button>';
      dlg += '<button class="btn btn-warning" type="button" ng-click="cancel()">Cancel</button>';
      dlg += '</div>';
      $scope.openDialog = function() {
        $uibModal.open({
          template: dlg,
          controller: ['$scope', '$uibModalInstance', function($scope, $mi) {
            $scope.cancel = function() {
              $mi.dismiss();
            };
            $scope.ok = function() {
              $mi.close();
            };
          }],
          backdrop: 'static'
        });
      };
    }]);
    angular._lazyLoadModule('module1');
    console.log('module1 loaded');
    angular.forEach(onloads, function(onload) {
      angular.isFunction(onload) && onload();
    });
  });
  return {
    onload: function(callback) {
      onloads.push(callback);
    }
  };
});

以上就是本文的全部内容,希望对大家的学习有所帮助。

Javascript 相关文章推荐
前端开发必须知道的JS之原型和继承
Jul 06 Javascript
js实现动态添加、删除行、onkeyup表格求和示例
Aug 18 Javascript
解决ueditor jquery javascript 取值问题
Dec 30 Javascript
在jQuery中处理XML数据的大致方法
Aug 14 Javascript
基于jquery实现鼠标滚轮驱动的图片切换效果
Oct 26 Javascript
jstl中判断list中是否包含某个值的简单方法
Oct 14 Javascript
JS实战篇之收缩菜单表单布局
Dec 10 Javascript
angular.js 路由及页面传参示例
Feb 24 Javascript
原生JS实现圆环拖拽效果
Apr 07 Javascript
React Native如何消除启动时白屏的方法
Aug 08 Javascript
JQuery事件冒泡和默认行为代码实例
May 13 jQuery
jQuery HTML css()方法与css类实例详解
May 20 jQuery
JavaScript代码生成PDF文件的方法
Feb 26 #Javascript
JavaScript 定时器 SetTimeout之定时刷新窗口和关闭窗口(代码超简单)
Feb 26 #Javascript
自动完成的搜索框javascript实现
Feb 26 #Javascript
jQuery实现控制文字内容溢出用省略号(…)表示的方法
Feb 26 #Javascript
js去字符串前后空格的实现方法
Feb 26 #Javascript
js判断鼠标位置是否在某个div中的方法
Feb 26 #Javascript
超实用的JavaScript表单代码段
Feb 26 #Javascript
You might like
php设计模式之命令模式的应用详解
2013/05/21 PHP
iOS自定义提示弹出框实现类似UIAlertView的效果
2016/11/16 PHP
php获取开始与结束日期之间所有日期的方法
2016/11/29 PHP
Javascript下的keyCode键码值表
2007/04/10 Javascript
jQuery对象和DOM对象相互转化
2009/04/24 Javascript
jQuery判断元素是否存在的可靠方法
2014/05/06 Javascript
BootStrap学习系列之布局组件(下拉,按钮组[toolbar],上拉)
2017/01/03 Javascript
原生js实现无限循环轮播图效果
2017/01/20 Javascript
JS模拟实现ECMAScript5新增的数组方法
2017/03/20 Javascript
JS验证input输入框(字母,数字,符号,中文)
2017/03/23 Javascript
Node.js  REPL (交互式解释器)实例详解
2017/08/06 Javascript
解决vue页面DOM操作不生效的问题
2018/03/17 Javascript
在vue中利用全局路由钩子给url统一添加公共参数的例子
2019/11/01 Javascript
微信小程序自定义联系人弹窗
2020/05/26 Javascript
原生js实现自定义难度的扫雷游戏
2021/01/22 Javascript
[04:11]DOTA2上海特级锦标赛主赛事首日TOP10
2016/03/03 DOTA
[44:40]Spirit vs Navi Supermajor小组赛 A组败者组第一轮 BO3 第一场 6.2
2018/06/03 DOTA
使用PYTHON创建XML文档
2012/03/01 Python
Python实现的检测网站挂马程序
2014/11/30 Python
简单介绍利用TK在Python下进行GUI编程的教程
2015/04/13 Python
python Django批量导入数据
2016/03/25 Python
Python中使用pypdf2合并、分割、加密pdf文件的代码详解
2019/05/21 Python
python中的split()函数和os.path.split()函数使用详解
2019/12/21 Python
python matplotlib画盒图、子图解决坐标轴标签重叠的问题
2020/01/19 Python
PyQt5+python3+pycharm开发环境配置教程
2020/03/24 Python
python数据库编程 Mysql实现通讯录
2020/03/27 Python
Django修改app名称和数据表迁移方案实现
2020/09/17 Python
阿迪达斯奥地利官方商城:adidas.at
2016/10/16 全球购物
如何利用find命令查找文件
2016/11/18 面试题
医院门卫岗位职责
2013/12/30 职场文书
运动会表扬稿大全
2014/01/16 职场文书
顶撞领导检讨书
2014/01/29 职场文书
工作失误检讨书(经典集锦版)
2014/10/17 职场文书
检讨书格式
2015/05/07 职场文书
高中政治教学反思
2016/02/23 职场文书
CPU不支持Windows11系统怎么办
2021/11/21 数码科技