Next.js|Next.js 集成状态管理器和共享Token
前言
最近项目中使用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项目,创建完之后,我们执行下面几个步骤来一步步实现集成。
- 创建store/axios.js文件
- 修改pages/_app.js文件
- 创建store/index.js文件
- 创建store/slice/auth.js文件
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。【Next.js|Next.js 集成状态管理器和共享Token】pages/_app.js
import {Provider} from 'react-redux'
import {store, wrapper} from '@/store'const MyApp = ({Component, pageProps}) => {
return
}export default wrapper.withRedux(MyApp)
2、创建store/index.js文件
- 使用
@reduxjs/toolkit
集成reducer并创建store, - 使用
next-redux-wrapper
连接next.js和redux, - 使用
next-redux-cookie-wrapper
注册要共享到cookie的slice信息。
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});
3. 创建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 = https://www.it610.com/article/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 ? (
<>>
) : ({error.show ?
: null}
密码
忘记密码
首次使用Seaurl?{" "}
创建一个账号
{/*
创建一个账号 */} );
}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-wrapper
和next-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
推荐阅读
- Activiti(一)SpringBoot2集成Activiti6
- 私有化轻量级持续集成部署方案--03-部署web服务(下)
- 停下“忙乱”的状态
- Java基础-高级特性-枚举实现状态机
- 老年状态
- Android超简单实现沉浸式状态栏
- 生活最好的状态
- Spring集成|Spring集成 Mina
- 2019-09-08
- VueX(Vuex|VueX(Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式)