当初之所以转前端,就是因为写JS代码不用去部署环境,方便,自由。然而,正是因为自由,才让我们在组织代码时十分头疼。在这里,我要感谢 Kener-林峰 @Kener-林峰 的 #echarts#,正是多次拜读了 echarts的源码,才让我知道如何去组织代码,谢谢!
firstBlood,模块化开发
模块化开发的好处在这里就不再赘述了,当然现在各种模块加载框架也很多,比如我之前在系统中用的 seajs。在表单设计器里面我还是沿用了 echarts里面的 esl.js。文档结构如下图:
对应的esl配置如下:
require.config({ packages: [{ name: 'designer', location: 'js/autotable/statement/src', main: 'designer' }], paths:{ designer : 'js/autotable/statement/src', form : 'js/autotable/statement/form', main: 'js/autotable/statement/formMain' } }); require(['main'], function (main) { main.init({ saveUrl : '', editId : '' }); });
抛开路径中的 ace(一个js代码插件)不看,来解读一下paths里面的3个路径:
designer 包含了表单设计所有需要用的代码块,供form调用
form是表单设计器的底层实现,包括业务数据存储,事件处理,dom生成等
通过 main 指定了 一个调用 form的程序入口,如果我们要对表单设计器做一些初始化操作(特别是生成报表之类)也是在main指定的这个js文件中。
特别的, 根据第三点,我们可以在不同的应用场景指定不同的main来实现 不同的需求,如本例所示:formMain.js只是表单设计器的一个入口,并没有做其他的初始化操作。而在tabMain.js中,除了开启表单设计程序,还在表单设计器中初始化了报表的表头以及一些固定的报表输入项。
secondBlood,form.js
form.js里面的实现 我是完全参考echarts底层的 zrender.js来设计和实现的,主要有一下几点:
定义_form对象,该对象包含了诸如 生成模板,添加表单输入对象,合并、拆分单元格的接口,供前面提到的main对象已经一些组件调用。而这些接口的最终实现全都不在该对象上,最终的实现全在下面3个构造函数里面
Storage,顾名思义,所有的数据都在该构造函数里面。需要说明的是,在zrender里面构造函数的所有方法和书写都不是通过原型链来实现的,实现方式如下:
function Storage(){ var self = this; self.XXX = ''; self.xxxx = function(){} }
在Storage里面也提供了很多 set/get方法 用来设置或者返回一些数据
Painter,嗯,这家伙儿是用来绘制dom的,同时所有的工具箱类也是放到他里面的,后面我们在讲工具箱类的时候再说。
handler,包含了页面上,特别是表单主题部分的事件捕获以及分配,比如 表格大小的拖动,右键弹出添加面板等等都是在这里面来监听。
总体来说form.js就实现这些功能。
thirdBlood,拨开迷雾见青天
打开src,我们可以看到一下文档结构:
4个文件夹:
componet,这里面包含了所有的工具箱方法实现,
nav,导航?没错,就是导航,在这个系列的第一篇文章中,有界面截图,界面里面顶部的导航不是在页面中写死了的,而是通过诸如formMain,tabMain中调用form的navInit来具体生成的,因为我们可能在不同的应用需求中使用不同的导航栏工具。
shape,所有的表单对象包括纯文本,Input, select这些都在shape里面
tool,包括在日常开发中,我们都需要一些特殊的方法集合来实现一些功能,tool里面js文件就提供了这些方法,如 div的 拖动等
首先,我们先来从shape讲起,先看看里面的内容:
这里面的每一个js文件都对应着我们鼠标右键时需要添加进去的表单对象。这里以input.js为例看看里面的代码内容:
define( function(require) { function Input() { this.type = 'input'; } Input.prototype = { create : function(params, _form){ var dom = this.createDom(); this._form = _form; if(undefined != params.callBack) params.callBack.call(this); return dom; }, createDom : function() { var wrapDiv = document.createElement('div'), input = document.createElement('input'); input.type = "text"; this.formTag = input; wrapDiv.appendChild(input); return wrapDiv; }, getConfig : function() { return '<li><span>id</span><strong>:</strong><input type="text" name="id" /></li><li><span>name</span><strong>:</strong><input type="text" name="name" /></li><li><span>中文名</span><strong>:</strong><input type="text" name="cname" /></li><li><span>验证</span><strong>:</strong><input type="text" class="validate-input" readonly="readonly" /><a class="config-panel-btn validate-add"></a></li><li><span>默认值</span><strong>:</strong><input type="text" name="defaultVal"></input></li><li><span>只读</span><strong>:</strong><input type="checkbox" name="readonly"></input></li><li><span>描述ID</span><strong>:</strong><input type="text" name="relevanceId"></input></li><li class="btn-wrap-li"><a class="remove-btn panel-btn"><i></i>删除该控件</a></li>'; } }; var shape = require('../shape'); shape.define('input', new Input()); return Input; } );
基本上,每个表单对象所对应的Js文件里面都会有 类型,配置,创建dom的方法或者属性。
细心的朋友可能已经发行了在代码的最后 引入了一个shape,那么这个shape是拿来干什么的呢,直接看代码:
define( function(/*require*/) { var _ = require('designer/tool/undersore'); //提供一些独立的方法 var self = {}; var _shapeLibrary = {}; //shape库 self.define = function(name, clazz) { _shapeLibrary[name] = clazz; //对每个图形实现进行扩展 _.extend(clazz.__proto__, { attrInject : function(obj) { //attr注入 for(var i in obj) { this._form.setAttr(i, obj[i]); } }, setDefaultVal : function(val){ if(this.formTag) this.formTag.value = val; }, setReadyOnly : function(flag){ if(flag){ this.formTag.setAttribute('readonly', 'readonly'); } else { this.formTag.removeAttribute('readonly'); } }, dictionaryInit : function(dicId){ this._form.getDictionary(this.dom, dicId); } }) return self; }; self.get = function(name) { return _shapeLibrary[name]; }; return self; } );
通过define这个方法里面的这句:
_shapeLibrary[name] = clazz;
我们可以得知,其实shape就是用 _shapeLibrary 这个对象建立了一个 表单对象名称和 实例对象的一个映射关系。并在其 get方法中返回这个实例。同时,如果我们对每个表单对象做的扩展也是在shape里面实现的(当然你也可以建立一个表单对象基类,让每个表单对象去继承,看个人喜好)。
完整的添加过程如下:
1,调用 storage里面的一个add方法,将将要添加的表单对象放到数组中
2,执行painter的refresh方法,在该方法会先去 storage里面取要添加的(当然实际过程中还包括要删除的)集合,根据该集合进行遍历,最终生成的方法还是会回到 表单对象里面的 create 方法,并执行需要的初始化方法(取决于callBack)。需要注意的是,添加也可能会隐性的产生删除操作,比如一个td里面已经有表单对象了就需要删除操作。
3,根据 添加/删除操作,调整表格的宽度和高度。
PS:为什么先说这个 表单元素 的添加过程,是因为我觉得这块其实是大家应该比较感兴趣的部分。如果你有额外的需求,可照葫芦画瓢添加其他的表单对象(当然更多的 可能是你封装的组件)。
接着 我们回到 componet里面
就个人而言,我不太喜欢用jq,所以你在这里看到 ajax.js。抛开这个js,每个js和其对应的作用关系如下:
addFormBox.js 当我们鼠标右击点击表格的时候,弹出的那个添加界面,包括里面的点击事件,已经添加的调用都在该js代码里面
configPanel.js 每个表单对象的配置面板,包括配置内容的更改 初始化,以及和表单对象dom之间的联动都在其中实现,这个地方感觉用mvvm更合适
lineRowManger.js 我们对表格进行的右键 添加/删除 行的操作处理代码
modelPanel.js 模板行配置处理js代码
shadowDIv.js 右键 或者左键 选择表单块时 生成的那个半透明遮罩 层的相关代码
按照我对 tom大叔博客里面对 SPA的解释,上面列出来的 5个功能 应该按照这样的方法来组织,包括tools里面提供的dialog,undersore,以及div移动的monving 都是一个道理,只是按照功能稍微做了一个区分。
最后, 我们来看看 nav这个文件夹,内容 如下:
这里面每个JS文件都对应着 导航工具栏的每个功能,还是那句话,我们可以根据不同的需求去自由的组织导航栏的功能。还是举个例子: 看看前面所说的入口formMain.js文件
define(function (require) { var self = {}; /** * 入口方法 */ self.init = function (config) { var self = this, tab = document.getElementById('tab-wrap'); var _form = require('form').init(tab, config, function(){ this.navInit(['name', 'code', 'relevance', 'view', 'save']); }); } return self; })
有一句 navInit方法,传入的正式 导航工具栏 对象,一看是数组大家就都懂了。
文章对 Storage, Painter, Handler并没有做过多的说明,其实现的细节大家可以参阅 zrender.js
总体来说,整个开发过程中用到的内容都没有太大的难度,我们需要做的就是把每个细节做好, 谢谢大家的阅读。