說在前頭
React因爲jsx的模式,比Vue的寫法更多,更雜亂,但勝在社區廣,開發者多,作爲Facebook爲後盾的開發者團隊們,更是底蘊深厚,出了一套又一套的擴展插件,理念也各不相同,有react-router3/4扁平化結構與過程式開發的碰撞,有css in js與傳統less、sass等的交錯,確實,他們讓react的羽翼更加豐滿,選擇上更加自由,不過如此帶來的代價就是規範上無法統一,在此編寫react書寫規範,屬個人規範性文章,僅供參考。
技術選型
- react:^16.5.0
- react-router:^4.3.1(也含有3.0JS配置項規範)
- whatwg-fetch:2.0.3
- redux:^4.0.0
- react-transition-group:^2.4.0
- antd:^3.9.2
- typescript:^3.0.3
- less:^3.8.1
- tslint:^5.7.0
- better-scroll:^1.12.6
- 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的優勢很多,不過根據架構來看,有很多也是沒必要的。
- 所有數據緩存(用了router載入子節點的方式單頁,父子頁不需要)
- 組件狀態共享(遇到刷新重置的問題,需要瀏覽器緩存配合,需要)
- 作爲全局變量(需要全局的,直接掛在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▅▅▅▆▇◤(清一色天空)