用JSON描述布局,是低代码热潮下的大势所趋。通过http传输一段JSON,就可以在前端基于一套引擎代码,渲染出完全不同的界面和交互,这是低代码分发的一种重要形式。这一背景下,JSON Schema被委以重任,被广泛采用。但在尝试使用之后,我发现JSON Schema只适合描述数据结构(如后端的数据模型),而不适合描述布局。而结合现代前端开发范式,我提出了一种HyperJSON,即HyperScript的JSON表达,更适合用来让JSON描述布局。
HyperScript
在react、vue中被广泛使用,hyperscript是用javascript描述hypertext(超文本)的一种方式。
var h = require('hyperscript')
h('div#page',
h('div#header',
h('h1.classy', 'h', { style: {'background-color': '#22f'} })),
h('div#menu', { style: {'background-color': '#2f2'} },
h('ul',
h('li', 'one'),
h('li', 'two'),
h('li', 'three'))),
h('h2', 'content title', { style: {'background-color': '#f22'} }),
h('p',
"so it's just like a templating engine,\n",
"but easy to use inline with javascript\n"),
h('p',
"the intention is for this to be used to create\n",
"reusable, interactive html widgets. "))
在react中,通过React.createElement实现这一效果,并进行了内部约束。
HyperJSON
为了用JSON描述布局,可将HyperScript的表达形式进行转化,并再进一步进行内部规范。例如,我们将下面的jsx用JSON来表达:
function MyComponent() {
return (
<div className="hyperjson-container">
<h3>HyperJSON</h3>
<p>HyperJSON is a schema for discribing JSX</p>
</div>
)
}
用JSON描述就是:
["div", { "className": "hyperjson-container" },
["h3", null, "HyperJSON"],
["p", null, "HyperJSON is a schema for describing JSX."]
]
简单说,就是将createElement的参数用嵌套(递归)的JSON来表示。数组的第一个值代表要使用的组件,第二个值代表渲染组件时使用的props,后面剩下的值表示组件的children,children部分将会作为hyperjson递归处理。
HyperJSON就是基于这样的表达方式,增加了一些语法规则:
- 动态语法:
- 表达式:在JSON中使用特殊的标记
{}
指定该标记内的内容为动态语法表达式 - 作用域:作用域中只能读取该作用域内预设的变量,预设变量由props指定
- 函数式:属性名以
()
或(p1,p2,p3)
的形式结尾的,表示这是一个函数,函数体为动态语法表达式
- 表达式:在JSON中使用特殊的标记
- 宏:属性末尾以
!
结尾,表示这是一个需要使用解析器再次解析的jsx表达体
一个简单的HyperJSON示例:
{ "props": ["value", "onChange"], "render!": [ "input, { "value": "{ value }", "onChange(e)": "{ onChange(e.target.value) }" } ] }
下面我会详细解释其中的每一部分。
动态语法
HyperJSON表达和JSX一致的布局描述,但是我们都知道,JSX是给视图框架实时运算的材料,要得到结果,往往依赖一些变量(状态)才能算出实时的布局。因此,在HyperJSON中,需要添加一些动态语法来支持这种计算。
表达式
动态语法表达式以 {}
为标记,例如:
["input", {
"value": "{ value }"
}]
其中 value
的值 "{ value }"
中的 value
部分就是表达式。表达式和javascript表达式非常像,你可以在表达式中使用一些运算符。例如
value + 1 ie ? '1' : '0' parent.child // 返回一个子属性的值 { a: 1 } // 返回一个对象
表达式不能有js中常见的语句,不支持复杂表达式。
作用域
在JSX中,我们习惯使用如下语法:
function MyComponent(props) {
return <input value={props.value} onChange={props.onChange} placeholder="请输入..." />
}
通过 {} 来表达值是动态值,"" 表达静态值。它的作用域就是组件所在的作用域,具体到上面这段代码,就是指 with(props) 这个作用域。
而在HyperJSON中,由于一段HyperJSON只描述一个组件的内容,因此,它需要有一个前置的描述,也就是对 props 的描述:
{ "props": ["value", "onChange"], "render!": [ ... ] }
上面的代码中,"props"指明了当前作用域可以使用那些从props上读取的属性值。它相当于真实js代码:
function Component(props) { const { value, onChange } = props ... }
"props"的形式有三种:
"props": "$props"
字符串,这种形式表示在内部用`$props`表示props本身"props": ["onSubmit"]
字符串数组,这种形式表示从 props 读取 onSubmit 属性后,传入内部,内部使用 onSubmit"props": { "name": "$name" }
对象,这种形式是给属性取一个别名,这里的意思是用$name代表props.name,在内部直接使用$name
例如:
{ "props": { "value": "$value" }, "render!": [ "input", { "value": "{ $value }" } ] }
通过在顶层的props属性中规定,我们给予了一个HyperJSON对象作用域。
函数式
上面示例代码中已经出现了函数式:
["input", { "onChange(e)": "{ onChange(e) }" }]
函数式中的参数,会覆盖上一级作用域中的变量。
宏
默认情况下props部分表达的是值或函数,但有的时候,你需要让它赋予新的含义,比如当你需要接收一个jsx作为props的一部分的时候:
<SomeItem
title={<h3>title</h3>}
content={<div>content</div>}
/>
这种时候,HyperJSON通过宏来定义这种特殊需求。
["SomeItem", {
"title!": ["h3", null, "title"],
"content!": ["div", null, "content"]
}]
通过在属性的末尾加一个 ! 来表达这是一个特殊的props属性,需要用特殊的解释器来执行它的值。
其他方面
Fragment
内置的组件,等于React.Fragment,例如:
["Fragment", {}, ["div"], ["div"] ]
null
在jsx中是支持null的,同时,基于某些特定的语法,也需要null支持。例如:
<div> {v === 1 ? <div>1111</div> : null} {v === 2 ? <div>222</div> : null} </div>
在HyperJSON中如下表达:
["div", {}, ["{ v === 1 ? 'div' : null }", {}, "1111"], ["{ v === 2 ? 'div' : null }", {}, "2222"] ]
当HyperJSON中的第一个值为null时,表示这一句将不被渲染。
结语
HyperJSON是一种新的协议,面对新事物,不同的人接受程度不同。目前 HyperJSON 已经在腾讯内源项目 Formast 和前端框架 Nautil 中使用。如果你觉得这种协议有助于你的项目,不妨和我一起探讨。
2021-03-01 3646
市面上很多低代码平台都说用json schema协议来表达视图,但是我发现其实都是用json;我们现在的json目前来说都是到组件级别的,还没到html标签级别
是的,在真实的场景下面,肯定是以组件为单位,不可能细到一个单个html标签为单位
老哥,你是作者吗?可以加个微信方便后面交流哇!805902285
你可以扫博客地下的二维码关注我和我聊天哦