Vue 2.0 起步 (3) 数据流 vuex 和 LocalStorage 实例 - 微信公众号 RSS
2017-05-23 17:51
866 查看
参考:
Vue
2.0 起步(2) 组件及vue-router实例 - 微信公众号RSS
Vue
2.0 起步(1) 脚手架工具vue-cli + Webstorm 2016 + webpack
本篇主要介绍设计思路和过程。假定读者已有简单的Vue入门知识,如果没有,请先行补课:Vue.js官网教程
本篇目标:给我们的应用 - “简读 - 微信公众号RSS”,添加搜索、订阅、取消公众号功能,以及实现本地数据持久化
功能:
用户搜索公众号 -> 左侧显示搜索结果
点击左侧搜索结果的公众号右边星星 -> 订阅,同时右侧状态栏会更新
再次点击星星 -> 取消订阅
右侧状态栏里,鼠标移动到某个订阅号上 -> 动态显示删除按钮
订阅列表 -> 保存到用户本地LocalStorage,关掉浏览器,下次打开依旧有效
DEMO网站
免费空间,有时第一次打开会等待启动 – 约10秒,后面打开就快了
最终完成是酱紫的:
起步(3)完成图
Vue是以数据驱动的框架,我们应用的各个组件之间,相互有共用数据的交互,比如订阅、取消等等。
对于大型应用来说,需要保证数据正确传输,防止数据被意外双向更改,或者出错后想调试数据的走向,这种情况下,vuex就可以帮忙,而且它有个灰常好用的Chrome插件 – vue-devtools,谁用谁知道!
当然,对于小应用,用eventbus也够用了。我们这里为了打好基础,就采用vuex了。
vuex-1.jpg
绿圈中是vuex 部分。它是整个APP的数据核心,相当于总管,所有“共用”数据的变动,都得通知它,且通过它来处理和分发,保证了“单向数据流”的特点:
客户端所有组件都通过action 中完成对流入数据的处理(如异步请求、订阅、取消订阅等)
然后通过action 触发mutation修改state (同步)。mutation(突变)就是指数据的改变
最后由state经过getter分发给各组件
另外,为了拿到搜索结果,我们用ajax请求搜狗网站搜索,会用到vue-resouce,当然,你用其它的ajax也没问题。
vuex官方文档:( http://vuex.vuejs.org/zh-cn/ )
vue-resource文档: ( https://github.com/pagekit/vue-resource )
安装vuex和vue-resource
创建/store目录,在目录里给vuex新建4个文件。它们对应于vuex的三个模块:Actions(actions.js),Mutations (另加types),State(index.js)
vuex-files.PNG
先考虑哪些数据是需要在组件之间交互的:
订阅列表subscribeList:肯定是要的
搜索结果列表mpList:搜索结果有很多页,需要有个总表来存储多个搜索页面。另外,左边搜索页面里公众号的订阅状态,需要跟右边订阅列表同步
没有了 ;)
把它们写入 vuex index.js:
定义各个突变类型:
触发的事件:
突变时处理数据:
项目中引用 store和 vue-resource:
后台数据处理到这里,就定义好了,下面就让各个组件来引用这些action了。
组件:在Search.vue里,删除假数据,真数据上场了!
这里面用到了vue.js的基本功能,比如:双向绑定、计算属性、事件处理器、Class绑定等等,查一下官网很容易理解的。
微信返回的数据,是个小坑!微信很坑爹,把数据定义成XML格式,而且tag和attribute混在一起,导致JavaScript 处理起来又臭又长。。。
这里不贴完整代码了,后面有项目的Git地址
这个组件相对简单,左侧搜索结果里点击某公众号后,vuex会记录这个公众号到subscribeList,那在Sidebar.vue里,用computed计算属性,读取subscribeList state就行。
如果我在Sidebar.vue里取消订阅某公众号,也会通知vuex。vuex就会把左侧搜索结果里,此公众号的状态,设为“未订阅”。这也是为什么我们在vuex里,需要mpList state的原因!
重点看一下代码中:
methods部分!订阅、取消订阅,都会使用store.dispatch(action, <data>),来通知 vuex更新数据</li><li style="line-height:1.7em; list-style-type:disc">computed部分,其它组件用计算属性,访问store.state.<数据源>来得到更新后的数据。
其实vuex没有想象中的复杂吧,哈哈~
好,现在下载源码,npm run dev试一下,最好用vue-devtools好好体会一下,订阅、取消操作时,vuex里action、state的变化,可以回退到任一状态哦:(只支持Chrome)
vue-devtools.png
LocalStorage是HTML5 window的一个属性,有5MB大小,足够了,而且各浏览器支持度不错:
2011052411384081.jpg
LS操作极其简单,我们只须用到保存、读取的函数就行:
由于vuex里,我们保存的状态,都是数组,而LS只支持字符串,所以需要用JSON转换:
然后,来更新我们的vuex mutation和Sidebar.vue组件。
mutations.js对subscribeList操作时,顺带操作LocalStorage:
每次新打开浏览器,对Sidebar组件初始化时,读取LocalStorage里存储的数据:
最后,放上 项目源码
DEMO网站
用户点击右侧订阅的公众号 -> 左侧显示公众号文章阅读记录,不再导向外部链接
用户注册、登录功能 -> 后台Flask
搜狗页面经常要输入验证码 -> 后台处理
敬请关注!
Vue
2.0 起步(4) 轻量级后端Flask用户认证 - 微信公众号RSS
你的关注和评论,鼓励作者写更多的好文章!
参考:
一步一步教你用Vue.js
+ Vuex制作专门收藏微信公众号的app
HTML5
LocalStorage 本地存储
原文 http://www.jianshu.com/p/fb758398268a http://www.tuicool.com/articles/ZRJNJbm
Vue
2.0 起步(2) 组件及vue-router实例 - 微信公众号RSS
Vue
2.0 起步(1) 脚手架工具vue-cli + Webstorm 2016 + webpack
本篇主要介绍设计思路和过程。假定读者已有简单的Vue入门知识,如果没有,请先行补课:Vue.js官网教程
本篇目标:给我们的应用 - “简读 - 微信公众号RSS”,添加搜索、订阅、取消公众号功能,以及实现本地数据持久化
功能:
用户搜索公众号 -> 左侧显示搜索结果
点击左侧搜索结果的公众号右边星星 -> 订阅,同时右侧状态栏会更新
再次点击星星 -> 取消订阅
右侧状态栏里,鼠标移动到某个订阅号上 -> 动态显示删除按钮
订阅列表 -> 保存到用户本地LocalStorage,关掉浏览器,下次打开依旧有效
DEMO网站
免费空间,有时第一次打开会等待启动 – 约10秒,后面打开就快了
最终完成是酱紫的:
起步(3)完成图
数据流管理vuex
Vue是以数据驱动的框架,我们应用的各个组件之间,相互有共用数据的交互,比如订阅、取消等等。对于大型应用来说,需要保证数据正确传输,防止数据被意外双向更改,或者出错后想调试数据的走向,这种情况下,vuex就可以帮忙,而且它有个灰常好用的Chrome插件 – vue-devtools,谁用谁知道!
当然,对于小应用,用eventbus也够用了。我们这里为了打好基础,就采用vuex了。
vuex-1.jpg
绿圈中是vuex 部分。它是整个APP的数据核心,相当于总管,所有“共用”数据的变动,都得通知它,且通过它来处理和分发,保证了“单向数据流”的特点:
客户端所有组件都通过action 中完成对流入数据的处理(如异步请求、订阅、取消订阅等)
然后通过action 触发mutation修改state (同步)。mutation(突变)就是指数据的改变
最后由state经过getter分发给各组件
另外,为了拿到搜索结果,我们用ajax请求搜狗网站搜索,会用到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
创建/store目录,在目录里给vuex新建4个文件。它们对应于vuex的三个模块:Actions(actions.js),Mutations (另加types),State(index.js)
vuex-files.PNG
先考虑哪些数据是需要在组件之间交互的:
订阅列表subscribeList:肯定是要的
搜索结果列表mpList:搜索结果有很多页,需要有个总表来存储多个搜索页面。另外,左边搜索页面里公众号的订阅状态,需要跟右边订阅列表同步
没有了 ;)
把它们写入 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: [], // 搜索结果列表 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';
触发的事件:
# /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) } }
突变时处理数据:
# /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')
后台数据处理到这里,就定义好了,下面就让各个组件来引用这些action了。
用搜狗接口,得到搜索公众号列表
组件:在Search.vue里,删除假数据,真数据上场了!这里面用到了vue.js的基本功能,比如:双向绑定、计算属性、事件处理器、Class绑定等等,查一下官网很容易理解的。
微信返回的数据,是个小坑!微信很坑爹,把数据定义成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"> // 循环显示搜索结果中,每个公众号的详细信息 。。。请查看源码 </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中取出数据 return this.$store.state.subscribeList }, mpList() { // 重要!从vuexstore中取出数据 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){ // 处理搜狗返回的数据,又臭又长 。。。请查看源码 } this.$store.dispatch('addSearchResultList', onePageResults); // 通知 vuex保存搜索结果 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, 网络似乎有问题') }); }, subscribe(idx) { if (this.mpList[idx].isSubscribed== true ) { // 如果已经订阅,再次点击则为取消订阅该公众号 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>
右侧的状态栏 Sidebar
这个组件相对简单,左侧搜索结果里点击某公众号后,vuex会记录这个公众号到subscribeList,那在Sidebar.vue里,用computed计算属性,读取subscribeList state就行。如果我在Sidebar.vue里取消订阅某公众号,也会通知vuex。vuex就会把左侧搜索结果里,此公众号的状态,设为“未订阅”。这也是为什么我们在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中取出数据 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更新数据</li><li style="line-height:1.7em; list-style-type:disc">computed部分,其它组件用计算属性,访问store.state.<数据源>来得到更新后的数据。
-> 事件触发(action) -> 突变(mutation) -> 更新(state) -> 读取(新state)
其实vuex没有想象中的复杂吧,哈哈~好,现在下载源码,npm run dev试一下,最好用vue-devtools好好体会一下,订阅、取消操作时,vuex里action、state的变化,可以回退到任一状态哦:(只支持Chrome)
vue-devtools.png
本地存储
LocalStorage是HTML5 window的一个属性,有5MB大小,足够了,而且各浏览器支持度不错:2011052411384081.jpg
LS操作极其简单,我们只须用到保存、读取的函数就行:
window.localStorage.setItem("b","isaac"); //设置b为"isaac" varb=window.localStorage.getItem("b"); //获取b的值,字符串 window.localStorage.removeItem("c"); //清除c的值
由于vuex里,我们保存的状态,都是数组,而LS只支持字符串,所以需要用JSON转换:
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里存储的数据:
# /src/components/Sidebar.vue (部分) 。。。 export default { name : 'Sidebar', data() { return {} }, created: function () { // 从LocalStorage中取出数据 return this.$store.dispatch('initFromLS', 'init from LS'); }, computed : { 。。。
最后,放上 项目源码
DEMO网站
TODO:
用户点击右侧订阅的公众号 -> 左侧显示公众号文章阅读记录,不再导向外部链接用户注册、登录功能 -> 后台Flask
搜狗页面经常要输入验证码 -> 后台处理
敬请关注!
Vue
2.0 起步(4) 轻量级后端Flask用户认证 - 微信公众号RSS
你的关注和评论,鼓励作者写更多的好文章!
参考:
一步一步教你用Vue.js
+ Vuex制作专门收藏微信公众号的app
HTML5
LocalStorage 本地存储
原文 http://www.jianshu.com/p/fb758398268a http://www.tuicool.com/articles/ZRJNJbm
相关文章推荐
- Vue 2.0 起步 (3) 数据流 vuex 和 LocalStorage 实例 - 微信公众号 RSS
- Vue 2.0 起步 (3) 数据流 vuex 和 LocalStorage 实例 - 微信公众号 RSS
- Vue 2.0 起步(2) 组件及 vue-router实例 - 微信公众号RSS
- Vue 2.0 起步(2) 组件及 vue-router实例 - 微信公众号RSS
- 用webpack2.0构建vue2.0单文件组件超级详细精简实例
- Vuex 模块化与项目实例 (2.0)
- vue-lazyload基础实例(基于vue2.0和vue-router2.0)
- Vue.js 2.0从入门到放弃---入门实例(二)
- VUE2.0+Element-UI+Echarts封装的组件实例
- Vuex 模块化与项目实例 (2.0)
- Vue2.0 多 Tab切换组件的封装实例
- vue2.0 computed 计算list循环后累加值的实例
- 整理文档,搜刮出一个vue2.0的contextmenu右键弹出菜单的实例代码
- Vue2.0 一个login跳转实例
- Vue2.0+ElementUI实现表格翻页的实例
- Vue2.0笔记——Vue常用实例属性,实例方法
- Vue2.0入门实例注意事项
- vuejs2.0 vue实例的生命周期
- Vuex 模块化与项目实例 (2.0)
- vue2.0 axios前后端数据处理实例代码