From 73d3ccdb5e384ef8d529b4d931b41de7ea907bf6 Mon Sep 17 00:00:00 2001 From: Zhi Date: Wed, 3 Sep 2025 22:54:48 +0800 Subject: [PATCH 1/5] Updated the frontmatter regex to accept both LF and CRLF line endings, improving compatibility with Windows environments. --- src/lib/frontmatter.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/frontmatter.js b/src/lib/frontmatter.js index e5cd8062c..796f848ba 100644 --- a/src/lib/frontmatter.js +++ b/src/lib/frontmatter.js @@ -7,7 +7,8 @@ import yaml from 'yaml'; * @returns {{title: string, description: string, body: string, meta: object}} */ export function parseFrontmatter(content, path = '') { - const FRONT_MATTER_REG = /^\s*---\n\s*([\s\S]*?)\s*\n---\n/i; + // Accept both LF and CRLF line endings so frontmatter is detected on Windows + const FRONT_MATTER_REG = /^\s*---\r?\n\s*([\s\S]*?)\s*\r?\n---\r?\n/i; const matches = content.match(FRONT_MATTER_REG); if (!matches) { From 3d763bbc5b663176b1d868d0a726da08e6bf351e Mon Sep 17 00:00:00 2001 From: Zhi Date: Thu, 4 Sep 2025 09:19:14 +0800 Subject: [PATCH 2/5] Introduces a comprehensive Chinese-language guide for getting started with Preact, covering no-build workflows, Vite setup, and integration with popular build tools. Includes instructions for JSX alternatives, aliasing React to Preact, and configuration examples for Webpack, Parcel, Rollup, Jest, TypeScript, and import maps. --- content/zh/guide/v11/getting-started.md | 275 ++++++++++++++++++++++++ 1 file changed, 275 insertions(+) create mode 100644 content/zh/guide/v11/getting-started.md 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)以获取更多示例。 From 0cdb7981b7f31fe8827e30a54fd5e1a224fb9011 Mon Sep 17 00:00:00 2001 From: Zhi Date: Thu, 18 Sep 2025 00:09:02 +0800 Subject: [PATCH 3/5] Add Chinese Preact v11 guide documentation --- content/zh/guide/v11/api-reference.md | 202 +++++++ content/zh/guide/v11/components.md | 203 +++++++ content/zh/guide/v11/context.md | 202 +++++++ content/zh/guide/v11/debugging.md | 221 +++++++ content/zh/guide/v11/differences-to-react.md | 224 +++++++ content/zh/guide/v11/forms.md | 452 +++++++++++++++ content/zh/guide/v11/hooks.md | 506 ++++++++++++++++ content/zh/guide/v11/no-build-workflows.md | 143 +++++ content/zh/guide/v11/options.md | 93 +++ content/zh/guide/v11/preact-iso.md | 424 ++++++++++++++ .../zh/guide/v11/preact-testing-library.md | 253 ++++++++ content/zh/guide/v11/refs.md | 232 ++++++++ content/zh/guide/v11/server-side-rendering.md | 142 +++++ content/zh/guide/v11/signals.md | 548 ++++++++++++++++++ content/zh/guide/v11/switching-to-preact.md | 142 +++++ content/zh/guide/v11/typescript.md | 163 ++++++ .../zh/guide/v11/unit-testing-with-enzyme.md | 164 ++++++ content/zh/guide/v11/upgrade-guide.md | 240 ++++++++ content/zh/guide/v11/web-components.md | 198 +++++++ content/zh/guide/v11/whats-new.md | 135 +++++ 20 files changed, 4887 insertions(+) create mode 100644 content/zh/guide/v11/api-reference.md create mode 100644 content/zh/guide/v11/components.md create mode 100644 content/zh/guide/v11/context.md create mode 100644 content/zh/guide/v11/debugging.md create mode 100644 content/zh/guide/v11/differences-to-react.md create mode 100644 content/zh/guide/v11/forms.md create mode 100644 content/zh/guide/v11/hooks.md create mode 100644 content/zh/guide/v11/no-build-workflows.md create mode 100644 content/zh/guide/v11/options.md create mode 100644 content/zh/guide/v11/preact-iso.md create mode 100644 content/zh/guide/v11/preact-testing-library.md create mode 100644 content/zh/guide/v11/refs.md create mode 100644 content/zh/guide/v11/server-side-rendering.md create mode 100644 content/zh/guide/v11/signals.md create mode 100644 content/zh/guide/v11/switching-to-preact.md create mode 100644 content/zh/guide/v11/typescript.md create mode 100644 content/zh/guide/v11/unit-testing-with-enzyme.md create mode 100644 content/zh/guide/v11/upgrade-guide.md create mode 100644 content/zh/guide/v11/web-components.md create mode 100644 content/zh/guide/v11/whats-new.md diff --git a/content/zh/guide/v11/api-reference.md b/content/zh/guide/v11/api-reference.md new file mode 100644 index 000000000..2c8f1c02c --- /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

Hello, {props.name}!

; + } +} +``` + +请参阅[组件文档](/guide/v10/components)来了解其概念及使用方法。 + +## render() + +`render(virtualDom, containerNode, [replaceNode])` + +将虚拟 DOM 元素渲染为父 DOM 元素的 `containerNode`,无返回值。 + +```jsx +// --repl +// 渲染前的 DOM 树: +//
+ +import { render } from 'preact'; + +const Foo = () =>
foo
; + +render(, document.getElementById('container')); + +// 渲染后: +//
+//
foo
+//
+``` + +首个参数必须为有效的虚拟 DOM 元素,可为组件或是普通元素。当您传入组件时,您需要让 Preact 实例化它,而不是直接调用组件。否则,这会出现问题: + +```jsx +const App = () =>
foo
; + +// 不要:直接调用组件会破坏钩子和更新顺序: +render(App(), rootElement); // 错误 +render(App, rootElement); // 错误 + +// 要:使用 h() 或 JSX 向 Preact 传入组件才能正确渲染: +render(h(App), rootElement); // 成功 +render(, rootElement); // 成功 +``` + +## hydrate() + +若您已经通过预渲染或服务端渲染的方式将应用转换为 HTML,Preact 可以在浏览器加载时跳过大部分渲染流程。您可以通过将 `render()` 替换为 `hydrate()` 的方式跳过大部分差异对比流程,而事件监听器和组件树仍能正常使用。此函数仅能与预渲染或[服务端渲染](/guide/v10/server-side-rendering)搭配使用。 + +```jsx +// --repl +import { hydrate } from 'preact'; + +const Foo = () =>
foo
; +hydrate(, document.getElementById('container')); +``` + +## h() / createElement() + +`h(type, props, ...children)` + +返回属性为 `props` 的虚拟 DOM 元素。虚拟 DOM 属性是您应用 UI 树中的轻量级节点描述,是形似 `{ type, props }` 的对象。 + +在 `type` 和 `props` 参数之后,其他所有参数均会转化为 `children` 子元素属性。 +子元素可以为: + +- 标量值 (string、number、boolean、null、undefined 等等) +- 嵌套的虚拟 DOM 元素 +- 无限嵌套的上述类型 Array + +```js +import { h } from 'preact'; + +h('div', { id: 'foo' }, 'Hello!'); +//
Hello!
+ +h('div', { id: 'foo' }, 'Hello', null, ['Preact!']); +//
Hello Preact!
+ +h('div', { id: 'foo' }, h('span', null, 'Hello!')); +//
Hello!
+``` + +## toChildArray + +此辅助函数将 `props.children` 值转化为一维 Array 数组。若 `props.children` 已是数组,此函数将返回其复制值。此函数适合在 `props.children` 里同时存在 JSX 静态和动态表达式时使用。 + +对于只有一个子元素的虚拟 DOM 元素而言,`props.children` 是其子元素的引用。当有多个子元素存在时,`props.children` 是一个 Array。`toChildArray` 辅助函数能帮您处理所有情况,返回统一的值。 + +```jsx +import { toChildArray } from 'preact'; + +function Foo(props) { + const count = toChildArray(props.children).length; + return
I have {count} children
; +} + +// props.children 是 "bar" +render(bar, container); + +// props.children 是 [

