65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Angular、AngularJS 和 Web API 显示 PDF 文档及下载文件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.70/5 (5投票s)

2018 年 11 月 15 日

CPOL

16分钟阅读

viewsIcon

73697

downloadIcon

1609

一个示例 Web 应用程序,并讨论了使用 Web API 数据源(包括 ASP.NET Core)、客户端 Angular CLI 或 AngularJS 组件创建、显示和下载 PDF 文档,以及解决了 Web 浏览器兼容性以处理 PDF 文档。

引言

查看和获取 PDF 文档是 Web 应用程序的重要功能。PDF 文档可以来自服务器,通过 PDF 渲染工具获取数据,或直接从物理文件检索;也可以来自客户端,通过 JavaScript 中的 PDF 渲染工具获取数据或标记页面内容。本文提供了一个示例应用程序,并详细讨论了如何根据旧的、演进的和最新的技术以及 Web 浏览器来显示和下载服务器端 PDF 文档。

库和工具

具有不同项目类型的示例应用程序使用了以下库和工具。

Web 浏览器

如果您希望从文章和示例应用程序中获得最多的实践,您可以在本地机器上下载并安装更多类型的 Web 浏览器,无论是新的还是旧的。安装的浏览器类型将自动显示在 Visual Studio 的 **IIS Express (浏览器)** 工具栏下拉列表中。然后,您可以在运行解决方案之前选择一种浏览器类型。

构建和运行示例应用程序

下载的源文件包含不同的 Visual Studio 解决方案/项目类型。请选择您需要的并设置到您的本地机器上。服务器端数据服务项目嵌入在每个解决方案中,以便于数据访问。

您可以在 _C:\Program Files (x86)\Microsoft SDKs\TypeScript_ 文件夹中查看 Visual Studio 的 TypeScript 可用版本。所有下载的示例应用程序项目类型都在 *.csproj* 文件的 TypeScriptToolsVersion 节点中将 Visual Studio 的 TypeScript 版本设置为 3.7。我已经测试过,3.5 到 3.8 的所有版本对于 Visual Studio 中的 *.ts* 文件编译都是兼容的。如果您需要 Visual Studio 的 3.7 版本,您可以从 Microsoft 网站下载 安装包

要设置和运行带有 Angular CLI 的项目类型,您需要在本地机器上全局安装 node.js(推荐版本 10.16.x LTS 或更高版本)和 Angular CLI(推荐版本 8.1.2 或更高版本)。请参阅 node.jsAngular CLI 文档了解详情。

Pdf_AspNetCore_Ng_Cli

  1. 您需要在本地机器上使用 Visual Studio 2019(版本 16.4.x)。.NET Core 3.1 SDK 已包含在 Visual Studio 安装和更新中。

  2. 将源文件下载并解压缩到您的本地工作空间。

  3. 进入您的本地工作空间的物理位置,依次双击 _SM.Ng.Pdf.Web\AppDev_ 文件夹下的 _npm_install.bat_ 和 _ng_build.bat_ 文件。

    注意:每次更改 TypeScript/JavaScript 代码后,可能都需要执行 ng build 命令,而 npm install 命令仅在 node 模块包有更新时才需要执行。

  4. 使用Visual Studio 2019打开解决方案,并使用Visual Studio重新构建解决方案。

  5. 从 **IIS Express** 工具栏下拉列表中选择浏览器,然后单击 **IIS Express** 工具栏命令(或按 **F5**)启动示例应用程序。

Pdf_AspNet5_Ng_Cli

  1. 依次双击 _SM.WebApi.Pdf\ClientApp_ 文件夹下的 _npm_install.bat_ 和 _ng_build.bat_ 文件(有关设置 _Pdf_AspNetCore_Ng_Cli_ 项目的说明,请参阅相同的 NOTE)。

  2. 使用 Visual Studio 2019 或 2017 打开并重新生成解决方案。
  3. 从 **IIS Express** 工具栏下拉列表中选择浏览器,然后单击 **IIS Express** 工具栏命令(或按 **F5**)启动示例应用程序。

Pdf_AspNet5_NgJS_1.5

  1. 使用 Visual Studio 2019 或 2017 打开并重新生成解决方案。

  2. 从 **IIS Express** 工具栏下拉列表中选择浏览器,然后单击 **IIS Express** 工具栏命令(或按 **F5**)启动示例应用程序。

在整篇文章中,我使用 Angular 的示例应用程序项目进行代码演示和讨论。AngularJS 项目是为了兼容旧版本,以防某些开发人员仍需要它。这是 Angular 示例应用程序的主页。

例如,当点击“查看 IFrame 中的 PDF”下的“所有浏览器选项式场景”链接时,PDF 数据文档将显示在带有 IFrame 的弹出对话框中。

PDF 文档源

示例应用程序使用服务器端方法来提供 PDF 文档源,其中 PDF 字节数组通过 Web API 服务请求的数据构建。然后将字节数组发送到客户端进行处理。也可以使用 JavaScript 中的客户端方法(如 jsPDF.js)来构建包含请求的原始数据或页面内容的 PDF 文档。相比之下,服务器端方法功能更强大,特性和灵活性也更多。服务器提供的字节数组也可用于各种 PDF 文档显示和文件下载场景,无论是通过直接的 MIME 类型数据传输还是客户端 AJAX 调用。

为便于演示,PDF 数据报告“Product Order Activity”(如上截图所示)是使用我之前发布的 PdfDataReport 工具从静态数据源(模拟数据库数据)生成的。该工具使用 PDF 渲染库 PdfFileWriter,并根据报告架构和样式的通用 List 数据以及 XML 描述符动态构建 PDF 字节数组。PDF 数据文档的创建不是本文的重点。感兴趣的读者可以查看源代码和文章 A Generic and Advanced PDF Data List Reporting Tool 以获取详细信息。

使用 .NET Framework 4.x 构建的 PdfFileWriter 库仅适用于 ASP.NET 5 项目。而 .NET Core 3.x 和 ASP.NET Core 3.x 现在完全支持 Windows 桌面开发,包含 System.DrawingSystem.Windows.FormsSystem.Windows.Forms.DataVisualizationPresentationCore 等命名空间。这使得可以通过 ASP.NET Core API 数据服务来处理和提供 PDF 文档字节数组数据。您可以在 Pdf_AspNet5_Ng_CliPdf_AspNet5_NgJS_1.5 示例应用程序中看到 ASP.NET 5 Web API 数据服务如何将 PDF 数据发送到客户端。您也可以在 Pdf_AspNetCore_Ng_Cli 应用程序中看到 ASP.NET Core 3.1 数据服务如何将 PDF 数据输出到客户端。

从 ASP.NET Web API 请求 PDF 字节

当请求发送到 Web API 的 Get_OrderActivityPdf() 方法时,会调用 GetOrderActivityPdfBytes() 方法,然后依次调用通用的 GetDataPdfBytes() 方法来获取 PDF 字节数组。Web API 服务器驱动器上不会创建物理 PDF 文件。结果字节数组使用 ByteArrayContent 对象分配给 HttpResponseMessage 的内容。在将响应返回给调用者之前,还会为响应头设置其他项。

[Route("~/api/orderactivitypdf")]
[Route("~/api/orderactivitypdf/{requestId:int}")]
public HttpResponseMessage Get_OrderActivityPdf(int requestId = 0)
{
    //Call to generate PDF byte array.
    var pdfBytes = GetOrderActivityPdfBytes();

    //Create a new response.
    var response = new HttpResponseMessage(HttpStatusCode.OK);

    //Assign byte array to response content.
    response.Content = new ByteArrayContent(pdfBytes);

    //Set "Content-Disposition: attachment" for downloading file through direct MIME transfer. 
    if (requestId == 1)
    {
        //Explicitly specified as file downloading.
        response.Content.Headers.ContentDisposition = 
                  new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment");

        //This FileName in Content-Disposition won't be taken by Edge 
        //if using Blob for downloading file.
        response.Content.Headers.ContentDisposition.FileName = "OrderActivity.pdf";
    }

    //Add default file name that can be used by client code for both MIME transfer and AJAX Blob.
    response.Content.Headers.Add("x-filename", "OrderActivity.pdf");

    //Set MIME type.
    response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");

    //Returning base HttpResponseMessage type.
    return response;
}
        
