React.js入門實(shí)踐:一個(gè)酷酷的日歷選擇器組件

之前有過一些vue.js的經(jīng)驗(yàn),打算學(xué)習(xí)以下React感受一下差異。看完React的基本概念,覺得react.js的官方文檔還是蠻凌亂的。官方的中文文檔已經(jīng)有點(diǎn)過期了,網(wǎng)上的一些其他教程大多不是新的。大概看了一些英文教程后,打算用react.js寫了一個(gè)萬(wàn)年歷的小應(yīng)用作為實(shí)踐。
寫下這篇文章,記錄一下自己學(xué)習(xí)react.js的想法,也分享給想學(xué)React的朋友看看。

先上個(gè)效果圖

React-Calendar

Demo 需啟用Javascript

開始之前

這里我用了webpackb引入了babel,為了將ES2015(ES6)的語(yǔ)法轉(zhuǎn)成ES5語(yǔ)法。如果對(duì)ES2015語(yǔ)法還不太熟悉,可以抽點(diǎn)時(shí)間看看,畢竟這是Js的規(guī)范,代表著未來(lái),很值得學(xué)習(xí)。
如果對(duì)webpack不是很熟悉,可以先快速瀏覽一下webpack的概念。這一篇內(nèi)容關(guān)于webpack的配置可以參考。

我想完成的功能:

  • 點(diǎn)擊最上方的日期控件,日歷選擇器下拉出來(lái)
  • 可以通過左右按鍵無(wú)限的檢索日期
  • 選中日期后,按確定折疊日歷選擇器
  • 提供一個(gè)簡(jiǎn)易的接口,返回所選的日期
  • 日歷要足夠酷炫嘿

以上功能用原生js也可以從容實(shí)現(xiàn),但用react分割組件會(huì)使代碼更清晰。
例子在我的github上可以download下來(lái),可以用作參考: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里。

主要說(shuō)一下babel-loader的配置,其中presets中react使babel支持jsx語(yǔ)法,es2015使babel支持ES6語(yǔ)法,stage-0使babel支持ES7語(yǔ)法。

這里還使用了SASS,demo里炫酷的星空背景就是依賴SASS里的函數(shù)寫出來(lái)的,是純的css實(shí)現(xiàn)。在本文的末尾有實(shí)現(xiàn)的原理:)

分割組件

React.js很重要的一點(diǎn)就是組件。每一個(gè)應(yīng)用可以分割成一個(gè)個(gè)獨(dú)立的組件。我將這個(gè)日歷分割成四個(gè)組件:

  • Calendar
  • CalendarHeader
  • CalendarMain
  • CalendarFooter

React的主流思想就是,所有的state狀態(tài)和方法都是由父組件控制,然后通過props傳遞給子組件,形成一個(gè)單方向的數(shù)據(jù)鏈路,保持各組件的狀態(tài)一致。于是,這其中Calendar將負(fù)責(zé)存儲(chǔ)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)=> {

  //定義每個(gè)月的天數(shù),如果是閏年第二月改為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
  }

  //以下為了獲取一年中每一個(gè)月在日歷選擇器上顯示的數(shù)據(jù),
  //從上個(gè)月開始,接著是當(dāng)月,最后是下個(gè)月開頭的幾天

  //定義一個(gè)數(shù)組,保存上一個(gè)月的天數(shù)
  let daysInPreviousMonth = [].concat(daysInMonth)
  daysInPreviousMonth.unshift(daysInPreviousMonth.pop())

  //獲取每一個(gè)月顯示數(shù)據(jù)中需要補(bǔ)足上個(gè)月的天數(shù)
  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
      }
    })

  //已數(shù)組形式返回一年中每個(gè)月的顯示數(shù)據(jù),每個(gè)數(shù)據(jù)為6行*7天
  return new Array(12)
    .fill([])
    .map((month, monthIndex)=> {
      let addDays = addDaysFromPreMonth[monthIndex],
        daysCount = daysInMonth[monthIndex],
        daysCountPrevious = daysInPreviousMonth[monthIndex],
        monthData = []
      //補(bǔ)足上一個(gè)月
      for (; addDays > 0; addDays--) {
        monthData.unshift(daysCountPrevious--)
      }
      //添入當(dāng)前月
      for (let i = 0; i < daysCount;) {
        monthData.push(++i)
      }
      //補(bǔ)足下一個(gè)月
      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
    }
  }

  //切換到下一個(gè)月
  nextMonth() {
    if (this.state.month === 11) {
      this.setState({
        year: ++this.state.year,
        month: 0
      })
    } else {
      this.setState({
        month: ++this.state.month
      })
    }
  }
  //切換到上一個(gè)月
  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'
  }
  //標(biāo)記日期已經(jīng)選擇
  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 (
      <div className="output">
        <div className="star1"></div>
        <div className="star2"></div>
        <div className="star3"></div>
        <p className="datePicked"
           onClick={::this.datePickerToggle}>
          {props.datePicked}
        </p>
        <div className="main" ref="main">
          <CalendarHeader prevMonth={::this.prevMonth}
                          nextMonth={::this.nextMonth}
                          year={this.state.year}
                          month={this.state.month}
                          day={this.state.day}/>
          <CalendarMain {...props}
                        prevMonth={::this.prevMonth}
                        nextMonth={::this.nextMonth}
                        datePick={::this.datePick}
                        year={this.state.year}
                        month={this.state.month}
                        day={this.state.day}/>
          <CalendarFooter
            picked={::this.picked}
            datePickerToggle={::this.datePickerToggle}/>
        </div>
      </div>
    )
  }
}

//將calender實(shí)例添加到window上以便獲取日期選擇數(shù)據(jù)
window.calendar = render(
  <Calendar/>,
  document.getElementById('calendarContainer')
)

我們可以從render函數(shù)看到整個(gè)組件的結(jié)構(gòu),可以看到其實(shí)結(jié)構(gòu)相當(dāng)簡(jiǎn)單。className為datePicked的元素用來(lái)顯示選擇的日期,點(diǎn)擊它便可下拉日期選擇器。
日期選擇器由CalendarHeader,CalendarMain和CalendarFooter三個(gè)組件組成,CalendarHeader用來(lái)控制月份的切換,CalendarMain用來(lái)展示日歷,CalendarFooter用來(lái)提供控制臺(tái)。

這其中主要的思想就是,方法在父組件定義,通過props傳給需要的子組件進(jìn)行調(diào)用傳參,最后返回到父組件上執(zhí)行函數(shù),存儲(chǔ)數(shù)據(jù)、改變state和重新render。

{...props}是ES6中的spread操作符,如果我們沒有用這個(gè)操作符,就要這樣寫:

<CalendarMain {...props} />
//等同于
<CalendarMain {...props} 
    viewData={props.viewData} 
    datePicked={props.datePicked} />

是不是優(yōu)雅多了呢

::是ES7中的語(yǔ)法,用來(lái)綁定this,方法需要bind(this),不然方法內(nèi)部的this指向會(huì)不正確。

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

CalendarHeader組件

import React from 'react'

export default class CalendarHeader extends React.Component {
  render() {
    return (
      <div className="calendarHeader">
        <span className="prev"
              onClick={this.props.prevMonth}>
          《
        </span>
        <span className="next"
              onClick={this.props.nextMonth}>
          》
        </span>
        <span className="dateInfo">
          {this.props.year}年{this.props.month + 1}月
        </span>
      </div>
    )
  }
}

CalendarHeader組件接收父組件傳來(lái)的日期,可以調(diào)用父組件的方法以前進(jìn)到下一月和退回上一個(gè)月。

CalendarMain組件

import React from 'react'

export default class CalendarMain extends React.Component {

  //處理日期選擇事件,如果是當(dāng)月,觸發(fā)日期選擇;如果不是當(dāng)月,切換月份
  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
    }
  }

  //處理選擇時(shí)選中的樣式效果
  //利用閉包保存上一次選擇的元素,
  //在月份切換和重新選擇日期時(shí)重置上一次選擇的元素的樣式
  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() {
    //確定當(dāng)前月數(shù)據(jù)中每一天所屬的月份,以此賦予不同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
      })()

    //把每一個(gè)月的顯示數(shù)據(jù)以7天為一組等分
    month.forEach((day, index)=> {
      if (index % 7 === 0) {
        rowsInMonth.push(month.slice(index, index + 7))
      }
    })

    return (
      <table className="calendarMain">
        <thead>
        <tr>
          <th>日</th>
          <th>一</th>
          <th>二</th>
          <th>三</th>
          <th>四</th>
          <th>五</th>
          <th>六</th>
        </tr>
        </thead>
        <tbody>
        {
          rowsInMonth.map((row, rowIndex)=> {
            return (
              <tr key={rowIndex}>
                {
                  row.map((day)=> {
                    return (
                      <td className={styleOfDays[i]}
                          onClick={
                            this.handleDatePick.bind
                            (this, i, styleOfDays[i])}
                          key={i++}>
                        {day}
                      </td>
                    )
                  })
                }
              </tr>
            )
          })
        }
        </tbody>
      </table>
    )
  }
}

