【KT】针对Hox,我写了个简陋组件版dev-tools

前言

📢 博客首发 : 阿宽的博客

🍉 授权转载团队博客 : SugarTurboS Blog

在上篇【KT】查缺补漏 React 状态管理探索发出之后,留下了一个问题,那就是 hox 真的很香吗 ? 香不香我不知道,下边是我使用 hox 之后的一些感想...

不得不说,实在是太难顶了,最后改了一下源码,实现了一个低配版的 model tree ...

以下内容是阿宽的个人感受,仅代表个人观点,如有错误,还望指出 🤝 ~

❓ 为什么这个专栏叫【KT】,我这人比较 low,专栏中文叫: 阿宽技术深文,K 取自阿宽中的宽,T,Technology,技术,有逼格。可以,感觉自己吹牛逼的技术又进了一步。

正文开始

网上搜了一下 Hox 文章,相对较少,并且大部分是介绍型的文章(就是告诉你这是什么库,怎么用),经过在项目中,落地实践了一个模块之后,也算是有所小收获,下边是我的一些感受...

如何划分颗粒度

想跟大家探讨如何“划分颗粒度”的问题,怎么理解呢?比如我们现在有个 reducer,里边有近 40 个字段,这应该很常见吧,对于一个复杂模块,存在一个 reducer 拥有三四十个字段,是个再正常不过的事情了~

问题在于我们如何定义“细”这个纬度,这个问题跟 hox 是没关系的,它只是能将你的 hooks 变成持久化,全局共享的数据,我们讨论的是 : 该写一个 model hooks 还是写 n 个 model hooks ?

这个问题在我重构实践的时候,也一直在困扰我,最后还是决定,分三种情况,如下 :

  • 一个字段就是一个 model 文件
  • 一个 useXXXModel 里边允许定义多个字段
  • 一个 model 文件,允许存在 n (n<5)个 useXXXModel

下边展开说说我为什么这么设置 :

  1. 一个字段就是一个 model 文件

其实我不太赞同这样的颗粒度划分,为什么呢? 划分的太细了,你想啊,如果一个小模块,有 10 个字段,那你就对应有 10 个文件,really ? 那为什么我还要说这种情况呢?因为如果你这个字段比较独立,与其他字段毫无关联,同时比较复杂,可能你会在你的 model 里边做一些副作用操作,那么写成这样,可能相对比较好 ?

哪有好与不好,最终看你自己如何定义,按照你自己喜欢的写就好了

但是这种情况,就会导致你代码量有点大,举个例子,你这个组件 index.js 需要这 10 个数据,那么代码就会是这样的

import useClassModel from'@src/model/user/useClassModel'

import useSubjectModel from'@src/model/user/useSubjectModel'

import useChapterModel from'@src/model/user/useChapterModel'

// 此处还需import 7个文件 ...

functionUser() {

const { classId, changeClassId } = useClassModel()

const { subjectName, changeSubjectName } = useSubjectModel()

const { chapterName, changeChapterName } = useChapterModel()

// 此处还有代码...

}

然后你修改时候,逻辑代码可能就是这样

function changeSelectClass (class) {

changeClassId(class.id); // 修改班级id

changeClassName(class.clsname); // 修改班级名

changeSubjectName(class[0].subjects[0].subjectName) // 修改班级,默认切换此班级下的第一个学科

changeSubjectCode(class[0].subjects[0].subjectCode)

changeChapterName(class[0].subjects[0].subjectCode.chapter[0].chapterName) // 默认选中此学科下的第一个章节

changeChapterCode(class[0].subjects[0].subjectCode.chapter[0].chapterCode)

}

想表达的就是,这种方式,你的代码量会上来,不过问题不大,个人认为这种代码阅读性还可以

  1. 一个 useXXXModel 里边允许定义多个字段

这种情况我建议是“强关联”的时候这么写,什么是强关联? 比如 classId 和 className,subjectCode 和 subjectName 这种,就是一个改变,另一个肯定也会改变,这种情况可以写在一个 useModel 中。如下

functionuseSelectSubject() {

const [subjectCode, changeSubjectCode] = useState(undefined)

const [subjectName, changeSubjectName] = useState(undefined)

const setSubjectCode = (subjectCode: string) => changeSubjectCode(subjectCode)

const setSubjectName = (subjectName: string) => changeSubjectName(subjectName)

return {

subjectCode,

subjectName,

setSubjectCode,

setSubjectName,

}

}

当然你也可以用一个对象包这两个字段,一切随你喜欢,我只是想表达,既然是有关系的,一个变另一个也变的,可以放在一块

  1. 一个 model 文件,允许存在 n (n<5)个 useXXXModel

会不会存在一种情况,就是你可能有好几个字段,这好几个字段可以按照上边 “强关联” 分割成 n 个,但你又不想有 n 个 .js 文件,并且可能这 n 个文件是这个模块公用的。

总不能新建一个文件夹,然后将 n 个文件都写成 useN1Model.js、useN2Model.js、useN3Model.js 吧,不会吧不会吧 ?

