React DDD 配合 Typescript
转载自前端备忘录-江湖术士
React DDD 可以 很好地配合 Typescript
使用时有以下原则:
类型必须自动获得,最好不要主动声明,参数声明,强制注解都最好不要有
class 结构应对复杂数据(注意!只是数据)时,对类型的支持比 function 强大
经常看我文章的人,一定会记得,我对待 Typescript 的看法:非 DDD 下使用 Typescript 体验非常差
非 管道函数式 下使用 Typescript 体验非常差
不提倡 Reducer ,但是承认其有一定作用(对于不熟悉管道的同学)
好了,接下来我们来详细阐述:
类型最好自动获取,自动推断
Typescript 是个工具,是来帮助你写代码的
不是来干扰你写代码的!
比如,一旦需要你手动声明类型(除非交由你定义),其实和无类型的体验差不多
比如,redux 的各种散件 function 提取:
1 | export const SEND_MESSAGE = "SEND_MESSAGE"; |
再来:
1 | import { |
各种 interface,各种 typeof,本身 Redux 样板代码已经很多了,结果加入了 Typescript ,不仅要注意 action 的碰撞问题,逻辑关系,还要注意类型匹配?
而且这一且还需要 人为保证 ?
Typescript 是个强类型语言没错,但是这不是代表它生成的 Javascript 是个编译型语言!
类型并不能完全约束运行时,错误的类型危害更大!
因此,类型除了声明阶段,其他时候都需要自动给到你,你什么时候需要,什么时候就能拿到!
所以,反对一切散件,应用的每一个部分都必须紧密相连,都必须自动提供类型
最多在 useState,useRef 时提供类型,其他时候类型最好感受不到 Typescript 的存在,才是好的使用方式:
1 | function useFunc() { |
这个 hooks 函数,使用时:
1 | const someService = useFunc(); |
这个 someService ,必然能拿到 useFunc 返回值的类型,你不需要重新指定,类型可以从自定义 hooks 函数,一路传导到组件中,不会有中断
如果想要注入也有类型推断怎么办?
1 | import { createContext } from "react"; |
这个 initialValue 都可以不要,因为注入后使用,一定是在注入节点后面,不然破坏了单向数据流(就是强制你跑不通注入前的使用)
这样的话,提供泛型约束(去掉 undefined,不写 initialValue):
1 | export const SomeService = getToken(useFunc); |
注入之后,子孙组件任何地方:
1 | const someService = useContext(SomeService); |
这里的 someService 也能拿到完整的类型
所以,你的类型仅仅在初始化时定义,其他地方的类型全部随叫随到 ——
你除了业务,根本不用考虑类型,他只是你的辅助
为什么不提倡 useReducer (即便 React 官方提倡),虽然它能让你少写 useMemo (惰性加载才是性能优化首选),但是它的 action 令牌却是类型重灾区,它的 initialState,action,reducer 需要分开声明,只要分开,你的类型负担必然加重 —— 尽量让类型畅通无阻——有良好的设计,才能直接去掉类型的不利部分,将他的优势发扬光大
其二,interface 必须少用(除非不得不用)
Typescript 不是 go,没办法直接 interface 然后打两个括号就出来一个对象,有这样行为的 api 是 class
我如果给函数参数声明为:
1 | interface Data { |
那我在调用函数的时候,我知道这个函数的参数有哪些属性么?
不,我不知道,我只能一个个地去试
interface 只是约束,它起不到完整的辅助作用
但是如果我这样:
1 | class Data { |
这样在设置 name 时,Data 不仅有默认值,还能直接点出来,选择自己需要变更的项目
React 不提倡 class 的原因是因为它不够函数式
但是作为应用开发者,面向对象和函数式都只是思想方法
在 Typescript 语法中,类的成员函数不可能保证纯度(因为要变更 this),但是同时,又不需要考虑纯度(变更 this,也不影响 class 的封装性)
为什么不取长补短呢?
如果参数对象非常复杂,这个时候,函数的 interface 只有约束作用,几乎没有任何辅助作用,为什么还死抓着不放呢?
你只要保证这个 class 不变更 class 以外的数据不就可以了?
奇怪的是,就连 React 自己的源码,都频繁使用 function this 的原生类方案,为何使用者还死抓着不放呢?
理解 props 的作用
props 到底有什么作用?
除了影响调度(函数式组件)外,props 其实在新版 React 中,只起到 map 节点传递 迭代标识 或者 注入令牌 的作用
因为用 props 层层传递数据有违单一数据原则
而且新版本是不用 React.memo 调优的,而是用的 useMemo,而且因为 React 的检测机制,解决相关性能问题,优先使用惰性加载保证性能,useMemo 也不会有语义保证,这方面 porps 作用已经丢失
其次,Context 修改单例数据永远比 props 层层传递好,而你又无法确定未来的业务是否变更,是否有更深层级的调用
所以全部逻辑,状态,一次性提取服务是最优解,并没有多少成本,(一个函数框的代码,没有几个字)组件只需要将服务返回值填入 jsx 即可
那这个时候,props 传入注入令牌或者迭代标识是仅存的作用
因为 props 需要手写类型声明:propsWithoutChildren<{/* interface */}>
,而这种需要手动配置的类型极其影响思路,你甚至需要去比对源数据结构,而且很难做部分类型提取和转化,又在给自己增加负担
所以,类似 Angular 那种 SOA 方案是必然,因为那是成本最低的方案,同时综合性能,类型支持最好的方案
无管道,不类型
在用极限函数式处理业务逻辑时,管道风格可以给到最佳类型提示
1 | useEffect(() => { |
但是你要是拆成一堆散函数,那你的类型提示可就没了,你得手动去配!
1 | function changeState(state: State, action: string) { |
你必须解放你自己,一般来说,除了 useCallback,props 传递 key 和 serviceToken,你没有需要指定 interface 的地方
结语
最后,你只有将你开发中技术领域的成本降到最低,才能将全部精力放在业务领域
hooks 是个复用状态和逻辑的方案,不是单单只复用状态
所有的开发,在业务逻辑方面,最好用管道风格的极限函数式,做响应式开发,这部分并不难,因为有很多第三方工具,比如 ahooks
还有很多框架已经支持此种方案,比如 antd
状态管理,分层架构,是只管 getter,不管 setter 的方案,复用的只有数据,响应式由其它第三方工具保证
这种方式是框架不支持逻辑复用的无奈之举,不是本因如此,希望大家能够看破