JSX使用TypeScript输入DSL


TypeScript具有对JSX语法的内置支持,并且TypeScript编译器提供了用于设置JSX编译过程的有用工具。 本质上,这使得使用JSX编写类型化DSL成为可能。 本文将对此进行详细讨论-如何编写DSL 从r 使用jsx。 有兴趣的,我要猫。


带有示例的存储库。


在本文中,我不会通过与Web,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属性,标题的两个字段( titletitle_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支持,并且编译器将所有JSX元素编译为React.createElement调用。 然后, "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接口,其中描述了标签本身。 编译器捕获它们并将它们用于类型检查和提示。


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

在这里,我们声明了DSL的所有JSX标签及其所有属性。 实际上,接口中的键名称是标签本身的名称,可以在代码中使用。 值是可用属性的描述。 一些标签(在我的情况下为i )可能没有任何属性,其他标签则是可选的,甚至是必需的。


工厂本身Template.create


tsconfig.json中的工厂是我们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 ,现在编译器将所有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/zh-CN433348/


All Articles