js模块化与面向对象编程思考与实践

为何作这个东西

我是作后端的,看到前端代码组织很难受。javascript

  • 各类乱放文件
  • 一样的代码复制一个页面一份
  • 一个文件囊括全部东西,更有甚者,一个html 三、4千行解决战斗。
  • 有一些把js放一个目录,css一个目录,html放一个目录。在写一个功能时html,js,css都会随时用到, 一下打开这个目录,一下打开那个路径。虽然java也分dao、services什么的,可能强类型语言IDE比较好跟踪,并且通常一层写完再写另外一层。 文件数量根据功能不一样时多时少,东西一多就乱。

项目业务一多很难维护。更别谈其余项目复用。固然前端开发人员的水平良莠不齐。另外不少项目都有后台管理相似的功能css

  • 搜索栏
  • 一张表格
  • 增删改查,其余操做等

功能差很少天然想到复用和封装。html

就算是用了vue,react,dva什么的,感受该乱仍是乱,这些框架什么的,最多只是解决代码级别组件的组织归类,谈到根据业务模块来分,该乱的仍是乱。前端

背景

公司项目比较low,通常都是后台管理系统,有不少的table,上面有查询条件,每一个表都是增删改查各类单记录操做。因此类似的功能不少。
最快速的方案是写完一个模块,复制粘贴修改修改,就成了一个新的模块。这样作最大的弊端是就后期维护成本很是高,基本很难管理,要统一改个东西就各个模块的代码都处理一遍。而后项目人员一多编码风格都没法管理,后面就无zf状态。
虽然有时候会把一些公用的对像或方法放在公用的js文件中,但这远远不够,还有有大量重复的代码。
$extends只能解决对象或者方法级别的东西,碰到须要几个步骤的东西就嗝屁了。vue

解决什么问题

  • (重点)代码文件组织,按业务模块分目录
  • (重点)按功能切分代码到不一样文件中。
  • (重点)解决复用个性化的问题,不仅仅是使用公用属性 或 $extends,而是使用对象继承与重写,理论上能够重写父类全部的方法。
  • 按须要加载,好比用户只打开查看界面,只加载相关文件,用户操做 新增、修改才加载编辑相关页面和代码。
  • 解决加载顺序依赖的问题,可能有人会奇怪,都8102年了还有这种事。没错公司的项目比较low,有些还存在这个问题。

用到的技术或知识点

  • js原型链,面向对象,继承与重写
  • requieJs,主要用来加载文件,用其余加载插件也同理。顺便处理闭包
  • 生命周期问题,如单例与从新new对象。使用requieJs加载文件时的闭包,执行一次性代码,定义context对象。
  • $.Deferred,处理异步的封装,顺便干掉回调地狱

文件组织结构

  • 前端代码module目录按业务划分各模块目录,各个模块目录下,放着该模块的各类文件包括js、html、css等
  • module目录下有一个base目录,里面放着各类组件的基类(如baseTable、baseEdit、baseDetail)。还有相关的面向对象相关工具类(如classUtil)。

组件划分

  • 通常组件有:
  1. table:分页数据列表、操做栏(查询条件和功能按钮,如查询、新增、其余按钮)
  2. edit:新增和修改,差异比较大能够分两个
  3. detail:查看详情
  4. 其余组件根据须要,角色分配置权限,用户重置密码,审核驳回

思路

  • 每一个组件有一个init的入口,
  • 每一个组件通常原型方法有如下几个,想要理解也能够参考vue的生命周期(我是后来才看vue,以为有些挺像的)
  1. 渲染(或初始化)
  2. 赋值,如查看详情、修改等,表格列表可能没有
  3. 执行,好比打开详情模态窗口,要把初始化完成的窗口show出来
  4. 第二次调用的时候,可能就要调用reset方法,好比vue不能重复new
  • 把有可能被子类重写的方法分出单独的原型方法出来,子类只要覆写这个方法,其余代码会调用基类的方法
  • 若是碰到须要在父类方法前面或后面增长代码,能够覆写该方法写上子类逻辑,并使用this.__proto__.__proto__.functionName访问父类方法。具体几层.__proto__,本身调试一下看看。

示例

以表格为例。注意代码只是截取一些关键部分的代码,只是为了说明逻辑和细节,有些可能运行不通。java

  • baseTable.js
define(['class-util', 'usage', 'vue', 'ELEMENT'], function (util, usage, Vue, ELEMENT) {
    Vue.use(ELEMENT);

    function BaseTable(bean) {
        var self = this;
        //功能对象
        this.inited = false;
        this.defers = {};

        //子类常量,通常由子类覆盖
        this.elm = '#t_datatable';//表格的选择器
        this.$elm = null;//表格的jq对象,通常须要加载页面后再赋值
        this.$table = null;//表格的jq对象,通常须要加载页面后再赋值
        this.url = {//相关的操做的接口
            search: '',//分页查询
            delete: '',
        };
        this.form;//提交的数据

        //操做不一样的数据记录,每次都会变的属性放在opt里。
        this.opt = {
            //类变量
            $table: null,
            $dialog: null,
            $form: null,
            vm: null,//主要的vue对象
            //bean变量
            id: null,
            bean: null,
            action: null//操做:add,modify,detail
        };
        //属性

        this.page = {
            detail: null,
            edit: null
        };
        this.js = {
            edit: null,
            detail: null,
            upload: 'base-upload',
            audit: 'base-audit',
            collect: 'base-collect',
            downloads: 'base-downloads',
        };
        //相关操做显示的窗口标题
        this.titles = {
            'delete': '您肯定删除该项记录吗?',
            'report': '您肯定上报该项记录吗?',
            'detail': '详细信息',
            'modify': '修改',
            'add': '新增',
            'audit': '审核',
        };

        //提示信息
        this.msg = {
            report: '上报成功!',
            delete: '删除成功!',
        };
        //表格操做列按钮的代码,由于经常使用就放在基类里,个性化的状况子类能够覆盖
        this.btn = {
            search: '#b_search',
            add: '#b_add',
            collect: '#b_collect',
            detail: function (id) {
                return '<a data-id="' + id + '" data-action="detail" class="blue action" title="查看" href="#">\<i class="icon-zoom-in  bigger-130" data-row="" data-path="" data-index=""></i>\</a>';
            },
            delete: function (id) {
                return '<a data-id="' + id + '" data-action="delete" class="red action" title="删除" href="#">\<i class="icon-trash  bigger-130" data-row="" data-path="" data-index=""></i>\</a>';
            },
            modify: function (id) {
                return '<a data-id="' + id + '" data-action="modify" class="green action" title="修改" href="#">\<i class="icon-pencil  bigger-130" data-row="" data-path="" data-index=""></i>\</a>';
            },
        };

        //Bootstrap Table(或者其余)的统一配置,个性化状况子类覆盖须要修改的属性便可
        this.tableOpt = {
            toolbarAlign: 'false',
            searchAlign: 'right',
            buttonsAlign: 'right',

            sidePagination: 'server',//指定服务器端分页
            url: this.baseUrl + this.url.search,
            method: 'POST',
            contentType: "application/x-www-form-urlencoded; charset=UTF-8",
            pagination: true,//是否分页
            pageNumber: 1, //初始化加载第一页,默认第一页
            pageSize: 10,//单页记录数
            // pageList:[5,10,20,30],//分页步进值
            queryParams: function (params) {
                var params2 = self.queryParams.call(self, params);
                var defParam = {
                    'access_token': $.cookie('token'),
                    'limit': params.limit, // 每页要显示的数据条数
                    'start': params.offset, // 每页显示数据的开始行号
                    columns: params.sort,
                    isDesc: params.order === 'desc' ? true : false
                };
                return $.extend(defParam, params2);
            },
            responseHandler: function (res) {
                var respData = {
                    total: 0,
                    rows: []
                };
                if (res && res.content) {
                    var content = res.content;
                    var total = content.recordsTotal;
                    var rows = content.data;
                    if (total && $.isNumeric(total)) {
                        respData.total = total;
                    }
                    if (rows && $.isArray(rows)) {
                        respData.rows = rows;
                    }

                }
                return respData;
            },
            clickToSelect: true,//是否启用点击选中行
            striped: true, //是否显示行间隔色

            sortable: true,
            sortOrder: 'desc',
            columns: [{title: '序号', field: 'p_id'},],//columns通常都是子类覆盖的
        };
        //汇总表默认选项
    };
    /**********************原型方法*************************/
    //原型方法主要有几个
    //1. init、
    //2. 渲染操做栏,以下拉选择框、日历选择框,用vue等就更不用说了、
    //3. 渲染操做栏按钮,绑定事件什么的
    //4. 初始化表格

    //初始化,主要的执行入口,传入参数的入口
    BaseTable.prototype.init = function (elm, bean, option) {
        this.$elm = $(elm || this.elm);
        if (bean) {
            this.bean = bean;
        }
        if (option) {
            this.opt = option;
        }
        this.defers.initToolbar = this.initToolbar();
        this.defers.initToolbar = this.initToolbarBtn();
        this.defers.initToolbar = this.initTable(this.$elm, this.tableOpt);
        this.$table = this.initTable(this.$elm, this.tableOpt);
    };

    /**
     * 初始化操做栏
     * 通常子类覆盖
     */
    BaseTable.prototype.initToolbar = function () {
    };

    /**
     * 初始化操做栏按钮
     * 子类基本不用重写
     */
    BaseTable.prototype.initToolbarBtn = function () {
        var self = this;
        $.when(this.defers).done(function () {
            //按钮
            if (self.btn.search) $(self.btn.search).on("click", null, self, self.search);
            if (self.btn.add) $(self.btn.add).on("click", null, self, self.edit);
            if (self.btn.template) $(self.btn.template).on("click", null, self, self.downloadTemplate);
            if (self.btn.upload) $(self.btn.upload).on("click", null, self, self.upload);
            if (self.btn.collect) $(self.btn.collect).on("click", null, self, self.collect);
            if (self.btn.downloads) $(self.btn.downloads).on("click", null, self, self.downloads);
        });
    };


    /**
     * 初始化表格,
     * 子类基本不用重写
     */
    BaseTable.prototype.initTable = function (elm, tableOpt) {

        var self = this;
        var $elm = $(elm);

        //初始化datatable
        tableOpt.$el = $elm;//这样就能够在bootstrap.table实例中访问当前的jq table 对象,方便调用bootstrap.table的方法
        var $myTable = $elm.bootstrapTable(tableOpt);

        //绑定操做列按钮事件
        $elm.off('click').on('click', 'tbody .action', $myTable, function (e) {
            var $btn = $(this);
            var action = $btn.data('action');
            var id = $btn.data('id');
            var bean = $elm.bootstrapTable('getRowByUniqueId', id);
            var actionFun = self.actions[action] || self.confirmAction;
            if ($.isFunction(actionFun)) {
                var defer = actionFun(e, id, bean, self, action);
                //操做完成后执行
                if (self.callbacks) {
                    var actionCallback = self.callbacks[action]
                    if (actionCallback) {
                        $.when(defer).done(function (data) {
                            actionCallback.apply(self, arguments);
                        })
                    }
                }
            }
        });
        return $myTable;
    };

    //其余只个重要方法
    /**
     * 查询条件方法
     * 通常子类覆盖
     * @param d:默认datatable参数
     * @returns {*}:提交的data集合
     */
    BaseTable.prototype.queryParams = function (params) {
        return {};
    };

    /**
     * 删除操做,实现省略,通常都同样,子类不用重写,只要定义好 url.delete给他调就好
     */
    BaseTable.prototype.delete = function (e, id, bean, self) {
    };
    /**
     * 显示详情操做
     */
    BaseTable.prototype.showDetail = function (e, id, bean, self, action) {
        var title = self.titles[action];
        var dialogDefer = usage.tableDialog(title, self.page.detail, action);
        $.when(dialogDefer).then(function (dialog) {
            var modulePath = self.js[action];
            require([modulePath], function (Detail) {
                self.Detail = Detail;
                self.detail = new self.Detail(bean, dialog, self);//新建实例
                self.detail.init();
            })
        });
    };

    /**
     * 新增/修改操做(设置form为disable后显示详情)
     */
    BaseTable.prototype.edit = function (e, id, bean, self, action) {
        if (!self) {
            self = e.data;
        }
        action = action || 'add';
        var modulePath = self.js.edit;
        require([modulePath], function (Edit) {
            self.Edit = Edit;
            self.edit = util.getInstance(Edit.context, Edit)
            self.edit.init({
                $table: self,
                action: action,
                bean: bean,
                id: id,
            });
            self.edit.run();
        });
    };

    return BaseTable;
});
  • userTable.js
define(['base-table', 'class-util', 'usage', 'vue', 'ELEMENT', 'component'], function (BaseTable, util, usage, Vue, ELEMENT) {
    Vue.config.devtools = true;
    Vue.use(ELEMENT);

    var _context = {};

    function UserTable(bean) {
        BaseTable.call(this, bean);//继承父类属性
        var self = this;
        this.elm = '#table';//override
        this.url.search = 'api/user/search';
        this.url.delete = 'api/user/delete';
        this.js.edit = 'assets/module/user/user-edit';

        this.tableOpt.url = this.url.search;
        this.tableOpt.sortName = 'id',
            this.tableOpt.sortOrder = 'desc';
        this.tableOpt.columns = [
            {title: '序号', field: 'id'},
            {title: '名称', field: 'username'},
            {title: '所属单位', field: 'depart_name'}
            {title: '角色', field: 'roleNames',},
            {
                title: '状态', field: 'user_enable',
                formatter: function (value, row, index) {
                    return userStatusMap[value];
                }
            },
            {title: '备注', field: 'user_remark',},
            {
                title: '操做',
                field: this.idField,
                formatter: function () {
                    return self.renderActionBtn.apply(self, arguments);
                }
            },
        ];
    };
    util.beget2(UserTable, BaseTable);//继承父类方法
    //原型方法
    UserTable.prototype.initToolbar = function () {
        if (!this.opt.vm) {
            var vm = this.opt.vm = new Vue({
                el: '#toolbar',
                data: {},
                mounted: function () {
                    this.defers.toolbar.resolve(this);
                },
            });
        }
        return this.defers.toolbar.promise();
    };
    UserTable.prototype.queryParams = function (params) {
        //中间的内容本身处理
        return params;
    };
    return UserTable;
});

其中class-util两个方法react

  • class-util.js
define([], function () {
    /**
     * 生孩子函数 beget:龙beget龙,凤beget凤。
     * 用于继承中剥离原型中的父类属性
     * @param obj
     * @returns {F}
     */
    function beget(obj) {
        var F = function () {
        };
        F.prototype = obj;
        return new F();
    }

    function beget2(Sub, Sup) {
        var F = function () {
        };
        F.prototype = Sup.prototype;
        var proto = new F();
        proto.constructor = Sub;//继承代码
        for (var key in Sub.prototype) {//若是在子类声明了prototype方法以后才调用此继承方法,复制子类方法以覆盖父类方法
            proto[key] = Sub.prototype[key];
        }
        Sub.prototype = proto;//继承代码
        return Sub;
    }


    /**
     * 获取单例对象
     * @param context 存放对象的上下文,用于检测是否已实例化,返回已实例化对象;存放其余对象如模态框的jq对象
     * @param clazz 须要new的类
     * @returns {*}
     */
    function getInstance(context, clazz) {
        if (!context.inst) {
            context.inst = new clazz();
            //context.inst.setDialog(context.$dialog);
        }
        return context.inst;
    };
    return {
        beget: beget,
        beget2: beget2,
        getInstance: getInstance,
    };
});

其余注意

  • beget方法中的原型链 模拟继承,得好好理解。
  • beget2主要是解决beget会把子类的原型方法会丢失,若是beget在子类的原型方法赋值以后,但使用beget2好像会致使 访问父类方法就要多一层__proto__。
相关文章
相关标签/搜索