//Get PDF byte array for a specific report. 
private byte[] GetOrderActivityPdfBytes()
{
    //Get data list fabricated for breaking page in the end of a group.
    var dataList = TestData.GetOrderDataList(50, 7, 9);

    //Define XML descriptor node.
    var descriptorFile = "report_desc_sm.xml";
    var descriptorNode = "reports/report[@id='SMStore302']";

    //Call to generate PDF byte arrays.
    return GetDataPdfBytes(dataList, descriptorFile, descriptorNode);
}

//Generic function for generating PDF byte array.
private byte[] GetDataPdfBytes<T>(List<T> dataList, string descriptorFile, string descriptorNode)
{
    string xmlDescriptor = string.Empty;
    XmlDocument objXml = new XmlDocument();
                        
    //Load XML report descriptor.            
    xmlDescriptor = File.ReadAllText(System.IO.Path.Combine
                    (System.Web.HttpRuntime.AppDomainAppPath, descriptorFile));
    objXml.LoadXml(xmlDescriptor);
            
    //Get node for a designated report.
    XmlNode elem = objXml.SelectSingleNode(descriptorNode);            

    //Call library tool to get PDF bytes.
    ReportBuilder builder = new ReportBuilder();
    var pdfBytes = builder.GetPdfBytes(dataList, elem.OuterXml);

    return pdfBytes;
}

对于 Get_OrderActivityPdf() 方法,有两个编码场景值得进一步讨论。

  1. 使用 IHttpActionResult 或 HttpResponseMessage 类型返回响应的选项。Web API 2.0 中的 IHttpActionResultHttpResponseMessage 的扩展包装器。它比 HttpResponseMessage 提供了几个好处,例如更好的实现结构、动作结果链式调用、简化的控制器单元测试、默认使用 AsyncAwait、易于创建自定义 ActionResult 等。由于示例应用程序演示的是单个 PDF 字节数组下载过程,并未考虑整个 Web API ActionResult 结构,因此这里的响应以最直接的方式作为基类 HttpResponseMessage 对象返回。如果您想使用 IhttpActionResult 作为返回类型,只需在方法中更改两行代码:方法定义和返回代码。
    public IHttpActionResult Get_OrderActivityPdf(int requestId = 0)
    {
        //Same code as shown previously...
    
        //Returning base HttpResponseMessage type.
        //return response;
       
        //Convert to IHttpActionResult type and return it.
        return ResponseMessage(response);
    }
  2. 关于 Content-Disposition 响应头项。该方法接受一个可选的 int 类型参数 requestId。此参数用于通过将硬编码的“attachment”分配给 ContentDispositionHeaderValue 类的构造函数来有条件地设置响应头中的 Content-Disposition: attachment。如果传递 requestId1,代码将添加此 Content-Disposition 头项,该头项指定当使用传统的 MIME 数据传输时,字节数组内容将被下载为文件。否则,响应内容应根据内容类型在浏览器中显示。这对于不支持 JavaScript Blob 对象及其派生结构的旧浏览器或浏览器很有用。对于使用 AJAX 数据传输和 JavaScript Blob 相关处理逻辑的文件下载,响应头中的 Content-Disposition: attachment 将被忽略。您可以使用 Fiddler2 或 Postman 等工具,从任何浏览器调用 Web API 方法时,检查响应头中是否包含 Content-Disposition 项。

ASP.NET Core 3.1 相关更改

ASP.NET Core 3.1 的 Web API 数据服务代码和工作流程与 ASP.NET 5 大致相同,除了返回自定义响应消息。ASP.NET 5 Web API 可以返回具有自定义头的 HttpResponseMessageIHttpActionResult 类型对象。当需要自定义头和内容时,ASP.NET Core 数据服务需要将它们添加到 HttpResponseMessage 对象实例中,然后通过自定义 IActionResult 包装器返回。

ASP.NET Core API 中的 Get_OrderActivityPdf 方法如下所示(与 ASP.NET 5 方法相同的大部分代码行已省略)。

public IActionResult Get_OrderActivityPdf(int requestId = 0)        
{
    - - -

    //Create a new response.
    var response = new HttpResponseMessage(HttpStatusCode.OK);

    - - -
    
    //Register HttpResponseMessage for disposing later.
    this.HttpContext.Response.RegisterForDispose(response);

    //Return IActionResult wrapper.
    return new HttpResponseMessageResult(response);
}

