0%

Next.js实践初步

Next.js + primsa + NextAuth + Postgresql。

什么是Next.js

Next.js 是一个用于构建全栈网络应用程序的 React 框架。你可以使用 React Components 构建用户界面,使用 Next.js 实现附加功能和优化。Next.js 抽象并自动配置 React 所需的工具,如捆绑、编译等。这样,开发者就可以专注于构建应用程序,而不必花时间进行配置。无论是个人开发者还是大型团队的一员,Next.js 都能帮助您构建交互式、动态和快速的 React 应用程序。

Next.js的主要功能

功能 描述
路由 基于文件系统的路由器构建于服务器组件之上,支持布局、嵌套路由、加载状态、错误处理等。
渲染 通过客户端和服务器组件进行客户端和服务器端渲染。利用 Next.js 进一步优化服务器上的静态和动态渲染。
数据获取 利用服务器组件中的 async/await 简化数据获取,并利用扩展的获取 API 实现请求备忘录化、数据缓存和重新验证。
样式设计 支持首选的样式设计方法,包括 CSS 模块、Tailwind CSS 和 CSS-in-JS
优化 图像、字体和脚本优化,以改善应用程序的核心网络性能和用户体验。
TypeScript 通过更好的类型检查和更高效的编译,以及自定义 TypeScript 插件和类型检查器,改进了对 TypeScript 的支持。

创建Next.js项目

1
2
3
4
5
6
7
8
9
$ npx create-next-app@latest

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

当使用create-next-app命令创建一个新的Next.js项目时,你将会被询问一系列选项,这些选项将决定项目的初始配置。以下是每个选项的含义:

  1. 项目名称:这是项目的名称,可以输入任何你喜欢的名称。
  2. 是否使用TypeScript:这个选项决定是否在项目中使用TypeScript作为主要的编程语言。如果你选择”Yes”,那么项目将会被初始化为TypeScript项目。选择使用TypeScript意味着你希望在项目中使用静态类型检查,以提高代码的可靠性和可维护性。TypeScript还可以帮助你在开发过程中捕获潜在的错误,并提供更好的代码编辑体验。此外,TypeScript还可以提供更好的代码自动补全和文档提示,以及更好的重构支持。因此,你对TypeScript可能有以下期望和需求:
    1. 静态类型检查:希望通过类型检查来捕获潜在的错误,提高代码的可靠性。
    2. 更好的编辑体验:希望在编辑器中获得更好的代码自动补全、文档提示和重构支持。
    3. 可维护性:希望通过类型定义和接口来提高代码的可读性和可维护性。
    4. 生态系统支持:希望利用TypeScript丰富的生态系统和社区支持,以便更好地开发和维护项目。
  3. 是否使用ESLint:ESLint是一个用于代码规范和错误检查的工具。选择”Yes”将会在项目中集成ESLint。ESLint可以根据预定义的规则集或自定义规则来检查代码,以确保代码的一致性、可读性和安全性。它还可以集成到开发工具和持续集成流程中,提供实时的代码检查和反馈。ESLint的主要作用是帮助开发团队维护一致的代码风格,减少潜在的错误和漏洞,并提高代码质量和可维护性。使用ESLint有以下优点:
    1. 自动检测潜在漏洞:ESLint可以自动检测潜在的漏洞和安全问题,帮助你在开发过程中及早发现并修复这些问题,从而提高代码的安全性。
    2. 可定制的规则:ESLint允许你根据项目的特定需求定制规则,这意味着你可以针对项目的特定情况设置规则,从而更好地关注对项目最相关的问题。
    3. 与其他工具的集成:ESLint可以轻松集成到其他工具和流程中,例如编辑器、构建工具和持续集成/持续部署(CI/CD)流程中,使其成为确保代码安全性的便捷选择。
  4. 是否使用Tailwind CSS:Tailwind CSS是一个实用的CSS框架,选择”Yes”将会在项目中集成Tailwind CSS。使用Tailwind CSS的优点包括:
    1. 快速开发:Tailwind CSS提供了大量的预定义样式类,可以快速构建页面和组件,节省了开发时间。
    2. 可定制性:Tailwind CSS允许开发人员根据项目需求自定义样式,而不需要编写自定义的CSS代码,使得样式更易于维护和管理。
    3. 一致性:通过使用Tailwind CSS的预定义样式类,可以确保项目中的样式具有一致的外观和行为,减少了样式的不一致性。
    4. 响应式设计:Tailwind CSS提供了丰富的响应式设计类,可以轻松地创建适应不同屏幕尺寸的布局和样式。
    5. 生态系统和社区支持:Tailwind CSS拥有庞大的生态系统和活跃的社区,提供了大量的插件、工具和资源,为开发人员提供了丰富的支持和帮助。
  5. 是否使用src/目录:这个选项决定是否在项目中使用src/目录来存放源代码文件。
  6. 是否使用App Router(推荐):App Router是Next.js的路由系统,选择”Yes”将会在项目中使用App Router。Next.js 的 App Router 是 Next.js 框架中的核心功能之一,它提供了一种用于处理路由的简单且强大的方式。下面是 App Router 的几个优点:
    1. 服务端渲染(Server-side Rendering):App Router 支持在服务器上进行页面渲染,这意味着用户在访问页面时可以立即看到内容,而不需要等待 JavaScript 的加载和执行。这提供了更好的性能和用户体验,并对搜索引擎优化(SEO)也有积极影响。
    2. 动态路由:App Router 允许您使用动态路由构建灵活的页面结构。您可以在路由中使用参数,从而根据参数的不同加载不同的内容。例如,您可以创建一个动态路由来显示特定用户的个人资料页面。
    3. 文件系统路由:Next.js 的 App Router 可以根据文件系统的结构自动创建路由。这意味着您可以在 pages 目录中创建一个文件,并将其自动映射到对应的 URL 路径上,无需手动配置路由。这种简单而直观的路由配置方式使得开发过程更加高效。
    4. 客户端导航:App Router 还提供了客户端导航功能,可以在不刷新整个页面的情况下进行导航。这种无需重新加载整个页面的导航方式使得用户体验更加流畅,并且能够在应用程序中实现各种交互效果。
    5. 生命周期钩子:App Router 提供了一些生命周期钩子函数,您可以在路由切换前后执行特定的操作。例如,您可以在路由切换前加载数据,或者在路由切换后进行一些清理操作。这使得处理页面之间的状态转换和数据加载变得更加灵活和可控。
  7. 是否自定义默认导入别名(@/):这个选项决定是否自定义默认的导入别名。如果你选择”Yes”,你可以输入你想要的导入别名。在 Next.js 中自定义默认导入别名(如 @ 或 )具有以下几个优点:
    1. 简化导入路径:通过设置默认导入别名,可以将导入路径简化为更短、更易读的形式。例如,可以将长路径 import { Component } from '../../components' 简化为 import { Component } from '@/components'。这样一来,代码中的导入语句更加清晰和易于理解。
    2. 提高可维护性:使用默认导入别名可以减少代码中的硬编码路径,并使代码更具可维护性。如果项目中的文件结构发生变化,只需要更新别名配置,而不需要在整个代码库中修改导入路径。
    3. 简化重构:当需要对项目进行重构或重新组织时,使用默认导入别名可以让这个过程更加简单和安全。您可以通过修改别名配置来实现文件移动或重命名,而无需修改所有相关的导入语句。
    4. 提高开发效率:默认导入别名可以减少编码时的重复劳动。您可以快速且一致地引用项目中的不同模块,而无需记住复杂的相对路径。这样可以节省时间和精力,提高开发效率。
    5. 跨平台兼容性:默认导入别名是一种通用的技术,在很多前端工具和框架中都可以使用。这意味着,如果您在将来决定切换到不同的工具或框架,您可以轻松地迁移别名配置,而无需修改大量的导入语句。

Next.js的典型目录结构

以使用Next.js 14 + next-auth + prisma + edgeStore构建博客网站为例。

  • root
    • app
      • (auth)
        • api/auth/[…nextauth]
          • route.ts
      • (site)
        • about
          • page.tsx
        • access
          • page.tsx
        • blog/[id]
          • page.tsx
        • contact
          • page.tsx
        • create
          • page.tsx
        • posts
          • page.tsx
        • userposts
          • page.tsx
        • globals.css
        • layout.tsx
        • page.tsx
      • actions
        • blogAction.ts
        • getCurrentUser.ts
      • api
        • [post]/[id]
          • route.ts
        • edgestore/[…edgestore]
          • route.ts
    • components
      • shared
        • Footer.tsx
        • Navbar.tsx
        • Posts.tsx
        • CreateForm.tsx
        • BlogCard.tsx
        • DeletePost.tsx
      • ui
        • Button.tsx
        • Form.tsx
        • Input.tsx
        • Map.tsx
        • Overlay.tsx
        • Route.tsx
        • SingleImageDropZone.tsx
        • Tag.tsx
    • constants
      • sampleData.ts
      • index.ts
    • context
      • AuthContext.tsx
    • hooks
      • useMenuActive.tsx
    • lib
      • edgestore.ts
      • prismadb.ts
    • prisma
      • schema.prisma
    • public
      • assets
        • about.jpg
        • post1.jpg
        • post2.jpg
    • types
      • postTypes.ts
      • userTypes.ts
    • utils
      • formatDate.ts
    • .env
    • .gitignore
    • next.config.js
    • package-lock.json
    • package.json
    • postcss.config.js
    • tailwind.config.js
    • tsconfig.json

