使用Backbone.js开发Chrome便签插件

来源:互联网 发布:雷吉米勒数据 编辑:程序博客网 时间:2024/05/02 06:10

在Web Store上没找到满意的便签插件,就只好自己动手写了Notty Notes,你可以试试看,多多提建议哦~Notty Notes

Backbone的流行,与前端复杂度的提高息息相关,尤其越来越多的大型单页应用的上线,代码的组织方面就产生很多新的问题。所以MV*的概念又一次在前端应用开来,不管最后那个*被定义成为什么,M(odel)和V(iew)这两层的分离,对于代码的组织大有好处。Backbone就是很简洁恰当的解决了这个问题,并没有带来一点点多余之物,这也是他的动人之处。

Chrome的美丽与Backbone有相同之处。他的插件开发流程令人愉悦,Chrome Web Store上展现率和安装率也挺让人安慰,那丁点的热情也不会被打消掉。所以,综合种种,我又结合Backbone.js的使用,总结出来这片Blog。

Backbone.js gives structure to web applications by providing models with key-value binding and custom events, collections with a rich API of enumerable functions, views with declarative event handling, and connects it all to your existing API over a RESTful JSON interface.

这是官方的介绍,Backbone.js给web应用开发约定了一种结构,包括用key-value绑定且可以自定义事件的Models,提供大量API方法的Models的集合Collections,以及用来响应事件的Views,并把这些很好的与你的RESTful的Json接口相结合,有效的组织复杂应用的代码。Backbone.js基于jQuery和Underscore。

Backbone算轻量级的MVC框架,他的优雅之处在于,他为复杂的代码约定了一种优美的组织方式。使用Backbone可以脱离处理复杂DOM的苦海。你的数据就是ModelsCollectionsModels的集合,Models的每一个变化都会触发change事件,Views用来响应数据的变化。就我的使用效果来说,对于相同功能的应用,代码量减少至少30%以上,更别说在可维护性方面带来的提高了。

相对于基本的HTML页面,Backbone.js的更适用于单页复杂应用(Single Page Application)。什么是单页复杂应用,比如Gmail、Google Reader、阿尔法城等,当然包括我将要讨论的便签插件。

便签是很直观、简单的东西,设计思路基本上模拟真实物品,所以也没多费神,实现的结果就是这样子的:Notes Draft

为了一些简单的自定义功能,还有这样的设置页:Notes Settings

总体来看,页面元素很简单,独立的三个模块:便签模板、关闭浮层、设置浮层。

为了不让过多的代码占据篇幅,简写如下,后面会给出源码地址:

  1. <body id="container">
  2. <!--便签模板-->
  3. <div class="note-tmpl" id="note-template">
  4. <!--Something Code...-->
  5. </div>
  6. <!--设置浮层-->
  7. <div class="modal hide fade" id="modal-settings">
  8. <div class="modal-body">
  9. <!--Something Code...-->
  10. </div>
  11. <div class="modal-footer">
  12. <!--Something Code...-->
  13. </div>
  14. </div>
  15. <!--关闭浮层-->
  16. <div id="tmpl-close" class="hide">
  17. <div class="alert fade in tmpl-close-confirm">
  18. <!--Something Code...-->
  19. </div>
  20. </div>
  21. </body>

为了快速搭建,使用了Bootstrap,其中的modal就是Bootstrap的模态对话框,Bootstrap更多特性可以参考官方文档,这里就不展开了。

样式我这样不懂审美的土鳖来说,是最困难的,只能根据直觉慢慢调整,还好CSS3方便了很多,不然最终肯定都是无聊的线框。不用兼容浏览器,那些漂亮的样式可以敞开了写,不过从设计角度来说,过于绚丽会导致操作效率降低,令用户产生厌烦,恰到好处是最好的,这方面我只到有直觉的水平,并无有价值的理论经验可分享。最终效果自己体会就好,关注如何实现,可以到源码中一看究竟。

