从零学习React+TS项目搭建(二)

本文接着上篇文章,继续路由配置+登录拦截+ant样例+redux样例。

添加多个目录结构

src
├─ components
│  └─ withRouter.tsx
├─ router
│  └─ index.tsx
├─ pages
│  ├─ layout
│  │  ├─ index.scss
│  │  └─ index.tsx
│  ├─ home
│  │  └─ index.tsx
│  ├─ 404
│  │  └─ index.tsx
│  ├─ login
│  │  └─ index.tsx
│  ├─ table
│  │  └─ index.tsx

安装路由

安装依赖react-router-dom这里用最新的v6版本

npm i react-router-dom

在src目录添加自定义路由高阶组件 withRouter.tsx

import { useParams, useLocation, useNavigate, useSearchParams } from 'react-router-dom';

import React, { ComponentType, PropsWithChildren } from 'react';

export interface WithRouterProps<T = ReturnType<typeof useParams>> extends PropsWithChildren {
  history: {
    back: () => void;
    goBack: () => void;
    location: ReturnType<typeof useLocation>;
    push: (url: string, state?: any) => void;
  };
  location: ReturnType<typeof useLocation>;
  params: T;
  searchParams: T;
  navigate: ReturnType<typeof useNavigate>;
}

const withRouter = <P extends object>(Component: ComponentType<P>) => {
  // 相当于给 MyCard组件添加各种props属性,还添加三个重要的属性params,location,navigate
  return (props: Omit<P, keyof WithRouterProps>) => {
    const location = useLocation();
    const params = useParams();
    const searchParams = useSearchParams();
    const navigate = useNavigate();
    const history = {
      back: () => navigate(-1),
      goBack: () => navigate(-1),
      location,
      push: (url: string, state?: any) => navigate(url, { state }),
      replace: (url: string, state?: any) =>
        navigate(url, {
          replace: true,
          state
        })
    };
    return (
      <Component
        history={history}
        location={location}
        navigate={navigate}
        params={params}
        searchParams={searchParams}
        {...(props as P)}
      />
    );
  };
};

export default withRouter;

路由拦截和路由配置 router/index.tsx

import Login from '../pages/login/index';
import Layout from '../pages/layout/index';
import Home from '../pages/home/index';
import Other from '../pages/other/index';
import NotPage from '../pages/404/index';
import { useRoutes, Navigate, RouteObject } from 'react-router-dom';
import React from 'react';

function getItem(
  path: RouteObject['path'],
  element: RouteObject['element'],
  children?: RouteObject['children']
): RouteObject {
  return {
    path,
    element,
    children
  } as RouteObject;
}

// 设置路由的地方
const routers: RouteObject[] = [
  getItem('/login', <Login></Login>),
  getItem('/', <Layout></Layout>, [
    getItem('/', <Home></Home>),
    getItem('/other', <Other></Other>)
  ]),
  getItem('*', <NotPage></NotPage>)
];

const isUserAuthenticated = () => {
  const token = localStorage.getItem('token');
  return token && token.length > 0;
};

const AuthRoute: React.FC<React.PropsWithChildren> = (props: React.PropsWithChildren) => {
  if (!isUserAuthenticated()) {
    return (
      <div>
        {props.children}
        <Navigate to="/login" />
      </div>
    );
  }
  return <div>{props.children}</div>;
};
AuthRoute.displayName = 'AuthRoute';

const RouterInterceptor: React.FC = (props: React.PropsWithChildren) => {
  const routerEls = useRoutes(routers);
  return <AuthRoute>{routerEls}</AuthRoute>;
};
RouterInterceptor.displayName = 'RouterInterceptor';
export default RouterInterceptor;

登录页面login/index.tsx

import React from 'react';
import { Input, Form, Button, Space } from 'antd';
import withRouter, { WithRouterProps } from '@/components/withRouter';

