整合营销服务商

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

免费咨询热线:

前端教程:JavaScript页面打印

多时候,你想给一个按钮,在网页上通过一个实际的打印机打印出网页的内容。

JavaScript可使用window对象的print函数就可以实现这样的功能。

当执行JavaScript的print函数window.print()将会打印当前页面。可以使用onclick事件如下直接调用此函数:

<head>
<script type="text/javascript">
<!--
//-->
</script>
</head>
<body>
<form>
<input type="button" value="Print" onclick="window.print()" />
</form>
</body>1234567891011复制代码类型:[javascript]

这将产生以下按钮,打印此页。

这符合打印出来的页面,但这个不是一个推荐的方式。打印机友好的页面实际上只是一个文本,没有图像,图形或广告页面。

可以使用以下页式打印机友好方式:

使页面的副本,并离开了不需要的文本和图形,然后从原始链接到该打印机友好的页面。

如果你不想让页面的额外副本,那么可以使用像适当的注释标记打印文本 <!-- PRINT STARTS HERE -->..... <!-- PRINT ENDS HERE --> 然后你可以使用PERL或其他脚本在后台清除打印文本和显示进行最后的打印。网站使用同样的方法给打印设备对我们网站的访客。

如何打印页面:

如果没有人在提供上述设备,那么你可以使用浏览器的标准工具栏让网页打印出来。按照链接如下:

File --> Print --> Click OK button.

开课吧广场-人才学习交流平台

您的 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 的电子邮件如下所示

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

容转载自:https://javascript.info/try-catch

不管你多么的精通编程,有时我们的脚本总还是会有一些错误。可能是因为我们的编写出错,或是与预期不同的用户输入,或是错误的的服务端返回或者是其他总总不同的原因。

通常,一段代码会在出错的时候“死掉”(停止执行)并在控制台将异常打印出来。

但是有一种更为合理的语法结构 try..catch,它会在捕捉到异常的同时不会使得代码停止执行而是可以做一些更为合理的操作。

“try..catch” 语法

try..catch 结构由两部分组成:try 和 catch:

try {
 // 代码...
} catch (err) {
 // 异常处理
}

它按照以下步骤执行:

  • 首先,执行 try {…} 里面的代码。
  • 如果执行过程中没有异常,那么忽略 catch(err) 里面的代码,try 里面的代码执行完之后跳出该代码块。
  • 如果执行过程中发生异常,控制流就到了 catch(err) 的开头。变量 err(可以取其他任何的名称)是一个包含了异常信息的对象。

所以,发生在 try {…} 代码块的异常不会使代码停止执行:我们可以在 catch 里面处理异常。

让我们来看更多的例子。

没有异常的例子:

try {
 alert('Start of try runs'); 
 // ...这里没有异常
 alert('End of try runs'); 
} catch(err) {
 alert('Catch is ignored, because there are no errors'); 
}
alert("...Then the execution continues");

包含异常的例子:

