乐趣区

关于next.js:Nextjs-集成状态管理器和Cookie

前言

最近我的项目中应用 SSR 框架 next.js,过程中会遇到 token 存储,状态治理等一系列问题,当初总结并记录下来分享给大家。

Token 存储

SSR 和 SPA 最大的区别就是 SSR 会辨别客户端 Client 和服务端 Server,并且 SSR 之间只能通过 cookie 能力在 Client 和 Server 之间通信,例如:token 信息,以往咱们在 SPA 我的项目中是应用 localStorage 或者 sessionStorage 来存储,然而在 SSR 我的项目中 Server 端是拿不到的,因为它是浏览器的属性,要想客户端和服务端同时都能拿到咱们能够应用 Cookie,所以 token 信息只能存储到 Cookie 中。

那么咱们选用什么插件来设置和读取 Cookie 信息呢?插件也有好多种,比方:cookie、js-cookie、react-cookie、nookie、set-cookie-parser 等等,然而它们有个最大的问题就是须要手动去管制读取和设置,有没有一种插件或者中间件主动获取和设置 token 呢?答案是必定的,就是接下来咱们要用到的 next-redux-cookie-wrapper 这个插件,它是 next-redux-wrapper 插件举荐的,而 next-redux-wrapper 插件是连贯 redux 中 store 数据的插件,接下来会讲到。

数据长久化

SSR 我的项目咱们不倡议数据做长久化,除了下面的 token 以及用户名等数据量小的数据须要长久化外,其它的都应该从后盾接口返回,否则就失去了应用 SSR 的目标 (间接从服务端返回带有数据的 html) 了,还不如去应用 SPA 来得间接。

状态治理

如果你的我的项目不是很大,且组件不是很多,你齐全不必思考状态治理,只有当组件数量很多且数据一直变动的状况下你须要思考状态治理。

咱们晓得 Next.js 也是基于 React,所以基于 React 的状态管理器同样实用于 Next.js,比拟风行的状态治理有:

  • mobx
  • redux
  • redux-toolkit(redux 的简化版)
  • recoil(react 官网出品)
  • rematch(模块化做得比拟好的)

这里有一篇文章专门介绍比照它们的,大家能够看看哪种比拟适宜本人。

最初咱们选用的是 redux 的轻量级版本:redux-toolkit

上面咱们会集成 redux-toolkit 插件及共享 cookie 插件 next-redux-cookie-wrapper 以及连贯 next.js 服务端与 redux store 数据通信办法 getServerSideProps 的插件next-redux-wrapper

集成状态管理器 Redux 及共享 Token 信息

首先咱们先创立 next.js 我的项目,创立完之后,咱们执行上面几个步骤来一步步实现集成。

  1. 创立 store/axios.js 文件
  2. 批改 pages/_app.js 文件
  3. 创立 store/index.js 文件
  4. 创立 store/slice/auth.js 文件

0. 创立 store/axios.js 文件

创立 axios.js 文件目标是为了对立治理 axios,不便 slice 中 axios 的设置和获取。

store/axios.js

import axios from 'axios';
import createAuthRefreshInterceptor from 'axios-auth-refresh';
import * as cookie from 'cookie';
import * as setCookie from 'set-cookie-parser';
// Create axios instance.
const axiosInstance = axios.create({baseURL: `${process.env.NEXT_PUBLIC_API_HOST}`,
  withCredentials: false,
});
export default axiosInstance;

1、批改 pages/_app.js 文件

应用 next-redux-wrapper 插件将 redux store 数据注入到 next.js。

pages/_app.js

import {Provider} from 'react-redux'
import {store, wrapper} from '@/store'

const MyApp = ({Component, pageProps}) => {return <Component {...pageProps} />
}

export default wrapper.withRedux(MyApp)

2、创立 store/index.js 文件

  1. 应用 @reduxjs/toolkit 集成 reducer 并创立 store,
  2. 应用 next-redux-wrapper 连贯 next.js 和 redux,
  3. 应用 next-redux-cookie-wrapper 注册要共享到 cookie 的 slice 信息。

store/index.js

