iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 16
5

Hi!大家好啊!因為大家的支持!鐵人賽總算過一半了!希望剩下來的賽程請各位大大繼續指教!

話說今天為了研究一個東西浪費了一堆時間,讓我現在感覺很沒勁,哈哈哈(超級苦笑XD)!我們準備要進入最後的React Router篇了(雖然今天可能還不會提到主角本身XD),為了迎接他,這一篇先來了解React的目錄結構!

React目錄結構篇

先前我們做的那些練習,都把jsxjshtml包含設定檔全部都丟在一起,相信如果有強迫症的大大,從第一篇忍到現在一定看得很辛苦!其實我在一開始就想要提目錄結構了,但是因為還有Redux的關係,就想說一次講一講,在這邊和各位大大說聲不好意思!

基本結構

說到結構這種東西,真的沒有基本的說法,有時候隨著公司的案子結構,有時候又照著維護案的結構,但是還是有主流的一個基本結構,至於要怎麼調整,大家可以為自己的目錄結構做個客製化XD,那以下先說說最簡單的基本目錄:
https://ithelp.ithome.com.tw/upload/images/20181008/20106935pCul4FC301.jpg

以下解釋一下每個資料夾的內容及用途:

  1. 藍色框框:ex01不是別的,就是專案名稱,也就是專案的資料夾名稱。
  2. 紅色框框:根目錄上就放置package.jsonwebpack.config.js等設定檔。
  3. 沒有框框但是在根目錄上的src:裡面放著我們自己寫的程式,包括頁面的index.html和負責頁面渲染的index.jsx,雖然他最後會被webpack打包成index_bundle.js
  4. 粉紅色框框:裡面放著所有我們寫的組件。
  5. 紫色框框:各自放著獨立的組件,並統一由index.jsexport的內容匯出,這樣在其他文件中import的時候看起來比較不會亂,而Main組件負責最後的輸出,而這裡的Main組件也只是一個頁面,當然也可以在實務上建立各個頁面匯出的總組件,所以也可以改成這樣子放:
    https://ithelp.ithome.com.tw/upload/images/20181010/20106935ia3eEGTg5x.png
    這裡針對第五點補充說明一下,為什麼是由index.js統一匯出,因為當我們使用import時,會預設取目錄文件的index.js詳細說明可以看這裡

有了目錄看起來專案的樣子是不是完整多了XD,可是為了保持整個目錄的結構,我們的程式碼也需要做相對應的修改:

  1. webpack.config.js
    因為之前我們都是從根目錄打包,但現在把它移到src中,所以打包的目錄和輸出的目錄都要更改。
    const path = require('path');
    module.exports = {
        //這個webpack打包的對象,這裡面加上剛剛建立的index.js
        entry: {
            index: ['./src/index.jsx']
        },
        output: {
            //這裡是打包後的檔案名稱
            filename: 'src/index_bundle.js',
            //打包後的路徑,這裡使用path模組的resolve()取得絕對位置,也就是目前專案的根目錄
            path: path.resolve('./'),
        },
        module:{
            rules:[
                {test:/\.jsx$/,use:{loader:'babel-loader',options:{presets:['@babel/preset-react','@babel/preset-env']}}},
                {test:/\.js$/,use:{loader:'babel-loader',options:{presets:['@babel/preset-env']}}}
            ]
        }
    };
    
  2. TitleMain組件:
    1. Title/Title.jsx
      import React from "react"
      
      class Title extends React.Component {
          render(){
              return <h1>{this.props.title}</h1>
          }
      }
      //把該目錄的Title組件匯出
      export {Title}
      
    2. Title/index.js
      這是把importexport寫在一起的寫法,等於直接匯出剛剛在Title.jsxexport的所有物件。
      export * from "./Title.jsx"
      
    3. Main/Main.jsx
      因為這是要渲染畫面的主要組件,所以在這裡先importTitle匯出的物件,因為前面有提到預設是index.js,所以只需要指定到目錄就好,如此一來就能夠使用Title組件了,最後一樣再把他匯出:
      import React from "react"
      import {Title} from "../Title"
      
      class Main extends React.Component{
          render(){
              return <Title title="Hello!World!" />
          }
      }
      
      export {Main}
      
    4. Main/index.js
      不多說,一樣就是匯出:
      export * from "./Main.jsx"
      
  3. 最後要打包的index.jsx
    從Main匯入組件後直接用ReactDOM.render輸出到root中:
    import React from "react"
    import ReactDOM from "react-dom"
    import {Main} from "./components/Main"
    
    ReactDOM.render(<Main />,document.getElementById('root'));
    

完成後就可以試著打包或使用webpack-dev-server執行:
https://ithelp.ithome.com.tw/upload/images/20181009/20106935YYDNAF1KC0.png

