近來常使用 React 開發前端,但無奈前端的開發工具五花八門,比較熟悉後端技術的開發者應該會相當不習慣,因此本文記錄我使用 React 開發前端的演進過程。

本文會從一個簡單而且不需要使用任何前端工具的 React 範例開始,然後慢慢地一個個地導入現今前端常用的開發工具(例如 Webpack, Babel) 等等,至於其他較深入的 React 功能則不多加贅述,希望可以讓其他與我相同不懂前端技術的朋友,在開始前端生涯之前有個好文章能夠參考。

本文環境

  • Chrome 81
  • React 16.13.1

p.s. React 有分 React JS 與 React Native, 本文皆是 React JS

React

React 是為了讓前端開發者能夠更方便、更有效率地開發使用者介面(User Interface, UI)而誕生的 Javascript 函式庫。

React 中最核心的概念是元件(Components),或稱組件。

前端開發者可以透過 React 將各種複雜的 UI 元件切成一個又一個小巧又獨立的組件,因此開發者可以視需求像疊樂高積木一樣,挑選組件進行組合。

例如登入按鈕可以是個組件,因此有需要登入按鈕的頁面,都可以將該組件放到頁面中即可,除了維持 UI 的一致性之外,也減少前端開發者一直開發重複的介面所造成的開發成本浪費。

但是,有些組件可能有狀態的需求,例如勾選欄(Checkbox)組件就得紀錄是否處於被勾選的狀態,因此 React 第 2 個核心概念為狀態(state),有了狀態之後就能讓組件能夠真正獨立運作。

男子漢的 React 開發法

Hello World

不需要任何前端開發工具的方法,就是直接在網頁上嵌入 CDN(Content Delivery Network) 上的 React.js 例如:

<html>
  <title>React 範例</title>
  <script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
  <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
  <body>
    <!-- 以下稍後實作 -->
  </bod>
</html>

有基礎樣板之後, Hello World 是少不了的。

首先,我們先增加一個名為 app 的空 div ,做為 React 的渲染(render)區域,可以想像 React 會在該 div 區塊內自動為我們放上組件。

<html>
  <title>React 範例</title>
  <script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
  <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
  <body>
    <div id="app">
    </div>
    <!-- 以下稍後實作 -->
  </bod>
</html>

設定好渲染區域之後,接著就能夠開發 HelloWorld 組件,這個組件十分單純就只有負責顯示一段文字,沒有任何狀態。

為了方便,我們直接在 app div 下方用 <script src="app.js"></script> 將組件放在 app.js 供載入。

<html>
  <title>React 範例</title>
  <script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
  <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
  <body>
    <div id="app">
    </div>
    <script src="app.js"></script>
  </bod>
</html>

app.js 的內容如下:

const e = React.createElement;

class HelloWorld extends React.Component {
  render() {
    return e('h1', {}, 'Hello World');
  }
}

const domContainer = document.querySelector('#app');
ReactDOM.render(e(HelloWorld), domContainer);

上述程式第 1 行先將 React.createElement 函式指定給 e ,可以讓我們後續少打很多字。

React.createElement 作用是建立一個 React.element , React 中所指的 element 與 component 是有差別的。

簡而言之 element 是 React 應用中最小的組成單位,element 指的是你在畫面上想要看到東西,譬如一串文字可以是一個 element ,而組件被渲染到畫面上時也是以 element 的形式呈現(也可以說 element 由組件組成)。

Elements are the smallest building blocks of React apps.

而且 element 是 Immutable 的,一旦建立好一個 element ,我們就無法改變該 element 的屬性,唯一改變它的方法就是另外建立一個新的 element 再交由 React 負責更新。

Once you create an element, you can’t change its children or attributes.

第 3 行開始,我們運用 ES6 的 class 語法讓 HelloWorld 繼承 React.Component 以創造 1 個組件(Component) 。

接著實作 React 組件中專門用來進行渲染的方法 render() ,並在該方法回傳一個很單純的 <h1>Hello World</h1> element 。

最後再透過 ReactDOM.render(e(HelloWorld), domContainer) 告訴 React 我們想將 HelloWorld 渲染於 <div id="app"></div> 內。

執行成功的話,可以在頁面上看到 Hello World 文字,如此就完成第 1 個 React 範例啦!

男子漢的 React Props & State

Props

男子漢的腳步不停歇!緊接著來到 React 一定要懂的 Props 以及 State.

既然是透過 React 開發組件,那麼組件肯定少不了像函式一樣可以放參數的功能,而這個功能就是 Props ,譬如我們想為 Hello World 文字後增加顯示一個 name ,這個 name 必須是組件參數,那麼該怎麼做?如下所示:

const e = React.createElement;

class HelloWorld extends React.Component {
  render() {
    return e('h1', {}, 'Hello World,' + this.props.name);
  }
}

const domContainer = document.querySelector('#app');
ReactDOM.render(e(HelloWorld, {name: 'React'}), domContainer);

上述更改後的範例可以看到 return e('h1', {}, 'Hello World,' + this.props.name); 多了 this.props.name 的部分,代表該組件可以接受 1 個名稱為 name 的 Props 參數,如果想接受其他參數則是 this.props.<你想要的參數名> 的形式。

