- 默默將開發者推向模組化的設計模式
- 讓寫好的模組不只在內部重用,也能延續到其他專案上
所有 Components 都放在/src/components
。通常有幾個建立 Component 的時機:
- 具有獨立功能的 UI 組,將在網頁各處重用
- 每個獨立的頁面,包含眾多 UI Components
如果該 Component 是表現性的 UI,通常不具有固定的數據操作對象,如 lebel、button 這種小 UI Components,它只接收上層傳下來的 props 來改變自己的表現方式,這種單純的東東就是 Pure Component。在本架構中可以這樣建立:
|-MyComponent
|—-index.js
|—-pure.js
pure.js
就是該 React Component 棲身之處,而 index.js
只是將 pure.js
引入再傳出去,這樣做是為了:
- 讓外面的世界可以只透過資料夾名稱就引入整個 Component
- 保留 Pure Component 變成 Container 的空間(下面將說明)
- 如此一來
pure.js
已經可以自己周遊列國被重複使用了,要不要 connect 不關他的事。
看到 pure.js
就會提醒自己,寫 UI 要盡量維持 pure
,避免奇怪的相依。
在該 Component 資料夾內新增 index.scss
,並在 pure.js
最上方以 import './index.scss';
方式引入。
|-MyComponent
|—-index.js
|——connected.js
|—-pure.js
|--index.scss
- 只寫自己的外觀樣式(formation)
- 不寫與外界的排版關係(relation),如以下:
- Positioning (position, top, left, right, bottom)
- Floats (float, clear)
- Margins (margin)
- Dimensions (width, height) *
- 所以自己與外界的 relation 就寫在上層的 Component 裡呦
如果今天的 Component 是自帶固定數據操作對象的,例如網頁的 sidebar,那他除了 pure 的部分,還會再多被 connect
到 store 裡。資料結構就會變成:
|-MyComponent
|—-index.js
|——connected.js
|—-pure.js
|--index.scss
請注意,pure.js
還是維持一樣的本質,只是 connected.js
引入了 pure.js
並 connect
起來,最終交由 index.js
傳遞到外面的世界。
不管有沒有 connect,Pure Component 的 props 接口要盡量扁平化以保留彈性。不要預設外面傳進來的會剛好是一個包好好的物件。如此一來如果缺少任何一個 props 都能很快的在上層就填起來(不論以什麼方法)。
// Oh God don't do this :(
render() {
return(
<div className="user-card">
<img src={this.props.user.image} />
<h1>{this.props.user.name} />
<p>{this.props.user.introduction} />
</div>
)
}
// Instead, do this every time :)
render() {
return(
<div className="user-card">
<img src={this.props.image} />
<h1>{this.props.name} />
<p>{this.props.introduction} />
</div>
)
}
render()
內的使用者事件命名,一律以 handle
起始,並以箭頭函式定義以方便 this
的綁定。如:
class Example extends React.Component {
handleClickButton = () => {
console.log('Clicked!')
}
render() {
return (
<div>
<button onClick={this.handleClickButton}>Click Me</button>
</div>
)
}
}
如果要 mapDispatchToProps
時,則 props
命名以 on
開頭,並一樣使用箭頭函式包裝後讓 render() 使用。用意在於:
- 藉由命名規則的確立讓自己搞清楚到底是哪裡來的 function
- 確保 render() 中
this.props
開頭的都是純值,而this.handle-
開頭的都是事件。比較清楚(吧
//connected.js
const mapDispatchToProps = (dispatch) => ({
onClickButton: (config) => {
dispatch(fetchData(config))
}
})
//pure.js
class Example extends React.Component {
handleClickButton = () => {
this.props.onClickButton({url: '/some/api/'})
}
render() {
return (
<div>
<button onClick={this.handleClickButton}>Fetch It</button>
</div>
)
}
}
傳統的 Redux 文件會把所有 actions 集合到同一個地方,用 action creator 讓不同人取用。但在這裏很特別的是,每個 Component 的 actions 將直接寫在自己的 connected.js
裡,因為一般的同步 action 結構都很簡單,沒有必要開一個新檔案再 import 進來,直接手寫反而比較快。除了設定很麻煩且必定跨元件都用得到的 fetchAction.js
。
///connected.js
const mapDispatchToProps = (dispatch) => ({
onClickIncrement: (id) => dispatch({type: 'INCREMENT_TIME', id})
})
Redux 裡的 Reducer 在技術上都是全域的,任何元件都可以讀取或對他 dispatch,因此很容易「濫用」reducer。然而在這裡,Reducer 將有較嚴格的設計方式,目的是更清晰的模組化與可維護性,但缺點就是一開始要規劃比較久。
- Reducer 將定義在與其最相關的某一 Component 之下
- 每個 Component 最多只能定義一個 Reducer
- 又如果發現某組 UI 需要到自己一個 Reducer,則那串東西必須成為 Component
由此可見,這個作法不只關乎 reducer 而已,其實更高層次上是在影響 Components 的設計,往模組化邁進一大步(我的推測啦)。另外一種 reducer 設計理念可以參考 這篇。
在 /src/App/reducer.js
引入所有 reducers 並 combine
。
唯一使用這個特殊的 Actions fetchActions
,同時必須理解:
- 他是對外唯一接口
- 他在語意上不屬於任何 Component
//connected.js
import { fetchData } from '../fetchActions';
const mapDispatchToProps = (dispatch) => ({
onClickFetch: (config) => {
dispatch(fetchData(config))
}
})
//pure.js
handleClickFetch = () => {
this.props.onClickFetch({url: 'https://jsonplaceholder.typicode.com/posts/1', id: 'randomPage'})
}
在送出請求時,除了 url
外還必須附帶 id
,id
是指這份欲讀取資料在 state.fetch 裡的唯一 key。dispatch(fetchData(config))
之後的分解動作詳解:
fetchAction
會先透過附帶的id
訪問 state.fetchedData[id],、- 確認不存在資料後,才會正式對 API 發送請求,也就是
dispatch(requestData(config))
- 如果請求成功,則會接著
dispatch(receiveData(config, response))
,並將response.data
存到state.fetchedData[id]
裡 - 然後
mapStateToProps
就能把熱騰騰的資料餵給pure.js
了
sass 會用到的 variables
、mixins
和 functions
全部寫在同一個檔案/src/styles/bootstrap.scss
,這個檔案會透過 sass-resources-loader
自動在每份 .scss
開頭注入,所以不用手動 @import
就能全域享用。這部分的設定寫在 webpack.config
裡。
跨元件的樣式定義,不要寫在任何 src/components
裡面,請寫在 src/styles
裡以 _
開頭,並在 shared.scss
之中逐一 @import
。