concent漸進式重構react應用使用詳解
正文
傳統的redux項目里,我們寫在reducer里的狀態(tài)一定是要打通到store的,我們一開始就要規(guī)劃好state、reducer等定義,有沒有什么方法,既能夠快速享受ui與邏輯分離的福利,又不需要照本宣科的從條條框框開始呢?本文從普通的react寫法開始,當你一個收到一個需求后,腦海里有了組件大致的接口定義,然后絲滑般的接入到concent世界里,感受漸進式的快感以及全新api的獨有魅力吧!

需求來了
上周天氣其實不是很好,記得下了好幾場雨,不過北京總部大廈的隔音太好了,以致于都沒有感受到外面的風雨飄搖,在工位上正在思索著整理下現有代碼時,接到一個普通的需求,大致是要實現一個彈窗。
- 左側有一個可選字段列表,點擊任意一個字段,就會進入右側。
- 右側有一個已選字段列表,該列表可以上下拖拽決定字段順序決定表格里的列字段顯示順序,同時也可以刪除,將其恢復到可選擇列表。
- 點擊保存,將用戶的字段配置存儲到后端,用戶下次再次使用查看該表格時,使用已配置的顯示字段來展示。
這是一個非常普通的需求,我相信不少碼神看完后,腦海里已經把代碼雛形大致寫完了吧,嘿嘿,但是還請耐性看完本篇文章,來看看在concent的加持下,你的react應用將如何變得更加靈活與美妙,正如我們的slogan:
concent, power your react
準備工作
產品同學期望快速見到一般效果原型,而我希望原型是可以持續(xù)重構和迭代的基礎代碼,當然要認真對待了,不能為了交差而亂寫一版,所以要快速整理需求并開始準備工作了。
因為項目大量基于antd來書寫UI,聽完需求后,腦海里冒出了一個穿梭框模樣的組件,但因為右側是一個可拖拽列表,查閱了下沒有類似的組件,那就自己實現一個吧,初步整理下,大概列出了以下思路。
- 組件命名為
ColumnConfModal,基于antd的Modal,Card實現布局,antd的List來實現左側的選擇列表,基于react-beautiful-dnd的可拖拽api來實現右側的拖拽列表。

