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: 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/]) }