本文介绍 "React + Shadow Widget" 应用于通用 GUI 开发的最佳实践,只聚焦于典型场景下最优开发方法。分上、下两篇讲解,上篇概述最佳实践,介绍功能块划分。javascript
按遵循 ES5 与 ES6+ 区分,Shadow Widget 支持两种开发方式,一是用 ES5 作开发,二是搭建 Babel 转译环境用 ES6+ 作开发,之因此划分两大类,由于它们之间差异不只仅是 javascript 代码转译,而是涉及在哪一个层面定义 React Class,进而与源码在上层仍是下层维护,以及与他人如何协做等相关。css
如本系列博客《shadow-widget 的非可视开发方法》一文介绍,用 ES5 定义 React class 的方式是:html
var MyButton = T.Button._createClass( { getDefaultProps: function() { var props = T.Button.getDefaultProps(); // props.attr = value; return props; }, getInitialState: function() { var state = this._getInitialState(this); // ... return state; } $onClick: function(event) { alert('clicked'); } });
而用 ES6+ 开发,这么定义 React class:前端
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; } $onClick: function(event) { alert('clicked'); } } var AbstractButton = new MyButton_(); // MyButton_ is WTC var MyButton = AbstractButton._createClass(); // MyButton is React class
因为 ES6+ 语法能兼容 ES5,因此,即便采用 ES6+ 开发方式,前一种 ES5 的 React class 定义方法仍然适用。但,自定义扩展一个 WTC 类必须用 ES6+,就象上面 "class MyButton_ extends T.Button_"
语法,只能在 ES6+ 下书写。java
考虑到用 ES5 编程没必要搭建 Babel 开发环境,ES5 能被 ES6+ 兼容,向 ES6+ 迁移只是总体平移,没必要改源码。加上 Shadow Widget 及第 3 方类库,已提供够用的基础 WTC 类(这意味着咱们并不迫切依赖于用 ES6+ 扩展 WTC),因此,咱们将 Shadow Widget 最佳实践肯定为:用 ES5 实施主体开发。node
Shadow Widget 最佳开发实践的大体操做过程以下:react
shadow-widget
库自身,其上还有 shadow-slide
,pinp-blogs
等扩展库,各个扩展项目通常会提供它本层的网页样板(一般放在 <project>/output/shared/pages/
目录下)。<script src='your_file.js'></script>
代码 your_file.js
文件编写 ES5 代码。"*.html"
网页文件中,而后你能够在 your_file.js
同步编写 JS 代码。html, js, css
等文件上传发布到服务器发布 js, css, png
等文件位置,或许您需 minify 某个 JS 文件,这些都是前端开发的基本技能,不是 Shadow Widget 特有的。最佳实践还建议多用 idSetter 函数定义各 component 的行为,不用(或少用)在 main[path]
定义投影类的方式,由于 idSetter 的函数式风格,让 MVVM 与 Flux 两种框架的交汇点处理起来更便利。git
接下来,在展开细节介绍以前,咱们先梳理一下 Shadow Widget 技术体系的几个特点概念。github
p-state
与 v-state
p-state
与 v-state
是 uglee 在 《少妇白洁系列之 React StateUp Pattern, Explained》 一文提出的概念,咱们借用过来解释 React 中的数据流转模式。p-state
指 persistent state,是生命周期超过组件自己的 state 数据,即便组件从 DOM 上销毁,这些数据仍然须要在组件外部持久化。v-state
指 volatile state,是生命周期和组件同样的 state 数据,若是组件从 DOM 上销毁,这些 state 将一块儿销毁。编程
结合 Flux 框架,v-state
就是 comp.props.xxx
与 comp.state.xxx
数据,p-state
就是 store 里的数据,这么说虽有失严谨,但大体如此。若是未使用 Flux 框架,对 comp
的 render()
过程产生影响的全部数据中,全局变量或其它节点(包括上级节点)中的属性,都算当前节点的 p-state
。
不过,v-state
与 p-state
划分是静态的,相对而言的。好比,初始设计界面只要求显示摄氏度(Celsius)格式的温度值,而后以为要适应全球化应用,摄氏度与华氏度(Fahrenheit)都得显示,再日后发现,Celsius 与 Fahrenheit 并列显示不够友好,就改为动态可配置,取国别信息后自动设成二者中一个。这种设计变迁中,“当前温度格式” 与 “并列显示或只显示一种” 的配置数据常常在 v-state
与 p-state
之间变迁。
React 工具链上几个 Flux 框架主要区别在于,如何定位与使用 p-state
,它们对 v-state
使用基本一致,咱们拿 reflux、redux、shadow-widget 三者分别举例。
Reflux 采用多 store,其 store 设计与 component 很接近,能够这么简单理解:既然跨 Component 存在数据交互,父子关系能够用 props
传递,非父子关系传不了,怎么办呢?那就设立第三方实体(也就是 store)处理此事。Redux 采用单 store,把它理解成一大坨全局变量就好,它以 action 设计为提纲,围绕 action 组织 reducer 函数,而 Reflux 中提纲挈领的东西则是 store 中的数据,围绕数据组织 action 定义。若对比这二者,Reflux 方式更易理解,需求分解与设计展开过程更人性化,不过,Reflux 没有突破 React 固有限制,由于多 store 模式,实践中你们常常很纠结某项数据该放在 component 中,仍是放在 store 中呢?如前所述,一项数据是否为 v-state
是相对的,产品功能叠代后,数据常常要从 v-state
提高到 p-state
,或者,若原设计偏于宽泛,还需将 p-state
降回 v-state
。Reflux 困境在于 Store 设计与 Component 不对称,顺应来回变迁的成本较高。
Shadow Widget 也是多 Store,Component 自身就是 store,这克服了 Reflux 主要不足。另外结合 MVVM 架构的可视化特色,Shadow Widget 还克服了 redux 主要不足。
Shadow Widget 介绍了一种 “逆向同步 & 单向依赖” 的机制,在以下节点树中,nodeE 要使用 nodeC 中的数据,但 nodeC 生存周期与 nodeE 并不一致,因此,引入一种机制,在它们共同的父节点 nodeA 设置一个属性(好比 attrX
),nodeC 中的该数据能自动同步到 nodeA 中,而后让 nodeE 只依赖 nodeA 中的数据(好比 attrX
),只要 NodeE 还存活,父节点 nodeD 与 nodeA 必然存活。
nodeA +-- nodeB | +-- nodeC +-- nodeD | +-- nodeE
React 官方介绍了一种 "Lifting State Up" 方法,借助函数式编程的特色,把控制界面显示效果的变量,从子节点提高到父节点,子节点的事件函数改在父节点定义,就达到 Lift State Up
的效果。
既然提高 state 能突破 React 对数据传递的限制,那么,极端一点,可否把全部用到的数据都改为全局变量呢?答案固然能够,不过缺乏意义,这么作,无非将分散在各节点的逻辑,转移处处理一堆全局变量而己,设计过程本该分解,而非合并。可视节点分层分布本是自然的功能划分方式,放弃它改换门庭无疑把事情搞复杂了,可恶的 Redux 就是这么干的。
从本质上看,Redux 把 state 数据全局化了(成为单 store),但它又以 action 主导切割数据,你并不能直接存取全局 store,而是改由 action 驱动各个 reducer,各 reducer 只孤立处理它自身可见的 state。由此我有两点推论:
为方便说明问题,咱们取 React 官方 "Lifting State Up" 一文介绍的,判断温度是否达到沸点的应用场景,编写一段样例代码。
咱们想设计以下界面:
若是输入温度未超沸点,界面显示 "The water would not boil"
,若超沸点则显示 "would boil"
。另外,用于输入温度的方框(即后述的 field
节点)要求可配置,用 scale='c'
指示以摄氏度表示,标题提示 "Temperature in Celsius"
,不然 scal='f'
指示华氏度,提示 "in Fahrenheit"
。
咱们在 Shadow Widget 可视设计器中完成设计,存盘后生成的转义标签以下:
<div $=BodyPanel key='body' klass='S5'> <div $=Panel key='calculator' klass='hidden-visible-auto row-reverse' height='{null}' width='{300}' $id__='calculator'> <div $=Fieldset key='field' width='{0.9999}' scale='c'> <span $=Legend key='legend'>legend</span> <span $=Input key='input' type='text' default-value='0'></span> </div> <div $=P key='verdict' klass='visible-auto-hidden' width='{0.9999}'></div> </div> </div>
而后在 JS 文件编写以下代码:
if (!window.W) { window.W = new Array(); W.$modules = [];} W.$modules.push( function(require,module,exports) { 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; if (W.__design__) return; (function() { // functionarity block var selfComp = null, verdictComp = null; var scaleNames = { c:'Celsius', f:'Fahrenheit' }; idSetter['calculator'] = function(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)); this.duals.temperature = [ sScale, parseFloat(inputComp.duals.value) || 0 ]; } else if (value == 0) { // unmount selfComp = verdictComp = null; } return; } 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]; } }; })(); });
上面 if (W.__design__) return
一句,让其后代码在 __design__
态时(即,在可视设计器中)不生效。
按咱们最佳实践的作法,界面可视化设计的结果保存在页面 *.html
文件,而界面的代码实现(包括定义事件响应、绑捆数据驱动等)在 JS 文件编写。因此,上面例子的设计结果包括两部分:*.html
文件中的转义标签与 *.js
文件中的 javascript 脚本。
多个组件共同完成某项特定功能,他们合起来造成逻辑上的总体叫作 “功能块” (Functionarity Block)。典型的 JS 文件一般按这个样式编写:
if (!window.W) { window.W = new Array(); W.$modules = [];} W.$modules.push( function(require,module,exports) { // 全局变量定义 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; if (W.__design__) return; // 功能块定义 (function() { // .... })() // 初始化定义 main.$onLoad.push( function() { // ... }); });
头部用来定义若干全局变量,而后定义功能块,功能块可能有多个,上面举例的判断温度是否超沸点,比较简单,定义一个功能块就够了,最后定义 main.$onLoad
全局初始化函数。
之因此将一个功能块用一个函数包裹,主要为了构造独立的命名空间(Namespace),好比前面举例的代码:
(function() { // functionarity block var selfComp = null, verdictComp = null; var scaleNames = { c:'Celsius', f:'Fahrenheit' }; idSetter['calculator'] = function(value,oldValue) { // ... }; })();
由功能块函数构造的 Namespace 也称 “功能块空间”(Functionarity Block Space),在功能块内共享的变量在此定义,好比这里的 selfComp, verdictComp, scaleNames
变量。
一个功能块的入口节点是特殊节点,它的生存周期反映了功能块的生存周期。它的各层子节点若还存在(即在 unmount 以前),入口节点必然存在。由于入口节点的生存期能完整覆盖它各级子节点的生存期,因此,咱们通常在入口节点定义 idSetter 函数,承担本功能块的主体逻辑处理。
上例的功能块定义了以下节点树:
Panel (key=calculator) +-- Fieldset (key=field) | +-- Legend (key=legend) | +-- Input (key=input) +-- P (key=verdict)
入口节点是 calculator
面板,结合该节点的 idSetter 函数书写特色,咱们接着介绍 Shadow Widget 最佳实践如何处理 "功能块" 以内的编程。
1) 为方便编程,不妨在 “功能块空间” 多定义变量
由于 “功能块空间” 的变量不外泄到其它功能块,咱们没必要担忧多定义变量会给其它部分编码带来 Side Effects。功能块里各个节点,只要不是动态建立、删除、再建立那种,均可定义成 “功能块空间” 的变量,咱们通常在入口节点 idSetter 函数的 unmount 代码段(即 if (value == 0)
),把各个节点的变量置回 null
值。
对于动态增删的节点,不妨用 this.componentOf(sPath)
动态方式定位。
2) 功能块内的数据主体流向,宜在界面设计时就指定
在功能块的 idSetter 函数也能以编程方式设计节点间数据流向,考虑到界面设计与数据流规则直接相关,能以描述方式(转义标签形式)表达数据流的,尽可能用描述方式,不方便的才用 JS 编程方式去实现。由于,一方面,Shadow Widget 的指令式 UI 描述能力够强,另外一方面,这么作有助于让 MVVM 中的 ViewModel
集中,从而下降设计复杂度。
界面设计时,不妨多用下述技巧:
$for=''
或 $$for=''
开启一层 callspace,方便其下节点的可计算属性用 duals.attr
引用数据。$trigger
同步数据NavPanel
与 NavDiv
),用 "./xx.xx"
相对路径方式让节点定位更方便
3) 善用变量共享机制
若按 React 原始开发方式编码,不借助任何 Flux 框架工具,你们确定以为编程很不方便,由于各节点除了能往子节点单向传递 props
外,与其它节点的交互几乎隔了一道黑幕。然而,不幸的是,React 几个主流的 Flux 工具,均没有妥善解决几个主要问题,上面提到的 Reflux、Redux 均如此,React 官方的 react-flux
更难用。
相对而言,Shadow Widget 的解决方案好不少,一方面,在 Component 节点引入 “双源属性”,功能强大,能让基于过程组装的 UI 渲染,过渡到 以属性变化来驱动渲染,即:除了 “功能块” 的入口节点需集中编写控制逻辑,其它节点的编程,基本简化为定制若干 duals 函数(用 defineDual()
注册)。另外一方面,Shadow Widget 借助 Functionarity Block 抽象层来重组数据,以功能远近做聚合依据,明显比以 Action 驱动的 Reducer 分割要高明。
从本质上讲,拎取 “功能块抽象层” 也是 Lift State Up
的一种手段,限制更少,结合于 JS 编程也更天然。虚拟 DOM 树中的各 component 节点有隔离措拖,不能互相识别,但函数编程没什么限制,好比上面例子,selfComp = this
把一个 Component 赋给 “功能块空间” 的变量 selfComp
后,同在一个功能块的其它函数都能使用它了。
(未完,下篇待续...)