如何为 Next.js 项目构建可扩展的架构

2025-05-24

如何为 Next.js 项目构建可扩展的架构

本教程中的所有代码作为完整包均可在此存储库中找到。

如果您觉得本教程有用,请与您的朋友和同事分享!想了解更多类似内容,您可以在YouTube上订阅我或在Twitter上关注我

如果您喜欢这种格式,本教程可以作为视频课程提供:

目录

  1. 什么是 Next.js?
  2. 介绍
  3. 项目设置
  4. 发动机锁定
  5. Git 设置
  6. 代码格式化和质量工具
  7. Git 钩子
  8. VS Code 配置
  9. 调试
  10. 目录结构
  11. 添加故事书
  12. 创建组件模板
  13. 使用组件模板
  14. 添加自定义文档
  15. 添加布局
  16. 部署
  17. 后续步骤
  18. 总结

什么是 Next.js?

Next.js 为您提供最佳的开发者体验,具备生产所需的所有功能:混合静态和服务器渲染、TypeScript 支持、智能捆绑、路由预取等等。无需任何配置。

正如他们上面所述,Next.js 是一款一体化的全栈现代应用程序构建解决方案。它包含对 Typescript 和 React 的一流支持,同时为现代应用程序中一些最常见的需求(例如路由、API、postCSS 工具和代码拆分)提供了简单的解决方案。

它还支持静态站点生成(用于可以在任何地方托管的闪电般快速的静态 HTML 页面)或运行 Node 服务器并支持完全按需数据加载和服务器端渲染页面的托管服务(如 Vercel/AWS/etc)。

Next.js 已迅速成为 Web 开发领域最热门的技能之一。本教程旨在作为文档的“实用”扩展,并帮助您使用大量最佳实践来设置项目,从而提高您在扩展时保持一切管理的可能性。

介绍

本教程并非旨在取代官方文档,官方文档本身也非常棒。我强烈建议您在开始本教程之前至少阅读一下“基本功能”部分,这样您就能熟悉其中的术语、工具以及它们提供的一些组件,这些组件与原生 HTML 版本类似,但通常比原生 HTML 版本“功能更强大”。

请查看目录,了解本教程中我们将涉及的每个主题。我承认其中许多内容都是严格且主观的配置,如果您对其中任何一个部分不感兴趣,那么在大多数情况下,您可以直接跳过这些部分,并且仍然能够轻松完成本教程。

现在,说了这么多,如果您准备好了,那就让我们开始吧!

项目设置

我们将首先使用 Typescript 模板创建一个默认的 Next.js 应用程序。



npx create-next-app --ts nextjs-fullstack-app-template

cd nextjs-fullstack-app-template


Enter fullscreen mode Exit fullscreen mode

首先,我们将进行测试以确保应用程序正常运行。我们将yarn在本例中使用,但您也可以选择使用 NPM。



yarn install

yarn dev


Enter fullscreen mode Exit fullscreen mode

您应该可以在http://localhost:3000上看到演示应用程序

首页加载

还建议运行



yarn build


Enter fullscreen mode Exit fullscreen mode

为了确保您能够成功进行项目的生产构建,建议(但非强制)在运行 Next.js 构建时关闭开发服务器。大多数情况下不会出现问题,但偶尔构建过程可能会导致开发服务器处于异常状态,需要重新启动。

您应该会在命令行中看到一份简洁的报告,其中包含所有已构建页面,并以绿色文本显示,这表示它们小巧高效。我们会在项目开发过程中尽量保持这种状态。

发动机锁定

我们希望所有参与此项目的开发者都使用与我们相同的 Node 引擎和包管理器。为此,我们创建了两个新文件:

  • .nvmrc- 将告诉项目的其他用途使用哪个版本的 Node
  • .npmrc- 将告诉项目的其他用户使用了哪个包管理器

我们正在为这个项目使用Node v14 Fermiumyarn因此我们像这样设置这些值:

.nvmrc



lts/fermium


Enter fullscreen mode Exit fullscreen mode

.npmrc



engine-strict=true


Enter fullscreen mode Exit fullscreen mode

我们之所以使用 Node v14 而不是 v16,是因为本教程的后面部分我们将在 Vercel 上部署,但遗憾的是 Vercel 仍然不支持 Node 16。或许在您阅读本教程时,Vercel 可能会支持 Node 16。您可以在此处关注进度。

你可以使用以下命令检查你的 Node 版本node --version,并确保你设置的版本正确。Node 版本代号列表可在此处找到。

请注意,的用法engine-strict并没有具体说明yarn,我们在中这样做package.json

package.json



  "name": "nextjs-fullstack-app-template",
  "author": "YOUR_NAME",
  "description": "A tutorial and template for creating a production-ready fullstack Next.js application",
  "version": "0.1.0",
  "private": true,
  "license" : "MIT"
  "homepage": "YOUR_GIT_REPO_URL"
  "engines": {
    "node": ">=14.0.0",
    "yarn": ">=1.22.0",
    "npm": "please-use-yarn"
  },
  ...


Enter fullscreen mode Exit fullscreen mode

engines字段用于指定您正在使用的工具的具体版本。如果您愿意,也可以填写您的个​​人信息。

Git 设置

这将是我们对远程存储库进行第一次提交的好时机,以确保我们的更改已备份,并在转移到新内容之前遵循最佳实践,将相关更改分组在单个提交中。

默认情况下,你的 Next.js 项目已经初始化了一个仓库。你可以使用 来检查当前所在的分支git status。它应该显示类似以下内容:



On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   README.md

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        .npmrc
        .nvmrc


Enter fullscreen mode Exit fullscreen mode

这告诉我们我们在main分支上并且我们还没有暂存或做出任何提交。

让我们提交迄今为止的更改。



git add .

git commit -m 'project initialization'


Enter fullscreen mode Exit fullscreen mode

第一个命令将添加并暂存项目目录中所有未被忽略的文件.gitignore。第二个命令将使用我们在标志后写入的消息提交当前项目的状态-m

