山软创新实训前端开发记录
【创新实训】前端开发记录(一):基础篇
【创新实训】前端开发记录(二):主页面
【创新实训】前端开发记录(三):特殊效果
前端组件库
该项目使用Material-UI作为组件库。这是一个符合Material Design设计规范的React组件库。
使用以下命令分别安装Material-UI的核心组件库、图标包和实验性功能。
$ npm install @material-ui/core
$ npm install @material-ui/icons
$ npm install @material-ui/lab
主页面设计
需求描述
主页面分为两个模块,分别为“电影展厅”和“表格展示”。电影展厅模块将电影列表以海报+评分的格式展现给用户,并可以给用户提供部分电影的信息(例如导演、主演等)。电影列表模块则使用表格详细列举电影信息,并可以按照电影评分和上映日期进行排序。
为了适应不同尺寸设备的浏览需求,需要使用响应式技术制作页面,达到在PC、平板和手机上都能有较好的浏览体验的效果。
效果预览
PC端
移动端
设计拆分
主页布局
主页面的整块布局大体可以分为:左侧抽屉Drawer
,顶部标题栏ToolBar
,搜索区域以及数据展示区域。其中除了数据展示区域在两个模块中无法共用,其余组件都可以进行复用。
抽屉中放入切换“电影展厅”模式和“列表展示”模式的按钮,并在PC端屏幕中位于左侧常驻,在移动端屏幕中隐藏,通过标题栏左侧的汉堡图标点击打开。这样设计也比较符合操作逻辑。
搜索栏
目前搜索栏的功能需求为:按照电影名称、评分、上映日期、主演、导演和来源等参数为限制进行搜索。电影名称、主演和导演为需要手动输入的文本框;评分和上映日期为了方便起见,除了文本框外还提供一个滑块,便于用户进行操作;来源提供一个下拉框供用户选择。
数据展示区域
对于电影展厅模式而言,PC端需要海报、电影名称和评分从上至下排布作为一个卡片,鼠标移到图片上时,卡片变大,展示更多信息。而这种操作和布局对于移动端来说是不可接受的,移动端需要采用更为直观的卡片展示方式:即一行一个卡片,海报和详细信息左右排布。
对于列表展示模式,PC端与移动端可以共用一个表格,只需要保证表格能够在数据超出时进行滑动即可。
部分代码实现
TopBar
左侧抽屉的宽度定义为标准的240px,样式使用Material-UI提供的makeStyles
函数动态生成,无需在CSS文件中定义。动态生成样式的好处是可以更方便地绘制响应式布局。
const drawerWidth = 240;
const navLabels = [ '电影展厅', '表格展示' ]
const navIcons = [ <AppIcon />, <ArtTrackIcon /> ]
const drawer = (
<div>
<div className={classes.toolbar} />
<Divider />
<List>
{navLabels.map((text, index) => (
<ListItem
button key={text}
selected={props.mode === index}
onClick={() => handleListItemClick(index)}>
<ListItemIcon>{navIcons[index]}</ListItemIcon>
<ListItemText primary={text} />
</ListItem>
))}
</List>
</div>
);
加上AppBar
和Drawer
组件后的JSX如下:
<div className={classes.root}>
<AppBar position="fixed" className={classes.appBar}>
<Toolbar>
<IconButton>
<MenuIcon />
</IconButton>
<Typography variant="h6" noWrap>
MVRankings 电影评价网
</Typography>
</Toolbar>
</AppBar>
<nav className={classes.drawer} aria-label="main">
<Hidden mdUp implementation="css">
<Drawer>
{drawer}
</Drawer>
</Hidden>
<Hidden smDown implementation="css">
<Drawer>
{drawer}
</Drawer>
</Hidden>
</nav>
<main className={classes.content}>
<div className={classes.toolbar} />
{props.child}
</main>
</div>
SearchBar
搜索栏部分使用ExpansionPanel
组件作为容器,并使用Grid
组件进行输入组件的编排。Grid
使用flex进行弹性布局,能够很好地兼容不同尺寸的屏幕。配合Material-UI的Grid
栅格系统,可以十分便捷地定义不同尺寸屏幕下组件所占大小。
表格区域的HTML如下:
<form noValidate autoComplete="off" style={{width: '100%'}}>
<Grid container spacing={4}>
<Grid item xs={12} sm={6} md={6} lg={4} xl={3}>
<SearchBarMovieName data={props.data} update={props.update} disabled={props.loading} />
</Grid>
<Grid item xs={12} sm={6} md={6} lg={4} xl={3}>
<SearchBarMovieRate data={props.data} update={props.update} disabled={props.loading} />
......
</Grid>
</form>
由于搜索数据需要被搜索栏和数据展示区域共享,所以这里利用React的“状态提升”,将搜索的数据保存至共同父组件的state中,即包含了这些组件的Page。
class HomePage extends React.Component {
constructor(props) {
super(props)
const movieSources = [
'任意', '豆瓣', '猫眼', '时光网'
]
this.state = {
loading: false,
data: {},
searchData: {
name: '',
rate_min: 0,
rate_max: 10,
time_min: 1895,
time_max: new Date().getFullYear() + 5,
directors: '',
stars: '',
source: movieSources[0]
},
totalPages: 1,
curPage: 1,
......
}
}
......
}
其中,输入框数据更改、提交查询等操作也转移到了父组件中,由HomePage
进行数据的请求和加载工作。
List
电影展厅模块
电影展厅模式的数据展示区域需要为PC端和移动端准备两个List,这里就利用makeStyles
函数动态生成样式,在屏幕尺寸的间断点切换display
属性,就能够做到移动端和PC端的不同显示了。这种做法虽然在开发上非常便捷,但带来一个问题,即同样的数据生成了2个不同的DOM元素,有影响性能之嫌,好在每页的数据量不会很大,性能问题可以忽略不计。
const useStyles = makeStyles((theme) => {
......
desktop: {
[theme.breakpoints.down('sm')]: {
display: 'none'
},
},
mobile: {
[theme.breakpoints.up('md')]: {
display: 'none'
},
}
})
export default function SquareList(props) {
......
return (
<>
<Grid className={classes.desktop} container justify="space-evenly" spacing={6} style={{flexGrow: 1}} onMouseEnter={moveHandler.cancelAll}>
{props.loading || props.data.data === undefined ? getLoadingItems() : getItems() }
</Grid>
<Grid className={classes.mobile} container justify="space-evenly" spacing={3} style={{flexGrow: 1}}>
{props.loading || props.data.data === undefined ? getMobileLoadingItems() : getMobileItems() }
</Grid>
</>
)
}
实现PC端鼠标移过卡片时卡片放大的效果,单纯地在CSS中使用:hover
伪元素实现起来比较困难,这时可以借助每个卡片的state来实现。分别在卡片组件的onMouseEnter
和onMouseLeave
事件中更新是否需要展示详情的状态即可实现。
handleMouseEnter() {
this.setState({
hover: true
})
}
handleMouseLeave() {
this.setState({
hover: false
})
}
return (
...
<div onMouseLeave={this.handleMouseLeave}>
详情卡片内容...
</div>
<div onMouseEnter={this.handleMouseEnter}>
卡片内容...
</div>
...
)
但是这种方法有一个弊端,即鼠标快速在多个卡片上滑过时,有大概率一些卡片的onMouseEnter
事件和onMouseLeave
事件不会触发,最终导致即使鼠标移开,也有一些卡片呈详情状态,严重影响使用体验。
这里的解决方案如下:使用一个额外的控制器控制所有卡片是否放大的状态,在一个卡片放大时,保证其余所有卡片都不会呈放大的状态。在鼠标移开卡片(也就是移入了列表的空白区域)时,对所有卡片进行取消放大的操作。
这里再次用到了状态提升。不过这一次是另一种做法。在卡片的父组件(列表)中使用一个全局的MoveHandler
保存所有卡片的this
引用,并在MoveHandler
中调用this.setState()
函数进行卡片状态的更新。列表在生成每个卡片时,调用handler的regComponent
函数进行组件的注册(即this
引用的注册)。如此一来,便解决了上述问题。
class MoveHandler {
components = {}
constructor() {
this.cancelAll = this.cancelAll.bind(this)
}
requestEnter(key) {
Object.keys(this.components).forEach(k => {
if (key !== k) {
this.requestQuit(k)
}
})
if (this.components[key]) {
this.components[key].setState({ hover: true })
}
}
requestQuit(key) {
if (this.components[key]) {
this.components[key].setState({ hover: false })
}
}
regComponent(card, key) {
this.components[key] = card
}
cancelAll() {
Object.keys(this.components ?? {}).forEach(k => {
this.requestQuit(k)
})
}
}
表格展示模块
该部分与电影展厅模块的列表实现非常相似,只是需要额外编写一个支持排序的表头。在列名旁边加上一个箭头即可,向下表示倒序,向上表示正序。Material-UI已经准备好了Table
组件相关的Demo可供参考。我们只需要处理排序的事件即可。
对于排序的处理,同样是交由HomePage
进行。HomePage
的状态中保存该列表的排序关键字和排序顺序,触发排序的函数中及时更新排序状态。
class HomePage extends React.Component {
constructor(props) {
super(props)
this.state = {
...
orderBy: 'rate_douban',
order: 'desc'
}
...
}
handleOrderChange(prop, order) {
this.setState({
orderBy: prop,
order: order
}, () => {
this.doSearch(this.state.searchData, this.state.curPage)
})
}
...
}
总结
主页面是这个项目中非常重要的一个部分,负责了大量的数据操作和展示的部分。主页面对于响应式UI和操作逻辑的要求也十分严格,在编码的过程中也出现了不少问题,包括网络数据在加载过程中的占位动画、数据加载出错时的应对处理等。