心血来潮,打算结合实际开发的经验,浅谈一下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