我的web开发最强组合:Play1+angularjs+bootstrap ++ (idea + livereload)
首先说明我开发web的情况:
- 个人开发
- 前后端全部自己搞定
- 网站类型多为传统多页面程序
- 注重开发效率
- Javascritp能力不强
- 美术细胞很少
由于每个人情况不同,选择技术及方案的重点也不同,所以内容仅供参考。对于我来说,这一套不论开发效率还是开发感受都是很好的。
编辑器
我选用的是idea Ultimate,它除了超强的java编辑功能之外,还提供了功能完善的play1和play2插件,angularjs插件,javascript/css编辑功能,让人开发时事半功倍。这里截几个图说明一下:
需要注意的是,上面提示的各功能,基本上都是idea ultimate才提供的(idea社区版中基本上没有上述功能)。如果你经济能力足够并且喜欢idea,不妨购买license对它进行支持;否则的话,自行google解决。
Live Reload
LiveReload是指当我们修改了项目中的文件时,浏览器会自动刷新,显示修改之后的效果。
在开发网站时,这个功能非常有用。想想你面前开着两个显示器,中间这个是编辑器,旁边的那个是浏览器。每当修改了java/html/css/js代码时,手不离键盘、光标不离编辑区,浏览器就自动刷新了!只需要眼睛轻轻一瞟,脖子都不用动,就能看到修改之后的效果,这种感觉何等舒畅!钛合金的F5都不需要了!
这种方式对于web框架有要求,首先是修改文件后不需要重启服务器。像纯html/php/rails都天生支持,java中某些框架支持,而play1的支持相当优秀。不论修改html/css/javascript,还是java源代码,甚至是配置文件,都不用重启,直接刷新浏览器了。刷新时间大约为1秒到4秒。
另一点是:因为livereload检查到文件修改后,只会触发浏览器刷新一次,所以要保证一次刷新就可以看到修改后的效果。某些框架利用tomcat/jetty的自动重启,无法很好的配合livereload。因为还没有重启完时,livereload就刷新了,取得的可能还是修改前的页面,必须手动刷新多次才能确定看到的是修改后的效果。对于这种情况,livereload几乎没用。而play在刷新过程中,会阻塞http请求,可以保证一次刷新就拿到修改之后的页面。
Livereload的官网是 http://livereload.com ,它支持mac/linux/windows,同时还有chrome/firefox的浏览器插件。它对windows的支持比较差,很容易崩溃,而且是收费的。所以我们只需要用它的浏览器插件就可以了(免费的),然后再找一个免费的替代器换掉服务器端。
我选择的是: https://github.com/lepture/python-livereload ,它是一个python程序,以命令行方式启动,可以跟livereload的浏览器插件通信,效果不错。注意最好从github中下载源代码安装,因为通过pip或easy_install安装的版本有点旧,使用过程中有问题。(不清楚现在是否已经更新)
使用如下:
cd myproject
cd app
livereload
然后启用浏览器的livereload插件即可。
来段gif演示一下(注意每次刷新都是我按了ctrl+s后自动触发的):
Why play, and why play1
参考我写的另一个日志: http://freewind.me/blog/20120728/965.html
Why angularjs
曾经有一段时间,我对前端javascript框架很感兴趣,试用了很多,比如backbone/knockout/knockback/angularjs/…(以及一大堆已经忘了名字的),其中有两个让我印象深刻。一个是backbone,一个是angularjs。
Backbone的优点在于学习成本很低,与jquery的思路接近,偏向于底层及手动控制。采用backbone的项目比较多,资料也比较多,社区也比较大,还有一些基于backbone发展起来的高一级的框架(如 https://github.com/backbone-boilerplate/grunt-bbb )。这个bbb我没有用过,只是在js群中有人说使用它之后,开发效率比之前纯backbone有很大的提高,所以这里提一下。对于技术较普通的团队来说,使用backbone可能会比较保险一些。
而angularjs则是一个让我感到惊艳的框架,相对于同类无数个mv**框架,它的优势达到了数量级。如果用几个词来形容它,应该是:学习成本高,开发效率高,写代码时思路流畅。
它拥有双向绑定、directive、直接改写html标签等特性,使用它你可以对现有的html标签进行改进和增强,甚至还可以重写一套完全属于自己的html标签。也许你会觉得像“双向绑定”这种烂大街的特性有什么值得拿出来说的,关键在于anguarljs的设计非常统一,各个功能搭配得很流畅、一气呵成。这种感觉就像是eclipse与idea在操作上的区别:idea虽然有很多功能eclipse也提供了,但是用起来总不像idea中那么流畅 — 不论编辑什么类型的文件,在idea中都可以使用非常类似的操作,得到非常类似的界面反馈。
Angularjs的学习成本比较高,主要原因是其设计思路与我们以前写jquery代码时有很大的不同,不能套用。Angularjs的核心思想就是“复用”,它的“复用”体现在"directive"上。Directive既是angularjs的核心,也是它的重点、难点和杀手级特性。简单的说,directive可以是一个自定义的html标签、或者属性、或者注释,它的背后是一些函数,可以对所在的html标签进行增强,比如修改html的dom内容,增强它的功能(如用第三方js插件包装它)。
编写Directive比较复杂,需要理解它的内部原理才能定义出自己的directive。在掌握它以前,以前一些很简单的事情可能都没办法做,容易让人沮丧。比如在使用jquery时,经常会这样操作:
$("#mydiv").dialog();
但这种写法在使用angularjs的html页面中,是无法使用的。你必须把它写成一个directive(比如ui-dialog),然后在它的postLink()方法中,对传入的element元素操作:
element.dialog()
如果不理解postLink的各参数以及它是如何被angularjs使用的话,很难写出来。所以在使用angularjs的前期,很容易被卡住。
在学习angularjs时,一定要细读官网提供的develop guide (http://docs.angularjs.org/guide/ ),把各章节读懂,知道angularjs的内部运行原理。千万别按jquery的方式学习,光看示例是绝对不够的。
Angularjs的另一个杀手级特性,就是把流程控制、事件绑定等代码,直接写在html标签上。这其实就是前面所说的directive的使用方式。先看一段代码:
在这段html代码中,你可以看到那些位于html标签上由蓝色背影标出来的内容,都是angularjs提供的directive。有的是绑定事件(如ng-click,ng-submit),有的是控制流程(如ng-repeat)。这种方式我非常喜欢,简单直接,可读性又很好。当然有人不喜欢这种方式,认为html就应该干干净净,应该把这些东西分享到javascript中,就像下面这样:
$("form").submit(function() { … });
其实对于这种情况,angularjs也有相似的做法,即为该form定义一个directive,比如my-add-form,然后把那些逻辑代码放到它里面:
<form my-add-form>…</form>
// js code
module.directive("myAddForm", function() {
// the logic
});
不过这种方式对于一个不那么通用的逻辑来说有点重。所以我们通常还是采用在html标签上写控制,在controller中写逻辑的方式来做,通过合理的分配,在可读性与方便性之间取得平衡。
直接在标签上绑定事情处理函数,可以减少大量的命名,减少无谓代码,而且阅读起来更直观。想当初看backbone代码时,发现有二分之一的代码,都是通地css selector来获取元素,再将其某个事件与某个函数绑定起来。这样的代码一旦写完,html就不敢随便动了。因为若更改了html结构、id或者css class,这边的js代码都可能无法正常执行。
熟悉angularjs以后,会发现实现前端效果时,开发效率很高。在写html的同时,基本上就可以把大部分的交互效果写出来。同时,angularjs以model为中心,在编码时只需要考虑model。当改变了model的内容时,view就会自动更新,这可以让我们需要关注的东西更少。使用了angularjs后,你会发现html标签的表现力变强了,以前需要一些js插件实现的功能(比如简单的tab、tree等),使用angularjs几行代码就可以实现,而且所有的东西都是可定制的。
比如一个tab:
<div>
<a ng-click="tab=1">Tab1</a>
<a ng-click="tab=2">Tab2</a>
<a ng-click="tab=3">Tab3</a>
</div>
<div ng-show="tab==1">This is tab1</div>
<div ng-show="tab==2">This is tab2</div>
<div ng-show="tab==3">This is tab3</div>
比如一个tree
<script type="ng/template" id="’node.html’">
{{node.name}}
<ul>
<li ng-repeat="node in node.children" ng-include="’node.html’"></li>
</ul>
</script>
<ul>
<li ng-repeat="node in rootNodes" ng-include="’node.html’"></li>
</ul>
我在做某个网站后台时,开始打算用angularjs,后来感觉功能比较简单,就直接采用jquery,少引用一个库。开始还好,很快就发现只要增加一点复杂的功能时,所花的时间就大大增加。如果同样的功能使用angularjs来写,会非常简单。
单页面程序以及前后端交互
一般认为前端mv**框架适合于单页面程序,无刷新、局部更新的那种。前后端完全分离,之间以restful api交互,使用json交换数据。在前端做好router,当点击了某个按钮需要展示新内容时,直接由前端获取并显示另一个局部html页面,同时调用某个restful api获取json数据填充。这种程序,通常前端的功能比较复杂,而对后端要求较少。采用这类mv**框架,前端程序员们可以充分发挥自己的才智,完全使用javascript/css/html来实现功能。而对于后台,只需知道restful api接口即可。这是前端mv**推荐的方式,也是目前来说比较好的方式。其特点是“ 以前端js框架为主,后端为辅 ”。
Anguarljs对于这种方式,有着非常好的支持。它除了提供前端router外,还提供了一些与后台交互的service,如与ajax相关的$http,与restful相关的$resource;对于cookie与本地存储支持也很好,基本上使用angularjs就可以把程序做完。后台可以使用各种语言、各种框架来提供restful api。比如,我尝试过couchdb这个数据库,它直接在数据库层面提供了restful api作为外界操作数据库的接口,angularjs与它配合起来,连服务端程序都不用了。
在开发android程序时,我也尝试过将phonegap与angularjs结合起来,直接使用angularjs来实现程序。与后台之间通过restful api交互。最后虽然因为性能要求改用了android原生方式,但对于普通的安卓或ios应用来说,这种方式是一种很好的选择,开发效率很高。
传统多页面程序
对于我来说,大部分的网站还是传统多页面的。比如一个信息管理系统的后台。这种情况下能否使用angularjs呢?
最开始的时候,我想采用单页面的方式来做,按照前面所说的流程。但是很快遇到了不少麻烦:
- 页面跳转:这种网站页面很多,在前端定义比较麻烦,因为需要写长长的url,容易出错,检查也不方便。而使用play的模板引擎,可以直接写@{Controller.action(params)},play会自动把它变为url,并且会检查是否有拼写错误,非常方便。
- 权限:页面上某些按钮的显示与隐藏,取决于当前用户的角色。所以每次显示某个新页面时,都需要后台传过来一些json数据,例如:{ buttons: [{show: true}, {edit: false}]},来告诉前台显示或隐藏哪些按钮。有的时候这个操作非常繁琐。
- 多次请求:当显示一个新页面时,可能需要多次请求。首先html模板一次,然后取json数据一次(或多次)。这样给人的感觉就有点慢。虽然可通过一些手段(如缓存html模板,为每一个请求返回一个大的合并过的json数据),但由于模板显示的时间与取得json数据的时间之间总有一些间隔,有时候还是会让人觉得不太流畅,卡。
这几个问题我想了很久也没好办法,甚至打算放弃angularjs,还是采用以前完全由服务端生成页面的方式来做。好在最后改变了思路,“ 以后端框架为主,前端为辅 ”,找到了比较好的办法:
- 后端能做的事情尽量在后端做,比如页面跳转,模板引擎,权限处理等,显然后端有优势
- 因为在angularjs中,各数据都要以model的方式由angularjs管理,所以在html中,将原来直接显示的数据以json方式嵌入,赋给angularjs。这样打开一个新页面时,angularjs所需的全部数据都有了,不需要再次请求。
- 后端不提供restful接口,相反生成一个js文件,把各action以函数的方式暴露出来,让angularjs直接调用,而不需要写长长的url。既方便又不容易出错。
- 前端完全不使用router,还是按照传统的方式跳转,整页刷新
这样就把前后端的优点结合起来了:前端专注于页面显示及效果,后端处理其它的杂事。不追求无刷新,而追求开发效率与写代码时的舒适性。
这种方式更注重后端的功能,对前端的美工要求较低。与前面单页面程序的开发正好相反。我觉得应该根据程序的情况采用不同的方式,不要硬套。
关于angularjs的$resource
在angularjs中提供了一个service叫$resource:http://docs.angularjs.org/api/ngResource.$resource ,它可以通过一个url和一些参数,定义一个resouce对象。通过调用该对象的某些方法,可以与后台交互,并且直接得到经过它处理之后的数据。使用的感觉有点像我们在后端常用的dao。
这个$resource服务对于“单页面,以restful api交互”的情况比较合适。它要求所给出来的url可以按restful api的方式调用,正好满足适合这种情况。
但对于“传统多页面程序”不好用,特别是那种信息管理系统。因为它们的url形式并不重要,是否restful也不重要,只要提供get/post两种方式,能把参数传过去就行了。如果在这里使用$resource,按它的规定来套,需要花很多心思来设计url,非常痛苦。我在这里卡了很长时间,因为我想不明白,为什么这个$resource看起来很好,但用起来就是不对劲呢。最后终于想通,原来我的情况不需要restful api。
最后我的做法是,在服务端把各action收集起来,生成一个js文件,在这个文件里把action以js函数的方式暴露出来,供angularjs直接调用(内部使用了angularjs提供的$http服务,而没有用$resource)。在本例中,这个js文件中定义了一个叫JsRoutes的object供使用。
Angularjs调用它的方式是这样的:
function Ctrl($scope, JsRoutes) {
$scope.submit = function() {
JsRoutes.Users.create.post({
username: username,
password: password
}, function(res) {
alert("ok");
});
}
}
这样,当我需要向后台传user相关的数据时,直接调用预定义的JsRoutes.Users.create即可,而不需要关注它对应的url到底是什么。
这个js文件的代码是这样的:
angular.module('JsRoutes', []).factory('JsRoutes', function ($http) {
var defaultErrorHandler = function (data, status, headers, config) {
alert('Sorry, server responses ' + status + ' error: ' + data);
};
// angular post json by default, change it to key value pairs
var keyValuesTransformFn = function (d) {
return jQuery.param(d);
};
var commonHandler = function (method, url, params, data, success, error, config) {
config = config || {};
config.method = config.method || method;
config.params = config.params || params;
config.data = config.data || data;
config.url = config.url || url;
config.timeout = config.timeout || 120 * 1000;
var postType = config.postType || 'form';
if (postType === 'form') {
config.transformRequest = keyValuesTransformFn;
config.headers = config.headers || {};
config.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
// config.headers['Accept'] = "application/json, text/html, text/plain, */*";
}
$http(config).success(success).error(error || defaultErrorHandler);
};
var jsRoutesHandler = function (path) {
return {
get: function (params, success, error, config) {
commonHandler('get', path, params, {}, success, error, config);
},
post: function (data, success, error, config) {
commonHandler('post', path, {}, data, success, error, config);
}
}
};
return {
Application: {
"index": jsRoutesHandler('/Application/index'),
"toAction": jsRoutesHandler('/Application/toAction'),
"showViewSource": jsRoutesHandler('/Application/showViewSource')
},
Admins: {
"index": jsRoutesHandler('/Admins/index')
}
};
});
采用这种方式后,在前端js代码中完全不需要跟url打交道,因为它们都是在服务器端根据routes文件生成的。以后url有什么变化,js这里不需要修改一行代码。
另外需要注意的是,angularjs默认会向服务端发送json格式的数据,而play对key-value形式的数据处理的比较好,所以我就把它默认值改为了’application/x-www-form-urlencoded’
Angularjs中如何集成第三方js插件
有时候需要用一些第三方插件,比如datepicker,slider,或者tree等。以前的做法是直接通过jquery取得某个元素,然后调用某个方法即可。但在angularjs中,不能直接这么写,必须写在directive中。
有一个叫augular-ui的项目: https://github.com/angular-ui/angular-ui ,已经集成了一些常用的插件(来自jqueryui),很方便。但如果还是需要自己定义,该怎么做呢?
基本的思路就是,创建一个directive,把调用jquery插件的代码放在它里面。这里以jqueryui的slider为例:
样子如上,其示例在: http://jqueryui.com/slider/
在jquery中,它的调用方式是这样的:
<div id="slider"></div>
$("#slider" ).slider({
min: 0,
max: 100,value: 10,
step: 5
});
而在angularjs中,我们需要定义一个directive,假设是ui-slider:
app.directive('slider', function () {
return {
require: '?ngModel',
restrict: 'A',
link: function (scope, element, attrs, ngModel) {
var opts;
opts = angular.extend({}, scope.$eval(attrs.slider));
var slider = element.slider({
min: opts.min || 0,
max: opts.max || 100,
step: opts.step || 10,
value: attrs.ngModel && scope.$eval(attrs.ngModel) || 50,
slide: function (event, ui) {
if (ngModel) {
scope.$apply(function () {
ngModel.$setViewValue(ui.value);
})
}
}
});
scope.$watch(attrs.ngModel, function (v) {
slider.slider({
value: v
});
});
}
};
});
在html中的调用方式是:
<div slider="{min:0,max:500,step:5}" ng-model="row.width"></div>
在directive中的代码比jquery的多了不少,不过主要是增加了与双向绑定有关的两个函数。例如在"slide: function(event, ui)“中,是把sider的值传回给某个model(本例中为row.width)。在后面的scope.$watch中,是把model的值传给slider。这样当拖动slider上的刻度时,row.witdh的值会自动改变;或者改变了row.width的值之后,slider的刻度也会自动变化。
基本的思路就是这样,更详细的需要看相关文档。与angularjs相关的就讲到这里,在这里你可以看到很多angularjs的可执行的例子:https://github.com/angular/angular.js/wiki/JsFiddle-Examples ,可以直接感受。
最后需要补充的两点:
- Angularjs的社区气氛很好。其google group人气很旺,有问题很快就能得到详细的回复
- Angularjs对IE6/7支持不好。如果一定要支持ie6/7,可考虑backbone或其它框架。
Bootstrap
Bootstrap是像我们这样缺少艺术细胞的程序员的福音。只需要记一些css class和某些js component的用法,就可以做出看起来比较美观、专业的页面效果出来。虽然过不了多久,人们也许就会对它出现审美疲劳,但好在它的社区已经形成,已经有不少基于它的模板出来,相信以后会有更多更美观的bootstrap theme可供选择: