React 可视化开发工具 Shadow Widget 非正经入门之二:分离界面设计

本系列博文从 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,需传递三项信息:ReactClasspropschildren 列表。我们把这三项改造成一个 array 数组格式:

[ [ReactClass,props],child1,child2...
  ]

其中,child1,child2 是子节点定义,格式是 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> 表达,后者用 <div> 表达,各标签中用 $=XXX 属性定义的方式指示实际使用哪个 React Class,如 $=Panel 表示用 T.Panel$=P 表示用 T.P。所有经 Shadow Widget 扩展出的构件模板类(也称 WTC,Widget Template Class)都应注册到 T 之下,这样,网页刚打开时,由转义标签中 $=XXX 指示,能找到相应的 React class,实现正常挂载。

在转义标签中定义的属性,比如上面的 src='http://example.com',在挂载前先整理出 props 表(如 {src:"http://example.com"}),而转义标签的上下级节点的从属关系,以及同级节点之间的前后关系,指明了 json-x 数据中的 children 定义。所以,ReactClasspropschildren 三项信息都有了,转义标签能转换成 json-x,所以它与 JSX 也是等效的。

转义标签具有良好可读性,所以,它在 *.html 文件中可以直接书写。另外,这种格式对搜索引擎也友好,若依赖 JSX 定义界面,搜索引擎无法分析 html 文件中定义了什么信息。

3. 第一眼是妖孽,多半就是妖孽

React 支持服务侧渲染,这个特性似乎鼓励了其生态链上若干工具额外拓展服务侧功能。比如 react-router 中 Router 组件的 history 属性,既可以是 browserHistory,也可以是 hashHistory。对于前者,客户侧路由(即 URL 路径)决定服务侧如何实现,将两侧的设计捆绑起来的,后者 hashHistory(即 #/some/path)完全在客户侧自主决定,与服务侧无关。很显然,后一方式优于前者,前者违背了软件设计的 "关注点分离"(Separation of concerns,SOC)原则,并且在实践上,服务侧只有用 webpack-dev-server (加 --history-api-fallback 参数)才能玩得好,不只绑架用 JS 语言,而且绑架用特定工具。要命的是,react-router 官方居然推荐大家首选 browserHistory

这个 browserHistory 就是充满妖气的特性,怪里怪气,表面看起来有用,实则禁不起推敲。React 的 propTypes 也很妖,官方让它存活了这么久,最终决定在 v15.5 之后弃用,连 context 也不建议用了,context 本是 React 为缓解跨节点数据共享不便,弄出的不伦不类的东西。

某种程度上 React 的服务侧渲染也多少沾点 "妖气",有些人仅为了解决 SEO 优化用它,仔细想想有点本末倒置了。它的初始需求源于 google 之类的搜索引擎不认 JSX,因为 JSX 服务于编程,编程脚本原不该由搜索引擎关注的,该关注的只是一些静态文本。处理静态文本没必要拉上 React 一家子吧?但事实却是,我们非得套用一个客户侧编程风格,用 JS 开发的服务侧渲染工具,你说妖不妖?

4. 分离界面设计

在分离界面之前,我们还需建立路径索引机制。

Shadow Widget 通过一颗树(Widget 树,R 树)管理由它定义的界面,各节点都有 key 值作标识,既可以显示指定一个 key 值,也可以缺省,缺省时由系统自动生成一个数字来表示。这果颗树的根节点是 ".body",如果根节点下有一个 key 值为 "toolbar" 的 Panel 节点,它的绝对路径就是 ".body.toolbar"

有了路径索引机制,我们能将界面描述与它的行为定义分离开了。比如这么定义界面:

<div $=BodyPanel key='body'>
  <div $=Panel key='toolbar'>
    <div $=P key='p'>
      <span $=Button key='btn1'>Test</span>
    </div>
  </div>
</div>

这么定义 Test 按钮的行为:

main['.body.toolbar.p.btn1'] = {
  $onClick: function(event) {
    alert('clicked');
  },};

界面的转义标签在 *.html 文件中书写,界面元素的行为定义在 *.js 文件进行,如此,界面设计分离出来了,界面描述与相关元素的行为定义通过该元素的绝对路径实现关联。如上例,用 javascript 编写某元素的行为定义,也称 "投影定义"。

5. 表达复杂的 props 数据

json-x 数据与转义标签都与 JSX 对等,但传递 props 数据有若干限制,比如转义标签不支持传递函数对象,json-x 可传函数对象,但也不鼓励(主要因为不规范)。函数定义应在投影类中定义,就像上面举例的 $onClick 函数,不通过转义标签的属性来传递,只在转义标签挂载时,到 main 下找到相应投影定义,然后捆绑相应的函数定义。

除了函数,描述复杂的 props 数据时,json-x 的表达能力是完整的,因为它本来就是 javascript 数据,但转义标签受 html 标签格式的影响,要改用 JSON 字串来表示,比如:

<div $=Panel title='tool bar' width='{400}'>
  </div>

属性值用 '{''}' 括起来,表示它是 JSON 字串,用 JSON.parse 前要先删掉首尾两个花括号,如上面 width 值为 JSON.parse('400')。另外,对于 string 类型的属性值,可以直接传递(避开字串首尾是花括号的情形),不必按 JSON 字串的方式,如上面 title 属性。

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() 中书写的代码,要挪到 idSetter 函数的 if (value == 1) 分支中,该在 componentDidMount() 中书写的代码移到 if (value == 2) 的分支中,该在 componentWillUnmount() 中书写的代码移到 if (value == 0) 的分支中。

使用 idSetter 函数的优点是,相应界面节点的绝对路径不必完整定义,即路径上各段不必显式给出 key 值,系统由 $id__='xxx' 属性值,自动找出 idSetter 函数。另一个优点是,编程风格更加函数式。

7. 建立 W 树供随时节点定位

Flux 框架要求节点间数据流向要遵守严格的约束,React 不惜牺牲编程便利性,刻意隐藏了内建的那颗虚拟 DOM 树,导致编程中跨节点调用非常不便,各节点都被一层黑墙包裹,无法探知周围都有哪些节点存在,好在 React 为这个黑墙开了一扇单向玻璃窗:refs,让父节点可以引用子节点,子节点引用不了父节点。克服引用不便的解药是引入 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 规划将变得简单明了,比方下图界面,把两个可切换的页 ArticleTalk 装到一个导航面板(NavPanel)中,若想切换到 Article 页,按 "/article" 导航,切换另一页用 "/talk" 导航。

(本文完)

本专栏历史文章:

  1. 介绍一项让 React 可以与 Vue 抗衡的技术

  2. React 可视化开发工具 Shadow Widget 非正经入门(之一:React 三宗罪)

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如果组件之中有复用的代码,需要重新创建一个父类,父类中存储公共代码,返回子类,同时把公用属性...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例如我们的 setState 函数式同步执行的,我们的事件处理直接绑定在了 dom 元素上,这些都跟 re...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom 转为真实 dom 进行挂载。其实函数是组件和类组件也是在这个基础上包裹了一层,一个是调...
react 本身提供了克隆组件的方法,但是平时开发中可能很少使用,可能是不了解。我公司的项目就没有使用,但是在很多三方库中都有使用。本小节我们来学习下如果使用该...
mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接触 react 就一直使用 mobx 库,上手简单不复杂。
我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc 端可以使用分页进行渲染数限制,在移动端可以使用下拉加载更多。但是对于大量的列表渲染,特别像有实时数据...
本小节开始前,我们先答复下一个同学的问题。上一小节发布后,有小伙伴后台来信问到:‘小编你只讲了类组件中怎么使用 ref,那在函数式组件中怎么使用呢?’。确实我们...
上一小节我们了解了固定高度的滚动列表实现,因为是固定高度所以容器总高度和每个元素的 size、offset 很容易得到,这种场景也适合我们常见的大部分场景,例如...
上一小节我们处理了 setState 的批量更新机制,但是我们有两个遗漏点,一个是源码中的 setState 可以传入函数,同时 setState 可以传入第二...
我们知道 react 进行页面渲染或者刷新的时候,会从根节点到子节点全部执行一遍,即使子组件中没有状态的改变,也会执行。这就造成了性能不必要的浪费。之前我们了解...
在平时工作中的某些场景下,你可能想在整个组件树中传递数据,但却不想手动地通过 props 属性在每一层传递属性,contextAPI 应用而生。
楼主最近入职新单位了,恰好新单位使用的技术栈是 react,因为之前一直进行的是 vue2/vue3 和小程序开发,对于这些技术栈实现机制也有一些了解,最少面试...
我们上一节了了解了函数式组件和类组件的处理方式,本质就是处理基于 babel 处理后的 type 类型,最后还是要处理虚拟 dom。本小节我们学习下组件的更新机...
前面几节我们学习了解了 react 的渲染机制和生命周期,本节我们正式进入基本面试必考的核心地带 -- diff 算法,了解如何优化和复用 dom 操作的,还有...
我们在之前已经学习过 react 生命周期,但是在 16 版本中 will 类的生命周期进行了废除,虽然依然可以用,但是需要加上 UNSAFE 开头,表示是不安...
上一小节我们学习了 react 中类组件的优化方式,对于 hooks 为主流的函数式编程,react 也提供了优化方式 memo 方法,本小节我们来了解下它的用...
开源不易,感谢你的支持,❤ star me if you like concent ^_^
hel-micro,模块联邦sdk化,免构建、热更新、工具链无关的微模块方案 ,欢迎关注与了解
本文主题围绕concent的setup和react的五把钩子来展开,既然提到了setup就离不开composition api这个关键词,准确的说setup是由...
ReactsetState的执行是异步还是同步官方文档是这么说的setState()doesnotalwaysimmediatelyupdatethecomponent.Itmaybatchordefertheupdateuntillater.Thismakesreadingthis.staterightaftercallingsetState()apotentialpitfall.Instead,usecom