[React 實戰系列] 布局、登錄、注冊的頁面實作及 Route 的封裝
- Layout
- 導航欄(Menu)
- 頁頭(PageHeader)
- 重構 Routes
- 新增 routeConfig.ts
- 修改 Routes.tsx
- 修改 Home.tsx
- 修改 Shop.tsx
- 修改 Navigation.tsx
- 登錄
- 配置登錄頁面
- 新建登錄組件
- 實作登錄表單
- 注冊
- 配置注冊頁面
- 新建注冊組件及組件實作
之前差不多將配置都實作得差不多了,現在就開始進行頁面的實作,
回顧一下上一篇文章中實作的效果:
這篇會從這里開始,完成 布局、注冊、登錄 三個部分的實作;以及對 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 差不多也實作完畢了,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/294988.html
標籤:其他
