首页 › 博客 › 文章

自定义 Gutenberg 区块:从注册到上架全流程

自定义 Gutenberg 区块:从注册到上架全流程

结论先行:做 Gutenberg 自定义区块,先用 block.json 注册并冻结属性契约,再分离编辑器与前端渲染,最后用样板、变体、自动化回归和回滚包完成上架。判断做对了的标准是:编辑器可保存、前端可输出、旧内容不报验证错误、禁用插件后页面不崩、主要 WordPress/PHP 组合可重复构建。

准备:确认 2026 年的运行环境与目录边界

2026 年生产环境建议以 WordPress 6.9/7.0 分支、PHP 8.2 或 8.3、Node.js 20 LTS 或 22 LTS 为基线;Gutenberg 插件仍可安装,但核心编辑器已内置 Block API、Block Hooks、Interactivity API 与 block.json 元数据加载能力。不要再把区块仅写成短代码,也不要把前端样式全部塞进编辑器脚本。若站点还涉及企业站点的信息架构和权限流转,可先参考 WordPress 企业网站开发,把内容模型、角色权限和上线窗口定下来,再进入代码。

目录建议固定为插件,而不是主题:主题切换会让历史文章里的自定义区块失去注册源,编辑器会显示“此区块包含意外或无效内容”。插件路径示例:wp-content/plugins/runok-notice/。内部至少包含 block.json、src/edit.js、src/save.js、src/editor.scss、src/style.scss、src/view.js、build/、runok-notice.php。构建产物必须提交到发布包,服务器不执行 npm。

mkdir -p wp-content/plugins/runok-notice/src
npx @wordpress/create-block@latest runok-notice --template @wordpress/create-block/template
cd wp-content/plugins/runok-notice
npm install
npm run build
wp plugin activate runok-notice --path=/var/www/html

为什么用 @wordpress/create-block:它生成的 package.json、block.json 与 scripts 对齐 2026 年 @wordpress/scripts 默认配置,能直接产出 block-editor 脚本依赖、editorStyle、style、viewScript 和 render.php 所需的 asset 文件。失败时先执行 wp plugin deactivate runok-notice、删除 build/ 后重新 npm ci && npm run build,不要手工改 asset.php 的 dependencies。

实施步骤流程:从注册到可回滚交付

  1. 建立插件骨架,确认 WordPress、PHP、Node 版本,提交初始 tag。
  2. 写 block.json,定义 name、attributes、supports、scripts 与 render 入口。
  3. 实现 edit 与 save 或服务端 render,分离编辑器与前端资源。
  4. 补块样板、变体、弃用迁移与内容校验。
  5. 跑构建、单元/E2E、旧内容回归、多版本矩阵与发布包校验。
  6. 生成回滚包,灰度启用,观察日志后全量上架。

这 6 步对应结构化交付:每一步都有产物和验证命令。任何一步没有产物,不进入下一步。上线前用 git tag v1.0.0 固化源码,用 npm run plugin-zip 生成可上传 zip,并把上一版插件 zip、数据库导出、block 属性快照放进同一发布记录。

register_block_type 与属性定义

2026 年推荐用 block.json 元数据注册,PHP 侧只调用 register_block_type( __DIR__ . '/build' ) 或 register_block_type( __DIR__ ),让核心读取 build/block.json。不要再用纯 PHP 数组重复声明 attributes,否则 JS 与 PHP 的默认值漂移后,REST 保存和前端渲染会出现两套解释。属性就是持久化契约:字段名、type、source、selector、default 一旦发布,修改必须配 deprecated 迁移。

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "runok/notice",
  "title": "Runok Notice",
  "category": "design",
  "icon": "megaphone",
  "description": "Notice box with tone, heading and body.",
  "attributes": {
    "tone": { "type": "string", "default": "info" },
    "heading": { "type": "string", "source": "html", "selector": "h3", "default": "" },
    "body": { "type": "string", "source": "html", "selector": "p", "default": "" },
    "dismissible": { "type": "boolean", "default": false }
  },
  "supports": { "align": ["wide", "full"], "color": { "background": true, "text": true }, "spacing": { "margin": true, "padding": true }, "html": false },
  "editorScript": "file:./index.js",
  "editorStyle": "file:./index.css",
  "style": "file:./style-index.css",
  "viewScript": "file:./view.js",
  "render": "file:./render.php"
}

PHP 入口文件 runok-notice.php 保持最小:

<?php
/**
 * Plugin Name: Runok Notice
 * Version: 1.0.0
 * Requires at least: 6.7
 * Requires PHP: 8.2
 */

add_action( 'init', function () {
    register_block_type( __DIR__ . '/build' );
} );

