许多开发人员面临着为Web应用程序创建PDF报告的任务,这是完全自然的要求。 在使用Rotativa库生成报告时,我想提醒您我对此类任务的经验。 在我看来,这是为此目的而设计的最方便的库之一,但是在使用它时,我遇到了几个我想谈的不明显的地方。
老实说,我想分享在集成该库的过程中踩到的所有耙子,这无疑是非常快速且非常方便的。
在本文中,我不会解决选择库的问题。 每个人都有其使用该主题的理由。 我选择Rotativa,是因为以最低的设置成本,它可以满足客户的需求。 除了她,我还尝试了三到四个选项。
问题陈述
ASP.NET MVC,.NET版本4.6上的Web应用程序。 除部署外,其他功能在此情况下均不相关。 预计将在Azure上进行部署。 这很重要,因为某些其他库(例如HiQPdf)不会在某些Azure配置中转移安装,已在文档中记录。
我需要通过一个链接打开某个静态HTML报告,并通过第二个链接打开同一报告的PDF版本。 报告本身只是一组表,字段和图形的集合,以向用户演示。 这两个版本都需要一个菜单,该菜单可导航报表的各个部分,表的存在,某些图形(颜色,文本大小,边框)。
使用Rotativa库
我认为Rotativa尽可能易于应用。
- 您已经有一个现成的HTML报告,形式为ASP.NET MVC模板和控制器,例如:
[HttpGet] public async Task<ActionResult> Index(int param1, string param2) { var model = await service.GetReportDataAsync(param1, param2); return View(model); }
安装nuget Rotativa软件包
为PDF报告添加新的控制器
[HttpGet] public async Task<ActionResult> Pdf(int param1, string param2) { var model = await service.GetReportDataAsync(param1, param2); return new ViewAsPdf("Index", model); }
从现在开始,从本质上讲,您将PDF作为文件返回,其中包含原始HTML报告中的所有数据。
我在这里没有描述路由,但是可以理解,您已经配置了路由以正确调用两个控制器
有趣的是,该库本身实质上是著名的控制台实用程序wkhtmltopdf的包装 。 您可以打赌,Azure可以在高空工作的速度-会起作用。 但是,我们将讨论一些功能。
页码
逻辑上假设客户将打印PDF并希望查看页码。 感谢Rotativa的创建者,这里的一切都非常简单。
根据Rotativa文档,通过CustomSwitches
参数, CustomSwitches
可以指定将传递给wkhtmltopdf
实用程序wkhtmltopdf
。 嗯,在线技巧有很多例子。 以下调用在每个页面的底部添加一个数字:
return new ViewAsPdf("Index", model) { PageMargins = new Rotativa.Options.Margins(10, 10, 10, 10), PageSize = Rotativa.Options.Size.A4, PageOrientation = Rotativa.Options.Orientation.Portrait, CustomSwitches = "--page-offset 0 --footer-center [page] --footer-font-size 8 };
效果很好。 页码本身是使用[page]
参数传递的,此类参数将替换为特定的值。
除了[page]
还有其他:
- [页面]替换为当前正在打印的页面数
- [frompage]替换为要打印的第一页的编号
- [topage]替换为要打印的最后一页的编号
- [网页]替换为要打印的页面的URL
- [section]替换为当前部分的名称
- [小节]替换为当前小节的名称
- [date]以系统本地格式替换为当前日期
- [isodate]替换为ISO 8601扩展格式的当前日期
- [time]以系统本地格式替换为当前时间
- [title]替换为当前页面对象的标题
- [doctitle]替换为输出文档的标题
- [sitepage]替换为要转换的当前站点中的页面数
- [sitepages]替换为当前网站中正在转换的页面数
目录表
大型多页报告需要PDF内容和页面导航。 当报表中的页面数超过一百时,这非常方便,并且至关重要。
wkhtmltopdf手册包含所有选项的完整列表,其中包括--toc
。 看到此参数,该实用程序实质上将收集文档中的所有标签<h1>, <h2>, ... <h6>
,并基于这些标签生成目录。 因此,有必要在HTML模板中正确使用这些标题标签。
但实际上,添加--toc
不会导致任何后果。 好像没有参数。 但是,其他选项也可以。 感谢在某个论坛上的帖子,我发现此参数应该在不带连字符的情况下传递: toc
。 实际上,在这种情况下,内容被添加为第一页。 当您单击内容中的一行时,您将转到文档中所需的页面,页码正确。
目前尚不清楚如何自定义样式,但是我还没有做到这一点。
JavaScript执行
我遇到的下一点是需要在报告中添加图形。 我的HTML页面包含JS代码,该代码使用dc.js
库添加了图形。 这是一个例子:
function initChart() { renderChart(@Html.Raw(Json.Encode(Model.Chart_1_Data)), 'chartDiv_1'); } function renderChart(data, chartElementId) { var colors = ['#03a9f4', '#67daff', '#8bc34a']; var barHeight = 45; var clientHeight = data.length * barHeight + 50; var clientWidth = document.getElementById(chartId).offsetWidth; var chart = dc.rowChart('#' + chartElementId); var ndx = crossfilter(dataToRender); var dimension = ndx.dimension(d => d.name); var group = dimension.group().reduceSum(d => d.value); chart .width(clientWidth) .height(clientHeight) .margins({ top: 16, right: 16, bottom: 16, left: 16 }) .ordinalColors(colors) .dimension(dimension) .group(group) .xAxis() .scale(d3.scaleLinear().domain([0, 2]).range([1, 3]).nice()); chart.render(); }
同时,在HTML中,我有一个对应的元素:
<div id="chart_C2" class="dc-chart"></div>
为了使此代码起作用,您需要导入适当的库: dc.js
, d3.js
, crossfilter.js
。 调用initChart
函数将创建一个图形并插入结果
svg到树中的指定项目。
但是PDF不包含任何图形。 以及PDF渲染之前执行JavaScript代码的其他任何方式。 检查这非常容易-只需添加基本代码以创建带有文本的简单<div>
元素,即可测试JavaScript调用的事实。
根据经验,事实证明, wkhtmltopdf
的JS代码的wkhtmltopdf
起着重要作用。 位于<html>
末尾或例如<body>
JS末尾的代码将不会执行。 实用程序似乎根本没有注意到他,或者不希望在那里见到他。
但是<head>
内部的代码已执行。 因此,当JavaScript代码位于<head>
内的样式声明之后并通过通常的构造调用JavaScript代码时,我来到了该方案:
<body onload="initCharts()">
在这种情况下,代码将按预期执行。
JavaScript限制
但是无论如何,输出PDF中仍然没有图形。 然后,我开始猜测pdf渲染和执行引擎不是一个完整的浏览器,很可能不是完美的,并且不了解最后的规则。 再次,通过实验,我发现箭头功能未被感知。 此外,如果口译员发现他不知道的东西,那么它将停止工作。
用更经典的function(x) { return x.value; }
替换形式x => x.value
箭头函数function(x) { return x.value; }
function(x) { return x.value; }
有所帮助,并且所有代码都已执行,结果图进入PDF文件。
图表宽度
根据经验,事实证明有必要清楚地指出图的父元素的宽度。 为此,我指定了dc-chart
样式。 它包含图形的宽度(以像素为单位)。 否则,PDF上的图表将很小,尽管事实上在HTML版本中它将占据整个宽度。 指定百分比宽度仅适用于HTML。
内联JavaScript / CSS
最后,我想指出的是,许多将HTML转换为PDF的库都接受某个baseUrl作为参数。 这是URL,转换器将以此URL为基础完成接收导入的CSS样式,JavaScirpt文件或字体的相对路径。 我不能肯定地说这在Rotativa中如何工作,但我想出了另一种方法。
为了加快报表的初始加载速度并消除转换期间嵌入脚本或样式文件的问题根源,我将必要的JS和CSS直接嵌入到HTML模板的主体中。
为此,请创建适当的捆绑包:
public class BundleConfig { public static void RegisterBundles(BundleCollection bundles) { bundles.Add(new StyleBundle("~/Styles/report-html") .Include("~/Styles/report-common.css") .Include("~/Styles/report-html.css") ); bundles.Add(new StyleBundle("~/Styles/report-pdf") .Include("~/Styles/report-common.css") .Include("~/Styles/report-pdf.css") ); bundles.Add(new ScriptBundle("~/Scripts/charts") .Include("~/Scripts/d3/d3.js") .Include("~/Scripts/crossfilter/crossfilter.js") .Include("~/Scripts/dc/dc.js") ); } }
将这些捆绑包的配置调用添加到Global.asax.cs
protected void Application_Start() { ... BundleConfig.RegisterBundles(BundleTable.Bundles); }
并添加适当的方法将代码嵌入页面中。 它需要与Global.asax.cs
放在同一命名空间中,以便可以从HTML模板调用该方法:
public static class HtmlHelperExtensions { public static IHtmlString InlineStyles(this HtmlHelper htmlHelper, string bundleVirtualPath) { string bundleContent = LoadBundleContent(htmlHelper.ViewContext.HttpContext, bundleVirtualPath); string htmlTag = $"<style rel=\"stylesheet\" type=\"text/css\">{bundleContent}</style>"; return new HtmlString(htmlTag); } public static IHtmlString InlineScripts(this HtmlHelper htmlHelper, string bundleVirtualPath) { string bundleContent = LoadBundleContent(htmlHelper.ViewContext.HttpContext, bundleVirtualPath); string htmlTag = $"<script type=\"text/javascript\">{bundleContent}</script>"; return new HtmlString(htmlTag); } private static string LoadBundleContent(HttpContextBase httpContext, string bundleVirtualPath) { var bundleContext = new BundleContext(httpContext, BundleTable.Bundles, bundleVirtualPath); var bundle = BundleTable.Bundles.Single(b => b.Path == bundleVirtualPath); var bundleResponse = bundle.GenerateBundleResponse(bundleContext); return bundleResponse.Content; } }
好吧,最后一点就是模板调用:
@Html.InlineStyles("~/Styles/report-pdf"); @Html.InlineScripts("~/Scripts/charts");
结果,所有必需的CSS和JavaScript都将直接以HTML格式显示,尽管在开发过程中您可以使用单个文件。
很可能,许多人会立即考虑到这种方法在浏览器缓存请求方面的效率低下。 但是我有两个具体目标:
- 因此,PDF转换器不必在某处请求样式或代码,而用户则必须分别等待;
- 因此,首次下载PDF和HTML报告所需的时间最少,而无需等待一些其他请求。 在我的项目中,这很重要;
分页符
将报告分为几部分可能需要从新页面开始新部分的要求。 在这种情况下,您可以成功使用简单的CSS方法:
.page-break-before { page-break-before: always; } .no-page-break-inside { page-break-before: auto; page-break-inside: avoid; }
wkhtmltopdf实用程序成功读取了这些类,并了解有必要启动一个新页面。 第一类page-break-before
before-告诉实用程序始终使用此元素开始一个新页面。 第二类no-page-break-inside
应该应用于希望在页面上保持尽可能完整的那些元素。 例如,您有连续的结构化信息块或表。 如果页面上有两个块-将找到它们。 如果第三个页面不适合该页面,则不会是下一个。 如果它大于一页,那么它的传输已经不可避免。 所有这些工作都充分且方便地进行。
wkhtmltopdf中的Flex行为
好吧,我注意到的最后一个功能与flexbox标记样式的使用有关。 我们都已经习惯了它们,几乎所有的标记都是由flexs进行的。 但是,wkhtmltopdf在这方面有点落后。 水平伸缩选项不起作用(至少在我而言,这不起作用。我在网络中看到,值得复制伸缩样式,如下所示:
display: -webkit-flex; display: flex; flex-direction: row; -webkit-flex-direction: row; -webkit-box-pack: justify; -webkit-justify-content: space-between; justify-content: space-between;
但是不幸的是,这并没有导致预期的PDF标记。 我必须重做一些元素的布局,以便按需要水平放置块。 如果有人在将flex集成到wkhtmltopdf方面具有成功的经验,请分享。 那会很有帮助。
一些链接: