【react】【ts】架構、書寫規範

說在前頭

React因爲jsx的模式,比Vue的寫法更多,更雜亂,但勝在社區廣,開發者多,作爲Facebook爲後盾的開發者團隊們,更是底蘊深厚,出了一套又一套的擴展插件,理念也各不相同,有react-router3/4扁平化結構與過程式開發的碰撞,有css in js與傳統less、sass等的交錯,確實,他們讓react的羽翼更加豐滿,選擇上更加自由,不過如此帶來的代價就是規範上無法統一,在此編寫react書寫規範,屬個人規範性文章,僅供參考


技術選型

  1. react:^16.5.0
  2. react-router:^4.3.1(也含有3.0JS配置項規範)
  3. whatwg-fetch:2.0.3
  4. redux:^4.0.0
  5. react-transition-group:^2.4.0
  6. antd:^3.9.2
  7. typescript:^3.0.3
  8. less:^3.8.1
  9. tslint:^5.7.0
  10. better-scroll:^1.12.6
  11. postcss-px2rem:^0.3.0

目錄架構

create-react-app my-app –scripts-version=react-scripts-ts
yarn eject

my-app
|
|--build
|--config
|--node_modules
|--public
|--scripts
|--src
    |--api
    |   |--config.ts
    |
    |--base
    |   |--better-scroll
    |   |   |--index.tsx
    |   |   |--css.less
    |   |
    |   |--slide-page
    |   |   |--index.tsx
    |   |   |--css.less
    |   |
    |   |--top-header
    |   |   |--index.tsx
    |   |   |--css.less
    |   |--......
    |--common
    |   |--fonts
    |   |   |--......
    |   |--js
    |   |   |--adaption.ts
    |   |   |--fetch-ajax.ts
    |   |   |--methods.ts
    |   |   |--......
    |   |--style
    |   |   |--base.less
    |   |   |--index.less
    |   |   |--public.less
    |   |   |--......
    |--components
    |   |--login
    |   |   |--index.tsx
    |   |   |--css.less
    |   |--my
    |   |   |--index.tsx
    |   |   |--css.less
    |   |   |--det
    |   |   |   |--index.tsx
    |   |   |   |--css.less
    |   |   |--......
    |   |--index.tsx
    |--store
    |   |--modules
    |   |   |--order.ts
    |   |   |--user.ts
    |   |   |--......
    |   |--index.ts
    |--index.tsx
    |--registerServiceWorker.ts
|--.gitignore
|--images.d.ts
|--package.json
|--README.md
|--tsconfig.json
|--tsconfig.prod.json
|--tsconfig.test.json
|--tslint.json
|--yarn.lock

AJAX統一封裝,公用組件跟業務組件分離,公共文件統一common接入,公用對象store,架構簡潔明瞭,如用的是react-router3,則建立單獨router目錄,進行扁平化管理。


書寫規範

一、最外層index.tsx的寫法


/* 調用模塊 */
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import { HashRouter, Route } from 'react-router-dom'
import registerServiceWorker from './registerServiceWorker'
......
/* 全局掛載 */
import 'common/js/adaption.ts' // rem 自適應
import 'store/index.ts' // redux window._STORE
import 'api/config.ts' // ajax window.API
......
/* 全局樣式 */
import 'antd/lib/notification/style/css'
import 'antd/lib/input/style/css'
import 'common/style/index.less' // 自定義全局樣式要在最後引入
/* 業務組件唯一入口 */
import App from './components/index'

declare global { // 定義暴露全局的屬性
  interface Window {
      API: any,
      _STORE: any
  }
}
/* 渲染 */
ReactDOM.render(
  <HashRouter>
      <Route path="/" component={ App } />
  </HashRouter>,
  document.getElementById('root') as HTMLElement
);
registerServiceWorker()

沒啥好說的,最外層的index.tsx必須保持純潔性


二、業務模塊寫法


