整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

Javascript基于模板生成PDF 文档

您的 js 应用程序中使用 eDocGen 从 JSON/XML/Database 创建 PDF 文档的指南。

文档生成是开发人员生活中非常普遍的需求。无论是电子商务网站、管理应用程序还是其他任何东西。它可以是发票生成、保险文件准备、医生处方、人力资源报价生成、工资单生成,你可以想到大量的用例。总是需要生成文档。

从开发人员的角度来看,有几种常见的方法可以完成这项工作。

  1. 创建 HTML 元素并打印它们以生成文档
  2. 使用一些库来生成文档
  3. 让服务器处理基于静态模板的文档生成

这些方法对我没有帮助。客户希望自己定制他们的文件。我一直在寻找一种方法,发现eDocGen是一种单点解决方案。

与其他服务不同,eDocGen 提供了可以集成到我们应用程序中的 RestAPI。

在本文中,我们将讨论如何将 eDocGen 集成到我们的 js 应用程序中,以从各种数据格式(如 JSON/XML/Database 模式)生成文档。请免费试用以开始编码。

让我们潜入并编写代码。

项目设置

出于演示目的,我创建了一个在 nodejs 上运行的示例 js 应用程序。

请按照以下步骤为我们设置编码游乐场。

步骤1:

用于npm init创建 package.json

第2步:

添加axios, form-data, request,xhr2开发此应用程序所需的依赖项npm install axios form-data request xhr2

第 3 步:

我们需要一个索引文件作为我们应用程序的起点。在根目录中创建一个 index.js 文件并修改 package.json 如下所示。

JSON

scripts": {
    "start": "node index.js"
  }

现在我们有一个基本的应用程序可以开始。这些步骤结束后,package.json 应该如下所示。

JSON

{
  "name": "nodejs-multiple-upload-files",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "axios": "^0.27.2",
    "form-data": "^4.0.0",
    "request": "^2.88.2",
    "xhr2": "^0.2.1"
  }
}

登录

虽然这篇文章是关于文档生成的,但我们需要登录才能获取我们的访问令牌。这是一个典型的JWT令牌,将用于授权文档生成 API。

JavaScript

var XMLHttpRequest = require("xhr2");
var xhr = new XMLHttpRequest();

module.exports.getToken = function (callback) {
  var data = JSON.stringify({
    username: "<your username>",
    password: "<password>",
  });

  xhr.addEventListener("readystatechange", function () {
    if (this.readyState === 4) {
      token = JSON.parse(this.responseText).token;

      console.log("User Token", token);
      callback(token);
    }
  });

  xhr.open("POST", "https://app.edocgen.com/login");
  xhr.setRequestHeader("content-type", "application/json");
  xhr.setRequestHeader("cache-control", "no-cache");
  xhr.send(data);
};

我们可以将令牌在应用程序中缓存一个小于过期时间的时间段,并使用它来生成文档或上传模板。到期时间过后,我们可以刷新令牌。缓存可以是 Redis 或内存缓存。这取决于您的应用程序设计。

模板设计

如上所述,eDocGen 允许用户自定义和上传模板。但是如何动态映射数据呢?有一些将数据映射到文档的规则。我们将看到如何使用规则创建模板。

看看这个文件。

eDocGen{}对动态字段使用由 括起来的标签。我们可以动态添加文字、logo、表格、条件语句、数学计算等。

例如,在上图中,

字符串字段: {Invoice_Number}{Invoice_Date}配置为替换为模板中的文本。模板中 {} 内的任何内容都将与输入数据匹配并替换。

动态表: 当表中存在需要循环和替换的数据数组时,动态表将是一个不错的选择。表中的行以 开头{#tablename}和结尾{/tablename}。在上面的示例中,发票表中的一行在第一列以 {#IT} 开头,在最后一列以 {/IT} 结尾。行中的列可以有字符串字段。在我们的示例中,{Item_description}并且{Amount}

图片: eDocGen 提供动态添加图片到模板的功能。请按照以下步骤操作。

  • 将图像上传到应以 image_id 响应的 eDogGen。
  • {%image_id}是用于填充图像的标签。图像image_id将从 eDocGen 存储中获取并替换为{%image_id}. 预计image_id将出现在输入数据中。

基于条件的动态字段(If-Else):可以使用条件标签有条件地显示内容。例如,当语言为英语时,文档中会显示{#language == "english"} 英语内容。同样,单个文档模板可以支持多种语言。

数学计算: eDocGen 支持基于模板中定义的公式的数学计算。可以使用以下公式计算发票中项目金额的总和。

JSON

{
    IT // array of items
    | summation:'Amount' // value that needs to be used for calculation 
    | format_number: ",” // format of the value
}

请前往JSON-to-pdf了解更多详情。

模板上传

准备好模板后,就可以将其上传以供使用。有两种方法。

  1. eDocGen 的交互式 UI - 与 Dropbox、驱动器、Evernote 集成
  2. eDocGen 的 RestAPI - 可以集成到客户端代码中以上传模板。

对于演示,我使用 UI 来上传模板。成功上传后,我们会得到一个 ID 作为响应。这是将用于生成文档的 ID。

如果您希望使用 API,请在此处留下 Upload API 结构供您参考。

JSON


"/api/v1/document": {
  "post": {
    "tags": [
      "Document"
    ],
    "description": "Upload template to eDocGen",
    "produces": [
      "application/json"
    ],
    "consumes": [
      "multipart/form-data"
    ],
    "parameters": [
      {
        "name": "documentFile",
        "description": "file to upload",
        "required": true,
        "type": "file",
        "in": "formData"
      },
      {
        "name": "x-access-token",
        "in": "header",
        "description": "JWT auth token from login",
        "required": true,
        "type": "string"
      }
    ],
    "responses": {
      "200": {
        "description": "Successfully uploaded document file"
      },
      "other": {
        "description": "Operation failed"
      }
    }
  }
}

JSON 到文档生成

现在我们准备好了模板。让我们生成文档。

文档生成有两个阶段。

  1. 请求生成文档
  2. 下载文件

第 1 步:请求生成文档

我们要求生成包含所需详细信息的文档,并得到确认。该过程异步发生在屏幕后面。

文档生成所需的参数

应用程序接口:POST-/api/v1/document/generate/bulk

请求正文

表格数据

文档 ID

模板的id

格式

pdf/docx(模板应支持格式)

输出文件名

输出文件的文件名。

输入文件

该文件包含标记值。支持 json、xlsx 和 xml。

标题

内容类型

多部分/表单数据

x-访问令牌

来自登录的 JWT 身份验证令牌

输入数据

inputFile 中的数据应该是模板定义的结构。例如,对于上面的模板映射将如下所示。

  • Invoice_Number在 JSON 中应该与{Invoice_Number}模板中的匹配。
  • 对于表数据,它应该是一个对象数组,带有Item_DescriptionAmount.
  • 金额应该是一个用于求和计算的数字。

第 2 步:下载文件

可以使用从上述步骤中获得的输出 ID 和输出文件的名称下载生成的文档。

我们将在这里使用两个 API。

  1. 了解文件存在的 API:/api/v1/output/name/${fileName}
  2. 下载文件的API:/api/v1/output/download/${outputId}

由于文档生成是异步发生的,要知道文档是否生成,我们将使用/api/v1/output/nameapi。

来自 API 的成功响应/api/v1/output/name将下载文件。

我将这两个步骤组合在一个 js 文件中,如下所示。

爪哇

let login = require("../edocgen_login");
const fs = require("fs");
const uuid = require("uuid");
const FormData = require("form-data");
let axios = require("axios");
let fileName = uuid.v4();
const headers = {
  "Content-Type": "multipart/form-data",
  "x-access-token": "null",
};

const hostName = "https://app.edocgen.com/api/v1/document/generate/bulk";


const outputFormat = "<format>";// pdf / docx
const documentId = "<template_id>";    // id of the template we want to use

module.exports.generateFiles =  function () {
  let authToken = login.getToken(function handleUsersList(token) {

    headers["x-access-token"] = token;

    var formBody = new FormData();
    formBody.append("documentId", documentId);
    formBody.append("format", outputFormat);
    formBody.append("outputFileName", fileName);
    // json data for the template
    formBody.append("inputFile", fs.createReadStream("./JSON_Data_Single.json"));   // local path forjson file

    let config = {
      method: "post",
      url: hostName,
      headers: headers,
      data: formBody,
    };

    console.log(`https://app.edocgen.com/api/v1/output/name/${fileName}.${outputFormat}`);
    let config_output = {
      method: "get",
      url:`https://app.edocgen.com/api/v1/output/name/${fileName}.${outputFormat}`,
      headers: headers,
    };

    const MAX_RETRY = 50;
    let currentRetry = 0;

    // max retry for 50 times
    function errorHandler() {
      if (currentRetry < MAX_RETRY) {
        currentRetry++;
        console.log("Document is not prepared yet! Retrying...");
        sendWithRetry(processResponse);
      } else {
        console.log("No luck. Document is not generated. Retried multiple times.");
      }
    }
    
    // sendWithRetry checks for file existence
    // on success, it proceeds to download the file
    // on failure, it retries 
    // todo: introduce spin lock
    function sendWithRetry(callback) {
      axios(config_output)
        .then(function (response) {
          if (response.data.output.length !== 1) {
            throw new axios.Cancel("Document is not found. Throw error.");
          } else {
            callback(response);
          }
        })
        .catch(errorHandler);
    }

    axios(config)
      .then(function (response) {
        sendWithRetry(processResponse);
      })
      .catch(function (error) {
        console.log(error);
      });
  });
};

function processResponse(response) {
  const outputId = response.data.output[0]._id;
  console.log(
    "Output Document is Generated. Id = ",
    response.data.output[0]._id
  );

  let config_download = {
    method: "get",
    url: `https://app.edocgen.com/api/v1/output/download/${outputId}`,
    headers: headers,
    responseType: "arraybuffer",

  };

  axios(config_download)
    .then(function (response) {
      console.log("Output file is downloaded " + `${fileName}.${outputFormat}`);
      fs.writeFileSync(`./${fileName}.${outputFormat}`, response.data);
    })
    .catch(function (error) {
      console.log("Error while downloading");
      console.log(error);
    });
}

单个与多个文档

当数据为单个 JSON 时,将生成给定格式的单个文档。

当数据是对象数组时,将生成每个数组元素的文档并将其压缩到文件中。

XML 到文档生成

XML 数据的过程很简单。我们需要做的就是传递 XML 文件来代替 JSON 数据。

就像JSON to documentXML to Document 我们也需要documentId, outputFileName, format and inputFile。除输入文件外,与 JSON 相同的所有内容都将是 XML 文件。

示例 XML 数据如下所示

XML

<?xml version="1.0" encoding="UTF-8" ?>
<marker>
  <values>
    <Invoice_Number>SBU-2053501</Invoice_Number>
    <Invoice_Date>31-07-2020</Invoice_Date>
    <Terms_Payment>Net 15</Terms_Payment>
    <Company_Name>ABC company</Company_Name>
    <Billing_Contact>ABC-Contact1</Billing_Contact>
    <Address>New york, United State</Address>
    <Email>support@edocgen.com</Email>
	<Logo>621cd2b783a6095d7b15a443</Logo> 
     <Sum1>6,751</Sum1>
	 <para>61b334ee7c00363e11da3439</para>
    <ITH>
      <Heading1>Item Description</Heading1>
      <Heading2>Amount</Heading2>
    </ITH>
    <IT>
      <Item_Description>Product Fees: X</Item_Description>
      <Amount>5,000</Amount>
    </IT>
  </values>
<marker>

我为 XML 作为数据源所做的代码更改很简单,如下所示

JavaScript

var formBody = new FormData();
formBody.append("documentId", documentId);
formBody.append("format", outputFormat);
formBody.append("outputFileName", fileName);
formBody.append("inputFile", fs.createReadStream("./XML_Invoice.xml"));

数据库到文档生成

从数据库生成文档几乎与其他数据源相同。但在这种情况下,我们需要提供连接详细信息和 SQL 查询,而不是上传 inputFile。

SQL 查询的输出列应与文档模板中的标签匹配。

让我们看看如何在代码中进行配置。

JavaScript

const templateId = "<template id>";
const dbVendor = "mysql";
const dbUrl = "<jdbc connection URL>";
const dbLimit = "100";
const dbPassword = "<database password>";
const dbQuery = "SELECT JSON_ARRAY(first, last) FROM customers;";
const outputFormat = "pdf";

// form data prepareation
let formBody = new FormData();
formBody.append("documentId", templateId);
formBody.append("format", outputFormat);
formBody.append("dbVendor", dbVendor);
formBody.append("dbUrl", dbUrl);
formBody.append("dbLimit", dbLimit);
formBody.append("dbPassword", dbPassword);
formBody.append("dbQuery", dbQuery);
formBody.append("outputFileName", fileName);

其他一切都将保持不变。

通过电子邮件发送文档

eDocGen 提供了通过电子邮件发送生成的文档的功能。

文档生成所需的参数

应用程序接口:POST-/api/v1/output/email

请求正文

JSON

出局

将需要通过电子邮件发送的输出 ID 放在这里

电子邮件ID

将用户电子邮件放在这里

标题

内容类型

多部分/表单数据

x-访问令牌

来自登录的 JWT 身份验证令牌

代码示例

let login = require("../edocgen_login");
let axios = require("axios");
const hostName = "https://app.edocgen.com/api/v1/output/email";
const headers = {
  "Content-Type": "application/json",
  "x-access-token": "null",
};

const outId = "<output ID>"; // Put output ID here which need to be sent via email
const emailId = "<user email>"; // Put user email here

module.exports.generateFiles = function () {
  let authToken = login.getToken(function handleUsersList(token) {
    headers["x-access-token"] = token;

    let payload = { outId: outId, emailId: emailId };
    let config = {
      method: "post",
      url: hostName,
      headers: headers,
      data: payload,
    };

    axios(config)
      .then(function (response) {
        console.log("Mail sent");
      })
      .catch(function (error) {
        console.log(error);
      });
  });
};

来自 eDocGen 的电子邮件如下所示

还有很多其他的功能我在这里无法涵盖。但我希望这篇文章可以为您提供一个从哪里开始的想法。

前一阵儿被某网站的 JS 反爬流程难住了,至今也没明白它的反扒原理和攻破方法。最终找到了一个自动化脚本工具 autoit 3,用一个笨方法将人手动点击浏览器的动作脚本化,达到网页数据获取目的,拿到网页文件后,再用代码解析,曲线完成任务。

本文将介绍这个自动化的过程,并带编写一个完整的 autoit 3 爬虫脚本,希望对各位读者朋友有所启发。

自动化操作分析

以国家信息安全漏洞共享平台为例,它在返回数据前发起了两次 512 响应,第三次浏览器带着动态生成的 Cookie 信息才能得到数据。

这次咱们直接从网页入手,操作键盘找到“下一页” 按钮,按下 Enter 键完全请求。通过键盘定位到 “下页” 按钮的过程为:

  1. 第一步,按下 “End” 键,到达网页底部;
  2. 第二步,反向 “Tab” 键,按 15 次就可定位到 “下页” 按钮。

接着就可以编写自动化脚本了,把刚刚的手动操作翻译成脚本命令:

  1. 切换为英文输入法,保证浏览器输入栏信息正确;
  2. 打开 Chrome 浏览器;
  3. 向浏览器地址栏输入目标 URL;
  4. 按下 Enter 键,等待 2 秒保证页面数据加载完成;
  5. 按下 Ctrl +S 键,并向存储路径发送存储文件名称,等待“保存”操作完成;
  6. 按下 End 键盘,定位到页面底部;
  7. 按下反向 Tab 键 15 次,定位到 “下页” 按钮;
  8. 按下 Enter 键,请求下一页数据;
  9. 循环 5-8 这个过程 N 次,N=需要爬的页数。

这个流程,对其他高反扒的信息发布网站,也是适用的。

编写自动化脚本

按照上面的流程,编写 autoit 自动化脚本,创建一个 myspider.au3 文件:

#include <AutoItConstants.au3>

;;切换为英文输入法,保证浏览器输入正常
$hWnd = WinGetHandle("[ACTIVE]");$hWnd 为目标窗口句柄,这里设置的是当前活动窗口
$ret = DllCall("user32.dll", "long", "LoadKeyboardLayout", "str", "08040804", "int", 1 + 0)
DllCall("user32.dll", "ptr", "SendMessage", "hwnd", $hWnd, "int", 0x50, "int", 1, "int", $ret[0])

$url = "https://www.cnvd.org.cn/flaw/list.htm"
spiderData($url)

Func spiderData($url)
	;;打开 Chrome 浏览器窗口
	$chromePath = "C:\Users\admin\AppData\Local\Google\Chrome\Application\chrome.exe"
	Run($chromePath)

	;;登录窗口显示
	WinWaitActive("[CLASS:Chrome_WidgetWin_1]")
	;; 休息2000毫秒
	Sleep(2000)
	;; 移动窗口
	WinMove("[CLASS:Chrome_WidgetWin_1]", "打开新的标签页 - Google Chrome", 0, 0,1200,740,2)

	;; 休息500毫秒
	Sleep(500)

	;;地址栏输入URL 并按下 Enter 键
	Send($url)
	Sleep(500)
	Send("{enter}")
	Sleep(3000)

	;; 循环爬取需要的页数,测试只爬3页
	For $i = 1 To 3 Step 1
		;;打开右键另存为按钮: Ctrl+S
		send("^s")
		Sleep(2000)
		WinWait("[CLASS:#32770]","",10)

		;;将存储路径设置到另存为组件输入框 Edit1 里
		$timeNow = @YEAR & "" & @MON & "" & @MDAY & "" & @HOUR & "" & @MIN
		$savePath = "F:\A2021Study\ListData\" &$timeNow &  "_page" & $i & ".html"
		ControlSetText("另存为","", "Edit1", $savePath)

		;;点击确定
		ControlClick("另存为","","Button2")

		;;再次确定
		WinWait("[CLASS:#32770]","",10)
		ControlClick("确认另存为","","Button1")

		;; 等待保存操作完成
		Sleep(3000)

		;; 定位到下一页按钮,并触发点击下一页
		send("{END}")
		Send("+{TAB 15}")
		Send("{enter}")

		;;点击确定后,等待网页加载完成
		Sleep(3000)
	Next

	;; 整个操作完成,则关闭浏览器
	Send("^w")
EndFunc

脚本编写过程中,有几点需要注意:

  • 第一,输入法切换很重要,否则 URL 地址栏的值很容易乱;
  • 第二, windows 的文件路径是反斜杠 \ ,否则会导致另存为的路径无法识别;
  • 第三,帮助文档里面提供的关闭方法是 WinClose ,但是反复测试,确定这个方法不靠谱,要么会引起浏览器异常关闭导致下次打开会恢复上次的网址;要么完全不生效。迂回的解决办法是用关闭按键 Ctrl+W ,完成了正常关闭的目的。

因为爬虫要作为定时任务运行的,为避免打开太多浏览器窗口,因此需要在脚本结束时关闭浏览器。

启示录

数据爬取一般分为列表页和详情页,定位点击每一条详情的过程比较麻烦,所以爬取详情页面的和列表分开,用 Java 代码解析所有详情 URL 后,再由另一个 autoit 脚本去获取详情页面,这个流程大家可以自己写一下,这里就不详细介绍了。

最后再汇总下整个爬取的流程:

第一步,执行爬取列表的 autoit 脚本,得到列表页面 html;
第二步,解析列表页 html ,得到所有详情页面的 URL ,写入到文件中;
第三步,执行爬取详情页面的 autoit 脚本,它遍历第二步的目标 URL ,得到详情页 html ;
第四步,解析详情页 html 文件,得到详情数据。

总控流程、第二步和第四步的解析都用 Java 代码完成,用 Runtime.getRuntime().exec("cmd /c E:\A2021Study\Autoit3\myspider.au3") 调用脚本,文件路径是反斜杠。

这个方法虽然有点笨,但完全是人工操作浏览器,能够对抗反爬虫策略,感兴趣的朋友可以执行下本文的脚本试试。

autoit 还是蛮有意思的,语法也很简单,DirCreate 创建文件,iniread 读取配置项,一行代码顶 Java 几十行,不得不承认 Java 操作文件才是最麻烦的哇!

:如何用 JS 一次获取 HTML 表单的所有字段 ?

考虑一个简单的 HTML 表单,用于将任务保存在待办事项列表中:

  <form>
    <label for="name">用户名</label>
    <input type="text" id="name" name="name" required>

    <label for="description">简介</label>
    <input type="text" id="description" name="description" required>

    <label for="task">任务</label>
    <textarea id="task" name="task" required></textarea>

    <button type="submit">提交</button>
  </form>

上面每个字段都有对应的的type,ID和 name属性,以及相关联的label。用户单击“提交”按钮后,我们如何从此表单中获取所有数据?

有两种方法:一种是用黑科技,另一种是更清洁,也是最常用的方法。为了演示这种方法,我们先创建form.js,并引入文件中。

从事件 target 获取表单字段

首先,我们在表单上为Submit事件注册一个事件侦听器,以停止默认行为(它们将数据发送到后端)。

然后,使用this.elements或event.target.elements访问表单字段:

相反,如果需要响应某些用户交互而动态添加更多字段,那么我们需要使用FormData。

使用 FormData

首先,我们在表单上为submit事件注册一个事件侦听器,以停止默认行为。接着,我们从表单构建一个FormData对象:

const form = document.forms[0];

form.addEventListener("submit", function(event) {
  event.preventDefault();
  const formData = new FormData(this);
});

除了append()、delete()、get()、set()之外,FormData 还实现了Symbol.iterator。这意味着它可以用for...of 遍历:

const form = document.forms[0];

form.addEventListener("submit", function(event) {
  event.preventDefault();
  const formData = new FormData(this);

  for (const formElement of formData) {
    console.log(formElement);
  }
})

除了上述方法之外,entries()方法获取表单对象形式:

const form = document.forms[0];

form.addEventListener("submit", function(event) {
  event.preventDefault();
  const formData = new FormData(this);
  const entries = formData.entries();
  const data = Object.fromEntries(entries);
});

这也适合Object.fromEntries() (ECMAScript 2019)

为什么这有用?如下所示:

const form = document.forms[0];

form.addEventListener("submit", function(event) {
  event.preventDefault();
  const formData = new FormData(this);
  const entries = formData.entries();
  const data = Object.fromEntries(entries);

  // send out to a REST API
  fetch("https://some.endpoint.dev", {
    method: "POST",
    body: JSON.stringify(data),
    headers: {
      "Content-Type": "application/json"
    }
  })
    .then(/**/)
    .catch(/**/);
});

一旦有了对象,就可以使用fetch发送有效负载。

小心:如果在表单字段上省略name属性,那么在FormData对象中刚没有生成。

总结

要从HTML表单中获取所有字段,可以使用:

  • this.elements或event.target.elements,只有在预先知道所有字段并且它们保持稳定的情况下,才能使用。

使用FormData构建具有所有字段的对象,之后可以转换,更新或将其发送到远程API。*


作者:VALENTINO GAGLIARDI 译者:前端小智 来源:valentinog

原文:https://www.valentinog.com/blog/form-data/