
No puede simplemente tomar e imprimir una página escrita en React: hay separadores de página, campos de entrada. Además, quiero escribir un render una vez para que genere ReactDom y HTML simple, que se puede convertir a PDF.
La parte más difícil es que React tiene su propio dsl y html tiene el suyo. ¿Cómo resolver este problema? Escribe otro!
Casi se me olvida, todo estará escrito en Kotlin, por lo que este es realmente un artículo sobre Kotlin dsl.
¿Por qué necesitamos nuestro propio Uruk-hai?
Hay muchos informes en mi proyecto y todos deben poder imprimirse. Hay varias opciones sobre cómo hacer esto:
- Juega con estilos de impresión, oculta todo lo que no necesitas y espera que todo esté bien. Solo los botones, filtros y similares se imprimirán tal cual. Y también, si hay muchas tablas, es necesario que cada una esté en una página separada. Y personalmente, los enlaces agregados, las fechas, etc. que salen al imprimir desde el sitio me están enfureciendo.
- Intente utilizar alguna biblioteca especializada en react, que puede representar PDF. Encontré este , es beta y, al parecer, no puedes reutilizar componentes de reacción normales.
- Convierte HTML en lienzo y crea PDF con él. Pero para esto necesitamos HTML, sin botones y similares. Deberá representarse en un elemento oculto para imprimirlo más tarde. Pero no parece que en esta opción pueda controlar los saltos de página.
Al final, decidí escribir código capaz de generar ReactDom y HTML. Enviaré el HTML al back-end para imprimir el PDF insertando etiquetas especiales sobre saltos de página en el camino.
Kotlin tiene una
biblioteca de capas para trabajar con React que proporciona dsl de tipo seguro para trabajar con React. Cómo se ve en general se puede encontrar en mi
artículo anterior.
JetBrains también escribió una biblioteca para
generar HTML . Es multiplataforma, es decir Se puede utilizar tanto en Java como en JS. Esto también es dsl, muy similar en estructura.
Necesitamos encontrar una manera de cambiar entre bibliotecas dependiendo de si necesitamos ReactDom o HTML puro.
¿Qué material tenemos?
Por ejemplo, tome una tabla con un cuadro de búsqueda en el encabezado. Así es como se ve la representación de tablas en React y HTML:
reaccionar
| html
|
---|
fun RBuilder.renderReactTable( search: String, onChangeSearch: (String) -> Unit ) { table { thead { tr { th { attrs.colSpan = "2"
| fun TagConsumer<*>.renderHtmlTable( search: String ) { table { thead { tr { th { colSpan = "2"
|
Nuestra tarea es combinar los lados izquierdo y derecho de la tabla.
Primero, descubramos cuál es la diferencia:
- En html, las
colSpan
style
y colSpan
asignan en el nivel superior, en React, en el objeto attr nested - El estilo se llena de manera diferente. Si en HTML esto es CSS regular como una cadena, entonces en React es un objeto js cuyos nombres de campo son ligeramente diferentes de CSS estándar debido a las limitaciones de JS.
- En la versión React, usamos la entrada para la búsqueda, en HTML simplemente mostramos el texto. Esto ya comienza a partir de la declaración del problema.
Bueno y lo más importante: estos son diferentes dsl con diferentes consumidores y diferentes api. Para el compilador, son completamente diferentes. Es imposible cruzarlos directamente, por lo que tendrá que escribir una capa que se verá casi igual, pero podrá trabajar con la API React y la API HTML.
Ensamblar el esqueleto
Por ahora, solo dibuja una tableta de una celda vacía:
table { thead { tr { th { } } } }
Tenemos un árbol HTML y dos formas de procesarlo. La solución clásica es implementar patrones compuestos y visitantes. Solo que no tendremos una interfaz para el visitante. Por qué, se verá más tarde.
Las unidades principales serán ParentTag y TagWithParent. ParentTag está genificado por la etiqueta HTML de la API de Kotlin (gracias a Dios que se usa tanto en HTML como en la API React), y TagWithParent almacena la etiqueta y dos funciones que la insertan en el padre en dos variantes de API.
abstract class ParentTag<T : HTMLTag> { val tags: MutableList<TagWithParent<*, T>> = mutableListOf()
¿Por qué necesitas tantos genéricos? El problema es que dsl para HTML es muy estricto al compilar. Si en React puede llamar a td desde cualquier lugar, incluso desde un div, en el caso de HTML puede llamarlo solo desde el contexto de tr. Por lo tanto, tendremos que arrastrar el contexto para la compilación en forma de genérico a todas partes.
La mayoría de las etiquetas se escriben aproximadamente de la misma manera:
- Implementamos dos métodos de visita. Uno para React, uno para HTML. Son responsables de la representación final. Estos métodos agregan estilos, clases y similares.
- Escribimos una extensión que inserta la etiqueta en el padre.
Aquí hay un ejemplo de 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) }
Finalmente, puede explicar por qué no se utilizó la interfaz para el visitante. El problema es que tr puede insertarse tanto en thead como en tbody. No pude expresar esto dentro del marco de una interfaz. Salieron cuatro sobrecargas de la función de visita.
Un montón de duplicaciones que no se pueden evitar. 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() } } }
Construir carne
Agregue texto a la celda:
table { thead { tr { th { +": " } } } }
Centrarse en '+' es bastante simple: basta con redefinir unaryPlus en las etiquetas, que pueden incluir texto.
abstract class TableCell<T : HTMLTag> : ParentTag<T>() { operator fun String.unaryPlus() { ... } }
Esto le permite llamar '+' en el contexto de td o th, lo que agregará una etiqueta con texto al árbol.
Cuero cabelludo la piel
Ahora tenemos que lidiar con lugares que difieren en el html y reaccionar api. Una pequeña diferencia con colSpan se resuelve por sí sola, pero la diferencia en la formación del estilo es más complicada. Si alguien no lo sabe, en React, el estilo es un objeto JS y no puede usar un guión en el nombre del campo. Entonces se usa camelCase en su lugar. En HTML api queremos css regulares de nosotros. Nuevamente necesitamos tanto esto como aquello al mismo tiempo.
Podría intentar llevar camelCase automáticamente a un guión y dejarlo como en React api, pero no sé si siempre funcionará. Por lo tanto, escribí otra capa:
Quien no es perezoso, puede ver cómo se ve 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 } ) } }
Sí, sé si desea una propiedad css más: agregue a esta clase. Sí, y un mapa con un convertidor sería más fácil de implementar. Pero de tipo seguro. Incluso uso enumeraciones en algunos lugares. Quizás, si no escribiera por mí mismo, de alguna manera resolvería la pregunta de manera diferente.
Hice un poco de trampa y permití este uso de la clase resultante:
th { attrs.style { border = "solid" borderColor = "red" } }
Cómo funciona: en el campo attr.style, de forma predeterminada, ya hay un Style () vacío. Si define la invocación divertida del operador, el objeto puede usarse como una función, es decir, puede llamar a
attrs.style()
, aunque el estilo es un campo, no una función. En dicha llamada, se deben pasar los parámetros especificados en invocación divertida del operador. En este caso, este es un parámetro: devolución de llamada: Estilo. () -> Unidad. Como se trata de una lambda, los (paréntesis) son opcionales.
Probándose diferentes armaduras
Queda por aprender cómo dibujar entradas en React, y solo texto en HTML. Me gustaría obtener esta sintaxis:
react { search(search, onChangeSearch) } html { +(search?:"") }
Cómo funciona: la función de reacción toma una lambda para la API Rreact y devuelve la etiqueta insertada. En la etiqueta, puede llamar a la función infijo y pasar el lambda a la API HTML. El modificador infijo permite que se llame a html sin un punto. Muy similar a if {} else {}. Y como en if-else, la llamada html es opcional, fue útil varias veces.
Implementación 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 }
Marca de Saruman
Otro toque Es necesario heredar ParentTag y TagWithParent de una interfaz de herida especial con una anotación de herida especial en la que se encuentra una
anotación especial
@DslMarker , ya desde el núcleo del lenguaje:
@DslMarker annotation class StyledTableMarker @StyledTableMarker interface Tag
Esto es necesario para que el compilador no permita escribir llamadas extrañas como estas:
td { td { } } tr { thead { } }
Sin embargo, no está claro quién pensaría en escribir algo así ...
A la batalla!
Todo está listo para que dibujemos una tabla desde el comienzo del artículo, pero este código ya generará ReactDom y HTML. Escribe una vez que corras a cualquier parte!
fun Table.renderUniversalTable(search: String?, onChangeSearch: (String?) -> Unit) { thead { tr { th { attrs.colSpan = 2 attrs.style { border = "solid" borderColor = "red" } +":" react { search(search, onChangeSearch)
Preste atención a (*): esta es exactamente la misma función de búsqueda que en la versión original de la tabla para React. No es necesario transferir todo a la nueva dsl, solo etiquetas comunes.
¿Cuál podría ser el resultado de dicho código? Aquí hay
un ejemplo de una impresión en PDF de un informe de mi proyecto. Naturalmente, reemplacé todos los números y nombres con al azar. A modo de comparación, una
impresión en PDF de la misma página, pero por el navegador. Artefactos desde romper una tabla entre páginas hasta superponer texto.
Al escribir dsl, obtienes una gran cantidad de código adicional centrado únicamente en la forma de uso. Además, se utilizan muchas funciones de Kotlin, que ni siquiera piensa en la vida cotidiana.
Quizás en otros casos será diferente, pero en este caso también hubo mucha duplicación de la que no pude deshacerme (por lo que sé, JetBarins usa la generación de código para escribir la biblioteca HTML).
Pero se me ocurrió construir dsl con una apariencia casi similar a la API React y HTML (casi no me asomo). Curiosamente, junto con la conveniencia de dsl, tenemos control total sobre el renderizado. Puede agregar una etiqueta de página para separar las páginas. Puede expandir el "
acordeón " al imprimir. Y puede intentar encontrar una forma de reutilizar este código en el servidor y generar html ya para los motores de búsqueda.
PD Seguramente, hay formas de imprimir PDF más fácilmente
Nabo con fuente para el artículoOtros artículos sobre Kotlin: