[React 实战系列] 布局登录注册的页面实现及 Route 的封装
Posted GoldenaArcher
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[React 实战系列] 布局登录注册的页面实现及 Route 的封装相关的知识,希望对你有一定的参考价值。
[React 实战系列] 布局、登录、注册的页面实现及 Route 的封装
之前差不多将配置都实现得差不多了,现在就开始进行页面的实现。
回顾一下上一篇文章中实现的效果:
这篇会从这里开始,完成 布局、注册、登录 三个部分的实现;以及对 Route 的封装,这是为了方便后面业务的运行,目前对路由只是进行了初步封装,等我过一下 TypeScript 部分的内容,还能够再精简一些。
顺便之前本来以为是购物车的页面看起来好像是商城页面,所以改成了 Shop。
这部分实现的资源在:React实战系列-布局、登录、注册的页面实现及 Route 的封装
之前已经实现的内容:
Layout
从页面结构上来说,每个页面上都会出现一个导航栏以及页头,这里就是用 antd 提供的两个组件实现:Menu 和 PageHeader。
Menu 用来实现导航栏的功能,PageHeader 用来实现页头的功能。
导航栏(Menu)
antd 中有好几个导航菜单的应用,包括横屏与竖屏几种不同的风格。代码页面上也有直接的案例,这里引用的基本结构如下:
|- Menu # 菜单
| |- Menu.Item # 菜单项
| | |- 元素
这里创建一个新的文件 Navigation.tsx 去实现导航栏的页面,进行业务逻辑的分离,基础实现如下:
import { Menu } from 'antd';
import { Link } from 'react-router-dom';
const Navigation = () => {
// mode 为菜单类型,支持垂直、水平、和内嵌模式三种,horizontal 为水平模式
// Menu.Item 需要一个 key 属性,否则页面上,即 console 上会出现报错信息
return (
<Menu mode="horizontal">
<Menu.Item key="homepage">
<Link to="/">首页</Link>
</Menu.Item>
<Menu.Item key="shop">
<Link to="/shop">商城</Link>
</Menu.Item>
</Menu>
);
};
export default Navigation;
当前效果如下:
能够发现,大致上没有什么问题,但是选中的组件没有高亮显示,现在就讲这一部分实现了。很神奇的是,Menu.Item
点了两次之后就可以正确的挂上高亮了,现在就打算重写高亮的这个功能。
高亮的实现还是通过禁用 selectable
,随后重写 className=‘ant-menu-title-content’
来实现。
实现如下:
function useActive(currentPath: string, path: string) {
return currentPath === path ? "ant-menu-item-selected" : "";
}
const Navigation = () => {
// RouterState 是 connected-react-router 提供的,可以触发 router. 的提示
const router = useSelector<AppState, RouterState>((state) => state.router);
const pathname = router.location.pathname;
const isHome = useActive(pathname, "/");
const isShop = useActive(pathname, "/shop");
return (
<Menu mode="horizontal" selectable={false}>
<Menu.Item key="" className={isHome}>
<Link to="/">首页</Link>
</Menu.Item>
<Menu.Item key="shop" className={isShop}>
<Link to="/shop">商城</Link>
</Menu.Item>
</Menu>
);
};
实现效果如下:
虽然功能实现了,但是能够看到,挂载 className 的方式还是很麻烦的,后面加更多的验证,相对而言也要写更多的变量去存储判断后的 className,最终再放入到 Menu.Item
。如果后面还要进行 Menu 的变动,修改起来也非常的麻烦,之后还是需要将其封装一下。
页头(PageHeader)
同样的,antd 中 PageHeader 的实现也有不少,这里依旧选中最简单的那个,只需要传 title 和 subTitle 两个属性即可。另外,再修改一下 {children}
的样式,现在的内容的宽度与页面持平,视觉上看起来不是很好。
Layout 的修改为:
import { PageHeader } from 'antd';
import { FC } from 'react';
import Navigation from './Navigation';
// 注意同步修改 Props 接收的参数问题
interface Props {
children: React.ReactNode;
title: string;
subTitle: string;
}
const Layout: FC<Props> = ({ children, title, subTitle }) => {
return (
<div>
<Navigation />
<PageHeader title={title} subTitle={subTitle} />
<div style={{ width: '85%', margin: '0 auto' }}>{children}</div>
</div>
);
};
export default Layout;
同时修改 Home 去传入对应的参数:
import { useSelector } from 'react-redux';
import Layout from './Layout';
const Home = () => {
const state = useSelector((state) => state);
return (
<Layout title="商城首页" subTitle="test">
Home {JSON.stringify(state)}
</Layout>
);
};
export default Home;
因为修改了 Layout 中的 Props,如果不同步修改 Shop 也会报错,对应修改 Shop:
import Layout from './Layout';
const Shop = () => {
return (
<Layout title="商城" subTitle="test2">
ShopCart
</Layout>
);
};
export default Shop;
最后结果如下:
这里其实还有一个同样的问题,那就是所有的属性都是在组件内写死的,如果发生要修改的情况下,就必须要跑到每个组件中去修改,有些的麻烦。
重构 Routes
上面提了,现在的实现还是有些麻烦的,在增添新的页面之前修改一下路由的视线。
新增 routeConfig.ts
这个 config 文件将会作为其他的路由文件的唯一入口,目前实现如下:
import Home from '../components/core/Home';
import Shop from '../components/core/Shop';
export const HOME_PATH = '/';
export const SHOP_PATH = '/shop';
const subTitle = 'GoldenaArcher的学习项目';
const routeConfig = {
home: {
name: '首页',
path: HOME_PATH,
component: Home,
title: '商城首页',
subTitle,
isExact: true,
},
shop: {
name: '商城',
path: SHOP_PATH,
component: Shop,
title: '商城页面',
subTitle,
isExact: false,
},
};
export default routeConfig;
使用对象而非数组的原因还是在于,使用 key 直接获取会方便一些。而且路由的数量一旦多了起来,通过索引进行页面的管理相对而言比较复杂也不是很直观。
修改 Routes.tsx
现在,HashRouter > Switch > Route
就可以通过循环遍历的方式,而不是写死的方式。
实现如下:
import { HashRouter, Route, Switch } from 'react-router-dom';
import routeConfig from './routeConfig';
const routes = () => {
const routes = [];
for (const [routeKey, routeVal] of Object.entries(routeConfig)) {
routes.push(
<Route
key={routeKey}
path={routeVal.path}
component={routeVal.component}
exact={routeVal.isExact}
/>
);
}
return routes;
};
const Routes = () => {
return (
<HashRouter>
<Switch>{routes()}</Switch>
</HashRouter>
);
};
export default Routes;
修改 Home.tsx
之前在 Home 组件中调用 Layout 需要手动传入 title 和 subTitle,相对而言较为麻烦。一旦路由的页面多了起来,需要手动传值的地方也会更多,也就更加难以管理。
但是 routeConfig 中已经有需要的值了,现在就可以通过调用 routeConfig 将对应的数据传入 Layout 中。使用 routeConfig 传值的另一个好处就在于静态检查,如果对象不存在的话,TypeScript 的静态检查就会显示对应的报错信息。如 routeConfig 中不存在 none,某个组件又调用了 routeConfig.none
,TypeScript 就会在编译之前跳出对应的报错信息:
实现如下:
import { useSelector } from 'react-redux';
import routeConfig from '../../router/routeConfig';
import Layout from './Layout';
const Home = () => {
const state = useSelector((state) => state);
// 另一个好处也在于可以不用一个个手动传值,而是可以通过 剩余操作符... 将剩下的值传到下一个组件去
return <Layout {...routeConfig.home}>Home {JSON.stringify(state)}</Layout>;
};
export default Home;
修改 Shop.tsx
同样的修改方式:
import routeConfig from '../../router/routeConfig';
import Layout from './Layout';
const Shop = () => {
return <Layout {...routeConfig.shop}>ShopCart</Layout>;
};
export default Shop;
修改 Navigation.tsx
同样的道理,之前的 Navigation 需要手动写所有的 MenuItem,也需要手动写死值,一旦出现拼写错误或者要修改值的问题,就需要修改很多地方。
routeConfig 中也包含了所有 Navigation 需要的参数,因此,也可以直接使用循环的方法去生成 MenuItem,实现如下:
import { Menu } from 'antd';
import { RouterState } from 'connected-react-router';
import { useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
import routeConfig from '../../router/routeConfig';
import { AppState } from '../../store/reducers';
function getMenuIte(pathname: string) {
const menuItems = [];
for (const [objKey, objVal] of Object.entries(routeConfig)) {
menuItems.push(
<Menu.Item
key={objKey}
className={pathname === objVal.path ? 'ant-menu-item-selected' : ''}
>
<Link to={objVal.path}>{objVal.name}</Link>
</Menu.Item>
);
}
return menuItems;
}
const Navigation = () => {
// RouterState 可以触发 router. 的提示
const router = useSelector<AppState, RouterState>((state) => state.router);
const pathname = router.location.pathname;
return (
<Menu mode="horizontal" selectable={false}>
{getMenuIte(pathname)}
</Menu>
);
};
export default Navigation;
加上了一点点 CSS 之后的效果如下:
效果和原本的跳转一样,没有任何的变化,但是配置完成之后,其他页面的实现会变得简单不少。
登录
这里先实现登录页面的 UI。
配置登录页面
这里在 routeConfig.ts 文件中新增一个 SIGNIN_PATH 常量,以及在 routeConfig 对象中新增 signin 属性:
export const SIGNIN_PATH = '/signin';
const routeConfig = {
// ...,
signin: {
name: '登录',
path: SIGNIN_PATH,
component: Signin,
title: '登录页面',
subTitle,
isExact: false,
isNavItem: true,
},
};
新建登录组件
适配 Layout 去渲染页面:
import routeConfig from '../../router/routeConfig';
import Layout from './Layout';
const Signin = () => {
return <Layout {...routeConfig.signin}>Signin</Layout>;
};
export default Signin;
Navigation 中是根据 routeConfig 的配置去进行实现的,只要 routeConfig 配置好了,,就不需要再去动 Navigation 了。
实现登录表单
表单同样是通过 antd 实现的组件:Form 表单 去实现的:
它的结构和 Menu 类似:
|- Form # 表单
| |- Form.Item # 表单项,表单相关的属性由它管理
| | |- 元素
这里就基于这个结构进行实现:
import { Button, Form, Input } from 'antd';
import routeConfig from '../../router/routeConfig';
import Layout from './Layout';
const Signin = () => {
return (
<Layout {...routeConfig.signin}>
<Form>
<Form.Item name="email" label="邮箱">
<Input />
</Form.Item>
<Form.Item name="password" label="密码">
<Input.Password />
</Form.Item>
<Form.Item name="email">
<Button type="primary" htmlType="submit">
登录
</Button>
</Form.Item>
</Form>
</Layout>
);
};
export default Signin;
因为登录和注册基本上都是一样的,效果展示等着做完注册页面再写。
注册
配置注册页面
export const SIGNUP_PATH = '/signup';
const routeConfig = {
// ...
signup: {
name: '注册',
path: SIGNUP_PATH,
component: Signup,
title: '注册页面',
subTitle,
isExact: false,
isNavItem: true,
},
};
新建注册组件及组件实现
import { Button, Form, Input } from 'antd';
import routeConfig from '../../router/routeConfig';
import Layout from './Layout';
const Signup = () => {
return (
<Layout {...routeConfig.signup}>
<Form>
<Form.Item name="name" label="昵称">
<Input />
</Form.Item>
<Form.Item name="email" label="邮箱">
<Input />
</Form.Item>
<Form.Item name="password" label="密码">
<Input.Password />
</Form.Item>
<Form.Item name="email">
<Button type="primary" htmlType="submit">
注册
</Button>
</Form.Item>
</Form>
</Layout>
);
};
export default Signup;
最终效果:
到这,UI 差不多也实现完毕了。
以上是关于[React 实战系列] 布局登录注册的页面实现及 Route 的封装的主要内容,如果未能解决你的问题,请参考以下文章
前端实战|React18极客园——布局模块(useRoutes路由配置处理Token失效退出登录)