Gutenberg区块动态数据绑定:实时内容更新

Gutenberg区块动态数据绑定:实时内容更新

【免费下载链接】gutenberg The Block Editor project for WordPress and beyond. Plugin is available from the official repository. 【免费下载链接】gutenberg 项目地址: https://gitcode.com/GitHub_Trending/gu/gutenberg

引言:告别静态内容的痛点

你是否还在为WordPress编辑器中静态内容的局限性而烦恼?当数据源更新时,你是否需要手动修改每个相关区块?Gutenberg(古腾堡)编辑器的动态数据绑定功能彻底改变了这一现状。本文将深入探讨如何利用Gutenberg的数据流架构和现代React hooks实现区块内容的实时更新,让你的WordPress网站内容保持鲜活。

读完本文后,你将能够:

  • 理解Gutenberg的数据流动机制和Store架构
  • 掌握useSelect和useDispatch hooks的使用方法
  • 实现区块与数据源的实时同步
  • 创建自定义动态区块并处理数据变更
  • 解决常见的动态数据绑定问题

Gutenberg数据架构解析

数据流动模型

Gutenberg采用单向数据流架构,确保数据变更的可预测性和可追踪性。核心数据流程如下:

mermaid

这种架构基于Redux模式,包含以下关键组件:

  • Store: 中央数据仓库,保存应用状态
  • Actions: 修改状态的唯一途径
  • Reducers: 指定如何响应Actions更新状态
  • Selectors: 获取和计算状态数据

核心数据API

Gutenberg提供了强大的数据操作API,主要包含在@wordpress/data@wordpress/core-data包中:

API作用
useSelect从Store中选择数据并订阅变化
useDispatch获取派发Actions的函数
getEntityRecords获取指定类型的实体记录
getEntityRecord获取单个实体记录
saveEntityRecord保存实体记录

实时数据绑定实战:Latest Posts区块解析

数据获取与订阅

Gutenberg的"最新文章"区块是动态数据绑定的典范。让我们深入分析其实现:

const { latestPosts } = useSelect(
  ( select ) => {
    const { getEntityRecords } = select( coreStore );
    const catIds = categories && categories.length > 0 
      ? categories.map( ( cat ) => cat.id ) 
      : [];
    const latestPostsQuery = {
      categories: catIds,
      author: selectedAuthor,
      order,
      orderby: orderBy,
      per_page: postsToShow,
      _embed: 'author,wp:featuredmedia',
      ignore_sticky: true,
    };

    return {
      latestPosts: getEntityRecords( 'postType', 'post', latestPostsQuery ),
    };
  },
  [ postsToShow, order, orderBy, categories, selectedAuthor ]
);

这段代码展示了useSelect的核心用法:

  1. 接收一个选择器函数,该函数接收select方法获取Store数据
  2. 使用getEntityRecords从WordPress REST API获取文章数据
  3. 第二个参数是依赖数组,当这些值变化时重新执行选择器

数据变更处理

当用户修改区块属性(如文章数量、排序方式)时,useSelect会自动重新执行查询并更新UI:

<QueryControls
  {...{ order, orderBy }}
  numberOfItems={ postsToShow }
  onOrderChange={ ( value ) => setAttributes( { order: value } ) }
  onOrderByChange={ ( value ) => setAttributes( { orderBy: value } ) }
  onNumberOfItemsChange={ ( value ) => setAttributes( { postsToShow: value } ) }
  categorySuggestions={ categorySuggestions }
  onCategoryChange={ selectCategories }
  selectedCategories={ categories }
  onAuthorChange={ ( value ) => setAttributes( { selectedAuthor: value } ) }
  authorList={ authorList ?? [] }
  selectedAuthorId={ selectedAuthor }
/>

这段代码中的事件处理器通过setAttributes更新区块属性,触发useSelect的依赖变化,从而重新获取数据并刷新UI。

从零构建动态区块

步骤1:设置基本区块结构

首先,创建一个基本的区块结构,包含必要的元数据和编辑组件:

import { __ } from '@wordpress/i18n';
import { useBlockProps } from '@wordpress/block-editor';
import { useSelect } from '@wordpress/data';

