react学习体验
03 Aug 2016
自学了一下react
,这次以做一个简易的链接收藏增删改查操作为目的推动。
当然少不了,先准备好react
的环境以及项目代码布局
react
采用1.15.x
编译自然要用到babel
了,不想直接用浏览器解析。
项目代码简单布局为:
-react
–src jsx文件目录
–build 放解析后的业务js文件
–lib 放react的库文件:react\react-dom
index.html
index.html为入口页面,引入react、react-dom(最基本需求)
其次,引入业务文件(build目录下的)
这样的话,我们只要用babel编译src目录的文件直接部署到build即可。
babel
先说一下babel,安装全局npm install -g babel-cli
主要是为了用babel执行指令
然后在项目目录路径下: babel src -d build
等等,报错啦,哦,jsx语法得装babel-preset-react
npm install babel-preset-react
等等,还是报错啊,当然还要在目录里准备一个.babelrc
的配置文件,用于告知babel
这里的执行要用到的配置插件
.babelrc
内容如下:
{
" presets " :[ " es2015 " , " react " ]
}
多了个es2015
用来编译es6语法的,记得也install一下.
webpack
现在react项目基本都用上webpack了,直接用webpack来做打包等处理,更方便。
需要先安装webpack
npm install webpack -g
解析babel需要安装babel-loader
webpack配置文件如下:
module . exports = {
entry : {
index : ' ./src/index.jsx ' ,
demo : ' ./src/demo.jsx '
},
module :{
loaders : [{
test : / \. css$/ ,
//loader:'style!css'
loader : ExtractTextPlugin . extract ( " style-loader " , " css-loader " )
},
{ //jsx的编译配置
test : / \. jsx ? $/ ,
loader : ' babel ' ,
query :{
presets :[ ' es2015 ' , ' react ' ]
}
}
]
},
output : {
filename : ' build/[name].js '
}
};
业务组件规划
react是ui组件,所以每一块ui都做成组件的形式
React . creatClass ({
...
})
(其实,我都是一个底层一个底层组件的写,慢慢写到父组件的,感觉这一种形式有点难以动手哦 ,有点像以前写java业务的时候,先考虑数据库,再考虑dao,然后service,然后action,往往这个思考的时间占据很多。)
应用的业务,大概就是一个添加表单、一个列表(列表带删除)
先写列表的项目,命为Item
var Item = React . createClass ({
render : function (){
return (
< p className = " item " >< a href = { this . props . link } > { this . props . children } < /a >
< a className = ' close ' href = " # " data - id = { this . props . dataId } onClick = { this . props . clickHandle } > 删除 < /a></ p >
);
}
});
最简单的组件的话 ,可能仅仅只有render一个方法了。
Item主要是展示url链接和描述,这里通过 this.props.xxx 来获取组建交互的值(父组件通过写子组件标签的时候带入的属性xxx传递来数据)。
同样也可以定义响应事件通过props的方式传入,实际上的调用,则是反馈到父组件代入的具体执行事件函数去。
比如这里Item 在render的时候,href用的是父组件传入的link属性值,children则是父组件调用子组件以起始标签下包裹的子节点内容,这里子节点仅仅只是一个文本节点,点击事件则写为
onClick = {this.props.clickHandle}
(即子组件触发点击时,调用的是父组传入给子组件的属性clickHandle)
那父组件那里怎么用的呢?来看List组件
var List = React . createClass ({
render : function (){
var dataTmp = this . props . data . map (( e , i ) => {
return ( < Item link = { e . link } key = { e . id } dataId = { e . id } type = { e . type } clickHandle = { this . props . clickHandle } >
{ e . desc }
< /Item> ) ;
});
return ( < div className = " list " > { dataTmp } < /div> ) ;
}
});
按照对Item的分析,List接受来自父组件传递的props.data, 以及 clickHandle(这个并不指的同一个,不过在这里,实际是一个多层组件父子传递,所以定成同一个名字而已);
这里多了一个数组的处理,props.data实际是一个对象数组,每个元素代表一个item(Item组件需要的数据单元);因此在List render的时候,先对data进行了数组遍历,组成一个个Item;最后再return;
需要注意的是,react 提示数组必须有一个唯一的key属性(应该是类似主键的标示),虽然是一个warning
,但刚开始没加的时候 ,我的click事件总是不触发。这里遍历数组,可以直接拿数组索引值做为key,不过我因为数据里有id,索性就用id里(key不能在子组件里通过props.key来获得)
另外,在遍历数组时注意用this的问题,如果map里面的function采用的不是es6写法,得先做一下self=this之类的转换,不然this引用就有问题了,用箭头函数省去了这一步。
到了这里,我们似乎已经得到相对纯净的List列表UI了,但还少了数据处理等等,我觉得数据处理等等,应该放到越往上层越好。所以我写了一个CustomList组件(该组件主要处理数据并传递变量给List)
先贴一下代码:
var CustomList = React . createClass ({
getInitialState : function (){
//console.log(gData.getTypeList());
return {
data : gData . getByType ( this . props . type ),
types : gData . getTypeList (),
currentType : this . props . type
}
},
deleteItem : function ( e ){
e . preventDefault ();
gData . dataAction ( ' delete ' , e . target . getAttribute ( ' data-id ' ));
if ( gData . getByType ( this . state . currentType ). length == 0 ){
this . setState ({
data : gData . getByType ( '' ),
types : gData . getTypeList (),
currentType : ''
});
return ;
}
this . setState ({
data : gData . getByType ( this . state . currentType ),
types : this . state . types ,
currentType : this . state . currentType
});
},
changeType : function ( e ){
e . preventDefault ();
var type = e . target . getAttribute ( ' data-type ' );
this . setState ({
data : gData . getByType ( type ),
types : this . state . types ,
currentType : type
});
},
componentWillReceiveProps : function ( nextProps ){
var type = nextProps . type ;
this . setState ({
data : gData . getByType ( type ),
types : gData . getTypeList (),
currentType : type
});
},
render : function (){
return ( < div className = " all-list " >
< div className = " list-type " > {
this . state . types . map (( type , i ) => {
if ( type == this . state . currentType ){
return ( < a className = " type-item active " key = { i } data - type = { type } onClick = { this . changeType } > { type } < /a> ) ;
}
return ( < a className = " type-item " key = { i } data - type = { type } onClick = { this . changeType } > { type } < /a> ) ;
})
} < /div >
< List data = { this . state . data } clickHandle = { this . deleteItem } / >
< /div> ) ;
}
});
组件内是通过修改state来重新渲染自己的,所以,我们可以通过setState
的方式来合并修改state
,然后根据新的state
重新渲染界面(包括子组件)。
想想,刚开始列表应该是要有数据的,那么我们就要在getInitialState
方法内return
我们要放的数据,简单的话应该只要一个data(列表数组数据)即可,不过我这里多了types
跟currentType
分别表示url类型数组以及当前url类型,这个后续会提到。
有了state
初始值,下一步应该是先render
了,当然是要调用List组件并给它传递要传的值了。
< List data = { this . state . data } clickHandle = { this . deleteItem } />
data自然传的是state
的data值,这里clickHandle
不再是赋props
值了,而且直接给的CustomList的deleteItem
方法(也就是再经由List传给Item的触发点击的方法)
从Item的render DOM 可以看到,当我们点击Item的“删除”时,最终触发的是CustomList这里定义的deleteItem方法,我们看到方法参数也带上了e 即event,我们获取e.target
得到当前触发的dom,并得到要删除的数据的id,并调用数据操作对象的删除方法来将数据删除。删除掉部分数据之后当然我们就是要改变state
的data来通知自己改变并传递给子组件让他们也有相应的改变。
做到删除操作,似乎也已经完成了List的任务了, 不过我这里多加了个内容,就是显示现有url的所有类型,并通过点击类型来展示List显示对应类型的url数据。这里就不做描述了,比较简单,而这个url类型数组数据就是一开始给state
赋值时传的types
, 当前点击的类型则是currentType;
做完List,该说说Form
组件了。
按我们说的List的思路,其实Form
更简单了,因为他并不需要底层组件了,仅仅只是一个表单。
var Form = React . createClass ({
getInitialState : function (){
return {
showNew : false ,
link : '' ,
newType : '' ,
desc : ''
};
},
addUrl : function ( e ){
var obj = {},
newType = document . getElementById ( ' new_type_input ' ). value ;
obj . link = document . getElementById ( ' url_input ' ). value ;
obj . desc = document . getElementById ( ' desc_input ' ). value ;
obj . type = document . getElementById ( ' type_input ' ). value ;
if ( obj . link == '' ){
alert ( ' 链接不能为空 ' );
return ;
}
if ( obj . link . indexOf ( ' http:// ' ) ==- 1 || obj . link . indexOf ( ' https:// ' )){
obj . link = ' http:// ' + obj . link ;
}
if ( obj . desc == '' ){
alert ( ' 描述不能为空 ' );
return ;
}
if ( obj . type == '' ){
alert ( ' 类型不能为空 ' );
return ;
}
if ( obj . type == ' add-new ' ){
if ( newType == '' ){
alert ( ' 新类型不能为空 ' );
return ;
} else if ( this . props . typeList . indexOf ( newType ) !=- 1 ){
alert ( " 已存在类型! " );
return ;
}
obj . type = newType ;
}
gData . dataAction ( ' add ' , obj );
this . props . addForm && this . props . addForm ();
this . setState ({
showNew : false ,
link : '' ,
newType : '' ,
desc : ''
});
},
addNewType : function ( e ){
var selectedType = e . target . value ;
if ( ! this . state . showNew ){
if ( selectedType == ' add-new ' ){
this . setState ({
showNew : true
});
}
} else if ( selectedType !== ' add-new ' ){
this . setState ({
showNew : false
});
}
},
handleUrlChange : function ( e ){
this . setState ({ link : e . target . value });
},
handleDescChange : function ( e ){
this . setState ({ desc : e . target . value });
},
handleNewTypeChange : function ( e ){
this . setState ({ newType : e . target . value });
},
render : function (){
return ( < div className = " add-form " >
< div className = " form-line " >< label htmlFor = " url_input " > URL & ensp ;: < /label><input id="url_input" type="text" value={this.state.link} onChange={this.handleUrlChange} placeholder="请输入url"/ >< /div >
< div className = " form-line " >< label htmlFor = " desc_input " > 描述 : < /label><input id="desc_input" type="text" value={this.state.desc} onChange={this.handleDescChange} placeholder="请输入描述"/ >< /div >
< div className = " form-line " >< div className = " styled-select blue semi-square " >< select defaultValue = { this . props . typeList . length ? this . props . typeList [ 0 ]: '' } id = " type_input " onChange = { this . addNewType } placeholder = " 请选择类型 " >
< option key = " 0 " value = " add-new " > 新增其他 < /option >
{ this . props . typeList . map (( e , i ) => {
if ( i == 0 ){
return ( < option key = { i + 1 } value = { e } > { e } < /option> ) ;
}
return ( < option key = { i + 1 } value = { e } > { e } < /option> ) ;
})}
< /select></ div >< /div >
< div className = { this . state . showNew ? ' form-line ' : ' form-line hidden ' } >< label htmlFor = " new_type_input " > 类型 : < /label><input id="new_type_input" value={this.state.newType} onChange={this.handleNewTypeChange} type="text" placeholder="请输入新类型"/ >< /div >
< button className = " btn " onClick = { this . addUrl } > 提交 < /button >
< /div> ) ;
}
});
表单用的input
,如果设置了value=""
则不接受输入了,改为defaultValue
才行,不过这里加了onChange
事件,并通过setState的方式来做接收输入(将input
的值存入state
)
做完List跟Form组件,代码上还有一些多的没提到的操作,实际上,那是Form跟List组件之间的交互,我们知道state
是组件内部的交互数据,props
是父子组件之间的交互,那没有父子关系的组件之间该怎么交互呢?
其实,我们可以设定一个父组件,来调用这两者,从而达到通过父组件来代为传递交互。
父组件通过props
来向子组件传递数据,又通过props
给子组件定义触发事件响应。
var UrlBox = React . createClass ({
getInitialState : function (){
return {
type : ""
};
},
refreshForm : function (){
this . setState ({
type : ""
});
},
render : function (){
return ( < div className = " url-box " >
< Form addForm = { this . refreshForm } typeList = { gData . getTypeList ()} / >
< CustomList type = { this . state . type } / >
< /div> ) ;
}
});
这里的UrlBox给Form
赋值addForm
属性做回调,当Form组件提交完数据后,调用addForm
,UrlBox再setState
,设置type,type
传递给力CustomList,从而做到组件之间的交互。
说罢,我们看回CustomList的render
发现,我们写的jsx标签并没用到props
,而仅仅只是用了state
,因为CustomList原本实现的逻辑是就是用state来做数据更新响应的,这时我们就要了解一下react的一个生命周期机制 了.
挂载:getInitialState,componentWillMount,componentDidMount
更新:commponentWillReceiveProps,shouldComponentUpdate,componentWillUpdate,componentDidUpdate
移除:componentWill
react
组件有一个componentWillReceiveProps
方法,顾名思义,这是组件接受新的props时要调用的方法,而参数nextProps
则为新的props
,我们运用这个方法,来setState
,进而得到通知render
响应。
综上,就是我第二次使用react的一个理解。
另外,这里数据的处理,我用的是本地存储,未涉及到异步接口,这也是为方便编写react demo。具体的内容如下:
var gData = {
data :[{
id : 1 ,
type : ' search ' ,
link : ' http://baidu.com ' ,
desc : ' 百度 '
},{
id : 2 ,
type : ' search ' ,
link : ' http://google.com ' ,
desc : ' google '
},{
id : 3 ,
type : ' sns ' ,
link : ' http://facebook.com ' ,
desc : ' facebook '
},{
id : 4 ,
type : ' sns ' ,
link : ' http://weibo.com ' ,
desc : ' 微博 '
},{
id : 5 ,
type : ' infos ' ,
link : ' http://qq.com ' ,
desc : ' QQ '
},{
id : 6 ,
type : ' infos ' ,
link : ' http://sina.com ' ,
desc : ' 渣浪 '
},{
id : 7 ,
type : ' infos ' ,
link : ' http://yahoo.com ' ,
desc : ' 雅虎 '
},{
id : 8 ,
type : ' knowledge ' ,
link : ' http://sf.gg ' ,
desc : ' sf '
},{
id : 9 ,
type : ' knowledge ' ,
link : ' http://zhihu.com ' ,
desc : ' 知乎 '
}],
getTypeList : function (){
var types = [];
this . data . forEach (( e , i ) => {
if ( types . indexOf ( e . type ) ==- 1 ){
types . push ( e . type );
}
});
return types ;
},
getByType : function ( type ){
if ( type == undefined || type == "" ){
return this . data ;
}
return this . data . filter ( function ( e ){
return type == e . type ;
});
},
add : function ( obj ){
var descData = this . data . slice ( 0 ). sort ( function ( a , b ){ return b . id - a . id ;});
//console.log(descData);
if ( descData . length == 0 ){
descData [ 0 ] = {
id : 0
};
}
obj . id = descData [ 0 ]. id + 1 ;
this . data . push ( obj );
},
delete : function ( id ){
var index = - 1 ;
this . data . forEach ( function ( e , i ){
if ( e . id == id ){
index = i ;
}
});
if ( index ==- 1 ){
return ;
}
this . data . splice ( index , 1 );
},
dataAction : function ( fnType ){
var arg = []. slice . call ( arguments , 1 );
try {
switch ( fnType ){
case ' add ' :
this . add ( arg [ 0 ]);
break ;
case ' delete ' :
this . delete ( arg [ 0 ]);
break ;
default :
return ;
}
} catch ( e ){
return ;
}
this . refresh ();
},
refresh : function (){
if ( window . localStorage ){
localStorage . setItem ( ' urls ' , JSON . stringify ( this . data ));
}
}
};
if ( window . localStorage ){
var store = localStorage . getItem ( ' urls ' );
try {
store = JSON . parse ( store );
if ( store && store . length ){
gData . data = store ;
}
} catch ( err ){
console . log ( err );
}
}