❤ star me if you like concent ^_^
進化中的元件
隨著react 16.8
釋出了穩定版本的hook
特性,原來官網文件裡對SFC
的描述也修改為了FC
,即無狀態函式元件變更為了函式元件,官方代言人Dan Abramov
也在各種場合開始向社群力推hook
,將其解讀為下一個5年React與時俱進的開端。
仔細想想,其實hook
只是改變了我們組織程式碼的方式,因為hook
的存在,我們原來在類元件裡的各種套路都可以在函式元件裡找到一一對應的寫法,但是依託於class元件
建立起來一系列最佳實踐在hook元件
裡全部都要改寫,所以官方也是推薦如非必要,為了穩妥起見老專案裡依然使用class元件
。
任何新技術的出現一定都是有相關利益在驅動的,hook
也不例外,官網對hook
出現的動機給了3點重要解釋
- 在元件之間複用狀態邏輯很難
- 複雜元件變得難以理解
- 難以理解的 class
當然class元件
最為詬病的包裹地獄因為hook
獨特的實現方式被消除了,所以class元件
似乎在未來的日子裡將慢慢被冷落掉,而hook
本質只是一個個函式,對函數語言程式設計將變得更加友好,同時還能繼續推進組合大於繼承
的中心思想,讓更多的開發者受益於這種全新的開始思路並提升開發體驗。
按照官方的願意表達,Hook既擁抱了函式,同時也沒有犧牲 React 的精神原則,提供了問題的解決方案,無需學習複雜的函式式或響應式程式設計技術。
concent如何看待元件
前面有一句話提到「任何新技術的出現一定都是有相關利益在驅動的」,所以concent
的誕生的動機也是非常明確:
- 讓類元件和函式元件擁有完全一致的編碼思路和使用體驗
- 用最少的程式碼表達狀態共享、邏輯複用等問題
- 從元件層面搭建一個更優的最小化更新機制
- 增強元件,賦予元件更多的強大特性
上面提到的第一點其實說白了統一類元件和函式元件,得益於concent
能為元件注入例項上下文的執行機制,無論是從api
使用層面還是渲染結果層面,都將高度給你一致的體驗,所以在concent
眼裡,類與函式都是ui
表達的載體而已,不再區分對待它們,給使用者更多的選擇餘地。
那麼廢話少說,我們直接開整,看看concent
提供了多少種建立元件很更新狀態的方式。
在展示和解讀元件建立和狀態更新程式碼之前,我們先使用run
介面載入一個示例的業務model名為demo
,在以下程式碼結構處於models資料夾。
這裡一個示例專案檔案組織結構,不同的人可能有不同的理解方式和組織習慣,這裡只是以一個基本上社群上公認的通用結構作為範本來為後面的程式碼解讀做基礎,實際的檔案元件方式使用者可以根據自己的情況做調節
|____runConcent.js # concent啟動指令碼
|____App.css
|____index.js # 專案的入口檔案
|____models # 業務models
| |____index.js
| |____demo # [[demo模組定義]]
| | |____reducer.js # 更新狀態(可選)
| | |____index.js # 負責匯出demo model
| | |____computed.js # 定義計算函式(可選)
| | |____init.js # 定義非同步的狀態初始化函式(可選)
| | |____state.js # 定義初始狀態(必需)
| |____...
|
|____components # [[基礎元件]]
| |____layout # 佈局元件
| |____bizsmart # 業務邏輯元件(可以含有自己的model)
| |____bizdumb # 業務展示元件
| |____smart # 平臺邏輯元件(可以含有自己的model)
| |____pure # 平臺展示元件
|
|____assets # 會被一起打包的資原始檔
|____pages # 路由對應的頁面元件(可以含有自己的model,即page model)
| |____...
| |
|____App.js
|____base
| |____config # 配置
| |____constant # 常量
|____services # 業務相關服務
|____utils # 通用工具函式
複製程式碼
demo的state定義
export function getInitialState(){
return {
name: 'hello, concent',
age: 19,
visible: true,
infos: [],
}
}
export default getInitialState();
複製程式碼
使用run
介面載入模組定義
// code in runConcent.js
import models from 'models';
import { run } from 'concent';
run(models);
複製程式碼
對以上例項程式碼有疑問可以參考往期文章:
聊一聊狀態管理&Concent設計理念
使用concent,體驗一把漸進式地重構react應用之旅
或者直接檢視官網文件瞭解更多細節
建立類元件
使用register
介面直接將一個普通類元件註冊為concent類元件
import { register } from 'concent';
import React, { Component } from 'react';
@register('demo')
export default class ClassComp extends Component {
render() {
const { name, age, visible, infos } = this.state;
return <div>...your ui</div>
}
}
複製程式碼
是的你沒看錯,這就完成了concent類元件的註冊,它屬於demo
模組,state裡將自動注入demo
模組的所有資料,讓我們把它渲染出來,看看結果
function App() {
return (
<div>
<ClassComp />
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
複製程式碼
開啟ReactDevTool
檢視dom結構
可以看到頂層沒有任何Provider
,資料直接打入元件內部,同時元件本身沒有任何包裹,只有一層,因為預設採用反向繼承的hoc策略,你的渲染的元件不再產生大量Wrapper Hell
...
或許有小夥伴會問這樣會不會打破了hoc
模式的約定,因為大家都是使用屬性代理方式來做元件修飾,不破壞元件原有的任何結構,同時還能複用邏輯,可是這裡我們需要多思考一下,如果邏輯複用不一定非要從屬性上穿透下來,而是直接能從例項上下文裡提供,那為何我們非要墨守成規的使用屬性代理的hoc模式呢?
當然concent對於類的修飾雖然預設使用了反向繼承,但是也允許使用者使用屬性代理,只需要開啟一個標記即可
@register({ module: 'demo', isPropsProxy: true })
export default class ClassComp extends Component{
constructor(props, context){
super(props, context);
this.props.$$attach(this);// 屬性代理模式需要補上這句話,方便包裹層接管元件this
}
render(){
const {name, age, visible, infos} = this.state;
return <div>...your ui</div>
}
}
複製程式碼
顯而易見的,我們發現已經多了一層包裹,之所以提供isPropsProxy
引數,是因為有些元件用到了多重灌飾器的用法,所以為了不破壞多重灌飾器下的使用方式而提供,但大多數時候,你都應該忘記這種用法,讓react dom樹保持乾淨清爽何樂而不為呢?
圖中我們看到元件名時$$CcClass1
,這是一個當使用者沒有顯示指定元件名時,concent
自己起的名字,大多數時候我們可以給一個與目標包裹元件同名的名字作為concent元件的名字
//第二個可選引數是concent元件名
@register('demo', 'ClassComp')
export default class ClassComp extends Component{...}
複製程式碼
建立CcFragment元件
CcFragment
是concent提供的內建元件,可以讓你不用定義和註冊元件,而是直接在檢視裡宣告一個元件例項來完成快速消費某個模組資料的例項。
我們在剛才的App
裡直接宣告一個檢視消費demo
模組的資料
function App() {
return (
<div>
<ClassComp />
<CcFragment register="demo" render={ctx => {
const { name, age, visible, infos } = ctx.state;
return <div>...your ui</div>
}} />
</div>
);
}
複製程式碼
渲染結果如下圖所示:
CcFragment
採用的是Render Props
方式來書寫元件,特別適合一些臨時多個模組資料的檢視片段
<CcFragment register={{connect:['bar', 'baz']}} render={ctx => {
// 該片段連線了bar,baz兩個模組,消費它們的資料
const { bar, baz } = ctx.connectedState;
return <div>...your ui</div>
}} />
複製程式碼
基於registerDumb
建立元件
使用者通常在某些場合會基於CcFragment
做經一步的封裝來滿足一些高緯抽象的需求,concent本身也提供了一個介面registerDumb
來建立元件,它本質上是CcFragment
的淺封裝
const MyFragment = registerDumb('demo', 'MyFragment')(ctx=>{
const { name, age, visible, infos } = ctx.state;
return <div>...I am MyFragment</div>
})
複製程式碼
渲染結果如下圖所示:
可以看到react dom tree上,出現了3層結構,最裡面層是無狀態元件例項。基於hook建立元件
雖然registerDumb
寫起來像函式元件了,但實際上出現了3層結構不是我們希望看到的,我們來使用hook
方式重構此元件吧,concent提供了useConcent
介面來建立元件,抹平類元件與函式元件之間的差異性。
function HookComp(){
const ctx = useConcent('demo', 'HookComp');
const { name, age, visible, infos } = ctx.state;
return <div>...I am HookComp</div>
}
複製程式碼
渲染結果如下圖所示:
基於registerHookComp
建立元件
registerHookComp
本質上是useConcent
的淺封裝,自動幫你使用React.memo
包裹
const MemoHookComp = registerHookComp({
module:'demo',
render: ctx=>{
const { name, age, visible, infos } = ctx.state;
return <div>...I am MemoHookComp</div>
}
});
複製程式碼
渲染結果圖裡我們可以看到tag上有一個Memo
,那是React.memo
包裹元件後DevTool
的顯示結果。
concent如何看待狀態更新
上面的所有元件示例裡,我們都只是完成的模組狀態的獲取和展示,並沒有做任何更新操作,接下來我們將對元件加入狀態更新操作行為。
利用setState
完成狀態更新
因為concent已接管了setState
行為,所以對於使用者來說,setState
就可以完成你想要的狀態更新與狀態同步。
在替換
setState
前,concent會保持一份引用reactSetState
指向原始的setState
,所以你大可不必擔心setState
會影響react
的各種新特性諸如fiber 排程
,time slicing
,非同步渲染
等,因為concent
只是利用接管setState
後完成自己的狀態分發排程工作,本身是不會去破壞或者影響react
自身的排程機制。
// 改寫ClassComp
@register('demo')
export default class ClassComp extends Component {
changeName = (e)=> this.setState({name:e.currentTarget.value})
render() {
const { name } = this.state;
return <input value={name} onChange={this.changeName} />
}
}
複製程式碼
// 改寫ClassComp
<CcFragment register="demo" render={ctx => {
const changeName = (e)=> ctx.setState({name:e.currentTarget.value});
const { name, age, visible, infos } = ctx.state;
return <input value={name} onChange={changeName} />
}} />
複製程式碼
// 改寫MyFragment
registerDumb('demo', 'MyFragment')(ctx=>{
const changeName = (e)=> ctx.setState({name:e.currentTarget.value});
const { name, age, visible, infos } = ctx.state;
return <input value={name} onChange={changeName} />
})
複製程式碼
// 改寫HookComp
function HookComp(){
const ctx = useConcent('demo', 'HookComp');
const { name, age, visible, infos } = ctx.state;
const changeName = (e)=> ctx.setState({name:e.currentTarget.value});
return <input value={name} onChange={changeName} />
}
複製程式碼
// 改寫MemoHookComp
const MemoHookComp = registerHookComp({
module:'demo',
render: ctx=>{
const { name, age, visible, infos } = ctx.state;
const changeName = (e)=> ctx.setState({name:e.currentTarget.value});
return <input value={name} onChange={changeName} />
}
});
複製程式碼
可以看到,所以的元件都是一樣的寫法,不同的是類元件還存在著一個this
關鍵字,而在函式元件裡都交給ctx
去操作了。
現在讓我們通過gif圖演示看看實際效果吧
因為這些例項都是屬於demo
模組的元件,所以無論我修改任何一處,其他地方檢視都會同步被更新,是不是將特別方便呢?
使用sync
更新
當然如果對於這種單個key的更新,我們也可以不用寫setState
,而是直接使用concent提供的工具函式sync
來完成值的提取與更新
// 改寫HookComp使用sync來更新,其他元件寫法都一樣,class元件通過this.ctx.sync來更新
function HookComp(){
const ctx = useConcent('demo', 'HookComp');
const {state: { name, age, visible, infos }, sync } = ctx.state;
return <input value={name} onChange={sync('name')} />
}
複製程式碼
使用dispatch
更新
當我們的業務邏輯複雜的時候,在真正更新之前要做很多資料的處理工作,這時我們可以將其抽到reducer
// 定義reducer,code in models/demo/reducer.js
export updateName(name, moduleState, actionCtx){
return {name, loading: false};
}
export updateNameComplex(name, moduleState, actionCtx){
// concent會自動在reducer檔案內生成一個名為setState的reducer函式,免去使用者宣告一次
await actionCtx.setState({loading:true});
await api.updateName(name);
// 在同一個reducer檔案類的函式,可以直接基於函式引用呼叫
await actionCtx.dispatch(updateName, name);
}
複製程式碼
在元件內部使用dispatch
觸發更新
function HookComp(){
const ctx = useConcent('demo', 'HookComp');
const { name, age, visible, infos } = ctx.state;
const updateNameComplex = (e)=>ctx.dispatch('updateNameComplex', e.currentTarget.value);
return <input value={name} onChange={updateNameComplex} />
}
複製程式碼
當然,這裡有更優的寫法,使用setup
靜態的定義相關介面。瞭解更多關於setup
const setup = ctx=>{
//這裡其實還可以搞更多的事兒,諸如ctx.computed, ctx.watch, ctx.effect 等,下期再聊✧(≖ ◡ ≖✿)
return {
updateNameComplex: (e)=>ctx.dispatch('updateNameComplex',e.currentTarget.value),
}
}
function HookComp(){
// setup只會在元件初次渲染之前觸發一次!
const ctx = useConcent({module:'demo', setup}, 'HookComp');
const { name, age, visible, infos } = ctx.state;
return <input value={name} onChange={ctx.settings.updateNameComplex} />
}
複製程式碼
使用invoke
更新
invoke
給予使用者更自由的靈活程度來更新檢視資料,因為本質來說concent的reducer函式就是一個個片段狀態生成函式,所以invoke
讓使用者可以不需要走dispatch
套路來更新資料。
因為reducer定義是跟著model走的,為了規範起見,實際編碼過程中定義reducer函式比invoke更能夠統一資料更新流程,很方便檢視和排除bug。
function updateName(name, moduleState, actionCtx){
return {name, loading: false};
}
function updateNameComplex(name, moduleState, actionCtx){
await actionCtx.setState({loading:true});
await api.updateName(name);
await actionCtx.invoke(updateName, name);
}
const setup = ctx=>{
return {
updateNameComplex: (e)=>ctx.invoke(updateNameComplex,e.currentTarget.value),
}
}
function HookComp(){
const ctx = useConcent({module:'demo', setup}, 'HookComp');
const { name, age, visible, infos } = ctx.state;
return <input value={name} onChange={ctx.settings.updateNameComplex} />
}
複製程式碼
結語
通過以上示例,讀者應該能體會到統一類元件和函式元件的好處,那就是滿足你任何時段漸進式的書寫你的應用,無論是元件的定義方式和資料的修改方式,你都可以按需採取不同的策略,而且concent裡的hook使用方式是遵循著reducer
承載核心業務邏輯,dispatch
派發修改狀態的經典組織程式碼方式的,但是並沒有強制約束你一定要怎麼寫,給予了你最大的自由度和靈活度,沉澱你個人的最佳實踐,甚至你可以通過修改少量的程式碼來100%複製社群裡現有的公認最佳實踐到你的concent應用。
(下2期預告:1 探究setup帶來的變革;2 concent love typescript,期望讀者多多支援,concent,衝鴨,to be the apple of your eyes)