其实这段时间我自己感觉很迷茫,在国内高强度的工作压力承受习惯之后,突然处于一个相对比较轻松的环境反而还不适应,我总担心到了国内自己毫无竞争力,所以我依然保持对新技术的学习和关注,今天正儿八经的教大家如何写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基础才是王道,基础好了学什么都是一下午的事。。。