Kotlin上的另一个DSL或我如何从React打印PDF



您不仅可以打印并打印用React编写的页面:还有页面分隔符,输入字段。 另外,我想编写一次渲染,以便它生成ReactDom和常规HTML,可以将其转换为PDF。

最难的部分是React有自己的dsl,而html有自己的。 如何解决这个问题? 写另一个!

我几乎忘了,所有内容都是用Kotlin编写的,因此这实际上是有关Kotlin dsl的文章。

为什么我们需要我们自己的Uruk-hai?


我的项目中有很多报告,并且所有报告都必须能够打印。 有几种方法可以做到这一点:

  • 发挥印刷风格,隐藏所有不需要的东西,并希望一切都会好起来。 仅按钮,过滤器等将按原样打印。 而且,如果有很多表,则必须将每个表放在单独的页面上。 就个人而言,从网站打印时丢失的添加的链接,日期等让我很生气
  • 尝试在react上使用一些专门的库,该库可以呈现PDF。 我找到了它的beta版,在其中似乎无法重用普通的react组件。
  • 将HTML转换为画布,然后制作出PDF。 但是为此,我们需要没有按钮等的HTML。 需要将其呈现为隐藏元素,以便以后打印。 但似乎在此选项中您无法控制分页符。

最后,我决定编写能够生成ReactDom和HTML的代码。 我将通过在此过程中插入有关分页符的特殊标签,将HTML发送到后端以打印PDF。

Kotlin有一个用于React的层库 ,该库提供了用于React的类型安全的dsl。 一般情况下可以在我的上一篇文章中找到。

JetBrains还编写了一个用于生成HTML的库。 它是跨平台的,即 它可以在Java和JS中使用。 这也是dsl,在结构上非常相似。

我们需要找到一种方法,这取决于我们需要ReactDom还是纯HTML。

我们有什么材料?


例如,拿一张表,表头有一个搜索框。 这就是React和HTML上的表格渲染的样子:
反应
html
fun RBuilder.renderReactTable( search: String, onChangeSearch: (String) -> Unit ) { table { thead { tr { th { attrs.colSpan = "2" //(1) attrs.style = js { border = "solid" borderColor = "red" } //(2) +":" search(search, onChangeSearch) //(3) } } tr { th { +"" } th { +"" } } } tbody { tr { td { +"" } td { +"" } } tr { td { +"" } td { +"" } } } } } 

 fun TagConsumer<*>.renderHtmlTable( search: String ) { table { thead { tr { th { colSpan = "2" //(1) style = """ border: solid; border-color: red; """ //(2) +": " +(search?:"") //(3) } } tr { th { +"" } th { +"" } } } tbody { tr { td { +"" } td { +"" } } tr { td { +"" } td { +"" } } } } } 



我们的任务是合并表格的左侧和右侧。

首先,让我们找出区别是什么:

  1. 在html中, stylecolSpan在attr嵌套对象的顶级,在React colSpan分配的
  2. 样式填充方式不同。 如果在HTML中这是作为字符串的常规CSS,那么在React中,这是一个JS对象,由于JS的限制,其字段名称与标准CSS略有不同。
  3. 在React版本中,我们使用输入进行搜索,在HTML中,我们仅显示文本。 这已经从问题陈述开始。

好吧,最重要的是:这些是具有不同使用者和API的不同DSL。 对于编译器,它们是完全不同的。 无法直接越过它们,因此您必须编写一个看起来几乎相同的图层,但能够同时使用React api和HTML api。

组装骨架


现在,只需从一个空单元格中绘制一个数位板:

 table { thead { tr { th { } } } } 

我们有一个HTML树和两种处理它的方法。 经典的解决方案是实现复合和访客模式。 只有我们没有访问者的界面。 为什么-稍后会看到。

主要单位将是ParentTag和TagWithParent。 ParentTag是由Kotlin api的HTML标签生成的(感谢上帝在HTML和React api中都使用了该标签),TagWithParent存储标签本身以及两个将其插入到两个api变体中的父标签的函数。

 abstract class ParentTag<T : HTMLTag> { val tags: MutableList<TagWithParent<*, T>> = mutableListOf() //     protected fun RDOMBuilder<T>.withChildren() { ... } //  reactAppender    protected fun T.withChildren() { ... } //  htmlAppender    } class TagWithParent<T, P : HTMLTag>( val tag: T, val htmlAppender: (T, P) -> Unit, val reactAppender: (T, RDOMBuilder<P>) -> Unit ) 

为什么需要这么多的泛型? 问题在于,HTML的dsl编译时非常严格。 如果在React中可以从任何地方调用td,甚至可以从div调用,那么在HTML中,只能从tr上下文调用它。 因此,我们将不得不以通用形式将编译上下文拖到任何地方。

大多数标签的拼写方式大致相同:

  1. 我们实现两种访问方法。 一个用于React,一个用于HTML。 他们负责最终渲染。 这些方法添加样式,类等。
  2. 我们编写了一个扩展程序,将标签插入父级。

这是thead的一个例子
 class THead : ParentTag<THEAD>() { fun visit(builder: RDOMBuilder<TABLE>) { builder.thead { withChildren() } } fun visit(builder: TABLE) { builder.thead { withChildren() } } } fun Table.thead(block: THead.() -> Unit) { tags += TagWithParent(THead().also(block), THead::visit, THead::visit) } 


最后,您可以解释为什么未使用访客界面。 问题是tr可以同时插入thead和tbody中。 我无法在一个界面的框架内表达这一点。 访问函数出现了四个重载。

一堆无法避免的重复
 class Tr( val classes: String? ) : ParentTag<TR>() { fun visit(builder: RDOMBuilder<THEAD>) { builder.tr(classes) { withChildren() } } fun visit(builder: THEAD) { builder.tr(classes) { withChildren() } } fun visit(builder: RDOMBuilder<TBODY>) { builder.tr(classes) { withChildren() } } fun visit(builder: TBODY) { builder.tr(classes) { withChildren() } } } 


造肉


将文本添加到单元格:

  table { thead { tr { th { +": " } } } } 

以“ +”为焦点非常简单:只需在标签(可能包含文本)中重新定义unaryPlus就足够了。

 abstract class TableCell<T : HTMLTag> : ParentTag<T>() { operator fun String.unaryPlus() { ... } } 

这使您可以在td或th的上下文中调用“ +”,这会将带有文本的标签添加到树中。

头皮


现在,我们需要处理html中不同的地方并做出响应api。 与colSpan的细微差异可以自己解决,但是样式形成上的差异更复杂。 如果任何人都不知道,在React中,style是一个JS对象,并且您不能在字段名称中使用连字符。 因此,使用camelCase代替。 在HTML api中,我们需要常规的CSS。 我们再次同时需要这个和那个。

我可以尝试自动将camelCase带连字符,然后像在React api中一样将其保留,但是我不知道它是否将一直有效。 因此,我写了另一层:

谁不偷懒,可以看看它的样子
 class Style { var border: String? = null var borderColor: String? = null var width: String? = null var padding: String? = null var background: String? = null operator fun invoke(callback: Style.() -> Unit) { callback() } fun toHtmlStyle(): String = properties .map { it.html to it.property(this) } .filter { (_, value) -> value != null } .joinToString("; ") { (name, value) -> "$name: $value" } fun toReactStyle(): String { val result = js("{}") properties .map { it.react to it.property(this) } .filter { (_, value) -> value != null } .forEach { (name, value) -> result[name] = value.toString() } return result.unsafeCast<String>() } class StyleProperty( val html: String, val react: String, val property: Style.() -> Any? ) companion object { val properties = listOf( StyleProperty("border", "border") { border }, StyleProperty("border-color", "borderColor") { borderColor }, StyleProperty("width", "width") { width }, StyleProperty("padding", "padding") { padding }, StyleProperty("background", "background") { background } ) } } 


是的,我知道您是否还要一个CSS属性-添加到此类。 是的,带有转换器的地图将更易于实现。 但是类型安全。 我什至在地方使用枚举。 也许,如果我不为自己写信,那我将以某种方式解决问题。

我作弊了一点,并允许对结果类的这种使用:

 th { attrs.style { border = "solid" borderColor = "red" } } 

进行方式:在attr.style字段中,默认情况下已经有一个空的Style()。 如果您定义了运算符有趣的调用,则该对象可以用作函数,即 您可以调用attrs.style() ,尽管style是一个字段,而不是一个函数。 在这样的调用中,必须传递在operator fun invoke中指定的参数。 在这种情况下,这是一个参数-callback:Style。()-> Unit。 由于这是lambda,因此(括号)是可选的。

试穿其他盔甲


还需要学习如何在React中绘制输入,以及仅在HTML中绘制文本。 我想得到这样的语法:

 react { search(search, onChangeSearch) } html { +(search?:"") } 

工作原理:react函数为Rreact api获取一个lambda并返回插入的标签。 在标记上,您可以调用infix函数并将lambda传递给HTML api。 infix修饰符使html可以不带点而被调用。 与if {} else {}非常相似。 和if-else一样,html调用是可选的,它派上了用场了好几次。

实作
 class ReactTag<T : HTMLTag>( private val block: RBuilder.() -> Unit = {} ) { private var htmlAppender: (T) -> Unit = {} infix fun html(block: (T).() -> Unit) { htmlAppender = block } ... } fun <T : HTMLTag> ParentTag<T>.react(block: RBuilder.() -> Unit): ReactTag<T> { val reactTag = ReactTag<T>(block) tags += TagWithParent<ReactTag<T>, T>(reactTag, ReactTag<T>::visit, ReactTag<T>::visit) return reactTag } 


萨鲁曼印记


另一种触动。 必须从带有特殊伤口注释的特殊伤口接口继承ParentTag和TagWithParent,该接口上已经放置了特殊的@DslMarker注释,该注释已经从语言的核心开始:

 @DslMarker annotation class StyledTableMarker @StyledTableMarker interface Tag 

这是必要的,以便编译器不允许编写如下奇怪的调用:

 td { td { } } tr { thead { } } 

但是,尚不清楚谁会想到写这样的东西...

去战斗!


从本文开始,一切准备就绪,我们可以绘制表格了,但是这段代码已经生成了ReactDom和HTML。 写一次就可以在任何地方运行!

 fun Table.renderUniversalTable(search: String?, onChangeSearch: (String?) -> Unit) { thead { tr { th { attrs.colSpan = 2 attrs.style { border = "solid" borderColor = "red" } +":" react { search(search, onChangeSearch) //(*) } html { +(search?:"") } } } tr { th { +"" } th { +"" } } } tbody { tr { td { +"" } td { +"" } } tr { td { +"" } td { +"" } } } } 

注意(*)-这与React表格的原始版本中的搜索功能完全相同。 无需将所有内容转移到新的dsl,只需将通用标签转移即可。

这样的代码的结果可能会起作用吗? 这是我的项目报告的PDF打印示例 。 自然,我用随机替换了所有数字和名称。 为了进行比较,使用浏览器浏览同一页面的PDF打印输出 。 从破坏页面之间的表格到覆盖文本的工件。

在编写dsl时,您会得到很多额外的代码,这些代码仅专注于使用形式。 此外,还使用了很多Kotlin功能,您甚至在日常生活中都不会想到这些功能。

也许在其他情况下会有所不同,但是在这种情况下,还有很多重复,我无法消除(据我所知,JetBarins使用代码生成来编写HTML库)。

但是事实证明,我构建的外观与React和HTML api几乎类似的dsl(我几乎没有偷窥)。 有趣的是,随着dsl的便利性,我们可以完全控制渲染。 您可以将页面标签添加到单独的页面。 您可以在打印时展开“ 手风琴 ”。 而且,您可以尝试找到一种在服务器上重用此代码并为搜索引擎生成html的方法。

PS当然,有多种方法可以更轻松地打印PDF

萝卜与文章来源

Source: https://habr.com/ru/post/zh-CN430238/


All Articles