1

嗨,我对 docxtemplater 很陌生,但我非常喜欢它的工作原理。现在我似乎能够生成一个新的 docx 文档,如下所示:

const functions = require('firebase-functions');
const admin = require('firebase-admin');
const {Storage} = require('@google-cloud/storage');
var PizZip = require('pizzip');
var Docxtemplater = require('docxtemplater');
admin.initializeApp();
const BUCKET = 'gs://myapp.appspot.com';

exports.test2 = functions.https.onCall((data, context) => {
// The error object contains additional information when logged with JSON.stringify (it contains a properties object containing all suberrors).
function replaceErrors(key, value) {
    if (value instanceof Error) {
        return Object.getOwnPropertyNames(value).reduce(function(error, key) {
            error[key] = value[key];
            return error;
        }, {});
    }
    return value;
}

function errorHandler(error) {
    console.log(JSON.stringify({error: error}, replaceErrors));

    if (error.properties && error.properties.errors instanceof Array) {
        const errorMessages = error.properties.errors.map(function (error) {
            return error.properties.explanation;
        }).join("\n");
        console.log('errorMessages', errorMessages);
        // errorMessages is a humanly readable message looking like this :
        // 'The tag beginning with "foobar" is unopened'
    }
    throw error;
}


let file_name = 'example.docx';// this is the file saved in my firebase storage
const File = storage.bucket(BUCKET).file(file_name);
const read = File.createReadStream();

var buffers = [];
readable.on('data', (buffer) => {
  buffers.push(buffer);
});

readable.on('end', () => {
  var buffer = Buffer.concat(buffers);  
  var zip = new PizZip(buffer);
  var doc;
  try {
      doc = new Docxtemplater(zip);
      doc.setData({
    first_name: 'Fred',
    last_name: 'Flinstone',
    phone: '0652455478',
    description: 'Web app'
});
try {
   
    doc.render();
 doc.pipe(remoteFile2.createReadStream());

}
catch (error) {
    errorHandler(error);
}

  } catch(error) {
      errorHandler(error);
  }

});
});

我的问题是我不断收到一个错误doc.pipe is not a function。我对nodejs很陌生,但是有没有办法让新生成的文档doc.render()直接保存到firebase存储中?

4

1 回答 1

2

看一下 的类型doc,我们发现它是一个Docxtemplater对象并且发现它doc.pipe不是那个类的函数。要从 中取出文件Docxtemplater,我们需要使用来返回文件(根据我们传递给构造函数的内容,doc.getZip()这将是 aJSZip v2或实例)。Pizzip现在我们有了 zip 的对象,我们需要生成 zip 的二进制数据 - 这是使用完成的generate({ type: 'nodebuffer' })(获取Buffer包含数据的 Node.JS)。不幸的是,由于docxtemplater库不支持JSZip v3+,我们无法使用该generateNodeStream()方法来获取与 一起使用的流pipe()

使用此缓冲区,我们可以将其重新上传到 Cloud Storage 或将其发送回调用该函数的客户端。

第一个选项实现起来相对简单:

import { v4 as uuidv4 } from 'uuid';
/* ... */

const contentBuffer = doc.getZip()
      .generate({type: 'nodebuffer'});
const targetName = "compiled.docx";
  
const targetStorageRef = admin.storage().bucket()
  .file(targetName);
await targetStorageRef.save(contentBuffer);

// send back the bucket-name pair to the caller
return { bucket: targetBucket, name: targetName };

但是,将文件本身发送回客户端并不容易,因为这涉及切换到使用HTTP 事件函数( functions.https.onRequest),因为可调用云函数只能返回与 JSON 兼容的数据。这里我们有一个中间件函数,它接受一个可调用的处理函数,但支持将二进制数据返回给客户端。

import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
import corsInit from "cors";

admin.initializeApp();

const cors = corsInit({ origin: true }); // TODO: Tighten

function callableRequest(handler) {
  if (!handler) {
    throw new TypeError("handler is required");
  }
  
  return (req, res) => {
    cors(req, res, (corsErr) => {
      if (corsErr) {
        console.error("Request rejected by CORS", corsErr);
        res.status(412).json({ error: "cors", message: "origin rejected" });
        return;
      }

      // for validateFirebaseIdToken, see https://github.com/firebase/functions-samples/blob/main/authorized-https-endpoint/functions/index.js
      validateFirebaseIdToken(req, res, () => { // validateFirebaseIdToken won't pass errors to `next()`

        try {
          const data = req.body;
          const context = {
            auth: req.user ? { token: req.user, uid: req.user.uid } : null,
            instanceIdToken: req.get("Firebase-Instance-ID-Token"); // this is used with FCM
            rawRequest: req
          };

          let result: any = await handler(data, context);

          if (result && typeof result === "object" && "buffer" in result) {
            res.writeHead(200, [
              ["Content-Type", res.contentType],
              ["Content-Disposition", "attachment; filename=" + res.filename]
            ]);
            
            res.end(result.buffer);
          } else {
            result = functions.https.encode(result);

            res.status(200).send({ result });
          }
        } catch (err) {
          if (!(err instanceof HttpsError)) {
            // This doesn't count as an 'explicit' error.
            console.error("Unhandled error", err);
            err = new HttpsError("internal", "INTERNAL");
          }

          const { status } = err.httpErrorCode;
          const body = { error: err.toJSON() };

          res.status(status).send(body);
        }
      });
    });
  };
})

functions.https.onRequest(callableRequest(async (data, context) => {
  /* ... */

  const contentBuffer = doc.getZip()
      .generate({type: "nodebuffer"});
  const targetName = "compiled.docx";

  return {
    buffer: contentBuffer,
    contentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
    filename: targetName
  }
}));

在您当前的代码中,您嵌套了许多奇怪的段try-catch不同范围内的块和变量。为了帮助解决这个问题,我们可以利用File#download()返回 aPromise来解析 Node.JS 中的文件内容,BufferFile#save()返回在上传Promise给定文件时解析的a。Buffer

将其汇总以重新上传到 Cloud Storage 可提供:

// This code is based off the examples provided for docxtemplater
// Copyright (c) Edgar HIPP [Dual License: MIT/GPLv3]

import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
import PizZip from "pizzip";
import Docxtemplater from "docxtemplater";

admin.initializeApp();

// The error object contains additional information when logged with JSON.stringify (it contains a properties object containing all suberrors).
function replaceErrors(key, value) {
  if (value instanceof Error) {
    return Object.getOwnPropertyNames(value).reduce(
      function (error, key) {
        error[key] = value[key];
        return error;
      },
      {}
    );
  }
  return value;
}

function errorHandler(error) {
  console.log(JSON.stringify({ error: error }, replaceErrors));

  if (error.properties && error.properties.errors instanceof Array) {
    const errorMessages = error.properties.errors
      .map(function (error) {
        return error.properties.explanation;
      })
      .join("\n");
    console.log("errorMessages", errorMessages);
    // errorMessages is a humanly readable message looking like this :
    // 'The tag beginning with "foobar" is unopened'
  }
  throw error;
}

exports.test2 = functions.https.onCall(async (data, context) => {
  const file_name = "example.docx"; // this is the file saved in my firebase storage
  const templateRef = await admin.storage().bucket()
      .file(file_name);
  const template_content = (await templateRef.download())[0];
  const zip = new PizZip(template_content);

  let doc;
  try {
    doc = new Docxtemplater(zip);
  } catch (error) {
    // Catch compilation errors (errors caused by the compilation of the template : misplaced tags)
    errorHandler(error);
  }

  doc.setData({
    first_name: "Fred",
    last_name: "Flinstone",
    phone: "0652455478",
    description: "Web app",
  });

  try {
    doc.render();
  } catch (error) {
    errorHandler(error);
  }

  const contentBuffer = doc.getZip().generate({ type: "nodebuffer" });

  // do something with contentBuffer
  // e.g. reupload to Cloud Storage
  const targetStorageRef = admin.storage().bucket().file("compiled.docx");
  await targetStorageRef.save(contentBuffer);

  return { bucket: targetStorageRef.bucket.name, name: targetName };
});

除了向调用者返回存储桶名称对之外,您还可以考虑向调用者返回访问 URL。这可能是一个 可以持续长达 7 天的签名 URLgetDownloadURL() ,一个可以持续到令牌被撤销的下载令牌 URL(如,此处描述的过程),Google 存储 URI ( gs://BUCKET_NAME/FILE_NAME)(不是访问 URL,但可以通过如果客户端通过存储安全规则,客户端 SDK 可以访问它)或使用其公共 URL直接访问它(在文件被标记为公共之后)。

根据上面的代码,你应该可以自己直接在返回文件中进行合并。

于 2021-06-15T19:28:47.290 回答