本文档是一份持续更新的文档,用于规定 Gutenberg 项目专用的编码指南。基础编码指南遵循 WordPress 编码标准。以下各节概述了 Gutenberg 项目中使用的额外模式和约定。

CSS

命名

为避免类名冲突,类名必须遵循以下指南,这些指南的灵感大致来源于 BEM(Block, Element, Modifier)方法论。

分配给元素的所有类名都必须以包名作为前缀,后跟一个连字符以及组件所在的目录名称。组件根元素的任何后代元素都必须附加一个以连字符分隔的描述符,该描述符与基础名称之间以两个连续的下划线 __ 分隔。

  • 根元素:package-directory
  • 子元素:package-directory__descriptor-foo-bar

根元素被视为 index.js 中默认导出所返回的最高层级祖先元素。值得注意的是,如果你的文件夹中包含多个文件,且每个文件都有各自默认导出的组件,则只有由 index.js 渲染的元素才能被视为根元素。所有其他元素都应被视为后代元素。

示例:

考虑以下位于 packages/components/src/notice/index.js 的组件:

export default function Notice( { children, onRemove, actions = [] } ) {
    return (
        <div className="components-notice">
            <div className="components-notice__content">{ children }</div>
            { actions.length > 0 && (
                <div className="components-notice__actions">
                    { /* 操作按钮 */ }
                </div>
            ) }
            <Button
                className="components-notice__dismiss"
                icon={ closeSmall }
                label={ __( 'Dismiss this notice' ) }
                onClick={ onRemove }
            />
        </div>
    );
}

通过 props 暴露的可选区域应在根元素的直接后代上使用额外的 __descriptor 类,并遵循与上例中其他子元素相同的模式。

组件可以带有指示状态的类名(例如“active”标签页或“opened”面板)。这些修饰符应作为单独的类名应用,以形容词表达式的形式加上 is- 前缀(is-active 或 is-opened)。在极少数情况下,你可能会遇到修饰符前缀的变体,通常是为了提高可读性(has-warning)。由于修饰符类名并未与特定组件绑定,因此在样式表中编写时,它应始终与被修饰的组件一同出现(.components-panel.is-opened)。

示例:

考虑一个可展开的面板。clsx 包是一个实用的工具,可用于有条件地应用修饰符类名。

import clsx from 'clsx';

export default function Panel( { children, isExpanded } ) {
    const classes = clsx( 'components-panel', {
        'is-expanded': isExpanded,
    } );

    return <div className={ classes }>{ /* ... */ }</div>;
}

组件的类名绝不应在其自身文件夹之外使用(极少数例外情况,例如 _z-index.scss)。如果你需要在自己的组件中继承另一个组件的样式,你应该渲染该组件的一个实例。在最坏的情况下,你也应该在自己的组件样式表中复制这些样式。这样做的目的是通过将共享组件隔离为可复用的接口来提高可维护性,并通过调整一组有限的通用组件以支持多样化的用例,从而减少类似 UI 元素的表面积。

区块的 SCSS 文件命名约定

当 Webpack 运行时,构建过程会将区块库目录中的 SCSS 拆分为两个独立的 CSS 文件。

放置在 style.scss 文件中的样式将被构建到 blocks/build/style.css 中,以便在前端主题以及编辑器中加载。如果你需要针对区块在编辑器中的显示效果的额外样式,请将它们添加到 editor.scss 中。

同时出现在主题和编辑器中的样式示例包括图库列数和首字下沉。

JavaScript

Gutenberg 中的所有新代码都应使用 TypeScript 编写,使用 .ts 文件扩展名;对于包含 JSX 语法的文件,则使用 .tsx 文件扩展名。

在以下一些例外情况下,允许编写纯 JavaScript(.js):

  • 如果代码需要在浏览器中直接运行,或由 Node.js 在不支持 TypeScript 语法的环境中运行。请注意,自 Node.js v22.18.0 及更新版本起,包含可擦除语法的 TypeScript 文件可以直接由 Node.js 运行时执行。
  • 如果在现有的 .js 或 .jsx 文件中编写新代码。如果迁移操作很简单,则鼓励将现有文件迁移为对应的 .ts 或 .tsx 文件。
  • 如果在一个基本没有类型定义的包中编写新文件,以至于新文件无法合理地访问现有的包类型定义。

Gutenberg 中的 JavaScript 使用 ECMAScript 语言规范的现代语言特性以及 JSX 语言语法扩展。这些特性通过一系列预设配置的组合来启用,其中尤为重要的是 @wordpress/babel-preset-default,它在项目的 Babel 配置中被用作一个预设。

虽然引入新 JavaScript 语言特性的分阶段流程提供了在特性被认定为完成之前使用新特性的机会,但Gutenberg 项目和 @wordpress/babel-preset-default 配置将仅以支持已达到第 4 阶段(“Finished”)的提案为目标。

导入

在 Gutenberg 项目中,我们使用 ES2015 导入语法,使我们能够创建模块化代码,在特定功能的代码、跨不同 WordPress 功能共享的代码以及第三方依赖之间实现清晰的分离。

导入可以引用第三方包、WordPress 包或本地文件。

将导入语句写成一个连续的块。不要用注释或空行分隔不同类型的依赖。

外部依赖

外部依赖是指不由 WordPress 贡献者维护的第三方代码,而是作为默认脚本包含在 WordPress 中,或从外部包管理器(如 npm)引用。

示例:

import moment from 'moment';

WordPress 依赖

为了鼓励功能之间的可重用性,我们的 JavaScript 被拆分为特定领域的模块,这些模块会 export 一个或多个函数或对象。在 Gutenberg 项目中,我们将这些模块放在顶级目录下加以区分。每个模块都服务于一个独立的目的,并且它们之间经常共享代码。例如,为了对其文本进行本地化,编辑器代码需要包含来自 i18n 模块的函数。

示例:

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

内部依赖

在一个特定的功能中,代码被组织到不同的文件和文件夹中。与外部依赖和 WordPress 依赖的情况一样,你可以使用 import 关键字将这些代码引入作用域。这里的主要区别在于,在导入内部文件时,你应该使用特定于你所工作的顶级目录的相对路径。

示例:

import VisualEditor from '../visual-editor';

遗留实验性 API、插件专用 API 和私有 API

遗留实验性 API

历史上,Gutenberg 一直使用 __experimental 和 __unstable 前缀来指示某个 API 尚不稳定,可能会发生变化。这是一种遗留约定,应当避免使用,而应采用下文所述的插件专用 API 模式或私有 API 模式。

使用这些前缀的问题在于,这些 API 很少会被稳定化或移除。截至 2022 年 6 月,WordPress 核心已包含 280 个公开导出的实验性 API,它们是在历次 WordPress 主要版本发布期间从 Gutenberg 插件合并而来的。许多插件和主题开始依赖这些实验性 API 来实现无法通过其他任何方式访问的重要功能。

遗留的 __experimental API 已不能再随意移除了。它们已成为 WordPress 公共 API 的一部分,并受WordPress 向后兼容性政策的约束。移除它们需要经过弃用流程。对于某些 API 来说,这可能相对容易;而对于另一些 API,则可能需要付出努力,并跨越多个 WordPress 版本。

总而言之,不要为新的 API 使用 __experimental 前缀。请改用插件专用 API 和私有 API。

插件专用 API

插件专用 API 是从模块导出的临时值,它们的存在要么有待未来的修订,要么只是提供了一种达成目的的即时手段。

致外部使用者:

插件专用 API 不附带任何支持承诺。它们可能会、也将会在没有事先警告的情况下被移除或更改,包括在次要版本或补丁版本中。作为外部使用者,你应该避免使用这些 API。

致项目贡献者:

插件专用 API 是指计划最终公开发布、但仍需进一步实验、测试和讨论的 API。应当尽早将其稳定化或移除。

插件专用 API 不会包含在 WordPress 核心中,仅在 Gutenberg 插件中可用:

// 使用 globalThis.IS_GUTENBERG_PLUGIN 可以让 Webpack 将此
// 导出从 WordPress 核心中排除:
if ( globalThis.IS_GUTENBERG_PLUGIN ) {
    export { doSomethingExciting } from './api';
}

这类 API 的公共接口尚未最终定型。除了代码中的引用之外,这些 API 既不应写入文档,也不应在任何 CHANGELOG(变更日志)中被提及。从外部角度来看,它们实际上应当被视为不存在。在大多数情况下,公开它们只是为了满足本仓库中所维护的各个包之间的需求。

虽然插件专用 API 往往可能会稳定为公开可用的 API,但这并无保证。

私有 API

每个希望以私有方式访问或公开私有 API 的 @wordpress 包,都可以
通过选择加入 @wordpress/private-apis 来实现:

// 在 packages/block-editor/private-apis.js 中:
import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis';
export const { lock, unlock } =
    __dangerousOptInToUnstableAPIsOnlyForCoreModules(
        'I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.',
        '@wordpress/block-editor' // 调用 __dangerousOptInToUnstableAPIsOnlyForCoreModules 的包的名称,
        // (而不是你想要访问其 API 的那个包的名称)
    );

每个 @wordpress 包只能选择加入一次。该过程清楚地表明扩展开发者不应
使用它。本文档将重点介绍用法示例,但你可以在其 README.md 中了解有关 @wordpress/private-apis 包的更多信息。

一旦包选择加入,你就可以使用 lock() 和 unlock() 实用工具:

// 假设这个对象是从某个包导出的:
export const publicObject = {};

// 然而,这个字符串是内部的,不应公开提供:
const privateString = 'private information';

// 解决方案:将字符串“锁定”在对象“内部”:
lock( publicObject, privateString );

// 该字符串没有嵌套在对象中,也无法从中提取:
console.log( publicObject );
// {}

// 访问该字符串的唯一方法是“解锁”这个对象:
console.log( unlock( publicObject ) );
// "private information"

// lock() 接受所有数据类型,而不仅仅是字符串:
export const anotherObject = {};
lock( anotherObject, function privateFn() {} );
console.log( unlock( anotherObject ) );
// function privateFn() {}

请继续阅读,了解如何使用 lock() 和 unlock() 来避免公开导出
各种不同类型的 private API。

私有选择器和动作

你可以将私有选择器和动作附加到公共 store 上:

// 在 packages/package1/store.js 中:
import { privateHasContentRoleAttribute } from './private-selectors';
import { privateToggleFeature } from './private-actions';
// `lock` 函数导出自调用 opt-in 函数的
// 内部 private-apis.js 文件。
import { lock, unlock } from './lock-unlock';

export const store = registerStore( /* ... */ );
// 将一个私有动作附加到导出的 store 上:
unlock( store ).registerPrivateActions( {
    privateToggleFeature,
} );

// 将一个私有动作附加到导出的 store 上:
unlock( store ).registerPrivateSelectors( {
    privateHasContentRoleAttribute,
} );
// 在 packages/package2/MyComponent.js 中:
import { store } from '@wordpress/package1';
import { useSelect } from '@wordpress/data';
// `unlock` 函数导出自调用 opt-in 函数的
// 内部 private-apis.js 文件。
import { unlock } from './lock-unlock';

function MyComponent() {
    const hasRole = useSelect(
        ( select ) =>
            // 使用私有选择器:
            unlock( select( store ) ).privateHasContentRoleAttribute()
        // 注意:这里必须使用 unlock()。下面这行代码是行不通的:
        // select( store ).privateHasContentRoleAttribute()
    );

    // 使用私有动作:
    unlock( useDispatch( store ) ).privateToggleFeature();

    // ...
}
私有函数、类和变量
// 在 packages/package1/index.js 中:
import { lock } from './lock-unlock';

export const privateApis = {};
/* 将私有数据附加到导出的对象上 */
lock( privateApis, {
    privateCallback: function () {},
    privateReactComponent: function PrivateComponent() {
        return <div />;
    },
    privateClass: class PrivateClass {},
    privateVariable: 5,
} );
// 在 packages/package2/index.js 中:
import { privateApis } from '@wordpress/package1';
import { unlock } from './lock-unlock';

const {
    privateCallback,
    privateReactComponent,
    privateClass,
    privateVariable,
} = unlock( privateApis );

请记住,务必始终在已注册的 store 上注册私有动作和选择器。

有时这很简单:

export const store = createReduxStore( STORE_NAME, storeConfig() );
// `register` 使用的是由 `createReduxStore` 创建的同一个 `store` 对象。
register( store );
unlock( store ).registerPrivateActions( {
    // ...
} );

不过,某些包可能会同时调用 createReduxStore 和 registerStore。在这种情况下,请始终选择被注册的那个 store:

export const store = createReduxStore( STORE_NAME, {
    ...storeConfig,
    persist: [ 'preferences' ],
} );
const registeredStore = registerStore( STORE_NAME, {
    ...storeConfig,
    persist: [ 'preferences' ],
} );
unlock( registeredStore ).registerPrivateActions( {
    // ...
} );

私有函数参数

要为稳定的函数添加私有参数,你需要
准备该函数的一个稳定版本和一个私有版本。
然后,导出稳定的函数,并用 lock() 将不稳定的函数
锁定在其中:

// 在 @wordpress/package1/index.js 中:
import { lock } from './lock-unlock';

// 私有函数包含所有逻辑
function privateValidateBlocks( formula, privateIsStrict ) {
    let isValid = false;
    // ...我们不希望重复编写的复杂逻辑...
    if ( privateIsStrict ) {
        // ...
    }
    // ...我们不希望重复编写的复杂逻辑...

    return isValid;
}

// 稳定的公共函数是一个薄封装,它会在禁用私有特性的
// 情况下调用私有函数
export function validateBlocks( blocks ) {
    privateValidateBlocks( blocks, false );
}

export const privateApis = {};
lock( privateApis, { privateValidateBlocks } );
// 在 @wordpress/package2/index.js 中:
import { privateApis as package1PrivateApis } from '@wordpress/package1';
import { unlock } from './lock-unlock';

// 借助稳定函数,私有函数可以被“解锁”:
const { privateValidateBlocks } = unlock( package1PrivateApis );
privateValidateBlocks( blocks, true );

私有 React 组件属性

要为稳定的组件添加私有参数,你需要
准备该组件的一个稳定版本和一个私有版本。
然后,导出稳定的函数,并用 lock() 将不稳定的函数
锁定在其中:

// 在 @wordpress/package1/index.js 中:
import { lock } from './lock-unlock';

// 私有组件包含所有逻辑
const PrivateMyButton = ( { title, privateShowIcon = true } ) => {
    // ...我们不想重复的复杂逻辑...

    return (
        <button>
            { privateShowIcon && <Icon src={ someIcon } /> } { title }
        </button>
    );
};

// 稳定的公共组件是一个薄封装,它会调用私有组件
// 并禁用其私有功能
export const MyButton = ( { title } ) => (
    <PrivateMyButton title={ title } privateShowIcon={ false } />
);

export const privateApis = {};
lock( privateApis, { PrivateMyButton } );
// 在 @wordpress/package2/index.js 中:
import { privateApis } from '@wordpress/package1';
import { unlock } from './lock-unlock';

// 在给定稳定组件的情况下,私有组件可以被“解锁”:
const { PrivateMyButton } = unlock( privateApis );
export function MyComponent() {
    return <PrivateMyButton data={ data } privateShowIcon={ true } />;
}

私有编辑器设置

WordPress 扩展开发者无法自行更新私有区块设置。@wordpress/block-editor store 的 updateSettings() action 会过滤掉所有不属于公共 API 的设置。要真正存储这些设置,唯一的方式是使用私有 action __experimentalUpdateSettings()。

要将某个区块编辑器设置设为私有,请将其添加到 /packages/block-editor/src/store/private-actions.js 中的 privateSettings 列表:

const privateSettings = [
    'inserterMediaCategories',
    // 在此处列出某个区块编辑器设置,使其成为私有
];

私有的 block.json 和 theme.json API

目前,还无法将 block.json 和 theme.json API
限制在 Gutenberg 代码库内。不过,未来新的私有 API
将仅适用于 WordPress 核心区块,插件和主题将无法
访问它们。

在 thunk 中内联小型 action

最后,与其引入新的 action creator,不如考虑使用 thunk:

export function toggleFeature( scope, featureName ) {
    return function ( { dispatch } ) {
        dispatch( { type: '__private_BEFORE_TOGGLE' } );
        // ...
    };
}