自定义 HttpResponseMessageResult 类转换响应头并将返回给调用者的响应内容流式传输。

public class HttpResponseMessageResult : IActionResult
{
    private readonly HttpResponseMessage _responseMessage;

    public HttpResponseMessageResult(HttpResponseMessage responseMessage)
    {
        _responseMessage = responseMessage; // could add throw if null
    }

    public async Task ExecuteResultAsync(ActionContext context)
    {
        context.HttpContext.Response.StatusCode = (int)_responseMessage.StatusCode;

        foreach (var header in _responseMessage.Content.Headers)
        {
            context.HttpContext.Response.Headers.TryAdd
                    (header.Key, new StringValues(header.Value.ToArray()));
        }

        using (var stream = await _responseMessage.Content.ReadAsStreamAsync())
        {
            await stream.CopyToAsync(context.HttpContext.Response.Body);
            await context.HttpContext.Response.Body.FlushAsync();
        }
    }
}

传统 MIME PDF 数据传输

RFC 3778 中定义的 MIME 类型“application/pdf”用于标准的 PDF 数据传输,并被所有主流浏览器(包括非常老的版本)支持。虽然这不是纯粹的 Angular 方式,但对于任何 Web 应用程序来说,使用传统的 MIME 类型数据传输来获取 PDF 文档是最简单直接的方式,尤其适用于需要支持大量旧版本浏览器的应用程序。

在 Angular 中,通过将 Web API URL 分配给目标源(如 iframe 标签、embed 标签或另一个窗口),可以轻松地从 Web API 方法获取 PDF 文档到浏览器。在示例应用程序中,iframe 用于使用任何浏览器的默认 PDF 查看器显示 PDF 数据报告“Product Order Activity”。

Angular 组件中的代码还使用 DomSanitizer API 的 bypassSecurityTrustResourceUrl 方法来包装源 URL。

//Default viewers with direct MIME transfer.
showPdfMimeType() {     
    //Request and assign MIME data to element src.
    this.iframeSrc = this.sanitizer.bypassSecurityTrustResourceUrl
                     (WebApiRootUrl + this.callerData.apiMethod);;
};

HTML 中的 iframe 标签及其设置也相当标准。

<iframe id="pdfViewer" [src]="iframeSrc" style="width: 100%; height: 450px;" 
 zindex="100" ></iframe>

要使用 MIME 类型数据传输直接下载 PDF 文件,只需在方法中指定一行代码。

downloadMimePdfFile(apiMethod) {
    //Assign MIME type data source to browser page.
    window.location.href = WebApiRootUrl + apiMethod;
}

点击演示主页上的“默认查看器,通过直接 MIME 类型传输”或“下载文件,通过直接 MIME 类型传输”链接将执行上述代码行并显示预期结果。

由于 Chrome、Firefox、Opera 和 Edge 浏览器都有自己的专有 PDF 查看器来显示 MIME 类型传输的文档,因此不需要考虑客户端设备上是否安装了 Adobe Reader。而 Internet Explorer 会嵌入 Adobe Reader 作为默认 PDF 查看器,因此您需要安装 Adobe Reader 到本地机器或将其设置为浏览器插件(请参阅 此链接 以获取支持)。如果您使用的是 Internet Explorer 11,即使安装了 Adobe Reader,仍然无法加载 PDF 查看器,您可能需要在 Adobe Reader 的“**编辑**”>“**首选项**”>“**安全(增强)**”面板中取消勾选两个复选框。

  • 启动时启用保护模式
  • 启用增强安全性

使用 JavaScript Blob 对象和 Blob URL

随着 Web 应用程序中 AJAX 数据传输模式的流行,Blob 对象在客户端 JavaScript 代码中处理文件相关操作变得越来越普遍。然而,不同浏览器之间的可用性和 API 可访问性差异很大,这给开发 Web 应用程序(关于文件内容显示或文件下载)带来了很多困惑和不便。让我们看看在 Angular 代码中如何使用 Blob 对象来处理 PDF 文档。

使用 Blob 显示 PDF 文档

使用 AJAX arraybuffer 数据源和 MIME 类型初始化 Blob 对象,在内存中创建 Blob URL,然后将 Blob URL 分配给 HTML 目标源,如以下代码所示,似乎很简单。

//Initiate blob object with byte array and MIME type.
let blob: any = new Blob([(response.data)], { type: 'application/pdf' });

//Create blobUrl from blob object.
let blobUrl: string = window.URL.createObjectURL(blob);

//Bind trustedUrl to element src.
this.iframeSrc = this.sanitizer.bypassSecurityTrustResourceUrl(blobUrl);          

//Revoking blobUrl.
window.URL.revokeObjectURL(blobUrl);

这对于支持 Blob 对象多年的 Chrome、Firefox 和 Opera 版本效果很好。虽然 Internet Explorer 自 10 版本以来就支持 Blob 对象,但该浏览器,甚至其后继者 Edge,都无法将 PDF 文档加载到目标源,即使 Blob URL 已生成。最可能的原因是浏览器处理 Blob URL 的逻辑和安全强制执行。

示例应用程序主页上的“通过 AJAX 调用使用 Blob 显示 PDF”链接演示了在 IFrame 中显示 PDF 数据报告。对于不支持的浏览器(如 Internet Explorer、Edge 和 Safari(Windows 版)),会显示一个消息对话框。

使用 Blob 下载 PDF 文件

使用 AJAX 数据源和 Blob URL 下载 PDF 文件对于 Chrome、Firefox 和 Opera 浏览器也有效,在这些浏览器中,需要一个带有 download 属性的动态 <a> 元素并模拟 click 事件来协调操作。

//Initiate blob object with byte array and MIME type.
let blob: any = new Blob([(response.data)], { type: 'application/pdf' });

//Create blobUrl from blob object.
let blobUrl: string = window.URL.createObjectURL(blob); 

