封装一个键盘快捷键的hook

1,707 阅读2分钟

设计思路

React hooks给我们提供了useState等hook,那么我们想要设计一个useKeyboardShortcut的hook,可以这样来思考:

  1. 需求:提供快捷键、快捷键组合和回调函数,当按下的按键为目标时,执行回调函数。
  2. 传入的参数:useKeyboardShortcut(shortcutString, callback)
  3. 解析传入的按键,将字符串进行处理,匹配到对应按键
  4. 编写按下和抬起的监听
  5. 将监听事件注册到window

实现过程

按键的解析

在这里我们需要对传入的字符串进行解析,传入的可能是单个按键:"f",或者是组合按键:"ctrl+f"

const usekeyboardShortcut = (command: string, callback: Function) => {
	const shortcutKeys = command.split("+") // 将传入的符号转变为数组
  	const initKeysMapping = shortcutKeys.reduce((pre, cur) => {
    		pre[cur.toLowerCase()] = false;
    		return pre
 	 },{});
}

经过这些预处理后,我们将传入的按键变为了一个map,{key: false},将其转化为了按键-是否按下的键值对形式。

创建reducer

这里我们需要reducer来进行按键状态的管理

const keysReducer = (state: any, action: any) => {
  switch(action.type) {
    case "key-down":
      const keyDownState = { ...state, [action.key]: true };
      return keyDownState

    case "key-up":
      const keyUpState = { ...state, [action.key]: false };
      return keyUpState

    case "reset-keys":
      const resetState = { ...action.data };
      return resetState;
    default:
      return state
  }
}

const [keys, setKeys] = useReducer(keysReducer, initKeysMapping);

按下把对应的key置为true,松开置为false,后文会解释为什么使用reducer来管理而不用state

创建相应按键的函数

 const keydownListener = assignedKey => (keydownEvent: any) => {
      const loweredKey = assignedKey.toLowerCase();

      keydownEvent.stopPropagation();
      keydownEvent.cancelBubble = true;
      if (keydownEvent.repeat) return
      if (blacklistedTargets.includes(keydownEvent.target.tagName)) return;
      if (loweredKey !== keydownEvent.key.toLowerCase()) return;
      if (keys[loweredKey] === undefined) return;

      setKeys({ type: "key-down", key: loweredKey });
      return false;
},

把传入key,和event进行对比,这里注意要判定repeate的情况,因为一直按下去会持续触发keydown,所以可以用repeat来判断只执行一次

适当的情况下进行触发

  useEffect(() => {
    if (!Object.values(keys).filter(value => !value).length) {
      callback(keys)
    } else {
      setKeys({ type: null });
    }
  }, [callback, keys, initKeysMapping]);

使用useeffect,在keys改变的时候,也就是reducer起作用的时候,判断是否所有的键都按下去了,如果都按下去,那就直接进行一个函数的触发,依赖列表也很清晰,就是keys,回调函数。这里也要说明一下为什么不能用state,如果用state,state改变,useeffect触发,就会造成死循环

react 的文档中,明确提出了 useReducer 是 useState 的替代方案,这里是文档原话

在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等

这里使用useReducer可以解决这个问题,这里是有个setKeys,但其实如果删掉的话,不在useEffect里面调用setKeys,也可以做,都可以实现的

添加到事件监听器

  useEffect(() => {
    shortcutKeys.forEach(key => window.addEventListener("keydown", keydownListener(key)));
    return () => shortcutKeys.forEach(key => window.removeEventListener("keydown", keydownListener(key)))
  }, [])

  useEffect(() => {
    shortcutKeys.forEach(key => window.addEventListener("keyup", keyupListener(key)));
    return () => shortcutKeys.forEach(key => window.removeEventListener("keyup", keyupListener(key)))
  }, [])

添加keydown和keyup listener,在停止挂载的时候进行一个删除回调