侧边栏壁纸
博主头像
落叶人生博主等级

走进秋风,寻找秋天的落叶

  • 累计撰写 130562 篇文章
  • 累计创建 28 个标签
  • 累计收到 9 条评论
标签搜索

目 录CONTENT

文章目录

【React教学】通用型DataTable组件——400行内

2024-05-12 星期日 / 0 评论 / 0 点赞 / 93 阅读 / 25633 字

其实严格意义来说,应该将Pagination(分页处理)和数据加载(AjaxLoad)作为一个独立的组件来处理,不过为了方便展示,就一股脑都做在这个Table里面了。 目前只实现到整个Table的数据

其实严格意义来说,应该将Pagination(分页处理)和数据加载(AjaxLoad)作为一个独立的组件来处理,不过为了方便展示,就一股脑都做在这个Table里面了。

目前只实现到整个Table的数据加载,不包含单独更新某行某个单元格数据的状态处理。

这一次用到的类库也比较多,这里先汇总一下:

npm install webpack webpack-dev-server react react-dom jquery lodash babel-loader babel-preset-es2015 babel-preset-react babel-plugin-transform-class-properties babel-plugin-transform-es2015-block-scoping babel-plugin-transform-es2015-computed-properties --save-dev

对,没错,好多,好啰嗦。

先上张预览图:

因为涉及到客户的资料,所以数据打了模糊滤镜了。

这个Table组件包含的特性:

  1. Ajax翻页
  2. 指定checkbox的字段
  3. checkbox翻页会保存(就是多页的checkbox的记录都维持着),这点对于大规模数据校对的时候很必要。
  4. 点击行选中checkbox
  5. 选中行(checkbox)更改行样式
  6. 支持数据排序,实际上数据排序用的是jquery-tablesort,没有嵌入到组件中,一个只有126行的table排序,非常实用。不过其实如果允许单元格数据更新的话,排序就要嵌入在renderTableBodyRows的方法中了。
  7. 有几个基本的事件,onMount,onInit,onRow,onFoot。
  8. 整个Table的操作(翻页)都是无刷新的。

其实要做一个满足各方面使用的Table组件,事情并不简单。除了Table组件外,我还定义了两个公共的方法,并放在了整个项目的入口文件中(webpack的entry),详情如下:

var _ = require('lodash');var jQuery = require('jquery');var React = require('react');var ReactDOM = require('react-dom');window.jQuery = jQuery;window._ = _;window.php = require('phpjs');window.filterContent = function filterContent(value, column) {	if (_.isObject(value)) {		if (_.isArray(value)) {			return value;		}		else if (React.isValidElement(value)) {			return value;		}		else {			if (value instanceof Date) {				return php.date(column.format || this.props.defaultDateFormat || 'Y-m-d H:i:s', value);			}			else {				return value.toString();			}		}	}	if (column) {		if (column.options && column.options[value])			return column.options[value];		else if (column.datetime && value) {			var timestamp = php.strtotime(value);			if (timestamp > 0)				return php.date(column.format || 'Y-m-d H:i', timestamp);			return '';		}	}	return value;};window.tag = function tag(tag, content, props) {	return React.createElement(tag, props || {}, filterContent(content));};var Table = require('./components/Table.jsx');

React开发要点精讲

filterContent函数

filterContent方法,用于过滤指定的内容,这里的过滤指将内容过滤为符合React.isValidElement的内容,并可以嵌入在react的html标签中的内容。

这里其实是React很重要需要掌握的一个技巧。JS中有几种变量的类型:字符、数值、NULL、Boolean、Array、Object。除了Object以外,其他的类型都可以直接插入到react的html中作为内容使用。这里所谓直接插入,包括以下两种形式:

1. html方式(JSX)

<div>{filterContent(content)}</div>

2. 使用React的JS API方法

React.createElement('div', {}, filterContent(content));

尤其注意,所有Object类型,除了React.isValidElement()判断为有效的对象以外,直接用上述两种方法作为标签内容使用,都会抛出React的异常。包括正则表达式和Date对象。

filterContent方法允许传入第二个参数,就是对过滤内容的一些配置参数。这个函数其实是我正式版本的一个缩略版本,但其实已可以用于实用了。

补充说一下,判断为数组的时候,最好打扁遍历这个数组,然后依次将数组元素放入filterContent中执行,最后返回过滤完毕的数组即可,React会进行后续的处理。

tag函数

这个函数其实就是对外讲React.createElement的方法精简化输出实现的一个方法,因为直接生成有状态的DOMElement实在太实用了,让人忍不住想在任意地方去使用。实际上这个方法,就是上面说的创建React HTML标签的方法二。

第一个参数,其实是可以直接传入你自己自定义的React.Component的。

第二个参数就是要插入的内容

第三个参数是这个标签的属性,也就是React组件的props。

获取组件的DOM对象

这里需要粗略的说一下ReactComponent(ReactClass)从实例化->DOM实例化的状态切换。

我们在使用React.createClass或者extends React.Component时,定义大多数的方法和属性,面向的是DOM实例化状态下的对象的方法和属性。

在我们执行React.createElement(或者你直接new ReactComponent)的时候,实际上是创建了这个Component(ReactClass)的普通实例化对象,这个对象并不具备完整的属性(props)、状态(state),以及你所定义的方法。

在React中,并不推荐直接去操作这个普通的实例化对象,也没有提供太多的接口给你去操作。React认为只有渲染到DOM节点树上的对象才是有效的控制对象。如下:

var el = ReactDOM.render(<HelloWorld />, document.getElementById('test'));

ReactDOM.render返回的,才是一个DOM实例化的对象。

而<HelloWorld />则是普通实例化对象。

那么在已经生成了实例化对象的时候,我们该如何获得这个实例化对象所关联的DOM节点呢?

// 接着上一段代码var domEl = ReactDOM.findDOMNode(el);

但是要注意,React有一套很严密的状态机处理的方法,有效的获取到这个DOM实例化对象的DOM节点,必须确保在componentDidMountcomponentDidUpdate之后,否则也会报异常。

好,今天要讲的内容基本上就到这,下面是Table组件的代码(Table.jsx):

var React = require('react');var ReactDOM = require('react-dom');var _ = require('lodash');var php = require('phpjs');var $ = require('jquery');class Table extends React.Component {	static defaultProps = {		columns: {},		mergeColumns: {},		data: [],		pageData: {},		pageLinksCount: 10,		pageOffset: 1,		url: '',		ajaxLoad: false,		ajaxGetColumns: true,		onInit: null,		onRow: null,		onFoot: null,		onMount: null,		checkbox: null,		checked: [],		thEmpty: '未指定表字段',		tdEmpty: '未指定表数据',		defaultDateFormat: 'Y-m-d H:i:s'	};	id = 0;	updateMount = false;	constructor(props) {		super(props);		this.id = _.uniqueId('table_');		this.state = {			error: null,			ajaxLoading: false,			ajaxGetColumns: true,			columns: this.props.columns,			mergeColumns: this.props.mergeColumns,			data: this.props.data,			pageData: this.props.data,			goPage: 0,			checked: this.props.checked		};	}	makeKey() {		return this.id + '_' + _.flattenDeep(arguments).join('_');	}	getCheckboxField() {		if (this.props.checkbox && this.state.columns[this.props.checkbox])			return 'checkbox_' + this.props.checkbox;		return false;	}	getFields() {		var fields = Object.keys(this.state.columns), checkboxField = this.getCheckboxField();		if (checkboxField !== false)			fields = [checkboxField].concat(fields);		return fields;	}	getColumn(field) {		let column = this.state.columns[field] || {}, checkboxField = this.getCheckboxField();		if (field === checkboxField) {			column.label = <input type="checkbox" value="check_all"			                      onChange={(e) => this.checkAll(e.target.checked)} checked={this.isCheckedAll()}/>		}		else {			if (_.isString(column))				column = {label: column};			else if (!_.isObject(column))				column = {label: field};			if (!column.label)				column.label = field;			if (this.state.mergeColumns[field])				column = _.merge(column, this.state.mergeColumns[field]);			if (_.isString(column))				column = {label: column};			else if (!_.isObject(column))				column = {label: field};			if (!column.label)				column.label = field;			if (this.state.mergeColumns[field])				column = _.merge(column, this.state.mergeColumns[field]);		}		return column;	}	loadData(page) {		this.setState({ajaxLoading: true});		$.ajax({			url: this.props.url,			data: {				columns: this.state.ajaxGetColumns ? 1 : 0,				page: page || 1			},			dataType: 'json'		}).success((data) => {			data.ajaxGetColumns = false;			data.ajaxLoading = false;			data.goPage = data.pageData.pageNumber || 1;			this.setState(data);		}).fail(() => {			this.setState({error: '网络错误,请重新尝试!'});		})	}	getData() {		return this.state.data || [];	}	isChecked(item) {		return this.getCheckboxField() !== false && this.state.checked.length > 0 && _.indexOf(this.state.checked, item + '') > -1;	}	isCheckedAll() {		var isChecked = false, checkboxField = this.getCheckboxField(), field = this.props.checkbox,			data = this.getData(), length = data.length, counter = 0;		if (checkboxField === false || this.state.checked.length <= 0)			return false;		_.each(data, (row) => {			if (row[field] && this.isChecked(row[field]))				counter += 1;		});		return counter >= length;	}	checkAll(isCheck) {		var items = [], checkboxField = this.getCheckboxField(), field = this.props.checkbox;		if (checkboxField !== false)			_.each(this.getData(), function (row) {				if (row[field])					items.push(row[field]);			});		return this.checkItem(items, isCheck);	}	checkItem(item, isCheck) {		isCheck = !!isCheck;		let checked = this.state.checked;		if (!_.isArray(item))			item = [item];		_.each(item, function (it) {			it = it + '';			if (isCheck) {				if (_.indexOf(checked, it) < 0)					checked.push(it);			}			else {				var index = _.indexOf(checked, it);				if (index > -1)					checked.splice(index, 1);			}		});		this.setState({checked: checked});		return this;	}	checkRow(event, value) {		var target = event.target, tag = target.tagName.toLowerCase();		if (tag !== 'input' && tag !== 'a' && tag !== 'button') {			this.checkItem(value, !this.isChecked(value));		}	}	dom() {		return ReactDOM.findDOMNode(this);	}	changeGoPage(value) {		var pageData = this.state.pageData;		if (isNaN(value))			value = pageData.pageNumber || 1;		this.setState({goPage: value});	}	componentDidMount() {		var data = this.getData();		if (this.props.ajaxLoad && this.props.url && data.length <= 0)			this.loadData(this.props.pageOffset);	}	componentDidUpdate() {		if (this.getData().length > 0 && _.isFunction(this.props.onMount))			this.props.onMount.call(this, ReactDOM.findDOMNode(this));	}	renderTableHead() {		return <thead>		<tr>			{this.renderTableHeadCells()}		</tr>		</thead>;	}	renderTableHeadCells() {		let fields = this.getFields(), length = fields.length;		if (length <= 0) {			if (this.state.ajaxLoading) {				return <th>正在获取数据,请稍候……</th>;			}			return <th>				<div className="at-table-empty">{filterContent(this.props.tdEmpty)}</div>			</th>;		}		return this.getFields().map((field) => {			let column = this.getColumn(field), isSort = typeof column.sort === 'undefined' || !!column.sort,				className = !isSort || field === this.getCheckboxField() ? 'no-sort' : 'sort-head';			return <th key={this.makeKey('head', field)} data-field={field} className={className}>				{this.getColumn(field).label}			</th>;		});	}	renderTableBody() {		return <tbody>{this.renderTableBodyRows()}</tbody>	}	renderTableBodyRows() {		let fields = this.getFields(), data = this.getData(), length = data.length,			checkboxField = this.getCheckboxField(), checkbox = this.props.checkbox;		if (length <= 0) {			if (this.state.ajaxLoading) {				return <tr>					<td>						<div className="at-ajax-loading">正在加载表数据,请稍候……</div>					</td>				</tr>;			}			return <tr>				<td>					<div className="at-table-empty">{filterContent(this.props.tdEmpty)}</div>				</td>			</tr>;		}		if (_.isFunction(this.props.onInit))			this.props.onInit.call(this, data);		return this.getData().map((row, i) => {			var clone = _.clone(row);			if (_.isFunction(this.props.onRow)) {				this.props.onRow.call(this, row, clone);			}			return <tr key={this.makeKey('tr', i)}			           className={clone[checkbox] && this.isChecked(clone[checkbox]) ? 'at-row-checked' : ''}			           onClick={(e) => this.checkRow(e, clone[checkbox])}>				{					fields.map((field) => {						let value = clone[field] || null;						let data = {							value: value,							text: value,							field: field,							index: i						};						if (this.props.onRow[field] && _.isFunction(this.props.onRow[field])) {							this.props.onRow[field].call(this, data, row);						}						if (field === checkboxField) {							return <td key={this.makeKey('td', i, field)}							           data-field={field}>								<input type="checkbox" value={clone[checkbox]} key={this.makeKey('checkbox_', i, field)}								       checked={this.isChecked(clone[checkbox])}								       onChange={(e) => this.checkItem(clone[checkbox], e.target.checked)}/>							</td>;						}						else {							return <td key={this.makeKey('td', i, field)} data-field={field} data-sort-value={data.value}>								{filterContent(data.text, this.getColumn(field))}							</td>;						}					})				}			</tr>;		});	}	renderTableFoot() {		var foot = {			data: {},			show: false		}, fields = this.getFields();		if (_.isFunction(this.props.onFoot))			this.props.onFoot.call(this, foot);		if (foot.show) {			return <tfoot>			<tr className="at-sum-row">				{					fields.map((field) => {						return <td key={this.makeKey('tfoot', field)}>{foot.data[field]}</td>					})				}			</tr>			</tfoot>;		}	}	renderPagination() {		let pageData = this.state.pageData, links = [], tail = [];		if (pageData.pageNumber > 0 && pageData.pageSize > 0) {			var current = parseInt(pageData.pageNumber), linksCount = parseInt(this.props.pageLinksCount),				middle = parseInt(linksCount / 2),				total = pageData.pageCount, start = 1, end = linksCount;			if (total > linksCount) {				if (current >= middle) {					start = current - (middle - 1);					end = linksCount + start - 1;					if (start > 1) {						links.push(<li className="pagination-item" key={this.makeKey('page_item_', 1)}>							<a href={'#page/' + (1)} onClick={() => this.loadData(1)}>{1}</a></li>);						end -= 1;					}					if (start > 2)						links.push(<li className="pagination-item pagination-item pagination-omission"						               key={this.makeKey('page_omission_', 'start')}><span>...</span></li>);				}				if (end >= total) {					start -= end - total;					end = total;				}				else {					if (end < total - 1)						tail.push(<li className="pagination-item pagination-item pagination-omission"						              key={this.makeKey('page_omission_', 'end')}><span>...</span></li>);					if (end !== total)						tail.push(<li className="pagination-item" key={this.makeKey('page_item_', total)}>							<a href={'#page/' + (total)} onClick={() => this.loadData(total)}>{total}</a></li>);				}			}			for (let i = start; i <= end; i++) {				let className = 'pagination-item';				if (i == pageData.pageNumber)					className += ' pagination-active';				let link = <li className={className}				               key={this.makeKey('page_item_', i)}				               key={this.makeKey('page_item_', i)}><a href={'#page/' + (i)}				                                                      onClick={() => this.loadData(i)}>{i}</a></li>;				links.push(link);			}			return <div className="pagination-box">				<ul className="pagination-list">					<li className="pagination-item">						{this.getCheckboxField() ? '选中' + this.state.checked.length + '行,' : ''}						{pageData.recordCount && pageData.recordCount > 0 ? '共' + pageData.recordCount + '条记录' : ''}					</li>					{links.concat(tail)}					<li className="pagination-item">						<input type="text"						       value={this.state.goPage}						       onChange={(e) => this.changeGoPage(e.target.value)}/>						<a href="#" onClick={() => this.loadData(this.state.goPage)}>跳转</a>					</li>				</ul>			</div>;		}	}	render() {		return <div className={this.state.ajaxLoading ? 'at-table-loading' : ''}>			<table className="at-table">				{this.renderTableHead()}				{this.renderTableBody()}				{this.renderTableFoot()}			</table>			{this.renderPagination()}		</div>;	}}$.fn.table = function (props) {	if (!this.get(0))		throw new ReferenceError('Invalid DOM Element!');	else if (!this.prop('data-table')) {		props = props || {};		props = _.merge(props, this.data());		let input = ReactDOM.render(<Table {...props}/>, this.get(0));		this.prop('data-table', input);	}	return this.prop('data-table');};module.exports = Table;

这个Table组件,其实是从我的正式项目中抽离出来,并且针对第一阶段使用ReactJS碰到的一些问题重新做了调整和优化。要说的话,可能距离正经的开源还有距离,但自己日常用用还是没啥问题的。

如何调用呢?

<!DOCTYPE html><html lang="en"><head>	<meta charset="UTF-8">	<title>Title</title>	<link rel="stylesheet" type="text/css" href="normalize.css"/>	<link rel="stylesheet" type="text/css" href="font-awesome.css"/>	<link rel="stylesheet" type="text/css" href="main.css"/></head><body><div id="table_header"></div><div id="table_container"></div><script type="text/javascript" src="app.js"></script><script type="text/javascript" src="jquery.tablesort.js"></script><script type="text/javascript">	(function() {		var $ = jQuery;		$(document).ready(function () {			var total = 0;			function confirmData() {				alert('123');			}			$('#table_container').table({				// ajax的数据url				url: 'http://localhost/ajax/purchase.json',				// 是否使用ajax加载				ajaxLoad: true,				// 数据内容,你可以不填写data,而让ajax来加载				// data: [],				// 默认页面的当前页,这个也会影响ajax第一次优先加载第几页				pageOffset: 1,				// 分页的连接显示多少个,实际上无论如何都会按照双数-1,即19 => 19,20 => 19				pageLinksCount: 20,				// checkbox对应的字段				checkbox: 'OrderID',				// 已经选中的行				checked: ['120014', '120009'],				// 表字段的设置,如果ajaxLoad,建议这里留空,附加的字段可以用mergeColumns来设定				// columns: {},				// 额外附加的字段说明,他会和columns相关的字段的设定内容合并				mergeColumns: {					DeliveryDate: { datetime: true, format: 'Y-m-d' },					OrderDate: { datetime: true, format: 'Y-m-d' },					Checked: { options: { 0: tag('strong', '否', { className: 'red' }), 1: tag('strong', '是') } },					Valid: { sort: false }				},				// 初始化接口,这里实际上是渲染到table head的时候,所以这里请不要做任何关于DOM节点的操作				onInit: function(data) {					total = 0;				},				// 这里实际上是应该叫做onDataMount,也即,当加载了有效的表数据的时候,才会执行这个结果				// 但因为他执行的时机实际上是比React渲染完成要略早的,所以这里执行的内容还是给一个延迟吧				onMount: function(dom) {					setTimeout(function() {						// 这里我们对这个表绑定了一个tablesort的操作,翻页的时候这个tablesort会更新						// 但这里就不处理翻页时默认的排序状态了。						$($(dom).find('table')).tablesort({						}).sort('th.sort-th');					}, 500);				},				// 每一行数据的处理过滤方式,下面这里这个演示的是针对每一行的每一个字段的过滤方式				onRow: {					// data是一个object,结构为:{ value: value, text: value, field: field, index: rowIndex }					// value为原值,text也是原值,但输出的时候会使用text来输出,而不使用value,field是字段名,index是行号					// row则是当前行的数据,因为过滤某个单元格的数据时,还是需要使用到行数据的。					OrderID: function(data, row) {						data.text = tag('strong', data.value);						total += parseInt(data.value) || 0;					},					Valid: function(data, row) {						data.text = tag('button', '未核实', { onClick: confirmData });					}				},				// 下面是行数据过滤的另一个版本,这个方式只能针对一行做过滤,两种模式只能任选一种				// row是默认的行数据,clone是复制出来的行数据,经过这个接口后,输出的每一行的数据实际上使用的是clone的内容				// 所以要通过这里修改输出的内容,请直接修改clone//				onRow: function(row, clone) {////				},				// foot这里只有两个属性:show 是否显示,data 相关显示在tfoot行的数据,一般tfoot主要用来输出汇总的数据内容				onFoot: function(foot) {					foot.show = true;					foot.data = {						OrderID: total					}				}			});		});	}) (jQuery);</script></body></html>

使用说明已经在注释中了,具体就不做多解释了。

额外补充一些说明,ajax的数据格式:

{    "columns": {        "id": ["label" => "主键"]    },    "data": [        {            "id": 1,            "name": "hello"        }    ],    "pageData": {        "pageCount": 426,        "pageNumber": "1",        "pageParam": "page",        "pageSize": 20,        "recordCount": 8513    }}

后记

其实在过去的2年里,我一直在考虑如何简化后端程序员如何简化操作HTML复杂性的问题。所以在AgiMVC后续的升级版本已经kephp中,都实现了HTML部分的操作函数在内。设计的思想就是用函数名取代繁琐的HTML标签嵌套,并且允许用户实现自定的函数,以实现自定义的标签组合。

而实际上当看到React的时候,我发现自己的想法,和他出发点是很相似的。而React的虚拟化DOM操作,很像我06-07年在某个网站写的一套基于内存操作DOM节点的方法。当然总体而言,React走得更远,还包括了ReactNative。

所以我在对ReactJS有了一个整体性的了解以后,决定入他的坑。

现在前端MVC实在太多,已经进入了前端写模板的时代了。后端程序只要关心数据接口的准确性,前端可以包揽一切。

比起诸多的jade、handlebars、mustcache等js前端模板语言而言,ReactJS最大的优势是保持了HTML与JS混合编写,并实时调用JS变量的内容,没有再经过一层模板系统过滤。这种方式使得你写出来的HTML标签,最终实际上是以JS API的方式保存的,对于团队而言,无非就是有一个写JS的地方而已。而无需额外再去学习一套模板的引擎自己一时脑洞设计出来的模板语言。

保持了DOM节点的另外一个好处就是,能够与HTML规范与时俱进,比如 SVG,这里的好处实在太多。同时还能够因应浏览器的JS引擎升级而升级,完全不需要去改变什么。

当然转用了ReactJS以后,并不能够马上改善后端程序员写HTML的局面,这需要有一个量变到质变的累积。

而经历过这么多年的前端改革洗礼,我已经决定,整个团队的前端的ReactJS组件,由自己的团队成员来写,杜绝使用任何外部插件,因为其实所有的插件,都只是因应一时一刻某一特定环境写成,比如jQuery系列的插件,进入到ReactJS时代,其实80%都可以作废扔掉了。而目前大多数的ReactJS插件,实际上也只是针对某个CSS框架,或者某套UI规范写的,如果哪天你觉得这个CSS看着烦了,要换,基本上全部代码作废。作为UI框架,应该考虑得更远,也应该考虑得更全面。这也包括整个团队的前端打包构建规范,一次性代码、多次性使用的问题。

好吧,随意东拉西扯的,扯远了!

忽然想用ReactJS来写一套替代phpmyadmin的东西,phpmyadmin现行版本实在太扯,各种bug,也敢release,要不要脸了。

广告 广告

评论区