说在前头

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

Logo

前往低代码交流专区

更多推荐