我发现我对使用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.js
import 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.js
import 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.js
export 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>