怎么做验证:执行 wp block list --fields=name,title 2>/dev/null | grep runok/notice;在编辑器插入区块、改 tone、保存、刷新,确认不出现块验证提示;用 wp post get 123 --field=content 检查序列化标记里只有 <!-- wp:runok/notice {...} /--> 和预期属性。再查 REST:curl -s -H 'Accept: application/json' https://example.com/wp-json/wp/v2/block-types/runok%2Fnotice | jq .attributes。

常见踩坑:source: html 的 selector 与 save.js 输出标签不一致;default 写成对象却声明 type: string;把可计算状态(例如“是否展开”)存进属性;supports.html 打开后用户手改 markup 导致迁移困难。失败回退:恢复上一版 block.json,清 wp cache flush 与 OPcache,重新打开旧文章并另存;若已产生无效块,用 deprecated 数组补旧 save 而不是直接改 selector。

编辑器与前端两套渲染

为什么必须拆两套:编辑器需要 InspectorControls、RichText、BlockControls 和即时交互;前端只需要可访问、可缓存、尽量少 JS 的静态 HTML。把 edit.js 的逻辑复制进前端,会带来 React 依赖、重复 hydration、CLS 和缓存穿透。2026 年的稳妥组合是:edit.js 负责编辑态;服务端 render.php 负责最终 HTML;view.js 只绑定 dismiss 这类轻量交互;style-index.css 前后端共用,index.css 只放编辑器专属样式。

步骤 A:编辑器 edit.js

获取报价

邮箱 + 需求 + 预算区间,24 小时内回复报价。

import { __ } from '@wordpress/i18n';
import { useBlockProps, InspectorControls, RichText } from '@wordpress/block-editor';
import { PanelBody, SelectControl, ToggleControl } from '@wordpress/components';

export default function Edit( { attributes, setAttributes } ) {
  const blockProps = useBlockProps( { className: `is-tone-${ attributes.tone }` } );
  return (
    <>
      <InspectorControls>
        <PanelBody title={ __( 'Notice settings', 'runok-notice' ) }>
          <SelectControl
            label={ __( 'Tone', 'runok-notice' ) }
            value={ attributes.tone }
            options={ [
              { label: 'Info', value: 'info' },
              { label: 'Warning', value: 'warning' },
            ] }
            onChange={ ( tone ) => setAttributes( { tone } ) }
          />
          <ToggleControl
            label={ __( 'Dismissible', 'runok-notice' ) }
            checked={ attributes.dismissible }
            onChange={ ( dismissible ) => setAttributes( { dismissible } ) }
          />
        </PanelBody>
      </InspectorControls>
      <div { ...blockProps } role="status">
        <RichText tagName="h3" value={ attributes.heading } onChange={ ( heading ) => setAttributes( { heading } ) } />
        <RichText tagName="p" value={ attributes.body } onChange={ ( body ) => setAttributes( { body } ) } />
      </div>
    </>
  );
}

注意上面 JSX 里的比较符与标签已转义为可复制文本;实际 src/edit.js 使用原生 <、>、=>。useBlockProps 必须透传,否则 align、anchor、className、spacing 支持会丢。

步骤 B:服务端 render.php 与前端 view.js

<?php
$tone = in_array( $attributes['tone'] ?? '', [ 'info', 'warning' ], true ) ? $attributes['tone'] : 'info';
$heading = wp_kses_post( $attributes['heading'] ?? '' );
$body    = wp_kses_post( $attributes['body'] ?? '' );
$dismiss = ! empty( $attributes['dismissible'] );
$wrapper = get_block_wrapper_attributes( [ 'class' => "is-tone-{$tone}", 'role' => 'status' ] );
?>
<div <?php echo $wrapper; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
  <h3><?php echo $heading; ?></h3>
  <p><?php echo $body; ?></p>
  <?php if ( $dismiss ) : ?>
    <button type="button" data-runok-dismiss><?php esc_html_e( 'Close', 'runok-notice' ); ?></button>
  <?php endif; ?>
</div>
document.querySelectorAll( '[data-runok-dismiss]' ).forEach( ( btn ) => {
  btn.addEventListener( 'click', () => btn.closest( '.wp-block-runok-notice' )?.remove() );
} );

使用 render.php 时不要在 save.js 再输出同样 HTML;save 返回 null,核心会按动态区块处理。若必须静态保存,则 save.js 的标签层级、属性顺序、文本节点必须与 block.json selector 完全一致,且后续任何 markup 改动都要写 deprecated。

