diff --git a/content/zh/guide/v11/api-reference.md b/content/zh/guide/v11/api-reference.md new file mode 100644 index 000000000..0fb7c734b --- /dev/null +++ b/content/zh/guide/v11/api-reference.md @@ -0,0 +1,202 @@ +--- +title: API 参考 +description: 了解 Preact 模块导出的所有函数 +--- + +# API 参考 + +此页为您提供所有导出函数的速查表。 + +--- + + + +--- + +## Component + +`Component` 是用于创建有状态 Preact 组件的基类。 + +渲染器会自动管理并按需创建组件,不会直接实例化。 + +```js +import { Component } from 'preact'; + +class MyComponent extends Component { + // (见下) +} +``` + +### Component.render(props, state) + +所有组件必须提供 `render()` 函数,其参数为组件的当前属性 (props) 与状态 (state),返回值则是虚拟 DOM 元素 (JSX 元素)、Array,或 `null`。 + +```jsx +import { Component } from 'preact'; + +class MyComponent extends Component { + render(props, state) { + // props 等同于 this.props + // state 等同于 this.state + + return
A
B
Something went badly wrong
{char}
+ {name}, Age: {age} +
`元素中,以便我们可以看到结果。 + + + +```jsx +// --repl +import { render, Component } from 'preact'; +// --repl-before +class BasicInput extends Component { + state = { name: '' }; + + onInput = e => this.setState({ name: e.currentTarget.value }); + + render(_, { name }) { + return ( + + + 姓名: + + 你好 {name} + + ); + } +} +// --repl-after +render(, document.getElementById('app')); +``` + +```jsx +// --repl +import { render } from 'preact'; +import { useState } from 'preact/hooks'; +// --repl-before +function BasicInput() { + const [name, setName] = useState(''); + + return ( + + + 姓名: setName(e.currentTarget.value)} /> + + 你好 {name} + + ); +} +// --repl-after +render(, document.getElementById('app')); +``` + + + +### 输入框(复选框和单选按钮) + + + +```jsx +// --repl +import { render, Component } from 'preact'; +// --repl-before +class BasicRadioButton extends Component { + state = { + allowContact: false, + contactMethod: '' + }; + + toggleContact = () => + this.setState({ allowContact: !this.state.allowContact }); + setRadioValue = e => this.setState({ contactMethod: e.currentTarget.value }); + + render(_, { allowContact }) { + return ( + + + 允许联系: + + + 电话:{' '} + + + + 电子邮件:{' '} + + + + 邮件:{' '} + + + + 您{allowContact ? '已允许' : '尚未允许'}联系 + {allowContact && `,通过${this.state.contactMethod}`} + + + ); + } +} +// --repl-after +render(, document.getElementById('app')); +``` + +```jsx +// --repl +import { render } from 'preact'; +import { useState } from 'preact/hooks'; +// --repl-before +function BasicRadioButton() { + const [allowContact, setAllowContact] = useState(false); + const [contactMethod, setContactMethod] = useState(''); + + const toggleContact = () => setAllowContact(!allowContact); + const setRadioValue = e => setContactMethod(e.currentTarget.value); + + return ( + + + 允许联系: + + + 电话:{' '} + + + + 电子邮件:{' '} + + + + 邮件:{' '} + + + + 您{allowContact ? '已允许' : '尚未允许'}联系 + {allowContact && `,通过${contactMethod}`} + + + ); +} +// --repl-after +render(, document.getElementById('app')); +``` + + + +### 选择框 + + + +```jsx +// --repl +import { render, Component } from 'preact'; +// --repl-before +class MySelect extends Component { + state = { value: '' }; + + onChange = e => { + this.setState({ value: e.currentTarget.value }); + }; + + render(_, { value }) { + return ( + + + A + B + C + + 您选择了: {value} + + ); + } +} +// --repl-after +render(, document.getElementById('app')); +``` + +```jsx +// --repl +import { render } from 'preact'; +import { useState } from 'preact/hooks'; +// --repl-before +function MySelect() { + const [value, setValue] = useState(''); + + return ( + + setValue(e.currentTarget.value)}> + A + B + C + + 您选择了: {value} + + ); +} +// --repl-after +render(, document.getElementById('app')); +``` + + + +## 基本表单 + +虽然单独的输入控件很有用,并且可以满足很多需求,但通常我们会看到输入控件组合成可以将多个控件组合在一起的*表单*。为了帮助管理这些,我们使用``元素。 + +为了演示,我们将创建一个新的``元素,其中包含两个``字段:一个用于用户的名字,一个用于用户的姓氏。我们将使用`onSubmit`事件来监听表单提交并用用户的全名更新状态。 + + + +```jsx +// --repl +import { render, Component } from 'preact'; +// --repl-before +class FullNameForm extends Component { + state = { fullName: '' }; + + onSubmit = e => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + this.setState({ + fullName: formData.get('firstName') + ' ' + formData.get('lastName') + }); + e.currentTarget.reset(); // 清除输入框,为下一次提交做准备 + }; + + render(_, { fullName }) { + return ( + + + + 名字: + + + 姓氏: + + 提交 + + {fullName && 你好 {fullName}} + + ); + } +} +// --repl-after +render(, document.getElementById('app')); +``` + +```jsx +// --repl +import { render } from 'preact'; +import { useState } from 'preact/hooks'; +// --repl-before +function FullNameForm() { + const [fullName, setFullName] = useState(''); + + const onSubmit = e => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + setFullName(formData.get('firstName') + ' ' + formData.get('lastName')); + e.currentTarget.reset(); // 清除输入框,为下一次提交做准备 + }; + + return ( + + + + 名字: + + + 姓氏: + + 提交 + + {fullName && 你好 {fullName}} + + ); +} + +// --repl-after +render(, document.getElementById('app')); +``` + + + +> **注意**:虽然在 React 和 Preact 表单中常见的做法是将每个输入字段链接到组件状态,但这通常是不必要的,而且可能会变得笨重。作为一个非常宽松的经验法则,在大多数情况下,您应该优先使用`onSubmit`和[`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) API,只在需要时使用组件状态。这可以减少组件的复杂性,并可能跳过不必要的重新渲染。 + +## 受控组件和非受控组件 + +在讨论表单控件时,您可能会遇到"受控组件"和"非受控组件"这两个术语。这些术语指的是表单控件的值是否由组件显式管理。通常,您应该尽可能使用*非受控*组件,DOM 完全能够处理``的状态: + +```jsx +// 非受控组件,因为Preact不设置值 + +``` + +然而,在某些情况下,您可能需要对输入值进行更严格的控制,这时可以使用*受控*组件。 + +```jsx +// 受控组件,因为Preact设置了值 + +``` + +Preact 在受控组件方面有一个已知问题:Preact 需要重新渲染才能控制输入值。这意味着如果您的事件处理程序没有更新状态或以某种方式触发重新渲染,输入值将不受控制,有时会与组件状态不同步。 + +这些问题情况的一个例子是:假设您有一个应该限制为 3 个字符的输入字段。您可能有这样的事件处理程序: + +```js +const onInput = e => { + if (e.currentTarget.value.length <= 3) { + setValue(e.currentTarget.value); + } +}; +``` + +这个问题在于当输入未通过该条件判断的情况:因为我们没有运行`setValue`,组件不会重新渲染,而由于组件不重新渲染,输入值就无法正确受控。然而,即使我们在处理程序中添加了`else { setValue(value) }`,Preact 也足够智能,能够检测到值没有改变,因此不会重新渲染组件。这就需要我们使用[`refs`](/guide/v10/refs)来弥合 DOM 状态和 Preact 状态之间的差距。 + +> 有关 Preact 中受控组件的更多信息,请参阅 Jovi De Croock 的[受控输入](https://www.jovidecroock.com/blog/controlled-inputs)。 + +以下是如何使用受控组件来限制输入字段中字符数的示例: + + + +```jsx +// --repl +import { render, Component, createRef } from 'preact'; +// --repl-before +class LimitedInput extends Component { + state = { value: '' }; + inputRef = createRef(null); + + onInput = e => { + if (e.currentTarget.value.length <= 3) { + this.setState({ value: e.currentTarget.value }); + } else { + const start = this.inputRef.current.selectionStart; + const end = this.inputRef.current.selectionEnd; + const diffLength = Math.abs( + e.currentTarget.value.length - this.state.value.length + ); + this.inputRef.current.value = this.state.value; + // 恢复选择 + this.inputRef.current.setSelectionRange( + start - diffLength, + end - diffLength + ); + } + }; + + render(_, { value }) { + return ( + + + 此输入框限制为3个字符:{' '} + + + + ); + } +} +// --repl-after +render(, document.getElementById('app')); +``` + +```jsx +// --repl +import { render } from 'preact'; +import { useState, useRef } from 'preact/hooks'; +// --repl-before +const LimitedInput = () => { + const [value, setValue] = useState(''); + const inputRef = useRef(); + + const onInput = e => { + if (e.currentTarget.value.length <= 3) { + setValue(e.currentTarget.value); + } else { + const start = inputRef.current.selectionStart; + const end = inputRef.current.selectionEnd; + const diffLength = Math.abs(e.currentTarget.value.length - value.length); + inputRef.current.value = value; + // 恢复选择 + inputRef.current.setSelectionRange(start - diffLength, end - diffLength); + } + }; + + return ( + + + 此输入框限制为3个字符:{' '} + + + + ); +}; +// --repl-after +render(, document.getElementById('app')); +``` + + diff --git a/content/zh/guide/v11/getting-started.md b/content/zh/guide/v11/getting-started.md new file mode 100644 index 000000000..cf485d9b7 --- /dev/null +++ b/content/zh/guide/v11/getting-started.md @@ -0,0 +1,275 @@ +--- +title: 入门指南 +description: 如何开始使用 Preact。我们将学习如何设置工具(如果需要)并开始编写应用程序 +--- + +# 入门指南 + +刚接触 Preact?刚接触虚拟 DOM?请查看[教程](/tutorial)。 + +本指南帮助您启动并运行以开始开发 Preact 应用程序,使用 3 种流行选项。 +如果您是 Preact 新手,我们推荐从 [Vite](#创建一个-vite-驱动的-preact-应用) 开始。 + +--- + + + +--- + +## 无构建工具路线 + +Preact 被打包为可在浏览器中直接使用,不需要任何构建或工具: + +```html + +``` + +[🔨 在 Glitch 上编辑](https://glitch.com/~preact-no-build-tools) + +这种开发方式的主要缺点是缺乏 JSX,这需要构建步骤。JSX 的一个符合人体工程学且高性能的替代方案记录在下一节中。 + +### JSX 的替代方案 + +编写原始的 `h` 或 `createElement` 调用可能很繁琐。JSX 的优势在于它看起来类似于 HTML,这使得许多开发人员更容易理解。在我们的经验中,JSX 需要构建步骤,因此我们强烈推荐一个称为 [HTM][htm] 的替代方案。 + +[HTM][htm] 是一种类似 JSX 的语法,可以在标准 JavaScript 中工作。它不要求构建步骤,而是使用 JavaScript 自己的[标记模板](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#Tagged_templates)语法,该语法于 2015 年添加,并在[所有现代浏览器](https://caniuse.com/#feat=template-literals)中受支持。这是一种日益流行的编写 Preact 应用程序的方式,因为与传统的前端构建工具设置相比,需要理解的移动部件更少。 + +```html + +``` + +[🔨 在 Glitch 上编辑](https://glitch.com/~preact-with-htm) + +> **提示:** HTM 还提供了一个方便的单导入 Preact 版本: +> +> `import { html, render } from 'https://esm.sh/htm/preact/standalone'` + +对于更具可扩展性的解决方案,请参见[导入映射 -- 基本用法](/guide/v10/no-build-workflows#basic-usage),有关 HTM 的更多信息,请查看其[文档][htm]。 + +[htm]: https://github.com/developit/htm + +## 创建一个 Vite 驱动的 Preact 应用 + +[Vite](https://vitejs.dev) 在过去几年中已成为跨多个框架构建应用程序的非常流行的工具,Preact 也不例外。它建立在 ES 模块、Rollup 和 ESBuild 等流行工具之上。通过我们的初始化器或他们的 Preact 模板,Vite 无需配置或事先知识即可开始使用,这种简单性使其成为使用 Preact 的非常流行方式。 + +要快速启动 Vite,您可以使用我们的初始化器 `create-preact`。这是一个可以在您的机器终端中运行的交互式命令行界面 (CLI) 应用程序。使用它,您可以通过运行以下命令创建一个新应用程序: + +```bash +npm init preact +``` + +这将引导您创建一个新的 Preact 应用程序,并为您提供一些选项,例如 TypeScript、路由(通过 `preact-iso`)和 ESLint 支持。 + +> **提示:** 这些决定都不需要是最终的,如果您改变主意,您可以随时在项目中添加或删除它们。 + +### 准备开发 + +现在我们准备启动应用程序。要启动开发服务器,请在新生成的项目的文件夹中运行以下命令: + +```bash +# 进入生成的项目文件夹 +cd my-preact-app + +# 启动开发服务器 +npm run dev +``` + +一旦服务器启动,它将打印一个本地开发 URL,您可以在浏览器中打开。 +现在您已经准备好开始编写应用程序代码了! + +### 创建生产构建 + +当您需要将应用程序部署到某个地方时,就会用到这个时刻。Vite 提供了一个方便的 `build` 命令,它将生成高度优化的生产构建。 + +```bash +npm run build +``` + +完成后,您将拥有一个新的 `dist/` 文件夹,可以直接部署到服务器上。 + +> 有关所有可用命令及其选项的完整列表,请查看 [Vite CLI 文档](https://vitejs.dev/guide/cli.html)。 + +## 集成到现有管道中 + +如果您已经设置了现有的工具管道,很可能其中包含一个打包器。最流行的选择是 [webpack](https://webpack.js.org/)、[rollup](https://rollupjs.org) 或 [parcel](https://parceljs.org/)。Preact 开箱即用地与所有这些工具配合使用,无需重大更改! + +### 设置 JSX + +要转译 JSX,您需要一个将它转换为有效 JavaScript 代码的 Babel 插件。我们都使用的是 [@babel/plugin-transform-react-jsx](https://babeljs.io/docs/en/babel-plugin-transform-react-jsx)。安装后,您需要指定应该使用的 JSX 函数: + +```json +{ + "plugins": [ + [ + "@babel/plugin-transform-react-jsx", + { + "pragma": "h", + "pragmaFrag": "Fragment" + } + ] + ] +} +``` + +> [Babel](https://babeljs.io/) 拥有最好的文档之一。我们强烈推荐查看它来解决有关 Babel 设置的问题。 + +### 将 React 别名指向 Preact + +在某个时候,您可能希望利用庞大的 React 生态系统。原本为 React 编写的库和组件与我们的兼容层无缝配合。要使用它,我们需要将所有 `react` 和 `react-dom` 导入指向 Preact。这个步骤称为*别名*。 + +> **注意:** 如果您使用 Vite(通过 `@preact/preset-vite`)、Preact CLI 或 WMR,这些别名默认会自动为您处理。 + +#### 在 Webpack 中设置别名 + +要在 Webpack 中为任何包设置别名,您需要在配置中添加 `resolve.alias` 部分。 +根据您使用的配置,这个部分可能已经存在,但缺少 Preact 的别名。 + +```js +const config = { + //...snip + resolve: { + alias: { + react: 'preact/compat', + 'react-dom/test-utils': 'preact/test-utils', + 'react-dom': 'preact/compat', // 必须在 test-utils 下面 + 'react/jsx-runtime': 'preact/jsx-runtime' + } + } +}; +``` + +#### 在 Node 中设置别名 + +在 Node 中运行时,打包器别名(Webpack、Rollup 等)将不起作用,就像在 NextJS 中一样。要修复这个问题,我们可以在 `package.json` 中直接使用别名: + +```json +{ + "dependencies": { + "react": "npm:@preact/compat", + "react-dom": "npm:@preact/compat" + } +} +``` + +#### 在 Parcel 中设置别名 + +Parcel 使用标准的 `package.json` 文件在 `alias` 键下读取配置选项。 + +```json +{ + "alias": { + "react": "preact/compat", + "react-dom/test-utils": "preact/test-utils", + "react-dom": "preact/compat", + "react/jsx-runtime": "preact/jsx-runtime" + } +} +``` + +#### 在 Rollup 中设置别名 + +要在 Rollup 中设置别名,您需要安装 [@rollup/plugin-alias](https://github.com/rollup/plugins/tree/master/packages/alias)。 +该插件需要放置在您的 [@rollup/plugin-node-resolve](https://github.com/rollup/plugins/tree/master/packages/node-resolve) 之前 + +```js +import alias from '@rollup/plugin-alias'; + +module.exports = { + plugins: [ + alias({ + entries: [ + { find: 'react', replacement: 'preact/compat' }, + { find: 'react-dom/test-utils', replacement: 'preact/test-utils' }, + { find: 'react-dom', replacement: 'preact/compat' }, + { find: 'react/jsx-runtime', replacement: 'preact/jsx-runtime' } + ] + }) + ] +}; +``` + +#### 在 Jest 中设置别名 + +[Jest](https://jestjs.io/) 允许重写模块路径,类似于打包器。 +这些重写使用正则表达式在您的 Jest 配置中进行配置: + +```json +{ + "moduleNameMapper": { + "^react$": "preact/compat", + "^react-dom/test-utils$": "preact/test-utils", + "^react-dom$": "preact/compat", + "^react/jsx-runtime$": "preact/jsx-runtime" + } +} +``` + +#### 在 TypeScript 中设置别名 + +TypeScript,即使与打包器一起使用,也有自己的类型解析过程。 +为了确保使用 Preact 的类型而不是 React 的类型,您需要在 `tsconfig.json`(或 `jsconfig.json`)中添加以下配置: + +```json +{ + "compilerOptions": { + ... + "skipLibCheck": true, + "baseUrl": "./", + "paths": { + "react": ["./node_modules/preact/compat/"], + "react/jsx-runtime": ["./node_modules/preact/jsx-runtime"], + "react-dom": ["./node_modules/preact/compat/"], + "react-dom/*": ["./node_modules/preact/compat/*"] + } + } +} +``` + +此外,您可能希望启用 `skipLibCheck`,就像我们在上面的示例中所做的那样。有些 +React 库使用 `preact/compat` 可能不提供的类型(尽管我们尽力修复这些),因此,这些库可能是 TypeScript 编译 +错误的来源。通过设置 `skipLibCheck`,您可以告诉 TS 它不需要对所有 +`.d.ts` 文件进行完整检查(通常这些文件仅限于 `node_modules` 中的库),这将修复这些错误。 + +#### 使用导入映射设置别名 + +```html + +``` + +另请参见[导入映射 -- 配方和常见模式](/guide/v10/no-build-workflows#recipes-and-common-patterns)以获取更多示例。 diff --git a/content/zh/guide/v11/hooks.md b/content/zh/guide/v11/hooks.md new file mode 100644 index 000000000..0775d244d --- /dev/null +++ b/content/zh/guide/v11/hooks.md @@ -0,0 +1,506 @@ +--- +title: 钩子 +description: Preact 中的钩子可让你组合行为和在不同组件中重用逻辑。 +--- + +# 钩子 + +钩子 API 是一个新概念,它可让你组合状态和副作用。利用钩子还可以在组件之间重用有状态的逻辑。 + +如果你已经使用过一段时间的 Preact,可能熟悉“渲染 prop”和“高阶组件”等解决上述难题的模式。这些解决方案使代码变得更难以理解和抽象。钩子 API 可以整洁地提取状态和副作用的逻辑,并简化了依赖此逻辑的组件的单元测试。 + +钩子可用于任何组件,并可以避免类组件依赖的 `this` 关键字的许多缺陷。钩子依赖于闭包,而不是从组件实例上访问属性。这使得它们的值是有界的,并消除了在处理异步状态时可能出现的旧数据问题。 + +有两种引入钩子的方式:`preact/hooks` 或 `preact/compat`。 + +--- + + + +--- + +## 介绍 + +理解钩子最简单的方式就是将其与等价的类组件比较。 + +我们将用一个简单的计数器组件作为示例,它渲染一个数字和一个将数字加一的按钮。 + +```jsx +// --repl +import { render, Component } from 'preact'; +// --repl-before +class Counter extends Component { + state = { + value: 0 + }; + + increment = () => { + this.setState(prev => ({ value: prev.value + 1 })); + }; + + render(props, state) { + return ( + + Counter: {state.value} + Increment + + ); + } +} +// --repl-after +render(, document.getElementById('app')); +``` + +下面这是一个使用钩子构建的等价函数式组件: + +```jsx +// --repl +import { useState, useCallback } from 'preact/hooks'; +import { render } from 'preact'; +// --repl-before +function Counter() { + const [value, setValue] = useState(0); + const increment = useCallback(() => { + setValue(value + 1); + }, [value]); + + return ( + + Counter: {value} + Increment + + ); +} +// --repl-after +render(, document.getElementById('app')); +``` + +在这一点上它们看起来很相似,但我们可以进一步简化钩子的版本。 + +让我们将计数器的逻辑提取到自定义钩子中,使其可以在不同组件中轻松重用: + +```jsx +// --repl +import { useState, useCallback } from 'preact/hooks'; +import { render } from 'preact'; +// --repl-before +function useCounter() { + const [value, setValue] = useState(0); + const increment = useCallback(() => { + setValue(value + 1); + }, [value]); + return { value, increment }; +} + +// 第一个计数器 +function CounterA() { + const { value, increment } = useCounter(); + return ( + + Counter A: {value} + Increment + + ); +} + +// 第二个渲染不同数值的计数器 +function CounterB() { + const { value, increment } = useCounter(); + return ( + + Counter B: {value} + I'm a nice counter + Increment + + ); +} +// --repl-after +render( + + + + , + document.getElementById('app') +); +``` + +注意 `CounterA` 和 `CounterB` 之间是完全独立的。它们都使用了 `useCounter()` 这个自定义钩子,但它们各自拥有钩子相关联的状态实例。 + +> 这看起来是否有点奇怪?你并不孤单! +> +> 有许多人都花了一段时间才适应这种方法。 + +## 依赖参数 + +许多钩子都接受一个用于限制钩子更新时机的参数。Preact 会检查依赖数组中的每一个并查看自上次钩子调用之后该值是否发生了变化。如果没有指定依赖参数则钩子将始终会执行。 + +In our `useCounter()` implementation above, we passed an array of dependencies to `useCallback()`: + +在上面 `useCounter()` 的实现中,我们向 `useCallback()` 传递了一个依赖数组: + +```jsx +function useCounter() { + const [value, setValue] = useState(0); + const increment = useCallback(() => { + setValue(value + 1); + }, [value]); // <-- 依赖数组 + return { value, increment }; +} +``` + +这里传递 `value` 使得每当 `value` 改变时 `useCallback` 会返回一个新的函数。 +这对于避免“旧闭包”是非常有必要的,因为回调函数将会永远引用它创建后第一次渲染时的 `value` 变量,导致 `increment` 只会把值设置为 `1`。 + +> 这使得每当 `value` 改变时创建一个新的 `increment` 回调函数。 +> 出于性能考虑,通常使用[回调](#usestate)更新状态值要比使用依赖保留当前值更好。 + +## 有状态钩子 + +此处我们将了解如何将有状态的逻辑引入函数式组件。 + +在引入钩子之前,需要状态的地方都需要类组件。 + +### useState + +这个钩子接受一个作为状态的初始值的参数。调用此钩子会返回包含两个变量的数组,第一个是当前状态,第二个是状态的设置器。 + +我们的设置器和传统状态的设置器很类似。它接受一个值或者参数是当前状态的函数。 + +当调用设置器且状态与原先不同时,就会从使用 useState 的组件开始重渲染。 + +```jsx +// --repl +import { render } from 'preact'; +// --repl-before +import { useState } from 'preact/hooks'; + +const Counter = () => { + const [count, setCount] = useState(0); + const increment = () => setCount(count + 1); + // 也可以传递一个回调给设置器 + const decrement = () => setCount(currentCount => currentCount - 1); + + return ( + + Count: {count} + Increment + Decrement + + ); +}; +// --repl-after +render(, document.getElementById('app')); +``` + +> 当初始状态比较重时,最好是传递函数给设置器而不是传递值。 + +### useReducer + +`useReducer` 钩子与 [redux](https://redux.js.org/) 很相似。比起 [useState](#usestate),当你有复杂的下一个状态取决于上一个的复杂逻辑时,它更易用。 + +```jsx +// --repl +import { render } from 'preact'; +// --repl-before +import { useReducer } from 'preact/hooks'; + +const initialState = 0; +const reducer = (state, action) => { + switch (action) { + case 'increment': + return state + 1; + case 'decrement': + return state - 1; + case 'reset': + return 0; + default: + throw new Error('Unexpected action'); + } +}; + +function Counter() { + // 返回当前状态和一个用于触发 action 的 dispatch 函数 + const [count, dispatch] = useReducer(reducer, initialState); + return ( + + {count} + dispatch('increment')}>+1 + dispatch('decrement')}>-1 + dispatch('reset')}>reset + + ); +} +// --repl-after +render(, document.getElementById('app')); +``` + +## 记忆化 + +在 UI 编程中,有时计算状态或结果成本很高。记忆化可以缓存并在输入相同时重用计算结果。 + +### useMemo + +通过 `useMemo` 钩子可以记忆计算结果并且只在它的依赖改变时重新计算。 + +```jsx +const memoized = useMemo( + () => expensive(a, b), + // 只在依赖中的任何一项改变时重新运行这个高成本的函数 + [a, b] +); +``` + +> 不要在 `useMemo` 中运行任何有作用的代码,副作用应该放在 `useEffect` 中。 + +### useCallback + +`useCallback` 钩子可用于确保依赖没有改变时返回的函数保持相同的引用。这可用于当子组件依赖引用相等性时(如 `shouldComponentUpdate`)优化子组件的更新。 + +```jsx +const onClick = useCallback(() => console.log(a, b), [a, b]); +``` + +> 有趣的事实:`useCallback(fn, deps)` 与 `useMemo(() => fn, deps)` 等价。 + +## useRef + +可以使用 `useRef` 钩子在函数式组件中获取 DOM 节点的引用。它与 [createRef](/guide/v10/refs#createref) 相似。 + +```jsx +// --repl +import { useRef } from 'preact/hooks'; +import { render } from 'preact'; +// --repl-before +function Foo() { + // 使用 `null` 初始化 useRef + const input = useRef(null); + const onClick = () => input.current && input.current.focus(); + + return ( + <> + + Focus input + > + ); +} +// --repl-after +render(, document.getElementById('app')); +``` + +> 注意不要混淆 `useRef` 和 `createRef`。 + +### useImperativeHandle + +要修改传递给子组件的 ref,我们可以使用 `useImperativeHandle` 钩子。它接受三个参数:要修改的`ref`、一个返回新 `ref` 值的执行函数,以及一个用于确定何时重新运行的依赖项数组。 + +```jsx +// --repl +import { render } from 'preact'; +import { useRef, useImperativeHandle, useState } from 'preact/hooks'; +// --repl-before +function MyInput({ inputRef }) { + const ref = useRef(null); + useImperativeHandle( + inputRef, + () => { + return { + // 仅暴露 .focus() 方法,不直接提供对 DOM 节点的访问权限 + focus() { + ref.current.focus(); + } + }; + }, + [] + ); + + return ( + + Name: + + ); +} + +function App() { + const inputRef = useRef(null); + + const handleClick = () => { + inputRef.current.focus(); + }; + + return ( + + + Click To Edit + + ); +} +// --repl-after +render(, document.getElementById('app')); +``` + +## useContext + +可以使用 `useContext` 钩子在函数式组件中访问上下文,这不需要任何高阶组件或封装。第一个参数必须是 `createContext` 创建的上下文对象。 + +```jsx +// --repl +import { render, createContext } from 'preact'; +import { useContext } from 'preact/hooks'; + +const OtherComponent = props => props.children; +// --repl-before +const Theme = createContext('light'); + +function DisplayTheme() { + const theme = useContext(Theme); + return Active theme: {theme}; +} + +// ...然后 +function App() { + return ( + + + + + + ); +} +// --repl-after +render(, document.getElementById('app')); +``` + +## 副作用 + +副作用是许多现代应用的核心。不管你是想从 API 获取数据还是在文档中触发作用,你会发现 `useEffect` 几乎满足所有需求。这正是钩子 API 的主要优势,它重塑你的思维,让你从作用思考,而不是组件的生命周期。 + +### useEffect + +顾名思义,`useEffect` 是触发各类副作用的主要方式。如果需要,你还可以返回一个用于清理作用的函数。 + +```jsx +useEffect(() => { + // 触发作用 + return () => { + // 可选项:清理用代码 + }; +}, []); +``` + +让我们以一个反映文档标题的 `Title` 组件开始,我们可以在浏览器的地址栏上看到它。 + +```jsx +function PageTitle(props) { + useEffect(() => { + document.title = props.title; + }, [props.title]); + + return {props.title}; +} +``` + +`useEffect` 的第一个参数是一个触发作用的无参回调。在这个示例中我们只想在 title 改变时触发它,当它保持不变的时候更新是没有意义的。这是使用第二个参数指定[依赖数组](#依赖参数)的原因。 + +但有时我们可能会遇到更复杂的场景。试想一个组件需要在挂载时订阅一些数据,并在卸载时取消订阅,这也可以使用 `useEffect` 做到。要执行清理代码,只需在回调中返回一个函数。 + +```jsx +// --repl +import { useState, useEffect } from 'preact/hooks'; +import { render } from 'preact'; +// --repl-before +// 组件会持续显示当前窗口宽度 +function WindowWidth(props) { + const [width, setWidth] = useState(0); + + function onResize() { + setWidth(window.innerWidth); + } + + useEffect(() => { + window.addEventListener('resize', onResize); + return () => window.removeEventListener('resize', onResize); + }, []); + + return Window width: {width}; +} +// --repl-after +render(, document.getElementById('app')); +``` + +> 清理函数是可选的。如果你无需执行清理,就不需要在 `useEffect` 的回调中返回任何东西。 + +### useLayoutEffect + +它的签名与 [useEffect](#useeffect) 等同,但会在组件执行 diff 算法和浏览器有机会绘制时触发。 + +### useErrorBoundary + +当子组件抛出错误时,你可以使用此钩子捕获错误并显示自定义的错误 UI。 + +```jsx +// error = 捕获到的错误,当没有发生错误时是 `undefined` +// resetError = 调用这个函数以标记此错误已经解决。 +// 至于这意味着什么以及是否可能从错误中恢复取决于你的应用。 +const [error, resetError] = useErrorBoundary(); +``` + +出于监控的目的,报告服务的所有错误很有用。我们可以给 `useErrorBoundary` 可选的第一个参数传递一个回调。 + +```jsx +const [error] = useErrorBoundary(error => callMyApi(error.message)); +``` + +完整的使用示例大概是这样的: + +```jsx +const App = props => { + const [error, resetError] = useErrorBoundary(error => + callMyApi(error.message) + ); + + // 显示精美的错误信息 + if (error) { + return ( + + {error.message} + Try again + + ); + } else { + return {props.children}; + } +}; +``` + +> 如果你曾使用过类组件的 API,这个钩子本质上是 [componentDidCatch](/guide/v10/whats-new/#componentdidcatch) 生命周期方法的替代。 +> 这个钩子在 Preact 10.2.0 中引入。 + +## 工具钩子 + +### useId + +这个钩子会为每个调用生成唯一的 ID,并确保[在服务端](/guide/v10/server-side-rendering))和客户端的一致性。一致 ID 的常见用例是表单,`` 元素使用 [`for`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label#attr-for)) 属性与 `` 元素关联,`useId` 钩子并不局限于表单,它可以在任何你需要唯一 ID 的时候使用。 + +> 要保证钩子一致,你需要同时在服务端和客户端使用 Preact。 + +完整的使用示例大概是这样的: + +```jsx +const App = props => { + const mainId = useId(); + const inputId = useId(); + + useLayoutEffect(() => { + document.getElementById(inputId).focus() + }, []) + + // 显示精美的错误信息。 + return ( + + + + ) +}; +``` + +> 这个钩子在 Preact 10.11.0 中引入,且需要 preact-render-to-string 5.2.4。 diff --git a/content/zh/guide/v11/no-build-workflows.md b/content/zh/guide/v11/no-build-workflows.md new file mode 100644 index 000000000..50bdbbdbd --- /dev/null +++ b/content/zh/guide/v11/no-build-workflows.md @@ -0,0 +1,143 @@ +--- +title: 无构建工具工作流 +description: 尽管 Webpack、Rollup 和 Vite 等构建工具功能强大且实用,但 Preact 完全支持在不使用这些工具的情况下构建应用。 +--- + +# 无构建工具工作流 + +尽管 Webpack、Rollup 和 Vite 等构建工具功能强大且实用,但 Preact 完全支持在不使用这些工具的情况下构建应用。 + +无构建工具工作流是一种在放弃构建工具的情况下开发 Web 应用的方式,转而依赖浏览器来实现模块加载和执行。这是开始使用 Preact 的绝佳方式,并且无论项目规模大小,都能持续高效运作。 + +--- + + + +--- + +## 导入映射(Import Maps) + +[导入映射](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/script/type/importmap) 是一项较新的浏览器特性,允许您控制浏览器如何解析模块标识符,通常用于将裸模块标识符(如 `preact`)转换为 CDN URL(如 `https://esm.sh/preact`)。尽管许多人更倾向于导入映射带来的美观性,但依赖集中化也有客观优势,例如更简便的版本管理、减少/消除重复依赖,以及更好地利用更强大的 CDN 功能。 + +对于选择放弃构建工具的开发者,我们通常推荐使用导入映射,因为它们能解决在导入标识符中使用裸 CDN URL 时可能遇到的一些问题(下文将详述)。 + +### 基础用法 + +[MDN](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/script/type/importmap) 提供了大量关于如何使用导入映射的信息,以下是一个基础示例: + +```html + + + + + + + + + + + +``` + +我们创建一个带有 `type="importmap"` 属性的 ` +``` + +#### 将 React 别名指向 Preact + +```html + +``` + +## HTM + +虽然 JSX 通常是编写 Preact 应用最流行的方式,但其需要构建步骤将非标准语法转换为浏览器及其他运行时原生可理解的代码。手动编写 `h`/`createElement` 调用可能较为繁琐且不够符合人体工学,因此我们推荐使用名为 [HTM](https://github.com/developit/htm) 的 JSX 替代方案。 + +HTM 无需构建步骤(尽管它可以使用,参见 [`babel-plugin-htm`](https://github.com/developit/htm/tree/master/packages/babel-plugin-htm)),而是利用自 2015 年起存在并受所有现代浏览器支持的 [标签模板](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Template_literals#%E6%A0%87%E7%AD%BE%E6%A8%A1%E6%9D%BF) 语法。这成为日益流行的 Preact 应用编写方式,尤其受放弃构建步骤的开发者青睐。 + +HTM 支持所有标准 Preact 功能,包括组件、Hooks、Signals 等,唯一区别在于编写 "JSX" 返回值的语法。 + +```js +// --repl +import { render } from 'preact'; +// --repl-before +import { useState } from 'preact/hooks'; +import { html } from 'htm/preact'; + +function Button({ action, children }) { + return html` + ${children} + `; +} + +function Counter() { + const [count, setCount] = useState(0); + + return html` + + <${Button} action=${() => setCount(count + 1)}>增加/> + + <${Button} action=${() => setCount(count - 1)}>减少/> + + `; +} +// --repl-after +render(, document.getElementById('app')); +``` diff --git a/content/zh/guide/v11/options.md b/content/zh/guide/v11/options.md new file mode 100644 index 000000000..368a8061c --- /dev/null +++ b/content/zh/guide/v11/options.md @@ -0,0 +1,93 @@ +--- +title: 选项钩子 +description: Preact 提供可附加到对比树差异过程的选项钩子。 +--- + +# 选项钩子 + +为修改 Preact 渲染流程的插件提供的回调函数。 + +Preact 支持多种观察或修改渲染流程的回调函数,即“选项钩子” (请勿与[钩子](/guide/v10/hooks)混淆)。这些函数常用于扩展 Preact 功能和打造专门的测试工具。我们的附加组件 (如 `preact/hooks` 和 `preact/compat`) 和开发工具扩展都基于此功能。 + +此 API 主要为扩展 Preact 功能的工具或库作者打造。 + +--- + + + +--- + +## 版本支持 + +Preact 自带选项钩子,以语义版本控制。但选项钩子的弃用周期与其他库不同,所以当在改变 API 的主要版本更新时,我们不会在其发行前宣布延长公告期。这同样适用于使用选项钩子的内部 API,如 `VNode` 对象。 + +## 使用选项钩子 + +您可以通过修改 Preact 导出的 `options` 对象来设置选项钩子。 + +当您在写钩子函数时,请确保先调用先前存在的相同名称钩子。否则,调用链中依赖其他钩子的功能将会失效,更可能导致 `preact/hooks` 和开发工具无法正常工作。请同时务必确保您的钩子函数匹配原钩子的方法签名——除非您有特别原因需要修改。 + +```js +import { options } from 'preact'; + +// 备份原钩子函数 +const oldHook = options.vnode; + +// 设置您自己的钩子函数 +options.vnode = vnode => { + console.log("Hey I'm a vnode", vnode); + + // 调用备份的钩子函数 (如果有) + if (oldHook) { + oldHook(vnode); + } +}; +``` + +除了 `options.event` 之外,其他钩子均无返回值,所以您无需为大部分钩子处理返回值。 + +## 选项钩子列表 + +#### `options.vnode` + +**函数签名:** `(vnode: VNode) => void` + +该选项钩子会在创建 VNode 后触发。VNode 是 Preact 的中虚拟 DOM 元素节点,又名 “JSX 元素”。 + +#### `options.unmount` + +**函数签名:** `(vnode: VNode) => void` + +在虚拟 DOM 节点取消联结前,DOM 节点仍存在于树上时调用。 + +#### `options.diffed` + +**函数签名:** `(vnode: VNode) => void` + +虚拟 DOM 节点渲染后,其 DOM 表示已构建完毕或已转化为正确状态时调用。 + +#### `options.event` + +**函数签名:** `(event: Event) => any` + +虚拟 DOM 监听器处理 DOM 事件前调用。设置 `options.event` 后,事件监听函数的传入事件将替换为 `options.event` 的返回值。 + +#### `options.requestAnimationFrame` + +**函数签名:** `(callback: () => void) => void` + +用于控制效果及 `preact/hooks` 的效果功能调度。 + +#### `options.debounceRendering` + +**函数签名:** `(callback: () => void) => void` + +全局组件渲染队列中用于批量定时延后渲染更新的函数。 + +默认情况下,Preact 使用 `Promise.resolve()` 的微任务计时。若 Promise 不可用,则使用 `setTimeout`。 + +#### `options.useDebugValue` + +**函数签名:** `(value: string | number) => void` + +在 `preact/hooks` 的 `useDebugValue` 被调用时调用。 diff --git a/content/zh/guide/v11/preact-iso.md b/content/zh/guide/v11/preact-iso.md new file mode 100644 index 000000000..0a935c61c --- /dev/null +++ b/content/zh/guide/v11/preact-iso.md @@ -0,0 +1,424 @@ +--- +title: preact-iso +description: preact-iso is a collection of isomorphic async tools for Preact +--- + +# preact-iso + +preact-iso 是 Preact 的同构异步工具集合。 + +"同构"描述的是可以在浏览器和服务器上(理想情况下是无缝地)运行的代码。`preact-iso` 专为支持这些环境而设计,允许用户构建应用程序,而无需创建单独的浏览器和服务器路由器,或担心数据或组件加载的差异。相同的应用程序代码可以在浏览器和预渲染期间的服务器上使用,无需调整。 + +> **注意:** 虽然这是一个来自 Preact 团队的路由库,但在更广泛的 Preact/React 生态系统中还有许多其他路由器可供使用,包括 [wouter](https://github.com/molefrog/wouter) 和 [react-router](https://reactrouter.com/)。它是一个很好的首选,但如果您愿意,可以将您喜欢的路由器带到 Preact。 + +--- + + + +--- + +## 路由 + +`preact-iso` 为 Preact 提供了一个简单的路由器,具有传统和基于 hooks 的 API。`` 组件是异步感知的:当从一个路由转换到另一个路由时,如果传入的路由暂停(抛出 Promise),则保留传出的路由,直到新路由准备就绪。 + +```jsx +import { + lazy, + LocationProvider, + ErrorBoundary, + Router, + Route +} from 'preact-iso'; + +// 同步 +import Home from './routes/home.js'; + +// 异步(抛出 promise) +const Profiles = lazy(() => import('./routes/profiles.js')); +const Profile = lazy(() => import('./routes/profile.js')); +const NotFound = lazy(() => import('./routes/_404.js')); + +function App() { + return ( + + + + + {/* 替代专用路由组件,以获得更好的 TS 支持 */} + + + {/* `default` 属性表示一个后备路由。对 404 页面很有用 */} + + + + + ); +} +``` + +**渐进式水合:** 当应用程序在客户端上水合时,路由(在本例中为 `Home` 或 `Profile`)暂停。这导致该页面部分的水合被推迟,直到路由的 `import()` 解析完成,此时页面的该部分自动完成水合。 + +**无缝路由:** 在客户端切换路由时,路由器了解路由中的异步依赖。路由器不会清除当前路由并在等待下一个路由时显示加载动画,而是保留当前路由,直到传入的路由完成加载,然后它们被交换。 + +## 预渲染 + +`prerender()` 使用 [`preact-render-to-string`](https://github.com/preactjs/preact-render-to-string) 将虚拟 DOM 树渲染为 HTML 字符串。从 `prerender()` 返回的 Promise 解析为一个包含 `html` 和 `links[]` 属性的对象。`html` 属性包含您预渲染的静态 HTML 标记,`links` 是在生成的页面上找到的任何非外部 URL 字符串的数组。 + +主要用于通过 [`@preact/preset-vite`](https://github.com/preactjs/preset-vite#prerendering-configuration) 或其他共享 API 的预渲染系统进行预渲染。如果您通过任何其他方法进行服务器端渲染您的应用程序,可以直接使用 `preact-render-to-string`(特别是 `renderToStringAsync()`)。 + +```jsx +import { + LocationProvider, + ErrorBoundary, + Router, + lazy, + prerender as ssr +} from 'preact-iso'; + +// 异步(抛出 promise) +const Foo = lazy(() => import('./foo.js')); + +function App() { + return ( + + + + + + + + ); +} + +hydrate(); + +export async function prerender(data) { + return await ssr(); +} +``` + +## 嵌套路由 + +通过使用多个 `Router` 组件支持嵌套路由。部分匹配的路由以通配符(`/*`)结尾,剩余的值将被传递以继续匹配,如果有任何进一步的路由。 + +```jsx +import { + lazy, + LocationProvider, + ErrorBoundary, + Router, + Route +} from 'preact-iso'; + +const NotFound = lazy(() => import('./routes/_404.js')); + +function App() { + return ( + + + + + + + + + ); +} + +const TrendingMovies = lazy(() => import('./routes/movies/trending.js')); +const SearchMovies = lazy(() => import('./routes/movies/search.js')); +const MovieDetails = lazy(() => import('./routes/movies/details.js')); + +function Movies() { + return ( + + + + + + + + ); +} +``` + +这将匹配以下路由: + +- `/movies/trending` +- `/movies/search` +- `/movies/Inception` + +--- + +## API 文档 + +### LocationProvider + +一个上下文提供者,向其子组件提供当前位置。路由器功能需要这个。 + +属性: + +- `scope?: string | RegExp` - 设置路由器将处理(拦截)的路径范围。如果路径不匹配范围,无论是以提供的字符串开头还是匹配正则表达式,路由器都会忽略它,并应用默认的浏览器导航。 + +通常,您会将整个应用程序包装在这个提供者中: + +```jsx +import { LocationProvider } from 'preact-iso'; + +function App() { + return ( + {/* 您的应用程序在这里 */} + ); +} +``` + +### Router + +属性: + +- `onRouteChange?: (url: string) => void` - 路由更改时要调用的回调。 +- `onLoadStart?: (url: string) => void` - 路由开始加载时要调用的回调(即,如果它暂停)。这不会在导航到同步路由之前或随后导航到异步路由之前被调用。 +- `onLoadEnd?: (url: string) => void` - 路由完成加载后要调用的回调(即,如果它暂停)。这不会在导航到同步路由之后或随后导航到异步路由之后被调用。 + +```jsx +import { LocationProvider, Router } from 'preact-iso'; + +function App() { + return ( + + console.log('路由更改为', url)} + onLoadStart={url => console.log('开始加载', url)} + onLoadEnd={url => console.log('完成加载', url)} + > + + + + + + ); +} +``` + +### Route + +使用 `preact-iso` 定义路由有两种方式: + +1. 直接将路由参数附加到路由组件:`` +2. 使用 `Route` 组件代替:`` + +在 JavaScript 中,将任意属性附加到组件上并不是不合理的,因为 JS 是一种动态语言,完全支持动态和任意接口。然而,TypeScript(我们许多人甚至在编写 JS 时也通过 TS 的语言服务器使用)对这种接口设计并不完全支持。 + +TS 尚不允许从父组件覆盖子组件的属性,因此我们不能,例如,将 `` 定义为不接受任何属性,除非它是 `` 的子组件,在这种情况下,它可以有一个 `path` 属性。这给我们带来了一个困境:要么我们将所有路由定义为接受 `path` 属性,以便在编写 `` 时不会看到 TS 错误,要么我们创建包装组件来处理路由定义。 + +虽然 `` 完全等同于 ``,但 TS 用户可能会发现后者更可取。 + +```jsx +import { LocationProvider, Router, Route } from 'preact-iso'; + +function App() { + return ( + + + {/* 这两个是等价的 */} + + + + + + + + + ); +} +``` + +任何路由组件的属性: + +- `path: string` - 要匹配的路径(继续阅读) +- `default?: boolean` - 如果设置,此路由是一个后备/默认路由,在没有其他匹配时使用 + +特定于 `Route` 组件: + +- `component: AnyComponent` - 路由匹配时要渲染的组件 + +#### 路径段匹配 + +路径使用简单的字符串匹配算法进行匹配。可以使用以下功能: + +- `:param` - 匹配任何 URL 段,将值绑定到标签(以后可以从 `useRoute()` 提取此值) + - `/profile/:id` 将匹配 `/profile/123` 和 `/profile/abc` + - `/profile/:id?` 将匹配 `/profile` 和 `/profile/123` + - `/profile/:id*` 将匹配 `/profile`、`/profile/123` 和 `/profile/123/abc` + - `/profile/:id+` 将匹配 `/profile/123`、`/profile/123/abc` +- `*` - 匹配一个或多个 URL 段 + - `/profile/*` 将匹配 `/profile/123`、`/profile/123/abc` 等。 + +这些可以组合起来创建更复杂的路由: + +- `/profile/:id/*` 将匹配 `/profile/123/abc`、`/profile/123/abc/def` 等。 + +`/:id*` 和 `/:id/*` 的区别在于,前者中,`id` 参数将包含之后的整个路径,而后者中,`id` 只是单个路径段。 + +- `/profile/:id*`,使用 `/profile/123/abc` + - `id` 是 `123/abc` +- `/profile/:id/*`,使用 `/profile/123/abc` + - `id` 是 `123` + +### useLocation() + +一个与 `LocationProvider` 配合使用的钩子,用于访问位置上下文。 + +返回一个具有以下属性的对象: + +- `url: string` - 当前路径和搜索参数 +- `path: string` - 当前路径 +- `query: Record` - 当前查询字符串参数(`/profile?name=John` -> `{ name: 'John' }`) +- `route: (url: string, replace?: boolean) => void` - 一个以编程方式导航到新路由的函数。`replace` 参数可以选择性地用于覆盖历史记录,导航它们离开而不保留当前位置在历史堆栈中。 + +### useRoute() + +一个访问当前路由信息的钩子。与 `useLocation` 不同,此钩子仅在 `` 组件内部工作。 + +返回一个具有以下属性的对象: + +- `path: string` - 当前路径 +- `query: Record` - 当前查询字符串参数(`/profile?name=John` -> `{ name: 'John' }`) +- `params: Record` - 当前路由参数(`/profile/:id` -> `{ id: '123' }`) + +### lazy() + +创建组件的懒加载版本。 + +`lazy()` 接受一个解析为组件的异步函数,并返回该组件的包装版本。包装组件可以立即渲染,即使组件只在第一次渲染时加载。 + +```jsx +import { lazy, LocationProvider, Router } from 'preact-iso'; + +// 同步,不代码分割: +import Home from './routes/home.js'; + +// 异步,代码分割: +const Profiles = lazy(() => + import('./routes/profiles.js').then(m => m.Profiles) +); // 期望有一个名为 `Profiles` 的命名导出 +const Profile = lazy(() => import('./routes/profile.js')); // 期望有一个默认导出 + +function App() { + return ( + + + + + + + + ); +} +``` + +`lazy()` 的结果还公开了一个 `preload()` 方法,可用于在需要渲染之前加载组件。完全可选,但在聚焦、鼠标悬停等情况下可能有用,以比原本更早地开始加载组件。 + +```jsx +const Profile = lazy(() => import('./routes/profile.js')); + +function Home() { + return ( + Profile.preload()}> + 个人资料页面 -- 将鼠标悬停在我上面预加载模块! + + ); +} +``` + +### ErrorBoundary + +一个简单的组件,用于捕获其下方组件树中的错误。 + +属性: + +- `onError?: (error: Error) => void` - 捕获错误时要调用的回调 + +```jsx +import { LocationProvider, ErrorBoundary, Router } from 'preact-iso'; + +function App() { + return ( + + console.log(e)}> + + + + + + + + ); +} +``` + +### hydrate() + +Preact 的 `hydrate` 导出的薄包装器,它根据当前页面是否已预渲染,在水合和渲染提供的元素之间切换。此外,它会检查以确保它在尝试任何渲染之前在浏览器上下文中运行,使其在 SSR 期间成为无操作。 + +与 `prerender()` 函数配对。 + +参数: + +- `jsx: ComponentChild` - 要渲染的 JSX 元素或组件 +- `parent?: Element | Document | ShadowRoot | DocumentFragment` - 要渲染到的父元素。如果未提供,默认为 `document.body`。 + +```jsx +import { hydrate } from 'preact-iso'; + +function App() { + return ( + + Hello World + + ); +} + +hydrate(); +``` + +然而,它只是一个简单的实用方法。绝不是必须使用的,您始终可以直接使用 Preact 的 `hydrate` 导出。 + +### prerender() + +使用 `preact-render-to-string` 将虚拟 DOM 树渲染为 HTML 字符串。从 `prerender()` 返回的 Promise 解析为包含 `html` 和 `links[]` 属性的对象。`html` 属性包含您预渲染的静态 HTML 标记,`links` 是在生成的页面上找到的任何非外部 URL 字符串的数组。 + +主要与 [`@preact/preset-vite`](https://github.com/preactjs/preset-vite#prerendering-configuration) 的预渲染配对。 + +参数: + +- `jsx: ComponentChild` - 要渲染的 JSX 元素或组件 + +```jsx +import { + LocationProvider, + ErrorBoundary, + Router, + lazy, + prerender +} from 'preact-iso'; + +// 异步(抛出 promise) +const Foo = lazy(() => import('./foo.js')); +const Bar = lazy(() => import('./bar.js')); + +function App() { + return ( + + + + + + + + + ); +} + +const { html, links } = await prerender(); +``` diff --git a/content/zh/guide/v11/preact-testing-library.md b/content/zh/guide/v11/preact-testing-library.md new file mode 100644 index 000000000..9674945c7 --- /dev/null +++ b/content/zh/guide/v11/preact-testing-library.md @@ -0,0 +1,253 @@ +--- +title: 通过preact 测试库测试 +description: Testing Preact applications made easy with testing-library +--- + +# 使用 Preact Testing Library 进行测试 + +[Preact Testing Library](https://github.com/testing-library/preact-testing-library) 是 `preact/test-utils` 的一个轻量级包装器。它提供了一组查询方法,用于以类似用户在页面上查找元素的方式访问渲染的 DOM。这种方法允许您编写不依赖于实现细节的测试。因此,当被测试的组件进行重构时,这使得测试更容易维护且更具弹性。 + +与 [Enzyme](/guide/v10/unit-testing-with-enzyme) 不同,Preact Testing Library 必须在 DOM 环境中调用。 + +--- + + + +--- + +## 安装 + +通过以下命令安装 testing-library 的 Preact 适配器: + +```bash +npm install --save-dev @testing-library/preact +``` + +> 注意:该库依赖于存在的 DOM 环境。如果您使用 [Jest](https://github.com/facebook/jest),它已经包含并默认启用。如果您使用其他测试运行器,如 [Mocha](https://github.com/mochajs/mocha) 或 [Jasmine](https://github.com/jasmine/jasmine),您可以通过安装 [jsdom](https://github.com/jsdom/jsdom) 为 node 添加 DOM 环境。 + +## 使用 + +假设我们有一个 `Counter` 组件,它显示一个初始值,并带有一个更新它的按钮: + +```jsx +import { h } from 'preact'; +import { useState } from 'preact/hooks'; + +export function Counter({ initialCount }) { + const [count, setCount] = useState(initialCount); + const increment = () => setCount(count + 1); + + return ( + + 当前值: {count} + 增加 + + ); +} +``` + +我们要验证我们的 Counter 显示初始计数,并且点击按钮将增加它。使用您选择的测试运行器,如 [Jest](https://github.com/facebook/jest) 或 [Mocha](https://github.com/mochajs/mocha),我们可以编写这两个场景: + +```jsx +import { expect } from 'expect'; +import { h } from 'preact'; +import { render, fireEvent, screen, waitFor } from '@testing-library/preact'; + +import Counter from '../src/Counter'; + +describe('Counter', () => { + test('应该显示初始计数', () => { + const { container } = render(); + expect(container.textContent).toMatch('当前值: 5'); + }); + + test('点击"增加"按钮后应该增加', async () => { + render(); + + fireEvent.click(screen.getByText('增加')); + await waitFor(() => { + // .toBeInTheDocument() 是来自 jest-dom 的断言。 + // 否则您可以使用 .toBeDefined()。 + expect(screen.getByText('当前值: 6')).toBeInTheDocument(); + }); + }); +}); +``` + +您可能已经注意到了 `waitFor()` 调用。我们需要这个来确保 Preact 有足够的时间渲染到 DOM 并刷新所有待处理的效果。 + +```jsx +test('应该增加计数器", async () => { + render(); + + fireEvent.click(screen.getByText('增加')); + // 错误:Preact 可能还没有完成渲染 + expect(screen.getByText("当前值: 6")).toBeInTheDocument(); +}); +``` + +在底层,`waitFor` 重复调用传递的回调函数,直到它不再抛出错误或超时(默认:1000ms)。在上面的例子中,我们知道更新完成是在计数器增加并且新值被渲染到 DOM 中时。 + +我们也可以使用 "findBy" 版本的查询而不是 "getBy" 来以异步优先的方式编写测试。异步查询在底层使用 `waitFor` 重试,并返回 Promise,所以您需要等待它们。 + +```jsx +test('应该增加计数器", async () => { + render(); + + fireEvent.click(screen.getByText('增加')); + + await screen.findByText('当前值: 6'); // 等待更改的元素 + + expect(screen.getByText("当前值: 6")).toBeInTheDocument(); // 通过 +}); +``` + +## 查找元素 + +有了完整的 DOM 环境,我们可以直接验证我们的 DOM 节点。通常,测试会检查存在的属性,如输入值,或者元素出现/消失。为此,我们需要能够在 DOM 中定位元素。 + +### 使用内容 + +Testing Library 的理念是"您的测试越像您的软件被使用的方式,它们能给您的信心就越大"。 + +与页面交互的推荐方式是通过文本内容以用户的方式查找元素。 + +您可以在 Testing Library 文档的 ['应该使用哪个查询'](https://testing-library.com/docs/guide-which-query) 页面上找到选择正确查询的指南。最简单的查询是 `getByText`,它查看元素的 `textContent`。还有针对标签文本、占位符、标题属性等的查询。`getByRole` 查询最强大,因为它抽象了 DOM,并允许您在可访问性树中查找元素,这是屏幕阅读器读取页面的方式。结合 [`role`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques) 和 [`accessible name`](https://www.w3.org/TR/accname-1.1/#mapping_additional_nd_name) 在单个查询中涵盖了许多常见的 DOM 遍历。 + +```jsx +import { render, fireEvent, screen } from '@testing-library/preact'; + +test('应该能够登录', async () => { + render(); + + // 使用文本框角色和可访问名称定位输入框, + // 无论您使用标签元素、aria-label 还是 + // aria-labelledby 关系,它都是稳定的 + const field = await screen.findByRole('textbox', { name: '登录' }); + + // 在字段中输入 + fireEvent.change(field, { value: 'user123' }); +}); +``` + +有时,当内容变化很大,或者如果您使用将文本翻译成不同语言的国际化框架时,直接使用文本内容会造成摩擦。您可以通过将文本视为可快照的数据来解决这个问题,使其易于更新,但将真相源保持在测试之外。 + +```jsx +test('应该能够登录', async () => { + render(); + + // 如果我们以另一种语言渲染应用,或者更改文本呢?测试失败。 + const field = await screen.findByRole('textbox', { name: '登录' }); + fireEvent.change(field, { value: 'user123' }); +}); +``` + +即使您不使用翻译框架,您也可以将字符串保存在单独的文件中,并使用与下面示例相同的策略: + +```jsx +test('应该能够登录', async () => { + render(); + + // 我们可以在测试中直接使用我们的翻译函数 + const label = translate('signinpage.label', 'zh-CN'); + // 快照结果,这样我们知道发生了什么 + expect(label).toMatchInlineSnapshot(`登录`); + + const field = await screen.findByRole('textbox', { name: label }); + fireEvent.change(field, { value: 'user123' }); +}); +``` + +### 使用测试 ID + +测试 ID 是添加到 DOM 元素的数据属性,用于在选择内容模糊或不可预测的情况下提供帮助,或者与实现细节解耦,如 DOM 结构。当其他查找元素的方法都不合适时,可以使用它们。 + +```jsx +function Foo({ onClick }) { + return ( + + 点击此处 + + ); +} + +// 仅在文本保持不变时有效 +fireEvent.click(screen.getByText('点击此处')); + +// 即使我们更改文本也有效 +fireEvent.click(screen.getByTestId('foo')); +``` + +## 调试测试 + +要调试当前的 DOM 状态,您可以使用 `debug()` 函数打印出一个美化版本的 DOM。 + +```jsx +const { debug } = render(); + +// 打印出一个美化版本的 DOM +debug(); +``` + +## 提供自定义上下文提供者 + +您经常会遇到依赖共享上下文状态的组件。常见的提供者通常从路由器、状态,有时是主题和其他特定于您的应用的全局提供者。对于每个测试用例重复设置这些可能变得繁琐,因此我们建议通过包装 `@testing-library/preact` 中的那个来创建自定义 `render` 函数。 + +```jsx +// helpers.js +import { render as originalRender } from '@testing-library/preact'; +import { createMemoryHistory } from 'history'; +import { FooContext } from './foo'; + +const history = createMemoryHistory(); + +export function render(vnode) { + return originalRender( + + {vnode} + + ); +} + +// 像往常一样使用。看,没有提供者! +render(); +``` + +## 测试 Preact Hooks + +使用 `@testing-library/preact`,我们还可以测试我们 hook 的实现! +想象一下,我们希望为多个组件重用计数器功能(我知道我们喜欢计数器!)并将其提取到一个 hook 中。现在我们想测试它。 + +```jsx +import { useState, useCallback } from 'preact/hooks'; + +const useCounter = () => { + const [count, setCount] = useState(0); + const increment = useCallback(() => setCount(c => c + 1), []); + return { count, increment }; +}; +``` + +与之前一样,背后的方法类似:我们想要验证我们可以增加我们的计数器。所以我们需要以某种方式调用我们的 hook。这可以通过 `renderHook()` 函数完成,它在内部自动创建一个包围组件。该函数在 `result.current` 下返回当前 hook 返回值,我们可以用它来进行验证: + +```jsx +import { renderHook, act } from '@testing-library/preact'; +import useCounter from './useCounter'; + +test('应该增加计数器', () => { + const { result } = renderHook(() => useCounter()); + + // 最初计数器应该是 0 + expect(result.current.count).toBe(0); + + // 让我们通过调用 hook 回调来更新计数器 + act(() => { + result.current.increment(); + }); + + // 检查 hook 返回值是否反映了新状态。 + expect(result.current.count).toBe(1); +}); +``` + +有关 `@testing-library/preact` 的更多信息,请查看 https://github.com/testing-library/preact-testing-library 。 diff --git a/content/zh/guide/v11/refs.md b/content/zh/guide/v11/refs.md new file mode 100644 index 000000000..c16bddeca --- /dev/null +++ b/content/zh/guide/v11/refs.md @@ -0,0 +1,232 @@ +--- +title: 引用 +description: 引用(Refs)是一种创建组件实例本地稳定值的方式,这些值可以在渲染过程中保持不变。 +--- + +# 引用 + +引用(References),简称 refs,是稳定的、局部的值,它们在组件渲染过程中保持不变,但当它们改变时不会像状态(state)或属性(props)那样触发重新渲染。 + +最常见的是,您会看到 refs 用于促进对 DOM 的命令式操作,但它们也可以用于存储您需要保持稳定的任何任意本地值。您可以使用它们来跟踪之前的状态值,保持对间隔或超时 ID 的引用,或者简单地作为计数器值。重要的是,refs 不应该用于渲染逻辑,而只应该在生命周期方法和事件处理程序中使用。 + +--- + + + +--- + +## 创建引用 + +在 Preact 中创建 refs 有两种方式,取决于您喜欢的组件风格:`createRef`(类组件)和`useRef`(函数组件/钩子)。这两个 API 的基本工作方式相同:它们创建一个具有`current`属性的稳定的普通对象,可以选择性地初始化为一个值。 + + + +```jsx +import { createRef } from 'preact'; + +class MyComponent extends Component { + countRef = createRef(); + inputRef = createRef(null); + + // ... +} +``` + +```jsx +import { useRef } from 'preact/hooks'; + +function MyComponent() { + const countRef = useRef(); + const inputRef = useRef(null); + + // ... +} +``` + + + +## 使用引用访问 DOM 节点 + +refs 最常见的用例是访问组件的底层 DOM 节点。这对于命令式 DOM 操作很有用,例如测量元素、调用各种元素上的原生方法(如`.focus()`或`.play()`),以及与用原生 JS 编写的第三方库集成。在以下示例中,在渲染后,Preact 将把 DOM 节点分配给 ref 对象的`current`属性,使其在组件挂载后可用。 + + + +```jsx +// --repl +import { render, Component, createRef } from 'preact'; +// --repl-before +class MyInput extends Component { + ref = createRef(null); + + componentDidMount() { + console.log(this.ref.current); + // 输出: [HTMLInputElement] + } + + render() { + return ; + } +} +// --repl-after +render(, document.getElementById('app')); +``` + +```jsx +// --repl +import { render } from 'preact'; +import { useRef, useEffect } from 'preact/hooks'; +// --repl-before +function MyInput() { + const ref = useRef(null); + + useEffect(() => { + console.log(ref.current); + // 输出: [HTMLInputElement] + }, []); + + return ; +} +// --repl-after +render(, document.getElementById('app')); +``` + + + +### 回调引用 + +使用引用的另一种方式是将函数传递给`ref`属性,其中 DOM 节点将作为参数传递。 + + + +```jsx +// --repl +import { render, Component } from 'preact'; +// --repl-before +class MyInput extends Component { + render() { + return ( + { + console.log('已挂载:', dom); + + // 从Preact 10.23.0开始,您可以选择返回一个清理函数 + return () => { + console.log('已卸载:', dom); + }; + }} + /> + ); + } +} +// --repl-after +render(, document.getElementById('app')); +``` + +```jsx +// --repl +import { render } from 'preact'; +// --repl-before +function MyInput() { + return ( + { + console.log('已挂载:', dom); + + // 从Preact 10.23.0开始,您可以选择返回一个清理函数 + return () => { + console.log('已卸载:', dom); + }; + }} + /> + ); +} +// --repl-after +render(, document.getElementById('app')); +``` + + + +> 如果提供的 ref 回调不稳定(例如上面所示的内联定义的回调),并且*没有*返回清理函数,则在所有重新渲染时**它将被调用两次**:一次传入`null`,然后一次传入实际引用。这是一个常见问题,`createRef`/`useRef` API 通过强制用户检查`ref.current`是否已定义,使这个问题变得更容易处理。 +> +> 相比之下,稳定的函数可以是类组件实例上的方法、组件外部定义的函数,或者例如使用`useCallback`创建的函数。 + +## 使用引用存储本地值 + +然而,refs 并不限于存储 DOM 节点;它们可以用于存储您可能需要的任何类型的值。 + +在下面的例子中,我们将一个间隔的 ID 存储在 ref 中,以便能够独立地启动和停止它。 + + + +```jsx +// --repl +import { render, Component, createRef } from 'preact'; +// --repl-before +class SimpleClock extends Component { + state = { + time: Date.now() + }; + intervalId = createRef(null); + + startClock = () => { + this.setState({ time: Date.now() }); + this.intervalId.current = setInterval(() => { + this.setState({ time: Date.now() }); + }, 1000); + }; + + stopClock = () => { + clearInterval(this.intervalId.current); + }; + + render(_, { time }) { + const formattedTime = new Date(time).toLocaleTimeString(); + + return ( + + 启动时钟 + {formattedTime} + 停止时钟 + + ); + } +} +// --repl-after +render(, document.getElementById('app')); +``` + +```jsx +// --repl +import { render } from 'preact'; +import { useState, useRef } from 'preact/hooks'; +// --repl-before +function SimpleClock() { + const [time, setTime] = useState(Date.now()); + const intervalId = useRef(null); + + const startClock = () => { + setTime(Date.now()); + intervalId.current = setInterval(() => { + setTime(Date.now()); + }, 1000); + }; + + const stopClock = () => { + clearInterval(intervalId.current); + }; + + const formattedTime = new Date(time).toLocaleTimeString(); + + return ( + + 启动时钟 + {formattedTime} + 停止时钟 + + ); +} +// --repl-after +render(, document.getElementById('app')); +``` + + diff --git a/content/zh/guide/v11/server-side-rendering.md b/content/zh/guide/v11/server-side-rendering.md new file mode 100644 index 000000000..e61e0b112 --- /dev/null +++ b/content/zh/guide/v11/server-side-rendering.md @@ -0,0 +1,142 @@ +--- +title: 服务端渲染 +description: 通过服务端渲染来向用户快速呈现您的 Preact 应用。 +--- + +# 服务端渲染 + +服务端渲染 (Server-Side Rendering, 或简称为 “SSR”) 将应用先渲染成 HTML 再发送给客户端以加快加载时间。除此之外,服务端渲染还能在测试中大显身手。 + +--- + + + +--- + +## 安装 + +Preact 的服务端渲染程序有一个[独立仓库](https://github.com/preactjs/preact-render-to-string/),您可以使用您偏好的包管理器安装: + +```bash +npm install -S preact-render-to-string +``` + +上述命令执行完毕后,您可以直接使用。我们可以通过下面的代码解释其所有的 API: + +## 基本用法 + +基本功能可以通过一个简单的代码片段来最好地解释: + +```jsx +import render from 'preact-render-to-string'; +import { h } from 'preact'; + +const App = 内容; + +console.log(render(App)); +// 内容 +``` + +## 使用 `Suspense` & `lazy` 进行异步渲染 + +你可能会发现自己需要渲染动态加载的组件,比如在使用 `Suspense` 和 `lazy` 来实现代码分割时(以及其他一些用例)。异步渲染器会等待 `Promise` 解析完成,从而让你能够完整地构建 `HTML` 字符串: + +```jsx +// page/home.js +export default () => { + return Home page; +}; +``` + +```jsx +// main.js +import { Suspense, lazy } from 'preact/compat'; + +// Creation of the lazy component +const HomePage = lazy(() => import('./pages/home')); + +const Main = () => { + return ( + Loading}> + + + ); +}; +``` + +上述内容是使用代码分割的 Preact 应用程序的典型设置,无需进行任何更改即可使用服务器端渲染。 + +要渲染此应用程序,我们需要略微偏离基本用法示例,并使用 `renderToStringAsync` 导出来渲染我们的应用程序: + +```jsx +import { renderToStringAsync } from 'preact-render-to-string'; +import { Main } from './main'; + +const main = async () => { + // Rendering of lazy components + const html = await renderToStringAsync(); + + console.log(html); + // Home page +}; + +// Execution & error handling +main().catch(error => { + console.error(error); +}); +``` + +## 浅层渲染 (Shallow Rendering) + +有些时候,您不需要渲染整个元素树。为此,您可以使用浅层渲染程序来输出子元素名称,而非其返回值。 + +```jsx +import { shallow } from 'preact-render-to-string'; +import { h } from 'preact'; + +const Foo = () => foo; +const App = ( + + + +); + +console.log(shallow(App)); +// +``` + +## 美化模式 + +如果您需要格式化/美化输出结果的话,没问题!您可以传入 `pretty` 选项来保留输出结果的空格和缩进。 + +```jsx +import render from 'preact-render-to-string/jsx'; +import { h } from 'preact'; + +const Foo = () => foo; +const App = ( + + + +); + +console.log(render(App, {}, { pretty: true })); +// 日志: +// +// foo +// +``` + +## JSX 模式 + +JSX 渲染模式特别适合快照测试。此渲染模式会将渲染内容视为 JSX。 + +```jsx +import render from 'preact-render-to-string/jsx'; +import { h } from 'preact'; + +const App = ; + +console.log(render(App)); +// 日志: +``` diff --git a/content/zh/guide/v11/signals.md b/content/zh/guide/v11/signals.md new file mode 100644 index 000000000..504ab64a8 --- /dev/null +++ b/content/zh/guide/v11/signals.md @@ -0,0 +1,548 @@ +--- +title: Signals +description: Composable reactive state with automatic rendering +--- + +# 信号 + +信号是用于管理应用程序状态的响应原始概念。 + +信号的独特之处在于,状态变化会自动更新组件和 UI,以实现尽可能高效的操作。自动状态绑定和依赖跟踪使信号供了出色的人体工程学和生产力,同时消除了最常见的状态管理陷阱。 + +信号在任何规模的应用中都有效,符合人体工程学的设计可以加快小型应用的开发速度,性能特征确保在任何规模的应用中,默认设置都是快速的 + +--- + +**Important** + +本指南将介绍如何在 Preact 中使用 Signals,虽然这在很大程度上适用于 Core 和 React 库,但会有一些使用差异。它们使用的最佳参考在各自的文档中 [`@preact/signals-core`](https://github.com/preactjs/signals), [`@preact/signals-react`](https://github.com/preactjs/signals/tree/main/packages/react) + +--- + + + +--- + +## 介绍 + +JavaScript 中许多状态管理的痛苦在于对给定值的变化做出反应,因为值本身并不是直接可观察的。通常的解决方案是通过将值存储在变量中并不断检查它们是否发生了变化来解决这个问题,这既繁琐又不利于性能。理想情况下,我们希望有一种方式来表达一个值,它可以告诉我们何时发生变化。这就是信号的作用。 + +在其核心概念中,信号是一个具有 .value 属性的对象,该属性保存了一个值。这有一个重要的特性:信号的值可以改变,但信号本身始终保持不变。 + +```js +// --repl +import { signal } from '@preact/signals'; + +const count = signal(0); + +// 访问 .value 以读取信号的值 +console.log(count.value); // 0 + +// 更新信号值 +count.value += 1; + +// 信号已改变 +console.log(count.value); // 1 +``` + +在 Preact 中,当信号通过组件树作为 props 或上下文传递时,我们只传递信号的引用。信号可以在不重新渲染任何组件的情况下更新,因为组件看到信号而不是其值。这让我们跳过所有昂贵的渲染工作,并立即跳转到实际访问信号的.value 属性的树中的任何组件。 + +信号还有一个重要的特性,那就是它们跟踪它们的值何时被访问以及何时更新。在 Preact 中,当从组件内部访问一个信号的 .value 属性时,该信号的值发生变化时,会自动重新渲染组件。 + +```jsx +// --repl +import { render } from 'preact'; +// --repl-before +import { signal } from '@preact/signals'; + +// 创建一个可以订阅的信号 +const count = signal(0); + +function Counter() { + // 在组件中访问 .value 将会在信号改变时自动重渲染: + const value = count.value; + + const increment = () => { + // 通过赋值 `.value` 属性更新信号 + count.value++; + }; + + return ( + + Count: {value} + click me + + ); +} +// --repl-after +render(, document.getElementById('app')); +``` + +最后,信号被深度集成到 Preact 中,以提供最佳的性能和人体工程学设计。 在上面的示例中,我们访问“count.value”来检索“count”信号的当前值,但这是不必要的。 相反,我们可以直接在 JSX 中使用“count”信号,让 Preact 为我们完成所有工作: + +```jsx +// --repl +import { render } from 'preact'; +// --repl-before +import { signal } from '@preact/signals'; + +const count = signal(0); + +function Counter() { + return ( + + Count: {count} + count.value++}>click me + + ); +} +// --repl-after +render(, document.getElementById('app')); +``` + +## 安装 + +可以通过将 `@preact/signals` 包添加到您的项目来安装信号: + +```bash +npm install @preact/signals +``` + +通过您选择的包管理器安装后,您已经准备好在你的 app 中引用它了。 + +## 例子 + +让我们在现实场景中使用信号。 我们将构建一个待办事项列表应用程序,您可以在其中添加和删除待办事项列表中的项目。 我们将从对状态建模开始。 我们首先需要一个包含待办事项列表的信号,我们可以用“数组”来表示: + +```jsx +import { signal } from '@preact/signals'; + +const todos = signal([{ text: 'Buy groceries' }, { text: 'Walk the dog' }]); +``` + +为了让用户为新的待办事项输入文本,我们还需要一个信号,表明我们很快就会连接到“input”元素。 现在,我们已经可以使用这个信号来创建一个函数,将待办事项添加到我们的列表中。 请记住,我们可以通过分配信号的“.value”属性来更新信号的值: + +```jsx +// 在后面我们会使用这个作为输入 +const text = signal(''); + +function addTodo() { + todos.value = [...todos.value, { text: text.value }]; + text.value = ''; // 在添加时清空输入值 +} +``` + +> :bulb: Tip: 仅当您为其分配新值时,信号才会更新。 如果您分配给信号的值等于其当前值,则它不会更新。 +> +> ```js +> const count = signal(0); +> +> count.value = 0; // 什么也不会发生 - 值已经是 0 +> +> count.value = 1; // 更新 - 值不同 +> ``` + +让我们检查一下到目前为止我们的逻辑是否正确。 当我们更新“text”信号并调用“addTodo()”时,我们应该看到一个新项目被添加到“todos”信号中。 我们可以通过直接调用这些函数来模拟这种场景 - 不需要用户界面! + +```jsx +// --repl +import { signal } from '@preact/signals'; + +const todos = signal([{ text: 'Buy groceries' }, { text: 'Walk the dog' }]); + +const text = signal(''); + +function addTodo() { + todos.value = [...todos.value, { text: text.value }]; + text.value = ''; // 在添加时重置输入值 +} + +// 检查逻辑是否正确 +console.log(todos.value); +// 输出: [{text: "Buy groceries"}, {text: "Walk the dog"}] + +// 模拟添加新的待办 +text.value = 'Tidy up'; +addTodo(); + +// 查看是否添加了新的项目且 `text` 信号已被清空 +console.log(todos.value); +// 输出: [{text: "Buy groceries"}, {text: "Walk the dog"}, {text: "Tidy up"}] + +console.log(text.value); // 输出: "" +``` + +我们要添加的最后一个功能是能够从列表中删除待办事项。 为此,我们将添加一个函数,用于从 todos 数组中删除给定的待办事项: + +```jsx +function removeTodo(todo) { + todos.value = todos.value.filter(t => t !== todo); +} +``` + +## 构建 UI + +现在我们已经对应用程序的状态进行了建模,是时候连接一个用户可以与之交互的漂亮 UI 了。 + +```jsx +function TodoList() { + const onInput = event => (text.value = event.currentTarget.value); + + return ( + <> + + Add + + {todos.value.map(todo => ( + + {todo.text} removeTodo(todo)}>❌ + + ))} + + > + ); +} +``` + +这样我们就有了一个完全可用的待办事项应用程序!您可以[在这里](/repl?example=todo-signals)尝试完整的应用程序 :tada: + +## 使用计算信号驱动状态 + +让我们向待办事项应用程序添加一项功能:每个待办事项都可以在已完成时进行核对,并且我们将向用户显示他们已完成的项目数。 为此,我们将导入 [`computed(fn)`](#computedfn) 函数,该函数允许我们创建一个根据其他信号的值计算的新信号。 返回的计算信号是只读的,当从回调函数内访问的任何信号发生变化时,其值会自动更新。 + +```jsx +// --repl +import { signal, computed } from '@preact/signals'; + +const todos = signal([ + { text: 'Buy groceries', completed: true }, + { text: 'Walk the dog', completed: false } +]); + +// 创建从其他信号计算而来的信号 +const completed = computed(() => { + // 当 `todos` 改变时,这将会自动重新运行 + return todos.value.filter(todo => todo.completed).length; +}); + +// 输出: 1,因为有一个标记为完成的待办 +console.log(completed.value); +``` + +我们简单的 TODO LIST 应用程序不需要很多计算的信号,但是更复杂的应用程序倾向于依靠 computed()来避免在多个位置重复状态。 + +> :bulb: Tip: 尽可能地派生状态以确保状态只有单一真实来源。这是信号的一个关键准则。这在以后应用出现逻辑缺陷时更容易调试,因为需要关心的地方更少。 + +## 管理全局应用程序状态 + +到目前为止,我们仅在组件树之外创建了信号。 这对于像待办事项列表这样的小应用程序来说很好,但是对于更大,更复杂的应用程序,这可能会使测试变得困难。测试通常涉及您的应用程序状态中的变化值以重现某种情况,然后将该状态传递给组件并主张渲染的 HTML。为此,我们可以将我们的待办事项列表状态提取到一个函数中: + +```jsx +function createAppState() { + const todos = signal([]); + + const completed = computed(() => { + return todos.value.filter(todo => todo.completed).length; + }); + + return { todos, completed }; +} +``` + +> :bulb: Tip: 请注意,我们故意没有使用 `addTodo()` 和 `removeTodo(todo)` 函数。 分离数据与修改它的函数通常有助于简化应用架构。详情请参阅[面向数据设计](https://en.wikipedia.org/wiki/data-oriented_design)。 + +现在,我们可以在渲染时将待办事项状态作为 props 传递: + +```jsx +const state = createAppState(); + +// ...然后: +; +``` + +这在我们的 TODO 列表应用程序中可行,因为状态是全局的,但大型应用通常会有多个需要访问相同状态的组件。 这通常需要“状态提升”到一个共同的祖先组件。为了避免使用 props 在每个组件中手动传递状态,可以将状态放入[上下文](/guide/v10/context)中,以便树中的任何组件都可以访问它。 下面是一个典型例子: + +```jsx +import { createContext } from 'preact'; +import { useContext } from 'preact/hooks'; +import { createAppState } from './my-app-state'; + +const AppState = createContext(); + +render( + + + +); + +// ...然后当你需要访问应用的状态时 +function App() { + const state = useContext(AppState); + return {state.completed}; +} +``` + +如果你想了解更多有关上下文的更信息,请参阅[上下文](/guide/v10/context)。 + +## 使用信号的本地状态 + +应用的大部分状态最终都是使用 props 和 context 传递的。但是,在许多情况下,组件具有特定于该组件的内部状态。没有理由让这些状态成为全局业务逻辑的一部分,因此应将其限制于需要它的组件中。在这些情况下,我们可以使用`useSignal()` 和 `useComputed()` 钩子: + +```jsx +import { useSignal, useComputed } from '@preact/signals'; + +function Counter() { + const count = useSignal(0); + const double = useComputed(() => count.value * 2); + + return ( + + + {count} x 2 = {double} + + count.value++}>click me + + ); +} +``` + +这两个钩子是[`signal()`](#signalinitialvalue) 和 [`computed()`](#computedfn) 的简单封装,它们在组件首次运行时构造信号,并简单地在之后的渲染中使用相同的信号。 + +> :bulb: 以下是幕后的实现: +> +> ```js +> function useSignal(value) { +> return useMemo(() => signal(value), []); +> } +> ``` + +## 信号高级用法 + +到目前为止,我们所涵盖的主题是已经可以满足需求。以下部分针对希望通过完全使用信号对应用程序状态建模来获得更多益处的读者。 + +### 对组件外的信号做出反应 + +在组件树外使用信号时,您可能已经注意到,除非您读取它的 .value,否则计算信号不会重新计算。这是因为默认情况下,信号是懒惰的:它们仅在访问其值时计算新值。 + +```js +const count = signal(0); +const double = computed(() => count.value * 2); + +// 尽管更新了 `double` 信号依赖的 `count` 信号, +// 但此时 `double` 的值不会更新,因为没有读取它的值。 +count.value = 1; + +// 读取 `double` 的值会触发它重新计算: +console.log(double.value); // 输出: 2 +``` + +这提出了一个问题:我们如何订阅组件树之外的信号?也许我们想在信号的值更改或保持时将某些内容记录到控制台上。[LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). + +要运行任意代码来响应信号变化,我们可以使用 [`effect(fn)`](#effectfn)。与计算信号类似,effect 跟踪访问了哪些信号,并在这些信号发生变化时重新运行其回调。 与计算信号不同,[`effect()`](#effectfn) 不返回信号 - 它是一系列更改的结束。 + +```js +import { signal, computed, effect } from '@preact/signals-core'; + +const name = signal('Jane'); +const surname = signal('Doe'); +const fullName = computed(() => `${name.value} ${surname.value}`); + +// 每次改变时输出它的值: +effect(() => console.log(fullName.value)); +// 输出: "Jane Doe" + +// 更新 `name` 会更新 `fullName`,然后会触发执行作用: +name.value = 'John'; +// 输出: "John Doe" +``` + +可以调用返回的函数来清除 effect 并取消订阅它访问的信号。 + +```js +import { signal, effect } from '@preact/signals-core'; + +const name = signal('Jane'); +const surname = signal('Doe'); +const fullName = computed(() => name.value + ' ' + surname.value); + +const dispose = effect(() => console.log(fullName.value)); +// 输出: "Jane Doe" + +// 销毁作用和订阅: +dispose(); + +// 更新 `name` 不会运行作用,因为它已经销毁了。 +// 也不会重新计算 `fullName`,因为此时它已经没有订阅了。 +name.value = 'John'; +``` + +> :bulb: Tip: 如果你大量使用 effect,不要忘了清理。否则会浪费内存。 + +## 读取信号而无需订阅它们 + +在极少数情况下,您需要在 [`effect(fn)`](#effectfn) 内写入信号,但不希望信号更改时重新运行 effect,您可以使用 `.peek()` 获取信号当前值而不订阅它们。 + +```js +const delta = signal(0); +const count = signal(0); + +effect(() => { + // 更新 `count` 但不订阅它: + count.value = count.peek() + delta.value; +}); + +// 设置 `delta` 会重新运行作用: +delta.value = 1; + +// 这不会重新云心作用,因为它没有访问 `.value`: +count.value = 10; +``` + +> :bulb: Tip: 不想订阅信号的情况很少见。大多数情况下你都希望 effect 订阅所有信号。只有在真正需要的时候才使用 `.peek()`。 + +## 批更新 + +还记得我们之前在待办事项应用程序中使用的`addTodo()`函数吗? 回顾一下它的样子: + +```js +const todos = signal([]); +const text = signal(''); + +function addTodo() { + todos.value = [...todos.value, { text: text.value }]; + text.value = ''; +} +``` + +请注意,该函数触发两个单独的更新:一个是在设置 `todos.value` 时,另一个是在设置 `text` 的值时。 出于性能或其他原因,这种情况有时并不理想,需要将这两个更新合并为一个。 [`batch(fn)`](#batchfn) 函数可用于在回调结束时将多个值更新合并为一个“提交”: + +```js +function addTodo() { + batch(() => { + todos.value = [...todos.value, { text: text.value }]; + text.value = ''; + }); +} +``` + +访问批处理内已修改的信号将反映其更新值。访问已被批处理内的另一个信号无效的计算信号时,将仅重新计算必要的依赖关系,以返回该计算信号的最新值。任何其他无效信号均不受影响,并且仅在批处理结束时更新。 + +```js +// --repl +import { signal, computed, effect, batch } from '@preact/signals-core'; + +const count = signal(0); +const double = computed(() => count.value * 2); +const triple = computed(() => count.value * 3); + +effect(() => console.log(double.value, triple.value)); + +batch(() => { + // 设置 `count` 会使 `double` 和 `triple` 失效: + count.value = 1; + + // 尽管正在批处理中,`double` 仍然反应新的计算值。 + // 然而,`triple` 只有在回调结束后在会更新。 + console.log(double.value); // 输出: 2 +}); +``` + +> :bulb: Tip: 批处理也可以嵌套,这种情况下只有最外层的批处理完成时才会进行更新。 + +### 渲染优化 + +通过信号我们可以绕过虚拟 DOM 渲染,将信号变化直接绑定到 DOM 操作。如果将信号传递到 JSX 的文本位置,它就会以文本形式自动就地更新,无需虚拟 DOM 差分计算: + +```jsx +const count = signal(0); + +function Unoptimized() { + // 当 `count` 变化时重新渲染组件: + return {count.value}; +} + +function Optimized() { + // 文本将自动更新而无需重新渲染组件: + return {count}; +} +``` + +要启用此优化,请将信号传递到 JSX,而不是访问其 `.value` 属性。 + +将信号作为 props 传递给 DOM 元素时,也支持类似的渲染优化。 + +## API + +本节是信号 API 概览。它的目的是为已经知道如何使用信号的人提供快速参考。 + +### signal(initialValue) + +以给定参数为初始值创建一个新的信号: + +```js +const count = signal(0); +``` + +在组件内创建信号时,请使用 hook 变体:`useSignal(initialValue)`。 + +返回的信号具有 `.value` 属性,可以获取或设置该属性来读取和写入其值。 要读取信号而不订阅它,请使用 `signal.peek()`。 + +### computed(fn) + +创建一个根据其他信号的值计算的新信号。返回的计算信号是只读的,当回调函数内访问的任何信号发生变化时,其值会自动更新。 + +```js +const name = signal('Jane'); +const surname = signal('Doe'); + +const fullName = computed(() => `${name.value} ${surname.value}`); +``` + +在组件内创建计算信号时,请使用钩子变体:`useComputed(fn)`。 + +### effect(fn) + +要根据信号变化运行任意代码,可以使用 `effect(fn)`。与计算信号类似,effect 会跟踪哪些信号被访问,并在这些信号发生变化时重新运行其回调。与计算信号不同的是,`effect()` 不返回信号 - 它是一系列更改的结束。 + +```js +const name = signal('Jane'); + +// 当 `name` 改变时输出到控制台: +effect(() => console.log('Hello', name.value)); +// 输出: "Hello Jane" + +name.value = 'John'; +// 输出: "Hello John" +``` + +当响应组件内的信号变化时,请使用钩子变体:`useSignalEffect(fn)`。 + +### batch(fn) + +`batch(fn)` 函数可用于在提供的回调结束时将多个值更新合并为一个“提交”。 批处理可以嵌套,并且只有在最外面的批处理回调完成后才会刷新更改。 访问批处理内已修改的信号将反映其更新值。 + +```js +const name = signal('Jane'); +const surname = signal('Doe'); + +// 将两次写组合为一次更新 +batch(() => { + name.value = 'John'; + surname.value = 'Smith'; +}); +``` + +### 未追踪(fn) + +`untracked(fn)` 函数可用于访问多个信号的值而无需订阅它们。 + +```js +const name = signal('Jane'); +const surname = signal('Doe'); + +effect(() => { + untracked(() => { + console.log(`${name.value} ${surname.value}`); + }); +}); +``` diff --git a/content/zh/guide/v11/switching-to-preact.md b/content/zh/guide/v11/switching-to-preact.md new file mode 100644 index 000000000..bba51042f --- /dev/null +++ b/content/zh/guide/v11/switching-to-preact.md @@ -0,0 +1,142 @@ +--- +title: 从 React 迁移到 Preact +description: 从 React 迁移到 Preact 时必要的说明。 +--- + +# 从 React 切换到 Preact + +`preact/compat` 是我们的兼容层,它允许您无需修改现有代码即可利用 React 生态系统中丰富的库。这是现有的 React 应用迁移到 Preact 的推荐选择。 + +通过这一兼容层,您可以沿用现有的 React/ReactDOM 代码和开发流程。只增加约 2kb 的打包体积,就能无缝兼容 npm 上绝大多数 React 生态模块。该包在 Preact 核心之上完美模拟了 `react` 和 `react-dom` 的完整功能,并将这两个模块整合在一个模块中。 + +--- + + + +--- + +## 设置 compat + +要启用 `preact/compat`,只需在构建工具中将 `react` 和 `react-dom` 的引用重定向到 `preact/compat`。[开始上手](/guide/v10/getting-started#aliasing-react-to-preact)页面提供了主流打包工具的详细配置方法。 + +## PureComponent + +`PureComponent` 是自带性能优化的组件基类:和 `Component` 类不同的是,当新旧 `props` 通过浅层比较(逐个属性值比对)相等时,将自动跳过渲染。这通过默认的 `shouldComponentUpdate` 生命周期钩子实现,能显著提升复杂应用的性能表现。 + +```jsx +import { render } from 'preact'; +import { PureComponent } from 'preact/compat'; + +class Foo extends PureComponent { + render(props) { + console.log('render'); + return ; + } +} + +const dom = document.getElementById('root'); +render(, dom); +// 输出: "render" + +// 第二次渲染,不会输出任何内容 +render(, dom); +``` + +> 注意:当组件渲染开销较大时,使用 `PureComponent` 才能体现优势。对于简单组件,直接渲染可能比 `props` 比对更高效。 + +## memo + +`memo` 为函数式组件提供类似 `PureComponent` 的优化能力。您既可以使用默认比较,也可以自定义比对逻辑来满足特定需求。 + +```jsx +import { memo } from 'preact/compat'; + +function MyComponent(props) { + return Hello {props.name}; +} + +// 使用默认比较函数 +const Memoed = memo(MyComponent); + +// 使用自定义比较函数 +const Memoed2 = memo(MyComponent, (prevProps, nextProps) => { + // 仅当 `name` 改变时才重新渲染 + return prevProps.name === nextProps.name; +}); +``` + +> 比对函数与 `shouldComponentUpdate` 不同之处在于,前者比较两个 props 是否**相同**(比对函数返回 `true` 表示属性相同),而后者比较两个 props 是否不同。 + +## forwardRef + +当需要暴露内部子元素引用时,`forwardRef` 可将 `ref` 属性透传到指定元素: + +```jsx +import { createRef, render } from 'preact'; +import { forwardRef } from 'preact/compat'; + +const MyComponent = forwardRef((props, ref) => { + return Hello world; +}); + +// 用法:`ref` 将持有内部 `div` 的引用,而不是 `MyComponent` 的引用 +const ref = createRef(); +render(, dom); +``` + +这个功能对可复用组件库作者尤为重要。 + +## 跨层级渲染 Portals + +当需要将内容渲染到组件树之外的 DOM 节点时,可以使用 `createPortal`。注意目标容器需预先存在。 + +```html + + + + + + + + +``` + +```jsx +import { createPortal } from 'preact/compat'; +import MyModal from './MyModal'; + +function App() { + const container = document.getElementById('modals'); + return ( + + 我是 App + {createPortal(, container)} + + ); +} +``` + +> 请记住,由于 Preact 重用了浏览器的原生事件系统,所以 Portal 内事件不会冒泡到父组件树当中。 + +## Suspense + +`Suspense` 允许在子组件加载过程中展示加载状态,常用于代码分割等异步场景。当子组件的加载未完成时显示备用内容。 + +```jsx +import { Suspense, lazy } from 'preact/compat'; + +const SomeComponent = lazy(() => import('./SomeComponent')); + +// 用法 +加载中...}> + + + +; +``` + +在此示例中,界面将显示 `加载中...` 文本,直到 `SomeComponent` 加载完毕并且 Promise 完成。 + +> React 和 Preact 中的 Suspense 仍处于演进阶段。虽然 React 团队仍然不鼓励用户直接将其用于数据获取,但它已被众多 Preact 开发者成功实践多年。尽管存在部分已知问题(最新动态请参考[我们的问题追踪系统](https://github.com/preactjs/preact/issues)),但普遍认为其稳定性足以满足生产环境需求。 +> +> 例如,您现在浏览的 Preact 官方网站,正是采用基于 Suspense 的异步数据加载策略来实现全站内容渲染。 diff --git a/content/zh/guide/v11/typescript.md b/content/zh/guide/v11/typescript.md new file mode 100644 index 000000000..b75457907 --- /dev/null +++ b/content/zh/guide/v11/typescript.md @@ -0,0 +1,604 @@ +--- +title: TypeScript +description: Preact 内置 TypeScript 支持。学习如何使用它! +--- + +# TypeScript + +Preact 附带 TypeScript 的类型定义,库本身也在使用这些类型定义! + +在支持 TypeScript 的编辑器(例如 VSCode)中使用 Preact 时,即使编写常规 JavaScript,也能受益于额外的类型信息。如果你想为自己的应用添加类型信息,可以使用 JSDoc 注释(例如:https://fettblog.eu/typescript-jsdoc-superpowers/),或者直接编写 TypeScript 并将其转译为普通 JavaScript。本节重点介绍后一种方式。 + +--- + + + +--- + +## TypeScript 配置 + +TypeScript 自带完整的 JSX 编译器,可以替代 Babel 使用。将以下配置添加到你的 `tsconfig.json`,以将 JSX 转译为与 Preact 兼容的 JavaScript: + +```json +// 经典转换 +{ + "compilerOptions": { + "jsx": "react", + "jsxFactory": "h", + "jsxFragmentFactory": "Fragment" + //... + } +} +``` + +```json +// 自动转换,TypeScript >= 4.1.1 可用 +{ + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "preact" + //... + } +} +``` + +如果你在 Babel 工具链中使用 TypeScript,请将 `jsx` 设置为 `preserve` 并让 Babel 处理转译。你仍需指定 `jsxFactory` 和 `jsxFragmentFactory` 以获得正确的类型信息。 + +```json +{ + "compilerOptions": { + "jsx": "preserve", + "jsxFactory": "h", + "jsxFragmentFactory": "Fragment" + //... + } +} +``` + +In your `.babelrc`: + +```javascript +{ + presets: [ + "@babel/env", + ["@babel/typescript", { jsxPragma: "h" }], + ], + plugins: [ + ["@babel/transform-react-jsx", { pragma: "h" }] + ], +} +``` + +将你的 `.jsx` 文件重命名为 `.tsx`,以便 TypeScript 正确解析 JSX。 + +## TypeScript 的 `preact/compat` 配置 + +如果你的项目需要支持更广泛的 React 生态,在编译时可能需要对 `node_modules` 禁用类型检查并为类型添加路径映射。如下配置可以在库导入 `react` 时让别名正常工作: + +```json +{ + "compilerOptions": { + ... + "skipLibCheck": true, + "baseUrl": "./", + "paths": { + "react": ["./node_modules/preact/compat/"], + "react/jsx-runtime": ["./node_modules/preact/jsx-runtime"], + "react-dom": ["./node_modules/preact/compat/"], + "react-dom/*": ["./node_modules/preact/compat/*"] + } + } +} +``` + +## 组件的类型定义 + +在 Preact 中为组件添加类型有多种方式。类组件使用泛型类型变量来确保类型安全。只要函数返回 JSX,TypeScript 就会将其视为函数式组件。对于函数式组件的 props,有多种定义方案。 + +### 函数组件 + +为普通函数组件添加类型非常简单,只需在函数参数处添加类型信息。 + +```tsx +interface MyComponentProps { + name: string; + age: number; +} + +function MyComponent({ name, age }: MyComponentProps) { + return ( + + My name is {name}, I am {age.toString()} years old. + + ); +} +``` + +你可以在函数签名中为参数设置默认值来实现默认 props。 + +```tsx +interface GreetingProps { + name?: string; // name is optional! +} + +function Greeting({ name = 'User' }: GreetingProps) { + // name 至少为 "User" + return Hello {name}!; +} +``` + +Preact 还提供了 `FunctionComponent` 类型用于注释匿名函数。`FunctionComponent` 会为 `children` 添加类型: + +```tsx +import { h, FunctionComponent } from 'preact'; + +const Card: FunctionComponent<{ title: string }> = ({ title, children }) => { + return ( + + {title} + {children} + + ); +}; +``` + +`children` 的类型为 `ComponentChildren`。你也可以使用该类型自行指定 children: + +```tsx +import { h, ComponentChildren } from 'preact'; + +interface ChildrenProps { + title: string; + children: ComponentChildren; +} + +function Card({ title, children }: ChildrenProps) { + return ( + + {title} + {children} + + ); +} +``` + +### 类组件 + +Preact 的 `Component` 类是一个带有两个泛型类型变量(Props 和 State)的泛型类。两个类型默认都是空对象,你可以根据需要指定它们。 + +```tsx +// Types for props +interface ExpandableProps { + title: string; +} + +// Types for state +interface ExpandableState { + toggled: boolean; +} + +// Bind generics to ExpandableProps and ExpandableState +class Expandable extends Component { + constructor(props: ExpandableProps) { + super(props); + // this.state is an object with a boolean field `toggle` + // due to ExpandableState + this.state = { + toggled: false + }; + } + // `this.props.title` is string due to ExpandableProps + render() { + return ( + + + {this.props.title}{' '} + this.setState({ toggled: !this.state.toggled })} + > + Toggle + + + {this.props.children} + + ); + } +} +``` + +类组件默认包含 children,其类型为 `ComponentChildren`。 + +## 继承 HTML 属性 + +当我们编写像 `` 这样的组件来包裹原生 `` 元素时,通常希望继承原生 HTML input 元素可用的属性。可以按如下方式实现: + +```tsx +import { InputHTMLAttributes } from 'preact'; + +interface InputProperties extends InputHTMLAttributes { + mySpecialProp: any; +} + +const Input = (props: InputProperties) => ; +``` + +现在使用 `Input` 时,它会识别诸如 `value` 等属性。 + +## 事件的类型定义 + +Preact 会触发常规的 DOM 事件。只要你的 TypeScript 项目在 `tsconfig.json` 中包含了 `dom` 库,就可以使用当前配置下所有事件类型。 + +```tsx +import type { TargetedMouseEvent } from "preact"; + +export class Button extends Component { + handleClick(event: TargetedMouseEvent) { + alert(event.currentTarget.tagName); // 会弹出 BUTTON + } + + render() { + return ( + + {this.props.children} + + ); + } +} +``` + +如果你偏好内联函数,可以不显式标注当前事件目标的类型,因为它会从 JSX 元素推断出来: + +```tsx +export class Button extends Component { + render() { + return ( + alert(event.currentTarget.tagName)}> + {this.props.children} + + ); + } +} +``` + +## 引用(refs)的类型定义 + +`createRef` 函数也是泛型的,允许你将引用绑定到特定的元素类型。在下面的示例中,我们确保引用只能绑定到 `HTMLAnchorElement`。若对其他元素使用该 `ref`,TypeScript 会报错: + +```tsx +import { h, Component, createRef } from 'preact'; + +class Foo extends Component { + ref = createRef(); + + componentDidMount() { + // current 的类型为 HTMLAnchorElement + console.log(this.ref.current); + } + + render() { + return Foo; + // ~~~ + // 💥 错误!该 ref 只可用于 HTMLAnchorElement + } +} +``` + +如果你想确保引用的元素是可聚焦(focusable)的输入元素,这点非常有用。 + +## 上下文(context)的类型定义 + +`createContext` 会尽可能从你传入的初始值推断出类型: + +```tsx +import { h, createContext } from 'preact'; + +const AppContext = createContext({ + authenticated: true, + lang: 'en', + theme: 'dark' +}); +// AppContext 的类型为 preact.Context<{ +// authenticated: boolean; +// lang: string; +// theme: string; +// }> +``` + +它同时要求你在提供 value 时包含初始值中定义的所有属性: + +```tsx +function App() { + // 这里会报错 💥 因为我们没有定义 theme + return ( + + {} + + + ); +} +``` + +如果你不想指定所有属性,可以将默认值与覆盖值合并: + +```tsx +const AppContext = createContext(appContextDefault); + +function App() { + return ( + + + + ); +} +``` + +或者你可以不使用默认值,而是在创建 context 时通过泛型类型变量为 context 绑定特定类型: + +```tsx +interface AppContextValues { + authenticated: boolean; + lang: string; + theme: string; +} + +const AppContext = createContext>({}); + +function App() { + return ( + + + + ); +``` + +所有值将变为可选,因此在使用时需要进行空值检查。 + +## Hooks 的类型定义 + +大多数 hooks 不需要特殊的类型声明,通常可从使用方式中推断类型。 + +### useState、useEffect、useContext + +`useState`、`useEffect` 和 `useContext` 都支持泛型类型,因此通常无需额外注解。下面是一个最小示例,展示了 `useState` 如何从函数签名的默认值推断出类型。 + +```tsx +const Counter = ({ initial = 0 }) => { + // 由于 initial 是数字(默认值),所以 clicks 是数字 + // setClicks 是一个接受以下参数的函数 + // - 一个数字 + // - 或者返回数字的函数 + const [clicks, setClicks] = useState(initial); + return ( + <> + Clicks: {clicks} + setClicks(clicks + 1)}>+ + setClicks(clicks - 1)}>- + > + ); +}; +``` + +`useEffect` 会做额外检查,因此你从 effect 回调返回的只能是一个没有参数的清理函数。 + +```typescript +useEffect(() => { + const handler = () => { + document.title = window.innerWidth.toString(); + }; + window.addEventListener('resize', handler); + + // ✅ if you return something from the effect callback + // it HAS to be a function without arguments + return () => { + window.removeEventListener('resize', handler); + }; +}); +``` + +`useContext` gets the type information from the default object you pass into `createContext`. + +```tsx +const LanguageContext = createContext({ lang: 'en' }); + +const Display = () => { + // lang 的类型将为 string + const { lang } = useContext(LanguageContext); + return ( + <> + Your selected language: {lang} + > + ); +}; +``` + +### useRef + +与 `createRef` 类似,`useRef` 通过为泛型类型变量指定 HTMLElement 的子类型来收获类型优势。在下面的示例中,我们确保 `inputRef` 只用于 `HTMLInputElement`。`useRef` 通常用 `null` 初始化;在启用 `strictNullChecks` 的情况下,需要检查 `inputRef` 是否存在。 + +```tsx +import { h } from 'preact'; +import { useRef } from 'preact/hooks'; + +function TextInputWithFocusButton() { + // initialise with null, but tell TypeScript we are looking for an HTMLInputElement + const inputRef = useRef(null); + const focusElement = () => { + // 在 strict null checks 下需要检查 ref 和 current 是否存在。 + // 但一旦 current 存在,它的类型为 HTMLInputElement,因此它 + // 因此它有 focus 方法 ✅ + if (inputRef && inputRef.current) { + inputRef.current.focus(); + } + }; + return ( + <> + {/* 此外,inputRef 仅可用于 input 元素 */} + + Focus the input + > + ); +} +``` + +### useReducer + +对于 `useReducer`,TypeScript 会尽可能从 reducer 函数中推断出类型。例如,下面展示了计数器的 reducer: + +```typescript +// reducer 函数的 state 类型 +interface StateType { + count: number; +} + +// action 的类型,`type` 可以是 +// "reset", "decrement", "increment" +interface ActionType { + type: 'reset' | 'decrement' | 'increment'; +} + +// 初始 state。无需注解 +const initialState = { count: 0 }; + +function reducer(state: StateType, action: ActionType) { + switch (action.type) { + // TypeScript 会确保我们处理所有可能的 action 类型,并为类型字符串提供自动完成 + case 'reset': + return initialState; + case 'increment': + return { count: state.count + 1 }; + case 'decrement': + return { count: state.count - 1 }; + default: + return state; + } +} +``` + +Once we use the reducer function in `useReducer`, we infer several types and do type checks for passed arguments. + +```tsx +function Counter({ initialCount = 0 }) { + // TypeScript 会确保 reducer 最多接收两个参数,并且初始 state 与 StateType 匹配。 + // 此外: + // - state 的类型为 StateType + // - dispatch 是用于发送 ActionType 的函数 + const [state, dispatch] = useReducer(reducer, { count: initialCount }); + + return ( + <> + Count: {state.count} + {/* TypeScript ensures that the dispatched actions are of ActionType */} + dispatch({ type: 'reset' })}>Reset + dispatch({ type: 'increment' })}>+ + dispatch({ type: 'decrement' })}>- + > + ); +} +``` + +唯一需要显式标注的地方通常是在 reducer 函数本身。`useReducer` 的类型还会确保 reducer 的返回值符合 `StateType`。 + +## 扩展内置的 JSX 类型 + +你可能会在 JSX 中使用自定义元素(参见 /guide/v10/web-components),或者想为所有或某些 HTML 元素添加额外属性以配合特定库使用。为此,需要使用“模块扩展(Module augmentation)”来扩展或修改 Preact 提供的类型。 + +### 为自定义元素扩展 `IntrinsicElements` + +```tsx +function MyComponent() { + return ; + // ~~~~~~~~~~~ + // 💥 错误!属性 'loading-bar' 在类型 'JSX.IntrinsicElements' 中不存在。 +} +``` + +```tsx +// global.d.ts + +declare global { + namespace preact.JSX { + interface IntrinsicElements { + 'loading-bar': { showing: boolean }; + } + } +} + +// 这个空导出很重要!它告诉 TypeScript 将此文件视为模块 +export {}; +``` + +### 为全局自定义属性扩展 `HTMLAttributes` + +如果你想向所有 HTML 元素添加自定义属性,可以扩展 `HTMLAttributes` 接口: + +```tsx +function MyComponent() { + return ; + // ~~~~~~ + // 💥 错误!类型 '{ custom: string; }' 无法赋值给类型 'DetailedHTMLProps, HTMLDivElement>'。 + // 属性 'custom' 在类型 'DetailedHTMLProps, HTMLDivElement>' 中不存在。 +} +``` + +```tsx +// global.d.ts + +declare module 'preact' { + interface HTMLAttributes { + custom?: string | undefined; + } +} + +// 这个空导出很重要!它告诉 TypeScript 将此文件视为模块 +export {}; +``` + +### 为单个元素扩展属性接口 + +有时你可能不想全局添加自定义属性,而仅针对特定元素扩展。这种情况下可以扩展该元素对应的接口: + +```tsx +// global.d.ts + +declare module 'preact' { + interface HeadingHTMLAttributes { + custom?: string | undefined; + } +} + +// 这个空导出很重要!它告诉 TypeScript 将此文件视为模块 +export {}; +``` + +但是,目前有 5 个特殊元素(``、``、``、`` 和 ``)需要稍作不同的处理:与其他元素不同,这些元素的接口以 `Partial...` 为前缀,因此你需要确保你的接口符合这一模式: + +```ts +// global.d.ts + +declare module 'preact' { + interface PartialAnchorHTMLAttributes { + custom?: string | undefined; + } +} + +// 这个空导出很重要!它告诉 TypeScript 将此文件视为模块 +export {}; +``` + +> **注意**:我们这样做是为了支持这些元素更完善的 ARIA/无障碍类型,因为根据规范这些元素的 ARIA 角色是判别联合类型(例如,如果 `` 有 `href` 属性,它可以具有几种特定角色;如果没有,它又可能具有另一组角色)。为实现这点,我们需要在 TypeScript 中使用 `type` 关键字,但这会阻止类型被扩展,因为它不再是简单的接口。不过,我们的无障碍类型与 `Partial...` 接口相交,因此可以通过扩展这些接口来实现需要的功能。 diff --git a/content/zh/guide/v11/unit-testing-with-enzyme.md b/content/zh/guide/v11/unit-testing-with-enzyme.md new file mode 100644 index 000000000..1ea0437db --- /dev/null +++ b/content/zh/guide/v11/unit-testing-with-enzyme.md @@ -0,0 +1,163 @@ +--- +title: 使用 Enzyme 进行单元测试 +description: 使用 Enzyme 简化对 Preact 应用的测试 +--- + +# 使用 Enzyme 进行单元测试 + +Airbnb 的 [Enzyme](https://airbnb.io/enzyme/) 是一个用于为 React 组件编写测试的库。它通过“适配器(adapters)”支持不同版本的 React 及类 React 库。Preact 团队维护了一个针对 Preact 的适配器。 + +Enzyme 支持在普通或无头浏览器中运行的测试(例如通过 [Karma](http://karma-runner.github.io/latest/index.html)),也支持在 Node 环境中使用 [jsdom](https://github.com/jsdom/jsdom) 模拟浏览器 API 来运行测试。 + +有关 Enzyme 的详细入门和 API 参考,请参阅 [Enzyme 文档](https://airbnb.io/enzyme/)。本指南余下部分说明如何将 Enzyme 与 Preact 配置在一起,以及 Enzyme 在与 Preact 配合使用时与 React 的差异。 + +--- + + + +--- + +## 安装 + +使用以下命令安装 Enzyme 及 Preact 适配器: + +```bash +npm install --save-dev enzyme enzyme-adapter-preact-pure +``` + +## 配置 + +在你的测试初始化代码中,需要将 Enzyme 配置为使用 Preact 适配器: + +```js +import { configure } from 'enzyme'; +import Adapter from 'enzyme-adapter-preact-pure'; + +configure({ adapter: new Adapter() }); +``` + +关于如何在不同测试运行器(如 Mocha、Jest)中使用 Enzyme 的更多指南,请参见 Enzyme 文档中的 [Guides](https://airbnb.io/enzyme/docs/guides.html) 部分。 + +## 示例 + +假设我们有一个简单的 `Counter` 组件,它显示一个初始值并提供一个按钮来增加计数: + +```jsx +import { h } from 'preact'; +import { useState } from 'preact/hooks'; + +export default function Counter({ initialCount }) { + const [count, setCount] = useState(initialCount); + const increment = () => setCount(count + 1); + + return ( + + Current value: {count} + Increment + + ); +} +``` + +使用例如 Mocha 或 Jest 的测试运行器,你可以编写如下测试来验证其行为: + +```jsx +import { expect } from 'chai'; +import { h } from 'preact'; +import { mount } from 'enzyme'; + +import Counter from '../src/Counter'; + +describe('Counter', () => { + it('should display initial count', () => { + const wrapper = mount(); + expect(wrapper.text()).to.include('Current value: 5'); + }); + + it('should increment after "Increment" button is clicked', () => { + const wrapper = mount(); + + wrapper.find('button').simulate('click'); + + expect(wrapper.text()).to.include('Current value: 6'); + }); +}); +``` + +要查看可运行的示例项目和其它示例,请参阅 Preact 适配器仓库中的 [examples/](https://github.com/preactjs/enzyme-adapter-preact-pure/blob/master/README.md#example-projects) 目录。 + +## Enzyme 的工作原理 + +Enzyme 使用已配置的适配器来渲染组件及其子节点。适配器会将渲染输出转换为一个标准化的内部表示(即“React Standard Tree”)。Enzyme 在此基础上封装了一个带有查询和触发更新方法的包装对象。该包装对象的 API 使用类似 CSS 的 [选择器](https://airbnb.io/enzyme/docs/api/selector.html) 来定位输出中的部分节点。 + +## 完整渲染、浅渲染和字符串渲染 + +Enzyme 提供三种渲染“模式”: + +```jsx +import { mount, shallow, render } from 'enzyme'; + +// 完整渲染组件树: +const wrapper = mount(); + +// 仅渲染 `MyComponent` 的直接输出(即将子组件“模拟”为占位符): +const wrapper = shallow(); + +// 将完整组件树渲染为 HTML 字符串并解析结果: +const wrapper = render(); +``` + +- `mount` 会以浏览器中相同的方式渲染组件及其所有后代节点。 + +- `shallow` 只渲染组件直接输出的 DOM 节点。任何子组件都会被替换为仅输出其子内容的占位符。 + + 这种模式的优点是可以在不依赖子组件实现细节的情况下为组件编写测试,从而无需构造所有子组件的依赖。 + + 注意:`shallow` 在 Preact 适配器中的内部实现与 React 不同。详情参见下文的“差异”一节。 + +- `render`(注意不要与 Preact 的 `render` 函数混淆)将组件渲染为 HTML 字符串,适用于在服务端测试渲染输出或在不触发副作用的情况下渲染组件。 + +## 使用 `act` 触发状态更新和副作用 + +在前面的示例中,使用了 `.simulate('click')` 来触发按钮点击。 + +Enzyme 知道对 `simulate` 的调用可能会改变组件状态或触发副作用,因此会在 `simulate` 返回之前立即应用相应的状态更新或副作用。Enzyme 在使用 `mount` 或 `shallow` 初次渲染组件以及通过 `setProps` 更新组件时也会执行相同的刷新行为。 + +但如果事件是在 Enzyme 的方法调用之外触发的,例如直接调用事件处理器(如按钮的 `onClick` 属性),Enzyme 并不会自动感知这些变化。在这种情况下,测试代码需要手动触发状态更新和副作用的执行,并让 Enzyme 刷新其对渲染输出的视图。 + +- 若要同步执行状态更新和副作用,可使用 `preact/test-utils` 中的 `act` 函数来包裹触发更新的代码。 +- 若要让 Enzyme 刷新其对渲染输出的视图,可使用包装对象的 `.update()` 方法。 + +例如,下面是对计数器测试的另一种写法,它直接调用按钮的 `onClick` 属性,而不是通过 Enzyme 的 `simulate`: + +```js +import { act } from 'preact/test-utils'; +``` + +```jsx +it('should increment after "Increment" button is clicked', () => { + const wrapper = mount(); + const onClick = wrapper.find('button').props().onClick; + + act(() => { + // 直接调用按钮的点击处理器(而不是通过 Enzyme 的 API) + onClick(); + }); + // 刷新 Enzyme 对渲染输出的视图 + wrapper.update(); + + expect(wrapper.text()).to.include('Current value: 6'); +}); +``` + +## 与 React 下的 Enzyme 的差异 + +总体目标是让使用 Enzyme + React 编写的测试能较容易地在 Enzyme + Preact 下工作,反之亦然。这避免了在将组件从 Preact 切换到 React(或反向)时需重写所有测试的需求。 + +不过,还是有一些行为差异需要注意: + +- `shallow` 渲染模式在底层的工作方式不同。它在只渲染组件“一层深度”方面与 React 一致,但与 React 不同的是它会创建真实的 DOM 节点,并且会运行所有常规的生命周期钩子和副作用。 +- `simulate` 方法会派发真实的 DOM 事件,而在 React 的适配器中,`simulate` 只是调用对应的 `on` 属性。 +- 在 Preact 中,状态更新(例如调用 `setState` 后)会被合并并异步应用。React 中状态更新可能会立即应用或根据上下文被批处理。为了简化测试,Preact 适配器会在初次渲染以及通过 `setProps` 或 `simulate` 触发的更新后刷新状态更新和副作用。当状态更新或副作用是通过其他方式触发时,测试代码可能需要使用 `preact/test-utils` 中的 `act` 手动触发刷新。 + +如需更多细节,请参阅 Preact 适配器的 [README](https://github.com/preactjs/enzyme-adapter-preact-pure#differences-compared-to-enzyme--react)。 diff --git a/content/zh/guide/v11/upgrade-guide.md b/content/zh/guide/v11/upgrade-guide.md new file mode 100644 index 000000000..6dfc308c1 --- /dev/null +++ b/content/zh/guide/v11/upgrade-guide.md @@ -0,0 +1,286 @@ +--- +title: 从 Preact 10.x 升级 +description: 将现有的 Preact 10.x 应用升级到 Preact 11 的指南 +--- + +# 从 Preact 10.x 升级 + +Preact 11 的目标是在尽量减少破坏性的前提下从 Preact 10.x 升级,因此我们可以提高所支持的浏览器版本并清除一些遗留代码。对大多数用户而言,此次升级应当简单快速,只有少数更改可能需要关注。 + +本文档旨在引导你将现有的 Preact 10.x 应用迁移到 Preact 11,涵盖可能存在的破坏性更改以及确保平滑迁移的步骤。 + +--- + + + +--- + +## 为应用做准备 + +### 支持的浏览器版本 + +Preact 11.x 默认在以下浏览器上工作,无需额外的 polyfill: + +- Chrome >= 40 +- Safari >= 9 +- Firefox >= 36 +- Edge >= 12 + +如果你需要支持更老的浏览器,则需要自行引入 polyfill。 + +### 支持的 TypeScript 版本 + +11.x 版本线将把 TypeScript 的最低支持版本提高到 v5.1。如果你现在使用的是较旧的 TypeScript,请在升级到 Preact 11 之前先升级 TypeScript。 + +提高最低 TypeScript 版本可以利用 TS 团队在 JSX 类型方面的重要改进,从而修复一些长期存在且较难在库内部解决的类型问题。 + +### ESM 打包产物使用 `.mjs` 后缀 + +Preact 11.x 会将所有 ESM 包以 `.mjs` 扩展名分发,移除 10.x 中的 `.module.js` 副本。这有助于修正部分工具链遇到的问题,并简化分发包。 + +CJS 与 UMD 包将继续提供,且保持不变。 + +## 新特性 + +### Hydration 2.0 + +Preact 11 在 hydration(服务器端渲染后的客户端恢复)方面带来显著改进,特别是在处理挂起(suspending)组件时。相比 Preact X 需要在每个异步边界总是返回恰好 1 个 DOM 节点的限制,Preact 11 允许返回 0 个或 2 个及以上 DOM 节点,从而支持更灵活的组件设计。 + +下面的示例在 Preact 11 中现在是合法的: + +```jsx +function X() { + // 一些懒加载操作,例如初始化分析工具 + return null; +} + +const LazyOperation = lazy(() => /* import X */); +``` + +```jsx +function Y() { + // 渲染后 `` 会被移除,留下两个 `` DOM 元素 + return ( + + Foo + Bar + + ); +} + +const SuspendingMultipleChildren = lazy(() => /* import Y */); +``` + +关于已知问题的更详细说明以及我们的解决方案,请参阅 [RFC: Hydration 2.0 (preactjs/preact#4442)](https://github.com/preactjs/preact/issues/4442)。 + +### 钩子参数使用 `Object.is` 进行相等性判断 + +Preact 11 在钩子(hooks)参数的相等性判断中使用 `Object.is`,更接近 React 的行为。这意味着现在可以将 `NaN` 用作状态值或 `useEffect`/`useMemo`/`useCallback` 的依赖项。 + +在 Preact 10 中,下面的例子在每次点击按钮时都会触发重新渲染,而在 Preact 11 中则不会: + +```jsx +import { useState, useEffect } from 'preact/hooks'; + +function App() { + const [count, setCount] = useState(0); + + return setCount(NaN)}>Set count to NaN; +} +``` + +## API 变更 + +### Ref 默认会被转发 + +现在 Ref 默认会被转发,可以像普通 prop 一样使用它们。你不再需要通过 `preact/compat` 的 `forwardRef` 来实现这一功能。 + +```jsx +function MyComponent({ ref }) { + return Hello, world!; +} + +; +// Preact 10: myRef.current 是 MyComponent 的实例 +// Preact 11: myRef.current 是 DOM 元素 +``` + +> 注意:当使用 `preact/compat` 时,refs 不会被转发到类组件。React 只将 refs 转发给函数组件,因此我们在 compat 层保持一致。 +> +> 对于纯 Preact 的使用者,refs 会被转发到类组件,与函数组件行为一致。 + +如果你需要继续使用旧行为,可以使用以下代码片段将行为恢复为 Preact 10: + +```js +import { options } from 'preact'; + +const oldVNode = options.vnode; +options.vnode = vnode => { + if (vnode.props && vnode.props.ref) { + vnode.ref = vnode.props.ref; + delete vnode.props.ref; + } + + if (oldVNode) oldVNode(vnode); +}; +``` + +### 将数值样式自动添加 `px` 的行为移到 `preact/compat` + +Preact 11 已将对数值类型样式属性自动添加 `px` 的逻辑从核心库移动到 `preact/compat`。 + +```jsx +Hello World! +// Preact 10: Hello World! +// Preact 11: Hello World! +``` + +### 将 `defaultProps` 支持移动到 `preact/compat` + +由于函数组件与 Hook 的普及,`defaultProps` 使用频率下降,因此该支持已移入 `preact/compat`。 + +### 从 `render()` 中移除 `replaceNode` 参数 + +`render()` 的第三个(可选)参数在 Preact 11 中被移除,因为该实现存在许多 bug 和边缘情况,且无法很好地满足某些关键用例。 + +如果你仍然需要此功能,我们提供了一个与 Preact 10 兼容的独立实现:[`preact-root-fragment`](https://github.com/preactjs/preact-root-fragment)。 + +```html + + Widget A + Widget B + Widget C + +``` + +```jsx +// Preact 10 +import { render } from 'preact'; + +render(, root, widgetC); + +// Preact 11 +import { render } from 'preact'; +import { createRootFragment } from 'preact-root-fragment'; + +render(, createRootFragment(root, widgetC)); +``` + +### 移除 `Component.base` 属性 + +我们将移除 `Component.base`,因为暴露组件所连接的 DOM 总显得有些泄露实现细节。 + +如果你仍然需要访问该 DOM,可以使用 `this.__v.__e`;其中 `.__v` 是组件关联的 VNode(经过混淆的属性名),`.__e` 则是该 VNode 关联的 DOM 节点。 + +### 从 `preact/compat` 移除 `SuspenseList` + +该功能的实现和服务端支持一直不够清晰和完整,因而我们决定移除它。 + +### 类型相关变更 + +#### `useRef` 现在需要初始值 + +与 React 19 中的更改类似,我们将 `useRef` 的类型签名改为需要提供初始值。提供初始值有助于类型推断并避免一些类型上的问题。 + +#### `JSX` 命名空间收缩 + +TypeScript 使用特殊的 `JSX` 命名空间来改变 JSX 的类型和解释方式。在 10 版本中,我们大幅扩展了该命名空间以包含多种有用类型,但其中许多更适合放到 `preact` 命名空间。 + +从 Preact 11 开始,`JSX` 命名空间将只包含 TypeScript 所需的基本类型(例如 `Element`、`IntrinsicElements` 等),其余类型将迁移到 `preact` 命名空间。这也有助于编辑器和 IDE 在自动导入提示时更好地解析类型。 + +```ts +// Preact 10 +import { JSX } from 'preact'; + +type MyCustomButtonProps = JSX.ButtonHTMLAttributes & { + /* ... */ +}; + +// Preact 11 +import { ButtonHTMLAttributes } from 'preact'; + +type MyCustomButtonProps = ButtonHTMLAttributes & { + /* ... */ +}; +``` + +```` + +### 将数值样式自动添加 `px` 的行为移到 `preact/compat` + +Preact 11 已将对数值类型样式属性自动添加 `px` 的逻辑从核心库移动到 `preact/compat`。 + +```jsx +Hello World! +// Preact 10: Hello World! +// Preact 11: Hello World! +```` + +### 将 `defaultProps` 支持移动到 `preact/compat` + +由于函数组件与 Hook 的普及,`defaultProps` 使用频率下降,因此该支持已移入 `preact/compat`。 + +### 从 `render()` 中移除 `replaceNode` 参数 + +`render()` 的第三个(可选)参数在 Preact 11 中被移除,因为该实现存在许多 bug 和边缘情况,且无法很好地满足某些关键用例。 + +如果你仍然需要此功能,我们提供了一个与 Preact 10 兼容的独立实现:[`preact-root-fragment`](https://github.com/preactjs/preact-root-fragment)。 + +```html + + Widget A + Widget B + Widget C + +``` + +```jsx +// Preact 10 +import { render } from 'preact'; + +render(, root, widgetC); + +// Preact 11 +import { render } from 'preact'; +import { createRootFragment } from 'preact-root-fragment'; + +render(, createRootFragment(root, widgetC)); +``` + +### 移除 `Component.base` 属性 + +我们将移除 `Component.base`,因为暴露组件所连接的 DOM 总显得有些泄露实现细节。 + +如果你仍然需要访问该 DOM,可以使用 `this.__v.__e`;其中 `.__v` 是组件关联的 VNode(经过混淆的属性名),`.__e` 则是该 VNode 关联的 DOM 节点。 + +### 从 `preact/compat` 移除 `SuspenseList` + +该功能的实现和服务端支持一直不够清晰和完整,因而我们决定移除它。 + +### 类型相关变更 + +#### `useRef` 现在需要初始值 + +与 React 19 中的更改类似,我们将 `useRef` 的类型签名改为需要提供初始值。提供初始值有助于类型推断并避免一些类型上的问题。 + +#### `JSX` 命名空间收缩 + +TypeScript 使用特殊的 `JSX` 命名空间来改变 JSX 的类型和解释方式。在 10 版本中,我们大幅扩展了该命名空间以包含多种有用类型,但其中许多更适合放到 `preact` 命名空间。 + +从 Preact 11 开始,`JSX` 命名空间将只包含 TypeScript 所需的基本类型(例如 `Element`、`IntrinsicElements` 等),其余类型将迁移到 `preact` 命名空间。这也有助于编辑器和 IDE 在自动导入提示时更好地解析类型。 + +```ts +// Preact 10 +import { JSX } from 'preact'; + +type MyCustomButtonProps = JSX.ButtonHTMLAttributes & { + /* ... */ +}; + +// Preact 11 +import { ButtonHTMLAttributes } from 'preact'; + +type MyCustomButtonProps = ButtonHTMLAttributes & { + /* ... */ +}; +``` diff --git a/content/zh/guide/v11/web-components.md b/content/zh/guide/v11/web-components.md new file mode 100644 index 000000000..cf4b079fa --- /dev/null +++ b/content/zh/guide/v11/web-components.md @@ -0,0 +1,198 @@ +--- +title: Web Components +description: How to use web components with Preact +--- + +# Web Components + +Preact 的微小体积和标准优先的方法使其成为构建 web components 的绝佳选择。 + +Web Components 是一套标准,使得构建新的 HTML 元素类型成为可能 - 自定义元素如 `` 或 ``。 +Preact [完全支持这些标准](https://custom-elements-everywhere.com/#preact),允许无缝使用自定义元素的生命周期、属性和事件。 + +Preact 被设计为可以渲染完整应用和页面的独立部分,使其自然适合构建 Web Components。许多公司使用它来构建组件或设计系统,然后将其封装成一组 Web Components,从而实现跨多个项目和其他框架的重用。 + +Preact 和 Web Components 是互补的技术:Web Components 提供了一组用于扩展浏览器的低级原语,而 Preact 提供了可以建立在这些原语之上的高级组件模型。 + +--- + + + +--- + +## 渲染 Web Components + +在 Preact 中,web components 的工作方式就像其他 DOM 元素一样。它们可以使用其注册的标签名进行渲染: + +```jsx +customElements.define( + 'x-foo', + class extends HTMLElement { + // ... + } +); + +function Foo() { + return ; +} +``` + +### 属性和特性 + +JSX 不提供区分属性(properties)和特性(attributes)的方式。自定义元素通常依赖于自定义属性,以支持设置无法通过特性表达的复杂值。这在 Preact 中运行良好,因为渲染器通过检查受影响的 DOM 元素自动确定是使用属性还是特性设置值。当自定义元素为给定属性定义了[设置器](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set)时,Preact 会检测其存在并使用设置器而不是特性。 + +```jsx +customElements.define( + 'context-menu', + class extends HTMLElement { + set position({ x, y }) { + this.style.cssText = `left:${x}px; top:${y}px;`; + } + } +); + +function Foo() { + return ... ; +} +``` + +> **注意:** Preact 不对命名方案做任何假设,也不会尝试强制转换 JSX 或其他方式中的名称为 DOM 属性。如果自定义元素有一个属性名 `someProperty`,则需要使用 `someProperty=...` 而不是 `some-property=...` 来设置它。 + +当使用 `preact-render-to-string`("SSR")渲染静态 HTML 时,复杂的属性值如上面的对象不会自动序列化。它们在客户端对静态 HTML 进行水合后应用。 + +### 访问实例方法 + +要能够访问自定义 web 组件的实例,我们可以利用 `refs`: + +```jsx +function Foo() { + const myRef = useRef(null); + + useEffect(() => { + if (myRef.current) { + myRef.current.doSomething(); + } + }, []); + + return ; +} +``` + +### 触发自定义事件 + +Preact 规范化了标准内置 DOM 事件的大小写,这些事件通常是区分大小写的。这就是为什么可以向 `` 传递 `onChange` 属性,尽管实际的事件名称是 `"change"`。自定义元素经常触发自定义事件作为其公共 API 的一部分,但是无法知道可能会触发什么自定义事件。为了确保在 Preact 中无缝支持自定义元素,传递给 DOM 元素的无法识别的事件处理程序属性会使用其完全按指定的大小写进行注册。 + +```jsx +// 内置 DOM 事件:监听 "click" 事件 + console.log('click')} /> + +// 自定义元素:监听 "TabChange" 事件(区分大小写!) + console.log('tab change')} /> + +// 修正:监听 "tabchange" 事件(小写) + console.log('tab change')} /> +``` + +## 创建 Web Component + +任何 Preact 组件都可以通过 [preact-custom-element](https://github.com/preactjs/preact-custom-element) 转变为 web 组件,这是一个非常轻量级的包装器,遵循自定义元素 v1 规范。 + +```jsx +import register from 'preact-custom-element'; + +const Greeting = ({ name = 'World' }) => Hello, {name}!; + +register(Greeting, 'x-greeting', ['name'], { shadow: false }); +// ^ ^ ^ ^ +// | HTML 标签名 | 使用 shadow-dom +// 组件定义 观察的属性 +``` + +> 注意:根据[自定义元素规范](http://w3c.github.io/webcomponents/spec/custom/#prod-potentialcustomelementname),标签名必须包含连字符(`-`)。 + +在 HTML 中使用新的标签名,属性键和值将作为 props 传递: + +```html + +``` + +输出: + +```html +Hello, Billy Jo! +``` + +### 观察的属性 + +Web Components 要求明确列出你想要观察的属性名称,以便在它们的值发生变化时做出响应。这些可以通过传递给 `register()` 函数的第三个参数指定: + +```jsx +// 监听 `name` 属性的变化 +register(Greeting, 'x-greeting', ['name']); +``` + +如果你省略 `register()` 的第三个参数,要观察的属性列表可以使用组件上的静态 `observedAttributes` 属性指定。自定义元素的名称也可以使用静态 `tagName` 属性指定: + +```jsx +import register from 'preact-custom-element'; + +// +class Greeting extends Component { + // 注册为 : + static tagName = 'x-greeting'; + + // 跟踪这些属性: + static observedAttributes = ['name']; + + render({ name }) { + return Hello, {name}!; + } +} +register(Greeting); +``` + +如果没有指定 `observedAttributes`,如果组件上存在 `propTypes`,它们将从 `propTypes` 的键中推断: + +```jsx +// 另一种选择:使用 PropTypes: +function FullName({ first, last }) { + return ( + + {first} {last} + + ); +} + +FullName.propTypes = { + first: Object, // 你可以使用 PropTypes,或者这个 + last: Object // 技巧来定义无类型的属性。 +}; + +register(FullName, 'full-name'); +``` + +### 将插槽作为属性传递 + +`register()` 函数有第四个参数用于传递选项;目前,只支持 `shadow` 选项,它将 shadow DOM 树附加到指定的元素。启用时,这允许使用命名的 `` 元素将自定义元素的子元素转发到 shadow 树中的特定位置。 + +```jsx +function TextSection({ heading, content }) { + return ( + + {heading} + {content} + + ); +} + +register(TextSection, 'text-section', [], { shadow: true }); +``` + +用法: + +```html + + 漂亮的标题 + 很棒的内容 + +```
你好 {name}
+ 您{allowContact ? '已允许' : '尚未允许'}联系 + {allowContact && `,通过${this.state.contactMethod}`} +
+ 您{allowContact ? '已允许' : '尚未允许'}联系 + {allowContact && `,通过${contactMethod}`} +
您选择了: {value}
你好 {fullName}
Counter: {state.value}
Counter: {value}
Counter A: {value}
I'm a nice counter
Count: {count}
Active theme: {theme}
Window width: {width}
{error.message}
Count: {value}
{state.completed}
+ {count} x 2 = {double} +
{count.value}
{count}
Clicks: {clicks}
Your selected language: {lang}
` DOM 元素 + return ( + + Foo + Bar + + ); +} + +const SuspendingMultipleChildren = lazy(() => /* import Y */); +``` + +关于已知问题的更详细说明以及我们的解决方案,请参阅 [RFC: Hydration 2.0 (preactjs/preact#4442)](https://github.com/preactjs/preact/issues/4442)。 + +### 钩子参数使用 `Object.is` 进行相等性判断 + +Preact 11 在钩子(hooks)参数的相等性判断中使用 `Object.is`,更接近 React 的行为。这意味着现在可以将 `NaN` 用作状态值或 `useEffect`/`useMemo`/`useCallback` 的依赖项。 + +在 Preact 10 中,下面的例子在每次点击按钮时都会触发重新渲染,而在 Preact 11 中则不会: + +```jsx +import { useState, useEffect } from 'preact/hooks'; + +function App() { + const [count, setCount] = useState(0); + + return setCount(NaN)}>Set count to NaN; +} +``` + +## API 变更 + +### Ref 默认会被转发 + +现在 Ref 默认会被转发,可以像普通 prop 一样使用它们。你不再需要通过 `preact/compat` 的 `forwardRef` 来实现这一功能。 + +```jsx +function MyComponent({ ref }) { + return
Foo
Bar
Hello, {name}!
Hello, Billy Jo!
{content}