
Anda tidak bisa hanya mengambil dan mencetak halaman yang ditulis dalam React: ada pemisah halaman, bidang input. Selain itu, saya ingin menulis render sekali sehingga menghasilkan ReactDom dan HTML biasa, yang dapat dikonversi ke PDF.
Bagian tersulit adalah bahwa Bereaksi memiliki dsl sendiri, dan html memiliki sendiri. Bagaimana cara mengatasi masalah ini? Tulis satu lagi!
Saya hampir lupa, semuanya akan ditulis di Kotlin, jadi ini sebenarnya artikel tentang Kotlin dsl.
Mengapa kita membutuhkan Uruk-hai kita sendiri?
Ada banyak laporan di proyek saya dan semuanya harus dapat dicetak. Ada beberapa opsi untuk melakukan hal ini:
- Mainkan dengan gaya cetak, sembunyikan semua yang tidak Anda butuhkan dan harap semuanya baik-baik saja. Hanya tombol, filter, dan sejenisnya yang akan dicetak apa adanya. Dan juga, jika ada banyak tabel, perlu masing-masing berada di halaman terpisah. Dan secara pribadi, tautan yang ditambahkan, tanggal, dll. Yang keluar saat mencetak dari situs membuat saya marah
- Cobalah untuk menggunakan beberapa perpustakaan khusus pada reaksi, yang dapat membuat PDF. Saya menemukan ini , ini beta dan sepertinya Anda tidak dapat menggunakan kembali komponen reaksi biasa.
- Ubah HTML menjadi kanvas dan buat PDF darinya. Tapi untuk ini kita perlu HTML, tanpa tombol dan sejenisnya. Ini harus dirender dalam elemen tersembunyi untuk mencetaknya nanti. Tapi sepertinya dalam opsi ini Anda tidak bisa mengontrol page break.
Pada akhirnya, saya memutuskan untuk menulis kode yang mampu menghasilkan ReactDom dan HTML. Saya akan mengirim HTML ke backend untuk mencetak PDF dengan menyisipkan tag khusus tentang page break di sepanjang jalan.
Kotlin memiliki
pustaka layer untuk bekerja dengan React yang menyediakan dsl tipe-aman untuk bekerja dengan React. Tampilannya secara umum dapat ditemukan di
artikel saya sebelumnya.
JetBrains juga menulis perpustakaan untuk
menghasilkan HTML . Ini adalah lintas platform, mis. dapat digunakan di Jawa dan JS. Ini juga dsl, strukturnya sangat mirip.
Kita perlu menemukan cara untuk beralih di antara pustaka tergantung pada apakah kita memerlukan ReactDom atau HTML murni.
Bahan apa yang kita miliki?
Misalnya, ambil tabel dengan kotak pencarian di header. Begini tampilan tabel pada React dan HTML:
bereaksi
| 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"
|
Tugas kita adalah menggabungkan sisi kiri dan kanan tabel.
Pertama, mari kita cari tahu perbedaannya:
- Dalam
colSpan
html, style
dan colSpan
ditugaskan di tingkat atas, di Bereaksi, pada objek bertingkat attr - Gaya mengisi berbeda. Jika dalam HTML ini adalah css biasa sebagai string, maka dalam Bereaksi itu adalah objek js yang nama bidangnya sedikit berbeda dari css standar karena keterbatasan JS.
- Dalam versi Bereaksi, kami menggunakan input untuk pencarian, dalam HTML kami cukup menampilkan teks. Ini sudah mulai dari pernyataan masalah.
Nah dan yang paling penting: ini adalah dsl berbeda dengan konsumen yang berbeda dan api yang berbeda. Untuk kompiler, mereka sangat berbeda. Tidak mungkin untuk melintasinya secara langsung, jadi Anda harus menulis layer yang akan terlihat hampir sama, tetapi akan dapat bekerja dengan React api dan HTML api.
Pasang kerangka
Untuk saat ini, cukup gambarkan tablet dari satu sel kosong:
table { thead { tr { th { } } } }
Kami memiliki pohon HTML dan dua cara untuk memprosesnya. Solusi klasiknya adalah menerapkan pola komposit dan pengunjung. Hanya kami tidak akan memiliki antarmuka untuk pengunjung. Mengapa - itu akan terlihat nanti.
Unit utama adalah ParentTag dan TagWithParent. ParentTag dibuat oleh tag HTML dari api Kotlin (terima kasih Tuhan digunakan baik dalam HTML dan di api React), dan TagWithParent menyimpan tag itu sendiri dan dua fungsi yang menyisipkannya ke dalam induk dalam dua varian api.
abstract class ParentTag<T : HTMLTag> { val tags: MutableList<TagWithParent<*, T>> = mutableListOf()
Mengapa Anda membutuhkan begitu banyak obat generik? Masalahnya adalah bahwa dsl untuk HTML sangat ketat saat kompilasi. Jika dalam Bereaksi Anda dapat memanggil td dari mana saja, bahkan dari div, maka dalam kasus HTML Anda dapat memanggilnya hanya dari konteks tr. Karena itu, kita harus menyeret konteks untuk kompilasi dalam bentuk generik di mana-mana.
Kebanyakan tag dieja kira-kira dengan cara yang sama:
- Kami menerapkan dua metode kunjungan. Satu untuk Bereaksi, satu untuk HTML. Mereka bertanggung jawab atas rendering akhir. Metode ini menambahkan gaya, kelas, dan sejenisnya.
- Kami menulis ekstensi yang menyisipkan tag ke induk.
Ini adalah contoh dari 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) }
Akhirnya, Anda dapat menjelaskan mengapa antarmuka untuk pengunjung tidak digunakan. Masalahnya adalah bahwa tr dapat dimasukkan ke dalam thead dan tbody. Saya tidak bisa mengungkapkan ini dalam kerangka satu antarmuka. Empat kelebihan fungsi kunjungan keluar.
Banyak duplikasi yang tidak bisa dihindari 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() } } }
Bangun daging
Tambahkan teks ke sel:
table { thead { tr { th { +": " } } } }
Berfokus dengan '+' cukup sederhana: cukup mendefinisikan ulang unaryPlus dalam tag, yang mungkin menyertakan teks, sudah cukup.
abstract class TableCell<T : HTMLTag> : ParentTag<T>() { operator fun String.unaryPlus() { ... } }
Ini memungkinkan Anda untuk memanggil '+' saat dalam konteks td atau th, yang akan menambahkan tag dengan teks ke struktur pohon.
Kulit kepala kulit
Sekarang kita perlu berurusan dengan tempat-tempat yang berbeda dalam html dan bereaksi api. Perbedaan kecil dengan colSpan diselesaikan dengan sendirinya, tetapi perbedaan dalam pembentukan gaya lebih rumit. Jika ada yang tidak tahu, di Bereaksi, gaya adalah objek JS, dan Anda tidak bisa menggunakan tanda hubung dalam nama bidang. Jadi camelCase digunakan sebagai gantinya. Dalam HTML api kami ingin css reguler dari kami. Kita lagi membutuhkan ini dan itu pada saat yang bersamaan.
Saya dapat mencoba membawa camelCase secara otomatis ke hyphenate dan membiarkannya seperti di React api, tetapi saya tidak tahu apakah itu akan selalu berhasil. Karena itu, saya menulis layer lain:
Siapa yang tidak malas, bisa melihat tampilannya 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 } ) } }
Ya, saya tahu jika Anda ingin satu lagi properti css - tambahkan ke kelas ini. Ya, dan peta dengan konverter akan lebih mudah diimplementasikan. Tapi tipe-aman. Saya bahkan menggunakan enum di beberapa tempat. Mungkin, jika saya tidak menulis untuk diri saya sendiri, entah bagaimana saya akan memecahkan pertanyaan dengan cara yang berbeda.
Saya sedikit curang dan mengizinkan penggunaan kelas yang dihasilkan ini:
th { attrs.style { border = "solid" borderColor = "red" } }
Bagaimana kelanjutannya: di bidang attr.style, secara default, sudah ada Style kosong (). Jika Anda mendefinisikan pemanggilan menyenangkan operator, maka objek dapat digunakan sebagai fungsi, mis. Anda dapat memanggil
attrs.style()
, meskipun gaya adalah bidang, bukan fungsi. Dalam panggilan semacam itu, parameter yang ditentukan dalam pemanggilan menyenangkan operator harus dilewati. Dalam hal ini, ini adalah satu parameter - callback: Style. () -> Unit. Karena ini adalah lambda, maka (kurung) adalah opsional.
Mencoba baju zirah yang berbeda
Tetap mempelajari cara menggambar input di Bereaksi, dan hanya teks dalam HTML. Saya ingin mendapatkan sintaks ini:
react { search(search, onChangeSearch) } html { +(search?:"") }
Cara kerjanya: Fungsi reaksi mengambil lambda untuk api Rereact dan mengembalikan tag yang dimasukkan. Pada tag, Anda dapat memanggil fungsi infix dan meneruskan lambda ke api HTML. Pengubah infiks memungkinkan html dipanggil tanpa titik. Sangat mirip dengan jika {} else {}. Dan seperti dalam if-else, panggilan html adalah opsional, berguna beberapa kali.
Implementasi 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 }
Tanda Saruman
Sentuhan lain. Adalah perlu untuk mewarisi ParentTag dan TagWithParent dari antarmuka luka khusus dengan anotasi luka khusus yang merupakan
anotasi khusus
@DslMarker , sudah dari inti bahasa:
@DslMarker annotation class StyledTableMarker @StyledTableMarker interface Tag
Ini diperlukan agar kompiler tidak mengizinkan penulisan panggilan aneh seperti ini:
td { td { } } tr { thead { } }
Tidak jelas, bagaimanapun, siapa yang akan berpikir untuk menulis hal seperti itu ...
Ke pertempuran!
Semuanya siap bagi kita untuk menggambar tabel dari awal artikel, tetapi kode ini sudah menghasilkan ReactDom dan HTML. Menulis sekali dijalankan di mana saja!
fun Table.renderUniversalTable(search: String?, onChangeSearch: (String?) -> Unit) { thead { tr { th { attrs.colSpan = 2 attrs.style { border = "solid" borderColor = "red" } +":" react { search(search, onChangeSearch)
Perhatikan (*) - ini persis fungsi pencarian yang sama seperti dalam versi asli tabel untuk Bereaksi. Tidak perlu mentransfer semuanya ke dsl baru, hanya tag umum.
Apa yang mungkin hasil dari kode tersebut bekerja? Ini adalah
contoh cetakan PDF dari laporan dari proyek saya. Secara alami, saya mengganti semua angka dan nama dengan acak. Sebagai perbandingan,
cetakan PDF dari halaman yang sama, tetapi oleh browser. Artefak dari memecah tabel antara halaman ke teks overlay.
Saat menulis dsl, Anda mendapatkan banyak kode tambahan yang hanya berfokus pada bentuk penggunaan. Selain itu, banyak fitur Kotlin yang digunakan, yang bahkan tidak Anda pikirkan dalam kehidupan sehari-hari.
Mungkin dalam kasus lain akan berbeda, tetapi dalam kasus ini ada juga banyak duplikasi yang tidak dapat saya singkirkan (sejauh yang saya tahu, JetBarin menggunakan pembuatan kode untuk menulis perpustakaan HTML).
Tetapi ternyata bagi saya untuk membangun dsl penampilannya hampir mirip dengan React dan HTML api (saya hampir tidak mengintip). Menariknya, bersama dengan kenyamanan dsl, kami memiliki kontrol penuh atas rendering. Anda dapat menambahkan tag halaman ke halaman terpisah. Anda dapat memperluas "
akordeon " saat mencetak. Dan Anda dapat mencoba menemukan cara untuk menggunakan kembali kode ini di server dan menghasilkan html sudah untuk mesin pencari.
PS Tentunya, ada cara untuk mencetak PDF lebih mudah
Turnip dengan sumber untuk artikel tersebutArtikel lain tentang Kotlin: