React 可视化开发工具 shadow-widget 最佳实践(上)
本文介绍 "React + Shadow Widget" 应用于通用 GUI 开发的最佳实践,只聚焦于典型场景下最优开发方法。分上、下两篇讲解,上篇概述最佳实践,介绍功能块划分。
1. 最佳实践概述
按遵循 ES5 与 ES6+ 区分,Shadow Widget 支持两种开发方式,一是用 ES5 做开发,二是搭建 Babel 转译环境用 ES6+ 做开发,之所以划分两大类,因为它们之间差别不仅仅是 javascript 代码转译,而是涉及在哪个层面定义 React Class,进而与源码在上层还是下层维护,以及与他人如何协作等相关。
如本系列博客《shadow-widget 的非可视开发方法》一文介绍,用 ES5 定义 React class 的方式是:
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+ 下书写。
考虑到用 ES5 编程不必搭建 Babel 开发环境,ES5 能被 ES6+ 兼容,向 ES6+ 迁移只是整体平移,不必改源码。加上 Shadow Widget 及第 3 方类库,已提供够用的基础 WTC 类(这意味着我们并不迫切依赖于用 ES6+ 扩展 WTC),所以,我们将 Shadow Widget 最佳实践确定为:用 ES5 实施主体开发。
Shadow Widget 最佳开发实践的大致操作过程如下:
- 创建一个新的工程,参见《Shadow Widget 用户手册》(下面简称《手册》)中 “5.1.1 创建工程” 一节
应选择一个合适的 "网页样板" 来创建,Shadow Widget 是一个可继承重用的 lib 库体系,最基础的是shadow-widget
库自身,其上还有shadow-slide
,pinp-blogs
等扩展库,各个扩展项目一般会提供它本层的网页样板(通常放在<project>/output/shared/pages/
目录下)。 - 在创建的网页文件追加
<script src='your_file.js'></script>
代码
然后在your_file.js
文件编写 ES5 代码。 - 使用 Shadow Widget 的可视设计器设计用户界面
用户界面设计的结果以转义标签的形式,保存在你的"*.html"
网页文件中,然后你可以在your_file.js
同步编写 JS 代码。 - 完成开发与测试后,把相关的
html, js, css
等文件上传发布到服务器发布
因为不必做 ES6 转译,发布操作很直接。或许您要调整js, css, png
等文件位置,或许您需 minify 某个 JS 文件,这些都是前端开发的基本技能,不是 Shadow Widget 特有的。
最佳实践还建议多用 idSetter 函数定义各 component 的行为,不用(或少用)在 main[path]
定义投影类的方式,因为 idSetter 的函数式风格,让 MVVM 与 Flux 两种框架的交汇点处理起来更便利。
接下来,在展开细节介绍之前,我们先梳理一下 Shadow Widget 技术体系的几个特色概念。
2. 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 主要不足。
3. 几种 Lift State Up 方式
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。由此我有两点推论:
- 弃用界面现成的分解方式,改建另一套体系并不明智
就像描述双人博击,最直接的方式是先区分场上谁是谁,谁出击,谁防守,出击者挥拳,防守者缩头躲避。Redux 行事风格是先设计 “挥拳”、“缩头” 之类的 action,然后分解实施这些 action,来驱动各种 state 变化。该模式之所以行得通,不是 Redux 有多好,而是人脑太奇妙,编程中除了脑补产品应用场景,偶尔还会插帧处理俊男靓女图片 :) - 数据隔离是必需的,否则无法应对大规模产品开发
后文我们将介绍最佳实践中的数据隔离方法,以功能场景为依据。
4. 功能块
为方便说明问题,我们取 React 官方 "Lifting State Up" 一文介绍的,判断温度是否达到沸点的应用场景,编写一段样例代码。
我们想设计如下界面:
4.1 样例程序的功能
如果输入温度未超沸点,界面显示 "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 blockvar selfComp = null, verdictComp = null;
var scaleNames = { c:'Celsius', f:'Fahrenheit' };idSetter['calculator'] = function(value,oldValue) {if (value <= 2) {if (value == 1) { // initselfComp = 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) { // mountverdictComp = 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) { // unmountselfComp = 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 0selfComp.duals.temperature = [scale,degree];}
};})();});
上面 if (W.__design__) return
一句,让其后代码在 __design__
态时(即,在可视设计器中)不生效。
4.2 功能块
按我们最佳实践的做法,界面可视化设计的结果保存在页面 *.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 blockvar selfComp = null, verdictComp = null;
var scaleNames = { c:'Celsius', f:'Fahrenheit' };idSetter['calculator'] = function(value,oldValue) {// ...
};})();
由功能块函数构造的 Namespace 也称 “功能块空间”(Functionarity Block Space),在功能块内共享的变量在此定义,比如这里的 selfComp, verdictComp, scaleNames
变量。
4.3 功能块入口节点
一个功能块的入口节点是特殊节点,它的生存周期反映了功能块的生存周期。它的各层子节点若还存在(即在 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
后,同在一个功能块的其它函数都能使用它了。
(未完,下篇待续...)
React 可视化开发工具 shadow-widget 最佳实践(上)相关推荐
- React 可视化开发工具 Shadow Widget 非正经入门(之四:flux、mvc、mvvm)
本系列博文从 Shadow Widget 作者的视角,解释该框架的设计要点.本篇解释 Shadow Widget 在 MVC.MVVM.Flux 框架之间如何做选择. 1. React Flux 框架 ...
- Echarts+D3可视化开发工具集
Echarts+D3可视化开发工具集 一.前言 大学三年期间,学的很杂,但唯一坚持的就是在做可视化开发,有个负责任的指导老师很重要.因为即将大四,之后不一定会再继续朝这个方面学习,所以把之前参加比 ...
- HTML——前端实时可视化开发工具
前端实时可视化开发工具:liveStyle.liveReload.Broswer-Sync. 一.liveStyle 如图,liveStyle支持三种文件. 需要安装两个插件:浏览器的插件,subli ...
- WINX可视化开发工具的初步设想
以WINX界面库为开发平台 是的,WINX可视化开发工具本身,可能会成为第一个基于WINX并开源的完整应用(当然,不排除其他人比我们更早一步). 以VS2003/VS2005的插件融合到Visual ...
- 桌面html开发工具,html5可视化开发工具_html5可视化_html5可视化设计工具 - 帆软
HTML5不是一种编程语言,而是一种标记语言,是程序员开发网页必用的一种开发工具.目前来说HTML5技术是非常火爆的,而且有专家预估在未来的10年HTML技术都不会过时.HTML5前景好,来学HTML ...
- python组件的react实现_React-Router动态路由设计最佳实践
写在前面 随着单页应用(SPA)概念的日趋火热,React框架在设计和实践中同样也围绕着SPA的概念来打造自己的技术栈体系,其中路由模块便是非常重要的一个组成部分.它承载着应用功能分区,复杂模块组织, ...
- 微服务开发的 10 个最佳实践
作者 | Md Kamaruzzaman 译者 | 冬雨 策划 | 万佳 随着软件系统越来越复杂,大型的软件系统变得难于开发.增强.维护.现代化和规模化.为解决这一问题,人们尝试过模块化软件开发.分层 ...
- eclipse插件开发_开发Eclipse插件的最佳实践
在为IDE Eclipse环境开发插件时,您有几个设计注意事项. 这些注意事项可确保您: 不要锁定用户界面线程. 在不影响性能的情况下装饰用户界面. 在后台处理数据. 本教程讨论了如何利用这些设计注意 ...
- html5游戏开发的五个最佳实践
HTML5是伟大的,因为它多才多艺的 - 它没有具体针对单一的平台.更重要的是,HTML5是无所不在的. 就我所知的,它在你的PC上,你的手机上,你的平板设备上,甚至在你的厨房电器上. 就凭HTML5 ...
- git 打tag_图文讲解,团队开发中的 Git 最佳实践
私信我,回复:学习,获取免费学习资源包. 在 2005 年的某一天,Linux 之父 Linus Torvalds 发布了他的又一个里程碑作品--Git.它的出现改变了软件开发流程,大大地提高了开发流 ...
最新文章
- MPB:中科院城环所杨军组-​​​基于DNA宏条形码的水体浮游细菌群落测序建库方法...
- python3 字符串 转 整型
- add多个文件 git 文件夹_利用 git 提取文件夹下多个特定文件里的内容写到特定文件内...
- 2021.12.20用ULN2003驱动四线步进电机
- 问:关于上升沿和下降沿触发的讨论
- 《JavaScript 高级程序设计》 7.1 正则表达式支持
- 移动端弹出层加遮罩后禁止滑动
- Oracle正则表达式的用法
- python while函数_Python:无法在while循环中调用函数
- SpringBoot框架下集成萤石云平台开发海康的摄像头
- 逆境之中见真章,JASMINER在众多出海企业中脱颖而出
- 化工专业和计算机专业哪个好就业前景,2019化学工程与工艺专业就业前景和就业方向分析...
- 转载:stm32的引脚有两种用途
- linux查看端口和防火墙的常用命令
- Windows 7/Windows Server 2008 R2深入核心剖析
- 【LEDE】x86软路由之路-09-AirPlay音乐推流(安卓+IOS)
- [kuangbin带你飞]专题一 简单搜索D - Fliptile(POJ 3279)
- java桌面宠物swing实现
- 《安富莱嵌入式周报》第234期:2021.10.11--2021.10.17
- 稻草人php,稻草人PHP系统1.0.4发布