const Login: React.FC<WithRouterProps> = (props: WithRouterProps) => {
  const [form] = Form.useForm();
  const layout = {
    labelCol: { span: 8 },
    wrapperCol: { span: 16 },
    style: { maxWidth: 200 }
  };

  const tailLayout = {
    wrapperCol: { offset: 8, span: 16 }
  };
  const onFinish = (values: { token: string }) => {
    localStorage.setItem('token', values.token);
    props.history.push('/');
  };

  const onReset = () => {
    form.resetFields();
  };

  return (
    <div>
      <Form {...layout} form={form} onFinish={onFinish}>
        <Form.Item name="token" label="Token" rules={[{ required: true }]}>
          <Input />
        </Form.Item>
        <Form.Item {...tailLayout}>
          <Space>
            <Button type="primary" htmlType="submit">
              登录
            </Button>
            <Button htmlType="button" onClick={onReset}>
              重置
            </Button>
          </Space>
        </Form.Item>
      </Form>
    </div>
  );
};
export default withRouter(Login);

主布局页面layout/index.tsx

import React from 'react';
import type { MenuProps } from 'antd';
import { Layout, Menu } from 'antd';
import { Outlet } from 'react-router-dom';
import withRouter, { WithRouterProps } from '@/components/withRouter';
import { HomeOutlined, UnorderedListOutlined } from '@ant-design/icons';
import './index.scss';

const { Header, Content, Footer, Sider } = Layout;

type MenuItem = Required<MenuProps>['items'][number];

function getItem(
  label: React.ReactNode,
  key: React.Key,
  icon?: React.ReactNode,
  children?: MenuItem[],
  type?: 'group'
): MenuItem {
  return {
    key,
    icon,
    children,
    label,
    type
  } as MenuItem;
}

class LayoutContent extends React.Component<WithRouterProps> {
  state: Readonly<{
    menuIndex: string;
    items: MenuItem[];
    collapsed: boolean;
    contentMarginLeft: number;
  }>;

  constructor(props: WithRouterProps) {
    super(props);
    const { location } = this.props;
    const menuItems: MenuItem[] = [
      getItem('Home', '/', <HomeOutlined />),
      getItem('Table', '/table', <UnorderedListOutlined />)
    ];
    this.state = {
      menuIndex: location.pathname,
      items: menuItems,
      collapsed: false,
      contentMarginLeft: 200
    };
  }

  onClick(index: { key: string }) {
    console.log(index);
    this.props.navigate(index.key);
  }

  onCollapse(value: boolean) {
    this.setState({ collapsed: value });
    this.setState({ contentMarginLeft: value ? 80 : 200 });
  }

  componentDidUpdate(
    prevProps: Readonly<WithRouterProps>,
    prevState: Readonly<object>,
    snapshot?: any
  ): void {
    if (this.props.location.pathname !== this.state.menuIndex) {
      this.setState({ menuIndex: this.props.location.pathname });
    }
  }

  render() {
    return (
      <Layout style={{ minHeight: '100vh' }} hasSider>
        <Sider
          collapsible
          collapsed={this.state.collapsed}
          style={{
            overflow: 'auto',
            height: '100vh',
            position: 'fixed',
            left: 0,
            top: 0,
            bottom: 0
          }}
          onCollapse={(value) => this.onCollapse(value)}
        >
          <div className="demo-logo-vertical" />
          <Menu
            theme="dark"
            onClick={this.onClick.bind(this)}
            selectedKeys={[this.state.menuIndex]}
            mode="inline"
            items={this.state.items}
          />
        </Sider>
        <Layout style={{ marginLeft: this.state.contentMarginLeft }}>
          <Header style={{ padding: 0, backgroundColor: '#fff' }} />
          <Content style={{ margin: '0 16px' }}>
            <Outlet />
          </Content>
          <Footer style={{ textAlign: 'center' }}>
            Demo ©{new Date().getFullYear()} Created by Demo
          </Footer>
        </Layout>
      </Layout>
    );
  }
}

export default withRouter(LayoutContent);

// index.scss
.demo-logo-vertical {
  height: 32px;
  margin: 16px;
  background: rgba(255, 255, 255, 0.2);
  border-radius: 6px;
}

首页redux样例home/index.tsx

import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement, CounterState } from '@/features/counter/counterSlice';
import { Button } from 'antd';