export default function Edit( { attributes, setAttributes } ) {
  const blockProps = useBlockProps();
  
  // 动态数据获取将在这里实现
  
  return (
    <div { ...blockProps }>
      {/* 区块内容将在这里渲染 */}
    </div>
  );
}

export const settings = {
  title: __( '动态数据示例区块' ),
  description: __( '展示如何实现Gutenberg区块的动态数据绑定' ),
  category: 'widgets',
  attributes: {
    // 区块属性定义
  },
  edit: Edit,
  save: () => null, // 动态内容不在编辑器中保存
};

步骤2:实现数据获取逻辑

使用useSelect从WordPress数据存储中获取动态数据:

import { useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';

// 在Edit组件内部
const { users, isLoading } = useSelect(
  ( select ) => {
    const { getEntityRecords } = select( coreStore );
    
    // 获取最新注册的5位用户
    const query = {
      per_page: 5,
      orderby: 'registered_date',
      order: 'desc',
    };
    
    return {
      users: getEntityRecords( 'root', 'user', query ),
      isLoading: select( coreStore ).isResolving( 'getEntityRecords', [ 'root', 'user', query ] ),
    };
  },
  [] // 空依赖数组表示只在组件挂载时获取一次数据
);

步骤3:渲染动态内容

根据获取的数据渲染区块内容,并处理加载状态:

return (
  <div { ...blockProps }>
    <h3>{ __( '最新注册用户' ) }</h3>
    { isLoading ? (
      <p>{ __( '加载中...' ) }</p>
    ) : users && users.length > 0 ? (
      <ul>
        { users.map( ( user ) => (
          <li key={ user.id }>
            { user.avatar_urls && user.avatar_urls[ 24 ] && (
              <img 
                src={ user.avatar_urls[ 24 ] } 
                alt={ user.name } 
                className="avatar" 
              />
            ) }
            <span className="user-name">{ user.name }</span>
            <span className="user-email">{ user.email }</span>
          </li>
        ) ) }
      </ul>
    ) : (
      <p>{ __( '未找到用户数据' ) }</p>
    ) }
  </div>
);

步骤4:添加数据刷新机制

为区块添加手动刷新按钮,允许用户强制更新数据:

import { useDispatch } from '@wordpress/data';
import { Button } from '@wordpress/components';
import { refresh } from '@wordpress/icons';

// 在Edit组件内部
const { invalidateResolution } = useDispatch( coreStore );

const refreshData = () => {
  const query = {
    per_page: 5,
    orderby: 'registered_date',
    order: 'desc',
  };
  invalidateResolution( 'getEntityRecords', [ 'root', 'user', query ] );
};

// 在渲染部分添加刷新按钮
<div className="refresh-controls">
  <Button
    icon={ refresh }
    label={ __( '刷新数据' ) }
    onClick={ refreshData }
  />
</div>

高级数据绑定技术

多数据源组合

复杂区块可能需要组合多个数据源的数据。以下示例展示如何同时获取文章和评论数据:

const { posts, comments, isLoading } = useSelect(
  ( select ) => {
    const { getEntityRecords } = select( coreStore );
    
    // 获取置顶文章
    const postsQuery = {
      sticky: 'only',
      per_page: 3,
    };
    const posts = getEntityRecords( 'postType', 'post', postsQuery );
    
    // 获取最新评论
    const commentsQuery = {
      per_page: 5,
      order: 'desc',
    };
    const comments = getEntityRecords( 'root', 'comment', commentsQuery );
    
    // 检查数据是否仍在加载中
    const isPostsLoading = select( coreStore ).isResolving( 
      'getEntityRecords', [ 'postType', 'post', postsQuery ] 
    );
    const isCommentsLoading = select( coreStore ).isResolving( 
      'getEntityRecords', [ 'root', 'comment', commentsQuery ] 
    );
    
    return {
      posts,
      comments,
      isLoading: isPostsLoading || isCommentsLoading,
    };
  },
  []
);

自定义数据存储

对于复杂应用,你可能需要创建自定义数据存储来管理特定领域的数据:

import { createReduxStore, register } from '@wordpress/data';

// 定义初始状态
const initialState = {
  items: [],
  isLoading: false,
  error: null,
};

// 定义reducer
function reducer( state = initialState, action ) {
  switch ( action.type ) {
    case 'FETCH_ITEMS_START':
      return { ...state, isLoading: true, error: null };
    case 'FETCH_ITEMS_SUCCESS':
      return { ...state, isLoading: false, items: action.items };
    case 'FETCH_ITEMS_ERROR':
      return { ...state, isLoading: false, error: action.error };
    default:
      return state;
  }
}

// 定义选择器
const selectors = {
  getItems( state ) {
    return state.items;
  },
  isLoading( state ) {
    return state.isLoading;
  },
  getError( state ) {
    return state.error;
  },
};

// 定义动作创建器
const actions = {
  fetchItems: () => {
    return {
      type: 'FETCH_ITEMS_START',
    };
  },
  fetchItemsSuccess: ( items ) => {
    return {
      type: 'FETCH_ITEMS_SUCCESS',
      items,
    };
  },
  fetchItemsError: ( error ) => {
    return {
      type: 'FETCH_ITEMS_ERROR',
      error,
    };
  },
};

// 创建并注册存储
const store = createReduxStore( 'my-custom-store', {
  reducer,
  selectors,
  actions,
} );
register( store );

使用异步数据控制

利用@wordpress/data-controls包处理异步数据获取:

import { createReduxStore, register } from '@wordpress/data';
import { controls } from '@wordpress/data-controls';

// 定义异步动作
const actions = {
  *fetchRemoteData() {
    try {
      yield { type: 'FETCH_START' };
      const response = yield wp.apiFetch( {
        path: '/my-custom-endpoint/v1/data',
      } );
      yield { type: 'FETCH_SUCCESS', data: response };
    } catch ( error ) {
      yield { type: 'FETCH_ERROR', error: error.message };
    }
  },
};

// 创建存储时包含controls
const store = createReduxStore( 'my-async-store', {
  reducer,
  selectors,
  actions,
  controls,
} );
register( store );

性能优化策略

数据缓存与失效

Gutenberg的Store会自动缓存数据请求,避免不必要的网络调用。你可以通过以下方法手动控制缓存:

// 使特定查询的缓存失效
invalidateResolution( 'getEntityRecords', [ 'postType', 'post', query ] );

// 使所有缓存失效
invalidateResolutionForStore( coreStore );

// 预加载数据
prefetch( coreStore ).getEntityRecords( 'postType', 'post', query );

选择性重渲染

使用useSelect的第二个参数(依赖数组)精确控制何时重新获取数据:

// 只有当categoryId变化时才重新获取数据
const { posts } = useSelect(
  ( select ) => {
    return {
      posts: select( coreStore ).getEntityRecords( 'postType', 'post', {
        categories: categoryId,
        per_page: 10,
      } ),
    };
  },
  [ categoryId ] // 仅在categoryId变化时重新执行
);

数据分页与懒加载

实现数据分页加载,提升大型数据集的性能:

const [ page, setPage ] = useState( 1 );
const { posts, totalPages } = useSelect(
  ( select ) => {
    const query = {
      per_page: 10,
      page,
      order: 'desc',
      orderby: 'date',
    };
    const posts = select( coreStore ).getEntityRecords( 'postType', 'post', query );
    const totalPages = select( coreStore ).getEntityRecords( 
      'postType', 
      'post', 
      { ...query, per_page: 1 } 
    )?._paging?.totalPages;
    
    return { posts, totalPages };
  },
  [ page ]
);

// 分页控件
<div className="pagination">
  <Button 
    label={ __( '上一页' ) }
    onClick={ () => setPage( page - 1 ) }
    disabled={ page <= 1 }
  />
  <span>{ __( `第 ${ page } 页,共 ${ totalPages } 页` ) }</span>
  <Button 
    label={ __( '下一页' ) }
    onClick={ () => setPage( page + 1 ) }
    disabled={ page >= totalPages }
  />
</div>

常见问题与解决方案

数据获取失败处理

实现健壮的错误处理机制:

const { data, isLoading, error } = useSelect(
  ( select ) => {
    const { getEntityRecords, getResolutionError } = select( coreStore );
    const query = { per_page: 5 };
    const data = getEntityRecords( 'postType', 'post', query );
    const isLoading = select( coreStore ).isResolving( 
      'getEntityRecords', [ 'postType', 'post', query ] 
    );
    const error = getResolutionError( 'getEntityRecords', [ 'postType', 'post', query ] );
    
    return { data, isLoading, error };
  },
  []
);

if ( error ) {
  return (
    <div className="error-message">
      <p>{ __( '数据加载失败:' ) }{ error.message }</p>
      <Button onClick={ refreshData }>
        { __( '重试' ) }
      </Button>
    </div>
  );
}

循环依赖问题

当多个组件相互依赖时,可能导致循环更新。解决方案是使用useEffect分离数据订阅和状态更新:

const [ posts, setPosts ] = useState( [] );
const { rawPosts } = useSelect(
  ( select ) => ( {
    rawPosts: select( coreStore ).getEntityRecords( 'postType', 'post' ),
  } ),
  []
);

useEffect( () => {
  if ( rawPosts ) {
    // 处理数据转换,避免在useSelect中直接修改
    const processedPosts = rawPosts.map( post => ( {
      id: post.id,
      title: post.title.rendered,
      excerpt: post.excerpt.rendered,
    } ) );
    setPosts( processedPosts );
  }
}, [ rawPosts ] );

编辑器与前端一致性

确保区块在编辑器和前端显示一致:

// edit.js - 编辑器组件
export default function DynamicBlockEdit() {
  const { data } = useSelect( /* ... */ );
  return <DynamicBlockContent data={ data } isEditor={ true } />;
}

// save.js - 保存组件(返回null,因为数据是动态获取的)
export default function DynamicBlockSave() {
  return null;
}

// frontend.js - 前端渲染
function DynamicBlockFrontend( props ) {
  const { data } = useSelect( /* ... */ );
  return <DynamicBlockContent data={ data } isEditor={ false } />;
}

// 共享渲染组件
function DynamicBlockContent( { data, isEditor } ) {
  // 渲染逻辑
}

实际应用案例

实时评论区块

创建一个显示最新评论并自动更新的区块:

import { useSelect, useDispatch } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import { store as noticesStore } from '@wordpress/notices';
import { useDebounce } from '@wordpress/compose';

export default function LiveCommentsBlock( { attributes, setAttributes } ) {
  const { postId } = attributes;
  const [ refreshInterval, setRefreshInterval ] = useState( 30 );
  const debouncedSetRefreshInterval = useDebounce( setRefreshInterval, 1000 );
  
  const { comments, isLoading } = useSelect(
    ( select ) => {
      const query = {
        post: postId,
        per_page: 10,
        order: 'desc',
      };
      return {
        comments: select( coreStore ).getEntityRecords( 'root', 'comment', query ),
        isLoading: select( coreStore ).isResolving( 
          'getEntityRecords', [ 'root', 'comment', query ] 
        ),
      };
    },
    [ postId ]
  );
  
  const { invalidateResolution } = useDispatch( coreStore );
  const { createNotice } = useDispatch( noticesStore );
  
  // 设置自动刷新
  useEffect( () => {
    const intervalId = setInterval( () => {
      const query = {
        post: postId,
        per_page: 10,
        order: 'desc',
      };
      invalidateResolution( 'getEntityRecords', [ 'root', 'comment', query ] );
      createNotice( 'success', __( '评论已更新' ), { type: 'snackbar' } );
    }, refreshInterval * 1000 );
    
    return () => clearInterval( intervalId );
  }, [ postId, refreshInterval ] );
  
  return (
    <div { ...useBlockProps() }>
      <div className="comment-controls">
        <span>{ __( '自动刷新间隔:' ) }</span>
        <input
          type="number"
          min="5"
          max="300"
          value={ refreshInterval }
          onChange={ ( e ) => debouncedSetRefreshInterval( parseInt( e.target.value ) ) }
        />
        <span>{ __( '秒' ) }</span>
      </div>
      
      { isLoading ? (
        <Spinner />
      ) : (
        <ul className="comments-list">
          { comments?.map( comment => (
            <li key={ comment.id } className="comment-item">
              <div className="comment-author">
                <img 
                  src={ comment.author_avatar_urls[ 48 ] } 
                  alt={ comment.author_name } 
                />
                <span className="author-name">{ comment.author_name }</span>
              </div>
              <div className="comment-content" 
                dangerouslySetInnerHTML={ { __html: comment.content.rendered } } 
              />
              <div className="comment-meta">
                <time dateTime={ comment.date }>
                  { dateI18n( getSettings().formats.date, comment.date ) }
                </time>
              </div>
            </li>
          ) ) }
        </ul>
      ) }
    </div>
  );
}

数据库查询区块

创建一个执行自定义数据库查询并显示结果的区块:

import { useSelect } from '@wordpress/data';
import { TextControl, Button, TextareaControl } from '@wordpress/components';

export default function CustomQueryBlock( { attributes, setAttributes } ) {
  const { query, results } = attributes;
  const { data, isLoading, error } = useSelect(
    ( select ) => {
      if ( ! query ) return { data: null };
      
      return {
        data: select( 'my-custom-store' ).getQueryResults( query ),
        isLoading: select( 'my-custom-store' ).isQueryLoading( query ),
        error: select( 'my-custom-store' ).getQueryError( query ),
      };
    },
    [ query ]
  );
  
  const runQuery = () => {
    // 这里应该有验证逻辑,防止SQL注入等安全问题
    dispatch( 'my-custom-store' ).executeQuery( query );
  };
  
  return (
    <div { ...useBlockProps() }>
      <TextareaControl
        label={ __( '自定义查询' ) }
        value={ query }
        onChange={ ( value ) => setAttributes( { query: value } ) }
        placeholder="SELECT * FROM wp_posts WHERE post_type = 'post' LIMIT 10"
        rows={ 4 }
      />
      <Button 
        variant="primary" 
        onClick={ runQuery } 
        disabled={ isLoading }
      >
        { isLoading ? __( '执行中...' ) : __( '执行查询' ) }
      </Button>
      
      { error && (
        <div className="query-error">{ error }</div>
      ) }
      
      { data && (
        <div className="query-results">
          <table>
            <thead>
              <tr>
                { data.columns.map( column => (
                  <th key={ column }>{ column }</th>
                ) ) }
              </tr>
            </thead>
            <tbody>
              { data.rows.map( ( row, index ) => (
                <tr key={ index }>
                  { data.columns.map( column => (
                    <td key={ column }>{ row[ column ] }</td>
                  ) ) }
                </tr>
              ) ) }
            </tbody>
          </table>
        </div>
      ) }
    </div>
  );
}

总结与展望

Gutenberg的动态数据绑定功能为WordPress开发者打开了全新的可能性。通过useSelect和useDispatch hooks,我们能够创建响应式的区块,实时反映数据源的变化,极大提升了用户体验。

本文介绍的技术包括:

  • Gutenberg的数据架构和单向数据流模型
  • 使用useSelect和useDispatch进行数据绑定
  • 创建自定义动态区块的完整流程
  • 性能优化和常见问题解决方案
  • 实际应用案例和代码示例

随着Gutenberg的不断发展,我们可以期待更多强大的数据功能,如:

  • Interactivity API的成熟,提供更简洁的声明式数据绑定
  • 实时协作编辑功能的增强
  • 更强大的离线数据同步能力

掌握动态数据绑定技术,将使你的WordPress网站更加灵活、响应迅速,并为用户提供更加丰富的交互体验。开始尝试这些技术,创建你自己的动态区块吧!

扩展学习资源

  • Gutenberg官方文档:https://developer.wordpress.org/block-editor/
  • WordPress数据模块源代码:https://github.com/WordPress/gutenberg/tree/trunk/packages/data
  • 高级React模式在Gutenberg中的应用:https://github.com/WordPress/gutenberg/tree/trunk/packages/components
  • Gutenberg性能优化指南:https://github.com/WordPress/gutenberg/blob/trunk/docs/performance.md

希望本文能帮助你更好地理解和应用Gutenberg的动态数据绑定功能。如果你有任何问题或建议,请在评论区留言。别忘了点赞、收藏并关注我的后续文章,获取更多WordPress开发技巧!

【免费下载链接】gutenberg The Block Editor project for WordPress and beyond. Plugin is available from the official repository. 【免费下载链接】gutenberg 项目地址: https://gitcode.com/GitHub_Trending/gu/gutenberg

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值