之前有過一些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è)效果圖
開始之前
這里我用了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)越愉快~