怎么验证做对了:前台 curl -s https://example.com/sample/ | grep 'wp-block-runok-notice' 应看到服务端 HTML;禁用 JS 后内容仍可见;编辑器 Network 面板不加载 view.js;前端不加载 @wordpress/components;Lighthouse 无“区块内容闪烁”;npm run lint:js && npm run lint:css && vendor/bin/phpcs runok-notice.php build/render.php 全绿。若前台空白,先查 wp-content/debug.log 中 render.php 的 undefined key,再临时把 render 换回静态 save 作为回退。

块样板与变体

块样板解决“编辑不知道怎么配”,变体解决“同一区块的预设入口”。两者都不改属性契约,只提供初始值,因此比复制多个区块名安全。2026 年核心仍支持 register_block_pattern、register_block_pattern_category 与 JS 侧 registerBlockVariation;Patterns 也可放在插件 patterns/ 目录由核心自动发现,但团队交付建议显式注册,便于按版本下线。

注册 PHP 样板

add_action( 'init', function () {
  register_block_pattern_category( 'runok-components', [ 'label' => __( 'Runok Components', 'runok-notice' ) ] );
  register_block_pattern( 'runok/notice-warning', [
    'title'       => __( 'Warning notice', 'runok-notice' ),
    'categories'  => [ 'runok-components' ],
    'blockTypes'  => [ 'runok/notice' ],
    'content'     => '<!-- wp:runok/notice {"tone":"warning","heading":"维护窗口","body":"2026-03-18 02:00 起短暂停服","dismissible":false} /-->',
    'description' => __( 'Preconfigured maintenance notice', 'runok-notice' ),
  ] );
}, 20 );

注册编辑器变体

import { registerBlockVariation } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';

registerBlockVariation( 'runok/notice', {
  name: 'notice-success',
  title: __( 'Success notice', 'runok-notice' ),
  icon: 'yes-alt',
  attributes: { tone: 'info', heading: __( 'Done', 'runok-notice' ), dismissible: true },
  isActive: ( blockAttributes ) => blockAttributes.dismissible === true,
  scope: [ 'inserter', 'transform' ],
} );

为什么加 isActive:否则插入器高亮状态会错判,编辑在切换面板时看到两个变体同时激活。scope 不要写 block,除非你明确要接管所有变换入口。

验证命令与动作:wp eval 'var_export( WP_Block_Patterns_Registry::get_instance()->is_registered( "runok/notice-warning" ) );' 应输出 true;编辑器插入器搜索“Warning notice”,插入后属性面板显示 tone=warning;用 wp post get 123 --field=content 确认样板没有写入额外 wrapper 区块。若样板在 2026 年主题切换后消失,检查是否被主题的 remove_theme_support( 'core-block-patterns' ) 连带影响;回退方法是把样板改回插件 PHP 注册并提升 priority 到 20,同时执行 wp rewrite flush --hard 清编辑器缓存。

对内容团队还要给一份“哪个场景用哪个入口”的验收表。若站点正处于选型阶段,可把 2026年 wordpress教程:选型到验收决策指南 作为非技术同事的前置阅读,避免把样板当成布局系统、把变体当成权限控制。

发布与兼容性回归

上架不是上传 zip 就结束。2026 年要同时回归 WordPress 最低支持版、当前稳定版、Gutenberg 插件最新版、PHP 8.2/8.3、经典主题与块主题、多站点、对象缓存开启/关闭。核心 Block API 文档见 Block Registration 官方文档;发布包校验规则可参考 Plugin Developer FAQ。若交付给客户运维,2026年 WordPress定制开发怎么选:交付、报价与验收 里的验收口径可直接并入工单模板。

构建与包体检查

npm ci
npm run lint:js
npm run lint:css
npm run test:unit
npm run build
npm run plugin-zip
unzip -l runok-notice.zip | grep -E 'block.json|render.php|index.asset.php|style-index.css'
wp plugin activate runok-notice --path=/var/www/html
wp block list --fields=name | grep runok/notice

包里必须有 build/index.asset.php、build/style-index.css、build/render.php;缺 asset.php 会导致编辑器白屏并报 wp.blocks is undefined。不要把 node_modules、src 映射、.git 打进生产 zip。

旧内容回归矩阵

用例操作通过标准
v0.9 静态块导入旧文章,打开编辑器并刷新无“无效内容”,deprecated 自动迁移,保存后属性为新 schema
动态 render关闭 JS 访问前台标题与正文仍输出,role=status 存在
样板插入插入器搜索并保存序列化注释仅含 4 个约定属性
多站点wp site list --field=url 逐站激活无重复 register 警告,网络激活不覆盖子站样式
缓存开启 Redis Object Cache 后清缓存render.php 输出不串站、不串用户
回滚停用插件并上传上一版 zip前台降级为可读文本或旧块,不出现 500

