导入导出 Excel 设计实践和思考

本文主要是在进行导入导出的思考和设计实践,有任何指导建议欢迎留言,也让我学习学习。

前言

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 方法主要就是用于从校验规则中获取字段需要进行哪些校验,然后进行对应规则的校验。以上的代码中主要提供了简单的三个校验,必填项、最大值、是否是手机格式。

还有重要的问题就是保存数据,最好是使用事务,这样就可以在每次发生错误时可以及时回滚。

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿

猜你喜欢

转载自juejin.im/post/7126449309926555678