
لا يمكنك فقط أخذ وطباعة صفحة مكتوبة بـ React: هناك فواصل صفحات وحقول إدخال. بالإضافة إلى ذلك ، أريد أن أكتب عرضًا مرة واحدة بحيث يولد كل من ReactDom و HTML العادي ، والذي يمكن تحويله إلى PDF.
الجزء الأصعب هو أن React له dsl خاص به ، و html له خاصته. كيف تحل هذه المشكلة؟ اكتب واحدة أخرى!
لقد نسيت تقريبا ، سيتم كتابتها كلها في Kotlin ، لذلك هذا في الواقع مقال حول Kotlin dsl.
لماذا نحتاج إلى أوروك هاي الخاصة بنا؟
هناك الكثير من التقارير في مشروعي ويجب أن تكون جميعها قادرة على الطباعة. هناك عدة خيارات لكيفية القيام بذلك:
- العب مع أنماط الطباعة ، وإخفاء كل ما لا تحتاج إليه ، ونأمل أن يكون كل شيء على ما يرام. ستتم طباعة الأزرار والفلاتر وما شابه فقط كما هي. وأيضًا ، إذا كان هناك الكثير من الجداول ، فمن الضروري أن يكون كل منها على صفحة منفصلة. وشخصيًا ، فإن الروابط والتواريخ المضافة وما إلى ذلك التي تخرج عند الطباعة من الموقع تثير غضبي
- حاول استخدام بعض المكتبات المتخصصة عند التفاعل ، والتي يمكن أن تجعل PDF. لقد وجدت هذا ، إنه بيتا وفيه ، على ما يبدو ، لا يمكنك إعادة استخدام مكونات التفاعل العادية.
- حول HTML إلى لوحة رسم وقم بعمل PDF منها. ولكن لهذا نحن بحاجة إلى HTML ، بدون أزرار وما شابه. ستحتاج إلى تقديمه في عنصر مخفي من أجل طباعته لاحقًا. ولكن لا يبدو أنه في هذا الخيار يمكنك التحكم في فواصل الصفحات.
في النهاية ، قررت كتابة تعليمات برمجية قادرة على توليد كل من ReactDom و HTML. سأرسل HTML إلى الواجهة الخلفية لطباعة ملف PDF عن طريق إدراج علامات خاصة حول فواصل الصفحات على طول الطريق.
يحتوي Kotlin على
مكتبة طبقة للعمل مع React توفر dsl آمن للنوع للعمل مع React. يمكن العثور على كيفية ظهوره بشكل عام في مقالتي السابقة.
كتب JetBrains أيضًا مكتبة
لتوليد HTML . إنه عبر منصة ، أي يمكن استخدامه في كل من Java و JS. هذا أيضًا dsl ، متشابه جدًا في الهيكل.
نحن بحاجة إلى إيجاد طريقة للتبديل بين المكتبات اعتمادًا على ما إذا كنا بحاجة إلى ReactDom أو HTML خالص.
ما هي المواد التي لدينا؟
على سبيل المثال ، خذ جدولًا به مربع بحث في الرأس. هذه هي الطريقة التي يظهر بها الجدول على React و 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"
|
مهمتنا هي الجمع بين الجانبين الأيسر والأيمن من الجدول.
أولاً ، دعونا نكتشف ما هو الفرق:
- في html ،
colSpan
تعيين colSpan
style
و colSpan
في المستوى الأعلى ، في React ، على الكائن المتداخل المتداخل - يملأ النمط بشكل مختلف. إذا كان هذا في HTML هو css عادي كسلسلة ، فإنه في React هو كائن js تختلف أسماء حقوله قليلاً عن css القياسي بسبب قيود JS.
- في إصدار React ، نستخدم الإدخال للبحث ، في HTML نقوم ببساطة بعرض النص. هذا يأتي بالفعل من بيان المشكلة.
حسنًا والأهم: هذه dsl مختلفة مع مستهلكين مختلفين و api مختلفين. بالنسبة للمترجم ، فهي مختلفة تمامًا. من المستحيل عبورهم مباشرة ، لذلك سيكون عليك كتابة طبقة تبدو متشابهة تقريبًا ، ولكن يمكن أن تعمل مع كل من React api و HTML api.
قم بتجميع الهيكل العظمي
في الوقت الحالي ، ما عليك سوى رسم جهاز لوحي من خلية واحدة فارغة:
table { thead { tr { th { } } } }
لدينا شجرة HTML وطريقتان لمعالجتها. الحل الكلاسيكي هو تنفيذ الأنماط المركبة والزائر. فقط لن يكون لدينا واجهة للزائر. لماذا - سيتبين لاحقا.
ستكون الوحدات الرئيسية ParentTag و TagWithParent. يتم إنشاء ParentTag من خلال علامة HTML من Kotlin api (الحمد لله أنه يستخدم في HTML وفي React api) ، ويقوم TagWithParent بتخزين العلامة نفسها ووظيفتين تضيفهما إلى الأصل في متغيرين api.
abstract class ParentTag<T : HTMLTag> { val tags: MutableList<TagWithParent<*, T>> = mutableListOf()
لماذا تحتاج إلى العديد من الأدوية الجنيسة؟ تكمن المشكلة في أن dsl لـ HTML صارم جدًا عند الترجمة. إذا كنت في React ، يمكنك استدعاء td من أي مكان ، حتى من div ، فعندئذٍ في حالة HTML ، يمكنك الاتصال به فقط من سياق tr. لذلك ، سيتعين علينا سحب سياق التجميع في شكل عام في كل مكان.
يتم تهجئة معظم العلامات بنفس الطريقة تقريبًا:
- ننفذ طريقتين للزيارة. واحد للتفاعل ، وواحد لـ HTML. هم مسؤولون عن التقديم النهائي. تضيف هذه الأساليب الأنماط والفئات وما شابه.
- نكتب ملحقًا يُدرج العلامة في الأصل.
هنا مثال على 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 ، فإن النمط هو كائن 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. () -> Unit. بما أن هذا لامدا ، فإن (الأقواس) اختيارية.
تحاول الدروع المختلفة
يبقى تعلم كيفية رسم الإدخال في رد الفعل ، والنص فقط في HTML. أود الحصول على هذا النحو:
react { search(search, onChangeSearch) } html { +(search?:"") }
كيف يعمل: تأخذ وظيفة التفاعل لامدا لـ Rreact api وتعيد العلامة المدرجة. على البطاقة ، يمكنك استدعاء وظيفة infix وتمرير لامدا إلى واجهة برمجة تطبيقات HTML. يسمح معدّل infix باستدعاء html بدون نقطة. يشبه إلى حد كبير إذا {} آخر {}. وكما هو الحال في 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)
انتبه إلى (*) - هذه هي بالضبط وظيفة البحث نفسها الموجودة في الإصدار الأصلي من الجدول لـ React. ليست هناك حاجة لنقل كل شيء إلى dsl الجديد ، فقط العلامات الشائعة.
ماذا يمكن أن تكون نتيجة هذا الرمز؟ في ما يلي
مثال لطباعة ملف PDF لتقرير من مشروعي. وبطبيعة الحال ، استبدلت جميع الأرقام والأسماء عشوائيًا. للمقارنة ، نسخة
مطبوعة من نفس الصفحة ، ولكن عن طريق المتصفح. التحف من كسر جدول بين الصفحات إلى تراكب النص.
عند كتابة dsl ، تحصل على الكثير من التعليمات البرمجية الإضافية التي تركز فقط على شكل الاستخدام. علاوة على ذلك ، يتم استخدام الكثير من ميزات Kotlin ، والتي لا تفكر فيها حتى في الحياة اليومية.
ربما في حالات أخرى ، سيكون الأمر مختلفًا ، ولكن في هذه الحالة كان هناك أيضًا الكثير من التكرار الذي لم أستطع التخلص منه (على حد علمي ، يستخدم JetBarins توليد التعليمات البرمجية لكتابة مكتبة HTML).
ولكن اتضح لي أن بناء dsl مشابه تقريبًا في المظهر لواجهة التفاعل و HTML (لم أكن زقزقة تقريبًا). من المثير للاهتمام ، إلى جانب راحة dsl ، لدينا سيطرة كاملة على العرض. يمكنك إضافة علامة صفحة لفصل الصفحات. يمكنك توسيع "
الأكورديون " عند الطباعة. ويمكنك محاولة إيجاد طريقة لإعادة استخدام هذا الرمز على الخادم وإنشاء html بالفعل لمحركات البحث.
PS بالتأكيد ، هناك طرق لطباعة PDF أسهل
اللفت مع مصدر المقال