我发现我对使用vuex并不擅长,现在跟我一起多多研究项目,好好补补vuex吧
这个开源项目地址为:https://github.com/bailicangdu/vue2-happyfri
这是一个答题的h5小项目,点击答案会保持状态,最后记录分数,还可以分享朋友圈
页面运行如下
我们接下来分析代码
在index.html中加了router-view入口
<!DOCTYPE html><html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, minimal-ui"> <meta name="screen-orientation" content="portrait"/> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="format-detection" content="telephone=no"> <meta name="full-screen" content="yes"> <meta name="x5-fullscreen" content="true"> <title>vue2-happyfri</title> </head> <body> <div id="app"> <router-view></router-view> </div> </body></html>
//app.vue<template> <div> <router-view></router-view> </div></template><script> export default { }</script><style> </style>
//main.jsimport Vue from 'vue'import VueRouter from 'vue-router'// 引入router 懒加载import routes from './router/router'// 引入状态管理import store from './store/'// 引入ajax方法import ajax from './config/ajax'import './style/common'import './config/rem'Vue.use(VueRouter)const router = new VueRouter({ routes})new Vue({ router, store,}).$mount('#app')
先看router.js中的懒加载路由怎么写的
//src\router\router.jsimport App from '../App'export default [{ path: '/', component: App, children: [{ path: '', component: r => require.ensure([], () => r(require('../page/home')), 'home') }, { path: '/item', component: r => require.ensure([], () => r(require('../page/item')), 'item') }, { path: '/score', component: r => require.ensure([], () => r(require('../page/score')), 'score') }]}]
接下来看ajax.js是怎么封装的
我们看下代码,其实是把ajax封装成了promise,不过真的超级优雅,有眼前一亮的感觉
//ajax.jsexport default (type='GET', url='', data={}, async=true) => { return new Promise((resolve, reject) => { //定义一个promise type = type.toUpperCase(); let requestObj; if (window.XMLHttpRequest) { requestObj = new XMLHttpRequest(); } else { requestObj = new ActiveXObject; } if (type == 'GET') { let dataStr = ''; //数据拼接字符串 Object.keys(data).forEach(key => { dataStr += key + '=' + data[key] + '&'; }) dataStr = dataStr.substr(0, dataStr.lastIndexOf('&')); url = url + '?' + dataStr; requestObj.open(type, url, async); requestObj.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); requestObj.send(); }else if (type == 'POST') { requestObj.open(type, url, async); requestObj.setRequestHeader("Content-type", "application/json"); requestObj.send(JSON.stringify(data)); }else { reject('error type'); } requestObj.onreadystatechange = () => { if (requestObj.readyState == 4) { if (requestObj.status == 200) { let obj = requestObj.response if (typeof obj !== 'object') { obj = JSON.parse(obj); } resolve(obj); }else { reject(requestObj); } } } })}
我们来看下config.js里面的rem是怎么封装的
//rem.js(function(doc, win) { var docEl = doc.documentElement, resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize', recalc = function() { var clientWidth = docEl.clientWidth; if (!clientWidth) return; docEl.style.fontSize = 20 * (clientWidth / 320) + 'px'; }; if (!doc.addEventListener) return; win.addEventListener(resizeEvt, recalc, false); doc.addEventListener('DOMContentLoaded', recalc, false);})(document, window);
接下来我们就是分析我不会的东西了store,这个里面的内容我们结合页面来看
这个store里面只有action.js,index.js,mutation.js可以想到的是在页面中定义的state具体内容没有抽出来
还有监听state的属性getters(getters定义:你也可以通过让 getter 返回一个函数,来实现给 getter 传参。)
我们看第一个页面,其实处理的很巧妙,按照我的来,就是一个.vue文件了,可是博主不是那样写的
他是写在生命周期中的
在初始化的过程中挂载的图片
看一下this.initializeData的调用
看一下initializeData的内容
他通过commit暴露出一个叫做INITIALIZE_DATA的方法,
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:
每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。
我们在mutation.js中可以看到这个INITIALIZE_DATA方法,按理说它是一个function()
//mutation.js [INITIALIZE_DATA](state) { state.itemNum = 1; state.allTime = 0; state.answerid = []; },}
我们看点击开始进入的页面,再结合代码看,也许发现乾坤
我们可以看到只是数据变化了,背景图片的之类的都没有变化,其实代码也是写在一起的,只是用了v-if
将首页和题目区分开来了。
我们接下来看一下关于题目的吧
在vue中我们知道data是占的大头,是比重最大的,也是最核心的部分,所有的东西围绕着data来的。
我们会选中一个题目,这个题目会有一个序号还有答案,我们会进行的操作是
点击下一题,每一题会有每一题的答案,选中答案的信息,到最后一题,我们交卷子,会跳到分数页面
这几个实现的方法如下
//点击下一题 nextItem(){ if (this.choosedNum !== null) { this.choosedNum = null; //保存答案, 题目索引加一,跳到下一题 this.addNum(this.choosedId) }else{ alert('您还没有选择答案哦') } }, //索引0-3对应答案A-B chooseType: type => { switch(type){ case 0: return 'A'; case 1: return 'B'; case 2: return 'C'; case 3: return 'D'; } }, //选中的答案信息 choosed(type,id){ this.choosedNum = type; this.choosedId = id; }, //到达最后一题,交卷,请空定时器,跳转分数页面 submitAnswer(){ if (this.choosedNum !== null) { this.addNum(this.choosedId) clearInterval(this.timer) this.$router.push('score') }else{ alert('您还没有选择答案哦') } },
看这些,我其实想象不到这个在store中的公共数据会怎么处理的,什么东西会放在action,mutation里面
看代码会发现把这些放在了mapState中,让其局部监听更新?
computed: mapState([ 'itemNum', //第几题 'level', //第几周 'itemDetail', //题目详情 'timer', //计时器 ]),
分析所有的代码我们发现这个addNum就是从大的状态中取出来的
mtation里面的addNum
在action里面通过commit暴露了两个方法
可以在mutation中进行一个更加细致的数据方法处理
itemcontainer全部代码如下
//src\components\itemcontainer.vue//itemcontainer.vue<template> <section> <header class="top_tips"> <span class="num_tip" v-if="fatherComponent == 'home'">{{level}}</span> <span class="num_tip" v-if="fatherComponent == 'item'">题目{{itemNum}}</span> </header> <div v-if="fatherComponent == 'home'" > <div class="home_logo item_container_style"></div> <router-link to="item" class="start button_style" ></router-link> </div> <div v-if="fatherComponent == 'item'" > <div class="item_back item_container_style"> <div class="item_list_container" v-if="itemDetail.length > 0"> <header class="item_title">{{itemDetail[itemNum-1].topic_name}}</header> <ul> <li v-for="(item, index) in itemDetail[itemNum-1].topic_answer" @click="choosed(index, item.topic_answer_id)" class="item_list"> <span class="option_style" v-bind:class="{'has_choosed':choosedNum==index}">{{chooseType(index)}}</span> <span class="option_detail">{{item.answer_name}}</span> </li> </ul> </div> </div> <span class="next_item button_style" @click="nextItem" v-if="itemNum < itemDetail.length"></span> <span class="submit_item button_style" v-else @click="submitAnswer"></span> </div> </section></template><script>import { mapState, mapActions } from 'vuex'export default { name: 'itemcontainer', data() { return { itemId: null, //题目ID choosedNum: null, //选中答案索引 choosedId:null //选中答案id } }, props:['fatherComponent'], computed: mapState([ 'itemNum', //第几题 'level', //第几周 'itemDetail', //题目详情 'timer', //计时器 ]), methods: { ...mapActions([ 'addNum', 'initializeData', ]), //点击下一题 nextItem(){ if (this.choosedNum !== null) { this.choosedNum = null; //保存答案, 题目索引加一,跳到下一题 this.addNum(this.choosedId) }else{ alert('您还没有选择答案哦') } }, //索引0-3对应答案A-B chooseType: type => { switch(type){ case 0: return 'A'; case 1: return 'B'; case 2: return 'C'; case 3: return 'D'; } }, //选中的答案信息 choosed(type,id){ this.choosedNum = type; this.choosedId = id; }, //到达最后一题,交卷,请空定时器,跳转分数页面 submitAnswer(){ if (this.choosedNum !== null) { this.addNum(this.choosedId) clearInterval(this.timer) this.$router.push('score') }else{ alert('您还没有选择答案哦') } }, }, created(){ //初始化信息 if(this.fatherComponent == 'home') { this.initializeData(); document.body.style.backgroundImage = 'url(./static/img/1-1.jpg)'; } }}</script><style lang="less"> .top_tips{ position: absolute; height: 7.35rem; width: 3.25rem; top: -1.3rem; right: 1.6rem; background: url(../images/WechatIMG2.png) no-repeat; background-size: 100% 100%; z-index: 10; .num_tip{ position: absolute; left: 0.48rem; bottom: 1.1rem; height: 0.7rem; width: 2.5rem; font-size: 0.6rem; font-family: '黑体'; font-weight: 600; color: #a57c50; text-align: center; } } .item_container_style{ height: 11.625rem; width: 13.15rem; background-repeat: no-repeat; position: absolute; top: 4.1rem; left: 1rem; } .home_logo{ background-image: url(../images/1-2.png); background-size: 13.142rem 100%; background-position: right center; } .item_back{ background-image: url(../images/2-1.png); background-size: 100% 100%; } .button_style{ display: block; height: 2.1rem; width: 4.35rem; background-size: 100% 100%; position: absolute; top: 16.5rem; left: 50%; margin-left: -2.4rem; background-repeat: no-repeat; } .start{ background-image: url(../images/1-4.png); } .next_item{ background-image: url(../images/2-2.png); } .submit_item{ background-image: url(../images/3-1.png); } .item_list_container{ position: absolute; height: 7.0rem; width: 8.0rem; top: 2.4rem; left: 3rem; -webkit-font-smoothing: antialiased; } .item_title{ font-size: 0.65rem; color: #fff; line-height: 0.7rem; } .item_list{ font-size: 0; margin-top: 0.4rem; width: 10rem; span{ display: inline-block; font-size: 0.6rem; color: #fff; vertical-align: middle; } .option_style{ height: 0.725rem; width: 0.725rem; border: 1px solid #fff; border-radius: 50%; line-height: 0.725rem; text-align: center; margin-right: 0.3rem; font-size: 0.5rem; font-family: 'Arial'; } .has_choosed{ background-color: #ffd400; color: #575757; border-color: #ffd400; } .option_detail{ width: 7.5rem; padding-top: 0.11rem; } }</style>
关于分数页面
代码如下
//index.vue<template> <div> <div class="your_scores_container"> <header class="your_scores"><span class="score_num">{{score}}</span><span class="fenshu">分!</span></header> <div class="result_tip">{{scoreTips}}</div> </div> <div class="share_button" @click="showCover"></div> <div class="share_code"> <header class="share_header">关注葡萄之家,获取答案。</header> <img src="../../images/4-4.png" height="212" width="212" class="code_img"> </div> <div class="share_cover" v-show="showHide" @click="showCover"> <img src="../../images/5-2.png" class="share_img"> </div> </div></template><script>import {mapState} from 'vuex';export default { name: 'score', data(){ return { showHide: false, //是否显示提示 score: 0, //分数 scoreTips:'', //分数提示 rightAnswer: [2, 7, 12, 13, 18], //正确答案 scoreTipsArr:['你说,是不是把知识都还给小学老师了?','还不错,但还需要继续加油哦!','不要嘚瑟还有进步的空间!','智商离爆表只差一步了!','你也太聪明啦,葡萄之家欢迎你!'], } }, computed: mapState(['answerid']), created(){ this.computedScore(); this.getScoreTip(); document.body.style.backgroundImage = 'url(./static/img/4-1.jpg)'; }, methods: { //计算分数 computedScore(){ this.answerid.forEach((item, index) => { if (item == this.rightAnswer[index]) { this.score += 20; } }) }, //是否显示分享提示 showCover: function (){ this.showHide = !this.showHide; }, //根据分数显示提示 getScoreTip: function (){ let index = Math.ceil(this.score/20)-1; this.scoreTips = this.scoreTipsArr[index]; } },}</script><style lang="less"> body{ background-image: url(../../images/4-1.jpg); padding-top: 1.2rem; } .your_scores_container{ width: 9.7rem; height: 9.1rem; background: url(../../images/4-2.png) no-repeat; background-size: 100% 100%; margin: 0 auto 0; position: relative; .your_scores{ position: absolute; width: 100%; text-indent: 3.3rem; top: 4.7rem; font-size: 1.4rem; font-weight: 900; -webkit-text-stroke: 0.05rem #412318; font-family: 'Microsoft YaHei'; .score_num{ font-family: Tahoma,Helvetica,Arial; color: #a51d31; } .fenshu{ color: #a51d31; } } .result_tip{ position: absolute; top: 7rem; width: 9rem; left: 0.6rem; color: #3e2415; font-size: 0.65rem; text-align: center; } } .share_button{ width: 6.025rem; height: 2.4rem; margin: 0.8rem auto 0; background: url(../../images/4-3.png) no-repeat 0.4rem 0; background-size: 5.825rem 100%; } .share_code{ width: 5.3rem; margin: 1.5rem auto 0; .share_header{ color: #664718; font-size: 0.475rem; font-family: 'Microsoft YaHei'; width: 7rem; font-weight: 500; } .code_img{ height: 5.3rem; width: 5.3rem; margin-top: 0.5rem; } } .share_cover{ position: fixed; bottom: 0; right: 0; top: 0; left: 0; background: url(../../images/5-1.png) no-repeat; background-size: 100% 100%; opacity: 0.92; } .share_img{ height: 10.975rem; width: 11.95rem; position: fixed; top: 0.5rem; left: 50%; margin-left: -5.975rem; }</style>