2016年10月21日 星期五

React入門筆記

必須說小編實在不喜歡React的撰寫方式跟風格,畢竟一開始吃了不少鱉。不過既然我學習過我還是簡易做個筆記吧。
Ps.本篇網誌是有段時間前寫的。

有人會問既然小編不喜歡React,但為什麼還是要學呢?一切都是因為……
網路上工作職缺打開,每個都要React & Angular開發經驗者佳,WTF!! 所以今天你只需要知道,你學會了 =  More Money!! 如何~有沒有動力了~

一言以蔽之就是:這是一個前端JS框架。OK~這句話似乎又衍生了更多問題,用在什麼地方?好不好用?優點是什麼?跟Angular比較起來差別是?為什麼我要用?

OK,為什麼我要用?大部分情況下,我們都會有專案期限的壓力,通常在這種壓力下,一般人(小編就是一般人啦)就會選用自己最擅長,最有把握的語言(工具)去完成專案,這有什麼優缺點呢優點就是專案如期完成的機率大幅提升。缺點呢井底之蛙,你會的永遠就是這些,你的速度永遠這麼慢,且你永遠不知道世界有多大,進步有多快。保哥曾在篇網誌上說過,一個工程師的價值在於它是否願意花時間去縮短他的coding時間。簡單說就是提升工作效率啦。一個網站從到尾自己刻的時代已經過去了,什麼都就講究自動化的今天,資料要越乾淨越好,工程師做的事情要越少越好,所以這些前端框架就應運而生。

比起Angular又如何呢?先說小弟我其實是支持Angular啦,特別是A2已經出到RC版的今天,差不多可以進場了,後面再講資料流的時候我會跟大家提到,不過有時專案需要,我們還是要搞懂React這傢伙在搞什麼。


接下來開始進入React的核心
React並不能算是個完整個框架,官方推出React的時候希望落實關注點分離,讓大家可以專注於View的開發,所以充其量React只是MVC中的View而已(有一點點的M),其最重要也是最強大的功能就是Render:宣染 VDOM(虛擬DOM),我敢保證,你看完說明肯定還是不懂我在說什麼,沒關係就直接進我們第一個範例吧

第一支React
1
2
3
4
5
6
7
8
9
10
11
HTML:
<div id="example"></div>

JSX:
ReactDOM.render(
    <h1>Hello, world!</h1>, //將這個物件,丟到下面這個元件之中
    document.getElementById('example') //丟到我裡面吧
);

render result:
<div id="example"><h1>Hello, world!</h1></div>
把這些寫在JS代碼中的HTML(VDOM)宣染到畫面上,這就是React最大的賣點,集中撰寫,跟宣染速度快,因為它會透過底層演算,用最小的幅度去修改畫面(不過最近被VueAngular2打臉了),附上一篇文章


React來說,很重要的一個觀念,你畫面上看到的所有東西都是元件(components)

Component props
1
2
3
4
5
6
7
8
9
10
11
12
13
var HelloMessage = React.createClass({
    render: function() {
      return <h1>Hello {this.props.name}</h1>; => 所以這邊會輸出Hello John
    }
});

ReactDOM.render(
    <HelloMessage name="John" />, =>設定這物件的name屬性為John
    document.getElementById('example')
);

開嗆時間:
angular 這樣寫就好了 <h1 ng-init="name='John'">Hello {{name}}</h1>


所以其中一個React很大的優勢就是,可以局部重購,因為可以把畫面拆成許多Components,元件中也可以包元件
JSX{}內可以正常的寫JS,若是放入array,也會批次被render出來

Array for React
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var names = ['Alice', 'Emily', 'Kate'];
ReactDOM.render(
      <div>
      {
          names.map(function (name) {
              return <div>Hello, {name}!</div>
          })
      }
      </div>,
      document.getElementById('example')
);
Demo

開嗆時間:
angular 這樣寫就好了 <div ng-repeat="name in names">{{name}}</div>



