尚品汇项目 - 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
    • 函数写法,将paramsquery打包成对象传给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组件

步骤:

  1. 静态页面完成
  2. 拆分出静态组件
  3. 获取服务器的数据进行展示
  4. 动态业务

全局组件

如果有一个组件被很多组件使用,可以考虑将其注册为全局组件,免去多次引入组件的麻烦。

// @/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-namedataset属性会被自动转化为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

使用步骤:

  1. 创建@/mock文件夹

  2. 准备相应的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"
        }
    ]
    
  3. 将JSON数据需要的图片放在public文件夹下(public文件夹在打包的时候,会把资源原封不动打包到dist文件夹中)

  4. 创建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 });
    
  5. 在入口文件引入(在入口文件执行一次)

    // @/main.js
    // 引入Mock
    import '@/mock/mockServe';
    

swiper

轮播图包安装:

npm i swiper@5

使用步骤:

  1. 引入CSS/JS

    // @/main.js
    import 'swiper/css/swiper.css';  // 引入轮播图样式
    
    // ListContainer.vue
    import Swiper from 'swiper';  // 引入轮播图
    
  2. 在页面结构中添加对应

    <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>
    
  3. 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);
posted @ 2022-05-02 21:20  lv6laserlotus  阅读(85)  评论(0)    收藏  举报