心血來潮,打算結合實際開發的經驗,淺談一下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,但實施難度略高
說到項目架構,每每要考慮不少方面:
而根據實際經驗來看,方即是必然首要地位,除此以外,應該是代碼管理了。團隊合做過程當中,各類協做,代碼衝突等等,都會給一個優秀框架帶來各類奇怪難題。因此,有好的框架還不夠,咱們還須要根據自身業務和團隊的狀況,按需裁剪或者修改框架,找到最佳的實施方案。
接下來,將分3個隨筆分別介紹一下我心目中前3種架構的較好實施方案,而最後一種,跟前3種有種道不一樣不相爲謀的感受,加上本身道行不夠,仍是暫且不提了。
這一篇,先說說第1種:requirejs+angular+angular-route
移動端單頁面Web相對多頁面來講,模塊化管理顯得很是重要,由於若是沒有模塊化,頁面初始化時就把全部的js和全部模版都加載進來,會致使首屏速度極慢。這一點,你們都理解的。
因此,requirejs或者相似的模塊化框架是必不可少的。requirejs比較流行,配合grunt能夠作好整套的自動化工具,咱們就以這個爲例子吧。
首先,來看看demo項目的總體架構。
除了類庫外,業務代碼都以模塊劃分目錄,這樣作便於實際開發中,按模塊化合並js和html,也利於多人並行開發,各自修改不一樣的模塊,互不影響。
另外,說說三個重點的根目錄文件:
第一步,先看看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和backbone http://www.cnblogs.com/kenkofox/p/4648472.html
相關代碼能夠在github找到:https://github.com/kenkozheng/HTML5_research/tree/master/AngularRequireJS