前端所有代码已经同步到 GitHub Repo: EthanLuu/soomooc: React + TypeScript 实践,在线教学直播平台。 (github.com)

项目的界面只是初步设计,后续还会随着功能的变更而更改界面

先去 iconfont-阿里巴巴矢量图标库 上挑选一个合适的图标作为网站的 LOGO。

icon

下载之后放到 assets 文件夹中。

新建 /components/header.tsx 组件,用来存放我们网站的公共页头。

简单写一点样式,用上 antd 提供的布局组件和按钮组件等。

我这边写了很多行内样式,后续考虑如果需要复用的话会抽象成组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import styled from '@emotion/styled'
import { Button, Layout, Menu, Row, Typography } from 'antd'
import ButtonGroup from 'antd/lib/button/button-group'
import logo from 'assets/logo.svg'
import { Search } from 'components/search'

export const Header = () => {
return (
<Layout.Header
style={{
background: 'white',
boxSizing: 'border-box',
boxShadow: '0 2px 8px #f0f1f2',
}}
>
<Row style={{ height: '6.4rem', alignItems: 'center' }}>
<a href={'/'} style={{ width: '20rem' }}>
<Logo src={logo} alt="logo" />
<Typography.Title
level={3}
style={{
lineHeight: '6.4rem',
marginBottom: 0,
marginLeft: '6rem',
}}
>
SooMooc
</Typography.Title>
</a>
<Menu
theme="light"
mode="horizontal"
defaultSelectedKeys={['1']}
style={{ background: 'white', border: 'none', flex: 'auto' }}
>
<Menu.Item key="1">首页</Menu.Item>
<Menu.Item key="2">课程</Menu.Item>
</Menu>
<Search />
<ButtonGroup
style={{
alignItems: 'center',
}}
>
<Button type="link">注册</Button>
<Button type="link">登陆</Button>
</ButtonGroup>
</Row>
</Layout.Header>
)
}

const Logo = styled.img`
height: 5rem;
width: 5rem;
float: left;
margin: 0.7rem 0;
`

单独写的一个搜索框的组件 /components/search.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
import { Input } from 'antd'

export const Search = () => {
const handleSearch = () => {}
return (
<Input.Search
placeholder={"搜索课程"}
style={{ width: 300, paddingRight: '5rem' }}
onSearch={handleSearch}
/>
)
}

效果图,还不错。

image-20210511225100310

我直接用 antd 的组件写了一个很简单的页脚。

1
2
3
4
5
6
7
8
9
import { Layout } from 'antd'

export const Footer = () => {
return (
<Layout.Footer style={{ textAlign: 'center' }}>
SooMooc ©2021 Created by EthanLoo
</Layout.Footer>
)
}

image-20210513210253874

Content

先把首页的大致样子做了一下。

参考的网站为慕课网

image-20210513205625398

包括两个最主要的组件。

  1. SideMenu,左边的可扩展菜单。
  2. Carousel,右边的轮播图(走马灯)。

在写组件前,首先把需要的数据,存到 __json_server_mock__/db.json 文件中去。

首先是菜单的内容,每个菜单可以包括子菜单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
"sideMenuList": [
{
"id": 1,
"title": "前端开发: HTML5 / Vue.js / Node.js",
"subMenu": [
{
"id": 8,
"title": "HTML5"
},
{
"id": 9,
"title": "Vue.js"
},
{
"id": 10,
"title": "Node.js"
}
]
}
//...
]

然后我去网上随便找了几张图,并且使用 PicGo 上传到了自己的图床上。

image-20210513212032028

再把对应的图片的地址存到 db.json 文件中,需要保证通过接口可以获取对应地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
"carousel": [
{
"id": 1,
"title": "carousel-1",
"url": "https://cdn.ethanloo.cn/img/20210513154859.webp"
},
{
"id": 2,
"title": "carousel-2",
"url": "https://cdn.ethanloo.cn/img/20210513154902.webp"
},
{
"id": 3,
"title": "carousel-3",
"url": "https://cdn.ethanloo.cn/img/20210513154901.webp"
},
{
"id": 4,
"title": "carousel-4",
"url": "https://cdn.ethanloo.cn/img/20210513154900.webp"
}
]

然后开始写主页相关的组件,都放在 src/screens/home 文件夹下。

image-20210513212401221

主页内容框架组件 content.tsx,将 CarouselSideMenu 整合到一起,在这儿使用 antd 提供的 Grid 布局,让左边的菜单占总宽度的 1/4,右边的轮播图占总宽度的 3/4。

为了让网站更美观,我还给这个 SideMnuCarousel 组合起来的外边界加了个阴影。

使用的是这个工具,可以直接通过可视化界面写出阴影的 CSS 代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import styled from '@emotion/styled'
import { Row, Col } from 'antd'
import { Carousel } from 'screens/home/carousel'
import { SideMenu } from './side-menu'

export const Content = () => {
return (
<Container>
<Banner>
<Col span={6}>
<SideMenu />
</Col>
<Col span={18}>
<Carousel />
</Col>
</Banner>
</Container>
)
}

const Container = styled.div`
padding: 3rem 10rem;
margin: 0 auto;
min-height: calc(100vh - 134px);
`

const Banner = styled(Row)`
border-radius: 1rem;
overflow: hidden;
box-shadow: 0 1.9px 4px rgba(0, 0, 0, 0.044),
0 4.6px 13.4px rgba(0, 0, 0, 0.066), 0 26px 60px rgba(0, 0, 0, 0.11);
`

接下去继续写 SideMenu.tsx,这是一个比较经典的流程(就我目前理解而言)。

  • 利用 MenuItemProp 接口来定义每个菜单项的数据格式。
  • 利用 useState 将菜单的数据存储在状态中。
  • 使用 useEffect 在首次加载该页面的时候,通过 API 获取所有菜单的数据,并且更新状态。
  • 使用状态中的数据生成组件并返回。

这个 SideMenu 组件唯一特殊的点就是我把菜单的数据结构定义为了统一的,也就是说无论是否有父菜单还是子菜单,都使用一样的数据结构。在渲染菜单的时候,通过利用函数的递归,来渲染父菜单和子菜单项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import styled from '@emotion/styled'
import { Menu } from 'antd'
import { useEffect, useState } from 'react'
import { http } from 'utils/http'

interface MenuItemProp {
id: number
title: string
subMenu?: MenuItemProp[]
}

export const SideMenu = () => {
const { SubMenu } = Menu
const [menuItems, setMenuItems] = useState<MenuItemProp[]>([])
useEffect(() => {
http('sideMenuList').then((MenuItems: MenuItemProp[]) => {
setMenuItems(MenuItems)
})
}, [])

const renderMenu = (items: MenuItemProp[]) => {
return items.map((item) => {
if (item.subMenu) {
return (
<SubMenu key={item.id} title={item.title} style={{ flex: 'auto' }}>
{renderMenu(item.subMenu)}
</SubMenu>
)
} else {
return <Menu.Item key={item.id}>{item.title}</Menu.Item>
}
})
}

return <MenuContainer mode="vertical">{renderMenu(menuItems)}</MenuContainer>
}

const MenuContainer = styled(Menu)`
display: flex;
flex-direction: column;
justify-items: space-around;
height: 100%;
padding: 1rem 0 0.5rem 1rem;
`

Carousel.tsx走马灯组件相比而言更简单一些,利用 antd 提供的 Carousel 组件可以很容易写出。

定义数据结构和调取接口的流程和上一个菜单组件一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import styled from '@emotion/styled'
import { Image, Carousel as AntCarousel } from 'antd'
import { useEffect, useState } from 'react'
import { http } from 'utils/http'

interface CarouselImageProp {
id: number
title: string
url: string
}

export const Carousel = () => {
const [imageUrls, setImageUrls] = useState<CarouselImageProp[]>([])

useEffect(() => {
http('carousel').then((urls: CarouselImageProp[]) => {
setImageUrls(urls)
})
}, [])

return (
<CarouselContainer autoplay>
{console.log(imageUrls)}
{imageUrls.map((image) => (
<Image src={image.url} key={image.id} object-fit={'cover'} />
))}
</CarouselContainer>
)
}

const CarouselContainer = styled(AntCarousel)`
text-align: center;
height: 40rem;
line-height: 40rem;
overflow: hidden;
`

OK,组件写完了,再把 Content 组件放到 App.tsx 中去吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import './App.css'
import { ErrorBoundary } from 'components/error-boundary'
import { FullPageErrorFallback } from 'components/lib'
import { Header } from 'components/header'
import { Footer } from 'components/footer'
import { Content } from 'screens/home/content'

function App() {
return (
<div className="App">
<ErrorBoundary fallbackRender={FullPageErrorFallback}>
<Header />
<Content />
<Footer />
</ErrorBoundary>
</div>
)
}

export default App

有没有发现多了一个 ErrorBoundary 组件?这是一个专门用来捕获和展示错误的组件。就不细讲了,有兴趣可以直接看源码。

赶快运行一下看看吧。

1
2
3
4
5
# 启动 json-server
yarn mock
# 启动 React
yarn start
# 这两个需要放在两个命令行窗口运行

动画

Thumbnail (1)-tuya