CalendarMain組件用來(lái)展示日歷,是最復(fù)雜的一個(gè)組件。主體思路是通過父組件傳來(lái)的長(zhǎng)度為12的viewData數(shù)組,將數(shù)組中每一項(xiàng)長(zhǎng)度為42的數(shù)組以7天為一組等分,以此來(lái)渲染表格。
由于在切換顏色時(shí)邏輯比較復(fù)雜,通過react處理事件會(huì)很麻煩,因此自己寫了一個(gè)代理,通過閉包函數(shù)來(lái)控制選擇日期時(shí)的樣式切換。

CalendarFooter組件

import React from 'react'

export default class CalendarFooter extends React.Component {

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

  render() {
    return (
      <div className="calendarFooter">
        <button onClick={::this.handlePick}>
          確定
        </button>
      </div>
    )
  }
}

很簡(jiǎn)單的組件,在點(diǎn)擊確定時(shí)調(diào)用日期選擇器折疊和改變?nèi)掌谝堰x擇屬性的布爾值

關(guān)于背景的樣式實(shí)現(xiàn)

最后,解釋一下用純css實(shí)現(xiàn)的的炫酷背景。
我們知道box-shadow屬性接收6個(gè)值,分別時(shí)水平偏移,豎直偏移,模糊半徑,陰影厚度,顏色和內(nèi)外陰影選擇。

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

而且,很關(guān)鍵一點(diǎn),一個(gè)元素可以設(shè)置多個(gè)陰影,每一個(gè)陰影用逗號(hào)隔開。所以可以這樣:

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

于是,你看到的每一個(gè)星星都是一個(gè)陰影,陰影形狀大小與產(chǎn)生他的元素形狀大小一致,每一個(gè)陰影有著隨機(jī)的位置偏移量。所以背景里有三個(gè)div,分別是1px,2px,3px,所有的星星都是他們的投影。
另外,三個(gè)div被添加了infinite的動(dòng)畫,以線性速度上移,因此所有星星也隨著他們上移。
還有一點(diǎn)很關(guān)鍵,在div后添加了一個(gè)after,其中也添加了同樣的星星陰影,這樣就能保證星星在上移的過程中下面會(huì)有新的星星補(bǔ)上來(lái),造成無(wú)窮無(wú)盡的錯(cuò)覺。
為了完成大量的陰影,所有借助了SASS提供的函數(shù),用來(lái)隨機(jī)化陰影的位置和數(shù)量。感興趣的同學(xué)可以看一下源碼。
其實(shí)利用這些特性,還可以實(shí)現(xiàn)很多酷炫的css樣式,留待想象了~

總結(jié)

之前有學(xué)習(xí)過vue.js,就學(xué)習(xí)難度而言,vue.js更容易一些。主要是官方文檔演示的很好,而react.js有一點(diǎn)凌亂的感覺。如果沒有接觸過Angular和vue,很容易會(huì)對(duì)一些新的名詞和概念產(chǎn)生疑惑。
個(gè)人覺得,JSX渲染函數(shù)包含邏輯比較復(fù)雜,這一點(diǎn)相對(duì)于vue.js,可能會(huì)使樣式對(duì)照設(shè)計(jì)起來(lái)不太方便。而且數(shù)組在循環(huán)里嵌套的時(shí)候沒有vue.js來(lái)的方便直觀。
無(wú)論如何,相對(duì)于原生JS,用這些框架寫起來(lái)真的舒服多了。相信未來(lái)前端開發(fā)應(yīng)越來(lái)越愉快~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,622評(píng)論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,716評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,746評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,991評(píng)論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,706評(píng)論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,036評(píng)論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,029評(píng)論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,203評(píng)論 0 290
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,725評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,451評(píng)論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,677評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,161評(píng)論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,857評(píng)論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,266評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,606評(píng)論 1 295
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,407評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,643評(píng)論 2 380

推薦閱讀更多精彩內(nèi)容