使用concent,體驗一把漸進式地重構react應用之旅

傳統的redux項目裏,咱們寫在reducer裏的狀態必定是要打通到store的,咱們一開始就要規劃好state、reducer等定義,有沒有什麼方法,既可以快速享受ui與邏輯分離的福利,又不須要照本宣科的從條條框框開始呢?本文從普通的react寫法開始,當你一個收到一個需求後,腦海裏有了組件大體的接口定義,而後絲滑般的接入到concent世界裏,感覺漸進式的快感以及全新api的獨有魅力吧!react

需求來了

上週天氣其實不是很好,記得下了好幾場雨,不過北京總部大廈的隔音太好了,以至於都沒有感覺到外面的搖搖欲墜,在工位上正在思索着整理下現有代碼時,接到一個普通的需求,大體是要實現一個彈窗。git

  • 左側有一個可選字段列表,點擊任意一個字段,就會進入右側。
  • 右側有一個已選字段列表,該列表能夠上下拖拽決定字段順序決定表格裏的列字段顯示順序,同時也能夠刪除,將其恢復到可選擇列表。
  • 點擊保存,將用戶的字段配置存儲到後端,用戶下次再次使用查看該表格時,使用已配置的顯示字段來展現。

這是一個很是普通的需求,我相信很多碼神看完後,腦海裏已經把代碼雛形大體寫完了吧,嘿嘿,可是還請耐性看完本篇文章,來看看在concent的加持下,你的react應用將如何變得更加靈活與美妙,正如咱們的slogan:es6

concent, power your reactgithub

準備工做

產品同窗指望快速見到通常效果原型,而我但願原型是能夠持續重構和迭代的基礎代碼,固然要認真對待了,不能爲了交差而亂寫一版,因此要快速整理需求並開始準備工做了。chrome

由於項目大量基於antd來書寫UI,聽完需求後,腦海裏冒出了一個穿梭框模樣的組件,但由於右側是一個可拖拽列表,查閱了下沒有相似的組件,那就本身實現一個吧,初步整理下,大概列出瞭如下思路。redux

  • 組件命名爲ColumnConfModal,基於antdModal, Card實現佈局,antdList來實現左側的選擇列表,基於react-beautiful-dnd的可拖拽api來實現右側的拖拽列表。

ui佈局

  • 由於這個彈窗組件在不一樣頁面被不一樣的table使用,傳入的列定義數據是不同的,因此咱們使用事件的方式,來觸發打開彈窗並傳遞表格id,打開彈窗後獲取該表格的全部字段定義,以及用戶針對表哥的已選擇字段數據,這樣把表格元數據的初始化工做收斂在ColumnConfModal內部。
  • 基於表格左右兩側的交互,大體定義一下內部接口 1 moveToSelectedList(移入到已選擇列表 ) 2 moveToSelectableList(移入到可選擇列表) 3 saveSelectedList(保存用戶的已選擇列表) 4 handleDragEnd(處理已選擇列表順序調整完成時) 5 其餘略.....

UI 實現

由於註冊爲concent組件後天生擁有了emit&on的能力,並且不須要手動offconcent在實例銷燬前自動就幫你解除其事件監聽,因此咱們能夠註冊完成後,很方便的監聽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來暴露concentreact帶來的新特性api。數組

消滅生命週期函數

由於事件的監聽只須要執行一次,因此例子中咱們在componentDidMount裏完成了事件openColumnConf的監聽註冊。bash

根據需求,顯然的咱們還要在這裏書寫獲取表格列定義元數據和獲取用戶的個性化列定義數據的業務邏輯

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裏的effectapi語法是否是和useEffect有點像?

effectuseEffect的執行時機是同樣的,即每次組件渲染完畢以後,可是effect只須要在setup調用一次,至關因而靜態的,更具備性能提高空間,假設咱們加一個需求,每次vibible變爲false時,上報後端一個操做日誌,就能夠寫爲

//依賴列表填入key的名稱,表示當這個key的值發生變化時,觸發反作用
    ctx.effect( ctx=>{
      if(!ctx.state.visible){
        //當前最新的visible已經是false,上報
      }
    }, ['visible']);
複製代碼

關於effect就點到爲止,說得太多扯不完了,咱們繼續回到本文的組件上。

提高狀態到store

咱們但願組件的狀態變動能夠被記錄下來,方便觀察數據變化,so,咱們先定義一個store的子模塊,名爲ColumnConf

定義其sate爲

// code in ColumnConfModal/model/state.js
export function getInitialState() {
  return {
    selectedColumnKeys: [],
    selectableColumnKeys: [],
	visible: false,
  };
}

export default getInitialState();
複製代碼

而後利用concentconfigure接口載入此配置

// 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各個模塊當前的最新數據。

window.sss

而後咱們把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插件,來輔助作狀態變動記錄的,小夥伴們千萬不要誤會,沒有reduxconcent同樣可以正常運做,可是因爲concent提供完善的插件機制,爲啥不利用社區現有的優秀資源呢,重複造無心義的輪子很辛苦滴(⊙﹏⊙)b......

如今讓咱們打開chrome的redux插件看看效果吧。

state tree

上圖裏是含有大量的ccApi/setState,是由於還有很多邏輯沒有抽離到reducerdispatch/***模樣的type就是dispatch調用了,後面咱們會提到。

這樣看狀態變遷是否是要比window.sss好多了,由於sss只能看當前最新的狀態。

這裏既然提到了redux-dev-tool,咱們就順道簡單瞭解下,concent提交的數據長什麼樣子吧

action

上圖裏能夠看到5個字段,renderKey是用於提升性能用的,能夠先不做了解,這裏咱們就說說其餘四個,module表示修改的數據所屬的模塊名,committedState表示提交的狀態,sharedState表示共享到store的狀態,ccUniqueKey表示觸發數據修改的實例id。

爲何要區分committedStatesharedState呢?由於setState調用時容許提交本身的私有key的(即沒有在模塊裏聲明的key),因此committedState是整個狀態都要再次派發給調用者,而sharedState是同步到store後,派發給同屬於module值的其餘cc組件實例的。

這裏就借用官網一張圖示意下:

cc-core

因此咱們能夠在組件裏聲明其餘非模塊的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;
  }
}
複製代碼

解耦業務邏輯與UI

雖然代碼可以正常工做,狀態也接入了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() {
}
複製代碼

利用concentconfigure接口把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;
  }
}
複製代碼

愛class,愛hook,讓二者和諧共處

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減少的壓力是顯而易見的。

因爲二者的寫法高度一致,從classHook是否是很是的天然呢?咱們其實不須要爭論該用誰更好了,按照你的我的喜愛就能夠,就算某天你看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能夠點這裏,更多在線示例列表點這裏

因爲本篇主題主要是介紹漸進式重構組件,因此其餘特性諸如synccomputed$watch、高性能殺手鐗renderKey等等內容就不在這裏展開講解了,留到下一篇文章,敬請期待。

若是看官以爲喜歡,就來點顆星星唄,concent致力於爲react帶來全新的編碼體驗和功能強化,敬請期待更多的特性和生態周邊。

相關文章
相關標籤/搜索