這幾天用react+redux+webpack寫了一個簡單的郵箱,這是我第一次用redux寫應用,以爲頗有必要記錄一下遇到的各類坑~~??css
DEMO在這裏:
https://yisha0307.github.io/M...
這邊是源碼:
https://github.com/yisha0307/...
(各位用github的旁友路過請隨意幫我點個贊喲!謝謝~)html
外觀如圖:node
webpack基礎的配置環境能夠先看我這篇文章:
https://segmentfault.com/a/11...react
而後再來說講此次特別的地方。webpack
考慮到最後bundle.js的大小問題,第三方庫我都用的cdn, 此次用到的有:
react / redux / react-dom / react-redux / font-awesome,
除了最後一個,其餘的四個都要在webpack.config.js裏用externals註明一下:git
//webpack.config.js externals: { "react":"React", "redux":"Redux", "react-dom" :"ReactDOM", "react-redux":"ReactRedux" }
而後在寫的js/jsx文件裏開頭引用一下就行:github
//相似這樣的格式: import React,{Component} from 'react'
font-awesome由於是css,原本就是全局的,因此就不須要externals,直接用就行了~web
這個plugin也是爲了縮小最後的bundle.js的~
不過由於裝了這個plugin以後熱加載的速度會變慢,因此建議開發的時候先不要用~
另外還有一些辦法可讓bundle.js變小,好比關掉devtool之類的,具體能夠看我以前寫的一篇筆記:
https://segmentfault.com/n/13...數據庫
此次整個的應用我是這麼安排的:json
node_modules不說了,反正基本不用管;
public裏面放的是最後的bundle.js和index.html,和我本身作頭像的一張照片嘿嘿~
src裏就是主要寫的東西啦~由於是用react-redux的provider和connect寫的,因此分紅了containers和components,components放UI組件,containers放容器組件;
css我用的是sass,此次試了下css-module,也挺容易的,只要在webpack.config.js裏面的css-loader後面加上?modules
就能夠用css-module了,
具體用法:https://segmentfault.com/n/13...
由於沒有服務器端,此次的郵件就用inbox.json這個文件模擬;
reducers.js記錄此次使用的全部reducer,最後用redux裏的combineReducers
合併成一個,用createStore
引入到<Provider store={store}>
裏。
先看一眼我此次的reducers:
//import MAILS from './src/inbox.json'; import {combineReducers} from 'redux' import MAILS from './src/inbox.json' //一、mails //數據庫裏全部的Mails(包括顯示的和沒顯示的) //先對MAILS進行處理,每一個加上一個id let id = 0 for(const mail of MAILS){ mail.id = id++; } console.log(MAILS); const mails = (state = MAILS, action) => { switch(action.type){ case 'COMPOSE': return [...state, {from: action.from, address: action.address, time:action.time, message: action.message, subject:action.subject, id: id++, tag: action.tag, read:'true'}] case 'DELETE_MAIL': //根據id把這封郵件找出來,tag改爲'deleted' return state.map(mail => { if(mail.id !== action.id){return mail;}else{ return(Object.assign({}, mail, {"tag": "deleted"})); } }) case 'OPEN_MAIL': return state.map(mail => { if(mail.id !== action.id){return mail;}else{ return(Object.assign({},mail,{"read":"true"})); } }) default: return state } } //二、currentSection //顯示在mailist裏的mails const currentSection = (state = 'inbox', action) => { switch(action.type){ case 'SELECT_TAG': return action.tag; default: return state } } //三、selected //顯示在maildetail裏的那封郵件 const selectedEmailID = (state = null, action) => { switch(action.type){ case 'OPEN_MAIL': return action.id; case 'DELETE_MAIL': const mails = action.mails const selected= mails.find(mail => mail.tag === action.tag && mail.id > action.id); if(!selected){return null} return selected.id case 'SELECT_TAG': return null default: return state } } //四、composeORnot //若是值爲true,maillist和maildetail不出現,只出現composepart //若是值爲false, 反過來 const composeORnot = (state = false,action) => { switch(action.type){ case 'TURN_COMPOSE': return !state; case 'SELECT_TAG': return false default: return state } } //五、新加一個unread const showUnread = (state = false,action) => { switch(action.type){ case 'TURN_UNREAD': return action.bool; default: return state } } const inboxApp = combineReducers({mails,currentSection,selectedEmailID,composeORnot,showUnread}); export default inboxApp
此次用的reducers:
1) mails:
COMPOSE: 每寫一封郵件在原來的mails後面插入一封;
DELETE: 目標郵件的tag改爲'deleted';
OPEN:目標郵件的'read'改爲'true'.
2) currentSection:
SELECT_TAG:在右邊欄選到哪一個tag就render那個tag下的郵件隊列;
3)selectedEmailID:
OPEN_MAIL: open的爲目標郵件;
DELETE_MAIL: delete的郵件的下一封且同tag的郵件選爲目標郵件;
SELECT_TAG:選其餘的tag,select的mail就取消,等待用戶選擇;
4)composeORnot:
TURN_COMPOSE: 就是在頁面上點'compose'這個按鈕,mailList和mailDetail不出現,出現的是compose郵件的地方;
SELECT_TAG:在右邊欄選tag的時候,自動回到mailList和mailDetail;
5) showUnread
其實就是在頁面上的'all'和'unread'切換;
reducers其實就是用來記錄state的變化,因此寫react會用到幾個state, 這邊就須要幾個reducers~ 不過有一點不同的是,若是是寫react應用,思惟邏輯是從action => state的變化,可是react+redux就是從state的變化 => action。
舉個例子來講,若是我在react裏寫delete mail這個action,是這樣的:
//react w/o redux deleteEmail(id){ const emails = this.state.emails; const index = emails.findIndex(x=>x.id === id); emails[index].tag='deleted'; selectedEmail = emails.find( x=> x.tag===emails[id].tag && x.id > id); this.setState({ selectedEmail: selectedEmail, emails }) }
這個deleteEmail的動做其實影響到了
emails(選中的這封mail的tag變成'deleted')
selectedEmail(自動選同一個tag隊列裏的下一封郵件)
兩個state。可是寫的時候,邏輯實際上是從actions的角度出發的。
可是若是用redux寫,就是從state的角度出發考慮,能夠看我上面的reducers裏的mails和selectedEmailId兩個state裏都有DELETE_MAIL
這個action。
另外設計states的時候,要儘可能減小各個states之間的耦合,由於它們之間在reducers.js裏是無法互相引用的;可是若是實在無法徹底剝離開也是有辦法解決的。好比我上面寫的selectedEmailId
這個reducer,在DELETE_MAIL這個動做發出以後,要選擇下一封郵件做爲target,可是不能直接用action.id+1, 由於無法肯定在inbox.json文件裏,隊列裏下一個mail的tag是和你刪掉的同樣的,因此這時候我選擇把mails當作參數傳進去:
case 'DELETE_MAIL': const mails = action.mails //注意這個 const selected= mails.find(mail => mail.tag === action.tag && mail.id > action.id); if(!selected){return null} return selected.id
用的時候就直接把mails放在mapStateToProps, 而後再傳給dispatch就行~就能夠解決兩個state互相引用的問題啦~
此次是用react-redux這個庫來鏈接的,固然也能夠選擇不用react-redux,直接在root組件上把全部的action都dispatch一下,而後一級一級傳下去。
react-redux這個庫也很好理解,主要使用了Provider和connect兩個方法.
使用provider就是讓全部的組件有取到redux裏保存的state的可能性。只要在root組件外面包一層就能夠。須要的屬性就是store。
//index.jsx const store = createStore(inboxApp) class App extends Component{ render(){ return( <Provider store={store}> <Mailbox /> </Provider>) } }
connect就是一個比較重要的方法啦,它的意思就是把容器組件和UI組件聯繫在一塊兒。這樣只要寫好UI組件,外面用connect()包一層就行啦~
這個應用的UI組件和容器組件是這樣的:
前面加大v的就是容器組件,基本都是一一對應的關係,固然有些不須要邏輯層的能夠只用UI組件就行,好比MailItem這個~
拿Sidebar舉個例子(截取部分):
//sidebar.jsx const Sidebar =({currentSection, unreadcount,trashcount,sentcount,handleCategory,turncompose}) => { return ( <div className={styles.sidebar}> <button onClick={turncompose}> <i className="fa fa-pencil-square-o"/>Compose</button> ......
上面({})裏的參數,基本都是須要外層的容器組件傳給它的。
再看一下vsidebar.jsx裏(截取部分):
const mapStateToProps = (state) => { return { currentSection: state.currentSection, unreadcount: countunread(state.mails), trashcount: counttrash(state.mails), sentcount: countsent(state.mails) } } const mapDispatchToProps = (dispatch,ownProps) =>{ return { turncompose: () => { dispatch({type: 'TURN_COMPOSE'}) }, handleCategory: (tag) =>{ dispatch({type: 'SELECT_TAG', tag: tag}) } } } const VSidebar = connect(mapStateToProps,mapDispatchToProps)(Sidebar) export default VSidebar
也就是把須要的state用mapStateToProps傳遞,把方法用mapDispatchToProps來傳遞便可~寫法就是return鍵值對~
記錄一下此次遇到的奇形怪狀的bug,都是很是細節的地方:
一、須要引用的組件必定要寫export!!須要引用的組件必定要寫export!!須要引用的組件必定要寫export!! 重要的話說三遍。我被這個疏忽折磨了一個晚上。?
二、若是要用ref取到input裏的value的話,這個被命名的input必定別忘記先定義一下變量,舉個例子:
<input type = 'text' ref={(v)=>towhom = v} placeholder = 'address'/>
若是隻是這麼寫,在其餘地方用towhom.value或者其餘towhom的方法就會報錯。
得先寫一行:
let towhom;
三、寫mapDispatchToProps的時候,必定別忘記dispatch外層包個大括號~
const mapDispatchToProps = (dispatch) => { return { handlecompose: (address,message,subject) => {dispatch({ type:'COMPOSE', from: 'Chen Yisha', address:address, time: timeFormat(new Date()), message:message, subject: subject, tag:'sent', read:true })}, deleteemail: (mails,id,tag)=> {dispatch({type: 'DELETE_MAIL',mails,id,tag})} } }
四、若是UI組件用函數的方法寫,須要兩個及以上的參數的時候要加大括號:
//若是隻有一個參數: const ComposePart = (display) => {...} //若是有兩個以上參數(別忘記這個大括號): const ComposePart = ({display, handleCompose}) => {...}
好啦,功能部分基本完成以後只要加上樣式表就ok~
我此次用的css-module,能夠解決全局classname混亂的問題,能夠參考下:
https://segmentfault.com/n/13...
其實css仍是很強大的,除了解決應用漂不漂亮的問題,還可使用className完成一些邏輯層面的東西。
好比我在多個組件裏都用到了style={{display:display}}
。後面的display是個參數,能夠用mapStateToProps傳進去,或者依靠其餘的state判斷一下。而後只要用一個三元運算符就能夠解決要不要這個組件顯示的問題。
好比:
// 在vcomposepart.jsx裏,須要依靠composeORnot這個reducer判斷用戶有沒有選擇'comopse’,若是選擇了就顯示composepart,沒有選擇就顯示mailList和mailDetail這兩個組件。 // vcomposepart.jsx const mapStateToProps = (state) => { return { display: state.composeORnot? 'block':'none' } } //composepart.jsx const ComposePart = ({display, handleCompose}) => { let towhom, subject, mailbody return( <div className ={styles.composepart} style={{display:display}}> ......
而後還有一些小技巧:
1)想要有投影一邊的box-shadow,能夠用:after(用在選擇某個sidebar的tag的時候)
.currentSection{ border-left: 5px solid $green; &:after{ content: ''; background:linear-gradient(90deg, $light-green, #fff); width:30px; height:40px; display: block; position: relative; left:-30px; top:-40px; z-index:-1; } }
2) 在整個應用外部用一個box-shadow, 我以爲會顯得精緻漂亮不少~
.mailbox{ position:absolute; top:50%; left:50%; transform: translate(-50%,-50%);//這三行可讓整個應用居中 height: auto; width:auto; background-color: #fff; box-shadow: 0 0 20px #eee; //注意這行 border-radius: 5px; }
3)若是須要幾個組件在x軸或者y軸上排齊,能夠在父級上使用flexbox:
.flexb{ display: flex; display: -webkit-flex; flex-direction:row; flex-wrap:nowrap; justify-content:center; height: 500px; }
基本就是這樣啦!還有什麼問題你們能夠看下個人源碼~ 謝謝支持~!
https://github.com/yisha0307/...