工作一年,主要職責是負責公司后台管理平台的開發與維護。此間面對各種業務需求,通過面向谷歌編程等常見方式,積累了一些微不足道的經驗。
本篇為總結的第一篇(也許有其他篇)—— table 篇
對於后台管理平台來說,最必不可少的元素就是 table 表格,幾乎每個頁面都涉及到表格的使用,甚至常作為是頁面的主體部分。 因此,如何維護這些 table,並且根據業務需求完善不同解決方案,便是此文所會表達的內容。
主要技術棧如題為 vue 全家桶配合 element-ui(其他技術棧其實思路是類似的),因此主要還是對 el-table 等的再封裝等。element-ui 的文檔已經非常通俗易懂,本文不涉及一些文檔上已有的基本用法。
接下來我會模擬一些簡單的數據來展示一些業務問題的解決方案,其目的在展示思路,代碼以簡易為主。
1. 自定義列表項
很多時候我們需要將后端數據作展示優化
// mock 數據(跳過直接往下看) tableData: [ { id: "12987122", name1: "王小虎", name2: "王小虎", name3: "王小虎", address1: "上海市普陀區金沙江路 1518 弄", address2: "上海市普陀區金沙江路 1518 弄", address3: "上海市普陀區金沙江路 1518 弄", amount1: "234", amount2: "3.2", amount3: 10, amount4: "4.43", amount5: 12 }, { id: "12987123", name1: "王小虎", name2: "王小虎", name3: "王小虎", address1: "上海市普陀區金沙江路 1518 弄", address2: "上海市普陀區金沙江路 1518 弄", address3: "上海市普陀區金沙江路 1518 弄", amount1: "165", amount2: "4.43", amount3: 12, amount4: "4.43", amount5: 12 }, { id: "12987124", name1: "王小虎", name2: "王小虎", name3: "王小虎", address1: "上海市普陀區金沙江路 1518 弄", address2: "上海市普陀區金沙江路 1518 弄", address3: "上海市普陀區金沙江路 1518 弄", amount1: "324", amount2: "1.9", amount3: 9, amount4: "4.43", amount5: 12 }, { id: "12987125", name1: "王小虎", name2: "王小虎", name3: "王小虎", address1: "上海市普陀區金沙江路 1518 弄", address2: "上海市普陀區金沙江路 1518 弄", address3: "上海市普陀區金沙江路 1518 弄", amount1: "621", amount2: "2.2", amount3: 17, amount4: "4.43", amount5: 12 }, { id: "12987126", name1: "王小虎", name2: "王小虎", name3: "王小虎", address1: "上海市普陀區金沙江路 1518 弄", address2: "上海市普陀區金沙江路 1518 弄", address3: "上海市普陀區金沙江路 1518 弄", amount1: "539", amount2: "4.1", amount3: 15, amount4: "4.43", amount5: 12 } ],
本次 table 數據以上面數據模擬后端傳值。我們除了要展示這些字段,還要將后面 5 個 數據作相除或求百分比等,常規寫法如下(不用細看):
<el-table class="table" :data="tableData" border show-summary highlight-current-row style="width: 100%" > <el-table-column prop="id" label="ID" width="180"></el-table-column> <el-table-column prop="name1" label="姓名1" width="100"></el-table-column> <el-table-column prop="name2" label="姓名2" width="100"></el-table-column> <el-table-column prop="name3" label="姓名3" width="100"></el-table-column> <el-table-column prop="address1" label="地址1" width="180" show-overflow-tooltip></el-table-column> <el-table-column prop="address2" label="地址2" width="180" show-overflow-tooltip></el-table-column> <el-table-column prop="address3" label="地址3" width="180" show-overflow-tooltip></el-table-column> <el-table-column prop="amount1" sortable label="數值1"></el-table-column> <el-table-column prop="amount2" sortable label="數值2"></el-table-column> <el-table-column prop="amount3" sortable label="數值3"></el-table-column> <el-table-column prop="amount4" sortable label="數值4"></el-table-column> <el-table-column prop="amount5" sortable label="數值5"></el-table-column> <el-table-column prop="amount6" sortable label="數值6"> <template slot-scope="scope"> <span>{{toFixedTwo(scope.row.amount1, scope.row.amount2)}}</span> <span> </template> </el-table-column> <el-table-column prop="amount7" sortable label="數值7"> <template slot-scope="scope"> <span>{{toFixedTwo(scope.row.amount1, scope.row.amount3)}}</span> <span> </template> </el-table-column> <el-table-column prop="amount8" sortable label="數值8"> <template slot-scope="scope"> <span>{{toFixedTwo(scope.row.amount1, scope.row.amount4)}}</span> <span> </template> </el-table-column> <el-table-column prop="amount9" sortable label="數值9"> <template slot-scope="scope"> <span>{{toFixedTwo(scope.row.amount1, scope.row.amount5)}}</span> <span> </template> </el-table-column> <el-table-column prop="amount10" sortable label="數值10"> <template slot-scope="scope"> <span>{{toPercent(scope.row.amount1, scope.row.amount2)}}</span> <span> </template> </el-table-column> <el-table-column prop="amount11" sortable label="數值11"> <template slot-scope="scope"> <span>{{toPercent(scope.row.amount1, scope.row.amount3)}}</span> <span> </template> </el-table-column> <el-table-column prop="amount12" sortable label="數值12"> <template slot-scope="scope"> <span>{{toPercent(scope.row.amount1, scope.row.amount4)}}</span> <span> </template> </el-table-column> <el-table-column prop="amount13" sortable label="數值13"> <template slot-scope="scope"> <span>{{toPercent(scope.row.amount1, scope.row.amount5)}}</span> <span> </template> </el-table-column> </el-table>

