之前有过一些vue.js的经验,打算学习以下React感受一下差异。看完React的基本概念,觉得react.js的官方文档还是蛮凌乱的。官方的中文文档已经有点过期了,网上的一些其他教程大多不是新的。大概看了一些英文教程后,打算用react.js写了一个万年历的小应用作为实践。
写下这篇文章,记录一下自己学习react.js的想法,也分享给想学React的朋友看看。
先上个效果图
开始之前
这里我用了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 (
<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实例添加到window上以便获取日期选择数据
window.calendar = render(
<Calendar/>,
document.getElementById('calendarContainer')
)
我们可以从render函数看到整个组件的结构,可以看到其实结构相当简单。className为datePicked的元素用来显示选择的日期,点击它便可下拉日期选择器。
日期选择器由CalendarHeader,CalendarMain和CalendarFooter三个组件组成,CalendarHeader用来控制月份的切换,CalendarMain用来展示日历,CalendarFooter用来提供控制台。
这其中主要的思想就是,方法在父组件定义,通过props传给需要的子组件进行调用传参,最后返回到父组件上执行函数,存储数据、改变state和重新render。
{...props}是ES6中的spread操作符,如果我们没有用这个操作符,就要这样写:
<CalendarMain {...props} />
//等同于
<CalendarMain {...props}
viewData={props.viewData}
datePicked={props.datePicked} />
是不是优雅多了呢
::是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 (
<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组件接收父组件传来的日期,可以调用父组件的方法以前进到下一月和退回上一个月。
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 (
<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组件用来展示日历,是最复杂的一个组件。主体思路是通过父组件传来的长度为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 (
<div className="calendarFooter">
<button onClick={::this.handlePick}>
确定
</button>
</div>
)
}
}
很简单的组件,在点击确定时调用日期选择器折叠和改变日期已选择属性的布尔值
关于背景的样式实现
最后,解释一下用纯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,用这些框架写起来真的舒服多了。相信未来前端开发应越来越愉快~