vue全家桶上手小项目

本文源码

实现功能

主要用到的技术:
vue-cli + vue2 + vue-router2 + vuex2 + axios + es6 + sass + eslint

主要实现的功能:
页面的数据通过 axios 模拟请求本地的 json 文件获得;
vue-router2 实现各页面的相互跳转;
vuex2 全局状态的管理,如头部导航的标题内容,侧栏的显示状态;
简易购物车功能,详情页加入购物车的商品,随机生成单价、商品名字;
购物车的信息通过localstorage存储在本地;
注册登录的信息也是通过localstorage存储在本地。

项目目录结构

proj5-shop 目录结构,主要看src目录和static目录的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
│--build
|--config
|--dist
|--src
|--assets
|--logo.png
|--components
|--cart 购物车页
|--cate 商品列表页,商品详情页
|--center 个人中心,注册登录
|--com 公共模块
|--header.vue 头部
|--loading.vue 加载
|--sidebar.vue 导航侧栏
|--swiper.vue 轮播
|--jam.js 公共功能函数
|--localDB.js localStorage本地存储
|--page 首页
|--Hello.vue
|--static 本地数据模拟请求(需放static目录下)
|--data
|--cart.json
|--cate.json
|--index.json
| .gitkeep
|--test
│ .babelrc
│ .editorconfig
│ .eslintignore
│ .eslintrc.js
│ .gitignore
│ index.html
│ package.json
│ README.md

vue-cli 初始化及配置修改

vue-cli 脚手架官方安装:https://github.com/vuejs-templates/webpack

1
2
3
4
5
$ npm install -g vue-cli
$ vue init webpack proj5-shop
$ cd proj5-shop
$ npm install
$ npm run dev

vue-cli初始化完成后,继续新增安装以下依赖:

1
cnpm install axios node-sass vuex sass-loader vue-swipe --save-dev

修改 build/webpack.base.conf.js,使其对import引入的sass支持:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
}
// 将上面的修改成下面的:
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
loaders: {
'scss': 'vue-style-loader!css-loader!sass-loader',
'sass': 'vue-style-loader!css-loader!sass-loader?indentedSyntax'
}
}
}

关键功能技术点剖析

template 与指令

商品分类页 src/components/cate/cate.vue 的 template:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<template>
<div class="s-cate">
<div class="cate-nav">
<div class="nav-out">
<div class="nav">
<a class="nav-a" href="javascript:;"
v-for="(type, index) in types"
:class="{'nav-a-act': index==nowIndex}"
@click="clickType(type.type_now, index)">
{{type.type_name}}
</a>
</div>
</div>
</div>
<div class="cate-cont">
<ul>
<li v-for="brand in allBrand" v-if="nowType==brand.type || nowType=='type_all'">
<router-link to="detail" class="cont-li" href="javascript:;">
<img class="pic" :src="brand.brand_pic_url"/>
<span class="name">{{brand.brand_name}}</span>
<span class="price">{{brand.brand_price}}</span>
</router-link>
</li>
</ul>
</div>
</div>
</template>

商品详情页 src/components/cate/detail.vue 的 template:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div class="s-detail">
<comSwiper></comSwiper>
<div class="cont">
<p class="name">{{detailData.cart_name}}</p>
<span class="price">¥{{detailData.cart_price}}</span>
<div class="goods-counter">
<a href="javascript:;" class="btn-sub" @click="changeNum(-1, detailData)"> - </a>
<input type="text" class="goods-num" readonly="readonly" v-model="detailData.cart_num">
<a href="javascript:;" class="btn-add" @click="changeNum(1, detailData)"> + </a>
</div>
</div>
<div class="bot">
<!-- <router-link class="add-cart" v-on:click.native="addCart" to="/cart">加入购物车</router-link> -->
<a class="add-cart" href="javascript:;" @click="addCart">加入购物车</a>
</div>
</div>
</template>

axios 数据请求

首页的数据请求:
首先在入口文件 main.js 引入 axios,并将其挂在到 Vue 全局方法下:

1
2
3
// main.js
import axios from 'axios'
Vue.prototype.$http = axios

在首页 page/index.vue 使用 axios:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<script>
import comSwiper from '../com/swiper'
import '../../css/index.scss'
export default {
// 首先声明一些页面数据变量
data () {
return {
dataIndex: {},
temai: {},
rexiao: {},
jingpin: {}
}
},
created () {
// created的时候请求数据
this.getDataIndex()
},
methods: {
// 请求到数据并赋值给data里声明的变量
getDataIndex () {
this.$http.get('../../static/data/index.json').then((response) => {
this.dataIndex = response.data
this.temai = this.dataIndex.data.temai
this.rexiao = this.dataIndex.data.rexiao
this.jingpin = this.dataIndex.data.jingpin
}, (response) => {
// error
})
}
}
}
</script>

最终将数据渲染在 template 上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div class="cont-main cont-rexiao">
<router-link to="/detail" class="cont-left" href="javascript:;"
v-for="(brand, key, index) in rexiao"
v-if="key==0"
:key="brand.id">
<span class="name">{{brand.brand_name}}</span>
<span class="desc">{{brand.brand_desc}}</span>
<img class="pic" :src="brand.brand_pic_url"/>
</router-link>
<div class="cont-right">
<router-link to="/detail" class="cont-right-one" href="javascript:;"
v-for="(brand, key, index) in rexiao"
v-if="key>=1"
:key="brand.id">
<p class="text">
<span class="name">{{brand.brand_name}}</span>
<span class="desc">{{brand.brand_desc}}</span>
</p>
<img class="pic" :src="brand.brand_pic_url"/>
</router-link>
</div>
</div>

router 的跳转

router/router.js 路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
import App from '../App.vue'
import Index from '../components/page/index.vue'
import Cate from '../components/cate/cate.vue'
import Detail from '../components/cate/detail.vue'
import Center from '../components/center/center.vue'
import Cart from '../components/cart/cart.vue'
export default new VueRouter({
routes: [
{
path: '/',
redirect: '/index',
component: App,
children: [
{path: 'index', name: 'index', component: Index},
{path: 'cate', name: 'cate', component: Cate},
{path: 'detail', name: 'detail', component: Detail},
{path: 'center', name: 'center', component: Center},
{path: 'cart', name: 'cart', component: Cart}
]
}
],
linkActiveClass: 'footer-act'
})

主要是通过 router-link 来跳转,比如导航栏 com/sidebar.vue 的跳转:

1
2
3
4
5
6
<ul class="ul-nav" v-show="show">
<li><router-link to="/index"><span>首页</span><i>></i></router-link></li>
<li><router-link to="/cate"><span>分类</span><i>></i></router-link></li>
<li><router-link to="/center"><span>我的</span><i>></i></router-link></li>
<li><router-link to="/cart"><span>购物车</span><i>></i></router-link></li>
</ul>

当然,在 加入购物车 的时候,采用的编程式导航跳转路由:

1
2
3
4
5
<!-- <router-link class="add-cart" v-on:click.native="addCart" to="/cart">加入购物车</router-link> -->
<a class="add-cart" href="javascript:;" @click="addCart">加入购物车</a>
// 编程式导航,点击时触发路由跳转
router.push({ path: 'cart' })

vuex 状态管理

vuex 状态管理主要是头部的显示信息、导航栏的显示隐藏状态:
先来看 store/store.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
sideBarState: false, //导航侧栏的显示状态
headerTitle: '默认的头部标题' //不同页面头部标题的变更
},
mutations: {
changeSideBarState (state, boolean) {
state.sideBarState = boolean
},
changeHeaderTitle (state, str) {
state.headerTitle = str
}
},
actions: {
// changeSideBarState (context, status) {
// context.commit('changeSideBarState', status)
// }
// es6解构写法
changeSideBarState ({commit}, status) {
commit('changeSideBarState', status)
},
changeHeaderTitle ({commit}, str) {
commit('changeHeaderTitle', str)
}
},
getters: {
getSideBarState (state) {
return state.sideBarState
},
getHeaderTitle (state) {
return state.headerTitle
}
}
})

例如,在进入分类页 cate/cate.vue 时,会在 created 的时候触发头部标题的变更;
当点击头部 导航 时,又会触发导航侧栏的显示状态的变更:

1
2
3
4
5
6
7
8
9
10
11
12
created () {
this.$store.dispatch('changeHeaderTitle', '分类')
},
methods: {
showSideBar () {
return this.$store.dispatch('changeSideBarState', true)
// return this.$store.commit('changeSideBarState', true)
},
hideSideBar () {
return this.$store.dispatch('changeSideBarState', false)
}
}

购物车

在进入商品详情页的时,会随机生成商品的名称和价格,使得 加入购物车 时能在购物车页面区分开个商品(主要是还没做后端node+mongodb的数据):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// cate/detail.vue
data () {
return {
detailData: {
id: 100048,
type: 'type_man',
isSelect: true,
cart_img: 'http://ohe5avf3y.bkt.clouddn.com/pro/vue/vue-shop/vue-proj-goods.jpg',
cart_name: '商品名字' + this.getRandom(10, 100),
cart_num: 1,
cart_price: this.getRandom(10, 100)
}
}
}

点击 加入购物车,实际就是将该商品的 data 信息加入到 localStorage 的本地存储中;这里主要用到一个自己定义的 localDB.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export default class todoDb {
constructor (name) {
this.name = name
if (JSON.stringify(this.get(this.name)) === '{}') {
this.set([])
}
}
set (val) {
window.localStorage.setItem(this.name, JSON.stringify(val))
}
get () {
return JSON.parse(window.localStorage.getItem(this.name)) || {}
}
}

购物车 cart/cart.vue 主要思路就是:读取本地存储购物车中的localStorage 所有产品信息并显示出来;根据用户增删操作、来更新本地购物车存储的产品信息。
具体的实现直接看 购物车源码

注册登录

注册登录的用户中心页面 center/center.vue,主要是控制三种状态的显示与隐藏:登录、注册、登录成功后的用户中心,主要是data里面的 showState ;
还有就是表单数据的一些绑定验证,主要是data里面的 dataLogin 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<input name="mobile" type="tel" placeholder="请输入手机号" maxlength="11" v-model="dataLogin.name">
<input name="password" type="tel" placeholder="请输入密码" maxlength="6" v-model="dataLogin.pass">
<script>
data () {
return {
tips: '',
showState: 'logined',
dataLogin: {
name: '',
pass: '',
code: ''
},
// showState: 'register'
// showState: 'logining'
}
}
</script>

详情看github源码中的 center/center.vue

新手的小坑总结

build 后需在服务器打开访问
执行 npm run build 后生成的dist,直接在浏览器以本地文件 file:// … 打开里面的index.html 、是访问不了的;需要放在服务器上才能访问;或者自己在本地开启一个服务器。

引入的component,外层要有容器
如下面的 ,它外层一定要有容器、把它包裹着:

1
2
3
<div class="shop">
<comHeader></comHeader>
</div>

v-for key
component lists rendered with v-for should have explicit keys:

1
2
3
<router-link to="/detail" class="cont-one" v-for="brand in temai" :key="brand.id">
// ...
</router-link>

详见:https://cn.vuejs.org/v2/guide/list.html#key

原生html报waring
当我们引入的component命名成原生的html时,会报warning,于是把

1
2
3
4
5
6
7
8
9
<hearder></header> // 报 warning
<comHearder></comHeader>
import comHeader from './components/com/header.vue'
components: {
// hearder: hearder
comHeader: comHeader
}

判断对象为空

1
2
3
if (typeof myObj == "undefined") {
 var myObj = {};
}

webstorm卡顿
webstorm 需要把 node_models 文件夹排除掉(exclued),不然很卡顿。

目前正学习node+mongodb,准备以此取代该项目中模拟的本地数据请求和localStorage,项目代码:https://github.com/gjincai/vue-node-proj