react学习体验

自学了一下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(列表数组数据)即可,不过我这里多了typescurrentType分别表示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);
    }
}