可以看到,僅僅是這十來個字段,就讓頁面顯得非常臃腫,而且很多重復,可想而知如果字段增致幾十上百,展示方式更加繁瑣,開發維護不易。
用表驅動編程進行優化
表驅動法是《代碼大全》里面提到編程方法,適用於多個 if-else 這樣形式的代碼,這里自然十分適用。
demo 代碼的目錄結構

tableData.js
將要展示的字段按順序,以一定參數形式的數組結構放在 TABLE_DATA_MAP
對象內,如目前僅有的 tableDemo 即表示為我們上面代碼的表結構數組。
/** * 參數作用說明: * key: 展示字段 * label: 列頭名稱 * width: 列寬 * sortable: 是否可篩選 * hidden: 隱藏默認展示字段 * Dict: 展示用字典 * isFixedTwo: 保留兩位(可配合分子/分母使用) * isPercent: 百分號展示(配合分子/分母使用) * molecule: 分子 * denominator: 分母 **/ export const TABLE_DATA_MAP = { tableDemo: [ { key: "name1", label: "姓名1", width: 100, }, { key: "name2", label: "姓名2", width: 100, }, { key: "name3", label: "姓名3", width: 100, }, { key: "address1", label: "地址1", width: 180, }, { key: "address2", label: "地址2", width: 180, }, { key: "address3", label: "地址3", width: 180, }, { key: "amount1", label: "數值1", width: 100, sortable: true, }, { key: "amount2", label: "數值2", width: 100, sortable: true, }, { key: "amount3", label: "數值3", width: 100, sortable: true, }, { key: "amount4", label: "數值4", width: 100, sortable: true, }, { key: "amount5", label: "數值5", width: 100, sortable: true, }, { key: "amount6", molecule: "amount1", denominator: "amount2", label: "數值6", width: 100, sortable: true, isFixedTwo: true, hidden: true, }, { key: "amount7", molecule: "amount1", denominator: "amount3", label: "數值7", width: 100, sortable: true, isFixedTwo: true, hidden: true, }, { key: "amount8", molecule: "amount1", denominator: "amount4", label: "數值8", width: 100, sortable: true, isFixedTwo: true, hidden: true, }, { key: "amount9", molecule: "amount1", denominator: "amount5", label: "數值9", width: 100, sortable: true, isFixedTwo: true, hidden: true, }, { key: "amount10", molecule: "amount1", denominator: "amount2", label: "數值10", width: 100, sortable: true, isPercent: true, hidden: true, }, { key: "amount11", molecule: "amount1", denominator: "amount3", label: "數值11", width: 100, sortable: true, isPercent: true, hidden: true, }, { key: "amount12", molecule: "amount1", denominator: "amount4", label: "數值12", width: 100, sortable: true, isPercent: true, hidden: true, }, { key: "amount13", molecule: "amount1", denominator: "amount5", label: "數值13", width: 100, sortable: true, isPercent: true, hidden: true, }, ] }
tableColumn.vue
用於對 el-table-colum 的二次封裝,配合上面表結構使用(直接看代碼,其中 toFixedTwo,toPercent 函數在 mixin 混入)
<template> <div> <div v-for="(item, index) in TABLE_DATA_MAP[tableName]" :key="index + item"> <el-table-column :label="item.label" :key="index + item" :min-width="item.width" :sortable="item.sortable" :prop="item.key" show-overflow-tooltip > <template slot-scope="scope"> <span v-if="!item.hidden">{{ scope.row[item.key] }}</span> <span v-if="item.Dict">{{ item.Dict[scope.row[item.key]] }}</span> <span v-if="item.isFixedTwo" >{{toFixedTwo(scope.row[item.molecule], scope.row[item.denominator])}}</span> <span v-if="item.isPercent" >{{toPercent(scope.row[item.molecule], scope.row[item.denominator])}}</span> </template> </el-table-column> </div> </div> </template> <script> import { TABLE_DATA_MAP } from "@/utils/tableData"; export default { name: "table-column", props: { tableName: String }, data() { return { TABLE_DATA_MAP }; } }; </script>
Table.vue
優化后的頁面如下,與之前相比是不是簡潔了不少
<template> <div> <el-table class="table" :data="tableData" border show-summary highlight-current-row style="width: 100%" > <el-table-column prop="id" label="ID" width="120" fixed="left"></el-table-column> <table-column tableName="tableDemo"></table-column> </el-table> </div> </template> <script> export default { data() { return { tableData: [ ... ], }; }, components: { "table-column": () => import("@/components/tableColumn") }, methods: { getSummaries(param) { ... } } }; </script>
除了一些必要參數(如 key label)外,你可以在 tableData.js 中自定義任何參數,配合 tableColumn.vue 使用。與此同時,你可以在 tableColumn.vue 上對一些單獨字段進行特殊處理
// 對 xxx 字段進行自定義
<template slot-scope="scope"> <div v-if="item.key === 'xxx'"> <span>{{(scope.row['xxx'] + scope.row['xxx1']+ scope.row['xxx2']).toFixed(2)}}</span> </div> </template>
合計列
此時如果需求要求合計值,也能夠通過 TABLE_DATA_MAP
內數據快速實現(表驅動法經典場景,你可以想象不用現在的方法需要幾個 if-else)
<template> <div> <el-table class="table" :data="tableData" border show-summary :summary-method="getSummaries" highlight-current-row style="width: 100%" > <el-table-column prop="id" label="ID" width="120" fixed="left"></el-table-column> <table-column tableName="tableDemo"></table-column> </el-table> </div> </template> <script> import { TABLE_DATA_MAP } from "@/utils/tableData"; export default { data() { return { TABLE_DATA_MAP, tableData: [ ... ], // totalData 模擬 amount 初始合計值(很可能合計值非簡單的疊加,一般由后端傳遞) totalData: { amount1: 1883, amount2: 15.83, amount3: 63, amount4: 22.15, amount5: 60 } }; }, ... methods: { getSummaries({ columns }) { let sums = []; columns.forEach((column, index) => { if (column.property === "id") { sums[index] = "合計"; return; } else { this.TABLE_DATA_MAP.tableDemo.forEach(keyObject => { if (keyObject.key.includes(column.property)) { if (keyObject.isPercent && keyObject.isPercent === true) { sums[index] = this.toPercent( this.totalData[keyObject.molecule], this.totalData[keyObject.denominator] ); } else if ( keyObject.isFixedTwo && keyObject.isFixedTwo === true ) { sums[index] = this.toFixedTwo( this.totalData[keyObject.molecule], this.totalData[keyObject.denominator] ); } else { sums[index] = this.totalData[column.property]; } } }); } }); return sums; } } }; </script>

動態列表配置
對於一些列表字段較多的 table 頁面,實現列表字段的動態配置的需求就自然而然產生了。 也是得益於我們的表驅動法,我們能夠很簡單得做到這一點。
更新的目錄結構:

Table.vue
<template> <div> <el-button style="margin-bottom:10px;" type="primary" @click="dialogs.configuration.show=true">列表配置</el-button> <el-table ... </el-table> <el-dialog :title="dialogs.configuration.title" :visible.sync="dialogs.configuration.show" :close-on-click-modal="false" width="700px" > <transfer :model="dialogs.configuration.data" :tableName="'tableDemo'" @close="dialogs.configuration.show=false" @editSuc="editSuc('configuration')" ></transfer> </el-dialog> </div> </template> <script> import { TABLE_DATA_MAP } from "@/utils/tableData"; const tableData = [ ... ]; export default { data() { return { TABLE_DATA_MAP, tableData: [], totalData: { ... }, dialogs: { configuration: { title: "動態列表配置", data: "", show: false } } }; }, components: {...}, mounted(){ this.getList() }, methods: { getList() { // 模擬數據獲取 setTimeout(() => { this.tableData = tableData; }, 1000); }, getSummaries({ columns }) { ... }, editSuc(obj) { this.dialogs[obj].show = false; this.$message({ message: "提交成功", type: "success" }); this.tableData = [] this.getList() } } }; </script>
transfer.vue
<template> <div> <el-transfer filterable :filter-method="filterMethod" filter-placeholder="請輸入表頭名" v-model="value" :data="data" ></el-transfer> <el-button type="primary" @click="doSubmit()">提交</el-button> </div> </template> <script> import { TABLE_DATA_MAP } from "@/utils/tableData"; export default { props: { tableName: String }, data() { return { TABLE_DATA_MAP, data: TABLE_DATA_MAP[this.tableName], // 當前頁默認值 value: [], // 現在在 transfer 右測的值 filterMethod(query, item) { return item.label.indexOf(query) > -1; } }; }, computed: { currentTableData() { return this.$store.state.currentTableData; } }, methods: { doSubmit() { let sData = []; this.value.map(items => { this.TABLE_DATA_MAP[this.tableName].forEach(item => { if (item.key === items) { sData.push(item); } }); }); // 這里如果是實際項目應該會給后端接口傳值來保存當前用戶該頁面的設置 this.$store.commit("SET_TABLE_DATA", { type: this.tableName, data: sData }); this.$emit("editSuc"); } }, mounted() { this.value = []; // 這里如果是實際項目 currentTableData 應該是后端獲取數據,而不是 vuex 獲取 if (this.currentTableData && this.currentTableData[this.tableName]) { this.currentTableData[this.tableName].forEach(item => { if (this.TABLE_DATA_MAP[this.tableName].includes(item.key)) { this.value.push(item.key); } }); } } }; </script>
tableColumn.vue
<template> <div> <div v-for="(item, index) in ((currentTableData && currentTableData[tableName])? currentTableData[tableName]: TABLE_DATA_MAP[tableName])" :key="index + item" > <el-table-column> ... </el-table-column> </div> </div> </template> <script> import { TABLE_DATA_MAP } from "@/utils/tableData"; export default { name: "table-column", props: { tableName: String }, data() { return { TABLE_DATA_MAP }; }, computed: { // currentTableData 實際工作中應該是保存在后端的值 currentTableData() { return this.$store.state.currentTableData; } } }; </script>
store/index.js
這里使用 vuex 存儲 currentTableData(現在所配置的列表字段),如果是實際工作中,該數據應該存儲於后端數據(后端保存當前用戶對該頁面的設置,而后在 tableColumn.vue 頁獲取)
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) export default new Vuex.Store({ state: { currentTableData: {} }, mutations: { SET_TABLE_DATA(state, { type, data }) { state.currentTableData[type] = data } }, actions: { }, modules: { } })


思路十分簡單,本質就是在后端保存一份當前頁面用戶表格的私人定制 TABLE_DATA_MAP
文件。
2. 前端導出 table
導出 table 表格是很常見的需求,基本上一些統計頁面必備。
導出有多種方式:
1. 后端實現數據
主要是后端將生成的 table 數據流給到前端,然后前端生成下載鏈接,模擬點擊效果。
downloadFile(data) { if (!data) { return } let url = window.URL.createObjectURL(new Blob([data])); let link = document.createElement('a'); link.style.display = 'none'; link.href = url; link.setAttribute('download', '導出數據.csv'); document.body.appendChild(link); link.click() }
此種方法適用於有分頁且分頁量十分大,還有頁面數據的展示和導出與后端傳遞數據(與上面我們需要對數據進行百分比等變化的數據不同)的情況。
2. 前端導出
需要引入 xlsx 和 file-saver
yarn add slsx file-saver -S
前端實現導出常見的又有兩種方法:
2.1. 通過頁面 Dom 元素獲取數據導出
/* eslint-disable */ import FileSaver from 'file-saver' import XLSX from 'xlsx' /** * 導出表格為 excel 格式 * param { Dom } id // document.getElementById('table') * param { string } fileName // test.xlsx * param { Boolean } rawBool 純文本解析將不會解析值 */ export function exportExcelByDom(id, fileName, rawBool = true) { /** * element-ui fixed 是生成兩個 table,一個僅用於固定 * 判斷要導出的節點中是否有 fixed 的表格 * 如果有,轉換 excel 時先將該 dom 移除,然后 append 回去 */ const fix = document.querySelector('.el-table__fixed') || document.querySelector('.el-table__fixed-right'); let wb; /** * 從表生成工作簿對象 */ if (fix) { wb = XLSX.utils.table_to_book(document.getElementById(id).removeChild(fix), { raw: rawBool }); document.getElementById(id).appendChild(fix); } else { wb = XLSX.utils.table_to_book(document.getElementById(id), { raw: rawBool }); } /* 獲取二進制字符串作為輸出 */ const wbout = XLSX.write(wb, { bookType: 'xlsx', bookSST: true, type: 'array' }) try { /** * Blob 對象表示一個不可變、原始數據的類文件對象。 * Blob 表示的不一定是JavaScript原生格式的數據。 * File 接口基於Blob,繼承了 blob 的功能並將其擴展使其支持用戶系統上的文件。 * 返回一個新創建的 Blob 對象,其內容由參數中給定的數組串聯組成。 * 設置導出文件名稱 */ FileSaver.saveAs(new Blob([wbout], { type: 'application/octet-stream' }), fileName) } catch (e) { if (typeof console !== 'undefined') console.log(e, wbout) } return wbout }
此種方法適用於無分頁、導出數據即為頁面看到的樣子的情況。
2.2 通過 Export2Excel.js
/* eslint-disable */ import FileSaver from 'file-saver' import XLSX from 'xlsx' /** * Export2Excel.js * param { Array } th // ['姓名'] * param { Array } keyArray // ['name'] * param { Array } jsonData // 處理好的所有數據 */ export function export_json_to_excel(th, keyArray, jsonData, defaultTitle) { /* original data */ let data = jsonData.map(v => keyArray.map(j => v[j])); data.unshift(th); let ws_name = "SheetJS"; let wb = new Workbook(), ws = sheet_from_array_of_arrays(data); /* add worksheet to workbook */ wb.SheetNames.push(ws_name); wb.Sheets[ws_name] = ws; let wbout = XLSX.write(wb, { bookType: 'xlsx', bookSST: false, type: 'binary' }); let title = defaultTitle || '導出數據' FileSaver(new Blob([s2ab(wbout)], { type: "application/octet-stream" }), title + ".xlsx") }; ... // 其他部分省略
Export2Excel.js 網上有很多版本,大同小異。我對其 export_json_to_excel
函數作了封裝,Export2Excel.js 里面也有通過 DOM 導出的方法,但使用時會崩潰,因此通過 DOM 導出推薦 2.1 方法
又得益於我們之前的 TABLE_DATA_MAP
文件,2.2 方法導出基本沒有工作量的問題,節省了很大時間(相信看到這里,你能夠體會到表驅動法對 table 的意義)
doExport2Excel() { const tHeader = ["ID"]; const keyArray = ["id"]; this.TABLE_DATA_MAP.tableDemo.forEach(item => { tHeader.push(item.label); keyArray.push(item.key); }); // 這里 jsonData 應該是所要導出的所有數據,可讓后端傳值 const jsonData = this.tableData; jsonData.forEach(list => { this.TABLE_DATA_MAP.tableDemo.forEach(keyObject => { if (keyObject.isPercent && keyObject.isPercent === true) { list[keyObject.key] = this.toPercent( list[keyObject.molecule], list[keyObject.denominator] ); } else if (keyObject.isFixedTwo && keyObject.isFixedTwo === true) { list[keyObject.key] = this.toFixedTwo( list[keyObject.molecule], list[keyObject.denominator] ); } }); }); export_json_to_excel(tHeader, keyArray, jsonData, "數據導出"); },
這種方法比 2.1 好在:很多時候導出的 table 列與展示的是不一致的(如通過列表配置,展示字段少於導出字段情況),我們甚至可以在導出時對某些字段作不同於頁面展示的數據處理。
與此同時其解決了后端導出數據會與展示數據不一致的問題,在主動性和靈活性上更勝一籌。
轉:https://zhuanlan.zhihu.com/p/137684511