• [Node.js] 后端服务导出CSV数据流给前端下载


    前端时间使用Java做了此功能,另一个使用Node.js开发的服务也需要此功能,所以使用TypeScript做了类似的封装,后来发现,TS做这些功能,代码看起来更简洁,嘿嘿。

    直接上代码吧。

    CsvUtils.ts

    import { Response } from "express";
    import { DateUtils, FXResponse } from "nodejs-fx";
    import { GenderType } from "../model/GenderType";
    
    const uuid = require('node-uuid');
    const _reg1: RegExp = new RegExp(""", 'g');
    const _reg2: RegExp = new RegExp("\"", 'g');
    
    /**
     * CSV 下载辅助类
     */
    export class CsvUtils {
        private static charset: String = "utf-8";
    
        /**
         * 导出 CSV
         * @param res Http请求Response
         * @param fileName 可选,文件名,用户下载的文件名
         * @param onLoadData 获取分页数据
         */
        static async writeCsv<T>(res: Response,
            _constructor: { new (...args: Array<any>): T },
            onLoadData: (page: number) => Promise<PageDTO<T>>,
            fileName: string = undefined
        ): Promise<any> {
            try {
                let cls = (new _constructor()).constructor.name;
                let items: T[] = [];
                let pageIndex: number = 1;
                let count: number = undefined;
                while (true) {
                    let result:PageDTO<T> = await onLoadData(pageIndex);
                    if (!result || !result.items || result.items.length == 0)
                        break;
                    if (pageIndex == 1) {
                        count = result.count;
                    }
                    if (pageIndex == 1 && count != undefined && count == result.items.length) {
                        return await this.writeCsvByItems(res, result.items, fileName, cls);
                    }
                    // items.push(...result.items);
                    result.items.forEach(item => {
                        items.push(item);
                    });
                    pageIndex++;
                    if (result.hasNext === true)
                        continue;
                    if (result.hasNext === false)
                        break;
                    if (count != undefined && items.length >= count)
                        break;
                }
                return await this.writeCsvByItems(res, items, fileName, cls);
            } catch (e) {
                return e;
            }
        }
    
        /**
         * 导出列表 CSV
         * @param res Http请求Response
         * @param items 数据列表
         * @param fileName 可选,文件名,用户下载的文件名
         */
        static async writeCsvByItems<T>(res: Response, items: Array<T>, fileName: string, className: string): Promise<any> {
            this.setHttpHeader(res, fileName);
            if (!items || items.length == 0)
                return "";
    
            // 筛选出拥有注解的字段
            let fields = new Array<any>();
            for (var o in items[0]) {
                let rKey = className + "." + o.toLowerCase();
                let reg = this.regMap.get(rKey);
                if (reg && reg.ingore === true)
                    continue;
                if (!reg || !reg.name) {
                    fields.push({v: o, t: o, conv: undefined});
                } else {
                    fields.push({v: o, t: reg.name, conv: reg.converter})
                }
            }
    
            if (fields.length == 0)
                return "";
    
            let result: string = "";
            // 写入utf-8 BOM xefxbbxbf
            result += "uFEFF";
    
            // 写入标题行
            let strs = new Array<string>();
            fields.forEach(v => {
                strs.push(JSON.stringify(v.t));
            });
            let text = this.stringToCsvLines(strs) + "
    ";
            result += text;
    
            // 写入内容
            items.forEach(item => {
                text = this.itemToString(item, fields);
                if (!text) return;
                result += text + "
    ";
            });
    
            return result;
        }
    
        /** 设置下载用的 Http 响应头部 */
        private static setHttpHeader(res: Response, fileName: string) {
            if (!fileName) fileName = this.generateRandomFileName() + ".csv";
            res.set({
                "Content-Type": "application/octet-stream; charset=" + this.charset,
                "Content-Disposition": "attachment;filename=" + encodeURIComponent(fileName),
                "Pragma": "no-cache",
                "Expires": 0
            });
        }
    
        private static itemToString(item: any, fields: Array<any>): string {
            let result = new Array<string>();
            fields.forEach(data => {
                let v = undefined;
                if (data.conv) {
                    data.conv.data = item;
                    v = data.conv.execute(item[data.v]);
                } else
                    v = item[data.v];
                if (v == undefined || v === "") {
                    result.push("");
                } else {
                    let txt = JSON.stringify(v);
                    if (txt.startsWith("{") || txt.startsWith("[")) {
                        txt = """ + txt.replace(_reg1, """") + """;
                    }
                    result.push(txt);
                }
            });
            return this.stringToCsvLines(result);
        }
    
        private static generateRandomFileName(): string {
            return uuid.v4().replace(new RegExp("-", 'g'), '');
        }
    
        private static stringToCsvLines(strs: Array<string>): string {
            if (!strs || strs.length == 0) return "";
            return strs.join(",");
        }
    
        // 注册的注解参数
        static regMap: Map<string, CsvParams> = new Map<string, CsvParams>();
    }
    
    export class PageDTO<T> {
        count: number = 0;
        hasNext: boolean = true;
        items: T[];
    
        static load<T>(data: FXResponse<T[]>, pageSize: number) {
            let result = new PageDTO<T>();
            if (data && data.code == 0 && data.data) {
                if (Array.isArray(data.data)) {
                    result.items = data.data;
                } else if (data.data.list && Array.isArray(data.data.list)) {
                    result.items = data.data.list;
                } else if (data.data.items && Array.isArray(data.data.items)) {
                    result.items = data.data.items;
                }
                if (result.items)
                    result.hasNext = result.items.length >= pageSize;
                else
                    result.hasNext = false;
            } else
                throw data;
            return result;
        }
    }
    
    /**
     * csv 注解
     * @param name 字段名称(导出后显示的名称)
     * @param ingore 是否忽略这个字段
     * @param _constructor 转换器
     * @param args 转换器构造参数(依次写)
     */
    export function csv<T>(name: string, ingore: boolean = false,
        _constructor: { new (...args: Array<any>): CsvConverterBase } = undefined,
        ...args: any
    ) {
        return function(target:any, propertyName:string){
            let p = new CsvParams();
            p.name = name;
            p.ingore = ingore;
            if (_constructor) {
                p.converter = new _constructor(...args);
            }
            CsvUtils.regMap.set(target.constructor.name + "." + propertyName.toLowerCase(), p);
        }
    }
    
    export class CsvParams {
        /** 字段名称 */
        name: string;
        /** 是否忽略 */
        ingore: boolean;
        /** 转换器 */
        converter: CsvConverterBase;
    }
    
    export abstract class CsvConverterBase {
        data: any;
        abstract execute(value: any): string;
    }
    
    /**
     * 时间戳转字符串 CSV转换器
     */
    export class TimestampCsvConverter extends CsvConverterBase {
        execute(value: any): string {
            if (value == undefined) return "";
            if (!Number.isNaN(value)) {
                return DateUtils.formatDateTime(value);
            } else
                return value;
        }
    }
    
    /**
     * 性别类型CSV转换器
     * @description @csv("会员标签", undefined, GenderTypeCsvConverter)
     */
    export class GenderTypeCsvConverter extends CsvConverterBase {
        execute(value: GenderType): string {
            if (value == GenderType.female) return "女";
            if (value == GenderType.male) return "男";
            return "未知"
        }
    }
    
    /**
     * 字符串数组  CSV转换器
     * @description @csv("会员标签", undefined, StringArrayCsvConverter)
     */
    export class  StringArrayCsvConverter extends CsvConverterBase {
        field: string;
        constructor(field: string) {
            super();
            this.field = field;
        }
    
        execute(value: any): string {
            if (Array.isArray(value) && value.length > 0) {
                if (typeof(value[0]) == 'string')
                    return value.join(",");
                if (this.field) {
                    let items = [];
                    value.forEach(item => items.push(item[this.field]));
                    return items.join(",");
                }
            }
            return value;
        }
    }
    
    /**
     * 布尔值  CSV转换器
     * @description @csv("允许登录APP", undefined, BoolCsvConverter, "是", "否")
     */
    export class BoolCsvConverter extends CsvConverterBase {
        p1: string;
        p2: string;
        p3: string;
    
        constructor(p1: string, p2: string, p3: string = "") {
            super();
            this.p1 = p1;
            this.p2 = p2;
            this.p3 = p3;
        }
    
        execute(value: any): string {
            if (value === true)
                return this.p1;
            if (value === false)
                return this.p2;
            return this.p3 == undefined ? "" : this.p3;
        }
    }
    
    /**
     * 枚举值 CSV 转换器
     * @description @csv("登录角色", undefined, EnumCsvConverter, {1: "管理员", 2: "普通员工", 3: "创建者"})
     */
    export class EnumCsvConverter extends CsvConverterBase {
        enumValue: Object;
    
        constructor(enumValue: Object) {
            super();
            this.enumValue = enumValue;
        }
    
        execute(value: any): string {
            if (value == undefined) return "";
            let v = this.enumValue[value];
            return v ? v : "";
        }
    }
    
    /**
     * 对象字段值 CSV 转换器
     * @description @csv("图像地址", undefined, ObjectCsvConverter, "url")
     */
    export class ObjectCsvConverter extends CsvConverterBase {
        field: string;
        constructor(field: string) {
            super();
            this.field = field;
        }
    
        execute(value: any): string {
            if (!value || !this.field) return "";
            if (Array.isArray(value)) {
                // 数组取出每项的字段值后,用","分隔连接
                let values = [];
                value.forEach(item => {
                    values.push(item[this.field]);
                });
                return values.join(",");
            } else
                return value[this.field];
        }
    }

    PageDTO 声明, 仅作参考: (主要是作分页用)

    export class PageDTO<T> {
        count: number = 0;
        hasNext: boolean = true;
        items: T[];
    
        static load<T>(data: Response<T[]>, pageSize: number) {
            let result = new PageDTO<T>();
            if (data && data.code == 0 && data.data) {
                if (Array.isArray(data.data)) {
                    result.items = data.data;
                } else if (data.data.list && Array.isArray(data.data.list)) {
                    result.items = data.data.list;
                } else if (data.data.items && Array.isArray(data.data.items)) {
                    result.items = data.data.items;
                }
                if (result.items)
                    result.hasNext = result.items.length >= pageSize;
                else
                    result.hasNext = false;
            } else
                throw data;
            return result;
        }
    }

    调用举例:

        @get("/list/pc/csv")
        @validate
        async getXXXListCsv(
             @query('a')  a: string,
             @query('b')  b: string,
             @query('c')  c: string
        ) {
            return await CsvUtils.writeCsv(this.res, TestDTO, async (page): Promise<PageDTO<any>> => {
                let data = await this.getList(page, 20, a, b, c);
                return PageDTO.load(data, 20);
            });
        }

    TestDTO 声明:

    export class TestDTO {
       /**
        * 会员名称
        */
        @csv("会员名称")
        name:string;
    
        /**
         * 头像
         */
        @csv("", true)
        memberImage:MediaModel;
    
       /**
        * 性别
        */
        @csv("性别", undefined, GenderTypeCsvConverter)
        gender:GenderType;
    
        /**
         * 会员标签名称数组
         */
        @csv("会员标签", undefined, StringArrayCsvConverter, "name")
        tags:string[]|TagsDetail[];
    
        /**
         * 加入时间
         */
        @csv("加入时间")
        jointime?: string;
    
        /**
         * 会员在该店铺的启用状态
         */
        @csv("启用状态", undefined, BoolCsvConverter, "启用", "未启用")
        enable?: boolean;
    }

    可以看到,使用 @csv 注解非常简单。 

  • 相关阅读:
    Vue.js 初尝试
    docker 搭建lnmp开发环境
    【转】【Salesfoece】在 Apex 中得到 sObject 的信息
    【转】【Salesfoece】Approval Process 在 Apex 中的使用
    【转】【Salesfoece】Apex 中 PageReference 的使用
    「codeforces
    「二次剩余」Tonelli
    「loj
    pytest---mock使用(pytest-mock)
    Django---setting文件详解
  • 原文地址:https://www.cnblogs.com/yangyxd/p/14026023.html
Copyright © 2020-2023  润新知