A

,

B

] +render( + +

A

+

B

+
, + container +); +``` + +## cloneElement + +`cloneElement(virtualElement, props, ...children)` + +此函数用于创建虚拟 DOM 元素的浅拷贝,通常在添加或覆盖组件 `props` 属性时使用: + +```jsx +function Linkout(props) { + // 为链接添加 target="_blank": + return cloneElement(props.children, { target: '_blank' }); +} +render( + + home + +); +// home +``` + +## createContext + +参见[上下文文档一节](/guide/v10/context#createcontext)。 + +## createRef + +提供渲染后引用元素或组件的方式。 + +参见[引用文档](/guide/v10/refs#createref)以了解详情。 + +## Fragment + +一种可包含子元素的特殊组件,但不渲染为 DOM 元素。片段无需您将多个子元素包裹在 DOM 容器中就能返回多个元素: + +```jsx +// --repl +import { Fragment, render } from 'preact'; + +render( + +
A
+
B
+
C
+
, + document.getElementById('container') +); +// 渲染结果: +//
; + +// 渲染结果:
我的名字叫张三。
+render(App, document.body); +``` + +> 请注意,在先前的版本中我们将其称之为`“无状态组件”`,但有了[钩子组件](/guide/v10/hooks)后就不是了。 + +## 类组件 + +类组件可以拥有状态及生命周期方法,后者是当组件添加到 DOM 或销毁时调用的特殊方法。 + +下面是一个显示当前时间的简单类组件 ``: + +```jsx +// --repl +import { Component, render } from 'preact'; + +// --repl-before +class Clock extends Component { + constructor() { + super(); + this.state = { time: Date.now() }; + } + + // 生命周期:在组件创建时调用 + componentDidMount() { + // 每秒钟更新一次时间 + this.timer = setInterval(() => { + this.setState({ time: Date.now() }); + }, 1000); + } + + // 生命周期:在组件销毁时调用 + componentWillUnmount() { + // 在无法渲染时停止时钟 + clearInterval(this.timer); + } + + render() { + let time = new Date(this.state.time).toLocaleTimeString(); + return {time}; + } +} +// --repl-after +render(, document.getElementById('app')); +``` + +### 生命周期方法 + +为了让时钟能每秒钟更新一次事件,我们需要知道 `` 什么时候会被挂载到 DOM 上。如果您用过 HTML5 自定义元素的话,您就会发现这和 `attachedCallback` 与 `detachedCallback` 生命周期方法很像。Preact 会自动为组件调用下列列表中存在的生命周期方法 : + +| 生命周期方法 | 被调用时间 | +| ---------------------------------------------------- | --------------------------------------------------------- | +| `componentWillMount()` | **(已弃用)** 组件将被挂载到 DOM 前调用 | +| `componentDidMount()` | 组件被挂载到 DOM 后调用 | +| `componentWillUnmount()` | 组件将从 DOM 移除前调用 | +| `componentWillReceiveProps(nextProps, nextState)` | **(已弃用)** 在传递进新属性前调用 | +| `getDerivedStateFromProps(nextProps)` | 在 `shouldComponentUpdate` 前调用,请小心使用! | +| `shouldComponentUpdate(nextProps, nextState)` | 在 `render()` 前调用,返回 `false` 来跳过渲染 | +| `componentWillUpdate(nextProps, nextState)` | **(已弃用)** 在 `render()` 前调用 | +| `getSnapshotBeforeUpdate(prevProps, prevState)` | 在 `render()` 前调用,返回值将传递进 `componentDidUpdate` | +| `componentDidUpdate(prevProps, prevState, snapshot)` | 在 `render()` 后调用 | + +这是它们之间关系的可视概览(源自 Dan Abramov 发布的[推文](https://web.archive.org/web/20191118010106/https://twitter.com/dan_abramov/status/981712092611989509)): + +![Diagram of component lifecycle methods](/guide/components-lifecycle-diagram.png) + +### 错误边界 + +错误边界是至少实现了 `componentDidCatch()` 和 `getDerivedStateFromError()` 两者之一的组件。这些特殊方法允许捕获渲染过程中发生的任何错误,通常用于提供更好的错误信息或默认内容并保存日志信息。需要注意的是,错误边界不能捕获所有的错误,事件处理程序和异步代码(如 `fetch()` 调用)需要单独处理。 + +当捕获到错误时,我们可以使用这些方法对错误做出响应并展示错误信息或默认内容。 + +```jsx +// --repl +import { Component, render } from 'preact'; +// --repl-before +class ErrorBoundary extends Component { + constructor() { + super(); + this.state = { errored: false }; + } + + static getDerivedStateFromError(error) { + return { errored: true }; + } + + componentDidCatch(error, errorInfo) { + errorReportingService(error, errorInfo); + } + + render(props, state) { + if (state.errored) { + return

