js模块化与面向对象编程思考与实践
2019独角兽企业重金招聘Python工程师标准>>>
为什么做这个东西
我是做后端的,看到前端代码组织很难受。
- 各种乱放文件
- 同样的代码复制一个页面一份
- 一个文件囊括所有东西,更有甚者,一个html 3、4千行解决战斗。
- 有一些把js放一个目录,css一个目录,html放一个目录。在写一个功能时html,js,css都会随时用到, 一下打开这个目录,一下打开那个路径。虽然java也分dao、services什么的,可能强类型语言IDE比较好跟踪,而且一般一层写完再写另一层。 文件数量根据功能不同时多时少,东西一多就乱。
项目业务一多很难维护。更别谈其他项目复用。当然前端开发人员的水平参差不齐。另外很多项目都有后台管理类似的功能
- 搜索栏
- 一张表格
- 增删改查,其他操作等
功能差不多自然想到复用和封装。
就算是用了vue,react,dva什么的,感觉该乱还是乱,这些框架什么的,最多只是解决代码级别组件的组织归类,谈到根据业务模块来分,该乱的还是乱。
背景
公司项目比较low,一般都是后台管理系统,有很多的table,上面有查询条件,每个表都是增删改查各种单记录操作。所以相似的功能很多。
最快速的方案是写完一个模块,复制粘贴修改修改,就成了一个新的模块。这样做最大的弊端是就后期维护成本非常高,基本很难管理,要统一改个东西就各个模块的代码都处理一遍。然后项目人员一多编码风格都无法管理,后面就无zf状态。
虽然有时候会把一些公用的对像或方法放在公用的js文件中,但这远远不够,还有有大量重复的代码。
$extends只能解决对象或者方法级别的东西,碰到需要几个步骤的东西就嗝屁了。
解决什么问题
- (重点)代码文件组织,按业务模块分目录
- (重点)按功能切分代码到不同文件中。
- (重点)解决
复用
和个性化
的问题,不单单是使用公用属性 或 $extends,而是使用对象继承与重写,理论上可以重写父类所有的方法。 - 按需要加载,比如用户只打开查看界面,只加载相关文件,用户操作 新增、修改才加载编辑相关页面和代码。
- 解决加载顺序依赖的问题,可能有人会奇怪,都8102年了还有这种事。没错公司的项目比较low,有些还存在这个问题。
用到的技术或知识点
- js原型链,面向对象,继承与重写
- requieJs,主要用来加载文件,用其他加载插件也同理。顺便处理闭包
- 生命周期问题,如单例与重新new对象。使用requieJs加载文件时的闭包,执行一次性代码,定义context对象。
- $.Deferred,处理异步的封装,顺便干掉回调地狱
文件组织结构
- 前端代码module目录按业务划分各模块目录,各个模块目录下,放着该模块的各种文件包括js、html、css等
- module目录下有一个base目录,里面放着各种组件的基类(如baseTable、baseEdit、baseDetail)。还有相关的面向对象相关工具类(如classUtil)。
组件划分
- 一般组件有:
- table:分页数据列表、操作栏(查询条件和功能按钮,如查询、新增、其他按钮)
- edit:新增和修改,差别比较大可以分两个
- detail:查看详情
- 其他组件根据需要,角色分配置权限,用户重置密码,审核驳回
思路
- 每个组件有一个init的入口,
- 每个组件一般原型方法有以下几个,想要理解也可以参考vue的生命周期(我是后来才看vue,觉得有些挺像的)
- 渲染(或初始化)
- 赋值,如查看详情、修改等,表格列表可能没有
- 执行,比如打开详情模态窗口,要把初始化完成的窗口show出来
- 第二次调用的时候,可能就要调用reset方法,比如vue不能重复new
- 把有可能被子类重写的方法分出单独的原型方法出来,子类只要覆写这个方法,其他代码会调用基类的方法
- 如果碰到需要在父类方法前面或后面增加代码,可以覆写该方法写上子类逻辑,并使用this.__proto__.__proto__.functionName访问父类方法。具体几层.__proto__,自己调试一下看看。
示例
以表格为例。注意代码只是截取一些关键部分的代码,只是为了说明逻辑和细节,有些可能运行不通。
- 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);//初始化datatabletableOpt.$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';//overridethis.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两个方法
- 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__。
转载于:https://my.oschina.net/u/2438634/blog/3052622
js模块化与面向对象编程思考与实践相关推荐
- JS中的面向对象编程
JS中的面向对象编程 小课堂 目录 1.背景介绍 2.知识剖析 3.常见问题 4.解决方案 5.编码实战 6.扩展思考 7.参考文献 8.更多讨论 1.背景介绍 什么是对象? ECMA-262把对象定 ...
- [js] 举例说明面向对象编程有什么缺点?
[js] 举例说明面向对象编程有什么缺点? 有实例化开销,内存消耗比较大,性能消耗比较大 个人简介 我是歌谣,欢迎和大家一起交流前后端知识.放弃很容易, 但坚持一定很酷.欢迎大家一起讨论 主目录 与歌 ...
- VSCode自定义代码片段9——JS中的面向对象编程
JavaScript的面向对象编程 {// JS'OOP// 9 如何自定义用户代码片段:VSCode =>左下角设置 =>用户代码片段 =>新建全局代码片段文件... =>自 ...
- Javascript面向对象编程思考与总结
Javascript面向对象编程 什么是对象?万物皆对象 -对象:特指的某个事物,具有属性和方法(一组无序的属性的集合) 特征:-----> 属性 行为:----->方法 对象是单个事物的 ...
- 这可能是Python面向对象编程的最佳实践
作者 | 崔庆才 来源 | 进击的Coder(ID:FightingCoder) Python 是支持面向对象的,很多情况下使用面向对象编程会使得代码更加容易扩展,并且可维护性更高,但是如果你写的多了 ...
- 这可能是 Python 面向对象编程的最佳实践
Python 是支持面向对象的,很多情况下使用面向对象编程会使得代码更加容易扩展,并且可维护性更高,但是如果你写的多了或者某一对象非常复杂了,其中的一些写法会相当相当繁琐,而且我们会经常碰到对象和 J ...
- 图形化编程实现模块化和面向对象编程
图形化编程支持更多功能 新增功能 支持模块化编程 支持面向对象编程 支持多线程编程 代码实现原理 其他特色实现 总结 新增功能 最近折腾了一个月,给图形化编程增加了很多功能,让图形化编程可以使用更多的 ...
- JS面向过程+面向对象编程区别,原型的应用
1.1面向过程编程 思路:面向过程就是分析出解决问题所需要的步骤,再用函数把这些步骤一步一步实现,使用的时候一个一个的依次调用即可. 1.2面向对象过程 思路:把事务分解成一个一个对象,然后由对象之间 ...
- JS 面向对象编程、原型链、原型继承(个人学习总结)
一.面向对象 1. 面向对象 是所有语言 都有的一种编程思想,组织代码的一种形式 基于对象的语言:JS语言 面向对象的语言:c++ java c# 2. 面向对象 3大特征 封装:将重用代码封装到函数 ...
最新文章
- URL/URI/URN的认识
- 谈谈C++继承中的重载,覆盖和隐藏
- You have not accepted the license agreements of the following SDK components
- 菜鸟之路-浅谈设计模式之单例设计模式
- ref是什么意思_终于有人说出A股不敢说的话:为什么股价不断下跌,大单却持续流入,你知道是什么缘由吗?...
- java class类_关于创建java中的class类的对象的方法
- shell下的常用语句
- 批量word转pdf——VBS脚本,在office宏中运行即可
- Unity object强转int、string
- PDF太大怎么办?缩小PDF的4种常用方法
- 人体的神经系统图 分布,人体脑神经系统分布图
- python sklearn metrics,在Python中sklearn.metrics.mean_squared_error越大越好(否定)?
- 什么意思中文_中文十级考题,请解释这四句话是什么意思!
- Work with Hans
- B站韩顺平老师Linux笔记(截止到85集)
- iOS 手机淘宝 自动创建一个人的群聊 实现源码 hook 代码源码
- Andrej Karpathy读博建议和写论文的方法
- Migrando电子商务可以实现Iluria para o Shopify(Python的标准)
- 订单管理系统 订货系统平台建设三阶段分享
- 微信公众号文章互阅平台
热门文章
- js img转换base64
- Spring AMQP + Rabbit 配置多数据源消息队列
- MySQL 半同步复制+MMM架构
- Delphi2CS破解 Delphi 转换C#
- 一些有关。NET界面处理与多线程的文章
- Visual Stdio下安装WTL的向导Widzard
- 大数据之-Hadoop3.x_MapReduce_MapJoin案例调试_debug---大数据之hadoop3.x工作笔记0135
- Mycat安全_监控平台简介---MyCat分布式数据库集群架构工作笔记0035
- STM32工作笔记0030---编写跑马灯实验--使用库函数
- 经典线程同步 事件Event