本文主要是在进行导入导出的思考和设计实践,有任何指导建议欢迎留言,也让我学习学习。
前言
SaaS 产品中经常会遇到这样的功能—数据的导入导出。对于很多数据量大的产品中,最基础的导出可能就是下载当前列表的数据。但是很多产品基本都会有:
1、导出模版;
2、根据模版导入基础数据;
3、汇总报表导出统计的数据。今天主要是介绍我在设计导入导出功能时的思考和实践,其主要针对的是 Excel 的导入导出。
问题分析
导出分析
导出模版和导出数据是两种不同的需求。导出 Excel 模版。这种使用的场景是在后续需要根据模版填充数据然后导入至系统中。
初版:固定模版
初级方案主要针对的是一些模块的固定模版,详细方案设计:
通过 Excel 创建一个固定模版,然后将其放在服务器或者存储对象上,再直接提供一个可访问到该 Excel 模版的地址。用户通过直接访问该地址即可直接下载。
该方案处理简单,但当面对的 SaaS 系统,各模块功能不一致,或者客户需求不一致,就需要去设置很多个模版,当需求修改时可能会全部都修改。
升级:代码控制
通过代码设置,根据不同的模块去生成 Excel ,该方式灵活性更大些,当遇到扩展的字段时也可以根据情况去生成一份 Excel 模版。
导入功能
导入功能,其实整个过程就是在校验数据。不仅需要校验是否按照模版导入,校验数据是否正常,校验数据是否唯一等等相关的问题。所以导入的功能可以抽象出一个系列的流程:
- 获取数据,转化数据。导入文件转化成可以处理的数据,比如数组或者 JSON格式
- 基础校验-是否符合模版,表头是否符合模版要求,每列是否按模版要求
- 数据校验-基本的列数据格式,如手机号格式校验,邮箱格式校验等一些常规的数据校验
- 数据校验-业务规范校验,如编号唯一,类型是系统可识别的类型值,数据一对多关系等
- 保存数据,数据校验都符合要求,则可将一行数据转为可写入 DB 的数据
详细设计
以下将主要以导入用户信息、部门组织信息为例子。
第一版本 各模块独立开发
开发时,直接创建对应的模块类,然后提供对应的导入导出方法
export class UserModule {
async exportHandler(){
// doing something
};
async importHandler(){
// doing something
};
}
export class DepartmentModule {
async exportHandler(){
// doing something
};
async importHandler(){
// doing something
};
}
export class RoleModule {
async exportHandler(){
// doing something
};
async importHandler(){
// doing something
};
}
每个模块都有对应类,并提供了对应的导入导出的方法。但是在开发的过程中,你会发现其中的共性的内容会很多,很多重复性的工作。每个模块都重复功能,这样在开发过程中当需要修改功能时就需要逐个进行修改,毫无扩展性,且影响开发效率。
第二版本 抽象功能
正如问题分析中指出可以将导入导出的功能再细化其中的步骤,,抽象出相关的功能步骤。增强代码的扩展性和复用能力。
class BaseExcelFunc{
async exportExcel(moduleType string,sheetName string){
// 1、获取导出模版数据
const templates = this.getExcelTemplate(moduleType);
// 2、设置excel 表格数据,如 表头
const excelData = this.setExcelData(templates);
// 3、 设置excel 样式
this.setExcelStyly(excelData);
const sheetName = xlsx.build([{ name: sheetName, data }]);
// 4、上传至 OSS 上
const buffer = converterToBuffer(excelData);
const url = this.OSSStore.Upload()
}
async getExcelTemplate(moduleType string){
// doing somthhing
}
async setExcelData(moduleType string){
// doing somthhing
}
async setExcelStyly(moduleType string){
// doing somthhing
}
}
class UserExcel extends BaseExcelFunc{
construct(){
super()
}
// 重写 基类中的 setExcelData方法
async setExcelData(moduleType string){
// doing somthhing
}
}
使用构建一个基类,基类中提供导出的方法,且将这2个方法中整个导入导出的功能切割出更细的步骤。然后各模块继承基类,重写部分和自己相关的方法,这样就可以实现共同功能公用,各模块有自己的实现方式,可以重写对应的步骤。
以下主要是对导入功能进行步骤拆解:
class BaseExcelFunc{
async improtExcel(){
// 1、excel 转data
const data = this.ExcelToData(filePath);
// 2、模版校验
const err = this.baseValidate(data);
// 3、数据校验- 唯一性,正确性,是否重复
err = this.tableValidate(data);
// 4、 数据业务处理
result = async this.dataHandle(data)
// 5、保存数据
err = async this.save(result);
}
}
该方式符合开闭原则,提高扩展性和代码复用,在后续其他模块的导入导出功能开发,可以更快速的开发支持,提高效率。
模版校验
对于表头的校验,基本都是固定行的数据校验。所以主要通过配置一个模板,然后获取到数据后只需和模版校验。
const USER_TEMPLATE = {
DESC:'模版说明:1、名字必填',
HEADER:[{ key:'name',label:'名称'},
{key:'age',label:'年纪'}]
}
function baseValidate(data){
const errMessage =[]
const headers = data[1]
headers.forEach((val, i)=>{
const index = this.USER_TEMPLATE.HEADER.findIndex(o=>o.label ===val);
if(i!==index){
// 未找到对应的值
errMessage.push('未找到对应的值')
}else{
// 符合模版
}
})
return errMessage;
}
通过使用定义一个模板,导入功能时解析excel 值后直接通过校验模版是否一一对应,如果不符合规则的则基础校验失败。
数据校验
在导入之前也需要校验 excel 表格中每个单元格的数据格式,基本校验的是数据格式,比如手机号、邮箱是否正确等等,基于之前设置的模版,每个单元格都有对应的key ,那么我们可以设置一系列的规则。
const USER_CELL_RULE ={
name:{
isRequired: true,
maxLength: 20
},
phone:{
isRequired: true,
isPhone
}
}
validateRule(key:string,value: string, label: string){
const errMsg;
const validRule = this.USER_CELL_RULE[key];
for (const field in validRule) {
switch (field) {
case 'isRequired':
if (!value) {
errMsg = `${label} 是必填的`;
}
break;
case 'maxLength':
if (value.length > validateRule['maxLength']) {
errMsg = `${label} 最大长度为${validateRule['maxLength']}`;
}
break;
case 'isPhone':
const phoneRegx = new RegExp('^[1][3,4,5,7,8][0-9]{9}$');
if (!phoneRegx.test(value)) {
errMsg = `${label} 不符合手机号格式`;
}
break;
}
return errMsg;
}
表格数据的校验,需要提供每个列数据的校验规则,其中将每个列对应的key 作为校验规则数据集的key ,然后通过key 找到对应的每个校验标识。validateRule 方法主要就是用于从校验规则中获取字段需要进行哪些校验,然后进行对应规则的校验。以上的代码中主要提供了简单的三个校验,必填项、最大值、是否是手机格式。
还有重要的问题就是保存数据,最好是使用事务,这样就可以在每次发生错误时可以及时回滚。
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。