├── .gitattributes ├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs └── .vitepress │ ├── config.mts │ └── theme │ ├── custom.css │ └── index.mts ├── package.json ├── pnpm-lock.yaml └── src ├── components ├── await.md ├── form.md ├── link-native.md ├── link.md ├── nav-link.md ├── navigate.md ├── outlet.md ├── route.md ├── routes.md └── scroll-restoration.md ├── fetch ├── json.md ├── redirect-document.md └── redirect.md ├── guides ├── api-development-strategy.md ├── contributing.md ├── data-libs.md ├── deferred.md ├── form-data.md ├── index-search-param.md └── ssr.md ├── hooks ├── use-action-data.md ├── use-async-error.md ├── use-async-value.md ├── use-before-unload.md ├── use-blocker.md ├── use-fetcher.md ├── use-fetchers.md ├── use-form-action.md ├── use-href.md ├── use-in-router-context.md ├── use-link-click-handler.md ├── use-link-press-handler.md ├── use-loader-data.md ├── use-location.md ├── use-match.md ├── use-matches.md ├── use-navigate.md ├── use-navigation-type.md ├── use-navigation.md ├── use-outlet-context.md ├── use-outlet.md ├── use-params.md ├── use-prompt.md ├── use-resolved-path.md ├── use-revalidator.md ├── use-route-error.md ├── use-route-loader-data.md ├── use-routes.md ├── use-search-params-rn.md ├── use-search-params.md ├── use-submit.md └── use-view-transition-state.md ├── index.md ├── public ├── favicon.ico ├── logo.svg └── logo_dark.svg ├── route ├── action.md ├── error-element.md ├── hydrate-fallback-element.md ├── lazy.md ├── loader.md ├── route.md └── should-revalidate.md ├── router-components ├── browser-router.md ├── hash-router.md ├── memory-router.md ├── native-router.md ├── router.md └── static-router.md ├── routers ├── create-browser-router.md ├── create-hash-router.md ├── create-memory-router.md ├── create-static-handler.md ├── create-static-router.md ├── picking-a-router.md ├── router-provider.md └── static-router-provider.md ├── start ├── concepts.md ├── faqs.md ├── overview.md └── tutorial.md ├── upgrading ├── reach.md ├── v5.md └── v6-data.md └── utils ├── create-routes-from-children.md ├── create-routes-from-elements.md ├── create-search-params.md ├── defer.md ├── generate-path.md ├── is-route-error-response.md ├── location.md ├── match-path.md ├── match-routes.md ├── render-matches.md └── resolve-path.md /.gitattributes: -------------------------------------------------------------------------------- 1 | *.md linguist-language=TypeScript 2 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | push-to-gh-pages: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | with: 15 | persist-credentials: false 16 | 17 | - name: Install pnpm 18 | uses: pnpm/action-setup@v2 19 | with: 20 | version: 8 21 | run_install: false 22 | 23 | - name: use Node.js 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: '20.x' 27 | 28 | - name: Build 29 | env: 30 | NODE_OPTIONS: '--max_old_space_size=4096' 31 | run: | 32 | pnpm install 33 | pnpm run docs:build 34 | 35 | - name: Deploy 36 | uses: JamesIves/github-pages-deploy-action@v4.3.3 37 | with: 38 | ACCESS_TOKEN: ${{ secrets.DEPLOY_KEY }} 39 | BRANCH: gh-pages 40 | FOLDER: ./dist 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | docs/.vitepress/cache/ 4 | 5 | .git 6 | .idea 7 | .vscode 8 | .DS_Store 9 | 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 baimingxuan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

React Router6 中文文档(v6.21.1)

3 |

文档内容详细翻译自官方英文文档

4 |

5 | 预览文档 6 |

7 |
8 |
9 | 10 | ### 开发 11 | 12 | ```sh 13 | # 克隆项目 14 | git clone https://github.com/baimingxuan/react-router6-doc.git 15 | 16 | # 安装依赖 17 | pnpm install 18 | 19 | # 开发 20 | pnpm run docs:dev 21 | ``` 22 | 23 | 项目基于 [vitepress](https://github.com/vuejs/vitepress) 24 | 25 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/custom.css: -------------------------------------------------------------------------------- 1 | .logo { 2 | margin-right: 0 !important; 3 | } -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.mts: -------------------------------------------------------------------------------- 1 | import DefaultTheme from 'vitepress/theme' 2 | import './custom.css' 3 | 4 | export default DefaultTheme -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-router6-doc", 3 | "version": "1.0.0", 4 | "description": "react router v6.21.1 chinese document", 5 | "repository": "https://github.com/baimingxuan/react-router6-doc.git", 6 | "author": "baimingxuan", 7 | "license": "MIT License", 8 | "scripts": { 9 | "docs:dev": "vitepress dev docs --host", 10 | "docs:build": "vitepress build docs", 11 | "docs:preview": "vitepress preview docs" 12 | }, 13 | "devDependencies": { 14 | "vitepress": "1.0.0-rc.24" 15 | } 16 | } -------------------------------------------------------------------------------- /src/components/await.md: -------------------------------------------------------------------------------- 1 | # `` 2 | 3 | 用于呈现具有自动错误处理的功能[延迟值](../utils/defer)。请务必查看[延迟数据指南](../guides/deferred),因为有一些 API 与此组件配合使用。 4 | 5 | ```jsx 6 | import { Await, useLoaderData } from "react-router-dom"; 7 | 8 | function Book() { 9 | const { book, reviews } = useLoaderData(); 10 | return ( 11 |
12 |

{book.title}

13 |

{book.description}

14 | }> 15 | Could not load reviews 😬
19 | } 20 | children={(resolvedReviews) => ( 21 | 22 | )} 23 | /> 24 | 25 | 26 | ); 27 | } 28 | ``` 29 | 30 | **注意:** `` 期望在 `` 或 `` 父级中呈现,以启用回退用户界面。 31 | 32 | ## 类型声明 33 | 34 | ```ts 35 | declare function Await( 36 | props: AwaitProps 37 | ): React.ReactElement; 38 | 39 | interface AwaitProps { 40 | children: React.ReactNode | AwaitResolveRenderFunction; 41 | errorElement?: React.ReactNode; 42 | resolve: TrackedPromise | any; 43 | } 44 | 45 | interface AwaitResolveRenderFunction { 46 | (data: Awaited): React.ReactElement; 47 | } 48 | ``` 49 | 50 | ## `children` 51 | 52 | 可以是React元素或函数。 53 | 54 | 使用函数时,值是唯一的参数。 55 | 56 | ```jsx 57 | 58 | {(resolvedReviews) => } 59 | 60 | ``` 61 | 62 | 使用React元素时,[`useAsyncValue`](../hooks/use-async-value)将提供数据: 63 | 64 | ```jsx 65 | 66 | 67 | ; 68 | 69 | function Reviews() { 70 | const resolvedReviews = useAsyncValue(); 71 | return
{/* ... */}
; 72 | } 73 | ``` 74 | 75 | ## `errorElement` 76 | 77 | 当Promise被拒绝时,错误元素会渲染,而不是子元素。您可以通过[`useAsyncError`](../hooks/use-async-error)访问错误。 78 | 79 | 如果Promise被拒绝,您可以提供一个可选的 `errorElement` ,通过 `useAsyncError` 钩子在上下文用户界面中处理该错误。 80 | 81 | ```jsx 82 | } 85 | > 86 | 87 | ; 88 | 89 | function ReviewsError() { 90 | const error = useAsyncError(); 91 | return
{error.message}
; 92 | } 93 | ``` 94 | 95 | 如果不提供 errorElement,被拒绝的值将上升到最近的路由级[`errorElement`](../route/error-element),并可通过[`useRouteError`](../hooks/use-route-error)钩子访问。 96 | 97 | ## `resolve` 98 | 99 | 获取从[延迟](../utils/defer) [加载器](../route/loader)返回的 `promise`值,并进行解析和渲染。 100 | 101 | ```jsx 102 | import { 103 | defer, 104 | Route, 105 | useLoaderData, 106 | Await, 107 | } from "react-router-dom"; 108 | 109 | // given this route 110 | { 112 | let book = await getBook(); 113 | let reviews = getReviews(); // not awaited 114 | return defer({ 115 | book, 116 | reviews, // this is a promise 117 | }); 118 | }} 119 | element={} 120 | />; 121 | 122 | function Book() { 123 | const { 124 | book, 125 | reviews, // this is the same promise 126 | } = useLoaderData(); 127 | return ( 128 |
129 |

{book.title}

130 |

{book.description}

131 | }> 132 | 136 | 137 | 138 | 139 |
140 | ); 141 | } 142 | ``` -------------------------------------------------------------------------------- /src/components/form.md: -------------------------------------------------------------------------------- 1 | # `
` 2 | 3 | 类型声明 4 | 5 | ```ts 6 | declare function Form(props: FormProps): React.ReactElement; 7 | 8 | interface FormProps 9 | extends React.FormHTMLAttributes { 10 | method?: "get" | "post" | "put" | "patch" | "delete"; 11 | encType?: 12 | | "application/x-www-form-urlencoded" 13 | | "multipart/form-data" 14 | | "text/plain"; 15 | action?: string; 16 | onSubmit?: React.FormEventHandler; 17 | fetcherKey?: string; 18 | navigate?: boolean; 19 | preventScrollReset?: boolean; 20 | relative?: "route" | "path"; 21 | reloadDocument?: boolean; 22 | replace?: boolean; 23 | state?: any; 24 | unstable_viewTransition?: boolean; 25 | } 26 | ``` 27 | 28 | `Form` 组件是对纯 HTM [form](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form)的封装,它模拟浏览器进行客户端路由和数据突变。它不像 React 生态系统中的表单验证/状态管理库(为此,我们推荐使用浏览器内置的[HTML 表单验证](https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation)和后端服务器上的数据验证)。 29 | 30 | > IMPORTANT 31 | > 32 | > 此功能只有在使用数据路由器时才有效,请参阅[选择路由](../routers/picking-a-router)。 33 | 34 | ```jsx 35 | import { Form } from "react-router-dom"; 36 | 37 | function NewEvent() { 38 | return ( 39 | 40 | 41 | 42 | 43 | 44 | ); 45 | } 46 | ``` 47 | 48 | > NOTE 49 | > 50 | > 确保您的输入有名称,否则 `FormData` 将不包含该字段的值。 51 | 52 | 所有这些都会触发任何已渲染[`useNavigation`](../hooks/use-navigation)钩子的状态更新,因此您可以在异步操作执行过程中构建待定指标和优化的用户界面。 53 | 54 | 如果表单*不像*导航,您可能需要 [`useFetcher`](../hooks/use-fetcher) 。 55 | 56 | ## `action` 57 | 58 | 提交表单的 URL,与[HTML 表单操作](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-action)一样。唯一不同的是默认操作。对于 HTML 表单,默认为完整的 URL。对于 `
` ,默认为上下文中最近路径的相对 URL。 59 | 60 | 思考以下路由和组件: 61 | 62 | ```jsx 63 | function ProjectsLayout() { 64 | return ( 65 | <> 66 | 67 | 68 | 69 | ); 70 | } 71 | 72 | function ProjectsPage() { 73 | return ; 74 | } 75 | 76 | 77 | } 80 | action={ProjectsLayout.action} 81 | > 82 | } 85 | action={ProjectsPage.action} 86 | /> 87 | 88 | ; 89 | ``` 90 | 91 | 如果当前 URL 是 `"/projects/123"` ,那么子路由 `ProjectsPage` 内的表单就会如你所想的那样有一个默认操作: `"/projects/123"` 。在这种情况下,当路由是最深匹配路由时, `` 和纯 HTML 表单的结果是一样的。 92 | 93 | 但是, `ProjectsLayout` 中的表单将指向 `"/projects"` ,而不是完整的 URL。换句话说,它指向的是渲染表单的路由中 URL 的匹配段。 94 | 95 | 如果在路由模块周围添加一些约定,这将有助于提高可移植性,并将表单及其动作处理程序放在同一位置。 96 | 97 | 如果需要发布到不同的路由,则添加一个操作属性: 98 | 99 | ```jsx 100 | 101 | ``` 102 | 103 | **另请参阅:** 104 | 105 | - [索引搜索参数](../guides/index-search-param)(索引与父路由消歧对比) 106 | 107 | > NOTE 108 | > 109 | > 请参阅 `useResolvedPath` 文档中的 [Splat Paths](../hooks/use-resolved-path#splat-paths) 部分,了解 `future.v7_relativeSplatPath` future 标志在 `splat` 路由中相对 `useNavigate()` 的行为。 110 | 111 | ## `method` 112 | 113 | 这决定了要使用的[HTTP verb](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods)。与纯 HTML[表单方法](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-method)相似,除 "get "和 "post "外,它还支持 "put"、"patch "和 "delete"。默认为 "get"。 114 | 115 | ### GET提交 116 | 117 | 默认方法为“get”。Get 提交*不会调用`action`*。Get 提交与普通导航(用户点击链接)相同,只是用户可以提供搜索参数,从表单进入 URL。 118 | 119 | ```jsx 120 | 121 | 126 | 127 | 128 | ``` 129 | 130 | 假设用户键入“running shoes”并提交表单。React Router 模拟浏览器,将表单序列化为 [URLSearchParams](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams),然后将用户导航到 `"/products?q=running+shoes"` 。这就好比你以开发者的身份渲染了一个 `` ,而不是让用户动态提供查询字符串。 131 | 132 | 路由`loader`访问这些值最方便的方法是从 `request.url` 创建一个新的[`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL),然后加载数据。 133 | 134 | ```jsx 135 | { 138 | let url = new URL(request.url); 139 | let searchTerm = url.searchParams.get("q"); 140 | return fakeSearchProducts(searchTerm); 141 | }} 142 | /> 143 | ``` 144 | 145 | ### 突变提交 146 | 147 | 所有其他方法都是 "突变提交",这意味着您打算通过 POST、PUT、PATCH 或 DELETE 来更改数据。请注意,纯 HTML 表单只支持 "post "和 "get",我们也倾向于使用这两种方法。 148 | 149 | 当用户提交表单时,React Router 会将 `action` 与应用程序的路由相匹配,并调用带有序列化[`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)的 `` 。操作完成后,页面上的所有`loader`数据将自动重新验证,以保持用户界面与数据同步。 150 | 151 | 该方法可在[`request.method`](https://developer.mozilla.org/en-US/docs/Web/API/Request/method)上调用路由操作。您可以使用该方法向您的数据抽象指示提交的意图。 152 | 153 | ```jsx 154 | } 157 | loader={async ({ params }) => { 158 | return fakeLoadProject(params.id); 159 | }} 160 | action={async ({ request, params }) => { 161 | switch (request.method) { 162 | case "PUT": { 163 | let formData = await request.formData(); 164 | let name = formData.get("projectName"); 165 | return fakeUpdateProject(name); 166 | } 167 | case "DELETE": { 168 | return fakeDeleteProject(params.id); 169 | } 170 | default: { 171 | throw new Response("", { status: 405 }); 172 | } 173 | } 174 | }} 175 | />; 176 | 177 | function Project() { 178 | let project = useLoaderData(); 179 | 180 | return ( 181 | <> 182 |
183 | 188 | 189 |
190 | 191 |
192 | 193 |
194 | 195 | ); 196 | } 197 | ``` 198 | 199 | 正如您所看到的,两个表单提交到相同的路径,但您可以使用 `request.method` 来分支您打算做的事情。操作完成后, `loader` 将重新验证,用户界面将自动与新数据同步。 200 | 201 | ## `navigate` 202 | 203 | 您可以指定 `
` ,让表单跳过导航,在内部使用[fetcher](../hooks/use-fetcher)。这基本上是 `useFetcher()` + `` 的简写,在这种情况下,您并不关心结果数据,而只想启动提交并通过[`useFetchers()`](../hooks/use-fetchers)访问待处理状态。 204 | 205 | ## `fetcherKey` 206 | 207 | 在使用非导航 `Form` 时,也可选择通过 `` 指定自己的取值器密钥。 208 | 209 | ## `replace` 210 | 211 | 指示表格替换历史堆栈中的当前条目,而不是推送新条目。 212 | 213 | ```jsx 214 | 215 | ``` 216 | 217 | 默认行为以表单行为为条件: 218 | 219 | - `method=get`表单默认为`false` 220 | 221 | - 提交方法取决于 `formAction` 和 `action` 的行为: 222 | 223 | - 如果您的 `action` 抛出异常,则默认为 `false` 224 | - 如果您的 `action` 重定向到当前位置,则默认为 `true` 225 | - 如果您的 `action` 重定向到其他位置,则默认为 `false` 226 | - 如果您的 `formAction` 是当前位置,则默认为 `true` 227 | - 否则默认为 `false` 228 | 229 | 我们发现,使用 `get` 时,用户往往希望能够点击 "返回 "查看之前的搜索结果/过滤器等。但其他方法的默认值是 `true` ,以避免出现 "您确定要重新提交表单吗?"的提示。请注意,即使 `replace={false}` React Router 也不会在点击返回按钮且方法为 post、put、patch 或 delete 时重新提交表单。 230 | 231 | 换句话说,这其实只对 GET 提交有用,你要避免返回按钮显示之前的结果。 232 | 233 | ## `relative` 234 | 235 | 默认情况下,路径是相对于路由层次结构而言的,因此 `..` 会向上移动一级 `Route` 。有时,你可能会发现有一些匹配的 URL 模式没有嵌套的意义,这时你更愿意使用相对路径路由。您可以使用 `` 236 | 237 | ## `reloadDocument` 238 | 239 | 指示表单跳过 React Router,使用浏览器内置行为提交表单。 240 | 241 | ```jsx 242 | 243 | ``` 244 | 245 | 建议使用 `` ,这样可以获得默认和相对 `action` 的好处,除此之外,它与普通 HTML 表单相同。 246 | 247 | 如果没有[Remix](https://remix.run/)这样的框架,或者没有自己的服务器来处理路由的帖子,这个功能就不是很有用。 248 | 249 | **另请参阅:** 250 | 251 | - [`useNavigation`](../hooks/use-navigation) 252 | - [`useActionData`](../hooks/use-action-data) 253 | - [`useSubmit`](../hooks/use-submit) 254 | 255 | ## `state` 256 | 257 | `state` 属性可用于为存储在[历史状态](https://developer.mozilla.org/en-US/docs/Web/API/History/state)中的新位置设置一个有状态的值。随后可通过 `useLocation()` 访问该值。 258 | 259 | ```jsx 260 | 265 | ``` 266 | 267 | 您可以在 "新路径 "路由上访问该状态值: 268 | 269 | ```jsx 270 | let { state } = useLocation(); 271 | ``` 272 | 273 | ## `preventScrollReset` 274 | 275 | 如果使用[``](../components/scroll-restoration),则可以防止在表单操作重定向到新位置时,滚动位置被重置到窗口顶部。 276 | 277 | ```jsx 278 | 279 | ``` 280 | 281 | **另请参阅:** 282 | 283 | [``](../components/link#preventscrollreset) 284 | 285 | ## `unstable_viewTransition` 286 | 287 | `unstable_viewTransition` 属性通过在 `document.startViewTransition()` 中封装最终状态更新,为该导航启用了[视图转换](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API)。如果需要为该视图转换应用特定样式,还需要利用 [`unstable_useViewTransitionState()`](../hooks/use-view-transition-state)。 288 | 289 | > IMPORTANT 290 | > 291 | > 请注意,此应用程序接口标记为不稳定状态,可能会在未发布重大版本时发生破坏性更改。 292 | 293 | # 实例 294 | 295 | ## 大型列表过滤 296 | 297 | GET 提交的常见用例是过滤大量列表,如电子商务和旅游预订网站。 298 | 299 | ```jsx 300 | function FilterForm() { 301 | return ( 302 | 303 | 308 | 309 |
310 | Star Rating 311 | 315 | 318 | 321 | 324 | 327 |
328 | 329 |
330 | Amenities 331 | 339 | 347 |
348 | 349 | 350 | ); 351 | } 352 | ``` 353 | 354 | 当用户提交该表单时,表单将根据用户的选择以类似这样的方式序列化到 URL 中: 355 | 356 | ```jsx 357 | /slc/hotels?sort=price&stars=4&amenities=pool&amenities=exercise 358 | ``` 359 | 360 | 您可以通过 `request.url` 访问这些值。 361 | 362 | ```jsx 363 | { 366 | let url = new URL(request.url); 367 | let sort = url.searchParams.get("sort"); 368 | let stars = url.searchParams.get("stars"); 369 | let amenities = url.searchParams.getAll("amenities"); 370 | return fakeGetHotels({ sort, stars, amenities }); 371 | }} 372 | /> 373 | ``` 374 | 375 | **另请参阅:** 376 | 377 | - [useSubmit](../hooks/use-submit) -------------------------------------------------------------------------------- /src/components/link-native.md: -------------------------------------------------------------------------------- 1 | # `` `(React Native)` 2 | 3 | > NOTE 4 | > 5 | > 这是 `` 的 React Native 版本。要查看Web版,[请点击此处](../components/link)。 6 | 7 | 类型声明 8 | 9 | ```ts 10 | declare function Link(props: LinkProps): React.ReactElement; 11 | 12 | interface LinkProps extends TouchableHighlightProps { 13 | children?: React.ReactNode; 14 | onPress?(event: GestureResponderEvent): void; 15 | replace?: boolean; 16 | state?: any; 17 | to: To; 18 | } 19 | ``` 20 | 21 | `` 是一种元素,用户可以通过点击它导航到另一个视图,类似于 `` 元素在网络应用中的工作方式。在 `react-router-native` 中, `` 会呈现 `TouchableHighlight` 。要覆盖默认样式和行为,请参阅[`TouchableHighlight`的属性参考文档](https://reactnative.dev/docs/touchablehighlight#props)。 22 | 23 | ```jsx 24 | import * as React from "react"; 25 | import { View, Text } from "react-native"; 26 | import { Link } from "react-router-native"; 27 | 28 | function Home() { 29 | return ( 30 | 31 | Welcome! 32 | 33 | Visit your profile 34 | 35 | 36 | ); 37 | } 38 | ``` -------------------------------------------------------------------------------- /src/components/link.md: -------------------------------------------------------------------------------- 1 | # `` 2 | 3 | > NOTE 4 | > 5 | > 这是`` 的 Web 版。有关 React Native 版本,[请访问此处](../components/link-native)。 6 | 7 | 类型声明 8 | 9 | ```ts 10 | declare function Link(props: LinkProps): React.ReactElement; 11 | 12 | interface LinkProps 13 | extends Omit< 14 | React.AnchorHTMLAttributes, 15 | "href" 16 | > { 17 | to: To; 18 | preventScrollReset?: boolean; 19 | relative?: "route" | "path"; 20 | reloadDocument?: boolean; 21 | replace?: boolean; 22 | state?: any; 23 | unstable_viewTransition?: boolean; 24 | } 25 | 26 | type To = string | Partial; 27 | 28 | interface Path { 29 | pathname: string; 30 | search: string; 31 | hash: string; 32 | } 33 | ``` 34 | 35 | `` 是一种元素,用户可以通过点击或轻点它来导航到另一个页面。在 `react-router-dom` 中, `` 会渲染一个可访问的 `` 元素,该元素带有一个真正的 `href` ,指向它所链接的资源。这意味着,右键单击 `` 等操作都能如您所愿。您可以使用 `` 跳过客户端路由,让浏览器正常处理转换(就像 `` 一样)。 36 | 37 | ```jsx 38 | import * as React from "react"; 39 | import { Link } from "react-router-dom"; 40 | 41 | function UsersIndexPage({ users }) { 42 | return ( 43 |
44 |

Users

45 |
    46 | {users.map((user) => ( 47 |
  • 48 | {user.name} 49 |
  • 50 | ))} 51 |
52 |
53 | ); 54 | } 55 | ``` 56 | 57 | 相对 `` 值(不以 `/` 开头)是相对于父路由解析的,这意味着它建立在渲染该 `` 的路由所匹配的 URL 路径之上。它可能包含 `..` ,以链接到层级更高的路由。在这种情况下, `..` 的工作原理与命令行函数 `cd` 完全相同;每 `..` 删除父路径中的一段。 58 | 59 | > 当当前 URL 以 `/` 结尾时,带有 `..` 的 `` 与普通 `
` 的行为不同。 `` 会忽略尾部斜线,并为每个 `..` 删除一个 URL 段。但是,当当前 URL 以 `/` 结尾时, `` 值处理 `..` 的方式与其不同。 60 | 61 | > NOTE 62 | > 63 | > 请参阅 `useResolvedPath` 文档中的 [Splat Paths](../hooks/use-resolved-path#splat-paths) 部分,了解 `future.v7_relativeSplatPath` future 标志在 `splat` 路由中相对 ` `的行为。 64 | 65 | ## `relative` 66 | 67 | 默认情况下,链接是相对于路由层次结构( `relative="route"` )而言的,因此 `..` 将从当前上下文路由向上移动一级 `Route` 。有时,您可能会发现匹配的 URL 模式没有嵌套的意义,而您更希望使用当前上下文路由路径的相对路径路由。您可以通过 `relative="path"` 选择这种行为: 68 | 69 | ```jsx 70 | // Contact and EditContact do not share additional UI layout 71 | }> 72 | } /> 73 | } 76 | /> 77 | ; 78 | 79 | function EditContact() { 80 | // Since Contact is not a parent of EditContact we need to go up one level 81 | // in the current contextual route path, instead of one level in the Route 82 | // hierarchy 83 | return ( 84 | 85 | Cancel 86 | 87 | ); 88 | } 89 | ``` 90 | 91 | ## `preventScrollReset` 92 | 93 | 如果使用的是[``](../components/scroll-restoration),则可以防止在点击链接时将滚动位置重置到窗口顶部。 94 | 95 | ```jsx 96 | 97 | ``` 98 | 99 | 这并不会阻止用户使用后退/前进按钮回到该位置时恢复滚动位置,而只是防止用户点击链接时重置滚动位置。 100 | 101 | 举例来说,如果一个标签列表操作的URL 搜索参数不在页面顶部,就可能需要这种行为。你不会希望滚动位置跳到顶部,因为这可能会将切换的内容滚出视口! 102 | 103 | ``` 104 | ┌─────────────────────────┐ 105 | │ ├──┐ 106 | │ │ │ 107 | │ │ │ scrolled 108 | │ │ │ out of view 109 | │ │ │ 110 | │ │ ◄┘ 111 | ┌─┴─────────────────────────┴─┐ 112 | │ ├─┐ 113 | │ │ │ viewport 114 | │ ┌─────────────────────┐ │ │ 115 | │ │ tab tab tab │ │ │ 116 | │ ├─────────────────────┤ │ │ 117 | │ │ │ │ │ 118 | │ │ │ │ │ 119 | │ │ content │ │ │ 120 | │ │ │ │ │ 121 | │ │ │ │ │ 122 | │ └─────────────────────┘ │ │ 123 | │ │◄┘ 124 | └─────────────────────────────┘ 125 | ``` 126 | 127 | ## `replace` 128 | 129 | 如果您希望通过[`history.replaceState`](https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState) 替换历史堆栈中的当前条目,而不是默认使用[`history.pushState`](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState),则可以使用 `replace` 属性。 130 | 131 | ## `state` 132 | 133 | `state` 属性可用于为存储在[历史状态](https://developer.mozilla.org/en-US/docs/Web/API/History/state)中的新位置设置一个有状态的值。随后可通过 `useLocation()` 访问该值。 134 | 135 | ```jsx 136 | 137 | ``` 138 | 139 | 您可以在 "新路径 "路由上访问该状态值: 140 | 141 | ```jsx 142 | let { state } = useLocation(); 143 | ``` 144 | 145 | ## `reloadDocument` 146 | 147 | `reloadDocument` 属性可用于跳过客户端路由,让浏览器正常处理转换(如同 `` )。 148 | 149 | ## `unstable_viewTransition` 150 | 151 | `unstable_viewTransition` 属性通过将最终状态更新封装在 `document.startViewTransition()` 中,启用了该导航的[视图转换](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API): 152 | 153 | ```jsx 154 | 155 | Click me 156 | 157 | ``` 158 | 159 | 如果需要为该视图转换应用特定样式,还需要利用[`unstable_useViewTransitionState()`](../hooks//use-view-transition-state) 钩子(或查看 [NavLink](../components/nav-link) 中的 `transitioning` 类和 `isTransitioning` 渲染属性): 160 | 161 | ```jsx 162 | function ImageLink(to) { 163 | const isTransitioning = 164 | unstable_useViewTransitionState(to); 165 | return ( 166 | 167 |

