基于React搭建一个简易版豆瓣

前言

之前写过一篇基于vue搭建一个简易版豆瓣的文章,近来接触到React,于是将该项目用React重写了一遍,用于体验两者在实际开发中的异同。本文会简述项目情况及构建项目过程中遇到的一些零散的问题,以作记录,见识浅薄,若有错漏处还望指正。


项目概述

项目地址:react-douban

项目简介:基于React搭建简易版豆瓣,实现将读书、电影、音乐、同城活动等内容按不同类型显示的功能。

技术栈:React + Mobx + react-router + axios + Sass + ES6/7等,同时,为保证代码质量和风格统一,项目也使用了ESLintstylelintprettier等优秀的开源工具对项目进行约束调整。

项目背景:本项目界面风格参考自豆瓣,数据获取来自豆瓣官方api,由于有请求限制,所以频繁请求的话会导致请求失败,还需注意。

开发环境:node v8.1.2,npm 6.1.0,yarn 1.7.0;浏览器:Chrome 67.0.3396.99,Firefox 61.0.1。


运行项目

运行项目前,请确保已正确安装node环境。

  • 克隆项目,若没有安装git环境,也可直接在react-douban处下载安装包;

git clone https://github.com/nh0007/react-douban.git
复制代码

  • 进入项目根目录,安装依赖;

npm install 
or 
yarn install
复制代码

  • 安装完依赖后,运行项目;

npm start
or
yarn start
复制代码

  • 等待运行完毕后,浏览器会自动弹出访问页,即可看到项目运行效果。

项目截图

本项目主要分为读书、电影、音乐、同城活动四大模块,以下是各个模块截图:










开发中需注意的问题

1、项目初始配置

俗话说:好的开始就是成功的一半。项目初期添加检验配置,是项目后期发展的基石。因此,在使用create-react-app脚手架构建完项目后,为添加自定义配置,先eject项目,然后添加如下配置:

刚开始接触这些配置的朋友可能会很别扭,甚至烦躁,配置完后开发过程总是报错,影响进度。但这部分成本还是有必要付出的,特别是当你维护过一些“紊乱”的代码后就更能体会其中价值~。具体的配置可以查看提交记录,里边有针对各项配置的提交修改。如果还觉得有点懵,油管上有个不到十分钟的配置视频,介绍了初始配置以及与编辑器插件配套使用过程,感觉挺清晰的,刚接触的朋友可以看看。


2、使用Mobx的几点注意事项

  • 添加装饰器语法配置,个人更为习惯装饰器语法,相关的配置可以看这里
  • 所有的observable数据都放到action中修改,无论observable数据是存放于store中还是组件里;
  • 使用action修改状态时可使用箭头函数写法,保证无论怎么调用函数,this的指向都不会出错,如下:

@action
setCurrentBookTags = tags => {
  if (tags.tagName !== this.currentBookTags.tagName) {
    this.currentBookTags = tags;
  }
};
复制代码

  • 异步修改状态官方文档推荐使用flow,这样可以不用@action、runInAction去包装异步代码,使代码更简洁。但在使用过程中有两点要注意:一是需引入flow,不然会报错,(我能说一开始我还以为是得修改ESLint配置查了好一会吗);二是绑定this。示例代码如下:

// 从mobx引入flow
import { observable, action, flow } from 'mobx';

// 异步获取数据时需要绑定this,无论你是this.props.bookStore.setTypeBooks(type)调用,
// 还是 const { setTypeBooks } = this.props.bookStore; setTypeBooks(type);调用都不会出错;
setTypeBooks = flow(
  function*(type, start, count, isAfresh = false) {
    const oldData = this.typeBooks.get(type);
    if (oldData && !isAfresh) return;
    try {
      const response = yield getCurrentTypeBooks(type, start, count);
      const data = response.data.books;
      this.typeBooks.set(type, data);
    } catch (error) {
      console.log(error);
    }
  }.bind(this)
);
复制代码

  • Mobx4中observable数组实际上是对象,不能使用PropTypes.array进行类型检查,Mobx5中使用Proxy追踪变化,数组可直接使用PropTypes.array进行类型检查;
  • Mobx中observable普通对象时新增对象属性不会触发更新,可以使用Map,使用Map尽管可以使用PropTypes.object进行类型检查,但感觉还是mobx-react中的observableMap来检查更加合适,同时引入prop-types和mobx-react中的PropTypes会有同名问题,可以这样:

import { observer, inject, PropTypes as mobxPropTypes } from 'mobx-react';
import PropTypes from 'prop-types';
复制代码


3、轮播实现

项目中有不少轮播展示内容页面,在实现过程遇到了些问题,此处记录一下。

先看看项目中的轮播效果:


虽然有一些优秀的轮播库,但此处暂不使用专用轮播库,而是借助过渡、动画效果来实现。

可以看看React和Vue在过渡、动画使用上的一些差异:

  • Vue中自带过渡的封装组件,React可引入react-transition-group库协助;
  • Vue中可结合v-if、v-show控制显示隐藏的动画,在react-transition-group中主要借助它的in属性;

关于react-transition-group的使用介绍,目前看到最清晰详细的还是官方文档,网上蛮多的中文资料介绍的是早期v1版本,这个还得结合自身使用的版本对号入座。

来看看官方文档中对于in的介绍:

Show the component; triggers the enter or exit states

type: boolean
default: false

Transition state is toggled via the in prop. When true the component begins the "Enter" stage. During this stage, the component will shift from its current transition state, to 'entering' for the duration of the transition and then to the 'entered' stage once it's complete.

When in is false the same thing happens except the state moves from 'exiting' to 'exited'.

简单来说,in值的变化会改变过渡组件的状态:

  • 当in值由false变为true时,组件进入enter状态;
  • 当in值由true变为false时,组件进入exit状态。

上面强调了变化二字,主要是因为在实践过程中会发现,当你一开始挂载CSSTransition组件时它的in值便为true,它是不会触发onEnter函数的,也不会在相应容器元素上添加对应的状态class,只有在由false变为true才会触发。false同理。

来看看项目中遇到的问题,由于轮播本身也算是列表的轮换展示,于是第一反应是使用TransitionGroup,之前在Vue也是使用了相似的<transition-group>组件包裹列表实现滑动效果,但在React中却不行,样例代码如下:

import React, { PureComponent } from 'react';
import { TransitionGroup, CSSTransition } from 'react-transition-group';

export default class Slider extends PureComponent {
  ...
  render() {
    ...
    return (
      <TransitionGroup className="slider-list">
        {itemArray.map((item, index) => (
          <CSSTransition
            key={item.id}
            in={currentPage === index}
            timeout={500}
            classNames={`slider-${slideDirection}`}
            unmountOnExit
          >
            ...
          </CSSTransition>
        ))}
      </TransitionGroup>
    )
  }
}
复制代码

一开始以为自己对TransitionGroup和CSSTransition的使用方法不对,或是漏了哪些重要属性,看着文档折腾了好一会仍没有效果,且给CSSTransition组件设置的in、unmountOnExit也没啥作用,之后尝试着把TransitionGroup标签换成div标签,滑动生效。再回头看看官网给的TransitionGroup的例子,我觉得是我一开始对TransitionGroup理解使我走入一个误区,TransitionGroup的适用情况应该是同时渲染整个列表,比如增删列表,像轮播这种每次仅显示列表中的其中一项显然是不适用的。

总结一下:在本项目中,主要借助react-transition-group的CSSTransition组件来实现轮播动画,通过in与unmountOnExit的搭配使用控制是否显示,使用classNames属性结合样式实现过渡效果。具体的实现可以参见源码,此处就不再赘述了。


4、组件拆分

在开始这个项目之前,我在网上看了一些React最佳实践的文章,普遍提及的一点是拆分组件,以便后期复用与维护。过于庞大的组件毫无疑问是有拆分的必要的,但仅凭组件的大小来拆分未免显得粗糙,组件拆分的标准是啥,官方文档提及的一点是单一功能原则,写代码的对单一功能原则肯定不陌生,但将这个原则用在组件的拆分上,还是遇到了以下的疑惑:

  • 依据单一功能原则划分组件,可能导致组件数量增多,组件划分较为零碎的情况;
  • 业务组件通常各不相同,拆分出来的组件往往仅使用一次,不仅达不到复用的效果,还会平添出更多代码;
  • 多组件意味着属性在更多的组件间传递,同时开发时会频繁的切换组件文件,往往会比较绕。

