在开发环境中如何设置Next. js 15

361 阅读14分钟

本文翻译自:medium.com/@jan.hester…

我曾经开发过Next. js应用程序,这些应用程序每月活跃用户超过100k,每月访问者超过数百万。在本文中,我将分享我通过无数次迭代学到的所有经验教训。

无论你是一个由一个、两个或三个开发人员组成的小团队的一员,还是你在同一个代码库上与多个团队一起从事一个大型Next. js项目,从一开始就打好应用程序的基础至关重要。即使你正在从事一个现有的项目,你也会发现一些来之不易的见解,这些见解可以应用到你的应用程序中。

让我牵着你的手,向你展示你想要设置的所有步骤和包,并向你解释它们为什么有用,这样你的应用程序就可以顺利扩展。

初始化您的项目

首先创建一个新的Next. js项目。

npx create-next-app@latest

它可能会问你是否可以安装最新create-next-app版本,只需点击yes。

Need to install the following packages:
create-next-app@15.0.0
Ok to proceed? (y)

然后通过在所有内容(TypeScript、TailWind、应用程序路由器)上点击yes来配置您的项目。

✔ **What is your project named?** … reactsquad-production
✔ **Would you like to use** **TypeScript****?** … No / Yes
Yes
✔ **Would you like to use** **ESLint****?** … No / Yes
Yes
✔ **Would you like to use** **Tailwind CSS****?** … No / Yes
Yes
✔ **Would you like to use** **`src/` directory****?** … No / Yes
Yes
✔ **Would you like to use** **App Router****? (recommended)** … No / Yes
Yes
✔ **Would you like to customize the default** **import alias** **(@/*)?** … No / No

然后切换到您的项目目录并在您喜欢的编辑器中打开它。

$ cd nextjs-for-production
~/dev/nextjs-for-production (main) 🤯
$ cursor .

运行开发服务器

您想验证您的设置是否有效。

  1. 运行npm run dev来启动开发服务器。
  2. 访问http://localhost:3000查看您的申请。

使用TypeScript进行类型检查

您的项目已经配置了TypeScript,但您还想在package.json中添加一个显式命令来检查所有文件的类型错误。

"type-check": "tsc -b"

稍后您将使用此命令以及其他自动静态分析检查。

代码格式化

当您在项目上与大型团队合作时,统一每个人编写代码的方式非常重要。与您的团队讨论使用分号、引号样式和制表符与空格等选择。

然后使用工具自动执行您的样式指南和格式化代码。

有两个工具:prettierESLint

Prettier

Pretier是一个固执己见的代码格式化程序,它消除了代码审查期间的样式讨论。

与tailwindcss插件一起安装。

npm install --save-dev prettier prettier-plugin-tailwindcss

使用首选规则创建一个prettier.config.js文件。

module.exports = {
  arrowParens: 'avoid',
  bracketSameLine: false,
  bracketSpacing: true,
  htmlWhitespaceSensitivity: 'css',
  insertPragma: false,
  jsxSingleQuote: false,
  plugins: ['prettier-plugin-tailwindcss'],
  printWidth: 80,
  proseWrap: 'always',
  quoteProps: 'as-needed',
  requirePragma: false,
  semi: true,
  singleQuote: true,
  tabWidth: 2,
  trailingComma: 'all',
  useTabs: false,
};

将格式化脚本添加到package.json

"format": "prettier --write .",

运行格式化程序以应用您的样式。

$ npm run format

> nextjs-for-production@0.1.0 format
> prettier --write .

next.config.mjs 4ms (unchanged)
package-lock.json 54ms (unchanged)
package.json 1ms (unchanged)
postcss.config.mjs 2ms (unchanged)
README.md 20ms (unchanged)
src/app/globals.css 17ms (unchanged)
src/app/layout.tsx 30ms (unchanged)
src/app/page.tsx 11ms (unchanged)
tailwind.config.ts 2ms (unchanged)
tsconfig.json 2ms (unchanged)

您的文件现在“更漂亮”,但您也想使用ESLint。

ESLint

ESLint可以扫描您的代码以查找样式和逻辑问题。安装ESLint及其插件,如unicorn、playwright和import排序。

npm install --save-dev @typescript-eslint/parser eslint-plugin-unicorn eslint-plugin-import eslint-plugin-playwright eslint-config-prettier eslint-plugin-prettier eslint-plugin-simple-import-sort

更新您的.eslintrc.json

{
  "extends": [
    "next/core-web-vitals",
    "plugin:unicorn/recommended",
    "plugin:import/recommended",
    "plugin:playwright/recommended",
    "plugin:prettier/recommended"
  ],
  "plugins": ["simple-import-sort"],
  "rules": {
    "simple-import-sort/exports": "error",
    "simple-import-sort/imports": "error",
    "unicorn/no-array-callback-reference": "off",
    "unicorn/no-array-for-each": "off",
    "unicorn/no-array-reduce": "off",
    "unicorn/prevent-abbreviations": [
      "error",
      {
        "allowList": {
          "e2e": true
        },
        "replacements": {
          "props": false,
          "ref": false,
          "params": false
        }
      }
    ]
  },
  "overrides": [
    {
      "files": ["*.js"],
      "rules": {
        "unicorn/prefer-module": "off"
      }
    }
  ]
}

本教程的插件提供不同的功能。有关详细说明,请访问各自的GitHub页面。

但简而言之,它们强制执行编码标准,组织导入,并确保正确使用现代JavaScript功能。由于ESLint和Pretier可能会发生冲突,这种设置使它们能够顺利地协同工作。插件还有助于防止错误并保持风格一致,尤其是在Vitest和Playwright等工具上。

package.json中添加lting脚本。

"lint:fix": "next lint --fix",

运行它以根据您的新规则格式化所有文件。

$ npm run lint:fix

> nextjs-for-production@0.1.0 lint:fix
> next lint --fix

✔ No ESLint warnings or errors

如果您收到TypeScript版本警告,您可以忽略它。

注意:在撰写本文时,ESLint 9可用,但本教程使用ESLint 8,因为许多插件还不支持最新版本。

Commitlint

当与大型团队协作时,强制执行一致的提交消息以保持项目历史清晰也很有帮助。通过选择正确的标准,您可以通过正确的语义版本控制自动更改日志和发布生成。

安装Commitlint及其必要的配置。这包括帮助管理Git钩子的Husky

npm install --save-dev @commitlint/cli@latest @commitlint/config-conventional@latest husky@latest

在您的项目中初始化Husky以设置基本配置。

npx husky-init && npm install

添加挂钩以在每次提交之前自动进行lting和类型检查,并自定义您的提交消息工作流程。

npx husky add .husky/pre-commit 'npm run lint && npm run type-check'
npx husky add .husky/prepare-commit-msg 'exec < /dev/tty && npx cz --hook || true'

这个pre-commit钩子运行在git commit之后,但是在提交消息完成之前,并且运行代码的lting和类型检查。

prepare-commit-msg启动后,提交消息编辑器打开之前,git commit钩子运行。它运行commitizenCLI,让您制作常规的提交消息。稍后您将学习如何使用这个钩子。

删除npm test.husky/_/pre-commit

确保这些脚本是可执行的。

chmod a+x .husky/pre-commit
chmod a+x .husky/prepare-commit-msg

这里,chmod代表“更改模式”。此命令允许您更改Unix和类Unix操作系统中文件或目录的访问权限或模式。参数a+x为所有用户添加执行权限。

安装commitizen,它提供了一个用于制作常规提交消息的CLI。

npm install --save-dev commitizen cz-conventional-changelog

在你的package.json中配置通信器以使用传统的更改日志标准。

"config": {
  "commitizen": {
    "path": "cz-conventional-changelog"
  }
},

传统的变更日志标准是为提交消息添加人类和机器可读含义的规范。它旨在根据项目的Git历史记录自动生成变更日志和版本。

以后的一篇文章将详细解释这个标准。它包含在本教程中,因为从一开始就正确使用它很重要。您可以使用它并从中受益,而无需深入了解。

使用适合您团队需求的规则创建您的commitlint.config.cjs文件。此设置可确保您的提交消息与所做的更改一致且相关。

const config = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'references-empty': [1, 'never'],
    'footer-max-line-length': [0, 'always'],
    'body-max-line-length': [0, 'always'],
  },
};

module.exports = config;

运行以下命令以开始使用引导CLI制作提交消息。

$ git add --all
$ npx cz
cz-cli@4.3.0, cz-conventional-changelog@3.3.0

? Select the type of change that you're
committing: (Use arrow keys)
❯ feat:     A new feature
  fix:      A bug fix
  docs:     Documentation only changes
  style:    Changes that do not affect the
meaning of the code (white-space, formatting,
missing semi-colons, etc)

cz命令会询问一系列问题,然后为您编写提交消息。

? Select the type of change that you\\'re committing: feat:     A new feature
? What is the scope of this change (e.g. component or file name): (press enter to skip)
? Write a short, imperative tense description of the change (max 63 chars):
 (61) set up TS type checks, ESLint, Prettier, Commitlint and Husky
? Provide a longer description of the change: (press enter to skip)
 Sets up package.json script to do TS type checks. Sets up Prettier and ESLint with good rules. Configures Husky and sets up Commitizen using the conventional commit standard.
? Are there any breaking changes? No
? Does this change affect any open issues? No
[main eb69ccd] feat(set up static analysis checks): set up TS type checks, ESLint, Prettier, Commitlint and Husky
 7 files changed, 3108 insertions(+), 159 deletions(-)
 create mode 100755 .husky/pre-commit
 create mode 100755 .husky/prepare-commit-msg
 create mode 100644 commitlint.config.cjs

当您回答所有问题时,Husky钩子将自动运行TypeScript类型检查和lting。

文件夹结构

通常有两种流行的方法来组织代码:按类型分组或按功能分组。

按类型分组如下所示:

.
├── components
│ ├── todos
│ └── user
├── reducers
│ ├── todos
│ └── user
└── tests
    ├── todos
    └── user

按功能分组如下所示:

.
├── todos
│ ├── component
│ ├── reducer
│ └── test
└── user
    ├── component
    ├── reducer
    └── test

在项目中按功能对文件进行分组可以将所有相关的组件、简化器和测试组织在一起,从而更轻松地管理和修改每个功能。它具有以下好处:

  • 可扩展性:大型应用程序更易于扩展和维护,因为每个功能都像一个迷你应用程序。您避免在文件列表中上下滚动以查找所需的所有文件。
  • 协作:开发人员可以专注于特定功能,而不会中断他人的工作。
  • 入职:新开发人员更快地了解项目结构,因为一个功能的所有文件都在一个地方。
  • 重构:更新功能是简化的,因为它的所有元素都组合在一起。
  • 模块化:功能可以更轻松地重用、共享或转换为独立包。
  • 微前端:如果您的应用程序增长到30名或更多工程师,那么很容易重构为微前端。

这是一个更具体的例子。

src/
├── app/
│   ├── ...
│   ├── (group)/
│   │   ├── about/
│   │   │   └── page.tsx
│   │   ├── settings/
│   │   │   └── page.tsx
│   │   └── layout.tsx
│   ├── dashboard/
│   │   ├── page.tsx
│   │   └── layout.tsx
│   ├── layout.tsx
│   └── page.tsx
├── components/
│   ├── ...
│   └── header/
│       ├── header-component.tsx
│       ├── header-component.test.ts
│       └── header.module.css
├── features/
│   ├── ...
│   ├── todos/
│   │   ├── ...
│   │   ├── todos-component.tsx
│   │   ├── todos-component.test.ts
│   │   ├── todos-container.ts
│   │   ├── todos-reducer.ts
│   │   ├── todos-reducer.test.ts
│   │   └── todos-styles.ts
│   └── user/
│       ├── ...
│       ├── user-reducer.ts
│       └── user-reducer.test.ts
├── hocs/
│   ├── ...
│   └── with-layout.tsx
├── hooks/
├── redux/
│   ├── root-reducer.ts
│   ├── root-saga.ts
│   └── store.ts
├── ...
└── middleware.ts

此示例向您展示了如何组织src/目录。通常,所有内容都按功能分组。测试文件位于各自的实现文件旁边。但是由多个功能共享的任何内容随后都会分组到通用文件夹中,例如components/HOCs/hooks/。状态管理的共享设置——在本例中为Redux——位于redux/文件夹中。

有些人还喜欢在app/文件夹中按功能分组。

src/
├── app/
│   ├── ...
│   ├── (group)/
│   ├── dashboard/
│   │   ├── components/
│   │   │   ├── dashboard-header.tsx
│   │   │   ├── dashboard-header.test.ts
│   │   │   ├── dashboard-widgets.tsx
│   │   │   └── dashboard-widgets.test.ts
│   │   ├── services/
│   │   │   ├── fetch-data.ts
│   │   │   ├── fetch-data.test.ts
│   │   │   ├── auth-service.ts
│   │   │   └── auth-service.test.ts
│   │   ├── page.tsx
│   │   └── layout.ts
│   ├── layout.tsx
│   └── page.tsx
├── ...
└── middleware.ts

在本教程中,您将以第一种方式按功能对文件进行分组。

Vitest

如果你想避免bug和防止回归,你需要编写测试。Vitest是一个很棒的选择,因为它与最流行的框架,即Jest具有相同的API,但它运行得更快。

安装Vitest。

npm install -D vitest

并在package.json中配置一个测试命令。

"test": "vitest --reporter=verbose",

然后创建一个example.test.ts文件并编写一个简短的测试来检查Vitest是否正常工作。

import { describe, expect, test } from 'vitest';

describe('example', () => {
  test('given a passing test: passes', () => {
    expect(1).toStrictEqual(1);
  });
});

它应该通过。

$ npm test

> nextjs-for-production@0.1.0 test
> vitest --reporter=verbose


 DEV  v2.0.5 /Users/jan/dev/nextjs-for-production

 ✓ src/example.test.ts (1)
   ✓ example (1)
     ✓ given a passing test: passes

 Test Files  1 passed (1)
      Tests  1 passed (1)
   Start at  14:06:24
   Duration  203ms (transform 20ms, setup 0ms, collect 17ms, tests 1ms, environment 0ms, prepare 56ms)


 PASS  Waiting for file changes...
       press h to show help, press q to quit

React测试库

您还想为您的React组件编写测试。为此,您可以使用React测试库,通常缩写为RTL。

npm install --save-dev @testing-library/react @testing-library/dom @testing-library/jest-dom @testing-library/user-event @types/react @types/react-dom happy-dom @vitejs/plugin-react vite-tsconfig-paths

然后,创建一个vitest.config.ts文件。

import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';

export default defineConfig({
  plugins: [react(), tsconfigPaths()],
  server: {
    port: 3000,
  },
  test: {
    environment: 'happy-dom',
    globals: true,
    setupFiles: ['./src/tests/setup-test-environment.ts'],
    include: ['./src/**/*.{spec,test}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
    watch: {
      ignored: [
        String.raw`.*\\/node_modules\\/.*`,
        String.raw`.*\\/build\\/.*`,
        String.raw`.*\\/postgres-data\\/.*`,
      ],
    },
    coverage: {
      reporter: ['text', 'json', 'html'],
    },
  },
});

