尚品汇项目 - 1. Home组件
项目初始化
vue-cli脚手架初始化
在对应项目文件夹下:
vue create app
app文件夹的结构如下:
|____node_modules  # 文件夹,项目依赖文件夹
|____src  # 文件夹,一般放置静态资源(图片等),webpack打包时会原封不动打包到dist文件夹中
|____src  # 文件夹,源码
| |____App.vue  # 项目中唯一的root组件
| |____main.js  # 入口文件
| |____assets  # 文件夹,一般放置静态资源(多组件共用),webpack打包时会当成一个模块打包在JS文件中
| |____components  # 文件夹,一般放置非路由组件(全局组件)
|____babel.config.js  # babel相关配置文件
|____package.json  # 记录项目名、存在哪些依赖、怎么运行等
|____package-lock.json  # 缓存性文件
其他配置:
- 关闭eslint校验功能,修改vue.config.js文件,添加属性:
lintOnSave: false
路由分析
路由组件:
- Home组件:首页
- Search组件:搜索
- Login组件:登录
- Register组件:注册
非路由组件:
- Hearder组件:出现在所有界面
- Footer组件:出现在首页、搜索页
路由的跳转
注册好路由之后,所有组件(不论是否是路由组件)都有$route和$router属性:
- $route:一般获取路由信息(路径、query、params等)
- $router:一般进行编程式导航进行路由跳转(push、replace)
路由的跳转有以下的两种形式:
- 声明式导航<router-link>,通过to属性指定跳转的路由
- 编程式导航,使用组件实例的$router.push或者$router.replace方法,能做到声明式导航的功能,且能完成一些业务逻辑
<button class="sui-btn btn-xlarge btn-danger" type="button" @click="goSearch"> 搜索 </button>
// 搜索按钮回调函数
goSearch() {
    this.$router.push('/search');
}
路由元信息
export default new VueRouter({
    routes: [
        {
            path: '/home',
            component: Home,
            meta: {
                show: true,  // 是否显示Footer
            }
        },
		// {...}, {...}, ...
        {
            // 重定向到主页
            path: '/*',
            redirect: '/home'
        }
    ]
});
通过上面设置的路由元信息,可以通过$route属性读取到当前路由是否要显示<Footer>标签:
<div>
    <Header></Header>
    <router-view></router-view>
    <Footer v-if="$route.meta.show"></Footer>
</div>
路由传参
- params参数,属于路径的一部分,需要注意在配置路由时占位
- query参数,不属于路径当中的一部分,类似AJAX中的- queryString
// @/router/index.js
routes: [
    {
        name: 'search',  // 添加name
        path: '/search/:keyword',  // 占位
        component: Search,
        meta: {
            show: true,
        }
    }
    // ...
]
可以通过字符串或者对象的方式传递参数:
goSearch() {
    // 方式1: 字符串
    this.$router.push(`/search/${this.keyword}?kw=${this.keyword.toUpperCase()}`);
}
goSearch() {
    // 方式2: 对象
    this.$router.push({
        name: 'search',  // 必须@/router/index.js中配置name属性
        params: { keyword: this.keyword },
        query: { kw: this.keyword.toUpperCase() },
    });
}
经过传递后,params参数和query参数都在$route属性中,可以进行使用:
<div>
    Search <br>
    <h1>params: {{this.$route.params.keyword}}</h1>
    <h1>query: {{this.$route.query.kw}}</h1>
</div>
注意事项
- 指定params参数可传可不传
如果进行占位,但是没有传递params参数,则会出现下面的警告:
[vue-router] missing param for named route "search": Expected "keyword" to be defined
可以在占位时添加?来解决:
path: '/search/:keyword?',  // 占位 (可不传)
- params参数如果传递空串,会导致URL异常,可以通过手动传递- undefined解决
params: { keyword: this.keyword || undefined },
- 路由组件可以传递props,但是一般不使用,需要在@/route/index.js中配置- props: true,布尔值写法,
- 对象写法,额外给路由组件传递props
- 函数写法,将params和query打包成对象传给props
 
