NextJs-react开发者的全栈最佳选择(从0-1的react全栈入门指南)

本文主要是介绍NextJs-react开发者的全栈最佳选择(从0-1的react全栈入门指南),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

NextJs:react开发者的全栈最佳选择(从0-1的react全栈入门指南)

目录

  • 前言
  • 学习路线
    • 1.TS速成
    • 2.React速成视频入门(四个板块)
    • 3.React官方文档
    • 4.学习React常用hooks
    • 5.学习tailwindcss
    • 6.做一个小项目:One Thing
    • 7.学习NextJS14
    • 8.学习MongoDB
    • 9.学习AuthJS/Next-Auth
    • 10.巩固NextJS
    • 11.NextJS实战:HaloChat
  • 随笔
    • 资源随笔
      • 1.web学习网站推荐:
    • vscode随笔
      • 插件
      • 设置
    • React随笔
      • 注意事项
      • React hooks
      • react快捷键
    • tailwindcss随笔
      • 盒模型相关
      • 文字相关
      • flex/grid相关
    • One Thing项目随笔
    • Mongoose随笔
      • Creating Model
      • insert
      • find
      • update
      • delete
      • ObjectId
      • 正则搜索
      • 在NextAuth-advanced项目中的使用
    • NextJS随笔
      • use server/client
      • Image组件
      • 动态路由
      • 路由组
    • AuthJS随笔
      • 大体鉴权流程
      • JWT
    • NextAuth-advanced项目随笔
      • 目录讲解
      • 为什么用cache存储session?
      • html表格标签
      • mongoose数据库使用
    • HaloChat实战项目重构随笔
      • 重构部分
      • 前言
      • 随笔

前言

该指南面向vue转react的同学和想学习react的同学,本指南本质上是我个人的学习随笔记录,但也可以用作学习参考。

学习路线

1.TS速成

在当下ts已经是前端必会语言,无论是Vuer还是Reacter,如果你有ts基础可以忽略这一条,如果没有,可以去速成一下:

【20分钟学会TypeScript 无废话速成TS 学不会你来评论区】https://www.bilibili.com/video/BV1gX4y177Kf?vd_source=0d0d6b12377aa593bc3a34f0884de98a

2.React速成视频入门(四个板块)

【30分钟学会React18核心语法 可能是你学会React最好的机会 前端开发必会框架 无废话精品视频】https://www.bilibili.com/video/BV1pF411m7wV?vd_source=0d0d6b12377aa593bc3a34f0884de98a

可以结合下面博客食用:

react速成指南-CSDN博客

3.React官方文档

React 官方中文文档

根据自身基础选看即可

4.学习React常用hooks

https://youtu.be/6wf5dIrryoQ?si=CaMTqsMXloKUk9cl

附带一个测验(很简单可做可不做,下面附上链接):

https://quiz.greatstack.dev/rhks

5.学习tailwindcss

由于tailwind在国外且在react上使用广泛,所以tailwindcss是reacter的必修课

【【tailwind】tailwind使用入门】https://www.bilibili.com/video/BV1nV4y1y7ec?vd_source=0d0d6b12377aa593bc3a34f0884de98a

下面附上tailwindcss随笔

6.做一个小项目:One Thing

【绝对React初学者的项目(一件事应用程序)】https://www.bilibili.com/video/BV1qd4y197Ep?vd_source=0d0d6b12377aa593bc3a34f0884de98a

比较可惜的是该教程没有用ts,下面附上one-thing项目随笔

7.学习NextJS14

https://youtu.be/GowPe3iiqTs?si=SeUrE7ogGPeUF47l

教程中的项目会用到MongoDB和next-auth,所以建议去补一下。

教程中的项目源码地址:

Mebius1916/nextjs-demo: next初学者必学的小demo (github.com)

8.学习MongoDB

个人感觉比mysql好用

https://youtu.be/soprdrmpO3M?si=WH7iaP7O-jnSOzgU

9.学习AuthJS/Next-Auth

该教程可以算作上面nexjs14教程的进阶版。

学习视频:

https://youtu.be/soprdrmpO3M?si=ZkKppA30G8Nn0yli

参考文章:

NextJS - 使用 next-auth 配置 JWT token - 炎黄子孙,龙的传人 - 博客园 (cnblogs.com)

视频项目NextAuth-advanced地址:

Mebius1916/NextAuth-advanced (github.com)

下面附上该项目随笔:

NextAuth-advanced项目随笔

10.巩固NextJS

学习视频:【[ Nextjs ] 关于NextJS你需要知道的12个概念 - ByteGrad - 管子版本】https://www.bilibili.com/video/BV1TC411b7H8?vd_source=0d0d6b12377aa593bc3a34f0884de98a

11.NextJS实战:HaloChat

技术栈:

  • 前端:React+Roast+TypeScript+MaterialUI+
  • 后端:NextJS+NextAuth+MongoDB+BcryptJS

学习视频:

https://youtu.be/2Zv8YYq1ymo?si=WF90w6Hq82UNlmnI

ts重构版代码地址:

Mebius1916/HaloChat — Mebius1916/HaloChat (github.com)

实战可自行挑选,我选择该项目的原因是我打算从0-1写一个集合了各种功能的聊天工具,然后该项目用到了NextJS, Next Auth, MongoDB, Tailwindcss,与之前学习的技术栈相符。

随笔

资源随笔

1.web学习网站推荐:

Web 开发技术 | MDN (mozilla.org)

我一般用来搜css,能实时编辑查看css的效果这点我比较喜欢。

vscode随笔

