design-pattern

MVC

mvc+mvp+mvvp DEMO例子

Java 版本

Java servlet MVC 版本,这里采用最基本的原理来测试,Spring或Struts道理类似

  // Model即业务实体对象,可以增添少许方法来处理数据
  // src/mvc/Model.java
  public class Model {
      private String name;
      private Integer num;
      public Model() {
      }
      public Model(String name, Integer num) {
          this.name = name;
          this.num = num;
      }
      public String getName() {
          return this.name;
      }
      public Integer getNum() {
          return this.num;
      }
  }

  // Processing bussiness logic,业务逻辑处理,组织与处理Model,此类可选
  // src/mvc/ModelService.java
  import java.util.Optional;
  public class ModelService {
      Model model;
      public Optional<Model> getModel() {
          if (this.model == null) {
              this.model = new Model("Thomas", 10);
          }
          // do something
          return Optional.of(this.model);
      }
  }

  // Controller,负责控制转发和模板映射。采用WebServlet注解,也可通过web.xml配置
  // src/mvc/Controller.java
  import javax.servlet.RequestDispatcher;
  import javax.servlet.ServletException;
  import javax.servlet.annotation.WebServlet;
  import javax.servlet.http.HttpServlet;
  import javax.servlet.http.HttpServletRequest;
  import javax.servlet.http.HttpServletResponse;
  import java.io.IOException;

  @WebServlet(
          name = "ModelServlet",
          urlPatterns = "/hello-model")
  public class Controller extends HttpServlet {
      private ModelService modelService = new ModelService();
      private void processRequest(
          HttpServletRequest request, HttpServletResponse response)
          throws ServletException, IOException {
          modelService.getModel().ifPresent(s -> {
              request.setAttribute("helloModel", s);
          });
          RequestDispatcher dispatcher = request.getRequestDispatcher(
                  "/WEB-INF/template/model.jsp");
          dispatcher.forward(request, response);
      }

      @Override
      protected void doGet(
              HttpServletRequest request, HttpServletResponse response)
              throws ServletException, IOException {
          processRequest(request, response);
      }

      @Override
      protected void doPost(
              HttpServletRequest request, HttpServletResponse response)
              throws ServletException, IOException {
          processRequest(request, response);
      }
  }

  // /WEB-INF/template/model.jsp
  // View,使用JSP
  <%@ page import="mvc.Model" %>
  <html>
  <head>
    <title>Hello Model</title>
  </head>
  <body>
  <h1>Hello Model</h1>
  <%
    if (request.getAttribute("helloModel") != null) {
        System.out.print(new Model());
        Model model = (Model) request.getAttribute("helloModel");
  %>
  <div>Name: <%= model.getName()%></div>
  <div>Num: <%= model.getNum()%></div>
  <% } %>
  </body>
  </html>
  // Java MVC版结束

JS 版本

1. MVC Basic JS版: 最基本版MVC,与后端MVC类似, M与C分离。事件绑定和数据更新均通过Controller来设置和转发。

  !(function(win, doc) {
    // View
    class View {
      constructor($el, tpl) {
        this.$el = $el
        this.tpl = tpl
      }
      render(data) {
        this.$el.innerHTML = this.compile(data)
      }
      compile(data) {
        return this.tpl || `<div><b>${data.name}</b>
         <em>${data.num}</em></div>`
      }
    }
    // Model
    class Model {
      constructor() {
        this.data = { name: 'MVC Basic', num: 1 }
      }
      update(value, cb) {
        const fn = (res) => {
          this.data.num += value
          cb(this.data)
        }
        fetch('./').then(fn).catch(fn)
      }
      change(name, cb) {
        this.data.name = name
        cb(this.data)
      }
    }
    // Controller
    class Controller {
      constructor(view, model) {
        this.view = view
        this.model = model
      }
      init() {
        this.view.$el.addEventListener('click', evt => {
          this.model.data.name = 'Click to add:Event dispatch.'
          this.model.update(1, data => this.view.render(data))
        })
        this.view.$el.addEventListener('mouseout', evt => {
          this.model.change('Click to add', data => this.view.render(data))
        })
        this.view.render(this.model.data)
      }
    }
    const model = new Model()
    const view = new View(doc.querySelector('#mvc-basic-view'))
    const controller = new Controller(view, model)
    controller.init()
  })(window, document)

