如何在前端编码时实现人肉双向编译
React+flux是目前最火的前端解决方案之一,但flux槽点颇多,例如store比较混乱,使用比较繁琐等,于是出现了很多第三方的基于flux优化的架构。
有人统计了目前主流的flux实现方案,感兴趣的可以看这里:Which Flux implementation should I use?
其中redux
是目前github
上star
最多的一个方案,该方案完全独立于react
,意味着这套理念可以作为架构层应用于其他的组件化方案。同时官方也提供了react-redux
库,帮助开发者直接使用react+redux
快速开发。
个人理解它的主要特性体现在以下几点:
- 强制使用一个全局的
store
,store
只提供了几个简单的api(实际上应该是4个),如subscribe
/dispatch
(订阅、发布),getState
,replaceReducer
。 store
负责维护一个唯一的叫做state
树的对象,其中state
存储了应用需要用到的所有数据。store
和顶层组件使用connect
方法绑定,并赋给props
一个dispatch
方法,可以直接在组件内部this.props.dispatch(action)
。 简单一点说,就是去掉了flux
中组件和store
的unbind/bind
环节。当state
变化时,自动更新components
,不需要手动操作。- 提供了
applyMiddleware
方法用于异步的action
,并且提供了加入中间件的能力,例如打印日志追踪应用的所有状态变化。 - 对全局的数据
state
的操作,由多个reducer
完成。每个reducer
都是一个纯函数,接收两个参数state
和action
,返回处理后的state
。这点类似管道的操作。
接下来我们可以回答标题的问题了,即:如何在前端编码时实现人肉双向编(zi)译(can)。
其实就是使用coffee来编写react+redux应用。
我们来写个简单的hello world玩玩。
view部分
这部分和redux/flux无关,纯粹react的实现,使用jsx的话,render部分的代码大概长这样:
- render:function(){
- return (
- <div>
- <div class="timer">定时器:{interval}</div>
- <div>{title}</div>
- <input ref="input"><button>click it.</button>
- </div>
- )
- }
那如何使用coffee写这段代码呢? 我们需要先将jsx编译这类似这样的js代码,请注意是用大脑编译:
- render:function(){
- return React.createElement('div',null,
- React.createElement('div',{className:'timer'},'定时器'+this.props.interval),
- React.createElement('div',null,this.props.title),
- React.createElement('input',{ref:'input'}),
- React.createElement('button',null,'click it.')
- );
- }
然后将js代码逆向编译为coffee。
这里我们可以用$
代替React.createElement
简化代码(终于可以用jQuery的坑位了),得益于coffee
的语法,借助React.DOM
可以用一种更简单的方式实现:
{div,input,button,span,h1,h2,h3} = React.DOM
这里就不单独放render部分,直接看完整代码:
- {Component,PropTypes} = React = require 'react'
- $ = React.createElement
- {div,input,button} = React.DOM
- class App extends Component
- clickHandle:->
- dom = this.refs.input.getDOMNode()
- this.props.actions.change(dom.value)
- dom.value = ''
- render:->
- {title,interval} = this.props
- div className:'timer',
- div null,'定时器:' + interval
- div null,title
- input ref:'input'
- button onClick:@clickHandle.bind(this),'click it.'
- App.propTypes =
- title: PropTypes.string
- actions: PropTypes.object
- interval: PropTypes.number
- module.exports = App
如果你能看到并看懂这段coffee,并在大脑里自动编译成js代码再到jsx代码,恭喜你。
连接store
这个环节的作用,主要是实现view层和store层的绑定,当store数据变化时,可自动更新view。
这里需要使用redux提供的createStore方法创建一个store,该方法接受2个参数,reducer和初始的state(应用初始数据)。
store.coffee的代码如下:
- {createStore} = require 'redux'
- reducers = require './reducers' # reducer
- state = require './state' # 应用初始数据
- module.exports = createStore reducers,state
然后我们在应用的入口将store和App绑定,这里使用了redux官方提供的react-redux库。
- {Provider,connect} = require 'react-redux'
- store = require './store'
- $ = React.createElement
- mapState = (state)->
- state
- rootComponent = $ Provider,store:store,->
- $ connect(mapState)(App)
- React.render rootComponent,document.body
可能有人会问,mapState和Provider是什么鬼?
mapState提供了一个类似选择器的效果,当一个应用很庞大时,可以选择将state
的某一部分数据连接到该组件。我们这里用不着,直接返回state
自身。
Provider是一个特殊处理过的react component,官方文档是这样描述的:
- This makes our store instance available to the components below.
- (Internally, this is done via React undocumented “context” feature,
- but it’s not exposed directly in the API so don’t worry about it.)
所以,放心的用就好了。
connect方法用于连接state和App,之后即可在App组件内部使用this.props.dispatch()方法了。
添加action和reducer
最后我们添加一个按钮点击的事件和定时器,用于触发action,并编写对应的reducer处理数据。
在前面的App
内部已经添加了this.props.actions.change(dom.value)
,这里看下action.coffee的代码:
- module.exports =
- change:(title)->
- type:'change'
- title: title
- timer:(interval)->
- type:'timer'
- interval:interval
再看reducer.coffee
- module.exports = (state,action)->
- switch action.type
- when 'change'
- Object.assign {},state,title:'hello ' + action.title
- when 'timer'
- Object.assign {},state,interval:action.interval
- else
- state
至此,代码写完了。
一些其他的东西
这里只介绍一个中间件的思想,其他的特性例如异步action,或者dispatch一个promise等原理基本类似:
- dispatch = store.dispatch
- store.dispatch = (action)->
- console.log action # 打印每一次action
- dispatch.apply store,arguments
作者:yisbug
来源:51CTO