import {configureStore, combineReducers} from '@reduxjs/toolkit';
import {createWrapper} from 'next-redux-wrapper';
import {nextReduxCookieMiddleware, wrapMakeStore} from "next-redux-cookie-wrapper";
import {authSlice} from './slices/auth';
import logger from "redux-logger";

const combinedReducers = combineReducers({[authSlice.name]: authSlice.reducer
});
export const store = wrapMakeStore(() => configureStore({
  reducer: combinedReducers,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().prepend(
      nextReduxCookieMiddleware({
        // 在这里设置你想在客户端和服务器端共享的 cookie 数据,我设置了上面三个数据,大家按照本人的需要来设置就好
        subtrees: ["auth.accessToken", "auth.isLogin", "auth.me"],
      })
    ).concat(logger)
}));
const makeStore = () => store;
export const wrapper = createWrapper(store, {storeKey: 'key', debug: true});
  1. 创立 store/slice/auth.js 文件

创立 slice,通过 axios 调用后盾接口返回 token 和 user 信息并保留到 reducer 数据中,下面的 nextReduxCookieMiddleware 会主动设置和读取这里的 token 和 me 及 isLogin 信息。

store/slice/auth.js

import {createAsyncThunk, createSlice} from '@reduxjs/toolkit';
import axios from '../axios';
import qs from "qs";
import {HYDRATE} from 'next-redux-wrapper';

// 获取用户信息
export const fetchUser = createAsyncThunk('auth/me', async (_, thunkAPI) => {
  try {const response = await axios.get('/account/me');
    return response.data.name;
  } catch (error) {return thunkAPI.rejectWithValue({errorMsg: error.message});
  }
});

// 登录
export const login = createAsyncThunk('auth/login', async (credentials, thunkAPI) => {
  try {
  
    // 获取 token 信息
    const response = await axios.post('/auth/oauth/token', qs.stringify(credentials));
    const resdata = response.data;
    if (resdata.access_token) {
      // 获取用户信息
      const refetch = await axios.get('/account/me', {headers: {Authorization: `Bearer ${resdata.access_token}`},
      });
      
      return {
        accessToken: resdata.access_token,
        isLogin: true,
        me: {name: refetch.data.name}
      };
    } else {return thunkAPI.rejectWithValue({errorMsg: response.data.message});
    }

  } catch (error) {return thunkAPI.rejectWithValue({errorMsg: error.message});
  }
});

// 初始化数据
const internalInitialState = {
  accessToken: null,
  me: null,
  errorMsg: null,
  isLogin: false
};

// reducer
export const authSlice = createSlice({
  name: 'auth',
  initialState: internalInitialState,
  reducers: {updateAuth(state, action) {
      state.accessToken = action.payload.accessToken;
      state.me = action.payload.me;
    },
    reset: () => internalInitialState,},
  extraReducers: {
    // 水合,拿到服务器端的 reducer 注入到客户端的 reducer,达到数据对立的目标
    [HYDRATE]: (state, action) => {console.log('HYDRATE', state, action.payload);
      return Object.assign({}, state, {...action.payload.auth});
    },
    [login.fulfilled]: (state, action) => {
      state.accessToken = action.payload.accessToken;
      state.isLogin = action.payload.isLogin;
      state.me = action.payload.me;
    },
    [login.rejected]: (state, action) => {console.log('action=>', action)
      state = Object.assign(Object.assign({}, internalInitialState), {errorMsg: action.payload.errorMsg});
      console.log('state=>', state)
      // throw new Error(action.error.message);
    },
    [fetchUser.rejected]: (state, action) => {state = Object.assign(Object.assign({}, internalInitialState), {errorMsg: action.errorMsg});
    },
    [fetchUser.fulfilled]: (state, action) => {state.me = action.payload;}
  }
});

export const {updateAuth, reset} = authSlice.actions;

这样就实现了所有插件的集成,接着咱们运行网页,登录输出用户名明码,你会发现下面的数据都以明码的模式保留在 Cookie 中。

剩下代码

pages/login.js

