本节代码对应 GitHub 分支: chapter6

仓库传送门 (opens new window)

# 构建路由

榜单详情页面在这里我们需要构建一个专门的路由,目前我们就以推荐歌单的数据来完成详情页开发。

首先在 routes/index.js 中,

import Album from '../application/Album';

// 在 /recommend 后面加上子路由
{
  path: "/recommend",
  component: Recommend,
  routes: [
    {
      path: "/recommend/:id",
      component: Album
    }
  ]
},

然后在 component/list/index.js 中设置跳转:

const enterDetail = (id) => {
  props.history.push (`/recommend/${id}`)
}
// 加入事件绑定逻辑
<ListItem key={item.id} onClick={() => enterDetail (item.id)}>
//...

注意,这里 List 组件作为 Recommend 的子组件,并不能从 props 拿到 history 变量,无法跳转路由。有两种解决方法:

  1. 将 Recommend 组件中 props 对象中的 history 属性传给 List 组件
  2. 将 List 组件用 withRouter 包裹

这里我们用第二种方式:

//List/index.js
import { withRouter } from 'react-router-dom';

// 省略组件代码

// 包裹
export default React.memo (withRouter (RecommendList));

这样,现在就能拿到 history 变量,顺利进行路由跳转了。

但是,Album 组件现在并没有编写。简单来写一下:

//src/application/Album/index.js
import React from 'react';
import {Container} from './style';

function Album (props) {
  return (
    <Container>
    </Container>
  )
}

export default Album;

在同目录下的 style.js:

import styled from'styled-components';
import style from '../../assets/global-style';

export const Container = styled.div`
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 100;
  background: #fff;
`

现在你点击一个歌单,url 地址确实变化了,但页面却没有任何改变,这是什么原因呢?这里我卖个关子,给大家提供一个解决问题的思路。 第一反应是层叠上下文的问题吗?结果改变了 z-index 值,页面还是一样。说明并不是这个问题。

接着,这个组件究竟渲染了没?在 Album/index.js 的组件函数中,输出一些内容,到页面中,跳转后这些内容并未输出。此时,可以断定是组件没有渲染的问题。但是路由都改变了,配置也没错,怎么会出现这个问题呢?在这个时候,就考验我们对路由配置原理的理解了,具体来说就是 renderRoutes 方法。这个方法中传入参数为路由配置数组,我们在组件中调用这个方法后只能渲染一层路由,再深层的路由就无法渲染。

因此,我们现在在 Recommend 组件中加入这些逻辑即可:

有人说下面的props.route.routes有问题,是因为之前的子路由名称写成了 children 而不是 routes,这里默认配置项的子路由名字都是 routes

import { renderRoutes } from 'react-router-config';

// 返回的 JSX
<Content>
  // 其他代码
  // 将目前所在路由的下一层子路由加以渲染
  { renderRoutes (props.route.routes) }
</Content>

现在就有跳转效果了。

# 动画实现

本项目所有的过渡动画采用成熟的第三方库 react-transition-group。首先安装:

npm install react-transition-group --save

接下来我们来初步地使用:

//Album/index.js
import React, {useState} from 'react';
import {Container} from './style';
import { CSSTransition } from 'react-transition-group';

function Album (props) {
  const [showStatus, setShowStatus] = useState (true);

  return (
    <CSSTransition
      in={showStatus}
      timeout={300}
      classNames="fly"
      appear={true}
      unmountOnExit
      onExited={props.history.goBack}
    >
      <Container>
      </Container>
    </CSSTransition>
  )
}

export default React.memo (Album);

然后在相应的 style.js 中,

import styled from'styled-components';
import style from '../../assets/global-style';

export const Container = styled.div`
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 1000;
  background: ${style ["background-color"]};
  transform-origin: right bottom;
  &.fly-enter, &.fly-appear {
    transform: translate3d (100%, 0, 0);
  }
  &.fly-enter-active, &.fly-appear-active {
    transition: transform .3s;
    transform: translate3d (0, 0, 0);
  }
  &.fly-exit {
    transform: translate3d (0, 0, 0);
  }
  &.fly-exit-active {
    transition: transform .3s;
    transform: translate3d (100%, 0, 0);
  }
`

现在你回到首页,然后点击一个歌单,你会看到一个滑入的动画。但是作为一个精致的项目,这个效果还不够,完整版的项目里面呈现的是一个切入的效果,那这个如何来实现?

也算是一个经验吧,这里直接分享给大家。需要把握两点:

  1. 设定 transfrom 的固定点,接下来的动画都是绕这个点旋转或平移
  2. 设置 rotateZ 的值,让整个页面能够拥有 Z 坐标方向的矢量

修改后如下:

  // 动画样式代码
  transform-origin: right bottom;
  &.fly-enter, &.fly-appear {
    transform: rotateZ (30deg) translate3d (100%, 0, 0);
  }
  &.fly-enter-active, &.fly-appear-active {
    transition: transform .3s;
    transform: rotateZ (0deg) translate3d (0, 0, 0);
  }
  &.fly-exit {
    transform: rotateZ (0deg) translate3d (0, 0, 0);
  }
  &.fly-exit-active {
    transition: transform .3s;
    transform: rotateZ (30deg) translate3d (100%, 0, 0);
  }

这个切入的动画就完成了。同样离开页面的时候,也有切出的动画。要检验整个效果,我们先来准备好路由的跳转。

# Header 基础组件开发

由于比较简单,就直接贴上 Header 组件的代码了。

//baseUI/header/index.js
import React from 'react';
import styled from'styled-components';
import style from '../../assets/global-style';
import PropTypes from "prop-types";

const HeaderContainer = styled.div`
  position: fixed;
  padding: 5px 10px;
  padding-top: 0;
  height: 40px;
  width: 100%;
  z-index: 100;
  display: flex;
  line-height: 40px;
  color: ${style ["font-color-light"]};
  .back {
    margin-right: 5px;
    font-size: 20px;
    width: 20px;
  }
  >h1 {
    font-size: ${style ["font-size-l"]};
    font-weight: 700;
  }
`
// 处理函数组件拿不到 ref 的问题,所以用 forwardRef
const Header = React.forwardRef ((props, ref) => {
  const { handleClick, title} = props;
  return (
    <HeaderContainer ref={ref}>
      <i className="iconfont back"  onClick={handleClick}>&#xe655;</i>
      <h1>{title}</h1>
    </HeaderContainer>
  )
})

Header.defaultProps = {
  handleClick: () => {},
  title: "标题",
};

Header.propTypes = {
  handleClick: PropTypes.func,
  title: PropTypes.string,
};

export default React.memo (Header);

现在在 Album 组件中直接使用:

// 先引入
import  Header  from './../../baseUI/header/index';
const handleBack = () => {
  setShowStatus (false);
};

//Container 组件下声明 Header
// 前面代码省略
<Header title={"返回"} handleClick={handleBack}></Header>

现在你就能看到返回的箭头和文字啦,虽然颜色比较淡,但点击能够正常的跳转并显示切出动画。那看到这里你不禁要问了,我们只是通过 setShowStatus 把状态置为了 false,让退出的动画执行一次,为什么会有路由跳转呢?

你可能忘了,在写 CSSTransition 的时候,我特意加上了这一句:

onExited={props.history.goBack}

什么意思?在退出动画执行结束时跳转路由。

那你可能会说,为什么不是直接在 handleBack 里面直接跳转路由呢?这里就是我踩过的一个坑,大家可以试试把 CSSTransition 中的 onExited 钩子删去,然后在 handleBack 中跳转路由。你会发现,动画根本就没有出现!

让我来给你解释一下这是为什么,当你点击后,执行路由跳转逻辑,这个时候路由变化,当前的组件会被立即卸载,相关的动画当然也就不复存在了。最后我的解决方案就是,先让页面切出动画执行一次,然后在动画执行完的瞬间跳转路由,这就达到我们的预期了,这也就是现在呈现给大家的方案。

OK,关于切页动画就分享到这里了,接下来我们开始核心页面的布局。

阅读全文