zoukankan      html  css  js  c++  java
  • 浅谈HTML5单页面架构(一)——requirejs + angular + angular-route

    本文转载自:http://www.cnblogs.com/kenkofox/p/4643760.html

    心血来潮,打算结合实际开发的经验,浅谈一下HTML5单页面App或网页的架构。

    众所周知,现在移动Webapp越来越多,例如天猫、京东、国美这些都是很好的例子。而在Webapp中,又要数单页面架构体验最好,更像原生app。简单来说,单页面App不需要频繁切换网页,可以局部刷新,整个加载流畅度会好很多。

    废话就不多说了,直接到正题吧,浅谈一下我自己理解的几种单页面架构:

    1、requirejs+angular+angular-route(+zepto)

      最后这个zepto可有可无,主要是给团队中实在用不爽angular的同学,可以灵活修改一下页面某些内容。当然,严谨的项目不应该出现zepto。

    2、requirejs+backbone+zepto+template

      这个方案更灵活,MVC味道更浓,使用自定义的template模版库

    3、requirejs+route+template

      这个方案最灵活,看破红尘,针对简单的业务用最简单的方式,只需要路由和模版,不用MVC框架

    4、react

      个人感觉,react更偏向于view层的组件,更native,但实施难度略高

    说到项目架构,往往要考虑很多方面:

    • 方便。例如使用jquery,必然比没有使用jquery方便很多,所以大部分网站都接入类似的库;
    • 性能优化。包括加载速度、渲染效率;
    • 代码管理。大型项目需要考虑代码的模块化,模块间低耦合高内聚,目的就为了团队合作效率;
    • 可扩展性。这个不用说了。
    • 学习成本。一个框架再好,团队新成员难以掌握,学习难度大,结果很容易造成代码混乱。

    而根据实际经验来看,方便是必然首要地位,除此之外,应该是代码管理了。团队合作过程中,各种协作,代码冲突等等,都会给一个优秀框架带来各种奇怪难题。所以,有好的框架还不够,我们还需要根据自身业务和团队的情况,按需裁剪或者修改框架,找到最佳的实施方案。

    接下来,将分3个随笔分别介绍一下我心目中前3种架构的较好实施方案,而最后一种,跟前3种有种道不同不相为谋的感觉,加上自己道行不够,还是暂且不提了。

    这一篇,先说说第1种:requirejs+angular+angular-route

    移动端单页面Web相对多页面来说,模块化管理显得非常重要,因为如果没有模块化,页面初始化时就把所有的js和所有模版都加载进来,会导致首屏速度极慢。这一点,大家都理解的。

    所以,requirejs或者类似的模块化框架是必不可少的。requirejs比较流行,配合grunt可以做好整套的自动化工具,我们就以这个为例子吧。

    首先,来看看demo项目的整体架构。

    除了类库外,业务代码都以模块划分目录,这样做便于实际开发中,按模块化合并js和html,也利于多人并行开发,各自修改不同的模块,互不影响。

    另外,说说三个重点的根目录文件:

    • index.html,这个就是单页面唯一一个html了,其他都只是片段模版(tpl.html)。一般可以把这个html放到动态服务器上,保持零缓存,同时这里可以携带各种js版本控制信息和必要的用户数据。
    • main.js,这个是由requirejs引入的第一个业务js,主要是配置requirejs;
    • router.js,这个是整个网站/app的路由配置,在实际部署中,可以把main.js和router.js合并。

    第一步,先看看index.html需要做什么变化

    复制代码
    <!DOCTYPE html>
    <html>
    <head lang="en">
        <meta charset="UTF-8">
        <title>Angular & Requirejs</title>
    </head>
    <body>
    <div id="container" ng-view></div>
    <script data-baseurl="./" data-main="main.js" src="libs/require.js" id="main"></script>
    </body>
    </html>
    复制代码

    相对angular的写法,这里由于使用requirejs管理全部模块,所以index.html中不需要引入angular等,只是设置了一个带ng-view属性的div,用于充当整个App的视图区域。

    data-baseurl是额外加入的属性,主要好处是可以轻松在html(0缓存)中对js的url进行修改。

    data-main就是requirejs的标准写法了,跳过不说。

    第二步,main.js,也就是requirejs的配置

    复制代码
    'use strict';
    
    (function (win) {
        //配置baseUrl
        var baseUrl = document.getElementById('main').getAttribute('data-baseurl');
    
        /*
         * 文件依赖
         */
        var config = {
            baseUrl: baseUrl,           //依赖相对路径
            paths: {                    //如果某个前缀的依赖不是按照baseUrl拼接这么简单,就需要在这里指出
                underscore: 'libs/underscore',
                angular: 'libs/angular',
                'angular-route': 'libs/angular-route',
                text: 'libs/text'             //用于requirejs导入html类型的依赖
            },
            shim: {                     //引入没有使用requirejs模块写法的类库。例如underscore这个类库,本来会有一个全局变量'_'。这里shim等于快速定义一个模块,把原来的全局变量'_'封装在局部,并导出为一个exports,变成跟普通requirejs模块一样
                underscore: {
                    exports: '_'
                },
                angular: {
                    exports: 'angular'
                },
                'angular-route': {
                    deps: ['angular'],   //依赖什么模块
                    exports: 'ngRouteModule'
                }
            }
        };
    
        require.config(config);
    
        require(['angular', 'router'], function(angular){
            angular.bootstrap(document, ['webapp']);
        });
    
    })(window);
    复制代码

    requirejs的语法,说来话长,简单在代码中做了注释。有兴趣了解详情的可以参考官网: http://requirejs.org/;angular可以参考:https://docs.angularjs.org/guide/filter

    这里配置好requirejs后,就做第一步工作,引入angular和angular的路由配置,然后用

    angular.bootstrap(document, ['webapp']);

    手工启动angular,这里webapp是router.js中定义的angular module。

    第三步,配置这个router

    复制代码
    define(['angular', 'require', 'angular-route'], function (angular, require) {
    
        var app = angular.module('webapp', [
            'ngRoute'
        ]);
    
        app.config(['$routeProvider', '$controllerProvider',
            function($routeProvider, $controllerProvider) {
                $routeProvider.
                    when('/module1', {
                        templateUrl: 'module1/tpl.html',
                        controller: 'module1Controller',
                        resolve: {
                            /*
                            这个key值会被注入到controller中,对应的是后边这个function返回的值,或者promise最终resolve的值。函数的参数是所需的服务,angular会根据参数名自动注入
                             对应controller写法(注意keyName):
                             controllers.controller('module2Controller', ['$scope', '$http', 'keyName',
                                 function($scope, $http, keyName) {
                             }]);
                             */
                            keyName: function ($q) {
                                var deferred = $q.defer();
                                require(['module1/module1.js'], function (controller) {
                                    $controllerProvider.register('module1Controller', controller);      //由于是动态加载的controller,所以要先注册,再使用
                                    deferred.resolve();
                                });
                                return deferred.promise;
                            }
                        }
                    }).
                    otherwise({
                        redirectTo: '/module1'      //angular就喜欢斜杠开头
                    });
            }]);
    
        return app;
    });
    复制代码

    上述代码看起来长,实际很短,因为有一堆绿色的注释,嘿嘿。。。

    如果大家用过angular-route,这里的语法就很简单,如果没用过,则建议直接阅读angular-route源代码中的注释,非常清晰。

    简单而言,就是when函数配置一个路由规则,对应一个template和一个controller。otherwise就是默认路由,也就是遇到一个未定义路径的时候如何跳转。

    如果没有使用requirejs,那么我们需要在路由配置前加载完全部controller。angular-route需要做的只是切换HTML模版,重新编译,绑定新的controller。

    但是。

    但是。。

    这里用了requirejs,事情就变化了。我们要按需加载,不可能页面刚加载就全部controller都load回来,这样得耗费多少流量。。。

    所以,这里利用了angular-route提供的resolve功能,也就是路由更改html前先把resolve里边该做的事完成。

    resolve的写法比较特殊,接受的是一个key:value对象,keyName将会导入到controller中(如果controller有注明依赖)。而value应该是一个函数,函数的写法类似controller,angular会自动根据参数名导入相应依赖的服务,例如$q、$route。

    上述例子中,module1.js定义了模块1的controller,后续我们再看代码。

    由于路由配置前还不存在这个controller,所以现在需要动态注册这个controller。也就是:

    $controllerProvider.register('module1Controller', controller);

    第四步,看看模块1的controller是怎么写的

    复制代码
    define(['angular'], function (angular) {
    
        //angular会自动根据controller函数的参数名,导入相应的服务
        return function($scope, $http, $interval){
            $scope.info = 'kenko';      //向view/模版注入数据
    
            //模拟请求cgi获取数据,数据返回后,自动修改界面,不需要啰嗦的$('#xxx').html(xxx)
            $http.get('module2/tpl.html').success(function(data) {
                $scope.info = 'vivi';
            });
    
            var i = 0;
            //angularjs修改了原来的setTimeout和setInterval,要用这两个玩意,必须引入$timeout和$interval,否则无法修改angular范围内的东西
            $interval(function () {
                i++;
                $scope.info = i;
            }, 1000);
        };
    });
    复制代码

    angular有太多牛逼的功能,但实际上我业务太简单,用不到。所以这里只演示了3种最简单的情况。

    这里不得不说,由于双向绑定,拉cgi和修改dom这些操作就变得非常简单了。

    貌似。

    貌似。。。

    一切解决了?这样的模块化似乎已经很好,跳转到某个模块的时候才加载对应的html和controller js。

    但是。

    但是。。

    对于追求极致的团队来说,模块的html和js应该打包在一起,一次请求就拉回来,这样能大大减少HTTP请求的时间。而现在按照angular-route,只能利用templateUrl单独拉取一个html文件。

    那么接下来,我们再动动歪脑筋,修改一下。

    第五步,修改angular-route,实现HTML和js打包加载。

    复制代码
    function ngViewFillContentFactory($compile, $controller, $route) {
      return {
        restrict: 'ECA',
        priority: -400,
        link: function(scope, $element) {
          var current = $route.current,
              locals = current.locals;
    
          $element.html(current.template);  //原来是locals.$template
    复制代码

    首先,先修改一下angular-route的源代码,这个源代码非常精简,不用太纠结,狠狠的去修改就好了。

    另外,想问我为什么知道或者想到在这修改?咳咳咳,我会大摇大摆的说我认识angular-route的作者么?。。。。。。。开玩笑,作者叫什么,我都没去找,还说认识作者。其实就是逐步调,稍加变量搜索,发现一些不对劲,就做了这个小刀。

    再另外,有专家要拍板了,这样乱修改,肯定带来毛病。是的,我不得不说,我自己都没彻底的检查是否有问题,但按照实际情况来看,暂时没遇到问题。

    然后,做一个新的when配置:

    复制代码
                    when('/module2', {
                        template: '',
                        controller: 'module2Controller',
                        resolve:{
                            keyName: function ($route, $q) {
                                var deferred = $q.defer();
                                require(['module2/module2.js'], function (module2) {
                                    $controllerProvider.register('module2Controller', module2.controller);
                                    $route.current.template = module2.tpl;
                                    deferred.resolve();
                                });
                                return deferred.promise;
                            }
                        }
                    })
    复制代码

    这里用module2做例子,跟module1不同,这里初始设置的template是空字符串,然后在resolve中require回来后,动态修改$route.current.template。

    因为我知道,这个修改能赶在angular-route修改HTML前,也就是小把戏能凑效。

    相应,看看module2怎么写:

    复制代码
    define(['angular', 'text!module2/tpl.html'], function (angular, tpl) {
    
        //angular会自动根据controller函数的参数名,导入相应的服务
        return {
            controller: function ($scope, $http, $interval) {
                $scope.date = '2015-07-13';
            },
            tpl: tpl
        };
    });
    复制代码

    大功告成,这样html模版就不由angular-route去接管了,而是由requirejs加载,我们可以控制的范围和灵活性就变大了。

    不过,这里controller的函数写法可能会因为压缩混淆时丢失了原来的参数名,所以,我们也可以采用显式注入的方式:

    //也可以使用这样的显式注入方式,angular执行controller函数前,会先读取$inject
        controller.$inject = ['$scope'];
        function controller(s){
            s.date = '2015-07-13';
        }
        return {controller:controller, tpl:tpl};

    到这里,整个架构基本就成型了,webapp中每个模块都能非常独立,这样对网站打开速度和协同开发都非常有好处。

    但是,路由表的配置还是略复杂,每次大家都要写一大堆代码,这不是我们想要的,那么可以抽取公用代码,再优化一下。

    第六步,优化路由表,变成真正的配置化。

    复制代码
    define(['angular', 'require', 'angular-route'], function (angular, require) {
    
        var app = angular.module('webapp', [
            'ngRoute'
        ]);
    
        app.config(['$routeProvider', '$controllerProvider',
            function($routeProvider, $controllerProvider) {
    
                var routeMap = {
                    '/module2': {                           //路由
                        path: 'module2/module2.js',         //模块的代码路径
                        controller: 'module2Controller'     //控制器名称
                    }
                };
                var defaultRoute = '/module2';              //默认跳转到某个路由
    
                $routeProvider.otherwise({redirectTo: defaultRoute});
                for (var key in routeMap) {
                    $routeProvider.when(key, {
                        template: '',
                        controller: routeMap[key].controller,
                        resolve:{
                            keyName: requireModule(routeMap[key].path, routeMap[key].controller)
                        }
                    });
                }
    
                function requireModule(path, controller) {
                    return function ($route, $q) {
                        var deferred = $q.defer();
                        require([path], function (ret) {
                            $controllerProvider.register(controller, ret.controller);
                            $route.current.template = ret.tpl;
                            deferred.resolve();
                        });
                        return deferred.promise;
                    }
                }
    
            }]);
    
        return app;
    });
    复制代码

    routeMap可以由服务器直出,实现0缓存,彻底解耦,更便于团队合作。

    最后最后,由于requirejs和angular都有模块管理,但两个概念又不一致,这里说说我的看法:

    • requirejs模块管理,不单单是代码模块化,还提供了模块加载的功能;
    • angular模块管理,更在乎的是代码逻辑上的模块化,避免全局变量污染,并不提供js文件层面的加载功能;
    • 作为逻辑模块管理,其实用requirejs的模块管理就够了,所以我觉得除了angular原生的controller、service外,我们业务相关的公用库,用requirejs吧。

    欢迎阅读,谢谢这么有耐心。

    敬请期待下一篇:requirejs和backbone http://www.cnblogs.com/kenkofox/p/4648472.html

    相关代码可以在github找到:https://github.com/kenkozheng/HTML5_research/tree/master/AngularRequireJS

  • 相关阅读:
    I.MX6 Surfaceflinger 机制
    理解 Android Fragment
    RPi 2B DDNS 动态域名
    RPi 2B IPC webcam server
    理解 Android MVP 开发模式
    I.MX6 system.img unpack repack
    can't set android permissions
    VMware Ubuntu 共享文件夹
    解决oracle数据库连接不上的问题
    perfect-scrollbar示例
  • 原文地址:https://www.cnblogs.com/tangyuchen/p/5944075.html
Copyright © 2011-2022 走看看