AMD模块的编写规范

React或Vue给我来带来的不是仅仅是View层的改变和便捷的组件化开发,更多的是围绕这套开发体系,整个技术栈的应用和实践。
但是往往在大多公司还没有切换到新的技术栈上,很多管理系统还是基于传统的AMD或CMD开发,配合Jquery插件使用,所以日常的组件和模块编写应该有一定的规范,以减少多人配合开发的沟通成本,统一编码风格,便于维护管理,也便于后期重构切换到新的技术栈。

是编写模块还是编写组件?

模块与组件的区别

  • 组件:可重用,对独立功能生命周期的封装。对外提供统一的配置和调用接口。遵循开闭原则:对外扩展开放,对内修改关闭。
  • 模块:不可重用,对一个业务处理的封装,有独有的业务处理代码,可调用各种组件。

依赖jQuery的组件

挂载为jQuery的静态方法 还是 挂载到jQuery的实例上?

  • 挂载到jQuery的实例上: 页面上需要实例化多个功能一样的组件时,依赖页面上的DOM结构,依赖父容器初始化
  • 挂载为jQuery的静态方法: 不依赖页面上的DOM,以处理数据为主

独立的组件

对纯数据层面的处理,比如常用的Utils模块、数据验证、xss模块、加密解密、cookie操作、特性检测等

设计思想

由前往后

  • 从前往后推,先构建使用方式和场景
  • 首先应该想到的是组件或模块的使用方法,它有那些方法和属性,应该怎样初始化
  • 需要预留怎么样的配置或接口去做模块的扩展
  • 需要依赖的资源

结构和接口设计

模块全局变量

  • CONF 模块配置: api地址,公用的配置和常量
  • pageVM 模块数据模型: 如果页面有大量表单元素,使用avalon的VM模型做数据的双向绑定,如果没有,则不需要此变量
  • pageView 模块业务逻辑: 页面主逻辑对象,包含:init/bindEvents/getData/initComponents等方法,init中只对业务做初始化操作
  • init 对外接口: 提供对外统一调用初始化方法

模块模板

define(['moment'],function(moment){
var pageVM, CONF, pageView;

// [ TODO: 模块配置 ]
CONF={
apiGetList:'getOrderList.json',
};

// [ TODO: 模块主逻辑处理 ]
pageView = {
init: function(){
// [ TODO: 模块数据模型初始化 ] (建议有表单时使用)
!pageVM && this.initVM(); //初始化ViewModel

this.initComponents(); // 初始化组件:select2、tolltip、uploader
this.getListData(); //加载列表数据
this.selectChangeEv(); //注册下拉框变化事件
},
initVM: function(){
pageVM = avalon.define({
$id:'recordsCtr',
searching:false,
searchPms:{
status:'0',
startTime:'',
endTime:'',
index:0,
length:10,
}
searchEv:function(e){
e.preventDefault();
pageVM.searching=true;
pageVM.searchPms.index=0;
page.getListData();
}
});
},
selectChangeEv: function(){ ... },
initComponents: function(){ ... },
getListData: function(){ ... }
}

// [ TODO: 对外提供统一的调用接口 ]
return {
init:function(){
pageView.init();
avalon.scan();
}
}
})

命名规则

  • 接口名称: 以 api 开头 + 接口名 {apiGetSalesList:'getSalesList'}
  • 事件名称: 事件名称 + 以 Ev 结尾 changeColorEv
  • this指向修正:统一以 _this 命名
  • jQuery变量以$符号开头 $btnSearch=$('#btnSearch')

注意点

  1. 每个模块 init 是入口函数,也就是第一次加载时,整个模块代码顺序执行,以后的每次页面初始化,都只会执行init中的代码
  2. 每个模块 VM$id 不允许重名
  3. 每个页面的artTemplate 模板id 不允许重名,因为模板有缓存,id相同会覆盖已有的模板
  4. 每个页面的DOM上的 id 尽量不要相同

数据加载处理

要考虑的问题:数据列表加载的重用性、Loading触发的时机、何时需要分页、点击分页的处理、搜索处理

getListData:function(){
// 构建查询参数
var pms = $.extend({},pageVM.$model.searchPms),
url = CONF.apiGetList, //数据加载接口
$tbl = $('#listTbl'), //列表所在的容器
$tbdBox = $('#listTbd'), //列表项所在的tbody
$total = $('#listTotal'), //数据总条数
$pageBox = $('#pageNumBox'); //分页容器

pms.startTime = Utils.getUnixTime(pms.startTime);
pms.endTime = Utils.getUnixTime(pms.endTime);

// 数据加载前显示Loading状态
$tbl.uiLoading('lg');

// 异步加载数据,并返回 deffered 对象
return $.post(url, pms)
.done(function(data){
if(data.status==='0'){
var listData = data.result.orderList,
count = data.result.count;

// 显示总数据条数
$total.text(count);

//如果有数据
if(count>0){
// 渲染数据
$tbdBox.html(template('list_record_tpl', {data: listData, url:CONF.orderUrl}));

// 如果总数据条数大于请求的数据条数,就显示分页
if(count > pms.length){
$pageBox.pagination({
totalData:count,
coping:true,
showData:pms.length,
callback:function(i){
// 计算起始索引
pms.index = (i-1)*pms.length;
//显示Loading
$tbl.uiLoading('lg');
//加载数据
$.post(url,pms)
.done(function(data){
// 渲染数据
$tbdBox.html(template('list_record_tpl', {data: data.result.orderList, url:CONF.orderUrl}));
// 隐藏Loading
$tbl.uiLoading('lg');
});
}
});
}
}else{
// 清空分页和列表数据,并添加为空说明
$tbdBox.html('<tr><td colspan="7"><p class="p20 c-8 text-center"> 未查询到相关数据 </p></td></tr>');
$pageBox.html('');
}
}else{
// 提示加载失败信息
toastr.error(data.errmsg || '服务器繁忙!');
}
})
.always(function(){
//加载失败时,隐藏loading状态
$tbl.uiLoading('lg');
pageVM.searching = false;
});
}

扩展:
进一步封装成一个组件 $(‘#salesListWrap’).getDataList(url, pms, tplId);