/* 調用模塊 */
import * as React from "react"
import { Route } from "react-router-dom"
import { $getTimeStore } from "common/js/methods.ts"
/* 公用組件 */
import Tab from "base/tab/index"
import SlidePageRouter from "base/slide-page/index"
......
/* 業務組件 */
import My from "components/my/index"
import TakeOut from "components/takeOut/index"
import Login from 'components/login/index'
import TakeOutSeach from 'components/takeOut/seach/index'
import TakeOutDet from 'components/takeOut/det/index'
......

/* 入口對象類型定義 */
interface Props extends React.Props<any> {
  history: any
  ......
}

/* 唯一的模塊導出 */
export default class App extends React.Component<Props, any> {
  constructor(props: any) {
    super(props)
    this.state = { // 定義該App組件所有對象集
      'test': 123,
      '_tab': { // *定義公用模塊Tab
        'main': { // *定義公用模塊Tab的版本 --- 版本爲‘main’
          item: [ // *定義詳細參數
            {
              id: "food",
              name: "外賣",
              components: TakeOut
            },
            {
              id: "my",
              name: "我的",
              components: My
            }
          ],
          itemClick: (item: any, arr: any) => {
            // ......
          },
          itemSelect: "food"
        }
      },
      '_slidePage': { // *定義公用模塊slidePage
        'normal': {} // *定義公用模塊slidePage的版本 --- 版本爲‘normal’
      }
    }
    this['methods'] = { // *定義該App組件所有方法集
        test: () => {
            // ......
        }
    }
  }
  public render() {
    return (
      <div className="App">
        <Tab type={this.state._tab} />
        <SlidePageRouter type={this.state._slidePage}>
          <Route path="/login" component={ Login } />
          <Route path="/takeOutSeach" component={ TakeOutSeach } />
          <Route path="/takeOutDet/:val" component={ TakeOutDet } />
          ......
        </SlidePageRouter>
        ......
      </div>
    )
  }
}

什麼模塊就幹什麼事,組件公用對象就老老實實的在state裏定義,拒絕東一處西一處

公用組件傳參以版本式傳參,以type爲唯一參數入口,擁有更高的可讀性與維護性,下一例子說明

組件方法統一封裝至methods(其實定義在state裏也不是不可)

方法不掛在原形下,保持react生命週期的整潔性(這裏確實多多少少有被Vue影響)


三、公用組件寫法


/* top-header 公用組件 */
import * as React from 'react';
import { Input } from 'antd'
import { Link } from "react-router-dom"
......

/* 入口對象類型定義 */
interface Props extends React.Props<any> {
  type: any
  ......
}

/* 唯一的模塊導出 */
export default class Top extends React.Component<Props, any> {
  constructor (props: any) {
    super(props)
    require ('./css.less')
    const key = Object.keys(this.props.type)[0]
    this.state = {
      'key': key,
      'data': this.props.type[key]
    }
  }
  /**
   * 版本:normal
   * @param { String } left 'fa-angle-left'
   * @param { String } right  'fa-angle-left'
   * @param { String } title  '首頁'
   */
  public normal (state: any = { // 默認參數
    left: {
      icon: 'fa-angle-left',
      to: '/'
    },
    right: {
      icon: '',
      to: '/'
    },
    title: ''
  }) {
    return (
      <header className="Top-normal">
        { state.left && <Link to={ state.left.to } className={ `left fa-fw fa ${ state.left.icon }` } /> }
        <p className={ `title` }>{ state.title }</p>
        { state.right && <Link to={ state.right.to } className={ `right fa-fw fa ${ state.right.icon }` } /> }
      </header>
    )
  }
  /**
   * 版本:seach1
   * @param { String } left 'fa-angle-left'
   * @param { Function } call 'val => {}'
   */
  public seach1 (state: any = { // 默認參數
    left: {
      to: '/',
      icon: 'fa-angle-left'
    },
    call: (val: any) => (console.log(val))
  }) {
    return (
      <header className="Top-seach1">
        <Link to={ state.left.to } className={ `left fa-fw fa ${ state.left.icon }` } />
        <Input.Search
          placeholder="input search text"
          onSearch={ state.call }
          className="input"
        />
      </header>
    )
  }
  /**
   * 版本:seach2
   * @param { String } to '/takeOutSeach'
   */
  public seach2 (state: any = { // 默認參數
    left: {
      to: '/',
      icon: 'fa-angle-left'
    }
  }) {
    return (
      <header className="Top-seach2">
        <Link className="link" to={ state.to }>
          <Input.Search
            readOnly={ true }
            placeholder="click this seach"
            className="input"
          />
        </Link>
      </header>
    )
  }
  ......
  public render () {
    return this[this.state['key']] && this[this.state['key']](this.state['data'])
  }
}

所有公用模塊的constructor與render都是一樣的,可變的只有中間的版本,好處自行體會


四、路由3.0JS配置寫法


const PATH = (path) => (require('components/' + path +'.jsx').default)

export default [{
    path: '/',
    component: PATH('index'),
    childRoutes: [
        {
            path: 'login',
            component: PATH('login/index')
        }, {
            path: 'takeOutSeach',
            component: PATH('takeOut/seach/index')
        }, {
            path: 'takeOutDet/:val',
            component: PATH('takeOut/det/index'),
            childRoutes: [
                ......
            ]
        },
        ......
    ]
}]

能寫一遍別浪費時間寫第二遍


五、AJAX API統一封裝寫法


import { notification } from 'antd'
import { GET, POST } from 'common/js/fetch-ajax.ts' // 可對庫進行抉擇

// const FAKE = false // true:假數據 false:真數據

// const URL: string = 'http://192.168.0.103' // 測試服務器
// const URL: string = location.protocol + '//' + location.host + '/api' // 用於反代
const URL: string = 'http://XXX.XXX.XXX.XXX' // 正式服務器

const CODE_OK: number = 0
const CODE_ERR = (r: any, type?: boolean) => { // 失敗的回調
    notification[type ? 'warning' : 'error']({
        message: 'Notification Title',
        description: 'This is the content of the notification. This is the content of the notification. This is the content of the notification.',
    })
    console.error(r)
}
const CODE_IS = (r: any, fn: any) => (r.status === CODE_OK ? fn(r) : CODE_ERR(r, true))

// 可進行數據預處理,避免直接操作業務組件 --- 如果模塊複雜,多人協作開發的話,以下API模塊可寫成中間件導入

window['API'] = {
    'takeOut-getList' (fn: any) {
        GET(URL + '/tpadmin/public/index.php/api/user/cplist').then((res: any) => res.json()).then((res: any) => CODE_IS(res, fn)).catch((err: any) => CODE_ERR(err))
    },
    'login' (data: object, fn: any) {
        POST(URL + '/tpadmin/public/index.php/api/user/log', data).then((res: any) => res.json()).then((res: any) => {
            // 這兒可以進行過程控制,避免動業務組件
            ...... ? CODE_ERR(res, true) : fn(res)
        }).catch((err: any) => CODE_ERR(err))
    },
    ......
}

上面真/假數據,例子沒寫,其實是有必要存在的,有些時候就是會出現一些服務器掛掉或某個接口掛掉,後端來不及的情況下,產品要你假數據先塞上去

該細的地方細,該實用的時候就得簡單粗暴,API直接掛在window下,並不會損耗多少內存,反過來,你每個模塊都得引入一下,開發效率只會只低不增。


六、methods(utils)公用方法集規範


// localStorag - 存儲信息有效期
export function $setlocalStorag(
    name: string,
    value: any,
    timeout: number = 365 * 24 * 60 * 60 * 1000
) {
  let now: number = Date.now()
  timeout = now + timeout
  value = Object.assign(value, {
    'savedate': now,
    'timeout': timeout
  })
  localStorage.setItem(name, JSON.stringify(value))
}
// localStorag - 獲取信息有效期
export function $getTimeStore(name: string) {
  let getLocal: any = localStorage.getItem(name)
  let data: any = getLocal ? JSON.parse(getLocal) : {}
  let now: any = Date.now()
  if (data.timeout) {
    return data.timeout < now ? {} : data
  } else {
    return {}
  }
}
// localStorag - 刪除信息
export function $deleteStore(name: string) {
  localStorage.removeItem(name)
}
export function $id(id: string) {
  return document.getElementById(id)
}
export function $class(klass: string) {
  return document.getElementsByClassName(klass)
}
......

公用方法統一每個都export導出,import依賴注入,不要用定義對象的方式,最後用export { XX, XX, …… }導出,用過的都懂,反正都是進來ctrl+f搜索的

每個導出對象,都加個標識符,例如:‘$’,用於區分組件內的私有函數


七、公用Less的規範

/* index.less */
@import "./base.less";
@import "./public.less";

#root .App {
    overflow: hidden;
}

爲什麼只有兩個?你看目錄架構就知道了,每個人對模塊化的理念是不一樣的,而我覺得將css、image進行模塊化目錄,與業務組件一般無二的時候,我認爲是冗餘的

base爲初始化css,public爲全局css,起初我認爲全局變量也是必要的,將它配置至webpack中,但後來發現,全局變量根本沒全局屬性好使


八、關於Redux

這邊有些話要先說,因爲跟個人理念有關

redux的優勢很多,不過根據架構來看,有很多也是沒必要的。

  1. 所有數據緩存(用了router載入子節點的方式單頁,父子頁不需要)
  2. 組件狀態共享(遇到刷新重置的問題,需要瀏覽器緩存配合,需要)
  3. 作爲全局變量(需要全局的,直接掛在window了,不需要)

那這邊針對第二點,進行書寫

/* user.ts */
const type: string = 'user'
const data: object = {
    name: '',
    ......
}
export default function (
    state: object = data,
    action: any
) {
    return action.type !== type ? state : Object.assign({}, state, action.param)
}
/* index.ts */
import { combineReducers } from 'redux'
import { createStore } from 'redux'

import { $setlocalStorag } from 'common/js/methods.ts'

const PATH = (path: string) => (require(path + '.ts').default)

window._STORE = createStore(combineReducers({ // 中轉合併
    user: PATH('./modules/user'),
    order: PATH('./modules/order'),
    ......
}))
window._STORE.subscribe(() => { // 數據變動則自動存儲localStorag
    let state = window._STORE.getState()
    Object.keys(state).map(key => $setlocalStorag(key, state[key]))
})
/* put */
window._STORE.dispatch({
    type: 'user',
    param: {
        name: 'Hello World!',
        ......
    }
})
/* get*/
import { $getTimeStore } from "common/js/methods.ts"

console.log($getTimeStore("user").name) // Hello World!

因爲是localStorage+redux的配合,所以localStorage自帶一些API,別直接改

所有數據緩存,我認爲它很重要,無疑是既優化了UE,又優化了後端dataTimeOut的問題,但劣勢也很明顯,更加複雜化了工程,store層將會跟業務組件一樣擁有相同的目錄結構,它又無法與less一般嵌入過程式開發的業務組件中(因爲它還可能是公用組件數據),如果store分公用跟私用?那是否要抽離業務組件的state直接映射私有store,但事實上也無法完全抽離,有時還是會存在不存store的state,私有store就會有私有commit,方法層也得抽離成模塊,開發視圖會變的非常複雜……零零碎碎的模塊化開發與個人的組件式開發違背,不喜, 但優勢也確實存在,所以各有優劣,需取捨


關於

make:o︻そ╆OVE▅▅▅▆▇◤(清一色天空)

blog:http://blog.csdn.net/mcky_love

掘金:https://juejin.im/user/59fbe6c66fb9a045186a159a/posts

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章