- 因為這個彈窗組件在不同頁面被不同的table使用,傳入的列定義數據是不一樣的,所以我們使用事件的方式,來觸發(fā)打開彈窗并傳遞表格id,打開彈窗后獲取該表格的所有字段定義,以及用戶針對表哥的已選擇字段數據,這樣把表格元數據的初始化工作收斂在
ColumnConfModal內部。 - 基于表格左右兩側的交互,大致定義一下內部接口 1 moveToSelectedList(移入到已選擇列表 ) 2 moveToSelectableList(移入到可選擇列表) 3 saveSelectedList(保存用戶的已選擇列表) 4 handleDragEnd(處理已選擇列表順序調整完成時) 5 其他略.....
UI 實現
因為注冊為concent組件后天生擁有了emit&on的能力,而且不需要手動off,concent在實例銷毀前自動就幫你解除其事件監(jiān)聽,所以我們可以注冊完成后,很方便的監(jiān)聽openColumnConf事件了。
我們先拋棄各種store和reducer定義,快速的基于class擼出一個原型,利用register接口將普通組件注冊為concent組件,偽代碼如下
import { register } from 'concent';
class ColumnConfModal extends React.Component {
state = {
selectedColumnKeys: [],
selectableColumnKeys: [],
visible: false,
};
componentDidMount(){
this.ctx.on('openColumnConf', ()=>{
this.setState({visible:true});
});
}
moveToSelectedList = ()=>{
//code here
}
moveToSelectableList = ()=>{
//code here
}
saveSelectedList = ()=>{
//code here
}
handleDragEnd = ()=>{
//code here
}
render(){
const {selectedColumnKeys, selectableColumnKeys, visible} = this.state;
return (
<Modal title="設置顯示字段" visible={state._visible} onCancel={settings.closeModal}>
<Head />
<Card title="可選字段">
<List dataSource={selectableColumnKeys} render={item=>{
//...code here
}}/>
</Card>
<Card title="已選字段">
<DraggableList dataSource={selectedColumnKeys} onDragEnd={this.handleDragEnd}/>
</Card>
</Modal>
);
}
}
// es6裝飾器還處于實驗階段,這里就直接包裹類了
// 等同于在class上@register( )來裝飾類
export default register( )(ColumnConfModal)可以發(fā)現,這個類的內部和傳統的react類寫法并無區(qū)別,唯一的區(qū)別是concent會為每一個實例注入一個上下文對象ctx來暴露concent為react帶來的新特性api。
消滅生命周期函數
因為事件的監(jiān)聽只需要執(zhí)行一次,所以例子中我們在componentDidMount里完成了事件openColumnConf的監(jiān)聽注冊。
根據需求,顯然的我們還要在這里書寫獲取表格列定義元數據和獲取用戶的個性化列定義數據的業(yè)務邏輯
componentDidMount() {
this.ctx.on('openColumnConf', () => {
this.setState({ visible: true });
});
const tableId = this.props.tid;
tableService.getColumnMeta(`/getMeta/${tableId}`, (columns) => {
userService.getUserColumns(`/getUserColumns/${tableId}`, (userColumns) => {
//根據columns userColumns 計算selectedList selectableList
});
});
}所有的concent實例可以定義setup鉤子函數,該函數只會在初次渲染前調用一次。
現在讓我們來用setup代替掉此生命周期
//class 里定義的setup加$$前綴
$$setup(ctx){
//這里定義on監(jiān)聽,在組件掛載完畢后開始真正監(jiān)聽on事件
ctx.on('openColumnConf', () => {
this.setState({ visible: true });
});
//標記依賴列表為空數組,在組件初次渲染只執(zhí)行一次
//模擬componentDidMount
ctx.effect(()=>{
//service call balabala.....
}, []);
}如果已熟悉hook的同學,看到setup里的effectapi語法是不是和useEffect有點像?
effect和useEffect的執(zhí)行時機是一樣的,即每次組件渲染完畢之后,但是effect只需要在setup調用一次,相當于是靜態(tài)的,更具有性能提升空間,假設我們加一個需求,每次vibible變?yōu)閒alse時,上報后端一個操作日志,就可以寫為
//依賴列表填入key的名稱,表示當這個key的值發(fā)生變化時,觸發(fā)副作用
ctx.effect( ctx=>{
if(!ctx.state.visible){
//當前最新的visible已是false,上報
}
}, ['visible']);關于effect就點到為止,說得太多扯不完了,我們繼續(xù)回到本文的組件上。
提升狀態(tài)到store
我們希望組件的狀態(tài)變更可以被記錄下來,方便觀察數據變化,so,我們先定義一個store的子模塊,名為ColumnConf,
定義其sate為
// code in ColumnConfModal/model/state.js
export function getInitialState() {
return {
selectedColumnKeys: [],
selectableColumnKeys: [],
visible: false,
};
}
export default getInitialState();然后利用concent的configure接口載入此配置
// code in ColumnConfModal/model/index.js
import { configure } from 'concent';
import state from './state';
// 配置模塊ColumnConf
configure('ColumnConf', {
state,
});注意這里,讓model跟著組件定義走,方便我們維護model里的業(yè)務邏輯。
整個store已經被concent掛載到了window.sss下,為了方便查看store,當當當當,你可以打開console,直接查看store各個模塊當前的最新數據。

然后我們把class注冊為'配置模ColumnConf的組件,現在class里的state聲明可以直接被我們干掉了。
import './model';//引用一下model文件,觸發(fā)model配置到concent
@register('ColumnConf')
class ColumnConfModal extends React.Component {
// state = {
// selectedColumnKeys: [],
// selectableColumnKeys: [],
// visible: false,
// };
render(){
const {selectedColumnKeys, selectableColumnKeys, visible} = this.state;
}
}大家可能注意到了,這樣暴力的注釋掉,render里的代碼會不會出問題?放心吧,不會的,concent組件的state和store是天生打通的,同樣的setState也是和store打通的,我們先來安裝一個插件concent-plugin-redux-devtool。
import ReduxDevToolPlugin from 'concent-plugin-redux-devtool';
import { run } from 'concent';
// storeConfig配置略,詳情可參考concent官網
run(storeConfig, {
plugins: [ ReduxDevToolPlugin ]
});注意哦,concent驅動ui渲染的原理和redux完全不一樣的,核心邏輯部分也不是在redux之上做包裝,和redux一點關系都沒有的^_^,這里只是橋接了redux-dev-tool插件,來輔助做狀態(tài)變更記錄的,小伙伴們千萬不要誤會,沒有redux,concent一樣能夠正常運作,但是由于concent提供完善的插件機制,為啥不利用社區(qū)現有的優(yōu)秀資源呢,重復造無意義的輪子很辛苦滴(⊙﹏⊙)b......
現在讓我們打開chrome的redux插件看看效果吧。

