【vuex】vue2-happyfri

时间:2022-01-02 18:02:41

我发现我对使用vuex并不擅长,现在跟我一起多多研究项目,好好补补vuex吧
这个开源项目地址为:https://github.com/bailicangdu/vue2-happyfri
这是一个答题的h5小项目,点击答案会保持状态,最后记录分数,还可以分享朋友圈
页面运行如下
【vuex】vue2-happyfri

我们接下来分析代码
在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中的懒加载路由怎么写的

//srcrouterrouter.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 传参。)
【vuex】vue2-happyfri
我们看第一个页面,其实处理的很巧妙,按照我的来,就是一个.vue文件了,可是博主不是那样写的
他是写在生命周期中的
【vuex】vue2-happyfri
在初始化的过程中挂载的图片
看一下this.initializeData的调用
【vuex】vue2-happyfri
看一下initializeData的内容
【vuex】vue2-happyfri
他通过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 = [];
    },
}

我们看点击开始进入的页面,再结合代码看,也许发现乾坤
【vuex】vue2-happyfri
我们可以看到只是数据变化了,背景图片的之类的都没有变化,其实代码也是写在一起的,只是用了v-if
将首页和题目区分开来了。
【vuex】vue2-happyfri
我们接下来看一下关于题目的吧
在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
【vuex】vue2-happyfri
在action里面通过commit暴露了两个方法
【vuex】vue2-happyfri

可以在mutation中进行一个更加细致的数据方法处理
【vuex】vue2-happyfri
itemcontainer全部代码如下

//srccomponentsitemcontainer.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>

关于分数页面
【vuex】vue2-happyfri
代码如下

//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>