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 :</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);
}
}