我个人建议是 : 该文件控制在 150 行代码以内,大概 5 个 model 即可,超出 5 个,则拆分,当然,这只是我个人的建议而已啦

// 比如我这个就叫做 useBaseModel.js

import { createModel } from'hox'

functionuseSelectClass() {}

functionuseSelectSubject() {}

functionuseSelectChapter() {}

exportdefault {

useSelectClass: createModel(useSelectClass),

useSelectSubject: createModel(useSelectSubject),

useSelectChapter: createModel(useSelectChapter),

}

在使用的时候只需要 import 一次就好了,而且这个在阅读性上,也还行,毕竟这三个 useModel 都是具有关联性的

import useBaseModel from'@src/model/user/useBaseModel'

functionUser() {

const { className } = useBaseModel.useSelectClass()

const { subjectName } = useBaseModel.useSelectSubject()

const { chapterName } = useBaseModel.useSelectChapter()

}

文件目录划分

官方对 hox 特性的说明 : 随地可用,所以 model 层的文件目录规范也要统一,虽然之前使用 redux 的时候,业务 reducer 也是放在业务文件夹下,但是因为 combineReducer 的存在,我们是可以在 store 的入口文件 index.js 看到所有 import 进来的 reducer

但是 hox,你不使用 createModel 时,它就是一个业务 hooks,随时建,随时用,会导致后期维护非常麻烦,特别是无 dev-tools 下,你根本不知道哪些 hooks 是变成了持久化、全局共享的 model hooks

会不会有一种情况,那就是 A 在开发的时候,一开始它写的就是一个业务 hooks,后边它需要把这个数据变成全局共享,于是它直接在当前目录下,直接用 createModel 包一下,完事,后边的开发人员发现有人这样写了,照猫画虎,渐渐的就成为了一种"规范"... 这就是典型的“破窗效应”啊

破窗效应 : 一幢有少许破窗的建筑,如果那些窗不被修理好,可能将会有破坏者破坏更多的窗户。一面墙,如果出现一些涂鸦没有被清洗掉,很快的,墙上就布满了乱七八糟、不堪入目的东西

我建议,原来我们写 store 的文件夹,改成 model,同时 model 下的文件夹架构和业务一致。

生态圈小

网上查询 hox 相关文章,几乎很少,github 目前 star 519issues 9 ,算小众的库,当然这也不是一个大问题,因为你不使用它的 createModel 将数据持久化,那么本质上就是自定义 hooks

如果有问题,大部分都还是自己逻辑问题,可能自定义写的 hooks 有点毛病,只需要自己 log 排查问题即可。

无 dev tools 支持

这才是痛点 !!!你想想,我们使用了 createModel 包裹之后,如何知道这个数据是否真的被持久化、全局共享呢 ?

常规操作就是,在组件中 import 这个数据源,然后 console.log 打印看看,但是如果是使用redux,那么我们可以在 redux-devtools 插件下,直接看到这颗 state tree 的。这就很方便了。

比如我想看看 user 下是否存在我想要的字段,那么我只需要在插件中看一下 state tree

还有一种场景,比如我们在 A 组件中,发送请求,然后往 model 中修改了一些数据,这些数据在 B 组件才用到,A 组件用不到,在没报错的情况下,我们无法知道数据是否真的被修改

解决方案

实在是很难过了,我总不能真的去 import 进来,然后去 console.log 吧?于是跟鹏哥一起琢磨了一下,直接修改源码,然后实现一个简单的 model tree 吧 ...

  1. 注入每个 hooks 的命名空间

这里有个问题,那就是我们给 createModel 传递的是一个 hooks,是你在 useSelectClass 中,return { classId, changeClassId } 这个对象,所以你是不知道当前这个 hook 的名称,为此我们给这个 hooks 注入一个命名空间

functionuseSelectClass() {}

useSelectClass.namespace = 'useSelectClass'

exportdefault {

useSelectClass: createModel(useSelectClass),

}

然后修改createModel源码,原来只需要给 Executor 传递 2 个字段,现在给它支持 namespace

// createModel.js

render(

<Executor

onUpdate={(val) => {

container.data = val

container.notify()

}}

hook={() => hook(hookArg)}

namespace={hook.namespace} // 新增此字段支持

/>

)

  1. 过滤 hooks 中的方法,毕竟我们只是想展示一个 model tree 嘛 ~ 然后将这个 hooks 挂载到 window 下

functionExecutor(props) {

// 原逻辑代码,巴拉巴拉xxxx

// 下边这段代码是新增的

if (!window.hox) {

window.hox = {}

}

let keys = Object.keys(data)

let maps = {}

keys.forEach((key) => {

if (typeof data[key] !== 'function') {

maps[key] = data[key]

}

})

window.hox = {

...window.hox,

[props.namespace]: { ...maps }, // 以namespace为key,过滤function后的数据为value

}

returnnull

}

  1. 通过 Object.defineProperty 重写 set、get,监听 window.hox 的变化

此时去打印 window.hox ,是有数据的,又离成功近了一步,但是问题来了,我们修改 model 值之后,我们要实时响应,该这么办?记得之前看 Vue 双向绑定原理,哦豁,有了,Object.defineProperty 用起来