是不是覺得反而花更大的心力在處理這些事情了XD,不過在專案越來越大的時候,這樣子的分類反而不會搞混各個組件的位置!以下是基本目錄結構的GitHub連結:
GitHub目錄連結

進階(?)結構

基本的沒問題後,接著要來加入Redux了!所以應該算稍微進階吧XD,不過我一直一直在猶豫要不要用之前的留言版,因為調整目錄就幾乎整個都要變動了,會改到超級多東西(這邊有點浮誇啦XD),雖然猶豫到最後我還是Do了!哈哈哈!那下面就一起來改寫吧!目標是這樣子:
https://ithelp.ithome.com.tw/upload/images/20181010/20106935c5asEowsPE.jpg
應該比第一次看的時候還要清楚多了!以下解釋每個資料夾和檔案的用途:

  1. 藍色框框:負責建構各個動作的物件,在這裡由index.js統一匯出。
  2. 粉紅色框框:就和上方一樣。
  3. 橘色框框:是指不會變的資料,例如存放所有動作指令的action-types.js和要給store管理的資料。
  4. 紫色框框:負責建構描述各種動作的Reducer,一樣由index.js匯出。
  5. 綠色框框:負責產生保管資料的store,一樣由index.js匯出。

接下來就重頭戲了,開始改寫之前留言板的內容吧!

  1. 首先處理constants中的檔案:
    1.action-types.js
    存放動作指令ADD_MESSAGE並匯出:
    export const ADD_MESSAGE = "ADD_MESSAGE"
    
    2.models.js
    將資料放在這個檔案中並匯出:
    const data = {message:[{id:'1',name:'神Q',message:'嗨!大家好啊!'},
    {id:'2',name:'小馬',message:'早安啊!昨天有沒有好好發文?'},
    {id:'3',name:'王子',message:'ㄛ!別說了,那真的超級累!'},
    {id:'4',name:'神Q',message:'哈哈哈!加油啦!再一下就結束了!'},
    {id:'5',name:'王子',message:'結束後我一定要爆睡一頓!'},]}
    
    export {data}
    
  2. 接著是actions:
    1. message.js
      從剛剛的action-types.js中匯入指令後建構動作並匯出:
      import {ADD_MESSAGE} from "../constants/action-types.js"
      
      export const addMessage = message => ({
          type : ADD_MESSAGE, payload : message
      })
      
    2. index.js
      負責匯出actions中的所有動作:
      export * from "./message.js"
      
  3. 然後是reducers:
    1. messageReducer.js
      models.js的資料和message.js建構的動作做成一個reducer,一樣在最後把他匯出:
      import {ADD_MESSAGE} from "../constants/action-types.js"
      import {data} from "../constants/models.js"
      
      const messageReducer = (state = data,action) =>{
          switch(action.type){
              case ADD_MESSAGE:{
                  action.payload.id = String(state.message.length+1)
                  return { ...state, message: [...state.message, action.payload] }
                  break
              }
              default:{
                  return state
                  break
              }
          }
      }
      
      export {messageReducer}
      
    2. index.js
      整理所有的reducer並匯出:
      export * from "./messageReducer.js"
      
  4. 最後在store資料夾內產生store
    1. configureStore.js匯入reducer來產生store
      import {createStore} from "redux"
      import {messageReducer} from "../reducers"
      
      const store = createStore(messageReducer)
      
      export {store}
      
    2. index.js負責整理匯出:
      export * from "./configureStore.js"
      
  5. 資料的部分完成,就剩下component了:
    組件的話基本上都一樣,只是差在這次完全各司其職,做好所有處理,包括connect後才交給Main去組合出最後的頁面匯出:
    1. InputMessage.jsx
      1. 從actions資料夾中匯入addMessage這個動作。
      2. mapDispatchToProps指定對store執行dispatch動作。
      3. 將組件和動作connect,產生InputMessage並匯出
      import React from "react"
      import {connect} from "react-redux"
      import {addMessage} from "../../actions"
      
      class ConnectInputMessage extends React.Component {
          constructor(props){
              super(props)
              this.state = ({name:'',message:''})
              this.changeState = this.changeState.bind(this)
              this.clearMessage = this.clearMessage.bind(this)
              this.submitMessage = this.submitMessage.bind(this)
          }
      
          changeState(event){
              this.setState({[event.target.name]:event.target.value})
          }
      
          clearMessage(){
              this.setState({name:'',message:''})
          }
      
          submitMessage(){
              let messageData = {
                  name:this.state.name,
                  message:this.state.message,
              }
              this.props.addMessage(messageData)
              this.clearMessage()
          }
      
          render(){
              return(
                  <div>
                      暱稱:<input type="text" name="name" 
                                  value={this.state.name}
                                  onChange={this.changeState} />
                      <br/>
                      訊息:
                      <br/><textarea name="message" 
                                      value={this.state.message}
                                      onChange={this.changeState}></textarea>
                      <input type="button" value="送出留言"
                              onClick={this.submitMessage} />
                  </div>
              )
          }
      }
      
      const mapDispatchToProps = dispatch => {
          return {
              addMessage : message =>{ dispatch(addMessage(message)) } 
          }
      }
      
      const InputMessage = connect(null,mapDispatchToProps)(ConnectInputMessage)
      
      export {InputMessage}
      
    2. MessageList.jsx
      1. mapStateToProps指定要向state取的資料。
      2. ConnectMessageListmapStateToPropsconnect產生MessageList後匯出。
      import React from "react"
      import {connect} from "react-redux"
      
      class ConnectMessageList extends React.Component {
          render(){
              let message = this.props.message.map((item)=>{
                  return <li key={item.id}>{item.name}:{item.message}</li>
              })
              return(
                  <ul>
                      {message}
                  </ul>
              )
          }
      }
      
      const mapStateToProps = state =>{
          return {message : state.message}
      }
      
      const MessageList = connect(mapStateToProps)(ConnectMessageList)
      
      export {MessageList}
      
    3. Main.jsx
      匯入MessageListInputMessage組件,並將它們組合起來後匯出:
      import React from "react"
      import {MessageList} from "../MessageList"
      import {InputMessage} from "../InputMessage"
      
      class Main extends React.Component{
          render(){
              return (
                  <div>
                      <InputMessage />
                      <MessageList />
                  </div>
              )
          }
      }
      
      export {Main}
      
    然後,以上雖然我都沒有提到idnex.js的內容,但是一樣都是他們提供匯出的功能,就和第一段一樣,我就不再另外PO了!只是不能忘記哦!
  6. index.jsHTML和上一個例子一樣,差別在要打包的index.jsx內容:
    這裡要匯入管理資料的store和連結reactredux用的Provider組件,之後把Main包在Provider中用render渲染到畫面上。
    import React from "react"
    import ReactDOM from "react-dom"
    import {Provider} from "react-redux"
    import {store} from "./store"
    import {Main} from "./components/Main"
    
    ReactDOM.render(<Provider store={store}>
                        <Main />
                    </Provider>
                    ,document.getElementById('root'));
    

