
Você não pode simplesmente pegar e imprimir uma página escrita em React: existem separadores de páginas, campos de entrada. Além disso, quero escrever uma renderização uma vez para gerar o ReactDom e o HTML comum, que podem ser convertidos em PDF.
A parte mais difícil é que o React possui seu próprio dsl e o html. Como resolver este problema? Escreva outro!
Eu quase esqueci, tudo será escrito em Kotlin, então este é realmente um artigo sobre o Kotlin dsl.
Por que precisamos do nosso próprio Uruk-hai?
Existem muitos relatórios no meu projeto e todos eles devem poder ser impressos. Existem várias opções de como fazer isso:
- Brinque com os estilos de impressão, oculte tudo o que você não precisa e espero que tudo corra bem. Apenas botões, filtros e similares serão impressos como estão. E também, se houver muitas tabelas, é necessário que cada uma esteja em uma página separada. E, pessoalmente, os links, datas etc. adicionados que são impressos ao imprimir no site estão me enfurecendo
- Tente usar alguma biblioteca especializada em react, que pode renderizar PDF. Encontrei este , é beta e, ao que parece, não é possível reutilizar componentes de reação comuns.
- Transforme HTML em tela e faça PDF com isso. Mas, para isso, precisamos de HTML, sem botões e similares. Ele precisará ser renderizado em um elemento oculto para imprimi-lo posteriormente. Mas não parece que nesta opção você possa controlar quebras de página.
No final, decidi escrever um código capaz de gerar ReactDom e HTML. Vou enviar o HTML ao back-end para imprimir o PDF inserindo tags especiais sobre quebras de página ao longo do caminho.
O Kotlin possui uma
biblioteca de camadas para trabalhar com o React que fornece dsl com segurança de tipo para trabalhar com o React. A aparência geral pode ser encontrada no meu
artigo anterior.
O JetBrains também escreveu uma biblioteca para
gerar HTML . É multiplataforma, ou seja, pode ser usado em Java e JS. Isso também é dsl, muito semelhante em estrutura.
Precisamos encontrar uma maneira de alternar entre bibliotecas, dependendo se precisamos do ReactDom ou HTML puro.
Que material temos?
Por exemplo, pegue uma tabela com uma caixa de pesquisa no cabeçalho. É assim que a renderização de tabela no React e HTML se parece:
reagir
| 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"
|
Nossa tarefa é combinar os lados esquerdo e direito da mesa.
Primeiro, vamos descobrir qual é a diferença:
- Em html, as
colSpan
style
e colSpan
atribuídas no nível superior, em React, no objeto aninhado - O estilo é preenchido de maneira diferente. Se em HTML esse CSS é regular como uma sequência, em React é um objeto js cujos nomes de campo são ligeiramente diferentes do CSS padrão devido a limitações de JS.
- Na versão React, usamos entradas para a pesquisa, em HTML, simplesmente exibimos o texto. Isso já está começando na declaração do problema.
Bem e o mais importante: são diferentes dsl com diferentes consumidores e diferentes APIs. Para o compilador, eles são completamente diferentes. É impossível cruzá-los diretamente, então você terá que escrever uma camada que pareça quase a mesma, mas que funcione com as APIs do React e HTML.
Monte o esqueleto
Por enquanto, basta desenhar um tablet a partir de uma célula vazia:
table { thead { tr { th { } } } }
Temos uma árvore HTML e duas maneiras de processá-la. A solução clássica é implementar padrões compostos e de visitantes. Só não teremos uma interface para o visitante. Por que - será visto mais tarde.
As principais unidades serão ParentTag e TagWithParent. ParentTag é gerado pela tag HTML da API do Kotlin (graças a Deus é usado tanto em HTML quanto na API do React), e o TagWithParent armazena a própria tag e duas funções que a inserem no pai em duas variantes da API.
abstract class ParentTag<T : HTMLTag> { val tags: MutableList<TagWithParent<*, T>> = mutableListOf()
Por que você precisa de tantos genéricos? O problema é que o dsl para HTML é muito rigoroso ao compilar. Se no React você pode chamar td de qualquer lugar, mesmo de uma div, então no caso do HTML você pode chamá-lo apenas no contexto de tr. Portanto, teremos que arrastar o contexto para a compilação na forma de genérico em todos os lugares.
A maioria das tags está escrita aproximadamente da mesma maneira:
- Implementamos dois métodos de visita. Um para o React, outro para o HTML. Eles são responsáveis pela renderização final. Esses métodos adicionam estilos, classes e similares.
- Escrevemos uma extensão que insere a tag no pai.
Aqui está um exemplo 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) }
Por fim, você pode explicar por que a interface do visitante não foi usada. O problema é que tr pode ser inserido no cabeçote e no tbody. Não pude expressar isso dentro da estrutura de uma interface. Saiu quatro sobrecargas da função de visita.
Um monte de duplicação que não pode ser evitada 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
Adicione texto à célula:
table { thead { tr { th { +": " } } } }
Focar com '+' é bastante simples: basta redefinir unaryPlus em tags, que podem incluir texto, é suficiente.
abstract class TableCell<T : HTMLTag> : ParentTag<T>() { operator fun String.unaryPlus() { ... } }
Isso permite que você chame '+' enquanto estiver no contexto de td ou th, o que adicionará uma tag com texto à árvore.
Couro cabeludo da pele
Agora, precisamos lidar com lugares que diferem no html e reagem à API. Uma pequena diferença com o colSpan é resolvida por si só, mas a diferença na formação do estilo é mais complicada. Se alguém não souber, em React, style é um objeto JS e você não poderá usar um hífen no nome do campo. Então o camelCase é usado. Na API HTML queremos css regulares de nós. Novamente precisamos disso e daquilo ao mesmo tempo.
Eu poderia tentar trazer automaticamente o camelCase para hifenizar e deixá-lo como na API do React, mas não sei se sempre funcionará. Portanto, eu escrevi outra camada:
Quem não é preguiçoso, pode ver como fica 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 } ) } }
Sim, eu sei se você deseja mais uma propriedade css - adicione a esta classe. Sim, e um mapa com um conversor seria mais fácil de implementar. Mas seguro para o tipo. Eu até uso enums em alguns lugares. Talvez, se eu não escrevesse para mim, resolveria a questão de maneira diferente.
Eu trapacei um pouco e permiti esse uso da classe resultante:
th { attrs.style { border = "solid" borderColor = "red" } }
Como funciona: no campo attr.style, por padrão, já existe um Style () vazio. Se você definir a chamada divertida do operador, o objeto poderá ser usado como uma função, ou seja, você pode chamar
attrs.style()
, embora estilo seja um campo, não uma função. Nessa chamada, os parâmetros especificados na chamada divertida do operador devem ser passados. Nesse caso, esse é um parâmetro - retorno de chamada: Style. () -> Unit. Como este é um lambda, os colchetes são opcionais.
Experimentando armaduras diferentes
Resta aprender a desenhar entradas no React e apenas texto em HTML. Eu gostaria de obter esta sintaxe:
react { search(search, onChangeSearch) } html { +(search?:"") }
Como funciona: A função react pega um lambda para a API Rreact e retorna a tag inserida. Na tag, você pode chamar a função infix e passar o lambda para a API HTML. O modificador infix permite que o html seja chamado sem um ponto. Muito semelhante ao if {} else {}. E, como no caso contrário, a chamada html é opcional, foi útil várias vezes.
Implementação 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
Outro toque. É necessário herdar ParentTag e TagWithParent de uma interface especialmente enrolada com uma anotação especialmente enrolada, na qual está uma
anotação especial
@DslMarker , já do núcleo do idioma:
@DslMarker annotation class StyledTableMarker @StyledTableMarker interface Tag
Isso é necessário para que o compilador não permita gravar chamadas estranhas como estas:
td { td { } } tr { thead { } }
Não está claro, no entanto, quem pensaria em escrever uma coisa dessas ...
Para a batalha!
Está tudo pronto para desenhar uma tabela desde o início do artigo, mas esse código já irá gerar o ReactDom e o HTML. Escreva uma vez executado em qualquer lugar!
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 atenção a (*) - essa é exatamente a mesma função de pesquisa da versão original da tabela para o React. Não há necessidade de transferir tudo para o novo dsl, apenas tags comuns.
Qual pode ser o resultado desse código? Aqui está
um exemplo de uma impressão em PDF de um relatório do meu projeto. Naturalmente, substituí todos os números e nomes por aleatórios. Para comparação, uma
impressão em PDF da mesma página, mas pelo navegador. Artefatos de quebrar uma tabela entre páginas e sobrepor texto.
Ao escrever dsl, você obtém muito código extra focado apenas na forma de uso. Além disso, muitos recursos do Kotlin são usados, os quais você nem pensa na vida cotidiana.
Talvez em outros casos seja diferente, mas nesse caso também houve muita duplicação, da qual não consegui me livrar (até onde sei, o JetBarins usa a geração de código para escrever a biblioteca HTML).
Mas acabei desenvolvendo o dsl quase de aparência semelhante à API React e HTML (quase não espiei). Curiosamente, juntamente com a conveniência do dsl, temos controle total sobre a renderização. Você pode adicionar uma tag de página para separar as páginas. Você pode expandir o "
acordeão " ao imprimir. E você pode tentar encontrar uma maneira de reutilizar esse código no servidor e gerar html já para os mecanismos de pesquisa.
PS Certamente, existem maneiras de imprimir PDF mais facilmente
Nabo com fonte para o artigoOutros artigos sobre Kotlin: