1. 使用脚手架vue-cli创建vue项目:vue create 项目名
2. 构建文件目录:
assets
css
img
common
components
common
content
network
router
store
views
3. 设置目录快速访问:根目录下新建vue.config.js
module.exports = {
configureWebpack: {
resolve: {
alias: {
'assets': '@/assets',
'common': '@/common',
'components': '@/components',
'network': '@/network',
'views': '@/views'
}
}
}
}
4. 创建主要的view,配置访问路径
使用路由懒加载方式,导入相应的view,如:
const Home = () => import('views/home/Home')
5. 构建TabBar,使用插槽slot和路由相关知识(this.$route)
6. 构建NavBar,使用插槽slot
7. 安装网络请求组件axios: npm install axios --save
7.1. 在network目录下新建request.js,封装axios组件
import axios from 'axios'
export function request(config) {
// 1.创建axios的实例
const instance = axios.create({
baseURL: 'http://152.136.185.210:8000/api/z8',
timeout: 5000
})
// 2.axios的拦截器
// 2.1.请求拦截器
instance.interceptors.request.use(config => {
// 1.比如config中的一些信息不符合服务器的要求
// 2.比如每次发送网络请求时,希望页面上显示一个加载中的图标
// 3.某些网络请求(比如登录(token)),必须携带一个特殊的信息
return config
}, error => {
console.log(error);
})
// 2.2.响应拦截
instance.interceptors.response.use(res => {
return res.data
}, error => {
console.log(error);
})
// 3.发送真正的网络请求(查看源码得知,这里返回的是一个Promise,故这里就不需要使用Promise封装再返回)
return instance(config)
}
7.2. 封装后的axios调用:
import {request} from "./request";
export function getHomeMultidata() {
return request({
url: '/home/multidata'
})
}
export function getHomeGoods(type, page) {
return request({
url: 'home/data',
params: {
type,
page
}
})
}
8. 安装better-scroll
8.1. 指定1.13.2版本:npm install better-scroll@1.13.2 --save
8.2. 封装better-scroll组件:
8.3. 。。。
9. 事件总线运用
9.1. 在main.js中添加事件总线对象:Vue.prototype.$bus = new Vue()
9.2. 使用事件总线定义事件:this.$bus.$emit('goodsImgLoad', 'params')
9.3. 调用总线事件:
this.$bus.$on('goodsImgLoad', (params) => {
console.log(params)
this.$refs.scroll.refresh()
})
10. 因商品图片很多,每张图片加载完就会调用总线事件,为了降低调用频率,可使用防抖函数:
export function debounce (func, delay=60) {
let timer = null
return function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, delay)
}
}
调用防抖函数:
const refresh = debounce(this.$refs.scroll.refresh, 100)
this.$bus.$on('goodsImgLoad', () => {
refresh()
})
11. 设置漂浮TabControl
解决思路:复制一份TabControl,设置好固定漂浮的位置并隐藏起来,当原先的TabControl滚动对应位置时,克隆的TabControl 就显示,这样就会达到TabControl从不漂浮变成漂浮的状态
11.1. 记录未滚动前TabControl的offsetTop,这里需要当图片加载完的时候再获取
11.2. 当滚动到的位置 >= 上面记录的offsetTop时,就显示克隆的TabControl
12. 上拉加载更多
12.1. better-scroll的pullUpLoad属性要设置为true
12.2. mounted生命周期函数中添加以下方法
if (this.pullUpLoad) {
this.scroll.on('pullingUp', () => {
this.$emit('pullUpLoadMore')
})
}
12.3. 因以上方法只会调用一次,故调用完之后需要回调完成加载函数,即可继续调用
methods中添加以下方法:
finishPullUpLoad() {
this.scroll && this.scroll.finishPullUp()
}
13. 返回顶部
13.1. 封装返回顶部组件
13.2. 定义一个常量,当滚动到此定义常量的位置时,就显示返回顶部组件
13.3. better-scroll中有scrollTo(x, y, time)方法,回到顶部即scrollTo(0, 0, 600)
14. 跳转到商品详情页
14.1. 跳转方式一:
路由设置为{path: '/detail/:iid', component: Detail},则跳转方式:this.$router.push('/detail/' + this.goodsItem.iid),获取参数:this.$route.params.iid
14.2. 跳转方式二:
路由设置为{path: '/detail', component: Detail},则跳转方式:this.$router.push({path: '/detail', query: {'iid': this.goodsItem.iid}}),获取参数:this.$route.query.iid
14.3. 获取到商品ID,发送网络请求获取数据。。。
14.4. 注意:因详情页很多图片,故需监听图片加载,使用防抖函数刷新scroll内容高度
15. 做到这里遇到一个better-scroll的一个BUG,就是从首页点击商品进入详情页,再返回首页,当滑动首页时,页面会自动滚回顶部
解决方法:当返回到首页时,在生命周期函数activated()中添加“this.$refs.scroll.refresh()”,重新刷新一下即可
16. 完成了首页和详情页,发现有些代码已经重复了,那么可以使用mixin混入,将重复的代码写到一个文字中
16.1. 新建mixin.js文件,混入“监听图片加载,刷新scroll内容区高度”和“回到顶部”
import BackTop from "components/common/backTop/BackTop";
import { debounce } from "./utils";
import { BACKTOP_DISTANCE } from "./const";
// 监听图片加载,刷新scroll内容区高度
export const goodsImgLoad = {
data() {
return {
scrollRefresh: null,
goodsImgListener: null
}
},
mounted() {
this.scrollRefresh = debounce(this.$refs.scroll.refresh, 60)
this.goodsImgListener = () => {
this.scrollRefresh()
}
this.$bus.$on('goodsImgLoad', this.goodsImgListener)
},
deactivated() {
// 离开页面时取消监听事件总线的首页图片加载
this.$bus.$off('goodsImgLoad', this.goodsImgListener)
},
destroyed() {
// 离开页面时,取消事件总线的图片监听(防止其他页面调用事件总线的图片监听时,此页面也随之调用)
this.$bus.$off('goodsImgLoad', this.goodsImgListener)
},
}
// 回到顶部
export const backTop = {
data() {
return {
isShowBackTop: false
}
},
components: {
BackTop
},
methods: {
listenShowBackTop(position) {
// 判断是否显示回到顶部按钮
this.isShowBackTop = -position.y > BACKTOP_DISTANCE
},
backTopClick() {
this.$refs.scroll.scrollTo(0, 0)
},
}
}
16.2. 调用:
import { goodsImgLoad, backTop } from "common/mixin";
mixins: [goodsImgLoad, backTop]
17. 详情页滚动内容至某个位置,对应的标题高亮显示
17.1. 当图片加载完成时,获取各个组件的offsetTop,为了防止频繁调用,此处可调用防抖函数
this.getThemeTopYs = debounce(() => {
this.coordinate = []
this.coordinate.push(0)
this.coordinate.push(this.$refs.params.$el.offsetTop)
this.coordinate.push(this.$refs.comment.$el.offsetTop)
this.coordinate.push(this.$refs.recommends.$el.offsetTop)
this.coordinate.push(Number.MAX_VALUE)
console.log(this.coordinate);
}, 100)
可见4个标题,却有5个坐标值(最后一个值去JS中的最大值),因为这里使用了hack做法(巧妙之处)
滚动的值在前后两个值的区间即可高亮显示
const positionY = -position.y
for (let i=0; i < this.coordinate.length-1; i++) {
if (this.currentIndex !== i && positionY >= this.coordinate[i] && positionY < this.coordinate[i+1]) {
this.currentIndex = i
this.$refs.nav.currentIndex = this.currentIndex
}
}
其中“this.currentIndex !== i”此条件是为了不重复赋值
18. vuex使用
18.1. 安装vuex:npm install vuex --save
18.2. 在mian.js中导入
import store from './store'
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
18.3. 在store文件夹下新建index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {},
mutations: {},
actions: {},
getters: {}
})
export default store
18.4. 为了方便管理,将mutations、actions、getters等抽成一个个文件
index.js:
import Vue from 'vue'
import Vuex from 'vuex'
import mutations from "./mutations";
import actions from "./actions";
import getters from "./getters";
Vue.use(Vuex)
const state = {
cartList: []
}
export default new Vuex.Store({
state,
mutations,
actions,
getters
})
mutations-type.js:
export const ADD_CART_COUNT = 'add_cart_count'
export const ADD_TO_CART = 'add_to_cart'
mutations.js: (补充:mutations用于改变state的数据,但是mutations只负责简单的修改,比较复杂的逻辑需actions来执行)
import { ADD_TO_CART, ADD_CART_COUNT } from "./mutations-type";
export default {
[ADD_TO_CART](state, payload) {
state.cartList.push(payload)
},
[ADD_CART_COUNT](state, payload) {
payload.count += 1
}
}
actions.js:
import { ADD_TO_CART, ADD_CART_COUNT } from "./mutations-type";
export default {
// context = {state, commit}(数据解构)
addToCartAction({state, commit}, payload) {
return new Promise((resolve, reject) => {
const product = state.cartList.find(res => res.iid === payload.iid)
if (product) {
commit(ADD_CART_COUNT, product)
resolve('商品数量+1')
} else {
payload.count = 1
payload.checked = true
commit(ADD_TO_CART, payload)
resolve('已加入到购物车')
}
})
}
}
getters.js:
export default {
getCartList(state) {
return state.cartList
},
getCartListLength(state) {
return state.cartList.length
}
}
18.5. mutations的方法调用:
this.$store.commit('addToCartAction', payload)
18.6. actions的方法调用:
this.$store.dispatch('addToCartAction', payload)
可简写为:
import { mapActions } from 'vuex'
methods: {
...mapActions(['addToCartAction']),
addToCart() {
this.addToCartAction(payload).then(res => {
console.log(res);
})
}
}
18.7. getters的方法调用
方式一:
在模板中调用:{{$store.getters.getCartListLength}}
方式二:
this.$store.getters.getCartListLength
方式三:
import { mapGetters } from 'vuex';
computed: {
...mapGetters(['getCartList'])
}
19. Toast插件方式封装
19.1. 新建toast文件夹,文件夹下有index.js和Toast.vue
19.2. mian.js中安装toast:
import toast from 'components/common/toast'
Vue.use(toast) //这里使用Vue的use方法,会执行toast中的install方法(即index.js中的install方法)
19.3. index.js:
import Toast from "./Toast"
const obj = {}
obj.install = function(Vue) {
// 1.创建组件构造器
const toastContrustor = Vue.extend(Toast)
// 2.new的方式,根据组件构造器,创建一个组件对象
const toast = new toastContrustor()
// 3.将组件对象手动挂载到某一个元素上
toast.$mount(document.createElement('div'))
// 4.toast.$el就是上面的div,将此代码添加到body标签中
document.body.appendChild(toast.$el)
Vue.prototype.$toast = toast
}
export default obj
19.4. Toast.vue:
{{message}}
19.5. Toast插件调用:
this.$toast.show(res)
20. 购物车相关知识(高阶函数的运用)
20.1. 判断所有商品是否被选中
isSelectAll() {
// 方法一
// return !this.getCartList.filter(res => !res.checked).length
// 方法二(效率比filter高)
// return !this.getCartList.find(res => !res.checked)
// 方法三
for (let item of this.getCartList)
if (!item.checked) return false
return true
}
20.2. 计算所有选中商品的总价
totalPrice() {
return this.getCartList.filter(item => {
return item.checked
}).reduce((prev, item) => {
return prev + item.price * item.count
}, 0).toFixed(2)
}
20.3. 全选按钮点击事件
allCheckClick() {
if (this.isSelectAll) {
this.getCartList.forEach(item => item.checked = false)
} else {
this.getCartList.forEach(item => item.checked = true)
}
}
21. fastclick-解决移动端300ms延迟的问题
21.1. 安装:npm install fastclick --save
21.2. 使用:
在mian.js中添加一下代码
import FastClick from 'fastclick'
FastClick.attach(document.body)
22. 图片懒加载
22.1. 安装:npm install vue-lazyload --save
22.2. 使用:
在mian.js中添加一下代码
import VueLazyload from "vue-lazyload";
// 图片懒加载调用
Vue.use(VueLazyload, {
loading: require('./assets/img/common/placeholder.png') //图片未加载完成时,显示该图片
})
22.3. 将“:src”改成“v-lazy”
23. 移动端适配 postcss-px-to-viewport
23.1. 安装:npm install postcss-px-to-viewport --save-dev
23.2. 修改配置文件postcss.config.js,在plugins{}中添加以下代码:
"postcss-px-to-viewport": {
viewportWidth: 375, // 视窗的宽度,对应的是我们设计稿的宽度
// viewportHeight: 667, // 视窗的高度,对应的是我们设计稿的高度(也可以不配置)
unitPrecision: 5, // 指定‘px’转换为视窗单位值的小数位数(很多时候无法整除)
viewportUnit: 'vw', // 指定需要转换成的视窗单位,建议使用vw
selectorBlackList: [], // 指定不需要转换的class类名
minPixelValue: 1, // 小于或等于‘1px’不转换为视窗单位
mediaQuery: false, // 允许在媒体查询中转换‘px’
exclude: [] // 不需要转换的文件(数组,数组元素要为正则表达式,如:[/MainTabBar.vue/])
}