插件
  1. AI插件

    选择你喜欢的AI插件即可,我用的是字节的MarsCode

  2. Material Icon Theme

    文件/文件夹的图标主题,好用好看。

设置
  1. 缩进设置

    原因:vscode的默认缩进是4,代码繁琐的话会导致代码结构不清晰,推荐设置成2。

    操作:打开setting,并输入:tabsize然后回车搜索。

    在这里插入图片描述

    在这里插入图片描述

React随笔

注意事项
  • 组件名必须以大写字母开头

  • 可以在组件中使用其它组件但不要定义组件

  • 组件导入分为具名导入和默认导入

    语法导出语句导入语句
    默认export default function Button() {}import Button from './Button.js';
    具名export function Button() {}import { Button } from './Button.js';
  • 函数传递与函数调用的区别

    • 传递:thisFunction
    • 调用:thisFunction()
  • useState与useReducer可实现相同功能:

    react hooks之useState与useReducer-CSDN博客

React hooks

1.useState

为什么要用useState?

用于解决修改变量值后由于不重新渲染导致页面仍保留渲染完成后的旧值(如ref),那么使用useState即可在改变变量的值后使页面重新渲染。

const [state, setState] = useState(0);

const [state, setState] = useState([]);

const [state, setState] = useState({});

state为绑定元素,setState用于对state进行操作

export default function Home() {const [todos, setTodos] = useState<Todo[]>([]);const addTodo = (text:string) => {const newTodo = {id:Date.now(),text,completed:false}setTodos([...todos,newTodo])//合并}
}

更新state中的数组注意事项

避免使用 (会改变原始数组)推荐使用 (会返回一个新数组)
添加元素pushunshiftconcat[...arr] 展开语法(例子)
删除元素popshiftsplicefilterslice(例子)
替换元素splicearr[i] = ... 赋值map(例子)
排序reversesort先将数组复制一份(例子)

2.useEffect

与vue中的watch类似,可通过封装⇒watch功能

无依赖项:

useEffect(()⇒{})

每次组件渲染时都会触发。

空依赖项数组:

useEffect(()⇒{},[])

只在组件初次渲染后执行一次。

有依赖项:

useEffect(()⇒{...},[count])

组件第一次加载且每当count变化时触发。

3.useRef

const ref = useRef(initialValue)

与useState相似之处在于都可以定义一个变量并对其操作,最大的不同点在于useRef的修改不会出发页面重新渲染,而useState的修改会出发页面重新渲染。

1、改变变量不重新渲染。

2、绑定dom元素:给dom元素绑定“别名”,通过别名对dom元素进行操作。

4.useMemo

const cachedValue = useMemo(calculateValue, dependencies)

为了防止state改变数据造成页面重新渲染从而导致的重复计算(复杂计算),useMemo 会在依赖项变化时重新计算值,而在依赖项没有变化时,返回上一次计算的结果,从而避免不必要的计算。

5.useCallback

const cachedFn = useCallback(fn, dependencies)

与useMemo类似,为了防止state改变数据造成页面重新渲染从而导致组件的重复渲染。

6.useContext

用于管理react中的全局数据,类似vue中的provide/inject

const value = useContext(SomeContext)

useContext – React 中文文档

如何使用自行参考文档或video,由于好理解且代码量大,所以不在此处展示,个人感觉没有provide/inject好用。

7.useReducer

通过抽离重复逻辑来减少代码量,常见的用法是控制变量加减

// @ts-nocheck
import {useReducer, useState} from "react"//定义逻辑
function countReducer(state,action){switch(action.type){case "increment":return state + 1case "decrement:":return state - 1default:throw new Error()}
}
export default function App() {const [state,dispatch] = useReducer(countReducer,0)//放入逻辑和初始值const handleIncrement = () => dispatch({type:"increment"})const handleDecrement = () => dispatch({type:"decrement"})return(<div style={{padding:10}}><button onClick={handleIncrement}>-</button><span>{count}</span><button onClick={handleDecrement}>+</button></div>)
}

8.useLayoutEffect

与useEffect功能相同,区别是useEffect在dom元素渲染后调用,而useLayoutEffect在dom元素渲染前调用。

无依赖项:

useLayoutEffect(()⇒{})

每次组件渲染时都会触发。

空依赖项数组:

useLayoutEffect(()⇒{},[])

只在组件初次渲染后执行一次副作用函数。

有依赖项:

useLayoutEffect(()⇒{...},[count])

组件第一次加载且每当count变化时触发。

9.usePathname

作用是获取到当前路径名

const pathname = usePathname()

react快捷键

rafce

import React from 'react'const page = () => {return (<div>page</div>)
}export default page

rfce

import React from 'react'function page() {return (<div>page</div>)
}export default page

tailwindcss随笔

盒模型相关

1.w-xx宽度||h-xx高度||bg-xx背景||min/max-w/h-xx最小宽度

2.p-xx表示padding||m-xx表示margin : xylrtbse

3.border-xx表示border||xx是长度或者颜色||rounded-xx表示圆角||shadow阴影

4.位置absolute||top-xx||left-xx||right-xx||z-xx(z-index)

文字相关
  1. 颜色大小与对齐 text-xxx
  2. 行距leading
  3. 字粗font-xx
flex/grid相关

【用在父容器】

  1. flex=display:flex;
  2. flex-[row/col-reverse]方向
  3. flex-wrap/nowrap溢出
  4. justify-xx横向瓦片排布 content-xx纵向瓦片排布
  5. justify-item-xx瓦片内dom横向排布item-xx瓦片内dom纵向排布

【用在item】

  1. flex-1 flex-auto自动扩缩
  2. basis-xx grow/shrink[-0] 手动设置三个参数
  3. justify-self-xx self-xx 调整自己这个瓦片的排布

One Thing项目随笔

一个很不错的入门项目,美中不足的是没有用ts,通过这个项目你能学会:

  • react的组件化思想
  • react的动态绑定
  • react隐藏组件思想(v-if效果,不是v-show)
  • tailwindcss的入门使用
  • heroicons图表库的使用
  • js-confetti五彩纸屑库的使用
  • 用vite搭建react项目(个人觉得vite是优于webpack的)

我在原有代码的基础上进行了一点点的修改来符合我的审美,下面附上源码github链接:

Mebius1916/One-thing (github.com)

Mongoose随笔

Creating Model
import mongoose from "mongoose";
const moviesSchema = new mongoose.Schema({name:{type:String,required:true,trim:true},ratings:{type:Number,required:true,min:1,max:5},money:{// @ts-ignoretype:mongoose.Decimal128,required:true,validate:v => v>=10,},genre:{type:Array,},isActive:{type:Boolean,default:true},comments:[{value:{type:String},published:{type:Date,default:Date.now}}],
})
const movieModel = mongoose.model('movies',moviesSchema);
insert

.save()

const result = await MovieModel.insertMany([]);

find

const result = await MovieModel.find();//all data

const result = await MovieModel.find({});

const result = await MovieModel.findById();

update

const result = await MovieModel.updateOne({});

const result = await MovieModel.updateMany({});

const result = await MovieModel.findByIdAndUpdate({});

delete

const result = await MovieModel.deleteOne();

const result = await MovieModel.deleteMany();

const result = await MovieModel.findByIdAndDelete();

ObjectId

type: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User'}]

通常与populate结合使用:

      .populate({path: "members",model: User,})

根据_id匹配对应的User模型对象

正则搜索

$ regex操作符用于执行正则表达式搜索,query变量中的用户输入被用作搜索模式。$ options: "i"表示搜索应不区分大小写。

    const searchedContacts = await User.find({$or: [{ username: { $regex: query, $options: "i" } },{ email: { $regex: query, $options: "i" } }]})
在NextAuth-advanced项目中的使用

作为Vuer的我如何快速通关React生态

NextJS随笔

use server/client

Next.js中的客户端渲染和服务端渲染-CSDN博客

  • 如有体积较大的依赖引入,可以选择"use server",这样就不用在客户端进行渲染,提高客户端性能。
  • 存在react hook的页面必须实用"use client"。
Image组件

nextjs中有自己的Image组件,实用性远强于普通的img的图片,其中功能包括但不限于以下几点:

  • 自动优化: 支持导入’.webp’图片,并且它会即时压缩优化图像,减少图像大小而不会显著损失质量,如果浏览器不支持压缩后的格式还会自动回退。
  • 懒加载: 图像默认采用懒加载方式。这意味着图像会在滚动到视口时才加载,有助于加快页面的初始加载速度。
  • SrcSet 支持: 它会自动生成每个图像的多个版本,适配不同的屏幕分辨率和设备,并通过srcset 属性提供适当的版本。
  • 占位符支持: 你可以使用placeholder 属性指定占位符的处理方式。例如,设置 placeholder="blur" 会在图像加载时提供一个模糊的版本,改善感知性能。
    布局选项:layout 属性控制图像的调整行为。常见的值包括 fillfixedintrinsicresponsive raw

基本使用:

import Image from 'next/image' 
export default function Page() {return (<Imagesrc="/profile.png"width={500}height={500}alt="Picture of the author"/>)
}
  • 结合 widthheight 属性,用于确定图像的纵横比,浏览器使用该纵横比在加载图像之前为图像预留空间。
  • 固有大小并不总是意味着浏览器中呈现的大小,这将由父容器确定。例如,如果父容器小于固有大小,则图像将按比例缩小以适合容器。
  • 当宽度和高度未知时,可以使用 fill 属性。
动态路由
  1. [folderName]

    folderName仅为一个名字,在url中携带的[folderName]部分会作为值进行传递。

    举个例子,假设路径为:/app/[id]/page.tsx,url为:http://localhost:3000/14,那么此时id=14就作为参数传入/app/[id]/page.tsx

  2. […folderName]

    在命名文件夹的时候,如果你在方括号内添加省略号,比如 [...folderName],这表示捕获所有后面所有的路由片段。

    也就是说,app/shop/[...slug]/page.js会匹配 /shop/clothes,也会匹配 /shop/clothes/tops/shop/clothes/tops/t-shirts等等。

    举个例子,app/shop/[...slug]/page.js的代码如下:

    // app/shop/[...slug]/page.js
    export default function Page({ params }) {return <div>My Shop: {JSON.stringify(params)}</div>
    }

路由组
  1. 按逻辑分组

    将路由按逻辑分组,但不影响 URL 路径:

    你会发现,最终的 URL 中省略了带括号的文件夹(上图中的(marketing)(shop))。

  2. 创建不同布局

    借助路由组,即便在同一层级,也可以创建不同的布局:

    在这个例子中,/account/cart/checkout 都在同一层级。但是 /account/cart使用的是 /app/(shop)/layout.js布局和app/layout.js布局,/checkout使用的是 app/layout.js

  3. 创建多个根布局

    创建多个根布局:

    创建多个根布局,你需要删除掉 app/layout.js 文件,然后在每组都创建一个 layout.js文件。创建的时候要注意,因为是根布局,所以要有 <html><body> 标签。

    这个功能很实用,比如你将前台购买页面和后台管理页面都放在一个项目里,一个 C 端,一个 B 端,两个项目的布局肯定不一样,借助路由组,就可以轻松实现区分。

AuthJS随笔

大体鉴权流程

1. 用户登录请求

  • 用户输入凭据: 用户通过客户端(如浏览器或移动应用)提交登录请求,输入凭据(如用户名和密码,或通过 OAuth 提供的访问令牌)。
  • 请求发送到服务器: 客户端将用户凭据发送到服务器端的 Auth.js 认证端点(API)。

2. 凭据验证

  • 验证用户凭据: 服务器使用 Auth.js 验证用户凭据是否正确。如果使用的是 OAuth 等第三方认证方式,Auth.js 会与对应的服务(如 Google、Facebook)进行通信来验证令牌。
  • 验证通过: 如果用户凭据正确,Auth.js 生成认证令牌(如 JWT)或创建一个会话来标识用户身份。
  • 验证失败: 如果凭据错误或无效,Auth.js 返回相应的错误信息(如 401 未授权)。

3. 生成和返回认证令牌

  • 创建会话或令牌: 一旦验证成功,Auth.js 会创建一个会话或生成一个认证令牌(例如 JWT),其中包含用户的身份信息和可能的权限信息。
  • 发送令牌给客户端: 服务器将会话 ID 或令牌返回给客户端。令牌通常包含在响应的 Authorization 头中,或者作为一个持久化的 cookie。

4. 客户端存储令牌

  • 存储方式: 客户端将接收到的令牌存储在安全的地方,如浏览器的 localStoragesessionStorage 或作为一个 HttpOnly 的 cookie,避免 XSS 攻击风险。
  • 自动附加令牌: 客户端在后续的每个请求中,将自动在请求头中附加令牌,以证明用户的身份。

5. 访问受保护资源

  • 发送带令牌的请求: 用户请求受保护的资源时,客户端将存储的令牌附加到请求头中,并发送到服务器。
  • 服务器验证令牌: Auth.js 在服务器端接收到请求后,解析并验证令牌的有效性(例如签名是否正确、令牌是否过期、用户是否有权限访问请求的资源)。

6. 权限验证

  • 基于角色或权限验证: 如果令牌验证通过,Auth.js 进一步检查用户是否具备访问请求资源的权限。通常,Auth.js 会基于用户的角色或自定义权限策略来进行验证。
  • 授权通过或拒绝: 如果用户有权限访问,服务器返回请求的资源。如果没有权限,服务器返回 403 Forbidden

7. 会话管理和令牌刷新

  • 会话保持或刷新: 如果 Auth.js 使用了会话机制,会定期刷新会话保持活跃。如果使用的是短期有效的 JWT,Auth.js 可能会提供刷新令牌的机制,允许客户端在 JWT 过期前获取一个新的令牌,而不需要用户重新登录。
  • 处理会话过期: 如果会话或令牌过期,Auth.js 会要求用户重新登录以获取新的认证信息。

8. 用户注销

  • 注销请求: 当用户选择注销时,客户端会发送注销请求到服务器。
  • 销毁会话或令牌: Auth.js 接收到注销请求后,销毁服务器端的会话或标记令牌为无效。客户端同时清除本地存储的令牌。
  • 确认注销: 服务器向客户端确认注销成功,用户被重定向到登录页面或首页。
JWT

NextAuth 默认使用 JWT 来管理用户的会话,它通过内置的机制自动生成和处理 JWT。

生成过程:

  1. 用户登录: 当用户通过任何一种认证提供者(如 GitHub、Google 或自定义的凭证登录)成功登录时,NextAuth 会生成一个包含用户信息的 JWT。
  2. JWT 的创建:NextAuth 中,JWT 的创建和签名是自动完成的。NextAuth 使用内部的 jsonwebtoken 库来生成 JWT。每当用户成功登录时,NextAuth 会创建一个新的 JWT,并将一些基础信息(如用户 ID、邮箱等)存储在这个 JWT 中。
  3. JWT 回调函数: 回调函数在 JWT 生成或更新时被调用。这里,你可以在 JWT 中添加自定义的字段,如用户角色 role。当用户登录成功后,user 对象会传递到这个回调中,你可以将用户角色附加到 token 上。
  4. JWT 的签名和存储: NextAuth 会使用在配置中设置的 NEXTAUTH_SECRET 环境变量对 JWT 进行签名。这个密钥用于确保 JWT 的安全性,防止未授权的修改。生成的 JWT 被发送到客户端并保存在客户端的 cookie 中。
  5. JWT 的验证: 每次用户发送请求时,这个 JWT 会被自动附加到请求中,服务器会验证这个 JWT。如果验证成功,用户会话就会被恢复。如果 JWT 无效或过期,用户可能需要重新登录。
// 创建 NextAuth 配置对象
export const { handlers, signIn, signOut, auth } = NextAuth({// 配置使用的认证提供者列表providers: [// 配置 Github 认证提供者,使用环境变量中的客户端 ID 和客户端密钥Github({clientId: process.env.GITHUB_CLIENT_ID,clientSecret: process.env.GITHUB_CLIENT_SECRET,}),// 配置 Google 认证提供者,使用环境变量中的客户端 ID 和客户端密钥Google({clientId: process.env.GOOGLE_CLIENT_ID,clientSecret: process.env.GOOGLE_CLIENT_SECRET,}),// 凭证Credentials({name: "Credentials",// 定义登录表单的字段credentials: {email: { label: "Email", type: "email" },password: { label: "Password", type: "password" },},// 通过凭证信息来授权authorize: async (credentials) => {// 提取用户提供的邮箱和密码const email = credentials.email as string | undefined;const password = credentials.password as string | undefined;// 如果邮箱或密码为空,抛出错误if (!email ||!password) {throw new CredentialsSignin("Please provide both email & password");}// 连接数据库await connectDB();// 在数据库中查找具有给定邮箱的用户,并包含密码和角色字段const user = await User.findOne({ email }).select("+password +role");// 如果没有找到用户,抛出错误if (!user) {throw new Error("Invalid email or password");}// 如果用户存在但没有设置密码(可能使用了第三方登录),抛出错误if (!user.password) {throw new Error("Invalid email or password");}// 比较用户提供的密码和数据库中存储的密码哈希值是否匹配const isMatched = await compare(password, user.password);// 如果密码不匹配,抛出错误if (!isMatched) {throw new Error("Password did not matched");}// 密码匹配成功,返回用户数据用于构建会话const userData = {firstName: user.firstName,lastName: user.lastName,email: user.email,role: user.role,id: user._id,};return userData;},}),],// 配置登录页面的 URLpages: {signIn: "/login",},// 定义在验证过程中将会话和令牌进行处理的回调函数callbacks: {// 登录后更新缓存async session({ session, token }) {if (token?.sub && token?.role) {session.user.id = token.sub;//@ts-ignoresession.user.role = token.role;}return session;},// 更新 JWT 令牌对象async jwt({ token, user }) {if (user) {//@ts-ignoretoken.role = user.role;}return token;},// 处理登录成功后的回调函数signIn: async ({ user, account }) => {if (account?.provider === "google") {try {// 从登录用户信息中提取必要属性const { email, name, image, id } = user;// 连接数据库await connectDB();// 查询数据库中是否已经存在具有给定邮箱的用户const alreadyUser = await User.findOne({ email });// 如果不存在,则创建新用户if (!alreadyUser) {await User.create({ email, name, image, authProviderId: id });} else {// 如果用户已经存在,直接返回 true 表示登录成功return true;}} catch (error) {// 如果在处理过程中发生任何错误,抛出错误信息throw new Error("Error while creating user");}}// 如果是通过用户名和密码登录,则直接返回 true 表示登录成功if (account?.provider === "credentials") {return true;} else {// 其他情况返回 false,表示登录失败return false;}},},
});

以上面代码为例:

  • jwt** 回调函数**:这个回调函数在每次 JWT 生成或更新时调用。它接收一个 token 对象和一个 user 对象作为参数。
    • token:包含当前的 JWT 信息。
    • user:包含从 authorize 函数返回的用户信息(即用户的 role 等信息)。
  • authorize 函数中,当用户成功通过凭证(例如电子邮件和密码)验证后,会返回一个包含用户信息的 userData 对象。这些信息(如 role)会被存储在 JWT 中,通过 jwt 回调函数传递给客户端。

NextAuth-advanced项目随笔

目录讲解

在这里插入图片描述

为什么用cache存储session?
import { auth } from "@/auth";
import { cache } from "react";export const getSession = cache(async () => {const session = await auth();return session;
});

维护session,提高性能。

  • session调用频繁,使用cache能减少负载,提高响应速度。
  • cache读取速度快,性能好。
  • session易丢失,使用cache能有效缓存会话数据。
html表格标签
  • <tr>表示表格的一行。
  • <td>表示表格的数据单元格。
  • <th>表示表格的表头单元格。
  • <table>作为最外层的容器,包含<thead><tbody>
  • <thead>通常位于<table>的顶部,包含<tr>元素。<tr>中的<th>元素表示列标题。
  • <tbody>位于<thead>之后,包含<tr>元素。<tr>中的<td>元素表示行数据。

在这里插入图片描述

mongoose数据库使用

**.env**环境配置

MONGO_URI='mongodb://127.0.0.1:27017/nextAuth'
AUTH_SECRET=klsgjcsr6ku987123kjdvlksadfadf0243
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=

**/models/User.ts**导出User模型

import mongoose from "mongoose";const userSchema = new mongoose.Schema({firstName: { type: String, required: true },lastName: { type: String, required: true },email: { type: String, required: true },password: { type: String, select: false },role: { type: String, default: "user" },image: { type: String },authProviderId: { type: String },
});export const User = mongoose.models?.User || mongoose.model("User", userSchema);

/lib/db.ts抽离连接数据库函数⇒自定义hook

import mongoose from "mongoose";const connectDB = async () => {try {await mongoose.connect(process.env.MONGO_URI!);console.log(`Successfully connected to mongoDB 🥂`);} catch (error: any) {console.error(`Error: ${error.message}`);process.exit(1);}
};export default connectDB;

/action/user.ts使用数据库

"use server";
import connectDB from "@/lib/db";
import { User } from "@/models/User";
import { redirect } from "next/navigation";
import { hash } from "bcryptjs";
import { CredentialsSignin } from "next-auth";
import { signIn } from "@/auth";
const login = async (formData: FormData) => {const email = formData.get("email") as string;const password = formData.get("password") as string;try {await signIn("credentials", {redirect: false,callbackUrl: "/",email,password,});} catch (error) {const someError = error as CredentialsSignin;return someError.cause;}redirect("/");
};
const register = async (formData: FormData) => {const firstName = formData.get("firstname") as string;const lastName = formData.get("lastname") as string;const email = formData.get("email") as string;const password = formData.get("password") as string;if (!firstName || !lastName || !email || !password) {throw new Error("Please fill all fields");}await connectDB(); // existing userconst existingUser = await User.findOne({ email }); if (existingUser) throw new Error("User already exists");const hashedPassword = await hash(password, 12);await User.create({ firstName, lastName, email, password: hashedPassword }); console.log(`User created successfully 🥂`);redirect("/login");
};
const fetchAllUsers = async () => {await connectDB(); const users = await User.find({}); return users;
};
export { register, login, fetchAllUsers };

auth.ts在next-auth配置中使用数据库

// 创建 NextAuth 配置对象
export const { handlers, signIn, signOut, auth } = NextAuth({// 配置使用的认证提供者列表providers: [// 配置 Github 认证提供者,使用环境变量中的客户端 ID 和客户端密钥Github({clientId: process.env.GITHUB_CLIENT_ID,clientSecret: process.env.GITHUB_CLIENT_SECRET,}),// 配置 Google 认证提供者,使用环境变量中的客户端 ID 和客户端密钥Google({clientId: process.env.GOOGLE_CLIENT_ID,clientSecret: process.env.GOOGLE_CLIENT_SECRET,}),// 凭证Credentials({name: "Credentials",// 定义登录表单的字段credentials: {email: { label: "Email", type: "email" },password: { label: "Password", type: "password" },},// 通过凭证信息来授权authorize: async (credentials) => {// 提取用户提供的邮箱和密码const email = credentials.email as string | undefined;const password = credentials.password as string | undefined;// 如果邮箱或密码为空,抛出错误if (!email ||!password) {throw new CredentialsSignin("Please provide both email & password");}// 连接数据库await connectDB(); // 在数据库中查找具有给定邮箱的用户,并包含密码和角色字段const user = await User.findOne({ email }).select("+password +role"); // 如果没有找到用户,抛出错误if (!user) {throw new Error("Invalid email or password");}// 如果用户存在但没有设置密码(可能使用了第三方登录),抛出错误if (!user.password) {throw new Error("Invalid email or password");}// 比较用户提供的密码和数据库中存储的密码哈希值是否匹配const isMatched = await compare(password, user.password);// 如果密码不匹配,抛出错误if (!isMatched) {throw new Error("Password did not matched");}// 密码匹配成功,返回用户数据用于构建会话const userData = {firstName: user.firstName,lastName: user.lastName,email: user.email,role: user.role,id: user._id,};return userData;},}),],// 配置登录页面的 URLpages: {signIn: "/login",},// 定义在验证过程中将会话和令牌进行处理的回调函数callbacks: {// 登录后更新缓存async session({ session, token }) {if (token?.sub && token?.role) {session.user.id = token.sub;//@ts-ignoresession.user.role = token.role;}return session;},// 更新 JWT 令牌对象async jwt({ token, user }) {if (user) {//@ts-ignoretoken.role = user.role;}return token;},// 处理登录成功后的回调函数signIn: async ({ user, account }) => {if (account?.provider === "google") {try {// 从登录用户信息中提取必要属性const { email, name, image, id } = user;// 连接数据库await connectDB(); // 查询数据库中是否已经存在具有给定邮箱的用户const alreadyUser = await User.findOne({ email });// 如果不存在,则创建新用户if (!alreadyUser) {await User.create({ email, name, image, authProviderId: id }); } else {// 如果用户已经存在,直接返回 true 表示登录成功return true;}} catch (error) {// 如果在处理过程中发生任何错误,抛出错误信息throw new Error("Error while creating user");}}// 如果是通过用户名和密码登录,则直接返回 true 表示登录成功if (account?.provider === "credentials") {return true;} else {// 其他情况返回 false,表示登录失败return false;}},},
});

HaloChat实战项目重构随笔

重构部分
  • js→ts
  • Next-Auth v4→v5
  • img→Image
前言

我在学习项目的同时将项目重构为了ts版本,这是一次很不错的经历,因为我实际上并没有ts的实战经验,基本上都是跟着视频学习没有真正上手过,这次重构确实对我的ts代码能力有很大提升,ts版本代码地址:

关于这个项目我想吐槽的一点是,这个项目几乎全是客户端渲染,我觉得这实际上和Next-auth是冲突的,虽然Next-auth客户端和服务端都能用,但是Next-auth其实更偏向于服务端渲染,在重构我选择保留了原视频客户端渲染的登录与注册,但我重构后发现以下问题:

  • 如果用"use client":可以用react-hook-form,但是无法使用Next-auth的provider below也就是github、google等第三方登录。
  • 如果用"use server":可以用Next-auth的provider below但是无法使用react-hook-form。

权衡下来其实我更偏向于服务端渲染登录注册页面,所以我推荐看了本文章的同学们可以尝试用服务端渲染重构此项目并加入其它的provider below,具体参考上面的NextAuth-advanced就好啦,我重构本项目的时候参考了很多NextAuth-advanced里的写法,不过登录注册页面由于服务端、客户端渲染冲突,为了保住react-hook-form就不了了之,回看下来其实登录注册对表单的应用挺简单的,不用react-hook-form自己写表单都行。


上面是关于登录注册页面的吐槽,而这里我想吐槽一下这个教程视频的结构和逻辑很混乱,得自己去看源码结构细品,然后视频中用authjs v4版本中的中间件来进行路由保护,而我重构为了v5版本,我自己尝试用中间件来进行路由保护会有bug所以就没加路由保护,前面NextAuth-advanced项目中有用session进行路由保护,可以参考参考。

随笔
  1. cloudinary上传修改图片

              const uploadPhoto = (result:any) => {setValue("profileImage", result?.info?.secure_url);};<imgsrc={watch("profileImage") ||user?.profileImage ||"/assets/person.jpg"}alt="profile"className="w-40 h-40 rounded-full"/><CldUploadButtonoptions={{ maxFiles: 1 }}onUpload={uploadPhoto}uploadPreset="kdm7bzdm"><p className="text-body-bold">Upload new photo</p></CldUploadButton>
    
    • 用户通过 <CldUploadButton> 上传新的图片后,uploadPhoto 函数会被触发,它通过 setValue 更新 profileImage 的值为新的图片 URL。
    • 由于 watch("profileImage") 监听了 profileImage,一旦值更新,<img> 标签的 src 也会实时更新,显示新上传的图片。
    • 一开始没有"profileImage"字段,通过uploadPhoto中的setValue给表单添加上"profileImage"字段,然后触发watch("profileImage")
      ps:推荐用于img,不推荐用于Image。我用Image来加载cloudinary,即使设置了next.config.mjs也会报错,而且此处为外部加载资源所以Image也提供不了多少优化。
  2. 关于profile(上传图片)页面的**loading**时机

    • 首次渲染页面时loading为默认true及 展示loading组件(可能时间过短看不见),随后触发useEffect,等待session.user获取到后通过setLoading(false)loading设置为false及展示页面内容。
    • updateUser上传数据时,先将loading初始化为true,等待请求完成后通过setLoading(false)loading设置为false表示加载完成展示页面内容。
  3. **Contacts.tsx**页面 搜索好友 逻辑

    1. 在输入框输入内容时触发setSearch函数

      const [search, setSearch] = useState("");

            <inputplaceholder="Search contact..."className="input-search"value={search}onChange={(e) => setSearch(e.target.value)}/>
      
    2. setSearch函数改变search的值后通过触发useEffect触发getContacts改变contacts

       useEffect(() => {if (currentUser) getContacts();}, [currentUser, search]);const getContacts = async () => {try {//从数据库搜索出匹配项const res = await fetch(search !== "" ? `/api/users/searchContact/${search}` : "/api/users");const data = await res.json();  //在所有信息里剔除自己的信息setContacts(data.filter((contact:SessionData) => contact._id !== currentUser._id));setLoading(false);} catch (err) {console.log(err);}};
    3. 通过map函数动态渲染contacts组件

                {contacts.map((user:SessionData, index) => (<divkey={index}className="contact"onClick={() => handleSelect(user as never)}>{selectedContacts.find((item) => item === user) ? (<CheckCircle sx={{ color: "red" }} />) : (<RadioButtonUnchecked />)}<imgsrc={user.profileImage || "/assets/person.jpg"}alt="profile"className="profilePhoto"/><p className="text-base-bold">{user.username}</p></div>))}
      
  4. **Contacts.tsx**页面 选择成员 逻辑

    1. 主体部分

                  //map将所有用户拆分成每一个current个体{contacts.map((current:SessionData, index) => (//渲染除自己外所有用户<divkey={index}className="contact"onClick={() => handleSelect(current as never)}>//selectedContacts为选中元素的数组{selectedContacts.find((item) => item === current) ? (//如果当前元速在选中数组里<CheckCircle sx={{ color: "red" }} />) : (//如果当前元速不在选中数组里<RadioButtonUnchecked />)}<imgsrc={current.profileImage || "/assets/person.jpg"}alt="profile"className="profilePhoto"/><p className="text-base-bold">{current.username}</p></div>))}
      

      handleSelect

      传入的current为当前点击的用户,之后进行判断:如果选中用户数组selectedContacts里没有当前点击用户则触发else将当前点击用户加入进去;如果没有,则从选中用户数组selectedContacts中去除当前点击用户。

      const [selectedContacts, setSelectedContacts] = useState([]);

      const handleSelect = (current: never) => {//取消选中if (selectedContacts.includes(current)) {setSelectedContacts((prevSelectedContacts) =>prevSelectedContacts.filter((item) => item !== current));} else {//选中setSelectedContacts((Contacts) => [...Contacts,//之前所选current,//当前所选]);}};
  5. **Contacts.tsx页面 群聊取名与成员标签 **​逻辑

    const [name, setName] = useState("");

          {isGroup && (<><div className="flex flex-col gap-3"><p className="text-body-bold">Group Chat Name</p><inputplaceholder="Enter group chat name..."className="input-group-name"value={name}//实时修改nameonChange={(e) => setName(e.target.value)}/></div><div className="flex flex-col gap-3"><p className="text-body-bold">Members</p><div className="flex flex-wrap gap-3">//将选中数组 selectedContacts渲染出来,键值为index {selectedContacts.map((contact:SessionData, index) => (<p className="selected-contact" key={index}>{contact.username}</p>))}</div></div></>)}
    
  6. **Contacts.tsx页面 创建群聊 **逻辑

    1. 前端逻辑
              const createChat = async () => {const res = await fetch("/api/chats", {method: "POST",body: JSON.stringify({//当前用户_idcurrentUserId: currentUser._id,//选中成员的_id数组members: selectedContacts.map((contact:SessionData) => contact._id),//是否是群组isGroup,//群组名称name,}),});const chat = await res.json();if (res.ok) {router.push(`/chats/${chat._id}`);}};<buttonclassName="btn"onClick={createChat}disabled={selectedContacts.length === 0}>
      
    2. 后端逻辑
      import { User } from "@/models/User";
      import { connectToDB } from "@/mongodb";
      import { pusherServer } from "@/lib/pusher";
      import { NextRequest, NextResponse } from 'next/server';
      import Chat from "@/models/Chat";
      import { ObjectId } from "mongoose";
      export const POST = async (req:NextRequest) => {try {await connectToDB();const body = await req.json();const { currentUserId, members, isGroup, name, groupPhoto } = body;//构建查询对象const query = isGroup//群组? { isGroup, name, groupPhoto, members: [currentUserId,...members] }//个人: { members: { $all: [currentUserId,...members], $size: 2 } };let chat = await Chat.findOne(query);//如果不存在则创建新群聊if (!chat) {chat = await new Chat(isGroup? query : { members: [currentUserId,...members] });await chat.save();//chat为新建的群组||个人,members为成员_id//用当前chat中成员_id进行查找,给每个成员(User对象)的chats中添加上当前群聊的_id const updateAllMembers = chat.members.map(async (currentId) => {await User.findByIdAndUpdate(currentId,{$addToSet: { chats: chat._id },},{ new: true });});// 并发地执行所有更新操作Promise.all(updateAllMembers);// 为每个成员触发一个实时事件推送,通知他们有新的聊天记录chat.members.map(async (member:{_id:ObjectId}) => {await pusherServer.trigger(member._id.toString(), "new-chat", chat);});}return new Response(JSON.stringify(chat), { status: 200 });} catch (err) {console.error(err);return new Response("Failed to create a new chat", { status: 500 });}
      };
  7. Contact.tsx页面 整体 逻辑

    在这里插入图片描述

  8. 数据库模型详解

    User模型就不说了,说一下ChatMessage模型

    首先是Chat模型:

    • members:群成员_id集合数组,通过populate_id替换为User个体。
    • messages:群消息_id集合数组,通过populate_id替换为Message个体。
      然后是Message模型:
    • chat:该消息所属群组的_id
    • sender:该消息的发送人。
    • text:消息本体。
    • seenBy:该消息订阅者的_id数组(包括自己),在项目中好像只用于判断该消息是否是当前用户发送的,那用sender不就行了?为什么还要整一个seenBy?个人猜测是一种规范,在更为复杂的项目中可能会用到。

这个项目其实还有很多地方可以探究,但是我打算通过一个基于nextron的实时通讯实战来巩固所学知识点(我学习这个项目就是为了做一个自己的ChatApp),就不浪费时间去写随笔了。

这篇关于NextJs-react开发者的全栈最佳选择(从0-1的react全栈入门指南)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1141090

相关文章

Vue3 的 shallowRef 和 shallowReactive:优化性能

大家对 Vue3 的 ref 和 reactive 都很熟悉,那么对 shallowRef 和 shallowReactive 是否了解呢? 在编程和数据结构中,“shallow”(浅层)通常指对数据结构的最外层进行操作,而不递归地处理其内部或嵌套的数据。这种处理方式关注的是数据结构的第一层属性或元素,而忽略更深层次的嵌套内容。 1. 浅层与深层的对比 1.1 浅层(Shallow) 定义

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

这15个Vue指令,让你的项目开发爽到爆

1. V-Hotkey 仓库地址: github.com/Dafrok/v-ho… Demo: 戳这里 https://dafrok.github.io/v-hotkey 安装: npm install --save v-hotkey 这个指令可以给组件绑定一个或多个快捷键。你想要通过按下 Escape 键后隐藏某个组件,按住 Control 和回车键再显示它吗?小菜一碟: <template

如何选择适合孤独症兄妹的学校?

在探索适合孤独症儿童教育的道路上,每一位家长都面临着前所未有的挑战与抉择。当这份责任落在拥有孤独症兄妹的家庭肩上时,选择一所能够同时满足两个孩子特殊需求的学校,更显得尤为关键。本文将探讨如何为这样的家庭做出明智的选择,并介绍星贝育园自闭症儿童寄宿制学校作为一个值得考虑的选项。 理解孤独症儿童的独特性 孤独症,这一复杂的神经发育障碍,影响着儿童的社交互动、沟通能力以及行为模式。对于拥有孤独症兄

【 html+css 绚丽Loading 】000046 三才归元阵

前言:哈喽,大家好,今天给大家分享html+css 绚丽Loading!并提供具体代码帮助大家深入理解,彻底掌握!创作不易,如果能帮助到大家或者给大家一些灵感和启发,欢迎收藏+关注哦 💕 目录 📚一、效果📚二、信息💡1.简介:💡2.外观描述:💡3.使用方式:💡4.战斗方式:💡5.提升:💡6.传说: 📚三、源代码,上代码,可以直接复制使用🎥效果🗂️目录✍️

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label标签名称为:lblRedCount、lblBlueCount

Retrieval-based-Voice-Conversion-WebUI模型构建指南

一、模型介绍 Retrieval-based-Voice-Conversion-WebUI(简称 RVC)模型是一个基于 VITS(Variational Inference with adversarial learning for end-to-end Text-to-Speech)的简单易用的语音转换框架。 具有以下特点 简单易用:RVC 模型通过简单易用的网页界面,使得用户无需深入了

透彻!驯服大型语言模型(LLMs)的五种方法,及具体方法选择思路

引言 随着时间的发展,大型语言模型不再停留在演示阶段而是逐步面向生产系统的应用,随着人们期望的不断增加,目标也发生了巨大的变化。在短短的几个月的时间里,人们对大模型的认识已经从对其zero-shot能力感到惊讶,转变为考虑改进模型质量、提高模型可用性。 「大语言模型(LLMs)其实就是利用高容量的模型架构(例如Transformer)对海量的、多种多样的数据分布进行建模得到,它包含了大量的先验

数论入门整理(updating)

一、gcd lcm 基础中的基础,一般用来处理计算第一步什么的,分数化简之类。 LL gcd(LL a, LL b) { return b ? gcd(b, a % b) : a; } <pre name="code" class="cpp">LL lcm(LL a, LL b){LL c = gcd(a, b);return a / c * b;} 例题: