Gutenberg区块:如何利用`useSelect`和`useDispatch`与数据存储交互,并实现自定义数据存储?

Gutenberg 区块:useSelectuseDispatch 的妙用,打造自定义数据存储

大家好!今天,我们深入探讨 Gutenberg 区块开发中数据存储的关键:useSelectuseDispatch。我们将不仅了解它们的基本用法,更要掌握如何利用它们与 WordPress 数据存储交互,甚至创建自定义的数据存储,从而构建更强大、更灵活的 Gutenberg 区块。

useSelect:高效的数据提取器

useSelect@wordpress/data 包提供的 React Hook,用于从 WordPress 数据存储中高效地提取数据。它的核心在于订阅机制:当数据存储中的数据发生变化时,useSelect 会自动重新执行选择器函数,更新组件的状态,从而实现组件的自动更新。

基本用法:

import { useSelect } from '@wordpress/data';

function MyComponent() {
  const postTitle = useSelect(
    (select) => {
      const post = select('core/editor').getCurrentPost();
      return post ? post.title.raw : '';
    },
    [] // 依赖项数组
  );

  return (
    <div>
      <h1>{postTitle}</h1>
    </div>
  );
}

代码解释:

  • import { useSelect } from '@wordpress/data';: 引入 useSelect Hook。

  • useSelect((select) => { ... }, []): 调用 useSelect Hook,它接受两个参数:

    • 选择器函数 (select) => { ... }: 这是一个函数,它接收一个 select 对象作为参数。select 对象提供了一系列方法,用于从不同的数据存储中选择数据。在这个例子中,我们使用 select('core/editor') 来访问 core/editor 数据存储,该存储包含了当前编辑的 post 相关的信息。
    • 依赖项数组 []: 这是一个可选的数组,用于指定选择器函数所依赖的值。当依赖项数组中的任何一个值发生变化时,选择器函数会被重新执行。如果依赖项数组为空,则选择器函数只会在组件挂载时执行一次,之后只会在数据存储发生变化时执行。
  • const post = select('core/editor').getCurrentPost();: 使用 select 对象从 core/editor 数据存储中获取当前 post 对象。

  • return post ? post.title.raw : '';: 如果 post 对象存在,则返回 post 的标题;否则返回一个空字符串。

  • <h1>{postTitle}</h1>: 在组件中渲染 post 的标题。

更高级的用法:

useSelect 还可以接收第二个参数,即依赖项数组。这个数组告诉 useSelect 何时重新执行选择器函数。如果依赖项数组为空,则选择器函数只会在组件挂载时执行一次。如果依赖项数组包含某些变量,则当这些变量的值发生变化时,选择器函数会被重新执行。

import { useSelect } from '@wordpress/data';
import { useState } from '@wordpress/element';

function MyComponent() {
  const [postId, setPostId] = useState(1);

  const postTitle = useSelect(
    (select) => {
      const post = select('core').getEntityRecord('postType', 'post', postId);
      return post ? post.title.raw : '';
    },
    [postId] // 依赖项数组:postId
  );

  return (
    <div>
      <h1>{postTitle}</h1>
      <button onClick={() => setPostId(postId + 1)}>Next Post</button>
    </div>
  );
}

在这个例子中,useSelect 的依赖项数组包含 postId 变量。当 postId 的值发生变化时,选择器函数会被重新执行,从而更新 post 的标题。

性能优化:

useSelect 的一个重要优势是其内置的性能优化。它会对选择器函数的结果进行浅比较,只有当结果发生变化时才会触发组件的重新渲染。这可以有效地减少不必要的渲染,提高组件的性能。

useDispatch:掌控数据流动的指挥官

useDispatch 同样是 @wordpress/data 包提供的 React Hook,用于获取数据存储的 dispatch 函数。dispatch 函数用于触发数据存储中的 actions,从而修改数据存储的状态。

基本用法:

import { useDispatch } from '@wordpress/data';

function MyComponent() {
  const { updatePost } = useDispatch('core/editor');

  const handleClick = () => {
    updatePost({ title: 'New Title' });
  };

  return (
    <button onClick={handleClick}>Update Title</button>
  );
}

代码解释:

  • import { useDispatch } from '@wordpress/data';: 引入 useDispatch Hook。
  • const { updatePost } = useDispatch('core/editor');: 调用 useDispatch Hook,它接收一个数据存储的名称作为参数,并返回一个包含该数据存储的所有 actions 的对象。在这个例子中,我们使用 useDispatch('core/editor') 来访问 core/editor 数据存储的 actions。
  • const handleClick = () => { ... };: 定义一个事件处理函数,当按钮被点击时,该函数会被调用。
  • updatePost({ title: 'New Title' });: 调用 updatePost action,并传递一个新的标题作为参数。这将更新当前 post 的标题。
  • <button onClick={handleClick}>Update Title</button>: 创建一个按钮,当按钮被点击时,调用 handleClick 函数。

更高级的用法:

useDispatch 可以与 useSelect 结合使用,实现更复杂的数据交互。例如,我们可以使用 useSelect 从数据存储中获取当前 post 的 ID,然后使用 useDispatch 更新该 post 的标题。

import { useSelect, useDispatch } from '@wordpress/data';

function MyComponent() {
  const postId = useSelect(
    (select) => select('core/editor').getCurrentPostId(),
    []
  );
  const { editEntityRecord } = useDispatch('core');

  const handleClick = () => {
    editEntityRecord(
      'postType',
      'post',
      postId,
      { title: 'Updated Title' }
    );
  };

  return (
    <button onClick={handleClick}>Update Title</button>
  );
}

在这个例子中,我们首先使用 useSelectcore/editor 数据存储中获取当前 post 的 ID。然后,我们使用 useDispatchcore 数据存储中获取 editEntityRecord action。最后,我们调用 editEntityRecord action,并传递 post 的 ID 和新的标题作为参数。

注意事项:

  • useDispatch 返回的 dispatch 函数是一个稳定的引用,这意味着它不会在每次渲染时都发生变化。这使得我们可以安全地将其传递给子组件,而不用担心子组件会不必要地重新渲染。
  • 在使用 useDispatch 时,需要确保传递给 actions 的数据是正确的类型。例如,如果 actions 期望接收一个字符串,则必须传递一个字符串,而不是一个数字或一个对象。

创建自定义数据存储:数据管理新思路

WordPress 数据存储 API 允许我们创建自定义的数据存储,从而更好地管理 Gutenberg 区块的数据。这在构建复杂区块或需要与其他系统集成时尤其有用。

步骤 1:定义数据存储

首先,我们需要定义数据存储。这包括定义数据存储的名称、状态、actions 和 selectors。

// my-plugin/src/data/index.js

import { registerStore } from '@wordpress/data';

const DEFAULT_STATE = {
  items: [],
};

const actions = {
  addItem(item) {
    return {
      type: 'ADD_ITEM',
      item,
    };
  },
  removeItem(itemId) {
    return {
      type: 'REMOVE_ITEM',
      itemId,
    };
  },
};

const reducer = (state = DEFAULT_STATE, action) => {
  switch (action.type) {
    case 'ADD_ITEM':
      return {
        ...state,
        items: [...state.items, action.item],
      };
    case 'REMOVE_ITEM':
      return {
        ...state,
        items: state.items.filter((item) => item.id !== action.itemId),
      };
    default:
      return state;
  }
};

const selectors = {
  getItems(state) {
    return state.items;
  },
  getItem(state, itemId) {
    return state.items.find((item) => item.id === itemId);
  },
};

const storeConfig = {
  reducer,
  actions,
  selectors,
  persist: false, // 是否将数据存储持久化到本地存储
};

registerStore('my-plugin/items', storeConfig);

代码解释:

  • registerStore('my-plugin/items', storeConfig);: 使用 registerStore 函数注册数据存储。第一个参数是数据存储的名称,第二个参数是数据存储的配置对象。
  • DEFAULT_STATE: 定义数据存储的初始状态。
  • actions: 定义数据存储的 actions。每个 action 都是一个函数,它返回一个包含 type 属性的对象。type 属性用于标识 action 的类型。
  • reducer: 定义数据存储的 reducer。reducer 是一个函数,它接收当前的状态和一个 action 作为参数,并返回一个新的状态。reducer 必须是一个纯函数,这意味着它不应该有任何副作用。
  • selectors: 定义数据存储的 selectors。每个 selector 都是一个函数,它接收当前的状态作为参数,并返回一个从状态中提取的数据。
  • persist: 指定是否将数据存储持久化到本地存储。如果设置为 true,则数据存储的状态会在浏览器关闭后仍然保留。