用到的插件除了Bootstrap以外,还有:

  • jQueryUI,包括拖拽和放大缩小插件。
  • Animate.css,很不错的CSS3动画插件,只包含css文件。
  • Backbone.localStorage.js,Backbone本地存储的适配器。
  • Sanitize.js,清理过滤HTML节点。
  • UnderScore,Backbone依赖UnderScore.js。

页面的部分不是这篇博客的重点,就像平常开发一样。在做这个完全我自己设计开发的东西的过程中,体会到想达到很优雅的用户体验,需要关注和解决的问题很多,是一个非常考验耐心的事情,不过最终完成时候的成就感自然也不同啦。

Model就是要操作对象的数据结构,存储需要用到的数据,基于这些数据,会有大量的交互、验证等等操作。

根据要做的便签的需要,数据结构定义为如下:

  1. var Note = Backbone.Model.extend({
  2. defaults:{
  3. position:{top:20,left:30}
  4. ,scale:{width:300,height:300}
  5. ,theme:'0'
  6. ,fonttheme:'1'
  7. ,customfont:'16'
  8. ,title:'Note'
  9. ,content:''
  10. ,collapsed:''
  11. ,locked:false
  12. ,contentHeight:248
  13. ,ref:{title:'',href:''}
  14. }
  15. ,initialize:function(){
  16. console.log('Model initialized!');
  17. }
  18. ,remove:function(){
  19. this.destroy();
  20. }
  21. });

可以看到,我们定义了Note的Model的默认值,还有initialize和remove方法,当new一个Note对象时候,initialize方法会执行,默认的值也会赋给new的对象。

也可以在new的时候修改覆盖默认值:

  1. var note1 = new Note({title:'New Note',content:'This is the new note'});//覆盖默认的title和content
  2. var note2 = new Note();
  3. note2.set({title:'New Note 2',content:'This is the new note 2'}); //设置Model的值
  1. var note3 = new Note();
  2. note3.set({title:'New Note 3',content:'This is the new note 3'}); //设置Model的值
  3. var title = note3.get('title'); // "New Note 3"

要监听change事件,可以在initialize方法中做,也可以在实例化之后做:

  1. var note4 = new Note({title:'New Note 4',content:'This is the new note 4'});
  2. note4.bind('change:title',function(){
  3. //some code...
  4. });
  5. note4.bind('change:theme',function(){
  6. //some code...
  7. });
  8. note4.bind('change:fonttheme',function(){
  9. //some code...
  10. });

在这个应用中,无需与服务端交互,用了那个localStorage的补充插件之后,同样调用save()和destory()方法就好,该插件会自动完成相应的工作,真正与服务端的交互也很简单:

  1. var UserModel = Backbone.Model.extend({
  2. urlRoot: '/user'
  3. ,defaults: {
  4. name: ''
  5. ,email: ''
  6. }
  7. });

其中urlRoot用来定义服务端的RESTful的API接口。

新建:

  1. var UserModel = Backbone.Model.extend({
  2. urlRoot: '/user'
  3. ,defaults: {
  4. name: ''
  5. ,email: ''
  6. }
  7. });
  8. var user = new Usermodel();
  9. // 注意这里没有id
  10. var userDetails = {
  11. name: 'notty'
  12. ,email: 'notty@example.com'
  13. };
  14. //因为没有设置id,所以服务端接收到请求的时候,会自动新建,并添加id
  15. user.save(userDetails, {
  16. success: function (user) {
  17. alert(user.toJSON());
  18. }
  19. })

获取:

  1. // 这里设置了id
  2. var user = new Usermodel({id: 1});
  3. // 下面这个方法会使用GET /user/1
  4. // 服务端返回相应的数据
  5. user.fetch({
  6. success: function (user) {
  7. alert(user.toJSON());
  8. }
  9. })

修改:

  1. // 我们修改id为1的用户数据
  2. var user = new Usermodel({
  3. id: 1,
  4. name: 'notes',
  5. email: 'notes@gmail.com'
  6. });
  7. // 因为有id,所以Backbone.js会触发 PUT /user/1,并发送相应的数据给服务端
  8. user.save({name: 'Davis'}, {
  9. success: function (model) {
  10. alert(user.toJSON());
  11. }
  12. });

删除:

  1. // 这里设置了model的id
  2. var user = new Usermodel({
  3. id: 1
  4. ,name: 'Notty'
  5. ,email: 'Notty@example.com'
  6. });
  7. // 因为有id,所以触发 DESTROY /user/1
  8. user.destroy({
  9. success: function () {
  10. alert('Destroyed');
  11. }
  12. });

Model还支持validate方法,可以对数据进行校验,校验不通过,则不能执行后续操作,在声明Model时候,增加validate方法就好:

  1. Person = Backbone.Model.extend({
  2. // 如果年龄小于0,就会报错
  3. validate: function( attributes ){
  4. if( attributes.age < 0 && attributes.name != "Dr Manhatten" ){
  5. return "You can't be negative years old";
  6. }
  7. }
  8. ,initialize: function(){
  9. alert("Welcome to this world");
  10. this.bind("error", function(model, error){
  11. alert( error );
  12. });
  13. }
  14. });

Collection的概念很好理解,他就是Model的一个简单集合,举几个例子:

  • Model是歌,Collection是专辑
  • Model是动物,Collection是动物园
  • Model是一个便签,Collection是所有便签(囧)

创建一个Collection,Model还是之前的便签的Model:

  1. var NoteCollection = Backbone.Collection.extend({
  2. model:Note
  3. ,localStorage:new Backbone.LocalStorage('notty-note')
  4. });
  5. var note1 = new Note({title:'New Note',content:'This is the new note'});
  6. var note2 = new Note({title:'New Note 2',content:'This is the new note 2'});
  7. var note3 = new Note({title:'New Note 3',content:'This is the new note 3'});
  8. var Notes = new NoteCollection([note1,note2,note3]);
  9. console.log(Notes.models) //[note1,note2,note3]

需要注意的是,我们的便签插件不需要与服务端交互,但是需要本地存储,所以使用localStorage。

好了,重头来了,View的声明和其他的基本上一样,一些参数查文档就很容易明白:

  1. var NoteView = Backbone.View.extend({
  2. tagName:'div'
  3. ,className:'note'
  4. ,template:function(tmpl,obj){
  5. }
  6. ,initialize:function(){
  7. }
  8. ,render:function(){
  9. return this;
  10. }
  11. ,events:{
  12. 'mousedown':'clickNote'
  13. ,'dblclick .note-nav':'foldContent'
  14. ,'click .note-nav-close':'deleteNote'
  15. ,'click .note-nav-lock':'lockNote'
  16. ,'click .note-nav-title':'settings'
  17. ,'keyup .note-content':'contentChange'
  18. ,'blur .note-content':'contentChange'
  19. }
  20. ,clickNote:function(e){
  21. }
  22. ,deleteNote:function(e){
  23. }
  24. ,lockNote:function(e){
  25. }
  26. ,settings:function(){
  27. }
  28. ,contentChange:function(e){
  29. }
  30. ,changeScaleAndContentHeight:function(view){
  31. }
  32. ,bringNoteToContainer:function(view){
  33. }
  34. ,bringNoteToFront:function($ele){
  35. }
  36. });

在这个应用中,分离了每个便签的View和整个app的View,这样做的好处是逻辑、代码更清晰。

this.el是这个View的DOM引用,使用它可以方便的做很多操作DOM的事情。注意到在这个View的声明里面,定义了template函数,不适用Underscore自带的template的函数的原因是Chrome Manifest V2的版本里面不允许出现new Function,导致很多模板库不能使用,只好自己重写一个简单的。template在这个场景还是能非常方便的解决一些问题的。

使用this.el我们如何给View填充数据呢,很简单:

  1. var $ele = $(this.el);
  2. $ele.html(
  3. this.template('#note-template',model.toJSON())
  4. ).draggable({
  5. handle:'.note-nav'
  6. ,stack:'.note'
  7. ,stop:function(){
  8. var $el = $(this);
  9. that.bringNoteToContainer(that);
  10. }
  11. }).css({
  12. position:'absolute'
  13. ,top:model.get('position').top
  14. ,left:model.get('position').left
  15. ,width:model.get('scale').width
  16. ,height:model.get('scale').height
  17. }).resizable({
  18. minWidth:200
  19. ,minHeight:200
  20. ,handles:'e,w,s,se'
  21. ,alsoResize:$ele.find('.note-content')
  22. ,stop:function(){that.changeScaleAndContentHeight(that)}
  23. });

initialize方法中,可以初始化这些事情,需要更多的初始化工作,继续写写下去就好。

你肯定注意到了声明View代码中的下面这些:

  1. ,events:{
  2. 'mousedown':'clickNote'
  3. ,'dblclick .note-nav':'foldContent'
  4. ,'click .note-nav-close':'deleteNote'
  5. ,'click .note-nav-lock':'lockNote'
  6. ,'click .note-nav-title':'settings'
  7. ,'keyup .note-content':'contentChange'
  8. ,'blur .note-content':'contentChange'
  9. }

这些用来给View绑定事件,就和平常使用jQuery一样的用法,相信你一看就明白。

appView并不是Backbone的内容,而是在这个应用中,我们用来粘合所有元素的一个容器,为了将整个流程串联起来,有一个总体的概念,我会详细解释这部分的代码:

  1. var appView = Backbone.View.extend({
  2. //指定appView的el,也是整个应用的总容器
  3. el:$('#container')
  4. //同样的initialize方法
  5. ,initialize:function(){
  6. //给数据绑定相应的事件
  7. this.collection.bind('add',this.addOne,this);
  8. this.collection.bind('reset',this.addAll,this);
  9. //这里的Notes就是便签Model的Collection
  10. //在初始化的时候,fetch方法从localStorage中取出所有数据
  11. Notes.fetch();
  12. }
  13. ,render:function(){
  14. return this;
  15. }
  16. //初始化一个便签note
  17. ,initOne:function(note){
  18. //这里的NoteView就是每个便签的View
  19. //NoteView使用数据Model就是note
  20. var view = new NoteView({model:note});
  21. var ele = view.render().el
  22. $('#container').append(ele)
  23. //细节处理
  24. view.bringNoteToContainer(view)
  25. return ele;
  26. }
  27. //当用户自主添加的时候,增加动画效果
  28. ,addOne:function(note){
  29. //前面的初始化还是一样
  30. var ele = this.initOne(note);
  31. //这里是Animate.css的动画
  32. $(ele).addClass('animated bounceIn');
  33. //洁癖,细节处理
  34. setTimeout(function(){
  35. $(ele).removeClass('animated bounceIn');
  36. },600);
  37. }
  38. //fetch之后会触发reset事件,我们绑定了这个方法
  39. ,addAll:function(){
  40. var that = this;
  41. var length = Notes.models.length;
  42. $.each(Notes.models,function(index,item){
  43. //一个个的初始化
  44. that.initOne(item)
  45. });
  46. }
  47. });
  48. //这里是真正的启动的代码
  49. //指定了数据Collection
  50. var app = new appView({collection:Notes});
  51. //用户添加的时候事件处理
  52. $(document).bind('dblclick',function(e){
  53. e.preventDefault();
  54. if(e.target==$('html')[0]){
  55. Notes.create({position:{top:e.pageY,left:e.pageX}});
  56. }
  57. });

注释很详细,也不用再赘述了。

基本上到这里,使用Backbone.js整体的框架已经搭建好了,剩下的就是填充血肉了,这里面充满了细节,写的过程中慢慢体会、完善。再提供几个比较好的资源,以及这个插件的源码地址:

  • Notty Notes源码
  • Babkbone 官方站点
  • Backbone Tutorials
  • 简单的Todo实例
  • Backbone 学习笔记
  • Backbone 架构分析
  • Backbone HelloWorld(中文)
  • Backbone 初探
  • Backbone has made me a better programmer

Chrome插件开发的流程很舒适、自然。为了先配好环境,就先大致的介绍一下插件开发的相关事宜。如果比较熟悉,可忽略跳过。更详细全面的说明参考官方文档。

每一个插件的manifest.json文件必不可少,看看Notty Notes,也就是我们要写的插件的manifest.json的内容吧:

  1. {
  2. "name": "Notty Notes"
  3. ,"version":"1.3"
  4. ,"manifest_version":2
  5. ,"description":"One of The Best Sticky Notes For Your Broswer"
  6. ,"app": {
  7. "launch": {
  8. "local_path": "main.html"
  9. }
  10. }
  11. ,"icons":{
  12. "16":"assets/32.png"
  13. ,"32":"assets/32.png"
  14. }
  15. ,"permissions": [
  16. "unlimitedStorage"
  17. ]
  18. }

顾名思义的就不多讲了,现在较新版本的Chrome,会对那些没有使用manifest_version为2的插件提示,升级为2之后,Chrome对插件的权限控制会更多,之前用到一些比较方便的写法都会有问题,比如内联的JS,new一个Function等等,Chrome extension 升级到 manifest version 2 的问题里说明了一些问题,可以了解一下。

根据插件的性质不同,app部分会有不同,我们要开发的便签插件是一个独立页面的应用,所以按照上述的方法写,如果是那种只打开一个在线页面的链接型插件,只要改成:

  1. ,"app": {
  2. "launch": {
  3. "web_url": "http://overapi.com/?chrome"
  4. }
  5. }

如果是浏览器增强型的,可以写成:

  1. ,"browser_action": {
  2. "default_icon":"assets/24.png"
  3. ,"default_title":"Notty Notes"
  4. ,"default_popup":"popup.html"
  5. }

无需app字段。按照你的需求,更多的可以参考Manifest Files。

permissions项是向Chrome请求需要的权限,在用户安装插件额时候,会有提示,可声明的权限可以参考Permissions。

关于文件的组织,除了manifest.json文件需要放在根目录之外,其他文件放在manifest中指定的位置即可,不得不说,很愉悦。

开发调试过程简单方便,在扩展程序的管理页面,选择载入正在开发的扩展程序,加载正在开发的文件目录就好,单页的应用,可直接在应用当页使用开发工具调试,background类型的也可以方便的在扩展程序页面看到打开调试工具的按钮。

做完准备工作,余下的开发过程就同平常一样,修改代码,刷新,看效果,调BUG,毫无不适。

Chrome Web Store在早起测试阶段,可以免费注册使用,现在注册一个可发布应用的账号需要支付5美元的费用,而且得是信用卡,之前一直没办信用卡,所以写了的插件只能自己打包发布管理,异常的纷杂,办信用卡之后,第一件事就是注册Chrome Web Store。

需要注意的一点是,填写地址部分,没有中国大陆的选项,不知道Google会不会检查信用卡发卡地和地址是否匹配,我选择了香港,瞎填了地址,也通过了,仅供参考。

Chrome Store上插件的发布很简单,把自己的插件按要求打包好,上传即可,介绍写的简洁动人一些,准备几个漂亮的图标,再做几张符合尺寸要求的宣传图,发布之后,很快就能生效在相关的分类里面看到。吐槽一下,我的宣传图上传好几天了,也没见通过审核的迹象,效率啊XD。

最后再广告一下我的便签插件吧,感谢靖哥哥帮我做宣传图:Notty Notes

0 0
原创粉丝点击