l

lynn1286

V1

2022/04/10阅读:83主题:默认主题

Nextjs 登录实现

文末有地址,可以结合文章观看,文中的接口均本地 mock 😁

之前写了一篇文章是 使用 next-auth 实现登录持久化,这几天想了下如何不使用第三方库自己实现一套登录逻辑。

思考

SSR 框架下的登录逻辑跟纯客户端渲染的项目不太一样, 前者需要考虑如何在两个端下都获取到登录凭证,而后者没有这个考虑。
不了解 SSR 框架的同学可能会有疑问, 为什么SSR下会不一样? 那是因为在 SSR 框架下 , 分服务端渲染和客户端渲染, 而服务端渲染的环境是处于 node 环境,客户端是处于 浏览器(window) 环境, 因为环境的不一样,我们在服务端下无法访问 window 对象, 也就意味着我们不能将 登录凭证存储到 local storage 中(在纯客户端渲染的情况下我们都这么干),所以在 SSR 下我们需要考虑这个问题,因为在项目中很可能需要在客户端下获取登录凭证,也可能会在服务端下获取登录凭证。

实现

1. createContext 创建一个全局对象

// lib/context.ts
import React from 'react'

export interface IMyContextProps {
	// 是否登录标记
  isLoggedIn: boolean
	// 用户信息
  user: {
    name: string
    email: string
  } | null
	// useState 创建的函数 , 用于设置用户信息
  setUser?: any
}

const MyContext = React.createContext<IMyContextProps>({
  isLoggedIn: false,
  user: null,
})

export default MyContext

2. 在 _app.tsx 中使用context provider

在 _app.tsx 中实现如下逻辑:

// pages/_app.tsx
import Head from 'next/head'
import Cookie from 'js-cookie'
import MyContext from '../lib/context'
import { useEffect, useState } from 'react'

import '@styles/globals.css'
import '@styles/antd.less'

import type { AppProps } from 'next/app'

function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
	// 1. 创建 state ,保存用户信息
  const [user, setUser] = useState<{ name: string; email: string } | null>(null)

  useEffect(() => {
		// 2. 从 cookie 中获取 登录凭证
    const jwt = Cookie.get('jwt')

    // 3. 如果有登录凭证, 发送请求给后端服务获取用户信息
    if (jwt) {
      fetch(`${process.env.NEXT_PUBLIC_API_URL}/me`, {
        headers: {
          Authorization: `Bearer ${jwt}`,
        },
      }).then(async (res) => {
        if (!res.ok) {
          Cookie.remove('jwt') // 如果请求user 失败, 清理 cookie ,这里可以根据请求的状态码来判断处理
          setUser(null)
        }
        const user = await res.json()
        setUser(user)
      })
    }
  }, [])

  return (
    <>
      <Head>
        <title>React-Nextjs-Template</title>
        <meta charSet="utf-8" />
        <meta
          name="viewport"
          content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"
        />
      </Head>
			// 4. 使用 provide ,将 prop 传入
      <MyContext.Provider
        value={{
          user: user,
          setUser,
					// 注意这里,isLoggedIn的值是根据 user 是否为空来决定的, 
					// 所以,我们要将 setUser 函数一并传入 context ,这样做是为了在其他组件中使用这个方法触发 isLoggedIn 的值改变
        	isLoggedIn: !!user, 
				}}
      >
        <Component {...pageProps} />
      </MyContext.Provider>
    </>
  )
}

export default MyApp

3. 登录页面

删除了一些代码,留下核心逻辑

// pages/auth/login.tsx
import Cookie from 'js-cookie'
import Head from 'next/head'
import React, { useState, useEffect, useContext } from 'react'
import MyContext from '@lib/context'
import { useRouter } from 'next/router'
import { login } from '@lib/auth'
import Link from 'next/link'
import { getUser } from '@lib/user' // 登录

export default function Login() {
	// Context 中获取数据
  const { isLoggedIn, setUser } = useContext(MyContext)
  const router = useRouter()
	
	
	// 登录
  const signIn = async () => {
    ...
    const reg: any = await login(email, password)
		// 获取到token
    if (reg.accessToken) {
			// 拿到token 获取用户信息
      const user = await getUser()
      if (user) setUser(user)
      router.push('/')
    } else {
      setErrors({ server: reg?.error?.message || 'Error from server' })
    }
  }

  useEffect(() => {
    if (isLoggedIn) {
      router.push('/home/dashboard')
    }
  }, [isLoggedIn])

  return (
    <div>
      ...
    </div>
  )
}

// lib/user.ts
import Cookie from 'js-cookie'
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'

export const getUser = async () => {
  const token = Cookie.get('jwt')

  try {
    let response = await fetch(`${API_URL}/me`, {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${token}`,
      },
    })

    response = await response.json()

    return response
  } catch (e) {
    return { error: 'An error occured' }
  }
}
// lib/auth.ts
import Cookie from 'js-cookie'

const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'

export interface IResponse extends Response {
  accessToken?: string
}

export const login = async (identifier: string, password: string) => {
  try {
    let response: IResponse = await fetch(`${API_URL}/auth/login`, {
      method: 'POST',
      body: JSON.stringify({ email: identifier, password }),
      headers: {
        'Content-Type': 'application/json',
      },
    })

    response = await response.json()

    if (response.accessToken) {
      Cookie.set('jwt', response.accessToken)
    }

    return response
  } catch (e) {
    return { error: '登录失败,请稍后重试' }
  }
}

以上三个步骤完成了登录的逻辑处理,包括刷新页面还是会拿到用户的信息。

pages 下中间件拦截

import type { NextFetchEvent, NextRequest } from 'next/server'
import { NextResponse } from 'next/server'

// NextResponse 在中间件中提供了四种方法。
// - redirect():redirect 方法将请求从一条路由重定向到另一条路由。
// - rewrite():重写你的退出响应。
// - next():next 方法将中间件链从一个中间件延续到另一个中间件。
// - json(): json 方法返回 JSON 响应或数据

export function middleware(req: NextRequest, ev: NextFetchEvent) {
	// 请求头中可以读取到 cookie 
  if (req.cookies) {
    return NextResponse.next()
  }
	// 如果没有cookie 则重定向回 login 页面
  return NextResponse.redirect('/login')
}

api 下中间件拦截

// This is an example of how to access a session from an API route

import type { NextApiRequest, NextApiResponse } from 'next'
import Cookie from 'js-cookie'

export default async (req: NextApiRequest, res: NextApiResponse) => {
	// 同样可以在请求头中读取 cookie 
  if (req.cookies) {
    res.send('401 forbidden')
  }

  res.status(200).send('请求成功')
}

总结

  1. 在 _app.tsx 中处理 每次刷新页面就去读取 token 请求 user 接口获取user信息存入 context ;
  2. 登录页面登录成功后拿到token调用user接口设置 context

如果文章能够帮到你,请给我点个 👍

github地址

分类:

前端

标签:

前端

作者介绍

l
lynn1286
V1