React 可视化开发工具 shadow-widget 的非可视开发方法
Shadow Widget 提倡在可视设计器中开发用户界面,输出转义标签,而非 JSX。许多童鞋可能不知道 SW 同样支持用 JSX 设计界面,开发体验比原生 React 编程好出很多,本文就介绍这方面知识。 1. 被官方忽视的开发方法Shadow Widget 区别于其它前端框架的关键特色是可视化设计,因为 JSX 与 javascript 混写,不能直接支持可视化设计。所以,SW 用 “转义标签” 表达可视设计的输出,因为 SW 强调可视化,所以如何运用 JSX 的内容,在官方文档中被弱化了,有一些零星介绍,分散在手册各个章节,本文将它们串接起来讲。 在 Shadow Widget 下做开发,既可以是主流的 “正交框架” 模式(也就是遵循 MVVM 思路设计可视界面,再用 Flux 框架组织横向数据流的开发方式),也可以完全顺从 React 原生模式,只把 SW 看作更好的 lib 库来使用。**下面我们结合代码实例,讲解后一开发方式。 由于 JSX 界面设计与用鼠标拖拉配置界面的设计是等价的,我们以 React 原生模式做开发,相对 SW 主流方式,主要损失可视化的直观特性,其它并不损失。当然,目前使用 JSX 还得借助 Babel 转译环境,搭建 "Babel + Browserify" 或 "Babel + Webpack" 开发环境是不得已的选择。 如何创建新工程及如何搭建 Browerify 或 Webpack 环境,请参考《Shadow Widget 用户手册》的 “3.1 搭建工程环境” 一章。 2. 几个等价概念1)
2) WTC(Widget Template Class,构件模板类) WTC 对应于 React 中各 Component 的 class 类定义。React 要求这么定义: class MyButton extends React.Component { constructor(props) { // ... } componentDidMount() { // ... } render() { // ... } } WTC 要求这么定义(必须从已有的 WTC 类继承,而且只能用 ES6+ 语法才做得到): class MyButton_ extends T.Button_ { constructor(name,desc) { super(name,desc); } getDefaultProps() { var props = super.getDefaultProps(); // props.attr = value; return props; } getInitialState() { var state = super.getInitialState(); // ... return state; } componentDidMount() { // ... } $onClick: function(event) { alert('clicked'); } } WTC 不是 React class,不能在 JSX 中直接使用,应先转成 React class。 3) render 函数 如果想把 React 程序写得更严谨,减少 Side Effects,应该在 Shadow Widget 按这个思路,把控制逻辑集中在 借助以下途径,在 Shadow Widget 可实现 render 过程的逻辑控制:
上面 3 点,第一点比较好理解,第二点是 Shadow Widget 增加的,原生 React 不提供这种操作,比方说,在一个页面提交一条反馈意见,用户可以点一下 “删除” 按钮可删掉刚提交的意见。若用原始 javascript 实现,大概用这么一条语句: commentNode.parentNode.removeChild(commentNode); 原生 React 处理这种需求要稍微绕一下,给 上面第三点,在传入的 3. 将 WTC 转化为 React class同为定义 component 类,WTC 继承链与 React class 继承链是不相干的两条链,前者起始于 比如: var AbstractButton = new MyButton_(); // MyButton_ is WTC var MyButton = AbstractButton._createClass(); // MyButton is React class var jsx = <MyButton>test</MyButton>; var MyButton2 = AbstractButton._createClass( { $onClick: function(event) { alert('another onClick'); } }); var jsx2 = <MyButton2>test2</MyButton2>; var MyButton3 = T.Button._createClass( { $onClick: function(event) { alert('yet another onClick'); } }); var jsx3 = <MyButton3>test3</MyButton3>; 简单理解 WTC,可把它看作 React class 的 class 定义,即,它是一种用于生成 React class 的模板,所以 WTC 是 "构件模板" 的类(Widget Template Class)。 我们之所以要插入 “模板” 一级的抽像物,主要为了适应可视化编程,Widget Template 不只用来生成 React class,也为可视设计器提供支持。另外,一个 Component 的行为在 WTC 中定义,还是在 Shadow Widget 提供 var t = utils.getWTC('*'); // or,utils.getWTC(['Panel','P']) var jsx = ( <t.Panel width={300}> <t.P>Hello,world!</t.P> <t.P><button>Test</button></t.P> </t.Panel> ); 请注意,使用来源于 WTC 的 React class 构造界面,系统会自动生成一颗树,各节点按层次串接起来,任何非 WTC 节点都不能成为 WTC 节点的父节点,反过来可以,即:非 WTC 节点能挂到 WTC 节点下,但 WTC 节点不能挂到非 WTC 节点下。比如上面 4. 用函数封装投影定义4.1 从 SFC 到 PRC,再到 idSetterReact 有两种纯渲染函数,其一是 "Stateless Functional Component"(SFC),如下: function HelloMessage(props) { return <div>Hello {props.name}</div>; } 还有一种 "Pure Render Component"(PRC),所谓 pure 是指,如果 import PureRender from 'react-addons-pure-render-mixin'; class HelloComponent extends React.Component { constructor(props) { super(props); this.shouldComponentUpdate = PureRender.shouldComponentUpdate.bind(this); } render() { return <div>Hello {this.props.name}</div>; } } SFC 似乎简单,易读易维护,也方便 "Lifting State Up"(后文有论述),但 SFC 比 PRC 缺少用 Shadow Widget 开创性的设计 idSetter 机制,将 SFC 与 PRC 的优点结合起来了。一方面,你不需要非得用 class 类定义一个 Component,在层层嵌套的函数式风格中,不宜随便找个地方就定义 class,另外,idSetter 是函数,在一个函数中定义 Component 所有行为。 比如: function btn__(value,oldValue) { if (value <= 2) { if (value == 1) { // init process } else if (value == 2) { // mount } else if (value == 0) { // unmount } return; } // rendering for evey render() // ... } var jsx = <MyButton $id__={btn__}>test</MyButton> 若用 SFC 方式编程,先定义的一个 设计界面时,手头的 tag 标签相当于食材(比如 “米饭”),给各 tag 指定各种属性来控制它的外观,这是最直接的设计方式。Stateless Functional Component 是 “紫菜包饭”,用函数形式包裹食材,idSetter 方式则相当于 “饭包紫菜包饭”,外观表现仍是 “米饭”,内层用紫菜包裹过。
4.2 idSetter 优点传递给 这么处理有几个好处:
我们取 React 官方 var React = require('react'); var ReactDOM = require('react-dom'); var W = require('shadow-widget'); var main = W.$main,utils = W.$utils,ex = W.$ex; var idSetter = W.$idSetter,t = utils.getWTC('*'); function calculatorUI() { var selfComp = null,verdictComp = null; var scaleNames = { c:'Celsius',f:'Fahrenheit' }; function onInputChange(value,oldValue) { var scale = this.parentOf().props.scale || 'c'; // 'c' or 'f' var degree = parseFloat(value) || 0; // take NaN as 0 selfComp.duals.temperature = [scale,degree]; } function calculator__(value,oldValue) { if (value <= 2) { if (value == 1) { // init selfComp = this; this.defineDual('temperature',function(value,oldValue) { if (Array.isArray(value) && verdictComp) { var scale = value[0],degree = value[1]; var isBoil = degree >= (scale == 'c'?100:212); verdictComp.duals['html.'] = isBoil? 'The water would boil.': 'The water would not boil.'; } }); } else if (value == 2) { // mount verdictComp = this.componentOf('verdict'); var field = this.componentOf('field'); var inputComp = field.componentOf('input'); var legend = field.componentOf('legend'); var sScale = field.props.scale || 'c'; legend.duals['html.'] = 'Temperature in ' + scaleNames[sScale]; inputComp.listen('value',onInputChange.bind(inputComp)); selfComp.duals.temperature = [ sScale,parseFloat(inputComp.duals.value) || 0 ]; } else if (value == 0) { // unmount selfComp = verdictComp = null; } return; } } return ( <t.Panel key='panel' width={300} $id__={calculator__}> <t.Fieldset key='field' width={0.9999} scale='c'> <t.Legend key='legend'></t.Legend> <t.Input key='input' type='text' defaultValue='0' /> </t.Fieldset> <t.P key='verdict' width={0.9999} /> </t.Panel> ); } main.$onLoad.push( function() { var bodyComp = W.body.component; var jsx = calculatorUI(); bodyComp.setChild(jsx); }); 在 Flux 框架中,由 Store 直接驱动的那个 View 也叫
Shadow Widget 的 "Lift State Up" 比 React 原生方式更好用。 1) 首先,Shadow Widget 有双源属性,更多过程处理转为对 duals 属性的读写,更简单,更直接,比如上面 2) 其次,双源属性的 listen 机制,也有助于 "Lift State Up",比如上面 3) 还有,idSetter 是函数,函数套函数很容易,很自然,如果下层节点需要处理复杂逻辑,里层嵌套定义另一个 idSetter 函数便可。我们可以把存在关联的上下多层节点的逻辑控制代码,都纳入外层节点的 idSetter 函数中。 4.3 在 idSetter 编程的等效性在 idSetter 函数中编写代码,等效于在 React class 的 function id__(comp) { // will call idSetter() // comp.state.xxx = xxx; // comp.$gui.comps = xxx; } class NewWTC extends T.BaseWTC_ { // ... render() { id__(this); var tagName = xxx,props = xxx,children = comp.$gui.comps; return React.createElement(tagName,props,children); } } 本处代码仅为概要示例,以伪码方式解释工作原理, 与在 render 中编码等效的 idSetter 函数举例: var fieldWidth = 0.9999; var sTitle = 'Temperature in Celsius'; function fieldset__(value,oldValue) { if (value <= 2) { // ... return; } this.duals.width = fieldWidth; utils.setChildren(this,[ <t.Legend key='legend'>{sTitle}</t.Legend>,<t.Input key='input' type='text' defaultValue='0' /> ]); } 函数 尽管在 idSetter 的 5. 高层设计过于 "函数式" 的陷阱即便放弃 Shadow Widget 的可视化开发特性,只把它当作一个常规的 lib 库使用也是很有价值的,主要表现在两方面:
过于 “函数式” 对于界面类开发肯定不好,比如 UI 设计时,我们想摆一个文本框,再摆一个按钮,分解设计的思路是,文本输入变化了( 由于 React 偏爱函数式编程,加上 Flux 强化了数据流设计,容易引导大家一开始就从数据设计入手,着眼于数据如何分解、如何传递、如何驱动响应函数等。采用 Shadow Widget 后,产品开发会往面向对象设计拉回一些,把握这一点就容易理解 Shadow Widget 的设计精髓了。 本专栏历史文章:
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |