BackBone
更新日期:
什么是前端, Html + Javascript
html javascript 混杂在一起,开起来是一个大杂烩
什么是好的代码,应当把你的应用解藕成一系列相互平等且独立的页面
使用类、继承、对象和设计模式
MVC:数据(模型) 展示层(视图) 用户交互层(控制器)
- 用户和应用产生交互
- 控制器的事件处理器被触发
- 控制器从模型中请求数据,并将其交给视图
- 视图将数据呈现给用户
模型(Model)
用来存放所有的数据对象
模型不必知晓视图和控制器的细节,只需要包行数据及直接和这些数据相关的逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | // model.js // 用户字段 var User = Backbone.Model.extend({ defaults: { identity: UserType.SelfEmployed, canSpouseGuarantee: UserMap.CanSpouseGuarantee.Canot, socialSecurityFundmonth: -1, }, isCompany: function(){ var identity = this.get('identity'); return identity != UserType.WageEarner && identity != UserType.Other; } }); // 记录页面信息 var PageInfo = Backbone.Model.extend({ defaults: { permission: [ UserType.BusinessOwner, UserType.SelfEmployed, UserType.WageEarner, UserType.Other ], current: false, // 是否当前页 showInNav: true, // 时候要显示在侧边栏上,针对 Index forInput: true, // 是否要显示出来以便用户填写 complete: 0, // 显示填写百分比 completeForCompany: 0, // 针对企业显示的百分比 validateKeys: [], // 必须填写的字段,需要验证的字段,如有特殊情况,重写 isAllFilled validateDepends: [] // 在一定条件下必须填写的字段 参考 UserInfo.js }, initialize: function( properties ){ _.extend( this, properties ); } } |
视图
视图层是呈现给用户的,用户与之产生交互。视图不必知晓模型和控制器中的细节
Html 页面, html 模板
1 2 3 4 | <script type="text/template" id="navTemplate"> <i></i> <span>${name}</span> </script> |
控制器
控制器是模型和视图之间的纽带。控制器从视图获得事件和输入,对它们进行处理, 并相应的更新视。当页面加载时,控制器会给视图添加事件监听。
不使用类库和框架也能实现视图
1 2 3 | $('').click(function(){ // any code here }); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 | // views.js (function(){ var models = $.app.models; var PageInfos = models.PageInfos; // 每个页面的原型 var PageView = Backbone.View.extend({ initialize: function() { _.bindAll(this, 'display'); this.setElement( this.model.get('el') ); this.model.set({view: this}); // 如果是当前页,则显示出来 this.model.bind('change:current', this.display); this.display(); if(this.init) this.init(); }, show: function(){ this.beforeEnter(); this.$el.show(); }, hide: function(){ this.$el.hide(); this.afterLeave(); }, // 在显示页面之前,需要如何,比如 slogan 隐藏侧边栏 beforeEnter: function(){ }, // 在离开页面之后需要如何 afterLeave: function(){}, display: function(model, current) { if( current ){ this.show(); } else { this.hide(); } }, // 因为要大量用到, 所以抽出来一个方法 // 将页面显示,绑定到用户变量 bindWithUser: function( user, field, event, selector, params){ var self = this; // 用户点击的时候, 回调函数 params = params || {}; var changeCallback = params.changeCallback; // 当同步的时候, 回调函数 var syncCallback = params.syncCallback; var change = function(e){ var $target = $(e.currentTarget); var type = $target.data('type'); user.set(field, type); // console.log(changeCallback); if(changeCallback) changeCallback(); }; var sync = function(){ self.$(selector).removeClass('cur'); self.$(selector + '[data-type=' + user.get(field) + ']').addClass('cur'); if(syncCallback) syncCallback(); }; sync(); self.listenTo(user, 'change:' + field, sync); this.$el.on(event, selector, change); } }); // 侧边栏控制器 var NavItem = Backbone.View.extend({ tagName: 'li', // TODO 点击实现 events: { "click": "switchTo" }, initialize: function(){ _.bindAll(this, 'toggleCurrent'); this.template = $("#navTemplate").template(); this.listenTo(this.model, 'change:current', this.toggleCurrent); this.listenTo(this.model, 'change:forInput', this.toggleVisible); }, // 切换到当前页面 switchTo: function(){ // appView 监听事件 Backbone.trigger("switch", this.model); }, render: function () { var element = $.tmpl(this.template, this.model.toJSON()); this.$el.html(element); this.toggleCurrent(); this.toggleVisible(); return this; }, toggleCurrent: function(){ if(this.model.get('current')) this.$el.addClass('cur'); else this.$el.removeClass('cur'); }, toggleVisible: function(){ if(this.model.get('forInput')) this.$el.show(); else this.$el.hide(); } }); // 主控制器 var AppView = Backbone.View.extend({ events: { 'click footer.next a': "gotoNext" }, initialize: function(data){ _.bindAll(this, 'syncForView', 'pageSwitch'); this.$nav = this.$('nav ul'); this.$navIndicator = this.$('nav .cur-step'); this.user = data.user; this.cur = null; // 记录当前的 page this.addNav(); this.gotoNext(); this.syncForView(); this.listenTo(Backbone, 'switch', this.pageSwitch); this.listenTo(Backbone, 'goto', this.gotoPage); // 根据用户选择身份来刷新是否要填写的内容 this.listenTo(this.user, 'change:identity', this.syncForView); }, // 根据id导航 gotoPage: function(pageId){ var page = this.model.find( function(m){ return m.get('el') == pageId; }); if(page) this.setCurrent(page); }, // 点击导航栏切换 pageSwitch: function(page){ if(page == this.cur ) return; if( !this.chkIsFilled() ) { $.gritter.add({ // (string | mandatory) the text inside the notification text: '请将字段填充' }); return; } // 获得没有填写完整 切 index 比要切换的page 小的 var isNotFilled = this.model.find(function(m){ return page.get('index') > m.get('index') && m.isAllFilled(this.user); }); if(!isNotFilled) this.setCurrent(page); }, syncForView:function(){ var user = this.user; this.model.each(function(page){ page.refresh(user.get('identity')); }); }, addNav: function(){ this.$nav.html(''); this.model.each( function (page) { if(page.get('showInNav')) { var view = new NavItem({ model: page }); this.$nav.append(view.render().el); } }, this); }, // 检查当前页面是否全部正确填写 chkIsFilled: function(){ if( !this.cur) return true; return this.cur.isAllFilled(this.user); }, // 点击切换到下一个页面 gotoNext: function(){ if( !this.chkIsFilled() ) { $.gritter.add({ text: '请填充字段' }); // return; } this.setCurrent(this.getNextView()); }, // 获取下一个页面 getNextView: function(){ var curIndex = -1; if(this.cur) curIndex = this.cur.get("index"); var avaliables = this.model.filter(function(p) { var pIndex = p.get('index'); return pIndex > curIndex && p.get('forInput');; }); var next = _.min( avaliables , function(m){ return m.get('index'); }); if(next == Infinity) next = this.cur; return next || this.cur; }, // 设置当前选中页面 setCurrent: function(cur){ if(this.cur) { this.cur.set({current: false}); } cur.set({ current: true }); this.cur = cur; this.setIndicator(); }, // 设置百分数 setIndicator: function(){ var self = this; var p = this.model.reduce(function(percent, page){ if(page.isAllFilled(self.user) && !page.get("showInNav") && page.get("forInput")) { var tmp = -1; if( self.user.isCompany() ) { tmp = page.get("complete"); } else { tmp = page.get("completeForCompany"); } return percent > tmp ? percent : tmp; } else { return percent; } }, 0); this.$navIndicator.html( p + '%'); } }); $.app.views.AppView = AppView; $.app.views.PageView = PageView; }).call(this); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | // 每个页面都有自己的控制器 (function(){ var models = $.app.models; var PageInfo = models.PageInfo; var UserType = models.UserType; var PageView = $.app.views.PageView; var telRegex = /^(0|86|17951)?(13[0-9]|15[012356789]|17[678]|18[0-9]|14[57])[0-9]{8}$/; $.app.pages.BasicInfo = function(user){ var info = new PageInfo({ el: "#partialBasicInfo", name: "基本信息", index: 1, complete: 0, // 如果所有的字段都要填写, 就不用重写 isAllFilled validateKeys: ['identity', 'userName', 'cellPhoneNumber'], // 重写 pageInfo 方法,因为并不是所有字段都要验证 isAllFilled: function(model) { // 调用父类方法 if(this.__proto__.isAllFilled(model)) { var id = model.get('identity'); // 如果是企业主那么 需要填写 share if(id == UserType.BusinessOwner || id == UserType.SelfEmployed) { return !this.isEmpty( model.get('share') ); } return true; } return false; } }); var BasicInfoView = PageView.extend({ init: function() { // 绑定作用域 _.bindAll(this, "syncIdentity", "syncShare"); // 缓存选择器 this.$telError = $('#telError'); this.$nameError = $('#nameError'); this.$nameInput = $("#username"); this.$telInput = $("#tel"); //初始化参数 this.syncIdentity(); this.syncShare(); this.$telInput.val(user.get('cellPhoneNumber')); this.$nameInput.val(user.get('userName')); // 监听变化 this.listenTo(user, 'change:identity', this.syncIdentity); this.listenTo(user, 'change:share', this.syncShare); this.listenTo(user, 'change:userName', function(model, val){ this.$nameInput.val(val); }); this.listenTo(user, 'change:cellPhoneNumber', function(model, val){ this.$telInput.val(val); }); }, events: { "change #tel": "chkTel", "change #username": "chkName", "click .form .identity li": "selectType", "click .form .info a": "selectShare" }, syncIdentity: function(){ var $cur = this.$('.identity li[data-type=' + user.get('identity') + ']'); $('.identity li').removeClass('cur'); $cur.addClass('cur'); }, syncShare: function(){ var $cur = this.$('.identity .info a[data-type=' + user.get('share') + ']'); $('.identity .info a').removeClass('cur'); $cur.addClass('cur'); }, chkTel: function(){ var tel = this.$telInput.val(); if( !telRegex.test(tel) ) { this.$telError.show(); } else { this.$telError.hide(); // 如果正确,就保存起来 user.set('cellPhoneNumber', tel); return true; } if( ret == "" ){ user.set('cellPhoneNumber', ""); } return false; }, chkName: function(){ var username = $.trim(this.$nameInput.val()); if(username=="") { this.showNameError("请输入用户名"); user.set('userName', ""); return false; } else if( /^\d.*$/.test( username ) ){ this.showNameError("用户名不能以数字开头"); return false; } else if(username.length<2 || username.length>15 ){ this.showNameError("合法长度为2-15个字符"); return false; } else { this.showNameError(""); } user.set('userName', username); return true; }, showNameError: function(notify) { this.$nameError.html(notify); if( notify != "" ) this.$nameError.show(); else this.$nameError.hide(); }, selectType: function(e){ var target = $(e.currentTarget); user.set('identity', target.data('type')); }, selectShare: function(e) { var target = $(e.currentTarget); user.set('share', target.data('type')); } }); var page = new BasicInfoView({ model: info }); return info; }; }).call(this); |
程序入口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | // app.js $(function(){ var views = $.app.views; var models = $.app.models; var pages = $.app.pages; var PageInfo = models.PageInfo; var PageInfos = models.PageInfos; var PageView = views.PageView; var AppView = views.AppView; var User = models.User; var app = app || {}; app.user = new User(); app.pages = new PageInfos(); app.pages.add( pages.Slogan(app.user) ); app.pages.add( pages.BasicInfo(app.user)); app.pages.add( pages.CompanyType(app.user)); app.pages.add( pages.IncomeType(app.user)); app.pages.add( pages.WorkingAge(app.user)); app.pages.add( pages.CompanyPosition(app.user)); app.pages.add( pages.CreditStanding(app.user)); app.pages.add( pages.SocialSecurityfund(app.user)); app.pages.add( pages.UserInfo(app.user) ); app.pages.add( pages.HouseInfo(app.user) ); app.pages.add( pages.MonthIncome(app.user) ); app.pages.add( pages.LoansInfo(app.user) ); app.pages.add(pages.CarInfo(app.user)); // TODO app.pages.add( pages.Region(app.user) ); app.pages.add( pages.Review(app.user) ); app.pages.add( pages.CompanyRegion(app.user) ); app.pages.add( pages.Bill(app.user) ); app.pages.add( pages.Complete(app.user) ); app.pages.add( pages.CreditInfo(app.user) ); var s = new AppView({model: app.pages, el: '.container', user: app.user}); }); |
TODO
- 模型的拷贝
- route
- 模块化 requirejs