日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網(wǎng)為廣大站長提供免費(fèi)收錄網(wǎng)站服務(wù),提交前請(qǐng)做好本站友鏈:【 網(wǎng)站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(wù)(50元/站),

點(diǎn)擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會(huì)員:747

來自前端備忘錄 - 江湖術(shù)士[1]

https://hqwuzhaoyi.github.io/2021/01/14/74.HookDDD/

領(lǐng)域驅(qū)動(dòng),各自只管各自的模塊,頂層再來進(jìn)行組裝和分配

  • 堅(jiān)持根據(jù)特性區(qū)命名目錄。
  • 堅(jiān)持為每個(gè)特性區(qū)創(chuàng)建一個(gè) NgModule。能提供限界上下文,將某些功能牢牢地鎖在一個(gè)地方,開發(fā)某個(gè)功能時(shí),只需要關(guān)心這個(gè)模塊就夠了。

視圖的歸試圖,邏輯的歸邏輯

function SomeComponent() {
  const someService = useService();
  return <div>{someService.state}</div>;
}

跨組件數(shù)據(jù)傳遞?

function useGlobalService() {
  return { state: "" };
}
const GlobalService = createContext(null);

function SomeComponent() {
  return (
    <GlobalService.Provider value={useGlobalService()}></GlobalService.Provider>
  );
}
function useSomeService() {
  const globalService = useContext(GlobalService);
  return <div>{globalService.state}</div>;
}

上下文注入節(jié)點(diǎn),本身就是按照試圖來的

函數(shù) DDD

只用函數(shù)實(shí)現(xiàn) DDD,它有多優(yōu)美

我們先比較一下這兩種寫法,對(duì)于一個(gè)類:

class SomeClass {
  name:string,
  password:string,
  constructor(name,password){
    this.name = name
    this.password = password
  }
}
const initValue = { name: "", password: "" };
function useClass() {
  const [state, setState] = useState(initValue);
  return { state, setState };
}

下面的自帶響應(yīng)式,getter,setter 也自動(dòng)給出了,同時(shí)使用了工廠模式,不需要了解函數(shù)內(nèi)部的邏輯。

生命周期復(fù)用

每個(gè) useFunc 都是 拆掉 的管道函數(shù),框架幫你組裝,簡直就是一步到位!

效率

function useSomeService() {
  const [form] = useForm();
  const request = useRequest();
  const model = useModel();
  useEffect(() => {
    form.onFieldsChange = () => {
      request.run(form.getFieldsValue);
    };
  }, [form]);
  return {
    model,
    form,
  };
}
<Form form={someService.form}>
   <Form.Item name="xxx" label="xxx">
      <!-- 沒你service啥事了!別看!這里是純視圖 -->
  </Form.Item>
</Form>

這個(gè)表單服務(wù)你想要在哪控制?

想要多個(gè)組件同時(shí)控制?

加個(gè) token,也就是 createContext,把依賴提上去!

他特么自然了!

React Hooks 版本架構(gòu)

執(zhí)行 LIFT 原則

  • 頂層文件夾最多包含:assets,pages,layouts,App 四個(gè)(其中 pages,layouts 是為了照顧某些 ssr 開發(fā)棧),名字可以變更,但是不可以有多余文件夾,激進(jìn)的話可以只有一個(gè) app 文件夾
  • 按功能劃分文件夾,每個(gè)功能只能包含以下四種文件:Xxx.less, Xxx.tsx, useXxx.ts,useXxx.spec.ts , 采用嵌套結(jié)構(gòu)組織
  • 一個(gè)文件夾包含該領(lǐng)域內(nèi)所有邏輯(視圖,樣式,測試,狀態(tài),接口),禁止將邏輯放置于文件夾以外
  • 如果需要由其他功能調(diào)用,利用 SOA 反轉(zhuǎn)為何如此?
  • 功能結(jié)構(gòu)即文件結(jié)構(gòu),開發(fā)人員可以快速定位代碼,掃一眼就能知道每個(gè)文件代表什么,目錄盡可能保持扁平,既沒有重復(fù)也沒有多余的名字
  • 當(dāng)有很多文件時(shí)(例如 10 個(gè)以上),在專用目錄型結(jié)構(gòu)中定位它們會(huì)比在扁平結(jié)構(gòu)中更容易
  • 惰性加載可路由的功能變得更容易
  • 隔離、測試和復(fù)用特性更容易
  • 管理上,相關(guān)領(lǐng)域文件夾可以分配給專人,開發(fā)效率高,可追責(zé)和計(jì)量工作量,很明顯應(yīng)該禁止多人同時(shí)操作同一層級(jí)文件
  • 只需要對(duì) useXxx 進(jìn)行測試,測試復(fù)雜度,工作量都很小,視圖測試交給 e2e

利用 SOA 實(shí)現(xiàn)跨組件邏輯復(fù)用

利用 注入令牌 + 服務(wù)函數(shù) + 注入點(diǎn),實(shí)現(xiàn)靈活的 SOA

命名格式為

XxxService = useToken(useXxxService)

XxxService 為注入令牌 和文件名

useXxxService 為服務(wù)函數(shù)

<XxxService.Provider value={useXxxService()} />

XxxService.Provider 為注入點(diǎn)

注入令牌與服務(wù)函數(shù)緊挨

與注入節(jié)點(diǎn)處于同一文件結(jié)構(gòu)層級(jí)

禁止除 SOA 以外的所有數(shù)據(jù)源


為何如此?

  • 符合單一數(shù)據(jù),單以職責(zé),接口隔離原則
  • 通過泛型約束,可以有更加自然的 Typescript 體驗(yàn),不需要手動(dòng)聲明注入數(shù)據(jù)類型,所有類型將自動(dòng)獲得
  • 層次化注入,可以實(shí)現(xiàn) DDD,將邏輯全部約束與一處,方便團(tuán)隊(duì)協(xié)作
  • 當(dāng)你在根注入器上提供該服務(wù)時(shí),該服務(wù)實(shí)例在每個(gè)需要該服務(wù)的組件和服務(wù)中都是共享的。當(dāng)服務(wù)要共享方法或狀態(tài)時(shí),這是數(shù)學(xué)意義上的最理想的選擇。
  • 配合組件和功能劃分,可以方便處理嵌套結(jié)構(gòu),防止對(duì)象復(fù)制被濫用,類似深復(fù)制之類的操作應(yīng)該禁止

實(shí)現(xiàn)一個(gè) IOC 注入令牌的方法為

import { createContext } from 'react';

/**
 * 泛型約束獲取注入令牌
 *
 * @export
 * @template T
 * @param {(...args: any[]) => T} func
 * @param {(T | undefined)} [initialValue=undefined]
 * @returns
 */
export default function useToken<T>(
  func: (...args: any[]) => T,
  initialValue: T | undefined = undefined,
) {
  return createContext(initialValue as T);
}

一個(gè)典型可注冊(cè)服務(wù)為:

import { useState } from "react";
import useToken from "./useToken";

export const AppService = useToken(useAppService);

export default function useAppService() {
  // 可注入其他服務(wù),以嵌套
  // eq:
  // const someOtherService = useSomeOtherService(SomeOtherService)
  const [appName, setAppName] = useState("appName");
  return {
    appName,
    setAppName,
    // ...
  };
}

最小權(quán)限

人為保證代碼結(jié)構(gòu)種,各個(gè)組成之間的最小權(quán)限,是一個(gè)好習(xí)慣

  • 所有大寫字母開頭的 tsx 文件都是組件
  • 所有 use 開頭的文件,都是服務(wù),其中,useXxxService 是可注入服務(wù),默認(rèn)將所有組件配套的服務(wù)設(shè)置為可注入服務(wù),可以方便進(jìn)行依賴管理
  • 禁止在組件函數(shù)種出現(xiàn)任何非服務(wù)注入代碼,禁止在組件中寫入與視圖不想關(guān)的
  • 為復(fù)雜結(jié)構(gòu)數(shù)據(jù)定義 class
  • 如果可以的話,將單例服務(wù)由全局 service 組織,嵌套結(jié)構(gòu),共享實(shí)例,頁面初始化 除外
  • ? 禁止深復(fù)制

為何如此?

  • 當(dāng)邏輯被放置到服務(wù)里,并以函數(shù)的形式暴露時(shí),可以被多個(gè)組件重復(fù)使用
  • 在單元測試時(shí),服務(wù)里的邏輯更容易被隔離。當(dāng)組件中調(diào)用邏輯時(shí),也很容易被模擬
  • 從組件移除依賴并隱藏實(shí)現(xiàn)細(xì)節(jié)
  • 保持組件苗條、精簡和聚焦
  • 利用 class 可以減少初始化復(fù)雜度,以及因此產(chǎn)生的類型問題
  • 局管理單例服務(wù),可以一步消滅循環(huán)依賴問題(道理同 Redux 替代 Flux)
  • 深復(fù)制有非常嚴(yán)重的性能問題,且容易產(chǎn)生意外變更,尤其是 useEffect 語境下

JUST USE REACT HOOKS

拋棄 class 這樣的,this 掛載變更的歷史方案,不可復(fù)用組件會(huì)污染整個(gè)項(xiàng)目,導(dǎo)致邏輯無法集中于一處,甚至出現(xiàn)耦合, LIFT,SOA,DDD 等架構(gòu)無從談起

項(xiàng)目只存在

  • 大寫并與文件同名的組件,且其中除了注入服務(wù)操作外,return 之前,無任何代碼
  • use 開頭并與文件夾同名的服務(wù)
  • use 開頭,Service 結(jié)尾,并與文件夾同名的可注入服務(wù)
  • 服務(wù)中只存在 基礎(chǔ) hooks,自定義 hooks,第三方 hooks,靜態(tài)數(shù)據(jù),工具函數(shù),工具類

以下為細(xì)化闡述為何如此設(shè)計(jì)的出發(fā)點(diǎn)

  • 快速定位 Locate
  • 一眼識(shí)別 Identify
  • 盡量保持扁平結(jié)構(gòu) (Flattest)
  • 嘗試 Try 遵循 DRY (Do Not Repeat Yourself, 不重復(fù)自己)

此為 LIFT 原則

  • 優(yōu)先將組件視為元素,而并非功能邏輯單位(視圖的歸視圖,業(yè)務(wù)的歸業(yè)務(wù))
  • 隔離原則(屬于一個(gè)成員的工作,必定屬于該成員負(fù)責(zé)的文件夾,也只能屬于該成員負(fù)責(zé)的文件夾)
  • 最小依賴(禁止不必要的工具使用,比如當(dāng)前需求下,引入 Redux/Flux/Dva/Mobx 等工具,并沒有解決什么問題,卻導(dǎo)致功能更加受限,影響隔離原則比如當(dāng)兩個(gè)組件需要服務(wù)的不同實(shí)例的情況,以上工具屬于上個(gè)版本或某種特殊需求,比如前后端同構(gòu),不能影響這個(gè)版本當(dāng)前需求的架構(gòu))
  • 優(yōu)先響應(yīng)式(普及管道風(fēng)格的函數(shù)式方案,大膽使用 useEffect 等 api,不提倡松散的函數(shù)組合,只要是視圖所用的數(shù)據(jù),必須全部都為響應(yīng)式數(shù)據(jù),并響應(yīng)變更)
  • 測試友好(邊界清晰,風(fēng)格簡潔,隔離完整,即為測試友好)
  • 設(shè)計(jì)友好(支持模塊化設(shè)計(jì))

建議的技術(shù)棧搭配

  • create-react-app + react-router-dom + antd + ahooks + styled-components (大多數(shù)場景下,強(qiáng)烈推薦!可以上 ProComponent,但是要注意提取功能邏輯,不可將邏輯寫于組件)
  • umi + ahooks (請(qǐng)刪除 models,services,components,utils 等非必要頂層文件夾,禁止使用 dva)
  • umi (ssr) + dva + ahooks(同上,但可僅基于 dva 溝通前后端和首屏數(shù)據(jù),非 ssr 同樣禁用 dva)
  • next.js + react suite/material ui + swr(利用不到 useAntdTable 之類的功能,ahooks 就雞肋了)

Hook 使你在無需修改組件結(jié)構(gòu)的情況下復(fù)用狀態(tài)邏輯

當(dāng)你思維聚焦于組件時(shí),在這種情況下,你是必須逼迫自己,在組件里寫業(yè)務(wù)邏輯,或者重新組織業(yè)務(wù)邏輯!

并且,因?yàn)?state 是不反應(yīng)業(yè)務(wù)邏輯的,它也天然不可以對(duì)業(yè)務(wù)邏輯進(jìn)行組合

function useSomeTable() {
  // 這個(gè)是個(gè)表單,抽象的
  const [form] = Form.useForm();
  // 這個(gè)是個(gè)表單聯(lián)動(dòng)的表格
  const { tableProps, loading } = useAntdTable(
    // 自動(dòng)處理分頁相關(guān)問題
    ({ curren, pageSize }, formData) => fetch("http://sdfdsfsdf"), // 抽象的狀態(tài)請(qǐng)求
    {
      form, // 表單在這里與表格組合,實(shí)現(xiàn)聯(lián)動(dòng)
      defaultParams: {
        // ...
      },
      // 很多功能都能集成,只需要一個(gè)配置項(xiàng)
      debounceInterval: 300, // 節(jié)流
    }
  );
  return {
    form,
    loading,
    tableProps,
  };
}
<Form form={SomeTable.form}><!--里面全部狀態(tài)無關(guān),不用看了--></Form>
<Table {...tableProps} columns={} rowKey="id"/>

這個(gè)組件,存粹只有注入和視圖,一丁點(diǎn)的邏輯都沒有

組件里沒有邏輯, 相關(guān)的邏輯再 useFunc 種就能隨意組合,結(jié)構(gòu)什么都都能你來定,結(jié)構(gòu)如果優(yōu)秀,任何邏輯都是 use 個(gè)函數(shù)的問題,你不會(huì)出現(xiàn)需要寫第二遍邏輯的情況,通過組件的 props 進(jìn)行分流(map + key),你能夠非常優(yōu)雅地處理嵌套復(fù)雜結(jié)構(gòu)

你能將視圖和邏輯完全組織為一個(gè)結(jié)構(gòu),交給一個(gè)特定的人,完全不用關(guān)心他到底是怎么開發(fā)的

這便是 —— 邏輯視圖分離


React SOA

基本的服務(wù)

function useSimpleService() {
  const [val1, setVal1] = useState(0);
  const [val2, setVal2] = useState(0);

  useEffect(() => {
    setVal2(val1);
  }, [val1]);

  return {
    val1,
    setVal1,
    val2,
  };
}
  • 叫它 service,是 SOA 模型下的管用叫法,意思是 —— 我只會(huì)在這樣的結(jié)構(gòu)種寫邏輯,組件中的邏輯全部消失(優(yōu)先將組件視為元素)
  • 只暴露你需要暴露的狀態(tài)邏輯(狀態(tài)邏輯必須一起說,只做狀態(tài)復(fù)用很扯淡,畢竟 2021 年了)
  • useRef,同樣也可以封裝在 Service 中,而且建議如此做,ref 的獲取不是視圖,是邏輯

組合服務(wù)

有另外一個(gè)服務(wù),useAnotherService

function userAnotherService() {
  const [val, setVal] = useLocalstorage(0);
  return { vale, setVal };
}

然后與基本服務(wù)進(jìn)行組合

function useSimpleService() {
  const [val1, setVal1] = useState(0);
  const [val2, setVal2] = useState(0);
  const { setVal } = userAnotherService();

  useEffect(() => {
    setVal2(val1);
  }, [val1]);

  useEffect(() => {
    setVal(val2);
  }, [val2]);

  return {
    val1,
    setVal1,
    val2,
  };
}

就能為基本服務(wù)動(dòng)態(tài)添加功能

  • 為什么不直接 import?因?yàn)樾枰蚣軆?nèi)的響應(yīng)式能力,這個(gè)叫控制反轉(zhuǎn),框架將響應(yīng)式的控制權(quán)轉(zhuǎn)交給了開發(fā)者
  • 如果有另外一個(gè)服務(wù),單單只要 AnotherService 的功能,你只需要調(diào)用 useAnthor Service 就好了
  • 最好是調(diào)用者修改被調(diào)用者,可以對(duì)比 ahooks 對(duì)代 useRef 的改動(dòng),就是本著這個(gè)次序,因?yàn)楸徽{(diào)用者可能被多次調(diào)用,保證復(fù)用性
  • useEffect 是一種管道模型,如同 rxjs 一般,只是框架幫你按順序組裝而已(你以為為啥非要你按順序來?),是極限的函數(shù)式方案,不存在純度問題,函數(shù)式得不要不要的。但是有個(gè)要求,依賴必須寫清楚,這個(gè)依賴是管道操作中的參數(shù),React 將你的 hook 重新組合成了管道,但是參數(shù)必須提供,在它能自動(dòng)分析依賴之前
  • 使用了 useAnotherService 的細(xì)節(jié)被隱藏,形成了一個(gè)樹形調(diào)用結(jié)構(gòu),這種結(jié)構(gòu)被稱作 “依賴樹” 或者 “注入樹”,別盯著我,名字不是我定的

注入單例服務(wù)

當(dāng)前服務(wù)如果需要被多個(gè)組件使用,服務(wù)會(huì)被初始化很多次,如何讓它只注入一次?

利用 createContext

export const SimpleService = createContext(null);
export default function useSimpleService() {
  // ...
}

但是,單例需要注入到唯一節(jié)點(diǎn),因此,你需要在所有需要用到這個(gè)服務(wù)的組件的最頂層:

<SimpleService.Provider value={useSimpleService()}>
  {props.children}
</SimpleService.Provider>

這樣,這個(gè)服務(wù)的單例就對(duì)所有子孫組件敞開了懷抱,同時(shí),所有子孫組件對(duì)其的修改都將生效

function SomeComponent(){
  const {val1,setVal1} = useContext(SomeService)
  return <div onClick={()=>{setVal1('fuck')}>val1</div>
}
  • 直接在 jsx 的 provider 種 value = {useSomeService ()} 在本組件沒有任何其它響應(yīng)式變量的情況下是可行的,因?yàn)椴粫?huì)重新初始化,在良好的架構(gòu)下 —— 組件除注入,無任何邏輯,return 之前沒有東西,同時(shí),上下文單獨(dú)封裝組件,可以作為 “模塊標(biāo)識(shí)”
  • 這個(gè)有共同單例 Service 的一系列組件,被稱為模塊,它們有自己的 “限界上下文”,并且,視圖,邏輯,樣式都在其中,如果這個(gè)模塊是按照功能劃分的,那么這種 SOA 實(shí)現(xiàn)被稱為 領(lǐng)域驅(qū)動(dòng)設(shè)計(jì) (DDD) ,某些架構(gòu)強(qiáng)推的所謂’微前端’,目的就是得到這個(gè)東西
  • 一定要注意,這個(gè)模塊的上層數(shù)據(jù)變更,模塊的限界上下文會(huì)刷新,這個(gè)是默認(rèn)操作,這也是為何 jsx 直接賦值 的原因,如果你不需要這個(gè)東西,可以采用 const value = useService () 包裹,或者直接 memo 這個(gè)模塊標(biāo)識(shí)組件

單例服務(wù),解決深層嵌套對(duì)象問題

深層嵌套對(duì)象怎么處理?useReducer?immutable? 還是直接深復(fù)制?

你首先明白你要實(shí)現(xiàn)什么邏輯,深層嵌套對(duì)象之所以難處理,是因?yàn)槟阆朐谧咏M件實(shí)現(xiàn) 對(duì)深層目標(biāo)的部分變更邏輯

之前你之所以有這些奇奇怪怪工具甚至深復(fù)制的需求,是因?yàn)槟銢]有辦法將邏輯也拆分給子組件,明白為什么如此

現(xiàn)在,邏輯可以拆分復(fù)用:

function useSomeService() {
  const [value, setValue] = useState({
    username: "",
    password: "",
    info: {
      nickname: "",
      others: [],
    },
  });
  return { value, setValue };
}
// 注入部分省略...

修改 info:

setValue((res) => {
  res.info.nickname === "fuck";
  return res;
});

配合 map 修改數(shù)組:

// 分形部分:
new Array(5).map((_, key) => <SomeCompo index={key} key={key} />);
// 組件
function SomeComponent(props) {
  const { setValue } = useContext(SomeService);
  return (
    <div
      onClick={() => {
        setValue((res) => {
          res.info.others[props.index] = "fuck";
          return res;
        });
      }}
    ></div>
  );
}

如果需要?jiǎng)澐帜K,通過 getter,setter 傳遞這個(gè)嵌套結(jié)構(gòu):

function subInjectedService() {
  const { value, setValue } = useContext(SomeService);
  const info = useMemo(() => value.info.others, [value]);
  const setInfo = useCallback((val) => {
    setValue((res) => {
      res.info.others[props.index] = val;
      return res;
    });
  }, []);
  return {
    info,
    setInfo,
  };
}
// 忽略注入部分...

這樣的話,這個(gè)重新劃分的模塊內(nèi)部,想要修改上層的數(shù)據(jù),只需要通過 info,setInfo 即可

  • 不用擔(dān)心純度和不變性的問題,因?yàn)?hooks 都是純的,沒有不純的情況
  • 全局副作用是狀態(tài) + 函數(shù)全局邏輯封裝(分層)考慮的問題,將函數(shù)和組件,視圖功能邏輯樣式全部作為模塊,副作用是以模塊為單位的,而 info 和 setInfo 的 getter,setter 封裝,叫做 —— 模塊間通訊
  • useReducer 只涉及調(diào)試,也就是有個(gè) action 名字方便你定位問題,模塊劃分如果足夠細(xì),你根本不需要這個(gè) action 來記錄你的變更,采用 useReducer 與 DDD 原則背離,但是也不會(huì)禁止。不過,全局 useReducer 必須明令禁止,這種方式是個(gè)災(zāi)難,useReducer 必須是以模塊為單位,不能更小,也不能更大
  • 組件和服務(wù)一起,處理一部分?jǐn)?shù)據(jù),保證了單例修改,不變性也不用擔(dān)心,hooks 來保證這個(gè)
  • 在這里,你會(huì)發(fā)現(xiàn) props 的功能好像只有’分形’,也就是 map 種將數(shù)據(jù)的標(biāo)識(shí)傳遞給子組件,是的 —— 優(yōu)先使用服務(wù)共享狀態(tài)邏輯
  • getter,setter 叫做響應(yīng)式,如果你不需要響應(yīng)式修改,setter 可以刪除,但是 getter 同時(shí)還有防止重新渲染的作用,保留即可,除非純組件

服務(wù)獲取時(shí)的類型問題

如果你使用的是 Typescript ,那么,用泛型約束獲得自動(dòng)類型推斷,會(huì)讓你如虎添翼

import { createContext } from 'react';
/**
 * 泛型約束獲取注入令牌
 *
 * @export
 * @template T
 * @param {(...args: any[]) => T} func
 * @param {(T | undefined)} [initialValue=undefined]
 * @returns
 */
export default function useToken<T>(
  func: (...args: any[]) => T,
  initialValue: T | undefined = undefined,
) {
  return createContext(initialValue as T);
}

然后將 createContext() 改為 useToken(SomeService) 即可,這樣你就擁有了指哪打哪的類型支持,無需單獨(dú)的類型聲明,代碼更加清爽

  • 如果是 JAVAscript 環(huán)境,建議老老實(shí)實(shí)寫 createContext 的 defaultValue,雖然注入之后,子孫組件都不會(huì)出現(xiàn) defaultValue,但是 JavaScript 語境下有代碼提示
  • 不建議 typescript 下聲明 defaultValue,因?yàn)槟K外的服務(wù)調(diào)用,應(yīng)該被禁止,這是 DDD 架構(gòu)的基礎(chǔ),如果你想要在外部使用單例服務(wù) —— 請(qǐng)將其提升至外部

頂層注入服務(wù)

平凡提升模塊服務(wù)層級(jí),可能會(huì)產(chǎn)生循環(huán)依賴,而且會(huì)影響模塊的封裝度,因此:

??優(yōu)先思考清楚自己應(yīng)用的模塊關(guān)系!

循環(huán)依賴產(chǎn)生根源是功能領(lǐng)域,功能模塊劃分有問題,優(yōu)先解決根本問題,而不是轉(zhuǎn)移矛盾。如果你實(shí)在思考不清楚,又想要立刻開始開發(fā),那么可以嘗試頂層注入服務(wù):

function useAppService(){
  return {
    someService:useSomeService()
    anotherService:useAnotherService()
  }
}
  • 模塊間進(jìn)行嵌套組合將變得無比困難,不再是一個(gè) getter,setter 能夠搞定的,如果不是絕對(duì)的必要,盡量不要采用此種方式!它有悖于 DDD 原則 —— 分治
  • 多組件共享不同實(shí)例將徹底失敗,這不是你愿意看到的

可選服務(wù)

模塊服務(wù)劃分的另一個(gè)巨大優(yōu)勢(shì),就是將邏輯變?yōu)榭蛇x項(xiàng),這在重型應(yīng)用中,幾乎就是采用 DDD 的關(guān)鍵

function useServiceByOneLogic() {
  return {
    activated,
    // ...
  };
}
function useServiceByAnotherLogic() {
  return {
    activated,
    // ...
  };
}
function useSomeService() {
  const [...servicList] = [useServiceByOneLogic(), useServiceByAnotherLogic()];
  // 選擇激活的服務(wù)
  const usedService = useMemo(() => {
    for (let service of serviceList) {
      if (service.activated === true) {
        return service;
      }
    }
  }, [serviceList]);
  return service;
}
// 注入過程省略...
  • 你也可以通過各種條件篩選服務(wù),這種方式是在前端實(shí)現(xiàn)的高可用
  • ? 注意,服務(wù)最好只是內(nèi)部實(shí)現(xiàn)不同,接口應(yīng)該盡可能相同,否者會(huì)出現(xiàn)可選類型
  • 最典型的應(yīng)用,就是多家云服務(wù)廠商的短信驗(yàn)證(驗(yàn)證碼,人機(jī)校驗(yàn)等),通過可選服務(wù)根據(jù)用戶網(wǎng)絡(luò)情況進(jìn)行篩選,用最適合當(dāng)前用戶的那一個(gè)
  • 還有一個(gè)非常有意思的方案,通過服務(wù)來做數(shù)據(jù) mock,因?yàn)榉?wù)直接對(duì)接視圖,你只需要模擬視圖數(shù)據(jù)即可,提供兩個(gè)服務(wù),一個(gè)真實(shí)服務(wù),一個(gè) mock 服務(wù),這樣是用真實(shí)數(shù)據(jù)還是 mock 數(shù)據(jù),都是服務(wù)自動(dòng)判斷的,對(duì)你來說沒有流程差別

樣式封裝

注意,模塊是包含了樣式的,上文在講述邏輯和視圖的封裝,接下來說說樣式

  • 典型的 cssModule, styled-components 之類的方案
  • shadowDom,仿真樣式(Angular 原生支持,React 可以用 cssModule 之類工具間接實(shí)現(xiàn)),可以實(shí)現(xiàn)跨技術(shù)棧樣式封裝(沒錯(cuò),所謂 ‘微前端’ 的樣式封裝)
  • 樣式最好只包含排版,企業(yè) vis 統(tǒng)一性是標(biāo)準(zhǔn),沒有必要違背這個(gè)

繼續(xù)分析 SOA

從上一篇文章的例子可以看出什么呢?

首先,按照功能領(lǐng)域劃分文件,可以很快分析出應(yīng)用的邏輯結(jié)構(gòu)

也就是邏輯可讀性更強(qiáng),這個(gè)可讀性不只是針對(duì)用戶的,還有針對(duì)軟件的

比如,TodoService 和 TableHandlerService 有什么關(guān)系?

useTableHandlerService

前端架構(gòu)之 React 領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)

 

useTableHandlerService

前端架構(gòu)之 React 領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)

 

useTodoService

這些邏輯關(guān)系,僅僅依靠相關(guān)工具就能定位,并生成圖形,輔助你分析領(lǐng)域間的關(guān)系

誰依賴誰,一目了然 —— 比如 有個(gè) useState 的值 依賴 useLocalStorageState,肉眼看起來比較困難,但是在圖中一目了然

只是,不具名這一點(diǎn)有點(diǎn)神煩!

還有,React 內(nèi)部因?yàn)闆]有管理好這個(gè)部分傳遞,沒辦法像 Angular 一樣,瞬間生成一大堆密密麻麻的依賴樹,這就給 React 在大項(xiàng)目工程化上帶來了阻礙

不過一般項(xiàng)目做不到那么大,領(lǐng)域驅(qū)動(dòng)可以幫助你做到 Angular 項(xiàng)目極限的 95%,剩下那 5%,也只是稍稍痛苦些而已,并且,沒有辦法給管理者看到完整藍(lán)圖

不過就國內(nèi)目前前端技術(shù)管理者和產(chǎn)品經(jīng)理的水品,你給他們看 uml 藍(lán)圖,我擔(dān)心他們也看不懂,所以這部分不用太在意,感覺有地方依賴拿不準(zhǔn),只顯示這個(gè)領(lǐng)域的藍(lán)圖就好

其次,測試邊界清晰,且易于模擬

視圖你不用測試,因?yàn)闆]有視圖邏輯,什么時(shí)候需要視圖測試?比如 Form 和 FormItem 等出現(xiàn)嵌套注入的地方,需要進(jìn)行視圖測試,這部分耦合出現(xiàn)的概率非常小,大部分都是第三方框架的工作

你只需要測試這些 useFunction 就好,并且提供兩個(gè)個(gè)框,比如空組件直接 use,嵌套組件先 provide 再 useContext,然后直接只模擬 useFunction 邊界,并提供測試,大家可以嘗試一下,以前覺得測試神煩,現(xiàn)在可以試試在清晰領(lǐng)域邊界下,測試可以有多愉悅

最后

誰再提狀態(tài)管理我和誰急!

你看看這個(gè)應(yīng)用,哪里有狀態(tài)管理插手的地方?任何狀態(tài)管理庫都不行,它是上個(gè)時(shí)代的遮羞布


服務(wù)間通訊結(jié)構(gòu)

全局單一服務(wù)(類 Redux 方案)

但是,單一服務(wù)是不得已而為之,老版本沒有邏輯復(fù)用導(dǎo)致的

在這種方式下,你的調(diào)試將變得無比復(fù)雜,任何一處變更將牽扯所有本該封裝為模塊的組件

所以必須配合相應(yīng)的調(diào)試工具

所有多人協(xié)作項(xiàng)目,采用此種方式,最后的結(jié)果只有項(xiàng)目不可維護(hù)一條路!

中臺(tái) + 其他服務(wù)(雙層結(jié)構(gòu))

由一個(gè),appService 提供基礎(chǔ)服務(wù),并管理服務(wù)間的調(diào)度,此種方式比第一種要好很多,但是還是有個(gè)問題,頂層處理服務(wù)關(guān)系,永遠(yuǎn)比服務(wù)間處理服務(wù)關(guān)系來的復(fù)雜,具體問題詳見上文 “頂層注入”

樹形結(jié)構(gòu)模塊

這是理論最優(yōu)的結(jié)構(gòu),它的優(yōu)勢(shì)不再贅述,上文有提到

劣勢(shì)有一個(gè):

跨模塊層級(jí)的變更,容易形成循環(huán)依賴(也不叫劣勢(shì),因?yàn)榇朔N變更對(duì)于其他方式來說,是災(zāi)難)

理清自己的業(yè)務(wù)邏輯,有必要?jiǎng)澇龉δ芙Y(jié)構(gòu)圖,再開始開發(fā),是個(gè)好習(xí)慣,同時(shí),功能層級(jí)發(fā)生改變,應(yīng)該敏銳意識(shí)到,及時(shí)提升服務(wù)的模塊層級(jí)即可


編程范式

首先,編程范式除了實(shí)現(xiàn)方式不同以外,其區(qū)別的根源在于 – 關(guān)注點(diǎn)不同

  • 函數(shù)的關(guān)注點(diǎn)在于 —— 變化
  • 面向?qū)ο蟮年P(guān)注點(diǎn)在于 —— 結(jié)構(gòu)

對(duì)于函數(shù),因?yàn)榻Y(jié)構(gòu)方便于處理變化,即輸入輸出是天然關(guān)注點(diǎn),所以 ——

管理狀態(tài)和副作用很重要

js

var a = 1;
function test(c) {
  var b = 2;
  a = 2;
  var a = 3;
  c = 1;
  return { a, b, c };
}

這里故意用 var 來聲明變量,讓大家又更深的體會(huì)

在函數(shù)中變更函數(shù)外的變量 —— 破壞了函數(shù)的封裝性

這種破壞極其危險(xiǎn),比如上例,如果其他函數(shù)修改了 a,在 重新 賦值之前,你知道 a 是多少么?如果函數(shù)很長,你如何確定本地變量 a 是否覆蓋外部變量?

無封裝的函數(shù),不可能有可裝配性和可調(diào)試性

所以,使用函數(shù)封裝邏輯,不能引入任何副作用!注意,這個(gè)是強(qiáng)制的,在任何多人協(xié)作,多模塊多資源的項(xiàng)目中 ——

封裝是第一要?jiǎng)?wù),更是基本要求?

所以,你必須將數(shù)據(jù)(或者說狀態(tài))全部包裹在函數(shù)內(nèi)部,不可以在函數(shù)內(nèi)修改任何函數(shù)以外的數(shù)據(jù)!

所以,函數(shù)天然存在一個(gè)缺點(diǎn) —— 封裝性需要人為保證(即你需要自己要求自己,寫出無副作用函數(shù))

當(dāng)然,還存在很多優(yōu)點(diǎn) —— 只需要針對(duì)輸入輸出測試,更加符合物體實(shí)際運(yùn)行情況(變化是哲學(xué)基礎(chǔ))

這部分沒有加重點(diǎn)符號(hào),是因?yàn)樗恢匾?—— 對(duì)一個(gè)思想方法提優(yōu)缺點(diǎn),只有指導(dǎo)意義,因?yàn)樗枷敕椒梢跃C合運(yùn)用,不受限制

再來看看面相對(duì)象,來看看類結(jié)構(gòu):

class Test {
  a = 1;
  b = 2;
  c = 3;
  constructor() {
    this.changeA();
  }
  changeA() {
    this.a = 2;
  }
}

這個(gè)結(jié)構(gòu)一眼看去就具有 —— 自解釋性,自封裝性

還有一個(gè)涉及應(yīng)用領(lǐng)域的優(yōu)勢(shì) —— 對(duì)觀念系統(tǒng)的模擬 —— 這個(gè)詞不打著重符,不需要太關(guān)心,翻譯過來就是,可以直譯人腦中的觀念(動(dòng)物,人,車等等)

但它也有非常嚴(yán)重的問題 —— 初始化,自解耦麻煩,組合麻煩

需要運(yùn)用到大量的’構(gòu)建’,’運(yùn)行’設(shè)計(jì)模式!

對(duì)的,設(shè)計(jì)模式的那些名字就是怎么來的

其實(shí),你仔細(xì)一想,就能明白為什么會(huì)這樣 ——

