MVC设计模式要点(二)MVC思想初体验

112 阅读8分钟

project from jirengu.com fangyinghang Frontend class


目录:

第一部分 MVC框架初步搭建

第二部分 引入类、继承优化代码

第三部分 MVC转向Vue

最小知识原则(基于模块化)的缺点
页面一开始是空白的,没内容没样式,解决方法如下:

  • index.html加菊花图 (<img>),main.js加载完就删掉菊花(img.remove())
  • index.html加骨架图 (<img>)
  • 用SSR技术(服务器端渲染技术)

模块功能是怎么使用的

  • 写index.html时,只需要写一个空的div,再引入一个main.js(<script src="main.js"></script>)\
  • 在main.js里面,引入css(import "./reset.css" 和import "./global.css") ; 依次引入app1.js一直到app4.js,不用关系这4个模块做什么

第一部分 MVC框架初步搭建

把上一章的html也与对应app合并在一起:

import './app1.css';
import $ from 'jquery';

const html =`
<section id="app1">
        <div class="output">
            <span id="number">100</span>
        </div>
        <div class="actions">
            <button id="add1">+1</button>
            <button id="minus1">-1</button>
            <button id="mul2">*2</button>
            <button id="divide2">÷2</button>
        </div>
</section>
`
const $element = $(html).appendTo($('body>.page'))  //把上述代码块添加回index.html里面
import './app2.css';
import $ from 'jquery';

const html =`
<section id="app2">
        <ol class="tab-bar">
            <li>1</li>
            <li>2</li>
        </ol>
        <ol class="tab-content">
            <li>内容1</li>
            <li>内容2</li>
        </ol>
</section>
`
const $element = $(html).appendTo($('body>.page'))  //把上述代码块添加回index.html里面
import $ from 'jquery';
import './app3.css'

const html =`
<section id="app3">
        <div class="square"></div>
</section>
`
const $element = $(html).appendTo($('body>.page'))  //把上述代码块添加回index.html里面
import $ from 'jquery';
import './app4.css';

const html =`
<section id="app4">
        <div class="circle"></div>
</section>
`
const $element = $(html).appendTo($('body>.page'))  //把上述代码块添加回index.html里面
  1. MVC思想整理重复代码:把app1.js里面的内容,按照绑定事件和渲染数据的两个方向,按照MVC原则把所有跟数据相关的都放到M中,所有跟视图相关的都放到V中,其他的放在C中
import './app1.css';
import $ from 'jquery';

//初始化 html
const html=`
<section id="app1">
        <div class="output">
            <span id="number">100</span>
        </div>
        <div class="actions">
            <button id="add1">+1</button>
            <button id="minus1">-1</button>
            <button id="mul2">*2</button>
            <button id="divide2">÷2</button>
        </div>
</section>
`
const $element = $(html).appendTo($('body>.page'))  //把上述代码块添加回index.html里面

//寻找重要的元素
const $button1 = $('#add1');
const $button2 = $('#minus1');
const $button3 = $('#mul2');
const $button4 = $('#divide2');
const $number = $('#number');
//初始化数据
const n = localStorage.getItem('n')
//将数据渲染到页面
$number.text(n || 100)
//绑定鼠标事件
$button1.on('click',() => {
    let n = parseInt($number.text());
    n += 1;
    localStorage.setItem('n',n)
    $number.text(n);
});
$button2.on('click',() => {
    let n = parseInt($number.text());
    n -= 1;
    localStorage.setItem('n',n)
    $number.text(n);
});
$button3.on('click',() => {
    let n = parseInt($number.text());
    n *= 2;
    localStorage.setItem('n',n)
    $number.text(n);
});
$button4.on('click',() => {
    let n = parseInt($number.text());
    n /= 2;
    localStorage.setItem('n',n)
    $number.text(n);
});
  • 先按MVC初步把代码整理成如下格式,但运行时,没有绑定事件的响应,
import './app1.css';
import $ from 'jquery';