重写push和replace方法
多次执行编程式路由导航跳转时,会抛出NavigationDuplicated的错误,这是因为:
- VueRouter.prototype.push()方法使用- Proimse
- 该方法的第二、三个参数分别为Promise对象的onFulfilled回调函数和onRejected回调函数
- 当没有第二、三个参数时,错误交给全局处理,于是就会报错
可以通过传入onFulfilled回调函数和onRejected回调函数来解决这个问题:
this.$router.push({
    name: 'search',
    params: { keyword: this.keyword || undefined },
    query: { kw: this.keyword.toUpperCase() },
}, () => {}, () => {});
如果想彻底避免这个问题,需要重写VueRouter.prototype.push()方法:
// @/router/index.js
// 保存原生push方法
let originPush = VueRouter.prototype.push;
// 重写push方法
VueRouter.prototype.push = function(location, onFulfilled, onRejected) {
    if (onFulfilled && onRejected) {
        originPush.call(this, location, onFulfilled, onRejected);
    }
    else {
        originPush.call(this, location, () => {}, () => {});
    }
}
axios二次封装
安装axios:
npm i --save axios
nprogress的使用
安装nprogress:
npm i --save nporogress
封装
将axios进行二次封装,并且发出请求时会有进度条显示:
// @/api/request.js
// 二次封装axios
import axios from 'axios';
import nprogress from 'nprogress';  // 引入进度条
import 'nprogress/nprogress.css';
// 1. 利用axios对象的create方法, 创建一个axios实例
const requests = axios.create({
    baseURL: '/api',  // 基础路径, 发送请求时, 会默认在最前面带上/api
    timeout: 5000,  // 当请求超过5未被响应时则失败
});
// 2. 请求拦截器: 在发送请求之前, 请求拦截器可以检测到, 可以在请求发送前执行callback
requests.interceptors.request.use(config => {
    // config: 配置对象, 其中的headers属性为响应头
    nprogress.start();  // 进度条开始动
    return config;
});
// 3. 响应拦截器
requests.interceptors.response.use(res => {
    // 响应成功的callback, 可以得到服务器响应数据
    nprogress.done();  // 进度条结束
    return res.data;
}, err => {
    // 响应失败的callback
    return Promise.reject(new Error('Fail'));
});
export default requests;
api统一管理
// @/api/index.js
// 统一管理api
import requests from "./request";
// 三级联动api
export const reqCategoryList = () => requests({
    url: '/product/getBaseCategoryList',
    mathod: 'get'
});
注意跨域问题,首先配置代理:
// vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  // ...
  devServer: {
    proxy: {
      '/api': {
        target: 'http://gmall-h5-api.atguigu.cn'  // 请求目标
      }
    }
  }
})
在@/main.js进行请求测试,请求成功:
// @/main.js
import { reqCategoryList } from './api';
console.log(reqCategoryList());  // 返回一个Promise对象
Vuex模块式开发
Vuex是官方的状态管理库,集中式管理项目中组件共用的数据。如果项目较大,组件很多,则需要使用Vuex来维护数据。首先安装Vuex:
npm i --save vuex@3
如果使用单一状态数,当数据过多时可能会导致store对象变得过于臃肿,于是可以将store分割成若干个模块:
// @/store/home/index.js
const state = {};  // 数据存储
const mutations = {};  // 修改state的唯一手段
const actions = {};  // 可以写业务逻辑, 例如异步
const getters = {};  // 类似计算属性
export default {
    state, 
    mutations,
    actions,
    getters,
};
// @/store/index.js
import Vue from "vue";
import Vuex from 'vuex';
Vue.use(Vuex);  // 使用插件
// 引入小仓库
import home from '@/store/home';
import search from '@/store/search';
// 对外暴露一个Store类实例
export default new Vuex.Store({
    modules: {
        home,
        search
    }
});
注册插件:
// @/main.js
// ...
import store from '@/store/home';  // 引入仓库
new Vue({
  render: h => h(App),
  router,  // 注册路由
  store,  // 注册仓库 
}).$mount('#app')
Home组件
步骤:
- 静态页面完成
- 拆分出静态组件
- 获取服务器的数据进行展示
- 动态业务
全局组件
如果有一个组件被很多组件使用,可以考虑将其注册为全局组件,免去多次引入组件的麻烦。
// @/main.js
import TypeNav from '@/components/TypeNav';
Vue.component(TypeNav.name, TypeNav);
三级联动组件:
- 挂载完毕之后向服务器发送请求
- 请求到的数据保存在Vuex实例中
- 在@/App.vue中派发action,这样可以减少ajax请求
// @/App.vue
mounted() {
    this.$store.dispatch("categoryList");
}
路由
路由跳转时,我们希望:
- 尽量减少@click绑定
- 能够传递参数
解决方法是在<a>标签上写自定义数据,需要注意:
- 需要以data-,这样才能使用DOM节点的dataset属性获取到自定义属性
- data-category-name在- dataset属性会被自动转化为- categoryName
<div
  class="all-sort-list2"
  @mouseleave="changeIndex(-1)"
  @click="goSearch"
>
  <!-- 一级分类 -->
  <a
    href="javascript:;"
    :data-category-name="c1.categoryName"
    :data-category1-id="c1.categoryId"
    >{{ c1.categoryName }}</a
  >
  <!-- 二级分类 -->
  <!-- 三级分类 -->
</div>
于是,可以通过event.target是否有自定义属性来过滤标签:
goSearch(e) {
    let { categoryName, category1Id, category2Id, category3Id } = e.target.dataset;
    // 如果点击到了<a>标签, 路由跳转到search
    if (categoryName) {
        let location = { name: "search" };
        // 设置query参数
        let query = { categoryName };
        if (category1Id) {
            query.category1Id = category1Id;
        } else if (category2Id) {
            query.category2Id = category2Id;
        } else {
            query.category3Id = category3Id;
        }
        location.query = query;
        
        // 参数合并
        if (this.$route.params) {
          location.params = this.$route.params;
        }
        
        this.$router.push(location);
    }
}
这样,当点击<a>标签,跳转的URL就变成了:
http://localhost:8080/#/search?categoryName=手机&category1Id=2
注意:
- 对象的结构赋值需要变量与属性同名,才能取到正确的值
- 参数合并是为了顾及到URL原来带有的参数,例如通过搜索按钮跳转到当前界面,就会有params参数
mock
Mock.js官网,生成随机数据,拦截Ajax请求
安装Mock.js:
npm i mockjs
使用步骤:
- 
创建 @/mock文件夹
- 
准备相应的JSON数据: @/mock/banner.json[ { "id": "1", "imgUrl": "/images/banner1.jpg" }, { "id": "2", "imgUrl": "/images/banner2.jpg" }, { "id": "3", "imgUrl": "/images/banner3.jpg" }, { "id": "4", "imgUrl": "/images/banner4.jpg" } ]
- 
将JSON数据需要的图片放在 public文件夹下(public文件夹在打包的时候,会把资源原封不动打包到dist文件夹中)
- 
创建 mockServe.js,通过Mock.js插件实现模拟数据// @/mock/mockServe.js // 引入Mock.js模块 import Mock from 'mockjs'; // 引入JSON数据 (webpack默认对外暴露:图片、JSON数据格式) import banner from './banner.json'; import floor from './floor.json'; // 第一个参数:请求地址, 第二个参数:请求数据 Mock.mock('/mock/banner', { code: 200, data: banner }); Mock.mock('/mock/floor', { code: 200, data: floor });
- 
在入口文件引入(在入口文件执行一次) // @/main.js // 引入Mock import '@/mock/mockServe';
swiper
轮播图包安装:
npm i swiper@5
使用步骤:
- 
引入CSS/JS // @/main.js import 'swiper/css/swiper.css'; // 引入轮播图样式// ListContainer.vue import Swiper from 'swiper'; // 引入轮播图
- 
在页面结构中添加对应 <div class="swiper-container" ref="mySwiper"> <div class="swiper-wrapper"> <!-- 服务器的数据成功返回之后才有该结构 --> <div class="swiper-slide" v-for="carousel in bannerList" :key="carousel.id" > <img :src="carousel.imgUrl" /> </div> </div> <!-- 如果需要分页器 --> <div class="swiper-pagination"></div> <!-- 如果需要导航按钮 --> <div class="swiper-button-prev"></div> <div class="swiper-button-next"></div> </div>
- 
new Swiper()实例mounted() { this.$store.dispatch("bannerList"); // 不能在此初始化轮播图, 因为此时刚刚向服务器发送异步请求, DOM结构还不完整 }, watch: { // 监听bannerList数组变化, 当接收到响应结果时, 数组大小会变成非0 bannerList: { handler(newValue, oldValue) { // 在下一次DOM更新结束后的回调 this.$nextTick(() => { // 尽量不在Vue中直接操作DOM, 如果必须, 则使用$refs new Swiper(this.$refs.mySwiper, { loop: true, // 分页 pagination: { el: ".swiper-pagination", clickable: true, // 点击小球可以切换 }, // 前进后退 navigation: { nextEl: ".swiper-button-next", prevEl: ".swiper-button-prev", }, }); }); }, }, },
由于轮播图在多处使用,所以考虑将其注册为全局组件:
<template>
  <div class="swiper-container" ref="mySwiper">
    <div class="swiper-wrapper">
      <div class="swiper-slide" v-for="carousel in list" :key="carousel.id">
        <img :src="carousel.imgUrl" />
      </div>
    </div>
    <!-- 如果需要分页器 -->
    <div class="swiper-pagination"></div>
    <!-- 如果需要导航按钮 -->
    <div class="swiper-button-prev"></div>
    <div class="swiper-button-next"></div>
  </div>
</template>
<script>
import Swiper from "swiper"; // 轮播图
export default {
  name: "Carousel",
  props: ["list"],
  watch: {
    list: {
      immediate: true,
      handler(newValue, oldValue) {
        // 在下一次DOM更新结束后的回调
        this.$nextTick(() => {
          new Swiper(this.$refs.mySwiper, {
            loop: true,
            // 分页
            pagination: {
              el: ".swiper-pagination",
              clickable: true, // 点击小球可以切换
            },
            // 前进后退
            navigation: {
              nextEl: ".swiper-button-next",
              prevEl: ".swiper-button-prev",
            },
          });
        });
      },
    },
  },
};
</script>
<style scoped>
</style>
在入口文件中注册:
// @/main.js
// 引入全局组件
import Carousel from '@/components/Carousel';
Vue.component(Carousel.name, Carousel);
 
                     
                    
                 
                    
                
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号