174 | Image Number {idx} 175 |

176 | {`Img 185 | 186 | ); 187 | } 188 | ``` 189 | 190 | > IMPORTANT 191 | > 192 | > `unstable_viewTransition` 仅在使用数据路由器时有效,请参阅 ["选择路由"](../routers/picking-a-router)。 193 | 194 | > IMPORTANT 195 | > 196 | > 请注意,此应用程序接口标记为不稳定状态,可能会在未发布重大版本时发生破坏性更改。 -------------------------------------------------------------------------------- /src/components/nav-link.md: -------------------------------------------------------------------------------- 1 | # `` 2 | 3 | `` 是一种特殊的 `` ,它知道自己是否处于 "激活"、"待定 "或 "过渡 "状态。这在几种不同的情况下都很有用: 4 | 5 | - 在创建导航菜单(如面包屑或一组选项卡)时,您希望显示当前选择了哪个选项卡 6 | - 它为屏幕阅读器等辅助技术提供了有用的背景信息 7 | - 它提供了一个 "过渡 "值,可让您对[视图转换](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API)进行更精细的控制 8 | 9 | ```jsx 10 | import { NavLink } from "react-router-dom"; 11 | 12 | 15 | isPending ? "pending" : isActive ? "active" : "" 16 | } 17 | > 18 | Messages 19 | ; 20 | ``` 21 | 22 | ## 默认 `active` 类 23 | 24 | 默认情况下,当 `` 组件处于活动状态时,会为其添加一个 `active` 类,以便使用 CSS 对其进行样式设置。 25 | 26 | ```jsx 27 | 30 | ``` 31 | 32 | ```css 33 | #sidebar a.active { 34 | color: red; 35 | } 36 | ``` 37 | 38 | ## `className` 39 | 40 | `className` 属性的作用与普通 className 类似,但您也可以将函数传递给它,以便根据链接的活动和待定状态自定义应用的 classNames。 41 | 42 | ```jsx 43 | 46 | [ 47 | isPending ? "pending" : "", 48 | isActive ? "active" : "", 49 | isTransitioning ? "transitioning" : "", 50 | ].join(" ") 51 | } 52 | > 53 | Messages 54 | 55 | ``` 56 | 57 | ## `style` 58 | 59 | `style` 属性的工作方式与普通样式属性类似,但您也可以通过一个函数,根据链接的活动和待定状态自定义应用的样式。 60 | 61 | ```jsx 62 | { 65 | return { 66 | fontWeight: isActive ? "bold" : "", 67 | color: isPending ? "red" : "black", 68 | viewTransitionName: isTransitioning ? "slide" : "", 69 | }; 70 | }} 71 | > 72 | Messages 73 | 74 | ``` 75 | 76 | ## `children` 77 | 78 | 您可以传递一个呈现属性作为子元素,以便根据活动和待定状态自定义 `` 的内容,这对更改内部元素的样式非常有用。 79 | 80 | ```jsx 81 | 82 | {({ isActive, isPending }) => ( 83 | Tasks 84 | )} 85 | 86 | ``` 87 | 88 | ## `end` 89 | 90 | `end` 属性更改了 `active` 和 `pending` 状态的匹配逻辑,使其只匹配到导航链接 `to` 路径的 "末端"。如果 URL 长于 `to` ,将不再被视为激活状态。 91 | 92 | | Link | Current URL | isActive | 93 | | ------------------------------ | ------------ | -------- | 94 | | `` | `/tasks` | true | 95 | | `` | `/tasks/123` | true | 96 | | `` | `/tasks` | true | 97 | | `` | `/tasks/123` | false | 98 | | `` | `/tasks` | false | 99 | | `` | `/tasks/` | true | 100 | 101 | **关于根路由链接的说明** 102 | 103 | `` 是一个特例,因为每个 URL 都匹配`/` 。为了避免默认情况下每条路由都匹配,它实际上忽略了 `end`属性,只在根路由上匹配。 104 | 105 | ## `caseSensitive` 106 | 107 | 添加 `caseSensitive` 属性后,匹配逻辑会发生变化,变得区分大小写。 108 | 109 | | Link | URL | isActive | 110 | | -------------------------------------------- | ------------- | -------- | 111 | | `` | `/sponge-bob` | true | 112 | | `` | `/sponge-bob` | false | 113 | 114 | ## `aria-current` 115 | 116 | 当 `NavLink` 处于活动状态时,它会自动将 `
` 应用于底层锚标签。请参阅 MDN 上的 [aria-current](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current)。 117 | 118 | ## `reloadDocument` 119 | 120 | `reloadDocument` 属性可用于跳过客户端路由,让浏览器正常处理转换(如同 `` )。 121 | 122 | ## `unstable_viewTransition` 123 | 124 | `unstable_viewTransition` 这个道具通过将最终状态更新封装在 `document.startViewTransition()` 中来启用该导航的[视图转换](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API)。默认情况下,在过渡期间,一个 `transitioning` 类将被添加到 `` 元素,你可以用它来定制视图过渡。 125 | 126 | ```jsx 127 | a.transitioning p { 128 | view-transition-name: "image-title"; 129 | } 130 | 131 | a.transitioning img { 132 | view-transition-name: "image-expand"; 133 | } 134 | ``` 135 | 136 | ```jsx 137 | 138 |

Image Number {idx}

139 | {`Img 140 |
141 | ``` 142 | 143 | 您还可以使用 `className` / `style` 属性或传递给 `children` 的渲染属性,根据 `isTransitioning` 值进一步定制。 144 | 145 | ```jsx 146 | 147 | {({ isTransitioning }) => ( 148 | <> 149 |

156 | Image Number {idx} 157 |

158 | {`Img 167 | 168 | )} 169 |
170 | ``` 171 | 172 | > IMPORTANT 173 | > 174 | > 请注意,此 API 已被标记为不稳定版,在未发布重大版本之前,可能会出现一些破坏性更新。 -------------------------------------------------------------------------------- /src/components/navigate.md: -------------------------------------------------------------------------------- 1 | # `` 2 | 3 | 类型声明 4 | 5 | ```ts 6 | declare function Navigate(props: NavigateProps): null; 7 | 8 | interface NavigateProps { 9 | to: To; 10 | replace?: boolean; 11 | state?: any; 12 | relative?: RelativeRoutingType; 13 | } 14 | ``` 15 | 16 | `` 元素在渲染时会改变当前位置。它是[`useNavigate`](../hooks/use-navigate)的组件包装器,并接受与 props 相同的参数。 17 | 18 | > NOTE 19 | > 20 | > 有了基于组件的 `useNavigate` 钩子版本,就可以在无法使用钩子的[`React.Component`](https://reactjs.org/docs/react-component.html)子类中更方便地使用这一功能。 21 | 22 | ```jsx 23 | import * as React from "react"; 24 | import { Navigate } from "react-router-dom"; 25 | 26 | class LoginForm extends React.Component { 27 | state = { user: null, error: null }; 28 | 29 | async handleSubmit(event) { 30 | event.preventDefault(); 31 | try { 32 | let user = await login(event.target); 33 | this.setState({ user }); 34 | } catch (error) { 35 | this.setState({ error }); 36 | } 37 | } 38 | 39 | render() { 40 | let { user, error } = this.state; 41 | return ( 42 |
43 | {error &&

{error.message}

} 44 | {user && ( 45 | 46 | )} 47 |
this.handleSubmit(event)} 49 | > 50 | 51 | 52 |
53 |
54 | ); 55 | } 56 | 57 | ``` -------------------------------------------------------------------------------- /src/components/outlet.md: -------------------------------------------------------------------------------- 1 | # `` 2 | 3 | 类型声明 4 | 5 | ```ts 6 | interface OutletProps { 7 | context?: unknown; 8 | } 9 | declare function Outlet( 10 | props: OutletProps 11 | ): React.ReactElement | null; 12 | ``` 13 | 14 | 父路由元素中应使用 `` 来呈现其子路由元素。这样就可以在呈现子路由时显示嵌套用户界面。如果父路由完全匹配,则会呈现子索引路由;如果没有索引路由,则不会呈现任何内容。 15 | 16 | ```jsx 17 | function Dashboard() { 18 | return ( 19 |
20 |

Dashboard

21 | 22 | {/* This element will render either when the URL is 23 | "/messages", at "/tasks", or null if it is "/" 24 | */} 25 | 26 |
27 | ); 28 | } 29 | 30 | function App() { 31 | return ( 32 | 33 | }> 34 | } 37 | /> 38 | } /> 39 | 40 | 41 | ); 42 | } 43 | ``` -------------------------------------------------------------------------------- /src/components/route.md: -------------------------------------------------------------------------------- 1 | # 路由 API 2 | 3 | 由于 `` 的应用程序接口和用例包括数据加载、突变等,因此 `` 有自己的文档类别。 4 | 5 | 请参阅: 6 | 7 | - [``](../route/route) 8 | - [`loader`](../route/loader) 9 | - [`action`](../route/action) 10 | - [`errorElement`](../route/error-element) 11 | - [`shouldRevalidate`](../route/should-revalidate) -------------------------------------------------------------------------------- /src/components/routes.md: -------------------------------------------------------------------------------- 1 | # `` 2 | 3 | 在应用程序中的任何地方, `` 都会匹配当前[位置](../utils/location)的一组子路由。 4 | 5 | ```tsx 6 | interface RoutesProps { 7 | children?: React.ReactNode; 8 | location?: Partial | string; 9 | } 10 | 11 | 12 | 13 | ; 14 | ``` 15 | 16 | > NOTE 17 | > 18 | > 如果您使用的是[`createBrowserRouter`](../routers/create-browser-router)这样的数据路由器,使用该组件的情况并不常见,因为作为 `` 树的后代的一部分定义的路由无法利用[`RouterProvider`](../routers/router-provider)应用程序可用的[数据 API](../routers/picking-a-router#data-apis)。[在迁移过程中](../upgrading/v6-data),您可以也应该在 `RouterProvider` 应用程序中使用该组件。 19 | 20 | 每当位置发生变化时, `` 就会查看其所有子路由,找出最匹配的路由,并渲染用户界面的该分支。 `` 元素可以嵌套,以表示嵌套的用户界面,这也与嵌套的 URL 路径相对应。父路由通过呈现 [` 24 | }> 25 | } 28 | /> 29 | } /> 30 | 31 | } /> 32 | 33 | ``` -------------------------------------------------------------------------------- /src/components/scroll-restoration.md: -------------------------------------------------------------------------------- 1 | # `` 2 | 3 | 该组件将在加载程序完成后,模拟浏览器在位置更改时的滚动恢复功能,以确保滚动位置恢复到正确位置,甚至跨域滚动。 4 | 5 | > IMPORTANT 6 | > 7 | > 此功能只有在使用数据路由器时才有效,请参阅["选择路由"](../routers/picking-a-router) 8 | 9 | 只需呈现其中一个,建议在应用程序的根路由中呈现: 10 | 11 | ```jsx 12 | import { ScrollRestoration } from "react-router-dom"; 13 | 14 | function RootRouteComponent() { 15 | return ( 16 |
17 | {/* ... */} 18 | 19 |
20 | ); 21 | } 22 | ``` 23 | 24 | ## `getKey` 25 | 26 | 可选属性,用于定义 React Router 恢复滚动位置时应使用的键。 27 | 28 | ```jsx 29 | { 31 | // default behavior 32 | return location.key; 33 | }} 34 | /> 35 | ``` 36 | 37 | 默认情况下,它使用 `location.key` ,在没有客户端路由的情况下模拟浏览器的默认行为。用户可以在堆栈中多次导航到相同的 URL,每个条目都有自己的滚动位置来还原。 38 | 39 | 有些应用可能希望覆盖这一行为,并根据其他内容恢复位置。例如,一个社交应用程序有四个主要页面: 40 | 41 | - "/home" 42 | - "/messages" 43 | - "/notifications" 44 | - "/search" 45 | 46 | 如果用户从"/home "开始,向下滚动一点,点击导航菜单中的 "信息",然后点击导航菜单中的 "主页"(而不是返回按钮!),历史堆栈中就会出现三个条目: 47 | 48 | ```js 49 | 1. /home 50 | 2. /messages 51 | 3. /home 52 | ``` 53 | 54 | 默认情况下,React Router(和浏览器)会为 `1` 和 `3` 存储两个不同的滚动位置,即使它们的 URL 相同。这意味着当用户从 `2` → `3` 浏览时,滚动位置会移到顶部,而不是恢复到 `1` 中的位置。 55 | 56 | 这里一个可靠的产品决策是,无论用户如何到达(返回按钮或新链接点击),都要保持他们在主页上的滚动位置。为此,您需要使用 `location.pathname` 作为关键字。 57 | 58 | ```jsx 59 | { 61 | return location.pathname; 62 | }} 63 | /> 64 | ``` 65 | 66 | 或者,您可能只想对某些路径使用路径名,而对其他路径使用正常行为: 67 | 68 | ```jsx 69 | { 71 | const paths = ["/home", "/notifications"]; 72 | return paths.includes(location.pathname) 73 | ? // home and notifications restore by pathname 74 | location.pathname 75 | : // everything else by location like the browser 76 | location.key; 77 | }} 78 | /> 79 | ``` 80 | 81 | ## 防止滚动重置 82 | 83 | 当导航创建新的滚动键时,滚动位置会重置为页面顶部。您可以防止链接和表单出现 "滚动到顶部 "行为: 84 | 85 | ```jsx 86 | 87 |
88 | ``` 89 | 90 | 另请参阅:[``](../components/link#preventscrollreset),[``](../components/form#preventscrollreset) 91 | 92 | ## 滚动闪烁 93 | 94 | 如果没有 [Remix](https://remix.run/) 这样的服务器端渲染框架,在初始页面加载时可能会出现一些滚动闪烁。这是因为 React Router 无法还原滚动位置,直到您的 JS 捆绑包下载完毕、数据加载完毕、整个页面渲染完毕(如果您正在渲染一个旋转器,视口很可能不是保存滚动位置时的大小)。 95 | 96 | 服务器渲染框架可以防止滚动闪烁,因为它们可以在首次加载时发送一个完整的文档,因此可以在页面首次渲染时恢复滚动。 -------------------------------------------------------------------------------- /src/fetch/json.md: -------------------------------------------------------------------------------- 1 | # `json` 2 | 3 | 快捷方式: 4 | 5 | ```jsx 6 | new Response(JSON.stringify(someValue), { 7 | headers: { 8 | "Content-Type": "application/json; utf-8", 9 | }, 10 | }); 11 | ``` 12 | 13 | 通常用于`loader`: 14 | 15 | ```jsx 16 | import { json } from "react-router-dom"; 17 | 18 | const loader = async () => { 19 | const data = getSomeData(); 20 | return json(data); 21 | }; 22 | ``` 23 | 24 | 另请参阅: 25 | 26 | - [从`loader`返回响应](../route/loader#returning-responses) -------------------------------------------------------------------------------- /src/fetch/redirect-document.md: -------------------------------------------------------------------------------- 1 | # `redirectDocument` 2 | 3 | 这是[`redirect`](../fetch/redirect) 的一个小封装,它将触发文档级重定向到新位置,而不是客户端导航。 4 | 5 | 当您的 React Router 应用程序与同一域名上的另一个应用程序相邻,并且需要通过 `window.location` 而不是 React Router 导航从 React Router 应用程序重定向到另一个应用程序时,这一点最为有用: 6 | 7 | ```jsx 8 | import { redirectDocument } from "react-router-dom"; 9 | 10 | const loader = async () => { 11 | const user = await getUser(); 12 | if (!user) { 13 | return redirectDocument("/otherapp/login"); 14 | } 15 | return null; 16 | }; 17 | ``` 18 | 19 | ## 类型声明 20 | 21 | ```ts 22 | type RedirectFunction = ( 23 | url: string, 24 | init?: number | ResponseInit 25 | ) => Response; 26 | ``` 27 | 28 | ## `url` 29 | 30 | 重定向到的 URL。 31 | 32 | ```jsx 33 | redirectDocument("/otherapp/login"); 34 | ``` 35 | 36 | ## `init` 37 | 38 | 要在响应中使用的[响应](https://developer.mozilla.org/en-US/docs/Web/API/Response/Response)选项。 -------------------------------------------------------------------------------- /src/fetch/redirect.md: -------------------------------------------------------------------------------- 1 | # `redirect` 2 | 3 | 由于可以在`loaders`和`actions`中返回或抛出响应,因此可以使用 `redirect` 重定向到另一个路由。 4 | 5 | ```jsx 6 | import { redirect } from "react-router-dom"; 7 | 8 | const loader = async () => { 9 | const user = await getUser(); 10 | if (!user) { 11 | return redirect("/login"); 12 | } 13 | return null; 14 | }; 15 | ``` 16 | 17 | 这其实只是一个快捷方式: 18 | 19 | ```jsx 20 | new Response("", { 21 | status: 302, 22 | headers: { 23 | Location: someUrl, 24 | }, 25 | }); 26 | ``` 27 | 28 | 当重定向是为了响应数据时,建议在加载器和操作中使用 `redirect` ,而不是在组件中使用 `useNavigate` 。 29 | 30 | 另请参阅: 31 | 32 | - [从`loader`返回响应](../route/loader#returning-responses) 33 | 34 | ## 类型声明 35 | 36 | ```ts 37 | type RedirectFunction = ( 38 | url: string, 39 | init?: number | ResponseInit 40 | ) => Response; 41 | ``` 42 | 43 | ## `url` 44 | 45 | 重定向到的 URL。 46 | 47 | ```jsx 48 | redirect("/login"); 49 | ``` 50 | 51 | ## `init` 52 | 53 | 要在响应中使用的[响应](https://developer.mozilla.org/en-US/docs/Web/API/Response/Response)选项。 -------------------------------------------------------------------------------- /src/guides/api-development-strategy.md: -------------------------------------------------------------------------------- 1 | # API开发战略 2 | 3 | 让我们直奔主题--主要版本升级可能会很麻烦。尤其是像框架或路由器这样的基础应用。对于 Remix 和 React Router,我们希望尽最大努力为您提供最顺畅的升级体验。 4 | 5 | > NOTE 6 | > 7 | > 我们的 "[Future Flags](https://remix.run/blog/future-flags) "博文对这一策略进行了更详细的讨论,如果你想了解更多信息,请在本文档末尾阅读该博文! 8 | 9 | ## 目标 10 | 11 | 我们发布 Remix 和 React Router 的主要目标是 12 | 13 | - 开发人员可以在SemVer-major功能*发布*时逐一选择采用这些功能,而不必等到NPM推出新的主要版本时才一次性全部采用。 14 | - 在提前选择功能后,开发人员只需在一个短命分支/提交中(几小时,而不是几周)就能升级到新的主要版本 15 | 16 | ## 实施情况 17 | 18 | 我们计划通过我们称之为 "Future Flags" 的方式来实现这一目标,您将在初始化[数据路由](../routers/picking-a-router)时提供这些标志。可以将其视为未来功能的功能标志。当我们实现新功能时,我们总是尝试以一种向后兼容的方式来实现它们。但是,当需要进行突破性更改时,我们不会将该功能放在 v7 版本的最终版本中。相反,我们会添加一个"Future Flags",并在 v6 小版本中与当前行为一起实现新功能。这样,用户就可以立即开始使用该功能、提供反馈并报告错误。 19 | 20 | 这样一来,用户不仅可以逐步采用新功能(而且无需大幅提升版本),我们还能在 *v7 版本发布前*逐步解决所有问题。最后,我们还会在 v6 版本中添加弃用警告,提示用户使用新行为。如果在 v6 发布时,应用程序已经选择了所有`future flags`并更新了代码,那么他们只需将其依赖关系更新到 v7,删除`future flags`,然后在几分钟内就能在 v7 上运行。 21 | 22 | ## 不稳定与 V7 标志 23 | 24 | `future flags`有两种形式: 25 | 26 | **`future.unstable_feature`** 27 | 28 | `unstable_` 标记允许我们与早期用户一起迭代 API,就像我们在`v0.x.x`版本中一样,但针对的是特定功能。这避免了为所有用户反复修改 API,从而在最终版本中获得更好的 API。这并不意味着我们认为该功能漏洞百出!我们绝对希望早期用户能够开始使用这些功能,这样我们才能对 API 进行迭代(和/或获得信心)。 29 | 30 | **`future.v7_feature`** 31 | 32 | `v7_` 表示对 v6 行为进行了突破性更改,并意味着:(1)API 被认为是稳定的,不会再有任何突破性更改;(2)API 将成为 v7 的默认行为。` v7_`标志并不意味着该功能没有错误--任何软件都是如此!我们建议您在有时间的情况下升级到 v7 标志,因为这将使您的 v7 升级更加顺利。 33 | 34 | ### 新功能流程示例 35 | 36 | 新功能的决策流程是这样的(注意,此图与 Remix v1/v2 有关,但也适用于 React Router v6/v7): 37 | 38 | ![Flowchart of the decision process for how to introduce a new feature](https://remix.run/docs-images/feature-flowchart.png) 39 | 40 | 因此,生命周期要么是: 41 | 42 | - 非破坏性 + 稳定的应用程序接口功能 -> 在 v6 中落地 43 | - 非断裂 + 不稳定 API -> `future.unstable_` 标志 -> 在 v6 中落地 44 | - 破坏 + 稳定 API 功能 -> `future.v7_` 标志 -> 在 v7 中落地 45 | - 破坏 + 不稳定的应用程序接口 -> `future.unstable_` 标志 -> `future.v7_` 标志 -> 在 v7 中落地 46 | 47 | ## 当前的 Future Flags 48 | 49 | 以下是 React Router v6 目前的`future flags`。 50 | 51 | ### `@remix-run/router` Future Flags 52 | 53 | 这些标志仅在使用[数据路由](../routers/picking-a-router)时适用,并在创建 `router` 实例时传递: 54 | 55 | ```jsx 56 | const router = createBrowserRouter(routes, { 57 | future: { 58 | v7_normalizeFormMethod: true, 59 | }, 60 | }); 61 | ``` 62 | 63 | | Flag | 说明 | 64 | | ------------------------------------------------------------ | ------------------------------------------------------ | 65 | | `v7_fetcherPersist` | 延迟活动的fetcher清理,直到它们返回到 `idle` 状态 | 66 | | `v7_normalizeFormMethod` | 将 `useNavigation().formMethod` 规范化为大写 HTTP 方法 | 67 | | [`v7_partialHydration`](../routers/create-browser-router#partial-hydration-data) | 支持服务端渲染应用程序的部分水合功能 | 68 | | `v7_prependBasename` | 将路由基名作为导航/获取路径的前缀 | 69 | | [`v7_relativeSplatPath`](../hooks/use-resolved-path#splat-paths) | 修复 splat 路由中相对路径解析的错误 | 70 | 71 | ### 路由 Future Flags React 72 | 73 | 这些标记同时适用于数据路由和非数据路由,并会传递给渲染的 React 组件: 74 | 75 | ```jsx 76 | 77 | {/*...*/} 78 | 79 | ``` 80 | 81 | ```jsx 82 | 86 | ``` 87 | 88 | | Flag | 说明 | 89 | | -------------------- | ------------------------------------------------------------ | 90 | | `v7_startTransition` | 将所有路由状态更新包裹着在[`React.startTransition`](https://react.dev/reference/react/startTransition)中 | -------------------------------------------------------------------------------- /src/guides/contributing.md: -------------------------------------------------------------------------------- 1 | # 为 React Router做出贡献 2 | 3 | 感谢您的贡献,您太棒了! 4 | 5 | 说到开放源代码,可以做出许多不同类型的贡献,所有这些贡献都很有价值。以下是一些指导原则,希望对您准备贡献有所帮助。 6 | 7 | ## Setup 8 | 9 | 在为代码库做出贡献之前,您需要 fork 代码库。根据您的贡献类型,这一点会有所不同: 10 | 11 | - 所有新功能、错误修复或**任何涉及 `react-router` 代码**的内容都应从 `dev` 分支中分离出来,并合并到 `dev` 分支中。 12 | - 只涉及文档的改动可以从 `main` 分支出来,合并到 `main` 分支中。 13 | 14 | 以下步骤将帮助您设置好如何向该版本库贡献更改: 15 | 16 | 1. `Fork`这个`repo`(点击[本页面](https://github.com/remix-run/react-router)右上角的`Fork`按钮) 17 | 2. 克隆您的分支到本地 18 | 19 | ```bash 20 | # in a terminal, cd to parent directory where you want your clone to be, then 21 | git clone https://github.com//react-router.git 22 | cd react-router 23 | 24 | # if you are making *any* code changes, make sure to checkout the dev branch 25 | git checkout dev 26 | ``` 27 | 28 | 3. 安装依赖项并构建。React Router 使用[`yarn`(版本1)](https://classic.yarnpkg.com/lang/en/docs/install),所以你也应该这样做。如果使用 `npm` 安装,则会生成不必要的 `package-lock.json` 文件。 29 | 30 | ## 您认为找到了错误? 31 | 32 | 请遵守问题模板,并通过代码示例提供明确的重现路径。最好是带有失败测试的`pull`请求。其次是 CodeSandbox 或说明错误的版本库链接。 33 | 34 | ## 添加示例? 35 | 36 | 示例可直接添加到主分支中。在本地克隆的主分支上创建一个分支。完成后,创建一个拉取请求并概述你的示例。 37 | 38 | ## 提出新的或更改后的 API? 39 | 40 | 请提供深思熟虑的评论和一些示例代码,说明您想在应用程序中使用 React Router 做什么。如果您能在得出需要更改和/或添加什么的结论之前,先向我们展示您是如何受限于当前的 API 的,这将有助于对话的进行。 41 | 42 | 我们的经验告诉我们,小的应用程序接口通常更好,因此我们可能不太愿意添加新内容,除非当前的应用程序接口存在明显的限制。尽管如此,我们还是非常希望听到一些我们以前没有考虑过的情况,所以请不要害羞! :) 43 | 44 | ## 问题没有得到关注? 45 | 46 | 如果你需要修复一个错误,但没有人在修复它,你最好的办法就是提供一个修复程序,并提出[pull 请求](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request)。开放源代码属于我们所有人,推动它向前发展是我们所有人的责任。 47 | 48 | ## 发起Pull 请求? 49 | 50 | `pull`请求只需得到两个或两个以上合作者的批准即可合并;当 PR 作者是合作者时,也算一个合作者。 51 | 52 | > IMPORTANT 53 | > 54 | > 在 GitHub 创建 PR 时,请确保将基本分支设置为正确的分支。如果您提交的 PR 涉及到任何代码,那么它应该是 `dev` 分支。在 GitHub 上创建 PR 时,可通过 "比较更改 "标题下的下拉菜单来设置基准分支:![img](https://raw.githubusercontent.com/remix-run/react-router/main/static/base-branch.png) 55 | 56 | ### 测试 57 | 58 | 所有修复错误或添加功能的提交都需要测试。 59 | 60 | 不要合并未经测试的代码! 61 | 62 | ### 文档+示例 63 | 64 | 所有更改或添加 API 的提交都必须在拉取请求中完成,同时更新所有相关示例和文档。 65 | 66 | ## 开发 67 | 68 | ### Packages 69 | 70 | React Router 使用 monorepo 来托管多个软件包的代码。这些软件包位于 `packages` 目录中。 71 | 72 | 我们使用[ Yarn 工作区](https://classic.yarnpkg.com/en/docs/workspaces/)来管理依赖项的安装和各种脚本的运行。要安装所有脚本,请确保已安装[Yarn(版本1)](https://classic.yarnpkg.com/lang/en/docs/install),然后从 repo 根目录运行 `yarn` 或 `yarn install` 。 73 | 74 | ### 构建 75 | 76 | 从根目录调用 `yarn build` 将运行编译,整个过程只需几秒钟。由于 `react-router-dom` 和 `react-router-native` 都使用 `react-router` 作为依赖关系,因此必须一起构建所有软件包。 77 | 78 | ### 测试 79 | 80 | 在运行测试之前,您需要运行构建。构建后,从根目录运行 `yarn test` 将运行每个软件包的测试。如果要运行特定软件包的测试,请使用 `yarn test --projects packages/` : 81 | 82 | ```bash 83 | # Test all packages 84 | yarn test 85 | 86 | # Test only react-router-dom 87 | yarn test --projects packages/react-router-dom 88 | ``` 89 | 90 | ## 仓库分支 91 | 92 | 该 repo 为不同的目的维护不同的分支。它们看起来是这样的: 93 | 94 | ```bash 95 | - main > the most recent release and current docs 96 | - dev > code under active development between stable releases 97 | - v5 > the most recent code for a specific major release 98 | ``` 99 | 100 | 可能还有其他分支用于各种功能和实验,但所有神奇的事情都发生在这些分支中。 101 | 102 | ## 新版本发布 103 | 104 | 当需要发布新版本时,我们会根据不同类型的版本,按照分支策略制定相应的流程。 105 | 106 | ### `react-router@next`发布 107 | 108 | 我们根据 `dev` 分支的当前状态创建实验版本。您可以使用 `@next` 标签安装它们: 109 | 110 | ```bash 111 | yarn add react-router-dom@next 112 | # or 113 | npm install react-router-dom@next 114 | ``` 115 | 116 | 当 PR 合并到 `dev` 分支时,这些版本将自动发布。 117 | 118 | ### 最新主要版本发布 119 | 120 | ```bash 121 | # Start from the dev branch. 122 | git checkout dev 123 | 124 | # Merge the main branch into dev to ensure that any hotfixes and 125 | # docs updates are available in the release. 126 | git merge main 127 | 128 | # Create a new release branch from dev. 129 | git checkout -b release/v6.1.0 130 | 131 | # Create a new tag and update version references throughout the 132 | # codebase. 133 | yarn run version minor # | "patch" | "major" 134 | 135 | # Push the release branch along with the new release tag. 136 | git push origin release/v6.1.0 --follow-tags 137 | 138 | # Wait for GitHub actions to run all tests. If the tests pass, the 139 | # release is ready to go! Merge the release branch into main and dev. 140 | git checkout main 141 | git merge release/v6.1.0 142 | git checkout dev 143 | git merge release/v6.1.0 144 | 145 | # The release branch can now be deleted. 146 | git branch -D release/v6.1.0 147 | git push origin --delete release/v6.1.0 148 | 149 | # Now go to GitHub and create the release from the new tag. Let 150 | # GitHub Actions take care of the rest! 151 | ``` 152 | 153 | ### 热修复发布 154 | 155 | 有时,我们会遇到需要立即修补的关键错误。如果错误影响到最新版本,我们可以直接从 `main` (或存在错误的相关主要版本分支)创建一个新版本: 156 | 157 | ```bash 158 | # From the main branch, make sure to run the build and all tests 159 | # before creating a new release. 160 | yarn && yarn build && yarn test 161 | 162 | # Assuming the tests pass, create the release tag and update 163 | # version references throughout the codebase. 164 | yarn run version patch 165 | 166 | # Push changes along with the new release tag. 167 | git push origin main --follow-tags 168 | 169 | # In GitHub, create the release from the new tag and it will be 170 | # published via GitHub actions 171 | 172 | # When the hot-fix is done, merge the changes into dev and clean 173 | # up conflicts as needed. 174 | git checkout dev 175 | git merge main 176 | git push origin dev 177 | ``` -------------------------------------------------------------------------------- /src/guides/data-libs.md: -------------------------------------------------------------------------------- 1 | # 数据仓库集成 2 | 3 | 自 v6.4 发布以来,有些人怀疑 React Router 是否试图取代[React Query](https://tanstack.com/query/v4/)、[useSwr](https://swr.vercel.app/)等库。 4 | 5 | 答案是“不!”。 6 | 7 | React Router 的数据 API 是关于何时加载、变异和重新验证数据,而不是如何加载、变异和重新验证数据。它涉及的是数据生命周期,而不是数据获取、变异、存储和缓存的实际实现。 8 | 9 | 考虑到 `` 和 `` 都是导航事件,而且都与数据相关(显示哪些数据或更改哪些数据),因此客户端路由器可以帮助您处理这两个元素的导航状态。但实际的数据实现则取决于您。 10 | 11 | 此处的示例改编自[TkDodo 的博客](https://tkdodo.eu/blog/react-query-meets-react-router),感谢您的精彩文章! 12 | 13 | ## 加载数据 14 | 15 | 您可以在加载器中使用数据抽象,而不是在组件中加载数据。请注意,这种加载发生在 React 渲染生命周期之外,因此不能使用 React Query 的 `useQuery` 等钩子,而需要直接使用查询客户端的方法。 16 | 17 | ```jsx 18 | import { queryClient } from "./query-client"; 19 | 20 | export const loader = ({ params }) => { 21 | return queryClient.fetchQuery(queryKey, queryFn, { 22 | staleTime: 10000, 23 | }); 24 | }; 25 | ``` 26 | 27 | 如果查询客户端能正确抛出错误,那么 React Router 的[`errorElement`](../route/error-element)也能正常工作。 28 | 29 | 当然,您可以使用数据仓库的所有功能,例如缓存。缓存数据可以确保当用户点击返回按钮进入已经看过的页面时,数据会立即从缓存中加载。有时缓存是正确的选择,有时你总是希望数据是新鲜的,但这并不在 React Router 数据 API 的决定范围之内。 30 | 31 | React Router 只保留*当前页面的 `loaderData`*。如果用户点击“返回”,所有加载器都会再次被调用。如果没有 React Query 这样的数据缓存库(或在 JSON API 上添加 HTTP 缓存头以使用浏览器自己的 HTTP 缓存),您的应用将再次重新获取所有数据。 32 | 33 | 因此,React Router 与*时间*有关,而 React Query 与*缓存*有关。 34 | 35 | ## 访问组件中的数据 36 | 37 | 虽然 React Router 的 `useLoaderData` 会返回您从`loader`返回的内容,但您可以使用数据抽象的钩子来访问该包的全部功能集。 38 | 39 | ```jsx 40 | export default function SomeRouteComponent() { 41 | - const data = useLoaderData(); 42 | + const { data } = useQuery(someQueryKey); 43 | } 44 | ``` 45 | 46 | ## 使突变中的数据无效 47 | 48 | 由于这些库中的大多数都有某种缓存机制,因此您需要在某些时候使这些缓存失效。 49 | 50 | React Router[操作](../route/action)就是使缓存失效的最佳场所。 51 | 52 | ```jsx 53 | import { queryClient } from "./query-client"; 54 | 55 | export const action = async ({ request, params }) => { 56 | const formData = await request.formData(); 57 | const updates = Object.fromEntries(formData); 58 | await updateContact(params.contactId, updates); 59 | await queryClient.invalidateQueries(["contacts"]); 60 | return redirect(`/contacts/${params.contactId}`); 61 | }; 62 | ``` 63 | 64 | ## 使用`defer` 65 | 66 | 同样,您也可以利用延迟 API: 67 | 68 | ```jsx 69 | function loader() { 70 | return defer({ 71 | // no await! 72 | someData: queryClient.fetchQuery("someKey", fn), 73 | }); 74 | } 75 | 76 | function Comp() { 77 | // *do* useLoaderData for promise 78 | const { someData } = useLoaderData(); 79 | return ( 80 |
81 |

Something

82 | Oops!
} 85 | > 86 | 87 | 88 | 89 | ); 90 | } 91 | 92 | function SomeView() { 93 | // instead of accessing with useAsyncValue 94 | // const someData = useAsyncValue(); 95 | // `useQuery` as usual 96 | const { data } = useQuery("someKey"); 97 | // ... 98 | } 99 | ``` 100 | 101 | ## 重合部分 102 | 103 | 像 `useQuery` 这样的钩子经常返回 pending 和 error 状态,您可以使用它们来拆分您的 UI。使用 React Router,您可以将所有这些分支逻辑放在[`errorElement`](../route/error-element)、[`useNavigation`](../hooks/use-navigation)和[`Await`](../components/await)中,避免在正常路径组件中处理分支逻辑。 104 | 105 | ## 结论 106 | 107 | 有了所有这些 API 的协同工作,您现在就可以使用来自 React Router 的[`useNavigation`](../hooks/use-navigation)来构建待处理状态、优化的用户界面等。使用 React Router 为数据加载、突变和导航状态计时,然后使用 React Query 等库实际实现加载、失效、存储和缓存。 -------------------------------------------------------------------------------- /src/guides/deferred.md: -------------------------------------------------------------------------------- 1 | # 延迟数据指南 2 | 3 | ## 问题 4 | 5 | 想象一下这样一种场景:您的某个路径`loader`需要检索一些数据,而由于某种原因,检索速度相当慢。例如,您要向用户显示一个包裹的位置,该包裹正被送往用户家中: 6 | 7 | ```jsx 8 | import { json, useLoaderData } from "react-router-dom"; 9 | import { getPackageLocation } from "./api/packages"; 10 | 11 | async function loader({ params }) { 12 | const packageLocation = await getPackageLocation( 13 | params.packageId 14 | ); 15 | 16 | return json({ packageLocation }); 17 | } 18 | 19 | function PackageRoute() { 20 | const data = useLoaderData(); 21 | const { packageLocation } = data; 22 | 23 | return ( 24 |
25 |

Let's locate your package

26 |

27 | Your package is at {packageLocation.latitude} lat 28 | and {packageLocation.longitude} long. 29 |

30 |
31 | ); 32 | } 33 | ``` 34 | 35 | 我们假设 `getPackageLocation` 速度较慢。这将导致初始页面加载时间和路径转换时间与最慢的数据一样长。有几种方法可以优化这种情况并改善用户体验: 36 | 37 | - 让缓慢的事情加速(😅)。 38 | - 使用 `Promise.all` 实现数据加载的并行化(在我们的示例中没有什么需要并行化的,但在其他情况下可能会有点帮助)。 39 | - 添加全局过渡旋转器(对用户体验有一定帮助)。 40 | - 添加本地化的骨架用户界面(对用户体验有一定帮助)。 41 | 42 | 如果这些方法效果不佳,那么您可能不得不将慢速数据从 `loader` 移到组件获取中(并在加载时显示骨架回退 UI)。在这种情况下,您需要在挂载时渲染后备 UI,然后启动数据获取。从 DX 的角度来看,这其实并不可怕,这要归功于[`useFetcher`](../hooks/use-fetcher)。从用户体验的角度来看,这改善了客户端转换和初始页面加载的加载体验。因此,这似乎确实解决了问题。 43 | 44 | 但由于以下两个原因,在大多数情况下(尤其是在对路由组件进行代码拆分的情况下),它仍然不是最佳选择: 45 | 46 | 1. 客户端获取将数据请求置于瀑布式流程中:文档 -> JavaScript -> 懒加载路由 -> 数据获取 47 | 2. 您的代码无法在组件获取和路由获取之间轻松切换(稍后将详细介绍)。 48 | 49 | ## 解决方案 50 | 51 | React Router 使用 [`defer`响应](../utils/defer) 实用程序和 [``](../components/await) 组件/[ `useAsyncValue`](../hooks/use-async-value) 钩子,并利用 React 18 的 Suspense 来获取数据。通过使用这些 API,您可以解决这两个问题: 52 | 53 | 1. 您的数据不再是瀑布式的:文档 -> JavaScript -> 懒加载路径和数据(并行)。 54 | 2. 您的代码可以在渲染回退和等待数据之间轻松切换 55 | 56 | 让我们深入了解一下如何做到这一点。 57 | 58 | ### 使用`defer` 59 | 60 | 首先,为您的慢速数据请求添加 `` ,在这种情况下,您更希望呈现一个回调 UI。让我们在上面的示例中这样做: 61 | 62 | ```jsx 63 | import { 64 | Await, 65 | defer, 66 | useLoaderData, 67 | } from "react-router-dom"; 68 | import { getPackageLocation } from "./api/packages"; 69 | 70 | async function loader({ params }) { 71 | const packageLocationPromise = getPackageLocation( 72 | params.packageId 73 | ); 74 | 75 | return defer({ 76 | packageLocation: packageLocationPromise, 77 | }); 78 | } 79 | 80 | export default function PackageRoute() { 81 | const data = useLoaderData(); 82 | 83 | return ( 84 |
85 |

Let's locate your package

86 | Loading package location...

} 88 | > 89 | Error loading package location!

93 | } 94 | > 95 | {(packageLocation) => ( 96 |

97 | Your package is at {packageLocation.latitude}{" "} 98 | lat and {packageLocation.longitude} long. 99 |

100 | )} 101 |
102 |
103 |
104 | ); 105 | } 106 | ``` 107 | 108 | 或者,也可以使用 `useAsyncValue` 钩子: 109 | 110 | 如果你不喜欢使用 render 属性,你可以使用一个 Hook,但你必须把代码拆分成另一个组件: 111 | 112 | ```jsx 113 | export default function PackageRoute() { 114 | const data = useLoaderData(); 115 | 116 | return ( 117 |
118 |

Let's locate your package

119 | Loading package location...

} 121 | > 122 | Error loading package location!

126 | } 127 | > 128 | 129 |
130 |
131 |
132 | ); 133 | } 134 | 135 | function PackageLocation() { 136 | const packageLocation = useAsyncValue(); 137 | return ( 138 |

139 | Your package is at {packageLocation.latitude} lat and{" "} 140 | {packageLocation.longitude} long. 141 |

142 | ); 143 | } 144 | ``` 145 | 146 | ## 评估解决方案 147 | 148 | 因此,我们不会在触发获取请求前等待组件,而是在用户开始转换到新路由时,立即启动对慢速数据的请求。这可以大大加快较慢网络的用户体验。 149 | 150 | 此外,React Router 为此提供的 API 非常人性化。您可以根据是否包含 `await` 关键字,在是否延迟之间进行切换: 151 | 152 | ```jsx 153 | return defer({ 154 | // not deferred: 155 | packageLocation: await packageLocationPromise, 156 | // deferred: 157 | packageLocation: packageLocationPromise, 158 | }); 159 | ``` 160 | 161 | 因此,您可以进行 A/B 延迟测试,甚至可以根据用户或请求的数据来决定是否延迟: 162 | 163 | ```jsx 164 | async function loader({ request, params }) { 165 | const packageLocationPromise = getPackageLocation( 166 | params.packageId 167 | ); 168 | const shouldDefer = shouldDeferPackageLocation( 169 | request, 170 | params.packageId 171 | ); 172 | 173 | return defer({ 174 | packageLocation: shouldDefer 175 | ? packageLocationPromise 176 | : await packageLocationPromise, 177 | }); 178 | } 179 | ``` 180 | 181 | `shouldDeferPackageLocation` 可以用来检查提出请求的用户、软件包位置数据是否在缓存中、A/B 测试的状态或其他任何你想要的信息。这真是太贴心了 🍭 182 | 183 | ## 常问问题 184 | 185 | ### 为什么不默认推迟一切? 186 | 187 | eact Router defer API 是 React Router 提供的另一个工具,它为您提供了一种在权衡之间做出选择的好方法。你想让页面渲染得更快吗?那就延迟吧。你想要更低的 CLS(内容布局偏移)?不要延迟。你想要更快的渲染速度,但也想要更低的 CLS?那就只延迟那些慢且不重要的内容。 188 | 189 | 这都是权衡的结果,而 API 设计的精妙之处在于,它非常适合你进行简单的实验,看看哪种权衡方式能为你的真实世界关键指标带来更好的结果。 190 | 191 | ### ``回退何时渲染? 192 | 193 | `` 组件只会在初始呈现 `` 组件时,在 `` 边界上抛出`promise`,且`promise`未确定。如果属性发生变化,它不会重新渲染回调。实际上,这意味着当用户提交表单并重新验证`loader`数据时,不会呈现回调。当用户使用不同的参数导航到相同的路径时(在上述示例中,如果用户从左侧的套餐列表中选择在右侧找到自己的位置),就会呈现回调。 194 | 195 | 一开始,我们可能会觉得这与直觉相悖,但请别急,我们已经仔细考虑过这个问题,而且这种工作方式非常重要。让我们想象一下没有延迟 API 的世界。在这种情况下,您可能需要为表单提交/重新验证实现优化用户界面。 196 | 197 | 当您决定尝试 `defer` 的权衡时,我们不希望您必须更改或移除这些优化,因为我们希望您能在推迟和不推迟某些数据之间轻松切换。因此,我们确保您现有的乐观状态以同样的方式运行。如果我们不这样做,您可能会体验到我们所说的 "Popcorn UI",即提交数据会触发回调加载状态,而不是您辛苦开发的优化的用户界面。 198 | 199 | 因此,请记住这一点:**延迟 100% 只涉及路由及其参数的初始加载**。 200 | 201 | ### 为什么加载器返回的响应对象不再起作用了? 202 | 203 | 当您使用 `defer` 时,您是在告诉 React Router 立即加载页面,而不使用延迟数据。在返回 `Response` 对象之前,页面已经加载完毕,因此响应的自动处理方式与使用 `return fetch(url)` 时不同。 204 | 205 | 因此,您需要处理自己的 `Response` 进程,并使用数据而不是 `Response` 实例来解决您的延迟 Promise 问题。 206 | 207 | ```jsx 208 | async function loader({ request, params }) { 209 | return defer({ 210 | // Broken! Resolves with a Response 211 | // broken: fetch(url), 212 | 213 | // Fixed! Resolves with the response data 214 | data: fetch(url).then((res) => res.json()), 215 | }); 216 | } 217 | ``` 218 | 219 | 或者考虑一下我们的延迟数据会返回重定向 `Response` 的情况。您可以检测重定向并将状态代码和位置作为数据发送回来,然后您可以通过 `useEffect` 和 `useNavigate` 在组件中执行客户端重定向。 220 | 221 | ```jsx 222 | async function loader({ request, params }) { 223 | let data = fetch(url).then((res) => { 224 | if (res.status == 301) { 225 | return { 226 | isRedirect: true, 227 | status: res.status, 228 | location: res.headers.get("Location"), 229 | }; 230 | } 231 | return res.json(); 232 | }); 233 | 234 | return defer({ data }); 235 | } 236 | ``` -------------------------------------------------------------------------------- /src/guides/form-data.md: -------------------------------------------------------------------------------- 1 | # 使用 FormData 2 | 3 | > NOTE 4 | > 5 | > TODO:本文档为存根 6 | 7 | 常见的技巧是使用[`Object.fromEntries`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries)将整个 formData 转换为对象: 8 | 9 | ```jsx 10 | const data = Object.fromEntries(await request.formData()); 11 | data.songTitle; 12 | data.lyrics; 13 | ``` 14 | 15 | -------------------------------------------------------------------------------- /src/guides/index-search-param.md: -------------------------------------------------------------------------------- 1 | # 索引查询参数 2 | 3 | 在提交表单时,您可能会发现应用程序的 URL 中出现一个乱码 `?index` 。 4 | 5 | 由于存在嵌套路由,路由层次结构中的多个路由都能与 URL 匹配。与导航不同的是,导航会调用所有匹配的路由加载器来构建用户界面,而提交表单时*只会调用一个操作*。 6 | 7 | 由于索引路由与其父路由共享相同的 URL,因此 `?index` 参数可帮助您区分两者。 8 | 9 | 例如,请看下面的路由和表单: 10 | 11 | ```jsx 12 | createBrowserRouter([ 13 | { 14 | path: "/projects", 15 | element: , 16 | action: ProjectsLayout.action, 17 | children: [ 18 | { 19 | index: true, 20 | element: , 21 | action: ProjectsPage.action, 22 | }, 23 | ], 24 | }, 25 | ]); 26 | 27 | ; 28 | ; 29 | ``` 30 | 31 | `?index` 参数将提交到索引路由,不带索引参数的操作将提交到父路由。 32 | 33 | 当 `` 在没有 `action` 的索引路由中呈现时,将自动附加 `?index` 参数,以便表单发布到索引路由。下面的表单在提交后会发布到 `/projects?index` ,因为它是在项目索引路由的上下文中呈现的: 34 | 35 | ```jsx 36 | function ProjectsIndex() { 37 | return ; 38 | } 39 | ``` 40 | 41 | 如果您将代码移至 `ProjectsLayout` 路由,它将转而发布到 `/projects` 。 42 | 43 | 这适用于 `` 及其所有同类产品: 44 | 45 | ```jsx 46 | let submit = useSubmit(); 47 | submit({}, { action: "/projects" }); 48 | submit({}, { action: "/projects?index" }); 49 | 50 | let fetcher = useFetcher(); 51 | fetcher.submit({}, { action: "/projects" }); 52 | fetcher.submit({}, { action: "/projects?index" }); 53 | ; 54 | ; 55 | ; // defaults to the route in context 56 | ``` 57 | 58 | -------------------------------------------------------------------------------- /src/guides/ssr.md: -------------------------------------------------------------------------------- 1 | # 服务器端渲染 2 | 3 | React Router 中最基本的服务器呈现非常简单。不过,除了获取正确的路由来进行呈现外,还有很多其他事项需要考虑。以下是您需要处理的事项的不完整列表: 4 | 5 | - 为服务器和浏览器打包代码 6 | - 不打包服务器端专用代码到浏览器端包中 7 | - 可在服务器端和浏览器端运行的代码拆分功能 8 | - 服务器端数据加载,让你真正有东西可呈现 9 | - 适用于客户端和服务器的数据加载策略 10 | - 处理服务器和客户端的代码分割 11 | - 正确的 HTTP 状态代码和重定向 12 | - 环境变量和机密信息 13 | - 部署 14 | 15 | 设置好这一切可能会很麻烦,但只有在服务器渲染时才能获得的性能和用户体验特性是值得的。 16 | 17 | 如果您想对 React Router 应用程序进行服务器渲染,我们强烈推荐您使用[Remix](https://remix.run/)。这是我们的另一个项目,它构建于 React Router 之上,可以处理上述所有问题,甚至更多。试试看吧! 18 | 19 | 如果您想自己解决这个问题,您需要在服务器上使用 `` 或 `` ,这取决于您选择的[路由](../routers/picking-a-router)。如果使用 `` ,请跳转到 ["不使用数据路由 "](../guides/ssr#without-a-data-router)部分。 20 | 21 | ## 使用数据路由 22 | 23 | 首先,您需要为数据路由定义路由,这些路由将在服务端和客户端中使用: 24 | 25 | `router.jsx` 26 | 27 | ```jsx 28 | const React = require("react"); 29 | const { json, useLoaderData } = require("react-router-dom"); 30 | 31 | const routes = [ 32 | { 33 | path: "/", 34 | loader() { 35 | return json({ message: "Welcome to React Router!" }); 36 | }, 37 | Component() { 38 | let data = useLoaderData(); 39 | return

{data.message}

; 40 | }, 41 | }, 42 | ]; 43 | 44 | module.exports = routes; 45 | ``` 46 | 47 | > NOTE 48 | > 49 | > 在这些示例中,我们使用了 CJS 模块,以简化服务器,但一般情况下,您会使用 ESM 模块或更高级别的打包程序,如 `esbuild` 、 `vite` 或 `webpack` 。 50 | 51 | 定义好路由后,我们就可以在 express 服务器中创建一个处理程序,然后使用 `createStaticHandler()` 为路由加载数据。请记住,数据路由器的主要目标是将数据获取与渲染解耦,因此,在使用数据路由器进行服务器渲染时,我们有不同的获取和渲染步骤。 52 | 53 | `server.jsx` 54 | 55 | ```jsx 56 | const express = require("express"); 57 | const { 58 | createStaticHandler, 59 | } = require("react-router-dom/server"); 60 | 61 | const createFetchRequest = require("./request"); 62 | const routes = require("./routes"); 63 | 64 | const app = express(); 65 | 66 | let handler = createStaticHandler(routes); 67 | 68 | app.get("*", async (req, res) => { 69 | let fetchRequest = createFetchRequest(req); 70 | let context = await handler.query(fetchRequest); 71 | 72 | // We'll tackle rendering next... 73 | }); 74 | 75 | const listener = app.listen(3000, () => { 76 | let { port } = listener.address(); 77 | console.log(`Listening on port ${port}`); 78 | }); 79 | ``` 80 | 81 | 请注意,我们必须先将传入的 Express 请求转换为 Fetch 请求,这正是静态处理程序方法的操作对象。 `createFetchRequest` 方法是针对 Express 请求的,在本例中是从 `@remix-run/express` 适配程序中提取的: 82 | 83 | `request.js` 84 | 85 | ```js 86 | module.exports = function createFetchRequest(req) { 87 | let origin = `${req.protocol}://${req.get("host")}`; 88 | // Note: This had to take originalUrl into account for presumably vite's proxying 89 | let url = new URL(req.originalUrl || req.url, origin); 90 | 91 | let controller = new AbortController(); 92 | req.on("close", () => controller.abort()); 93 | 94 | let headers = new Headers(); 95 | 96 | for (let [key, values] of Object.entries(req.headers)) { 97 | if (values) { 98 | if (Array.isArray(values)) { 99 | for (let value of values) { 100 | headers.append(key, value); 101 | } 102 | } else { 103 | headers.set(key, values); 104 | } 105 | } 106 | } 107 | 108 | let init = { 109 | method: req.method, 110 | headers, 111 | signal: controller.signal, 112 | }; 113 | 114 | if (req.method !== "GET" && req.method !== "HEAD") { 115 | init.body = req.body; 116 | } 117 | 118 | return new Request(url.href, init); 119 | }; 120 | ``` 121 | 122 | 通过执行所有匹配的路由`loader`加载数据后,我们使用 `createStaticRouter()` 和 `` 渲染 HTML 并将响应发送回浏览器: 123 | 124 | `server.jsx` 125 | 126 | ```jsx 127 | app.get("*", async (req, res) => { 128 | let fetchRequest = createFetchRequest(req); 129 | let context = await handler.query(fetchRequest); 130 | 131 | let router = createStaticRouter( 132 | handler.dataRoutes, 133 | context 134 | ); 135 | let html = ReactDOMServer.renderToString( 136 | 140 | ); 141 | 142 | res.send("" + html); 143 | }); 144 | ``` 145 | 146 | 将 HTML 发送回浏览器后,我们需要使用 `createBrowserRouter()` 和 `` 在客户端 "水合 "应用程序: 147 | 148 | `entry-client.jsx` 149 | 150 | ```jsx 151 | import * as React from "react"; 152 | import * as ReactDOM from "react-dom/client"; 153 | import { 154 | createBrowserRouter, 155 | RouterProvider, 156 | } from "react-router-dom"; 157 | 158 | import { routes } from "./routes"; 159 | 160 | let router = createBrowserRouter(routes); 161 | 162 | ReactDOM.hydrateRoot( 163 | document.getElementById("app"), 164 | 165 | ); 166 | ``` 167 | 168 | 这样,一个服务器端渲染和水合的应用程序就完成了!有关工作示例,您也可以参考 Github 代码库中的[示例](https://github.com/remix-run/react-router/tree/main/examples/ssr-data-router)。 169 | 170 | ### 其他概念 171 | 172 | 如上所述,服务器端渲染在大规模应用和生产级应用中非常棘手,如果你想实现这一目标,我们强烈推荐你使用 [Remix](https://remix.run/) 。但是,如果你要走手动的路由,这里有一些额外的概念你可能需要考虑: 173 | 174 | #### Hydration 175 | 176 | 服务器端渲染的一个核心概念是 [hydration](https://react.dev/reference/react-dom/client/hydrateRoot),它涉及将客户端 React 应用程序 "附加 "到服务器渲染的 HTML 上。要正确地做到这一点,我们需要在与服务器渲染时相同的状态下创建客户端 React Router 应用程序。当服务器通过 `loader` 函数渲染加载数据时,我们需要将这些数据发送上去,这样就可以使用相同的加载器数据创建客户端路由器,用于初始渲染/水合。 177 | 178 | 本指南中介绍的 `` 和 `createBrowserRouter` 的基本用法可以在内部处理这一问题,但如果需要控制水合过程,可以通过 [``](../routers/static-router-provider#hydrate) 禁用自动水合过程。 179 | 180 | 在某些高级用例中,您可能希望对客户端 React Router 应用程序进行部分水合。您可以通过传给 `createBrowserRouter` 的 [`future.v7_partialHydration`](../routers/create-browser-router#partial-hydration-data) 标志来实现这一点。 181 | 182 | #### 重定向 183 | 184 | 如果有任何`loader`重定向, `handler.query` 将直接返回 `Response` ,因此应检查这一点并发送重定向响应,而不是尝试呈现 HTML 文档: 185 | 186 | `server.jsx` 187 | 188 | ```jsx 189 | app.get("*", async (req, res) => { 190 | let fetchRequest = createFetchRequest(req); 191 | let context = await handler.query(fetchRequest); 192 | 193 | if ( 194 | context instanceof Response && 195 | [301, 302, 303, 307, 308].includes(context.status) 196 | ) { 197 | return res.redirect( 198 | context.status, 199 | context.headers.get("Location") 200 | ); 201 | } 202 | 203 | // Render HTML... 204 | }); 205 | ``` 206 | 207 | #### 懒加载路由 208 | 209 | 如果您在路由中使用了[`route.lazy`](../route/lazy),那么在客户端上,您可能已经拥有了水合所需的所有数据,但还没有路由定义!理想情况下,您的设置会在服务器上确定匹配的路由,并在关键路径上交付路由包,这样您就不会在最初匹配的路由上使用 `lazy` 。但如果情况并非如此,则需要在水合之前加载这些路由并更新到位,以避免路由器退回到加载状态: 210 | 211 | `entry-client.jsx` 212 | 213 | ```jsx 214 | // Determine if any of the initial routes are lazy 215 | let lazyMatches = matchRoutes( 216 | routes, 217 | window.location 218 | )?.filter((m) => m.route.lazy); 219 | 220 | // Load the lazy matches and update the routes before creating your router 221 | // so we can hydrate the SSR-rendered content synchronously 222 | if (lazyMatches && lazyMatches?.length > 0) { 223 | await Promise.all( 224 | lazyMatches.map(async (m) => { 225 | let routeModule = await m.route.lazy(); 226 | Object.assign(m.route, { 227 | ...routeModule, 228 | lazy: undefined, 229 | }); 230 | }) 231 | ); 232 | } 233 | 234 | let router = createBrowserRouter(routes); 235 | 236 | ReactDOM.hydrateRoot( 237 | document.getElementById("app"), 238 | 239 | ); 240 | ``` 241 | 242 | 另请参阅: 243 | 244 | - [`createStaticHandler`](../routers/create-static-handler) 245 | - [`createStaticRouter`](../routers/create-static-router) 246 | - [``](../routers/static-router-provider) 247 | 248 | ## 不使用数据路由 249 | 250 | 首先,你需要某种在服务器和浏览器上呈现的 "应用程序 "或 "根 "组件: 251 | 252 | `App.jsx` 253 | 254 | ```jsx 255 | export default function App() { 256 | return ( 257 | 258 | 259 | Server Rendered App 260 | 261 | 262 | 263 | Home} /> 264 | About} /> 265 | 266 |