Something went badly wrong

; + } + return props.children; + } +} +// --repl-after +render(, document.getElementById('app')); +``` + +## 片段 (Fragment) + +`Fragment` 允许一次返回多个元素,解决了 JSX 每个“代码块”只能有一个根元素的限制。你将会经常在列表、表格、flexbox 等中间元素会影响样式的情况遇到它。 + +```jsx +// --repl +import { Fragment, render } from 'preact'; + +function TodoItems() { + return ( + +
  • A
  • +
  • B
  • +
  • C
  • +
    + ); +} + +const App = ( +
      + +
    • D
    • +
    +); + +render(App, container); +// 渲染结果: +//
      +//
    • A
    • +//
    • B
    • +//
    • C
    • +//
    • D
    • +//
    +``` + +请注意,大部分现代转译器支持 `Fragments` 的简写语法,这种语法更为常见: + +```jsx +// 如下代码 +const Foo = foo; +// ...与下列代码等同: +const Bar = <>foo; +``` + +您也可以从组件中返回数组: + +```jsx +function Columns() { + return [Hello, World]; +} +``` + +若要在循环中创建 `Fragments`,别忘了为其添加键: + +```jsx +function Glossary(props) { + return ( +
    + {props.items.map(item => ( + // 没有键值的话,Preact 需要猜测哪些元素在重渲染时存在变化。 + +
    {item.term}
    +
    {item.description}
    +
    + ))} +
    + ); +} +``` diff --git a/content/zh/guide/v11/context.md b/content/zh/guide/v11/context.md new file mode 100644 index 000000000..fc9b190a6 --- /dev/null +++ b/content/zh/guide/v11/context.md @@ -0,0 +1,202 @@ +--- +title: 上下文 +description: 上下文允许您通过中间组件传递属性。本文档描述了新旧两种API +--- + +# 上下文 + +上下文是一种在组件树中传递数据的方式,无需通过 props 将其传递给中间的每个组件。简而言之,它允许层次结构中的任何位置的组件订阅一个值并在其变化时收到通知,为 Preact 带来发布-订阅风格的更新。 + +在某些情况下,需要将祖父组件(或更高层级)的值传递给子组件,而中间组件往往不需要这个值,这种情况并不少见。这种传递 props 的过程通常被称为"prop 钻取"(prop drilling),它可能会变得繁琐、容易出错,而且非常重复,尤其是随着应用程序的增长,更多的值必须通过更多的层级传递。这是上下文旨在解决的关键问题之一,它提供了一种方式让子组件订阅组件树中更高层级的值,无需通过 prop 传递就能访问该值。 + +在 Preact 中使用上下文有两种方式:通过较新的`createContext` API 和传统上下文 API。如今,几乎没有理由使用传统 API,但为了完整性这里也会进行记录。 + +--- + + + +--- + +## 现代上下文 API + +### 创建上下文 + +要创建新的上下文,我们使用`createContext`函数。此函数接受一个初始状态作为参数,并返回一个具有两个组件属性的对象:`Provider`,使上下文对后代可用;以及`Consumer`,用于访问上下文值(主要在类组件中)。 + +```jsx +import { createContext } from 'preact'; + +export const Theme = createContext('light'); +export const User = createContext({ name: 'Guest' }); +export const Locale = createContext(null); +``` + +### 设置 Provider + +创建上下文后,我们必须使用`Provider`组件使其对后代可用。`Provider`必须提供一个`value`属性,表示上下文的初始值。 + +> 只有在树中消费者上方没有`Provider`的情况下,才会使用从`createContext`设置的初始值。这对于单独测试组件可能很有帮助,因为它避免了在组件周围创建包装`Provider`的需要。 + +```jsx +import { createContext } from 'preact'; + +export const Theme = createContext('light'); + +function App() { + return ( + + + + ); +} +``` + +> **提示:** 您可以在整个应用程序中拥有同一上下文的多个 provider,但只会使用离消费者最近的那个。 + +### 使用上下文 + +消费上下文有两种方式,主要取决于您喜欢的组件风格:`Consumer`(类组件)和`useContext`钩子(函数组件/钩子)。 + + + +```jsx +// --repl +import { render, createContext } from 'preact'; + +const SomeComponent = props => props.children; +// --repl-before +const ThemePrimary = createContext('#673ab8'); + +function ThemedButton() { + return ( + + {theme => } + + ); +} + +function App() { + return ( + + + + + + ); +} +// --repl-after +render(, document.getElementById('app')); +``` + +```jsx +// --repl +import { render, createContext } from 'preact'; +import { useContext } from 'preact/hooks'; + +const SomeComponent = props => props.children; +// --repl-before +const ThemePrimary = createContext('#673ab8'); + +function ThemedButton() { + const theme = useContext(ThemePrimary); + return ; +} + +function App() { + return ( + + + + + + ); +} +// --repl-after +render(, document.getElementById('app')); +``` + + + +### 更新上下文 + +静态值可能有用,但更多时候,我们希望能够动态更新上下文值。为此,我们利用标准组件状态机制: + +```jsx +// --repl +import { render, createContext } from 'preact'; +import { useContext, useState } from 'preact/hooks'; + +const SomeComponent = props => props.children; +// --repl-before +const ThemePrimary = createContext(null); + +function ThemedButton() { + const { theme } = useContext(ThemePrimary); + return ; +} + +function ThemePicker() { + const { theme, setTheme } = useContext(ThemePrimary); + return ( + setTheme(e.currentTarget.value)} + /> + ); +} + +function App() { + const [theme, setTheme] = useState('#673ab8'); + return ( + + + + {' - '} + + + + ); +} +// --repl-after +render(, document.getElementById('app')); +``` + +## 传统上下文 API + +此 API 被视为传统 API,应在新代码中避免使用,它有已知问题,存在仅出于向后兼容性原因。 + +此 API 与新 API 之间的一个关键区别是,当子组件和提供者之间的组件通过`shouldComponentUpdate`中止渲染时,此 API 无法更新子组件。当这种情况发生时,子组件**将不会**接收到更新的上下文值,这通常会导致撕裂(部分 UI 使用新值,部分使用旧值)。 + +要通过上下文传递值,组件需要具有`getChildContext`方法,返回预期的上下文值。然后,后代可以通过函数组件中的第二个参数或类组件中的`this.context`访问上下文。 + +```jsx +// --repl +import { render } from 'preact'; + +const SomeOtherComponent = props => props.children; +// --repl-before +function ThemedButton(_props, context) { + return ; +} + +class App extends Component { + getChildContext() { + return { + theme: '#673ab8' + }; + } + + render() { + return ( +
    + + + +
    + ); + } +} +// --repl-after +render(, document.getElementById('app')); +``` diff --git a/content/zh/guide/v11/debugging.md b/content/zh/guide/v11/debugging.md new file mode 100644 index 000000000..f20f25b69 --- /dev/null +++ b/content/zh/guide/v11/debugging.md @@ -0,0 +1,221 @@ +--- +title: 调试 Preact 应用 +description: 如何调试 Preact 应用。 +--- + +# 调试 Preact 应用 + +Preact 自带一系列方便您调试的工具,您可以通过导入 `preact/debug` 包来使用。 + +我们为 Chrome 和 Firefox 提供 [Preact 开发工具]扩展程序,集成您的应用开发。 + +我们还会在错误发生时 (如 `` 嵌套出错时) 输出警告信息。 + +--- + + + +--- + +## 安装 + +您可在您浏览器的扩展程序商店中下载安装 [Preact 开发工具]。 + +- [Chrome 版](https://chrome.google.com/webstore/detail/preact-developer-tools/ilcajpmogmhpliinlbcdebhbcanbghmd) +- [Firefox 版](https://addons.mozilla.org/en-US/firefox/addon/preact-devtools/) +- [Edge 版](https://microsoftedge.microsoft.com/addons/detail/hdkhobcafnfejjieimdkmjaiihkjpmhk) + +安装后,您需要在您的代码中导入 `preact/debug` 包来初始化与扩展的连接。请确保此包为您整个应用程序中第一个导入的包。 + +> 如果你在使用 `@preact/preset-vite`,它会自动添加 `preact/debug` 包,你可以直接跳过安装和从生产中剥离的步骤! + +下面是您的应用入口文件可能的样子: + +```jsx +// 必须为第一个导入的包 +import 'preact/debug'; +import { render } from 'preact'; +import App from './components/App'; + +render(, document.getElementById('root')); +``` + +### 从生产环境中移除开发工具 + +大多数打包工具会在检测到 `if` 中存在不可达的分支时自动为您去除代码。我们仅会在开发环境中导入 `preact/debug` 包,在生产环境中则会使用此方法来删除此包、削减空间。 + +```jsx +// 必须为第一个导入的包 +if (process.env.NODE_ENV === 'development') { + // 只能在此处使用 require 语句,import 语句仅支持顶级模块。 + require('preact/debug'); +} + +import { render } from 'preact'; +import App from './components/App'; + +render(, document.getElementById('root')); +``` + +请确保您的构建工具为 `NODE_ENV` 变量设置了正确的值。 + +## 调试警告和错误 + +有时候您可能会在 Preact 检测到无效代码时遇到警告或错误,这些是为了确保您的应用能完美工作而发出的。 + +### `undefined` 父元素被传递进 `render()` + +这意味着您的代码尝试渲染空内容,而非 DOM 节点,其区别在于: + +```jsx +// Preact 实际收到的 +render(, undefined); + +// 和您期望的 +render(, actualDomNode); +``` + +这个错误发生的主要原因是 DOM 节点在 `render()` 调用时暂不存在,请确保其存在后再调用。 + +### `undefined` 组件被传递至 `createElement()` + +Preact 会在您传递 `undefined` 而非组件时抛出此错误,其常见原因是您混用了 `default` 和命名导出。 + +```jsx +// app.js +export default function App() { + return
    Hello World
    ; +} + +// index.js:错误,因为 `app.js` 没有命名导出 +import { App } from './app'; +render(, dom); +``` + +当您声明命名导出,并尝试将其作为 `default` 导出使用时此错误也会发生。您可以输出导入结果来快速检查 (假如您的编辑器没有自动为您检查的话): + +```jsx +// app.js +export function App() { + return
    Hello World
    ; +} + +// index.js +import App from './app'; + +console.log(App); +// 日志:{ default: [Function] } 而非组件 +``` + +### 两次将 JSX 字面值传递为 JSX + +再次将 JSX 字面值或组件传递进 JSX 是无效的,将触发如下错误。 + +```jsx +const Foo =
    foo
    ; +// 无效,Foo 已包含 JSX 元素 +render(, dom); +``` + +要修复此问题,我们可以直接传递变量。 + +```jsx +const Foo =
    foo
    ; +render(Foo, dom); +``` + +### 检测到表格中存在错误嵌套 + +HTML 对表格的结构有着严格规则,违反其中一条都会导致渲染错误,且很难调试。在 Preact 中,我们会检测此问题并输出错误。要了解表格结构,我们强烈推荐您参阅 [MDN 文档](https://developer.mozilla.org/zh-CN/docs/Learn/HTML/Tables/Basics) + +> **注意:** 在这个语境中,“严格模式” 指的是 HTML 解析器的 输出,而非 输入。浏览器通常非常宽容,会尽可能尝试修正无效的 HTML 代码,以确保页面仍能正常显示。然而,对于 Preact 这类虚拟 DOM 库而言,这可能会引发问题。因为一旦浏览器修正了 HTML 代码,输入内容与输出内容可能会不一致,而 Preact 对此并不知情。 +> +> 例如,根据规范,`
    ` 元素必须始终作为 ``、`` 或 `` 元素的子元素存在。但如果你直接将 `` 写在 `
    ` 内部,浏览器会尝试自动修正这一问题,为其添加一个 `` 包装。此时,Preact 预期的 DOM 结构是 `
    `,但浏览器实际构建的 DOM 结构却是 `
    `。 + +### 无效 `ref` 属性 + +当 `ref` 属性包含异常值时我们将抛出此错误。这包括很久前即启用的字符串型 `refs`。 + +```jsx +// 有效 +
    {/* ... */)}} /> + +// 有效 +const ref = createRef(); +
    + +// 无效 +
    +``` + +### 无效事件处理程序 + +有些时候,您可能不小心传递了错误的事件处理程序。您必须传入 `function`,或传入 `null` 来移除,其他所有类型均无效。 + +```jsx +// 有效 +
    console.log("click")} /> + +// 无效 +
    +``` + +### 仅渲染方法可调用钩子 + +此错误会在您尝试在组件外使用钩子函数时发生,它们仅支持函数组件。 + +```jsx +// 无效,必须在组件内使用 +const [value, setValue] = useState(0); + +// 有效 +function Foo() { + const [value, setValue] = useState(0); + return ; +} +``` + +### 已弃用获取 `vnode.[property]` + +Preact X 中,我们对内部的 `vnode` 结构做出了重大变更。 + +| Preact 8.x | Preact 10.x | +| ------------------ | ---------------------- | +| `vnode.nodeName` | `vnode.type` | +| `vnode.attributes` | `vnode.props` | +| `vnode.children` | `vnode.props.children` | + +### 发现具有相同键的子元素 + +基于虚拟 DOM 的库一大独特性质是需要检测子元素何时被移动。但要知道何时被移动,我们先要标记它们。**注意,您仅需要在动态创建子元素时注意这点。** + +```jsx +// 两个子元素都会有相同键值 "A" +
    + {['A', 'A'].map(char => ( +

    {char}

    + ))} +
    +``` + +正确的方法是为元素提供唯一键值。大多数情况下,您所遍历的数据会有一个 `id` 键。 + +```jsx +const persons = [ + { name: '张三', age: 22 }, + { name: '李四', age: 24 } +]; + +// 您组件之后的操作 +
    + {persons.map(({ name, age }) => { + return ( +

    + {name}, Age: {age} +

    + ); + })} +
    ; +``` + +[preact 开发工具]: https://preactjs.github.io/preact-devtools/ diff --git a/content/zh/guide/v11/differences-to-react.md b/content/zh/guide/v11/differences-to-react.md new file mode 100644 index 000000000..e8102d86a --- /dev/null +++ b/content/zh/guide/v11/differences-to-react.md @@ -0,0 +1,224 @@ +--- +title: 与 React 的差别 +description: 本文详细说明了 Preact 和 React 之间的差别 +--- + +# 与 React 的差别 + +Preact 本身并没有去重新实现一遍 React。它们之间有些不同之处。但大部份差别都是很细微的,而且可以通过一个轻量的 [preact/compat] Preact 层实现 100% 兼容 React。 + +Preact 并没有去尝试实现 React 的每一个特性,是因为它想保持**轻量**而**专注**—— 不然给 React 项目提交优化方案会更为明智和简单,而 React 本身也已经是一个非常复杂和良好设计的代码库。 + +--- + + + +--- + +## 主要差别 + +Preact 与 React 的主要差别是 Preact 并没有实现一个为了大小和性能而去实现合成事件系统(synthetic event system)。Preact 是使用浏览器标准 `addEventListener` 去注册事件函数,这意味着 Preact 中的事件名称和行为都是和原生 JavaScript/ DOM 行为一致的。参考 [MDN's Event Reference] 来了解所有的 DOM 事件句柄. + +标准浏览器事件的工作方式与 React 中事件的工作方式非常相似,但有一些细微的差别。在 Preact 中: + +- 事件不会冒泡到 `` 组件 +- 在表单输入中时应当使用标准的 `onInput` 来代替 React 的 `onChange` (**仅当不使用 `preact/compat` 的时候**) +- 应当用 `onDblClick` 来代替 React 的 `onDoubleClick` (**仅当不使用 `preact/compat` 的时候**) +- `` 应当使用 `onSearch`,因为在 IE11 上 "x" 按钮并不支持 `onInput` + +另一个显著的区别是 Preact 更严格地遵循 DOM 规范。支持像任何其他元素一样的自定义元素,并且支持区分大小写的自定义事件名称(就像它们在 DOM 中一样)。 + +## 版本兼容 + +对于 preact 和 [preact/compat], 版本兼容通过*当前*和*之前*的 React 主要版本去衡量。当 React 团队公布新的特性的时候,若符合 [Project Goals] 它们可能会被添加到 Preact 的核心代码当中。这是一个相对民主的迭代过程,我们会持续使用 issues 和 pull request 来公开、持续进行讨论和决策。 + +> 因此当讨论兼容性和作对比的时候,官网和文档会指明 React `15.x` 和 `17.x`。 + +## 调试信息和错误 + +我们灵活的架构允许插件以任何他们想要的方式增强 Preact 体验。其中一个插件 `preact/debug` 添加了 [帮助性的警告和错误信息](/guide/v10/debugging)且附加了 [Preact Developer Tools](https://preactjs.github.io/preact-devtools/) 浏览器插件。这些能帮助你在开发 Preact 应用时更容易发现问题。你可以通过以下代码启用这些: + +```js +import 'preact/debug'; // <-- 在主入口文件的顶部添加此行 +``` + +这与 React 不同:React 需要通过一个 bundler 检查 `NODE_ENV != "production"` 以便在构建时去除调试信息。 + +## Preact 独有功能 + +Preact 实际上添加了一些受 (P)React 社区工作启发的便捷功能: + +### 原生支持 ES Modules + +Preact 从一开始就考虑到了 ES Modules,并且是最早支持 ES 模块的框架之一。 您可以直接在浏览器中通过 `import` 关键字加载 Preact,而无需先通过 bundler。 + +### `Component.render()` 中的参数 + +方便起见,我们将 `this.props` 和 `this.state` 传给了类组件的 `render()` 方法。看一下这个使用了一个 prop 和一个 state 属性的组件。 + +```jsx +// 在 Preact 和 React 都能运行 +class Foo extends Component { + state = { age: 1 }; + + render() { + return ( +
    + Name: {this.props.name}, Age: {this.state.age} +
    + ); + } +} +``` + +在 Preact 里也可以这样写: + +```jsx +// Only works in Preact +class Foo extends Component { + state = { age: 1 }; + + render({ name }, { age }) { + return ( +
    + Name: {name}, Age: {age} +
    + ); + } +} +``` + +两段代码渲染的是完全相同的东西,提供渲染参数是为了方便。 + +### 原始 HTML attribute/property 名称 + +Preact 旨在遵守所有主流浏览器支持的 DOM 规范。当将 `props` 应用于元素时,Preact 会*检测*每个 prop 是否应设置为属性或 HTML 属性。这使得在自定义元素上设置复杂的属性成为可能,但这也意味着您可以在 JSX 中使用 `class` 等属性名称: + +```jsx +// This: +
    + +// ...和下面的一样: +
    +``` + +大多数 Preact 开发者喜欢使用 `class`,因为它更短,但是两者都是支持的。 + +### JSX 中的 SVG + +当你看到 SVG 的 properties 和 attributes 的一些名称会感觉挺有趣的。SVG 对象上的一些 properties 和 attributes 是驼峰的比如 [clipPath 元素上的 clipPathUnits](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/clipPath#Attributes),而一些 attributes 是短横线的(比如 [许多 SVG 元素上的 clip-path](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/Presentation), 剩下的一些 attributes (大多数继承自 DOM,比如 `oninput`) 都是小写的。 + +Preact 按原样应用 SVG 属性。这意味着可以直接复制未修改的 SVG 片段并粘贴到代码中,并让它们开箱即用。这使得与设计人员用来生成图标或 SVG 插图的工具具有更好的互操作性。 + +```jsx +// React + + + +// Preact (note stroke-width and stroke-linejoin) + + + +``` + +如果你之前使用 React,你可能习惯于以驼峰命名法指定所有属性。这时你可以将 [preact/compat] 添加到项目中来继续使用驼峰命名的 SVG 属性名称, Preact 会映射 React API 并格式化这些属性。 + +### 使用 `onInput` 而不是 `onChange` + +由于历史原因,React 的 `onChange` 事件的语义实际上与所有浏览器提供并支持的 `onInput` 事件相同。 大多数情况下,当你希望当一个表单发生变化时做出响应时使用 `input` 事件是最合适的。 在 Preact 核心中,当元素的值被用户*提交*时会触发标准的 [DOM 更改事件](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/change_event) `onChange`。 + +```jsx +// React + console.log(e.currentTarget.value)} /> + +// Preact + console.log(e.currentTarget.value)} /> +``` + +如果你使用 [preact/compat],大多数 `onChange` 事件会在内部被转换为 `onInput` 来模拟 React 行为。这是我们用来确保与 React 生态系统最大兼容性的技巧之一。 + +### JSX 构造器 + +JSX 是 JavaScript 的语法扩展,可转换为嵌套函数调用。使用这些嵌套调用来构建树结构的想法早在 JSX 之前就已存在,并且之前由 [hyperscript] 项目在 JavaScript 中普及。这种方法的价值远远超出了 React 生态系统的范围,因此 Preact 推广了最初的通用社区标准。 要更深入地讨论 JSX 及其与 Hyperscript 的关系,请[阅读这篇有关 JSX 如何工作的文章](https://jasonformat.com/wtf-is-jsx)。 + +**Source:** (JSX) + +```jsx + + Home + +``` + +**Output:** + +```js +// Preact: +h('a', { href: '/' }, h('span', null, 'Home')); + +// React: +React.createElement( + 'a', + { href: '/' }, + React.createElement('span', null, 'Home') +); +``` + +最后,如果您查看 Preact 应用程序生成的输出代码,很明显,较短的无命名空间 “JSX pragma” 既更易于阅读,也更适合压缩等优化。在大多数 Preact 应用程序中,您都会遇到 `h()`,尽管使用哪个名称并不重要,因为还提供了 `createElement` 别名导出。 + +### 无 contextTypes 需要 + +旧的 `Context` API 要求组件使用 React 的 `contextTypes` 或 `childContextTypes` 声明特定属性以便接收这些值。Preact 没有这个要求:默认情况下,所有组件都会接收由 `getChildContext()` 生成的所有 context 属性。 + +## `preact/compat` 的功能 + +`preact/compat` 是我们用来翻译 React 代码为 Preact 的 **兼容** 层。对于现有的 React 用户来说,通过在你的打包工具配置中[设置一些别名](/guide/v10/getting-started#aliasing-react-to-preact),这是一种无需更改任何代码即可尝试 Preact 的简单方法。 + +### Children API + +`Children` API 是一组专门用于处理 `props.children` 值的方法。对于 Preact 这通常是不必要的,我们建议使用内置的数组方法。在 Preact 中,`props.children` 可以是虚拟 DOM 节点、空值(如 `null`)或虚拟 DOM 节点数组。前两种情况是最简单和最常见的,因为可以按原样使用或返回 `children`: + +```jsx +// React: +function App(props) { + return ; +} + +// Preact: use props.children directly: +function App(props) { + return ; +} +``` + +对于需要迭代传递给组件的子级的特殊情况,Preact 提供了一个 `toChildArray()` 方法,该方法接受任何 `props.children` 值并返回一个扁平且规范化的虚拟 DOM 节点数组。 + +```jsx +// React +function App(props) { + const cols = Children.count(props.children); + return
    {props.children}
    ; +} + +// Preact +function App(props) { + const cols = toChildArray(props.children).length; + return
    {props.children}
    ; +} +``` + +`preact/compat` 提供了与 React 兼容的 `Children` API,可与现有组件库无缝集成。 + +### ​ 专用组件 + +[preact/compat] 附带了并非每个应用程序都必需的专用组件。 这些包括: + +- [PureComponent](/guide/v10/switching-to-preact#purecomponent): 仅当 `props` 或 `state` 改变时发生 +- [memo](/guide/v10/switching-to-preact#memo):与 `PureComponent` 类似,但允许使用自定义比较函数 +- [forwardRef](/guide/v10/switching-to-preact#forwardref):向指定的子组件提供 `ref`。 +- [Portals](/guide/v10/switching-to-preact#portals):将当前树渲染到不同的 DOM 容器中 +- [Suspense](/guide/v10/switching-to-preact#suspense-experimental):**实验性** 允许在树未准备好时显示 fallback 内容 +- [lazy](/guide/v10/switching-to-preact#suspense-experimental):**实验性** 延迟加载异步代码并相应地将树标记为就绪/未就绪。 + +[project goals]: /about/project-goals +[hyperscript]: https://github.com/dominictarr/hyperscript +[preact/compat]: /guide/v10/switching-to-preact +[mdn's event reference]: https://developer.mozilla.org/en-US/docs/Web/Events diff --git a/content/zh/guide/v11/forms.md b/content/zh/guide/v11/forms.md new file mode 100644 index 000000000..fdff9dde8 --- /dev/null +++ b/content/zh/guide/v11/forms.md @@ -0,0 +1,452 @@ +--- +title: 表单 +description: 表单和表单控件允许您在应用程序中收集用户输入,是大多数Web应用程序的基础构建块。 +--- + +# 表单 + +Preact 中的表单与 HTML 和 JS 中的工作方式相同:您渲染控件,添加事件监听器,并提交信息。 + +--- + + + +--- + +## 基本表单控件 + +通常,您需要在应用程序中收集用户输入,这时``、`