//数据相关M
const m ={
//初始化数据
    data: {
        n: localStorage.getItem('n')
    }

}
//视图相关V
const v ={
    html:`
<section id="app1">
        <div class="output">
            <span id="number">100</span>
        </div>
        <div class="actions">
            <button id="add1">+1</button>
            <button id="minus1">-1</button>
            <button id="mul2">*2</button>
            <button id="divide2">÷2</button>
        </div>
</section>
`,

    render(){
      const $element = $(v.html).appendTo($('body>.page'))  //把上述代码块添加回index.html里面
    }, 
    update(){
        //将数据渲染到页面
        c.ui.number.text(m.data.n || 100)
    },
}
//其他C(用户看不见的)
const c = {
    ui: {
        //寻找重要的元素,目的是绑定事件,为用户看不见的,放在C中
        button1: $('#add1'),
        button2: $('#minus1'),
        button3: $('#mul2'),
        button4: $('#divide2'),
        number: $('#number')
        
    },
    //绑定鼠标事件
    bindEvents(){
        c.ui.button1.on('click',() => {
            let n = parseInt(c.ui.number.text());
            n += 1;
            localStorage.setItem('n',n)
            $number.text(n);
        });
        c.ui.button2.on('click',() => {
            let n = parseInt(c.ui.number.text());
            n -= 1;
            localStorage.setItem('n',n)
            $number.text(n);
        });
        c.ui.button3.on('click',() => {
            let n = parseInt(c.ui.number.text());
            n *= 2;
            localStorage.setItem('n',n)
            $number.text(n);
        });
        c.ui.button4.on('click',() => {
            let n = parseInt(c.ui.number.text());
            n /= 2;
            localStorage.setItem('n',n)
            $number.text(n);
        })
    }
}

//第一次渲染(初始化 html)
v.render()
  • 解决绑定事件等其他问题

分析点击没有反应的原因:

console.log("$('#add1')")
debugger
console.log($('#add1'))
bindEvents(){
        console.log('bindevents 执行了')
        console.log(c.ui.button1)
  • 控制台执行结果:
bindevents 执行了
app1.js:50 jQuery.fn.init {}
  • 可能是button初始化在render之前,写一个初始化方法,不是一开始把ui声明好,而是在调用初始化init的时候,再去取ui的各个属性,这样可以保证此时取的是同步的。
const c = {
    init(){
    c.ui = {
        //寻找重要的元素,目的是绑定事件,为用户看不见的,放在C中
        button1: $('#add1'),
        button2: $('#minus1'),
        button3: $('#mul2'),
        button4: $('#divide2'),
        number: $('#number')
        
    },
    c.bindEvents()
    
    
    
v.render()   //先渲染
c.init()     //再初始化,然后绑定事件
  • 1)接着把初始<span>标签里的100,改为一个占位符,2)再把render()渲染时的内容与这个占位符关联,也就是把这个空占位符,替换为m.data.n,MVC中的数据M中的,3)绑定事件里的内容也对应改一下
const m ={
//初始化数据
    data: {
        n: localStorage.getItem('n')
    }

}

<div class="output">
   <span id="number">{{n}}</span>    //1)
</div>

render(){
  const $element = $(v.html.replace('{{n}}',m.data.n))     //2)
   .appendTo($('body>.page'))  //把上述代码块添加回index.html里面
}, 

