diff --git a/package.json b/package.json index 918d42fa20..c1cd16741a 100644 --- a/package.json +++ b/package.json @@ -15,12 +15,13 @@ "prettier:diff": "yarn nit:source", "lint-heading-ids": "node scripts/headingIdLinter.js", "fix-headings": "node scripts/headingIdLinter.js --fix", - "ci-check": "npm-run-all prettier:diff --parallel lint tsc lint-heading-ids rss", + "ci-check": "npm-run-all prettier:diff --parallel lint tsc lint-heading-ids rss deadlinks", "tsc": "tsc --noEmit", "start": "next start", "postinstall": "is-ci || husky install .husky", "check-all": "npm-run-all prettier lint:fix tsc rss", - "rss": "node scripts/generateRss.js" + "rss": "node scripts/generateRss.js", + "deadlinks": "node scripts/deadLinkChecker.js" }, "dependencies": { "@codesandbox/sandpack-react": "2.13.5", @@ -61,6 +62,7 @@ "autoprefixer": "^10.4.2", "babel-eslint": "10.x", "babel-plugin-react-compiler": "19.0.0-beta-e552027-20250112", + "chalk": "4.1.2", "eslint": "7.x", "eslint-config-next": "12.0.3", "eslint-config-react-app": "^5.2.1", diff --git a/scripts/deadLinkChecker.js b/scripts/deadLinkChecker.js new file mode 100644 index 0000000000..ab8761e260 --- /dev/null +++ b/scripts/deadLinkChecker.js @@ -0,0 +1,342 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); +const globby = require('globby'); +const chalk = require('chalk'); + +const CONTENT_DIR = path.join(__dirname, '../src/content'); +const PUBLIC_DIR = path.join(__dirname, '../public'); +const fileCache = new Map(); +const anchorMap = new Map(); // Map> +const contributorMap = new Map(); // Map +let errorCodes = new Set(); + +async function readFileWithCache(filePath) { + if (!fileCache.has(filePath)) { + try { + const content = await fs.promises.readFile(filePath, 'utf8'); + fileCache.set(filePath, content); + } catch (error) { + throw new Error(`Failed to read file ${filePath}: ${error.message}`); + } + } + return fileCache.get(filePath); +} + +async function fileExists(filePath) { + try { + await fs.promises.access(filePath, fs.constants.R_OK); + return true; + } catch { + return false; + } +} + +function getMarkdownFiles() { + // Convert Windows paths to POSIX for globby compatibility + const baseDir = CONTENT_DIR.replace(/\\/g, '/'); + const patterns = [ + path.posix.join(baseDir, '**/*.md'), + path.posix.join(baseDir, '**/*.mdx'), + ]; + return globby.sync(patterns); +} + +function extractAnchorsFromContent(content) { + const anchors = new Set(); + + // MDX-style heading IDs: {/*anchor-id*/} + const mdxPattern = /\{\/\*([a-zA-Z0-9-_]+)\*\/\}/g; + let match; + while ((match = mdxPattern.exec(content)) !== null) { + anchors.add(match[1].toLowerCase()); + } + + // HTML id attributes + const htmlIdPattern = /\sid=["']([a-zA-Z0-9-_]+)["']/g; + while ((match = htmlIdPattern.exec(content)) !== null) { + anchors.add(match[1].toLowerCase()); + } + + // Markdown heading with explicit ID: ## Heading {#anchor-id} + const markdownHeadingPattern = /^#+\s+.*\{#([a-zA-Z0-9-_]+)\}/gm; + while ((match = markdownHeadingPattern.exec(content)) !== null) { + anchors.add(match[1].toLowerCase()); + } + + return anchors; +} + +async function buildAnchorMap(files) { + for (const filePath of files) { + const content = await readFileWithCache(filePath); + const anchors = extractAnchorsFromContent(content); + if (anchors.size > 0) { + anchorMap.set(filePath, anchors); + } + } +} + +function extractLinksFromContent(content) { + const linkPattern = /\[([^\]]*)\]\(([^)]+)\)/g; + const links = []; + let match; + + while ((match = linkPattern.exec(content)) !== null) { + const [, linkText, linkUrl] = match; + if (linkUrl.startsWith('/') && !linkUrl.startsWith('//')) { + const lines = content.substring(0, match.index).split('\n'); + const line = lines.length; + const lastLineStart = + lines.length > 1 ? content.lastIndexOf('\n', match.index - 1) + 1 : 0; + const column = match.index - lastLineStart + 1; + + links.push({ + text: linkText, + url: linkUrl, + line, + column, + }); + } + } + + return links; +} + +async function findTargetFile(urlPath) { + // Check if it's an image or static asset that might be in the public directory + const imageExtensions = [ + '.png', + '.jpg', + '.jpeg', + '.gif', + '.svg', + '.ico', + '.webp', + ]; + const hasImageExtension = imageExtensions.some((ext) => + urlPath.toLowerCase().endsWith(ext) + ); + + if (hasImageExtension || urlPath.includes('.')) { + // Check in public directory (with and without leading slash) + const publicPaths = [ + path.join(PUBLIC_DIR, urlPath), + path.join(PUBLIC_DIR, urlPath.substring(1)), + ]; + + for (const p of publicPaths) { + if (await fileExists(p)) { + return p; + } + } + } + + const possiblePaths = [ + path.join(CONTENT_DIR, urlPath + '.md'), + path.join(CONTENT_DIR, urlPath + '.mdx'), + path.join(CONTENT_DIR, urlPath, 'index.md'), + path.join(CONTENT_DIR, urlPath, 'index.mdx'), + // Without leading slash + path.join(CONTENT_DIR, urlPath.substring(1) + '.md'), + path.join(CONTENT_DIR, urlPath.substring(1) + '.mdx'), + path.join(CONTENT_DIR, urlPath.substring(1), 'index.md'), + path.join(CONTENT_DIR, urlPath.substring(1), 'index.mdx'), + ]; + + for (const p of possiblePaths) { + if (await fileExists(p)) { + return p; + } + } + return null; +} + +async function validateLink(link) { + const urlAnchorPattern = /#([a-zA-Z0-9-_]+)$/; + const anchorMatch = link.url.match(urlAnchorPattern); + const urlWithoutAnchor = link.url.replace(urlAnchorPattern, ''); + + if (urlWithoutAnchor === '/') { + return {valid: true}; + } + + // Check if it's an error code link + const errorCodeMatch = urlWithoutAnchor.match(/^\/errors\/(\d+)$/); + if (errorCodeMatch) { + const code = errorCodeMatch[1]; + if (!errorCodes.has(code)) { + return { + valid: false, + reason: `Error code ${code} not found in React error codes`, + }; + } + return {valid: true}; + } + + // Check if it's a contributor link on the team or acknowledgements page + if ( + anchorMatch && + (urlWithoutAnchor === '/community/team' || + urlWithoutAnchor === '/community/acknowledgements') + ) { + const anchorId = anchorMatch[1].toLowerCase(); + if (contributorMap.has(anchorId)) { + const correctUrl = contributorMap.get(anchorId); + if (correctUrl !== link.url) { + return { + valid: false, + reason: `Contributor link should be updated to: ${correctUrl}`, + }; + } + return {valid: true}; + } else { + return { + valid: false, + reason: `Contributor link not found`, + }; + } + } + + const targetFile = await findTargetFile(urlWithoutAnchor); + + if (!targetFile) { + return { + valid: false, + reason: `Target file not found for: ${urlWithoutAnchor}`, + }; + } + + // Only check anchors for content files, not static assets + if (anchorMatch && targetFile.startsWith(CONTENT_DIR)) { + const anchorId = anchorMatch[1].toLowerCase(); + + // TODO handle more special cases. These are usually from custom MDX components that include + // a Heading from src/components/MDX/Heading.tsx which automatically injects an anchor tag. + switch (anchorId) { + case 'challenges': + case 'recap': { + return {valid: true}; + } + } + + const fileAnchors = anchorMap.get(targetFile); + + if (!fileAnchors || !fileAnchors.has(anchorId)) { + return { + valid: false, + reason: `Anchor #${anchorMatch[1]} not found in ${path.relative( + CONTENT_DIR, + targetFile + )}`, + }; + } + } + + return {valid: true}; +} + +async function processFile(filePath) { + const content = await readFileWithCache(filePath); + const links = extractLinksFromContent(content); + const deadLinks = []; + + for (const link of links) { + const result = await validateLink(link); + if (!result.valid) { + deadLinks.push({ + file: path.relative(process.cwd(), filePath), + line: link.line, + column: link.column, + text: link.text, + url: link.url, + reason: result.reason, + }); + } + } + + return {deadLinks, totalLinks: links.length}; +} + +async function buildContributorMap() { + const teamFile = path.join(CONTENT_DIR, 'community/team.md'); + const teamContent = await readFileWithCache(teamFile); + + const teamMemberPattern = /]*permalink=["']([^"']+)["']/g; + let match; + + while ((match = teamMemberPattern.exec(teamContent)) !== null) { + const permalink = match[1]; + contributorMap.set(permalink, `/community/team#${permalink}`); + } + + const ackFile = path.join(CONTENT_DIR, 'community/acknowledgements.md'); + const ackContent = await readFileWithCache(ackFile); + const contributorPattern = /\*\s*\[([^\]]+)\]\(([^)]+)\)/g; + + while ((match = contributorPattern.exec(ackContent)) !== null) { + const name = match[1]; + const url = match[2]; + const hyphenatedName = name.toLowerCase().replace(/\s+/g, '-'); + if (!contributorMap.has(hyphenatedName)) { + contributorMap.set(hyphenatedName, url); + } + } +} + +async function fetchErrorCodes() { + try { + const response = await fetch( + 'https://raw.githubusercontent.com/facebook/react/main/scripts/error-codes/codes.json' + ); + if (!response.ok) { + throw new Error(`Failed to fetch error codes: ${response.status}`); + } + const codes = await response.json(); + errorCodes = new Set(Object.keys(codes)); + console.log(chalk.gray(`Fetched ${errorCodes.size} React error codes\n`)); + } catch (error) { + throw new Error(`Failed to fetch error codes: ${error.message}`); + } +} + +async function main() { + const files = getMarkdownFiles(); + console.log(chalk.gray(`Checking ${files.length} markdown files...`)); + + await fetchErrorCodes(); + await buildContributorMap(); + await buildAnchorMap(files); + + const filePromises = files.map((filePath) => processFile(filePath)); + const results = await Promise.all(filePromises); + const deadLinks = results.flatMap((r) => r.deadLinks); + const totalLinks = results.reduce((sum, r) => sum + r.totalLinks, 0); + + if (deadLinks.length > 0) { + for (const link of deadLinks) { + console.log(chalk.yellow(`${link.file}:${link.line}:${link.column}`)); + console.log(chalk.reset(` Link text: ${link.text}`)); + console.log(chalk.reset(` URL: ${link.url}`)); + console.log(` ${chalk.red('✗')} ${chalk.red(link.reason)}\n`); + } + + console.log( + chalk.red( + `\nFound ${deadLinks.length} dead link${ + deadLinks.length > 1 ? 's' : '' + } out of ${totalLinks} total links\n` + ) + ); + process.exit(1); + } + + console.log(chalk.green(`\n✓ All ${totalLinks} links are valid!\n`)); + process.exit(0); +} + +main().catch((error) => { + console.log(chalk.red(`Error: ${error.message}`)); + process.exit(1); +}); diff --git a/src/content/blog/2023/03/16/introducing-react-dev.md b/src/content/blog/2023/03/16/introducing-react-dev.md index 9643cc79ba..ef95d50ad7 100644 --- a/src/content/blog/2023/03/16/introducing-react-dev.md +++ b/src/content/blog/2023/03/16/introducing-react-dev.md @@ -41,8 +41,13 @@ description: 今天,我们非常高兴推出 react.dev,React 及其文档的 **新文档从一开始就使用 Hook 来介绍 React**。新文档分为两个主要部分: +<<<<<<< HEAD * **[学习 React](/learn)** 是一个自学课程,从头开始介绍 React。 * **[API 参考](/reference)** 提供了每个 React API 的详细信息和使用示例。 +======= +* **[Learn React](/learn)** is a self-paced course that teaches React from scratch. +* **[API Reference](/reference/react)** provides the details and usage examples for every React API. +>>>>>>> d52b3ec734077fd56f012fc2b30a67928d14cc73 让我们更仔细地看看可以从每个部分中找到什么。 @@ -607,7 +612,11 @@ button { display: block; margin-top: 10px; } +<<<<<<< HEAD 一些 API 页面还包括针对常见问题的 [故障排除](/reference/react/useEffect#troubleshooting) 和针对弃用 API 的 [替代方案](/reference/react-dom/findDOMNode#alternatives)。 +======= +Some API pages also include [Troubleshooting](/reference/react/useEffect#troubleshooting) (for common problems) and [Alternatives](https://18.react.dev/reference/react-dom/findDOMNode#alternatives) (for deprecated APIs). +>>>>>>> d52b3ec734077fd56f012fc2b30a67928d14cc73 我们希望 API 参考不仅仅是用来查找参数,还可以用来查看任何给定 API 可以做的所有不同事情以及与其他 API 相关联的方法。 diff --git a/src/content/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023.md b/src/content/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023.md index 2c3a6c071d..f34d60a8d2 100644 --- a/src/content/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023.md +++ b/src/content/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023.md @@ -31,7 +31,11 @@ RSC 将面向服务器的多页面应用程序的简单“请求/响应”思维 现在我们已经相当好地解决了数据提取的问题,我们正在探索另一个方向:从客户端向服务器发送数据,以便可以执行数据库变更和实现表单。我们通过在服务器/客户端边界传递 Server Action 函数来实现这一点。客户端可以调用该函数,提供无缝 RPC。而在 JavaScript 加载之前,Server Action 还可以提供逐步增强的表单。 +<<<<<<< HEAD RSC 已经在 [Next.js App Router](/learn/start-a-new-react-project#nextjs-app-router) 中发布,展示了一个真正深度集成的路由器,它使用了 RSC 并将其作为 primitive。但这不是构建 RSC 兼容的路由器和框架的唯一方法。RSC 规范和实现提供了特定功能的明确分离,旨在成为适用于兼容 React 框架的组件规范。 +======= +React Server Components has shipped in [Next.js App Router](/learn/creating-a-react-app#nextjs-app-router). This showcases a deep integration of a router that really buys into RSC as a primitive, but it's not the only way to build a RSC-compatible router and framework. There's a clear separation for features provided by the RSC spec and implementation. React Server Components is meant as a spec for components that work across compatible React frameworks. +>>>>>>> d52b3ec734077fd56f012fc2b30a67928d14cc73 我们通常建议使用现有的框架,但你仍然可以构建自定义框架。由于需要深度集成 bundler,构建自定义 RSC 兼容的框架并不像想象中那么容易。当前的若代 bundler 非常适合在客户端使用,但它们并没有专门为将单个模块图分割为服务器和客户端提供一流的支持而设计。因此我们选择直接与 bundler 开发人员合作,以将内置 RSC 作为 primitive。 @@ -92,7 +96,11 @@ React 的核心思想是开发人员将其 UI 定义为当前状态的函数。 ## 追踪 Transition {/*transition-tracing*/} +<<<<<<< HEAD 追踪 Transition 的 API 可以检测 [React Transition](/reference/react/useTransition) 变慢的原因,并调查为什么会变慢。在上次更新后,我们完成了 API 的初始设计,并发布了一个 [RFC](https://github.com/reactjs/rfcs/pull/238),基本功能也已经实现。该项目目前处于暂停状态。我们欢迎对 RFC 进行反馈,并期待恢复其开发,为 React 提供更好的性能测量工具。这将特别对基于 React Transition 构建的路由非常有用,例如 [Next.js App Router](/learn/start-a-new-react-project#nextjs-app-router)。 +======= +The Transition Tracing API lets you detect when [React Transitions](/reference/react/useTransition) become slower and investigate why they may be slow. Following our last update, we have completed the initial design of the API and published an [RFC](https://github.com/reactjs/rfcs/pull/238). The basic capabilities have also been implemented. The project is currently on hold. We welcome feedback on the RFC and look forward to resuming its development to provide a better performance measurement tool for React. This will be particularly useful with routers built on top of React Transitions, like the [Next.js App Router](/learn/creating-a-react-app#nextjs-app-router). +>>>>>>> d52b3ec734077fd56f012fc2b30a67928d14cc73 * * * 除了这个更新,我们的团队最近还在社区播客和直播中客串,更多地讲述我们的工作并回答问题。 diff --git a/src/content/blog/2024/02/15/react-labs-what-we-have-been-working-on-february-2024.md b/src/content/blog/2024/02/15/react-labs-what-we-have-been-working-on-february-2024.md index 6b75e59f63..05abf4bd9b 100644 --- a/src/content/blog/2024/02/15/react-labs-what-we-have-been-working-on-february-2024.md +++ b/src/content/blog/2024/02/15/react-labs-what-we-have-been-working-on-february-2024.md @@ -107,7 +107,11 @@ Activity 仍处于研究阶段,我们剩下的工作是最终确定向库开 除了此更新之外,我们的团队还在会议上发表了演讲并在播客中露面,更多地讲述我们的工作并回答问题。 +<<<<<<< HEAD - [Sathya Gunasekaran](/community/team#sathya-gunasekaran) 在 [React India](https://www.youtube.com/watch?v=kjOacmVsLSE) 大会上介绍了 React 编译器。 +======= +- [Sathya Gunasekaran](https://github.com/gsathya) spoke about the React Compiler at the [React India](https://www.youtube.com/watch?v=kjOacmVsLSE) conference +>>>>>>> d52b3ec734077fd56f012fc2b30a67928d14cc73 - [Dan Abramov](/community/team#dan-abramov) 在 [RemixConf](https://www.youtube.com/watch?v=zMf_xeGPn6s) 上发表了名为“React from Another Dimension”的演讲,探讨了 React 服务器组件与 Action 可能是如何创建的另一种历史。 diff --git a/src/content/blog/2024/04/25/react-19-upgrade-guide.md b/src/content/blog/2024/04/25/react-19-upgrade-guide.md index cb83a61764..2e9131eb35 100644 --- a/src/content/blog/2024/04/25/react-19-upgrade-guide.md +++ b/src/content/blog/2024/04/25/react-19-upgrade-guide.md @@ -129,7 +129,7 @@ For a list of all available codemods, see the [`react-codemod` repo](https://git In previous versions of React, errors thrown during render were caught and rethrown. In DEV, we would also log to `console.error`, resulting in duplicate error logs. -In React 19, we've [improved how errors are handled](/blog/2024/04/25/react-19#error-handling) to reduce duplication by not re-throwing: +In React 19, we've [improved how errors are handled](/blog/2024/12/05/react-19#error-handling) to reduce duplication by not re-throwing: - **Uncaught Errors**: Errors that are not caught by an Error Boundary are reported to `window.reportError`. - **Caught Errors**: Errors that are caught by an Error Boundary are reported to `console.error`. @@ -499,7 +499,7 @@ function AutoselectingInput() { ### Deprecated: `element.ref` {/*deprecated-element-ref*/} -React 19 supports [`ref` as a prop](/blog/2024/04/25/react-19#ref-as-a-prop), so we're deprecating the `element.ref` in place of `element.props.ref`. +React 19 supports [`ref` as a prop](/blog/2024/12/05/react-19#ref-as-a-prop), so we're deprecating the `element.ref` in place of `element.props.ref`. Accessing `element.ref` will warn: diff --git a/src/content/blog/2024/12/05/react-19.md b/src/content/blog/2024/12/05/react-19.md index c6c5ba6d2b..f31fdb5bf0 100644 --- a/src/content/blog/2024/12/05/react-19.md +++ b/src/content/blog/2024/12/05/react-19.md @@ -355,7 +355,11 @@ For more information, see [React DOM Static APIs](/reference/react-dom/static). 服务器组件是一种新的选项,允许在打包前提前渲染组件,在与你的客户端应用程序或 SSR 服务器不同的环境中。这个独立的环境就是 React 服务器组件中的 "服务器"。服务器组件可以在你的 CI 服务器上在构建时运行一次,或者可以在每次请求时使用 web 服务器运行。 +<<<<<<< HEAD React 19 包含了所有从 Canary 渠道引入的 React 服务器组件功能。这意味着,现在可以将 React 19 作为 peer 依赖项来发布带有服务器组件的库,使用 `react-server` [导出条件](https://github.com/reactjs/rfcs/blob/main/text/0227-server-module-conventions.md#react-server-conditional-exports) 用于支持 [全栈 React 架构](/learn/start-a-new-react-project#which-features-make-up-the-react-teams-full-stack-architecture-vision) 的框架。 +======= +React 19 includes all of the React Server Components features included from the Canary channel. This means libraries that ship with Server Components can now target React 19 as a peer dependency with a `react-server` [export condition](https://github.com/reactjs/rfcs/blob/main/text/0227-server-module-conventions.md#react-server-conditional-exports) for use in frameworks that support the [Full-stack React Architecture](/learn/creating-a-react-app#which-features-make-up-the-react-teams-full-stack-architecture-vision). +>>>>>>> d52b3ec734077fd56f012fc2b30a67928d14cc73 @@ -389,7 +393,11 @@ React 19 包含了所有从 Canary 渠道引入的 React 服务器组件功能 服务器 Actions 可以在服务器组件中创建并作为 props 传递给客户端组件,或者可以在客户端组件中导入和使用。 +<<<<<<< HEAD 有关更多信息,请参阅 [React 服务器 Actions](/reference/rsc/server-actions) 文档。 +======= +For more, see the docs for [React Server Actions](/reference/rsc/server-functions). +>>>>>>> d52b3ec734077fd56f012fc2b30a67928d14cc73 ## React 19 中的改进 {/*improvements-in-react-19*/} diff --git a/src/content/blog/2025/04/23/react-labs-view-transitions-activity-and-more.md b/src/content/blog/2025/04/23/react-labs-view-transitions-activity-and-more.md index 80fb35a1c3..1d93abc5f1 100644 --- a/src/content/blog/2025/04/23/react-labs-view-transitions-activity-and-more.md +++ b/src/content/blog/2025/04/23/react-labs-view-transitions-activity-and-more.md @@ -2495,7 +2495,11 @@ root.render( ``` +<<<<<<< HEAD 并使用[视图过渡类](/reference/react/ViewTransition#view-transition-classes)在 CSS 中定义 `slow-fade`: +======= +And define `slow-fade` in CSS using [view transition classes](/reference/react/ViewTransition#view-transition-class): +>>>>>>> d52b3ec734077fd56f012fc2b30a67928d14cc73 ```css ::view-transition-old(.slow-fade) { diff --git a/src/content/learn/add-react-to-an-existing-project.md b/src/content/learn/add-react-to-an-existing-project.md index 6708659ebb..2ec1009002 100644 --- a/src/content/learn/add-react-to-an-existing-project.md +++ b/src/content/learn/add-react-to-an-existing-project.md @@ -20,11 +20,19 @@ title: 将 React 添加到现有项目中 以下是推荐的配置方式: +<<<<<<< HEAD 1. 使用一个 [基于 React 的框架](/learn/start-a-new-react-project) 构建 **应用的 React 部分**。 2. **在框架配置中将 `/some-app` 指定为基本路径**(这里有 [Next.js](https://nextjs.org/docs/app/api-reference/config/next-config-js/basePath) 与 [Gatsby](https://www.gatsbyjs.com/docs/how-to/previews-deploys-hosting/path-prefix/) 的配置样例)。 3. **配置服务器或代理**,以便所有位于 `/some-app/` 下的请求都由 React 应用处理。 这可以确保应用的 React 部分可以受益于这些框架中内置的 [最佳实践](/learn/start-a-new-react-project#can-i-use-react-without-a-framework)。 +======= +1. **Build the React part of your app** using one of the [React-based frameworks](/learn/creating-a-react-app). +2. **Specify `/some-app` as the *base path*** in your framework's configuration (here's how: [Next.js](https://nextjs.org/docs/app/api-reference/config/next-config-js/basePath), [Gatsby](https://www.gatsbyjs.com/docs/how-to/previews-deploys-hosting/path-prefix/)). +3. **Configure your server or a proxy** so that all requests under `/some-app/` are handled by your React app. + +This ensures the React part of your app can [benefit from the best practices](/learn/creating-a-react-app#full-stack-frameworks) baked into those frameworks. +>>>>>>> d52b3ec734077fd56f012fc2b30a67928d14cc73 许多基于 React 的框架都是全栈的,从而可以让你的 React 应用充分利用服务器。但是,即使无法或不想在服务器上运行 JavaScript,也可以使用相同的方法。在这种情况下,将 HTML/CSS/JS 导出(Next.js 的 [`next export` output](https://nextjs.org/docs/advanced-features/static-html-export),Gatsby 的 default)替换为 `/some-app/`。 @@ -149,7 +157,11 @@ root.render(); 请注意 `index.html` 中的原始 HTML 内容是如何保留的,但现在你自己的 `NavigationBar` React 组件出现在 HTML 的 `