try {
 alert('Start of try runs'); // (1) <--
 lalala; // 异常,变量未定义!
 alert('End of try (never reached)'); // (2)} catch(err) {
 alert(`Error has occured!`); // (3) <--}
alert("...Then the execution continues");

try..catch only works for runtime errors

要使得 try..catch 能工作,代码必须是可执行的,换句话说,它必须是有效的 JavaScript 代码。

如果代码包含语法错误,那么 try..catch 不能正常工作,例如含有未闭合的花括号:

try {
 {{{{{{{{{{{{
} catch(e) {
 alert("The engine can't understand this code, it's invalid");
}

JavaScript 引擎读取然后执行代码。发生在读取代码阶段的异常被称为 “parse-time” 异常,它们不会被 try..catch 覆盖到(包括那之间的代码)。这是因为引擎读不懂这段代码。

所以,try..catch 只能处理有效代码之中的异常。这类异常被称为 “runtime errors”,有时候也称为 “exceptions”。

try..catch works synchronously

如果一个异常是发生在计划中将要执行的代码中,例如在setTimeout中,那么 try..catch 不能捕捉到:

try {
 setTimeout(function() {
 noSuchVariable; // 代码在这里停止执行
 }, 1000);
} catch (e) {
 alert( "won't work" );
}

因为 try..catch 包裹了计划要执行的 setTimeout 函数。但是函数本身要稍后才能执行,这时引擎已经离开了 try..catch 结构。

要捕捉到计划中将要执行的函数中的异常,那么 try..catch 必须在这个函数之中:

setTimeout(function() {
 try {
 noSuchVariable; // try..catch 处理异常!
 } catch (e) {
 alert( "error is caught here!" );
 }
}, 1000);

Error 对象

当一个异常发生之后,JavaScript 生成一个包含异常细节的对象。这个对象会作为一个参数传递给 catch:

try {
 // ...
} catch(err) { 
// “异常对象”,可以用其他参数名代替 err
 // ...
}

对于所有内置的异常,catch 代码块捕捉到的相应的异常的对象都有两个属性:

name : 异常名称,对于一个未定义的变量,名称是 “ReferenceError”

message : 异常详情的文字描述。

还有很多非标准的属性在绝大多数环境中可用。其中使用最广泛并且被广泛支持的是:

stack : 当前的调用栈:用于调试的,一个包含引发异常的嵌套调用序列的字符串。

例如:

try {
 lalala; // 异常,变量未定义!
} catch(err) {
 alert(err.name); // ReferenceError
 alert(err.message); // lalala 未定义
 alert(err.stack); // ReferenceError: lalala 在... 中未定义
 // 可以完整的显示一个异常
 // 可以转化成 "name: message" 形式的字符串
 alert(err); // ReferenceError: lalala 未定义
}

使用 “try..catch”

让我们一起探究一下真实使用场景中 try..catch 的使用。

正如我们所知,JavaScript 支持 JSON.parse(str) 方法来解析 JSON 编码的值。

通常,它被用来解析从网络,从服务器或是从其他来源收到的数据。

我们收到数据后,像下面这样调用 JSON.parse:

let json = '{"name":"John", "age": 30}'; // 来自服务器的数据let user = JSON.parse(json); // 将文本表示转化成 JS 对象// 现在 user 是一个解析自 json 字符串的有自己属性的对象
alert( user.name ); // John
alert( user.age ); // 30

如果 json 格式错误,JSON.parse 就会报错,代码就会停止执行。

得到报错之后我们就应该满意了吗?当然不!

如果这样做,当拿到的数据出错,用户就不会知道(除非他们打开开发者控制台)。代码执行失败却没有提示信息会导致糟糕的用户体验。

让我们来用 try..catch 来处理这个错误:

let json = "{ bad json }";
try {
 let user = JSON.parse(json); // <-- 当这里抛出异常...
 alert( user.name ); // 不工作
} catch (e) {
 // ...跳到这里继续执行
 alert( "Our apologies, the data has errors, we'll try to request it one more time." );
 alert( e.name );
 alert( e.message );
}

我们用 catch 代码块来展示信息,但是我们可以做的更多:发送一个新的网络请求,给用户提供另外的选择,把异常信息发送给记录日志的工具,… 。所有这些都比让代码直接停止执行好的多。

抛出自定义的异常

如果这个 json 数据语法正确,但是少了我们需要的 name 属性呢?

像这样:

let json = '{ "age": 30 }'; // 不完整的数据
try {
 let user = JSON.parse(json); // <-- 不抛出异常
 alert( user.name ); // 没有 name!
} catch (e) {
 alert( "doesn't execute" );
}

这里 JSON.parse 正常执行,但是缺少 name 属性对我们来说确实是个异常。

为了统一的异常处理,我们会使用 throw 运算符。

“Throw” 运算符

throw 运算符生成异常对象。

语法如下:

throw <error object>

技术上讲,我们可以使用任何东西来作为一个异常对象。甚至可以是基础类型,比如数字或者字符串。但是更好的方式是用对象,尤其是有 name 和 message 属性的对象(某种程度上和内置的异常有可比性)。

JavaScript 有很多标准异常的内置的构造器:Error、 SyntaxError、ReferenceError、TypeError 和其他的。我们也可以用他们来创建异常对象。

他们的语法是:

let error = new Error(message);// 或者let error = new SyntaxError(message);let error = new ReferenceError(message);// ...

对于内置的异常对象(不是对于其他的对象,而是对于异常对象),name 属性刚好是构造器的名字。message 则来自于参数。

例如:

let error = new Error("Things happen o_O");
alert(error.name); // Error
alert(error.message); // Things happen o_O

让我们来看看 JSON.parse 会生成什么样的错误:

try {
 JSON.parse("{ bad json o_O }");
} catch(e) {
 alert(e.name); // SyntaxError
 alert(e.message); // Unexpected token o in JSON at position 0
}

如我们所见, 那是一个 SyntaxError。

假定用户必须有一个 name 属性,在我们看来,该属性的缺失也可以看作语法问题。

所以,让我们抛出这个异常。

let json = '{ "age": 30 }'; // 不完整的数据
try {
 let user = JSON.parse(json); // <-- 没有异常
 if (!user.name) {
 throw new SyntaxError("Incomplete data: no name"); // (*)
 }
 alert( user.name );
} catch(e) {
 alert( "JSON Error: " + e.message ); // JSON Error: Incomplete data: no name
}

在 (*) 标记的这一行,throw 操作符生成了包含着我们所给的 message 的 SyntaxError,就如同 JavaScript 自己生成的一样。try 里面的代码执行停止,控制权转交到 catch 代码块。

现在 catch 代码块成为了处理包括 JSON.parse 在内和其他所有异常的地方。

再次抛出异常

上面的例子中,我们用 try..catch 处理没有被正确返回的数据,但是也有可能在 try {…} 代码块内发生另一个预料之外的异常,例如变量未定义或者其他不是返回的数据不正确的异常。

例如:

let json = '{ "age": 30 }'; // 不完整的数据
try {
 user = JSON.parse(json); // <-- 忘了在 user 前加 "let"
 // ...
} catch(err) {
 alert("JSON Error: " + err); // JSON Error: ReferenceError: user is not defined
 // ( 实际上并没有 JSON Error)
}

当然,一切皆有可能。程序员也是会犯错的。即使是一些开源的被数百万人用了几十年的项目 —— 一个严重的 bug 因为他引发的严重的黑客事件被发现(比如发生在 ssh 工具上的黑客事件)。

对我们来说,try..catch 是用来捕捉“数据错误”的异常,但是 catch 本身会捕捉到所有来自于 try 的异常。这里,我们遇到了预料之外的错误,但是仍然抛出了 “JSON Error” 的信息,这是不正确的,同时也会让我们的代码变得更难调试。

幸运的是,我们可以通过其他方式找出这个异常,例如通过它的 name 属性:

try {
 user = { /*...*/ };
} catch(e) {
 alert(e.name); // "ReferenceError" for accessing an undefined variable
}

规则很简单:

catch 应该只捕获已知的异常,而重新抛出其他的异常。

“rethrowing” 技术可以被更详细的理解为:

  • 捕获全部异常。
  • 在 catch(err) {…} 代码块,我们分析异常对象 err。
  • 如果我们不知道如何处理它,那我们就做 throw err 操作。

在下面的代码中,为了达到只在 catch 代码块处理 SyntaxError 的目的,我们使用重新抛出的方法:

let json = '{ "age": 30 }'; // 不完整的数据
try {
 let user = JSON.parse(json);
 if (!user.name) {
 throw new SyntaxError("Incomplete data: no name");
 }
 blabla(); // 预料之外的异常
 alert( user.name );
} catch(e) {
 if (e.name == "SyntaxError") {
 alert( "JSON Error: " + e.message );
 } else {
 throw e; // rethrow (*)
 }
}

(*) 标记的这行从 catch 代码块抛出的异常,是独立于我们期望捕获的异常之外的,它也能被它外部的 try..catch 捕捉到(如果存在该代码块的话),如果不存在,那么代码会停止执行。

所以,catch 代码块只处理已知如何处理的异常,并且跳过其他的异常。

下面这段代码将演示,这种类型的异常如何被另外一层 try..catch 代码捕获。

function readData() {
 let json = '{ "age": 30 }';
 try {
 // ...
 blabla(); // 异常!
 } catch (e) {
 // ...
 if (e.name != 'SyntaxError') {
 throw e; // 重新抛出(不知道如何处理它)
 }
 }
}
try {
 readData();
} catch (e) {
 alert( "External catch got: " + e ); // 捕获到!
}

例子中的 readData 只能处理 SyntaxError,而外层的 try..catch 能够处理所有的异常。

try..catch..finally

然而,这并不是全部。

try..catch 还有另外的语法:finally

如果它有被使用,那么,所有条件下都会执行:

  • try 之后,如果没有异常。
  • catch 之后,如果没有异常。

该扩展语法如下所示:

try {
 ... 尝试执行的代码 ...
} catch(e) {
 ... 异常处理 ...
} finally {
 ... 最终会执行的代码 ...
}

试试运行这段代码:

try {
 alert( 'try' );
 if (confirm('Make an error?')) BAD_CODE();
} catch (e) {
 alert( 'catch' );
} finally {
 alert( 'finally' );
}

这段代码有两种执行方式:

  • 如果对于 “Make an error?” 你的回答是 “Yes”,那么执行 try -> catch -> finally。
  • 如果你的回答是 “No”,那么执行 try -> finally。

finally 的语法通常用在:我们在 try..catch 之前开始一个操作,不管在该代码块中执行的结果怎样,我们都想结束的时候执行某个操作。

比如,生成斐波那契数的函数 fib(n) 的执行时间,通常,我们在开始和结束的时候测量。但是,如果该函数在被调用的过程中发生异常,就如执行下面的代码就会返回负数或者非整数的异常。

任何情况下,finally 代码块就是一个很好的结束测量的地方。

这里,不管前面的代码正确执行,或者抛出异常,finally 都保证了正确的时间测量。

let num = +prompt("Enter a positive integer number?", 35)let diff, result;function fib(n) {
 if (n < 0 || Math.trunc(n) != n) {
 throw new Error("Must not be negative, and also an integer.");
 }
 return n <= 1 ? n : fib(n - 1) + fib(n - 2);}let start = Date.now();try {
 result = fib(num);} catch (e) {
 result = 0;} finally {
 diff = Date.now() - start;}
alert(result || "error occured");
alert( `execution took ${diff}ms` );

你可以通过后面的不同的输入来检验上面代码的执行:先在 prompt 弹框中先输入 35 —— 它会正常执行,try 代码执行后执行 finally 里面的代码。然后再输入 -1,会立即捕获一个异常,执行时间将会是 0ms。两次的测量结果都是正确的。

换句话说,有两种方式退出这个函数的执行:return 或是 throw,finally 语法都能处理。

Variables are local inside try..catch..finally

请注意:上面代码中的 result 和 diff 变量,都需要在 try..catch 之前声明。

否则,如果用 let 在 {…} 代码块里声明,那么只能在该代码块访问到。

finally and return

finally 语法支持任何的结束 try..catch 执行的方式,包括明确的 return。

下面就是 try 代码块包含 return 的例子。在代码执行的控制权转移到外部代码之前,finally 代码块会被执行。

function func() {
 try {
 return 1;
 } catch (e) {
 /* ... */
 } finally {
 alert( 'finally' );
 }}
alert( func() ); // 先 alert "finally" 里面的内容,再执行这里
try..finally

try..finally 结构也很有用,当我们希望确保代码执行完成不想在这里处理异常时,我们会使用这种结构。

function func() {
 // 开始做需要被完成的操作(比如测量)
 try {
 // ...
 } finally {
 // 完成前面要做的事情,即使 try 里面执行失败
 }}

上面的代码中,由于没有 catch,try 代码块中的异常会跳出这块代码的执行。但是,在跳出之前 finally 里面的代码会被执行。

全局 catch

Environment-specific

这个部分的内容并不是 JavaScript 核心的一部分。

设想一下,try..catch 之外出现了一个严重的异常,代码停止执行,可能是因为编程异常或者其他更严重的异常。

那么,有没办法来应对这种情况呢?我们希望记录这个异常,给用户一些提示信息(通常,用户是看不到提示信息的),或者做一些其他操作。

虽然没有这方面的规范,但是代码的执行环境一般会提供这种机制,因为这真的很有用。例如,Node.JS 有 process.on(‘uncaughtException’) 。对于浏览器环境,我们可以绑定一个函数到 window.onerror,当遇到未知异常的时候,它就会执行。

语法如下:

window.onerror = function(message, url, line, col, error) {
 // ...};

message : 异常信息。

url : 发生异常的代码的 URL。

line, col : 错误发生的代码的行号和列号。

error : 异常对象。

例如:

<script>
 window.onerror = function(message, url, line, col, error) {
 alert(`${message}\n At ${line}:${col} of ${url}`);
 };
 function readData() {
 badFunc(); // 哦,出问题了!
 }
 readData();</script>

window.onerror 的目的不是去处理整个代码的执行中的所有异常 —— 这几乎是不可能的,这只是为了给开发者提供异常信息。

也有针对这种情况提供异常日志的 web 服务,比如 https://errorception.com 或者 http://www.muscula.com。

它们会这样运行:

  • 我们注册这个服务,拿到一段 JS 代码(或者代码的 URL),然后插入到页面中。
  • 这段 JS 代码会有一个客户端的 window.onerror 函数。
  • 发生异常时,它会发送一个异常相关的网络请求到服务提供方。
  • 我们只要登录服务方提供方的网络接口就可以看到这些异常。

总结

try..catch 结构允许我们处理执行时的异常,它允许我们尝试执行代码,并且捕获执行过程中可能发生的异常。

语法如下:

try {
 // 执行此处代码
} catch(err) {
 // 如果发生异常,跳到这里
 // err 是一个异常对象
} finally {
 // 不管 try/catch 怎样都会执行
}

可能会没有 catch 代码块,或者没有 finally 代码块。所以 try..catch 或者 try..finally 都是可用的。

异常对象包含下列属性:

  • message —— 我们能阅读的异常提示信息。
  • name —— 异常名称(异常对象的构造函数的名称)。
  • stack (没有标准) —— 异常发生时的调用栈。

我们也可以通过使用 throw 运算符来生成自定义的异常。技术上来讲,throw 的参数没有限制,但是通常它是一个继承自内置的 Error 类的异常对象。更对关于异常的扩展,请看下个章节。

重新抛出异常,是一种异常处理的基本模式:catch 代码块通常处理某种已知的特定类型的异常,所以它应该抛出其他未知类型的异常。

即使我们没有使用 try..catch,绝大多数执行环境允许我们设置全局的异常处理机制来捕获出现的异常。浏览器中,就是 window.onerror。