React.js入门实践:一个酷酷的日历选择器组件

之前有过一些vue.js的经验,打算学习以下React感受一下差异。看完React的基本概念,觉得react.js的官方文档还是蛮凌乱的。官方的中文文档已经有点过期了,网上的一些其他教程大多不是新的。大概看了一些英文教程后,打算用react.js写了一个万年历的小应用作为实践。
写下这篇文章,记录一下自己学习react.js的想法,也分享给想学React的朋友看看。

先上个效果图

React-Calendar

Demo 需启用Javascript

开始之前

这里我用了webpackb引入了babel,为了将ES2015(ES6)的语法转成ES5语法。如果对ES2015语法还不太熟悉,可以抽点时间看看,毕竟这是Js的规范,代表着未来,很值得学习。
如果对webpack不是很熟悉,可以先快速浏览一下webpack的概念。这一篇内容关于webpack的配置可以参考。

我想完成的功能:

  • 点击最上方的日期控件,日历选择器下拉出来
  • 可以通过左右按键无限的检索日期
  • 选中日期后,按确定折叠日历选择器
  • 提供一个简易的接口,返回所选的日期
  • 日历要足够酷炫嘿

以上功能用原生js也可以从容实现,但用react分割组件会使代码更清晰。
例子在我的github上可以download下来,可以用作参考:react-calendar

Webpack配置

var path = require('path')
var webpack = require('webpack')

module.exports = {
    entry: './src/main.js', 
    output: { 
        path: path.resolve(__dirname, './public'),
        publicPath: '/public/',
        filename: 'build.js',
    },
    resolveLoader: {
        root: path.join(__dirname, 'node_modules'),
    },
    module: {
        loaders: [
            {
                test: /\.js[x]?$/,
                exclude: /node_modules/,
                loader: 'babel-loader',
                query: {
                    presets: [
                        'es2015',
                        'react',
                        'stage-0'
                    ]
                }
            },
            {
                test: /\.(woff|svg|eot|ttf)\??.*$/,
                loader: 'url-loader?limit=50000&name=[path][name].[ext]'
            },
            {
                test: /\.scss$/
                , loader: "style!css!sass"
            },
        ]
    },
    devServer: {
        historyApiFallback: true,
        noInfo: true
    },
    devtool: '#eval-source-map'
}

if (process.env.NODE_ENV === 'production') {
    module.exports.devtool = '#source-map'
    module.exports.plugins = (module.exports.plugins || []).concat([
        new webpack.DefinePlugin({
            'process.env': {
                NODE_ENV: '"production"'
            }
        }),
        new webpack.optimize.UglifyJsPlugin({
            output: {
                comments: false,
            },
            compress: {
                warnings: false
            }
        }),
        new webpack.optimize.OccurenceOrderPlugin()
    ])
}

可以看到入口文件是在src文件夹里的main.js,然后输出文件放在public文件夹的build.js里。

主要说一下babel-loader的配置,其中presets中react使babel支持jsx语法,es2015使babel支持ES6语法,stage-0使babel支持ES7语法。

这里还使用了SASS,demo里炫酷的星空背景就是依赖SASS里的函数写出来的,是纯的css实现。在本文的末尾有实现的原理:)

分割组件

React.js很重要的一点就是组件。每一个应用可以分割成一个个独立的组件。我将这个日历分割成四个组件:

  • Calendar
  • CalendarHeader
  • CalendarMain
  • CalendarFooter

React的主流思想就是,所有的state状态和方法都是由父组件控制,然后通过props传递给子组件,形成一个单方向的数据链路,保持各组件的状态一致。于是,这其中Calendar将负责存储state和定义方法。

Calendar组件

import React from 'react'
import {render} from 'react-dom'

import CalendarHeader from './CalendarHeader'
import CalendarMain from './CalendarMain'
import CalendarFooter from './CalendarFooter'

const displayDaysPerMonth = (year)=> {

  //定义每个月的天数,如果是闰年第二月改为29天
  let daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
  if ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0) {
    daysInMonth[1] = 29
  }

  //以下为了获取一年中每一个月在日历选择器上显示的数据,
  //从上个月开始,接着是当月,最后是下个月开头的几天

  //定义一个数组,保存上一个月的天数
  let daysInPreviousMonth = [].concat(daysInMonth)
  daysInPreviousMonth.unshift(daysInPreviousMonth.pop())

  //获取每一个月显示数据中需要补足上个月的天数
  let addDaysFromPreMonth = new Array(12)
    .fill(null)
    .map((item, index)=> {
      let day = new Date(year, index, 1).getDay()
      if (day === 0) {
        return 6
      } else {
        return day - 1
      }
    })

  //已数组形式返回一年中每个月的显示数据,每个数据为6行*7天
  return new Array(12)
    .fill([])
    .map((month, monthIndex)=> {
      let addDays = addDaysFromPreMonth[monthIndex],
        daysCount = daysInMonth[monthIndex],
        daysCountPrevious = daysInPreviousMonth[monthIndex],
        monthData = []
      //补足上一个月
      for (; addDays > 0; addDays--) {
        monthData.unshift(daysCountPrevious--)
      }
      //添入当前月
      for (let i = 0; i < daysCount;) {
        monthData.push(++i)
      }
      //补足下一个月
      for (let i = 42 - monthData.length, j = 0; j < i;) {
        monthData.push(++j)
      }
      return monthData
    })
}

class Calendar extends React.Component {
  constructor() {
    //继承React.Component
    super()
    let now = new Date()
    this.state = {
      year: now.getFullYear(),
      month: now.getMonth(),
      day: now.getDate(),
      picked: false
    }
  }

  //切换到下一个月
  nextMonth() {
    if (this.state.month === 11) {
      this.setState({
        year: ++this.state.year,
        month: 0
      })
    } else {
      this.setState({
        month: ++this.state.month
      })
    }
  }
  //切换到上一个月
  prevMonth() {
    if (this.state.month === 0) {
      this.setState({
        year: --this.state.year,
        month: 11
      })
    } else {
      this.setState({
        month: --this.state.month
      })
    }
  }
  //选择日期
  datePick(day) {
    this.setState({day})
  }
  //切换日期选择器是否显示
  datePickerToggle() {
    this.refs.main.style.height =
      this.refs.main.style.height === '460px' ?
        '0px' : '460px'
  }
  //标记日期已经选择
  picked() {
    this.state.picked = true
  }

  render() {
    let props = {
      viewData: displayDaysPerMonth(this.state.year),
      datePicked: `${this.state.year} 年
                   ${this.state.month + 1} 月
                   ${this.state.day} 日`
    }
    return (
      

{props.datePicked}

) } } //将calender实例添加到window上以便获取日期选择数据 window.calendar = render( , document.getElementById('calendarContainer') )

我们可以从render函数看到整个组件的结构,可以看到其实结构相当简单。className为datePicked的元素用来显示选择的日期,点击它便可下拉日期选择器。
日期选择器由CalendarHeader,CalendarMain和CalendarFooter三个组件组成,CalendarHeader用来控制月份的切换,CalendarMain用来展示日历,CalendarFooter用来提供控制台。

这其中主要的思想就是,方法在父组件定义,通过props传给需要的子组件进行调用传参,最后返回到父组件上执行函数,存储数据、改变state和重新render。

{...props}是ES6中的spread操作符,如果我们没有用这个操作符,就要这样写:


//等同于

是不是优雅多了呢

::是ES7中的语法,用来绑定this,方法需要bind(this),不然方法内部的this指向会不正确。

prevMonth={::this.prevMonth}
//等同于
prevMonth={this.prevMonth.bind(this)}

CalendarHeader组件

import React from 'react'

export default class CalendarHeader extends React.Component {
  render() {
    return (
      
{this.props.year}年{this.props.month + 1}月
) } }

CalendarHeader组件接收父组件传来的日期,可以调用父组件的方法以前进到下一月和退回上一个月。

CalendarMain组件

import React from 'react'

export default class CalendarMain extends React.Component {

  //处理日期选择事件,如果是当月,触发日期选择;如果不是当月,切换月份
  handleDatePick(index, styleName) {
    switch (styleName) {
      case 'thisMonth':
        let month = this.props.viewData[this.props.month]
        this.props.datePick(month[index])
        break
      case 'prevMonth':
        this.props.prevMonth()
        break
      case 'nextMonth':
        this.props.nextMonth()
        break
    }
  }

