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