hox 状态调试神器

avatar
SugarTurboS Club @SugarTurboS

背景

博主最近对 React 状态管理异常喜欢,前段时间也写了个 redux 中间件: rc-redux-model,但是已经被 hook 黑化,有没有 hook 写法,就能做到状态管理?于是了解到了 : hox

如果你对 hox 不了解,可以前往官方网站了解一波,如果你想了解 hox 源码,可以看这篇文章hox 源码解读。那么从官方中,我们可以得到 Hox 的相关介绍:下一代的 React 状态管理器,只存在一个 API,那就是 createModel

真香,于是去把 hox 源码读了一遍之后,在实际落地后,我发现,其他问题都可以避免/解决,但是很蛋疼的莫过于 : 无 dev tools 支持,你想想,我们使用了 createModel 包裹之后,如何知道这个数据是否真的被持久化、全局共享呢 ?

常规操作就是,在组件中 import 这个数据源,然后 console.log 打印看看,还有一种情况,那就是项目有其他小伙伴接手了,然后他想查看下目前全局共享的状态有哪些,怎么办?

官方 issues 也有人问到了此问题,同时我们项目中,有些小型项目或者独立模块,都有用到了 hox,于是,我决定先写一个简易版的 devtools,凑合着用,所以 sugar-hox-devtools 出现了...

经过邮件询问 umi/hox 的开发者之一 brickspert ,在征得同意后,博主 fork 仓库,然后进行源码修改,写了一个 npm 包。

最终效果

相关说明

  • sugar-hox-dev-tools 是一个带有 hox 原 API 及导出一个展示组件的 npm 包,用于展示被 createModel 包裹的数据。[更多功能计划中]

  • 修改了 hox 一小部分源码,如果依赖 hox 包,那么得在打包的时候,webpack 注入我自己修改的部分代码,相对麻烦,成本也高。于是邮箱问过 umi/hox 的开发者之一 brickspert,经过同意且此仓库为 MIT 协议,所以 fork 了此仓库并拉取源码进行修改,相关 API 仍保持与 hox 一致

  • 之所以做这个 devtools,原因在于我们使用了 createModel 包裹之后,不知道这个数据是否真的被持久化、全局共享,只能通过在组件中 import 这个数据源,然后 console.log 打印,然后看是否真的被修改。

  • 还有就是当其他人接手项目之后,他想查看一些已经存在于全局共享的 Model,并没有一个展示全局共享数据的渠道,所以 sugar-hox-dev-tools 出生了。

由于我们项目中,有些小型项目或者独立模块,都有用到了 hox,于是,我决定先写一个简易版的 devtools,凑合着用,于是 sugar-hox-devtools 出现了

快速开始

安装

npm install sugar-hox-devtools --save

创建一个 model

这里的说明拷至 hox 文档,更多详情点击这里访问

import { useState } from 'react'
import hoxAPI from 'sugar-hox-devtools'

function useCounter() {
  const [count, setCount] = useState(0)
  const decrement = () => setCount(count - 1)
  const increment = () => setCount(count + 1)
  return {
    count,
    decrement,
    increment,
  }
}

useCounter.namespace = 'useCounter' // 这里需要给每一个 model 都添加命名空间标识

export default hoxAPI.createModel(useCounter)

使用 model

这里的说明拷至 hox 文档,更多详情点击这里访问

// 在组件中调用这个 Hook ,就可以获取到 model 的数据了。
import counterModel from '../models/counter'

function App(props) {
  const { counter, increment, decrement } = counterModel()
  return (
    <div>
      <p>{counter}</p>
      <button onClick={decrement}>-1</button>
      <button onClick={increment}>+1</button>
    </div>
  )
}

开启 DevTools

sugar-hox-devtools 抛出一个组件,调用此组件即可;所有经过createModel包裹后的 model,都会被注入添加到 window.sugarHox 上。控制台打印 window.sugarHox 就能拿到数据。

import React, { useState } from 'react'
import sugar from 'sugar-hox-devtools'

const SugarHoxDevTools = sugar.SugarHoxDevTools

function App() {
  const [showDevTools, setShowDevTools] = useState(true)

  return (
    <div>
      ...
      {showDevTools && (
        <SugarHoxDevTools onClose={() => setShowDevTools(false)} />
      )}
    </div>
  )
}

组件参数

参数说明类型默认值
title自定义stringsugar-hox-devtools
closeIcon关闭 iconReactNodeX
onClose点击 closeIcon 方法() => void-
maxScrollHeight容器滚动的最大高度number200

主要思路

我们要知道一点,那就是 hox 提供的 API ,是通过 custom Hooks 来定义 model 的

在我们不使用 hox 的 API 时,我们写的 hook 就是一个普通 hook,如果通过 hox 的 createModel 包裹之后,这个 hook 就变成了持久化,且全局共享的数据。

清楚这个特点很关键!搞清楚这点之后,开发这个 devtools 就变得简单了~

大致思路 :

  • 将所有被 createModel 包裹后的 hook,都挂载到 window 下 [这就得修改 hox 的源码了]
  • 目前暂定只做 model 数据的展示,自然而然的,得把 custom hook 中的函数类型剔除掉
  • 解决掉数据响应问题,得到 model 的最新值,用于组件中展示
  • 递归遍历,数据处理

步骤

实现每个 hook 唯一

我们给 createModel 传递的是一个 hook,那么我们给每一个 hook 注入一个命名空间,确保其唯一性,同时也是为了挂载到 window 上时,每一个 key 对应的 model 数据

// 自定义的hook
function useSugarModel() {}
useSugarModel.namespace = 'useSugarModel'

export default createModel(useSugarModel)

然后修改 hox 中 createModel 源码,原来只需要给 Executor 传递 2 个字段,现在给它支持 namespace,代码可看这里

export function createModel<T, P>(hook: ModelHook<T, P>, hookArg?: P) {
  const container = new Container(hook);
  render(
    <Executor
      onUpdate={val => {
        container.data = val;
        container.notify();
      }}
      hook={() => hook(hookArg)}
      namespace={(hook as any).namespace} // 新增支持 namespace 标识
    />
  );
}

过滤函数方法,挂载到 window 上

我们前期暂时只需要数据,并不需要知道有什么方法,所以可以现将 hook 中的方法过滤,然后将 hook 挂载到 window 下,代码可看这里

export function Executor<T>(props: {
  hook: () => ReturnType<ModelHook<T>>
  onUpdate: (data: T) => void
  namespace: string
}) {
  const data = props.hook()
  props.onUpdate(data)
  // 下面为修改的代码,挂载到 window.sugarHox 下
  if (!(window as any).sugarHox) {
    window.sugarHox = {}
  }
  let maps = {}
  const keys = Object.keys(data)

  for (let key of keys) {
    if (typeof data[key] !== 'function') {
      maps[key] = data[key]
    }
  }

  window.sugarHox = {
    ...window.sugarHox,
    [props.namespace]: { ...maps },
  }

  return null as ReactElement
}

数据响应,监听 window.sugarHox 的改变

我们可以通过 window.sugarHox 去得到所有通过 createModel 挂载到全局的共享状态数据,但还有个问题,那就是我们修改 model 值之后,我们要实时响应,于是通过 Object.defineProperty 进行重写 set 、get 方法,代码看这里

// sugarDevTools 组件
componentDidMount() {
  window.tempHox = {};
  const _this = this;
  Object.defineProperty(window, 'sugarHox', {
    set: function(value) {
      window.tempHox = value;
      _this.setState({
        model: value
      });
    },
    get: function() {
      return window.tempHox;
    }
  });
}

由于使用 Object.defineProperty 会存在一些问题,于是改用 Proxy 进行代理,感兴趣的可以去看看这两者的区别

递归遍历数据

核心点就是将 window.sugarHox 对象,构造成 antd tree 所支持的数据格式,代码在这里

最后

读到这你已经把这个简易版的 sugar-hox-devtools 基本思想掌握了,后期正在规划该 devtools 的更多功能,借鉴 redux-devtools-extension 的一些功能,核心不是造轮子,而是通过造轮子去思考一些问题,去学到一些东西。加油,打工人~

相关链接