前端架构之 React 领域驱动设计

2021-10-27 16:33:28 浏览数 (1)

领域驱动,各自只管各自的模块,顶层再来进行组装和分配

  • 坚持根据特性区命名目录。
  • 坚持为每个特性区创建一个 NgModule。 能提供限界上下文,将某些功能牢牢地锁在一个地方,开发某个功能时,只需要关心这个模块就够了。

视图的归试图,逻辑的归逻辑

代码语言:javascript复制
function SomeComponent() {
  const someService = useService();
  return <div>{someService.state}</div>;
}

跨组件数据传递?

代码语言:javascript复制
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>;
}

上下文注入节点,本身就是按照试图来的

函数 DDD

只用函数实现 DDD,它有多优美

我们先比较一下这两种写法,对于一个类:

代码语言:javascript复制
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 };
}

下面的自带响应式,getter,setter 也自动给出了,同时使用了工厂模式,不需要了解函数内部的逻辑。

生命周期复用

每个 useFunc 都是 拆掉 的管道函数,框架帮你组装,简直就是一步到位!

效率

代码语言:javascript复制
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>

这个表单服务你想要在哪控制?

想要多个组件同时控制?

加个 token,也就是 createContext,把依赖提上去!

他特么自然了!


React Hooks 版本架构

执行 LIFT 原则

  • 顶层文件夹最多包含:assets,pages,layouts,app 四个(其中 pages,layouts 是为了照顾某些 ssr 开发栈),名字可以变更,但是不可以有多余文件夹,激进的话可以只有一个 app 文件夹
  • 按功能划分文件夹,每个功能只能包含以下四种文件:Xxx.less, Xxx.tsx, useXxx.ts,useXxx.spec.ts , 采用嵌套结构组织
  • 一个文件夹包含该领域内所有逻辑(视图,样式,测试,状态,接口),禁止将逻辑放置于文件夹以外
  • 如果需要由其他功能调用,利用 SOA 反转 为何如此?
  • 功能结构即文件结构,开发人员可以快速定位代码,扫一眼就能知道每个文件代表什么,目录尽可能保持扁平,既没有重复也没有多余的名字
  • 当有很多文件时(例如 10 个以上),在专用目录型结构中定位它们会比在扁平结构中更容易
  • 惰性加载可路由的功能变得更容易
  • 隔离、测试和复用特性更容易
  • 管理上,相关领域文件夹可以分配给专人,开发效率高,可追责和计量工作量,很明显应该禁止多人同时操作同一层级文件
  • 只需要对 useXxx 进行测试,测试复杂度,工作量都很小,视图测试交给 e2e

利用 SOA 实现跨组件逻辑复用

利用 注入令牌 服务函数 注入点,实现灵活的 SOA

命名格式为

代码语言:javascript复制
XxxService = useToken(useXxxService)

XxxService 为注入令牌 和文件名

useXxxService 为服务函数

代码语言:javascript复制
<XxxService.Provider value={useXxxService()} />

XxxService.Provider 为注入点

注入令牌与服务函数紧挨

与注入节点处于同一文件结构层级

禁止除 SOA 以外的所有数据源


为何如此?

  • 符合单一数据,单以职责,接口隔离原则
  • 通过泛型约束,可以有更加自然的 Typescript 体验,不需要手动声明注入数据类型,所有类型将自动获得
  • 层次化注入,可以实现 DDD,将逻辑全部约束与一处,方便团队协作
  • 当你在根注入器上提供该服务时,该服务实例在每个需要该服务的组件和服务中都是共享的。当服务要共享方法或状态时,这是数学意义上的最理想的选择。
  • 配合组件和功能划分,可以方便处理嵌套结构,防止对象复制被滥用,类似深复制之类的操作应该禁止

实现一个 IOC 注入令牌的方法为

代码语言:javascript复制
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);
}

一个典型可注册服务为:

代码语言:javascript复制
import { useState } from "react";
import useToken from "./useToken";

export const AppService = useToken(useAppService);

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

0 人点赞