bindEvents(){
    c.ui.button1.on('click',() => {
     let n = m.data.n
     n += 1;
     localStorage.setItem('n',n)
     c.ui.number.text(n);   //原来是$number,无法引用number
});
  • 继续优化一下数据获取,删掉updata
//数据相关M
const m ={
    data: {
        //n: localStorage.getItem('n') 只能保存字符串
        n: parseInt(localStorage.getItem('n'))
        //由原来的字符串变成一个整数
    }
}

const v ={
    el:null,
    
render(){
      if(v.el === null) {
        v.el = $(v.html.replace('{{n}}',m.data.n))
        .appendTo($('body>.page'))
      }else{
        const newEl = $(v.html.replace('{{n}}',m.data.n))
        v.el.replaceWith(newEl)
        v.el = newEl
      } 
}, 
  • 以上代码初步实现点击页面的任何一点,就整个刷新自己,但是绑定事件失效了,因为之前是绑定的原button,而现在整个页面刷新后,已经变动了。
  1. 用最小知识原则,把render()放在初始化里面,让init接收一个参数,渲染到页面中的哪一块,在render()渲染时候,把container传给视图,与其相关的代码更新如下:
const c = {
    init(container){
    v.render(container)

render(container){
      if(v.el === null) {
        v.el = $(v.html.replace('{{n}}',m.data.n))
        .appendTo($(container))
  1. 在初始化c的时候,要得到一个东西,而app1不知道外面有什么div,此时把c.init()替换为export default c,app1需要别人传一个参数,才能运行
<body>
    <div class="page">
        <section id="app1"></section>
    </div>
    <script src="main.js"></script>
</body>
import x from "./app1.js";

x.init('#app1') //把页面中的app1传给这个模块,让其初始化
const v ={
    el:null,
    html:`
<div>        //改动的
        <div class="output">
            <span id="number">{{n}}</span>
        </div>
        <div class="actions">
            <button id="add1">+1</button>
            <button id="minus1">-1</button>
            <button id="mul2">*2</button>
            <button id="divide2">÷2</button>
        </div>
</div>       //改动的
`,
  1. 首先做到这个container是不可变的,每次刷新时,section不变,点击事件时更整个里面的div,而绑定事件在section上,实现的代码如下:
import './app1.css';
import $ from 'jquery';

//数据相关M
const m ={
//初始化数据
    data: {
        //n: localStorage.getItem('n') 只能保存字符串
        n: parseInt(localStorage.getItem('n'))
        //由原来的字符串变成一个整数
    }

}
//视图相关V
const v ={
    el:null,
    html:`
<div>
        <div class="output">
            <span id="number">{{n}}</span>
        </div>
        <div class="actions">
            <button id="add1">+1</button>
            <button id="minus1">-1</button>
            <button id="mul2">*2</button>
            <button id="divide2">÷2</button>
        </div>
</div>
`,
    init(container){
        v.container = $(container);
        v.render()
    },
    render(){
      if(v.el === null) {
        v.el = $(v.html.replace('{{n}}',m.data.n))
        .appendTo($(v.container))  //存储container
      }else{
        const newEl = $(v.html.replace('{{n}}',m.data.n))
        v.el.replaceWith(newEl)
        v.el = newEl
      } 
    }, 
    
}
//其他C(用户看不见的)
const c = {
    init(container){
    v.init(container)
    c.bindEvents()
    },
    //绑定鼠标事件
    bindEvents(){
        v.container.on('click','#add1',()=>{
            m.data.n += 1;
            v.render()
        }),   
        v.container.on('click','#minus1',()=>{
            m.data.n -= 1;
            v.render()
        }),
        v.container.on('click','#mul2',()=>{
            m.data.n *= 2;
            v.render()
        }),
        v.container.on('click','#divide2',()=>{
            m.data.n /= 2;
            v.render()
        })
    }
}
export default c
  1. 抽象为MVC经典结构,代码简化为:
import './app1.css';
import $ from 'jquery';

//数据相关M
const m ={
 data: {}
}

//视图相关V
const v ={
 el:null,
 container:null,
 html:`...`init(container){},
 render(){}
}

//其他C(用户看不见的)
const c = {
  init(container){},
  bindEvents(){}
}

export default c
  1. 再次抽象为view=render(data),视图即为渲染数据,

21212.png

  • 按照上图视图即为渲染数据思路,代码更新如下:
import './app1.css';
import $ from 'jquery';

//数据相关M
const m = {
    data: {
        n:parseInt(localStorage.getItem('n'))
    }
}
//视图相关V
const v ={
    el:null,  //el即为原来的container
    html:`
<div>
        <div class="output">
            <span id="number">{{n}}</span>
        </div>
        <div class="actions">
            <button id="add1">+1</button>
            <button id="minus1">-1</button>
            <button id="mul2">*2</button>
            <button id="divide2"2</button>
        </div>
</div>
    `,
    init(container){
        v.el = $(container);
    },
    render(n){
      if(v.el.children.length !== 0) v.el.empty()
        $(v.html.replace('{{n}}', n))
        .appendTo(v.el)  
    }
    
}
//其他C(用户看不见的)
const c = {
    init(container){
    v.init(container)
    v.render(m.data.n)   //第一次view = render(data)
    c.bindEvents()
    },
    //绑定鼠标事件
    bindEvents(){
        v.el.on('click','#add1',()=>{
            m.data.n += 1;
            v.render(m.data.n)
        }),   
        v.el.on('click','#minus1',()=>{
            m.data.n -= 1;
            v.render(m.data.n)
        }),
        v.el.on('click','#mul2',()=>{
            m.data.n *= 2;
            v.render(m.data.n)
        }),
        v.el.on('click','#divide2',()=>{
            m.data.n /= 2;
            v.render(m.data.n)
        })
    }
}
export default c
  • 去掉重复的代码:
import './app1.css';
import $ from 'jquery';

//数据相关M
const m = {
    data: {
        n:parseInt(localStorage.getItem('n'))
    }
}
//视图相关V
const v ={
    el:null,  //el即为原来的container
    html:`
<div>
        <div class="output">
            <span id="number">{{n}}</span>
        </div>
        <div class="actions">
            <button id="add1">+1</button>
            <button id="minus1">-1</button>
            <button id="mul2">*2</button>
            <button id="divide2">÷2</button>
        </div>
</div>
    `,
    init(container){
        v.el = $(container);
    },
    render(n){
      if(v.el.children.length !== 0) v.el.empty()
        $(v.html.replace('{{n}}', n))
        .appendTo(v.el)  
    }
    
}
//其他C(用户看不见的)
const c = {
    init(container){
    v.init(container)
    v.render(m.data.n)   //第一次view = render(data)
    c.autoBindEvents()
    },
    events:{
        'click #add1':'add',
        'click #minus1':'minus',
        'click #mul2':'mul',
        'click #divide2':'div',
    },
    add(){
        m.data.n += 1;
    },
    minus(){
        m.data.n -= 1;
    },
    mul(){
        m.data.n *= 2;
    },
    div(){
        m.data.n /= 2;
    },
    autoBindEvents(){
        for(let key in c.events){
          const value = c[c.events[key]]
          const spaceIndex = key.indexOf(' ')
          const part1 = key.slice(0,spaceIndex)
          const part2 = key.slice(spaceIndex + 1)
          console.log(part1,',',part2)
          v.el.on(part1,part2,value)
        }
    },
}
export default c
  • 使用表驱动编程eventBus对象间通信:
import './app1.css';
import $ from 'jquery';

const eventBus = $({})  //引入一个空对象,主要是用它的on监听鼠标事件方法和trigger触发事件方法
console.log(eventBus);
//数据相关M
const m = {
    data: {
        n:parseInt(localStorage.getItem('n'))
    },
    create(){},
    delete(){},
    update(data){
        Object.assign(m.data,data)  //把data的所有属性赋值给m.data
        eventBus.trigger('m:updated') //触发更新,别人去监听
        localStorage.setItem('n',m.data.n)  //每次刷新页面都保存更新后的值
    },
    get(){}
}
//视图相关V
const v ={
    el:null,  //el即为原来的container
    html:`
<div>
        <div class="output">
            <span id="number">{{n}}</span>
        </div>
        <div class="actions">
            <button id="add1">+1</button>
            <button id="minus1">-1</button>
            <button id="mul2">*2</button>
            <button id="divide2">÷2</button>
        </div>
</div>
    `,
    init(container){
        v.el = $(container);
    },
    render(n){
      if(v.el.children.length !== 0) v.el.empty()
        $(v.html.replace('{{n}}', n))
        .appendTo(v.el)  
    }
    
}
//其他C(用户看不见的)
const c = {
    init(container){
    v.init(container)
    v.render(m.data.n)   //第一次view = render(data)
    c.autoBindEvents()
    eventBus.on('m:updated',()=>{
        v.render(m.data.n)
    })
    },
    events:{
        'click #add1':'add',
        'click #minus1':'minus',
        'click #mul2':'mul',
        'click #divide2':'div',
    },
    add(){
        m.update({n: m.data.n + 1})
    },
    minus(){
        m.update({n: m.data.n - 1})
    },
    mul(){
        m.update({n: m.data.n * 2})
    },
    div(){
        m.update({n: m.data.n / 2})
    },
    autoBindEvents(){
        for(let key in c.events){
          const value = c[c.events[key]]
          const spaceIndex = key.indexOf(' ')
          const part1 = key.slice(0,spaceIndex)
          const part2 = key.slice(spaceIndex + 1)
          v.el.on(part1,part2,value)
        }
    },
}
export default c

以上代码完成对模块1的全部MVC优化

第二部分 引入类、继承优化代码

因为多次重构,代码已经乱套了,就不附代码了,把整个思考演进的过程做一个记录

  • 基于事不过三的原则,有一个触发简化代码的点,那就是同样代码写3遍,可以抽象为一个函数;同样属性写3遍,抽象为共用属性(原型或类),同样原型写3遍,用继承。

  • 本例app1和app2中,MVC的模块化以后,每个模块中还是发现有共用的属性,也就是可以抽象为共有属性,也就是原型

2.1 引入类:新建一个base文件夹,存放共用属性,分别建Model.jsView.js

class Model{
    constructor(options){
        this.data = options.data     //data是在传参数时,赋值到对象本身上,而不是赋值到原型链里,下面的4个函数都是在原型里
    }
    create(){}
    delete(){}    
    update(){}
    get(){}
}

export default Model
import Model from './base/Model.js'

const m = new Model ({
    data:{n:parseInt(localStorage.getItem('n'))}
})
m.update = (data)=>{
    Object.assign(m.data, data)
    eventBus.trigger('m:updated')
    localStorage.setItem('n', m.data.n)
}

console.dir(m)得到的控制台信息为:\

15464.png

  • 上面的信息中,Model是m的构造函数,也就是其所属的类,本身有个data属性,n值为205,update是他自己的函数,原型中有constructorcreatedeleteget,即为类声明的五个方法。所以在调用update时,首先看自身有没有,若自身没有,就去原型调用

2.2 简化Model代码:

class Model {
  constructor(options){
    ['data','update','create','delete','get'].forEach(key => {
        if (key in options){
            this[key] = options[key]
        }
    });
    // this.data = options.data
    // this.update = options.update
    // this.create = options.create
    // this.delete = options.delete
    // this.get = options.get
  }
}

export default Model

2.3 简化View代码:

import $ from 'jquery'
class View {
  constructor({el,html,render}){
    this.el = $(el)
    this.html = $(html)
    this.render = $(render)
}

export default View

2.4 发现View和Control有很多交叉的地方,说明v和c有非常重要的联系,所以考虑合并VC,也就是把v和c共用一个对象,让c就是v,v就是c。

2.5 合并以后的vc到底怎么命名呢?Vue.js框架认为:这是一个前端的库,主要处理的是视图,所以这个核心的库应该叫做View,重构一下所有c为v

2.6 新建EventBus的类,让M和V也继承EventBus,也就是让EventBus放成ModelView的父类,底层逻辑其实是EventBus就是EventTarget,所有的dom元素的类的类的类...倒数第2层都是EventTarget

import $ from 'jquery'
class EventBus {
    constructor(){
    this._eventBus = $(window)
    }
    on(eventName,fn){
     return this._eventBus.on(eventName,fn)
    }
    trigger(eventName,data){
     return this._eventBus.trigger(eventName,data)
    }
    off(eventName,fn){
     return this._eventBus.off(eventName,data) 
    }
}

export default EventBus

2.7 引入继承:如何实现EventBus作为M和V的父类呢?如果你继承EventBus的类,那么必须在初始化constructor里面去调用父类的初始化,注意不加分号报错情况,

import EventBus from './EventBus'
class Model extends EventBus{
    constructor(option) {
    super()       //调用 EventBus#constructor()
    const keys = ['data','update','create','delete','get']
    keys.forEach((key) => {
        if (key in options){
            this[key] = options[key]
        }
    }
}

第三部分 MVC转向Vue

  • (MVC简化代码的极限就是Vue)

用Vue来写app1.js,终端中安装Vue,yarn add vue模块用不同的技术,互相不影响。

  • 因为Vue有两个版本,默认是不完整版,parcel如何引用Vue完整版,在package.json添加如下代码,并重新运行parcel
import Vue from 'vue'
'alias':{
    "Vue" :"./node_modules/vue/dist/vue.common.js
}
  • Vue 认为Model模块也没必要了,所以就有
new Vue({
    el:el;
    data:{n:parseFloat(localStorage.getItem('n'))}
})
  • Vue 如何完成事件绑定,把运算的函数都放在methods里面,下面代码用Vue写完以后,MVC和jquery就都没有用了,Vue就是MVC简化代码的极限了,如何保存数据到本地呢,用监听事件watch
const init = (el) => {
  new Vue({
    el: el;
    data:{n:parseFloat(localStorage.getItem('n'))},
    methods:{
      add(){
        this.n += 1     //Vue封装了this.data.n简化为this.n
      },
      minus(){
        this.n -= 1
      },
      mul(){
        this.n *= 2
      },
      div(){
        this.n /= 2
      },
    }
    watch:{
        n:function(){    //本质是当n变化时,执行一个函数就把n存起来
         localStorage.setItem('n', this.n)  //用this就不能用剪头函数,因为箭头函数里面的this是window
        }
    }
    template: `     //template就是以前的 html
  <section>
    <div class="output">
      <span id="number">{{n}}</span>
    </div>
    <div class="actions">
      <button @click="add">+1</button>
      //原来的代码为:<button id="add1">+1</button>
      <button @click="minus">-1</button>
      <button @click="mul">*2</button>
      <button @click="div"2</button>
    </div>
  </section>
`
  })
}

第四部分 MVC总结

  • MVC设计模式的思想,是从烂代码向框架式代码的过渡:
  1. 模块化实现最小知识原则:不同功能的实现分开写,各担其责,互不影响
  1. MVC应用于需求复杂的场景,具有普遍性,即代码都可以用M+V+C的思想来写,Vue局限只能满足最基础的需求
  1. 哈希表驱动编程
  1. 事不过三原则
  1. view = render() ,这是React的思维;在Vue中没有体现,而是监听了n的变化再保存到本地