前言

自从next.js14发布之后,app router变成了官网主推的架构区别于pages router的传统架构,app router更适合最新的react,于是自己动手把next-authredux-toolkitant-designtailwindcss也一同集成进来,分享给大家,如果有错误之处欢迎大家指正。

操作

1、创建项目

使用下面命令创建项目,并且选择tailwind cssapp router模式

➜ npx create-next-app@latest
Need to install the following packages:
create-next-app@14.1.4
Ok to proceed? (y) y
✔ What is your project named? … next-auth-redux
✔ 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
✔ What import alias would you like configured? … @/*
Creating a new Next.js app in /Users/justinzhang/WorkSpace/com.seaurl/web/next-auth-redux.

2、引入依赖包

添加下面的依赖包:

 "dependencies": {
    "@ant-design/nextjs-registry": "^1.0.0",
    "@ant-design/icons": "^5.3.6",
    "@reduxjs/toolkit": "^2.2.3",
    "antd": "^5.16.0",
    "install": "^0.13.0",
    "next": "14.1.4",
    "next-auth": "5.0.0-beta.16",
    "react": "^18",
    "react-dom": "^18",
    "react-redux": "^9.1.0",
    "redux-logger": "^3.0.6"
  },
  "devDependencies": {
    "autoprefixer": "^10.0.1",
    "postcss": "^8",
    "tailwindcss": "^3.3.0"
  }

3、配置ant-design
首先,我们来创建src/context/antdProvider.js文件,如下所示:

"use client";

import {AntdRegistry} from '@ant-design/nextjs-registry';
import {ConfigProvider, theme} from 'antd'

export default function AntdProvider({children}) {
    return <AntdRegistry>
        <ConfigProvider>
            {children}
        </ConfigProvider>
    </AntdRegistry>
}

我们在src/app/layout.js中配置ant-design如下所示:

import AntdProvider from "@/context/antdProvider";

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
          <AntdProvider>
              <div>
                  {children}
              </div>
          </AntdProvider>
      </body>
    </html>
  );
}

这样就完成了ant-design的配置,我们就可以在页面中安心的使用它提供的组件了。

4、配置next-auth

同样的,我们创建文件src/app/(auth)/auth.js,代码如下所示:

import CredentialsProvider from "next-auth/providers/credentials"
import NextAuth from "next-auth"


export const authOptions = {
    providers: [
        CredentialsProvider({
            // The name to display on the sign in form (e.g. 'Sign in with...')
            name: 'Credentials',
            // The credentials is used to generate a suitable form on the sign in page.
            // You can specify whatever fields you are expecting to be submitted.
            // e.g. domain, username, password, 2FA token, etc.
            // You can pass any HTML attribute to the <input> tag through the object.
            credentials: {
                userName: {label: "用户名", type: "text", placeholder: "用户名"},
                password: {label: "密码", type: "password"}
            },
            async authorize(credentials, req) {
                const user = {id: "42", name: "justin", password: "123456", role: "manager"}

                if (credentials?.userName === user.name && credentials?.password === user.password) {
                    return user
                } else {
                    return null
                }
            }
        })
    ]

}

export const {
    handlers: { GET, POST },
    auth,
} = NextAuth(authOptions)

在这个文件里面我们使用CredentialsProvider来自定义认证,然后在authorize中验证用户,并且返回用户信息,这里我是固定的写法,大家可能使用fetch查询并验证,如下所示:

const res = await fetch(`${process.env.NEXT_PUBLIC_API_HOST}/users/web/login`, {
    method: 'POST',
    body: JSON.stringify(credentials),
    headers: { "Content-Type": "application/json" }
})
const resData = await res.json()

// If no error and we have user data, return it
if (res.ok && resData && resData.status === 0) {
    return resData.data
}
// Return null if user data could not be retrieved
return null

接着,我们可以再创建src/context/authProvider.js文件,代码如下所示:

'use client'

import { SessionProvider } from 'next-auth/react'

export default function AuthProvider({ children }) {
    return (
        <SessionProvider>
            {children}
        </SessionProvider>
    )
}

接着,我们在src/app/layout.js中配置一下,如下所示:

import AntdProvider from "@/context/antdProvider";
import AuthProvider from "@/context/authProvider";
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
          <AuthProvider>
              <AntdProvider>
                  <div>
                      {children}
                  </div>
              </AntdProvider>
          </AuthProvider>
      </body>
    </html>
  )
}

这样我们就完成了配置next-auth,下面我们来看看server componentclient component组件如何调用,如下所示:

服务端页面:src/app/page.js

import {auth} from "@/app/(auth)/auth";

export default async function Home() {
    const session = await auth()

    return (
        <main className="p-12">
            <div>Access Token: {session.user.name}</div>
        </main>
    );
}

客户端页面:src/app/components/auth/login-btn.js

'use client'
import { useSession, signIn, signOut } from "next-auth/react"
import {Button} from 'antd'
import { useRouter } from 'next/navigation'

export default function Component() {
    const router = useRouter()
    const { data: session } = useSession()
    if (session) {
        return (
            <div>
                <div className={'flex flex-row justify-between px-6 py-6'}>
                    <div className={'flex gap-4 '}>
                        <Button onClick={() => goto('/news')}>跳转到news页面</Button>
                        <Button onClick={() => goto('/')}>跳转到首页面</Button>

                    </div>
                    <div>
                        <Button onClick={() => signOut()}>退出</Button>
                    </div>
                </div>
            </div>
        )
    }

    function goto(val) {
        router.push(val)
    }

    return (
        <div className={'flex justify-between px-6 py-6'}>
            <Button onClick={() => signIn()}>登录</Button>
        </div>
    )
}

注意它们获取session的两种方式:

服务端:

import {auth} from "@/app/(auth)/auth";

const session = await auth()

客户端:

import { useSession, signIn, signOut } from "next-auth/react"

const { data: session } = useSession()

5、配置redux-toolkit

首先,我们创建一个新文件src/context/storeProvider.js,如下所示:

'use client'
import { useRef } from 'react'
import { Provider } from 'react-redux'
import { makeStore } from '@/lib/store'

export default function StoreProvider({ children }) {
    const storeRef = useRef()
    if (!storeRef.current) {
        // Create the store instance the first time this renders
        storeRef.current = makeStore()
    }

    return <Provider store={storeRef.current}>{children}</Provider>
}

再创建src/lib/store.jssrc/lib/hooks.jssrc/lib/slices/homeSlice.js文件,如下所示:

src/lib/store.js

// "use client";
import logger from "redux-logger";

import {configureStore, combineReducers} from '@reduxjs/toolkit'
import {homeSlice} from "@/lib/slices/homeSlice";

const rootReducer = combineReducers({
    [homeSlice.name]: homeSlice.reducer
})

export const makeStore = () => {
    return configureStore({
        reducer: rootReducer,
        devTools: false,
        // middleware: new MiddlewareArray().concat(logger),
        middleware: (getDefaultMiddleware) => getDefaultMiddleware({
            serializableCheck: false, // 禁用序列化检查
        }).concat(logger)
    })
}

src/lib/hooks.js

import { useDispatch, useSelector, useStore } from 'react-redux'

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = useDispatch.withTypes()
export const useAppSelector = useSelector.withTypes()
export const useAppStore = useStore.withTypes()

src/lib/slices/homeSlice.js

import { createSlice } from "@reduxjs/toolkit";
const initialState = {
    value: 0,
};

export const homeSlice = createSlice({
    name: "home",
    initialState,
    reducers: {
        increment: (state) => {
            state.value += 1;
            console.log('increment state value = ', state.value)
        },
        decrement: (state) => {
            state.value -= 1;
            console.log('decrement state value = ', state.value)
        },
        incrementByAmount: (state, action) => {
            state.value += action.payload;
        },
    },
})

export const { increment, decrement, incrementByAmount } = homeSlice.actions;

最后我们重新调整layout.js文件

import { Inter } from "next/font/google";
import "./(global)/globals.css";
import Link from 'next/link'
import AntdProvider from "@/context/antdProvider";

import AuthProvider from "@/context/authProvider";
import StoreProvider from "@/context/storeProvider";
import {Button} from "antd";
const inter = Inter({ subsets: ["latin"] });
import LoginBtn from '@/components/auth/login-btn'
export const metadata = {
  title: "Next.js & Next-Auth & Redux-Toolkit & Antd & Tailwindcss",
  description: "集成示例",
};

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body className={inter.className}>
          <StoreProvider>
              <AuthProvider>
                  <AntdProvider>
                      <div>
                          <LoginBtn />
                      </div>
                      <div>
                          {children}
                      </div>
                  </AntdProvider>
              </AuthProvider>
          </StoreProvider>
      </body>
    </html>
  );
}

这里我们配置好了redux store相关文件,然后我们在组件中使用,如下所示:

src/components/user/userDetail/index.js

'use client'

import {useAppDispatch, useAppSelector} from "@/lib/hooks";
import {useEffect} from "react";
import {increment} from "@/lib/slices/homeSlice";

export default function UserDetail() {
    const dispatch = useAppDispatch()
    const {value} = useAppSelector(state => state.home)

    useEffect(() => {
        console.log('useeffect value is = ', value)
        dispatch(increment())
    }, []);

    return (
        <div>
            user detail components
        </div>
    );
}

我们可以观察打印的结果,如果能够打印说明配置成功,这样我们就完成了全部配置,接下来,我们启动项目,然后测试登录退出看看能不能通过session获取用户信息,如下所示:

登录前
image.png

登录中
image.png

登录后
image.png

成功!

设置夜间模式

事实上设置夜间模式是每个网站的标配,这里我会使用tailwind cssantd教你如何设置。

1、设置antd组件的夜间模式
设置方法官网已经提供,大家可以看下,如下所示:

import {ConfigProvider, theme} from 'antd'
"use client";

import {AntdRegistry} from '@ant-design/nextjs-registry';
import {ConfigProvider, theme} from 'antd'
import zhCN from 'antd/locale/zh_CN';
import 'dayjs/locale/zh-cn';
import {useAppSelector} from "@/lib/hooks";
import {useEffect, useState} from "react";

export default function AntdProvider({children}) {

    const {myTheme, defaultTheme, darkTheme} = useAppSelector(state => state.system)
    useEffect(() => {
        if (myTheme === 'dark'){
            document.documentElement.classList.add('dark')
        }else{
            document.documentElement.classList.remove('dark')
        }

    }, [myTheme])
    return <AntdRegistry>
        <ConfigProvider locale={zhCN} theme={{
            token: {
                colorPrimary: myTheme === 'dark' ? darkTheme.primary : defaultTheme.primary
            },
            algorithm: myTheme === 'dark' ? theme.darkAlgorithm : theme.defaultAlgorithm
        }}>
            <div>
                {children}
            </div>
        </ConfigProvider>
    </AntdRegistry>
}

2、设置除了antd组件外的夜间模式(也就是页面)
设置页面的夜间模式我们使用tailwind css来实现,官网也有实例大家可以看下,如下所示:

tailwind.config.js

/** @type {import('tailwindcss').Config} */
module.exports = {
  darkMode: 'selector',
  content: [
    "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  plugins: [],
};

src/app/layout.js

import { Inter } from "next/font/google";
import "./globals.css";
import Link from 'next/link'
import AntdProvider from "@/context/antdProvider";
import AuthProvider from "@/context/authProvider";
import StoreProvider from "@/context/storeProvider";
const inter = Inter({ subsets: ["latin"] });
import LoginBtn from '@/components/auth/login-btn'
export const metadata = {
  title: "Next.js & Next-Auth & Redux-Toolkit & Antd & Tailwindcss",
  description: "集成示例",
};

export default function RootLayout({ children }) {
  return (
    <html>
      <body className="bg-white dark:bg-black dark:text-white">
          <StoreProvider>
              <AuthProvider>
                  <AntdProvider>
                      <div>
                          <LoginBtn/>
                      </div>
                      <div>
                          {children}
                      </div>
                  </AntdProvider>
              </AuthProvider>
          </StoreProvider>
      </body>
    </html>
  );
}

src/context/antdProvider.js

const {myTheme, defaultTheme, darkTheme} = useAppSelector(state => state.system)
useEffect(() => {
    if (myTheme === 'dark'){
        document.documentElement.classList.add('dark')
    }else{
        document.documentElement.classList.remove('dark')
    }

}, [myTheme])

注意看上面的body设置了<body class='bg-white dark:bg-black dark:text-white'>,所以你只要js触发设置html属性<html class='dark'>就可以完成黑色和白色主题的切换了,效果如下所示:
image.png

总结

1、完整代码
2、app routerpages router更好用,配置更少,尤其使用pages router使用next-redux-wrapper组件也不维护了,有问题也解决不了
3、app rouer的文件夹模式更出色,再加上server compoentclinet component的分离,可以让你更专注业务
4、app router好多新功能不知道怎么用可以参考next.js 官网文档
5、设置夜间模式时,只要在每个body上面添加class='dark:'前缀,然后在html标签上面添加<html class='dark'>就会有效果。

引用

Redux Toolkit Setup with Next.js
Upgrade Guide (v5)
next-auth-role-based
在 Next 14 的 appRouter 模式中接入 React-Redux
分享 7 个你可能不知道的 Next.js 14 小技巧


Awbeci
3.1k 声望213 粉丝

Awbeci