公开暴露私有 API

某些私有 API 可以从社区反馈中受益,因此将它们暴露给 WordPress 扩展开发者是有意义的。但同时,将它们变成 WordPress 核心中的公共 API 又没有意义。该怎么办呢?

你可以将该私有 API 重新导出为仅限插件使用的 API,从而只在 Gutenberg 插件中公开暴露它:

// 此函数在任何上下文中都不能被扩展开发者使用:
function privateEverywhere() {}

// 此函数可以在使用 Gutenberg 插件时被扩展开发者使用,但不能在原生 WordPress 核心中使用:
function privateInCorePublicInPlugin() {}

// Gutenberg 在内部将这两个函数都视为私有 API:
const privateApis = {};
lock( privateApis, { privateEverywhere, privateInCorePublicInPlugin } );

// privateInCorePublicInPlugin 函数被显式导出,
// 但由于 globalThis.IS_GUTENBERG_PLUGIN 检查,
// 该导出不会被合并到 WordPress 核心中。
if ( globalThis.IS_GUTENBERG_PLUGIN ) {
    export const privateInCorePublicInPlugin =
        unlock( privateApis ).privateInCorePublicInPlugin;
}

对象

在定义对象属性值时,请尽可能使用简写表示法:

const a = 10;

// 不好:
const object = {
    a: a,
    performAction: function () {
        // ...
    },
};

// 好:
const object = {
    a,
    performAction() {
        // ...
    },
};

字符串

字符串字面量应使用单引号声明,除非字符串本身包含需要转义的单引号——在这种情况下:使用双引号。如果字符串同时包含单引号和双引号,可以使用 ES6 模板字符串来避免转义引号。

注意:在面向用户的字符串中,对于 it’s 或 haven’t 这样的单词,永远不要用单引号字符(')来代替撇号(’)。对于测试代码,仍鼓励使用真正的撇号。

一般来说,应避免使用反斜杠转义引号:

// 不好:
const name = "Matt";
// 好:
const name = 'Matt';

// 不好:
const pet = "Matt's dog";
// 同样不好(未使用撇号):
const pet = "Matt's dog";
// 好:
const pet = 'Matt’s dog';
// 同样好:
const oddString = "She said 'This is odd.'";

只要有可能,就应使用 ES6 模板字符串而非字符串拼接:

const name = 'Stacey';

// 不好:
alert( 'My name is ' + name + '.' );
// 好:
alert( `My name is ${ name }.` );

可选链

可选链是 ECMAScript 规范 2020 版本中引入的一项新语言特性。虽然该特性对于访问可能为空值(null 或 undefined)的对象属性来说非常方便,但在使用可选链时有一些常见的陷阱需要注意。这些问题将来或许可以通过代码检查(linting)和/或类型检查来帮助防范。在此之前,你需要留意以下事项:

  • 当对使用可选链求值得到的结果取反(!)时,你应注意:如果可选链执行到无法继续的位置,它会产生一个假值,该值取反后会被转换为 true。在很多情况下,这并不是预期的结果。
    • 示例:const hasFocus = ! nodeRef.current?.contains( document.activeElement ); 在 nodeRef.current 未被赋值时会得到 true。
    • 参见相关问题:#21984
    • 参见类似的 ESLint 规则:no-unsafe-negation
  • 在赋布尔值时,请注意可选链可能产生假值(undefined、null),但并非严格意义上的 false。当这个值被以预期为布尔值(true 或 false)的方式传递时,就可能成为问题。虽然布尔值经常出现这种情况——因为布尔值常被用在逻辑上宽泛考虑真值性和假值性的场合——但当贸然假定属性访问链末端产生的类型时,其他可选链的使用也可能出现这些问题。类型检查可能有助于防止这类错误。
    • 示例:document.body.classList.toggle( 'has-focus', nodeRef.current?.contains( document.activeElement ) ); 可能会错误地添加该 class,因为第二个参数是可选的。如果传入的是 undefined,它将不会像传入 false 时那样移除该 class。
    • 示例:<input value={ state.selected?.value.trim() } /> 可能会因在受控输入与非受控输入之间切换而在 React 中意外引发警告。当贸然假定 trim() 的结果总会返回字符串值时,就很容易掉进这个陷阱,因为这忽略了可选链可能已导致求值提前中止并返回 undefined 值这一事实。

React 组件

建议将所有组件实现为函数组件,并使用 hooks 来管理组件的状态和生命周期。除了错误边界之外,你不应遇到必须使用类组件的情况。请注意,WordPress 代码重构指南在此同样适用:无需集中精力批量更新类组件。相反,应将其视为与其他更改相结合进行重构的好机会。

使用 JSDoc 编写 JavaScript 文档

Gutenberg 遵循 WordPress JavaScript 文档标准,此外还针对其在组织文件时对 import 语义的独特用法,以及使用 @wordpress/docgen 进行自动化文档生成,提供了额外的相关准则。

如需更多指导,请查阅 JSDoc 官方文档。

自定义类型

在可能的情况下,优先使用 TypeScript 语法定义类型。此指导适用于尚未迁移到 TypeScript 的文件。

使用 JSDoc 的 @typedef 标签来定义自定义类型。

自定义类型应包含描述,并且应始终包含其基础类型。

自定义类型的命名应尽可能简洁,同时保持含义清晰,并避免与其他全局类型或作用域类型发生冲突。所有自定义类型都应加上 WP 前缀。避免使用多余或冗余的前缀和后缀(例如 Type 后缀或 Custom 前缀)。自定义类型默认并非全局的,因此无需过度针对某个特定的包进行命名。不过,它们的命名仍应足够具体,以避免在与其他类型一同引入相同作用域时产生歧义或命名冲突。

/**
 * 区块选择对象。
 *
 * @typedef WPBlockSelection
 *
 * @property {string} clientId     区块客户端 ID。
 * @property {string} attributeKey 区块属性键。
 * @property {number} offset       属性值偏移量,基于富文本值。
 */

注意,@typedef 与类型名称之间没有 {Object}。由于下面的 @property 已表明这是一个对象类型,因此建议在为对象定义类型时不要使用 {Object}。

自定义类型还可用于描述一组预定义的选项。虽然 类型联合可以将字面值作为内联类型使用,但要在遵守 80 个字符的最大行长限制的同时对齐各标签可能会比较困难。使用自定义类型来定义联合类型,可以借此机会描述这些选项的用途,并有助于避免这些行长问题。

/**
 * 具名断点尺寸。
 *
 * @typedef {'huge'|'wide'|'large'|'medium'|'small'|'mobile'} WPBreakpoint
 */

注意在定义一组字符串字面量时引号的用法。与 JavaScript 编码标准中的规定一致,在将字符串字面量指定为类型或 默认函数参数时,以及在 指定导入类型的路径时,都应使用单引号。

导入和导出类型

在可能的情况下,优先使用 TypeScript 语法导入和导出类型。此指导适用于尚未迁移到 TypeScript 的文件。

使用 TypeScript 的 import 函数从其他文件或第三方依赖项导入类型声明。

由于导入的类型声明可能会占用过多的可用行长度,并且在被多次引用时会显得冗长,因此建议在文件顶部、紧随 import 分组之后,使用 @typedef 声明为外部类型创建别名。

/** @typedef {import('@wordpress/data').WPDataRegistry} WPDataRegistry */

请注意,在其他文件中定义的所有自定义类型都可以被导入。

在考虑应从 WordPress 包中公开提供哪些类型时,包入口点脚本中的 @typedef 语句应被视为实际上等同于该包的公共 API。意识到这一点很重要,这既有助于避免在公共接口上无意暴露内部类型,也是公开项目公共类型的一种方式。

// packages/data/src/index.js

/** @typedef {import('./registry').WPDataRegistry} WPDataRegistry */

在此代码片段中,@typedef 将支持上一个示例中 import('@wordpress/data') 的用法。

外部依赖项

许多第三方依赖项会发布它们自己的 TypeScript 类型定义。对于这些依赖项,import 语义应该“直接可用”。

<img src="https://user-images.githubusercontent.com/1779930/70167742-62198800-1695-11ea-9c21-82a91d4a60e2.png" alt="有效示例:import 类型” />

如果你为编辑器使用了 TypeScript 集成,那么通常只要看到类型解析为回退类型 any 以外的其他类型,就可以确认它是有效的。

对于不发布自己的 TypeScript 类型的包,如果存在 DefinitelyTyped 社区维护的类型定义,欢迎你安装并使用。

泛型

在可能的情况下,优先使用 TypeScript 语法标注类型。此指导适用于尚未迁移到 TypeScript 的文件。

在为 Object、Function、Promise 等泛型类型编写文档时,请始终包含有关预期记录类型的详细信息。

// 不好:

/** @type {Object} */
/** @type {Function} */
/** @type {Promise} */

// 好:

/** @type {Record<string,number>} */ /* 或 */ /** @type {{[setting:string]:any}} */
/** @type {(key:string)=>boolean} */
/** @type {Promise<string>} */

当对象被用作字典时,可以通过两种方式定义其类型:可索引接口({[setting:string]:any})或 Record。当对象的键名能为开发者提供操作提示时(例如 setting),应使用可索引接口;否则,应使用 Record。

这里的函数表达式使用了 TypeScript 的函数类型语法,有助于提供有关预期参数的名称和类型的更详细信息。更多信息请查阅 TypeScript @type 标签的函数建议。

在更高级的场景中,你可以使用 TypeScript 的 @template 标签将自己的自定义类型定义为泛型。

与上文关于类型联合和字面量的“自定义类型”建议类似,你可以考虑创建自定义类型 @typedef,以便更好地描述对象记录的预期键值,或者提取复杂的函数签名。

/**
 * 一个 apiFetch 中间件处理器。传入 fetch 选项后,该中间件应在完成
 * 其处理后调用 `next` 中间件。
 *
 * @typedef {(options:WPAPIFetchOptions,next:WPAPIFetchMiddleware)=>void} WPAPIFetchMiddleware
 */
/**
 * 具名断点尺寸。
 *
 * @typedef {"huge"|"wide"|"large"|"medium"|"small"|"mobile"} WPBreakpoint
 */

/**
 * 以断点名称为键、以断点生效时的像素宽度为值的哈希表。
 *
 * @type {Record<WPBreakpoint,number>}
 */
const BREAKPOINTS = { huge: 1440 /* , ... */ };

可空、undefined 和 void 类型

在可能的情况下,优先使用 TypeScript 类型。此指导适用于尚未迁移到 TypeScript 的文件。

可以使用前导 ? 来表示可空类型。只有在描述类型本身或显式的 null 值时,才使用类型的可空形式。不要将可空形式用作可选参数的指示标志。

/**
 * 如果存在,则返回给定键的配置值。如果没有已配置的值,
 * 则返回 null。
 *
 * @param {string} key 要获取的配置键。
 *
 * @return {?*} 配置值(如果存在)。
 */
function getConfigurationValue( key ) {
    return config.hasOwnProperty( key ) ? config[ key ] : null;
}

类似地,只有在预期出现显式的 undefined 值时,才使用 undefined 类型。

/**
 * 如果下一个 HTML token 闭合当前 token,则返回 true。
 *
 * @param {WPHTMLToken}           currentToken 要与之比较的当前 token。
 * @param {WPHTMLToken|undefined} nextToken    要比较的下一个 token。
 *
 * @return {boolean} 如果 `nextToken` 闭合 `currentToken`,则为 true,否则为 false。
 */

如果参数是可选的,请使用方括号表示法。如果可选参数具有默认值,且该默认值可以在函数表达式中表示为默认参数,则无需在 JSDoc 中包含该值。如果函数参数的有效默认值需要复杂的逻辑,无法使用默认参数语法来表示,则可以选择在 JSDoc 中包含该默认值。

/**
 * 渲染一个工具栏。
 *
 * @param {Object} props             组件 props。
 * @param {string} [props.className] 要设置在容器 `<div />` 上的 class。
 */

当函数不包含 return 语句时,就称其返回值为 void。如果返回类型为 void,则无需包含 @return 标签。

如果函数存在多个代码路径,其中部分(但非全部)条件会执行 return 语句,则可以将其记录为包含 void 类型的联合类型。

/**
 * 返回给定键的配置值(如果存在)。
 *
 * @param {string} key 要检索的配置键。
 *
 * @return {*|void} 配置值(如果存在)。
 */
function getConfigurationValue( key ) {
    if ( config.hasOwnProperty( key ) ) {
        return config[ key ];
    }
}

在记录函数类型时,必须始终包含 void 返回值类型,否则该函数会被推断为返回混合(“any”)值,而不是 void 结果。

/**
 * 一个 apiFetch 中间件处理器。传入 fetch 选项后,该中间件应在完成其处理后
 * 调用 `next` 中间件。
 *
 * @typedef {(options:WPAPIFetchOptions,next:WPAPIFetchMiddleware)=>void} WPAPIFetchMiddleware
 */

记录示例

由于使用 @wordpress/docgen 工具生成的文档会包含已定义的 @example 标签,因此为函数和组件提供使用示例被视为最佳实践。对于包的公共 API 中已记录的成员而言,这一点尤为重要。

在记录示例时,请使用 markdown 的 \`\`\` 代码块来界定代码示例的开头和结尾。示例可以跨越多行。

/**
 * 根据已注册 store 的名称,返回包含该 store 的 selectors 的对象。这些
 * selector 函数已经过预绑定,会自动传入当前状态。作为使用者,你只需传入
 * selector 的参数(如果适用)。
 *
 * @param {string} name store 名称。
 *
 * @example
 * ```js
 * select( 'my-shop' ).getPrice( 'hammer' );
 * ```
 *
 * @return {Record<string,WPDataSelector>} 包含该 store 的 selectors 的对象。
 */

记录 React 组件

组件的 prop 类型应尽可能使用 TypeScript 语法编写。本指南适用于尚未迁移到 TypeScript 的文件。

在可能的情况下,所有组件都应实现为函数组件,并使用 hooks 来管理组件的生命周期和状态。

记录函数组件的方式与记录任何其他函数相同。记录组件时需要注意的主要一点是,该函数通常只接受一个参数(即 “props”),其中可能包含许多属性成员。请使用参数属性的点语法来记录各个 prop 的类型。

/**
 * 将区块已配置的标题渲染为字符串,若无法确定标题则为空。
 *
 * @example
 *
 * ```jsx
 * <BlockTitle clientId="afd1cb17-2c08-4e7a-91be-007ba7ddc3a1" />
 * ```
 *
 * @param {Object} props
 * @param {string} props.clientId 区块的客户端 ID。
 *
 * @return {?string} 区块标题。
 */

对于类组件,目前没有关于如何记录组件 props 的推荐做法。Gutenberg 不使用也不推荐 propTypes 静态类成员。

PHP

我们使用
phpcs(PHP_CodeSniffer)配合 WordPress Coding Standards 规则集,对本项目中的所有 PHP 代码运行大量自动化检查。这确保了我们与 WordPress PHP 编码标准保持一致。

使用 PHPCS 最简单的方法是通过本地环境。安装完成后,你就可以运行 npm run lint:php 来检查你的 PHP 代码。

如果你更倾向于在本地安装 PHPCS,则应使用 composer。首先在你的计算机上安装 composer,然后运行 composer install。这将安装 phpcs 和 WordPress-Coding-Standards,之后你便可以通过 composer lint 来运行它们。

GitHub Actions 工作流文件

GitHub Actions 工作流在特权软件供应链环境中运行,因此所有工作流文件都必须遵守极高的质量和安全标准。

这些文件在被修改时会使用 Actionlint 和 Zizmor 进行静态扫描。Actionlint 会扫描 .github/workflows 目录中的 YAML 工作流文件,而 Zizmor 还会扫描位于仓库中任意位置的 action 文件(action.yml)。建议你使用包管理器在本地安装这两个工具,并在提交对工作流或 action 文件的更改之前先运行它们。

运行 Actionlint:

actionlint

对所有工作流文件运行 Zizmor(注意末尾的句点):

zizmor .

注意:当 Zizmor 检测到问题时,不会导致工作流运行失败。相反,生成的报告会被提交到 GitHub Code Scanning,并通过状态检查以及拉取请求中上下文相关的行内注解呈现出来。根据仓库配置的 Code Scanning 设置,某些在本地报告的问题可能会被忽略。