React架構之路

筆者本人其實對react的項目經驗不多,主要是用Angular框架。前段時間和同窗合做作了一個酒店加盟平臺項目,我負責後臺管理系統,採用的是Angular框架。他負責微信小程序,採用react框架。但隨着項目的進行,我發現他的項目文件我一時難以理清,整個項目結構比較零散。只有他本身對本身的項目很熟悉。我也提出了一些疑問,說react架構爲什麼如此鬆散。固然,後續的故事就不贅述了。css

筆者如今在校答辯中,利用空餘時間對react作了較爲深刻的一些研究。如下是筆者的我的心得分享。前端

不少開發者都知道react並非一個完整的前端框架,它僅僅是一個UI層面的框架。若是咱們須要用react來進行開發,那做爲一個開發者必須整合react周邊的生態,本身搭建出一個完整的框架。例如咱們一般須要redux進行數據管理,須要特定的HTTP模塊進行先後端通訊,須要react-router來進行路由管理等等。這帶來一個問題:react讓我有更多的選擇的同時也帶來的架構鬆散性的問題。正由於react過於開放的環境,使得不一樣開發者搭建出來的框架結構也是不盡相同。若是一個開發者缺少必定的經驗,他極可能寫出難以維護的代碼結構出來。react

在通過反覆思考事後,筆者按照本身在實際項目中的經驗搭建了一個react的架構。筆者很欣賞Angular嚴謹的架構,因此在架構React的時候參照了不少Angular的架構設計。git

 首先讓咱們來看看總體項目結構。github

一般一個應用能夠先拆分爲三個部分:登錄頁、註冊頁、主體業務頁面。這三個功能模塊是平級的,對應圖中login、regist、pc三個文件夾。其中pc文件就是主體業務模塊,我的喜愛根據終端類型來命名如pc、mobile。也有不少人喜歡用相似pages、home來命名。根據我的和團隊喜愛而定。typescript

咱們要寫的絕大部分頁面都放在主體頁面下,即pc文件下。如今pc文件夾下有hotel(酒店模塊)、order(訂單模塊)、room(房間模塊)。咱們先無論這些模塊的細節,只須要知道當前主體業務模塊下有這幾個模塊。它們在頁面上表現以下:redux

咱們重點來看pc文件夾下的pc.ui.tsx、pc.css、pc.component.tsx、pc.router.tsx、pc.reducer.tsx這幾個文件裏都有什麼,以及它們各自的做用。這裏我採用了typescript。採用typescript的緣由是強類型在多人協做開發方面能帶來很大好處,類型檢測能夠防止不一樣的開發者不按項目規範寫代碼形成項目混亂,同時類型提示也方便不一樣開發者默契地交流。小程序

