原生JS表格数据常用总结
主要是在数据报表这块, 做了好几年发现, 其实用户最终想要看的并不是酷炫的BI大屏, 而是最基础也是最复杂的 中国式报表
. 更多就是倾向于从表格中去获取数据信息, 最简单的就是最好的, 于是还是来总结一下表格这块的东西.
基础表格
先来实现一个最基础的表格, 用 table
标签, 为更好语义话, 用 thead, tbody
将表格分为表头和表数据,
同时给表格行添加一个悬浮的的效果, 表头也进行加粗这样的, 稍微好看一点.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>基础表格</title>
<style>
/* 单元格边框合并, 宽度 100% */
table {
border-collapse: collapse;
width: 100%;
}
/* 每个格子都加上边框 */
th, td {
border: 1px solid #ccc;
text-align: center;
height: 2em;
}
/* 表头加个背景 */
th {
background-color: #f8f8f9;
}
/* 行加一个悬停 */
tr:hover {
background-color: #f8f8f9;
}
</style>
</head>
<body>
<div class="tableCon">
<table>
<thead>
<tr>
<th>姓名</th>
<th>年龄</th>
<th>性别</th>
</tr>
</thead>
<tbody>
<tr>
<td>小王</td>
<td>28</td>
<td>男</td>
</tr>
<tr>
<td>小红</td>
<td>24</td>
<td>女</td>
</tr>
<tr>
<td>小张</td>
<td>18</td>
<td>男</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
表格字段可拖拽列宽
这个功能很重要, 尤其是对用户的一些交互上
// 拖拽列宽
var table = document.querySelector('table')
var resizableColumn = null;
var startX = 0;
var startWidth = 0;
table.addEventListener("mousedown", function (event) {
if (event.target.tagName === "TH" && event.offsetX > event.target.offsetWidth - 10) {
resizableColumn = event.target;
startX = event.pageX;
startWidth = resizableColumn.offsetWidth;
}
});
document.addEventListener("mousemove", function (event) {
if (resizableColumn) {
var newWidth = startWidth + (event.pageX - startX);
resizableColumn.style.width = newWidth + "px";
}
});
document.addEventListener("mouseup", function () {
resizableColumn = null;
});
固定首行首列
很多推荐的方式, 包括一些UI框架都是通过三至四个表格组合,然后js处理同步滚动来实现,这样的好处是容易实现,PC端也不会出现什么问题。
但是在手机端时,会有严重的不同步滚动现象,处理的不好时,甚至会出现错位等,体验不好. 其实我们用纯 css 实现即可.
- table-layout: fiexd
- position: sticky
为了让表格呈现滚动效果, 需给表格容器设置宽度, 同时给表格布局为 table-layout: fixed
表格的 fiexd 布局下, 单元格宽度根据第一行进行设定, 提前确定下来而不用再动态去根据内容计算, 可以加快渲染速度, 即我们可以事先固定表格宽度, 内容撑大则隐藏啥的操作 (针对数据指标)
固定行列则给需设置的行/列 单元格进行 position: sticky
. 它的表现类似于 relative + fixed 的合体, 当超过目标区域时, 会固定于目标的位置.
<style>
.table-con {
width: 200px;
height: 300px;
overflow: auto;
border: 1px solid gray;
border-right: 0;
border-bottom: 0;
}
table {
table-layout: fixed;
border-collapse: separate;
border-spacing: 0;
width: 100%;
}
td,
th {
/* 设置td,th宽度高度 */
width: 100px;
height: 2em;
/* 超出宽度显示为... */
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-align: center;
/* 只设置右, 下的边框线 */
border-right: 1px solid #ccc;
border-bottom: 1px solid #ccc;
}
th {
background-color: #f8f8f8;
}
/* 列首永远固定在头部 */
thead tr th {
position: sticky;
top: 0;
}
/* 首行永远固定在左侧 */
td:first-child,
th:first-child {
position: sticky;
left: 0;
z-index: 1;
background-color: #f8f8f8;
}
th:first-child {
z-index: 2;
background-color: #f8f8f8;
}
</style>
动态数据生成表格
就模拟一下后端传过来的 JSON 数据而已, 然后这里用原生 js 来创建表格和渲染数据而已.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>动态表格</title>
<style> /* 同上面一样的内容 */
</head>
<body>
<div id="table-con"></div>
<script>
// 模拟后端传过来的数据
var tableData = [
{ name: 'youge', age: 28, sex: 'M'},
{ name: 'yaya', age: 18, sex: 'F'},
{ name: 'jiezai', age: 30, sex: 'M'},
]
// 当行数到 10w, 浏览器就有点卡了
// for (var i = 1; i < 100000; i++) {
// tableData.push({ name: 'test', age: 30, sex: 'F'})
// }
var tableCon = document.getElementById('table-con')
var table = document.createElement('table')
var thead = document.createElement('thead')
var tbody = document.createElement('tbody')
// 创建表头, 并获取第一个对象的键作为表头
headRow = document.createElement('tr')
headCells = Object.keys(tableData[0])
for (var i = 0; i < headCells.length; i++) {
var th = document.createElement('th')
th.innerHTML = headCells[i]
headRow.appendChild(th)
}
// 将表头挂载到 thead中
thead.appendChild(headRow)
// 创建表格数据行, tr, td 根据数据
for (var i = 0; i < tableData.length; i++) {
var tr = document.createElement('tr')
for (var j = 0; j < headCells.length; j++) {
const td = document.createElement('td')
td.innerHTML = tableData[i][headCells[j]]
tr.appendChild(td)
}
// 将每行数据挂载到 tbody 下
tbody.appendChild(tr)
}
// 表格树: tableCon -> table -> thead/tbody -> tr -> td
table.appendChild(thead)
table.appendChild(tbody)
tableCon.appendChild(table)
</script>
</body>
</html>
这里的模拟数据也可以这样做一个 fake 数据生成, 方便后续的测试.
// 模拟后端传过来的数据
function genData(rows, columns) {
var arr = []
for (var i = 0; i < rows; i++) {
var obj = {i}
for (var j = 0; j < columns; j++) {
obj[j] = j
}
arr.push(obj)
}
return arr
}
// 生成一个 1000行, 30列的 对象数据 [ {}, {}, {}...]
var tableData = genData(10000, 30
表格分页
如果表格内容过多,我们可以添加分页功能,使用户能够浏览大量数据。
这要结合后端接口来实现, 这里我们可用通过数据行隐藏的方式来模拟分页的实现:
前端
- pageNum: 前端传给后端, 要访问的页码
- pageSise: 前端传给后端, 每页显示的数量
后端
- totalCount: 假设数据是按照自增id存储的, 先算出总的数据量 totalCount , 假设是 101
- totalPages: 根据前端传过来的每页数量 pageSize, 假设是 5, 则算出一共要分 ceil( 101 / 5 ) = 21 页
- currentPageData: 从总表总截取当前页条数返回, 偏移位置为: pageSize * pageNum, 偏移量是 pageSize
select * from tb limit [offset], rows
-- 假设要获取第1页, 每页5行, 则:(1-1) * 5, 5; id 为 1, 2, 3, 4, 5
select * from tb limit 0, 5
-- 假设要获取第4页, 每页4行, 则:(4-1) * 5, 5; id 为 16, 17, 18, 19
select * from tb limit 15, 5
数据的开始索引 startIndex = (pageNum -1) * pageSize
,
数据的结束索引 endIndex = startIndex + pageSize
<div id="table-con"></div>
<div id="paginationContainer"></div>
// 表格数据渲染部分 ...
// 表格分页
var pageSize = 10 // 每页显示行数
var currentPage = 1 // 当前页码
// 渲染某个页码的数据 (模拟接口就筛选出来和隐藏其他行)
function displayOnePage(page) {
// var table = document.getElementsByTagName('table')
var rows = table.rows
var totalRows = rows.length - 1 // 减去表头行
// 计算起始行索引 和 结束行索引
var startIndex = (page - 1) * pageSize + 1
var endIndex = Math.min(startIndex + pageSize - 1, totalRows)
// 遍历表格行, 只显示当前页, 其余的隐藏 (真实环境是后端直接返回当前页哦)
for (var i = 1; i <= totalRows; i++) {
if ( i >= startIndex && i <= endIndex) {
console.log(i);
rows[i].style.display = ''
} else {
rows[i].style.display = 'none'
}
}
}
// 分页器
function renderPagination() {
// var table = document.getElementsByTagName('table')
var rows = table.rows
var totalRows = rows.length -1
var paginationContainer = document.getElementById('paginationContainer')
paginationContainer.innerHTML = ''
// 分页器页码数, 比如有 100行, 每页显示 10行, 则一共有10页
var totalPages = Math.ceil(totalRows / pageSize)
// 根据计算的页码数, 生成分页的按钮
for (var i = 1; i <= totalPages; i++) {
var btn = document.createElement('button')
btn.textContent = i
// 点击哪页, 就用上面的 displayOnePage() 来显示页面数据
btn.onclick = function () {
currentPage = parseInt(this.textContent)
displayOnePage(currentPage)
}
// 将创建的页码按钮进行挂载
paginationContainer.appendChild(btn)
}
}
// 监听加载事件
document.addEventListener('DOMContentLoaded', function() {
displayOnePage(currentPage)
})
renderPagination()
对于页码可以进行样式美化一下:
/* 分页器-按钮美化 */
#paginationContainer button {
display: inline-block;
margin-right: 10px;
margin-top: 10px;
padding: 10 20px;
width: 40px;
border: none;
font-size: 16px;
color: #fff;
text-align: center;
background-color: #008CBA;
}
表格字段点击排序
原理就是获取表格数据所有行组成的类数组 rows
, 通过对数组的排序 sort (fn)
, 里面的函数根据用户点击的是哪一列来作为排序判断的依据.
- 获取表格数据行数组
rows
- 根据点击的哪个列进行
sort(function( td1, td2) )
- 对
tbody
的内容进行替换 (追加一样效果)tbody.appendChild(rows[i])
<script>
var table = document.getElementById('myTable')
// 获取表格字段列表
var columnList = []
var headers = table.getElementsByTagName('th')
for (var i = 0; i < headers.length; i++) {
columnList.push(headers[i].textContent)
}
function sortTable(colIndex) {
// 获取表格数据行, 并将其copy转为一个数组, 主要是为了调用 sort 方法
var tbody = table.tBodies[0]
var rows = tbody.getElementsByTagName('tr')
rows = Array.prototype.slice.call(rows)
// 根据点击表头的 字段索引 进行比较 sort
var flag = false
rows.sort(function (row1, row2) {
var a = row1.getElementsByTagName('td')[colIndex].textContent
var b = row2.getElementsByTagName('td')[colIndex].textContent
return a > b ? 1 : -1
})
for (var i = 0; i < rows.length; i++) {
tbody.appendChild(rows[i])
}
}
table.querySelector('thead').addEventListener('click', function(e) {
var column = e.target.textContent
var colIndex = columnList.indexOf(column)
sortTable(colIndex)
})
</script>
表格模糊筛选
就是遍历单元格的的每行, 每列, 找到匹配项, 然后显示该行而已. 当然是模拟哈.
htmlCopy code
<input type="text" id="searchInput" onkeyup="filterTable()" placeholder="搜索...">
<script>
// 关键字筛选
function filterTable() {
var input = document.getElementById('searchInput')
var keyword = input.value.toLowerCase() // 英文的话统一大小写
var table = document.querySelector('table')
var rows = table.getElementsByTagName("tr") // 所有的表格行
// 遍历每个单元格, 先行后列
for (var i = 0; i < rows.length; i++) {
// 获取一行内的所有列 cells
var cells = rows[i].getElementsByTagName('td')
var found = false
// 遍历每个格子进行查找
for (var j = 0; j < cells.length; j++) {
var cell = cells[j]
if (cell) {
// 获取当前单元格的值 和 输入的 keyword 进行比对
var cellValue = cell.textContent || cell.innerText
if (cellValue.toLocaleLowerCase().indexOf(keyword) > -1) {
found = true
break;
}
}
}
// 如果找到就显示这些行呗
if (found) {
rows[i].style.display = ""
} else {
rows[i].style.display = "none"
}
}
}
</script>
数据响应式和打印样式
让我们使表格在不同屏幕尺寸下具有良好的显示效果,通过CSS媒体查询来实现响应式设计:
例如, 当屏幕宽度小于600px时,表格单元格的内边距和字体大小会变小,以适应小屏幕设备
为了让用户能够打印表格时获得最佳打印效果,我们可以添加打印样式, ctr + P
可以预览。
<style>
/* 鼠标悬停效果 */
tr:hover {
background-color: #f5f5f5;
}
/* 响应式设计 */
@media screen and (max-width: 600px) {
th, td {
padding: 6px;
font-size: 12px;
}
}
@media print {
table {
width: 100%;
border-collapse: collapse;
}
th, td {
border: 1px solid #000;
padding: 8px;
// 字体变红
color: red;
}
caption {
font-weight: bold;
}
}
</style>
在这个步骤中,我们使用了@media print
媒体查询,定义了打印时表格的样式,使得打印出来的表格具有清晰的边框和适当的间距
导出CSV
对于简单的导出需求,如导出少量数据到CSV或JSON格式,前端可以实现。这通常通过创建一个隐藏的<a>
标签,设置其href
为包含数据的Blob URL,然后模拟点击这个标签来实现下载。
对于更复杂或需要特定格式的导出(如Excel、PDF等),后端通常更合适。后端可以处理数据的查询、转换和格式化,然后生成相应的文件供前端下载。
当数据量很大时,前端处理可能会导致性能问题或浏览器崩溃。在这种情况下,后端可以处理数据的分页、筛选和导出。后端还可以处理权限和安全性问题,确保只有授权的用户可以导出数据。
因此最佳的方案是, 前端传查询条件, 后端来导出.
- 前端负责触发导出操作,并将必要的参数(如筛选条件、排序等)传递给后端。
- 后端根据这些参数查询数据库,处理数据,并生成导出文件。然后,后端将文件发送给前端,前端负责显示下载链接或触发下载操作。
- 解决中文乱码问题:
blob = new Blob([String.fromCharCode(0xFEFF), blob], { type: blob.type });
// 前端导出csv
function exportTableToCSV() {
var csv = '';
var rows = document.querySelectorAll('table tr');
for (var i = 0; i < rows.length; i++) {
var row = rows[i];
var rowArray = [];
for (var j = 0; j < row.cells.length; j++) {
var cell = row.cells[j];
rowArray.push(cell.textContent.replace(/(\r\n|\n|\r)/gm, ''));
}
csv += rowArray.join(',') + '\n';
}
var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
// 专门来处理中文问题, 强制编码为 unicode
blob = new Blob([String.fromCharCode(0xFEFF), blob], { type: blob.type });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.setAttribute('download', 'table.csv');
a.click();
}
基本常用的一些功能就这些了, 对于表格, 主要我是搞数据为主, 因此核心是能显示数据, 数据排序, 检索数据 和下载, 基本就行了. 其实根本就可以不用各类 ui 组件, 原生的就行啦.