Lucifier129 / Lucifier129.github.io

Lucifier129.github.io
409 stars 54 forks source link

不用框架,也能用MVC模式组织代码 #2

Open Lucifier129 opened 9 years ago

Lucifier129 commented 9 years ago

用MVC模式组织代码

MVC这个名词,在前端领域还处于意义不明确的阶段。打着MV*名号的框架层出不穷,概括MVC的文章也让人应接不暇。然而究竟什么是MVC,那些作者各执一词。

winter 做了下正本清源的工作:

既然经典MVC模式与前端的view.onclick天然的互斥,不再适用;既然大家都在打扮MVC,本文也尝试重新演绎。介绍一种以ModelViewController为命名的代码组织方案。

这个方案给我带来了良好的编程体验,并且几乎立即可以应用在所有项目中,不管历史遗留的代码多么庞杂,新增的业务需求都能按照新方案进行组织。

Talk is cheap, show me the code

Github的todomvc项目,提供了各种以MV*为名号的框架的实现,帮助前端工程师选择合适的框架或库。其中的vanillajs版本,是一个无框架无库的原生JavaScript实现;它也是按照MVC的理念来组织代码。框架或库不是MVC模式的必需品,只是说有了相应的框架与库,写起来更为便利。

本文也准备了一个原生实现todos-vanillajs,可作MVC模式参考案例。

在线体验地址:http://lucifier129.github.io/todos-vanillajs/index.html

MVC模式

一个模块是一个文件夹

在一些nodejs前端模块化编程的介绍中,常常能看到一个文件就是一个模块的说法,用module.exports的值作为模块输出。暂且把这类模块叫做nodejs模块AMD/CMD模块

我们真正关注的模块,其实是业务模块,它是指与DOM相关的视图模型、数据模型与事件交互模型的总和。比如一个页面的header模块,它包含html模板填充的数据以及随着滚动高度而固定在顶部的事件交互等结构。

业务模块不等价于AMD/CMD模块!如果产生这个误解,可能将所有东西塞一个js文件。只把html模板提取出来,也不足够。

一个业务模块应该是好几个AMD/CMD模块的组合,它是一个文件夹,包含好几个AMD/CMD模块文件,其中:

并且:

controller.js的一般形式如下:

function Controller(View, Model) {
    this.View = View
    this.Model = Model
}

Controller.prototype = {
    init: function() {
        //初始化
    },
    listen: function() {
        //事件绑定
    }
}

controller.js拿到了视图模型与数据模型,也就掌握了如何处理数据、如何渲染视图的所有方法;关于数据处理与视图渲染的时机问题,则由设计事件绑定来完成。所以它是事件交互控制器

定义和调用分离

todomvc是一个功能简单的页面,因此用一个js文件夹足以。其中的view.jsmodel.js以及controller.js都只是定义了一些方法接口, app.js这个出口文件才产生了调用。

//view.js
app.View = {
        TodoList: TodoList,
        TodoElem: TodoElem,
        Counter: Counter,
        Footer: Footer,
        ToogleAll: ToogleAll,
        Filters: Filters
    }

视图只有7个信息渲染点,用6个定义了渲染方法。没有产生调用。

//model.js

    function Model(name) {
        this.name = name
        this.todos = localStorage.getItem(name)
        if (this.todos) {
            this.todos = JSON.parse(this.todos)
        } else {
            this.todos = []
        }
    }

    Model.prototype = {
        getTodo: function(id) {
            //根据id获取todo项
        },
        getAll: function() {
            //获取所有todo项
        },
        getActive: function() {
            //获取未完成的todo项
        },
        getCompleted: function() {
            //获取已完成的todo项
        },
        addTodo: function(todo) {
            //添加新的todo项
        },
        removeTodo: function(id) {
            //根据id删除todo项
        },
        save: function() {
            //保存数据
        }
    }

数据模型Model提供了所有跟数据处理相关的方法。没有产生调用。

//controller.js

    function Controller(View, Model) {
        this.View = View
        this.Model = Model
    }

    Controller.prototype.init = function() {
        //初始化
    }

    Controller.prototype.listen = function() {

        //绑定事件代理
        //每个事件中,涉及渲染的用View提供的方法,涉及数据的用Model提供的方法

        tools.$listen('change', '#new-todo', function() {
            //输入框的change事件中,添加新todo项
        })

        tools.$listen('keyup', '#new-todo', function(e) {
            if (e.keyCode === ENTER_KEY) {
                //输入框聚焦时,按回车键也添加新todo项
            }
        })

        tools.$listen('dblclick', '#todo-list label', function() {
            //todo-list里双击进行内容编辑
        })

        tools.$listen('change', '#todo-list .edit', function() {
            //内容编辑框的change事件,标志完成编辑
        })

        tools.$listen('keyup', '#todo-list .edit', function(e) {
            var keyCode = e.keyCode

            if (keyCode === ESCAPE_KEY || keyCode === ENTER_KEY) {
                //内容编辑框按回车键,或者ESC退出键,标志完成编辑
            }
        })

        tools.$listen('change', '#todo-list .toggle', function() {
            //todo-list每一项提供的checkbox,其change事件中切换[未完成-已完成]状态
        })

        tools.$listen('change', '#toggle-all', function() {
            //该checlbox的切换所有todo项的状态
        })

        tools.$listen('click', '#todo-list .destroy', function() {
            //每个todo项的视图中,提供删除按钮,其click事件触发删除
        })

        tools.$listen('click', '#clear-completed', function() {
            //一个按钮,其click事件触发时,清除所有已完成的todo项
        })

        //dom ready时更新页面数据
        document.addEventListener('DOMContentLoaded', this.update.bind(this), false)
        //hashchange时,更新页面数据
        window.addEventListener('hashchange', this.update.bind(this), false)
        //页面关闭时,保存数据到localStorage
        window.addEventListener('beforeunload', this.model.save.bind(this.model), false)
    }

    Controller.prototype.otherMethod = function() {
        //其它方法,用于封装可复用的model与view的交互方式
    }

事件交互控制器虽然写了与业务逻辑耦合严重的硬编码,比如事件代理的各个特定的selector;但是它仍然封装在实例方法中,不形成调用。

//app.js
app.todos = new app.Controller(app.View, app.Model).init()

真正的调用,发生在出口模块中,并且也是因为此app.js是整个页面的出口模块。假设它只是其中一个业务模块,很多时候也不应调用init方法,而是放到页面出口模块中,择时机初始化。

定义与调用分离的好处:

以上所谓的MVC模式,算不上最佳实践,只是一个经验分享。如果你有好的建议,或者更好的方案,欢迎指出与分享。

margox commented 9 years ago

确实,有时候并不一定流行的框架才是符合自己业务需求的.而且我也是个懒人,懒到什么程度呢?有时候宁愿自己造个自行车,也不愿花时间去学驾照Orz...

Lucifier129 commented 9 years ago

@margox 我也处于相似阶段,并认为这是必要的。自己造轮子,才知道别人的轮子怎么造的,以及为什么比我的造得好,或者哪里没我造的好。

steelli commented 9 years ago

controller逻辑 有点重,需要细分

Lucifier129 commented 9 years ago

@steelli 用 react 来管理 view ,用 flux 来管理 model/store,应该可以显著降低 controller 的逻辑厚度。

norfish commented 9 years ago

只有自己造过轮子,才能更好的看到别人轮子的优劣

luqin commented 9 years ago

我做业务组件的时候就是懒得用框架了,直接发布订阅事件搞定,哈哈