2. MVC:前端MVC,与后端MVC并不太一样,View与Model是绑定的。在View中监测Model的行为通知来更新视图,Model更新则通过Controller注册转发

  !(function(win, doc) {
    class Observer {
      constructor() {
        this.observers = []
      }
      register(callback) {
        this.observers.push(callback)
      }
      notify(args) {
        for (let i = 0, l = this.observers.length; i < l; i++) {
          this.observers[i](args)
        }
      }
    }
    // View
    class View {
      constructor($el, model, tpl) {
        this.$el = $el
        this.model = model
        this.tpl = tpl
        this.onAdd = new Observer(this)
        this.onChange = new Observer(this)
        this.model.onUpdate.register(value => this.render())
        this.model.onChange.register(name => this.render())
        this.$el.addEventListener('click', evt => this.onAdd.notify(1))
        this.$el.addEventListener('mouseout', evt => this.onChange.notify('Click to add'))
      }
      render() {
        this.$el.innerHTML = this.compile(this.model.data)
      }
      compile(data) {
        return this.tpl || `<div><b>${data.name}</b>
         <em>${data.num}</em></div>`
      }
    }
    // Model
    class Model {
      constructor() {
        this.data = { name: 'MVC', num: 1 }
        this.onUpdate = new Observer(this)
        this.onChange = new Observer(this)
      }
      update(value) {
        const fn = (res) => {
          this.data.num += value
          this.onUpdate.notify(value)
        }
        fetch('./').then(fn).catch(fn)
      }
      change(name) {
        this.data.name = name
        this.onChange.notify(name)
      }
    }
    // Controller
    class Controller {
      constructor(view, model) {
        this.view = view
        this.model = model
      }
      init() {
        this.view.onAdd.register(value => {
          this.model.data.name = 'Click to add:Event dispatch.'
          this.model.update(value)
        })
        this.view.onChange.register(name => this.model.change(name))
        this.view.render()
      }
    }
    const model = new Model()
    const view = new View(doc.querySelector('#mvc-view'), model)
    const controller = new Controller(view, model)
    controller.init()
  })(window, document)

3. MVP:Contorller升级为Presenter,将View与Model分离。通过Presenter来绑定View事件和代理双方行为

  !(function(win, doc) {
    // View
    class View {
      constructor($el, tpl) {
        this.$el = $el
        this.tpl = tpl
      }
      render(data) {
        this.$el.innerHTML = this.compile(data)
      }
      compile(data) {
        return this.tpl || `<div><b>${data.name}</b>
         <em>${data.num}</em></div>`
      }
    }
    // Model
    class Model {
      constructor() {
        this.data = { name: 'MVP', num: 1 }
      }
      update(value) {
        const fn = (res) => {
          this.data.num += value
          return res
        }
        return fetch('./').then(fn).catch(fn)
      }
      change(name) {
        this.data.name = name
      }
    }
    // Presenter
    class Presenter {
      constructor(view, model) {
        this.view = view
        this.model = model
        view.$el.addEventListener('click', evt => this.add(1))
        view.$el.addEventListener('mouseout', evt => this.change('Click to add'))
      }
      init() {
        this.render(this.model.data)
      }
      render(data) {
        this.view.render(data)
      }
      add(value) {
        this.model.data.name = 'Click to add:Event proxy.'
        this.model.update(value).then(res => {
          this.render(this.model.data)
        })
      }
      change(name) {
        this.model.change(name)
        this.render(this.model.data)
      }
    }
    const view = new View(doc.querySelector('#mvp-view'))
    const presenter = new Presenter(view, new Model())
    presenter.init()
  })(window, document)

4. MVP+Observer: 增加Observer,View与Model分离。事件由View绑定,Presenter增加对应函数,代理View与Model全部行为

  !(function(win, doc) {
    class Observer {
      constructor() {
        this.observers = []
      }
      register(callback) {
        this.observers.push(callback)
      }
      notify(args) {
        for (let i = 0, l = this.observers.length; i < l; i++) {
          this.observers[i](args)
        }
      }
    }
    // View
    class View {
      constructor($el, tpl) {
        this.$el = $el
        this.tpl = tpl
        this.onAdd = new Observer(this)
        this.onChange = new Observer(this)
        this.$el.addEventListener('click', evt => this.onAdd.notify(1) )
        this.$el.addEventListener('mouseout', evt => this.onChange.notify('Click to add'))
      }
      render(data) {
        this.$el.innerHTML = this.compile(data)
      }
      compile(data) {
        return this.tpl || `<div><b>${data.name}</b>
         <em>${data.num}</em></div>`
      }
    }
    // Model
    class Model {
      constructor() {
        this.data = { name: 'MVP+Observer', num: 1 }
        this.onUpdate = new Observer(this)
        this.onChange = new Observer(this)
      }
      update(value) {
        const fn = (res) => {
          this.data.num += value
          this.onUpdate.notify(value)
          return res
        }
        return fetch('./').then(fn).catch(fn)
      }
      change(name) {
        this.data.name = name
        this.onChange.notify(name)
      }
    }
    // Presenter
    class Presenter {
      constructor(view, model) {
        this.view = view
        this.model = model
        this.view.onAdd.register(value => this.add(value))
        this.view.onChange.register(name => this.change(name))
        this.model.onUpdate.register(res => this.render())
        this.model.onChange.register(res => this.render())
      }
      init() {
        this.render()
      }
      render() {
        this.view.render(this.model.data)
      }
      add(value) {
        this.model.data.name = 'Click to add:Event proxy.'
        this.model.update(value)
      }
      change(name) {
        this.model.change(name)
      }
    }
    const $el = doc.querySelector('#mvp-observer-view')
    const presenter = new Presenter(new View($el), new Model())
    presenter.init()
  })(window, document)