const Home: React.FC = () => {
  const count = useSelector((state: { counter: CounterState }) => state.counter.value);
  const dispatch = useDispatch();
  return (
    <div>
      <h3>这个是redux的例子:</h3>
      <div style={{ display: 'flex' }}>
        <Button aria-label="Increment value" onClick={() => dispatch(increment())}>
          Increment
        </Button>
        <div style={{ minWidth: '20px', lineHeight: '30px', textAlign: 'center' }}>{count}</div>
        <Button aria-label="Decrement value" onClick={() => dispatch(decrement())}>
          Decrement
        </Button>
      </div>
    </div>
  );
};
export default Home;

404/index.tsx

import React from 'react';
const NotPage: React.FC = () => {
  return <div>404</div>;
};
export default NotPage;

ant Form + Table结合的页面 table/index.tsx

import React, { useState } from 'react';
import { Button, Table, Form, Row, Col, Space, Input } from 'antd';
import { DownOutlined } from '@ant-design/icons';
import type { TableColumnsType, TablePaginationConfig } from 'antd';

interface DataType {
  key: React.Key;
  name: string;
  age: number;
  address: string;
}

const columns: TableColumnsType<DataType> = [
  {
    title: 'Name',
    dataIndex: 'name'
  },
  {
    title: 'Age',
    dataIndex: 'age'
  },
  {
    title: 'Address',
    dataIndex: 'address'
  }
];

const data: DataType[] = [];
for (let i = 0; i < 46; i++) {
  data.push({
    key: i,
    name: `Edward King ${i}`,
    age: 32,
    address: `London, Park Lane no. ${i}`
  });
}

interface SearchFormFields {
  label: string;
  name: string;
  element: any;
  rule?: object[];
}

const fields: SearchFormFields[] = [
  {
    label: 'Name',
    name: 'name',
    element: <Input placeholder="请输入" />
  },
  {
    label: 'Age',
    name: 'age',
    element: <Input placeholder="请输入" />
  },
  {
    label: 'Address',
    name: 'address',
    element: <Input placeholder="请输入" />
  }
];

const TableList: React.FC = () => {
  const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
  const [loading, setLoading] = useState(false);
  const [expand, setExpand] = useState(false);

  const [form] = Form.useForm();

  const getFields = () => {
    const count = expand ? fields.length : 3;
    const children = [];
    for (let i = 0; i < count; i++) {
      const element = fields[i];
      children.push(
        <Col span={8} key={i}>
          <Form.Item name={element.name} label={element.label} rules={element.rule}>
            {element.element}
          </Form.Item>
        </Col>
      );
    }
    return children;
  };

  const pageSetting: TablePaginationConfig = {
    showSizeChanger: true,
    showQuickJumper: true,
    showTotal: (total) => `Total ${total} items`
  };

  const start = () => {
    setLoading(true);
    // ajax request after empty completing
    setTimeout(() => {
      setSelectedRowKeys([]);
      setLoading(false);
    }, 1000);
  };

  const onSelectChange = (newSelectedRowKeys: React.Key[]) => {
    console.log('selectedRowKeys changed: ', newSelectedRowKeys);
    setSelectedRowKeys(newSelectedRowKeys);
  };

  const rowSelection = {
    selectedRowKeys,
    onChange: onSelectChange
  };

  const formStyle: React.CSSProperties = {
    maxWidth: 'none',
    padding: 24
  };

  const onFinish = (values: any) => {
    console.log('Received values of form: ', values);
  };

  const hasSelected = selectedRowKeys.length > 0;
  return (
    <div>
      <Form form={form} name="advanced_search" style={formStyle} onFinish={onFinish}>
        <Row gutter={24}>{getFields()}</Row>
        <div style={{ textAlign: 'right' }}>
          <Space size="small">
            <Button type="primary" htmlType="submit">
              Search
            </Button>
            <Button
              onClick={() => {
                form.resetFields();
              }}
            >
              Clear
            </Button>
            {fields.length > 3 ? (
              <a
                style={{ fontSize: 12 }}
                onClick={() => {
                  setExpand(!expand);
                }}
              >
                <DownOutlined rotate={expand ? 180 : 0} /> Collapse
              </a>
            ) : (
              <span></span>
            )}
          </Space>
        </div>
      </Form>
      <Table
        pagination={pageSetting}
        rowSelection={rowSelection}
        columns={columns}
        dataSource={data}
      />
    </div>
  );
};
export default TableList;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容