你可能见过一些专业的 Web 应用,例如 Google Sheet、Figma 或其他功能丰富的应用。键盘交互可以提升这些应用的用户体验。你也可以在你的 React 应用中添加这些键盘交互。在本文中,我将尝试解释如何在你的应用中构建一个更简洁、开发者友好且符合最佳实践的键盘交互。
方法:中央快捷方式管理器
假设你的应用有一个智能的快捷键管理器,可以管理所有键盘快捷键。它的工作很简单:
- 聆听应用程序中任意位置按下的每个按键
- 了解所有可用的快捷键及其作用
- 忽略在文本(或可编辑)字段中按下的键(关键细节!)
- 立即触发正确的操作
这就是核心思想。你不再需要将数十个独立的按键监听器分散在各个组件中,而是拥有一个集中的大脑,可以集中管理快捷键。这让一切变得简单:避免冲突、管理内存,并保持代码简洁。
实施概览
此实现将包含两个主要部分或文件:
- 上下文提供程序 (ShortcutProvider):此组件包装了整个应用程序。它成为保存所有快捷方式及其功能的中央注册表。
- 自定义钩子 (useShortcuts):这是任何组件与提供程序通信的方式。它是一个简单的 API,包含两个函数:注册和注销。简单来说,就像这样:“嘿,管理员,请帮我注册这个快捷方式,这是我触发此快捷方式时要执行的功能”或“我已完成此快捷方式的使用,请注销它”。
这是使用此模式的美妙之处:
// Deep inside any component, you can easily add a shortcut.
function SaveButton() {const { register} = useShortcuts();
// Register 'Ctrl+S' to save the document
useEffect(() => {const handleSave = () => {/* Save logic here */};
register({key: 's', modifiers: ['Ctrl'] }, handleSave);
return () => unregister({ key: 's', modifiers: ['Meta'] }); // Cleanup!
}, []);
简单至极,组件无需了解全局事件监听器,你还可以在“ShortcutProvider”中添加更严谨的冲突逻辑。它只需请求用户组件注册其意图即可。其余部分由中央管理器处理。
为什么这是一个更好的方法
- 清除代码
- 使用 最佳实践,结合上下文来响应钩子模式
- 团队友好,因为它为团队中的每个人创建了一种标准、简单的方法来添加和删除快捷方式,而不会互相干扰。
以下是完整代码:
ShortcutsProvider.tsx
"use client";
import React, {createContext, useEffect, useRef} from "react";
import {
Modifier,
Shortcut,
ShortcutHandler,
ShortcutRegistry,
ShortcutsContextType,
} from "./types";
export const ShortcutsContext = createContext<ShortcutsContextType>({register: () => {},
unregister: () => {},
});
const normalizeShortcut = (shortcut: Shortcut): string => {const mods = shortcut.modifiers?.slice().sort() || []; // Sort alphabetically
const key = shortcut.key.toUpperCase(); // Normalize case
return [...mods, key].join("+");
};
const ShortcutProvider = ({children}: {children: React.ReactNode}) => {
// Registry for shortcut with key as shortcut combination and value as the handler
const ShortcutRegisteryRef = useRef<ShortcutRegistry>(new Map());
const register = (
shortcut: Shortcut,
handler: ShortcutHandler,
override = false
) => {
const ShortcutRegistery = ShortcutRegisteryRef.current;
// before proceeding with logic let normalized the key
// first we sort the modifiers from shortcut alphabetically
// then make key uppercase for consistency
const modifiers = shortcut.modifiers
? shortcut.modifiers.slice().sort()
: [];
const key = shortcut.key.toUpperCase();
const normalizedKey = [...modifiers, key].join("+");
// here checking for conflicts
if (ShortcutRegistery.has(normalizedKey) && !override) {
console.warn(`Conflict: "${normalizedKey}" is already registered for shortcut. Use override=true to replace or handle conflict.`
);
return;
}
ShortcutRegistery.set(normalizedKey, handler);
};
const unregister = (shortcut: Shortcut) => {
// again normaizing the key, we are repeating this code so better to make function out of this
const modifiers = shortcut.modifiers
? shortcut.modifiers.slice().sort()
: [];
const key = shortcut.key.toUpperCase();
const normalizedKey = [...modifiers, key].join("+");
ShortcutRegisteryRef.current.delete(normalizedKey);
};
const handleKeyDown = (event: KeyboardEvent) => {
const target = event.target as HTMLElement;
// this check is important as without this we wont be able to write in these inputs
if (
target.tagName === "INPUT" ||
target.tagName === "TEXTAREA" ||
target.isContentEditable
) {return;}
const modifiers: Modifier[] = [];
if (event.ctrlKey) modifiers.push("Ctrl");
if (event.altKey) modifiers.push("Alt");
if (event.shiftKey) modifiers.push("Shift");
if (event.metaKey) modifiers.push("Meta");
const key = event.key.toUpperCase();
const normalizedKey = [...modifiers.sort(), key].join("+");
const handler = ShortcutRegisteryRef.current.get(normalizedKey);
if (handler) {event.preventDefault();
handler(event);
}
};
useEffect(() => {window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, []);
return (<ShortcutsContext.Provider value={{ register, unregister}}>
{children}
</ShortcutsContext.Provider>
);
};
export default ShortcutProvider;
useShortcuts.tsx
import {useContext} from "react";
import {ShortcutsContext} from "./ShortcutsProvider";
const useShortcuts = () => {const shortcutContext = useContext(ShortcutsContext);
if (!shortcutContext) {console.error("Shortcut context must be wrapped inside Shortcut provider");
}
return shortcutContext;
};
export default useShortcuts;
types.ts
export type Modifier = string;
export type Key = string;
export interface Shortcut {
key: Key;
modifiers?: string[];
}
export type ShortcutHandler = (e: KeyboardEvent) => void;
export interface ShortcutsContextType {register: (shortcut: Shortcut, handler: ShortcutHandler) => void;
unregister: (shortcut: Shortcut) => void;
}
export type ShortcutRegistry = Map<string, ShortcutHandler>;
主应用程序组件(此处使用 Next js)
export default function RootLayout({children,}: Readonly<{children: React.ReactNode;}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<ShortcutProvider>{children}</ShortcutProvider>
</body>
</html>
);
}
您可以在 GitHub 上找到带有可行示例的完整解决方案。
正文完