5. MVP+Dispatch: 轻量级的MVP,与最基本MVC也类似,将View与Model分离,通过Presenter注册Model行为,但不增加代理函数,直接将View与Model绑定

  !(function(win, doc) {
    class Observer {
      constructor() {
        this.observers = []
      }
      register(callback) {
        this.observers.push(callback)
      }
      notify(args) {
        for (let i = 0, l = this.observers.length; i < l; i++) {
          this.observers[i](args)
        }
      }
    }
    // View
    class View {
      constructor($el, tpl) {
        this.$el = $el
        this.tpl = tpl
        this.onAdd = new Observer(this)
        this.onChange = new Observer(this)
      }
      render(data) {
        this.$el.innerHTML = this.compile(data)
      }
      compile(data) {
        return this.tpl || `<div><b>${data.name}</b>
         <em>${data.num}</em></div>`
      }
    }
    // Model
    class Model {
      constructor() {
        this.data = { name: 'MVP+Dispatch', num: 1 }
        this.onUpdate = new Observer(this)
        this.onChange = new Observer(this)
      }
      update(value) {
        const fn = (res) => {
          this.data.num += value
          this.onUpdate.notify(value)
        }
        fetch('./').then(fn).catch(fn)
      }
      change(name) {
        this.data.name = name
        this.onChange.notify(name)
      }
    }
    // Controller
    class Controller {
      constructor(view, model) {
        this.view = view
        this.model = model
      }
      init() {
        this.view.$el.addEventListener('click', evt => this.view.onAdd.notify(1))
        this.view.$el.addEventListener('mouseout', evt => this.view.onChange.notify('Click to add'))
        this.view.onAdd.register(value => {
          this.model.data.name = 'Click to add:Event dispatch.'
          this.model.update(value)
        })
        this.view.onChange.register(name => this.model.change(name))
        this.model.onUpdate.register(value => this.view.render(this.model.data))
        this.model.onChange.register(name => this.view.render(this.model.data))
        this.view.render(this.model.data)
      }
    }
    const model = new Model()
    const view = new View(doc.querySelector('#mvp-dispatch-view'))
    const controller = new Controller(view, model)
    controller.init()
  })

6. 前端MVVM模式,通过观察者模式将数据与视图进行双向绑定,然后由数据驱动更改

  !(function(win, doc) {
    class Agent {
      constructor(target, watch) {
        const handle = {
          set(obj, prop, value) {
           if (obj[prop] !== value && obj.hasOwnProperty(prop)) {
             obj[prop] = value
             watch && watch(obj, prop, value)
           }
           return Reflect.set(obj, prop, value)
          },
          get(obj, prop) {
            if (typeof obj[prop] === 'object' && obj[prop] !== null) {
              return new Proxy(obj[prop], handle)
            } else {
              return Reflect.get(obj, prop)
            }
          }
        }
        return new Proxy(target, handle)
      }
    }
    // View
    class View {
      constructor($el, tpl) {
        this.$el = $el
      }
      compile(data) {
        return this.tpl || `
         <div><b>${data.name}</b>
         <em>${data.num}</em></div>`
      }
      render(data) {
        this.$el.innerHTML = this.compile(data)
      }
    }
    // Model
    class Model {
      constructor() {
        this.data = { name: 'MVVM', num: 1 }
      }
      update(data) {
        const fn = (res) => {
          data.num += 1
          return data
        }
        return fetch('./').then(fn).catch(fn)
      }
    }
    // Controller
    class ViewModel {
      constructor({ $el, view, model }) {
        this.view = view
        this.data = new Agent(model.data, (obj, prop, value) => {
          this.view.render(this.data)
        })
        this.model = model
        this.init()
      }
      init() {
        console.log('render:', this.data)
        this.view.$el.addEventListener('click', (evt) => this.add(evt))
        this.view.$el.addEventListener('mouseout', (evt) => this.change(evt))
        this.view.render(this.data)
      }
      change(evt) {
        this.data.name = 'Click to add'
      }
      add(evt) {
        this.data.name = 'Click to add:Watch data.'
        this.model.update(this.data)
      }
    }
    const viewModel = new ViewModel({
      view: new View(doc.querySelector('#mvvm-view')), 
      model: new Model()
    })
  })(window, document)

7. 前端MVVM最简版框架模式,增加事件标记提取绑定和Watch行为变化,通过观察者模式将数据与视图进行双向绑定,然后由数据驱动更改

  !(function(win, doc) {
    class Observer {
      constructor(vm) {
        this.vm = vm
        this.observers = []
      }
      register(callback) {
        this.observers.push(callback)
      }
      notify(args) {
        for (let i = 0, l = this.observers.length; i < l; i++) {
          this.observers[i](args)
        }
      }
    }
    class Watcher {
      constructor(vm, cb) {
        this.vm = vm
        this.cb = cb
        return this.run.bind(this)
      }
      run(obj, prop, value) {
        console.log('watch:', arguments)
        if (this.vm.watch[prop]) {
          (this.vm.watch[prop]).call(this.vm.data, value, this.vm)
        }
        this.cb.call(this.vm, obj, prop, value, this)
      }
    }
    class Agent {
      constructor(target, observer) {
        const handle = {
          set(obj, prop, value) {
           if (obj[prop] !== value && obj.hasOwnProperty(prop)) {
             obj[prop] = value
             observer && observer.notify(obj, prop, value)
           }
           return Reflect.set(obj, prop, value)
          },
          get(obj, prop) {
            if (typeof obj[prop] === 'object' && obj[prop] !== null) {
              return new Proxy(obj[prop], handle)
            } else {
              return Reflect.get(obj, prop)
            }
          }
        }
        return new Proxy(target, handle)
      }
    }
    function template(strings, ...keys) {
      return function(...values) {
        const dict = values[values.length - 1] || {}
        const result = [strings[0]]
        keys.forEach((key, i) => {
          let value = dict[key]
          result.push(value, strings[i + 1])
        })
        return result.join('')
      }
    }

    // View
    class View {
      constructor($el, tpl) {
        this.$el = $el
        this.tpl = tpl
        this.events = []
      }
      compile(data) {
        const content = this.tpl(data)
        let reg = new RegExp('e-([a-z]+)\=\"([a-z0-9]+)' + '\"', 'igm')
        const result = content.match(reg) || []
        result.forEach((item) => {
          const [evt, fn] = item.split('=')
          this.events.push([evt.substr(2), fn.substr(0, fn.length - 1).substr(1)])
        })
        return content
      }
      render(data) {
        this.$el.innerHTML = this.compile(data)
      }
    }
    const Model = { name: 'Tiny MVVM Framework', num: 1 }
    // ViewModel
    class TinyMVVM {
      constructor({ $el, tpl, data, methods = {}, watch = {}, computed = {} }) {
        this.view = new View($el, tpl)
        this.observer = new Observer(this)
        this.watcher = new Watcher(this, (obj, prop, value) => {
          this.render()
        })
        this.observer.register(this.watcher)
        this.data = new Agent(data(), this.observer)
        this.methods = methods
        this.watch = watch
        this.computed = computed
        this.init()
      }
      render() {
        const data = Object.assign({}, this.data)
        for (let attr in this.computed) {
          data[attr] = (this.computed[attr]).call(this.data, this)
        }
        this.view.render(data)
      }
      init() {
        this.render()
        this.mount()
      }
      mount() {
        this.view.events.forEach((item) => {
          this.view.$el.addEventListener(item[0], (evt) => (this.methods[item[1]].bind(this))(evt))
        })
      }
    }
    new TinyMVVM({
      $el: doc.querySelector('#mvvm-watch-view'),
      tpl: template`
         <div e-click="add" e-mouseout="change"><b>${'name'}</b>
         <em>${'num'}</em> | ${'computedProp'}</div>`,
      data() {
        return Model
      },
      methods: {
        fetch(value) {
          const fn = (res) => {
            this.data.num += value
            return this.data
          }
          return fetch('./').then(fn).catch(fn)
        },
        change(evt) {
          this.data.name = 'Click to add'
        },
        add(evt) {
          this.data.name = 'Click to add:Watch data.'
          this.methods.fetch.bind(this)(1)
        }
      },
      computed: {
        computedProp: function(vm) {
          return this.name + ' computed.'
        }
      },
      watch: {
        num(value, vm) {
          this.name = 'Click to add:Watch data.'
        }
      }
    })
  })(window, document)