javascript - Enviar resposta binária de UInt8Array no Express.js
Estou usando Express.js com Typescript e eu gostaria de enviar um UInt8Array como dados binários.
Isto é o que eu uso até agora e funciona, mas eu gostaria de não salvar o arquivo antes, porque eu acho que isso desperdiça desempenho:
const filePath = path.resolve(__dirname, 'template.docx');
const template = fs.readFileSync(filePath);
const buffer: Uint8Array = await createReport({
template,
data: {
productCode: data.productCode,
},
});
fs.writeFileSync(path.resolve(__dirname, 'output.docx'), buffer);
res.sendFile(path.resolve(__dirname, 'output.docx'));
Aliás, estou usando docx-templates para gerar o arquivo.
Resposta #1
Você pode usar um fluxo PassThrough para esse propósito, ele manterá o arquivo na memória sem necessidade de escrever no disco.
Algo assim deve funcionar:
const stream = require("stream");
const readStream = new stream.PassThrough();
// Pass your output.docx buffer to this
readStream.end(buffer);
res.set("Content-disposition", 'attachment; filename=' + "output.docx");
res.set("Content-Type", "application/vnd.openxmlformats-officedocument.wordprocessingml.document");
readStream.pipe(res);
O código completo do node.js:
const fs = require("fs");
const express = require("express");
const port = 8000;
const app = express();
const stream = require("stream");
app.get('/download-file', (req, res) => {
const buffer = fs.readFileSync("./test.docx");
console.log("/download-file: Buffer length:", buffer.length);
const readStream = new stream.PassThrough();
readStream.end(buffer);
res.set("Content-disposition", 'attachment; filename=' + "test.docx");
res.set("Content-Type", "application/vnd.openxmlformats-officedocument.wordprocessingml.document");
readStream.pipe(res);
});
app.listen(port);
console.log(`Serving at http://localhost:${port}`);
Para testar, adicione um arquivo 'test.docx' ao mesmo diretório, e então aponte seu navegador para http://localhost:8000/download-file
Resposta #2
Terry,
Obrigado pela atualização da sua resposta e por fornecer o código completo. No entanto, isso ainda não ajuda muito. Estou tentando entender como posso lidar com isso no lado do front-end, no meu caso em Vue. Aqui está o código a seguir:
router.post('/chart/word', async (req, res, next) => {
try {
if (!req.body.chartImage) throw new BadRequest('Missing the chart image from the request body')
const wordTemplate = await s3GetFile('folder', 'chart-templates-export/charts-template.docx')
const template = wordTemplate.Body
const buffer = await createReport({
cmdDelimiter: ["{", "}"],
template,
additionalJsContext: {
chart: () => {
const dataUrl = req.body.chartImage.src
const data = dataUrl.slice("data:image/jpeg;base64,".length);
return { width: 18 , height: 12, data, extension: '.jpeg' }
}
}
})
const stream = require('stream')
const readStream = new stream.PassThrough()
readStream.end(buffer)
res.set("Content-disposition", 'attachment; filename=' + "output.docx")
res.set("Content-Type", "application/vnd.openxmlformats-officedocument.wordprocessingml.document")
readStream.pipe(res)
} catch (err) {
console.log(err)
next(err)
}
})
E aqui está meu código Vue, testei várias coisas, mas nada...:
async exportCharts() {
console.log('this.$refs.test: ', this.$refs.test)
let img = {
src: this.$refs.test.getDataURL({
type: 'jpeg',
pixelRatio: window.devicePixelRatio || 1,
backgroundColor: '#fff'
}),
width: this.$refs.test.getWidth(),
height: this.$refs.test.getHeight()
}
const answersReq = await this.axios({
method: 'post',
url: '/pollAnswers/chart/word',
data: {
chartImage: img
}
responseType: 'arraybuffer' // 'blob' // 'document'
})
console.log('answersReq: ', answersReq)
if (answersReq.data) {
downloadURL(answersReq.data, 'report.docx')
}
}
O que eu basicamente estou fazendo é: enviando uma imagem para a API (tirada de um elemento html vue-echart), e então inserindo-a em um modelo docx, usando a biblioteca docx-templates, que me retorna um Uint8Array que eu quero exportar como o novo Documento do Word com os gráficos populados. Então, o usuário (na interface) deve poder escolher o destino.
Aqui está o código para o URL de download:
export function downloadURL(data, fileName) {
const mimeType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
const blob = new Blob([data], { type: mimeType })
const url = URL.createObjectURL(blob)
const element = document.createElement('a')
element.href = url
element.download = fileName
element.style.display = 'none'
document.body.appendChild(element)
element.click()
URL.revokeObjectURL(element.href)
document.body.removeChild(element)
}
P.S. Só para mencionar, se eu salvar diretamente o buffer (o Uint8Array retornado do createReport) na API, funciona, o arquivo é baixado com sucesso e eu posso lê-lo sem nenhum problema - ele preenche o gráfico correto no arquivo.
ATUALIZAÇÃO: Eu descobri isso, mas não tenho certeza de por que isso é necessário e por que funciona dessa maneira e não de outra. Então, no endpoint /chart/word, estou convertendo o Uint8Array buffer em um stream, e então passando isso como resposta (do mesmo jeito que você usou). Depois, no Vue, eu busquei isso como responseType: 'arraybuffer', o que converteu a resposta do stream novamente em Uint8Array buffer, então, usei o mesmo método para o download e funcionou. Inicialmente, tentei enviar diretamente o buffer (sem convertê-lo em stream como você mencionou), mas então, no front-end, a resposta foi recebida como objeto que continha os valores do Uint8Array buffer, o que não era o esperado e eu não consegui criar um arquivo docx válido. Então, por alguma razão, é necessário converter o buffer em stream na API, antes de enviá-lo como resposta. Depois, no front-end, eu tenho que convertê-lo de volta para arraybuffer e, finalmente, realizar o download do docx.
Se você puder me explicar por que funciona assim, eu ficarei muito feliz.