步骤 2:使用数据存储

在定义了数据存储之后,我们就可以在 Gutenberg 区块中使用它了。

import { useSelect, useDispatch } from '@wordpress/data';

function MyBlock() {
  const items = useSelect((select) => select('my-plugin/items').getItems(), []);
  const { addItem, removeItem } = useDispatch('my-plugin/items');

  const handleAddItem = () => {
    const newItem = {
      id: Date.now(),
      name: 'New Item',
    };
    addItem(newItem);
  };

  const handleRemoveItem = (itemId) => {
    removeItem(itemId);
  };

  return (
    <div>
      <button onClick={handleAddItem}>Add Item</button>
      <ul>
        {items.map((item) => (
          <li key={item.id}>
            {item.name}
            <button onClick={() => handleRemoveItem(item.id)}>Remove</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

代码解释:

  • const items = useSelect((select) => select('my-plugin/items').getItems(), []);: 使用 useSelect Hook 从 my-plugin/items 数据存储中获取所有 items。
  • const { addItem, removeItem } = useDispatch('my-plugin/items');: 使用 useDispatch Hook 从 my-plugin/items 数据存储中获取 addItemremoveItem actions。
  • handleAddItem: 定义一个事件处理函数,当点击 "Add Item" 按钮时,该函数会被调用。该函数创建一个新的 item,并使用 addItem action 将其添加到数据存储中。
  • handleRemoveItem: 定义一个事件处理函数,当点击 "Remove" 按钮时,该函数会被调用。该函数使用 removeItem action 从数据存储中删除指定的 item。

步骤 3:插件入口文件

最后,需要在插件的入口文件中引入数据存储的定义。

// my-plugin/index.php
<?php
/**
 * Plugin Name: My Plugin
 */

function my_plugin_register_block() {
    wp_register_script(
        'my-plugin-block',
        plugins_url( 'build/index.js', __FILE__ ),
        array( 'wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-data' ),
        filemtime( plugin_dir_path( __FILE__ ) . 'build/index.js' )
    );

    register_block_type( 'my-plugin/my-block', array(
        'editor_script' => 'my-plugin-block',
    ) );
}
add_action( 'init', 'my_plugin_register_block' );

function my_plugin_enqueue_block_editor_assets() {
    wp_enqueue_script(
        'my-plugin-data',
        plugins_url( 'src/data/index.js', __FILE__ ),
        array( 'wp-data' ),
        filemtime( plugin_dir_path( __FILE__ ) . 'src/data/index.js' )
    );
}
add_action( 'enqueue_block_editor_assets', 'my_plugin_enqueue_block_editor_assets' );

代码解释:

  • my_plugin_enqueue_block_editor_assets 函数使用 wp_enqueue_script 函数将 src/data/index.js 文件加载到 Gutenberg 编辑器中。这确保了数据存储在 Gutenberg 编辑器中可用。

通过以上步骤,我们就成功创建了一个自定义的数据存储,并在 Gutenberg 区块中使用了它。

案例分析:构建一个可排序的列表区块

让我们通过一个更具体的案例来演示 useSelectuseDispatch 的强大功能:构建一个可排序的列表区块。

数据存储定义:

// src/data/sortable-list.js
import { registerStore } from '@wordpress/data';

const DEFAULT_STATE = {
  items: [],
};

const actions = {
  setItems(items) {
    return {
      type: 'SET_ITEMS',
      items,
    };
  },
  moveItem(fromIndex, toIndex) {
    return {
      type: 'MOVE_ITEM',
      fromIndex,
      toIndex,
    };
  },
};

const reducer = (state = DEFAULT_STATE, action) => {
  switch (action.type) {
    case 'SET_ITEMS':
      return {
        ...state,
        items: action.items,
      };
    case 'MOVE_ITEM':
      const newItems = [...state.items];
      const item = newItems.splice(action.fromIndex, 1)[0];
      newItems.splice(action.toIndex, 0, item);
      return {
        ...state,
        items: newItems,
      };
    default:
      return state;
  }
};

const selectors = {
  getItems(state) {
    return state.items;
  },
};

const storeConfig = {
  reducer,
  actions,
  selectors,
};

registerStore('my-plugin/sortable-list', storeConfig);

区块组件:

// src/block/index.js
import { useSelect, useDispatch } from '@wordpress/data';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, TextControl } from '@wordpress/components';
import { useState, useEffect } from '@wordpress/element';

function SortableListBlock({ attributes, setAttributes }) {
  const { items: attributeItems } = attributes;
  const [tempItems, setTempItems] = useState(attributeItems || []);
  const { setItems, moveItem } = useDispatch('my-plugin/sortable-list');
  const items = useSelect((select) => select('my-plugin/sortable-list').getItems(), [tempItems]);
  const blockProps = useBlockProps();

  useEffect(() => {
    setItems(tempItems);
  }, [tempItems, setItems]);

  useEffect(() => {
    setAttributes({ items: items });
  }, [items, setAttributes]);

  const handleItemChange = (index, value) => {
    const newItems = [...tempItems];
    newItems[index] = value;
    setTempItems(newItems);
  };

  const handleMoveUp = (index) => {
    moveItem(index, index - 1);
    setTempItems([...items])

  };

  const handleMoveDown = (index) => {
    moveItem(index, index + 1);
    setTempItems([...items])
  };

  const handleAddItem = () => {
    setTempItems([...tempItems, ""]);
  }

  const handleRemoveItem = (index) => {
    const newItems = [...tempItems];
    newItems.splice(index, 1);
    setTempItems(newItems);
  }

  return (
    <div {...blockProps}>
      <InspectorControls>
        <PanelBody title="List Items">
          {tempItems.map((item, index) => (
            <div key={index}>
              <TextControl
                label={`Item ${index + 1}`}
                value={item}
                onChange={(value) => handleItemChange(index, value)}
              />
              <button onClick={() => handleMoveUp(index)} disabled={index === 0}>Up</button>
              <button onClick={() => handleMoveDown(index)} disabled={index === tempItems.length - 1}>Down</button>
              <button onClick={() => handleRemoveItem(index)}>Remove</button>
            </div>
          ))}
          <button onClick={handleAddItem}>Add Item</button>
        </PanelBody>
      </InspectorControls>
      <ul>
        {items.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

export default SortableListBlock;

代码解释:

  • useSelect: 用于从 my-plugin/sortable-list 数据存储中获取 items
  • useDispatch: 用于获取 setItemsmoveItem actions。
  • handleItemChange: 当列表项的内容发生变化时,更新 tempItems 状态。
  • handleMoveUphandleMoveDown: 调用 moveItem action 来移动列表项的位置,并更新 tempItems 状态。
  • handleAddItem: 添加新的列表项到 tempItems 状态。
  • handleRemoveItem: 从 tempItems 状态删除列表项。
  • 使用 useEffect 同步 tempItemsitems 和 Block的Attributes

注册区块:

// src/index.js
import { registerBlockType } from '@wordpress/blocks';
import SortableListBlock from './block';
import './data/sortable-list'; // 引入数据存储

registerBlockType('my-plugin/sortable-list-block', {
  title: 'Sortable List',
  icon: 'list-view',
  category: 'common',
  attributes: {
    items: {
      type: 'array',
      default: [],
    },
  },
  edit: SortableListBlock,
  save: () => null, // 动态渲染,无需保存静态 HTML
});

这个案例展示了如何使用 useSelectuseDispatch 来管理区块的复杂状态,并实现交互式功能。

数据存储的优势与局限

优势:

  • 状态管理集中化: 将区块的状态集中存储在数据存储中,方便管理和维护。
  • 组件解耦: 区块组件不需要直接操作数据,而是通过 actions 来修改数据存储的状态,从而实现组件的解耦。
  • 可测试性: 数据存储的 actions 和 reducers 都是纯函数,易于测试。
  • 性能优化: useSelect 具有内置的性能优化机制,可以减少不必要的渲染。

局限:

  • 学习曲线: 掌握 WordPress 数据存储 API 需要一定的学习成本。
  • 过度使用: 对于简单的区块,使用数据存储可能会显得过于复杂。

区块开发的精髓

通过深入学习 useSelectuseDispatch,我们可以更好地理解 Gutenberg 区块开发的数据流,并能够构建更复杂、更强大的区块。自定义数据存储为我们提供了更大的灵活性,可以更好地管理区块的数据,并实现与其他系统的集成。选择合适的数据管理方式,才能写出更优秀的代码。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注