[React 实战系列] 布局登录注册的页面实现及 Route 的封装

Posted GoldenaArcher

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[React 实战系列] 布局登录注册的页面实现及 Route 的封装相关的知识,希望对你有一定的参考价值。

之前差不多将配置都实现得差不多了,现在就开始进行页面的实现。

回顾一下上一篇文章中实现的效果:

这篇会从这里开始,完成 布局注册登录 三个部分的实现;以及对 Route 的封装,这是为了方便后面业务的运行,目前对路由只是进行了初步封装,等我过一下 TypeScript 部分的内容,还能够再精简一些。

顺便之前本来以为是购物车的页面看起来好像是商城页面,所以改成了 Shop。

这部分实现的资源在:React实战系列-布局、登录、注册的页面实现及 Route 的封装

之前已经实现的内容:

Layout

从页面结构上来说,每个页面上都会出现一个导航栏以及页头,这里就是用 antd 提供的两个组件实现:MenuPageHeader

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 的封装的主要内容,如果未能解决你的问题,请参考以下文章

[React 实战系列] 注册功能的优化

Android实战学习了多个控件实现登录及记住密码功能

Flask博客实战 - 实现登录注册功能

前端实战|React18极客园——布局模块(useRoutes路由配置处理Token失效退出登录)

Vue项目实战——基于 Vue3.x + Vant UI实现一个多功能记账本(登录注册页面,验证码)

[React 实战系列] 项目开始前的准备工作