一個簡單郵箱應用的實現:使用react+redux+webpack+css-modules

這幾天用react+redux+webpack寫了一個簡單的郵箱,這是我第一次用redux寫應用,以爲頗有必要記錄一下遇到的各類坑~~??css

DEMO在這裏:
https://yisha0307.github.io/M...
這邊是源碼:
https://github.com/yisha0307/...
(各位用github的旁友路過請隨意幫我點個贊喲!謝謝~)html

外觀如圖:
圖片描述node

零:開始以前

一、webpack:

webpack基礎的配置環境能夠先看我這篇文章:
https://segmentfault.com/a/11...react

而後再來說講此次特別的地方。webpack

1)CDN

考慮到最後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

2)UglifyJsPlugin

這個plugin也是爲了縮小最後的bundle.js的~
不過由於裝了這個plugin以後熱加載的速度會變慢,因此建議開發的時候先不要用~
另外還有一些辦法可讓bundle.js變小,好比關掉devtool之類的,具體能夠看我以前寫的一篇筆記:
https://segmentfault.com/n/13...數據庫

二、React和Redux

此次整個的應用我是這麼安排的:
圖片描述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}>裏。

1、實現功能

先看一眼我此次的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互相引用的問題啦~

2、react和redux的鏈接

此次是用react-redux這個庫來鏈接的,固然也能夠選擇不用react-redux,直接在root組件上把全部的action都dispatch一下,而後一級一級傳下去。

react-redux這個庫也很好理解,主要使用了Provider和connect兩個方法.

1)Provider

使用provider就是讓全部的組件有取到redux裏保存的state的可能性。只要在root組件外面包一層就能夠。須要的屬性就是store。

//index.jsx
const store = createStore(inboxApp)
class App extends Component{
    render(){
        return(
        <Provider store={store}>
            <Mailbox />
        </Provider>)
    }
}

2)connect()

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鍵值對~

3、須要注意的細節

記錄一下此次遇到的奇形怪狀的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}) => {...}

4、css部分

好啦,功能部分基本完成以後只要加上樣式表就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/...

相關文章
相關標籤/搜索