背景

最近的一個項目中需要提供一個數據導出成 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;
}
}

推薦閱讀:

查看原文 >>
相关文章