React 企业级实践指南(五)
十三、编写配置文件表单并将其同步到组件
之前,我们展示了如何保护应用的某些部分免受未经身份验证或授权的人的攻击。本章将开始编写一个概要文件表单,并将该概要文件同步到各个组件。
由于这一部分相当长,我们将把它分成三个章节系列。在第一部分中,我们将更多地关注使用 Formik 和 JWT 进行身份验证来创建配置文件表单、注册表单和登录表单。在这里,我们将学习如何将配置文件表单同步到应用中的各个组件。
在第二部分中,我们将更新仪表板导航,并同步侧边栏导航和顶部导航栏之间的数据。在 Redux、Formik 和 Yup 验证模式的帮助下,我们将为应用添加更多的功能。
在本章系列的最后一部分,我们将继续巩固我们对 Redux 的了解,因为我们构建了完成应用 UI 所需的少数剩余组件。
这里的总体目标是从应用的不同层创建几个组件,并使用 Redux 将数据从一个组件无缝地传递到另一个组件——例如,使用 Redux 将个人资料数据同步到导航栏、侧栏和顶部导航栏。
在我们继续之前,让我在本章系列的最后向您展示完成的 UI。
图 13-1 到 13-4 显示了我们应用的完整 UI。
图 13-1 是设置页面。
图 13-1
设置页面
图 13-2 是订阅页面。
图 13-2
订阅表
图 13-3 是通知页面。
图 13-3
通知页面
图 13-4 为安全页面。
图 13-4
章节系列末尾的安全页面
创建索赔类型
好了,我们开始吧。首先,让我们为索赔创建一个模型或类型。
打开 models 文件夹,新建一个名为 claims-type.ts 的文件,复制清单 13-1 所示的代码。
export type ClaimsType = {
readonly email: string;
readonly iat: number;
readonly exp: number;
readonly sub: string;
};
Listing 13-1Creating the ClaimsType
ClaimsType:包含read-only email, iat, exp, and sub。这里的形状是根据解码后的访问令牌或 JWT 的有效载荷设计的。
iat - (issued at claim) :标识 JWT 的发行时间。
exp - (expiration time claim) :设置过期时间,在该时间或之后不得接受访问令牌进行处理。
sub - (subject claim):标识访问令牌或 JWT 的主题。
仍然有很多保留的 JSON Web Token 声明,如果你想了解更多,可以访问类似 iana.org 或 https://tools.ietf.org/ 的网站。
接下来,让我们添加用户的形状。在 models 文件夹中,添加 user-type.ts 并复制代码,如清单 13-2 所示。
创建用户类型
export type Subscription = {
name: string;
price: number;
currency: string;
proposalsLeft: number;
templatesLeft: number;
invitesLeft: number;
adsLeft: number;
hasAnalytics: boolean;
hasEmailAlerts: boolean;
};
export type UserType = {
id: string;
email: string;
password: string;
country: string;
isPublic: boolean;
phone: string;
role: string;
state: string;
tier: string;
name: string;
avatar: string;
city: string;
canHire: boolean;
subscription?: Subscription;
};
Listing 13-2Creating the UserType
添加 API:用户和用户数据库
然后我们需要再次更新我们的端点。所以转到 axios.ts 并再添加两个端点,如清单 13-3 所示。
export const EndPoints = {
sales: 'sales',
products: 'products',
events: 'events',
login: 'login',
register: 'register',
users: 'users',
usersDb: 'users-db',
};
Listing 13-3Updating the Endpoints for Users and UsersDb
Users:用于编辑或更新用户密码。我们将允许用户更新或编辑他们的密码。
UsersDb:这是为了在我们的 models 文件夹中存储用户类型和订阅类型的详细信息。理想情况下,这应该放在一个单独的数据库中,就像在现实世界中,我们将用于用户身份验证的数据库与用户的配置文件或订阅分开一样。
创建 userDbService
让我们创建一个新的服务文件。转到 services 文件夹,添加一个名为 userDbService.ts 的新文件,如清单 13-4 所示。
import api, { EndPoints } from 'api/axios';
import { UserType } from 'models/user-type';
export async function getUserByIdFromDbAxios(id: string) {
return await api.get<UserType>(`${EndPoints.usersDb}/${id}`);
}
export async function putUserFromDbAxios(user: UserType) {
return await api.put<UserType>(`${EndPoints.usersDb}/${user.id}`, user);
}
Listing 13-4Creating UserDbService
userDbService有两个 HTTP 方法或函数。我们从模型中导入了UserType,我们将我们的第一个函数命名为尽可能具体和描述性的getUserByIdFromDbAxios,,返回类型是UserType.
另一个 axios 函数我们命名为putUserFromDbAxios,,它接受UserType,我们正在用UserType的预期响应进行更新。
更新授权服务
之后,我们将需要更新authService.打开authService .ts并添加以下函数,如清单 13-5 所示。
export type ChangePasswordModel = {
email: string;
password: string;
id: string;
};
export async function changePassWordAxios(
changePasswordModel: ChangePasswordModel,
) {
return await axios.put<void>(
`${EndPoints.users}/${changePasswordModel.id}`,
changePasswordModel,
);
}
Listing 13-5Adding changePasswordAxios in authService
changePasswordAxios是一个异步服务函数,它使用类型changePasswordModel来更改密码。我们使用 axios 发送一个 put 请求,用这个 id 更新特定用户的密码。
Redux 的另一种用法
接下来,我们将创建一个profileActionTypes,,我们将在这里使用 Redux。
这是 Redux 的另一种写法。在前面的章节中,我们把所有的东西都放在一个文件中的calendarSlice,中,但是这次我们将为动作和片做单独的文件。这样做的主要原因是关注点的分离和代码的可读性。
这是图 13-5 中文件夹结构的一个快照,我们计划将配置文件文件夹作为构建 React-Redux 应用的一种方式。
图 13-5
配置文件的文件夹结构
创建 profileactioinotys
在 features 文件夹中,我们创建一个新目录,并将其命名为 profile。
在配置文件文件夹中,添加一个文件并将其命名为profileActionTypes.ts:
features ➤ profile ➤ profileActionTypes.ts
在清单 13-6 中,我们正在创建 profileActionTypes。
import { UserType } from 'models/user-type';
export type ProfileStateType = {
readonly profile: UserType;
readonly loading: boolean;
readonly error: string;
};
export const profileNamespace = 'profile';
/* action types */
export const ProfileActionTypes = {
FETCH_AND_SAVE_PROFILE: `${profileNamespace}/FETCH_AND_SAVE_PROFILE`,
UPDATE_PROFILE: `${profileNamespace}/UPDATE_PROFILE`,
};
Listing 13-6Creating the profileActionTypes.ts
创建配置文件操作
接下来,我们需要在概要文件文件夹中添加一个新文件。我们将其命名为profileAsyncActions.ts,如清单 13-7 所示。
import { createAsyncThunk } from '@reduxjs/toolkit';
import { UserType } from 'models/user-type';
import { ProfileActionTypes } from './profileActionTypes';
import {
getUserByIdFromDbAxios,
putUserFromDbAxios,
} from 'services/userDbService';
export const getProfileAction = createAsyncThunk(
ProfileActionTypes.FETCH_AND_SAVE_PROFILE,
async (id: string) => {
return (await getUserByIdFromDbAxios(id)).data;
},
);
export const putProfileAction = createAsyncThunk(
ProfileActionTypes.UPDATE_PROFILE,
async (user: UserType) => {
return (await putUserFromDbAxios(user)).data;
},
);
Listing 13-7Creating the profileAsyncActions
我们从 Redux 工具包 中导入模块createAsyncThunk来处理副作用。我们还有来自模型的UserType,动作类型profileActionTypes,以及来自userDbService.的两个 axios 函数
这是我们第一次异步操作。它来自于createAsyncThunk,的一个实例,我们需要传递一个字符串(FETCH_AND_SAVE_PROFILE)作为第一个参数,第二个参数是一个异步和等待函数。
string 类型的id参数与getProfileAction,相连,所以每当我们使用这个getProfileAction,时,我们都需要传递一个参数字符串。
然后我们将在getUserByIdFromDbAxios中使用那个id,用开-闭括号把它括起来,这样我们就可以用点(.)批注然后data.
**putProfileAction:用于更新个人资料。我们还有createAsyncThunk实例,然后将字符串指令传递给 reducers 来更新概要文件。第二个参数是基于承诺的匿名函数或异步和等待函数。
这是实现 Redux 工具包 的模式,即使在未来的 Redux 项目中,您也可以将它作为指导方针。
创建配置文件目录
之后,我们需要创建切片。在配置文件文件夹中,创建一个名为profileSlice .ts.的新文件
我们在下面有导入命名的组件,包括 profile state typeprofileNamespace,``profileActionTypes,和来自profileAsyncActions的getProfileAction和putProfileAction,如清单 13-8 所示。
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { UserType } from 'models/user-type';
import { profileNamespace, ProfileStateType } from './profileActionTypes';
import { getProfileAction, putProfileAction } from './profileAsyncActions';
/* profile state */
/* initial state or default state or initial values, it's up to you */
export const initialState: ProfileStateType = {
profile: {} as UserType,
loading: false,
error: '',
};
/* profile store */
export const profileSlice = createSlice({
/*
name: is your feature or also called module, or namespace,
or context, etc. The terminologies here can be interchangeable.
This is required.
*/
name: profileNamespace,
/*initialState is the default value of this namespace/module and it is required.*/
initialState,
/*Non asynchronous actions. Does not require Axios.*/
reducers: {},
/*Asynchronous actions. Actions that require Axios.
extraReducers - allows createSlice to respond not only to its own
action type but other action types also*/
/*state - is coming from the initialState; no need to define it because the Redux 工具包 can already infer what particular state it is. */
extraReducers: builder => {
builder.addCase(
getProfileAction.fulfilled,
(state, action: PayloadAction<UserType>) => {
state.profile = action.payload;
},
);
builder.addCase(
putProfileAction.pending,
(state, action: PayloadAction) => {
state.loading = true;
state.error = '';
},
);
builder.addCase(
putProfileAction.fulfilled,
(state, action: PayloadAction<UserType>) => {
state.loading = false;
state.profile = action.payload;
},
);
builder.addCase(
putProfileAction.rejected,
(state, action: PayloadAction<any>) => {
state.loading = false;
state.error = 'Something wrong happened';
console.log(action?.payload);
},
);
},
});
export default profileSlice.reducer;
Listing 13-8Creating profileSlice.ts
这里我们没有使用任何非异步或同步动作。对于 Redux 中的异步动作,我们需要extraReducers,,它也是切片的一部分。
要使用extraReducers,,我们需要设置或添加一个名为builder.的函数签名
builder:返回我们将要构建的addCase。
addCase:需要一个字符串,是动作类型。
如果你看看各种各样的addCases,你会发现它似乎只是一个大的 try-catch 块函数。
getProfileAction.fulfilled:例如,如果我们收到一个 2xx 状态代码,前面的代码块就会运行。
putProfileAction.pending:在被拒绝和被执行的功能之前运行。这是我们向任何 web 服务发送请求的时间。我们通过将 pending 设置为 true 来启用 spinner 或 loader。我们不需要挂起的有效负载,因为我们直接将 loading 从 false 改为 true。
在这里,我们得到状态代码 2xx,然后我们更新减速器。
putProfileAction.rejected:每当我们得到除 2xx 之外的状态码时就会运行,例如,4xx,表示未经授权,或 5xx,表示服务器有问题。如果发生这种情况,我们将 loading 设置为 false 并运行错误消息。
在底部,我们导出了缩减器: profileSlice.reducer 。
FOR YOUR ACTIVITY
在getProfileAction.中再添加两个addCases添加待定和拒绝。按照putProfileAction.里的模式就行了
清单 13-9 是一个 ToDo 活动:在 getProfileAction 中创建 addCase pending 和 addCase rejected。
extraReducers: builder => {
// todo activity: create addCase pending
builder.addCase(
getProfileAction.fulfilled,
(state, action: PayloadAction<UserType>) => {
state.profile = action.payload;
},
);
// todo activity: create addCase rejected
Listing 13-9Activity for Chapter 13
一旦您完成了活动,现在让我们来更新根缩减器。去商店➤ reducers.ts。
将 profileReducer 添加到异径管
我们需要从profileSlice a中导入profileReducer并将其添加到根减速器的injectedReducers中,如清单 13-10 所示。
import { combineReducers } from '@reduxjs/toolkit';
import calendarReducer from 'features/calendar/calendarSlice';
import profileReducer from'features/profile/profileSlice';
/* easier way of registering a reducer */
const injectedReducers = {
calendar: calendarReducer,
profile: profileReducer,
};
Listing 13-10Adding the profileReducer in the reducers.ts
我们店里有profileReducer;现在在每个应用组件中都可以访问到profileReducer。
创建 authSlice
接下来,我们将创建另一个切片。我们需要authSlice将访问令牌和声明保存到全局存储中。
因此,在文件夹特性中,添加一个新文件夹并将其命名为 auth,并在其中创建一个名为d authSlice.ts:的新文件
features ➤ auth ➤ authSlice.ts
打开它,从 Redux 工具包 中导入createSlice和PayloadAction,并从模型中导入ClaimsType。首先导入命名的组件,如清单 13-11 所示。
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { ClaimsType } from 'models/claims-type';
Listing 13-11Importing Modules in the authSlice.ts
然后我们添加名称空间、AuthStateType 的形状和初始状态,如清单 13-12 所示。
const authNamespace = 'auth';
export type AuthStateType = {
readonly accessToken: string;
readonly claims: ClaimsType;
};
/*we are using the AuthStateType to type safe our initial state */
export const initialState: AuthStateType = {
accessToken: '',
claims: null,
};
Listing 13-12Adding the Namespace, Type, and initialState in authSlice.ts
接下来,让我们使用createSlice来接受namespace, initialState、充满了reducer函数的对象以及我们创建的片名,如清单 13-13 所示。
export const authSlice = createSlice({
/*namespace for separating related states. Namespaces are like modules*/
name: authNamespace,
/* initialState is the default value of this namespace/module and it is required */
initialState,
/*Non asynchronous actions. Does not require Axios.*/
reducers: {
saveTokenAction: (state, action: PayloadAction<string>) => {
state.accessToken = action?.payload;
},
saveClaimsAction: (state, action: PayloadAction<ClaimsType>) => {
state.claims = action?.payload;
},
},
/*Asynchronous actions. Actions that require Axios.*/
extraReducers: builder => {},
});
/* export all non-async actions */
export const { saveClaimsAction, saveTokenAction } = authSlice.actions;
export default authSlice.reducer;
Listing 13-13Passing createSlice in our authSlice
在清单 13-13 中,我们有两个非异步动作,这意味着它们不需要使用 axios。
两个非异步动作—saveTokenAction和saveClaimsAction—不需要 axios 的帮助。
接下来,我们通过从 authSlice.actions 中提取这些非异步操作来导出它们。
最后,我们导出 authSlice.reducer,这样我们可以在根 reducer 中调用它。
将 authSlice 添加到 Reducers
转到reducers.ts,我们将authReducer注入到injectedReducers中,并导入命名组件,,如清单 13-14 所示。
import authReducer from '../features/auth/authSlice';
const injectedReducers = {
calendar: calendarReducer,
auth: authReducer,
profile: profileReducer,
};
Listing 13-14Adding the authReducer in the reducers.ts
authReducer现在是组合减速器和存储的一部分。
安装 JWT 解码
接下来,我们需要安装一个流行的 JavaScript 库 jwt-decode。它解码 JWT 令牌,对浏览器应用很有用:
npm i jwt-decode
安装 jwt-decode 之后,让我们前往受保护的路由并对其进行改进:组件➤受保护的路由
更新受保护的路由
首先,让我们在 protected-route.tsx 中导入额外的命名组件,如清单 13-15 所示。
import { useDispatch } from 'react-redux';
import jwt_decode from 'jwt-decode';
import { saveClaimsAction } from 'features/auth/authSlice';
import { ClaimsType } from 'models/claims-type';
Listing 13-15Adding Named Components in protected-route.tsx
接下来,让我们更新 ProtectedRoute 组件,如清单 13-16 所示。
const ProtectedRoute = props => {
const dispatch = useDispatch();
const token = localStorage.getItem('token');
/* this is cleaning up the localStorage and redirecting user to login */
if (!token) {
localStorage.clear();
return <Redirect to={{ pathname: '/login' }} />;
}
const decoded: ClaimsType = jwt_decode(token);
const expiresAt = decoded.exp * 1000;
const dateNow = Date.now();
const isValid = dateNow <= expiresAt;
dispatch(saveClaimsAction(decoded));
return isValid ? (
<Route {...props} />
) : (
<Redirect to={{ pathname: '/login' }} />
);
};
export default ProtectedRoute;
Listing 13-16Updating the ProtectedRoute Component
我们需要useDispatch和来自localStorage的令牌。我们正在清理localStorage,如果它是假的或者没有令牌,我们会将用户重定向到登录页面。
我们将把 toke n 传递给jwt_decode来解码它。相对于dateNow或当前日期,解码的令牌将与expiresAt或令牌的到期日期进行比较。
dateNow应等于或小于expiresAt。
接下来,我们将通过saveClaimsAction中的解码后的 token进行调度。
在 return 语句中,如果令牌isValid,则转到用户正在导航的地方;否则,将用户重定向到登录页面。
更新登录表单
我们现在将更新登录表单。首先,我们将添加一些命名的组件,如清单 13-17 所示。
import jwt_decode from 'jwt-decode';
import { useDispatch } from 'react-redux';
import { saveClaimsAction, saveTokenAction } from 'features/auth/authSlice';
import { loginAxios } from 'services/authService';
import { ClaimsType } from 'models/claims-type';
Listing 13-17Updating the Named Components in LoginForm
我们添加了来自 React Redux 的 useDispatch 和 jwt_decode。我们还需要 saveClaimsAction、saveTokenAction 和 ClaimsType。
接下来,我们需要更新 LoginForm 函数,如清单 13-18 所示。
const LoginForm = () => {
const key = 'token';
const history = useHistory();
const dispatch = useDispatch();
const [error, setError] = useState('');
Listing 13-18Hooks in Login Form
我们使用清单 13-18 中的useDispatch来调度一个动作,以保存对商店的访问令牌和声明,如清单 13-19 所示。
const saveUserAuthDetails = (data: { accessToken: string }) => {
localStorage.setItem(key, data.accessToken);
const claims: ClaimsType = jwt_decode(data.accessToken);
console.log('Claims::', claims); /*just to check it */
dispatch(saveTokenAction(data.accessToken));
dispatch(saveClaimsAction(claims));
};
Listing 13-19Updating the saveUserAuthDetails Function in the LoginForm
更新登记表
我们将对注册表进行类似的更新。我们开始导入命名的组件,如清单 13-20 所示。
import { useDispatch } from 'react-redux';
import { saveClaimsAction, saveTokenAction } from 'features/auth/authSlice';
import jwt_decode from 'jwt-decode';
import { ClaimsType } from 'models/claims-type';
Listing 13-20Updating the Named Components in the Register Form
在清单 13-20 中,我们添加了 useDispatch、saveClaimsAction、saveTokenAction、jwt_decode 和 ClaimsType。
接下来,我们更新 RegisterForm,如清单 13-21 所示。
const RegisterForm = () => {
const key = 'token';
const history = useHistory();
const dispatch = useDispatch();
const [error, setError] = useState('');
const [isAlertVisible, setAlertVisible] = useState(false);
Listing 13-21Hooks in the Registration Form
在清单 13-21 中,我们还使用了 useDispatch 来调度或发送一个动作,以保存对商店的访问令牌和声明,如清单 13-22 所示。
const saveUserAuthDetails = (data: { accessToken: string }) => {
localStorage.setItem(key, data.accessToken);
const claims: ClaimsType = jwt_decode(data.accessToken);
console.log('Claims::', claims);
dispatch(saveTokenAction(data.accessToken));
dispatch(saveClaimsAction(claims));
};
Listing 13-22saveUserAuthDetails Function
一旦保存在存储中,现在就可以在应用的任何地方访问访问令牌和声明。
让我们运行我们的应用:
npm run start:fullstack
刷新浏览器,打开 Redux DevTools,如图 13-6 所示。
图 13-6
Redux DevTools 的屏幕截图,没有本地存储中的数据
注意,在 auth 和 profile 状态下还没有数据。都是默认值。
接下来,让我们进入登录页面,让我们登录。当我们登录时,这将保存商店中的状态,如图 13-7 所示。
图 13-7
Redux DevTools 登录后的截图
在 Redux DevTools 中,我们可以看到saveClaimsAction和saveTokenAction.,我们有accessToken的数据和claims中的值。
我们现在已经验证了可以从服务器接收 accessToken 和声明。
配置文件仍然是空的,因为我们将使用声明 id (sub)来获取特定用户的数据。
创建标题配置文件
接下来,我们继续创建一个新的 React 组件,并将其命名为header-profile.tsx:
app ➤ components ➤ header-profile.tsx
让我们导入我们需要的命名组件,如清单 13-23 所示。
import React, { useState, MouseEvent } from 'react';
import clsx from 'clsx';
import { Theme, withStyles } from '@material-ui/core/styles';
import Menu, { MenuProps } from '@material-ui/core/Menu';
import { LogOut as LogOutIcon, Hexagon as HexagonIcon } from 'react-feather';
import { useSelector } from 'react-redux';
import { RootState } from 'store/reducers';
import { createStyles } from '@material-ui/styles';
import {
Avatar,
Box,
Divider,
ListItemIcon,
ListItemText,
makeStyles,
MenuItem,
} from '@material-ui/core';
Listing 13-23Adding the Named Components of the HeaderProfile
好吧,有什么新鲜事吗?从 Material-UI 核心样式中,我们已经从 React Feather 中导入了菜单和 MenuProps 以及六边形。
我们还使用 Redux 中的 useSelector 和 RootState 进行类型化。
然后让我们添加 HeaderProfile 函数,如清单 13-24 所示。
const HeaderProfile = () => {
const classes = useStyles();
/*using the profile to render an avatar */
const { profile } = useSelector((state: RootState) => state.profile);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const handleClick = (event: MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const handleLogout = () => {
localStorage.clear();
};
Listing 13-24Creating the HeaderProfile Function Component
在清单 13-24 中,我们使用 useSelector 来获取配置文件,并使用 profile.avatar 来呈现头像组件。参见清单 13-25 添加 HeaderProfile React 组件的返回语句。
return (
<div>
<Box display="flex" justifyContent="center" onClick={handleClick}>
<Avatar
variant={'circle'}
alt="User"
className={clsx(classes.avatar, classes.small)}
src={profile.avatar}
/>
</Box>
<StyledMenu
id="customized-menu"
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={handleClose}
>
<MenuItem>
<ListItemText primary={profile.email} />
</MenuItem>
<Divider />
<MenuItem>
<ListItemIcon>
<HexagonIcon />
</ListItemIcon>
<ListItemText primary="Partners" />
</MenuItem>
<a className={classes.link} href={'/'}>
<MenuItem onClick={handleLogout}>
<ListItemIcon>
<LogOutIcon />
</ListItemIcon>
<ListItemText primary="Logout" />
</MenuItem>
</a>
</StyledMenu>
</div>
);
};
export default HeaderProfile;
Listing 13-25Adding the Return Statement of the HeaderProfile
最后,添加样式组件,如清单 13-26 所示。
const useStyles = makeStyles((theme: Theme) =>
createStyles({
avatar: {
cursor: 'pointer',
width: 64,
height: 64,
},
link: { textDecoration: 'none', color: 'inherit' },
small: {
width: theme.spacing(3),
height: theme.spacing(3),
},
}),
);
const StyledMenu = withStyles({
paper: {
border: '1px solid #d3d4d5',
},
})((props: MenuProps) => (
<Menu
elevation={0}
getContentAnchorEl={null}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center',
}}
{...props}
/>
));
Listing 13-26Styling Components for the HeaderProfile
我们在这里使用的所有样式组件都来自 Material- UI。比如去网站 material-ui。并搜索菜单。
搜索您需要的特定菜单,复制代码,并根据您的喜好进行调整。你通常可以在网页底部看到 API。
更新导航栏
让我们前往导航栏。转到layouts ➤ main-layout ➤ navigation-bar.tsx,我们将对其进行更新。
当用户登录时,我们需要隐藏或删除菜单中的登录按钮。
首先,我们需要添加the useSelector, RootState,和HeaderProfile,,useSelector用于访问或获取声明,如清单 13-27 所示。
import {useSelector} from 'react-redux';
import {RootState} from 'store/reducers';
import HeaderProfile from 'app/components/header-profile';
export default function NavigationBar() {
const classes = useStyles();
const { claims } = useSelector((state: RootState) => state.auth);
Listing 13-27Adding Import Components and Using useSelector
接下来,我们从 Auth 获取声明,并将使用这些声明来更新仪表板和登录。
我们将把仪表板按钮和登录按钮放在一些花括号内,然后添加声明,并在按钮仪表板下添加 HeaderProfile。
{claims ? (
<Button color="inherit">
<Link className={classes.link} to={'/dashboard'}>
Dashboard
</Link>
</Button>
<HeaderProfile/>
) : (
<Button color="inherit">
<Link className={classes.link} to={'/login'}>
Login
</Link>
</Button>
)
}
Listing 13-28Using the Claims to Wrap the Dashboard and Login Buttons in the Navigation Bar
因此,如果声明有效,就会呈现仪表板链接。否则,将呈现登录链接。
刷新浏览器,应该会在登录页面的右上角看到头像,如图 13-8 所示。
图 13-8
显示登录页面的仪表板头像
并且登录后看到仪表盘头像,如图 13-9 。
图 13-9
登录后显示仪表板头像
创建帐户视图
现在我们转到 views 文件夹,创建一个名为account的新文件夹,并在其中创建另一个名为AccountView的文件夹。在AccountView,里面加上index.tsx:
views ➤ account ➤ AccountView ➤ index.tsx
现在,我们将添加带有标签<h1> AccountView Page Works</h1>的标准页面模板。参见清单 13-29 。
import React from 'react';
import { Container, makeStyles } from '@material-ui/core';
import Page from 'app/components/page';
const AccountView = () => {
const classes = useStyles();
return (
<Page className={classes.root} title="Settings">
<Container maxWidth="lg">
<h1>AccountView Page Works</h1>
</Container>
</Page>
);
};
export default AccountView;
const useStyles = makeStyles(theme => ({
root: {
minHeight: '100%',
paddingTop: theme.spacing(3),
paddingBottom: theme.spacing(3),
},
}));
Listing 13-29Creating the Standard Page for the AccountView
目前,帐户页面只是为了显示我们可以在这里导航;我们稍后将回到这一点来更新它。
添加图像
让我们在应用中添加更多的图像。转到图像➤产品。
你可以在我的 GitHub 中抓取这些图片:
将这四张图片放在产品中:
product_extended.svg,
product_premium.svg,
product_premium—outlined.svg,
product_standard.svg.
最后,将这个头像添加到图片目录:
阿凡达 _6.png
你可以从这里抓取图像:
创建定价页面
接下来,让我们创建一个新组件,并将其命名为定价页面:
views ➤ pages ➤ pricing ➤ PricingPage.tsx
这个页面纯粹是为了美观或者设计。我们将从 Material-UI 导入标准的样式组件,如清单 13-30 所示。
import React from 'react';
import clsx from 'clsx';
import {
Box,
Button,
Container,
Divider,
Grid,
Paper,
Typography,
makeStyles,
} from '@material-ui/core';
import Page from 'app/components/page';
Listing 13-30Adding the Named Components of the PricingPage
然后我们添加将从 Material-UI 中重用的 PricingPage 组件。我们展示了我们通常在网站上看到的不同价格产品或定价选项,例如,标准选项、高级选项和扩展选项。参见清单 13-31 。
const PricingPage = () => {
const classes = useStyles();
return (
<Page className={classes.root} title="Pricing">
<Container maxWidth="sm">
<Typography align="center"
variant="h2" color="textPrimary">
Start Selling!
</Typography>
<Box mt={3}>
<Typography align="center"
variant="subtitle1" color="textSecondary">
Welcome to the best platform for selling products
</Typography>
</Box>
</Container>
<Box mt="160px">
<Container maxWidth="lg">
<Grid container spacing={4}>
<Grid item md={4} xs={12}>
<Paper className={classes.product}
elevation={1}>
<img
alt="Product"
className={classes.productImage}
src="images/products/product_standard.svg"
/>
<Typography
component="h4"
gutterBottom
variant="overline"
color="textSecondary"
>
Standard
</Typography>
<div>
<Typography
component="span"
display="inline"
variant="h4"
color="textPrimary"
>
$5
</Typography>
<Typography
component="span"
display="inline"
variant="subtitle2"
color="textSecondary"
>
/month
</Typography>
</div>
<Typography variant="overline"
color="textSecondary">
Max 1 user
</Typography>
<Box my={2}>
<Divider />
</Box>
<Typography variant="body2"
color="textPrimary">
20 proposals/month
<br />
10 templates
<br />
Analytics dashboard
<br />
Email alerts
</Typography>
<Box my={2}>
<Divider />
</Box>
<Button
variant="contained"
fullWidth
className={classes.chooseButton}
>
Choose
</Button>
</Paper>
</Grid>
<Grid item md={4} xs={12}>
<Paper
className={clsx(classes.product,
classes.recommendedProduct)}
elevation={1}
>
<img
alt="Product"
className={classes.productImage}
src="images/products/product_premium--outlined.svg"
/>
<Typography
component="h4"
gutterBottom
variant="overline"
color="inherit"
>
Premium
</Typography>
<div>
<Typography
component="span"
display="inline"
variant="h4"
color="inherit"
>
$29
</Typography>
<Typography
component="span"
display="inline"
variant="subtitle2"
color="inherit"
>
/month
</Typography>
</div>
<Typography variant="overline" color="inherit">
Max 3 user
</Typography>
<Box my={2}>
<Divider />
</Box>
<Typography variant="body2" color="inherit">
20 proposals/month
<br />
10 templates
<br />
Analytics dashboard
<br />
Email alerts
</Typography>
<Box my={2}>
<Divider />
</Box>
<Button
variant="contained"
fullWidth
className={classes.chooseButton}
>
Choose
</Button>
</Paper>
</Grid>
<Grid item md={4} xs={12}>
<Paper className={classes.product} elevation={1}>
<img
alt="Product"
className={classes.productImage}
src="images/products/product_extended.svg"
/>
<Typography
component="h4"
gutterBottom
variant="overline"
color="textSecondary"
>
Extended
</Typography>
<div>
<Typography
component="span"
display="inline"
variant="h4"
color="textPrimary"
>
$259
</Typography>
<Typography
component="span"
display="inline"
variant="subtitle2"
color="textSecondary"
>
/month
</Typography>
</div>
<Typography variant="overline" color="textSecondary">
Unlimited
</Typography>
<Box my={2}>
<Divider />
</Box>
<Typography variant="body2"
color="textPrimary">
All from above
<br />
Unlimited 24/7 support
<br />
Personalised Page
<br />
Advertise your profile
</Typography>
<Box my={2}>
<Divider />
</Box>
<Button
variant="contained"
fullWidth
className={classes.chooseButton}
>
Choose
</Button>
</Paper>
</Grid>
</Grid>
</Container>
</Box>
</Page>
);
};
Listing 13-31Creating the PricingPage Component
接下来,我们需要添加样式组件,如填充、定位、过渡主题等。来自 Material-UI,如清单 13-32 所示。
const useStyles = makeStyles(theme => ({
root: {
minHeight: '100%',
height: '100%',
paddingTop: 120,
paddingBottom: 120,
},
product: {
position: 'relative',
padding: theme.spacing(5, 3),
cursor: 'pointer',
transition: theme.transitions.create('transform', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
'&:hover': {
transform: 'scale(1.1)',
},
},
productImage: {
borderRadius: theme.shape.borderRadius,
position: 'absolute',
top: -24,
left: theme.spacing(3),
height: 48,
width: 48,
fontSize: 24,
},
recommendedProduct: {
backgroundColor: theme.palette.primary.main,
color: theme.palette.common.white,
},
chooseButton: {
backgroundColor: theme.palette.common.white,
},
}));
export default PricingPage;
Listing 13-32Adding the Styling Components to the PricingPage
我们不会在这里添加任何功能;对于我们目前的应用来说,这只是为了美观。但是你可以摆弄它,根据你的喜好改变它。
更新路线
好了,现在是时候更新添加PricingPage and the AccountView.的路线了,将它们分别添加到登录路线路径和日历路线路径下,如清单 13-33 所示。
<Route
exact
path={'/pricing'}
component={lazy(() => import('./views/pages/pricing/PricingPage'))}
/>
...
<Route
exact
path={path + '/account'}
component={lazy(
() => import('./views/dashboard/account/AccountView'),
)}
/>
Listing 13-33Updating the routes.tsx
摘要
在这一章中,我们已经学习了如何创建我们的 Profile 表单,并展示了在 Redux 的帮助下,我们如何轻松地将它同步到应用中的另一个组件。我们还使用了一个流行的 JavaScript 库 jwt-decode,它可以用来解码 jwt 令牌。我们还使用 Formik 和 JWT 创建了用于身份验证的登录和注册表单。
在本章的第二部分,我们将继续更新侧边栏导航。**
十四、更新仪表板侧栏导航
我们刚刚完成了配置文件、登录和注册表单的构建,并通过更新路线结束了第一部分。我们现在进入这个由三部分组成的章节系列的第二部分,更新仪表板侧边栏导航。
既然我们已经更新了路线,我们现在将继续到仪表板-侧栏-导航。
让我们从分别添加来自 React-Redux 和 reducers 的useDispatch和useSelector开始。见清单 14-1 。
import { useSelector, useDispatch } from 'react-redux';
import {RootState} from 'store/reducers';
Listing 14-1Updating the Named Components in the dashboard-sidebar-navigation
让我们使用DashboardSidebarNavigation component中的useDispatch和useSelector,如清单 14-2 所示。
const DashboardSidebarNavigation = () => {
const classes = useStyles();
const dispatch = useDispatch();
const {profile} = useSelector((state: RootState) => state.profile);
const {claims} = useSelector((state: RootState) => state.auth);
const { url } = useRouteMatch();
const [open, setOpen] = useState(false);
Listing 14-2Updating the DashboardSidebarNavigation
在清单 14-2 中,我们从auth reducer.得到profile减速器轮廓和claims
接下来,从 profileAsyncActions 导入命名模块 getProfileAction。在useEffect中,我们调用dispatch将带有claims.sub的getProfileAction发送到减速器。这意味着我们需要导入清单 14-3 中所示的getProfileAction,。
...
import { getProfileAction } from 'features/profile/profileAsyncActions';
...
useEffect(() => {
dispatch(getProfileAction(claims.sub));
}, []);
Listing 14-3Dispatching the getProfileAction in the DashboardSidebarNavigation
getProfileAction :我们调用传递the claims.sub并获取user's id的函数。
让我们从 Material-UI Core 添加头像和附加组件的样式——头像、盒子和字体,如清单 14-4 所示。
import { Collapse, Divider, ListSubheader, Avatar, Box, Typography} from '@material-ui/core';
...
const useStyles = makeStyles(theme =>
createStyles({
avatar: {
cursor: 'pointer',
width: 64,
height: 64,
},
...
Listing 14-4Adding the Avatar Style in the DashboardSidebarNavigation
现在,我们准备好更新到新的 UI。将包含徽标的工具栏替换为下面的工具栏,如清单 14-5 所示。
{/* check first if profile.name is true before rendering what's inside the Box, including the avatar */}
{profile.name && (
<Box p={2}>
<Box display="flex" justifyContent="center">
<Avatar
alt="User"
className={classes.avatar}
src={profile.avatar}
/>
</Box>
<Box mt={2} textAlign="center">
<Typography>{profile.name}</Typography>
<Typography variant="body2" color="textSecondary">
Your tier: {profile.tier}
</Typography>
</Box>
</Box>
)}
Listing 14-5Updating
the DashboardSidebarNavigation
更新 db.json
好的,在我们刷新浏览器之前,我们需要首先更新db.json并添加 users-db。
目前,我们只有用于认证的用户。现在我们来补充一下。参见清单 14-6 。
"users-db": [
{
"id": "7fguyfte5",
"email": "demo@acme.io",
"name": "Mok Kuh",
"password": "$2a$10$.vEI32nHFyG15ZACR7q/J.DNT/7iFC1Gfi2fFPMsG09LCPtwk0q/.",
"avatar":img/avatar_6.png",
"canHire": true,
"country": "United States",
"city": "NY",
"isPublic": true,
"phone": "+40 777666555",
"role": "admin",
"state": "New York",
"tier": "Premium",
"subscription": {
"name": "Premium",
"price": 29,
"currency": "$",
"proposalsLeft": 12,
"templatesLeft": 5,
"invitesLeft": 24,
"adsLeft": 10,
"hasAnalytics": true,
"hasEmailAlerts": true
}
}
]
Listing 14-6Adding the users-db in the db.json
刷新浏览器,您应该会看到如图 14-1 所示的浏览器。
图 14-1
更新用户界面
你会注意到边栏导航和导航栏正在同步;他们在渲染同一个图像。它们从 Redux 存储中获得相同的状态。
检查 Redux DevTools 并单击状态,您将看到概要文件,它现在在应用的任何部分都可用,如图 14-2 所示。
图 14-2
检查 Redux 开发工具
好了,现在我们知道我们可以在任何组件中同步用户的配置文件。但是我们还没有完成;我们仍然需要更新仪表板、帐户和定价菜单。
更新仪表板侧栏导航
让我们再次打开仪表板-侧栏-导航。让我们从 React Feather 导入几个图标,并在日历后添加帐户和定价菜单,如清单 14-7 所示。
...
User as UserIcon,
DollarSign as DollarSignIcon,
LogOut as LogOutIcon,
} from 'react-feather';
...
<ListSubheader>Applications</ListSubheader>
<Link className={classes.link} to={`${url}/calendar`}>
<ListItem button>
<ListItemIcon>
<CalendarIcon />
</ListItemIcon>
<ListItemText primary={'Calendar'} />
</ListItem>
</Link>
<ListSubheader>Pages</ListSubheader>
<Link className={classes.link} to={`${url}/account`}>
<ListItem button>
<ListItemIcon>
<UserIcon />
</ListItemIcon>
<ListItemText primary={'Account'} />
</ListItem>
</Link>
<Link className={classes.link} to={`/pricing`}>
<ListItem button>
<ListItemIcon>
<DollarSignIcon />
</ListItemIcon>
<ListItemText primary={'Pricing'} />
</ListItem>
</Link>
<a className={classes.link} href={'/'}>
<ListItem button onClick={handleLogout}>
<ListItemIcon>
<LogOutIcon />
</ListItemIcon>
<ListItemText primary={'logout'} />
</ListItem>
</a>
</List>
Listing 14-7Adding the Account and Pricing Menus in the dashboard-sidebar-navigation
刷新浏览器,你会在侧边栏看到两个新的附加菜单。该帐户现在是空的,但是价格已经有了一些样式,这是由 Material-UI 组件提供的。见图 14-3 。
图 14-3
更新了侧边栏菜单
点击定价可以看到如图 14-4 所示的相同界面。
图 14-4
定价页面的屏幕截图
如果你点击了账户,你会看到这只是最低限度。所以让我们在这里做点什么。但在此之前,我们需要创建一个“是”验证。这是一个广泛的概要文件验证,所以最好把它写在一个单独的文件中。
创建 Yup 配置文件验证
新建一个文件:特色 ➤ 简介 ➤ 没错 ➤ 简介.验证. ts 。
让我们添加 Yup 配置文件验证,如清单 14-8 所示。
import * as Yup from 'yup';
const profileYupObject = Yup.object().shape({
canHire: Yup.bool(),
city: Yup.string().max(255),
country: Yup.string().max(255),
email: Yup.string()
.email('Must be a valid email')
.max(255)
.required('Email is required'),
isPublic: Yup.bool(),
name: Yup.string().max(255).required('Name is required'),
phone: Yup.string(),
state: Yup.string(),
});
export { profileYupObject };
Listing 14-8Adding the Yup Profile Validation
之后,我们现在可以开始构建AccountView页面。
创建帐户视图页面
让我们在AccountView .下创建一个新文件夹,将新文件夹命名为 General,并在该文件夹下创建一个名为GeneralSettings.tsx:的新文件
account ➤ AccountView ➤ General ➤ GeneralSettings.tsx
让我们导入我们需要的命名组件,如清单 14-9 所示。
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import clsx from 'clsx';
import { Formik } from 'formik';
import { useSnackbar } from 'notistack';
import Autocomplete from '@material-ui/lab/Autocomplete';
import {
Box,
Button,
Card,
CardContent,
CardHeader,
Divider,
FormHelperText,
Grid,
Switch,
TextField,
Typography,
makeStyles,
} from '@material-ui/core';
import { UserType } from 'models/user-type';
import { putProfileAction } from 'features/profile/profileAsyncActions';
import { profileYupObject } from 'features/profile/yup/profile.validation';
Listing 14-9Importing the Named Components of the GeneralSettings
这里有什么新鲜事?我们从 Material-UI 实验室得到了Autocomplete。对于我们的自动完成,我们将使用Country select组件。
除了其他常见的 Material-UI 组件,我们还导入了UserType, putProfileAction,和profileYupObject.
接下来,我们将创建GeneralSettings的形状和我们将要返回的本地州,如清单 14-10 所示。
type Props = {
className?: string;
user: UserType;
};
const GeneralSettings = ({ className, user, ...rest }: Props) => {
const dispatch = useDispatch();
const classes = useStyles();
const [error, setError] = useState('');
const { enqueueSnackbar } = useSnackbar();
Listing 14-10Creating the Shape and Local States of the GeneralSettings
然后,让我们在 return 语句中使用 Formik,如清单 14-11 所示。
return (
<Formik
enableReinitialize
initialValues={user}
validationSchema={profileYupObject}
onSubmit={async (values, formikHelpers) => {
try {
dispatch(putProfileAction(values));
formikHelpers.setStatus({ success: true });
formikHelpers.setSubmitting(false);
enqueueSnackbar('Profile updated', {
variant: 'success',
});
} catch (err) {
setError(err);
formikHelpers.setStatus({ success: false });
formikHelpers.setSubmitting(false);
}
}}
>
{({
errors,
handleBlur,
handleChange,
handleSubmit,
isSubmitting,
touched,
values,
setFieldValue,
}) => (
Listing 14-11Creating the Formik Props
在 Formik 中,我们使用enableReinitialize,它允许我们更新或编辑表单。当我们有一个现有的对象或数据,并且想用 Formik 编辑它时,我们需要使用 Formik prop。我们有这种双向数据绑定。
initialValues:我们正在传递来自GeneralSettings.的父组件的用户
validationSchema:我们正在传递 profileYupObject。
onSubmit:我们正在调度putProfileAction并传递用户输入的值。
现在让我们使用 Formik 属性来构建我们需要的不同的文本字段。
我们将为 Country 创建一个 TextField,我们将在其中集成自动完成功能。参见清单 14-12 。
<form onSubmit={handleSubmit}>
<Card className={clsx(classes.root, className)} {...rest}>
<CardHeader title="Profile" />
<Divider />
<CardContent>
<Grid container spacing={4}>
<Grid item md={6} xs={12}>
<TextField
error={Boolean(touched.name && errors.name)}
fullWidth
helperText={touched.name && errors.name}
label="Name"
name="name"
onBlur={handleBlur}
onChange={handleChange}
value={values?.name}
variant="outlined"
/>
</Grid>
<Grid item md={6} xs={12}>
<TextField
error={Boolean(touched.email && errors.email)}
fullWidth
helperText={
touched.email && errors.email
? errors.email
: 'We will use this email to contact you'
}
label="Email Address"
name="email"
onBlur={handleBlur}
onChange={handleChange}
required
type="email"
value={values?.email}
variant="outlined"
/>
</Grid>
<Grid item md={6} xs={12}>
<TextField
error={Boolean(touched.phone && errors.phone)}
fullWidth
helperText={touched.phone && errors.phone}
label="Phone Number"
name="phone"
onBlur={handleBlur}
onChange={handleChange}
value={values?.phone}
variant="outlined"
/>
</Grid>
<Grid item md={6} xs={12}>
<Autocomplete
id="country"
options={countries}
value={values?.country}
getOptionLabel={option => option.toString()}
renderOption={option => <>{option.text}</>}
onChange={(e: any) => {
setFieldValue('country', e.target.innerText);
}}
renderInput={params => (
<TextField
{...params}
value={values?.country}
fullWidth
label="Country"
name="country"
onChange={handleChange}
variant="outlined"
inputProps={{
...params.inputProps,
autoComplete: 'country',
}}
/>
)}
/>
</Grid>
<Grid item md={6} xs={12}>
<TextField
error={Boolean(touched.state && errors.state)}
fullWidth
helperText={touched.state && errors.state}
label="State/Region"
name="state"
onBlur={handleBlur}
onChange={handleChange}
value={values?.state}
variant="outlined"
/>
</Grid>
<Grid item md={6} xs={12}>
<TextField
error={Boolean(touched.city && errors.city)}
fullWidth
helperText={touched.city && errors.city}
label="City"
name="city"
onBlur={handleBlur}
onChange={handleChange}
value={values?.city}
variant="outlined"
/>
</Grid>
<Grid item md={6} xs={12}>
<Typography variant="h6" color="textPrimary">
Make Contact Info Public
</Typography>
<Typography variant="body2" color="textSecondary">Means that anyone viewing your profile will be able to see your contacts details
</Typography>
<Switch
checked={values?.isPublic}
edge="start"
name="isPublic"
onChange={handleChange}
/>
</Grid>
<Grid item md={6} xs={12}>
<Typography variant="h6" color="textPrimary">
Available to hire
</Typography>
<Typography variant="body2" color="textSecondary">
Toggling this will let your teammates know that you are
available for acquiring new projects
</Typography>
<Switch
checked={values?.canHire}
edge="start"
name="canHire"
onChange={handleChange}
/>
</Grid>
</Grid>
{error && (
<Box mt={3}>
<FormHelperText error>{error}</FormHelperText>
</Box>
)}
</CardContent>
<Divider />
<Box p={2} display="flex" justifyContent="flex-end">
<Button
color="secondary"
disabled={isSubmitting}
type="submit"
variant="contained"
>
Save Changes
</Button>
</Box>
</Card>
</form>
)}
</Formik>
);
};
const useStyles = makeStyles(() => ({
root: {},
}));
export default GeneralSettings;
Listing 14-12Using the Formik Props in the GeneralSettings
文本字段绑定到属性名称,例如,姓名、电子邮件、电话、状态、州、地区、城市等。
自动完成功能与状态绑定在一起。我们将状态包装在自动完成中,我们从 Material-UI 的自动完成中得到这个 API,包括图 14-5 中的状态列表。
之后,我们将在所有状态列表的下方进行硬编码。以下只是它的截图。您可以通过此链接复制粘贴完整的列表:
图 14-5
在常规设置中添加自动完成状态选择
创建个人资料详细信息
不过,在通用文件夹下,我们将创建一个名为ProfileDetails .tsx.的新文件。这个新组件只是一个用户头像。
我们需要导入的命名组件如清单 14-13 所示。
import React from 'react';
import { Link as RouterLink } from 'react-router-dom';
import clsx from 'clsx';
import {
Avatar,
Box,
Button,
Card,
CardActions,
CardContent,
Link,
Typography,
makeStyles,
} from '@material-ui/core';
import { UserType } from 'models/user-type';
Listing 14-13Adding the Named Components in the ProfileDetails
然后添加 ProfileDetails React 函数组件,如清单 14-14 所示。
type Props = {
className?: string;
user: UserType;
};
const ProfileDetails = ({ className, user, ...rest }: Props) => {
const classes = useStyles();
return (
<Card className={clsx(classes.root, className)} {...rest}>
<CardContent>
<Box
display="flex"
alignItems="center"
flexDirection="column"
textAlign="center"
>
<Avatar className={classes.avatar} src={user?.avatar} />
<Typography
className={classes?.name}
color="textPrimary"
gutterBottom
variant="h4"
>
{user?.name}
</Typography>
<Typography color="textPrimary" variant="body1">
Your tier:{' '}
<Link component={RouterLink} to="/pricing">
{user?.tier}
</Link>
</Typography>
</Box>
</CardContent>
<CardActions>
<Button fullWidth variant="text">
Remove picture
</Button>
</CardActions>
</Card>
);
};
const useStyles = makeStyles(theme => ({
root: {},
name: {
marginTop: theme.spacing(1),
},
avatar: {
height: 100,
width: 100,
},
}));
export default ProfileDetails;
Listing 14-14Adding the ProfileDetails Function Component
用户对象来自配置文件详细信息的父组件。调用用户来渲染avatar、name,和tier。我们也有 移除图片, 但是我们不打算在这里放任何功能。只是为了美观。
创建常规设置
现在我们继续在通用文件夹下创建一个index.tsx:
account ➤ AccountView ➤ General ➤ index.tsx
让我们首先添加命名的组件,如清单 14-15 所示。
import React from 'react';
import clsx from 'clsx';
import { useSelector } from 'react-redux';
import { Grid, makeStyles } from '@material-ui/core';
import ProfileDetails from './ProfileDetails';
import GeneralSettings from './GeneralSettings';
import { RootState } from 'store/reducers';
Listing 14-15Adding the Named Components in the index.tsx of the General Settings
然后,让我们创建组件的形状和useSelector来访问商店状态的一部分,如清单 14-16 所示。
type Props = {
className?: string;
};
const General = ({ className, ...rest }: Props) => {
const classes = useStyles();
const { profile } = useSelector((state: RootState) => state.profile);
Listing 14-16Creating the Shape and useSelector in the index.tsx of the General Settings
在返回语句中,我们传递ProfileDetails和General Settings' user props中的概要状态,如清单 14-17 所示。
return (
<Grid
className={clsx(classes.root, className)}
container
spacing={3}
{...rest}
>
<Grid item lg={4} md={6} xl={3} xs={12}>
<ProfileDetails user={profile} />
</Grid>
<Grid item lg={8} md={6} xl={9} xs={12}>
<GeneralSettings user={profile} />
</Grid>
</Grid>
);
};
const useStyles = makeStyles(() => ({
root: {},
}));
export default General;
Listing 14-17Using the Profile to Pass to ProfileDetails and General Settings
创建标题
之后,我们需要回到AccountView来添加更多的组件:
account ➤ AccountView ➤ Header.tsx
让我们添加命名的组件,如清单 14-18 所示。
import React from 'react';
import { Link as RouterLink } from 'react-router-dom';
import clsx from 'clsx';
import {
Typography,
Breadcrumbs,
Link,
makeStyles,
Box,
} from '@material-ui/core';
import NavigateNextIcon from '@material-ui/icons/NavigateNext';
Listing 14-18Adding the Named Components of Header.tsx in AccountView
这里有什么新鲜事吗?我们有来自 Material-UI 核心的 Breadcrumbs 和来自 Material-UI 图标的 NavigateNextIcon。
Breadcrumbs :让用户能够从一系列值中进行选择。
然后,让我们为 Header 组件添加 return 语句,如清单 14-19 所示。
type Props = {
className?: string;
};
const Header = ({ className, ...rest }: Props) => {
const classes = useStyles();
return (
<div className={clsx(classes.root, className)} {...rest}>
<Breadcrumbs
separator={<NavigateNextIcon fontSize="small" />}
aria-label="breadcrumb"
>
<Link color="inherit" to="/app" component={RouterLink}>
Dashboard
</Link>
<Box>
<Typography variant="body1" color="inherit">
Account
</Typography>
</Box>
</Breadcrumbs>
<Typography variant="h4" color="textPrimary">
Settings
</Typography>
</div>
);
};
const useStyles = makeStyles(() => ({
root: {},
}));
export default Header;
Listing 14-19Creating the Header Component
摘要
在本章中,我们成功地同步了侧边栏导航和导航栏之间的数据,因为数据来自全局存储或 Redux 存储。
我们还更新了仪表板、帐户和定价菜单,并使用 Formik 和 Yup 验证模式来构建它们。
接下来,我们将继续构建这个由三部分组成的章节系列,并添加另外三个组件,通知、安全性和订阅页面,以完成我们的应用的 UI。