我创建了一个非常简单的导航视图控制器,模仿iOS UINavigationController的概念。它基本上只维护路线的历史,并动画推送到另一条路线或流行音乐。
(我希望你不介意coffeescript;)
createView = (spec) -> React.createFactory(React.createClass(spec))
{div, span, input, img, button} = React.DOM
cond = (condition, result, otherwise) -> if condition then result?() else otherwise?()
Transition = React.createFactory(React.addons.CSSTransitionGroup)
NavVC = createView
displayName: 'NavVC'
mixins: [React.addons.PureRenderMixin]
propTypes:
rootScene: React.PropTypes.string.isRequired
renderScene: React.PropTypes.func.isRequired
getInitialState: ->
transition: 'navvc-appear'
stack: [@props.rootScene]
push: (route) ->
@setState
stack: React.addons.update(@state.stack, {$push: [route]})
transition: 'navvc-push'
pop: ->
if @state.stack.length is 1
console.warn("You shouldn't pop off the root view of a NavVC!")
else
@setState
stack: React.addons.update(@state.stack, {
$splice:[[@state.stack.length - 1, 1]]
})
transition: 'navvc-pop'
popFront: ->
@setState
stack: [@state.stack[0]]
transition: 'navvc-pop'
render: ->
route = @state.stack[@state.stack.length - 1]
pop = @pop if @state.stack.length > 1
popFront = @popFront if @state.stack.length > 1
div
className: 'navvc'
Transition
transitionName: @state.transition
@props.renderScene(route, @push, pop, popFront)
这很有效并且符合预期。这是一个使用NavVC递归推送和弹出的页面的简单示例。
Page = createView
displayName: 'Page'
mixins: [React.addons.PureRenderMixin]
propTypes:
title: React.PropTypes.string.isRequired
push: React.PropTypes.func.isRequired
pop: React.PropTypes.func
push: ->
@props.push(Math.random().toString(36).substr(2,100))
render: ->
div
className: 'page'
div
className: 'title'
onClick: @push
@props.title
cond @props.pop,
=> div
className: 'back'
onClick: @props.pop
"< BACK"
App = createView
renderScene: (route, push, pop, popFront) ->
Page
key: route
title: route
pop: pop
push: push
render: ->
NavVC
rootScene: 'Hello NavVC'
renderScene: @renderScene
React.render App(), document.body
此处它在JSFiddle中实施。
现在这是我的问题。当子组件正在执行推送和弹出时,这非常有用。但是如果父母或兄弟组件正在进行推送或弹出呢?
假设页面本身完全没有意识到NavVC:
Page = createView
displayName: 'Page'
mixins: [React.addons.PureRenderMixin]
propTypes:
title: React.PropTypes.string.isRequired
render: ->
div
className: 'page'
div
className: 'title'
@props.title
假设我们有一些按钮是NavVC的兄弟姐妹/父母,它们为我们推送和弹出。
App = createView
getInitialState: -> {}
renderScene: (route, push, pop, popFront) ->
@setState({push, pop})
Page
key: route
title: route
push: ->
@state.push?(Math.random().toString(36).substr(2,100))
render: ->
div
className: 'app'
NavVC
rootScene: 'Hello NavVC'
renderScene: @renderScene
cond @state.pop,
=> div
className: 'left'
onClick: @state.pop
'<'
div
className: 'right'
onClick: @push
'>'
React.render App(), document.body
这会给我们带来错误。
不变违规:setState(...):无法在现有状态转换期间更新(例如在
render
内)。渲染方法应该是道具和状态的纯函数。
当App渲染时,NavVC将调用renderScene,它将调用setState,这需要立即重新渲染。目前,我提出的唯一解决方案是将setState推迟到渲染周期之后:
window.setTimeout =>
@setState({push, pop})
, 0
您可以在以下JSFiddle中查看此示例的工作版本。
但是这种解决方案在某些非常根本的方面似乎是错误的。它不是声明性的,它每次都涉及两次渲染,它打破了单向数据流模式。我非常想在两个用例中使用相同的组件 - 孩子推送和弹出,或者父/兄弟推送和弹出,或者可能是孩子推动和父母弹出的混合。我似乎无法想出一个功能模式来实现这一点,而不会破坏一些非常基本的良好编程规则。
编辑1:
所以我认为我已朝着正确的方向迈出了一步:EventEmitters。按钮发出事件,NavVC侦听这些事件。我们可以通过App组件连接这些事件,NavVC将在mount上侦听这些事件,并在unmount上取消注册这些侦听器。这样可以保持单向数据流!
但仍然存在一个问题。 NavVC需要能够告诉后退按钮是否可以弹出,所以后退按钮知道它是否可以显示......我还没想出来。
答案 0 :(得分:0)
好的。这是一个巨大的痛苦,但我找到了一个令人满意的解决方案 - 我喜欢一些输入。
我创建了一个代理组件的概念,它适合某些空间,可以由父节点或兄弟节点控制。
App = createView
componentWillMount: ->
@PushProxy = createProxy(@renderPush)
@PopProxy = createProxy(@renderPop)
renderPush: (push) ->
div
className: 'right'
onClick: -> push(Math.random().toString(36).substr(2,100))
'>'
renderPop: (pop) ->
cond pop,
=> div
className: 'left'
onClick: pop
'<'
renderScene: (route, push, pop, popFront) ->
Page
key: route
title: route
render: ->
console.log "render app"
div
className: 'app'
@PushProxy()
@PopProxy()
NavVC
rootScene: 'Hello NavVC'
renderScene: @renderScene
pushProxy: @PushProxy
popProxy: @PopProxy
这些代理组件从其渲染函数中的其他组件呈现。
NavVC = createView
# clip
render: ->
console.log "render navvc"
route = @state.stack[@state.stack.length - 1]
pop = @pop if @state.stack.length > 1
popFront = @popFront if @state.stack.length > 1
@props.pushProxy.dispatch(@push)
@props.popProxy.dispatch(pop)
div
className: 'navvc'
Transition
transitionName: @state.transition
@props.renderScene(route, @push, pop, popFront)
这个方法仍然需要一个零的setTimeout,但它的优化不是为了渲染超过必要的,所以我觉得我没关系。
rememberLast = (f) ->
lastArg = (->)
lastResult = false
(arg) ->
if lastArg isnt arg
lastResult = f(arg)
lastArg = arg
return lastResult
defer = (f) ->
(arg) ->
window.setTimeout ->
f(arg)
, 0
createProxy = (renderComponent) ->
value = undefined
listeners = {}
listen = (f) ->
id = Math.random().toString(36).substr(2,100)
listeners[id] = f
if value isnt undefined then f(value)
{stop: -> delete listeners[id]}
dispatch = (x) ->
value = x
for id, f of listeners
f(x)
Proxy = createView
displayName: 'Proxy'
getInitialState: ->
value: undefined
componentWillMount: ->
@listener = listen (value) =>
@setState({value})
componentWillUnMount: ->
@listener.stop()
render: ->
if @state.value isnt undefined
console.log "proxy render"
renderComponent(@state.value)
else
false
Proxy.dispatch = rememberLast defer dispatch
return Proxy
查看this JSFiddle演示它的实际效果并查看日志语句,以便最佳地看到它的渲染效果。
我很想听听别人的反应,如果这是正确的想法。也许有一种更有原则的方式来做到这一点。