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


心血來潮,打算結合實際開發的經驗,淺談一下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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM