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__。
相關文章
相關標籤/搜索