那就不拆分了吗?也不是的,组件拆分还有一个好处,就是优化性能,详情可以看看这篇文章,写得很清晰,这里简略总结一下:

默认情况下,组件状态更新,子组件即便状态不变,依然会执行re-render和vdom-diff操作

理想情况下,如果组件状态不变,则不需要进行额外的操作

如何达到理想情况?

  1. 使用对shouldComponentUpdate生命周期函数返回值的判断减少额外操作,可借助PureRenderMin或PureComponent默认实现;
  2. 单纯使用shouldComponentUpdate是不够的,影响组件的状态越多,组件re-render等操作的次数就越多,可能性就越大。因此,通过扁平化的状态去拆分组件时很有必要的。

嗯,总结一下,什么时候需要拆分组件?

  • 组件较臃肿时,可考虑拆分组件,提高代码可读性,方便后期维护;
  • 组件性能有优化空间时,可以考虑拆分组件,减少性能隐患。

如何拆分?

  • 根据单一功能原则,使得组件职责分明;
  • 根据状态划分,使得划分后的组件能尽量减少不必要的更新操作。

最后,来看一个项目中的例子:


可以看到,上图有四个轮播页,当我点击下一页的时候,上一页逐渐消失,下一页逐渐显示,其余两个轮播页是不变的,当我不拆分组件,把四个轮播页放在一个组件中时,每次当前显示的页数变化时,每个轮播页都要被更新,执行re-render和vdom-diff等操作,样例代码如下:

{bookList.map((bookArray, index) => (
  <ul
    key={bookArray[0].id}
    style={{display: currentPage === index ? 'block' : 'none'}}
  >
    ...
  </ul>
))}
复制代码

这个时候,可以考虑拆分组件,于是我把每个轮播页拆分成独立的组件,这样,当我从第一页滑动到第二页时,只有第一二页因为状态变化而需要进行更新操作,而第三第四页则不需要,样例代码如下,具体代码可参见源码:

{bookList.map((bookArray, index) => (
  <BookTagDisplayItem
    key={bookArray[0].id}
    isShow={currentPage === index}
  />
))}
复制代码

以上是个人在组件拆分上的一些主观的想法与实践,仅供参考。个人觉得组件的拆分是一门玄学,组件的粒度是一个抽象而又没有绝对答案的事情。我们很少会遇到一个组件十分臃肿以至于必须拆分的情况,而在界面元素不多的情况下,我们也很少遇到组件不拆分这一下就会对性能产生多大的影响的情况,更多时候我们遇到的是一个可拆可不拆的情况,拆分了吧,过零碎,平白又多了很多文件;不拆吧,又有点强迫症,如何把握拆分的度也是一门学问。当然了,可能是想的多做的少的原因,希望以后能在过多的实践中找到平衡点,此处纯属抛砖。


React使用体验

  • React使用JSX构建页面元素,初期可能有点不习惯,但有JS基础的话,JSX上手成本不高,后面熟悉了觉得还是很方便的;
  • 使用React时会下意识思考是否需要拆分组件的问题,以前用其他框架也会思考这个问题,但个人感觉React的粒度会更细,当然了,拆分的度在哪里,上面也说了,之后仍需要在实践中去探索;
  • React生态庞大,单就路由管理和状态管理来说,路由有传统的react-router,也有新贵reach/router,状态管理有redux,也有Mobx,如果你选择了redux,那么在异步加载数据时,你还有redux-thunkredux-promiseredux-saga等选择,在redux最佳实践方面,你可能还会接触到dva或者rematch,生态的庞大一方面让你解决问题的手段多样化,另一方面,如何在纷杂的信息中筛选出适合自己的也是一种能力的考验。


结语

感觉对React的理解尚有许多待加强之处,行文若有不准确的地方,还请大家批评指正。若觉得稍有助益,不烦star本项目一下~


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值