超凡魔力

君子善思,善假于物,而不物于物。

0%

打印输出

今天接到一个奇葩的工作,要求打印出清单。我的想法是,直接生成一个html,然后打印。打开一个新页面使用window.open,但是打开了之后,内容如何传递过去呢?网上找到了答案。

相关参考

参考1

参考2

下面的示例,演示了xtemplate、window.open的用法。

var newWin = window.open();

var tpl = new Ext.XTemplate(
	'<html>',
	'<head>',
	'<title>{meeting_name}-预约费用清单</title>',
	'<style>',
		'body{background: #eee;}',
		'table{width: 900px;text-align: center;margin: 0 auto;font-size: 18px;line-height: 35px;    border-collapse: collapse;} ',
		'.container{width: 900px;height:1200px;  margin: 0 auto;   padding: 40px;background: white;}',
		'.bottom div{width: 32%;display: inline-block;font-size: 20px;padding: 20px 0;}',
		'h1{text-align: center;}',
	'</style>',
	'</head>',
	'<body>',
	'<div class="container">',
		'<h1>预约费用清单</h1>',
		'<table border="1">',
			'<tr>',
				'<th>序号</th>',
				'<th>单品</th>',
				'<th>单价</th>',
				'<th>数量</th>',
				'<th>总价</th>',
			'</tr>',
			'<tpl for="commodity_id_list">',
			'<tr>',
				'<td>{#}</td>',
				'<td>{commodity_name}</td>',
				'<td>¥{commodity_price}</td>',
				'<td>{count}</td>',
				'<td>¥{total}</td>',
			'</tr>',
			'</tpl>',
			'<tr>',
				'<td colspan="4">合计</td>',
				'<td>¥{meeting_cost_details}</td>',
			'</tr>',
		'</table>',
		'<div class="bottom">',
			'<div>预约方领导签字:</div>',
			'<div>管理方领导签字:</div>',
			'<div>主管领导签字:</div>',
		'</div>',
	'</div>',
	'</body>',
	'</html>'
);
newWin.document.body.innerHTML = tpl.apply(record.data);

兼容性问题

var newWin = window.open();
将一段HTML代码,插入到一个页面中有三种方法:

  • 1)document.body.appendChild(yourCode)
  • 2 ) document.body.innerHTML = yourCode
  • 3 ) document.write(yourCode)

使用newWin.doucment.body.appendChild(formElm )方法将表单元素插入到空页面,IE8下会报错Error:不支持此方法接口 ,而其它浏览器正常,因此选择另外两种方法了。

另外一种方式是,在window.open()的页面内,通过window.opener的方式与父页面进行数据通信。

[TOC]

柱状图使用

例子

具体的学习例子,见官方的sdk例子,地址:extjs/sdk/ext-6.5.3/build/examples/kitchensink/?classic#column-basic

简单粗暴,直接将其items(cartesian)复制过来,然后在其上面更改。将一些函数内的数具注释掉。刚开始,进可能的使用较少的功能。先将整个图显示出来再说。然后再深入每个配置项,调节功能。

每项的参数分析

store

以date作为x轴,注意2019-01-01重复了,实际上,同名的,只有最后一个起作用。但是数字,显示,两个都会显示。具体见图。

data:[
	{'date':'2019-07-01','basehospital':'华山医院','price':'11','count':'1'},
	{'date':'2019-07-01','basehospital':'华山医院','price':'13','count':'1'},
	{'date':'2019-07-02','basehospital':'华山医院','price':'22','count':'1'},
]

1562227106896

axes

设置横轴、纵轴。Ext.chart.axis.Axis xtype: axis的一个实例

axes: [{
	type: 'numeric',
	position: 'left',
	minimum: 0,
	titleMargin: 20,
	title: {
		text: '人次'
	},
	// listeners: {
	// 	rangechange: 'onAxisRangeChange'
	// }
}, {
	type: 'category',
	position: 'bottom', //显示在底部
	title:'日期'
}],

series,设置数据源

series: {
	type: 'bar',
	xField: 'date',
	yField: 'price',
	// style: {
	// 	minGapWidth: 20  //每个柱状图的间隔
	// },
	highlight: {//每个轴的高亮
		strokeStyle: 'black',  //选择时的边框颜色
		fillStyle: 'gold'  //选择时的高亮
	},
	label: {
		field: 'price',
		display: 'insideEnd',
		// renderer: 'onSeriesLabelRender'
	}
},

图表自带预览功能

这个功能自Ext.draw.Container继承而来,具体的实现比较荣长,可以在控制台输出,就能跳到对应的代码位置。

{
	text:"预览",
	handler:function(){
		var chart = me.down('cartesian');
		chart.preview();
	}
}

错误处理

sencha app build testing模式下打包,能显示具体的报错信息。如下,提示,某个依赖的组件未认出。

1562235669387

解决方法:

添加依赖,如下。大概就这么多。

requires:[
		'Ext.chart.axis.Numeric',
		'Ext.chart.axis.Category',
		'Ext.chart.series.Bar',
		'Ext.chart.interactions.ItemHighlight',
	],

之前一直以为是项目的配置有问题,但是添加了依赖了啊。

1562236182844

数据处理

由于每次都要频繁的从后台加载数据,所以略做了优化,将请求的参数作为缓存的键值,对数据进行缓存。

me.cacheData={}  //在组件init的时候,进行初始化。   而不是直接在属性上初始化,避免多个公用一个。

var cacheKey=query['start_date']+'#'+query['stop_date'];
//尝试读取缓存,如果存在,直接读缓存,并处理
if(me.cacheData[cacheKey]){
	me.analysisData(me.cacheData[cacheKey]);
	return ;
}
//如果不存在,则发送ajax请求
...

树形结构

针对树级结构,常用的策略就是递归,另外一种,就是组装成字段。然后从字段里面读字段,这样可以减少递归。

如下,对树级的data进行遍历,但是开销略大。

public static function setRemark(&$data,$pid,$remark){
		foreach($data as &$row){
			if(!isset($row->parentid)) continue;
			if($row->parentid == $pid){
				$row->remark=$remark.'>'.$row->name;//设置remark  分割 >
				self::setRemark($data,$row->id,$row->remark);
			}
		}
	}

例外一种,就是想将数组保存成map,或者关联数组,如下代码:

var cityList =[];//_city中的对象
	var cityCode =item.substring(0,6);
	var cur  = _city[cityCode];
	cityList.push(cur);
	while(cur.pid!=='0'){
		//前进一步
		cur=_city[cur.pid];
		cityList.push(cur);
	}
	//取反顺序,即由根节点往下
	cityList.reverse();
	city= cityList.map(x=>x.name).join(' ');

php的代码示例

<?php
//机构管理

class StatisticsUtils{
	public static $organizationData;
	public static $organizationPath;
	
	/**********	
		2018/7/23
		显示机构列表名称
	*********/
	public static function hospital($params){
		$db = Zend_Registry::get('db'); 
		$structureId = $params['structure_id'];	//structure_id
		$searchDateKey = isset($params['searchDateKey'])?$params['searchDateKey']:1;
		$startdate =  date("Y-m-d 00:00:00",strtotime($params['start_date'])); 
		$stopdate =  date("Y-m-d 23:59:59",strtotime($params['stop_date']));		

		//时间
		$where = "";
		if($searchDateKey==1){//预约日期
			$where=" and ( c.conversation_schedule_date between '$startdate' and '$stopdate' )";
		}else{//下单日期
			$where=" and ( ct.pay_time between '$startdate' and '$stopdate' )";
		}

		//查询内容  "专家医生"; 
		if(isset($params['doctor'])){
			$schedule_ids =$db->fetchCol('select schedule_id from schedule where user_id =:id',array('id'=>$params['doctor']));	
			if(count($schedule_ids)){//查找到医生的排班
				$where .= $db->quoteInto('and ct.schedule_id IN(?)', $schedule_ids);
			}else{//否则直接返回空结果集
				return array();
			}
		}

		$sql= "SELECT ct.organization_id,conversation_schedule_date,pay_price  FROM conversation_treatment AS ct  ".
				"LEFT JOIN conversation AS c ON ct.conversation_id = c.conversation_id  ".
				"WHERE  ".
				"	ct.treatment_type = 1  ". //门诊
				"AND c.conversation_type = 2  ". //医疗
				"AND c.structure_id = $structureId   ".//当前机构
				" $where ORDER BY ct.conversation_id ASC  ";
		// echo $sql;exit;
		$res = $db->query($sql);
		
		$rows = $res->fetchAll();
		
		//对数据进行统计分析
		$groupM = self::getOrganization($structureId);
		$data = array();  //data ['organization_id']['date'][]
		foreach($rows as $k=>$row){
			$oid =$groupM[$row['organization_id']];
			$date = $row['conversation_schedule_date'];
			if(!isset($data[$oid])){
				$data[$oid]=array();
			}
			if(!isset($data[$oid][$date])){
				$data[$oid][$date]=array('price'=>0,'count'=>0);
			}
			//统计
			$data[$oid][$date]['price']+=$row['pay_price'];
			$data[$oid][$date]['count']+=1;
		}
		
		$result =array();
		foreach($data as $oid=>$oidArr){
			foreach($oidArr as $date=>$dateArr){
				$basehospital = self::$organizationData[$oid];
				// {'date':'2019-07-01','basehospital':'华山医院','price':'11','count':'1'},
				$result[]=array('date'=>$date,'basehospital'=>$basehospital,'price'=>$dateArr['price'],'count'=>$dateArr['count']);
			}
		}
		//排序
		function cmp($a,$b){
			if($a['date']==$b['date']){
				return 0;
			}
			return date($a['date'])>date($b['date'])?1:-1;
		}
		usort($result,'cmp');
		return $result;
	}
	/****************
	 * 2019年7月5日
	 * 返回每个组织所归属的医院的map
	 ****************/
	public static function getOrganization($structureId){
		$db = Zend_Registry::get('db'); 
		$rows=$db->fetchAll('select organization_id id ,organization_parent_id pid,organization_name name from  organization where structure_id =:id;',array('id'=>$structureId));
		self::$organizationData = array_column($rows,'name','id');
		$mapping = array_column($rows,'pid','id');

		// echo json_encode($mapping );exit;
		$result=array();
		foreach($mapping as $id=>$pid){
			$cur=$pid;
			$path=array($pid);
			while($cur>0){
				// var_dump($cur);
				$cur=$mapping[$cur];
				$path[]=$cur;
				if(count($path)>20){
					die("maximum recursion depth exceeded ");
				}
			}
			$result[$id]=join(',',$path);
		}
		self::$organizationPath = $result;
		$data = array();
		foreach($result as $k=>$v){
			$tmp = explode(',',$v);
			$len = count($tmp);
			if($len>1){
				$data[$k]=$tmp[$len-2];
			}
		}
		return $data;
	}
	/****************
	 * 2019年7月8日
	 * 获取医院
	 ****************/
	public static function getHospital($params){
		$db = Zend_Registry::get('db'); 
		$rows=$db->fetchAll('select organization_id value ,organization_name name from  organization where structure_id =:id and organization_parent_id = 0;',array('id'=>$params['structure_id']));
		$option =  isset($params['option']) ? '请选择机构':'全部机构';
		return array_merge(array(array('name'=>$option,'value'=>-1)),$rows);
	}
	/************
	 * 2019年7月8日
	 * 获取指定医院的医生
	 ************/
	public static function getDoctor($params){
		$db = Zend_Registry::get('db'); 
		$structureId = $params['structure_id'];
		$id  = $params['hospital_id'];

		$org = self::getOrganization($structureId);
		$orgIds=array();
		foreach($org as $k=>$val){
			if($val == $id){
				$orgIds[]=$k;
			}
		}
		$orgIds[]=$id;
		$where = $db->quoteInto('organization_id in (?)',$orgIds);
		$rows=$db->fetchAll("select user.user_id,user_name from user left join user_extend on user.user_id = user_extend.user_id where $where");
		return $rows;
	}
	
}

[TOC]

前端Ext

行编辑器功能,动态设置,某列可编辑

思路有两种,一种是在编辑前,获取editor,然后设置其 disabled属性,但是麻烦。

另外一种,即放在viewmodel中,通过绑定,来达到动态的设置效果。(多采取此类方法)

viewModel设置:

viewModel:{
	data:{
		selections:0,
		item_type:0 //这一项需要动态的设置。在beforeedit
	}
}

整个grid表格的监听事件:

listeners:{
    //在这个事件中,进行监听
	"beforeedit":function(editor, context, eOpts ){
		var width=me.columns[2].getWidth();
		console.log(context.record);
		var item_type=context.record.get('item_type');
		me.getViewModel().setData({item_type:item_type});
	},
	'edit':function(editor, context, eOpts){
		//编辑功能
		var record =context.record;
		console.log(record);
		console.log(1222222222222222222);
		Ext.Ajax.request({
			url: SHINEVMSHTTP + '/admin/structure/index/edit',
			params:record['data'],
			success: function(response, opts) {
				var obj = Ext.decode(response.responseText);
				me.currentEditRecordId=obj['id'];
				me.store.reload();
				
			}
		});

	},
	'canceledit':function( editor, context, eOpts ){
		//添加时,如果点击取消,那么将删除此行
		var record=context.record;
		var id = record.data.structure_id;
		if(id==0){
			me.store.remove(record);
		}
	},		
	'selectionchange':function( cmp, selected, eOpts ){
		this.getViewModel().setData({
			selections:selected.length
		});
	}
},
{
		text:'出诊显示天数',
		dataIndex:'structure_day',
		editor:{					
			xtype:'numberfield',
			labelAlign:"right",			
			valueField: 'key',
			bind:{
				disabled:'{item_type!=2}',
			}
			
		},
		renderer:function(v,metaData,record,rowIndex,colIndex){
			return record.get('item_type') != 2?'':(parseInt(v)+'天');
		}	
}

效果图:

1560481792860

行编辑器:注意,新增加的字段,必须在store的field中添加,否则不会生效。第二次踩坑。

排序科室,未注意到共用组件。

而且共用组件的配置属性,如果是复杂变量,其也是共享内存的。

在组件初始化的时候,对其进行了判断,如果为特定的组件,则增加“排序”字段。结果,有两个问题。

  • 组件因为是共有的,多次执行后,add多了多个列。

1560739140891

  • 其他不想用到的这个列,也增加了该字段。

1560739191839

错误代码如下:

initComponent方法内,在调用me.callParent();方法之前。

if(me.cmpbox && me.cmpbox=='departmenthome'){
	console.log('我执行啦');
	me.columns.push({ 
		text: '排序',
		sortable: false,
		dataIndex: 'show_order',
		width:100
	});
}

修改为:

防止复杂变量属性(数组、对象)在原型上共用同一地址。

var col1={ 
	xtype:"treecolumn",
	text: '科目名称',
	sortable: false,
	height:0,
	dataIndex: 'department_name',
	flex: 1
};
var col2={ 
	text: '排序',
	sortable: false,
	dataIndex: 'show_order',
	width:100
};
if(me.cmpbox && me.cmpbox=='departmenthome'){
	me.columns = [col1,col2];
}else{
	me.columns = [col1];
}
me.callParent();

上面的代码似曾相识,在别人的老代码中见到过类似的写法。

总结:1、遇到通用组件,需要在不同的状态下,实例不同的属性,一定要用赋值一个新的变量,而不要修改。(之前,总是关注自定义的)2、与到通用的组件,要注意,在其他组件的显示特性。

门诊管理菜单改造

  • 创建没有标签的下拉选择框

    只需要将fieldLabel等属性注释掉即可。代码如下:

    me.searchDateCmb = Ext.create('Ext.form.ComboBox', {
    	// fieldLabel:'查询类型',
    	// labelAlign:"right",
    	// labelWidth:60,
    	value:1,
    	width:160,
    	editable:false,
    	store:{
    		fields: ['key', 'name'],
    		data :[
    			{key:1,name:"预约日期"},					
    			{key:2,name:"下单日期"}				
    		]
    	},
    	listeners:{
    		change:function(c, newValue, oldValue, eOpts){
    			me.searchDateKey = newValue;
    		}
    	},
    	queryMode: 'local',
    	displayField: 'name',
    	valueField: 'key'
    });
  • 双层菜单栏设计:

    简单的原理如下:

    tbar:[{
    	xtype:'panel',
    	layout:'vbox',
    	items:[{
    		html:'我是第一行'
    	},{
    	html:'我是第二行'
     	}]
     }],

需要注意的是:vbox布局,需要设置宽度,没有则可能显示不正常。所以每个items里面都是一个panel,设置width:’100%’。然后每个panel都有一个tbar。 或者,不想每个都设置高度的话,则修改布局如下:

layout: {
	type: 'vbox',
	align: 'stretch'//默认每个100%宽度
},

另外,别人代码是这样设计的。

grid表格没有toobar,然后单独的制作两个toobar。

Ext.define("View.AppointMenu", {
    extend: "Ext.panel.Panel", alias: "widget.AppointMenu",
    layout: { type: "vbox", align: "stretch" },
    items: [
       { xtype: "appointmain_toolbar", height: 40, border: 1 },
       { xtype: "appointmain_toolbar2", height: 40, border: 1 },
       { xtype: "appointmain_grid", flex: 1.0, border: false, },
    ],
});

//菜单对象创建

Ext.define("View.AppointMain_Toolbar", {
    extend: "Ext.toolbar.Toolbar", alias: "widget.appointmain_toolbar",
    items: [
        //{ xtype: "container", html: "<div>预约日期</div>", },
        {}]
})

发现问题:在tbar:[]中实例化时,括号中,如果是已创建的实例,则不能共用,(共用,则出现,最后一个能用到,前面引用的实例是不会显示的。)如果是配置文件,则互不干扰。

效果如图:

1560843640304

tbar想采用方式一,设置,结果总是报错。无奈采用第二种方式。(没想到这也行。)

//方式一
dockedItems: [{
	xtype: 'toolbar-overflowbar',
	dock: 'top',
	overflowHandler: 'menu'
}]
//方式二:
tbar:{
	items:[me.searchDateCmb],
	overflowHandler :'scroller'
}
  • store的data无法使用绑定。(好像)。延伸:是不是复杂变量,都不能进行绑定?
//定义好数组,供store用
viewModel:{
	data:{
		doctorList:[
			{"display":"一星", "v":1},
			{"display":"二星", "v":2},
			{"display":"三星", "v":3},
			{"display":"四星", "v":4},
			{"display":"五星", "v":5}
		],
		doctor_id:null,
		date:null,
	},
},


{//组合框   //修改医生
	xtype:"combobox",
	fieldLabel: '修改医生',
	name: 'doctor_id',
	forceSelection:true,//强制选择项
	editable:false,
	store: {
		fields: ['display', 'v'],
		bind:{
			data:'{doctorList}' //事实上,好像并不起作用
		}
	},
	queryMode: 'local',
	displayField: 'display',
	valueField: 'v',
	submitValue:true
}
/**************
     * 2019年6月20日
     * 获取当前的查询条件
     **************/
	queryParams:function(){
		var me =this;
		var v= me.getViewModel().getData();
		console.log(v);
		var fn = function(v){
			return Ext.Date.format(new Date(v),'Y-m-d');
		}
		v['startdate']=fn(v['startdate']);
		v['stopdate']=fn(v['stopdate']);
		v['structureId']=ShineMessageHub.structureId;
		console.log(v);
		return v;
	},

共用查询条件(导出)

查询在页面显示跟下载,共用一组条件,这样好处是,保证了所见即所得。另外,两个也共用了一套的查询逻辑。导出,利用了自己抽象的组件(参见:数据导出)

另一点,创新之出,利用了viewModel,将各个选择的项进行了绑定,这样获取当前的查询条件比较方便。(多用viewModel思路)

/**************
 * 2019年6月20日
 * 获取当前的查询条件
 **************/
queryParams:function(){
	var me =this;
	var v= me.getViewModel().getData();
	console.log(v);
	var fn = function(v){
		return Ext.Date.format(new Date(v),'Y-m-d');
	}
	v['startdate']=fn(v['startdate']);
	v['stopdate']=fn(v['stopdate']);
	v['structureId']=ShineMessageHub.structureId;
	console.log(v);
	return v;
},
//数据加载前,添加搜索条件
'beforeload':function( store, operation, eOpts){
	operation['_params'] = me.queryParams();;
}

缺少滚动条

使用了hbox vbox布局,一定要仔细处理高度跟宽度。避免造成布局问题。

问题如下 :

1561359094251

原因就是,hbox,左右布局,但是没有为每个items指定高度,所以右侧虽然撑开了,但是组件不知道自己的高度。就缺少了滚动条。

解决方式:

指定第二个组件的高度为100%,另外也可以从layout来解决。

items:[{
					xtype:"organizationlist",
					split:true,
					//collapsible:true,
					hideCollapseTool : true,
					collapseDirection:'left',
					collapseMode : 'mini',
					title:"出诊基层",
					switchHandler:8,
					width:450,
					height:'100%',
				}
//解决方式二,但是,这个却不起作用。  难道stretch只适合vbox?
layout: {
	type: 'hbox',
	align: 'stretch'//默认每个100%宽度。
}

listeners

组件已经包含了listeners,引用组件时候,还能再次添加listeners,这两个listeners其实都能起作用。

items:[{
		xtype:"departmentgrid",
		region:"east",
		tbarIsHidden:1,
		title:"科目信息",
		split:true,
		cmpbox:"medicinehome",
		width:300,
		listeners:{
			"rowcontextmenu" :function( c, record, tr, rowIndex, e, eOpts ){
				console.log('右键菜单-药品');
			}
		}
]

listeners中的cmp并不总是this

//Ext.tree.Panel  组件的listeners
listeners:{
		"rowcontextmenu" :function( c, record, tr, rowIndex, e, eOpts ){
			c == this //false
            //这里面的c居然并不是组件  的this.
            //输出c,发现   xtype: "treeview"
		}
}

用textfield替代hiddenfield

如下代码,可以将一个输入项隐藏起来。代码本身没有什么亮点,重点在于,用其替代hiddenfield。为啥要替代,因为hiddenfield的功能,没有textfield功能齐全。比如,是否要提交、是否为空等验证。

{
	hidden:true,
	xtype:"textfield",
	name:"lastaccountid"
},

尽量用viewmodel来代替具体的索引

在远程医疗模块中,添加终端功能,其内部大量使用了items.getAt(索引号)功能,但是随着需求的更改,变得非常难维护,增加一个items项,代码影响很大。所以能用viewmodel的时候,尽量用。或者用me.query(‘’)方式,尽量代码与其具体的索引无关。

me.editPanel.items.getAt(me.editPanel.items.length-4).setValue(me.LastAccountID);
me.editPanel.items.getAt(me.editPanel.items.length-3).setValue(Ext.encode(OutData));

allowBlank改造,为组件增加set方法

组件的viewmodel方法绑定时,只能绑定有set方法的配置项。如果没有,则会报错。在切换时,有的时候想动态的设置allowBlank,即,在有的选项下进行验证,有的方法下不验证。想通过listeners的blur方法进行切换,markInvalid标记验证结果,触发验证。结果是,在allowBlank=true的情况下,都不会再验证了。后来,在源码观察中,发现allowBlank可以进行改造。遂改造,并实现了效果:动态开启、关闭验证

{
	fieldLabel: "IP地址",
	labelAlign:"right",
	name: 'device_ip',
	xtype: 'textfield',
	regex:/^((25[0-5]|2[0-4]\d|[01]?\d\d?)($|(?!\.$)\.)){4}$/,
	regexText : '请输入正确的ip地址!',
	//注意allowBlank,没有设置方法,下面的绑定,会报错。
	bind:{
		allowBlank:'{virtual==1}'
	},
	//为了解决动态设置allowBlank的问题,自己手动的增加。
	setAllowBlank:function(v){
		this.allowBlank=v;
	}
	//下面的方法,没有达到预定效果。
	// listeners:{
	// 	'blur':function( cmp, event, eOpts ){
	// 		var v=me.getViewModel().getData();
	// 		if(v['virtual']==0){
	// 			console.log('真实设备');
	// 			cmp.markInvalid(cmp.regexText);
	// 			cmp.isValid();
	// 		}
	// 	}
	// }
}

获取上传文件的内容

只有一种方式,items.getAt(1).getValue(),其他的方式,都无法获取到上传表单的值。难道这算是一个bug?

  • 几种方式对比如下:
var bform = me.down('form').getForm();
var filename=bform.getValues()['attachment'];
console.log(bform.getValues());
var filename1=me.down('form').items.getAt(1).getValue();
console.log({filename,filename1});
  • 结果图:

1561711141877

window的自适应

设置window的组件的布局 layout:’fit’,其可以被内部的元素主动撑开,不需要自己再手动的调整高度。

这个对那种需要动态隐藏某些组件的来说,非常方便。

需要优化的嵌套if语句

if的嵌套,有时候可以变成并列条件,也可以反过来。对下面的进行分析,GroupType与filename进行合并,然后else正好合并到了上一级。但是 Msg这一块却没有办法优化,因为其需要回掉。同时,才将submitAction合并成一个函数。另外,这个应该也可以用消息传递、或者promise对象来完成。

1561947417403

优化成这个样子:

1561947737954

fieldcontainer,按钮同行布局

说明:有这样的需求,在原有的行上面增加一个按钮。之前试过用代码动态的插,费时费力,效果也不是很好。后来,fieldcontainer很好用。

但是需要注意的是,fieldcontainer会将内部的标签fieldLabel提到外部,但是字段的name不能提升,因为其没有。另外,整体的布局样式,应该也是加载fieldcontainer上的,一般都是放在defaults上面。

代码如下:

{
	xtype:'fieldcontainer',
	fieldLabel:'宣传片',
	layout: 'hbox',
	hidden: (GroupType==1)?false:true,
	items:[{
		xtype:'filefield',
		name: 'organization_video',
		buttonText: '浏览',
		flex:1,
	},{
		xtype:'button',
		margin:'0 0 0 5',
		text:'清空',
		idth:42
	}]
}

效果图如下:

1561951575932

小技巧

  • 初始化变量的一些小技巧

下面的方式初始化数组。(有用过类似的方法,但下面的例子让我感兴趣。)

fields: "name,price,count".split(","),
  • items数组的动态

其中items3为根据flag的值动态加载。若加,设置为null,不会加载。

items:[items1,items2,(flag)?item3:null,item4]
  • 发布版本,清除输出
console.log= function(){}
  • 数组合并或者复制数组
a=[1,2,3,4]
b=[...a] //拷贝a,并形成一个副本
//同理,合并数组
c=[...a,...b]
//之前用的是比较low的方法
c=[]
c.splice(c.length,0,...a)

store loadData方法使用

重要的事说三遍,是数组,一定要是数组,否则不起作用。 loadData应该是立即执行的。

var item = [{id:-1,name:'请选择地区'}];//一定要是数组格式。
var data=me.cityList[newValue]?Ext.Array.merge(item,me.cityList[newValue]):item;
cityCombo.store.loadData(data);//data一定要是数组格式,否则不起作用

combo选择功能,再次遇到坑。花费了接近1天的时间才解决。具体代码参见;OrganizationGrid.js

说一下具体的思路:

省、市、县的三级联动。默认从后台获取好数据,并组织好,以父级的id作为分组。然后整体一次加载到数据,并存到变量中。每次选择变化时,监听省、市的变化,值变化后,在change事件中,获取到下级的组件store,并动态的为其加载数据,loadData,注意,loadData方法加载很快,然后由于更改了选择,应使下级选择重置。

每级只需要关注change事件,并为下级动态的加载tstore。

部分代码:

{					
	xtype:"combo",
	fieldLabel : '城  市', 									
	name : 'city_id',
	store:{
		fields: ['id', 'name'],
		data  :[{id:-1,name:'请选择地区'}]  //增加了请选择的功能
	},
	queryMode: 'local',
	displayField: 'name',
	editable:false,
	forceSelection:true,//有无均可,editable已经限制了。
	valueField: 'id',
	value:-1,  //默认选择
	listeners:{
		'change':function( cmp, newValue, oldValue, eOpts ) {
			var countyCombo=me.DepartmentWin.query('combo[name=county_id]')[0];
			var item = [{id:-1,name:'请选择区县'}];  //注意其为数组。
			var data=me.cityList[newValue]?Ext.Array.merge(item,me.cityList[newValue]):item;
			countyCombo.store.loadData(data); //加载的一定要为数组,否则,则不成功
			countyCombo.setValue(-1);
		}
	}					
}


//直接使用下列方式,进行重置
// bform.setValues({province_id:3,city_id:39,county_id:336})
//在beforeshow事件中,进行调用。
getCityValue:function(id){
	var me=this;
	Ext.Ajax.request({
		url: SHINEVMSHTTP+'/admin/organization/Organization/cityvalue',
		method:'POST',
		params:{"GroupID":id},
		success: function(response, opts) {
			var obj = Ext.decode(response.responseText);
			console.log(obj);
			if(!obj){
				return ;
			}
			var bform = me.DepartmentWin.items.items[0].getForm();
			//直接使用下列方式,进行重置
			// bform.setValues({province_id:3,city_id:39,county_id:336})
			bform.setValues({province_id:obj.province_id,city_id:obj.city_id,county_id:obj.county_id});
		}
	});
},

loadData

me.down、me.query、getForm().findField(‘name’) 查找组件

查找组件,不只有一种方法。每种方法,都有各自的有缺点。但是最好,别用getAt索引的方式查找,代码扩展性不好,增加减少或者重新排序组件,都需要重新更改索引。

//常用的
me.down() //只能找到第一个组件
me.items.getAt(0) //通过索引查找
//根据特殊的属性查找,能保存找到的是唯一,但是注意,其为数组。
me.query('combo[name=doctor]')[0]
//如果是form组件,则可以使用下列方式查找。如果不是,理论上也能包裹一层form
getForm().findField('name')

//只能往查找
me.up()

filefield设置value(重点在思路)

核心的做法,找到dom,利用原生的js代码,修改dom值。

var bform=c.down('form').getForm();
bform.findField ('attachment').inputEl.dom.value= 12343431;

先说一下排查的思路。首先,为啥setValue不起作用?其实我也不知道。后来,直接在控制台输出,发现如下:
输入cmp.setValue,得到一个空函数。

ƒ () {}

输出cmp.getValue,得到如下

ƒ () {
        var me = this,
            val = me.rawToValue(me.processRawValue(me.getRawValue()));
        me.value = val;
        return val;
    }

输入,cmp.getRawValue,如下:

ƒ () {
        var me = this,
            v = (me.inputEl ? me.inputEl.getValue() : Ext.valueFrom(me.rawValue, ''));
        me.rawValue = v;
        return v;
    }

输入cmp.inputEl.getValue

ƒ (asNumber) {
            var value = this.dom.value;
            return asNumber ? parseInt(value, 10) : value;
        }

然后发现,实际上是dom的值。this指 cmp.inputEl。所以直接就修改了。(之前在Ext的源码里面,也试图找,结果没有找到)

所以,很重要的一点就是,学会利用控制台查看,函数的源码。

另外,之前做了另外一个版本的代码是真长。


var videoSrc=me.currentEditRecord.organization_video;
		var html='<div class="x-form-text-wrap x-form-text-wrap-default"><input type="text" value="'+videoSrc+'" readonly="readonly" class="x-form-field x-form-text x-form-text-default x-form-text-file  x-form-empty-field x-form-empty-field-default"></div>';

//这个是主要的逻辑,因为show的时候,要显示能改变的html的值
'show':function(){
	var video=me.DepartmentWin.down('filefield[name=organization_video]');
	var el=video.getEl();
	var div=me.DepartmentWin.inputDiv=el.down('#'+el.id+'-inputWrap');
	div.setStyle('display','none');
	me.DepartmentWin.inputNew=Ext.DomHelper.insertAfter(div,html,true);
}
    
//change后,就显示新的原本的textfield的原本的值

listeners:{
	'change':function(cmp, value, eOpts ){
		var div=me.DepartmentWin.inputDiv;
		div && div.setStyle('display','block');
		me.DepartmentWin.inputNew.setStyle('display','none');
	}
}

利用nginx在本地开发

按之前的套路,开发的时候利用netty,在本地跑一个web服务,跑前端服务,然后在首页index.html配置好后端的服务器,以后代码中所有的url,均利用该配置,这样凡是ajax调用的地方,均访问真实的后端服务。在去年沈阳加班的时候,那个时候就在想,如何整合前、后端的。

所以,我那时就想到了,用nginx将前后整合到一台服务器上。再后来,我又发现netty本身是个服务器,也能代理后端的请求。我记得我当时还作了笔记。有空,放到一起。

之前采取方式的弊端:

  • 前后端分离,ajax等存在跨域,虽然可以通过设置响应通来允许访问。但是对与session、cookie和Ext中的表单支持不好。十分不方便调试。如果,1、上传文件,能上传成功,但是具体的相应是不对的。2、form提交表单,也会返回错误。
  • 无法利用session,那么无法利用到角色、登录功能。那么开发过程中,只能手动的设置角色。先将角色写死,然后后期整合的时候,再进行调整,非常的不方便。

那么,具体说,nginx的代理服务。

  • 设置本地的开发域名host。win+r 运行,输入system32,依次找到drivers\etc,在host文件中,增加一行,dev.com。我host文件内容如下,最后一行是新添加的。

    127.0.0.1       localhost
    127.0.0.1       zend.scc
    127.0.0.1       www.openpoor.com
    127.0.0.1       www.myspace.com
    127.0.0.1       dev.com
  • 配置nginx的代理功能

    找到nginx目录,在conf/nginx.conf文件中,有设置。在http 大括号内,最末尾添加自己的代理内容。记得配置好后,运行 nginx.exe -t 进行测试配置文件。如果错误了,查看错误,并解决错误。

    server{
    	listen 80;
    	server_name dev.com;
    	location / {
    		proxy_pass http://localhost:1841/;
    	}
    	location /admin {
    		proxy_pass http://172.168.4.36/admin;
    	}
    }

    说明:默认将所有的访问都重定向到前端服务器locahost:1841,然后将以/admin开头的重定向到后端服务器172.168.4.36

    完美访问:

1560829022696

奇葩的代理需求

需求:同事要做版本的迁移工作,需要将数据从原有的系统中迁移到新版本中。所有就有了两个数据库。想根据不同的请求,使用不同的数据库。

核心原理是,添加不同的请求头,根据不同的请求头,选择不同的库。如下:

  • php代码:

    $params =  参数1 ;// 数据库参数
    if(isset($_SERVER["HTTP_DATABASE"]) && strpos($_SERVER["HTTP_DATABASE"],'shinejinyu') >= 0){
    	
    	$params  =  参数2;
    }
  • nginx配置:

    server{
    	listen 80;
    	server_name dev.com;
    	location / {
    		proxy_set_header database 'shinejinyu';
    		proxy_pass http://172.168.4.36/;
    	}
    }

php

Zend引入模块

Zend要求引入的模块,跟类的名称必须一致。否则会报如下错误。

1561019557714

显示的具体原因。

1561019586941

字符串变量

$fun='hello';
self::$fun();//会调用静态函数  hello,并输出。

数据导出功能

利用现成的PHPExcel功能,要求自己对原有的功能,再进行一次封装。所以,对调用者来说,更方便、更通用。满足两个功能:1、从数据库查出的数据,每行一条记录,填充到excel中;2、有简单的替换功能,通过设置header功能。

代码的通用性比较好,是要对其进行更高级的抽象。这个时候确实感受到了抽象的重要性。另外,这个代码核心的思想就是:1、用数组写一行数组。2、用数组写一列数据。3、用数组,指定起点,填充一列数据。这些都是要对原有提供的底层的api进行封装。

代码质量不高。但是应该从中学会抽象。

  • 遇到的第一个下马威:

错误提示如下,只能将570的break注释掉,然后能正常的生成文件呢。(不明白为啥会有这个错误)

Fatal error: 'break' not in the 'loop' or 'switch' context in D:\ShineVMS\xampp\htdocs\admin\library\PHPExcel\PHPExcel\Calculation\Functions.php on line 570

1561024246876

  • 准备工作
//入口文件,添加默认的库的导入路径 set_include_path 添加
set_include_path( . PATH_SEPARATOR . 'library/phpexcel')
//将库文件放到 library/phpexcel,大小写没有关系。
//引入库
Zend_Loader::loadClass('PHPExcel');
  • 核心php代码:
<?php
/*********
 * 2019年6月20日
 * 将从数据库查询出的数据导出为excel格式。 author 苏传超
*********/
class DataExportUtils{
	
	static protected $titleInfo = array(
		array('patient_name','患者',10),
		array('patient_sex','性别',10,array(1=>'男',2=>'女')),
		array('patient_birthday','年龄',10,'getBrithAge'),
		array('patient_id_card','身份证',10,'tostring'),
		array('patient_phone','手机号',10),
		array('department_name','科室',10),
		// array('conversation_id','病区',10),
		array('hospital','所属医院',10),
		array('doctor','医生',10),
		array('conversation_schedule_date','预约日期',10),
		array('visit_status','就诊状态',10,array(0=>'未预检',1=>'预检等候',2=>'叫号',3=>'过号',4=>'复诊',5=>'诊结',6=>'复诊等候')),//门诊(未预检=0,预检等候=1,叫号=2,过号=3,复诊=4,诊结=5,复诊等候=6)
		array('order_type','预约方式',10,array(1=>'预约机',2=>'手机',3=>'工作站预约')),  //预约机=1,手机=2,工作站预约=3
		array('pay_type','支付方式',10,array(1=>'微信',2=>'支付宝')), //微信=1,支付宝=2
		array('pay_status','支付状态',10,array(0=>'未支付',1=>'等待支付',2=>'已支付',3=>'等待退款',4=>'已退款')),  //未支付=0,等待支付=1,已支付=2,等待退款=3,已退款=4
		array('pay_ordernumber','订单号',10,'tostring'),
		array('pay_price','价格',10),
		array('pay_time','下单时间',10)
	);
	static protected $colIndex = array('A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','AA','AB','AC','AD','AE','AF','AG','AH','AI','AJ','AK','AL','AM','AN','AO','AP','AQ','AR','AS','AT','AU','AV','AW','AX','AY','AZ');
	/***********
	 * 2019年6月20日
	 * 导出数据
	***********/
	public static function exportData($records){
		// echo json_encode($records);exit;
		$objPHPExcel=new PHPExcel();
		$objPHPExcel->getProperties()
					->setCreator("神州视翰")
					->setLastModifiedBy("神州视翰")
					->setTitle("数据EXCEL导出")
					->setSubject("数据EXCEL导出")
					->setDescription("会议数据")
					->setKeywords("excel")
					->setCategory("result file");

		$objPHPExcel->setActiveSheetIndex(0);
		//设置表头
		foreach(self::$titleInfo as $k =>$v){
			$key = self::$colIndex[$k].'1';
			$value= $v[1];
			$objPHPExcel->getActiveSheet()->setCellValue($key, $value);
		}
		//填充数据
		foreach($records as $index=>$value){
			foreach(self::$titleInfo as $k =>$v){
				$key = self::$colIndex[$k].($index+2);
				$cellValue = self::convert($value[$v[0]],$k);
				$objPHPExcel->getActiveSheet()->setCellValue($key, $cellValue);// PHPExcel_Cell_DataType::TYPE_STRING
			}
		}
		//设置居左
		$objPHPExcel->getDefaultStyle()->getAlignment()->setHorizontal(PHPExcel_Style_Alignment::HORIZONTAL_LEFT);
		//设置列宽
		foreach(self::$titleInfo as $k =>$v){
			$key = self::$colIndex[$k];
			if(!empty($v[2])){
				$objPHPExcel->getActiveSheet()->getColumnDimension($key)->setWidth($v[2]);
			}
		}
		$objPHPExcel->getActiveSheet()->setTitle("会议导出记录");	
		$objPHPExcel->setActiveSheetIndex(0);
		$fileName = iconv('utf-8', 'gb2312',"会议导出");
		$time = date('Y-m-d-H-i-s',time()).".xls";
		
		//保存结果
		$objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel,'Excel5');
		$objWriter -> save($_SERVER['DOCUMENT_ROOT']."/".$fileName.$time);//			 
		$settingdata = array('code'=>0,'url'=>"会议导出".$time);
		return $settingdata;
	}
	/***********
		2019年1月7日
		获取数组的值
	***********/
	public static function getValue($map,$key){
		if(array_key_exists($key,$map)){
			return $map[$key];
		}
		return $key;//修改bug,原来是返回$map
	}
	/***********
	 * 2019年6月21日  
	 * 数值转换函数  苏传超
	***********/
	public static function convert($v,$index){
		if(empty(self::$titleInfo[$index][3])){
			return $v;
		}
		$cv=self::$titleInfo[$index][3];
		if(is_array($cv)){
			return self::getValue($cv,$v);
		}
		if(is_string($cv)){
			return self::$cv($v);
		}
	}
	/***********
	 * 2019年6月21日  
	 * 根据出生日期,求年龄  苏传超
	***********/
	public static function getBrithAge($v){
		return date('Y')-date('Y',strtotime($v));
	}
	public static function tostring($v){
		return "'$v";
	}
}

产生虚拟ip、mac地址

具体代码如下:

$ip=sprintf("0.%d.%d.%d",mt_rand(1,255),mt_rand(1,255),mt_rand(1,255));
$mac=sprintf("00:00:%02X:%02X:%02X:%02X",mt_rand(1,255),mt_rand(1,255),mt_rand(1,255),mt_rand(1,255));

上面的代码,虽然是简单,但是想记录当时的思维过程。使用了sprintf函数,第一次感觉到C语言的标准函数功能强大之处。前人,设计C语言函数库的时候,真是非常强大。记得有一次,用js做时间处理的时候,就在想js里面为啥没有这样的函数,所以只好手动补0,1:2转换为01:02的格式。

写代码,首先要有大局的思路。首先想大体的想,可能有几种方式。然后逐一考虑各个方法的优缺点。择优选择。上面代码的思维过程:1、将ip作为一个整体来处理。比如:192.168.2.235当成256进制的数,然后将产生的一个比较大的数字,再转换为ip,再进行分隔。所以,有点麻烦了不是。2、后来,中午吃饭,快走回来的时候,想到了,ip呢,当随机数处理。MAC地址,就当作字符处理。随机选择[0-9A-F]中的一个字符。再join。3、实际编码的过程中,简单粗暴,直接按上面的代码处理。反正为啥要用到sprintf函数,不知道,灵光一现吧。

(差点想要循环,join等函数呢。)

php中对应的序列解包功能 - 可变数量的参数列表

php中,好像没有办法,将一个数组,直接解包成逐个的变量,代入到函数的调用中。那么只好使用了替代的方法,call_user_func_array,跟js中的apply,有点像。算是一种替代的方法。

$params=array("00.00.%02X.%02X.%02X.%02X");
for($i=0;$i<substr_count($params[0], '%');$i++){
	$params[]=mt_rand(1,255);
}
echo call_user_func_array('sprintf',$params);

作为对比,看js的代码

function(a,b,c){return c}
test.apply(this,[1,2,3])  //3  这个跟call_user_func_array像。
test.call(this,1,2,3)    //这个是直接调用
test(...[1,2,3])        //其实,想要的是这个运算符

后来发现,php中也有类似的写发。叫可变数量的参数列表(PHP5.6版本以上)

//直接调用
echo sprintf('00.00.%02X.%02X.%02X.%02X',...array(1,2,3,4));

//就是将传入的参数变成一个索引数组
function app(...$num) {
    var_dump($num); //输出为一个索引数组
}
echo app(1, 2, 3, 4);
//就是传入一个数组,参数依次接收
function app($a, $b)
{
    echo $a + $b; //输出3
}
$arr = [1, 2];
app(...$num);

php上传文件格式判断

上传文件时,当然需要对上传文件的格式进行判断。之前一直采用获取文件的扩展名,转换小写,并判断是否在允许上传的格式中。然后,又发现另外一种好方法,好像浏览器会根据上传的文件,进行自动判断格式。type字段,有上传的格式信息。如下:

之前的老方法:

//允许上传的文件类型
if(!in_array($ext,array('.avi','.mov','.rmvb','.mpeg','mpg','mpe','.mkv','.flv','.m4v','.rm','.3gp','.dat','.mp4','.wmv'))){
	return array('msg'=>'文件上传失败,视频格式不支持!','success'=>false);
}
if(strpos($_FILES[$field]['type'],'video/')!==0){
	 return array('msg'=>'文件上传失败,视频格式不支持!','success'=>false);
}

php中的树数据结构,遍历

public static function getOrganization($structureId){
	$db = Zend_Registry::get('db'); 
	$rows=$db->fetchAll('select organization_id id ,organization_parent_id pid,organization_name name from  organization where structure_id =:id;',array('id'=>$structureId));
	$mapping = array_column($rows,'pid','id');
	self::$organizationData = $mapping;
	$result=array();
	foreach($mapping as $id=>$pid){
		$cur=$pid;
		$path=array($pid);
		while($cur>0){//末级节点
			$cur=$mapping[$cur];
			$path[]=$cur;
			if(count($path)>20){//防止死循环
				die("maximum recursion depth exceeded ");
			}
		}
		$result[$id]=join(',',$path);//路径
	}
	return $result;
}

分组统计

分组统计,则需要对数据创建数组。有可能是高维的。高维数组,注意初始化数组。js代码估计也差不多。

//对数据进行分组统计,创建一个三维的数组,然后每个数组叠加。

$data = array();  //data ['organization_id']['date'][]
foreach($rows as $k=>&$row){
	$oid=$row['organization_id'];
	$date = $row['conversation_schedule_date'];
	if(!isset($data[$oid])){//如果当前的,不存在,则进行初始化。
		$data[$oid]=array();
	}
	if(!isset($data[$oid][$date])){
		$data[$oid][$date]=array();
	}
	$data[$oid][$date][]=$row;
}
//最后再对每个数组,进行求和、或者处理。或者在上一步的时候,进行处理。

然后将多维的数组,再次降成一维数组,输出。

需要注意的时候,嵌套的循环,内部的foreach,则是外层的循环变量,如$oidArr。我在写的过程成,错写成了$data。

$result =array();
foreach($data as $oid=>$oidArr){
	foreach($oidArr as $date=>$dateArr){
		$basehospital = self::$organizationData[$oid];
		// {'date':'2019-07-01','basehospital':'华山医院','price':'11','count':'1'},
		$result[]=array('date'=>$date,'basehospital'=>$basehospital,'price'=>$dateArr['price'],'count'=>$dateArr['count']);
	}
}

数据库

sql中如果有BETWEEN关键字,由于其已经包含了and,所以整句应该添加一个括号,包裹。

and (visit_status BETWEEN 1 and 10)
  • sql替换

    两种其实差不多,后一种为了查询更多内容。

$sql = "select user_name from user_extend where user_id = (select user_id from schedule where schedule_id = $id)";

$sql= "select s.user_id,user_name from schedule  as s  left join user_extend as u  on s.user_id=u.user_id where schedule_id = $id";

typecho 源码解读(二)

接之前的源码分析,继续对其进行分析。

首页是如何显示出来的

需要经具体的路由呢。
Router.php中的注释(TODO 增加cache缓存)依然没有实现。

dispatch方法:

循环路由表,根据匹配到的值,匹配到出口,然后执行try中的语句。跟match方法很像。

/**
    * 路由分发函数
    *
    * @return void
    * @throws Exception
    */
   public static function dispatch()
   {
       /** 获取PATHINFO */
       $pathInfo = self::getPathInfo();

       foreach (self::$_routingTable as $key => $route) {
           if (preg_match($route['regx'], $pathInfo, $matches)) {
               self::$current = $key;

               try {
                   /** 载入参数 */
                   $params = NULL;

                   if (!empty($route['params'])) {
                       unset($matches[0]);
                       $params = array_combine($route['params'], $matches);
                   }

                   $widget = Typecho_Widget::widget($route['widget'], NULL, $params);

                   if (isset($route['action'])) {
                       $widget->{$route['action']}();
                   }

                   Typecho_Response::callback();
                   return;

               } catch (Exception $e) {
                   if (404 == $e->getCode()) {
                       Typecho_Widget::destory($route['widget']);
                       continue;
                   }

                   throw $e;
               }
           }
       }

       /** 载入路由异常支持 */
       throw new Typecho_Router_Exception("Path '{$pathInfo}' not found", 404);
   }

在路由分发之前,其实已经获取到了路由表。可是从哪一步获取到的呢?正常加载,config.inc.php然后到index.php。加载是在Typecho_Widget::widget(‘Widget_Init’);过程中有了路由表,那么全局Router::setRoutes只调用了一次。而其作用是设置路由表的。那么其又是如何被调用的?通过全局搜索,找到Init.php中有如下代码:

/** 初始化路由器 */
Typecho_Router::setRoutes($options->routingTable);

如果上述初始化代码被注释,有如下报错,而且还有404页面:


Notice: Undefined index: feed in D:\phpStudy\PHPTutorial\zend\typecho\var\Typecho\Router.php on line 176

Warning: Invalid argument supplied for foreach() in D:\phpStudy\PHPTutorial\zend\typecho\var\Typecho\Router.php on line 180

重点来了**Typecho_Widget::widget(‘Widget_Init’);**作用是,初始化,Widget文件夹下面的Init.php。并执行execute方法体。(稍后研究)

index.php中,调试路由表的过程:

//index.php
/** 开始路由分发 */
var_dump(Typecho_Router::getTable());
Typecho_Router::dispatch();

//在Router.php中增加方法,输出私有变量
public static function getTable()
{
	return self::$_routingTable;
}

路径获取,类似于,有限从类的缓存,静态变量中获取,如果没有,则重新获取(实际上是默认参数),代码如下(略过):

/**
 * 设置全路径
 *
 * @access public
 * @param string $pathInfo
 * @return void
 */
public static function setPathInfo($pathInfo = '/')
{
    self::$_pathInfo = $pathInfo;
}

/**
 * 获取全路径
 *
 * @access public
 * @return string
 */
public static function getPathInfo()
{	
	//优先从“缓冲”中读取,如果没有,则先设置“缓存”再读取
    if (NULL === self::$_pathInfo) {
        self::setPathInfo();
    }

    return self::$_pathInfo;
}

实例化Widget组件

在dispatch中,其实有这样的代码:

$widget = Typecho_Widget::widget($route['widget'], NULL, $params);

if (isset($route['action'])) {
    $widget->{$route['action']}();
}

即实例化路由表中的组件,已经调用action。打印(Router::dispatch中var_dump(self::$_routingTable);)得到路由表,如下:

["index"]=>
  array(6) {
    ["url"]=>
    string(1) "/"
    ["widget"]=>
    string(14) "Widget_Archive"
    ["action"]=>
    string(6) "render"
    ["regx"]=>
    string(8) "|^[/]?$|"
    ["format"]=>
    string(1) "/"
    ["params"]=>
    array(0) {
    }
  }

所以,首页会渲染Widget_Archive组件,并调用render方法。那么根据Typecho_Widget的定义,widget方法会检测,如果没有实例化这个变量,则实例化这个变量,然后调用execute方法。

Widget_Archive组件的继承关系:Widget_Archive→Widget_Abstract_Contents→Widget_Abstract→Typecho_Widget。所以首页的文章是这样的出来的。具体细节待再分析。

Widget_Init类

先贴代码:

/**
 * 初始化模块
 *
 * @package Widget
 */
class Widget_Init extends Typecho_Widget
{
    /**
     * 入口函数,初始化路由器
     *
     * @access public
     * @return void
     */
    public function execute()
    {
        /** 对变量赋值 */
        $options = $this->widget('Widget_Options');

        /** 检查安装状态 */
        if (!$options->installed) {
            $options->update(array('value' => 1), Typecho_Db::get()->sql()->where('name = ?', 'installed'));
        }

        /** 语言包初始化 */
        if ($options->lang && $options->lang != 'zh_CN') {
            $dir = defined('__TYPECHO_LANG_DIR__') ? __TYPECHO_LANG_DIR__ : __TYPECHO_ROOT_DIR__ . '/usr/langs';
            Typecho_I18n::setLang($dir . '/' . $options->lang . '.mo');
        }

        /** 备份文件目录初始化 */
        if (!defined('__TYPECHO_BACKUP_DIR__')) {
            define('__TYPECHO_BACKUP_DIR__', __TYPECHO_ROOT_DIR__ . '/usr/backups');
        }

        /** cookie初始化 */
        Typecho_Cookie::setPrefix($options->rootUrl);

        /** 初始化charset */
        Typecho_Common::$charset = $options->charset;

        /** 初始化exception */
        Typecho_Common::$exceptionHandle = 'Widget_ExceptionHandle';

        /** 设置路径 */
        if (defined('__TYPECHO_PATHINFO_ENCODING__')) {
            $pathInfo = $this->request->getPathInfo(__TYPECHO_PATHINFO_ENCODING__, $options->charset);
        } else {
            $pathInfo = $this->request->getPathInfo();
        }

        Typecho_Router::setPathInfo($pathInfo);

        /** 初始化路由器 */
        Typecho_Router::setRoutes($options->routingTable);

        /** 初始化插件 */
        Typecho_Plugin::init($options->plugins);

        /** 初始化回执 */
        $this->response->setCharset($options->charset);
        $this->response->setContentType($options->contentType);

        /** 初始化时区 */
        Typecho_Date::setTimezoneOffset($options->timezone);

        /** 开始会话, 减小负载只针对后台打开session支持 */
        if ($this->widget('Widget_User')->hasLogin()) {
            @session_start();
        }

        /** 监听缓冲区 */ 
        ob_start();//只有两处,另外一处是在install.php中
    }

ob_start缓存区开了,但是找不到关的地方。backup.php中确实有,但好像并不是总会调用。

ajax请求

查看,ajax请求,是如何响应的。那么第一个问题是,怎么鉴定一个请求是不是ajax请求?从network中,过滤xhr发现并没有,而登录好像并不是ajax请求?那么这个框架,能实现ajax请求吗?暂略。

Widget_Ajax这个好像是跟ajax相关的。但是这个类的方法比较少。那么猜想,这个Widget组件是如何被调用的呢?全局搜索Widget_Ajax只在Widget_Do extends Typecho_Widget中发现。这个类,有个映射关系变量,另外实现了一个execute方法。此文件的原理,有待考究。有一处重点代码,如下:

if (isset($widgetName) && class_exists($widgetName)) {
    //类反射
    $reflectionWidget =  new ReflectionClass($widgetName);
    //如果这个类实现了Widget_Interface_Do接口,则执行
    if ($reflectionWidget->implementsInterface('Widget_Interface_Do')) {
        $this->widget($widgetName)->action();
        return;
    }
}

Widget_Do这个只在Upgrade中使用。升级文件洋洋洒洒2千行,主要功能是对数据库文件进行转换格式等,确保数据库的格式兼容。以后有用到升级相关的,考究一下。

数据库的深层机理

一些特别的代码

switch

位于Archive.php中。switch一般都是一个变量,case是一个常量。而下面的true即为case表达式中的返回值情况,如果未真,则执行。不知道其他语言,有没有这个特点。

/** 判断聚合类型 */
switch (true) {
    case 0 === strpos($this->request->feed, '/rss/') || '/rss' == $this->request->feed:
        /** 如果是RSS1标准 */
        $this->request->feed = substr($this->request->feed, 4);
        $this->_feedType = Typecho_Feed::RSS1;
        $this->_currentFeedUrl = $this->options->feedRssUrl;
        $this->_feedContentType = 'application/rdf+xml';
        break;
    case 0 === strpos($this->request->feed, '/atom/') || '/atom' == $this->request->feed:
        /** 如果是ATOM标准 */
        $this->request->feed = substr($this->request->feed, 5);
        $this->_feedType = Typecho_Feed::ATOM1;
        $this->_currentFeedUrl = $this->options->feedAtomUrl;
        $this->_feedContentType = 'application/atom+xml';
        break;
    default:
        $this->_feedType = Typecho_Feed::RSS2;
        $this->_currentFeedUrl = $this->options->feedUrl;
        $this->_feedContentType = 'application/rss+xml';
        break;
}

php中的一个bug的排查过程

1、首先,根据出问题的地方,确定好文件。2、通过echo 来确定查找到的文件是否正常。通过在一段代码中,设置多出的echo,发现有的echo并未输出。3、然后以这种方式,确定了出问题的地方是一个函数。(差点误导我了,我最开始以为代码未执行完,是因为代码中有exit,所以主动放弃执行了,所以导致代码未执行完。没有想到出错导致代码未完成。)

  • 出问题的代码如下:
echo 2222;//这个正常输出了
$sock = socket_create(AF_INET,SOCK_STREAM,getprotobyname("tcp"));
echo 3333;//这个没有输出了
  • 这个时候才想到,显示所有的错误(有点晚了):
error_reporting(E_ALL);
  • 然后看到错误提示:
Call to undefined function socket_create() in 

然后开启php扩展:extension=php_sockets.dll

typecho 源码解读

typecho没有用到命名空间。此框架用到较多以类封装的静态函数的方式。解读这个框架的源码,首先,了解其大致的加载流程。其次呢,了解一些重要的类的加载,比如数据库的加载。再次,就是根据实际操作,查看请求的具体信息,来了解一个请求,是如何加载进来的。还有,主题里面的各个布局文件,是如何正确的加载进来。整个框架,一些重要的变量是如何传递的?

通过这个框架的研究,加上之前多媒体后台,前后端不分离的项目,php、js、html混合在一起,开发的难度确实大。如果选择的框架又不好用或者对代码没有一定的约束力,那么整体代码都失控了。

源码解读

入口文件index.php

首先,检测 __TYPECHO_ROOT_DIR__这个变量(全部文件都以此变量区分是否属于入口加载,)加载config.inc.php,判断结构 if( ! a && !b) 等价与 if(!(a || b)) ,即a,b只要有一个为真,就不执行判断内部的逻辑,也即__TYPECHO_ROOT_DIR__定义或者成功加载 config.inc.php,就不会进入if内部。但实际变量不存在,那么就看能不能加载到配置文件,config.inc.php,如果没有加载成功,则进重定向到安装模块,进行安装。

正常加载到配置文件,那么执行后面四个语言:

/** 初始化组件 */
Typecho_Widget::widget('Widget_Init');

/** 注册一个初始化插件 */
Typecho_Plugin::factory('index.php')->begin();

/** 开始路由分发 */
Typecho_Router::dispatch();

/** 注册一个结束插件 */
Typecho_Plugin::factory('index.php')->end();

那么就需要看看配置文件到底是啥?怎么能为上面四句提供环境的?

安装模块install.php

此文件代码较多,但是php代码200行,其他都是html。目的,如果没有定义config,那么正常安装。如果有config,检查发现有数据库,数据库中有install等字段,那么直接屏蔽用户的请求,给个404。
主要的逻辑是:定义根目录,设置系统加载的路径,定义根目录,(全局变量的形式: __TYPECHO_ROOT_DIR__),

  • 根目录、
  • 插件目录/usr/plugins
  • 模板目录/usr/themes
  • 后台目录/admin/。
    设置加载目录: /var(框架的主要目录),插件目录。 代码:
    //跟zendframework  很象
    @set_include_path(get_include_path() . PATH_SEPARATOR .
    载入API支持(var目录下), require_once ‘Typecho/Common.php’;
    然后调用程序初始化。Typecho_Common::init();

以上是不存在配置文件的执行方式,如果存在则加载执行下面的代码。

获取数据库对象,$db = Typecho_Db::get();
根据table.options表中installed字段,如果$installed为空值或者有值value=1都会进行404响应Typecho_Response::setStatus(404);,并退出。

正常往下执行的时候,有一端根据referer来阻止访问的代码:

// 挡掉可能的跨站请求
if (!empty($_GET) || !empty($_POST)) {
    if (empty($_SERVER['HTTP_REFERER'])) {
        exit;
    }

    $parts = parse_url($_SERVER['HTTP_REFERER']);
	if (!empty($parts['port'])) {
        $parts['host'] = "{$parts['host']}:{$parts['port']}";
    }

    if (empty($parts['host']) || $_SERVER['HTTP_HOST'] != $parts['host']) {
        exit;
    }
}

后面就是定义一些功能函数,然后进入到安装界面。代码分析不够透彻,先略过。

配置文件config.inc.php

配置文件是安装之后,产生的,跟上面安装文件,流程大致一样。程序的Typecho_Common::init();具体是什么作用?需要研究。
配置文件的代码:

/** 定义根目录 */
define('__TYPECHO_ROOT_DIR__', dirname(__FILE__));

/** 定义插件目录(相对路径) */
define('__TYPECHO_PLUGIN_DIR__', '/usr/plugins');

/** 定义模板目录(相对路径) */
define('__TYPECHO_THEME_DIR__', '/usr/themes');

/** 后台路径(相对路径) */
define('__TYPECHO_ADMIN_DIR__', '/admin/');

/** 设置包含路径 */
@set_include_path(get_include_path() . PATH_SEPARATOR .
__TYPECHO_ROOT_DIR__ . '/var' . PATH_SEPARATOR .
__TYPECHO_ROOT_DIR__ . __TYPECHO_PLUGIN_DIR__);

/** 载入API支持 */
require_once 'Typecho/Common.php';

/** 程序初始化 */
Typecho_Common::init();

/** 定义数据库参数 */
$db = new Typecho_Db('Pdo_SQLite', 'typecho_');
$db->addServer(array (
  'file' => 'D:/phpStudy/PHPTutorial/zend/typecho/usr/5cc69e9d3bb13.db',
), Typecho_Db::READ | Typecho_Db::WRITE);
Typecho_Db::set($db);

那么剩下的就是,看index.php的流程了。主要分为:

  • 程序初始化Typecho_Common::init();
  • 初始化组件Typecho_Widget::widget(‘Widget_Init’);
  • 注册一个初始化插件Typecho_Plugin::factory(‘index.php’)->begin();
  • 开始路由分发Typecho_Router::dispatch();
  • 注册一个结束插件Typecho_Plugin::factory(‘index.php’)->end();

程序初始化

Typecho_Common是一个类文件,类之前定义了一些函数。总代码比较多。init代码如下:

此代码的作用是,提供了一个自动加载类的方式。对比Zend、Thinkphp5.1、php-crud-api中加载的方式。同样也是注册一个加载函数,spl_autoload_register,另外,加上前面的自动加载目录,所以能自动加载到类。


/**
* 自动载入类
*
* @param $className
*/
public static function __autoLoad($className)
{
@include_once str_replace(array('\\', '_'), '/', $className) . '.php';
}

/**
* 程序初始化方法
*
* @access public
* @return void
*/
public static function init()
{
/** 设置自动载入函数 */
spl_autoload_register(array('Typecho_Common', '__autoLoad'));

/** 兼容php6 */
if (function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc()) {
    $_GET = self::stripslashesDeep($_GET);
    $_POST = self::stripslashesDeep($_POST);
    $_COOKIE = self::stripslashesDeep($_COOKIE);

    reset($_GET);
    reset($_POST);
    reset($_COOKIE);
}

/** 设置异常截获函数 */
set_exception_handler(array('Typecho_Common', 'exceptionHandle'));
}

初始化组件

Typecho_Widget::widget(‘Widget_Init’);

这代码Typecho_Widget::widget的大致所有是:静态化一个实例,并储存到主件池$_widgetPool变量中(私有静态变量)。如果这个类需要有多个实例的话,那么可以起一个别名,以@符号进行分隔,如Widget_Init@first,那么如果要获得这个实例,alias(‘Widget_Init’,’first’)。除此之外,还为组件自动绑定变量(跟传入的参数差不多),这个功能有点象容器注入(控制反转)。
此函数的注释如下:

   /**
    * 工厂方法,将类静态化放置到列表中
    *
    * @access public
    * @param string $alias 组件别名
    * @param mixed $params 传递的参数
    * @param mixed $request 前端参数
    * @param boolean $enableResponse 是否允许http回执
    * @return Typecho_Widget
    * @throws Typecho_Exception
    */
   public static function widget($alias, $params = NULL, $request = NULL, $enableResponse = true)
   {
   	//$className 跟$alias 以@分隔,否则,两者都是$alias
$parts = explode('@', $alias);
       $className = $parts[0];
       $alias = empty($parts[1]) ? $className : $parts[1];

       if (isset(self::$_widgetAlias[$className])) {
           $className = self::$_widgetAlias[$className];
       }
//1、从组件池中获取,如果不存在,那么先创建一个放到池子内。
       if (!isset(self::$_widgetPool[$alias])) {
           /** 如果类不存在 */
           if (!class_exists($className)) {
               throw new Typecho_Widget_Exception($className);
           }

           /** 初始化request */
           if (!empty($request)) {
               $requestObject = new Typecho_Request();
               $requestObject->setParams($request);
           } else {
               $requestObject = Typecho_Request::getInstance();
           }

           /** 初始化response */
           $responseObject = $enableResponse ? Typecho_Response::getInstance()
           : Typecho_Widget_Helper_Empty::getInstance();

           /** 初始化组件 */
           $widget = new $className($requestObject, $responseObject, $params);//注入了一些参数,有点象容器

           $widget->execute();
           self::$_widgetPool[$alias] = $widget;
       }
//2、直接从组件池中获取
       return self::$_widgetPool[$alias];
   }

从实际的请求中,发现manage-pages.php其实已经是一个独立的页面,那么此页面是如何完成页面渲染的呢?通过导入include ‘common.php’;完成页面的初始化工作。然后,发现下面的代码,作用是,将值分配到$pages对象中,然后遍历得到页面的结果。

Typecho_Widget::widget('Widget_Contents_Page_Admin')->to($pages)

上面的Request、Response都还未深入分析。那么,下一步:

注册一个初始化插件

Typecho_Plugin::factory(‘index.php’)->begin();插件代码功能大概有400行。

功能也是将插件进行了缓存。(缓存?是不是类似Zend_Register?也能集中管理?)

代码如下:

/**
* 获取实例化插件对象
*
* @access public
* @param string $handle 插件
* @return Typecho_Plugin
*/
public static function factory($handle)
{
	return isset(self::$_instances[$handle]) ? self::$_instances[$handle] :
	(self::$_instances[$handle] = new Typecho_Plugin($handle));
}
//Typecho_Plugin的构造函数
/**
* 插件初始化
*
* @access public
* @param string $handle 插件
*/
public function __construct($handle)
{
/** 初始化变量 */
	$this->_handle = $handle;
}

从上面的代码来看,注册一个插件,并没有做什么实质的动作。估计是在其他地方,方便调用。类似注册成全局的变量一个。(暂时这样猜测)。那么到下一步。

开始路由分发

Typecho_Router::dispatch();

这个类的代码两百多行。

代码如下:

/**
 * 路由分发函数
 *
 * @return void
 * @throws Exception
 */
public static function dispatch()
{
    /** 获取PATHINFO */
    $pathInfo = self::getPathInfo();

    foreach (self::$_routingTable as $key => $route) {
        if (preg_match($route['regx'], $pathInfo, $matches)) {
            self::$current = $key;//当前匹配到的路由表

            try {
                /** 载入参数 */
                $params = NULL;

                if (!empty($route['params'])) {
                    unset($matches[0]);
                    $params = array_combine($route['params'], $matches);
                }
                //同样,将当前匹配到的$route['widget'],注册成一个组件。
                $widget = Typecho_Widget::widget($route['widget'], NULL, $params);
  //如果当前路由有action属性,那么调用此方法
                if (isset($route['action'])) {
                    $widget->{$route['action']}();
                }
  //下面的,是如何获取到数据的??
                Typecho_Response::callback();
                return;

            } catch (Exception $e) {
                if (404 == $e->getCode()) {
                    Typecho_Widget::destory($route['widget']);
                    continue;
                }

                throw $e;
            }
        }
    }

    /** 载入路由异常支持 */
    throw new Typecho_Router_Exception("Path '{$pathInfo}' not found", 404);
}

那么在顺道看有一下Typecho_Response::callback()这个函数是做哪些东西?
代码如下:

/**
    * 结束前的统一回调函数
    */
   public static function callback()
   {
       //注意此变量为静态变量,相当于锁功能。即在一次请求中,后面的代码只执行一次。
       static $called;

       if ($called) {
           return;
       }

       $called = true;
       foreach (self::$_callbacks as $callback) {
           call_user_func($callback);
       }
   }
   //Typecho_Response类并不是真正的单例模式,因为其__construct、__clone没有做任何处理,也未定义。
   /**
    * 获取单例句柄
    *
    * @access public
    * @return Typecho_Response
    */
   public static function getInstance()
   {
       if (null === self::$_instance) {
           self::$_instance = new Typecho_Response();
       }

       return self::$_instance;
   }

另外,Typecho_Response类还有中写方法throwXml、throwJson、redirect、goBack

注册一个结束插件

Typecho_Plugin::factory(‘index.php’)->end();

有点迷糊了,Typecho_Plugin::factory(‘index.php’)返回的就是一个Typecho_Plugin,参数只是一个区分而已。而插件并没有begin、end方法。这是为什么呢?难道有更深的机制?

这个到底是什么?为什么能调用这样了两个方法begin、start?

类反射,查看一下,也没有什么想要的信息。

$tmp=Typecho_Plugin::factory('index.php');
var_dump(get_class_methods($tmp));

测试过了,如果注释掉注册插件这两行代码,其实对页面也没有任何影响。另外,随便写一个方法Typecho_Plugin::factory(‘index.php’)->ttt(),也不报错。这是因为使用了__call魔术方法。另外还使用了__set,__get魔术方法。如果将此Typecho_Plugin::__call方法注释掉,错误提示如下:

//下面这个是框架的报错提示,非系统提醒
Call to undefined method Typecho_Plugin::begin();

Typecho_Plugin::__call源代码(实现调用任意方法):

/**
     * 回调处理函数
     *
     * @access public
     * @param string $component 当前组件
     * @param string $args 参数
     * @return mixed
     */
    public function __call($component, $args)
    {
        $component = $this->_handle . ':' . $component;
        $last = count($args);
        $args[$last] = $last > 0 ? $args[0] : false;

        if (isset(self::$_plugins['handles'][$component])) {
            $args[$last] = NULL;
            $this->_signal = true;
            foreach (self::$_plugins['handles'][$component] as $callback) {
                $args[$last] = call_user_func_array($callback, $args);
            }
        }

        return $args[$last];
    }

那么到了这一步,自然想知道,此框架的插件机制是如何的?

官网给的helloworld例子,结合本地的helloworld插件/usr/plugins/HelloWorld的代码,

//
class HelloWorld_Plugin implements Typecho_Plugin_Interface
{
    /**
     * 激活插件方法,如果激活失败,直接抛出异常
     * 
     * @access public
     * @return void
     * @throws Typecho_Plugin_Exception
     */
    public static function activate()
    {
        //这其实就是设置了一个回调函数。navBar确实在admin/menu.php中调用了。
        Typecho_Plugin::factory('admin/menu.php')->navBar = array('HelloWorld_Plugin', 'render');
    }
  • activate: 插件的激活接口,主要填写一些插件的初始化程序。
  • deactivate: 这个是插件的禁用接口,主要就是插件在禁用时对一些资源的释放。
  • config: 插件的配置面板,用于制作插件的标准配置菜单。
  • personalConfig: 个人用户的配置面板,基本用不到。
  • render: 自己定义的方法,用来实现插件要完成的功能。

admin/menu.php

在此处调用:navBar(); ?>

<?php if(!defined('__TYPECHO_ADMIN__')) exit; ?>
<div class="typecho-head-nav clearfix" role="navigation">
    <nav id="typecho-nav-list">
        <?php $menu->output(); ?>
    </nav>
    <div class="operate">
        <?php Typecho_Plugin::factory('admin/menu.php')->navBar(); ?><a title="<?php
                    if ($user->logged > 0) {
                        $logged = new Typecho_Date($user->logged);
                        _e('最后登录: %s', $logged->word());
                    }
                    ?>" href="<?php $options->adminUrl('profile.php'); ?>" class="author"><?php $user->screenName(); ?></a><a class="exit" href="<?php $options->logoutUrl(); ?>"><?php _e('登出'); ?></a><a href="<?php $options->siteUrl(); ?>"><?php _e('网站'); ?></a>
    </div>
</div>

所以呢,结合此插件,大致明白了注册开始、结束插件的意思呢,反正就是预留了一些代码钩子,如果用户确实实现了这个插件,那么就调用了。

理解这种插件机制的作用?为啥这样设计?

个人理解,其实这样设计并没有什么太大的好处。插件嘛,类似于AOP,在固定的位置调用方法。其实也能理解js的事件回调。在固定的位置触发一个事件,然后响应这个事件。测试过,确实,一个地方,触发了这个插件,多个插件都响应了。插件代码中的注释,居然还能作为内容显示在页面中。

页面流转

发现这个框架的页面跳转,其实跟之前想得很不一样。首先,管理后台,是要跳到/admin/文件夹中,然后就不再是统一的入口呢。

/admin文件夹中,有两个文件,common-js.php跟common.php。估计前面的是为了加载js用的,后面,相当于一个入口函数。

common-js.php

php往js脚本中注入了一些变量。js脚本功能:处理消息机制(立即执行函数形式,防止变量泄漏成全局)、导航菜单 tab 聚焦时展开下拉菜单。加载js库jquery.js、jquery-ui.js、typecho.js。

common.php

数据库操作

定义数据库

/** 定义数据库参数 */
$db = new Typecho_Db('Pdo_SQLite', 'typecho_');
$db->addServer(array (
  'file' => 'D:/phpStudy/PHPTutorial/zend/typecho/usr/5cc69e9d3bb13.db',
), Typecho_Db::READ | Typecho_Db::WRITE);
Typecho_Db::set($db);

获取数据库对象实例:

/** 初始化数据库 */
$this->db = Typecho_Db::get();

知识

PHP获取当前类名、方法名
  __CLASS__ 获取当前类名
  __FUNCTION__ 当前函数名(confirm)
  __METHOD__ 当前方法名 (bankcard::confirm)
__FUNCTION__    函数名称(PHP 4.3.0 新加)。自 PHP 5 起本常量返回该函数被定义时的名字(区分大小写)。在 PHP 4 中该值总是小写字母的。
__CLASS__    类的名称(PHP 4.3.0 新加)。自 PHP 5 起本常量返回该类被定义时的名字(区分大小写)。在 PHP 4 中该值总是小写字母的。
__METHOD__    类的方法名(PHP 5.0.0 新加)。返回该方法被定义时的名字(区分大小写)。

get_class(class name);//取得当前语句所在类的类名
get_class_methods(class name);//取得class name 类的所有的方法名,并且组成一个数组
get_class_vars(class name);//取得class name 类的所有的变亮名,并组成一个数组

反射的形式获取:

//用反射类可以获得私有属性和私有方法
$ref = new ReflectionClass(Class1);//Class1 可以为对象实例 $class = new Class();
 
print_r($ref->getMethods());
print_r($ref->getProperties());

右键菜单

在grid表格中,经常用到右键菜单,一般的右键菜单如下,但是存在问题,每次创建右键时,都会创建一个新的右键菜单实例,虽然没有啥大的问题(存在对象泄漏),本着负责的态度,在隐藏的时候,主动删除这个对象:

另外一种常见的处理方式是,像之前其他的一样,在主grid表格创建时,手动创建一个实例,每次右键的时候,调用这个实例的show方法。就不存在泄漏。但在grid表格销毁的时候,所有手动create的组件都要手动销毁。

//右键菜单
"rowcontextmenu":function ( c, record, tr, rowIndex, e, eOpts ){
    var me = this;
    var v=me.getViewModel().getData();
    //增加右键菜单可用情况判断
    Ext.create('Ext.menu.Menu', {
        bodyStyle:"padding:5px;",
        items: [{
            text: '编辑护士',
            iconCls:'edit',
            disabled:v['selections']!=1,
            handler:function(btn){
                me.addNurseWin.animateTarget = btn;
                me.addNurseWin.actionType=2;
                me.addNurseWin.show();
            }
        },{
            text: '删除护士',
            iconCls:'delete',
            disabled:v['selections']==0,
            handler:function(){
                me.confirmDelete();
            }
        }],
        //下面的代码,hide时主动删除此菜单。
        listeners:{
            'destroy':function( cmp, eOpts) {
                console.log('我销毁啦');
            },
            hide:function ( cmp, eOpts ){
                console.log('我隐藏啦');
                this.destroy();
            }
        }
    }).showAt(e.getXY());
    e.preventDefault();
}

tbar中用到的菜单实例

代码如下,重点关注,button中menu的写法结构。(button有menu这个属性)。

{
    text:'护理菜单',
    iconCls:"grade",
    menu:{
        defaults: {
            iconCls:'icon-book'
        },
        items:[{
            text:'护理文书',
            handler:function(btn){
                var menu=btn.up('button');
                var win=Ext.widget({
                    xtype:'window',
                    title:'护理文书',
                    layout:'fit',
                    width:650,
                    height:500,
                    modal:true,
                    resizable:false,
                    animateTarget:menu,
                    items:[{
                        xtype:'descriptiongrid',
                        width:'100%',
                    }]
                });
                win.show();
            }
        },{
            text:'护理等级',
            handler:function(btn){
                var menu=btn.up('button');
                var win=Ext.widget({
                    xtype:'window',
                    title:'护理等级',
                    layout:'fit',
                    width:650,
                    height:500,
                    modal:true,
                    resizable:false,
                    animateTarget:menu,
                    items:[{
                        xtype:'nursinggradegrid',
                        width:'100%',
                    }]
                });
                win.show();
            }
        }]
    }
}

Ext.Deferred使用教程

前几天写的,今天才贴出来。主要是Deferred的使用,这个需要与ES6的语法进行对比学习。

生成deferred

下面是个简单的例子,参考手册上新的。

deleteUserDevices:function(){
        var me=this;
        var deferred = new Ext.Deferred();
        if(!me.userDeviceList){
            return deferred.reject("未选择任何数据.");
        }
        var ids=Ext.Array.map(me.userDeviceList,function(item){
            return item.id;
        });
        Ext.Ajax.request({
            method:"DELETE",
            url:SHINEECALLHOST+'/api.php/records/hospital_user_devices/'+ids.join(),
            success: function(response, opts) {
                console.log(response.responseText);
                deferred.resolve();
            }
        });
        return deferred.promise;
    },

触发deferred

使用下面的方式,进行触发。注意参数形式,deleteUserDevices函数内部的this,需要根据第二个参数确定。

Ext.Deferred.sequence([me.deleteUserDevices,me.submitUserDevices],me);

使用感受

对于不需要异常捕捉的话,还不如直接传入一个函数参数进去。这样没有什么理解难度。即如下:

submitUserDevices:function(id,fn){
        var me=this;
        Ext.Ajax.request({
            method:"POST",
            params:{DeviceID:id,UserID:me.currentEditRecordId},
            url:SHINEECALLHOST+'/api.php/records/hospital_user_devices/',
            success: function(response, opts) {
                console.log(response.responseText);
		fn();//调用回调函数
            }
        });
    },

后来发现,删除、添加新的,之间没有严格的顺序依赖。

fieldcontainer使用

一直没有实现的布局样式,即文本输入框,跟按钮在一行内的布局,终于实现了。fieldcontainer有fieldLabel属性,属于整行的标签,这样整体布局不会乱。具体如下:

{
    xtype: 'fieldcontainer',
    layout: "hbox",
    //1 注意下面,标签放在1位置跟2位置有很大的差别。
    fieldLabel: '责任医生',
    items: [{
            xtype: 'textfield',
	    //2 这个标签就不用了。
            flex: 1,
            name: 'Doctor_Name'
        }, {
            xtype: "button",
            text: "添加",
            margin: '0 0 0 10',
            handler: function () {
                // me.priceProductWindow.show();
            },
            width: 100,
        }]
}

另外,见别人这样写,利用panel作为容器,严格控制各个宽度,也能达到效果。代码如下:

{
	xtype:"panel",
	layout:"hbox",
	height:30,
	border:0,
	items:[{
		width:363,
		xtype:'textfield',
		labelWidth:70,
		editable:false,
		fieldLabel: SHINEECALL_PATIENT_PATIENTINFO_PATIENTFORM_PATIENT_DOCTOR,
		labelAlign:"right",
		name: 'Doctor_Name'
	},{
		xtype:"button",
		text:SHINEECALL_PATIENT_PATIENTINFO_PATIENTFORM_SELECT_DOCTOR,
		style:{
			'margin-left':"10px",
			width:"90px"
		},
		handler:function(){
		}
	}]
}

行编辑器中难排查的bug

一般来说store中的fields可以不写。但是在行编辑器中,store的fields字段必须要填。否则,context.record始终没有任何变化。而且还很难排查。

store:{
	//行编辑器中的fields必须要填
	fields:[
	    {name: 'Grade_ID',type: 'int'},
	    {name: 'Grade_Name',type: 'string'},
	    {name: 'Grade_Color',type: 'string'},
	    {name: 'Grade_Background',type: 'string'}
	],
//否则下面的context,context.record始终没有变化,而且也没有触发验证
//记录上也没有红点。
 listeners:{
        "edit":function(editor, context, eOpts){
            var me=this;
            //点击保存触发ajax请求
            var record =context.record;
            console.log(record['data']);

表格渲染效果

一般常用renderer来进行数据的转换展示,比如,将1、2转换成男、女展示。但此函数还有很强大的功能,比如更改展示的效果,在renderer返回值中,添加要插入的图标,一并插入的展示中。下面两个示例。

  • 结果转换,记得,这个转换好像会污染到变量。
renderer:function(v){
    //下面的这个m也能使用全局或者类成员的属性,或者
    //通过ajax从后台获取,完成 一对多的 联表效果(但是性能嘛,不高。)
    var m={1:'男',2:'女'};
    return m[v];
},
  • 这个是通过返回值来进行更改效果。
{
    text: "字体颜色",
    width:100,
    sortable: false, 
    dataIndex: 'Grade_Color',
    editor: {
	allowBlank: false//要求数据不能为空。
    },
    //这个是通过返回值来进行更改效果。
    renderer:function(v){
	var m = '<div style="background:'+v+'">'+v+'</div>';
	return m;
    }
},
  • 通过metaData更改效果。 metaDa.tdCls 指定类名、metaData.tdStyle写css样式(能批量)、metaData.tdAttr指定一个属性(不能批量)。下面使用了两种方式:
 {
    text: "背景颜色",
    width:100,
    sortable: false, 
    dataIndex: 'Grade_Background',
    editor: {
	allowBlank: false//要求数据不能为空。
    },
    renderer:function(v,metaData){
        //注意,下面是使用了 等号=
	//这种只更改一种属性,写法比较简单。
	metaData.tdAttr = 'bgcolor="' + v + '"';
	return v;
    }
},
{
    text: "预览",
    width:100,
    sortable: false,
    dataIndex: 'Grade_Background',
    //metaData.tdStyle  可以直接写css,记得使用冒号:分隔,另外
    //background-color不能写成backgroundColor,这个地方不会进行转换。
    renderer:function(v,metaData,record){
	var color=record.get('Grade_Color');
	metaData.tdStyle='background-color:'+ v +';color:'+color+';';
	return '示例效果';
    }
},

dataview 选择功能

在查看dataview的帮助时,发现其有selectionModel这个属性。既然有这个属性,就想设置一些选择特性。但是遗憾的是,没有成功。虽然,用dataview的getSelection()、getSelectionModel()来获取,选择模型确实生效了,但是效果却没有。而且测试时,dataview的并未添加任何listeners事件。我猜测,可能跟选择模型有关,这个选择模型,可能没有这个功能。测试使用了其他的选择模型,没有报错,但是一样没有效果。
beforeselect方法,在选择边框的时候,也并不能触发。

看来还是只能使用以前的土方法呢。(之前记载过这个方法,即桌面版选择会议室功能)

由此,应该关注SelectionModel的使用方法。类的继承关系。

selectionModel:{
    type: 'dataviewmodel',
    allowDeselect:false,
    mode : "MULTI" ,
    toggleOnClick:true
}

但是,改进了dataview的保持选择一个的功能,可以不用引入一个变量保存上次选择的。直接用lastSelected

xtype:"dataview",
listeners:{
    'selectionchange':function(cmp, selected, eOpts ){//只选择一个。
	var selModel=this.getSelectionModel();
	if(selected.length===0){//没有选择项,则不能取消
	    if(selModel.lastSelected){
		selModel.select(selModel.lastSelected);
	    }
	}
    },
},

select 不要触发事件

第一次选择的时候,不希望触发select事件,之前想到过,使用一个标志位进行标记,但突然想到,框架里面好像有忽略触发消息的功能。然后隧完成如下。

如果不加这个参数,在这个颜色选择模块会造成事循环。

selectColor:function(){
        var me=this;
        var color = me.getValue();
        var dv=me.popupWin.down('dataview');
        var selRd=null;
        dv.store.each(function(rd){//查找选中的record
            if(rd.get('color')==color){
                selRd=rd;
                return false;
            }
        });
        if(selRd){
	    //这个是初始化的时候,使用的,所以呢,应该忽略触发消息。
            dv.getSelectionModel().select(selRd,false,true);
        }else{
            dv.getSelectionModel().deselectAll(true);
        }
    }

rowedit

对与每个editor而言,生命周期比较长,在rowedit所属的grid的销毁时,才销毁。