上圖里是含有大量的ccApi/setState,是因為還有不少邏輯沒有抽離到reducer,dispatch/***模樣的type就是dispatch調用了,后面我們會提到。
這樣看狀態(tài)變遷是不是要比window.sss好多了,因為sss只能看當前最新的狀態(tài)。
這里既然提到了redux-dev-tool,我們就順道簡單了解下,concent提交的數據長什么樣子吧

上圖里可以看到5個字段,renderKey是用于提高性能用的,可以先不作了解,這里我們就說說其他四個,module表示修改的數據所屬的模塊名,committedState表示提交的狀態(tài),sharedState表示共享到store的狀態(tài),ccUniqueKey表示觸發(fā)數據修改的實例id。
為什么要區(qū)分committedState和sharedState呢?因為setState調用時允許提交自己的私有key的(即沒有在模塊里聲明的key),所以committedState是整個狀態(tài)都要再次派發(fā)給調用者,而sharedState是同步到store后,派發(fā)給同屬于module值的其他cc組件實例的。
這里就借用官網一張圖示意下:

所以我們可以在組件里聲明其他非模塊的key,然后在this.state里獲取到了
@register('ColumnConf')
class ColumnConfModal extends React.Component {
state = {
_myPrivKey:'i am a private field value, not for store',
};
render(){
//這里同時取到了模塊的數據和私有的數據
const {selectedColumnKeys, selectableColumnKeys, visible, _myPrivKey} = this.state;
}
}解耦業(yè)務邏輯與UI
雖然代碼能夠正常工作,狀態(tài)也接入了store,但是我們發(fā)現class已經變得臃腫不堪了,利用setState懟固然快和方便,但是后期維護和迭代的代價就會慢慢越來越大,讓我們把業(yè)務抽到reduder吧
export function setLoading(loading) {
return { loading };
};
/** 移入到已選擇列表 */
export function moveToSelectedList() {
}
/** 移入到可選擇列表 */
export function moveToSelectableList() {
}
/** 初始化列表 */
export async function initSelectedList(tableId, moduleState, ctx) {
//這里可以不用基于字符串 ctx.dispatch('setLoading', true) 去調用了,雖然這樣寫也是有效的
await ctx.dispatch(setLoading, true);
const columnMeta = await tableService..getColumnMeta(`/getMeta/${tableId}`);
const userColumsn = await userService.getUserColumns(`/getUserColumns/${tableId}`);
//計算 selectedColumnKeys selectableColumnKeys 略
//僅返回需要設置到模塊的片斷state就可以了
return { loading: false, selectedColumnKeys, selectableColumnKeys };
}
/** 保存已選擇列表 */
export async function saveSelectedList(tableId, moduleState, ctx) {
}
export function handleDragEnd() {
}利用concent的configure接口把reducer也配置進去
// code in ColumnConfModal/model/index.js
import { configure } from 'concent';
import * as reducer from 'reducer';
import state from './state';
// 配置模塊ColumnConf
configure('ColumnConf', {
state,
reducer,
});還記得上面的setup嗎,setup可以返回一個對象,返回結果將收集在settiings里,現在我們稍作修改,然后來看看class吧,世界是不是清靜多了呢?
import { register } from 'concent';
class ColumnConfModal extends React.Component {
$$setup(ctx) {
//這里定義on監(jiān)聽,在組件掛載完畢后開始真正監(jiān)聽on事件
ctx.on('openColumnConf', () => {
this.setState({ visible: true });
});
//標記依賴列表為空數組,在組件初次渲染只執(zhí)行一次
//模擬componentDidMount
ctx.effect(() => {
ctx.dispatch('initSelectedList', this.props.tid);
}, []);
return {
moveToSelectedList: (payload) => {
ctx.dispatch('moveToSelectedList', payload);
},
moveToSelectableList: (payload) => {
ctx.dispatch('moveToSelectableList', payload);
},
saveSelectedList: (payload) => {
ctx.dispatch('saveSelectedList', payload);
},
handleDragEnd: (payload) => {
ctx.dispatch('handleDragEnd', payload);
}
}
}
render() {
//從settings里取出這些方法
const { moveToSelectedList, moveToSelectableList, saveSelectedList, handleDragEnd } = this.ctx.settings;
}
}愛class,愛hook,讓兩者和諧共處
react社區(qū)轟轟烈烈推動了Hook,讓大家逐步用Hook組件代替class組件,但是本質上Hook逃離了this,精簡了dom渲染層級,但是也帶來了組件存在期間大量的臨時匿名閉包重復創(chuàng)建。
來看看concent怎么解決這個問題的吧,上面已提到setup支持返回結果,將被收集在settiings里,現在讓稍微的調整下代碼,將class組件吧變身為Hook組件吧。
import { useConcent } from 'concent';
const setup = (ctx) => {
//這里定義on監(jiān)聽,在組件掛載完畢后開始真正監(jiān)聽on事件
ctx.on('openColumnConf', (tid) => {
ctx.setState({ visible: true, tid });
});
//標記依賴列表為空數組,在組件初次渲染只執(zhí)行一次
//模擬componentDidMount
ctx.effect(() => {
ctx.dispatch('initSelectedList', ctx.state.tid);
}, []);
return {
moveToSelectedList: (payload) => {
ctx.dispatch('moveToSelectedList', payload);
},
moveToSelectableList: (payload) => {
ctx.dispatch('moveToSelectableList', payload);
},
saveSelectedList: (payload) => {
ctx.dispatch('saveSelectedList', payload);
},
handleDragEnd: (payload) => {
ctx.dispatch('handleDragEnd', payload);
}
}
}
const iState = { _myPrivKey: 'myPrivate state', tid:null };
export function ColumnConfModal() {
const ctx = useConcent({ module: 'ColumnConf', setup, state: iState });
const { moveToSelectedList, moveToSelectableList, saveSelectedList, handleDragEnd } = ctx.settings;
const { selectedColumnKeys, selectableColumnKeys, visible, _myPrivKey } = ctx.state;
// return your ui
}在這里要感謝尤雨溪老師的這篇Vue Function-based API RFC,給了我很大的靈感,現在你可以看到所以的方法的都在setup里定義完成,當你的組件很多的時候,給gc減小的壓力是顯而易見的。
由于兩者的寫法高度一致,從class到Hook是不是非常的自然呢?我們其實不需要爭論該用誰更好了,按照你的個人喜好就可以,就算某天你看class不順眼了,在concent的代碼風格下,重構的代價幾乎為0。
使用組件
上面我們定義了一個on事件openColumnConf,那么我們在其他頁面里引用組件ColumnConfModal時,當然需要觸發(fā)這個事件打開其彈窗了。
import { emit } from 'concent';
class Foo extends React.Component {
openColumnConfModal = () => {
//如果這個類是一個concent組件
this.ctx.emit('openColumnConfModal', 3);
//如果不是則可以調用頂層api emit
emit('openColumnConfModal', 3);
}
render() {
return (
<div>
<button onClick={this.openColumnConfModal}>配置可見字段</button>
<Table />
<ColumnConfModal />
</div>
);
}
}上述寫法里,如果有其他很多頁面都需要引入ColumnConfModal,都需要寫一個openColumnConfModal,我們可以把這個打開邏輯抽象到modalService里,專門用來打開各種彈窗,而避免在業(yè)務見到openColumnConfModal這個常量字符串
//code in service/modal.js
import { emit } from 'concent';
export function openColumnConfModal(tid) {
emit('openColumnConfModal', tid);
}現在可以這樣使用組件來觸發(fā)事件調用了
import * as modalService from 'service/modal';
class Foo extends React.Component {
openColumnConfModal = () => {
modalService.openColumnConfModal(6);
}
render() {
return (
<div>
<button onClick={this.openColumnConfModal}>配置可見字段</button>
<Table />
<ColumnConfModal />
</div>
);
}
}結語
以上代碼在任何一個階段都是有效的,想要了解漸進式重構的在線demo可以點這里
由于本篇主題主要是介紹漸進式重構組件,所以其他特性諸如sync、computed$watch、高性能殺手锏renderKey等等內容就不在這里展開講解了,更多關于concent重構react的資料請關注腳本之家其它相關文章!
相關文章
使用webpack搭建react開發(fā)環(huán)境的方法
本篇文章主要介紹了使用webpack搭建react開發(fā)環(huán)境的方法,在這篇文章中我們開始利用我們之前所學搭建一個簡易的React開發(fā)環(huán)境,用以鞏固我們之前學習的Webpack知識。一起跟隨小編過來看看吧2018-05-05
React函數組件useContext useReducer自定義hooks
這篇文章主要為大家介紹了React函數組件useContext useReducer自定義hooks示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-08-08
React Native中WebView與html雙向通信遇到的坑
這篇文章主要介紹了React Native中WebView與html雙向通信的一些問題,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習吧2023-01-01
react?hooks?d3實現企查查股權穿透圖結構圖效果詳解
這篇文章主要為大家介紹了react?hooks?d3實現企查查股權穿透圖結構圖效果詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-01-01