如果你關(guān)注變化,想要對(duì)真實(shí)世界直接模擬,你就需要處理靜態(tài)數(shù)據(jù),需要自己對(duì)一個(gè)領(lǐng)域進(jìn)行人為解釋
如果你關(guān)注結(jié)構(gòu),想要對(duì)人的觀念進(jìn)行模擬,你就需要處理運(yùn)行時(shí)問題,需要自己處理一個(gè)運(yùn)行時(shí)對(duì)象的生成問題

魚與熊掌,不可兼得,按住了這頭,那頭就會(huì)翹起來,你按住了那頭,這頭就會(huì)翹起來

想要只通過一個(gè)編程范式解決所有問題,就像用手去抓沙子,最后你什么都得不到

極限的函數(shù)式,面向?qū)ο?/h1>

通過函數(shù)和對(duì)象(注意是對(duì)象,類是抽象的,觀念中的對(duì)象)的分析,很容易發(fā)現(xiàn)他們的優(yōu)勢(shì)

函數(shù) —— 測試簡單,模擬真實(shí)(效率高)

對(duì)象 —— 自封裝性,模擬觀念(繼承多態(tài))

將兩者發(fā)揚(yáng)光大,更加極限地使用,你會(huì)得到以下衍生范式:

管道 / 流

既然函數(shù)只需要對(duì)輸入輸出進(jìn)行測試,那么,我將無數(shù)函數(shù)用函數(shù)串聯(lián)起來,就形成了只有統(tǒng)一輸入輸出的結(jié)構(gòu)

聽不懂?換個(gè)說法 ——

只需要 e2e 測試,不需要單元測試!

如果我加上類型校驗(yàn),就可以構(gòu)造出 —— 理想無 bug 系統(tǒng)

這樣的話,你就只剩調(diào)試,沒有測試(如果頂層加個(gè)校驗(yàn)取代 e2e 的話)

而且,還有模式識(shí)別,異步親和性等很多好處,甚至可以自建設(shè)計(jì)語言(比如麻省老教材《如何設(shè)計(jì)計(jì)算機(jī)語言》就是以 lisp 作為標(biāo)準(zhǔn))

在 js 中, Cycle.js 和 Rxjs 就是極限的管道風(fēng)格函數(shù)式,還有大家熟悉并且討厭的 Node.js 的 Stream 也是如此,即便他是用 類 實(shí)現(xiàn)的,骨子里也是濃濃的函數(shù)式

分析一下這樣的系統(tǒng),你會(huì)發(fā)現(xiàn) ——

它首先關(guān)注底層邏輯 —— 也就是 or/c , is-a,and/c,not/c 這樣的函數(shù),最后再組裝

按照范疇學(xué)的語言(就是函數(shù)式的數(shù)學(xué)解釋,不想看這個(gè)可以不看,只是補(bǔ)充說明):

前端架構(gòu)之 React 領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)

 

范疇學(xué)

u of i2,i2 of g 的講法,與它的真實(shí)運(yùn)行方向,是相反的!

函數(shù)的組合方式,與開發(fā)目標(biāo)的構(gòu)建方式,也是相反的!

它的構(gòu)建方法叫做 —— 自底向上

這也是為啥你在很多 JS 的庫中發(fā)現(xiàn)了好多零零碎碎的東西,還有為何會(huì)有 lodash,ramda 等粒度非常小的庫了

在極限函數(shù)式編程下 ——

我先做出來,再看能干什么,比先確定干什么,再做,更重要!

因?yàn)檫@部分,可以第三方甚至官方自己提供!

所以,函數(shù)式是庫的第一優(yōu)先級(jí)構(gòu)建范式!因?yàn)樽鳛閹斓奶峁┱撸愀静豢赡茴A(yù)測用戶會(huì)用這個(gè)庫來干什么

領(lǐng)域模塊

函數(shù)式可以將其優(yōu)勢(shì)通過管道發(fā)揮到極致,面向?qū)ο笠粯涌梢詫⑵鋬?yōu)勢(shì)發(fā)揮到極致,這便是領(lǐng)域模塊

領(lǐng)域,就是一系列相同目的,相同功能的資源的集合

比如,學(xué)校,公司,這兩個(gè)類,如果分別封裝了大量的其他類以及相關(guān)資源,共同構(gòu)成一個(gè)整體,自行管理,自行測試,甚至自行構(gòu)建發(fā)布,對(duì)外提供統(tǒng)一的接口,那這就是領(lǐng)域

這么說,如果實(shí)現(xiàn)了一個(gè)類和其相關(guān)資源的自行管理,自行測試,這就是 —— DDD

如果實(shí)現(xiàn)了對(duì)其的自行構(gòu)建發(fā)布,這就是 —— 微服務(wù)

這種模型給了應(yīng)用規(guī)模化的能力 —— 橫向,縱向擴(kuò)展能力

還有高可用,即類的組合間的松散耦合范式

對(duì)于這樣的范式,你首先思考的是 —— 你要做什么!

這就是 ——

** 這種模型給了應(yīng)用規(guī)?;哪芰?—— 橫向,縱向擴(kuò)展能力

還有高可用,即類的組合間的松散耦合范式

對(duì)于這樣的范式,你首先思考的是 —— 你要做什么!

這就是 —— 自頂向下

  • 我要做什么應(yīng)用?
  • 這個(gè)應(yīng)用有哪些功能?
  • 我該怎么組織我的資源和代碼?
  • 該怎么和其他職能合作?
  • 工期需要多久?

現(xiàn)實(shí)告訴你,單用任何一種都不行
開發(fā)過程中,不止有自底向上封裝的工具,還有自頂向下設(shè)計(jì)的結(jié)構(gòu)

產(chǎn)品經(jīng)理不會(huì)把要用多少個(gè) isObject 判斷告訴你,他只會(huì)告訴你應(yīng)用有哪些功能

同理,再豐富細(xì)致的功能劃分,沒有底層一個(gè)個(gè)工具函數(shù),也完成不了任何工作

這個(gè)世界的確處在變化之中,世界的本質(zhì)就是變化,就是函數(shù),但是軟件是交給人用,交給人開發(fā)的

觀念系統(tǒng)和實(shí)際運(yùn)行,缺一不可!

凡是動(dòng)不動(dòng)就跟你說某某框架函數(shù)式,某某應(yīng)用要用函數(shù)式開發(fā)的人,大多都學(xué)藝不精,根本沒有理解這些概念的本質(zhì)

人類編程歷史如此久遠(yuǎn),真正的面向用戶的純粹函數(shù)式無 bug 系統(tǒng),還沒有出現(xiàn)過……

當(dāng)然,其在人工智能,科研等領(lǐng)域有無可替代的作用。不過,是人,就有組織,有公司,進(jìn)而有職能劃分,大家只會(huì)通過觀念系統(tǒng)進(jìn)行交流 —— 你所說的每一個(gè)詞匯,都是觀念,都是類!

React 提倡函數(shù)式

class OOStyle {
  name: string;
  password: string;
  constructor() {}
  get nameStr() {}
  changePassword() {}
}
function OOStyleFactory() {
  return new OOStyle(/* ... */);
}

這是面向?qū)ο箫L(fēng)格的寫法(注意,只是風(fēng)格,不是指只有這個(gè)是面向?qū)ο螅?/p>

function funcStyle(name, password) {
  return {
    name,
    password,
    getName() {},
    changePassword() {},
  };
}

這個(gè)是函數(shù)風(fēng)格的寫法(注意,這只是風(fēng)格,這同時(shí)也是面向?qū)ο螅?/p>

這兩種風(fēng)格的邏輯是一樣的,唯一的區(qū)別,只在于可讀性

不要理解錯(cuò),這里的可讀性,還包括對(duì)于程序而言的可讀性,即:
自動(dòng)生成文檔,自動(dòng)生成代碼結(jié)構(gòu)
或者由產(chǎn)品設(shè)計(jì)直接導(dǎo)出代碼框架等功能

但是函數(shù)風(fēng)格犧牲了可讀性,得到了靈活性這一點(diǎn),也是值得考慮的

編程其實(shí)是個(gè)權(quán)衡過程,對(duì)于我來說,我愿意

  • 在處理復(fù)雜結(jié)構(gòu)時(shí)使用 面向?qū)ο?風(fēng)格
  • 在處理復(fù)雜邏輯時(shí),使用 函數(shù) 風(fēng)格
    各取所長,才是最佳方案!

Redux

// redux reducer
function todoApp(state, action) {
  if (typeof state === "undefined") {
    return initialState;
  }

  // 這里暫不處理任何 action,
  // 僅返回傳入的 state。
  return state;
}

這其實(shí)就是用函數(shù)風(fēng)格實(shí)現(xiàn)的 面向?qū)ο?封裝,沒有這一步,你無法進(jìn)行頂層設(shè)計(jì)!

用類的寫法來轉(zhuǎn)換一下 redux 的寫法:

class MonistRedux {
  // initial,想要不變性可以將 name,password 組合為 state
  name = "";
  password = "";
  // 惰性初始化(配合工廠)
  constructor() {
    this.name = "";
    this.password = "";
  }
  // action
  changeName() {}
}

只有 函數(shù) 的封裝性才受副作用限制

注意這一點(diǎn),React 程序員非常容易犯的錯(cuò)誤,就是到了 class 里面還在想純度的問題,恨不得將每個(gè)成員函數(shù)都變成純函數(shù)

沒必要以詞害意,需要融匯貫通

同樣,以上例子也說明,如果你的技術(shù)棧提供直接生成對(duì)象的方案 —— 你可以只用函數(shù)直接完成面向?qū)ο蠛秃瘮?shù)式的設(shè)計(jì)

function Imaclass() {
  return {
    // ...
  };
}

我就說這個(gè)是類!為什么不行?

他要成員變量有成員變量,要成員函數(shù)有成員函數(shù),封裝,多態(tài),哪個(gè)特性沒有?

什么?繼承?這年頭還有搞面向?qū)ο蟮奶崂^承?組合優(yōu)于繼承是常識(shí)!

拋棄了繼承,你需要 this 么?你不需要,本來你就不需要 this(除了裝飾器等附加邏輯,但是函數(shù)本身就能夠?qū)崿F(xiàn)附加邏輯 —— 高階函數(shù))

同樣,你也可以綜合面向?qū)ο蠛秃瘮?shù)式的特點(diǎn),各取所長,對(duì)你的項(xiàng)目進(jìn)行頂層構(gòu)建和底層實(shí)現(xiàn)

這也是很方便的

Hooks,Composition,ngModule

我們來看看上面的那個(gè)函數(shù)風(fēng)格的類

像不像什么東西?

function useThisClass() {
  const [val1, setVal1] = useState(0);
  const [val2, setVal2] = useState(0);
  useEffect(() => {}, []);
  const otherObject = useotherClass();
  return { val1, setVal1, val2, setVal2, otherObject };
}

Hooks 恭喜各位,用得開心!

以 React 為例,老一代的 React 在 組件結(jié)構(gòu)上是管道,也就是單向數(shù)據(jù)流,但是對(duì)于我們這些使用者來說,我們寫的邏輯,基本上是放養(yǎng)狀態(tài),根本沒有接入 React 的體系,完全游離在函數(shù)式風(fēng)格以外:

前端架構(gòu)之 React 領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)

 

老一代的 React

換句話來說,只有 React 寫的代碼才叫函數(shù)式風(fēng)格,你寫的頂多叫函數(shù)!

你沒有辦法把邏輯寫在 React 的組件之外,注意,是完全沒有辦法!

好辦,我邏輯全部寫在頂層組件,那不就行了?

前端架構(gòu)之 React 領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)

 

新一代的 React

(其中 s/a 指的是 state,action)

為什么要有 state,action?

為什么要在每個(gè)組件里用 s/a ?

action 其實(shí)是用命令模式,將邏輯復(fù)寫為狀態(tài),以便 Context 傳遞,為何?

因?yàn)樯芷谠诮M件里,setState 在組件的 this 上

換句話說,框架沒有提供給你,將用戶代碼附加于框架之上的能力!

這個(gè)能力,叫做 IOC 控制反轉(zhuǎn),即 框架將功能控制權(quán)移交到你的手上

不要把這個(gè)類 Redux 開發(fā)模式作為最自然的開發(fā)方式,否則你會(huì)非常痛苦!

只有集成度不高的系統(tǒng),才需要中介模式,才需要 MVC

之前的 React/Vue 集成度不高,沒有 Redux 作為中介者 Controller,你無法將用戶態(tài)代碼在架構(gòu)層級(jí)和 React/Vue 產(chǎn)生聯(lián)系,并且這個(gè)層級(jí)天然應(yīng)該用領(lǐng)域模塊的思想方法來處理問題

因?yàn)?strong>框架沒有這個(gè)能力,所以你才需要這些工具

所謂的狀態(tài)管理,所謂的單一 Store ,都是沒有 IOC 的妥協(xié)之舉,并且是在完全拋棄面向?qū)ο笏枷氲幕A(chǔ)上強(qiáng)行用函數(shù)式語言解釋的后果,是一種畸形的技術(shù)產(chǎn)物,是框架未達(dá)到最終形態(tài)之前的臨時(shí)方案,不要作為核心技術(shù)去學(xué)習(xí),這些東西不是核心技術(shù)!

回頭看看 React 那些曖昧的話語,有些值得玩味:

  • Hook 使你在無需修改組件結(jié)構(gòu)的情況下復(fù)用狀態(tài)邏輯 (注意!是狀態(tài)邏輯,不是狀態(tài),是狀態(tài)邏輯一起復(fù)用,不是狀態(tài)復(fù)用)
  • 我們推薦用 自定義 hooks 探索更多可能
  • 提供漸進(jìn)式策略,提供 useReducer 實(shí)現(xiàn)大對(duì)象操作(好的領(lǐng)域封裝哪來的操作大對(duì)象?)

他決口不提 面向?qū)ο?,領(lǐng)域驅(qū)動(dòng),和之前的設(shè)計(jì)失誤,是因?yàn)樗枰櫦坝绊懞蜕鐓^(qū)生態(tài),但是使用者不要被這些欺騙!

當(dāng)你有了 hooks,Composition ,Service/Module 的時(shí)候,你應(yīng)該主動(dòng)拋棄所有類似

  • 狀態(tài)管理
  • 一定要寫純函數(shù)
  • 副作用要一起處理
  • 一定要保證不變形

這之類的所有言論,因?yàn)樵谀闶稚?,不僅僅只有函數(shù)式一件武器

你還有面向?qū)ο蠛皖I(lǐng)域驅(qū)動(dòng)

用領(lǐng)域驅(qū)動(dòng)解決高層級(jí)問題,用函數(shù)式解決低層級(jí)問題,才是最佳開發(fā)范式

也就是說,函數(shù)式和面向?qū)ο?,沒有好壞,他們只是兩個(gè)關(guān)注點(diǎn)不同的思想方法而已

你要是被這種思想方法影響,眼里只有對(duì)錯(cuò) ——

實(shí)際上是被忽悠了


管道風(fēng)格的函數(shù)式(unidirectional network 單項(xiàng)數(shù)據(jù)流)

這是函數(shù)式語言基本特性,將一個(gè)個(gè)符合封裝要求的函數(shù)串聯(lián)起來,你就能得到統(tǒng)一輸入輸出

函數(shù)將淪為算式,單測作用將消失,理想無 bug 系統(tǒng)呼之欲出

它會(huì)形如以下結(jié)構(gòu):

func1(func2(), func3(func4(startParams)));

或者:

func1().func2().func3().func4();

看到這些形態(tài),大家會(huì)首先想到什么?

沒錯(cuò) jsx 就是第一種結(jié)構(gòu)(是的,jsx 是正宗函數(shù)式,純粹無副作用,無需測試,僅需輸入輸出校驗(yàn)調(diào)試)

也沒錯(cuò),promise.then().then() 就是第二種,將函數(shù)式處理并發(fā),異步的優(yōu)勢(shì)發(fā)揮了出來

那第二種和第一種,有什么區(qū)別呢?

區(qū)別就是 ——

調(diào)度

函數(shù)是運(yùn)行時(shí)的結(jié)構(gòu),如果沒有利用模式匹配,每次函數(shù)執(zhí)行只有一個(gè)結(jié)果,那么整個(gè)串行函數(shù)管道的返回也只會(huì)有一個(gè)結(jié)果

如果利用了呢?它將會(huì)向路牌一樣,指示著邏輯倒流到特定的 lambda 函數(shù)中,形成分形結(jié)構(gòu)

請(qǐng)注意,這個(gè)分形結(jié)構(gòu)不只是空間上的,還有時(shí)間上的

這,就是調(diào)度?。。?/p>

說的很懸奧,大家領(lǐng)會(huì)一下意思就可以了,如果想要了解更多,直接去成熟工具處了解,他們有更多詳細(xì)的說明:

微軟出品的 ReactiveX[2] (脫胎于 linq),就是這方面的集大成者,網(wǎng)絡(luò)上有非常多的講解,還有視頻演示

Hooks api 就是 React 的調(diào)度控制權(quán)

useState 整個(gè)單項(xiàng)數(shù)據(jù)流的調(diào)度發(fā)起者

React 將它的調(diào)度控制權(quán),拱手交到了你的手上,這就是 hooks,它的意義絕對(duì)不僅僅只是 “在函數(shù)式組件中修改狀態(tài)” 那么簡單

useState,加上 useReducer (個(gè)人不推薦,但是與 useState 返回類型一致)

屬于:響應(yīng)式對(duì)象(注意,這里的對(duì)象是 subject,不是 object,有人能幫忙翻譯一下么?)

dispatch 是整個(gè)應(yīng)用開始渲染的根源

沒錯(cuò),this.setState 也有同樣功能(但是僅僅是組件內(nèi))