跳转到您首选的 Git 托管服务提供商(例如Github),并创建一个新的仓库来托管此项目。请确保默认分支的名称与您本地计算机上的分支名称相同,以免造成混淆。

在 Github 上,您可以通过以下方式将全局默认分支名称更改为您喜欢的任何名称:



Settings -> Repositories -> Repository default branch


Enter fullscreen mode Exit fullscreen mode

现在,您可以添加仓库的远程源并进行推送了。创建时,Github 会提供具体的说明。您的语法可能与我的略有不同,具体取决于您使用的是 HTTPS 还是 SSH。



git remote add origin git@github.com:{YOUR_GITHUB_USERNAME}/{YOUR_REPOSITORY_NAME}.git

git push -u origin {YOUR_BRANCH_NAME}


Enter fullscreen mode Exit fullscreen mode

请注意,从现在开始,我们将使用约定提交标准,特别是此处描述的Angular 约定

原因和这个项目中的许多其他功能一样,只是为了为所有开发人员设定一个一致的标准,以最大限度地减少为项目贡献代码时的培训时间。我个人不太关心选择什么标准,只要每个人都同意遵循,这才是最重要的。

一致性就是一切!

代码格式化和质量工具

为了制定一个所有项目贡献者都可使用的标准,以保持代码风格的一致性和遵循基本的最佳实践,我们将实施两种工具:

  • eslint - 编码标准的最佳实践
  • prettier - 用于自动格式化代码文件

ESLint

我们将从 ESLint 开始,它很简单,因为它会自动安装并预先配置 Next.js 项目。

我们只需添加一些额外的配置,使其比默认设置更严格一些。如果您不同意其中设置的任何规则,无需担心,您可以轻松手动禁用其中任何规则。我们配置了所有.eslintrc.json应该已经存在于您根目录中的内容:

.eslintrc.json



{
  "extends": ["next", "next/core-web-vitals", "eslint:recommended"],
  "globals": {
    "React": "readonly"
  },
  "rules": {
    "no-unused-vars": [1, { "args": "after-used", "argsIgnorePattern": "^_" }]
  }
}


Enter fullscreen mode Exit fullscreen mode

在上面的小代码示例中,我们添加了一些额外的默认值,我们已经说过React即使我们没有专门导入它也会始终定义它,并且我添加了一个我喜欢的个人自定义规则,该规则允许您在变量前加上下划线 _ 如果您已经声明了它们但没有在代码中使用它们。

我发现,当您正在处理某个功能并想要准备变量以供稍后使用,但尚未达到实现它们的程度时,这种情况经常出现。

您可以通过运行以下命令来测试您的配置:



yarn lint


Enter fullscreen mode Exit fullscreen mode

您应该会收到如下消息:



✔ No ESLint warnings or errors
Done in 1.47s.


Enter fullscreen mode Exit fullscreen mode

如果你遇到任何错误,ESLint 会非常清楚地解释它们是什么。如果你遇到不喜欢的规则,你可以在“规则”中将其设置为 1(警告)或 0(忽略)来禁用它,如下所示:



  "rules": {
    "no-unused-vars": 0, // As example: Will never bug you about unused variables again
  }


Enter fullscreen mode Exit fullscreen mode

现在让我们提交一条消息build: configure eslint

Prettier

Prettier 会自动帮我们格式化文件。现在就把它添加到项目中吧。

它只在开发过程中需要,所以我会将它添加devDependency-D



yarn add -D prettier


Enter fullscreen mode Exit fullscreen mode

我还建议您获取Prettier VS Code 扩展,这样 VS Code 就可以为您处理文件格式,而您无需依赖命令行工具。在项目中安装并配置它意味着 VSCode 将使用您项目的设置,因此仍然需要在此处添加它。

我们将在根目录中创建两个文件:

.prettierrc



{
  "trailingComma": "es5",
  "tabWidth": 2,
  "semi": true,
  "singleQuote": true
}


Enter fullscreen mode Exit fullscreen mode

这些价值观完全由您自行决定,以决定什么对您的团队和项目最有利。

.prettierignore



.yarn
.next
dist
node_modules


Enter fullscreen mode Exit fullscreen mode

在该文件中,我放置了一个目录列表,其中列出了我不想让 Prettier 浪费任何资源处理的目录。您也可以选择使用 *.html 之类的模式来忽略特定类型的文件。

现在我们添加一个新脚本以便package.json可以运行 Prettier:

package.json



  ...
  "scripts: {
    ...
    "prettier": "prettier --write ."
  }


Enter fullscreen mode Exit fullscreen mode

您现在可以运行



yarn prettier


Enter fullscreen mode Exit fullscreen mode

自动格式化、修复并保存项目中所有未忽略的文件。我的格式化程序默认更新了大约 5 个文件。您可以在 VS Code 左侧的源代码管理选项卡中的已更改文件列表中看到它们。

让我们用 进行另一次提交build: implement prettier

Git 钩子

在开始组件开发之前,我们再来聊聊配置。记住,如果你打算长期在此基础上进行开发,尤其是与其他开发人员团队合作,你肯定希望这个项目尽可能稳固。从一开始就做好配置是值得的。

我们将实现一个名为Husky的工具

Husky 是一个在 git 进程的不同阶段运行脚本的工具,例如添加、提交、推送等。我们希望能够设置某些条件,并且只有当我们的代码满足这些条件时才允许提交和推送等操作成功,假设这表明我们的项目质量是可以接受的。

安装 Husky 运行



yarn add -D husky

npx husky install


Enter fullscreen mode Exit fullscreen mode

第二条命令将.husky在你的项目中创建一个目录。你的钩子将存放在这里。请确保此目录包含在你的代码仓库中,因为它不仅供你自己使用,也供其他开发者使用。

将以下脚本添加到您的package.json文件中:

package.json



  ...
  "scripts: {
    ...
    "prepare": "husky install"
  }


Enter fullscreen mode Exit fullscreen mode

这将确保其他开发人员运行该项目时自动安装 Husky。

创建钩子运行



npx husky add .husky/pre-commit "yarn lint"


Enter fullscreen mode Exit fullscreen mode

上面的代码表明,为了确保提交成功,yarn lint脚本必须首先运行并成功。此处的“成功”表示没有错误。它允许你发出警告(记住,在 ESLint 配置中,设置 1 表示警告,2 表示错误,以便你调整设置)。

让我们创建一个新的提交,并添加以下消息ci: implement husky。如果所有设置都正确,你的 lint 脚本应该会在提交之前运行。

我们将添加另一个:



npx husky add .husky/pre-push "yarn build"


Enter fullscreen mode Exit fullscreen mode

上述代码确保了除非我们的代码能够成功构建,否则不允许推送到远程仓库。这看起来似乎很合理,不是吗?您可以提交此更改并尝试推送来测试一下。


最后,我们将添加一个工具。到目前为止,我们所有的提交信息都遵循一个标准约定,让我们确保团队中的每个人(包括我们自己!)也都遵循这些约定。我们可以为提交信息添加一个 linter:



yarn add -D @commitlint/config-conventional @commitlint/cli


Enter fullscreen mode Exit fullscreen mode

为了配置它,我们将使用一组标准默认值,但我喜欢将该列表明确包含在commitlint.config.js文件中,因为我有时会忘记有哪些可用的前缀:

commitlint.config.js



// build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
// ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
// docs: Documentation only changes
// feat: A new feature
// fix: A bug fix
// perf: A code change that improves performance
// refactor: A code change that neither fixes a bug nor adds a feature
// style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
// test: Adding missing tests or correcting existing tests

module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'body-leading-blank': [1, 'always'],
    'body-max-line-length': [2, 'always', 100],
    'footer-leading-blank': [1, 'always'],
    'footer-max-line-length': [2, 'always', 100],
    'header-max-length': [2, 'always', 100],
    'scope-case': [2, 'always', 'lower-case'],
    'subject-case': [
      2,
      'never',
      ['sentence-case', 'start-case', 'pascal-case', 'upper-case'],
    ],
    'subject-empty': [2, 'never'],
    'subject-full-stop': [2, 'never', '.'],
    'type-case': [2, 'always', 'lower-case'],
    'type-empty': [2, 'never'],
    'type-enum': [
      2,
      'always',
      [
        'build',
        'chore',
        'ci',
        'docs',
        'feat',
        'fix',
        'perf',
        'refactor',
        'revert',
        'style',
        'test',
        'translation',
        'security',
        'changeset',
      ],
    ],
  },
};


Enter fullscreen mode Exit fullscreen mode

然后使用 Husky 启用 commitlint:



npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'
# Sometimes above command doesn't work in some command interpreters
# You can try other commands below to write npx --no -- commitlint --edit $1
# in the commit-msg file.
npx husky add .husky/commit-msg \"npx --no -- commitlint --edit '$1'\"
# or
npx husky add .husky/commit-msg "npx --no -- commitlint --edit $1"


Enter fullscreen mode Exit fullscreen mode

随意尝试一些符合规则的提交,看看它们是如何不被接受的,并且您会收到旨在帮助您纠正它们的反馈。

我现在要创建一个新的提交并附带消息ci: implement commitlint

您可以在下面的屏幕截图中看到此设置的完整结果,希望您的结果看起来类似:

开发经验

VS Code 配置

现在我们已经实现了 ESLint 和 Prettier,我们可以利用一些方便的 VS Code 功能让它们自动运行。

在项目根目录中创建一个名为 的目录.vscode,并在其中创建一个名为 的文件settings.json。这将是一个值列表,用于覆盖已安装的 VS Code 的默认设置。

我们想要将它们放在项目文件夹中的原因是我们可以设置仅适用于该项目的特定设置,并且可以通过将它们包含在代码存储库中与我们团队的其他成员共享它们。

我们将在其中settings.json添加以下值:

.vscode/settings.json



{
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll": true,
    "source.organizeImports": true
  }
}


Enter fullscreen mode Exit fullscreen mode

以上内容将告诉 VS Code 使用 Prettier 扩展作为默认格式化程序(如果您愿意,可以手动使用另一个扩展覆盖)并在每次保存时自动格式化文件并组织导入语句。

非常方便的东西,您不再需要考虑它,因此您可以专注于解决业务问题等重要的事情。

我现在将提交一条带有消息 的信​​息build: implement vscode project settings

调试

让我们建立一个方便的环境来调试我们的应用程序,以防我们在开发过程中遇到任何问题。

在您的.vscode目录中创建一个launch.json文件:

launch.json



{
  "version": "0.1.0",
  "configurations": [
    {
      "name": "Next.js: debug server-side",
      "type": "node-terminal",
      "request": "launch",
      "command": "npm run dev"
    },
    {
      "name": "Next.js: debug client-side",
      "type": "pwa-chrome",
      "request": "launch",
      "url": "http://localhost:3000"
    },
    {
      "name": "Next.js: debug full stack",
      "type": "node-terminal",
      "request": "launch",
      "command": "npm run dev",
      "console": "integratedTerminal",
      "serverReadyAction": {
        "pattern": "started server on .+, url: (https?://.+)",
        "uriFormat": "%s",
        "action": "debugWithChrome"
      }
    }
  ]
}


Enter fullscreen mode Exit fullscreen mode

脚本安装完成后,您有三种调试方式。点击 VS Code 左侧的小“调试并播放”图标,或按下Ctrl + Shift + D进入调试菜单。您可以选择要运行的脚本,并使用“开始/停止”按钮来启动/停止它。

VS Code 调试器

除此之外,或者如果您没有使用 VS Code,我们还可以在您的项目中设置一些有用的调试脚本。

首先,我们将安装跨环境;如果您的队友在不同的环境(Windows、Linux、Mac 等)上工作,则需要设置环境变量。



yarn add -D cross-env


Enter fullscreen mode Exit fullscreen mode

安装该软件包后,我们可以更新package.json dev脚本,使其如下所示:

package.json



{
  ...
  "scripts": {
    ...
    "dev": "cross-env NODE_OPTIONS='--inspect' next dev",
  },
}


Enter fullscreen mode Exit fullscreen mode

这将允许您在开发模式下工作时在浏览器中记录服务器数据,从而更容易调试问题。

在这个阶段,我将创建一个新的提交,并附带消息build: add debugging configuration

目录结构

本节将介绍如何在项目中设置文件夹结构。这是一个很多人会非常反感的话题,而且理由充分!如果目录结构失控,从长远来看,它真的可以成就或毁掉一个项目,尤其是当团队成员不得不花费不必要的时间来猜测文件的位置(或查找文件的位置)时。

我个人喜欢采用一种相当简单的方法,基本上以类模型/视图的风格来区分各个部分。我们将使用三个主要文件夹:



/components
/lib
/pages


Enter fullscreen mode Exit fullscreen mode
  • component- 组成应用程序的各个 UI 组件将放在这里
  • lib- 业务/应用程序/域逻辑将存在于此处。
  • pages- 将是根据所需的 Next.js 结构的实际路由/页面。

除此之外,我们还会设置其他文件夹来支持该项目,但我们正在构建的独特应用程序的几乎所有内容的核心都将存储在这三个目录中。

我们将在其中components创建子目录,用于将类似类型的组件分组。您可以使用任何您喜欢的方法来实现这一点。我曾经多次使用 MUI 库,因此我倾向于遵循其文档中组件的组织方式。

例如输入、表面、导航、实用程序、布局等。

您无需提前创建这些目录并将其留空。我建议您在构建组件时创建它们。

本节仅用于解释我将如何设置这个项目,您可以选择许多其他方式来组织您的项目,我鼓励您选择最适合您和您的团队的方式。

此时我将提交一条消息rfc: create directory structure

添加故事书

如果您还不熟悉的话,我们可以使用的其中一个出色的现代工具就是Storybook

Storybook 为我们提供了一个环境来展示和测试我们在使用它们的应用程序之外构建的 React 组件。它是连接开发人员和设计师的绝佳工具,并且能够在隔离的环境中验证我们开发的组件的外观和功能是否符合设计要求,而无需应用程序其余部分的开销。

请注意,Storybook 是一个可视化测试工具,我们稍后将实现其他工具用于功能单元测试和端到端测试。

学习如何使用 Storybook 的最佳方式是安装它并尝试它!



npx sb init --builder webpack5


Enter fullscreen mode Exit fullscreen mode

我们将使用 webpack5 版本来保持与 webpack 的最新版本同步(我不确定为什么它仍然不是默认版本。也许当您使用本教程时它就会成为默认版本)。

Storybook 安装时会自动检测项目的诸多信息,例如它是否是一个 React 应用,以及你正在使用的其他工具。它应该会自行处理所有这些配置。

如果出现关于 eslintPlugin 的提示,请选择“是”。不过,我们将手动配置它,所以如果您收到提示“未自动配置”的消息,也不用担心。

打开.eslintrc.json并更新如下内容:

.eslintrc.json



{
  "extends": [
    "plugin:storybook/recommended", // New
    "next",
    "next/core-web-vitals",
    "eslint:recommended"
  ],
  "globals": {
    "React": "readonly"
  },
  // New
  "overrides": [
    {
      "files": ["*.stories.@(ts|tsx|js|jsx|mjs|cjs)"],
      "rules": {
        // example of overriding a rule
        "storybook/hierarchy-separator": "error"
      }
    }
  ],
  "rules": {
    "no-unused-vars": [1, { "args": "after-used", "argsIgnorePattern": "^_" }]
  }
}


Enter fullscreen mode Exit fullscreen mode

我添加了// New标记以标记 Storybook 特有的两个新部分和行。

您会注意到 Storybook 还将目录添加到了/stories您的项目根目录中,其中包含许多示例。如果您是 Storybook 新手,我强烈建议您浏览它们并将它们留在那里,直到您能够在没有模板的情况下轻松地创建自己的示例。

在运行之前,我们需要确保使用的是 webpack5。将以下内容添加到package.json文件中:

package.json



{
  ...
  "resolutions": {
    "webpack": "^5"
  }
}


Enter fullscreen mode Exit fullscreen mode

然后运行



yarn install


Enter fullscreen mode Exit fullscreen mode

确保 webpack5 已安装。

接下来我们必须更新.storybook/main.js文件:

storybook/main.js



module.exports = {
  stories: ['../**/*.stories.mdx', '../**/*.stories.@(js|jsx|ts|tsx)'],
  /** Expose public folder to storybook as static */
  staticDirs: ['../public'],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
  ],
  framework: '@storybook/react',
  core: {
    builder: '@storybook/builder-webpack5',
  },
};


Enter fullscreen mode Exit fullscreen mode

在这里,我们改变了故事文件的模式,以便它可以拾取.stories我们的组件(或其他)目录内的任何文件。

我们还将 Next.js 的“公共”文件夹公开为静态目录,以便我们可以在 Storybook 中测试图像、媒体等内容。

最后,在运行 Storybook 本身之前,让我们在 中添加一些有用的值storybook/preview.js。这是我们可以控制故事渲染方式的默认值的文件。

storybook/preview.js



import '../styles/globals.css';
import * as NextImage from 'next/image';

const BREAKPOINTS_INT = {
  xs: 375,
  sm: 600,
  md: 900,
  lg: 1200,
  xl: 1536,
};

const customViewports = Object.fromEntries(
  Object.entries(BREAKPOINTS_INT).map(([key, val], idx) => {
    console.log(val);
    return [
      key,
      {
        name: key,
        styles: {
          width: `${val}px`,
          height: `${(idx + 5) * 10}vh`,
        },
      },
    ];
  })
);

// Allow Storybook to handle Next's <Image> component
const OriginalNextImage = NextImage.default;

Object.defineProperty(NextImage, 'default', {
  configurable: true,
  value: (props) => <OriginalNextImage {...props} unoptimized />,
});

export const parameters = {
  actions: { argTypesRegex: '^on[A-Z].*' },
  controls: {
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  },
  viewport: { viewports: customViewports },
};


Enter fullscreen mode Exit fullscreen mode

上面有一些个人偏好设置,但您可以根据需要进行配置。请务必设置默认断点,以匹配您在应用中需要注意的内容。我们还添加了一个处理程序,以便 Storybook 能够处理 Next 的<Image>组件而不会崩溃。

现在我们准备测试它。运行:



yarn storybook


Enter fullscreen mode Exit fullscreen mode

如果一切顺利,您将在控制台中看到如下消息:

故事书已开始

您可以通过http://localhost:6006访问它

故事书主页

如果您以前从未使用过它,我鼓励您尝试一下并熟悉这些示例。

在此阶段,我将提交一条带有消息的提交build: implement storybook

创建组件模板

现在是时候将我们所做的所有配置汇总在一起,并研究如何使用我们为自己设定的标准来创建和实现我们的第一个组件。

我们只创建一张简单的卡片。创建以下目录结构:

/components/templates/base

在该目录中,我们将创建BaseTemplate.tsx。它将遵循与指向它的目录匹配的标准文件名模式。例如,这允许我们在cards目录中放置其他类型的卡片,例如PhotoCardTextCard等等。

BaseTemplate.tsx



export interface IBaseTemplate {}

const BaseTemplate: React.FC<IBaseTemplate> = () => {
  return <div>Hello world!</div>;
};

export default BaseTemplate;


Enter fullscreen mode Exit fullscreen mode

我们的每一个组件都将遵循这个精确的结构。即使它不使用 props,它仍然会为组件导出一个空的 props 接口。这样做的原因是,它允许我们在多个组件和文件中复制这个精确的结构,并使用相同的预期模式交换组件/导入,只需查找/替换组件的名称即可。

当您开始处理故事和模拟道具等时,您很快就会发现为所有组件文件维护一致的命名方案和界面是多么方便和强大。

这又回到了我们之前提到的“一致性就是一切”的观点。

接下来,我将创建一个样式模块文件,放在组件旁边。默认情况下,Next.js 会提供一个/styles目录,我个人并不使用它,但如果您希望将所有样式放在同一个地方,这是一个不错的选择。我只是更喜欢将它们与组件放在一起。

BaseTemplate.module.css



.component {
}


Enter fullscreen mode Exit fullscreen mode

作为组件顶层样式的标准空模板。您可以BaseTemplate按如下方式更新:

BaseTemplate.tsx



import styles from './BaseTemplate.module.css';

export interface IBaseTemplate {}

const BaseTemplate: React.FC<IBaseTemplate> = () => {
  return <div className={styles.container}>Hello world!</div>;
};

export default BaseTemplate;


Enter fullscreen mode Exit fullscreen mode

现在我们有一个干净的样式模板。

让我们在模板中添加一个示例道具,以便我们可以处理将用于组件道具的标准:

BaseTemplate.tsx



import styles from './BaseTemplate.module.css';

export interface IBaseTemplate {
  sampleTextProp: string;
}

const BaseTemplate: React.FC<IBaseTemplate> = ({ sampleTextProp }) => {
  return <div className={styles.container}>{sampleTextProp}</div>;
};

export default BaseTemplate;


Enter fullscreen mode Exit fullscreen mode

对于我们创建的每个组件,我们都希望有一种非常快速简便的方法,可以在不同的环境中进行测试(例如 Storybook,也包括应用程序,甚至单元测试)。能够快速访问数据来渲染组件将会非常方便。

让我们创建一个文件来存储该组件的一些模拟数据以供测试使用:

BaseTemplate.mocks.ts



import { IBaseTemplate } from './BaseTemplate';

const base: IBaseTemplate = {
  sampleTextProp: 'Hello world!',
};

export const mockBaseTemplateProps = {
  base,
};


Enter fullscreen mode Exit fullscreen mode

这种结构可能看起来有点复杂,但我们很快就会看到它的好处。我特意使用了一致的命名模式,因此这个模板很容易复制粘贴到你创建的每个新组件中。

现在让我们为这个组件创建一个故事:

BaseTemplate.stories.tsx



import { ComponentStory, ComponentMeta } from '@storybook/react';
import BaseTemplate, { IBaseTemplate } from './BaseTemplate';
import { mockBaseTemplateProps } from './BaseTemplate.mocks';

export default {
  title: 'templates/BaseTemplate',
  component: BaseTemplate,
  // More on argTypes: https://storybook.js.org/docs/react/api/argtypes
  argTypes: {},
} as ComponentMeta<typeof BaseTemplate>;

// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
const Template: ComponentStory<typeof BaseTemplate> = (args) => (
  <BaseTemplate {...args} />
);

export const Base = Template.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args

Base.args = {
  ...mockBaseTemplateProps.base,
} as IBaseTemplate;


Enter fullscreen mode Exit fullscreen mode

我不会详细介绍stories文件的每个不同部分所包含的内容,因为最好的资源是官方 Storybook 文档。

这里的目标是创建一个一致的、易于复制/粘贴的组件构建和测试模式。

让我们尝试一下。运行:



yarn storybook


Enter fullscreen mode Exit fullscreen mode

如果一切顺利,您将看到精美的基础组件(如果没有,我鼓励您重新访问上一节并检查是否错过了任何配置)。

故事书基础模板

现在我们开始创建更多文件,最好养成yarn lint在提交之前运行一下的习惯,以确保一切正常,随时可用。我将使用 message 进行提交build: create BaseTemplate component

使用组件模板

既然我们有了模板,让我们来看看使用它来创建真实组件的过程。

创建components/cards目录。然后将整个base目录从templates复制到cards并重命名cat。我们将创建一个CatCard。将每个文件重命名以匹配。完成后应如下所示:

组件目录结构

现在,您可以ctrl + shift + F在 VS Code 中按下 (或 Mac 等效键)来执行完整的项目搜索和替换。仅包含components/cards/cat,然后执行替换CatCardBaseTemplate它应该如下所示:

VS Code 查找替换

现在您可以开始工作了,您已经拥有一个干净的预生成模板,其中包含卡片的故事和模拟数据。非常方便!让我们让它看起来像一张真正的卡片:

(需要说明的是,这张漂亮的卡片不是我制作的,它是基于才华横溢的 Lyon Etyo在此处创建的示例)

CatCard.tsx



import styles from './CatCard.module.css';
import Image from 'next/image';

export interface ICatCard {
  tag: string;
  title: string;
  body: string;
  author: string;
  time: string;
}

const CatCard: React.FC<ICatCard> = ({ tag, title, body, author, time }) => {
  return (
    <div className={styles.container}>
      <div className={styles.card}>
        <div className={styles.card__header}>
          <Image
            src="/time-cat.jpg"
            alt="card__image"
            className={styles.card__image}
            width="600"
            height="400"
          />
        </div>
        <div className={styles.card__body}>
          <span className={`${styles.tag} ${styles['tag-blue']}`}>{tag}</span>
          <h4>{title}</h4>
          <p>{body}</p>
        </div>
        <div className={styles.card__footer}>
          <div className={styles.user}>
            <Image
              src="https://i.pravatar.cc/40?img=3"
              alt="user__image"
              className={styles.user__image}
              width="40"
              height="40"
            />
            <div className={styles.user__info}>
              <h5>{author}</h5>
              <small>{time}</small>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};

export default CatCard;


Enter fullscreen mode Exit fullscreen mode

设置样式:

CatCard.module.css



@import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@300..700&display=swap');

.container {
  margin: 1rem;
}

.container * {
  box-sizing: border-box;
  padding: 0;
  margin: 0;
}

.card__image {
  max-width: 100%;
  display: block;
  object-fit: cover;
}

.card {
  font-family: 'Quicksand', sans-serif;
  display: flex;
  flex-direction: column;
  width: clamp(20rem, calc(20rem + 2vw), 22rem);
  overflow: hidden;
  box-shadow: 0 0.1rem 1rem rgba(0, 0, 0, 0.1);
  border-radius: 1em;
  background: #ece9e6;
  background: linear-gradient(to right, #ffffff, #ece9e6);
}

.card__body {
  padding: 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.tag {
  align-self: flex-start;
  padding: 0.25em 0.75em;
  border-radius: 1em;
  font-size: 0.75rem;
}

.tag-blue {
  background: #56ccf2;
  background: linear-gradient(to bottom, #2f80ed, #56ccf2);
  color: #fafafa;
}

.card__body h4 {
  font-size: 1.5rem;
  text-transform: capitalize;
}

.card__footer {
  display: flex;
  padding: 1rem;
  margin-top: auto;
}

.user {
  display: flex;
  gap: 0.5rem;
}

.user__image {
  border-radius: 50%;
}

.user__info > small {
  color: #666;
}


Enter fullscreen mode Exit fullscreen mode

并设置模拟数据:

CatCard.mocks.ts



import { ICatCard } from './CatCard';

const base: ICatCard = {
  tag: 'Felines',
  title: `What's new in Cats`,
  body: 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Sequi perferendis molestiae non nemo doloribus. Doloremque, nihil! At ea atque quidem!',
  author: 'Alex',
  time: '2h ago',
};

export const mockCatCardProps = {
  base,
};


Enter fullscreen mode Exit fullscreen mode

(/time-cat.jpg)请注意,这使用了项目公共目录中的一张猫的图片。您可以在项目仓库中找到它。

我们唯一需要更新的CatCard.stories是将故事标题从templates/CatCard改为cards/CatCard

我们确实需要更新我们的,next.config.js因为我们使用的域名尚未明确声明为允许的(用于头像)。只需将您的配置文件更新为如下所示:

next.config.js



/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  images: {
    domains: ['i.pravatar.cc'],
  },
};

module.exports = nextConfig;


Enter fullscreen mode Exit fullscreen mode

或者,您可以将头像图像放在您自己的公共目录中,但为了学习使用外部域的过程,我们将保留此设置。

现在来看看《故事书》,如果你幸运的话,你会看到:

故事书猫卡

然后,您可以轻松地将此组件拖放到实际应用程序中的任何位置。mock在测试期间短期使用这些 props,并在准备就绪后将其替换为真正的 props!

pages/index.tsx



import type { NextPage } from 'next';
import Head from 'next/head';
import Image from 'next/image';
import CatCard from '../components/cards/cat/CatCard';
import { mockCatCardProps } from '../components/cards/cat/CatCard.mocks';
import styles from '../styles/Home.module.css';

const Home: NextPage = () => {
  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>
          Welcome to <a href="https://nextjs.org">Next.js!</a>
        </h1>

        <div className={styles.grid}>
          <CatCard {...mockCatCardProps.base} />
          <CatCard {...mockCatCardProps.base} />
          <CatCard {...mockCatCardProps.base} />
          <CatCard {...mockCatCardProps.base} />
        </div>
      </main>

      <footer className={styles.footer}>
        <a
          href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
          target="_blank"
          rel="noopener noreferrer"
        >
          Powered by{' '}
          <span className={styles.logo}>
            <Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
          </span>
        </a>
      </footer>
    </div>
  );
};

export default Home;


Enter fullscreen mode Exit fullscreen mode

让我们来看看最终的杰作:



yarn dev


Enter fullscreen mode Exit fullscreen mode

最终杰作

添加自定义文档

虽然目前还不需要,但您可能希望对<head>应用程序的内容进行更细粒度的控制。_document.tsx在您的目录中创建一个自定义文件pages可以实现这一点。现在就创建该文件。

pages/_document.tsx



import Document, { Head, Html, Main, NextScript } from 'next/document';

class MyDocument extends Document {
  render() {
    return (
      <Html>
        <Head>
          <link rel="preconnect" href="https://fonts.googleapis.com" />
          <link rel="preconnect" href="https://fonts.gstatic.com" />
          <link
            href="https://fonts.googleapis.com/css2?family=Quicksand:wght@300..700&display=swap"
            rel="stylesheet"
          />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;


Enter fullscreen mode Exit fullscreen mode

请注意,我已@import从中删除了 URL 字体components/cards/cat/CatCard.module.css,并将 Google 字体放置在头部以便预加载。

<head>现在,您可以在此文件中完成元素中需要执行或自定义的任何其他操作。

请注意,这<Head>与从 导入的版本不同next/head。它们可以协同工作,而这个版本仅用于加载您希望在每个页面上加载的数据。

有关如何使用自定义的更多信息,_document请参阅文档

添加布局

布局是 Next.js 中的一个重要概念。它帮助你管理页面之间的状态。在本节中,我们将使用官方示例中提供的基本模板,并对其进行一些自定义以适应我们的项目。

创建一个名为 的新目录layoutscomponents我们将复制该templates/case目录两次。一次复制到primary,另一次复制到sidebar。完成后应如下所示:

布局文件夹

BaseTemplate在每个文件内部执行区分大小写的查找/替换,并分别替换为PrimaryLayoutSidebarLayout

如果您在此步骤中遇到任何困难,请随意从 repo 中获取结构

这些布局模板的结构全部归功于_来自 Vercel 的 leerobJJ Kasper_

PrimaryLayout.tsx将和的内容更新为PrimaryLayout.module.css

components/layouts/primary/PrimaryLayout.tsx



import Head from 'next/head';
import styles from './PrimaryLayout.module.css';

export interface IPrimaryLayout {}

const PrimaryLayout: React.FC<IPrimaryLayout> = ({ children }) => {
  return (
    <>
      <Head>
        <title>Primary Layout Example</title>
      </Head>
      <main className={styles.main}>{children}</main>
    </>
  );
};

export default PrimaryLayout;


Enter fullscreen mode Exit fullscreen mode

components/layouts/primary/PrimaryLayout.module.css



.main {
  display: flex;
  height: calc(100vh - 64px);
  background-color: white;
}

.main > section {
  padding: 32px;
}


Enter fullscreen mode Exit fullscreen mode

然后是侧边栏:

components/layouts/sidebar/SidebarLayout.tsx



import Link from 'next/link';
import styles from './SidebarLayout.module.css';

export interface ISidebarLayout {}

const SidebarLayout: React.FC<ISidebarLayout> = () => {
  return (
    <nav className={styles.nav}>
      <input className={styles.input} placeholder="Search..." />
      <Link href="/">
        <a>Home</a>
      </Link>
      <Link href="/about">
        <a>About</a>
      </Link>
      <Link href="/contact">
        <a>Contact</a>
      </Link>
    </nav>
  );
};

export default SidebarLayout;


Enter fullscreen mode Exit fullscreen mode

components/layouts/sidebar/SidebarLayout.module.css



.nav {
  height: 100%;
  display: flex;
  flex-direction: column;
  width: 250px;
  background-color: #fafafa;
  padding: 32px;
  border-right: 1px solid #eaeaea;
}

.nav > a {
  margin: 8px 0;
  text-decoration: none;
  background: white;
  border-radius: 4px;
  font-size: 14px;
  padding: 12px 16px;
  text-transform: uppercase;
  font-weight: 600;
  letter-spacing: 0.025em;
  color: #333;
  border: 1px solid #eaeaea;
  transition: all 0.125s ease;
}

.nav > a:hover {
  background-color: #eaeaea;
}

.input {
  margin: 32px 0;
  text-decoration: none;
  background: white;
  border-radius: 4px;
  border: 1px solid #eaeaea;
  font-size: 14px;
  padding: 8px 16px;
  height: 28px;
}


Enter fullscreen mode Exit fullscreen mode

现在这些模板已经创建完毕,我们需要使用它们。我们将更新主页,并创建另一个名为 的页面,about.tsx以展示如何使用共享布局并在页面之间持久化组件状态。

首先,我们需要添加一个扩展默认接口的类型,NextPage因为出于某种原因,它不包含getLayout开箱即用的功能。受此解决方案启发,创建一个自定义类型文件来处理这个问题。

pages/page.d.ts



import { NextPage } from 'next';
import { ComponentType, ReactElement, ReactNode } from 'react';

export type NextPageWithLayout<P = {}> = NextPage<P> & {
  getLayout?: (_page: ReactElement) => ReactNode;
  layout?: ComponentType;
};


Enter fullscreen mode Exit fullscreen mode

现在,您可以在需要创建具有自定义布局的页面时使用该NextPageWithLayout界面。NextPage

现在让我们更新我们的主页:

pages/index.tsx



import CatCard from '../components/cards/cat/CatCard';
import { mockCatCardProps } from '../components/cards/cat/CatCard.mocks';
import PrimaryLayout from '../components/layouts/primary/PrimaryLayout';
import SidebarLayout from '../components/layouts/sidebar/SidebarLayout';
import styles from '../styles/Home.module.css';
import { NextPageWithLayout } from './page';

const Home: NextPageWithLayout = () => {
  return (
    <section className={styles.main}>
      <h1 className={styles.title}>
        Welcome to <a href="https://nextjs.org">Next.js!</a>
      </h1>
      <CatCard {...mockCatCardProps.base} />
    </section>
  );
};

export default Home;

Home.getLayout = (page) => {
  return (
    <PrimaryLayout>
      <SidebarLayout />
      {page}
    </PrimaryLayout>
  );
};


Enter fullscreen mode Exit fullscreen mode

about在目录中创建一个新页面pages

pages/about.tsx



import PrimaryLayout from '../components/layouts/primary/PrimaryLayout';
import SidebarLayout from '../components/layouts/sidebar/SidebarLayout';
import { NextPageWithLayout } from './page';

const About: NextPageWithLayout = () => {
  return (
    <section>
      <h2>Layout Example (About)</h2>
      <p>
        This example adds a property <code>getLayout</code> to your page,
        allowing you to return a React component for the layout. This allows you
        to define the layout on a per-page basis. Since we&apos;re returning a
        function, we can have complex nested layouts if desired.
      </p>
      <p>
        When navigating between pages, we want to persist page state (input
        values, scroll position, etc.) for a Single-Page Application (SPA)
        experience.
      </p>
      <p>
        This layout pattern will allow for state persistence because the React
        component tree is persisted between page transitions. To preserve state,
        we need to prevent the React component tree from being discarded between
        page transitions.
      </p>
      <h3>Try It Out</h3>
      <p>
        To visualize this, try tying in the search input in the{' '}
        <code>Sidebar</code> and then changing routes. You&apos;ll notice the
        input state is persisted.
      </p>
    </section>
  );
};

export default About;

About.getLayout = (page) => {
  return (
    <PrimaryLayout>
      <SidebarLayout />
      {page}
    </PrimaryLayout>
  );
};


Enter fullscreen mode Exit fullscreen mode

然后更新_app.tsx如下:

pages/_app.tsx



import type { AppProps } from 'next/app';
import './globals.css';
import { NextPageWithLayout } from './page';

interface AppPropsWithLayout extends AppProps {
  Component: NextPageWithLayout;
}

function MyApp({ Component, pageProps }: AppPropsWithLayout) {
  // Use the layout defined at the page level, if available
  const getLayout = Component.getLayout || ((page) => page);

  return getLayout(<Component {...pageProps} />);
}

export default MyApp;


Enter fullscreen mode Exit fullscreen mode

最后,在mocks文件中,我已更新PrimaryLayout.mocks.ts为用作children: '{{component}}'占位符值,以在 Storybook 中显示组件的位置,并且我已删除了模拟道具SidebarLayout.mocks.ts(尽管我没有删除文件,因此我已准备好界面以防我需要添加道具)。

我还将故事标题从 更改templates/...layouts/...

最后我们可以测试一下。保存并运行



yarn dev


Enter fullscreen mode Exit fullscreen mode

点击侧边栏上的两个页面(主页和关于)即可切换。请注意,所使用的布局将保持不变,无需重新加载(这正是我们的初衷),让您享受超快速、流畅的体验。

下一个布局 01

下一个布局 02

在 Storybook 端,我们甚至可以独立于应用程序查看和测试布局组件。PrimaryLayout如果没有内容,它就没什么用,不过侧边栏还是很不错的。



yarn storybook


Enter fullscreen mode Exit fullscreen mode

故事书侧边栏

部署

我们的最后一步是展示 Next.js 应用程序的部署过程。

我们将使用 Vercel,因为它是 Next.js 应用程序最简单、最直接的部署解决方案(主要是因为 Vercel 拥有 Next,因此人们总是可以假设他们将提供一流的支持)。

请注意,Vercel 绝对不是唯一的选择,如果您选择这条路线,其他主要服务(如AWSNetlify等)也可以正常工作。

最终,您只需要一个服务,您可以在其中运行next start命令,假设您没有使用完全静态生成的站点(在这种情况下,任何静态托管工具都可以工作,并且不需要自定义的 Next 服务器)。

作为业余爱好者,在 Vercel 上部署是完全免费的。首先,我们将在 Vercel 上创建一个帐户

登录后,点击+ New Project并授予 Vercel 访问您的 Github 仓库的权限。您可以授予全局访问权限,也可以仅选择要部署的仓库。我将选择名为 的仓库nextjs-fullstack-app-template

选择后,您需要进行配置。在本Build and Output Settings节中,请确保将默认的 NPM 命令替换为 yarn 命令(除非您使用 NPM)。

下一步配置

我们尚未使用任何环境变量,因此无需添加任何环境变量。

一旦完成,只需单击即可Deploy完成!就这么简单。

部署成功

(上面的截图有点过时了,我最初是在布局部分之前写的部署部分,但你明白我的意思)

您的站点不仅现在已部署,而且每次提交到主分支时,它都会自动重新部署。如果您不想这样做,可以在 Vercel 仪表板中轻松配置。

好消息是,yarn build在推送代码之前,您已经配置了命令以确保生产构建能够正常运行,因此您可以放心地推送,并假设部署将会成功。

您唯一需要注意的是两个环境之间的差异。如果您的脚本不同(使用 NPM 而不是 yarn 或反之亦然),或者更常见的情况是缺少环境变量,则您的构建仍然有可能在本地成功,但在 Vercel 上失败。

我们将env在未来的教程中添加值,因此您需要确保这些值在本地和生产环境中都进行了配置,因为它们是秘密,永远不应提交到公共(如果可以避免,甚至可以是私有)存储库。

后续步骤

我希望您找到本教程并学到一些有关为您和您的团队设置可靠且可扩展的 Next.js 项目的知识。

这是关于创建生产质量 Next.js 应用程序的系列文章的第一部分。

以下是我对未来分期付款的一些想法,我鼓励您留下一些反馈,告诉您哪些想法最有用(或者如果您在下面没有看到其他想法)。

  • 如何使用 API 路由和 Tailwind CSS 构建全栈 Next.js 应用
  • 如何使用 Recoil 向 Next.js 应用添加全局状态管理器
  • 如何使用 Jest 和 Playwright 在 Next.s 应用中实现单元测试和端到端测试
  • 如何使用 Github Actions 和 Vercel 创建 CI/CD 管道
  • 如何使用 NextAuth 和 i18next 在 Next.js 应用中实现 SSO 身份验证和国际化
  • 如何使用 Prisma 和 Supabase 将数据库连接到 Next.js 应用程序
  • 如何使用 Next.js 和 Nx 在 monorepo 中管理多个应用程序

请继续关注,如有任何问题请随时提出,如果可以我很乐意回答!

总结

请记住,本教程中的所有代码作为完整包都可以在此存储库中找到。

请查看我的其他学习教程。如果您觉得其中有任何内容有用,请随时发表评论或提出问题并与他人分享:

文章来源:https://dev.to/alexeagleson/how-to-build-scalable-architecture-for-your-nextjs-project-2pb7
PREV
如何将 React 应用连接到 Notion 数据库
NEXT
我如何在全职软件开发的同时,创办了一家五位数的创业公司。你也可以这样做