寫在最前
這次使用react&redux,來模擬了一個購票app,需要關注的是本次全部資料均為mock實現,不涉及後臺。同時其中不會涉及react與redux的語法,只關注到一些模擬原生效果的實現理念。沒有接觸過react的童鞋們可以關注下阮一峰老師的react入門教程,至於redux,redux中文文件上面也有著詳細的說明。不過作者對redux也很感興趣,打算學習一波原始碼後(如果整個明白了),可能也會出一個分享,屆時歡迎前來交流~ #github地址,捂臉求star
部署
本應用全部執行在開發模式下,開啟了devserver,沒有進行過生產環境測試,如果出現問題大家可以留言~
git clone https://github.com/Aaaaaaaty/react_movie
cd react_movie
cnpm i || npm i
將./data及./src/images 檔案 拷貝進dist //專案依賴的圖片及假資料
npm start複製程式碼
重點實現 —— 一個電影選座元件
本次分享的重點是一個基於react的選座元件demo。作者在開發這個元件的時候有觀察過微信和支付寶內嵌的影院選座功能。但是無奈看不到程式碼,一切純平臆想,說錯勿噴。個人感覺微信裡面外包的微票兒內的選座模組裡面的手勢功能為原生瀏覽器自帶的縮放,那麼控制上會相對粗暴,縮放上面相對沒有支付寶精細。而支付寶上面不僅縮放手感好同時包含了左上方小窗預覽功能,可謂使用者體驗良好(我不是阿里腦殘粉hhhhh,雖然事實如此?),所以作者並沒有感覺出來這個是混合開發的元件還是原生的還是什麼的。。。好了bb了半天,現在輪到作者自己來實現一個了。
效果圖
很可惜chrome的模擬器下無法演示手勢的操作。其實這裡面實現了縮放功能,以及在選座介面放大的時候左側上方的預覽圖中的紅色標示線則會相應的縮小來指出你選中的範圍在整個影院中的位置。這次作者使用了react來書寫這個元件,所有的移動縮放全部通過js計算,在真機測試中頁面會有些許卡頓。不過作者相信如果進行防抖和節流的優化,在手機瀏覽器中的體驗應該可以更優秀一些。
核心思路
- 按照後端介面mock資料
- 渲染座位
- 增加手勢操作
- 管理選座資訊
- 渲染預覽小圖
mock資料
// ./dist/data/filmSeat.json
{
"seatId":"0000002-1-1",
"rowId": 1, //行index
"columnId": 1, //列index
"xAxis":3, //行絕對定位
"yAxis":1, //列絕對定位
...
"isSold":false //是否賣出(用於渲染座位顏色)
}複製程式碼
在這裡需要注意的是:行和列的index值與其絕對定位的區別。我們在電影院中座位擺放的地理位置是千奇百怪的,但是索引序號一定是從1到X。從而就有了如上的四個屬性。在渲染座位佈局的時候一定是採用xAxis & yAxis
才能達到展示影廳座位排布的效果。如果還有點懵請看上圖的演示中的座位的排布。
渲染座位
在這裡我們先假設要渲染一個佔裝置視口80%寬的區域來擺放我們的座椅。那麼由此就會有一個問題就是我們不確定座椅的數量。故座椅的寬是不能定死的(方便起見,讓座椅為正方形,寬高相等),即寬度應為 視口寬*80% / 座椅數量。
當然如果座椅太少那麼就會導致寬太大這種情況這些極端條件如果有興趣可以後期再進行判斷
// ./src/Components/FilmSeat/FilmSeat.js
let list = seatList.map((item, index) => {
let style = {
position: `absolute`,
left: `${seatWidth * item.xAxis + seatWidth / 2 }rem`,
top: `${seatWidth * item.yAxis}rem`, // 根據資料中的絕對定位來動態渲染座位位置
width: `${seatWidth}rem`
}
return (
<img key={ `seatId` + index }
style={ style }
src={ `./images/${isSoldUrl[index]}.png` }
onTouchTap={ this.changeSeat.bind(this, isSoldUrl, index, item) }
className={ styles.seatItem }></img> // 每個座位都是一張小圖
)
})複製程式碼
手勢操作
// ./src/Components/FilmSeat/FilmSeat.js
<div ...
onTouchStart={ this.onTouchStart.bind(this) }
onTouchMove={ this.onTouchMove.bind(this) }
onTouchEnd={ this.onTouchEnd.bind(this) }>複製程式碼
對於手勢操作,採用了瀏覽器的三個原生觸控事件。下面主要說明如何使用react實現一個原生的拖拽效果:
// ./src/Components/FilmSeat/FilmSeat.js
onTouchStart(e) { //三個事件均會傳入event事件
e.preventDefault()
let { left, top... } = this.state
...
if(e.touches.length === 1) { //判斷是否為一個手指觸控
let startX = e.touches[0].clientX //得到起始橫座標
let startY = e.touches[0].clientY //得到起始縱座標
state = {
startX: startX,
startY: startY,
lastDisX: left, //記錄上一次橫軸偏移量
lastDisY: top, //記錄上一次縱軸偏移量
...
}
}
...
this.setState(state)
}
onTouchMove(e) {
e.preventDefault()
let { startX, startY ... } = this.state
if(e.touches.length === 1) {
let moveX = e.touches[0].clientX //記錄當前的位置
let moveY = e.touches[0].clientY
let disX = moveX - startX + lastDisX //記錄現在手指相對螢幕左側距離
let disY = moveY - startY + lastDisY
...
this.setState({
moveX: moveX,
moveY: moveY,
left: disX,
top: disY,
})
} else if(e.touches.length === 2) {
...
}
}
onTouchEnd(e) {
e.preventDefault()
...
//主要做一些拖拽完成之後的判斷,重置初始值等等
}複製程式碼
總結來說核心思路是,e.touches[0].clientX/Y
可以提供手指在螢幕中的絕對距離,我們滑動中可以記錄到滑動了的相對距離。那麼在下次滑動前就需要記錄下上一次的相對距離,下次滑動時就要加上上次的距離。不然每次重新拖拽就會從0,0點重新開始。
管理選座資訊
通過效果圖我們可以知道,在元件中同時需要渲染座位的選取,下方彈出/關閉座位資訊等效果。雖然效果多樣但是基本可以看為兩個狀態即座位是否選中,這就使用到了redux來作為狀態管理。通過redux來抽象出公共狀態,讓不同的效果渲染都基於同一個狀態,從而達到效果聯動。
// ./src/Container/FilmChooseSeat.js
changeSeatConf(item, isSoldUrl, type) {
const { changeFilmBuySeatList } = this.props // 拿到store中傳出來的方法
let data = {
item: item, //座位資訊
isSoldUrl: isSoldUrl, //所有座位顏色列表
type: type
}
changeFilmBuySeatList(data)
}
render() {
let { filmSeatList, filmBuyList, location } = this.props
...
return (
<div>
<FilmSeatTitle location={ location }/>
<FilmSeat filmSeatList={ filmSeatList } //選座拖拽區域
filmBuyList={ filmBuyList }
animationTime={ 200 }
changeSeatConf={ this.changeSeatConf.bind(this) }/>
//通過這個函式將元件中事件傳遞到container中,
//由container發起action來進行改變state
<FilmSeatSale filmBuyList= { filmBuyList } //選座資訊
filmSeatList={ filmSeatList }
changeSeatConf={ this.changeSeatConf.bind(this) }/>
</div>
)
}
// ./src/Redux/Store/Store.js
export const mapStateToProps =(state)=> {
return {
...
filmSeatList:state.filmChooseSeatReducer.filmSeatList,//電影座位列表
filmBuyList:state.filmChooseSeatReducer.filmBuyList,//電影選座列表
}
}
export const mapDispatchToProps=(dispatch)=> {
return {
...
getFilmSeatList:(url,data)=>dispatch(FilmChooseSeatActions.fetchFilmSeatList(url,data)),//獲取電影座位列表
changeFilmBuySeatList:(data)=>dispatch(FilmChooseSeatActions.changeFilmBuySeatList(data))//選中座位購票
}
}複製程式碼
發起action後,在reducer中改變維護的filmBuyList
陣列狀態,就可以同時渲染好整個介面的變化。
// ./src/Redux/Reducer/FilmChooseSeatReducer.js
export const filmBuyList = (state = {item:[],isSoldUrl:{},type:``}, action={})=>{
switch(action.type){
case FilmChooseSeatActions.CHANGE_FILM_BUYSEAT:
let _state = Object.assign({}, state)
if(action.text.type === `add`) {
_state.item.push(action.text.item)
} else {
let index = _state.item.indexOf(action.text.item)
_state.item.splice(index, 1)
}
_state.isSoldUrl = action.text.isSoldUrl
_state.type = action.text.type
return _state
default:
return state
}
}複製程式碼
渲染預覽小圖
當完成了大圖的渲染以及選座狀態切換的工作之後,只需要複製一份大圖的渲染的那段jsx修改css樣式就可以完成一個預覽小圖。在這期間你不需要做任何事就可以看到小圖上面同樣會存在選座狀態的切換,這就是狀態管理的好處。只要你的介面效果和狀態進行了繫結,那麼在之後的工作中你就不需要再去關注效果而只需要關注狀態是否正確即可。在這其中唯一有一點問題的地方是預覽圖中紅色提示框的縮放和大圖的縮放是成反比的。大圖放大預覽圖中的紅色框應該縮小,同時大圖可拖拽的範圍應該和紅框的移動範圍有一個比例係數。在這次的實現中作者用了scaleNum
這個狀態來控制其縮放的係數,有興趣的童鞋可以自己嘗試一下如何計算一個正確的係數來保證大圖和預覽圖縮放後紅框移動距離和大圖拖拽範圍的匹配。
其他功能元件
區域選擇元件
電影列表元件
電影詳情元件
電影排期元件