  //处理选择时选中的样式效果
  //利用闭包保存上一次选择的元素,
  //在月份切换和重新选择日期时重置上一次选择的元素的样式
  changeColor() {
    let previousEl = null
    return function (event) {
      let name = event.target.nodeName.toLocaleLowerCase()
      if (previousEl && (name === 'i' || name === 'td')) {
        previousEl.style = ''
      }
      if (event.target.className === 'thisMonth') {
        event.target.style = 'background:#F8F8F8;color:#000'
        previousEl = event.target
      }
    }
  }

  //绑定颜色改变事件
  componentDidMount() {
    let changeColor = this.changeColor()
    document.getElementById('calendarContainer')
      .addEventListener('click', changeColor, false);

  }

  render() {
    //确定当前月数据中每一天所属的月份,以此赋予不同className
    let month = this.props.viewData[this.props.month],
      rowsInMonth = [],
      i = 0,
      styleOfDays = (()=> {
        let i = month.indexOf(1),
          j = month.indexOf(1, i + 1),
          arr = new Array(42)
        arr.fill('prevMonth', 0, i)
        arr.fill('thisMonth', i, j)
        arr.fill('nextMonth', j)
        return arr
      })()

    //把每一个月的显示数据以7天为一组等分
    month.forEach((day, index)=> {
      if (index % 7 === 0) {
        rowsInMonth.push(month.slice(index, index + 7))
      }
    })

    return (
      
        {
          rowsInMonth.map((row, rowIndex)=> {
            return (
              
                {
                  row.map((day)=> {
                    return (
                      
                    )
                  })
                }
              
            )
          })
        }
        
{day}
) } }

CalendarMain组件用来展示日历,是最复杂的一个组件。主体思路是通过父组件传来的长度为12的viewData数组,将数组中每一项长度为42的数组以7天为一组等分,以此来渲染表格。
由于在切换颜色时逻辑比较复杂,通过react处理事件会很麻烦,因此自己写了一个代理,通过闭包函数来控制选择日期时的样式切换。

CalendarFooter组件

import React from 'react'

export default class CalendarFooter extends React.Component {

  handlePick() {
    this.props.datePickerToggle()
    this.props.picked()
  }

  render() {
    return (
      
) } }

很简单的组件,在点击确定时调用日期选择器折叠和改变日期已选择属性的布尔值

关于背景的样式实现

最后,解释一下用纯css实现的的炫酷背景。
我们知道box-shadow属性接收6个值,分别时水平偏移,竖直偏移,模糊半径,阴影厚度,颜色和内外阴影选择。

box-shadow: h-shadow v-shadow blur spread color inset;

而且,很关键一点,一个元素可以设置多个阴影,每一个阴影用逗号隔开。所以可以这样:

box-shadow: h-shadow v-shadow blur spread color inset, h-shadow v-shadow blur spread color inset;

于是,你看到的每一个星星都是一个阴影,阴影形状大小与产生他的元素形状大小一致,每一个阴影有着随机的位置偏移量。所以背景里有三个div,分别是1px,2px,3px,所有的星星都是他们的投影。
另外,三个div被添加了infinite的动画,以线性速度上移,因此所有星星也随着他们上移。
还有一点很关键,在div后添加了一个after,其中也添加了同样的星星阴影,这样就能保证星星在上移的过程中下面会有新的星星补上来,造成无穷无尽的错觉。
为了完成大量的阴影,所有借助了SASS提供的函数,用来随机化阴影的位置和数量。感兴趣的同学可以看一下源码。
其实利用这些特性,还可以实现很多酷炫的css样式,留待想象了~

总结

之前有学习过vue.js,就学习难度而言,vue.js更容易一些。主要是官方文档演示的很好,而react.js有一点凌乱的感觉。如果没有接触过Angular和vue,很容易会对一些新的名词和概念产生疑惑。
个人觉得,JSX渲染函数包含逻辑比较复杂,这一点相对于vue.js,可能会使样式对照设计起来不太方便。而且数组在循环里嵌套的时候没有vue.js来的方便直观。
无论如何,相对于原生JS,用这些框架写起来真的舒服多了。相信未来前端开发应越来越愉快~

你可能感兴趣的:(React.js入门实践:一个酷酷的日历选择器组件)