浅谈前端框架中的DSL
一、前言
一个普通的web网站应用使用 html、xhml 等更具描述能力的 external dsl(domain-specific language)来描述界面,然后使用javascript代码来解决界面上的一些逻辑问题,使用css来描绘界面的样式。这些 external dsl 用于将数据配置跟代码逻辑分离开来
一些现代语言加入了 internal dsl 这种东西,它赋予你在代码中写 dsl 的能力:比如jsx
语法,vue语法。就是在javascript中使用类似于 html 的语法。
前端编程界的趋势是将 external dsl 混写 javascript 这类着重表达逻辑的语言里面。
分享一段flutter dart代码:
// 这段代码可以类比于`React.createElement`
class Drawer extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Drawer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
DrawerHeader(
decoration: BoxDecoration(color: Colors.green),
child: Padding(
padding: const EdgeInsets.only(bottom: 20),
child: Icon(
Icons.account_circle,
color: Colors.green.shade800,
size: 96,
),
),
),
// Long drawer contents are often segmented.
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Divider(),
),
ListTile(
leading: SettingsTab.androidIcon,
title: Text(SettingsTab.title),
onTap: () {
Navigator.pop(context);
Navigator.push<void>(context,
MaterialPageRoute(builder: (context) => SettingsTab()));
},
),
],
),
)
}
}
从上面的Dart代码可以看出,使用OO的编程语言去描述界面,会显得代码语义不够直观(请注意BoxDecoration
, Padding
的使用)。
因此,描述界面我们更倾向于专门使用描述性质的语言,描述性语言可拓展性好,对人类友好,结构清晰易读。比如xml,从这个角度来看待React组件中的render
方法,和Vue组件中的<template></template>
就发现一切都明了了起来。
虽然React,Vue中的 DSL 会被框架本身转化成javascript代码。但其 DSL 部分的内容,对开发者的更友好,开发心智负担减少
另外的思考:html,xml 语言从控制流的角度来看,无法写条件分支和循环分支。而Vue内置的v-for
, v-if
关键字优雅的解决了这一问题。
二、从Vue看React
既然DSL语言描述界面有天然优势,我们更希望,在一个文件中,DSL的归DSL,OO的归OO。因此,对于如下的代码:
// Example.js
class Example extends component {
this.state = { list: [1, 2, 3] }
render() {
<ul>{this.state.list.map(val => <li>{val}</li>)}</ul>
}
}
// Example.vue
<template>
<ul><li v-for="val in list">{{ val }}</ul>
</template>
<script>
export default {
data() {
return {
list: [1, 2, 3]
}
}
}
</script>
我更倾向采用vue的写法。
但是react本身是没有开发自定义指令相关的功能。好在社区根据babel的转码es5的流程,自定义封装了相应的babel插件,以满足我们在react的render
函数中使用类似于v-for
, v-if
的指令功能
实际上,我们可以通过外包一层组件的方式来达到相似的目的。
1. if[Hidden]组件
/**
* 对于返回多个节点的情况,请用`<template></template>`标签包裹即可
*/
const Hidden = (props) => {
const { visible, children, render = null } = props
const comp =
children.type === 'template' ? children.props.children : children
if (visible) {
if (typeof render === 'function') {
return render()
} else {
return comp
}
} else {
return null
}
}
2. Switch组件
const Switch = (props) => {
const { value, children } = props
return value ? children[0] : children[1]
}
3. Match组件
const Match = (props) => {
const { exp, children, default } = props
let content = default
children.forEach((child) => {
if (child.props.value === exp) content = child
})
if (content && content.type === 'template') content = content.props.children
return content
}
最终的写法如下:
<Hidden visible={true}><div>this text should be display</div></Hidden>