كتبته DSL في TypeScript من JSX


يحتوي TypeScript على دعم داخلي لبناء جملة JSX ويوفر برنامج التحويل البرمجي لـ TypeScript أدوات مفيدة لإعداد عملية تجميع JSX. بشكل أساسي ، هذا يجعل من الممكن كتابة DSL المكتوب باستخدام JSX. هذه المقالة سوف تناقش هذا بالضبط - كيفية كتابة DSL من ص باستخدام jsx. مهتمة ، أطلب القط.


مستودع مع مثال الجاهزة.


في هذه المقالة ، لن أعرض الإمكانيات بأمثلة متعلقة بالويب ، React ، وما شابه. مثال على ذلك ليس من الويب سيوضح أن قدرات JSX لا تقتصر على React ومكوناته وتوليد html في الحالة العامة. في هذه المقالة ، سأوضح كيفية تنفيذ DSL لإنشاء كائنات الرسائل لـ Slack .


إليك الكود الذي نأخذه كأساس. هذا هو مصنع رسالة صغيرة من نفس النوع:


interface Story { title: string link: string publishedAt: Date author: { name: string, avatarURL: string } } const template = (username: string, stories: Story[]) => ({ text: `:wave:  ${username},    .`, attachments: stories.map(s => ({ title, color: '#000000', title_link: s.link, author_name: s.author.name, author_icon: s.author.avatarURL, text: `  _${s.publishedAt}_.` }) }) 

يبدو أنها تبدو جيدة ، ولكن هناك نقطة واحدة يمكن تحسينها بشكل كبير - إمكانية القراءة . على سبيل المثال ، انتبه إلى خاصية color غير title_link ، أو title_link بالعنوان ( title و title_link ) ، أو title_link السفلية في text (النص الموجود داخل _ سيكون مائلًا ). كل هذا يمنعنا من فصل المحتوى عن التفاصيل الأسلوبية ، مما يجعل من الصعب العثور على ما هو مهم. ومع مثل هذه المشاكل ، ينبغي أن يساعد DSL.


هنا هو نفس المثال الذي كتب للتو في JSX:


 const template = (username: string, stories: Story[]) => <message> :wave:  ${username},    . {stories.map(s => <attachment color='#000000'> <author icon={s.author.avatarURL}>{s.author.name}</author> <title link={s.link}>{s.title}</title>   <i>{s.publishedAt}</i>. </attachment> )} </message> 

أفضل بكثير! كل شيء يجب أن يعيش معًا لديه تفاصيل ومحتوى موحد ، مفصولان بوضوح - جمال بكلمة واحدة.


كتابة DSL


تخصيص المشروع


تحتاج أولاً إلى تمكين JSX في المشروع وإخبار المترجم أننا لا نستخدم React ، وأن JSX الخاص بنا يحتاج إلى تجميع بطريقة مختلفة.


 // tsconfig.json { "compilerOptions": { "jsx": "react", "jsxFactory": "Template.create" } } 

"jsx": "react" يتضمن "jsx": "react" دعم JSX في المشروع ويقوم المترجم بترجمة جميع عناصر React.createElement إلى مكالمات React.createElement . "jsxFactory" خيار "jsxFactory" بتكوين المترجم لاستخدام مصنعنا لعناصر JSX.


بعد هذه الإعدادات البسيطة ، رمز النموذج:


 import * as Template from './template' const JSX = <message>Text with <i>italic</i>.</message> 

سوف تجميع في


 const Template = require('./template'); const JSX = Template.create('message', null, 'Text with ', Template.create('i', null, 'italic'), '.'); 

وصف JSX العلامات


الآن وبعد أن تعرف المترجم ما الذي يجب ترجمة JSX إليه ، نحتاج إلى إعلان العلامات بأنفسهم. للقيام بذلك ، نستخدم واحدة من ميزات TypeScript الرائعة - وهي إعلانات مساحة الاسم المحلية. بالنسبة إلى حالة JSX ، تتوقع TypeScript أن يحتوي المشروع على مساحة اسم JSX (لا يهم الموقع المحدد للملف) مع واجهة IntrinsicElements التي يتم فيها وصف العلامات نفسها. المحول البرمجي catches لهم ويستخدمها للتحقق من النوع وتلميحات.


 // jsx.d.ts declare namespace JSX { interface IntrinsicElements { i: {} message: {} author: { icon: string } title: { link?: string } attachment: { color?: string } } } 

هنا أعلنا عن جميع علامات JSX الخاصة بـ DSL وكل سماتها. في الواقع ، اسم المفتاح في الواجهة هو اسم العلامة نفسها ، والتي ستكون متاحة في الكود. القيمة هي وصف للسمات المتاحة. قد لا تحتوي بعض العلامات ( i في حالتنا) على أي سمات ، وقد يكون البعض الآخر اختياريًا أو ضروريًا.


المصنع نفسه - Template.create


لدينا مصنع من tsconfig.json هو موضوع المحادثة. سيتم استخدامه في وقت التشغيل لإنشاء كائنات.


في أبسط الحالات ، قد يبدو مثل هذا:


 type Kinds = keyof JSX.IntrinsicElements //    type Attrubute<K extends Kinds> = JSX.IntrinsicElements[K] //    export const create = <K extends Kinds>(kind: K, attributes: Attrubute<K>, ...children) => { switch (kind) { case 'i': return `_${chidlren.join('')}_` default: // ... } } 

من السهل كتابة العلامات التي تضيف أنماط فقط إلى النص بداخلها ( i في حالتنا): يلتف المصنع ببساطة بمحتويات العلامة في سلسلة مع _ على كلا الجانبين. تبدأ المشاكل بعلامات معقدة. معظم الوقت قضيته معهم ، أبحث عن حل أنظف. ما هي المشكلة الفعلية؟


وهو أن المترجم يطبع نوع <message>Text</message> على any . الأمر الذي لم يقترب من خط المشترك الرقمي (DSL) المكتوب ، حسنًا ، حسنًا ، الجزء الثاني من المشكلة هو أن نوع كل العلامات سيكون واحدًا بعد المرور عبر المصنع - وهذا قيد JSX نفسه (في React ، يتم تحويل جميع العلامات إلى ReactElement).


الأدوية الجنيسة تذهب إلى الإنقاذ!


 // jsx.d.ts declare namespace JSX { interface Element { toMessage(): { text?: string attachments?: { text?: string author_name?: string author_icon?: string title_link?: string color?: string }[] } } interface IntrinsicElements { i: {} message: {} author: { icon: string } title: { link?: string } attachment: { color?: string } } } 

Element إضافة Element فقط وسيقوم برنامج التحويل البرمجي الآن بإخراج جميع علامات JSX إلى نوع Element . هذا أيضًا سلوك مترجم قياسي - استخدم JSX.Element كنوع لجميع العلامات.


يحتوي Element على طريقة شائعة واحدة فقط - تحويلها إلى نوع كائن رسالة. لسوء الحظ ، لن تعمل دائمًا ، فقط على <message/> ذات المستوى الأعلى ، وسيكون هذا في مهلة.


وتحت المفسد هو النسخة الكاملة من المصنع.


رمز المصنع نفسه
 import { flatten } from 'lodash' type Kinds = keyof JSX.IntrinsicElements //    type Attrubute<K extends Kinds> = JSX.IntrinsicElements[K] //    const isElement = (e: any): e is Element<any> => e && e.kind const is = <K extends Kinds>(k: K, e: string | Element<any>): e is Element<K> => isElement(e) && e.kind === k /*         () */ const buildText = (e: Element<any>) => e.children.filter(i => !isElement(i)).join('') const buildTitle = (e: Element<'title'>) => ({ title: buildText(e), title_link: e.attributes.link }) const buildAuthor = (e: Element<'author'>) => ({ author_name: buildText(e), author_icon: e.attributes.icon }) const buildAttachment = (e: Element<'attachment'>) => { const authorNode = e.children.find(i => is('author', i)) const author = authorNode ? buildAuthor(<Element<'author'>>authorNode) : {} const titleNode = e.children.find(i => is('title', i)) const title = titleNode ? buildTitle(<Element<'title'>>titleNode) : {} return { text: buildText(e), ...title, ...author, ...e.attributes } } class Element<K extends Kinds> { children: Array<string | Element<any>> constructor( public kind: K, public attributes: Attrubute<K>, children: Array<string | Element<any>> ) { this.children = flatten(children) } /* *          `<message/>` */ toMessage() { if (!is('message', this)) return {} const attachments = this.children.filter(i => is('attachment', i)).map(buildAttachment) return { attachments, text: buildText(this) } } } export const create = <K extends Kinds>(kind: K, attributes: Attrubute<K>, ...children) => { switch (kind) { case 'i': return `_${children.join('')}_` default: return new Element(kind, attributes, children) } } 

مستودع مع مثال الجاهزة.


بدلا من الاستنتاج


عندما أجريت هذه التجارب ، كان لدى فريق TypeScript فقط فهم القوة والقيود على ما فعلوه مع JSX. الآن قدراته أكبر ويمكن أن يكون المصنع أكثر نظافة. إذا كنت تريد البحث عن المستودع وتحسينه بمثال - Wellcome مع طلبات السحب.

Source: https://habr.com/ru/post/ar433348/


All Articles