Children
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
 var Comment = React.createClass({
    render: function() {
        return ( < div className = "comment" >
            < h2 className = "commentAuthor" >
                { this.props.author}
            < /h2>
            {this.props.children} < /div>
        );
    }
});
var CommentList = React.createClass({
    render: function() {
        return (
            < div className = "commentList" >
                < Comment author = "Pete Hunt" > This is one comment < /Comment>
                < Comment author = "Jordan Walke" > This is * another * comment < /Comment >
             < /div>
        );
       }
});
ReactDOM.render(
    <CommentList />,
    document.getElementById('container')
);

/*
這裡可以用children取子節點,如果子節點是很多個物件,那取回來就會是陣列,例如這樣
</Parent>
    <ChildrenA />
    <ChildrenB />
    <ChildrenC />
</Parent>
*/



attr 共用屬性
1
2
3
4
5
6
7
8
9
10
11
var attr = {};  => 共用的屬性,不想重複打可以這樣做
attr['id'] = "a";
attr['name'] = "b";
attr['class'] = "c";

render( 
    <input {...attr} onChange={this.valueChg.bind(this,tr_index,td_index)}/>
) //event如果要綁定參數,必須用bind(this......)的方式呼叫

render result:
<input id="a" name="b" class="c" onChange={this.valueChg.bind(this,tr_index,td_index)}/>



event, refs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 var MyComponent = React.createClass({
  handleClick: function() {
    this.refs.myTextInput.focus();
  },
  render: function() {
    return (
      <div>
        <input type="text" ref="myTextInput"/> // reference參考,在這可以把它當成向指定idㄧ樣指定物件
        <input type="button" value="Focus the text input" onClick={this.handleClick} /> //onClick觸發handleClick()
      </div>
    );
  }
});

ReactDOM.render(
  <MyComponent />,
  document.getElementById('example')
);
Demo
*
若是有兩個物件ref 到相同名稱,呼叫時會呼叫到後面的物件。


state
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 var LikeButton = React.createClass({
  getInitialState: function() { =>初始化state
    return {liked: false}; => liked = false;
  },
  handleClick: function(event) {
    this.setState({liked: !this.state.liked}); => 設定liked 反向
  },
  render: function() {
    var text = this.state.liked ? 'like' : 'haven\'t liked';
    return (
      <p onClick={this.handleClick}>
        You {text} this. Click to toggle.
      </p>
    );
  }
});

ReactDOM.render(
  <LikeButton />,
  document.getElementById('example')
);



React裡面有個很重要的觀念,state改變,元件就會重新render,所以要想改變畫面上的東西,只有model改變是不夠的,看看以下範例

rerender
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 var Input = React.createClass({
    getInitialState: function() { // 設定初始state
      return {value: 'Hello!'};
    },
    handleChange: function(event) {
        this.setState({value: event.target.value}); // 若不藉此改變state畫面上的數值是不會更動的,除非是改用defaultValue,但如此便無法掌握資料了
    },
    render: function () {
        var value = this.state.value;
        return (
          <div>
            <input type="text" value={value} onChange={this.handleChange} />
            <p>{value}</p>
          </div>
        );
    }
});

ReactDOM.render(<Input/>, document.getElementById('example'));

開嗆時間:
Angular只要這樣寫
<input type="text" ng-model={value} />
<p>{{value}}</p>
this.props通常是使用在父元素傳值給子元素的情境上this.props 以及 this.state。這兩個屬性有不同的特性與行為,在使用上,官方建議靜態資料使用this.props,動態資料使用this.state


