前言

最近我的项目中应用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};// reducerexport 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