E2E 建议用 npx wp-env start 起干净环境,再跑 npm run test:e2e;至少覆盖“插入 → 改 tone → 发布 → 前台断言 → 回编辑 → 不提示修复”。CI 里固定 WP_VERSION=6.9 与 WP_VERSION=latest 两个 job,PHP 矩阵跑 8.2、8.3。2026 年常见失败是 Interactivity API 的 viewScript 与主题自带 hydration 重复绑定,现象是关闭按钮点两次;回退是给 view.js 加 data-runok-bound 哨兵,或临时从 block.json 移除 viewScript 只保留静态 render。

灰度、观测与回滚

发布顺序: staging 全量回归 → 生产低流量时段上传 → 仅对一个编辑角色启用 → 观察 30 分钟 wp-content/debug.log、PHP-FPM slowlog、前端 Sentry/浏览器控制台 → 全量。回滚包必须包含上一版 zip、mysqldump --single-transaction wp_posts > posts-before.sql、当前 block.json 副本、停用命令。执行回滚:wp plugin deactivate runok-notice && wp plugin delete runok-notice && wp plugin install runok-notice-0.9.2.zip --activate;若旧内容已被新属性写坏,用 posts-before.sql 按 ID 单篇恢复,不要整库覆盖。

判断发布成功的硬指标:编辑器块验证错误数为 0;前台 HTML 与 render.php 模板一致;旧文章 diff 只增加迁移后的属性;回滚演练在 staging 10 分钟内完成;wp plugin list --update=available 不出现自家插件的意外升级提示。

验证:上线前后逐项签字

验证:上线前后逐项签字

站点负责人不需要读 JS,但要签这 6 项:插件版本号与发布记录一致;插入器能看到区块、样板和变体;手机与桌面前台样式一致;关闭 JS 后正文不丢;旧文章打开无修复弹窗;停用插件后页面不 500。开发者额外签:block.json schema 通过、asset 依赖完整、phpcs 与 npm lint 通过、E2E 录屏归档、回滚包可解压可激活。

验收不是“我本地能拖进去”,而是“换一台干净机器、换一个编辑账号、换一个旧文章,结果仍然一致”。

回滚:失败场景与对应动作

  • 编辑器白屏:查 index.asset.php 是否随包上传;回退上一版 build/,执行 wp cache flush 与浏览器硬刷新。
  • 块验证报错:不要点“尝试修复”后批量保存;先补 deprecated 旧 save,再用 staging 导入生产 posts 做迁移演练。
  • 前台样式串到全站:确认 style-index.css 全部以 .wp-block-runok-notice 开头;临时把冲突规则移入 editorStyle 并重新发版。
  • render.php 500:打开 WP_DEBUG_LOG,对 $attributes 全部用 ?? 兜底;紧急时切回静态 save 分支 tag。
  • 样板/变体不出现:确认 init priority、JS 是否被打进 index.js、 inserter 缓存;回退到 PHP 样板单入口。

更多排障记录可归入团队技术栏目 技术文章归档,但每次发布仍以本站的 debug.log 和 posts 快照为准,不复用别的站点结论。

常见问题

动态区块还需要写 save.js 吗?

需要文件但可返回 null。block.json 指定 render 后,核心按动态块处理;save 只用于兼容旧序列化内容,不要重复输出 HTML。

改了 attributes 一定要写 deprecated 吗?

只要旧文章里已存在该字段、且 type/source/selector 发生变化,就必须写。仅新增带 default 的字段通常可平滑读取,但仍要跑旧内容回归。

2026 年还必须装 Gutenberg 插件吗?

不必须。核心已足够;只有要用尚未进核心的实验 API 时才装,并在 CI 增加“装插件/不装插件”两组 job。

上线动作清单

  1. 用 npx @wordpress/create-block@latest 重建干净骨架,锁定 Node 20/22 与 PHP 8.2+,打 v1.0.0 tag。
  2. 把 block.json 属性表贴进评审单,逐字段确认 default、source、selector 与 render.php 兜底一致。
  3. 跑 npm run build && npm run plugin-zip,用 unzip 核对 4 个必需产物后上传 staging。
  4. 执行旧文章、无 JS、多站点、对象缓存、双 WordPress 版本矩阵,归档 E2E 结果和 debug.log。
  5. 准备上一版 zip 与 posts-before.sql,在 staging 完成一次 10 分钟内回滚演练,再开生产灰度。

需要把区块纳入企业交付与长期维护?查看 服务页,或直达 WordPress 开发服务 获取评估。

参考资料与延伸阅读

WPDaiwei

WPDaiwei-专业Wordpress网站定制开发提供商

需要 WordPress 开发服务?

24 小时内免费出方案与报价,国内国外都接单。

QQ