尚品汇项目 - 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号