useEffect 整個(gè)單項(xiàng)數(shù)據(jù)流調(diào)度的指揮者

useEffect 是分形指示器

在 Rxjs 中 被稱作 操作函數(shù)(operational function),將你的某部分變更,衍射到另一處變更

在這個(gè) api 中,大量模式匹配得以正常工作

useMemo 整個(gè)單項(xiàng)數(shù)據(jù)流調(diào)度的控制者

最后,useMemo

它能夠通過只判斷響應(yīng)值是否改變,而輸出控制

當(dāng)然,你可以用 if 語句在 useEffect 中判斷是否改變來實(shí)現(xiàn),但是 —— 模式匹配就是為了不寫 if 啊~

單獨(dú)提出 useMemo,是為了將 設(shè)計(jì)部分 和 運(yùn)行時(shí)調(diào)度控制 部分分離,即靜態(tài)和動(dòng)態(tài)分離

調(diào)度永遠(yuǎn)是你真正開始寫函數(shù)式代碼,最應(yīng)該考慮的東西,它是精華

糾結(jié)什么是什么并不重要,即便 useMemo = useEffect + if + useState,這些也不是你用這部分 api 的時(shí)候應(yīng)該考慮的問題

最后

你明白這些,再加上 hooks 書寫時(shí)的要求:

不要在循環(huán),條件或嵌套函數(shù)中調(diào)用 Hook,確??偸窃谀愕?React 函數(shù)的最頂層調(diào)用他們。遵守這條規(guī)則,你就能確保 Hook 在每一次渲染中都按照同樣的順序被調(diào)用。這讓 React 能夠在多次的useState和useEffect調(diào)用之間保持 hook 狀態(tài)的正確。

你就能明白,為什么很多人說 React hooks 和 Rx 是同一個(gè)東西了

但是請(qǐng)注意,React 只是將它自己的調(diào)度控制權(quán)交給你,你若是自己再用 rx 等調(diào)度框架,且不說 requestAnimationFrame 的調(diào)度頻道React占了,兩個(gè)調(diào)度機(jī)制,彌合起來會(huì)非常麻煩,小心出現(xiàn)不可調(diào)式的大bug

函數(shù)式在處理業(yè)務(wù)邏輯上,有著非常恐怖的鋒利程度,學(xué)好了百利而無一害

但是請(qǐng)注意,函數(shù)式作為對(duì)計(jì)算過程的一般抽象,只在組件服務(wù)層級(jí)以下發(fā)揮作用,用它處理通訊,算法,異步,并發(fā)都是上上之選

但是在軟件架構(gòu)層面,函數(shù)式?jīng)]有任何優(yōu)勢(shì),組件層級(jí)以上,永遠(yuǎn)用面向?qū)ο蟮乃季S審視,是個(gè)非常好的習(xí)慣~


組件明明就只是邏輯和狀態(tài)(服務(wù))調(diào)配的地方,它壓根就不應(yīng)該有邏輯

把邏輯放到服務(wù)里

  • 堅(jiān)持在組件中只包含與視圖相關(guān)的邏輯。所有其它邏輯都應(yīng)該放到服務(wù)中。
  • 堅(jiān)持把可復(fù)用的邏輯放到服務(wù)中,保持組件簡單,聚焦于它們預(yù)期目的。
  • 為何?當(dāng)邏輯被放置到服務(wù)里,并以函數(shù)的形式暴露時(shí),可以被多個(gè)組件重復(fù)使用。
  • 為何?在單元測試時(shí),服務(wù)里的邏輯更容易被隔離。當(dāng)組件中調(diào)用邏輯時(shí),也很容易被模擬。
  • 為何?從組件移除依賴并隱藏實(shí)現(xiàn)細(xì)節(jié)。
  • 為何?保持組件苗條、精簡和聚焦。

React DDD 下如何處理類型問題?

泛型約束 InjectionToken

/**
 * 泛型約束,對(duì)注入數(shù)據(jù)的類型推斷支持
 *
 * @export
 * @template T
 * @param {(...args: any) => T} useFunc
 * @param {(T | undefined)} [initialData=undefined]
 * @returns
 */
export default function getServiceToken<T>(
  useFunc: (...args: any) => T,
  initialData: T | undefined = undefined
) {
  return createContext(initialData as T);
}

這樣,你的 useContext 就能有完整類型支持了

虛擬數(shù)據(jù)泛型約束

/**
 * 獲得虛擬的服務(wù)數(shù)據(jù)
 *
 * @export
 * @template T
 * @param {(...args: any) => T} useFunc
 * @param {(T | undefined)} [initialData=undefined]
 * @returns
 */
export function getMockService<T>(
  useFunc: (...args: any) => T,
  initialData: T | undefined = undefined
) {
  return initialData as T;
}

類服務(wù)

類服務(wù)在功能上,其實(shí)和函數(shù)服務(wù)是一樣的

函數(shù)返回對(duì)象,本身也是構(gòu)造方式之一,屬于 js 特色 (返回?cái)?shù)組的話,本質(zhì)也是對(duì)象)

所以功能上來說,函數(shù)服務(wù)完全可以覆蓋類服務(wù),實(shí)現(xiàn)面向?qū)ο?/p>

但是,需求可不止有功能一說:

  • 需要有更好自解釋性
  • 需要更好自封裝性
  • 需要更好的可讀性(自動(dòng)生成文檔,自動(dòng)分析)

類型自動(dòng)化:

function getClassContext<T>(constructor: new (...args: any) => T) {
  return createContext((undefined as unknown) as T);
}

實(shí)現(xiàn)一個(gè)類

/**
 * some service
 *
 * @export
 * @class SomeService
 */
export class SomeService {
  static Context = getClassContext(SomeService);
  name: string;
  setName: Dispatch<SetStateAction<string>>;
  constructor() {
    // eslint-disable-next-line react-hooks/rules-of-hooks
    const [name, setName] = useState("");
    this.name = name;
    this.setName = setName;
  }
}

使用這個(gè)類

最后,自動(dòng)分析類的結(jié)構(gòu):

可自動(dòng)文檔化,在配合其他工具實(shí)現(xiàn)框架外需求時(shí),能夠帶給你方便的使用體驗(yàn)

但是注意,目前官方是禁止在 class 中使用 hooks 的

需要禁用提示

同時(shí),需要保證在 constructor 中使用 hooks


React DDD 下的一些常見問題 FAQ

React DDD 下,class 風(fēng)格組件能用么?

最好不要用,因?yàn)?class 風(fēng)格組件的邏輯無法提取,無法連接到統(tǒng)一的服務(wù)注入樹中,因此會(huì)破壞應(yīng)用的單一數(shù)據(jù)原則,破壞封裝復(fù)用性

所以盡量不要使用,至于目前 getSnapshotBeforeUpdate,getDerivedStateFromError 和 componentDidCatch 的等價(jià)寫法 Hooks 尚未加入,這其實(shí)并不是大問題,因?yàn)樵诠艿里L(fēng)格中,錯(cuò)誤優(yōu)先表征為狀態(tài),比如 useRequest 中的 error

React DDD 是相比 之前的 類 redux 分層是更簡單還是更難?

簡單太多了,整個(gè) React DDD 的體系只需要 useXxx 表示服務(wù) Xxx 表示組件,只需要用幾個(gè) hooks api,通過注入提供上下文,不需要高階組件,好的模式也不需要 render props,更不需要太過重視性能調(diào)優(yōu)(別擔(dān)心性能問題,除了高消耗運(yùn)算惰性加載以外),你只是無法在組件層級(jí)處理錯(cuò)誤而已,個(gè)人認(rèn)為,錯(cuò)誤還是在用戶端處理吧

尤其是在 Typescript 下,你代碼的幾乎任何一處都有完整的類型提示,不需要你去附加類型聲明或者指定 interface

但是,它會(huì)將業(yè)務(wù)問題提前暴露,在沒有想好業(yè)務(wù)邏輯關(guān)系的情況下,請(qǐng)不要下手寫代碼,但是這也不是它的缺點(diǎn),因?yàn)樵谶壿嬪e(cuò)亂的情況下,分層直接會(huì)變得不可維護(hù)

React DDD 需要用到什么工具么?

不需要,直接使用 React hooks 就好,沒有其他工具需求

React DDD 性能會(huì)有問題么?

不會(huì),React DDD 的方案性能比 class 風(fēng)格組件 + 類 redux 分層要強(qiáng)得多,而且你可以精細(xì)化控制組件的調(diào)度和響應(yīng)式,下限比 redux 上限還要高,上限幾乎可以摸到框架極限

React DDD 適合大體量項(xiàng)目么?

是的,從最小體量項(xiàng)目,到超大體量項(xiàng)目,React DDD 都很適合,原因在于回歸面向?qū)ο?,承認(rèn)面向?qū)ο髮?duì)頂層設(shè)計(jì)有優(yōu)勢(shì)的同時(shí),業(yè)務(wù)邏輯采用極限函數(shù)式開發(fā)

