傳統的redux項目裏,咱們寫在reducer裏的狀態必定是要打通到store的,咱們一開始就要規劃好state、reducer等定義,有沒有什麼方法,既可以快速享受ui與邏輯分離的福利,又不須要照本宣科的從條條框框開始呢?本文從普通的react寫法開始,當你一個收到一個需求後,腦海裏有了組件大體的接口定義,而後絲滑般的接入到concent世界裏,感覺漸進式的快感以及全新api的獨有魅力吧!
上週天氣其實不是很好,記得下了好幾場雨,不過北京總部大廈的隔音太好了,以至於都沒有感覺到外面的搖搖欲墜,在工位上正在思索着整理下現有代碼時,接到一個普通的需求,大體是要實現一個彈窗。react
這是一個很是普通的需求,我相信很多碼神看完後,腦海裏已經把代碼雛形大體寫完了吧,嘿嘿,可是還請耐性看完本篇文章,來看看在concent的加持下,你的react
應用將如何變得更加靈活與美妙,正如咱們的slogan: git
concent, power your reactes6
產品同窗指望快速見到通常效果原型,而我但願原型是能夠持續重構和迭代的基礎代碼,固然要認真對待了,不能爲了交差而亂寫一版,因此要快速整理需求並開始準備工做了。github
由於項目大量基於antd
來書寫UI,聽完需求後,腦海裏冒出了一個穿梭框模樣的組件,但由於右側是一個可拖拽列表,查閱了下沒有相似的組件,那就本身實現一個吧,初步整理下,大概列出瞭如下思路。chrome
ColumnConfModal
,基於antd
的Modal
, Card
實現佈局,antd
的List
來實現左側的選擇列表,基於react-beautiful-dnd
的可拖拽api來實現右側的拖拽列表。
ColumnConfModal
內部。1 moveToSelectedList(移入到已選擇列表 )
2 moveToSelectableList(移入到可選擇列表)
3 saveSelectedList(保存用戶的已選擇列表)
4 handleDragEnd(處理已選擇列表順序調整完成時)
5 其餘略.....redux
由於註冊爲concent
組件後天生擁有了emit&on
的能力,並且不須要手動off
,concent
在實例銷燬前自動就幫你解除其事件監聽,因此咱們能夠註冊完成後,很方便的監聽openColumnConf
事件了。後端
咱們先拋棄各類store和reducer定義,快速的基於class
擼出一個原型,利用register
接口將普通組件註冊爲concent
組件,僞代碼以下api
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)
能夠發現,這個類的內部和傳統的react
類寫法並沒有區別,惟一的區別是concent
會爲每個實例注入一個上下文對象ctx
來暴露concent
爲react
帶來的新特性api。數組
由於事件的監聽只須要執行一次,因此例子中咱們在componentDidMount
裏完成了事件openColumnConf
的監聽註冊。antd
根據需求,顯然的咱們還要在這裏書寫獲取表格列定義元數據和獲取用戶的個性化列定義數據的業務邏輯
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監聽,在組件掛載完畢後開始真正監聽on事件 ctx.on('openColumnConf', () => { this.setState({ visible: true }); }); //標記依賴列表爲空數組,在組件初次渲染只執行一次 //模擬componentDidMount ctx.effect(()=>{ //service call balabala..... }, []); }
若是已熟悉hook
的同窗,看到setup
裏的effect
api語法是否是和useEffect
有點像?
effect
和useEffect
的執行時機是同樣的,即每次組件渲染完畢以後,可是effect
只須要在setup
調用一次,至關因而靜態的,更具備性能提高空間,假設咱們加一個需求,每次vibible
變爲false時,上報後端一個操做日誌,就能夠寫爲
//依賴列表填入key的名稱,表示當這個key的值發生變化時,觸發反作用 ctx.effect( ctx=>{ if(!ctx.state.visible){ //當前最新的visible已經是false,上報 } }, ['visible']);
關於effect
就點到爲止,說得太多扯不完了,咱們繼續回到本文的組件上。
咱們但願組件的狀態變動能夠被記錄下來,方便觀察數據變化,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
裏的業務邏輯。
整個store
已經被concent
掛載到了window.sss
下,爲了方便查看store,噹噹噹當,你能夠打開console,直接查看store
各個模塊當前的最新數據。
而後咱們把class註冊爲'配置模ColumnConf
的組件,如今class
裏的state聲明能夠直接被咱們幹掉了。
import './model';//引用一下model文件,觸發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
插件,來輔助作狀態變動記錄的,小夥伴們千萬不要誤會,沒有redux
,concent
同樣可以正常運做,可是因爲concent
提供完善的插件機制,爲啥不利用社區現有的優秀資源呢,重複造無心義的輪子很辛苦滴(⊙﹏⊙)b......
如今讓咱們打開chrome
的redux插件看看效果吧。
上圖裏是含有大量的ccApi/setState,是由於還有很多邏輯沒有抽離到reducer
,dispatch/***
模樣的type就是dispatch
調用了,後面咱們會提到。
這樣看狀態變遷是否是要比window.sss
好多了,由於sss
只能看當前最新的狀態。
這裏既然提到了redux-dev-tool
,咱們就順道簡單瞭解下,concent提交的數據長什麼樣子吧
上圖裏能夠看到5個字段,renderKey
是用於提升性能用的,能夠先不做了解,這裏咱們就說說其餘四個,module
表示修改的數據所屬的模塊名,committedState
表示提交的狀態,sharedState
表示共享到store
的狀態,ccUniqueKey
表示觸發數據修改的實例id。
爲何要區分committedState
和sharedState
呢?由於setState
調用時容許提交本身的私有key的(即沒有在模塊裏聲明的key),因此committedState
是整個狀態都要再次派發給調用者,而sharedState
是同步到store
後,派發給同屬於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; } }
雖然代碼可以正常工做,狀態也接入了store,可是咱們發現class已經變得臃腫不堪了,利用setState
懟當然快和方便,可是後期維護和迭代的代價就會慢慢愈來愈大,讓咱們把業務抽到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監聽,在組件掛載完畢後開始真正監聽on事件 ctx.on('openColumnConf', () => { this.setState({ visible: true }); }); //標記依賴列表爲空數組,在組件初次渲染只執行一次 //模擬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; } }
react
社區轟轟烈烈推進了Hook
革命,讓你們逐步用Hook
組件代替class
組件,可是本質上Hook
逃離了this
,精簡了dom
渲染層級,可是也帶來了組件存在期間大量的臨時匿名閉包重複建立。
來看看concent
怎麼解決這個問題的吧,上面已提到setup
支持返回結果,將被收集在settiings
裏,如今讓稍微的調整下代碼,將class
組件吧變身爲Hook
組件吧。
import { useConcent } from 'concent'; const setup = (ctx) => { //這裏定義on監聽,在組件掛載完畢後開始真正監聽on事件 ctx.on('openColumnConf', (tid) => { ctx.setState({ visible: true, tid }); }); //標記依賴列表爲空數組,在組件初次渲染只執行一次 //模擬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
時,固然須要觸發這個事件打開其彈窗了。
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
裏,專門用來打開各類彈窗,而避免在業務見到openColumnConfModal
這個常量字符串
//code in service/modal.js import { emit } from 'concent'; export function openColumnConfModal(tid) { emit('openColumnConfModal', tid); }
如今能夠這樣使用組件來觸發事件調用了
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
帶來全新的編碼體驗和功能強化,敬請期待更多的特性和生態周邊。