Vue 2.0 起步(3) 數(shù)據(jù)流vuex和LocalStorage實例 - 微信公眾號RSS

參考:

如果你是克隆git里源碼的宪摧,注意工程目錄名是vue-tutorial/绢淀,步驟:http://www.reibang.com/p/b3c76962e3d4
https://github.com/kevinqqnj/vue-tutorial
請使用新的template: https://github.com/kevinqqnj/flask-template-advanced

本篇目標:給我們的應用 - “簡讀 - 微信公眾號RSS”,添加搜索而昨、訂閱舶担、取消公眾號功能,以及實現(xiàn)本地數(shù)據(jù)持久化

功能:

  • 用戶搜索公眾號 -> 左側(cè)顯示搜索結(jié)果
  • 點擊左側(cè)搜索結(jié)果的公眾號右邊星星 -> 訂閱箕昭,同時右側(cè)狀態(tài)欄會更新
  • 再次點擊星星 -> 取消訂閱
  • 右側(cè)狀態(tài)欄里灵妨,鼠標移動到某個訂閱號上 -> 動態(tài)顯示刪除按鈕
  • 訂閱列表 -> 保存到用戶本地LocalStorage,關(guān)掉瀏覽器落竹,下次打開依舊有效

DEMO網(wǎng)站
免費空間泌霍,有時第一次打開會等待啟動 -- 約10秒,后面打開就快了

最終完成是醬紫的:


起步(3)完成圖

數(shù)據(jù)流管理vuex

Vue是以數(shù)據(jù)驅(qū)動的框架述召,我們應用的各個組件之間朱转,相互有共用數(shù)據(jù)的交互,比如訂閱积暖、取消等等藤为。
對于大型應用來說,需要保證數(shù)據(jù)正確傳輸夺刑,防止數(shù)據(jù)被意外雙向更改缅疟,或者出錯后想調(diào)試數(shù)據(jù)的走向,這種情況下遍愿,vuex就可以幫忙存淫,而且它有個灰常好用的Chrome插件 -- vue-devtools,誰用誰知道错览!
當然纫雁,對于小應用,用eventbus也夠用了倾哺。我們這里為了打好基礎(chǔ)轧邪,就采用vuex了刽脖。

vuex-1.jpg

綠圈中是vuex 部分。它是整個APP的數(shù)據(jù)核心忌愚,相當于總管曲管,所有“共用”數(shù)據(jù)的變動,都得通知它硕糊,且通過它來處理和分發(fā)院水,保證了“單向數(shù)據(jù)流”的特點:

  • 客戶端所有組件都通過action 中完成對流入數(shù)據(jù)的處理(如異步請求、訂閱简十、取消訂閱等)
  • 然后通過action 觸發(fā)mutation修改state (同步)檬某。mutation(突變)就是指數(shù)據(jù)的改變
  • 最后由state經(jīng)過getter分發(fā)給各組件

另外,為了拿到搜索結(jié)果螟蝙,我們用ajax請求搜狗網(wǎng)站搜索恢恼,會用到vue-resouce,當然胰默,你用其它的ajax也沒問題场斑。

vuex官方文檔:(http://vuex.vuejs.org/zh-cn/)
vue-resource文檔: (https://github.com/pagekit/vue-resource)

安裝vuex和vue-resource

cnpm i vuex vue-resource -S

創(chuàng)建/store目錄,在目錄里給vuex新建4個文件牵署。它們對應于vuex的三個模塊:Actions(actions.js)漏隐,Mutations (另加types),State(index.js)


vuex-files.PNG

先考慮哪些數(shù)據(jù)是需要在組件之間交互的:

  1. 訂閱列表subscribeList:肯定是要的
  2. 搜索結(jié)果列表mpList:搜索結(jié)果有很多頁奴迅,需要有個總表來存儲多個搜索頁面青责。另外,左邊搜索頁面里公眾號的訂閱狀態(tài)取具,需要跟右邊訂閱列表同步
  3. 沒有了 爽柒;)

把它們寫入 vuex index.js:

# /src/store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
import mutations from './mutations'
import actions from './actions'

Vue.use(Vuex);

const state = {
    mpList: [],         // 搜索結(jié)果列表
    subscribeList: []   // 訂閱列表
};

export default new Vuex.Store({
    state,
    mutations,
    actions
})

定義各個突變類型:

# /src/store/mutation-types.js
// 訂閱公眾號
export const SUBSCRIBE_MP = 'SUBSCRIBE_MP';
export const UNSUBSCRIBE_MP = 'UNSUBSCRIBE_MP';

// 搜索列表處理
export const ADD_SEARCHRESULT_LIST = 'ADD_SEARCHRESULT_LIST';
export const UNSUBSCRIBE_SEARCHRESULT = 'UNSUBSCRIBE_SEARCHRESULT';
export const CLEAR_SEARCHRESULT = 'CLEAR_SEARCHRESULT';

觸發(fā)的事件:

# /src/store/actions.js
import * as types from './mutation-types'

export default {
     subscribeMp({ commit }, mp) {
        commit(types.SUBSCRIBE_MP, mp)
    },
    unsubscribeMp({ commit }, weixinhao) {
        commit(types.UNSUBSCRIBE_MP, weixinhao)
    },
    addSearchResultList({ commit }, mp) {
        commit(types.ADD_SEARCHRESULT_LIST, mp)
    },
    unsubSearchResult({ commit }, weixinhao) {
        commit(types.UNSUBSCRIBE_SEARCHRESULT, weixinhao)
    },
    clearSearchResult({ commit }, info) {
        commit(types.CLEAR_SEARCHRESULT, info)
    }
}

突變時處理數(shù)據(jù):

# /src/store/mutations.js
import * as types from './mutation-types'

export default {
     // 在搜索列表中,訂閱某公眾號
    [types.SUBSCRIBE_MP] (state, mp) {
        state.subscribeList.push(mp);
        for(let item of state.mpList) {
            if(item.weixinhao == mp.weixinhao) {
                var idx = state.mpList.indexOf(item);
                state.mpList[idx].isSubscribed = true;
                break;
            }
        }
    },
    // 在Sidebar中者填,取消某公眾號訂閱
    [types.UNSUBSCRIBE_MP] (state, weixinhao) {
        for(let item of state.mpList) {
            if(item.weixinhao == weixinhao) {
                var idx = state.mpList.indexOf(item);
                state.mpList[idx].isSubscribed = false;
                break;
            }
        }
        for(let item of state.subscribeList) {
            if(item.weixinhao == weixinhao) {
                var idx = state.subscribeList.indexOf(item);
                console.log('unscrib:'+idx);
                break;
            }
        }
        state.subscribeList.splice(idx, 1);
    },
    // 搜索列表更新
    [types.ADD_SEARCHRESULT_LIST] (state, mps) {
        state.mpList = state.mpList.concat(mps);
    },
    // 在搜索列表中,取消某公眾號訂閱
    [types.UNSUBSCRIBE_SEARCHRESULT] (state, weixinhao) {
        for(let item of state.mpList) {
            if(item.weixinhao == weixinhao) {
                var idx = state.mpList.indexOf(item);
                state.mpList[idx].isSubscribed = false;
                break;
            }
        }
        for(let item of state.subscribeList) {
            if(item.weixinhao == weixinhao) {
                var idx = state.subscribeList.indexOf(item);
                console.log('unscrib:'+idx);
                break;
            }
        }
        state.subscribeList.splice(idx, 1);
    },
    // 清空搜索列表
    [types.CLEAR_SEARCHRESULT] (state, info) {
        console.log('clear search result:' + info);
        state.mpList = [];
    }
};

項目中引用 store和 vue-resource:

# src/main.js
import VueResource from 'vue-resource'
import store from './store'

Vue.use(VueResource)

new Vue({
    // el: '#app',
    router,
    store,
    ...App
}).$mount('#app')

后臺數(shù)據(jù)處理到這里做葵,就定義好了占哟,下面就讓各個組件來引用這些action了。

用搜狗接口酿矢,得到搜索公眾號列表

組件:在Search.vue里榨乎,刪除假數(shù)據(jù),真數(shù)據(jù)上場了瘫筐!

這里面用到了vue.js的基本功能蜜暑,比如:雙向綁定、計算屬性策肝、事件處理器肛捍、Class綁定等等隐绵,查一下官網(wǎng)很容易理解的。
微信返回的數(shù)據(jù)拙毫,是個小坑依许!微信很坑爹,把數(shù)據(jù)定義成XML格式缀蹄,而且tag和attribute混在一起峭跳,導致javascript 處理起來又臭又長。缺前。蛀醉。

這里不貼完整代碼了,后面有項目的git地址

# /src/components/Search.vue

<template>
    <div class="card">
        <div class="card-header" align="center">
            <form class="form-inline">
                <input class="form-control form-control-lg wide" v-model="searchInput" type="text"
                       @keyup.enter="searchMp(1)" placeholder="搜索公眾號">
                <button type="button" class="btn btn-outline-success btn-lg" :disabled="searchInput==''"
                        @click="searchMp(1)" ><i class="fa fa-search"></i></button>
            </form>
        </div>
        <div class="card-block">
            <div class="media" v-for="(mp,index) in mpList">
  // 循環(huán)顯示搜索結(jié)果中衅码,每個公眾號的詳細信息
拯刁。。肆良。請查看源碼
            </div>
        </div>
        <div class="card card-block text-xs-right" v-if="hasNextPage && searchResultJson && !isSearching">
            <h5 class="btn btn-outline-success btn-block" @click="searchMp(page)"> 下一頁 ({{page}})
                <i class="fa fa-angle-double-right"></i></h5>
        </div>
    </div>
</template>

<script>
    export default {
        name : 'SearchResult',
        data() {
            return {
                searchKey: '',
                searchInput: '',    // 輸入框的值
                searchResultJson: '',
                isSearching: false,
                page: 1,
                hasNextPage: true
            }
        },
        computed : {
            subscribeList() {
                // 重要筛璧!從vuex store中取出數(shù)據(jù)
                return this.$store.state.subscribeList
            },
            mpList() {
                // 重要!從vuexstore中取出數(shù)據(jù)
                return this.$store.state.mpList
            }
        },
        methods:{
            searchMp(pg) {
                this.isSearching = true;
                if (pg==1) {
                    this.searchKey = this.searchInput;
                    this.$store.dispatch('clearSearchResult', 'clear');
                    this.page = 1;
                    this.hasNextPage = true
                }
                this.$nextTick(function () { });
                this.$http.jsonp("http://weixin.sogou.com/weixinwap?_rtype=json&ie=utf8",
                    {
                        params: {
                            page: pg,
                            type: 1, //公眾號
                            query: this.searchKey
                        },
                        jsonp:'cb'
                    }).then(function(res){
    // 處理搜狗返回的數(shù)據(jù)惹恃,又臭又長
夭谤。。巫糙。請查看源碼
                    }
                    this.$store.dispatch('addSearchResultList', onePageResults);    // 通知 vuex保存搜索結(jié)果
                    this.searchInput = '';
                    this.page = this.page+1;
                    if (this.page > this.searchResultJson.totalPages) {
                        this.hasNextPage = false;
                    }
                    this.isSearching = false;
                },function(){
                    this.isSearching = false;
                    alert('Sorry, 網(wǎng)絡似乎有問題')
                });
            },
            subscribe(idx) {
                if (this.mpList[idx].isSubscribed== true ) {
                    // 如果已經(jīng)訂閱朗儒,再次點擊則為取消訂閱該公眾號
                    return this.$store.dispatch('unsubSearchResult',this.mpList[idx].weixinhao);
                }
                var mp = {
                    mpName : this.mpList[idx].title,
                    image : this.mpList[idx].image,
                    date : this.mpList[idx].date,
                    weixinhao : this.mpList[idx].weixinhao,
                    encGzhUrl : this.mpList[idx].encGzhUrl,
                    subscribeDate : new Date().getTime(),
                    showRemoveBtn: false
                };
                for(let item of this.subscribeList) {
                    if(item.mpName == mp.mpName) return false
                }
  // 通知 vuex,訂閱某公眾號
                this.$store.dispatch('subscribeMp', mp);
            }
        }
    }
</script>

右側(cè)的狀態(tài)欄 Sidebar

這個組件相對簡單参淹,左側(cè)搜索結(jié)果里點擊某公眾號后醉锄,vuex會記錄這個公眾號到subscribeList,那在Sidebar.vue里浙值,用computed計算屬性恳不,讀取subscribeList state就行。
如果我在Sidebar.vue里取消訂閱某公眾號开呐,也會通知vuex烟勋。vuex就會把左側(cè)搜索結(jié)果里,此公眾號的狀態(tài)筐付,設(shè)為“未訂閱”卵惦。這也是為什么我們在vuex里,需要mpList state的原因瓦戚!

# /src/components/Sidebar.vue
<template>
    <div class="card">
        <div class="card-header" align="center">
            <img src="http://avatar.csdn.net/1/E/E/1_kevin_qq.jpg"
                 class="avatar img-circle img-responsive" />
            <p><strong> 非夢</strong></p>
            <p class="card-title">訂閱列表</p>
        </div>
        <div class="card-block">
            <p v-for="(mp, idx) in subscribeList" @mouseover="showRemove(idx)" @mouseout="hideRemove(idx)">
                <small>
                    <a class="nav-link" :href="mp.encGzhUrl" target="_blank">
                        ![](mp.image) {{ mp.mpName }} </a>
                    <a href="javascript:" @click="unsubscribeMp(mp.weixinhao)">
                        <i class="fa fa-lg float-xs-right text-danger sidebar-remove"
                           :class="{'fa-minus-circle': mp.showRemoveBtn}"></i></a></small>
            </p>
        </div>
    </div>
</template>

<script>
    export default {
        name : 'Sidebar',
        data() {
            return { }
        },
        computed : {
            subscribeList () {
                // 從store中取出數(shù)據(jù)
                return this.$store.state.subscribeList
            }
        },
        methods : {
            unsubscribeMp(weixinhao) {
                // 刪除該公眾號
                return this.$store.dispatch('unsubscribeMp',weixinhao);
            },
            showRemove(idx) {
                return this.subscribeList[idx]['showRemoveBtn']= true;
            },
            hideRemove(idx) {
                return this.subscribeList[idx]['showRemoveBtn']= false;
            }        
        }
    }
</script>

重點看一下代碼中:

  • methods部分沮尿!訂閱、取消訂閱较解,都會使用$store.dispatch(action, <data>)畜疾,來通知 vuex更新數(shù)據(jù)
  • computed部分赴邻,其它組件用計算屬性,訪問$store.state.<數(shù)據(jù)源>來得到更新后的數(shù)據(jù)庸疾。
-> 事件觸發(fā)(action) -> 突變(mutation) -> 更新(state) -> 讀取(新state)

其實vuex沒有想象中的復雜吧乍楚,哈哈~

好,現(xiàn)在下載源碼届慈,npm run dev試一下徒溪,最好用vue-devtools好好體會一下,訂閱金顿、取消操作時臊泌,vuex里action、state的變化揍拆,可以回退到任一狀態(tài)哦:(只支持Chrome)


vue-devtools.png

本地存儲

LocalStorage是HTML5 window的一個屬性渠概,有5MB大小,足夠了嫂拴,而且各瀏覽器支持度不錯:


2011052411384081.jpg

LS操作極其簡單播揪,我們只須用到保存、讀取的函數(shù)就行:

window.localStorage.setItem("b","isaac");  //設(shè)置b為"isaac"
varb=window.localStorage.getItem("b");  //獲取b的值筒狠,字符串
window.localStorage.removeItem("c");  //清除c的值

由于vuex里猪狈,我們保存的狀態(tài),都是數(shù)組辩恼,而LS只支持字符串雇庙,所以需要用JSON轉(zhuǎn)換:

JSON.stringify(state.subscribeList);   // array -> string
JSON.parse(window.localStorage.getItem("subscribeList"));    // string -> array 

然后,來更新我們的vuex mutation和Sidebar.vue組件灶伊。
mutations.js對subscribeList操作時疆前,順帶操作LocalStorage:

# /src/store/mutations.js(部分)
import * as types from './mutation-types'

export default {
     // 訂閱某公眾號
    [types.SUBSCRIBE_MP] (state, mp) {
。聘萨。竹椒。
        window.localStorage.setItem("subscribeList", JSON.stringify(state.subscribeList))
    },
    // 刪除某公眾號
    [types.UNSUBSCRIBE_MP] (state, weixinhao) {
。米辐。碾牌。
        state.subscribeList.splice(idx, 1);
        window.localStorage.setItem("subscribeList", JSON.stringify(state.subscribeList))
    },
    //從LocalStorage 初始化訂閱列表
    [types.INIT_FROM_LS] (state, info) {
        console.log(info + window.localStorage.getItem("subscribeList"));
        if (window.localStorage.getItem("subscribeList")) {
            state.subscribeList = JSON.parse(window.localStorage.getItem("subscribeList")) ;
        }
        else state.subscribeList = []
    }
};

每次新打開瀏覽器,對Sidebar組件初始化時儡循,讀取LocalStorage里存儲的數(shù)據(jù):

# /src/components/Sidebar.vue (部分)
。征冷。择膝。
    export default {
        name : 'Sidebar',
        data() {
            return {}
        },
        created: function () {
            // 從LocalStorage中取出數(shù)據(jù)
            return this.$store.dispatch('initFromLS', 'init from LS');
        },
        computed : {
。检激。肴捉。

最后腹侣,放上項目源碼

DEMO網(wǎng)站

TODO:

  • 用戶點擊右側(cè)訂閱的公眾號 -> 左側(cè)顯示公眾號文章閱讀記錄,不再導向外部鏈接
  • 用戶注冊齿穗、登錄功能 -> 后臺Flask
  • 搜狗頁面經(jīng)常要輸入驗證碼 -> 后臺處理

敬請關(guān)注傲隶!
Vue 2.0 起步(4) 輕量級后端Flask用戶認證 - 微信公眾號RSS

你的關(guān)注和評論,鼓勵作者寫更多的好文章窃页!

參考:

http://www.reibang.com/p/ab778fde3b99

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末跺株,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子脖卖,更是在濱河造成了極大的恐慌乒省,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,576評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件畦木,死亡現(xiàn)場離奇詭異袖扛,居然都是意外死亡,警方通過查閱死者的電腦和手機十籍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評論 3 399
  • 文/潘曉璐 我一進店門蛆封,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人勾栗,你說我怎么就攤上這事惨篱。” “怎么了械姻?”我有些...
    開封第一講書人閱讀 168,017評論 0 360
  • 文/不壞的土叔 我叫張陵妒蛇,是天一觀的道長。 經(jīng)常有香客問我楷拳,道長绣夺,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,626評論 1 296
  • 正文 為了忘掉前任欢揖,我火速辦了婚禮陶耍,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘她混。我一直安慰自己烈钞,他們只是感情好,可當我...
    茶點故事閱讀 68,625評論 6 397
  • 文/花漫 我一把揭開白布坤按。 她就那樣靜靜地躺著毯欣,像睡著了一般。 火紅的嫁衣襯著肌膚如雪臭脓。 梳的紋絲不亂的頭發(fā)上酗钞,一...
    開封第一講書人閱讀 52,255評論 1 308
  • 那天,我揣著相機與錄音,去河邊找鬼砚作。 笑死窘奏,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的葫录。 我是一名探鬼主播着裹,決...
    沈念sama閱讀 40,825評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼米同!你這毒婦竟也來了骇扇?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,729評論 0 276
  • 序言:老撾萬榮一對情侶失蹤窍霞,失蹤者是張志新(化名)和其女友劉穎匠题,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體但金,經(jīng)...
    沈念sama閱讀 46,271評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡韭山,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,363評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了冷溃。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片钱磅。...
    茶點故事閱讀 40,498評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖似枕,靈堂內(nèi)的尸體忽然破棺而出盖淡,到底是詐尸還是另有隱情,我是刑警寧澤凿歼,帶...
    沈念sama閱讀 36,183評論 5 350
  • 正文 年R本政府宣布褪迟,位于F島的核電站,受9級特大地震影響答憔,放射性物質(zhì)發(fā)生泄漏味赃。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,867評論 3 333
  • 文/蒙蒙 一虐拓、第九天 我趴在偏房一處隱蔽的房頂上張望心俗。 院中可真熱鬧,春花似錦蓉驹、人聲如沸城榛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽狠持。三九已至,卻和暖如春瞻润,著一層夾襖步出監(jiān)牢的瞬間喘垂,已是汗流浹背献汗。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留王污,地道東北人。 一個月前我還...
    沈念sama閱讀 48,906評論 3 376
  • 正文 我出身青樓楚午,卻偏偏與公主長得像昭齐,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子矾柜,可洞房花燭夜當晚...
    茶點故事閱讀 45,507評論 2 359

推薦閱讀更多精彩內(nèi)容