它设置开发服务器,指定测试参数,如环境是happy-dom和文件路径,并定义覆盖率报告格式。

并创建一个名为src/tests/setup-test-environment.ts的文件来设置您的测试环境。

import '@testing-library/jest-dom/vitest';

// See <https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#configuring-your-testing-environment>.

// @ts-ignore
globalThis.IS_REACT_ACT_ENVIRONMENT = true;

第一行导入额外的断言,这些断言扩展了Vitest的内置断言,允许您更容易地测试DOM节点。例如,您可以检查一个元素是否可见、是否具有特定的文本内容或是否包含特定的属性。

全局环境标志确保涉及状态更新的测试按预期工作而不会出现时间问题。

接下来,要设置自定义渲染方法src/tests/react-test-utils.tsx

/* eslint-disable import/export */
import type { RenderOptions } from '@testing-library/react';
import { render } from '@testing-library/react';
import type { ReactElement } from 'react';

const customRender = (
  ui: ReactElement,
  options?: Omit<RenderOptions, 'queries'>,
) =>
  render(ui, {
    wrapper: ({ children }) => <>{children}</>,
    ...options,
  });

// re-export everything
export * from '@testing-library/react';

// override render method
export { customRender as render };
export { default as userEvent } from '@testing-library/user-event';

您可以使用它将代码包装在提供程序中,例如,在使用Redux或添加布局和样式时。

通过为React组件编写测试来确保您的配置正常工作。

import { describe, expect, test } from 'vitest';

import { render, screen } from '@/tests/react-test-utils';

function MyReactComponent() {
  return <div>My React Component</div>;
}

describe('MyReactComponent', () => {
  test('given no props: renders a text', () => {
    render(<MyReactComponent />);

    expect(screen.getByText('My React Component')).toBeInTheDocument();
  });
});

您可以从测试工具文件导入renderscreen,而不是直接从RTL导入。

toBeInTheDocument()是您之前在测试环境设置文件中配置的特殊断言之一。

它也应该过去。

✓ src/features/example.test.tsx (1)
   ✓ MyReactComponent (1)
     ✓ given no props: renders a text

 Test Files  1 passed (1)
      Tests  1 passed (1)
   Start at  09:33:32
   Duration  402ms (transform 24ms, setup 55ms, collect 87ms, tests 9ms, environment 80ms, prepare 45ms)


 PASS  Waiting for file changes...
       press h to show help, press q to quit

style

现在让我们谈谈样式。要做对的两个最重要的方面是可访问性可运维性Shadcn是最受欢迎的库之一。它使用TailWind来简化样式管理,使用Radix来增强可访问性。

在您的项目中初始化Shadcn。

$ npx shadcn@latest init

✔ **Which** **style** **would you like to use?** › New York
✔ **Which color would you like to use as** **base color****?** › Slate
✔ **Would you like to use** **CSS variables** **for colors?** … no / yes

✔ Writing components.json...
✔ Initializing project...
✔ Installing dependencies...

Success! Project initialization completed. You may now add components.

现在,如果您需要卡片或任何其他组件,您可以使用Shadcn命令行界面轻松地将其添加到您的项目中。

npx shadcn@latest add card

Internationalization

随着您的应用程序扩展,您希望将其翻译成多种语言,以便它可以接触到更多用户。

在应用程序的后续周期中添加internationalization或国际化可能会很痛苦,因为您必须找到所有硬编码字符串并用翻译函数调用替换它们。

安装协商器和语言环境匹配器。

npm install negotiator @formatjs/intl-localematcher

@formatjs/intl-localematcher包根据用户的语言偏好为您的应用内容选择最佳语言。negotiator包帮助您的应用根据浏览器发送的信息确定用户的浏览器可以最好地处理哪种类型的内容(如语言或格式)。

您还需要为协商器包安装TypeScript类型。

npm install --save-dev @types/negotiator

然后,在新文件中创建i18n配置src/features/internationalization/i18n-config.ts

export const i18n = {
  defaultLocale: 'en-US',
  locales: ['en-US'],
} as const;

export type Locale = (typeof i18n)['locales'][number];

使用 i18n配置,在您的 localization-middlewaresrc/features/internationalization/localization-middleware.ts.

import { match } from '@formatjs/intl-localematcher';
import Negotiator from 'negotiator';
import { type NextRequest, NextResponse } from 'next/server';

import { i18n } from './i18n-config';

function getLocale(request: NextRequest) {
  const headers = {
    'accept-language': request.headers.get('accept-language') ?? '',
  };
  const languages = new Negotiator({ headers }).languages();
  return match(languages, i18n.locales, i18n.defaultLocale);
}

export function localizationMiddleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const pathnameHasLocale = i18n.locales.some(
    locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`,
  );

  if (pathnameHasLocale) {
    return;
  }

  const locale = getLocale(request);
  request.nextUrl.pathname = `/${locale}${pathname}`;
  return NextResponse.redirect(request.nextUrl);
}

此中间件的目的是根据您的用户的浏览器语言偏好自动检测并将其重定向到网站的适当语言版本。

然后在项目的根目录中创建一个中间件文件src/middleware.ts,并在那里使用本地化中间件。

import { NextRequest } from 'next/server';

import { localizationMiddleware } from './features/internationalization/localization-middleware';

// Matcher ignoring `/_next/` and `/api/` and svg files.
export const config = { matcher: ['/((?!api|_next|.*.svg$).*)'] };

export function middleware(request: NextRequest) {
  return localizationMiddleware(request);
}

是时候添加您的翻译了。为您的英语翻译创建一个json词典,src/features/internationalization/dictionaries/en-us.json

{
  "counter": {
    "decrement": "Decrement",
    "increment": "Increment"
  },
  "landing": {
    "welcome": "Welcome"
  }
}

然后,为您的getDictionary函数创建一个文件src/features/internationalization/get-dictionaries.ts

import "server-only";
import type { Locale } from "./i18n-config";

// We enumerate all dictionaries here for better linting and typescript support.
// We also get the default import for cleaner types.
const dictionaries = {
  "en-US": () => import("./dictionaries/en-US.json").then((module) => module.default),
};

export const getDictionary = async (locale: Locale) =>
  dictionaries[locale]?.() ?? dictionaries["en-US"]();

它接受一个语言环境,然后返回相应的字典。

如果您想添加一个挂钩,让您的用户选择他们的语言,它可以看起来像这样。

import { usePathname } from 'next/navigation';

import { Locale } from './i18n-config';

export function useSwitchLocaleHref() {
  const pathName = usePathname();

  const getSwitchLocaleHref = (locale: Locale) => {
    if (!pathName) return '/';
    const segments = pathName.split('/');
    segments[1] = locale;
    return segments.join('/');
  };

  return getSwitchLocaleHref;
}

然后你可以在组件中使用这个钩子,在<Link />中使用它的返回值。

<Link href={getSwitchLocaleHref(locale)}>{locale}</Link>

更改您的URL结构以支持国际化。在您的app/文件夹中创建一个新文件夹[lang],这将为该语言创建一个动态段。

将您的page.tsxlayout.tsx文件移动到该文件夹中,并修改布局以在<html />标记上设置正确的语言。

import '../globals.css';

import type { Metadata } from 'next';
import { Inter } from 'next/font/google';

import { Locale } from '@/features/internationalization/i18n-config';

const inter = Inter({ subsets: ['latin'] });

export const metadata: Metadata = {
  title: "Jan Hesters' Next.js for production tutorial",
  description: 'Brought to you by ReactSquad.io',
};

export default function RootLayout({
  children,
  params,
}: Readonly<{
  children: React.ReactNode;
  params: { lang: Locale };
}>) {
  return (
    <html lang={params.lang}>
      <body className={inter.className}>{children}</body>
    </html>
  );
}

展望未来,您可以在任何服务器组件中使用getDicword函数。

import { getDictionary } from '@/features/internationalization/get-dictionaries';
import { Locale } from '@/features/internationalization/i18n-config';

import { CounterComponent } from './counter-component';

export default async function IndexPage({
  params: { lang },
}: {
  params: { lang: Locale };
}) {
  const dictionary = await getDictionary(lang);

  return (
    <div>
      <p>Current locale: {lang}</p>
      <p>This text is rendered on the server: {dictionary.landing.welcome}</p>
      <CounterComponent dictionary={dictionary.counter} />
    </div>
  );
}

对于客户端组件,您需要传入相应的字典。

'use client';

import { useState } from 'react';

import type { getDictionary } from '@/features/internationalization/get-dictionaries';

export function CounterComponent({
  dictionary,
}: {
  dictionary: Awaited<ReturnType<typeof getDictionary>>['counter'];
}) {
  const [count, setCount] = useState(0);

  return (
    <p>
      This component is rendered on the client:
      <button onClick={() => setCount(n => n - 1)}>
        {dictionary.decrement}
      </button>
      {count}
      <button onClick={() => setCount(n => n + 1)}>
        {dictionary.increment}
      </button>
    </p>
  );
}

数据库

本教程将使用Postgres作为其数据库,因为它经过了很好的实战测试,但是您将使用Prisma ORM来抽象数据库层。这使您可以灵活地使用各种数据库,并简化了用于与之交互的API。

npm install prisma --save-dev

您还需要安装Prisma客户端。

npm install @prisma/client

初始化棱镜。

npx prisma init

这会自动生成您的prisma/schema.prisma文件。

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  // Uses connection pooling
  url = env("DATABASE_URL")
}

model UserProfile {
  id                         String   @id @default(cuid())
  createdAt                  DateTime @default(now())
  updatedAt                  DateTime @updatedAt
  email                      String   @unique
  name                       String   @default("")
  acceptedTermsAndConditions Boolean  @default(false)
}

向其中添加一个UserProfile模型,其中包含一个电子邮件和一个名称以及一个布尔值,无论他们是否接受您的条款和条件。

如果您运行npx prisma init,请将.env文件重命名为.env.local,否则创建它并确保它包含Prisma数据库的凭据。

DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"

现在创建一个src/lib/prisma.ts文件,其中包含您的Prisma客户端连接。

import { PrismaClient } from '@prisma/client';

declare global {
  var __database__: PrismaClient;
}

let prisma: PrismaClient;

if (process.env.NODE_ENV === 'production') {
  prisma = new PrismaClient();
} else {
  if (!global.__database__) {
    global.__database__ = new PrismaClient();
  }
  prisma = global.__database__;
}

export default prisma;

重新分配确保在非正式生产环境中(如在开发或测试期间),只有一个Prisma客户端实例被创建并在整个应用程序中重用。这种方法可以防止每次导入需要数据库访问的模块时重复初始化与数据库的新连接的开销。

修改您的package.json以包含以下用于Prisma的辅助命令。

"prisma:deploy": "npx prisma migrate deploy && npx prisma generate",
"prisma:migrate": "npx prisma migrate dev --name",
"prisma:push": "npx prisma db push && npx prisma generate",
"prisma:reset-dev": "run-s prisma:wipe prisma:seed dev",
"prisma:seed": "tsx ./prisma/seed.ts",
"prisma:setup": "prisma generate && prisma migrate deploy && prisma db push",
"prisma:studio": "npx prisma studio",
"prisma:wipe": "npx prisma migrate reset --force && npx prisma db push",

其中一些命令使用run-stsx dotenv包,您需要安装它们。

npm install --save-dev npm-run-all tsx dotenv

以下是对每个Prisma命令的解释:

  • “prisma:deploy”:部署数据库迁移并生成Prisma Client。它在生产数据库上运行迁移并更新客户端API。
  • “prisma:migrate”:根据Prisma模式中的更改创建新的迁移,并将其应用于开发环境。您必须在命令之后指定迁移的名称。
  • “prisma:push”:将架构更改直接推送到数据库并更新Prisma客户端。对于不创建迁移文件的原型设计很有用。
  • “prisma:reset-dev”:通过擦除、重新播种和在开发模式下应用迁移来重置开发数据库。
  • “prisma:seed”:运行TypeScript种子脚本以使用初始数据填充数据库。
  • “prisma:setup”:生成Prisma客户端,将迁移部署到数据库,并将模式推送到数据库。
  • “prisma:studio”:打开Prisma Studio,一个用于查看和编辑数据库记录的GUI。
  • “prisma:wipe”:通过强制删除所有数据和迁移来重置数据库,然后推送架构更改。
npm run prisma:setup

如果您的Prisma无法识别您的. env.local文件,请在终端中手动设置您的环境变量。在Mac上,这可以使用导出命令来完成。

export DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"

创建一个prisma/seed.ts文件。您可以使用它为数据库播种用于开发的数据。

import { exit } from 'node:process';

import { PrismaClient } from '@prisma/client';
import dotenv from 'dotenv';

dotenv.config({ path: '.env.local' });

const prisma = new PrismaClient();

const prettyPrint = (object: any) =>
  console.log(JSON.stringify(object, undefined, 2));

async function seed() {
  const user = await prisma.userProfile.create({
    data: {
      email: 'jan@reactsquad.io',
      name: 'Jan Hesters',
      acceptedTermsAndConditions: true,
    },
  });

  console.log('========= 🌱 result of seed: =========');
  prettyPrint({ user });
}

seed()
  .then(async () => {
    await prisma.$disconnect();
  })
  // eslint-disable-next-line unicorn/prefer-top-level-await
  .catch(async error => {
    console.error(error);
    await prisma.$disconnect();
    exit(1);
  });

如果您运行它,它将创建您的用户。

$ npm run prisma:seed

> reactsquad-production@0.1.0 prisma:seed
> tsx ./prisma/seed.ts

========= 🌱 result of seed: =========
{
  "user": {
    "id": "clzekb5sp0000ock9gsp72p33",
    "createdAt": "2024-08-03T20:04:27.289Z",
    "updatedAt": "2024-08-03T20:04:27.289Z",
    "email": "jan@reactsquad.io",
    "name": "Jan Hesters",
    "acceptedTermsAndConditions": true
  }
}

在服务器组件中,可以使用prisma获取任何数据。

import prisma from '@/lib/prisma';
import {
  Card,
  CardContent,
  CardHeader,
  CardTitle,
} from '@/components/ui/card';

export default async function Dashboard() {
  const user = await prisma.userProfile.findUnique({
    where: { email: 'jan@reactsquad.io' },
  });

  return (
    <Card className="max-w-md mx-auto">
      <CardHeader>
        <CardTitle>User Profile</Card.Title>
      </CardHeader>
      <CardContent>
        {user ? (
          <ul>
            <li>Name: {user.name}</li>
            <li>Email: {user.email}</li>
          </ul>
        ) : (
          <p>User not found.</p>
        )}
      </CardContent>
    </Card>
  );
}

Facades

使用facade抽象出数据库调用是个好主意。facade是一种设计模式,您可以在其中为复杂的子系统提供简化的接口。

创建一个文件,其中包含与您的用户配置文件模型features/user-profile/user-profile-model.ts相关的所有外观。

import { UserProfile } from '@prisma/client';

import prisma from '@/lib/prisma';

export async function retrieveUserProfileFromDatabaseByEmail(
  email: UserProfile['email'],
) {
  return await prisma.userProfile.findUnique({ where: { email } });
}

使用立面有两个主要原因。

  1. 增加供应商阻力-您可以轻松切换第三方提供商。例如,您可以从Firebase切换到Supabase,反之亦然。您只需更新外观,而不是更新整个代码库以反映更改。
  2. 简化你的代码——外观可以减少你需要在应用程序中编写的代码,因为它减少了你特定应用程序需求的应用程序接口。同时,它使你的代码更容易理解,因为你可以给外观起描述性的名字。

然后在服务器组件中使用您的facade。

import { retrieveUserProfileFromDatabaseByEmail } from '@/features/user-profiles/user-profiles-model';
import {
  Card,
  CardContent,
  CardHeader,
  CardTitle,
} from '@/components/ui/card';

export default async function Dashboard() {
  const user =
    await retrieveUserProfileFromDatabaseByEmail('jan@reactsquad.io');

  return (
    <Card className="max-w-md mx-auto">
      <CardHeader>
        <CardTitle>User Profile</Card.Title>
      </CardHeader>

      <CardContent>
        {user ? (
          <ul>
            <li>Name: {user.name}</li>
            <li>Email: {user.email}</li>
          </ul>
        ) : (
          <p>User not found.</p>
        )}
      </CardContent>
    </Card>
  );
}

Vercel Postgres

您可以使用Vercel Postgres进行生产部署。他们有一个易于遵循的指南,您可以在Vercel文档中查看。但是为了您的方便,这里有一些快速的步骤。

要在您的Vercel项目中设置数据库,请执行以下步骤:

  1. 转到存储选项卡并单击创建数据库按钮。
  2. 当浏览存储模式打开时,选择Postgres并单击继续。

用于创建新数据库:

  1. 在对话框中,在商店名称字段中键入sample_postgres_db(或您喜欢的名称)。确保名称只有字母数字字符“_”或“-”,并且不超过32个字符。
  2. 选择一个区域。为了降低延迟,请选择一个靠近您的功能区域的区域,默认为美国东部。
  3. 单击创建。

然后,您需要将POSTGRES_URL_NON_POOLING添加到Prisma模式中的datasource中。

datasource db {
  provider  = "postgresql"
  // Uses connection pooling
  url       = env("DATABASE_URL")
  // Uses direct connection, ⚠️ make sure to keep this to `POSTGRES_URL_NON_POOLING`
  // or you'll have dangling databases from migrations
  directUrl = env("POSTGRES_URL_NON_POOLING")
}

Vercel使用连接池,它管理可由应用程序不同部分重用的数据库连接池,而不是为每个数据库请求建立新连接。directUrl属性用于确保需要直接数据库访问的操作(如迁移)可以绕过连接池以可靠执行。

您可以通过从Vercel中提取它们来获取Vercel数据库的环境变量。

vercel env pull .env

playwright

您还希望使用E2E测试,因为E2E测试让您最有信心您的应用程序按预期工作。E2E测试经常被跳过,但您真的应该养成编写它们的习惯。好处是复合的。

初始化playwright

$ npm init playwright@latest
Getting started with writing end-to-end tests with Playwright:
Initializing project in '.'Where to put your end-to-end tests? · playwright
✔ Add a GitHub Actions workflow? (y/N) · falseInstall Playwright browsers (can be done manually via 'npx playwright install')? (Y/n) · true

如果这是您第一次使用Playwright,您可以查看初始化脚本创建的test-examples/文件夹,然后将其删除,因为您不需要它。

修改playwright. config.ts文件中的playwright.config.ts``webServer密钥。

webServer: {
  command: process.env.CI ? 'npm run build && npm run start' : 'npm run dev',
  port: 3000,
},

将两个用于E2E测试的脚本添加到package.json.

"test:e2e": "npx playwright test",
"test:e2e:ui": "npx playwright test --ui",

第一个在无头模式下运行您的Playwright测试,而第二个在UI模式下运行您的测试,这为您提供了时间旅行调试、观看模式等。

import { expect, test } from '@playwright/test';

test.describe('landing page', () => {
  test('given any user: shows the test user', async ({ page }) => {
    await page.goto('/');

    await expect(page.getByText('Jan Hesters')).toBeVisible();
    await expect(page.getByText('jan@reactsquad.io')).toBeVisible();
  });
});

运行您的测试以检查您的Playwright设置是否有效。

$ npm run test:e2e

> reactsquad-production@0.1.0 test:e2e
> npx playwright test


Running 3 tests using 3 workers
  3 passed (3.9s)

T运行了三个测试,因为默认情况下Playwright配置为在Chrome、Safari和Firefox中运行。

GitHub Action

使用CI/CD运行您的应用程序是一种很好的做法。CI/CD代表持续交付和持续部署。

将数据库URL的机密添加到GitHub中存储库的设置中

DATABASE_URL="postgresql://postgres:postgres@localhost:5432/testdb"

然后,为全面的CI/CD管道(包括lting、类型检查、测试等)在. github/workflow/larl-request.yml文件中创建您的GitHub Actions YAML配置。

name: Pull Request

on: [pull_request]

jobs:
  lint:
    name: ⬣ ESLint
    runs-on: ubuntu-latest
    steps:
      - name: ⬇️ Checkout repo
        uses: actions/checkout@v3

      - name: ⎔ Setup node
        uses: actions/setup-node@v3
        with:
          node-version: 20

      - name: 📥 Download deps
        uses: bahmutov/npm-install@v1

      - name: 🔬 Lint
        run: npm run lint

  type-check:
    name: ʦ TypeScript
    runs-on: ubuntu-latest
    steps:
      - name: ⬇️ Checkout repo
        uses: actions/checkout@v3

      - name: ⎔ Setup node
        uses: actions/setup-node@v3
        with:
          node-version: 20

      - name: 📥 Download deps
        uses: bahmutov/npm-install@v1

      - name: 🔎 Type check
        run: npm run type-check --if-present

  commitlint:
    name: ⚙️ commitlint
    runs-on: ubuntu-latest
    if: github.actor != 'dependabot[bot]'
    env:
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    steps:
      - name: ⬇️ Checkout repo
        uses: actions/checkout@v3
        with:
          fetch-depth: 0
      - name: ⚙️ commitlint
        uses: wagoid/commitlint-github-action@v4

  vitest:
    name: ⚡ Vitest
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:12
        env:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: testdb
        ports:
          - 5432:5432
    steps:
      - name: ⬇️ Checkout repo
        uses: actions/checkout@v3

      - name: ⎔ Setup node
        uses: actions/setup-node@v3
        with:
          node-version: 20

      - name: 📥 Download deps
        uses: bahmutov/npm-install@v1

      - name: 🛠 Setup Database
        run: npm run prisma:wipe
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb

      - name: ⚡ Run vitest
        run: npm run test -- --coverage
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb

  playwright:
    name: 🎭 Playwright
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:12
        env:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: testdb
        ports:
          - 5432:5432
    steps:
      - name: ⬇️ Checkout repo
        uses: actions/checkout@v3

      - name: ⎔ Setup node
        uses: actions/setup-node@v3
        with:
          node-version: 20

      - name: 📥 Download deps
        uses: bahmutov/npm-install@v1

      - name: 🌐 Install Playwright Browsers
        run: npx playwright install --with-deps

      - name: 🛠 Setup Database
        run: npm run prisma:wipe
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb

      - name: 🎭 Playwright Run
        run: npx playwright test
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb

      - name: 📸 Playwright Screenshots
        uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

现在,每次您向您的应用程序发出拉取请求时,它都会自动运行您的测试以确保它正常工作,运行TypeScript类型检查并对其进行对齐,以便每个人都贡献具有相同格式的代码。