reactJs学习心得

    reactJs最近火的不要不要的,作为一个前端工程师,不得不乘着东风去撸一把reactJs,本文会教大家如何搭建一个reactJs工程,并且做出一个提成计算的计算器。

    首先是环境搭建

    网上有很多教程,教大家如何搭建reactJs开发环境,总结起来一共有三种。

    不使用构建工具,而是在html中引入JSXTransformer.js和react.js,然后将jsx代码写在type值为text/jsx的script标签下。也就是由jsx转为js的过程在前端完成,这是最简单的运行reactJs的方法。缺点也显而易见,因为jsx转为js需要花费的时间比较长,这个时间会使前端体验极差。所以一般不使用这种方式。以下是使用这种方式的html结构。

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <script src="build/react.js"></script>
        <script src="build/JSXTransformer.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/0.3.2/marked.min.js"></script>
    </head>
    <body>
    <div id="content"></div>
    <script type="text/jsx">
          React.render(
            <h1>Hello, world!</h1>,
            document.getElementById('example')
          );
        </script>
    <!--<script src="build/helloword.js"></script>-->
    <!--<script type="text/jsx" src="src/helloword.js"></script>-->
    </body>
    </html>

    使用这种方法需要事先准备react的sdk,可以从github上获取,地址为:

    http://facebook.github.io/react

    下面来看第二种方式:

    官方建议我们将jsx转js的过程放在服务端完成,生成一个转换好的js文件,加载到html中去,这样就可以直接在前端运行reactJs代码。所以就有了第二种方式。

    首先安装node npm,这个过程请自行百度,npm是node的包管理工具,可以帮我们方便的安装环境需要用的各种模块。

    建立一个项目的根目录文件夹,并使用命令行进入该目录。

    安装react工具包 npm install -g react-tools 或者 npm install react-tools --save。 前者是全局安装,后者是目录下安装。

    建立src文件夹存放源码,建立build文件夹存放编译后的代码。

    命令行运行 jsx --watch src/ build/ 

    上面的命令会自动监听src目录下的文件,当发现有jsx文件语法时,会在build中自动生成目标文件。此时,在html中直接引入编译好的目标文件就可以了。比如上面的helloword工程。

    src下编写一个helloworld.js内容如下:

    React.render(
            <h1>Hello, world!</h1>,
            document.getElementById('example')
          );

    此时build下会生成一个helloworld.js文件,内容如下:

    React.render(
        React.createElement("h1", null, "Hello, world!"),
        document.getElementById('example')
    );

    之后在html中引入build下的js文件即可:

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <script src="build/react.js"></script>
        <!--<script src="build/JSXTransformer.js"></script>-->
        <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/0.3.2/marked.min.js"></script>
    </head>
    <body>
    呵呵
    <div id="content"></div>
    <!--<script type="text/jsx">
          React.render(
            <h1>Hello, world!</h1>,
            document.getElementById('example')
          );
        </script>-->
    <script src="build/helloword.js"></script>
    <!--<script type="text/jsx" src="src/helloword.js"></script>-->
    </body>
    </html>

    注意上面代码已经将JSXTransformer.js注释掉了,也就是转换的过程由服务器端完成了。

    第三种方式:

    使用包构建工具,这里选用官方推荐的webpack,具体的webpack是什么请百度获取,这里只介绍使用方法。

    首先还是安装node npm。

    建立一个项目的根目录,使用命令行工具进入该目录。

    建立一个modules文件夹放置js源码,src文件夹放置css和图片等文件

    运行命令 npm install -g webpack 或者 npm install webpack --save

    运行命令 npm install -g jsx-loader --save

    新建一个webpack.config.js的文件,内容如下:

    module.exports = {
        entry: './modules/main.js',
        output: {
            filename: 'bundle.js'
        },
        resolve:{
            extensions:['','.coffee','.js','.jsx']
        },
        module: {
            loaders: [
                { test: /\.jsx$/, loaders: ['jsx?harmony'] },
                { test: /\.js$/, loaders: ['jsx?harmony'] }
            ]
        }
    };

    这个配置文件是webpack运行的入口文件,上面的含义大概可以解释成这样:webpack会将entry指向的文件打包压缩到output所指向的文件中去,打包文件的扩展名包含coffeeScript文件,js文件和jsx文件,在加载js和jsx文件时使用jsx的加载器。

    webpack是一个包构建工具,支持commonJs和AMD标准,也就是说在代码编写时可以使用require关键字来加载模块,也可以使用module.exports或者define来定义模块。这种方式给我们组织代码带来方便,所以值得推崇。

    最后一步运行命令 webpack -w

    最后一步的命令 可以动态监听源码的变化,编译打包成一个bundle.js文件,最后前端只要引入bundle.js就可以了。

    值得注意的是,因为使用webpack,可以在代码中这么写:

    var React = require('react');

    这样就可以将reactJs直接打包进bundle.js中去,也就是在html中不需要再额外引入react.js了。当然你也可以不引入这个包,而在js中另外引入react.js文件。

    html代码如下:

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/0.3.2/marked.min.js"></script>
    </head>
    <body>
    呵呵
    <div id="content"></div>
    <script src="bundle.js"></script>
    </body>
    </html>

    以上工作完成,我们就可以开始开发reactJs了,我们采用第三种方式来组织代码。

    目标是完成一个提成计算器。首先来看提成计算器的计算规则。

    假设一个销售人员的提成规则如下图:

    根据上面的表格可以计算出如下表达式:

        年标转换公式:

        y = t*m/360 ;//其中t为标的时间  m 为标的投入金额  y即等效年标金额     (公式1)

        

        首投业绩提成公式:

        假设年化业绩为M元,提成 f 元 

        M < 10w 提成为0;

        10w < M < 60w f = 0.0015*M - 150

        60w < M < 110wf = 0.002*M - 450

        110w < M f = 0.0025*M - 1000

        

        (M为当月年化业绩,先判断M落入哪个区间,然后代入对应区间的公式 就可以计算提成)

        再投业绩提成公式:

        假设年化业绩为M1元,提成 f1 元 

        f1 = M1*0.0007 


    如果不使用reactJs,只用jquery也可以完成以上需求,而且很简单,我已经完成了效果可以通过下面的网址访问查看。

    http://gagalulu.wang/blog/fe/detail/9

    效果图是这个样纸的:

    下面我们要使用reactJs来完成以上需求,

    reactJs最突出的特点应该不是它的虚拟节点功能,因为对于一般的页面来说,dom节点的渲染压力还是可以承受的,毕竟没有reactJs时,浏览器大部分也是可以正常工作的。所以reactJs的亮点在于提供组件式封装的途径。让组件的切分更加细,而组件之间的组合更加灵活。所以第一步是将整体功能切分成不同的组件,然后配合起来使用。

    下面是我对这个需求的切分示意图:

    如图,三个黄色的框为一级组件,内部橘黄色的框为二级组件。接下来我们来一点点构建整个项目。

    首先分析第一个黄框,内部有五个功能相似的文本输入框。所以单一的一个文本输入框为一个组件,我们取名叫做LabelText。在modules文件夹下建立一个input文件夹,此文件夹用来存放关于输入输出的组件。labelText就放在这里,我们在此目录下新建一个labelText.js文件,内部内容如下:

    var React = require('react');
    
    
    var LabelText = React.createClass({
        getInitialState:function(){
            var item = this.props.item;
            return {value:item.value};
        },
        getData:function(){
            var value = this.state.value;
            var item = this.props.item;
            var day = item["day"];
            return {day:day,value:value};
        },
        reset:function(){
            this.setState(this.getInitialState());
        },
        render:function(){
            var item = this.props.item;
            var value = this.state.value;
            return (
                <div className="timeitem"><span className="lb">{item.label}</span><span className="lbi"><input type="text" value={value} placeholder={item.placeholder} className="money-input" day={item.day} onChange={this.handleChange}/></span></div>
                );
        },
        handleChange:function(e){
            var item = this.props.item;
            var value = e.target.value;
            console.log(item.label+"的值改变为:"+e.target.value);
            this.setState({value:value});
    //        item.value = value;
        }
    });
    
    module.exports = LabelText;

    上面的语法其实是jsx语法,请注意props和state的用法,props是只由上层组件传入该组件的初始化数据信息,一般来讲,props在组件内部是不可以修改的,这是一个隐形原则,其实是为了保存控件的初始化状态。state则用来保存组件内部可以操作变化的数据信息,原则上外部组件也是不可以直接操作组件内部的setState方法的,但是可以通过暴露内部的共有方法来达到外部组件操作内部组件数据的功能。比如上面的reset方法,就是暴露给外部组件的一个重置方法。

    大家可以查看reactJs的官方文档来理解上面的语法:

    官方文档

    这里简单的说一下,React.createClass是react提供的一个创造组件的方法,此方法内部必须有一个render方法,render方法用来返回该组件的html骨架,当然这个html骨架中也是可以嵌套其他组件的。

    其他要说的就是关于表单控件要绑定标签的onChange事件,在回调中调用setState方法,如果不使用这种方式value的值就是固定的不会改变的,这个根源在于react是使用虚拟节点映射dom节点的,但是我们对表单控件的操作却是对dom节点的操作,react的渲染只会从虚拟dom到实际dom,所以我们必须通过事件回调改变react虚拟节点的数据,否则实际dom只会和原先虚拟节点的数据保持一致。这点大家理解就好,而且关于表单控件的处理说明,官方文档上也有实例。

    值得一说的是,props和state都是相对于当前组件的。而且这两个是reactJs中相当重要的角色。reactJs组件间的通信问题依赖props。而表单控件的交互修改则依赖state。这里给出一篇说明reactJs中组件之间通信的文章,请大家自行参考。

    http://www.tuicool.com/articles/AzQzEbq

    同时要说下我自己对组件间通信的理解。按照上文的说法,reactJs对于父子之间的信息传递都依赖于props,但我却有别的想法,对于子调父,我没有意见,父组件将需要的回调函数传递给子组件,子组件需要通信时调用这些回调函数就可以完成通信。而父组件中改变子组件还使用props就必须将传递给子组件的参数放在父组件的state中,然后才可以使用setState方法将变化传递给子组件,这样就有大量的数据结构定义问题。可谓烧脑行动。而一般来讲,我们参考面向对象的编程思想,最好的通信和交互方式是方法的调用,也就是说如果父组件能够拿到子组件的对象,使用对象的方法调用,就可以完美的解决通信问题,当然reactJs是提供这样的对象引用的,使用refs这个对象来完成,这个我们下文遇到再讲。

    啰嗦了那么多,接着向下开发。下一步是将上面的组件生成五个组合成一个组件,命名为TextGroup,同样是input文件夹,我们建立一个TextGroup.js文件,内容是:

    var React = require('react');
    var LabelText = require('./labelText');
    
    
    var TextGroup = React.createClass({
        triggerReset:function(){
            var _this = this;
            this.props.data.map(function(item,index){
                _this.refs["text"+index].reset();
            });
        },
        getCurrentData:function(){
            var _this = this;
            var data = [];
            this.props.data.map(function(item,index){
                var dataItem = _this.refs["text"+index].getData();
                data.push(dataItem);
            });
            return data;
        },
        render:function(){
            var _this = this;
            var texts = this.props.data.map(function(textItem,index){
                var refvalue = "text"+index;
                return (
                    <LabelText ref={refvalue} item={textItem}/>
                    );
            });
            return (
                <div>{texts}</div>
                )
        }
    });
    module.exports = TextGroup;

    注意看代码中组件LabelText的引入方式,使用require方法,同时注意,triggerReset方法和getCurrentData方法,triggerReset方法是用来遍历内部的labelText组件并将其全部reset,执行后相当于整个组件重置。getCurrentData则是返回所有labelText的特征数据值,当时是约定好的数据格式,这两个方法都是组件暴露给外部的方法。

    render方法则是根据传入的数据,组装labelText。到此第一个黄框的组件就做好了。

    同理第二个黄框组件是一个按钮组,内部有两个外观一样的按钮,所以可以将单个按钮封装成组件。

    在modules文件夹下建立一个button文件夹,用来存放和按钮相关的组件,单个按钮组件我们命名为SimpleBtn保存在simpleBtn.js中,组合后的组件保存在btnGroup.js中,代码如下:

    simpleBtn.js

    var React = require('react');
    var SimpleBtn= React.createClass({
        handleClick:function(e){
            console.log(this.props.btnName+"被点击了");
            this.props.onclick(e);
        },
        render:function(){
            return (
                <div className="btn" onClick={this.handleClick}>
                    {this.props.btnName}
                </div>
                )
        }
    });
    
    //console.log(SimpleBtn);
    module.exports = SimpleBtn;

    btnGroup.js

    var React = require('react');
    var SimpleBtn = require('./simplebtn.js');
    
    var BtnGroup = React.createClass({
        render:function(){
            var btns = this.props.data.map(function (btnItem) {
                var html;
                switch (btnItem.type){
                    case "simple":
                        console.log(btnItem.click)
                        html = <SimpleBtn onclick={btnItem.click} btnName={btnItem.btnName}/>
                        break;
                    case "big":
                        html = <div>{btnItem.btnName}</div>
                        break;
                }
                return (
                    html
                    );
            });
            return (
                <div className="btn-group">
                {btns}
                </div>
                );
        }
    });
    
    module.exports = BtnGroup;

    reactJs的事件绑定都是直接绑在组件标签上的,当然这里说的不是实际的dom节点,react帮我们将组件标签上的事件回调使用事件委托的方式绑在根节点上。所以生成的html上是看不到类似onclick这样的attribute的。

    最后一个框就是一个简单的文本显示,命名为SimpleText保存在simpleText.js中代码如下:

    var React = require('react');
    var SimpleText = React.createClass({
    //    getInitialState:function(){
    //        return {value:this.props.value}
    //    },
    //    setValue:function(){
    //
    //    },
        render:function(){
            var value = this.props.value;
            return (
                <span className="result-text">{value}</span>
                );
        }
    });
    
    module.exports = SimpleText;

    是一个直接显示的控件,不多说了。

    所有的组件准备好了,现在就是要把他们组合在一起,这些逻辑就编写在入口main.js中,代码如下:

    var React = require('react');
    //var Event = require('./event/event');
    
    var BtnGroup = require('./button/btnGroup');
    
    var TextGroup = require('./input/TextGroup');
    
    var SimpleText = require('./input/simpleText');
    
    
    var data = [
        {
            label:"45天业绩",
            placeholder:"请输入45天业绩",
            day:45
        },
        {
            label:"90天业绩",
            placeholder:"请输入90天业绩",
            day:90
        },
        {
            label:"180天业绩",
            placeholder:"请输入180天业绩",
            day:180
        },
        {
            label:"360天业绩",
            placeholder:"请输入360天业绩",
            day:360
        },
        {
            label:"复投业绩",
            placeholder:"请输入复投业绩",
            day:-1
        }
    ];
    
    var texts = <TextGroup data={data}/>
    var textsReactDom = React.render(texts, document.getElementById('timeTransform'));
    
    function calresultfirst(currentData){
        var dataItem = currentData[0];
        var total = 0;
        for(var i=0;dataItem;dataItem=currentData[++i]){
            var moneyStr = dataItem["value"];
            if(!moneyStr||moneyStr=="")moneyStr=0;
            var money = parseFloat(moneyStr);
            var day = parseInt(dataItem["day"]);
            if(day>0)
                total += money/360*day;
        }
    
        if(total<100000){
            return 0;
        }
        if(total<600000){
            return 0.0015*total- 150;
        }
        if(total<1100000){
            return 0.002*total - 450;
        }
        return 0.0025*total - 1000;
    }
    
    function calresultScecond(currentData){
        var dataItem = currentData[0];
        for(var i=0;dataItem;dataItem=currentData[++i]){
            var moneyStr = dataItem["value"];
            if(!moneyStr||moneyStr=="")moneyStr=0;
            var money = parseFloat(moneyStr);
            var day = parseInt(dataItem["day"]);
            if(day<0)
                return money * 0.0007;
        }
        return 0;
    }
    
    function calresult(currentData){
        var t1 = calresultfirst(currentData);
        var t2 = calresultScecond(currentData);
        console.log(t1);
        console.log(t2);
        return t1+t2;
    }
    
    var btnData = [
        {
            click:function(e){
                var currentData = textsReactDom.getCurrentData();
                var money = calresult(currentData);
                console.log(money);
                resultReactDom.setProps({value:money});
                console.log(currentData)
            },
            type:"simple",
            btnName:"开始计算"
        },
        {
            click:function(){
    //            Event.trigger("needReset");
                textsReactDom.triggerReset();
    
            },
            type:"simple",
            btnName:"重置"
        }
    ];
    var btns = <BtnGroup data={btnData}/>
    React.render(btns, document.getElementById('btnGroup'));
    
    var resultDom = <SimpleText />
    var resultReactDom = React.render(resultDom,document.getElementById('resultText'));

    请注意上面代码中业务逻辑和渲染逻辑的区别,还有各个组件的通信方式,上文中提到了,都是利用refs这个对象获取到组件对象,进而调用组件方法完成的。

    再有就是html代码:

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <title>ReactDemo测试页面</title>
        <link rel="stylesheet" href="src/css/main.css"/>
    </head>
    <body>
    <div>
        提成计算器
    </div>
    <div>
        <div id="timeTransform">
    
        </div>
        <div>以上没有项目不填(单位:元)</div>
        <div id="btnGroup">
    
        </div>
        <!--<div>
            <div id="cal">
                开始计算
            </div>
            <div id="reset">
                重置
            </div>
        </div>-->
    
        <div id="result" style="clear: both;">
            <span>应得提成:</span>
            <div id="resultText"></div>
            <!--<span></span>-->
        </div>
    </div>
        <script src="bundle.js"></script>
    </body>
    </html>

    这里引入一个main.css用来控制页面的样式,也可以将样式绑定按照组件进行切分,并绑定在组件模块内。这个大家自己探讨。这个demo的github地址:

    git@github.com:gagaprince/reactDemo.git

    大家可以clone下来看下~

    下面是我对reactJs的一些初学者心得。

    首先值得一说的是,最近reactJs的火爆程度不亚于当时吹捧html5的感觉。但是reactJs是否能真正的取代其他类似MVC MVVM类的框架而一统前端还是一个未知数,前端这几年的额发展日新月异,新技术新框架应接不暇,这其中不乏在一些领域浅尝辄止的感觉。前端工程师的价码越来越高,也和这些前端技术的发展有关。但是过分吹捧前端无所不能是严重不可取的。也不要妄想nodejs会取代java作为服务器语言的地位。不要妄想react能取代java或者objectiveC在客户端上开发的地位。所谓术业有专攻,每种语言都有它存在的必要性和长短之处,我们作为软件工程师,就是要组合这些语言,扬长避短。任何鼓吹一种框架或者语言打压另一种框架和语言的做法都是不可取的。

    还有react的最大的特点应该是组件化的切分和组合变的更加规范化,很容易将组件切分成更细的粒度结构。这点显然是给组件化带来了福音。但是开发中我发现,react组件间的沟通成本有点大,数据结构是自定义的,对于粒度很低的组件,我们很难一次将其搞的很完美,需要长期的更新迭代,所以一个react组件库的出现势在必行。

    另外,reactJs张扬了组合模式,却丢掉了继承模式,作为一个长期面向对象编程的程序员,对这种方案深表遗憾,组合模式有他的好处,但是继承的代码重用能力也同样值得关注。比如cocos2d-js这个框架,自上而下的使用继承模式,开发起来非常爽,就是因为它将很多共性的功能都封装到顶层类中,在我看来组合和继承两种模式一点都不冲突,组合带来了灵活性,继承却可以重用代码。两者互补。

    reactJs的性能优势也是被工程师津津乐道的特点,使用虚拟dom做diff,来更新渲染dom树,的确可以增加浏览器的渲染速度。这是一个亮点,可未来相信浏览器的更新迭代也会使渲染的效率越来越高。但开发react的工程师能从这个角度想问题,还是值得敬佩的~!

    reactJs还处在快速更新迭代的过程中,未来肯定会变的越来越好,一个好的工程师一定要不断的接收新的知识,包括框架语言,但一定要有自己的判断,不可以人云亦云,我认为reactJs的发展空间有限,会带来一些变化,但预感并不会是一个前端的革命。

    当然这是我一家之言,希望和大家一起学习进步。

    转载请注明出处:http://gagalulu.wang/blog/detail/23 您的支持是我最大的动力!