文章目录
  1. 1. 模型(Model)
  2. 2. 视图
  3. 3. 控制器
  4. 4. 程序入口
  5. 5. TODO

什么是前端, Html + Javascript

html javascript 混杂在一起,开起来是一个大杂烩

什么是好的代码,应当把你的应用解藕成一系列相互平等且独立的页面

使用类、继承、对象和设计模式

MVC:数据(模型) 展示层(视图) 用户交互层(控制器)

  1. 用户和应用产生交互
  2. 控制器的事件处理器被触发
  3. 控制器从模型中请求数据,并将其交给视图
  4. 视图将数据呈现给用户

模型(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

  1. 模型的拷贝
  2. route
  3. 模块化 requirejs
文章目录
  1. 1. 模型(Model)
  2. 2. 视图
  3. 3. 控制器
  4. 4. 程序入口
  5. 5. TODO