在我们写的 dev-tools 组件中,监听一下 window.hox ,将最新的 model 数据,存入 state,然后搭配 Ant Design Tree 组件,最终渲染

componentDidMount() {

window.b = {};

const _this = this;

Object.defineProperty(window, 'hox', {

set: function(value) {

window.b = value;

_this.setState({

model: value

});

},

get: function() {

returnwindow.b;

}

});

}

原本我们用的不是 window.b = {} , 而是 let a = {},然后在 set 中,a = value,但是发现会存在问题,什么问题? 小伙伴们可以想一下,或者实践一波

  1. 递归遍历,构造 Tree 组件需要的数据,然后渲染

这是最费时的了,我们要将这种数据格式,转成 Tree 想要的数据格式,写了好多遍都没写对,后边还是鹏哥给了提示,感谢鹏哥

// 我们的数据

window.hox = {

useUserModel: {

name: '彭道宽',

school: [

{

s_name: 'xxx大学',

time: '2015-2019',

},

{

s_name: 'xxx高中',

time: '2012-2015',

},

],

currentCompany: {

c_name: 'CVTE',

c_job: '前端工程师',

},

},

}

// Ant Design Tree 数据

antdTree = [

{

title: 'useUserModel',

key: '0',

children: [

{

title: 'name',

key: '0-0',

children: [],

},

{

title: 'school',

key: '0-1',

children: [

{

title: 's_name',

key: '0-1-0',

children: [],

},

],

},

],

},

]

上边的我就不一一写的,问题在于,如何遍历 ? 小伙伴们可以想一想 ? 不想的就直接到下一步吧

format = (model) => {

let result = []

const deep = (children, value, idx) => {

Object.keys(value).forEach((key, index) => {

let temp = {}

temp.key = `${idx}-${index}`

// 这是我对title的处理,因为非function/object,直接就渲染值在后面

temp.title = this.renderTitle(value[key], key)

temp.children = []

if (this.checkIsObjectOrArray(value[key])) {

// 如果是对象或者数组,那就递归

deep(temp.children, value[key], temp.key)

}

children.push(temp)

})

}

Object.keys(model).forEach((key, index) => {

let temp = {}

temp.key = index

temp.title = this.renderTitle(model[key], key)

temp.children = []

if (this.checkIsObjectOrArray(model[key])) {

deep(temp.children, model[key], index)

}

result.push(temp)

})

return result

}

效果如图 👇

  1. 问题不大,基本完工,我们再稍微完善一下这个 dev-tools 组件,参考一下 redux 的插件,最终效果如图


这个还是有点小问题的,后期打算优化一波,问题不大,大家知道一下思路就好~

提问环节

如果我代码这么写,你们觉得有什么问题吗?

functionuseSelectClass() {

const [classId, setClassId] = useState('')

const changeClassId = (classId) => setClassId(classId)

useEffect(() => {

// 一顿操作,然后把classId改了

const data = handle()

setTimeout(() => {

setClassId(data)

}, 10000)

})

return { classId, changeClassId }

}

exportdefault createModel(useSelectClass)

你们觉得我这么写,会不会有问题呢 ?有,如果我作为使用者,我取这个 classId 值,那么在这个 10s 内,我取的是一个 undefined,然后 10s 后,突然 classId 变了;还有一种可能,你依赖 classId,当 classId 变了之后你发送请求,10s 之前你发现没问题,10s 之后,哦豁,怎么突然多发了一遍请求 ?

所以我认为要约束,规定 model 的 hooks 不要写副作用,它就存粹的 getValue、setValue 就好了

或许有人会说,那你也可以在 redux 这么做啊,不是吧,reducer 可是一个纯函数啊,小老弟,不知道纯函数的去科普一下哈

还有一个问题,不应该说是缺点,就是想讨论 : 相对于 redux 的集中式管理 store 与 hox 的分散式且无 dev-tools 情况下的管理

最后

如果我是一个新人,我就只会 react,什么 redux、什么 dva、mobx 我都不会,我就只想写一些小项目,那么我会选择 hox ~~

对于小项目,可能需要 store 存储的字段比较少,那么配上简陋版本的 dev-tools,那么hox 或许比较适合此场景,如果引入 redux 那套 action->saga->reducer 等,前期投入比较大;但是对于大项目,还是用 redux 吧,毕竟 redux 算比较成熟,生态圈也很丰富

个人还是觉得 hox 这个设计思想还是挺不错的,但是我很担心一点,那就是 : 这会不会是一个 KPI 的产物,到后期没人维护,也没人更新。

好了,对于 hox,今天就聊到这,大家散了散了。最后建议,感兴趣的可以去看看源码,确实还可以,涨知识了...

相关链接

  • hox
  • 团队博客
  • 阿宽的博客
  • 【KT】查缺补漏 React 状态管理探索
  • 【KT】轻松搞定 Redux 源码解读与编程艺术

对了,未经原博主同意,禁止转载,不然我见一个,举报一个,有点硬~~

以上是 【KT】针对Hox,我写了个简陋组件版dev-tools 的全部内容, 来源链接: utcz.com/a/35180.html

回到顶部