import React, {useState, useEffect} from "react";
import {Form, Input, Button, Checkbox, message, Alert, Typography} from "antd";
import Record from "../../components/layout/record";
import styles from "./index.module.scss";
import {useRouter} from "next/router";
import {useSelector, useDispatch} from 'react-redux'
import {login} from '@/store/slices/auth';
import {wrapper} from '@/store'


const {Text, Link} = Typography;
const layout = {labelCol: {span: 24},
  wrapperCol: {span: 24}
};
const Login = props => {const dispatch = useDispatch();
  const router = useRouter();
  const [isLoding, setIsLoading] = useState(false);
  const [error, setError] = useState({
    show: false,
    content: ""
  });

  function closeError() {
    setError({
      show: false,
      content: ""
    });
  }

  const onFinish = async ({username, password}) => {if (!username) {
      setError({
        show: true,
        content: "请输出用户名"
      });
      return;
    }
    if (!password) {
      setError({
        show: true,
        content: "请输出明码"
      });
      return;
    }
    setIsLoading(true);
    let res = await dispatch(login({
      grant_type: "password",
      username,
      password
    }));
    if (res.payload.errorMsg) {message.warning(res.payload.errorMsg);
    } else {router.push("/");
    }
    setIsLoading(false);
  };

  function render() {
    return props.isLogin ? (<></>) : (<div className={styles.container}>
        <div className={styles.content}>
          <div className={styles.card}>
            <div className={styles.cardBody}>
              <div className={styles.error}>{error.show ?
                <Alert message={error.content} type="error" closable afterClose={closeError}/> : null}</div>
              <div className={styles.cardContent}>
                <Form
                  {...layout}
                  name="basic"
                  initialValues={{remember: true}}
                  layout="vertical"
                  onFinish={onFinish}
                  // onFinishFailed={onFinishFailed}
                >
                  <div className={styles.formlabel}>
                    <b> 用户名或邮箱 </b>
                  </div>
                  <Form.Item name="username">
                    <Input size="large"/>
                  </Form.Item>
                  <div className={styles.formlabel}>
                    <b> 明码 </b>
                    <Link href="/account/password_reset" target="_blank">
                      遗记明码
                    </Link>
                  </div>
                  <Form.Item name="password">
                    <Input.Password size="large"/>
                  </Form.Item>

                  <Form.Item>
                    <Button type="primary" htmlType="submit" block size="large" className="submit" loading={isLoding}>
                      {isLoding ? "正在登录..." : "登录"}
                    </Button>
                  </Form.Item>
                </Form>
                <div className={styles.newaccount}>
                  首次应用 Seaurl?{" "}
                  <Link href="/join?ref=register" target="_blank">
                    创立一个账号
                  </Link>
                  {/* <a className="login-form-forgot" href="" >
                                    创立一个账号 </a> */}
                </div>
              </div>
            </div>

            <div className={styles.recordWrapper}>
              <Record/>
            </div>
          </div>
        </div>
      </div>
    );
  }

  return render();};

export const getServerSideProps = wrapper.getServerSideProps(store => ({ctx}) => {const {isLogin, me} = store.getState().auth;
  if(isLogin){
    return {
      redirect: {
        destination: '/',
        permanent: false,
      },
    }
  }
  return {props: {}
  };
});

export default Login;

留神

1、应用了 next-redux-wrapper 肯定要加 HYDRATE,目标是同步服务端和客户端 reducer 数据,否则两个端数据不统一造成抵触

[HYDRATE]: (state, action) => {console.log('HYDRATE', state, action.payload);
      return Object.assign({}, state, {...action.payload.auth});
    },

2、留神 next-redux-wrappernext-redux-cookie-wrapper版本

"next-redux-cookie-wrapper": "^2.0.1",
"next-redux-wrapper": "^7.0.2",

总结

1、ssr 我的项目不要用长久化,而是间接从 server 端申请接口拿数据间接渲染,否则失去应用 SSR 的意义了,
2、Next.js 分为动态渲染和服务端渲染,其实 SSR 我的项目如果你的我的项目很小,或者都是静态数据能够思考间接应用客户端静态方法 getStaticProps 来渲染。

援用

redux-toolkit

next-redux-cookie-wrapper

next-redux-wrapper

nextjs-auth

Next.js DEMO next-with-redux-toolkit

退出移动版