关于primsa

首先,确保已经在项目中安装了Prisma CLI,并且已经初始化了Prisma项目。可以使用以下命令来安装Prisma CLI并初始化项目:

1
2
npm install @prisma/cli --save-dev
npx prisma init

在Prisma项目初始化完成后,你需要编辑Prisma的schema文件(通常是prisma/schema.prisma,代码在后面),定义你的数据库模型和连接信息。

定义好Prisma的schema后,你可以使用Prisma CLI来生成Prisma Client。运行以下命令来生成Prisma Client:

1
npx prisma generate

生成Prisma Client后,你可以在Next.js项目中的API路由或页面组件中使用Prisma Client来连接和操作数据库。以下是一个简单的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// pages/index.js

import prisma from '../lib/prisma';

export default function Home({ users }) {
return (
<div>
<h1>用户列表</h1>
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}

export async function getServerSideProps() {
const users = await prisma.user.findMany();
return {
props: { users },
};
}

当数据库的结构发生变化时,你需要更新Prisma的schema文件以反映这些变化,并生成新的Prisma Client。以下是操作步骤:

  1. 更新Prisma Schema:根据数据库结构的修改,你需要更新Prisma的schema文件(通常是prisma/schema.prisma),修改或添加相应的数据模型、关系和字段定义。
  2. 生成Prisma Client:运行以下命令来生成新的Prisma Client,以便它能够反映数据库结构的变化
    1
    npx prisma generate
  3. 数据迁移:如果数据库结构的变化需要进行迁移操作,你可以使用Prisma Migrate来创建和应用数据库迁移。运行以下命令来创建一个新的迁移:
    1
    npx prisma migrate dev --name your-migration-name

实现email注册/登录的方法

.env文件

1
2
3
4
5
6
7
8
9
10
# 设置数据库,以postgresql为例
DATABASE_URL=postgresql://admin:password6@服务.器主.机.地址:5432/blog

# 设置邮件服务器
EMAIL_SERVER=smtp://1234566@qq.com:password@smtp.qq.com:587
EMAIL_FROM=1234567@qq.com

# 设置edgeStore
EDGE_STORE_ACCESS_KEY=xxxxxxx
EDGE_STORE_SECRET_KEY=xxxxxxx

schema.prisma文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}

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

model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String?
access_token String?
expires_at Int?
token_type String?
scope String?
id_token String?
session_state String?

user User @relation(fields: [userId], references: [id], onDelete: Cascade)

@@unique([provider, providerAccountId])
}

model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
Blog Blog[]
}

model VerificationToken {
id String @id @default(cuid())
identifier String
token String @unique
expires DateTime

@@unique([identifier, token])
}

model Blog {
id String @id @default(cuid())
createdAt DateTime @default(now())
title String
description String
img String?
category String
userEmail String
featured Boolean @default(false)
topPost Boolean @default(false)
user User @relation(fields: [userEmail], references: [email])
}

app/(auth)/api/auth/[…nextauth]/route.ts内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import NextAuth, { AuthOptions } from "next-auth";
import EmailProvider from "next-auth/providers/email";
import { PrismaAdapter } from "@auth/prisma-adapter";
import client from "@/lib/prismadb";

export const authOptions: AuthOptions = ({
adapter: PrismaAdapter(client),
providers: [
EmailProvider({
server: process.env.EMAIL_SERVER as string,
from: process.env.EMAIL_FROM as string,
}),
],
debug: process.env.NODE_ENV === "development",
secret: process.env.NEXTAUTH_SECRET
})

const handler = NextAuth(authOptions)

export { handler as GET, handler as POST}

context/AuthContext.tsx内容

1
2
3
4
5
6
7
8
9
10
11
12
13
"use client";

import { SessionProvider } from "next-auth/react";

export interface AuthContextProps {
children: React.ReactNode;
}

export default function AuthContext({
children,
}: AuthContextProps) {
return <SessionProvider>{children}</SessionProvider>;
}

(site)/layout.tsx内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import type { Metadata } from 'next'
import { Roboto } from 'next/font/google'
import './globals.css'

import Navbar from '@/components/shared/Navbar'
import Footer from '@/components/shared/Footer'

import AuthContext from '@/context/AuthContext'
import getCurrentUser from '../actions/getCurrentUser'

import { EdgeStoreProvider } from '@/lib/edgestore'

const roboto = Roboto({ subsets: ['latin'], weight: ["100","400","700","900"] })

export const metadata: Metadata = {
title: 'ExploreX',
description: 'Travel Blog',
}

export default async function RootLayout({
children,
}: {
children: React.ReactNode
}) {
const user = await getCurrentUser()
return (
<html lang="en">
<AuthContext>
<EdgeStoreProvider>
<body className={`${roboto.className} overflow-x-hidden bg-light`}>
<Navbar user={user}/>
{children}
<Footer/>
</body>
</EdgeStoreProvider>
</AuthContext>
</html>
)
}

后端

types内定义的两个类型文件

postTypte.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { User } from "@prisma/client";

export interface PostTypes {
id: string;
title: string;
img: string | null;
desc: string;
featured: boolean;
topPost: boolean;
category: string;
authorImage: string;
authorName: string;
publishDate: string;
createdAt: string | Date
user: User;
}

userTypes.ts

1
2
3
4
5
6
7
export type userTypes = {
id: string;
name: string | null;
email: string | null;
emailVerified: Date | string | null;
image: string | null;
} | null ;

Prismadb.ts内容

1
2
3
4
5
6
7
8
9
10
11
import { PrismaClient } from "@prisma/client";

declare global {
var prisma: PrismaClient | undefined
}

const client = globalThis.prisma || new PrismaClient()

if (process.env.NODE_ENV !== "production") globalThis.prisma = client

export default client

页面访问后端

userposts/page.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import client from "@/lib/prismadb";
import getCurrentUser from "@/app/actions/getCurrentUser";
import BlogCard from "@/components/shared/BlogCard";
import DeletePosts from "@/components/shared/DeletePosts";

const page = async () => {
const user = await getCurrentUser();
const posts = await client.blog.findMany({
where: {
userEmail: user?.email ?? undefined
},
orderBy: {
createdAt: "desc"
},
include: {
user: true
}
})
return (
<div className="w-full flex justify-center items-center">
{!user ? (
<h1 className="text-3xl font-extrabold">Sign in to view your posts.</h1>
) :
(<div className="max-w-[90%] mx-auto">
<div className="w-full text-center mb-10">
<h1 className="text-3xl font-extrablold text-tertiary">
Hello { user?.name }
</h1>
<span className="text-lg">
You have published {posts.length} posts.
</span>
</div>
<div className="grid md:grid-cols-2 grid-cols-1 justify-center items-center gap-10">
{posts.map((post) => (
<div key={post.id} className="relative">
<BlogCard post={post as any}/>
<DeletePosts post={post as any} />
</div>
))}
</div>
</div>)}
</div>
)
}

export default page;

Post数据

app/(site)/create/page.tsx

1
2
3
4
5
6
7
8
9
10
11
import getCurrentUser from "@/app/actions/getCurrentUser";
import CreateForm from "@/components/shared/CreateForm";

const page = async () => {
const user = await getCurrentUser();
return (
<CreateForm user={user} />
)
}

export default page

components/ui/Form.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
"use client"

import { useRef, FormEvent } from "react"
import { ReactNode } from "react"

interface FormProps {
children: ReactNode
action: (formData: FormData) => Promise<void | boolean>
className?: string
onSubmit?: (e: FormEvent<HTMLFormElement>) => void
}

const Form = ({
children,
action,
className,
onSubmit
}: FormProps) => {
const ref = useRef<HTMLFormElement>(null)
return (
<form
className={className}
onSubmit={onSubmit}
ref={ref}
action={async (formData) => {
await action(formData)
ref.current?.reset()
}}
>
{children}
</form>
)
}

export default Form

components/shared/CreatForm.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
"use client"

import Form from "../ui/Form";
import Input from "../ui/Input";
import { useEdgeStore } from "@/lib/edgestore";
import { useState, useEffect } from "react";
import Button from "../ui/Button";
import { SingleImageDropzone } from "../ui/SingleImageDropZone";
import { userTypes } from "@/types/userTypes";
import { createPost } from "@/app/actions/blogActions";