pc.ui.tsx後端

 1 import * as React from "react";  2 import { NavLink } from 'react-router-dom';  3 import PcRouter from './pc.router';  4 import "./pc.ui.css";  5 import { Menu, Icon, Layout, Avatar, Row, Col  } from "antd";  6 
 7 
 8 const { Header, Content, Footer, Sider } = Layout;  9 const SubMenu = Menu.SubMenu;  10 const height = document.body.clientHeight;  11 const Index = [  12  {  13     icon: 'pie-chart',  14     path: '/pc/order',  15     name: '訂單管理',  16  },  17  {  18     icon: 'desktop',  19     path: '/pc/room',  20     name: '房態管理',  21  },  22  {  23     icon: 'desktop',  24     path: '/pc/hotel',  25     name: '酒店管理',  26  children: [  27  {  28         path: '/pc/hotel/qualification',  29         name: '資質管理'
 30  },  31  {  32         path: '/pc/hotel/info',  33         name: '信息管理'
 34  }  35  ]  36  }  37 ];  38 
 39 interface Props {  40  index: string[];  41   getIndex: () => void;  42   getList: () => void;  43 }  44 interface State {  45   collapsed: boolean
 46 }  47 
 48 class Pc extends React.Component<Props, State> {  49  constructor(props: Props, state: State) {  50  super(props);  51     this.state = state;  52  }  53 
 54   change = () => {  55     this.setState({  56       collapsed: !this.state.collapsed  57  })  58  }  59 
 60  render() {  61     return (  62       <Layout style={{ height: height }}>
 63         <Sider  64           breakpoint="lg"
 65           collapsedWidth="0"
 66         >
 67           <div className="logo" />
 68           <Menu  69             defaultSelectedKeys={["1"]}  70             defaultOpenKeys={["sub1"]}  71             mode="inline"
 72             theme="dark"
 73             inlineCollapsed={this.state.collapsed}  74           >
 75             {Index.map((i) => {  76               if (i.children) {  77                 return (  78                   <SubMenu  79                     key={i.path}  80                     title={  81                       <span>
 82                         <Icon type={i.icon} />
 83                         <span>{i.name}</span>
 84                       </span>
 85                     }>
 86  {  87                       i.children.map((child) =>
 88                         <Menu.Item key={child.path}>
 89  {child.name}  90                           <NavLink to={child.path}></NavLink>
 91                         </Menu.Item>
 92  )  93  }  94                   </SubMenu>
 95  )  96               } else {  97                 return (  98                   <Menu.Item key={i.path}>
 99                     <Icon type={i.icon} />
100                     <span>{i.name}</span>
101                     <NavLink to={i.path}></NavLink>
102                   </Menu.Item>
103  ) 104  } 105  })} 106           </Menu>
107         </Sider>
108         <Layout>
109           <Header style={{ background: '#fff', padding: 0 }} >
110               <Row>
111                 <Col span={1} offset={1}>
112                   <Avatar src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png" />
113                 </Col>
114                 <Col span={2} >
115  yangkai.sun 116                 </Col>
117               </Row>
118           </Header>
119           <Content style={{ margin: '24px 16px 0' }}>
120               <div style={{ padding: 24, background: '#fff', minHeight: 360 }}>
121               <PcRouter></PcRouter>
122             </div>
123           </Content>
124           <Footer style={{ textAlign: 'center' }}>
125             住行科技©2018 Created by sun.yangkai 126         </Footer>
127         </Layout>
128       </Layout>
129  ); 130  } 131 } 132 export default Pc;

 pc.ui.tsx這個文件很顯然就是UI組件,它只負責UI層面的展示。這裏的命名範式是:模塊名.功能名.tsx。pc.ui.tsx這個文件裏放了一個側邊欄<Sider></Sider>標籤和主體內容標籤<Content></Content>。重點請看<Content></Content>:微信小程序

1  <Content style={{ margin: '24px 16px 0' }}>
2               <div style={{ padding: 24, background: '#fff', minHeight: 360 }}>
3                   <PcRouter></PcRouter>
4             </div>
5 </Content>

裏面有一個咱們自定義的組件標籤<PcRouter></PcRouter>。在Angular裏它被稱之爲路由出口,如今咱們在react裏實現了一個路由出口。pc模塊下的全部路由組件將會被渲染到<PcRouter></PcRouter>標籤裏。咱們能夠看到這個標籤來自於 import PcRouter from './pc.router'; 即pc.router.tsx文件。

pc.router.tsx

 1 import * as React from "react";  2 import { Route  } from 'react-router-dom';  3 
 4 import Order from './order/order.component';  5 import Room from './room/room.ui';  6 import Hotel from './hotel/hotel.ui';  7 
 8 const routes = [  9  { 10                 path: '/pc/order', 11  component: Order, 12  }, 13  { 14                 path: '/pc/room', 15  component: Room, 16  }, 17  { 18                 path: '/pc/hotel', 19  component: Hotel, 20  }, 21 ]; 22 
23 class PcRouter extends React.Component{ 24  
25  render() { 26         return ( 27             routes.map((route) => 
28                 <Route key={route.path} path={route.path} component={route.component}>   
29                 </Route>
30  ) 31  ) 32  } 33 } 34 
35 export default PcRouter;

pc.router.tsx是路由配置文件,它導入了pc模塊下的三個路由組件Room、Hotel、Order並按照react-router的語法配置渲染。最終導出一個PcRouter的路由標籤供Pc模塊使用。

pc.component.tsx

 1 import { connect } from 'react-redux';  2 import  Pc from './pc.ui';  3 import  { State } from '../reducer';  4 import { actionType } from './pc.reducer';  5 import { HTTPS } from '../network/network';  6 
 7 
 8 
 9 const mapStateToProps = (state: State) => { 10     return { 11  index: state.pc.index 12  } 13 } 14 
15 const mapDispatchToProps = (dispatch) => ({ 16     getIndex: () => { 17  dispatch({ 18  type: actionType.pc_first 19  }) 20  }, 21 
22     getList: () => { 23         HTTPS.post('/getList', {id: 2}).subscribe({ 24             next: (res) => { 25  dispatch({ 26  type: actionType.pc_getList, 27  list: res.data 28  }) 29  }, 30             error: (e) => { 31 
32  } 33  }) 34  } 35 }) 36 
37  export default connect(mapStateToProps, mapDispatchToProps)(Pc);

pc.component.tsx就是容器組件,能夠看到它導入了pc.ui.tsx而且將之鏈接造成一個完整的組件:export default connect(mapStateToProps, mapDispatchToProps)(Pc);

在這裏,咱們編寫一個組件的主要邏輯,例如Ajax請求函數。這裏的 mapStateToProps, mapDispatchToProps兩個函數都是redux的語法規則,筆者就再也不贅述。本文主要研究react架構。

值得注意的是並不是任何函數都要寫在容器組件裏,例如在pc.ui.tsx中有一個 change函數就寫在ui組件中。由於此函數修改的數據並不在redux提供的全局store中。這個change函數事實上是一個控制側邊欄是否收起的函數。顯然圖中的collapsed變量是存放在pc組件自己所維護的state當中。也就是說一些數據只須要組件本身維護就能夠了,不須要藉助redux來維護其狀態。一般來將這些數據都是決定組件自己狀態的數據。咱們在處理這樣的數據時應該把它當成一個UI層面的事件,天然咱們也應該把這些函數寫在UI組件裏。

pc.reducer.tsx

 1 const initialState = {  2     index: ['test']  3 }  4 
 5 export interface PcState {  6  index: string[]  7 }  8 
 9 export const actionType = { 10     pc_first: 'pc_first', 11     pc_getList: 'pc_getList'
12 } 13 
14 const pc = (state: PcState = initialState, action: any) => { 15     switch (action.type) { 16         case actionType.pc_first: 17             return { 18  ...state, 19                 index: ['sun', 'yang', 'kai'] 20  } 21         
22         case actionType.pc_getList: 23             return { 24  ...state, 25  index: action.list 26  } 27         default: 28             return state; 29  } 30 } 31   
32 export { pc };

pc.reducer.tsx很顯然就是專門用於維護組件數據的文件。它負責去修改和更新store樹上的數據。store樹也是redux的概念,這裏也再也不贅述。修改store樹上的數據的惟一方式是發起一個action,這是redux的規則。隨着咱們的action愈來愈多,咱們須要對action進行範式化的命名。例如這裏的pc_getList。它表明pc模塊下的getList函數,也就是pc模塊下獲取一個列表的函數。

而後咱們來看整個應用的store是如何構成的:

首先找到根reducer:reducer.tsx

reducer.tsx

 1 import { combineReducers } from 'redux';  2 import { pc, PcState } from './pc/pc.reducer';  3 import { pc_order, PcOrderState } from './pc/order/order.reducer';  4 
 5 export default combineReducers({  6  pc,  7  pc_order  8 });  9 
10 export interface State { 11  pc: PcState, 12  pc_order: PcOrderState 13 }

State接口包含咱們整個應用的數據,它定義redux的store的類型. 咱們能夠看到reducer.tsx導入了pc模塊和pc_order模塊下各自的reducer並用combineReducers將它們合併。根據咱們的範式化設計能夠看出pc_order就是pc模塊下的order模塊。這裏的store樹的設計依舊是按照範式化扁平化的設計原則,爲的也是提升store樹的性能。若是咱們將pc_order命名爲order並將之嵌套 在pc下:

1 export interface State { 2  pc: { 3  order: PcOrderState 4  } 5 
6 }

隨着業務愈來愈複雜,order模塊下可能還會有其它其它模塊,order下又會嵌套更多的對象。最終整個store樹層級太深變得臃腫不堪,會影響性能。因此採起範式化扁平化的設計會提升store樹的性能。

同時請注意咱們在reducer.tsx導入各個模塊的reducer時還導入了它們的類型如PcState、PcOrderState。咱們利用這些類型完整地定義了整個全局State類型。因此從此不管咱們在哪一個地方操做store樹只須要導入此State接口,就能對整個store樹地結構一目瞭然,由於typescript會有類型提示。

例如咱們在order組件中操縱全局的store, 根據導入的State類型提示,咱們清楚地知道store的每個細節:

咱們已經討論完pc模塊下的文件劃分以及它們的功能。其它任何文件均可以按照這樣的方式劃分。最後咱們總結一下劃分思路:咱們首先在第一維度上是按照業務層次(頁面層次)上來劃分文件夾,如pc文件夾表明主體頁面,這個頁面下包含訂單頁面、酒店頁面、房間頁面。因此咱們又在pc文件夾下劃分了三個文件夾room、order、hotel。這樣總體業務層次劃分就很是清晰。其次在第二維度上,咱們根據頁面元素能夠再次拆分一個頁面。例如pc頁面。一般首頁包含的元素有不少,例如輪播圖、導航欄、頁眉頁腳等等。咱們能夠將這些元素拆分出來放進pc下的components文件夾中。固然這裏筆者偷了懶,components文件夾是空的。對於任何一個頁面咱們均可以這樣作,像order訂單頁面,咱們也能夠在其下建一個components文件夾用於存放拆分出來的頁面元素。

最後作一點補充:network文件夾裏存放了網絡相關的配置。這裏只是簡單得封裝了一個post請求,用的rxjs。我不喜歡promise,明明rxjs功能更增強悍。

mock文件夾下模擬了後臺服務得接口,這裏簡單寫了幾個接口:

 在order.ui.tsx中,藉助immutable.js進行了渲染優化,利用react提供的shouldComponentUpdate函數避免沒必要要的渲染。感興趣得同窗能夠本身研究一下

import * as React from "react"; import { Collapse, Button } from 'antd'; import { Order as OrderProps } from './order.reducer'; import { is, Map } from 'immutable'; const Panel = Collapse.Panel; interface OrderItemProps { order: OrderProps } interface OrderItemState {} class OrderItem extends React.Component<OrderItemProps, OrderItemState> { constructor(props: OrderItemProps, state: OrderItemState) { super(props); this.state = {} } shouldComponentUpdate(nextProps,nextState){ const thisProps = this.props; const thisState = this.state; if(!is(Map({...thisProps.order}), Map({...nextProps.order}))) { return true; } for (const key in nextState) { if (thisState[key] !== nextState[key] && !is(thisState[key], nextState[key])) { return true; } } return false; } render() { const order = this.props.order; console.log('render: ' + order.name); return( <span>{order.name}</span>
 ) } } interface State {} interface Props { orders: OrderProps[], getOrders: () => void } class Order extends React.Component<Props, State> { constructor(props: Props, state: State) { super(props); } render() { return ( <div>
              <Button onClick={this.props.getOrders}>獲取新訂單</Button>
              <Collapse defaultActiveKey={['key0']}> { this.props.orders.map((order, index) => 
                        <Panel header={order.name} key={'key' + index}>
                             <OrderItem order={order} ></OrderItem>
                        </Panel>
 ) } </Collapse>
            </div>
 ) } } export default Order;

最後作一點總結吧。我認爲架構好一個前端應用須要從頁面層次上清晰地劃分整個應用,再從頁面元素層次上清晰地劃分每個頁面。另外關於框架的選擇問題,網上也有不少討論。可是每每他們只是羅列了一大堆各個框架的特性,最終並無給出一個明確的建議。老是以它們各有格的特色爲由不給出答案。我其實並不徹底這樣認爲。拿React和Angular來講,React更適合有必定經驗的團隊,Angular更適合沒有經驗的團隊。由於React它不是一個完整的框架卻有着龐大的生態環境,若是你和你的團隊足夠老練,那麼大家能夠爲所欲爲地架構起適合本身項目的框架,這樣就很是地靈活,所構建地應用也和當前項目契合度很高。Angular是一個完整地框架,它把一切都規定好限制好了,雖然它很優秀,但對於一個有經驗地團隊實在是限制過分了。打個比方,Angular就像是倚天屠龍劍,一個初出茅廬的小子拿着它也能和各路江湖高手過上幾招,可是若是你重度依賴它,本身是很難突破自我提高能力的,厲害的是劍而不是使用者。而一個熟練使用React的人,他就像一個武器大師,草木皆爲劍。沒有固定的武器,可是他能在不一樣的境地找到適合本身的武器,他真正的提高了本身的能力而不是依賴武器自己。

項目github地址:https://github.com/sunyangkai/ReactDemo

以上就是筆者對react架構方面的一些思考。文中如有不當之處還請各位少俠不吝賜教!

相關文章
相關標籤/搜索