资讯专栏INFORMATION COLUMN

angularjs+requirejs实现按需加载的全面实践

TerryCai / 1349人阅读

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

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

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

一、一步一步实现

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

1、框架页

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

index.html

注意:采用手动启动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


spa.js

app.config(["$locationProvider", "$routeProvider", function($locationProvider, $routeProvider) {
    /* 必须设置生效,否则下面的设置不生效 */
    $locationProvider.html5Mode(true);
    /* 根据url的变化加载内容 */
    $routeProvider.when("/test/lazyspa/page1", {
        template: "
page1
", }).when("/test/lazyspa/page2", { template: "
page2
", }).otherwise({ template: "
main
", }); }]);
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: "
page1
", controller: "ctrlPage1" }).when("/test/lazyspa/page2", { template: "
page2
", }).otherwise({ template: "
main
", }); 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", "
"); }]); }]);
4、动态加载模块

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

动态加载模块深入分析可以参考这篇文章:
http://www.tuicool.com/articles/jmuymiE

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

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

routeProvider中动态加载模块文件。

template: "
page2
", 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 = "";
            dlg += "";
            dlg += "";
            $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



    
        
        
        
        SPA
    
    
        
        

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: "
page1
", controller: "ctrlPage1" }).when("/test/lazyspa/page2", { template: "
page2
", 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: "
main
", }); }]); 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", "
"); }]); 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 = "";
            dlg += "";
            dlg += "";
            $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);
        }
    };
});
写后感

年初定下的目标是坚持每周写一篇自己在开发过程碰到的问题总结,本以为是个简单的事情,写起来才发现写文章的时间比写代码的花的时间还要长。因为写代码的时候只要功能实现了就行了,但是,写文章的时候就一定要把代码搞清楚才敢写,实际上就是逼着自己要认真研究源代码,虽然压力很大,但收获更大。另一方面,发现找到一个好题目挺难的,只是简单的贴别人的代码没意思,可是自己想出来有价值,有意思的问题挺难的。因此大家要是觉得有啥有意思,有价值前端问题,分享一下吧,给我的年度写作计划帮帮忙

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/78737.html

相关文章

  • avalon 单页面程序 (种子工程)之一 用requirejs引入avalon

    摘要:现在微软终于痛定思痛决定放弃了不支持的安全更新,对我们前端来说,真的是重大利好啊言归正传,这篇文章的目的就是把怎么用构建一个单页面程序介绍以下,是对自己的一个总结,也喜欢对大家有一定的借鉴作用,写的不好不对的地方希望大家多评论评论谢谢。 这篇文章是写在公司项目结束之后的,因为我个人不太会把没有实践过的东西写出来,实践是检验真理的唯一标准么,用的怎么样,好不好用,在成熟实践过的项目上能体...

    solocoder 评论0 收藏0
  • angularjs学习笔记——使用requirejs动态注入控制器

    摘要:最近一段时间在学习,由于觉得直接使用它需要加载很多的文件,因此想使用来实现异步加载,并动态注入控制器。手动启动,特别说明此处的不是那个框架,而是的一个手动启动框架的函数中完成了各模块的初始化,并且引入了。 最近一段时间在学习angularjs,由于觉得直接使用它需要加载很多的js文件,因此想使用requirejs来实现异步加载,并动态注入控制器。简单搜索了下发现好多教程写的都很复杂,所...

    王军 评论0 收藏0
  • avalon 单页面程序 (种子工程)之二 按需加载和路由系统

    摘要:的成功离开不这三个东西,分层架构,路由系统,储存系统。分层架构是我们组织复杂代码的关键,路由系统是将多个页面压缩在一个页面的关键。在这个种子工程中,我都调用了同一个方法,就比较适合目录动态生成,需要按需调用不同的页面的情况。 SPA的成功离开不这三个东西,分层架构,路由系统,储存系统。分层架构是我们组织复杂代码的关键,路由系统是将多个页面压缩在一个页面的关键。 其中avalon路由用到...

    fanux 评论0 收藏0
  • 前端资源系列(4)-前端学习资源分享&前端面试资源汇总

    摘要:特意对前端学习资源做一个汇总,方便自己学习查阅参考,和好友们共同进步。 特意对前端学习资源做一个汇总,方便自己学习查阅参考,和好友们共同进步。 本以为自己收藏的站点多,可以很快搞定,没想到一入汇总深似海。还有很多不足&遗漏的地方,欢迎补充。有错误的地方,还请斧正... 托管: welcome to git,欢迎交流,感谢star 有好友反应和斧正,会及时更新,平时业务工作时也会不定期更...

    princekin 评论0 收藏0
  • Regularjs是什么

    摘要:目前已经在大大小小多个线上产品中使用了,也收集了一些有效的建议好了,该看下一个最简单的组件长什么样吧免费领取验证码内容安全短信发送直播点播体验包及云服务器等套餐更多网易技术产品运营经验分享请访问网易云社区。文章来源网易云社区 本文由作者郑海波授权网易云社区发布。 此文摘自regularjs的指南, 目前指南正在全面更新, 把老文档的【接口/语法部分】统一放到了独立的 Reference...

    seal_de 评论0 收藏0

发表评论

0条评论

TerryCai

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<