本系列博文从 Shadow Widget 作者的视角,解释该框架的设计要点。本篇讲解转义标签、json-x、投影定义,这几项与 "如何分离界面设计" 有关。
1. 找一个 JSX 替代品
如上一篇 "非正经入门(之一)" 所述,Shadow Widget 要克服 "JSX浆糊" 的不利影响,要找一个 JSX 替代品。
比如下面 JSX 表达方式:
return <h1 id={user.id}>Dear {user.name},{welcomeMsg(user)}</h1>;
等效于:
return React.createElement( H1,{id:user.id},"Dear ",user.name,",welcomeMsg(user) );
创建一个 Element,需传递三项信息:ReactClass@H_301_16@,
props@H_301_16@,
children@H_301_16@ 列表。我们把这三项改造成一个 array 数组格式:
[ [ReactClass,props],child1,child2... ]
其中,child1,child2@H_301_16@ 是子节点定义,格式是 string 字串,或 array 数组。这种以 array 数组表达一个 Element 节点的格式叫 json-x 描述方式,与 JSX 完全等效。
2. 转义标签
为了方便在 html 网页文件中描述用户界面,我们定义 "转义标签" 的表达方式,如下:
<div $=Panel> <div $=P> <span $=Span>Referece:</span> <span $=A src='http://example.com'>example.com</span> </div> </div>
转义标签无非将所有 HTML 标签划分为行内标签与 block 标签,前者用 <span>@H_301_16@ 表达,后者用
<div>@H_301_16@ 表达,各标签中用
$=XXX@H_301_16@ 属性定义的方式指示实际使用哪个 React Class,如
$=Panel@H_301_16@ 表示用
T.Panel@H_301_16@,
$=P@H_301_16@ 表示用
T.P@H_301_16@。所有经 Shadow Widget 扩展出的构件模板类(也称 WTC,Widget Template Class)都应注册到
T@H_301_16@ 之下,这样,网页刚打开时,由转义标签中
$=XXX@H_301_16@ 指示,能找到相应的 React class,实现正常挂载。
在转义标签中定义的属性,比如上面的 src='http://example.com'@H_301_16@,在挂载前先整理出 props 表(如
{src:"http://example.com"}@H_301_16@),而转义标签的上下级节点的从属关系,以及同级节点之间的前后关系,指明了 json-x 数据中的 children 定义。所以,
ReactClass@H_301_16@,
props@H_301_16@,
children@H_301_16@ 三项信息都有了,转义标签能转换成 json-x,所以它与 JSX 也是等效的。
转义标签具有良好可读性,所以,它在 *.html@H_301_16@ 文件中可以直接书写。另外,这种格式对搜索引擎也友好,若依赖 JSX 定义界面,搜索引擎无法分析 html 文件中定义了什么信息。
3. 第一眼是妖孽,多半就是妖孽
React 支持服务侧渲染,这个特性似乎鼓励了其生态链上若干工具额外拓展服务侧功能。比如 react-router 中 Router 组件的 history 属性,既可以是 browserHistory@H_301_16@,也可以是
hashHistory@H_301_16@。对于前者,客户侧路由(即 URL 路径)决定服务侧如何实现,将两侧的设计捆绑起来的,后者
hashHistory@H_301_16@(即
#/some/path@H_301_16@)完全在客户侧自主决定,与服务侧无关。很显然,后一方式优于前者,前者违背了软件设计的 "关注点分离"(Separation of concerns,SOC)原则,并且在实践上,服务侧只有用 webpack-dev-server (加
--history-api-fallback@H_301_16@ 参数)才能玩得好,不只绑架用 JS 语言,而且绑架用特定工具。要命的是,react-router 官方居然推荐大家首选
browserHistory@H_301_16@。
这个 browserHistory@H_301_16@ 就是充满妖气的特性,怪里怪气,表面看起来有用,实则禁不起推敲。React 的
propTypes@H_301_16@ 也很妖,官方让它存活了这么久,最终决定在
v15.5@H_301_16@ 之后弃用,连
context@H_301_16@ 也不建议用了,
context@H_301_16@ 本是 React 为缓解跨节点数据共享不便,弄出的不伦不类的东西。
某种程度上 React 的服务侧渲染也多少沾点 "妖气",有些人仅为了解决 SEO 优化用它,仔细想想有点本末倒置了。它的初始需求源于 google 之类的搜索引擎不认 JSX,因为 JSX 服务于编程,编程脚本原不该由搜索引擎关注的,该关注的只是一些静态文本。处理静态文本没必要拉上 React 一家子吧?但事实却是,我们非得套用一个客户侧编程风格,用 JS 开发的服务侧渲染工具,你说妖不妖?
4. 分离界面设计
在分离界面之前,我们还需建立路径索引机制。
Shadow Widget 通过一颗树(Widget 树,R 树)管理由它定义的界面,各节点都有 key 值作标识,既可以显示指定一个 key 值,也可以缺省,缺省时由系统自动生成一个数字来表示。这果颗树的根节点是 ".body"@H_301_16@,如果根节点下有一个 key 值为
"toolbar"@H_301_16@ 的 Panel 节点,它的绝对路径就是
".body.toolbar"@H_301_16@。
有了路径索引机制,我们能将界面描述与它的行为定义分离开了。比如这么定义界面:
<div $=BodyPanel key='body'> <div $=Panel key='toolbar'> <div $=P key='p'> <span $=Button key='btn1'>Test</span> </div> </div> </div>
这么定义 Test@H_301_16@ 按钮的行为:
main['.body.toolbar.p.btn1'] = { $onClick: function(event) { alert('clicked'); },};
界面的转义标签在 *.html@H_301_16@ 文件中书写,界面元素的行为定义在
*.js@H_301_16@ 文件进行,如此,界面设计分离出来了,界面描述与相关元素的行为定义通过该元素的绝对路径实现关联。如上例,用 javascript 编写某元素的行为定义,也称 "投影定义"。
5. 表达复杂的 props 数据
json-x 数据与转义标签都与 JSX 对等,但传递 props@H_301_16@ 数据有若干限制,比如转义标签不支持传递函数对象,json-x 可传函数对象,但也不鼓励(主要因为不规范)。函数定义应在投影类中定义,就像上面举例的
$onClick@H_301_16@ 函数,不通过转义标签的属性来传递,只在转义标签挂载时,到
main@H_301_16@ 下找到相应投影定义,然后捆绑相应的函数定义。
除了函数,描述复杂的 props 数据时,json-x 的表达能力是完整的,因为它本来就是 javascript 数据,但转义标签受 html 标签格式的影响,要改用 JSON 字串来表示,比如:
<div $=Panel title='tool bar' width='{400}'> </div>
属性值用 '{'@H_301_16@ 与
'}'@H_301_16@ 括起来,表示它是 JSON 字串,用
JSON.parse@H_301_16@ 前要先删掉首尾两个花括号,如上面
width@H_301_16@ 值为
JSON.parse('400')@H_301_16@。另外,对于 string 类型的属性值,可以直接传递(避开字串首尾是花括号的情形),不必按 JSON 字串的方式,如上面
title@H_301_16@ 属性。
6. idSetter 函数
实施界面与底层分离除了投影定义,还有一种指定 idSetter 函数的方式,若简单去理解,该方式是投影定义的一个变种,同样实现特定界面元素的行为定义的动态绑捆。
举例来说,界面这么描述:
<div $=BodyPanel key='body'> <div $=Panel key='toolbar'> <div $=P> <span $=Button $id__='btn1'>Test</span> </div> </div> </div>
Javascript 这么定义:
idSetter['btn1'] = function(value,oldValue) { if (value <= 2) { if (value == 1) { // init process this.setEvent( { $onClick: function(event) { alert('clicked'); },}); // ... } else if (value == 2) { // did mount // ... } else if (value == 0) { // will unmount // ... } return; } // render process ... };
这种书写方式与上面投影定义的方式是等效的,投影类中该在 getInitialState()@H_301_16@ 中书写的代码,要挪到 idSetter 函数的
if (value == 1)@H_301_16@ 分支中,该在
componentDidMount()@H_301_16@ 中书写的代码移到
if (value == 2)@H_301_16@ 的分支中,该在
componentWillUnmount()@H_301_16@ 中书写的代码移到
if (value == 0)@H_301_16@ 的分支中。
使用 idSetter 函数的优点是,相应界面节点的绝对路径不必完整定义,即路径上各段不必显式给出 key 值,系统由 $id__='xxx'@H_301_16@ 属性值,自动找出 idSetter 函数。另一个优点是,编程风格更加函数式。
7. 建立 W 树供随时节点定位
Flux 框架要求节点间数据流向要遵守严格的约束,React 不惜牺牲编程便利性,刻意隐藏了内建的那颗虚拟 DOM 树,导致编程中跨节点调用非常不便,各节点都被一层黑墙包裹,无法探知周围都有哪些节点存在,好在 React 为这个黑墙开了一扇单向玻璃窗:refs@H_301_16@,让父节点可以引用子节点,子节点引用不了父节点。克服引用不便的解药是引入 redux 那样的框架,把存在交叉影响的两个或多个节点中的数据,提升到一个公共区域去编程。
既然 Shadow Widget 引入 MVVM 框架,在 Component 的 API 层面限制节点间互通已不合时宜,单向数据流应该在更高层面的设计去保证。所以,Shadow Widget 引入了 "W 树" 的概念,也就是,所有符合规格的 Component 节点(即源于 WTC 类创建的节点)都串接在一颗树上。树中各节点都有唯一 "路径" 标示,节点之间还可以用 "相对路径" 或 "绝对路径" 引用,比如:
this.componentOf('//') // get parent component this.componentOf('//brother') // brother node this.componentOf('sub.child') // child node this.componentOf('./seg.child') // by relative path this.componentOf('.body.top.toolbar') // by absolute path
有了 W 树设计,router 规划将变得简单明了,比方下图界面,把两个可切换的页 Article@H_301_16@ 与
Talk@H_301_16@ 装到一个导航面板(NavPanel)中,若想切换到 Article 页,按
"/article"@H_301_16@ 导航,切换另一页用
"/talk"@H_301_16@ 导航。
(本文完)
本专栏历史文章: