二次封装 XLSX 插件为 Book 类
简介
XLSX 插件默认提供的都是一些函数,或者是对象下的函数,使用起来多少有些不便,因此,将插件的导出二次封装为 Book 类,并将部分功能重新组合,使之更便于使用。
适用版本:XLSX 0.18.5
已封装的功能介绍
- 新建 WorkBook 对象(单张表):
const book = new Book({ data: excelData, sheetName: '表1' });
- 新建 WorkBook 对象(多张表):
const book = new Book([ { data: this.excelData, sheetName: '表1' }, { data: this.excelData, sheetName: '表2' }, ]);
- 新建 WorkSheet 对象:
let sheet = book.createSheet(this.excelData, sheetOptions);
- 添加 WorkSheet :
book.appendSheet(sheet, '表2');
- 新建并添加 WorkSheet :
book.addSheet('表3', this.excelData, sheetOptions);
- 获取 WorkSheet 对象,提供了两种方式:
- 一种与 XLSX 相同,通过
book.Sheets[sheetName]
访问 - 另一种是重新封装的
book.getSheets(sheetIdentity, basedIndex)
,第一个参数可以提供表名字符串或者表的索引,第二个参数指示索引从 0 或是 1 开始,默认从 1 开始
- 一种与 XLSX 相同,通过
- 自定义表头文案:添加了 headers 属性后,默认忽略原表头(
sheetOptions.skipHeader = true
);注意此处只是忽略原表头并替换为新表头,并不是删除表头const book = new Book( { data: this.excelData, sheetName: '表1' }, { headers: { id: '编号', age: '年龄', gender: '性别', }, } );
- 合并单元格:支持多种形式表示合并区域:对象形式、数组形式,合并区域的内容可以直接传字符串或者符合 XLSX 插件的单元格对象形式
const book = new Book({ data: this.excelData, sheetName: '表1', options: { // 双对象,对象属性为数组 // merges: { // ranges: [this.mergeRange], // contents: [{ v: '111' }], // }, // 双对象,对象属性为对象 // merges: { // ranges: { // 1: this.mergeRange, // }, // contents: { // 1: { v: '111' }, // }, // }, // 数组,数组元素为单个合并区域 merges: [ { range: this.mergeRange, content: '111', // content: { v: '111' }, }, ], origin: 'A3', // 注意该属性还是手动添加;或者可以自行添加一段逻辑,将最高的合并单元格的下一行作为数据的起始行 }, });
- 导出到本地:
book.writeFile('info');
;第二个参数为扩展名,默认 'xlsx',可以修改
公共方法:
- 列序号与字母转换:
Excel.Digit2ColChars(digit, capitalized)
、Excel.ColChars2Digit(chars, capitalized)
- 从 R1C1 或 A1 表示的字符串转换为对象形式 (
{ s: { r: 1, c: 1} }
):Excel.FromA1(range, basedIndex)
、Excel.FromR1C1(range, basedIndex)
- 从对象形式转换为 R1C1 或 A1 形式:
Excel.ToA1(range)
、Excel.ToR1C1(range)
' - 开启异步处理任务:
Excel.startTask()
,方便大量计算时不会影响主进程
几个常用的正则规则:
- 行号有效范围
Excel.RegexRowIndex = /104857[0-6]|10485[0-6][0-9]|1048[0-4][0-9]{2}|104[0-7][0-9]{3}|10[0-3][0-9]{4}|[1-9][0-9]{1,5}|[1-9]/
- 列字母有效范围:
Excel.RegexColumnChar = /XF[A-D]|XE[A-Z]|[A-W][A-Z]{2}|[A-Z]{1,2}/g;
- R1C1 有效范围:
Excel.RegexR1C1 = /R(?<r>104857[0-6]|10485[0-6][0-9]|1048[0-4][0-9]{2}|104[0-7][0-9]{3}|10[0-3][0-9]{4}|[1-9][0-9]{1,5}|[1-9])C(?<c>1638[0-4]|163[0-7][0-9]|16[0-2][0-9]{2}|1[0-5][0-9]{3}|[1-9][1-9]{1,3}|[1-9])/;
data 对象示例:
excelData = [
{
id: 1,
age: 17,
gender: 'male',
},
{
id: 2,
age: 20,
gender: 'male',
},
{
id: 3,
age: 18,
gender: 'female',
},
],
实现
外部依赖
import * as XLSX from 'xlsx';
import { isEmpty } from '../validator/jsValidator'; // 自定义的验证对象是否问空的方法,对象必须带自有属性,才会返回 true;空对象返回 false
const { utils } = XLSX;
基本结构
class Book {
_book = null; // 将 XLSX 插件生成的 book 对象作为当前 Book 类实例的一个属性,避免部分函数使用时还需要将 book 作为参数传递
_basedIndex = false; // 为真表示 Sheets 索引从 1 开始
/**
*
* @param {包含 data, sheetName 和 options 的对象,或由对象组成的数组} params
* @param {所有 WorkSheet 的默认配置,且以各 WorkSheet 的独立配置为优先} defaultOptions
*/
constructor(params, defaultOptions) {
this.book = utils.book_new();
this._initBook(params, defaultOptions);
}
get Sheets() {
return this._book.Sheets;
}
...
}
初始化
表格数据会交由 initBook 方法,创建 WorkSheet 对象并添加到 XLSX 插件生成的 book 对象
_initBook(params, defaultOptions = {}) {
// 初始化 WorkBook
this._basedIndex = defaultOptions.indexBase;
// 初始化 WorkSheet
if (params instanceof Array) {
Object.values(params).forEach((param) => this._initSheet(param));
} else if (params instanceof Object) {
this._initSheet(params);
} else {
throw new Error('Type Error: params should be a object or an array');
}
}
_initSheet({ data, sheetName, options }) {
let sheetData = [].concat(data);
let sheetOptions = options || defaultOptions;
const headers = options.headers;
// 自定义表头文本
if (!isEmpty(headers)) {
sheetData.unshift(headers);
sheetOptions.skipHeader = true; // 自定义表头时默认忽略原表头
}
const sheet = this.createSheet(sheetData, sheetOptions);
this.appendSheet(sheet, sheetName);
}
创建表格、添加表格
createSheet(data, options = {}) {
if (data?.length > 0) {
let sheet = utils.json_to_sheet(data, options);
const { merges, origin } = options;
if (merges) {
// 将 merges 参数统一为两个数组
const { mergeRanges, mergeContents } = this._normalizeMerges(merges);
if (origin) {
// 将字符串表示的合并区域转为对象形式
const mergeConfigs = this._setupMergeConfigs(mergeRanges);
this._setMergeConfigs(sheet, mergeConfigs);
Object.entries(mergeConfigs).forEach(([i, config]) => {
let { r: sr, c: sc } = config.s,
{ r: er, c: ec } = config.e;
// 由于 mergeConfigs 中将开始索引设置为 0,此处需要重新加上
const s = Excel.ToA1({ s: { r: sr + 1, c: sc + 1 } }),
e = Excel.ToA1({ s: { r: er + 1, c: ec + 1 } });
const content = mergeContents[i];
if (content) {
if (!content.t) {
this.setSheetContent(sheet, s, { v: content });
} else {
this.setSheetContent(sheet, s, content);
}
} else {
throw new Error(
`Reference Error: content for mergeRange ${s}:${e} is empty!`
);
}
});
} else {
throw new Error(
'Reference Error: options.origin is necessary when using merge feature!'
);
}
}
return sheet;
}
return null;
}
appendSheet(sheet, sheetName) {
if (!sheet) {
throw new Error('Fail to append, sheet is empty');
} else {
let book = this._book;
utils.book_append_sheet(book, sheet, sheetName);
}
}
// 一步添加新表
addSheet(sheetName, data, options) {
const sheet = this.createSheet(data, options);
this.appendSheet(sheet, sheetName);
}
获取表格对象
/**
*
* @param {表的索引} index
* @param {表的开始索引,默认为 true,表示从 1 开始;不影响内存中的表数组} basedIndex
* @returns
*/
getSheetNameByIndex(index, basedIndex = true) {
const book = this._book;
if (index < 0 || index > book.SheetNames.length) {
throw new Error(`Range Error: sheet index exceeded!`);
} else {
return book.SheetNames[index - basedIndex];
}
}
/**
*
* @param {表的索引或表名的字符串} sheetIdentity
* @param {同上} basedIndex
* @returns Sheet 对象
*/
getSheet(sheetIdentity, basedIndex) {
switch (typeof sheetIdentity) {
case 'number':
sheetIdentity = this.getSheetNameByIndex(sheetIdentity, basedIndex);
case 'string':
return this.Sheets[sheetIdentity];
default:
throw new Error(
'Type Error: please provide a WorkSheet index or WorkSheet name string'
);
}
}
设置表格内容
/**
*
* @param {同上} sheetIdentity
* @param {单元格区域,R1C1 或 A1 表示} range
* @param {字符串或符合 XLSX 单元格对象的格式} content
* @param {同上} basedIndex
*/
setSheetContent(sheetIdentity, range, content, basedIndex) {
if (sheetIdentity instanceof Object) {
sheetIdentity[range] = content;
} else {
let sheet = this.getSheet(sheetIdentity, basedIndex);
sheet[range] = content;
}
}
合并单元格
// 将所有形式提供的 merges 参数统一为两个数组:mergeRanges 和 mergeContents
_normalizeMerges(merges) {
let mergeRanges = [],
mergeContents = [];
if (merges) {
const { ranges, contents } = merges;
if (ranges) {
if (ranges instanceof Array) {
// 双对象形式
/*
merges: {
ranges: ['R1C1:R2C2'],
contents: ['111'],
},
*/
if (ranges.length > 0 && contents.length > 0) {
if (ranges.length === contents.length) {
mergeRanges = [].concat(ranges);
mergeContents = [].concat(contents);
} else {
throw new Error(
`Reference Error: the ranges count doesn't correspond to contents`
);
}
} else {
console.warn(
'Merge ranges or contents are empty, please check the "options.merges'
);
}
} else if (ranges instanceof Object) {
// 类似 el-form 验证功能的 model 和 rules 对象格式
/* merges: {
ranges: {
1: this.mergeRange,
},
contents: {
1: '111',
},
}
*/
Object.entries(ranges).forEach(([key, range]) => {
const content = contents[key];
if (content) {
mergeRanges.push(range);
mergeContents.push(content);
} else {
throw new Error(
`Reference Error: content for merge range ${range} is empty`
);
}
});
} else {
throw new Error(`Type Error: ranges must be an Array or a Object!`);
}
} else {
// 数组形式,数组元素为单个合并区域及内容
/* merges: [
{
range: 'A1:A3',
content: '表格标题',
},
],
*/
Object.values(merges).forEach(({ range, content }) => {
if (range && content) {
mergeRanges.push(range);
mergeContents.push(content);
} else {
const tag = range ? 'content' : 'range',
reverseTag = { content: 'range', range: 'content' };
const reverse = { content: range, range: content };
throw new Error(
`Reference Error: ${tag} is empty where ${reverseTag[tag]} 'is' ${reverse[tag]}`
);
}
});
}
return { mergeRanges, mergeContents };
} else {
throw new Error(`Reference Error: merges are empty`);
}
}
// 合并单元格
_setMergeConfigs(sheet, mergeConfigs) {
if (sheet) {
if (mergeConfigs) {
sheet['!merges'] = mergeConfigs;
} else {
throw new Error(`Reference Error: mergeConfigs can no be empty!`);
}
} else {
throw new Error(`Reference Error: sheet can no be empty!`);
}
}
// 参数为数组;range 格式:R1C1:R2C2 或 A1:B2
_setupMergeConfigs(mergeRanges) {
if (mergeRanges.length > 0) {
const regex = Excel.RegexR1C1;
const result = mergeRanges[0].match(regex);
let setupMergeConfig = () => {};
if (result.length === 1) {
throw new Error(
`Formatting Error: please use a pair of range expressions with R1C1 or A1 from start cell to end cell`
);
} else if (result.length === 0) {
setupMergeConfig = Excel.FromA1;
} else {
// result.length === 2
setupMergeConfig = Excel.FromR1C1;
}
let mergeConfigs = [];
Object.values(mergeRanges).forEach((mergeRange) => {
// 注意这里的开始索引是 0,因为 XLSX 插件内部保存的单元格区域是 js 对象,索引必须从 0 开始
mergeConfigs.push(setupMergeConfig.call(null, mergeRange, false));
});
return mergeConfigs;
} else {
throw new Error(`Reference Error: mergeRanges can no be empty!`);
}
}
导出 Excel
writeFile(fileName, extension = 'xlsx') {
return new Promise((res, rej) => {
if (this._book.SheetNames.length > 0) {
if (fileName.indexOf('xls') > -1) {
rej('Parameter filename should not contain extension');
} else {
try {
XLSX.writeFile(this._book, `${fileName}.${extension}`);
res(true);
} catch (err) {
rej(err);
}
}
} else {
rej('Can not write workbook with empty sheets');
}
});
}
公共方法和属性
内部公共方法和属性
const CODES = {
A: 65,
a: 97,
};
function fromRangeStr(range, isCellFormatValid, cbParseFrom) {
const parseFrom = (range) => {
let result = cbParseFrom(range);
if (result) {
return result;
} else {
// 列序号超范围时 groups 为空,解构会报错;序号前包含前导 0 或其他非法字符也会报错
throw new Error(
'Range Error: row or column format invalid or index exceeded!'
);
}
};
if (range.indexOf(':') > -1) {
const [s, e] = range.split(':');
return { s: parseFrom(s), e: parseFrom(e) };
} else {
if (isCellFormatValid(range)) {
throw new Error(
`Formatting Error: merge ranges should contain a separator ":". You can only ignore it in cell`
);
} else {
return { s: parseFrom(range) };
}
}
}
/**
*
* @param {对象格式 { s:{ r:1, c:1 }, e: { r:1, c:1 }}} range
* @param {(r,c): string; 返回 R1C1 或 A1 格式的字符串} cbToCell
* @returns
*/
function toRangeStr(range, cbToCell) {
const toCell = (cellRange) => {
const { r, c } = cellRange;
return cbToCell(r, c);
};
if (isEmpty(range.e)) {
return toCell(range.s);
} else {
return toCell(range.s) + ':' + toCell(range.e);
}
}
可导出的公共方法和属性
export class Excel {
// 这里的正则为了匹配尽量长的字符串(代表更大的数字),因此将能匹配到较大结果的规则写在 `|` 左侧,较短的写在右侧
static RegexRowIndex =
/104857[0-6]|10485[0-6][0-9]|1048[0-4][0-9]{2}|104[0-7][0-9]{3}|10[0-3][0-9]{4}|[1-9][0-9]{1,5}|[1-9]/;
// 这里使用了命名捕获组,因此不能添加 g 标志, g 标志会导致分组丢失,不过会返回所有的匹配项;是否使用 g 根据实际需求选择
static RegexR1C1 =
/R(?<r>104857[0-6]|10485[0-6][0-9]|1048[0-4][0-9]{2}|104[0-7][0-9]{3}|10[0-3][0-9]{4}|[1-9][0-9]{1,5}|[1-9])C(?<c>1638[0-4]|163[0-7][0-9]|16[0-2][0-9]{2}|1[0-5][0-9]{3}|[1-9][1-9]{1,3}|[1-9])/;
static RegexColumnChar = /XF[A-D]|XE[A-Z]|[A-W][A-Z]{2}|[A-Z]{1,2}/g;
// 10 进制列序号与 26 进制互换
/**
*
* @param {1-16384 之间的数字} digit
* @param {是否启用大写字母;默认启用} capitalized
* @returns
*/
static Digit2ColChars(digit, capitalized = true) {
if (digit > 0 && digit < 16385) {
let codeList = [];
const start = CODES[capitalized ? 'A' : 'a'] - 1;
const divide = (digit, codeList) => {
let mod = digit % 26;
if (!mod) mod = 26;
codeList.push(String.fromCharCode(start + mod));
digit = (digit - mod) / 26;
if (digit > 0) divide(digit, codeList);
};
divide(digit, codeList);
return codeList.reduce((result, code) => {
return `${code}${result}`;
}, '');
} else {
throw new Error(
`Range Error: please provide valid column index within [1~16384]! Wrong index: ${digit}`
);
}
}
/**
*
* @param {26 进制的字符串} chars
* @param {是否启用大写字母;默认启用} capitalized
* @returns
*/
static ColChars2Digit(chars, capitalized = true) {
const regex = Excel.RegexColumnChar;
let result = chars.match(regex);
if (result && result[0] === chars) {
// 将字母逐个转为 ASCII 码中的索引
const codeList = [];
for (const char of chars) {
let code = char.charCodeAt(0);
// 比 A/a 小 1 位,比 Z/z 大 1 位
let start = CODES[capitalized ? 'A' : 'a'] - 1,
end = start + 27;
if (code > start && code < end) {
codeList.push(code - start);
} else {
throw new Error(
`Range Error: please provide valid char within ${
capitalized ? 'A-Z' : 'a-z'
}! Wrong column char: ${char}`
);
}
}
// 将 ASCII 码索引转为 10 进制
let digit = 0,
i = codeList.length - 1;
for (const code of codeList) {
digit += 26 ** i-- * code;
}
return digit;
} else {
throw new Error(
'Range Error: please provide column chars within [A~XFD]!'
);
}
}
// 统一格式化为对象数组 [{ r:1, c:1 }]
static FromA1(range, basedIndex = true) {
const regexCol = Excel.RegexColumnChar,
regexRow = Excel.RegexRowIndex;
const parseFrom = (range) => {
const colChar = range.match(regexCol)[0];
const r = range.match(regexRow)[0];
if (colChar && r && colChar.length + r.length === range.length) {
const c = Excel.ColChars2Digit(colChar);
return basedIndex ? { r, c } : { r: r - 1, c: c - 1 };
}
return false;
};
return fromRangeStr(
range,
(range) => range.match(regexCol, regexRow).length > 1,
parseFrom
);
}
static FromR1C1(range, basedIndex = true) {
const regex = Excel.RegexR1C1;
const parseFrom = (range) => {
const result = range.match(regex);
if (result) {
const { r, c } = result.groups;
if (r && c) {
return basedIndex ? { r, c } : { r: r - 1, c: c - 1 };
}
}
return false;
};
return fromRangeStr(
range,
(range) => range.match(regex).groups.length > 1,
parseFrom
);
}
/**
* 从对象重新格式化为 A1 或 R1C1
* @param {参数格式为: { s: {r:1, c:1}, e: {r:2, c:2} }} range
*/
static ToA1(range) {
return toRangeStr(range, (r, c) => `${Excel.Digit2ColChars(c)}${r}`);
}
static ToR1C1(range) {
return toRangeStr(range, (r, c) => `R${r}C${c}`);
}
// A1 与 R1C1 格式互换
static A1ToR1C1(range) {
return Excel.ToR1C1(Excel.FromA1(range));
}
static R1C1ToA1(range) {
return Excel.ToA1(Excel.FromR1C1(range));
}
// 开启一个异步处理任务;由于不需要链式调用,直接在函数内部执行所有异步代码,因此本方法不返回 Promise 对象
static startTask(fn) {
setTimeout(() => {
fn();
});
}
}