尚品汇
|字数总计:6.7k|阅读时长:29分钟|阅读量:
尚品汇项目
学习时间:2022年7月14日
学习来源:尚硅谷
1 简介
技术架构
- 前端
- vue,webpack,vuex,vue-router,axios,less
后端
- vue,webpack,vuex,vue-router,axios,scss,elementUI
数据可视化
- echarts数据可视化开源库
- canvas画布
- svg矢量图
2 初始化项目
使用vue-cli
脚手架进行项目的初始化,选择vue2版本:
2.1 文件目录分析
2.2 项目配置
修改package.json
:
1 2 3 4 5
| "scripts": { "serve": "vue-cli-service serve --open --host=localhost", "build": "vue-cli-service build", "lint": "vue-cli-service lint" }
|
在根目录下的vue.config.js
进行配置:
1 2 3 4
| module.exports = { lintOnSave: false }
|
创建jsconfig.json
,用@/
代替src/
,exclude
表示不可以使用该别名的文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| { "compilerOptions": { "baseUrl": "./", "paths": { "@/*": [ "src/*" ] } }, "exclude": [ "node_modules", "dist" ] }
|
2.3 路由配置
2.3.1 分析
前端所谓路由,本质就是KV键值对。
- key:URL(地址栏中的路径)
- value:相应的路由组件
简要分析
- 路由组件:Home首页路由组件,Search搜索路由组件,Login登录路由组件,Register注册路由组件
- 非路由组件:Header,Footer(在首页和搜索有,但在登录和注册页中没有)
2.3.2 非路由组件
开发步骤:
- 书写静态页面(HTML和CSS,但不是本课程重点)
- 拆分组件
- 获取服务器的数据动态展示
- 完成相应的动态业务逻辑
- 注意:创建组件的时候为
组件结构 + 组件的样式 + 图片资源
使用组件的步骤
- 创建或者定义组件
- 引入组件:
App.vue
中引入
- 注册组件:
App.vue
中注册
- 使用组件
项目结构
部分代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <template> <div> <!-- 使用组件 --> <Header></Header> <Footer></Footer> </div> </template>
<script> // 引入组件 import Header from './components/Header' import Footer from './components/Footer' export default { components: { Header, Footer, } } </script>
|
效果
2.3.3 路由组件
- 在开发中,通常在
src/components
文件夹下放置非路由组件(共用的全局组件),在src/pages|views
文件夹下放置路由组件。
配置路由:一般放置在src/router
文件夹中
路由组件和非路由组件的区别
路由的跳转
- 声明式导航:
router-link
- 编程式导航:
push|replace
- 区别:后者既可以完成前者的功能,还能实现其他的业务逻辑,例如登录时,除了要进行路由跳转,还需要进行其他的一些业务逻辑
部分代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| import Vue from 'vue'; import VueRouter from 'vue-router';
Vue.use(VueRouter);
import Home from '@/pages/Home' import Login from '@/pages/Login' import Search from '@/pages/Search' import Register from '@/pages/Register'
export default new VueRouter({ routes: [ { path: '/home', component: Home }, { path: '/login', component: Login }, { path: '/search', component: Search }, { path: '/register', component: Register }, { path: '/', redirect: '/home' } ] })
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import Vue from 'vue' import App from './App.vue'
import router from '@/router'
Vue.config.productionTip = false
new Vue({ render: h => h(App), router }).$mount('#app')
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <template> <div> <!-- 使用组件 --> <Header></Header> <!-- 路由组件出口的地方:路由组件展示的地方 --> <router-view></router-view> <Footer></Footer> </div> </template>
<script> // 引入组件 import Header from "./components/Header"; import Footer from "./components/Footer"; export default { components: { Header, Footer, }, }; </script>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| <template> <!--其他代码...-->
<!-- <a href="###">登录</a> --> <!-- <a href="###" class="register">免费注册</a> --> <router-link to="/login">登录</router-link> <router-link class="register" to="/register">免费注册</router-link>
<!--其他代码...-->
<button class="sui-btn btn-xlarge btn-danger" type="button" @click="goSearch"> 搜索 </button>
<!--其他代码...--> </template>
<script> export default { methods: { // 搜索按钮的回调,向search路由跳转 goSearch() { // 其他的业务逻辑... // ... // 路由跳转 this.$router.push('/search'); } } }; </script>
|
2.3.4 路由元信息
文档:https://router.vuejs.org/zh/guide/advanced/meta.html
有时,你可能希望将任意信息附加到路由上,如过渡名称、谁可以访问路由等。这些事情可以通过接收属性对象的meta
属性来实现,并且它可以在路由地址和导航守卫上都被访问到。
需求:Footer组件:在Home、Search显示,而在Register和Login中不显示。
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| export default new VueRouter({ routes: [ { path: '/home', component: Home, meta: {show: true} }, { path: '/login', component: Login, meta: {show: false} }, { path: '/search', component: Search, meta: {show: true} }, { path: '/register', component: Register, meta: {show: false} } ] })
|
1 2
| <!-- 在Home、Search显示,在Register和Login隐藏 --> <Footer v-show="$route.meta.show"></Footer>
|
2.3.5 路由传递参数
- 路由跳转有几种方式?
- 路由传参的参数写法?
- 例如
/home/1?name=zs&age=24
params
参数:即路径参数,属于路径中的一部分(/1
),注意在配置路由的时候,需要占位
query
参数:即查询参数,不属于路径中的一部分(?
后面的参数),不需要占位
- 二者都挂载在参数对象
$route
上
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| export default new VueRouter({ routes: [ { name: 'search', path: '/search/:keyword', component: Search, meta: {show: true} } ] })
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| <template> <header class="header"> <!--头部第二行 搜索区域--> <div class="bottom"> <h1 class="logoArea"> <router-link class="logo" to="/home"> <img src="./images/logo.png" alt="" /> </router-link> </h1> <div class="searchArea"> <form action="###" class="searchForm"> <input type="text" id="autocomplete" class="input-error input-xxlarge" v-model="keyword" /> <button class="sui-btn btn-xlarge btn-danger" type="button" @click="goSearch" > 搜索 </button> </form> </div> </div> </header> </template>
<script> export default { data() { return { keyword: "", }; }, methods: { // 搜索按钮的回调,向search路由跳转 goSearch() { // 例如向 /search/abc?k=ABC 跳转 // 方式1:纯字符串 // this.$router.push("/search/" + this.keyword + "?k=" + this.keyword.toUpperCase());
// 方式2:模板字符串 // this.$router.push(`/search/${this.keyword}?k=${this.keyword.toUpperCase()}`);
// 方式3:对象形式 常用 this.$router.push({ // 注意:这里不是path属性,而是name属性:要跳转的路由的名称 // path: "/search" 错误的写法 name: "search", // kv键值对 params: { keyword: this.keyword, }, // kv键值对 query: { k: this.keyword.toUpperCase(), }, }); }, }, }; </script>
|
1 2 3 4 5 6
| <template> <div> <h1>params参数---{{ $route.params.keyword }}</h1> <h1>query参数---{{ $route.query.k }}</h1> </div> </template>
|
相关问题
例如从/home
跳转到/search?k=v
,不携带路径参数params
1 2 3 4 5 6 7
| { name: 'search', path: '/search/:keyword?', component: Search, meta: {show: true} }
|
1 2 3 4 5 6 7 8
| this.push({ name: "search", params: "", query: { k: this.keyword.toUpperCase() } });
|
问题:跳转到/search/?k=ABC
解决:使用undefined
1 2 3 4 5 6 7 8
| this.push({ name: "search", params: "" || undefined, query: { k: this.keyword.toUpperCase() } });
|
能,详见vue2学习笔记
常用函数写法:可以将params和query参数通过props传递给路由组件
1 2 3 4 5 6 7 8 9 10 11 12
| { name: 'search', path: '/search/:keyword?', component: Search, meta: { show: true }, props:($route) => { return { keyword: $route.params.keyword, k: $route.query.k } }, }
|
Search组件:
1 2 3 4 5 6 7 8 9 10 11 12
| <template> <div> <h1>params参数---{{ $route.params.keyword }}---{{ keyword }}</h1> <h1>query参数---{{ $route.query.k }}---{{ k }}</h1> </div> </template>
<script> export default { props: ["keyword", "k"] } </script>
|
2.3.6 重写push和replace方法
问题:编程式路由跳转到当前路由(参数不变),多次执行会抛出NavigationDuplicated
的警告错误。该问题在vue3中已经得到了解决。
此外,各版本的vue对于声明式导航没有这类问题。
原因:vue-router
引入了promise
解决:通过给push方法传递相应的成功和失败的回调函数,可以捕获到当前的错误。
2.4 Home模块组件拆分
分析:略
2.4.1 三级联动组件
三级联动组件:在Home、Search和Detail组件中出现,因此拆分并注册为为全局组件。好处:只需要注册一次,就可以在项目中的任意地方使用。
1 2 3 4 5 6 7 8 9
|
import TypeNav from '@/pages/Home/TypeNav'
Vue.component(TypeNav.name, TypeNav)
|
1 2 3 4 5 6
| <script> export default { // 给组件起一个名字 name: "TypeNav", }; </script>
|
1 2 3 4 5 6
| <template> <div> <!-- 三级联动组件 --> <TypeNav></TypeNav> </div> </template>
|
2.4.2 其余静态组件
拆分结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| <template> <div> <!-- 三级联动组件 --> <TypeNav></TypeNav> <ListContainer></ListContainer> <Recommend></Recommend> <Rank></Rank> <Like></Like> <Floor></Floor> <Brand></Brand> </div> </template>
<script> // 引入其余的组件 import ListContainer from "@/pages/Home/ListContainer"; import Recommend from "@/pages/Home/Recommend"; import Rank from "@/pages/Home/Rank"; import Like from "@/pages/Home/Like"; import Floor from "@/pages/Home/Floor" import Brand from "@/pages/Home/Brand" export default { components: { ListContainer, Recommend, Rank, Like, Floor, Brand, }, }; </script>
|
其余代码略。
效果:
3 项目开发
3.1 接口相关
3.1.1 Poatman接口测试
3.1.2 axios二次封装
- 为什么需要进行二次封装axios?
- 请求拦截器:可以在发送请求之前处理一些业务
- 响应拦截器:当服务器返回数据后,可以处理一些业务
安装
一般在项目中会有一个文件夹/src/api
,里面存放封装好的axios
代码示例
新建/api/request.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| import axios from "axios";
const api = axios.create({ baseURL: "/api", timeout: 5000 });
api.interceptors.request.use((config) => { return config; });
api.interceptors.response.use((res) => { return res.data; }, (error) => { console.log(error); return Promise.reject(new Error("fail")); });
export default api;
|
3.1.3 API接口统一管理
代码示例
新建/api/index.js
1 2 3 4 5 6 7 8 9 10
|
import api from "./request";
export const reqCategoryList = () => api({url: "/product/getBaseCategoryList",method: "GET"});
|
跨域解决
通常有三种方案:JSONP,CROS和代理,本项目使用代理。
在vue.config.js
中:
1 2 3 4 5 6 7 8 9 10
| module.exports = { devServer: { proxy: { "/api": { target: "http://gmall-h5-api.atguigu.cn", }, }, } }
|
测试
入口文件:
1 2 3
| import {reqCategoryList} from "@/api" reqCategoryList();
|
测试结果:
3.2 nprogress进度条
安装:
1
| npm i nprogress@0.2.0 -s
|
在拦截器request.js
中设置:(部分代码)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import nprogress from "nprogress";
import "nprogress/nprogress.css"
api.interceptors.request.use((config) => { nprogress.start(); return config; });
api.interceptors.response.use((res) => { nprogress.done(); return res.data; }, (error) => { console.log(error); return Promise.reject(new Error("fail")); });
|
3.3 vuex模块式开发
vuex官网
vuex是官方提供的一个插件,状态管理库:集中式管理项目中组件共用的数据。
vuex模块式开发:不将所有组件的共享数据放置在总的index.js中,而是分模块放置数据。
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割。
结构
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
const state = {};
const mutations = {};
const actions = {};
const getters = {};
export default { state, mutations, actions, getters }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import Vue from "vue"; import Vuex from "vuex";
Vue.use(Vuex);
import home from './home' import search from "./search";
export default new Vuex.Store({ modules: { home, search } });
|
1 2 3 4 5 6 7 8 9 10
| import store from '@/store'
new Vue({ render: h => h(App), store }).$mount('#app')
|
3.4 三级联动导航开发
3.4.1 动态展示数据
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| import { reqCategoryList } from "@/api";
const state = { categoryList: [] };
const mutations = { CATEGORYLIST(state, categoryList) { state.categoryList = categoryList; } };
const actions = { async categoryList({ commit }) { let res = await reqCategoryList(); console.log(res); if (res.code === 200) { commit("CATEGORYLIST", res.data); } } };
|
其中,后端传过来的数据res的形式为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| <template> <!-- 商品分类导航 --> <div class="type-nav"> <div class="container"> <div class="sort"> <div class="all-sort-list2"> <div class="item" v-for="(c1, index) in categoryList" :key="c1.categoryId" > <h3> <a href="">{{ c1.categoryName }}</a> </h3> <div class="item-list clearfix"> <div class="subitem" v-for="(c2, index) in c1.categoryChild" :key="c2.categoryId" > <dl class="fore"> <dt> <a href="">{{ c2.categoryName }}</a> </dt> <dd> <em v-for="(c3, index) in c2.categoryChild" :key="c3.categoryId" > <a href="">{{ c3.categoryName }}</a> </em> </dd> </dl> </div> </div> </div> </div> </div> </div> </div> </template>
<script> import { mapState } from "vuex"; export default { name: "TypeNav", // 组件挂载完毕时,向服务器发送请求获取数据 mounted() { // 触发名为categoryList的action this.$store.dispatch("categoryList"); }, computed: { // mapState可以传入数据或对象 ...mapState({ // 此处传入的是一个对象 // 右侧需要的是一个函数,当使用这个计算属性时,右侧的函数会立即执行一次 // 注入一个参数state,即为大仓库的数据 categoryList: (state) => state.home.categoryList, }), }, }; </script>
|
补充:mapState可以传入数据或对象
1 2 3 4 5 6 7 8 9
|
computed: { mapState({ sCounter: state => state.name, }) }
|
3.4.2 动态背景颜色
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| <template> <!-- 商品分类导航 --> <div class="type-nav"> <div class="container"> <div @mouseleave="leaveIndex"> <h2 class="all">全部商品分类</h2> <!-- 一级分类 --> <div class="sort"> <div class="all-sort-list2"> <div class="item" v-for="(c1, index) in categoryList" :key="c1.categoryId" :class="{ cur: currentIndex == index }" > <h3 @mouseenter="changeIndex(index)"> <a href="">{{ c1.categoryName }} --- {{ index }}</a> </h3> <!-- 二三级分类 --> <div class="item-list clearfix"> <div class="subitem" v-for="c2 in c1.categoryChild" :key="c2.categoryId" > <dl class="fore"> <dt> <a href="">{{ c2.categoryName }}</a> </dt> <dd> <em v-for="c3 in c2.categoryChild" :key="c3.categoryId"> <a href="">{{ c3.categoryName }}</a> </em> </dd> </dl> </div> </div> </div> </div> </div> </div> <nav class="nav"> <a href="###">服装城</a> <a href="###">美妆馆</a> <a href="###">尚品汇超市</a> <a href="###">全球购</a> <a href="###">闪购</a> <a href="###">团购</a> <a href="###">有趣</a> <a href="###">秒杀</a> </nav> </div> </div> </template>
<script> import { mapState } from "vuex"; export default { name: "TypeNav", data() { return { // 存储用户鼠标移动到哪一个一级分类 currentIndex: -1, }; }, methods: { // 鼠标进入时,修改currentIndex changeIndex(index) { this.currentIndex = index; }, // 鼠标移除时的回调 leaveIndex() { this.currentIndex = -1; }, }, }; </script>
<style lang="less" scoped> .cur { background: skyblue; } </style>
|
3.4.3 子级分类的显示和隐藏
最开始的时候是采用css来控制display: bolck|none
。下面利用js来实现:
代码示例
1 2 3 4
| <div class="item-list clearfix" :style="{ display: currentIndex == index ? 'block' : 'none' }">
|
3.4.4 防抖与节流
① 概念
- 节流:在规定的间隔时间范围内不会重复触发回调,只有大于这个时间间隔才会触发回调,即把频繁触发变为少量触发
- 防抖:前面的所有的触发都被取消,最后一次执行在规定的时间之后才会触发,也就是说如果连续快速的触发,只会执行一次。
② 三级联动节流
利用lodash
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <script> // 按需加载lodash的节流功能throttle import throttle from "lodash/throttle"; export default { name: "TypeNav", methods: { // 鼠标进入时,修改currentIndex changeIndex: throttle(function (index) { // 正常情况(用户慢慢的操作):鼠标进入,每一个一级分类h3都会触发鼠标进入事件 // 非正常情况(用户操作很快):只有部分h3事件触发了 // 原因:浏览器反应不过来,如果当前回调中有大量业务,可能出现卡顿现象 this.currentIndex = index; }, 50), }, }; </script>
|
3.4.5 路由跳转
分析:用户可以点击的:一二三级分类,当点击的时候,会从Home模块跳转到Search模块,一级会把用户选中的产品(产品名称和id)在路由跳转的时候进行传递。
注意:如果使用声明式导航router-link
,可以实现路由的跳转与传递参数,但要注意会出现卡顿现象,原因在于它是一个组件,当服务器的数据返回后,循环出许多router-link
组件,非常消耗内存。
解决:最好的方式是编程式导航+事件委派(把全部的子节点【h3,dt,dl,em】的事件委派给父亲结点,这样整个结构只有一个事件的回调,防止页面卡顿)
代码思路
1 2
| <div class="all-sort-list2" @click="goSearch">
|
1 2 3 4 5 6
| goSearch() { this.$router.push("/search"); },
|
代码示例
如何确定点击的标签是否是a标签?
- 在a标签上加上自定义属性
data-categoryName
,其余子节点没有
如何传递路由参数(产品id和名称)?
- 产品名称:利用上面的自定义属性
data-categoryName
- 产品id:新增自定义属性
data-categoryid
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <a :data-categoryName="c1.categoryName" :data-category1Id="c1.categoryId" >{{ c1.categoryName }} </a>
<a :data-categoryName="c2.categoryName" :data-category2Id="c2.categoryId" >{{ c2.categoryName }} </a>
<a :data-categoryName="c3.categoryName" :data-category3Id="c3.categoryId" >{{ c3.categoryName }} </a>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| goSearch(e) {
let element = e.target; let { categoryname, category1id, category2id, category3id } = element.dataset; if (categoryname) { let location = { name: "search" }; let query = { categoryName: categoryname }; if (category1id) { query.category1id = category1id; } else if (category2id) { query.category2id = category2id; } else { query.category3id = category3id; } location.query = query; this.$router.push(location); } },
|
3.5 Search模块开发
3.5.1 商品分类菜单
在search模块中,三级联动导航默认是折叠的,鼠标放上后出现菜单,鼠标移除时折叠菜单,因此需要在TypeNav组件上新增data属性show
,默认为真。
首先在Search模块使用全局组件TypeNav:
1 2 3 4 5
| <template> <div> <TypeNav></TypeNav> </div> </template>
|
TypeNav组件部分代码:
1 2 3 4
| <div @mouseleave="leaveIndex" @mouseenter="enterShow">
<div class="sort" v-show="show">
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| <script> export default { name: "TypeNav", data() { return { show: true, }; }, methods: { // 鼠标移除时的回调 leaveIndex() { this.currentIndex = -1; if (this.$route.path != "/home") { this.show = false; } }, // 鼠标移入的时候,让商品分类列表进行展示 enterShow() { if (this.$route.path != "/home") { this.show = true; } }, }, // 组件挂载完毕时,向服务器发送请求获取数据 mounted() { // 当组件挂载完毕,让show属性变为false // 如果不是Home路由组件,则将TypeNav隐藏 if (this.$route.path != "/home") { this.show = false; } }, }; </script>
|
3.5.2 分类菜过渡动画
1 2 3 4
| <!-- 过渡动画 --> <transition name="sort"> <div class="sort" v-show="show"></div> </transition>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <style> // 过渡动画 // 进入 .sort-enter { height: 0px; } // 结束 .sort-enter-to { height: 461px; } // 时间和速率 .sort-enter-active { transition: all .5s linear; } </style>
|