背景

最近的一个项目中需要提供一个数据导出成 Excel 的功能,这个项目是基于 Egg.js + GraphQL 的一个 node.js 工程。我们都知道 GraphQL 提供了一种更加灵活的数据查询设计,那么我们在导出数据的时候是不是也可以借助 GraphQL ,让导出数据由前端控制导出想要的数据。

实现思路

在前端根据特定的 OperationName 调用特定的处理数据导出的路由 /export 。然后在服务端先执行 GraphQL 的数据查询,根据查询的结果生成 Excel 返回。

载入超时,点击重试

前端修改 HttpLink

前端在调用请求之前根据 OperationName 决定是否调用到 /export 路由。

const customFetch = async (uri, options) => {
const { operationName, variables } = JSON.parse(options.body);

if (operationName === FeedbacksExport) {
return fetch(/export, options).then((response) =>
response
.clone()
.blob()
.then((blob) => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement(a);
a.href = url;
a.download = variables.fileName;
a.click();
})
);
}

return fetch(uri, options);
};

const httpLink = createHttpLink({
uri: /graphql,
credentials: same-origin,
fetch: customFetch
});

前端表单处理

作为一个灵活的数据导出功能,对于支持导出的栏位完全由前端控制。

client.query({
query: FeedbacksExport,
variables: {
feedbackQuery,
dataRoot: currentUser.joinedFeedbacks,
columns,
fileName: feedbacks.xlsx
}
});

GraphQL 查询语句

query FeedbacksExport($feedbackQuery: FeedbackQuery!) {
currentUser {
joinedFeedbacks(query: $feedbackQuery) {
id
title
content
stateName
gmtCreate
gmtModified
owner {
name
}
}
}
}

扩展 Variables 参数

  • dataRoot: 通过 lodash 的 get 方法获取需要导出处理的数据集
  • columns: 导出 Excel 的列描述,借助 JSONPath 获取对应列的值
  • fileName: 导出的文件名

列描述示例

{
key: id,
name: ID,
path: $.id
},
{
key: feedbackTitle,
name: 标题,
path: $.title
},
{
key: content,
name: 内容,
path: $.content
},
{
key: stateName,
name: 状态,
path: $.stateName
},
{
key: gmtCreate,
name: 创建时间,
path: $.gmtCreate,
formatter: Date
}

服务端处理

Egg Controller 先将参数交给 GraphQL 执行查询,然后根据返回值生成 Excel。

// POST /export
async export() {
const { ctx } = this;
const { query, variables, operationName } = ctx.request.body;
const { columns, dataPath, fileName } = variables;
const { data } = await ctx.service.graphql.query({
query,
variables,
operationName
});
const workbook = ctx.service.excel.create(_.get(data, dataPath), columns);

if (workbook) {
ctx.set(
Content-Type,
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
);
ctx.set(Content-Disposition, `attachment; filename=${fileName || download.xlsx}`);
ctx.status = 200;
await workbook.xlsx.write(ctx.res);
ctx.res.end();
} else {
ctx.body = data;
}
}

GraphQL Service

class GraphQLService extends app.Service {
async query({query, variables, operationName}) {
const { ctx, logger } = this;

try {
const documentAST = gql`
${query}
`;
const { schema } = app.graphql;

const result = await execute(
schema,
documentAST,
null,
ctx,
variables,
operationName
);

if (result && result.errors) {
result.errors = result.errors.map(formatError);
}
return result;
} catch (e) {
logger.error(e);
return {
data: {},
errors: [e]
};
}
}
}

Excel Service 借助 exceljs 库生成 Excel

class ExcelExport extends Service {
create(data, columns) {
const workbook = new Excel.Workbook();
const sheet = workbook.addWorksheet(Sheet 1);

sheet.columns = columns.map((c) => ({
header: c.name,
key: c.key,
style: c.formatter === Date ? {numFmt: yyyy/m/d\ h:mm} : {},
width: 20,
hidden: false
}));

_.each(data, (row) => {
sheet.addRow(
_.reduce(
columns,
(pre, c) => {
const d = JSONPath({json: { ...row }, path: c.path});
let formatData = d.length > 1 ? d : d[0];
const {formatter} = c;

// TODO: format data by excel data type
if (formatter === Date) {
formatData = formatData ? moment(formatData).utc(+08:00).toDate() : null;
} else if (_.isArray(formatData)) {
formatData = _.toString(d);
} else if (_.isNull(formatData)) {
formatData = "";
}

return {
...pre,
[c.key]: formatData
};
},
{}
)
);
});
return workbook;
}
}

推荐阅读:

查看原文 >>
相关文章