淺談HTML5單頁面架構(一)——requirejs + angular + angular-route

心血來潮,打算結合實際開發的經驗,淺談一下HTML5單頁面App或網頁的架構。html

 

衆所周知,如今移動Webapp愈來愈多,例如天貓、京東、國美這些都是很好的例子。而在Webapp中,又要數單頁面架構體驗最好,更像原生app。簡單來講,單頁面App不須要頻繁切換網頁,能夠局部刷新,整個加載流暢度會好不少。react

廢話就很少說了,直接到正題吧,淺談一下我本身理解的幾種單頁面架構:jquery

一、requirejs+angular+angular-route(+zepto)git

  最後這個zepto無關緊要,主要是給團隊中實在用不爽angular的同窗,能夠靈活修改一下頁面某些內容。固然,嚴謹的項目不該該出現zepto。angularjs

二、requirejs+backbone+zepto+templategithub

  這個方案更靈活,MVC味道更濃,使用自定義的template模版庫web

三、requirejs+route+templatebootstrap

  這個方案最靈活,看破紅塵,針對簡單的業務用最簡單的方式,只須要路由和模版,不用MVC框架promise

四、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

相關文章
相關標籤/搜索