看一下 的类型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 中的文件内容,Buffer
并File#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直接访问它(在文件被标记为公共之后)。
根据上面的代码,你应该可以自己直接在返回文件中进行合并。