const CreateForm = ({user}: {user:userTypes}) => {
const [file, setFile] = useState<File>();
const {edgestore} = useEdgeStore();
const [imagePath, setImagePath] = useState("");

const uploadImageHandler =async () => {
if (file) {
const res = await edgestore.publicFiles?.upload({
file,
});
setImagePath(res.url);
}
}

useEffect(() => {
if (file) {
uploadImageHandler();
}
}, [file]);

return (
<div className="mt-8 mx-auto w-full max-w-3xl px-4">
<div className="bg-white py-8 shadow rounded-lg px-10">
<h1 className="text-center text-2xl font-extrabold mb-10">
Create a Post
</h1>
{!user ? (
<h2 className="text-center text-xl font-extrabold uppercase">
Please Sign or Log in to create a post!
</h2>
): (
<>
<SingleImageDropzone
onChange={(file) => {
setFile(file)
}}
value={file}
width={200}
height={200}
/>
<Form
action={createPost}
className="flex flex-col gap-5 mt-5"
onSubmit={() => setFile(undefined)}
>
<Input
type="hidden"
name="image"
value={imagePath}
/>
<Input
name="title"
type="text"
placeholder="Enter Title"
/>
<textarea
name="description"
rows={10}
placeholder="Write Here..."
className="text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-primary sm:text-sm sm:leading-6 border w-full border-gray-200 p-2 rounded-md py-1.5"
>
</textarea>
<select
required
name="category"
className="text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-primary sm:text-sm sm:leading-6 border w-full border-gray-200 p-2 rounded-md py-1.5"
>
<option value="" disabled selected>
Choose Tag
</option>
<option value="Adventure">
Adventure
</option>
<option value="Culture">
Culture
</option>
<option value="Journey">
Journey
</option>
<option value="Wanderlust">
Wanderlust
</option>
</select>
<Input
name="email"
type="hidden"
value={user.email || ""}
/>
<Button
type="submit"
text="Create"
aria="create blog"
/>
</Form>
</>
) }
</div>
</div>
)
}

export default CreateForm

app/actions/blogActions.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
"use server"

import client from "@/lib/prismadb";
import { revalidatePath } from "next/cache";

export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
const desc = formData.get("description") as string;
const cat = formData.get("category") as string;
const userEmail = formData.get("email") as string;
const image = formData.get("image") as string;

await client.blog.create({
data: {
img: image,
title: title,
description: desc,
category: cat,
userEmail: userEmail
}
});

revalidatePath("/create");
}

export async function deletePost(formData: FormData) {
const id = formData.get("postId") as string;
console.log('the id is:' + id);

await client.blog.delete({
where: {
id: id
}
});

revalidatePath("/userposts");
}

tailwind.config.ts 自定义配色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import type { Config } from 'tailwindcss'

const config: Config = {
content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
colors: {
primary: "#eb5e28",
secondary: "#252422",
tertiary: '#403d39',
light: "#fffcf2",
}
},
},
plugins: [],
}
export default config

几种文件类型的区别

JavaScript (JS)、TypeScript (TS) 和 TypeScript with JSX (TSX) 文件的异同如下:

JavaScript (JS) 文件:

  • JS文件是纯粹的JavaScript代码文件,其中包含的代码是标准的JavaScript语法。
  • JS文件通常以.js为扩展名,例如:example.js。
  • JS文件中的代码不会经过类型检查,因此在开发过程中需要更加小心地处理类型相关的问题。

TypeScript (TS) 文件:

  • TS文件包含的是使用TypeScript语言编写的代码,它是JavaScript的超集,提供了静态类型检查和其他高级特性。
  • TS文件通常以.ts为扩展名,例如:example.ts。
  • TS文件中的代码会经过类型检查,可以在开发过程中提供更好的类型安全和代码提示。

TypeScript with JSX (TSX) 文件:

  • TSX文件是TypeScript的一种变体,用于编写包含JSX语法的代码,通常用于React应用程序的组件开发。
  • TSX文件通常以.tsx为扩展名,例如:example.tsx。
  • TSX文件中的代码除了经过类型检查外,还可以包含JSX语法,用于编写React组件。

总的来说,JS文件是标准的JavaScript代码文件,TS文件是使用TypeScript语言编写的代码文件,而TSX文件是用于编写包含JSX语法的TypeScript代码文件,它们在语法和类型检查方面有所不同,适用于不同的开发场景和需求。