value defaultValue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
var MyComponent = React.createClass({
    getInitialState: function() {
        return {a: "a"};
    },
    handleClick1: function() {
        this.refs.myTextInput1.value="b";
        this.refs.myTextInput2.value="b";
    },
    handleClick2: function() {
        this.setState({a:"b"});
    },
    change1: function(e) {
        alert(e.target.value)
    },
    change2: function(e) {
        alert(e.target.value)
    },
    render: function() {
        return (
            <div>
                <input type="text" ref="myTextInput1" defaultValue={this.state.a} onChange={this.change1}/> 
//直沒有被綁定,可以透過onChange偵聽,也可以透過事件改變,但資料不好掌握
                <br/> // 因為必須是完整的巢狀式迴圈,所以一定要加上反斜線
                <input type="text" ref="myTextInput2" value={this.state.a} onChange={this.change2}/> 
//值被state.a綁定了,也可以透過onChange偵聽,但值不會更改,但可以被事件改變(不觸發onChange),但這樣它的優勢(資料好掌握)就沒了,一旦資料被rerender,值會再回到state.a
                <br/>
                <input type="button" value="changeA" onClick={this.handleClick1} />
                <br/>
                <input type="button" value="changeState" onClick={this.handleClick2} />
            </div>
        );
    }
});
ReactDOM.render(
    <MyComponent />,
    document.getElementById('container')
);
 Demo
關於value & defaultValue 這有編文章寫得好棒棒 傳送門



className
1
2
3
4
5
6
7
8
9
10
11
 var HelloMessage = React.createClass({
    render: function() {
      var classString = "msg";
      return <div className={classString}>Hello, I'll be there</div>; => 其實就是正常htmlclass,但class這名詞是React的專有名詞,所以不能直接使用
    }
});

ReactDOM.render(
    <HelloMessage />,
    document.getElementById('example')
);


其實React也可以做到two-way-data-binding

two-way-data-binding
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 var WithLink = React.createClass({
    /* 載入 React.addons.LinkedStateMixin 中的方法 (this.linkState)*/
    mixins: [React.addons.LinkedStateMixin], //應該是類似擴充方法
    getInitialState: function () {
        return {message: 'Hello!'}
    },
    render: function () {
        /*邏輯處理的地方*/
            return (
                <div>
                <input type='text' valueLink={this.linkState('message')} />
                {this.state.message}
                </div>

        )
    }
});
但是two-way-data-binding做完可以在render裡面做些簡單的視覺邏輯,但render裡面本來就不適合放資料處理邏輯,所以如果真的要走資料流,也不適合這種方式,這也是這個好用的功能卻一值沒沒無聞的原因。


大致上,元件執行生命週期方法的情形可分為四種
初始化,第一次 render
getDefaultProps()
getInitialState()
componentWillMount()
render()
componentDidMount()
props 發生改變時
componentWillReceiveProps(nextProps)
shouldComponentUpdate(nextProps, nextState)
componentWillUpdate(nextProps, nextState)
render() => 我試不出來
componentDidUpdate(prevProps, prevState)
state 發生改變時
shouldComponentUpdate(nextProps, nextState)
componentWillUpdate(nextProps, nextState)
render()
componentDidUpdate(prevProps, prevState)
元件 unmount 卸載時
componentWillUnmount()

來看個簡單範例

生命週期範例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
 var Hello = React.createClass({
    getInitialState: function () {
        return {
            opacity: 1.0
        };
    },

    componentDidMount: function () { => 必須在宣染完後才能開始動作
        this.timer = setInterval(function () {
            var opacity = this.state.opacity;
            opacity -= .05;
            if (opacity < 0.1) {
                opacity = 1.0;
            }
            this.setState({
                opacity: opacity => 變更透明度後重新寫入statereact會自動重新宣染該物件
            });
        }.bind(this), 100); => bind(this)JSX裡面會常常用到,但這不是JSX特有的寫法,而是JS本身的特性,告訴在這個func裡面的this是誰
    },

    render: function () {
        return (
            <div style={{opacity: this.state.opacity}}>
                Hello {this.props.name}
            </div>
        );
    }
});

ReactDOM.render(
    <Hello name="world"/>,
    document.getElementById('example')
);


Ajax
react本身是無法做到這件事的,必須透過其他框架,例如jQueryReact還有個很大的特色是,跟各家框架都幾乎完美相容,低耦合,甚至它已可以做到類似lazyLoad的效果