而將 name 傳進去 Props 的部分是 e(HelloWorld, {name: 'React'}) 。 Props 是 React.createElement 所接受的第 2 個參數,它會幫我們將 name 丟進 HelloWorld 組件中,這也是為什麼能夠輕鬆透過 this.props.name 拿到 name 值的原因。

this.props.children

再來談談一個特殊的 props: children .

在開發 Web 時很常會有區塊包區塊的情況出現,例如:

<div class="parent">
    <div class="children">
    </div>
</div>

上述情況,被包住的區塊稱為 children ,在 React 中也可以組件包組件,這時候被包住的組件就放在 this.props.children 中。

例如以下範例,其實是 <h1>Hello World, <i>React</i></h1> 組件包組件:

const e = React.createElement;

class ItalicText extends React.Component {
  render() {
    return e('i', {}, this.props.text)
  }
}

class HelloWorld extends React.Component {
  render() {
    return e('h1', {}, ['Hello World, ', this.props.children]);
  }
}

const domContainer = document.querySelector('#app');
ReactDOM.render(
  e(HelloWorld, {},
    e(ItalicText, {text: 'React'}
    )
  ),
  domContainer
);

上述範例最重要的部分是 return e('h1', {}, ['Hello World, ', this.props.children]); ,我們將 e() 的第 3 個參數改為陣列 ['Hello World', this.props.children] ,之所以需要改為陣列是由於 Hello World 是字串,而 this.props.childrenelement ,兩者沒辦法簡單透過 + 串接在一起,因此用陣列形式交給 React.createElement() , React 就會自動幫我們渲染。

State

最後談談與 Props 類似的 State 。

State 可以讓組件管理自己的狀態,讓組件具有能夠獨立運作的功能。

假設我們想將 HelloWorld 組件加上一個被點擊幾次的計數器,被點擊幾次的數值就很適合使用 state 進行實作,因為它完全屬於組件特有的狀態。

那麼,如何加上 state 呢?在組件的 constructor(props) 建構方法內加上 this.state 的宣告即可,例如以下範例宣告 count 在 state 內,我們就可以透過 this.state.count 取得 count 的數值:

const e = React.createElement;

class HelloWorld extends React.Component {
  constructor(props) {
      super(props);
      this.state = {
        count: 0,
      }
  }
  render() {
    return e('h1', {}, 'Hello World, ' + this.props.name + ' ' + this.state.count);
  }
}

const domContainer = document.querySelector('#app');
ReactDOM.render(e(HelloWorld, {name: 'React'}), domContainer);

上述範例成功的話,就可以看到顯示結果 Hello World, React 0

setState

前述範例尚未實作點擊 Hello World 後更新數值的部分,本段將實作該功能。

首先 state 內的任何數值更新,一定要透過 setState() 方法更新,原因在於 React 是透過 setState() 方法得知 state 內的數值有更新,才會進一步更新畫面,所以一定要使用 setState() 方法更新 state 內的數值。

接著我們用 setState()HelloWorld 組件加上更新數值的功能吧!

const e = React.createElement;

class HelloWorld extends React.Component {
  constructor(props) {
      super(props);
      this.state = {
        count: 0,
      }
      this.addCount = this.addCount.bind(this)
  }

  addCount() {
      let count = this.state.count + 1
      this.setState({count: count})
  }

  render() {
    return e('h1', {onClick: this.addCount},
      'Hello World, ' + this.props.name + ' ' + this.state.count
    );
  }
}

const domContainer = document.querySelector('#app');
ReactDOM.render(e(HelloWorld, {name: 'React'}), domContainer);

首先說明第 12 - 15 行將原本 state 中的 count 取出 +1 後,再用 this.setState({count: count}) 更新 state 中的數值。

觸發 +1 的部分則在第 18 行中傳進去的 Props {onClick: this.addCount}onClick 是 React 規定的 event 等同於 HTML 中的 onclick attribute ,在 React 中一定要使用 onClick 代替。

p.s. 各種 event 可以在 SyntheticEvent 文件中找到。

傳進去的 onClick props 會在我們點擊文字時觸發 this.addCount ,進而更新 state 中的 count

最後回到第 9 行解釋 this.addCount = this.addCount.bind(this) 的作用,少了這行的話,將會出現以下的錯誤訊息:

Uncaught TypeError: Cannot read property 'state' of undefined

原因在於自行定義的 property 存取不到 React 執行時的 context ,也就是 React 自己的 this ,所以我們需要在 constructor 時將 React 的 context 跟自定義的 property 進行綁定:

this.addCount = this.addCount.bind(this)

如此一來, addCount 就能夠存取 this.state 囉!

p.s. 關於 thisbind() 可以閱讀 MDN web docs - this

此部分也許有點難以理解,不過只要記住 render, constructor 這些 React 文件有提及的函式不需要綁定 bind(this) 之外,其他自定義的函式如果需要存取 React 內的 property 或者函式,就需要在 constructor 內 bind(this)

小結

礙於篇幅限制,本篇先介紹不用任何前端開發工具開發 React 的過程,下一篇將介紹用 Webpack 更有效率地進行開發。

References

https://reactjs.org/