此 React 项目使用 TypeScript 和 Hooks API,本文介绍配置 Redux 并结合本地存储设置 token
安装依赖
yarn add redux -S
yarn add react-redux -S
- redux 可以脱离 react 使用, react-redux 的作用主要是提供 <Provider> 标签包裹页面组件。
store 目录,以加减运算为例
src
├── store
│ ├── actions
│ │ └── counter.ts
│ ├── reducers
│ │ ├── counter.ts
│ │ └── index.ts
│ └── index.ts
./src/store/index.ts
import { createStore } from 'redux';
import allReducers from './reducers';
// 注册
const store = createStore(
allReducers,
// @ts-ignore
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() // 引入Redux调试工具
);
// 导出
export default store;
./src/store/actions/counter.ts
export interface action {
type: 'INCREMENT' | 'DECREMENT';
num?: number;
}
export const increment = (num: number = 1): action => ({
type: 'INCREMENT',
num
});
export const decrement = (num: number = 1): action => ({
type: 'DECREMENT',
num
});
./src/store/reducers/index.ts
import { combineReducers } from "redux";
import counter from "./counter";
// 整合
const allReducers = combineReducers({ counter });
export default allReducers;
./src/store/reducers/counter.ts
interface action {
type: "INCREMENT" | "DECREMENT";
num?: number;
}
const counter = (state = 0, action: action) => {
switch (action.type) {
case "INCREMENT":
return state + (action.num as number);
case "DECREMENT":
return state - (action.num as number);
default:
return state;
}
};
export default counter;
再看 ./src/app.tsx
import { FC } from 'react';
import { Provider } from 'react-redux';
import store from 'src/store';
...
const App: FC = () => {
return (
<Provider store={store}>
...
</Provider>
);
}
- 只列出和 react-redux、store 有关的代码。
- <Provider> 放在最外层,里面放路由组件。
用一个组件页面 CounterComponent 测试 store 中的 counter 模块。
import { FC } from 'react';
import { Button } from 'antd';
import { useDispatch, useSelector } from 'react-redux';
import { increment, decrement } from "src/store/actions/counter";
const CounterComponent: FC = () => {
const dispatch = useDispatch();
const num = useSelector(state => (state as any).counter);
return(
<>
<div className="text-blue-500">
{ num }
</div>
<Button type="default" onClick={() => dispatch(decrement())}>-1</Button>
<Button type="primary" onClick={() => dispatch(increment())}>+1</Button>
</>
)
}
export default CounterComponent;
- 注意 react-redux 提供的 useDispatch、useSelector 两个 Hooks 的使用。
...
return(
<>
<div className="text-blue-500">
{ num }
</div>
<Button type="default" onClick={() => dispatch({
type: 'DECREMENT',
num: 1
})}>-1</Button>
<Button type="primary" onClick={() => dispatch({
type: 'INCREMENT',
num: 1
})}>+1</Button>
</>
)
...
- dispatch 也可以像上面这样写,如此可以省略 src/store/actions/counter 相关方法的引入。
const num = useSelector(state => (state as any).counter);
- useSelector 可以访问并返回全部 store 中的子模块,这里只返回 counter 子模块。
可以参照上面例子写一个保存登录 login_token 的子模块,并结合 localStorage 根据登录状态控制页面跳转。
至于已经有 redux 为什么还要结合 localStorage ,这样的疑问,有两点原因:
- redux 在页面刷新后值会被初始化,无法实现数据持久化。但是 redux 的数据可以影响子路由页面响应式变化。
- localStorage 保存的数据不会被刷新等操作影响,可以持久化。但是 localStorage 不具备 redux 的响应式变化功能。
在 redux 中创建用户模块 user 里面保存 login_token。
注意: 这里的 login_token 是调登录接口返回的经过加密的 32 位字符串,不是 JWT 标准格式的 token
修改一下目录,增加 user 相关文件。
src
├── store
│ ├── actions
│ │ ├── counter.ts
│ │ └── user.ts
│ ├── reducers
│ │ ├── counter.ts
│ │ ├── index.ts
│ │ └── user.ts
│ └── index.ts
./src/store/actions/user
export interface action {
type: "SET_TOKEN" | "DELETE_TOKEN";
login_token?: string;
}
export const setToken = (login_token: string): action => ({
type: "SET_TOKEN",
login_token
});
export const deleteToken = (): action => ({
type: "DELETE_TOKEN"
});
./src/store/reducers/user
interface action {
type: "SET_TOKEN" | "DELETE_TOKEN";
token?: string;
}
const user = ( state='', action: action ) => {
switch (action.type) {
case "SET_TOKEN":
state = action.token as string;
localStorage.setItem('login_token', state);
break
case "DELETE_TOKEN":
localStorage.removeItem('login_token');
state = '';
break
default:
state = localStorage.getItem('login_token') || '';
break
}
return state;
};
export default user;
- 所有对 login_token 的设置、获取、删除都先对本地存储进行响应操作,然后返回值。
修改 ./src/store/reducers/index.ts
import { combineReducers } from "redux";
import counter from "./counter";
import user from "./user";
// 整合
const allReducers = combineReducers({ counter, user });
export default allReducers;
页面相关操作
登录:
import { useDispatch } from 'react-redux';
import { setToken } from "src/store/actions/user";
import { useHistory } from 'react-router-dom';
interface LoginEntity {
username: string;
password: string;
}
const Login = () => {
const dispatch = useDispatch();
const history = useHistory();
...
// 登陆按钮逻辑
const handleLogin = async (login:LoginEntity) => {
// 调用登陆Api,获取结果
let res = await doLogin({...login});
dispatch(setToken(res.data.login_token));
// 跳转到 home 页面
history.push('/home');
}
...
}
- 在验证登录信息后,调用登录接口,接口返回 login_token
- dispatch(setToken(res.data.login_token)) 方法存储到 redux 中并页面跳转。
登出的逻辑:
...
dispatch(deleteToken());
history.push('/login');
...
useDispatch 属于 Hooks API ,它只能被用在函数式组件中。如果要在一些配置文件如 API 接口的配置文件中使用,需要换一种写法。
...
import store from "src/store";
// axios实例拦截请求
axios.interceptors.request.use(
(config: AxiosRequestConfig) => {
...
Object.assign(config['post'], {
login_token: store.getState().user
});
...
return config;
},
(error:any) => {
return Promise.reject(error);
}
)
...
- 在调接口前拦截请求,在请求参数中添加 login_token
- 注意写法: store.getState() 后面接的是模块名