雖然看起來很複雜,但是經過調整基本架構後再加入redux進目錄中,感覺就比較不會那麼卡手了,只要對各個資料夾及檔案負責的內容清楚,剩下就只是importexport的事情而已,這裡附上留言板的目錄架構:
GitHub目錄架構連結
因為是留言板,所以也有Page:GitPage連結

小弟我第一次就拿留言板來改,結果怎麼改都錯誤,才決定砍掉從練從Hello!World!開始,搞到最後這篇感覺像半複習一樣XD,還請各位多多包涵,另外因為一開始就有提到說,目錄架構真的有非常非常多種,幾乎沒有任何一種標準答案,所以如果大大們有推薦的架構或可以改進的,麻煩留言告訴我!小弟我感激不盡/images/emoticon/emoticon02.gif


最後要感謝各位大大的觀看,如果文中有任何錯誤或解釋不清楚的地方,還麻煩各位大大留言告訴我,小弟會盡快修改會補充文章內容的,謝謝大家/images/emoticon/emoticon41.gif

參考資料:

  1. https://github.com/kdchang/reactjs101/blob/master/Ch07/react-redux-real-world-example.md
  2. https://juejin.im/post/58cbfcb05c497d0057b9b228
  3. https://www.zhihu.com/question/50750032

上一篇
[筆記][React]當React遇上Redux(3)-從React組件操作Reducer
下一篇
[筆記][React]React網頁好朋友Router(1)-基本用法篇!
系列文
一步一腳印的React旅程30

2 則留言

1
SunAllen
iT邦高手 1 級 ‧ 2018-10-21 18:25:46

這篇的內容,好豐富啊......等之後有空時,再來照大大練習。 謝謝!

我以為大大是創作派的!
原來是我有眼不識泰山/images/emoticon/emoticon17.gif

1
fysh711426
iT邦研究生 4 級 ‧ 2018-10-21 22:06:34

之前維護的 Angular 專案也有類似 index.js 的用法,每個資料夾下會有一個檔案用來整理匯出,不過我不喜歡這種用法,哈哈哈。
/images/emoticon/emoticon13.gif

其實我是第一次接觸到這種用法,
因為目前用React做的都是小作品,
所以只有覺得在import時寫起來很簡潔很方便,
不曉得大大有甚麼看法/images/emoticon/emoticon19.gif

我要留言

立即登入留言