gutenbergdocs/docs/contributors/code/coding-guidelines.md
2025-10-22 01:40:18 +08:00

33 KiB
Raw Permalink Blame History

记录 React 组件文档

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

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

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

对于类组件没有关于记录组件属性的推荐方式。Gutenberg 不使用也不认可 propTypes 静态类成员

PHP

我们使用 phpcsPHP_CodeSnifferWordPress 编码标准规则集 对本项目中的所有 PHP 代码进行大量自动化检查。这确保我们符合 WordPress 的 PHP 编码标准。

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

如果更倾向于在本地安装 PHPCS应使用 composer。在计算机上安装 composer,然后运行 composer install。这将安装 phpcsWordPress-Coding-Standards,之后可以通过 composer lint 运行检查。

编程规范

本文档旨在为古腾堡项目制定专属的编程规范。基础编程规范遵循WordPress编码标准。以下章节将阐述古腾堡项目中使用的额外模式与约定。

CSS

命名规范

为避免类名冲突,所有类名必须遵循以下准则,这些准则借鉴了BEM块、元素、修饰符方法论的核心思想。

所有分配给元素的类名都必须以软件包名称为前缀,后接连字符和组件所在目录的名称。组件根元素的所有后代元素必须追加以连字符分隔的描述符,并通过双下划线__与基础类名分隔。

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

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

示例:

假设存在位于packages/components/src/notice/index.js的组件:

export default function Notice( { children, onRemove } ) {
	return (
		<div className="components-notice">
			<div className="components-notice__content">{ children }</div>
			<Button
				className="components-notice__dismiss"
				icon={ check }
				label={ __( '关闭此通知' ) }
				onClick={ onRemove }
			/>
		</div>
	);
}

组件可被赋予表示状态的类名(例如“激活”标签页或“展开”面板)。这些修饰符应作为独立类名应用,通过is-前缀构成形容词表达式(如is-activeis-opened)。特殊情况下,可能会遇到修饰符前缀的变体,通常是为了提升可读性(如has-warning)。由于修饰符类名不限定于特定组件,在样式表中应始终与被修饰的组件配合使用(.components-panel.is-opened)。

示例:

再次以通知组件为例。我们可能需要为可关闭通知应用特定样式。clsx工具包可辅助实现条件化应用修饰符类名。

import clsx from 'clsx';

export default function Notice( { children, onRemove, isDismissible } ) {
	const classes = clsx( 'components-notice', {
		'is-dismissible': isDismissible,
	} );

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

组件的类名绝不应在其所属文件夹之外使用(极少数例外情况如_z-index.scss。若需在自己组件中继承其他组件的样式应渲染该组件的实例。最不推荐的做法是在自身组件的样式表中复制样式。此举旨在通过将共享组件隔离为可复用接口来提升可维护性通过适配有限通用组件支持多样化使用场景从而减少相似UI元素的实现复杂度。

区块组件的SCSS文件命名规范

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

置于style.scss文件中的样式将被编译至blocks/build/style.css,同时在前端主题和编辑器中加载。如需针对区块在编辑器中的显示添加特定样式,请将其加入editor.scss文件。

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

React 组件

推荐将所有组件实现为函数式组件,并使用钩子来管理组件状态和生命周期。除错误边界外,您不应遇到必须使用类组件的情况。请注意,WordPress 代码重构指南在此适用:无需集中批量更新类组件,而应将其视为结合其他更改进行重构的良好机会。

使用 JSDoc 的 JavaScript 文档

Gutenberg 遵循 WordPress JavaScript 文档标准,并针对其在文件组织中对导入语义的特殊使用、使用 TypeScript 工具进行类型验证,以及使用 @wordpress/docgen 自动生成文档,制定了相关附加指南。

更多指导请参考以下资源:

自定义类型

使用 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 编码标准,在为类型或默认函数参数分配字符串字面量时,或在指定导入类型的路径时,应使用单引号。

可空类型、undefined 类型与 void 类型

你可以使用前置问号 ? 表示可空类型。仅当描述类型本身或显式的 null 值时,才使用可空类型形式。不要将可空形式作为可选参数的标识符。

/**
 * 获取指定键的配置值(若存在)。若未配置该值则返回 null。
 *
 * @param {string} key 要获取的配置键名。
 *
 * @return {?*} 配置值(若存在)。
 */
function getConfigurationValue( key ) {
	return config.hasOwnProperty( key ) ? config[ key ] : null;
}

类似地,仅当需要显式的 undefined 值时,才使用 undefined 类型。

/**
 * 若下一个 HTML 标签闭合当前标签,则返回 true。
 *
 * @param {WPHTMLToken}           currentToken 要比对的当前标签。
 * @param {WPHTMLToken|undefined} nextToken    要比对的下一个标签。
 *
 * @return {boolean} 若 `nextToken` 闭合 `currentToken` 则返回 true否则返回 false。
 */

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

/**
 * 渲染工具栏组件。
 *
 * @param {Object} props             组件属性。
 * @param {string} [props.className] 要设置到容器 `<div />` 上的类名。
 */

当函数不包含 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 中间件处理器。在传入获取选项后,中间件需在完成处理时调用 `next` 中间件。
 *
 * @typedef {(options:WPAPIFetchOptions,next:WPAPIFetchMiddleware)=>void} WPAPIFetchMiddleware
 */

示例文档规范

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

编写示例时,请使用 Markdown 的 ``` 代码块标记来界定代码段的起始与结束。示例可跨越多行。

/**
 * 根据已注册存储的名称,返回该存储选择器对象。选择器函数已预绑定,可自动传递当前状态。
 * 作为使用者,仅需传递选择器所需参数(若适用)。
 *
 * @param {string} name 存储名称。
 *
 * @example
 * ```js
 * select( 'my-shop' ).getPrice( 'hammer' );
 * ```
 *
 * @return {Record<string,WPDataSelector>} 包含存储选择器的对象。
 */

JavaScript

Gutenberg 中的 JavaScript 采用了 ECMAScript 语言规范 的现代语言特性以及 JSX 语言语法扩展。这些功能通过预设配置组合实现,特别是项目中 Babel 配置使用的预设 @wordpress/babel-preset-default

虽然引入新 JavaScript 语言特性的 分阶段流程 提供了在特性尚未完全定型前使用的机会,但 Gutenberg 项目和 @wordpress/babel-preset-default 配置仅支持已达到第 4 阶段("已完成")的提案

导入方式

在 Gutenberg 项目中,我们使用 ES2015 导入语法 来创建模块化代码,明确区分特定功能代码、跨 WordPress 功能共享代码以及第三方依赖代码。

这些区分通过文件顶部的多行注释来标识,这些注释标注了从其他文件或源导入的代码。

外部依赖

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

示例:

/**
 * 外部依赖
 */
import moment from 'moment';

WordPress 依赖

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

示例:

/**
 * WordPress 依赖
 */
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这些 API 是在主要的 WordPress 发布期间从 Gutenberg 插件合并而来的。许多插件和主题开始依赖这些实验性 API 来实现无法通过其他方式访问的关键功能。

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

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

类型导入与导出

使用 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 语义应“直接可用”。

工作示例:import 类型

如果您的编辑器使用了 TypeScript 集成,通常可以看到此功能生效,只要类型解析结果不是回退的 any 类型即可。

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

泛型类型

在记录泛型类型(如 ObjectFunctionPromise 等)时,请始终包含有关预期记录类型的详细信息。

// 不推荐:

/** @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 中间件处理程序。传入获取选项后,中间件应在完成处理时调用 `next` 中间件。
 *
 * @typedef {(options:WPAPIFetchOptions,next:WPAPIFetchMiddleware)=>void} WPAPIFetchMiddleware
 */
/**
 * 命名的断点尺寸。
 *
 * @typedef {"huge"|"wide"|"large"|"medium"|"small"|"mobile"} WPBreakpoint
 */

/**
 * 断点名称及其生效像素宽度的哈希表。
 *
 * @type {Record<WPBreakpoint,number>}
 */
const BREAKPOINTS = { huge: 1440 /* , ... */ };
私有函数、类与变量
// 在 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 );

请务必始终在已注册的存储上注册私有操作和选择器。

有时这很简单:

export const store = createReduxStore( STORE_NAME, storeConfig() );
// `register` 使用与 `createReduxStore` 创建的相同 `store` 对象
register( store );
unlock( store ).registerPrivateActions( {
	// ...
} );

但某些包可能同时调用 createReduxStore registerStore。此时请始终选择已注册的存储:

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 } />;
}

插件专用 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 最终可能稳定成为公共 API但这并非必然结果。

私有 API

每个需要内部访问或暴露私有 API 的 @wordpress 包,可通过启用 @wordpress/private-apis 来实现:

// 在 packages/block-editor/private-apis.js 中:
import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis';
export const { lock, unlock } =
	__dangerousOptInToUnstableAPIsOnlyForCoreModules(
		'我知晓私有功能不可在主题或插件中使用,否则将在下一版 WordPress 中失效。',
		'@wordpress/block-editor' // 调用 __dangerousOptInToUnstableAPIsOnlyForCoreModules 的包名称
		//(非目标访问包的名称)
	);

每个 @wordpress 包仅可启用一次。该流程明确告知扩展者不应使用此机制。本文档将重点介绍使用示例,但您可在其 README.md 中了解更多关于 @wordpress/private-apis 包的信息

启用后,您可使用 lock()unlock() 工具:

// 假设此对象从包中导出:
export const publicObject = {};

// 但此字符串属于内部内容,不应公开:
const privateString = '私有信息';

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

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

// 访问字符串的唯一方式是"解锁"该对象:
console.log( unlock( publicObject ) );
// "私有信息"

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

继续阅读了解如何使用 lock()unlock() 来避免公开导出各类私有 API。

私有选择器与操作

您可将私有选择器和操作附加到公共存储:

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

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

// 将私有选择器附加到导出的存储:
unlock( store ).registerPrivateSelectors( {
	privateHasContentRoleAttribute,
} );
// 在 packages/package2/MyComponent.js 中:
import { store } from '@wordpress/package1';
import { useSelect } from '@wordpress/data';
// `unlock` 函数从内部 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();

	// ...
}

私有编辑器设置

WordPress扩展开发者无法自行更新私有区块设置。@wordpress/block-editor存储库的updateSettings()操作会过滤掉所有不属于公共API的设置。实际存储这些设置的唯一方式是通过私有操作__experimentalUpdateSettings()

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

const privateSettings = [
	'inserterMediaCategories',
	// 在此列出区块编辑器设置以使其私有化
];

私有block.json与theme.json接口

截至目前,尚无法将block.jsontheme.json接口限制在Gutenberg代码库内使用。但未来新的私有接口将仅适用于WordPress核心区块插件和主题将无法访问这些接口。

在thunk中内联小型操作

最后,与其创建新的操作生成器,不如考虑使用thunk

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

公开私有接口

某些私有接口可通过社区反馈获益因此有必要向WordPress扩展开发者公开。但同时将其转化为WordPress核心的公共接口并不合理。此时该如何处理

您可以将该私有接口重新导出为仅限插件使用的接口使其仅在Gutenberg插件中公开

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

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

// Gutenberg在内部将这两个函数都视为私有接口
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模板字符串来避免转义引号。

注意: 在面向用户的字符串中,切勿将单引号字符(')用作撇号()(如itshavent)。在测试代码中仍鼓励使用真正的撇号。

通常应避免使用反斜杠转义引号:

// 不推荐:
const name = "Matt";
// 推荐:
const name = 'Matt';

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

应尽可能使用ES6模板字符串替代字符串拼接

const name = 'Stacey';

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

可选链

可选链是ECMAScript 2020版本引入的新语言特性。虽然该特性对于可能为空值nullundefined)的对象属性访问非常方便,但在使用可选链时需要注意一些常见陷阱。未来或许可以通过代码检查或类型检查来避免这些问题。目前需要警惕以下情况:

  • 当对通过可选链求值的值进行取反(!)操作时,需注意:如果可选链执行到无法继续的位置,会产生一个假值,该值取反后会转换为true。这在多数情况下不符合预期。
    • 示例:const hasFocus = ! nodeRef.current?.contains( document.activeElement );nodeRef.current未赋值时将返回true
    • 相关issue#21984
    • 类似ESLint规则no-unsafe-negation
  • 当赋值布尔值时,注意可选链可能产生非严格false假值undefinednull)。当该值以期望为布尔值(truefalse)的方式传递时,可能引发问题。虽然布尔值常出现这种情况——因为布尔值通常用于考虑广义真值性的逻辑中——但当急切假设属性访问链末端的结果类型时,其他类型的可选链也可能出现此类问题。类型检查有助于预防这类错误。
    • 示例:document.body.classList.toggle( 'has-focus', nodeRef.current?.contains( document.activeElement ) ); 可能错误地添加类,因为第二个参数是可选的。如果传入undefined,将不会像传入false时那样取消设置该类
    • 示例:<input value={ state.selected?.value.trim() } /> 可能因在受控和非受控输入间切换而触发React警告。当急切假设trim()始终返回字符串值,却忽略了可选链可能导致求值提前中止并返回undefined时,很容易陷入此陷阱