Reactjs - 簡易聊天室

November 5, 2015

Intro

在開始之前,我覺得大家應該可以先讀一下 Thinking in React 這篇文章,非常的有幫助! 我自己剛開始在寫 reactjs 的時候真的是跌跌撞撞的(我本身程式又不是很好), 那時候都是簡單的模仿然後感受一下 reactjs,但是發現當自己在實作的時候就會發現問題了, 這篇文章是由 Pete Hunt 所寫的一篇教學,讓我們用 component 角度去思考如何建構自己的 App。

五大步驟:

  1. Break the UI into a component hierarchy
  2. Build a static version in React
  3. Identify the minimal (but complete) representation of UI state
  4. Identify where your state should live
  5. Add Inverse data flow

上面的五大步驟其實大概是這樣:

  • 首先,將自己的模型定義出來
  • 將你的 UI 介面拆成各個 component
  • 用 React 建立一個靜態版本
  • 確認並表示出 UI 最小(但是完整的)的狀態
  • 確認你的狀態(state)應該在哪裡
  • 加入反向數據資料流

Static Interface

我們先看一下我們要實作的介面,我們先建立一個靜態的網頁:Chat Room Model

使用方法請參考下方 README

Static Version

Component

左邊有一個對話者清單有三個對話者,右邊則是對話記錄中的對話內容,下方則有一個輸入框

Component

為什麼要這樣分呢?就像上面所提到的 Break the UI into a component hierarchy,我們將這個畫面拆成好幾個元件,每個元件負責一個功能(單一責任原則),這樣我們很可以很易於掌控我們每個元件所負責的事情。

Static Version In React

我們接下來就可以用 reactjs 先建立一個靜態版本的 chatroom 囉!,我們依照上面的 component 地方所提到的,我們將它拆成五個部分:

  • App
  • Messager
  • MessageList
  • MessageItem
  • UserInput

我們將各個 component 組合起來就變成一個完整的 App,大概解釋一下各個元件的功能:

  1. Messager 是對話者清單,這個元件負責將使用者姓名、大頭照、最後對話內容、對話時間顯示出來。
  2. MessageList 則是使用者間的聊天內容。
  3. MessageItem 處理則是每條對話內容。
  4. UserInput 讓使用者輸入訊息的輸入框。

reactjs 靜態版本範例 (尚未加入 MessageItem):reactjs static version

State

在這個 chatroom 中,我們根據上面來定義了一個最簡單的狀態:

const initialState = {
  newMessage: '',
  threads: [
    {
      target: {
        name: 'Elsa',
        profilePic: 'http://lorempixel.com/50/50/people/1'
      },
      messages: [
        { fromMe: false, text: '對啊', time: '12:27am' },
      ]
    }
  ],
  currentIndex: 0
};

newMessage 是我們的輸入訊息,threads 內則是使用者相關的資料和對話訊息,currentIndex 則是目前的使用者。

讓我們開始吧!

demo

畫的感覺不是很好的圖,但是想透過圖片看能不能更清楚表達一些(笑)。

Chat Room - 點我看完整範例!

App 就是我們整個的應用程式,在這次的 chatroom 中,我們在 App 定義了一個 initialState 是我們的應用程式的初始狀態。 但是 App 不一定要擁有 State,如果是 Flux 實作,則是會把 State 抽出來放在 Store,所以 App 會變成 Stateless component 但是我們先不討論這一塊。 在 App 中,我們可以將 state 或者是 function 當作 props 傳遞下去,需要被當作 props 傳遞的 function 是那些會 mutate state 的。

首先,我們先來討論左半部的 component,App 在 render 時先解構了 state

const { threads, currentIndex } = this.state;

解構後,我們可以從 threads 中我們拿到所有使用者的資料,我們透過 map 方式並在 return 的時候我們將 Messager 的元件加入,把資料往下傳到 Messeager

{threads.map((thread, id) => {
  const { target, messages } = thread;
  const lastMessage = messages[messages.length - 1];
  return (
    <Messager
      key={id}
      src={target.profilePic}
      name={target.name}
      content={lastMessage.text}
      time={lastMessage.time}
      handleMessagerChange={this.handleMessagerChange.bind(this, id)}
    />
  );
})}

Messager 可以接收到從上層傳下來的 props

const { src, name, content, time, handleMessagerChange } = this.props;

我們在 Messeager 中先解構由 App 所傳下來的this.props,這邊特別注意到 handleMessagerChange,是由 App 所傳下來的 function,這個 function 的目的是為了 切換使用者

需要被當作 props 傳遞的 function 是那些會 mutate state 的。

我們將 handleMessagerChange 加入到這裡:

<li className="thread-item" onClick={handleMessagerChange}>

讓我們在點擊 (onClick) 的時候可以觸發動作切換使用者。 左半部的 Component 的功能大概就是以上,接著我們來看右半部的地方囉!

右半部MessageList Component 會從 App 中拿到 state 內的 threadscurrentIndex, 接著就是把這些 state 當作 props 傳下去。

<MessageList threads={threads} index={currentIndex} />

進到 MessageList component 內,我們在 render 時先解構 this.props 得到 threadsindex

const { threads, index } = this.props;
const messages = threads[index].messages;

接著,我們將 messages map ,這時候將 MessageItem 加入進來。

<div>
  {messages.map((message, id) => (
    <MessageItem key={id} fromMe={message.fromMe} text={message.text} />
  ))}
</div>;

接下來我們來到了MessageItem,這裡是負責每條訊息的顯示,我們從 this.props 中將 fromMetext 解構出來,fromMe 用來表示訊息是否由自己發出的,text 則是訊息內容。

const { fromMe, text} = this.props;
return (
  <div className={`message-item ${fromMe ? 'message-from-me' : 'message-from-other'}`}>
    <span>{text}</span>
  </div>
);

最後剩下的是 UserInput component:

<UserInput
    newMessage={this.state.newMessage}
    messageChange={this.handleMessageChange.bind(this)}
    handleKeyDown={this.handleKeyDown.bind(this)}
/>

在 App Component 最後我們加入了 UserInput component,並將 App 內的 state 和 function 當做 props 傳到 UserInput

  • handleMessageChange => function
  • handleKeyDown => function

首先,一樣先將接收到的 this.props 做解構:

const { newMessage, messageChange, handleKeyDown} = this.props;

在 input 的地方可以將解構後得到的 newMessagemessageChangehandleKeyDown 帶入:

return (
    <input className="new-message"
        value={newMessage}
        onChange={messageChange}
        onKeyDown={handleKeyDown}
    />
);

這樣 input 在觸發 onChangeonKeyDown 時,就可以透過由 props 傳下來的 function 來操作了。

結論

整個操作流程我只是大概描述一下比較重要的地方,其他細節可以參考原始碼做對照, 相信應該不會有太大的問題,只要把握住上方所提到的那五大要點,相信你可以很快地了解這個範例。

我自己也還是個 reactjs 初學者,但希望可以透過記錄和分享方式和大家一起學習:)。Happy Coding!