基于 GraphQL 的数据导出
背景
最近的一个项目中需要提供一个数据导出成 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;
}
}
推荐阅读: