Vue项目实战(一)

作者: 我是上帝可爱多 | 来源:发表于2017-08-21 19:52 被阅读115次

    其实这段时间我自己感觉很迷茫,在国内高强度的工作压力承受习惯之后,突然处于一个相对比较轻松的环境反而还不适应,我总担心到了国内自己毫无竞争力,所以我依然保持对新技术的学习和关注,今天正儿八经的教大家如何写vue2,前提是有一定基础spa和node的同学。

    1 项目目录结构

    • asserts 放置静态资源的目录,包括css和image。
    • components 这是大家比较熟悉的组件目录。
    • fetch 如果对es6fetch 比较熟悉的同学就知道,抓取数据的。
    • page 自定义的小组件目录,往往都是component里面的子组件。
    • router 路由控制页面的跳转,spa的关键。
    • util 自定义工具类函数。
    • vuex vue的状态管理工具。

    我们来看一下入口app.vue的代码

    <template>
      <div id="app">
        <router-view></router-view>
      </div>
    </template>
    
    <script>
    export default {
      name: 'app',
      components: {
      }
    }
    </script>
    
    <style >
    @import './assets/style/reset.css';
    </style>
    

    通过这里你能够看到如何引用css文件,所有的内容都会被渲染到<router-view></router-view>里面。
    如果我需要引用的是scss文件

    step 1
    npm install sass-loader node-sass --save-dev
    
    step 2  webpack.base.config.js在loaders里面加上
    {
          test: /\.scss$/,
          loaders: ["style", "css", "sass"]
        }
    
    step 3
    <style lang="scss" scope>
    
    </style>
    

    接下来就是app.js这个入口js文件了,这个很关键。

    import Vue from 'vue'
    import App from './App'
    import router from './router'
    import MintUI from 'mint-ui'
    import 'mint-ui/lib/style.css'
    // 引入swiper
    import VueAwesomeSwiper from 'vue-awesome-swiper'
    import iView from 'iview'
    import 'iview/dist/styles/iview.css'
    // Vuex
    import Vuex from 'vuex'
    import store from './vuex/store'
    require('vue2-animate/dist/vue2-animate.min.css')
    Vue.config.productionTip = false
    Vue.use(Vuex)
    Vue.use(VueAwesomeSwiper)
    Vue.use(MintUI)
    Vue.use(iView)
    
    new Vue({
      el: '#app',
      router,
      Vuex,
      store,
      template: '<App/>',
      components: { App }
    })
    

    这个入口文件有许多写的是Vue.use,这就是想在项目中用插件的方式,本例中有VueAwesomeSwiper,MintUI,iView三个控件都是视图方面的,如果我想用jquery,那么你需要自己安装jquery,然后import进来,用Vue.use(jquery)。
    需要注意的是vuex也需要这样操作。

    问题来了,当我们npm run dev之后首先进入的是哪个页面?
    来看一下router目录下的index.js

    import Vue from 'vue'
    import Router from 'vue-router'
    
    // 首页
    import Index from '@/page/index/index'
    import Recommend from '@/page/index/recommend'
    import Limit from '@/page/index/limit'
    import Home from '@/page/index/home'
    import Cook from '@/page/index/cook'
    import Parts from '@/page/index/parts'
    import Cloth from '@/page/index/cloth'
    import Wash from '@/page/index/wash'
    import Baby from '@/page/index/baby'
    import Messy from '@/page/index/messy'
    import Drink from '@/page/index/drink'
    import Hobby from '@/page/index/hobby'
    Vue.use(Router)
    
    export default new Router({
      routes: [
        {
          path: '/',
          name: 'Index',
          component: Index,
          meta: { scrollToTop: true },
          children: [
            {
              path: '/',
              name: 'indexIndex',
              component: Recommend
            },
            {
              path: '/recommend',
              name: 'Recommend',
              component: Recommend
            },
            {
              path: '/limit',
              name: 'Limit',
              component: Limit
            },
            {
              path: '/home',
              name: 'Home',
              component: Home
            },
            {
              path: 'cook',
              name: 'Cook',
              component: Cook
            },
            {
              path: '/parts',
              name: 'Parts',
              component: Parts
            },
            {
              path: '/cloth',
              name: 'Cloth',
              component: Cloth
            },
            {
              path: '/wash',
              name: 'Wash',
              component: Wash
            },
            {
              path: '/baby',
              name: 'Baby',
              component: Baby
            },
            {
              path: '/messy',
              name: 'Messy',
              component: Messy
            },
            {
              path: '/drink',
              name: 'Drink',
              component: Drink
            },
            {
              path: '/hobby',
              name: 'Hobby',
              component: Hobby
            }
          ]
        }
    ]
    

    一上来就搞事情,这么复杂的一个路由。。。
    分析:进入项目,第一步是 '/',那么就会使用name=Index组件,注意他还有chilren路由,所以默认还会在Index组件里面加载name=indexIndex组件,也就是Recommend。想到这里我们肯定能想到Index组件里肯定有 <router-view></router-view>,话不多说,来看吧。

    @/page/index/index
    <template>
      <div class="Home">
        <v-header class="header"></v-header>
        <router-view class="content"></router-view>
        <v-footer class="footer"></v-footer>
        <go-top></go-top>
      </div>
    </template>
    

    我们看到这段template代码就会知道,这是一个普通的首页模式,一个header,content用来占坑,一个footer,一个gotop返回顶部。

    问题来了,这里有router-view占坑,但是router-link在哪里呢?先卖个官司,看一下这个文件里的js代码。

    import Header from '@/components/public/Header'
    import Footer from '@/components/public/Footer'
    import IndexTabs from '@/components/public/Tabs'
    import goTop from '@/components/public/GoTop'
    export default {
      name: 'index',
      created () {
        console.log('created')
        this.$Loading.config({
          color: '#b4282d',
          failedColor: '#f0ad4e',
          height: 5
        })
        this.$Loading.start()
        this.$store.dispatch('changeActive', 0)
      },
      mounted () {
        this.$Loading.finish()
        console.log('recommend mounted')
      },
      components: {
        'v-header': Header,
        'v-footer': Footer,
        'v-indexTabs': IndexTabs,
        goTop
      }
    }
    

    我们看到了create和mounted这种关键钩子函数,create在mounted之前,mouted是挂载dom节点,具体这里不讲了。

    this.$store.dispatch这是vuex里面的,等会儿会讲到。
    需要注意的是

    components: {
    'v-header': Header,
    'v-footer': Footer,
    'v-indexTabs': IndexTabs,
    goTop
    }

    我们要将引进的组件注册,你可以重新命名,也可以不必,如goTop组件。

    我们来看看header头部是如何写的。

    @/component/public/header
    <template>
      <div class="header-container">
        <div class="line" >
          <router-link to="/"  class="logo"></router-link>
          <router-link to="/search" class="m-topSearchIpt ipt" >
              <i class="icon" ></i>
              <span class="placeholder" >
                <span >商品搜索, 共</span> <span >5116</span> <span >款好物</span>
              </span>
          </router-link>
        </div>
        <v-indexTabs :tabs="tabs"></v-indexTabs>
      </div>
    </template>
    
    <script>
    import IndexTabs from '@/components/public/Tabs'
    export default {
      name: 'index',
      data () {
        return {
        }
      },
      components: {
        'v-indexTabs': IndexTabs
      },
      computed: {
        tabs () {
          return this.$store.getters.headertabList
        }
      }
    }
    </script>
    

    好了我们这里看到了v-indexTabs就知道所有的菜单选择都在这个里面,
    :tabs="tabs"父组件传递数据给子组件,这里tabs = this.$store.getters.headertabList,之前我有写过vuex的文章,看过的都知道这是在干嘛,等下再讲,咱们继续看IndexTabs。

    @/components/public/Tabs
     <template>
        <header >
              <div class="inner" >
                   <div class="list" >
                       <div class="list-container">
                        <div class="tab"  :class="{active: item.isActive}"
                                 v-for="item in tabs" :key="item.id">
                        <router-link :to="item.linkTo">
                                 <span class="txt" @click="activethis(item.id)">  {{item.name}} </span>
                     </router-link>
              </div>
              </div>
            </div>
          </div>
      </header>
    </template>
    
    <script>
    export default {
      props: ['tabs'],
      data () {
        return {
        }
      },
      methods: {
        activethis (id) {
          // 找出当前激活的选项,当前点击的选项
          if (this.$route.path.indexOf('type') >= 0) {
            this.$store.dispatch('changeTypesabActive', id)
            return false
          }
          if (this.$route.path.indexOf('mylist') >= 0) {
            this.$store.dispatch('changeMylistActive', id)
            return false
          }
          console.log(id)
          this.$store.dispatch('changeHeadertabActive', id)
        }
      }
    }
    </script>
    

    这里通过一个v-for指令把一个router-link渲染出来了,:class="{active: item.isActive}"这个我也不多说了,相信大家都懂,注意router-link里面要写:to,通过activethis控制跳转。

    this.$route.path.indexOf('type') >= 0  这里是判断当前路由里面是否包含type
    

    到这里大家宏观上应该已经完全把控,接下里就看一下vuex里面是如何写的,因为这里路由跳转也是通过dispatch来实现的。

    2 状态管理

    下面我们来看下vuex目录结构

    我们来看下store.js里面是如何写的

    store.js
    import Vue from 'vue'
    import Vuex from 'vuex'
    
    import user from './modules/user'
    import footer from './modules/footer'
    // 头部分类
    import headerTabs from './modules/headertabs'
    import home from './modules/home'
    import cook from './modules/cook'
    import type from './modules/type'
    // 脚部分类
    import footclassification from './modules/footclassification'
    import shopCart from './modules/shopCart'
    import order from './modules/order'
    import mylist from './modules/mylists'
    
    Vue.use(Vuex)
    
    export default new Vuex.Store({
      modules: {
        user,
        footer,
        home,
        cook,
        type,
        shopCart,
        order,
        mylist,
        footclassification,
        headerTabs
      }
    })
    

    如果没记错我们是通过tabs是通过this.$store.getters.headertabList获得的,这个store把modules里面的每一个状态都包含进来了。我们来看下hedertabs的内容

    import * as types from '../types'
    const state = {
      headertabList: [
            {id: 0, name: '推荐', isActive: true, linkTo: '/recommend'},
            {id: 1, name: '居家', isActive: false, linkTo: '/home'},
            {id: 2, name: '餐厨', isActive: false, linkTo: '/cook'},
            {id: 3, name: '配件', isActive: false, linkTo: '/parts'},
            {id: 4, name: '服装', isActive: false, linkTo: '/cloth'},
            {id: 5, name: '洗护', isActive: false, linkTo: '/wash'},
            {id: 6, name: '婴童', isActive: false, linkTo: '/baby'},
            {id: 7, name: '杂货', isActive: false, linkTo: '/messy'},
            {id: 8, name: '饮食', isActive: false, linkTo: '/drink'},
            {id: 9, name: '志趣', isActive: false, linkTo: '/hobby'}
      ]
    }
    
    const actions = {
      changeHeadertabActive ({commit}, id) {
        commit(types.CHANGE_HEADER_TAB, id)
      },
      changeTypesabActive ({commit}, id) {
        commit(types.CHANGE_TYPES_TAB, id)
      },
      changeMylistActive ({commit}, id) {
        commit(types.CHANGE_MYLIST_TAB, id)
      }
    }
    const getters = {
      headertabList: state => state.headertabList,
      typesTabs: state => state.typesTabs,
      selfmylist: state => state.mylist
    }
    const mutations = {
      [types.CHANGE_HEADER_TAB] (state, id) {
        state.headertabList.forEach(list => {
          list.isActive = false
        })
        state.headertabList[id].isActive = true
      },
      [types.CHANGE_TYPES_TAB] (state, id) {
        state.typesTabs.forEach(list => {
          list.isActive = false
        })
        state.typesTabs[id].isActive = true
      },
      [types.CHANGE_MYLIST_TAB] (state, id) {
        state.mylist.forEach(list => {
          list.isActive = false
        })
        state.mylist[id].isActive = true
      }
    }
    export default {
      state,
      actions,
      getters,
      mutations
    }
    

    注意在module下面的文件格式都是这样的,一个state,一个actions,一个getters,一个mutations,最后别忘了

    export default {
      state,
      actions,
      getters,
      mutations
    }
    

    所以我们弄清楚了tabs内容的来源,就是headertabs里面的state.headertabList。

    我们继续看一个module下面的文件

    shopCart.js
    import * as types from '../types'
    import Util from '../../util/common'
    const STORAGE_CARTLIST_KEY = 'STORAGE_CARTLIST_KEY'
    const state = {
      cartList: Util.getLocal(STORAGE_CARTLIST_KEY) || [],
      isExist: false
    }
    const actions = {
      // set
      setCartList ({commit}, obj) {
        commit(types.SET_CART_LISTS, obj)
      },
      saveCartList ({commit}) {
        commit(types.SAVE_CART_LIST)
      },
      checkIsExist ({commit}, obj) {
        commit(types.CHECK_CART_ISEXIST, obj)
      },
      delCart ({commit}, obj) {
        commit(types.DEL_CART_CART, obj)
      }
    }
    const getters = {
      cartList: state => state.cartList,
      total: state => state.cartList.length,
      isExist: state => state.isExist,
      // 已经加入购物车的商品总量
      allNum: state => {
        let total = 0
        state.cartList.forEach(item => {
          total += item.number
        })
        return total
      }
    }
    const mutations = {
      [types.SET_CART_LISTS] (state, obj) {
        state.cartList.push(obj)
      },
      // 保存到购物车到本地
      [types.SAVE_CART_LIST] (state) {
        Util.setLocal(state.cartList, STORAGE_CARTLIST_KEY)
      },
      // exist this.++ else insert a new record
      [types.CHECK_CART_ISEXIST] (state, obj) {
        // 没有数据不做检查
        if (state.cartList.length === 0) return false
        let existIndex = state.cartList.findIndex((item) => {
          return item.type === obj.type && item.gid === obj.gid && item.picked === obj.picked
        })
        console.log(existIndex)
        // exist
        if (existIndex >= 0) {
          console.log(state.cartList[existIndex].number)
          state.cartList[existIndex].number ++
          state.isExist = true
        } else {
          state.isExist = false
        }
      },
      [types.DEL_CART_CART] (state, objs) {
        console.log(objs.length)
        objs.forEach(obj => {
          let index = state.cartList.findIndex((item) => {
            return item.gid === obj.id && item.type === obj.type
          })
           // 找出索引删除一个
          state.cartList.splice(index, 1)
        })
        Util.setLocal(state.cartList, STORAGE_CARTLIST_KEY)
      }
    }
    export default {
      state,
      actions,
      getters,
      mutations
    }
    

    里面的内容不重要,关键是我们看得出来他的写法,都是这种形式,另外可以通过this.$store.getters. 获取任意一个module下面的数据。

    当我们初次进入'/'会调用this.$store.dispatch('changeHeadertabActive', 0)。

    调用路线:

    changeHeadertabActive ({commit}, id) {
        commit(types.CHANGE_HEADER_TAB, id)
      }
    
    [types.CHANGE_HEADER_TAB] (state, id) {
        state.headertabList.forEach(list => {
          list.isActive = false
        })
        state.headertabList[id].isActive = true
      }
    

    所以其实任何改动都是在mutation里面进行的。

    3 页面分析

    当我们点击tab为home时,会用home组件加载。

    <template>
      <div>
         <each-tab :desc="homeDesc" ></each-tab>
      </div>
    </template>
    <script>
    import { Indicator } from 'mint-ui'
    import eachTab from '@/components/public/EachTab'
    export default {
      data () {
        return {
        }
      },
      components: {
        eachTab
      },
      created () {
        this.$store.dispatch('changeHeadertabActive', 1)
        Indicator.open('加载中...')
        this.$store.dispatch('gethomeDesc', 'home')
      },
      computed: {
        homeDesc () {
          Indicator.close()
          return this.$store.getters.homeDesc
        }
      }
    }
    </script>
    

    each-tab很明显是home下面每个商品的描述组件,在进入此组件时同样的会调用changeHeadertabActive来改变tab状态切换,同时会调用gethomeDesc来填充数据,最后在computed的时候就会将数据呈上。

    所以我只需看下home.js里面是如何写的即可

    @/index/home.js
    import * as types from '../types'
    import data from '@/fetch/api'
    const state = {
      homeDesc: {},
      homeDetail: {}
    }
    const actions = {
      // home简要
      gethomeDesc ({commit}, type) {
        console.log('type', type)
        data.getTypeDesc(type).then(res => {
          // console.log('type data:', res)
          commit(types.SET_HOME_DESC, res)
        })
      },
      gethomeDetail ({commit}, type, id) {
        data.getTypeDetail(type, id).then(res => {
          console.log('type data:', res)
          commit(types.SET_HOME_DETAIL, res)
        })
      }
    
    }
    const getters = {
      homeDesc: state => state.homeDesc
    }
    const mutations = {
      [types.SET_HOME_DESC] (state, res) {
        state.homeDesc = res
      },
      [types.SET_HOME_DETAIL] (state, res) {
        state.homeDetail = res
      }
    }
    export default {
      state,
      actions,
      getters,
      mutations
    }
    

    我们可以看出来gethomeDesc是按照type来fetch数据填充到homedesc里面,这个fetch涉及到promise,下节我们再讲。

    接下来看each-tab

    <template>
      <div>
          <div class="slideWarp">
            ![](desc.adpic)
      </div>
      <goods-grid :data="desc.data"></goods-grid>
      </div>
    </template>
    
    <script>
    import goodsGrid from '@/components/public/GoodsGrid'
    
    export default {
      props: ['desc'],
      data () {
        return {
        }
      },
      components: {
        goodsGrid
      }
    }
    </script>
    

    很明显最后商品列表都被渲染进了goods-grid。

    goods-grid
    <template>
      <div>
    <div class="goodsgrid" v-for="eachdata in data">
     <header>
       <h3 class="title" >{{eachdata.title}} <p>{{eachdata.subtitle}}</P></h3>
       
     </header>
     <div class="m-goodGrid">
       <ul class="list clearfix" >
         <li class="item" v-for="item in eachdata.lists" :key="item.id">
           <router-link :to="{ name: 'seeDetails', params: { type: item.type, id: item.id }}">
           ![](item.src)
           <div class="desc">{{item.desc}}</div>
           <div class="name" >{{item.name}}</div>
           <div class="price">¥{{item.price}}</div>
           </router-link>
           
         </li>
       </ul>
     </div>
    </div>
    </div>
    </template>
    <script>
    export default {
      props: ['data']
    }
    </script>
    

    每个li.item就是具体商品,然后点击它可以跳转到这个商品详情页,注意这里的路由。

     {
              path: '/detail/:type/id/:id',
              name: 'seeDetails',
              component: seeDetails
            }
    

    router里面有这样一段,我们就知道了上面的意思是说到seeDetails,并把params的参数携带过去。问题来了,商品详情都是用seeDeatils组件,那么这个数据填充是如何做的。。
    来看goodsDetail.vue

    <template>
      <div class="details">
        <div class="swiper">
          <swiper :options="swiperOption">
            <swiper-slide v-for="pic in detail.detailPic" :key="pic.id">
              ![](pic)
            </swiper-slide>
            <div class="swiper-pagination" slot="pagination"></div>
          </swiper>
        </div>
        <div class="m-detailBaseInfo">
          <goods-info :info="detail"></goods-info>
          <!--服务-->
          <goods-service ></goods-service>
          <!--评价-->
          <goods-comment :comment="detail.comment"></goods-comment>
        </div>
         <!--商品参数-->
         <goods-attr :attr="detail.attr"></goods-attr>
        <!--图片描述-->
        <div class="dt-section dt-section-1">
          <div class="m-detailHtml">
            <p v-for="item in detail.detailHtml">![](item)</p>
          </div>
        </div>
        <go-top></go-top>
      </div>
    </template>
    <script>
    import { Indicator } from 'mint-ui'
    import goodsComment from './goodsComment'
    import goodsAttr from './goodsAttr'
    import goodsService from './goodsService'
    import goodsInfo from './goodsInfo'
    import goTop from '@/components/public/GoTop'
    export default {
      name: 'Detail',
      data () {
        return {
          swiperOption: {
            autoplay: 3500,
            loop: true,
            setWrapperSize: true,
            pagination: '.swiper-pagination',
            paginationType: 'fraction',
            paginationClickable: true,
            mousewheelControl: true,
            observeParents: true
          }
        }
      },
      components: {
        goodsComment,
        goodsAttr,
        goodsService,
        goodsInfo,
        goTop
      },
      created () {
        Indicator.open('加载中...')
        console.log(1)
        let type = this.$route.path.split('/')[2]
        let id = this.$route.path.split('/')[4]
        console.log('detail', {type, id})
        this.$store.dispatch('getDetail', {type, id})
      },
      mounted () {
        console.log(this.$route.path)
      },
      computed: {
        detail () {
          Indicator.close()
          console.log(this.$store.getters.Detail)
          return this.$store.getters.Detail
        }
      }
    }
    </script>
      }
    

    这里面有许多swiper这种ui插件,暂且不管,我们来看看是如何做到把商品详情的数据拿到的

    let type = this.$route.path.split('/')[2]
        let id = this.$route.path.split('/')[4]
        console.log('detail', {type, id})
        this.$store.dispatch('getDetail', {type, id})
    
    computed: {
        detail () {
          Indicator.close()
          console.log(this.$store.getters.Detail)
          return this.$store.getters.Detail
        }
      }
    

    所以我们看到是用this.$route.path拿到地址的参数进而去dispatch改变数据,做到的。

    getDetail ({commit}, obj) {
        console.log(`post ${obj.type}${obj.id} data:`)
        data.getTypeDetail(obj.type, obj.id).then(res => {
          // console.log('res', res)
          commit(types.SET_TYPE_DETAIL, res)
        })
      }
    
    [types.SET_TYPE_DESC] (state, res) {
        state.Desc = res
      }
    

    之前一直在搞angular,现在发现框架对这样的问题处理都一样,所以我劝那些想把前端学好的同学先不要慌着搞这些,js基础才是王道,基础好了学什么都是一下午的事。。。

    相关文章

      网友评论

        本文标题:Vue项目实战(一)

        本文链接:https://www.haomeiwen.com/subject/dkthdxtx.html