var RepoList = React.createClass({
  getInitialState: function() {
    return {
      loading: true,
      error: null,
      data: null
    };
  },

  componentDidMount() {
    this.props.promise.then( => 等待ajax取資料回來
      value => this.setState({loading: false, data: value}),
      error => this.setState({loading: false, error: error}));
  },

  render: function() {
    if (this.state.loading) {// 開始資料分析
      return <span>Loading...</span>;//資料Loading
    }
    else if (this.state.error !== null) {
      return <span>Error: {this.state.error.message}</span>;
    }
    else {
      var repos = this.state.data.items;
      var repoList = repos.map(function (repo, index) { // map在寫react中也常用到,作用類似forEach把陣列資料處裡回傳(angular 就是 ng-repeat)
        return (
          <li key={index}><a href={repo.html_url}>{repo.name}</a> ({repo.stargazers_count} stars) <br/> {repo.description}</li>
        );
      });
      return (
        <main>
          <h1>Most Popular JavaScript Projects in Github</h1>
          <ol>{repoList}</ol>
        </main>
      );
    }
  }
});

ReactDOM.render(
    <RepoList promise={$.getJSON('https://api.github.com/search/repositories?q=javascript&sort=stars')} />,//$.getJSONjQuery的方法
    document.getElementById('example')
);
所以網路上許多人也會用React+backbone讓它變成一個完整的MVC框架,讓它開發起來更加靈活


甚至,React還可以寫在server端,當然前提是你要用同樣是JS開發的NodeJS伺服器。這樣可以在server端直接render出畫面,並且在前端繼續開始react的生命週期。
不過感覺有點像phpecho 輸出html .netlazor輸出html一樣,前後端就又絞在一起了,成為所謂的全端程式,小編是不太喜歡這樣啦
.....下面就nodejsreact混用,沒啥好介紹的。

React in server
 var http = require('http'),
    browserify = require('browserify'),
    literalify = require('literalify'),
    React = require('react'),
    ReactDOMServer = require('react-dom/server');

var App = require('./app');

http.createServer(function(req, res) {
  if (req.url == '/') {
    res.setHeader('Content-Type', 'text/html');
    var props = {
      items: [
        'Item 0',
        'Item 1'
      ]
    };
    var html = ReactDOMServer.renderToStaticMarkup(
      <body>
        <div id="content" dangerouslySetInnerHTML={{__html:
          ReactDOMServer.renderToString(<App items={props.items}/>)
        }} />

        <script dangerouslySetInnerHTML={{__html:
        'var APP_PROPS = ' + JSON.stringify(props) + ';'
        }}/>
        <script src="//fb.me/react-0.14.0.min.js"/>
        <script src="//fb.me/react-dom-0.14.0.min.js"/>
        <script src="/bundle.js"/>
      </body>
    );
    res.end(html);

  } else if (req.url == '/bundle.js') {
    res.setHeader('Content-Type', 'text/javascript');
    browserify()
      .add('./browser.js')
      .transform(literalify.configure({
        'react': 'window.React',
        'react-dom': 'window.ReactDOM',
      }))
      .bundle()
      .pipe(res);
  } else {
    res.statusCode = 404;
    res.end();
  }
}).listen(3000, function(err) {
    if (err) throw err;
    console.log('Listening on 3000...');
})

學到這裡已經可以開始開發小型的React專案了,還可以有簡單的資料流。

=================================================  我是分隔線,休息一下,喝口水 =========================================================


WTF,簡單資料流??
你應該有發現他的資料流似乎都是父傳子,我想要兄弟相傳,親戚相傳,薪火相傳可以嗎?原則上是可以的,可以透過共同的祖先來資料源與資料處裡。
那問題來了,如果沒有共同的祖先呢?記得React的特性就是東一塊西一塊嗎?而且React當初設計本來就不是為了傳遞資料而設計,所以在資料傳遞上的寫法都會很複雜,所以如果有頻繁的資料傳遞,開發簡單的小程式你就頭大了(說好的React學習曲線平緩呢?),所以這時候官方就提出了一個設計模式Flux,看清楚我講的不是框架,不是語言,是設計模式!!
那這個Flux又是何方神聖呢?這個.......我們就留到後話再說吧!