Pesquisa de site

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.

Artigos relacionados