//Use a download link.
let link: any = window.document.createElement('a'); 
if ('download' in link) {
    link.setAttribute('href', blobUrl);

    //Set the download attribute.
    //Edge doesn’t take filename here.
    link.setAttribute("download", fileName);

    //Simulate clicking download link.
    let event: any = window.document.createEvent('MouseEvents');
    event.initMouseEvent
        ('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
    link.dispatchEvent(event);    
}

Internet Explorer 11 和 Edge (EdgeHTML) 仍无法根据上述代码使用 Blob URL 下载文件。该浏览器也不支持动态链接中的 download 属性。因此,这些浏览器在使用 Blob URL 时会显示错误。

幸运的是,Internet Explorer 和 Edge (EdgeHTML) 都提供了 navigator.msSaveBlob 方法,可以直接使用 Blob 对象完成文件下载任务。默认的 fileName 值也会被此过程拾取。

//Use msSaveBlob if supported.
let blob: any = new Blob([response.body], { type: "application/pdf" });
if (navigator.msSaveBlob) {
    navigator.msSaveBlob(blob, fileName);
}

msSaveBlob 是 Internet Explorer 11 和 Edge (EdgeHTML) 在使用面向浏览器的兼容性选项时下载 PDF 文件的重要功能。请参阅后面的“使用选项式场景解决浏览器兼容性问题”部分。

新的 Microsoft Edge (Chromium) 的行为与 Chrome 浏览器相同,支持 Blob URL。

使用 PDF.js 查看器显示 PDF 文档

PDF.js 是 mozilla.org 拥有的 JavaScript PDF 渲染工具。其 Viewer API 提供了一个 UI,可在浏览器中基于 PDF.js 显示 PDF 文档。PDF.js 查看器实际上是 Firefox 浏览器的默认 PDF 查看器。开发人员可以构建自己的 PDF 查看器,方法是使用 _PDF.js_ 渲染器或修改现有的 _PDF.js_ 查看器。为方便演示,示例应用程序使用了基本未修改的 _PDF.js_ 查看器版本(仅通过在 _web/viewer.js_ 文件中将其重置为空字符串 var DEFAULT_URL = ''; 来删除默认 PDF 文件 URL)。所有 _PDF.js_ 和查看器库文件都位于 SM.WebApi.Pdf 项目的 _ClientApp/PdfViewer_ 或 _wwwroot/ClientApp/PdfViewer_ 文件夹中。

在撰写本文时,_PDF.js_ 的最新稳定版本是 2.0.943。此版本在所有最新主流浏览器版本上运行良好,但 Internet Explorer 11 除外,在 IE 11 中,在 IFrame 中关闭查看器时会抛出运行时错误。附带 _PDF.js_ 1.8.188 的示例应用程序是支持市场上所有主流浏览器的最新发布版本,包括 Internet Explorer 11。在您的实际应用程序中,如果您的应用程序不打算支持 Internet Explorer 11,您可以在 _…/ClientApp/PdfViewer_ 中用最新的稳定版本替换文件。您可以从 此处 的网站找到 _PDF.js_ 的所有发布版本。

要打开 IFrame 中的 PDF 文档,应从 Angular 组件调用 _viewer.js_ 中的 PDFViewerApplication.open 方法。在下面的行中,iframe 是 DOM 对象,response.data 是 PDF 字节数组对象。

//Call PDFJS Viewer open method and pass byte array data.
iframe.contentWindow.PDFViewerApplication.open(response.data);

示例应用程序主页上的“通过 AJAX 调用使用 PDFJS 查看器”链接可以显示所有主流浏览器的 Product Order Activity 报告,但 Windows 版 Safari 除外,因为其不支持 _PDF.js_,与 JavaScript Blob 对象一样。Apple 在 5.1.7 版本之后停止发布 Windows 版 Safari。我没有使用任何 Macintosh 机器,但我认为 Macintosh 后续版本的 Safari 应该都能很好地支持 Blob 对象和 PDF.js。

尽管 _PDF.js_ 查看器提供了不错的 PDF 内容显示选项和功能,但在某些情况下(尤其是使用较旧版本的 _PDF.js_ 或浏览器时)它比较庞大,并且会产生性能影响。我确实注意到,当使用最新的主流浏览器版本(包括 IE 11)时,较新版本的 _PDF.js_ 查看器加载具有大字节数组的数据速度更快。

使用选项式场景解决浏览器兼容性问题

对于所有主流浏览器,使用传统的 MIME 类型数据传输显示 PDF 文档和下载 PDF 文件通常没有问题,即使是 Safari(Windows 版)和较旧版本的 Internet Explorer。但是,当切换到使用 AJAX 调用和 JavaScript Blob 对象时,由于 JavaScript 对象和 API 的支持状态不同,浏览器表现也会不同。过去,Web 开发人员通常使用代码显式检查浏览器类型和版本,以有条件地指向特定代码段的执行。现在更好的做法是采用可用的选项式场景来解决可能的浏览器兼容性问题。示例应用程序提供了显示 PDF 文档和下载 PDF 文件的此类场景,如演示主页上“所有浏览器选项式场景”链接所示。由于本文前面部分已详细介绍了每种选项式方法的实现功能和代码片段,因此下面仅列出选项选择和执行顺序。读者可以尝试代码并进行任何更改以满足他们的需求。

在 IFrame 中查看 PDF

  • 首选使用 JavaScript Blob URL 和默认 PDF 查看器。
  • 如果失败,则渲染 _PDF.js_ 查看器,然后使用字节数组对象加载 PDF。
  • 如果仍然失败,则表示浏览器无法通过 AJAX 数据和 JavaScript 显示 PDF 文档。此时将使用直接 MIME 类型 PDF 数据传输。

下载 PDF 文件

  • 首选使用 JavaScript Blob URL。
  • 如果失败,则尝试调用其中一个 save-blob 方法。
  • 如果仍然失败,则切换到直接 MIME 类型 PDF 文件下载。

请注意,在选项式场景中将 MIME 类型数据传输作为最后一种选择存在一个缺点。由于 Blob 对象方法已经调用服务器加载了 AJAX 数据,如果浏览器不支持 Blob 对象,它将再次调用服务器直接传输 MIME 类型数据。如果数据量较大,这可能会导致明显的额外延迟。

历史

  • 2018/11/15:首次发布
  • 2020/2/22:使用 ASP.NET Core 3.1 Web API 数据服务和 Angular 8 CLI 更新了示例应用程序。添加了新章节,并编辑了文章中的大部分现有章节。之前的 Angular 6 CLI 源代码仍然可以 在此处下载
© . All rights reserved.