react经验
在公司用 React 写界面的已经有一段时间了, 有些习惯可以沉淀一下
目前代码当中主要还是我个人的使用习惯, 后面应该会改善
最近进行的一个页面是参照 Flux 架构做的, 只能说尝试 Flux 还太浅了
后面我会大致罗列一些点, 用 React 的同学可以对比下或者指点一些问题
使用 CoffeeScript 写 JSX
我对缩进语法的认同以及对重复写打开关闭符号的反感不解释了
Teambition 前后端代码大部分是 CoffeeScript, 所以我不用有太多的顾虑
JSX 当中的语法, 比如是这样:
<div class="avatar" data-tip="name">{text}</div>
编译的 JavaScript 是这样:
React.createElement("div", {class: "avatar", 'data-tip': "name"}, text)
用 CoffeeScript 目前是写成这样:
# $ = React.DOM
$.div className: 'avatar', 'data-tip': 'name', text
createElement
是 0.12
出现的, 目前依然兼容 React.DOM
当中的方法调用
长期看也许会修改适应新的语法, 但目前暂时不动:
# $ = React.createElement
$ 'div', className: 'avatar', 'data-tip': 'name', text
在 0.12
中调整增加了 createFactory
, 所以实际项目当中处理并不优雅
目前导出模块时会有很多这样的代码, 未来模块化时是有必要调整的:
React.createFactory React.createClass
displayName: 'app'
render: ->
$.div {}
使用 CoffeeScript 写逻辑
因为 JSX 编译结果当中一个标签或者模块就是调用函数返回对象
所以在 CoffeeScript 当中直接把标签当作普通数据来处理就好了
而 undefined
这样的数值, 在 React 当中自动忽略, 比较方便
所以经常会有类似模版引擎的用法:
$.div className: 'demo',
if @state.showMenu
$.div className: 'menu', 'demo of menu'
实践当中, 由于有些位置业务逻辑复杂繁多, 一个 render
当中节点可能很多
常用的方案就是创建 renderMenu
这样的方法, 在 render
当中调用
我的理解是复杂的结构中 render
适合写逻辑判断, 其他方法写具体的模块
这样, 以后复制代码或者改逻辑, 在 render
一个方法里就看完了map
是列表里常用的一个办法, 通常我也会拆出 renderItem
这样的方法
React 的 addons
里有个 classSet
方法, 对于拼接 className
非常有用
http://facebook.github.io/react/docs/class-name-manipulation.html
我最初是自己写公共函数拼接的, 后来切换到了这个方案
CoffeeScript 语法容易出错的地方
CoffeeScript 很简洁是一方面, 不过这个简洁确实会有一些容易出现问题的地方
比如这样两行, 第一行结尾的逗号, 在修改代码时经常容易错误删除:
$.div className: 'a',
$.span {}
还有 Object 在 CoffeeScript 里的写法, 如果属性较多单行不够也容易出错:
# 这段代码示范的是一个错误
$.input className: 'a',
onClick: @onClick
# 订正后如果用多行, 比如可以这样
$.input
className: 'a'
onClick: @onClick
另外 CoffeeScript 的 if
是只返回最后一个值的, 注意不要写这样的代码:
$.div {},
if @state.showMenu
# 这个例子里第一行不能正确返回的..
$.span {}, "first line"
$.span {}, "second line"
上边提到的 createFactory
API 对于类库也有一些影响
如果从第三方导入 API, 就可能要考虑用上边的 API 进行封装
相对 JSX 来说, 这里是一个不方便的地方
模块都是自己写的
公司当前项目当中几乎没有用到第三方的 Component
原因大致理解有这么几个:
- 通常界面都挺简单的, 不用费很多的事情就写出来了
- 几乎所有的 UI, 设计界面有较明确的要求, 我们要自己保持风格一致
- React 没有看到非常流行的组件库
虽然追求模块化当初对代码重用的热情是非常高的, 但 React 并不自动解决问题
我想说, 使用 React 拆分模块的成本比 Backbone 拆分 View 低非常多
于是在我的应用中有着比 Backbone 稍微细的模块拆分, 也就稍微多了一点重用
但是, 注意重用的模块是不带逻辑的非常小的模块, 大的模块一般很难重用
按照我们公司设计对界面的不断演进, 我注意到模块当中逻辑会缓缓增加
这不断给模块复用增加难度, 因为我需要加上配置项才能继续复用模块
按我最近的微博上的讨论, 我感到稍微大的模块, 就应该以不进行复用来设想
http://weibo.com/1651843872/BA2tIks5x
Modal
大量使用 Modal 组件我认为是对单页面应用界面的探索不够深入造成的
当界面上需要更多的交互, 马上想到不破坏初始的状态, 弹一层出来...
不过确实是干脆利落的办法, 结果是我们有很多的 Modal 需要处理
以往用 Backbone 和 BootStrap, Modal 是附着在 <body>
之下的
在 React 当中, 涉及到 Modal 打开状态的问题, 我目前的处理是在模块内
也就是说, Modal 是深深嵌套, 然后用 fixed
定位打开的:
$.div {},
$.div {},
$.div {},
$.div {},
if @state.showModal
$.div
className: 'modal-demo', # style: {position: 'fixed'}
onClose: @onClose
$.span {}, 'content demo'
同样的方案, 也用在菜单上, 弹出菜单也需要一个状态来控制打开个关闭
不过菜单会涉及到一些定位的问题, 处理起来更加麻烦一些
关于菜单有个全局唯一菜单的问题, 就是一个菜单出现后要关闭其他菜单
首先, 点击菜单内部的元素不能关闭菜单, 需要截断事件冒泡
其次, 点击菜单外部需要失焦关闭菜单, 需要在 window
上进行监听
问题来了, React 假如创建好了菜单, 又监听到冒泡, 关闭了怎么办?
目前我控制了编辑按钮延时触发, 避免监听, 牺牲是打开菜单点击触发依然打开而不是关闭
我询问了下公司 Backbone 里的处理, 用到了对 event.target
父节点探测
但是在我目前的代码当中, 一个模块会被限制只获取尽可能少的数据...
而且由于减少了 jQuery 的依赖, 意味着检查父节点不会那么容易
我还没有想到个清晰的方案, 需要继续探索
方法的分类, 命名的习惯
大多的 Component, 定义的结构和命名的习惯大致已经开始固定下来了
我注意到定义模块的分类还跟 Elm 有挺多的相似之处, 比如 Elm 是这样:
https://github.com/evancz/elm-todomvc/blob/master/Todo.elm
- Model 初始状态
- Update 对状态可以有哪些更新
- View 模块组合跟渲染
- Input 绑定模块和外部的交互
在我这边项目当中通常是这样的:
- 标记
propTypes
, 初始化state
, 绑定模块生命周期的 hook - 一些自定义的方法
- 事件绑定
- 渲染相关的方法
好吧... 这个其实也不那么像.. 而且通常 View 都是需要完成这几部分功能的
跟 Backbone 区别是, Backbone 很可能因为不方便拆模快, 方法很多甚至变乱
我估计还有监听数据更新操作 DOM 的方法太多, 以及处理其他功能的方法等等
React 里的方法和模块比较紧密, 因此也就明确了几个类别
关于命名, 自定义方法比较随便, 但会使用和模块 API 差别较大的名字state
当中命名, 我目前统计下来以 show
开头为主, 用来控制界面
至于渲染, 自然是 render
开头的, 比如 renderName
, renderActions
在事件当中, 尽量都是 on
开头, 比如 onNameClick
, onTextChange
之类
有一些事件是子模块的事件, 我也用的是 on
做前缀来处理
不过有的模块子模块很多, 方法也就有些乱, 这也需要想点办法改进
列表的节点改变顺序的渐变
界面是这样的, 一个列表, 会不停改变顺序, 过程中使用 CSS 进行动画
需要注意, DOM 里的顺序改变, 节点删除和创建, CSS 动画就不生效了
于是就需要保持 DOM 顺序不变, 但是通过 absolute
定位控制节点
我采用的手法是使用 id
进行排序, 保证 DOM 的顺序不变:
data
.map (a) ->
key: a.id
data: a
.sort (wrap) ->
wrap.id
.map (a, index) ->
$.div style: {top: "#{index * 20}px"}
Ajax 代码设计的问题
React 教程给的 Demo, Ajax 没有很明确的介绍, 我也没深入挖..
Ajax 拿到的数据, 要么设置在 props
, 要么设置在 state
当然我考虑的是, 应用当中有 Store, 就尽量用 Store 了, 方便复用
不过实际的应用当中, 高达 10 层的模块嵌套也可能存在, 并没那么自由
我之前一直操心 Ajax 代码放在哪, 是在 Component 方法内部?
还是放在 Store 当中? 还是独立存放在另一个位置?
麻烦在于, Ajax 加载状态可能跟模块内部紧密相关..
同时, Ajax 代码写在模块当中, 重用的可能性又明显下降了
Ajax 大部分是可以引起 Store 更新的, 那么在 Store 里也许不错
可是, 不在 View 当中, View 就要用 action 通知 Ajax 了, 这很不方便
不好取舍... 当前牺牲的是模块的复用性...
与此相关的一个问题是模块被移除之后, 异步方法调用 setState
会报错
修补的办法是临时加上 isMounted
对模块进行检验...
不过这个方案让我有点困惑, 显然不是一个漂亮的能免于各种问题的方案..
也许异步调用尽量放到模块以外是更好的办法, 但并不明朗..
强制使用 propTypes
我是写了很多模块后才开始接触到 propTypes
这个属性的:
http://facebook.github.io/react/docs/reusable-components.html
这个可以理解成函数来说, 是参数的类型, 起到提示(console.warn
)的作用
当模块参数多到一定程度, 也不太规则, 就非常需要定义好参数类型
一方面, 这个是文档, 另一方面, 进行重构时是非常重要的提醒
全局的多语言的处理
我们的单页面应用需要处理英文两种, 以及设置语言后立即在界面范围内更新
以前我们模仿的是 Wunderlist, 用特性的属性配合 jQuery 来切换
但是在 React 当中, 全局刷新是非常轻松愉快的事情,
不需要写上属性给选择器调用, 也不会影响 DOM 状态
同时需要注意, 为了达到全局更新的效果, 根节点就需要监听语言的改变
我目前在做的方案是将语言抽象为全局的 Store, 作为数据交给模块渲染
而语言发生改变时, Store 马上就发送事件, 界面自然就更新了
思维的转换, 数据流
React 编写应用的方式和 Backbone 差别很大, 甚至和 MVVM 也有不小差别
我认为这种转变跟从过程式编程到函数式编程的转变非常相似
在 Backbone 用进行怎样的操作, 想的就是先做什么, 后做什么
甚至于发送怎样的事件, 让其他的模块来完成这一部分的功能
而在 React 当中, View 之间不是通过事件进行交流的, 而是 props
和 state
而 React 的界面, 就像个不断渲染的 canvas
, 你只能修改数据来改变界面
而不是界面是很多个有状态的 View, 每个 View 都需要额外去照顾
于是就需要思考界面由几个状态控制, 用户操作怎样改变那几个状态等等
思考数据以怎样的途径流动, 每个模块根据这一个数据流如何更新自己
想要简单, 就要尽可能用简单的途径, 就是单向的数据流
不过比较可能的是, 我们很难马上用单向数据流解决到各种业务问题
然后需要一些手段, 按照 @kejunz 今天微博讲的:
http://weibo.com/1640297597/BBnTtnxWz
组件上位后,组件间通讯问题又出来了,之前好像要么pub/sub这种模式,要么回调,数据总是带有业务逻辑色彩的,要和通用组件完全隔离目前没有特别理想的方案。组件并发工作的构想很诱人,实现和通讯都有待解决。CSP没细看过,感觉是很酷的东西。期待有实践的人发文章分享。
React 在数据层面的没有给出相近的方案, 这方面有好多要探索的
一个趋势是很多函数式编程的概念会被借鉴过来, 特别是从 Clojure
CSP 最初是 Go 当中的.. 不畏惧艰难的话尝试一下 Clojure 当中的概念:
http://phuu.net/2014/08/31/csp-and-transducers.html
Router 模块不稳定
这是我这两天对付 react-router
遇到了问题, API 更新比较直接
我这会没跟上 API 更新后具体如何使用, 虽然对其功能并不质疑
当然在项目里语法这样的事情挺郁闷的
react-router
和 RequireJS 搭配问题也挺多的, 模块很特殊啊
代码使用 CommonJS 写的, 用 Browserify 打包, 而且需要全局要 React 对象
按我参与 Issue 上的记录, 事情并不乐观, 我只能尽量考虑 CommonJS 的方案:
https://github.com/rackt/react-router/issues/171
当然未来还有 ES6 需要考虑, 我现在暂时先抛开模块的问题了
Backbone 和 React 混合的架构
在 Backbone View 销毁的时候, 需要调用一下下面的 API
React.unmountComponentAtNode
否则可能出现同时存在两个节点的情况, 在调试工具可以看到
Modal 原先在 Backbone 当中挂在 body, React 中没那么灵活
如果把 Modal 放在外部, 就要考虑额外的, 比如用事件去通知外部了
在 React 当中使用事件是让人不放心事情, 我考虑还是放在嵌套的 DOM 内部
有些 Component 就是应该从 Store 取数据的
虽然一厢情愿希望 Component 都是从 props 取数据, 实际上不合适
某些位置, 直接从 Store 的当中获取, 能比较轻易得简化的逻辑
特别明显的比如, 从位置 A 打开的 Modal B, 且 B 是比较复杂的
而 A 的界面, 可能很简单, 或许都不需要 Store 当中的数据
那么这时, B 直接访问 Store, 效果非常好. 我认为大部分的 Modal 都是这样的
这也就是说, 并不是所有的 Component 都是适合模块化的
React 的 Component 写界面, 就是普普通通地会遇到各种数据传递的问题
有一部分数据, 特别是嵌入在简单 UI 当中的复杂 UI, 很可能需要很大的权限
以此为代价, B 上层的模块 A 就可以只传入更少的数据, 从而被简化
使用 state 即便数据很容易拿到
React Devtools 的 Chrome 插件, 很容易看到 Component 的 props 和 state
在设计 Component 的 state 和 props 时, 这是很好的一个参考
比如说, 哪些 UI 是很频繁需要确认 props 或者 state, 就拆为模块
这种情况下, 模块拆分就能够带来实际的用处, 就是方便查看状态
类似的情况下, 比如我们用 Store, 而数据可以很容易从 Store 拿到
一种可能但不好的做法是, 监听更新, 然后调用 .forceUpdate()
更新
把这个数据放在 state 会是一个更好的做法, 可以避免手动调用强制更新
同时, 通过调试工具查看模块当前的 state 就很方便了
把方便理解当作一个原则来控制 Component 的粒度应该还是不错的