無論在架構(gòu)上,還是業(yè)務(wù)邏輯實(shí)現(xiàn)上,都會(huì)將效率,可復(fù)用性,封裝度,集成度,可調(diào)式,可測試性直接拉滿,所以不用擔(dān)心

React DDD 效率高么?

它不是高不高的問題,它是直接可以將大部分業(yè)務(wù)效率直接提高十倍的大殺器,而且還有很多第三方庫可以被直接使用,讓第三方幫你處理邏輯,比如 ahooks,swr 等等

于此同時(shí),它直接跟業(yè)界主流工程化模式對(duì)接,有領(lǐng)域模塊的加持,多人協(xié)作將變得更加有效率,也能形成特別多的技術(shù)資產(chǎn)

Redux 之類的工具還有意義么?

沒有意義了,它只是解決框架沒有 IOC 情況下,保持和框架相同的單向數(shù)據(jù)流,保持用戶態(tài)代碼的脫耦而已,由于狀態(tài)分散不易測試,提供一個(gè)切面給你調(diào)試而已

這種方案相當(dāng)于強(qiáng)制在前端封層,相當(dāng)不合理,同時(shí) typescript 支持還很差

在框架有 IOC 的情況下,用戶代碼的狀態(tài)邏輯實(shí)際上形成了一個(gè)和組件結(jié)構(gòu)統(tǒng)一的樹,稱之為邏輯樹或者注入樹,依賴樹,很自然地與組件相統(tǒng)一,很自然地保證單向數(shù)據(jù)流和一致性

所以,Redux 之類的工具最好不要用,妄圖在應(yīng)用頂層一個(gè)服務(wù)解決問題的方法,都很傻

現(xiàn)有項(xiàng)目能直接改成 React DDD 么?

這是它最大的缺點(diǎn),不能!

因?yàn)閱栴}的根源出在框架上,IOC 應(yīng)該是框架的大變更,個(gè)人認(rèn)為 React 應(yīng)該直接暴力更新,摒棄所有老舊寫法,如同 15 年的 Angular 一樣,雖然有陣痛,但是對(duì)提升社區(qū)的好處大于壞處,當(dāng)然,這是沒有考慮市場的想法

如果你想更純粹使用 React DDD,最好還是采取重構(gòu)的方案

React DDD 下,應(yīng)該怎么劃分文件結(jié)構(gòu)?

按照功能劃分,你的功能有哪些包含關(guān)系,你的文件結(jié)構(gòu)就是如何

你的功能在哪個(gè)范圍需要提供限界上下文,哪里就進(jìn)行服務(wù)注入

所以類似拆分 store,action,models 之類的文件夾,就不要有了,前端沒有數(shù)據(jù)庫,即便有,也沒有到需要抽象 dao 的程度,即便抽象 dao,dao 本身也是不符合工程化和 DDD 的

所以微前端可以由 React DDD 實(shí)現(xiàn)是么?

是的,有了限界上下文,分開開發(fā),分開測試,分開部署都可以實(shí)現(xiàn)

但是一定要在相同框架內(nèi),個(gè)人認(rèn)為前端采用不同框架開發(fā)是個(gè)偽需求

現(xiàn)如今 Angular,React,Vue,都有 IOC,寫法都可以互通

你要講模塊剝離,直接構(gòu)建的時(shí)候把模塊文件夾 cp 到指定目錄,覆蓋掉占位的文件夾即可

不過注意,‘微應(yīng)用’需要有模擬頂層 context 的可選服務(wù),當(dāng)然,這些東西不管你怎么實(shí)現(xiàn)都是需要的

至于說重構(gòu)兼容老代碼而采用 shadowDom 和 iframe 的,我只能說,作為終端開發(fā),重構(gòu)兼容老代碼只是臨時(shí)狀態(tài),他不能作為架構(gòu),更不能作為一個(gè)技術(shù)來推廣

React DDD 和 Angular 的架構(gòu)好像,為什么?

因?yàn)?Angular 15 年首先實(shí)現(xiàn)了 IOC,組件中推薦不要寫邏輯,也是 Angular 最早提出的方案

service(狀態(tài)邏輯單元),module(service+component+template+css+staticAssets…),領(lǐng)域模塊封裝也是 Angular 最早提出的,但是因?yàn)楫?dāng)時(shí)它走太快,社區(qū)沒跟上,生命周期復(fù)用也麻煩,因?yàn)椴捎醚b飾器,組件還保留了一個(gè)殼,推進(jìn)最佳實(shí)踐的難度比較大

而 React 的 hooks 可以更加抽象,也更簡單直接,直接就是兩個(gè)函數(shù),服務(wù)注入也是通過組件,也就是強(qiáng)制與組件保持一致

這時(shí)候再推動(dòng) DDD 就非常容易且水到渠成了

但是 Angular 的很多特性 React 還不支持,比如組件樣式封裝,多語言依賴到視圖,服務(wù)搖樹,動(dòng)態(tài)組件搖樹,異步服務(wù)(suspense,concurrent 還在試驗(yàn)階段),還有真正解決性能問題的大殺器 platform-webworker(能夠在應(yīng)用層級(jí)支持瀏覽器高刷)

但是這些需求,在沒有超大體量(世界級(jí)應(yīng)用)下,用到的可能性很小,不妨礙 React 的普適性

而且 React 社區(qū)更活躍,管道風(fēng)格函數(shù)是對(duì)社區(qū)的依賴是很強(qiáng)的,這方面 Angular 干寫 rxjs 管道有些磨人

React DDD 會(huì)是未來趨勢(shì)么?

這點(diǎn)我覺得毫無疑問,因?yàn)?DDD 是整個(gè)軟甲開發(fā)架構(gòu)設(shè)計(jì)的趨勢(shì),而且這個(gè)趨勢(shì)伴隨著 微服務(wù) 的普及已經(jīng)不可逆轉(zhuǎn),只要前端承認(rèn)自己是編程,這個(gè)趨勢(shì)同樣也逃不過去

前端還是單節(jié)點(diǎn),但是未來會(huì)有端到端

這個(gè)東西現(xiàn)在的可用性易用性在 React 語境下已經(jīng)相當(dāng)高了,未來也只會(huì)更有用

其他的不用說,光效率的提升,就可以讓開發(fā)者大呼過癮了

只是 React/Vue 還有很多歷史問題需要解決,等這些問題解決了, DDD 肯定會(huì)大跨步向前的

管道流難度會(huì)不會(huì)很高?

是的,作為極限函數(shù)式開發(fā),在給你提供更好的類型支持,容易調(diào)試測試的支持后,首當(dāng)其沖的,就是純粹函數(shù)式的爆炸難度

正常模式下,你是需要先化簡范疇運(yùn)算式再寫代碼的,不過這明顯老學(xué)究了,哈哈哈

但是,React hooks 有非常活躍的社區(qū),你不需要自己實(shí)現(xiàn)封裝很多邏輯,這部分可以直接求助于社區(qū)實(shí)現(xiàn)

需要你實(shí)現(xiàn)的管道功能很少

不像 Angular 寫 rxjs ,管道需要自己根據(jù)一百多個(gè)操作函數(shù)配置,腦力負(fù)擔(dān)太大,并且操作函數(shù)都是抽象的,調(diào)度權(quán)限給到你之后,復(fù)雜度又加了個(gè) 3 次方

React 的管道復(fù)用第三方,大多都是直接面向業(yè)務(wù)的,比如 swr 和 ahooks ,要直接很多

所以在,真正需要你寫的管道邏輯并不多,這一點(diǎn)值得慶幸

但是,管道風(fēng)格也是未來趨勢(shì),可以說管道和領(lǐng)域,分別是函數(shù)式和面向?qū)ο笸蒲莸綐O致的結(jié)果,兩者都是最佳范式,兩者都得學(xué)習(xí)

你只需要學(xué)思想方法,而且這樣的思想方法,放諸四海皆可,任何編程平臺(tái),除了特別純粹的那種:無類型 lisp 和無 lambdajava,基本上這些概念都是想通且能夠交流的

管道也是存在于編程的方方面面,elasticSearch,mongo aggregation,node stream,graphQL,等等等等…

謝謝支持

歡迎加關(guān)注@Web代碼工,我會(huì)第一時(shí)間和你分享前端行業(yè)趨勢(shì),學(xué)習(xí)途徑等等。2021 陪你一起度過!

分享到:
標(biāo)簽:架構(gòu)
用戶無頭像

網(wǎng)友整理

注冊(cè)時(shí)間:

網(wǎng)站:5 個(gè)   小程序:0 個(gè)  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會(huì)員

趕快注冊(cè)賬號(hào),推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨(dú)大挑戰(zhàn)2018-06-03

數(shù)獨(dú)一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學(xué)四六

運(yùn)動(dòng)步數(shù)有氧達(dá)人2018-06-03

記錄運(yùn)動(dòng)步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績?cè)u(píng)定2018-06-03

通用課目體育訓(xùn)練成績?cè)u(píng)定