1、vue 文件目录分析
vue create 项目名称
目录分析public
文件夹:静态资源,webpack 进行打包的时候会原封不动打包到 dist 文件夹中。
pubilc/index.html
是一个模板文件,作用是生成项目的入口文件,webpack 打包的 js,css 也会自动注入到该页面中。我们浏览器访问项目的时候就会默认打开生成好的 index.html。
src
文件夹(程序员代码文件夹)
src/assets
: 存放公用的静态资源src/components
: 非路由组件(全局组件),其他组件放在 views 或者 pages 文件夹中src/App.vue
: 唯一的根组件src/main.js
: 程序入口文件,最先执行的文件
babel.config.js
: babel 配置文件,把 es6 翻译成 es5package.json
:看到项目描述、项目依赖、项目运行指令package-lock.json
: 缓存性文件(各种包的来源)README.md
: 项目说明文件
2 项目配置
1、项目运行,浏览器自动打开package.json
中 "serve": "vue-cli-service serve --open",
"scripts": {
"serve": "vue-cli-service serve --open",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
2、关闭 eslint 校验工具、开启自动刷新
(不关闭会有各种规范,不按照规范就会报错)
vue.config.js ,
进行配置
module.exports = {
//默认打开地址http://localhost:8080/
devServer: {
host: 'localhost',
port: 8080,
},
//关闭eslint
lintOnSave: false
}
3、src 文件夹配置别名,jsconfig.json
,用@/
代替src/
,exclude
表示不可以使用该别名的文件
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": [
"src/*"
]
}
},
"exclude": [
"node_modules",
"dist"
]
}
4、路由的配置
2 个非路由组件,四个路由组件
两个非路由组件:Header
【首页、搜索页】Footer
【在首页、搜索页】在登录页是没有的
路由组件:Home
、Search
、Login
(没有底部的 Footer 组件,带有二维码的)、Register
(没有底部的 Footer 组件,带二维码的)
开发一个前端模块可以概括为以下几个步骤:
(1)写静态页面、拆分为静态组件;
(2)发请求(API);
(3)vuex(actions、mutations、state 三连操作);
(4)组件获取仓库数据,动态展示;
5、组件页面样式
组件页面的样式使用的是 less 样式,浏览器不识别该样式,需要下载相关依赖 npm install --save less less-loader@5
如果想让组件识别 less 样式,则在组件中设置 <script scoped lang="less">
6、清除 vue 页面默认的样式
vue 是单页面开发,我们只需要修改 public 下的 index.html 文件 <link rel="stylesheet" href="reset.css">
7、写footer
组件和header
非路由组件
非路由组件使用分为几步:
第一步:定义footer/index.vue
第二步:引入import myHeader from "./components/Header"
第三步:注册components: { myHeader}
第四步: 使用<my-header></my-header>
8、安装 npm install --save vue-router@3
3 路由组件
新建pages
文件夹,并创建四个路由组件Home
、Search
、Login
、Register
3.1 创建 router 文件夹
创建router
文件夹,并创建index.js
进行路由配置,最终在main.js
中引入注册
1. router/index.js
//配置路由的地方
import Vue from "vue";
import VueRouter from "vue-router";
Vue.use(VueRouter);//使用插件
//引入路由文件
import Home from '@/pages/Home'
import Search from '@/pages/Search'
import Login from '@/pages/Login'
import Register from '@/pages/Register'
//配置路由
export default new VueRouter({
routes:[
{
path:"/home",
component:Home
},
{
path:"/search",
component:Search
},
{
path:"/login",
component:Login
},
{
path:"/register",
component:Register
},
]
})
2. main.js 引入注册
import Vue from 'vue'
import App from './App.vue'
//引入路由
import router from '@/router'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
//注册路由,底下的写法是kv一致,省略v【router小写的】
//注册路由信息:当这里写router的时候,组件身上都拥有$route,$router属性
router
}).$mount('#app')
3. App.vue
中写出口
<!-- 路由组件出口的地方 -->
<router-view></router-view>
3.2 总结
1. 路由组件和非路由组件区别:
- 非路由组件放在
components
中,路由组件放在pages
或views
中 - 非路由组件通过标签使用,路由组件通过路由使用
- 在 main.js 注册完路由,所有的路由和非路由组件身上都会拥有
$router、
$route
属性
$router
:一般进行编程式导航进行路由跳转【push | replace】$route
: 一般获取路由信息(name path params 等)
2. A->b 路由跳转方式
- 声明式导航
router-link
标签 , 可以把 router-link (务必有to属性) 理解为一个 a 标签,它也可以加 class 修饰 - 编程式导航 :声明式导航能做的编程式【$router.push | replace】都能做,而且还可以处理一些业务
编程式导航除了跳转业务外,还可以进行其他的业务逻辑
Header/index.vue 声明式导航
<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="goSeacrh">搜索</button>
methods:{
//搜索按钮的回调函数,需要向search路由进行跳转
goSeacrh(){
this.$router.push('/search')
}
}
3. 路由传参有几种方式
query、params
query
、params
两个属性可以传递参数1、query
参数:不属于路径当中的一部分,类似于 get 请求,地址栏表现为
/search?k1=v1&k2=v2
query
参数对应的路由信息 path: "/search"
2、params
参数:属于路径当中的一部分,需要注意,在配置路由的时候,需要占位 , 地址栏表现为 /search/v1/v2
params
参数对应的路由信息要修改为 path: “/search/:keyword
” 这里的/:keyword
就是一个 params 参数的占位符
有几种写法:
第一种 字符串,parms 参数和 query 参数
this.$router.push('/search/' +this.keyword +"?k=" +this.keyword.toUpperCase())
第二种:模板字符串
this.$router.push(`/search/${this.keyword}?k=${this.keyword.toUpperCase()}`)
第三种:对象(常用)
this.$router.push({name:"search",params:{keyword:this.keyword},query:{k:this.keyword.toUpperCase()}})
以对象方式传参时,如果我们传参中使用了 params,只能使用name
,不能使用 path,如果只是使用 query 传参,可以使用 path
{
// 占位
path:"/search/:keyword",
component:Search,
meta:{show:true},
name:"search"
},
面试题 1: 路由传递参数(对象写法),path 是否可以结合 params 参数一起使用?
答:路由跳转参数的时候,对象的写法可以是 name、path 形式,但需要注意的是 path 这种写法不能与 params 参数一起使用
this.$router.push({path:"/search",params:{keyword:this.keyword},query:{k:this.keyword.toUpperCase()}})
面试题 2: 如何指定 params 参数可传可不传
如果路由path要求传递params参数,但是没有传递,会发现地址栏URL有问题,详情如下:
Search路由项的path已经指定要传一个keyword的params参数,如下所示:
path: "/search/:keyword",
执行下面进行路由跳转的代码:
this.$router.push({name:"Search",query:{keyword:this.keyword}})
当前跳转代码没有传递params参数
地址栏信息:http://localhost:8080/#/?keyword=asd
此时的地址信息少了/search
正常的地址栏信息: http://localhost:8080/#/search?keyword=asd
解决方法:可以通过改变path来指定params参数可传可不传
path: "/search/:keyword?", ?表示该参数可传可不传
面试题 3: params 可传可不传,但是如果传递的时空串,如何解决
this.$router.push({name:"Search",query:{keyword:this.keyword},params:{keyword:''}})
出现的问题和2中的问题相同,地址信息少了/search
解决方法: 加入||undefined,当我们传递的参数为空串时地址栏url也可以保持正常
this.$router.push({name:"Search",query:{keyword:this.keyword},params:{keyword:''||undefined}})
面试题 4: 路由组件能不能传递 props 数据
可以,而且有三种写法
//布尔写法:params参数
props:true,
// 对象写法:给路由传递props
props:{a:1,b:2}
// 函数写法:可以将params参数,query参数,通过props传递给路由组件
props:($route)=>{
return {keyword:$route.params.keyword,k:$route.query.k};
}
接收 props 参数
路由组件可以传递props参数
props:['keyword','k']
4 footer 组件显示与隐藏
footer 在登录注册页面是不存在的,所以要隐藏,v-if
或者 v-show
这里使用v-show
,因为v-if
会频繁的操作 dom 元素消耗性能,v-show
只是通过样式将元素显示或隐藏即display:show|none
- 配置路由的时候,可以给路由配置元信息
meta,
- 在路由的原信息中定义 show 属性,用来给
v-show
赋值,判断是否显示 footer 组件
代码:
1. router/index.js 配置元信息meta,
{
path:"/search",
component:Search,
meta:{show:true}
},
{
path:"/login",
component:Login,
meta:{show:false}
},
2. v-show
判断是否显示 footer 组件
<!-- 在home和search中显示,在登录、注册隐藏 -->
<my-footer v-show="$route.meta.show"></my-footer>
多次执行相同的 push 问题
多次执行相同的 push 问题,控制台会出现警告
例如:使用 this.$router.push({name:‘Search’,params:{keyword:“…”||undefined}}) 时,如果多次执行相同的 push,控制台会出现警告。
编程式导航(push|replace)才会有这种情况的异常,声明式导航是没有这种问题,因为声明式导航内部已经解决这种问题
原因:push 是一个promise
,promise 需要传递成功和失败两个参数,我们的 push 中没有传递
方法:this.$router.push({name:‘Search’,params:{keyword:"…"||undefined}},()=>{},()=>{})
后面两项分别代表执行成功和失败的回调函数
这种写法治标不治本,将来在别的组件中 push|replace, 编程式导航还是会有类似错误
push 是 VueRouter.prototype 的一个方法,在 router 中的 index 重写该方法即可 (看不懂也没关系,这是前端面试题)
this
: 当前组件实例(search)this.$router
属性:当前这个属性,属性值 VueRouter 类的一个实例,当在入口文件注册路由的时候,给组件实例添加$router
、$route
属性
push:VueRouter 类的一个实例
//1、先把VueRouter原型对象的push,保存一份
let originPush = VueRouter.prototype.push;
//2、重写push|replace
//第一个参数:告诉原来的push,跳转的目标位置和传递了哪些参数
VueRouter.prototype.push = function (location,resolve,reject)`在这里插入代码片`{
if(resolve && reject){
originPush.call(this,location,resolve,reject)
}else{
originPush.call(this,location,() => {},() => {})
}
}
call 和 apply 的区别
相同点:都可以调用函数一次,都可以篡改函数的上下文一次
不同点:call 和 apply 传递参数:call
传递参数用逗号隔开,apply
方法执行,传递数组
定义全局组件
我们的三级联动组件是全局组件,全局的配置都需要在 main.js 中配置
//将三级联动组件注册为全局组件
import TypeNav from '@/pages/Home/TypeNav';
//第一个参数:全局组件名字,第二个参数:全局组件
Vue.component(TypeNav.name,TypeNav);
在 Home 组件中使用该全局组件
5 封装 axios
发请求的方式:XMLHttpRequest、$、fetch、axios
AJAX: 客户端可以’敲敲的’向服务器端发请求,在页面没有刷新的情况下,实现页面的局部更新。
为什么要二次封装 axios
请求拦截器、响应拦截器。
请求拦截器:可以在发请求之前可以处理一些业务
响应拦截器:当服务器返回数据以后,可以处理一些事情
axios 文档:使用说明 · Axios 中文说明 · 看云
安装:npm install --save axios
工作的时候src
目录下的API
文件夹,一般关于axios
二次封装的文件baseURL:'/api'
是配置基础路径,发请求中路径中会出现api
-
在
src
目录下创建api
文件夹,创建request.js
文件 -
request.js
文件代码
// 对axios二次封装
import axios from "axios";
// 1. 利用axios对象的方法create,去创建一个axios实例
// 2. request就是axios,只是稍微配置了一下
const requests = axios.create({
// 基础路径,requests发出的请求在端口号后面会跟改baseURl
baseURL:'/api',
timeout: 5000,//超时5秒,5秒没有响应就失败了
})
// 请求拦截器:在请求之前,请求拦截器可以检测到,可以在请求之前做一些事情
requests.interceptors.request.use((config)=>{
// config:配置对象,对象里面有一个属性很重要,headers请求头
return config;
})
// 响应拦截器
requests.interceptors.response.use((res)=>{
// 成功的回调函数:服务器响应数据回来以后,响应拦截器可以检测到,可以做一些事情
return res.data;
},(error)=>{
// 失败的回调函数
console.log("响应失败"+error)
return Promise.reject(new Error('fail'))
})
// 对外暴露
export default requests;
6 接口统一管理
-
在文件夹
api
中创建index.js
文件,用于封装所有请求
将每个请求封装为一个函数,并暴露出去,组件只需要调用相应函数即可,这样当我们的接口比较多时,如果需要修改只需要修改该文件
即可。 -
index.js
代码如下:
// 当前模块:API统一管理
import requests from "./request";
// 三级联动接口文档
// /api/product/getBaseCategoryList get 无参数
// 发请求:axios发请求返回结果Promise对象
export const reqCategoryList = ()=>requests({
url:'/product/getBaseCategoryList',
method:'get'
})
6.1 解决跨域问题
方法:jsonp
、cros
、proxy代理
- 在根目录下的
vue.config.js
中devServer
中配置,proxy
为通过代理解决跨域问题。 vue.config.js
代码如下:
module.exports = {
// 默认打开地址http://localhost:8080/
devServer: {
host: "localhost",
port: 8080,
proxy: {
// 配置代理
// 1、会把请求路径中的/api换为后面的代理服务器
"/api": {
// 2、 提供数据的服务器地址
// target: 'http://39.98.123.211',
target: "http://gmall-h5-api.atguigu.cn",
},
},
},
//关闭eslint
lintOnSave: false
}
7 nprogress 进度条插件
打开一个页面时,往往会伴随一些请求,并且会在页面上方出现进度条。它的原理时,在我们发起请求的时候开启进度条,在请求成功后关闭进度条,所以只需要在request.js
中进行配置。
如下图所示,我们页面加载时发起了一个请求,此时页面上方出现蓝色进度条
- 安装:
npm install --save nprogress
- 引入
:import nprogress from 'nprogress';
- 方法
start
:进度条开始,done
:进度条结束 request.js
中增加代码:
// 引入进度条
import nprogress from 'nprogress';
// 引入进度条样式
import "nprogress/nprogress.css"
// 请求拦截器:在请求之前,请求拦截器可以检测到,可以在请求之前做一些事情
requests.interceptors.request.use((config)=>{
// 1. 进度条开始
nprogress.start()
return config;
})
// 响应拦截器
requests.interceptors.response.use((res)=>{
// 2.进度条结束
nprogress.done()
//成功的回调函数
console.log('响应成功',res.data)
return res.data;
},(error)=>{
//失败的回调函数
console.log("响应失败"+error)
return Promise.reject(new Error('fail'))
})
- 可以通过修改 nprogress.css 文件的 background 来修改进度条颜色。
8 vuex
vuex:
Vue 官方提供的一个插件,插件可以管理项目共用数据
书写任何项目都需要vuex
?
项目大的时候,组件多,模块多,需要有一个地方 '统一管理数据’ 即为仓库 store,可以集中式管理数据。
- 安装:
npm install --save vuex@3
- 在 src 下新建
store
文件夹,新建index.js
文件 index.js
,内容如下:
import Vue from "vue";
import Vuex from "vuex";
// 使用Vuex
Vue.use(Vuex);
// state:是仓库,存储数据的地方
const state = {};
// mutations:修改state的唯一手段
const mutations = {};
// action:处理action,可以书写自己的业务逻辑,也可以处理异步
const actions = {};
// getters:理解成计算属性,用于简化仓库数据,让组件获取仓库数据更加方便
const getters = {};
// 对外暴露Store类的一个实例
export default new Vuex.Store({
state,
mutations,
actions,
getters
})
4、main.js
中引入
但凡是在 main.js 中的 Vue 实例中注册的实体,在所有的组件中都会有(this.$. 实体名)属性
import store from './store'
new Vue({
render: h => h(App),
// 注册路由,此时组件中都会拥有$router $route属性
router,
// 注册store,此时组件中都会拥有$store
store
}).$mount('#app')
Vuex 基本使用
在其他组件中使用 store 中的数据
1、例如在 Home/index.vue 中
<button @click="add">点我加1</button>
<span>仓库的数据{
{count}}</span>
<button>点我减1</button>
import {mapState} from 'vuex'
export default{
computed:{
...mapState(['count'])
},
methods:{
add(){
// 派发action
this.$store.dispatch('add');
}
}
}
2、store/index.js
中:
const state ={
count:1
};
// mutations:修改state的唯一手段
const mutations ={
// 2.
ADD(state){
state.count++;
}
};
// action:处理action,可以书写自己的业务逻辑,也可以处理异步
const actions={
// 1. 这里可以书写业务逻辑,但是不能修改state
add({commit}){
commit('ADD')
}
};
const getters={};
// 对外暴露Store类的一个实例
export default new Vuex.Store({
state,
mutations,
actions,
getters
})
Vuex 的模块化
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store
对象就有可能变得相当臃肿。
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)
。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:
1、在store
中分别建立home/index.js
和search/index.js
2、search/index.js
代码如下:
// search的小仓库
const state ={};
const mutations ={};
const actions={};
const getters={};
export default{
state,
mutations,
actions,
getters
}
3、在store/index.js
中引入两个仓库,并注册
// 引入小仓库
import home from './home'
import search from "./search";
// 对外暴露Store类的一个实例
export default new Vuex.Store({
// 实现Vuex仓库模块化开发存储数据
modules:{
home,
search
}
})
全局组件放在 components 下,在 mian.js 中引入
4、使用 home 仓库中的数据
TypeNav
获取三级联动的数据
export default {
name: 'TypeNav',
// 组件挂载完毕,可以向服务器发请求
mounted(){
// 通知Vuex发请求,获取数据,存储在仓库中
this.$store.dispatch('categoryList')
console.log(state)
},
computed: {
// 右侧需要的是一个函数,当使用这个计算属性的时候,右侧函数会立马执行一次
// 注入一个参数state,其实即为大仓库中的数据
...mapState({categoryList: state => state.home.categoryList}),
},
}
home/index.js
代码
// 引入三级联动发请求的
import{reqCategoryList} from '@/api'
// home的小仓库
const state ={
// 服务器返回的是对象,初始化就是对象,服务器返回是数组,初始化就是数组
categoryList:[]
};
const mutations ={
GATEGORYLIST(state,categoryList){
state.categoryList= categoryList
}
};
const actions={
// 通过API里面的接口函数调用,向服务器发请求,获取服务器数据
async categoryList({commit}){
let result =await reqCategoryList();// reqCategoryList()返回的是promise
if(result.code==200){
commit("GATEGORYLIST",result.data);
}
}
};
9 给一级菜单加颜色
鼠标放上去是浅蓝色
第一种方案,采用样式
.item:hover{
background:skyblue;
}
第二种方案,使用 js
<h3 @mouseenter="changIndex(index)" >
<a href="">{
{c1.categoryName}}-{
{index}}</a>
</h3>
data(){
return {
currentIndex:-1,
}
},
methods:{
// 鼠标进入,修改currentIndex
changIndex(index){ //index 就是鼠标移上一级菜单的索引
this.currentIndex=index
},
// 鼠标移出,修改currentIndex
leaveIndex(){
this.currentIndex=-1
}
}
事件委派,
@mouseleave="leaveIndex
写在外层 div
<div @mouseleave="leaveIndex">
<h2 class="all">全部商品分类</h2>
<div class="sort">
</div>
</div>
控制二级、三级分类显示与隐藏
<!-- 二级、三级分类 -->
<div class="item-list clearfix" :style="{display:currentIndex==index?'block':'none'}">
10 loadsh 插件防抖和节流(* 重要)
正常:事件触发非常频繁,而且每一次的触发,回调函数都要去执行(如果时间很短
,而回调函数内部有计算,那么很可能出现浏览器卡顿
)(鼠标划太快,不会每次都打印)
防抖:前面的所有的触发都被取消
,最后一次执行在规定的时间之后才会触发,也就是说如果连续快速的触发,只会执行最后一次,减少业务负担。
1、搜索框搜索输入。只需用户最后一次输入完,再发送请求。
2、手机号、邮箱验证输入检测。
3、窗口大小 Resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。
节流:在规定的间隔时间范围内不会重复触发回调,只有大于这个时间
间隔才会触发回调,把频繁触发变为少量
触发。(比如 1 秒就执行 1 次)原生 js 闭包 + 定时器实现
1、滚动加载,加载更多或滚到底部监听。
2、谷歌搜索框,搜索联想功能。
3、高频点击提交,表单重复提交。
安装lodash
插件,该插件提供了防抖
和节流
的函数
我们可以引入 js 文件,直接调用。当然也可以自己写防抖和节流的函数
下面代码就是将changeIndex
设置了节流,如果操作很频繁,限制 50ms 执行一次。这里函数定义采用的键值对形式。throttle 的返回值就是一个函数,所以直接键值对赋值就可以,函数的参数在 function 中传入即可。
import {throttle} from 'lodash'
methods: {
//鼠标进入修改响应元素的背景颜色
//采用键值对形式创建函数,将changeIndex定义为节流函数,该函数触发很频繁时,设置50ms才会执行一次
changeIndex: throttle(function (index){
this.currentIndex = index
},50),
//鼠标移除触发时间
leaveIndex(){
this.currentIndex = -1
}
}
11 编程式导航 + 事件委托实现路由跳转
三级标签列表有很多,每一个标签都是一个页面链接,我们要实现通过点击表现进行路由跳转。
路由跳转的两种方法:导航式路由,编程式路由
。
对于声明式路由,我们有多少个
a
标签就会生成多少个router-link
标签,这样当我们频繁操作时会出现卡顿现象。
对于编程式路由,我们是通过触发点击事件实现路由跳转。同理有多少个
a
标签就会有多少个触发函数
。虽然不会出现卡顿,但是也会影响性能。
上面两种方法无论采用哪一种,都会影响性能。
我们提出一种:编程式导航+事件委派
的方式实现路由跳转。事件委派即把子节点的触发事件都委托给父节点
。这样只需要一个回调函数goSearch
就可以解决。
(1)如何确定我们点击的一定是a
标签呢?如何保证我们只能通过点击a
标签才跳转呢?
为三个等级的a标签添加自定义属性date-categoryName绑定商品标签名称来标识a标签(其余的标签是没有该属性的)。
(2)如何获取子节点标签的商品名称和商品id
(我们是通过商品名称和商品 id 进行页面跳转的)
为三个等级的a标签再添加自定义属性data-category1Id、data-category2Id、data-category3Id来获取三个等级a标签的商品id,用于路由跳转。
<!-- 利用事件委派+编程式导航实现路由的跳转与传递参数 -->
<div class="all-sort-list2" @click="goSearch">
<div class="item" v-for="(c1,index) in categoryList" :key="c1.categoryId"
:class="{cur:currentIndex==index}">
<h3 @mouseenter="changIndex(index)" >
<a :data-category>{
{c1.categoryName}}-{
{index}}</a>
</h3>
<!-- 二级、三级分类 -->
<div class="item-list clearfix" :style="{display:currentIndex==index?'block':'none'}">
<div class="subitem" v-for="c2 in c1.categoryChild" :key="c2.categoryId">
<dl class="fore">
<dt>
<a :data-category>{
{c2.categoryName}}</a>
</dt>
<dd>
<em v-for="c3 in c2.categoryChild" :key="c3.categoryId">
<a :data-category>{
{c3.categoryName}}
节点有一个属性dataset属性,可以获取节点的自定义属性值与属性值。
我们可以通过在函数中传入event
参数,获取当前的点击事件,通过event.target
属性获取当前点击节点,再通过 dataset 属性获取节点的属性信息。
对应的goSearrch
函数
goSearch(event){
let element = event.target
//html中会把大写转为小写
//获取目前鼠标点击标签的categoryname,category1id,category2id,category3id,
// 通过四个属性是否存在来判断是否为a标签,以及属于哪一个等级的a标签
let {categoryname,category1id,category2id,category3id} = element.dataset
//categoryname存在,表示为a标签
if(categoryname){
//category1id一级a标签
//整理路由跳转的参数
let location = {name:'search'}//跳转路由name
let query = {categoryName:categoryname}//路由参数
if(category1id){
query.category1Id = category1id
}else if(category2id){
//category2id二级a标签
query.category2Id = category2id
}else if(category3id){
//category3id三级a标签
query.category3Id = category3id
}
//整理完参数
location.query = query
//路由跳转
this.$router.push(location)
}
},
11 TypeNav 在 Search 上隐藏 & 过渡动画
注意:过渡动画的前提是组件、元素必须要有 v-if | v-show 命令,才能进行过渡动画
1、挂载的时侯,不是 home 就不显示
mounted(){
// 通知Vuex发请求,获取数据,存储在仓库中
this.$store.dispatch('categoryList')
// console.log('挂载完毕',state)
// 如果路由不是home,将typeNav隐藏
if(this.$route.path!='/home'){
this.show = false;
}
},
2、search 鼠标进入显示三级菜单,离开的隐藏
// 当鼠标离开的时候,让商品分类隐藏
leaveshow(){
this.currentIndex=-1;
if(this.$route.path!='/home'){
this.show = false;
}
},
// 鼠标进入的时候,显示
entershow(){
if(this.$route.path!='/home'){
this.show = true;
}
}
3、过渡动画的样式
.sort-enter{
height: 0px;
}
.sort-enter-to{
height: 461px;
}
.sort-enter-active{
transition: all .5s linear;
}
12 Vue 路由销毁问题 - 性能优化
Vue 在路由切换的时候会销毁旧路由
我们在三级列表全局组件TypeNav
中的mounted
进行了请求一次商品分类列表数据。
由于 Vue 在路由切换的时候会销毁
旧路由,当我们再次使用三级列表全局组件时还会发一次请求。
由于信息都是一样的,出于性能的考虑我们希望该数据只请求一次
,所以我们把这次请求放在App.vue
的mounted
中。
【根组件App.vue
的mounted
只会执行一次】
注意:虽然main.js
也是只执行一次,但是不可以放在main.js
中。因为只有组件的身上才会有$store
属性。
13 params 和 query 参数合并
searh
搜索跳转前,判断有没有query
参数
if(this.$route.query){// 如果有query参数也要带上
let location = {name:"search",params:{keyword:this.keyword}};
location.query=this.$route.query;
this.$router.push(location);
}
三级标签跳转前判断有没有params
参数
// 判断: 如果路由跳转的时候,带有params参数, 要一起带过去
if(this.$route.params){
this.params = this.$route.params;
}
// 动态给location配置对象添加query属性
location.query = query;
// 路由跳转
this.$router.push(location);
14 mock 插件使用
mock
用来拦截前端ajax
请求,返回我么们自定义的数据用于测试前端接口
第一步: 安装依赖包 mockjs
第二步:在src
文件夹下创建一个文件夹,文件夹mock
文件夹。
第三步: 准备模拟的数据
把mock
数据需要的图片放置于public/images
文件夹中【public 文件夹在打包的时候,会把相应的资源原封不动打包到 dist 文件夹】
比如: listContainer 中的轮播图的数据
[
{id:1,imgUrl:'xxxxxxxxx'},
{id:2,imgUrl:'xxxxxxxxx'},
{id:3,imgUrl:'xxxxxxxxx'},
]
第四步:在 mock 文件夹中创建一个mockServer.js
文件
注意:在mockServer.js
文件当中对于banner.json||floor.json
的数据没有暴露,但是可以在 server 模块中使用。
对于 webpack 当中一些模块:图片、json,不需要对外暴露,因为默认就是对外暴露。
mockServer.js
// 引入morkjs模块
import Mock from "mockjs";
// 把json数据格式引进来
import banner from './banner.json'
import floor from './floor.json'
// mock 数据: 第一个参数 请求地址,第二个参数:请求数据
Mock.mock("/mock/banner",{code:200,data:banner});
Mock.mock("/mock/floor",{code:200,data:floor});
在main.js
入口引入MockSever.js
第五步: 通过 mock 模块模拟出数据
通过 Mock.mock 方法进行模拟数据
第六步: 回到入口文件,引入 serve.js
mock 需要的数据 | 相关 mock 代码页书写完毕,关于 mock 当中 serve.js 需要执行一次,
如果不执行,和你没有书写一样的。
// 引入MockSever.js----mock数据
import '@/mock/mockServer'
** 第七步:** 在 API 文件夹中创建mockRequest
【axios 实例:baseURL:‘/mock’】
专门获取模拟数据用的 axios 实例。
在开发项目的时候:切记,单元测试,某一个功能完毕,一定要测试是否 OK
15 vuex 数据存储与使用
我们会把公共的数据放在 store 中,然后使用时再去 store 中取。
以我们的首页轮播图数据为例。
- 在轮播图组件 ListContainer.vue 组件加载完毕后发起轮播图数据请求
mounted(){
// 派发action:通过Vuex发起Ajax请求,将数据存储在仓库中
this.$store.dispatch('getBannerList')
}
- 请求实际是在
store
中的actions
中完成的store/home.index.js
const actions={
//获取首页轮播图的数据
async getBannerList({commit}){
let result = await reqGetBannerList();
console.log(result);
if(result.code==200){
// console.log('------',result.data)
commit("GATBANNERLIST",result.data);
}
}
};
- 获取到数据后存入
store
仓库,在mutations
完成
const mutations ={
GATBANNERLIST(state,bannerList){
state.bannerList= bannerList
}
};
- 轮播图组件
ListContainer.vue
组件在store
中获取轮播图数据。由于在这个数据是通过异步请求获得的,所以我们要通过计算属性computed
获取轮播图数据。
ListContainer.vue
代码
computed:{
...mapState({
bannerList:state=>state.home.bannerList
})
}
16 swiper 插件实现轮播图
(1)安装 swiper
(2)在需要使用轮播图的组件内导入 swpier 和它的 css 样式
(3)在组件中创建 swiper 需要的 dom 标签(html 代码,参考官网代码)
(4)创建 swiper 实例
-
在 banner 页引入
import Swiper from 'swiper'
-
在全局引入在 main.js 中
import 'swiper/css/swiper.css'
组件里代码 -
接下来要考虑的是什么时候去加载这个
swiper
,我们第一时间想到的是在mounted
中创建这个实例。
但是会出现无法加载轮播图片的问题。
原因:
我们在mounted
中先去异步请求了轮播图数据,然后又创建的 swiper 实例。由于请求数据是异步
的,所以浏览器不会等待该请求执行完再去创建swiper
,而是先创建了swiper
实例,但是此时我们的轮播图数据
还没有获得,就导致了轮播图展示失败。
解决方法一:等我们的数据请求完毕后再创建 swiper 实例。只需要加一个1000ms
时间延迟再创建 swiper 实例.。(实际中可以先这么写,以后再优化)
mounted() {
this.$store.dispatch("getBannerList")
setTimeout(()=>{
let mySwiper new Swiper(this.$refs.mySwiper,{
pagination:{
el: '.swiper-pagination',
clickable: true,
},
// 如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
// 如果需要滚动条
scrollbar: {
el: '.swiper-scrollbar',
},
})
},1000)
},
获取 dom 用
$refs
解决方法二:我们可以使用watch
监听bannerList
轮播图列表属性,因为bannerList
初始值为空,当它有数据时,我们就可以创建swiper
对象。
watch:{
bannerList(newValue,oldValue){
let mySwiper = new Swiper(this.$refs.mySwiper,{
pagination:{
el: '.swiper-pagination',
clickable: true,
},
// 如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
// 如果需要滚动条
scrollbar: {
el: '.swiper-scrollbar',
},
})
}
我们的watch
只能保证在bannerList
变化时创建swiper
对象,但是并不能保证此时v-for
已经执行完了。假如watch
先监听到bannerList
数据变化,执行回调函数创建了swiper
对象,之后v-for
才执行,这样也是无法渲染轮播图图片
【因为 swiper 对象生效的前提是 html 即 dom 结构已经渲染好了】
完美解决方案:使用watch+this.$nextTick()
官方介绍:this. $nextTick 它会将回调延迟到下次 DOM 更新循环之后执行(循环就是这里的 v-for)。
个人理解:无非是等我们页面中的结构都有了再去执行回调函数
.$nextTick()
将回调延迟到下次 DOM 更新循环
之后执行。在修改数据之后
立即使用它,然后等待 DOM 更新。
文档:https://cn.vuejs.org/v2/api/#vm-nextTick
watch:{
// 监听bannerList数据的变化:因为这条数据发生了变化
bannerList(newValue,oldValue){
//this.$nextTick()使用###################
this.$nextTick(()=>{
let mySwiper = new Swiper(this.$refs.mySwiper,{
pagination:{
el: '.swiper-pagination',
clickable: true,
},
// 如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
// 如果需要滚动条
scrollbar: {
el: '.swiper-scrollbar',
},
})
})
}
}
之前我们在学习watch
时,一般都是监听的定义在data
中的属性,但是我们这里是监听的computed
中的属性,这样也是完全可以的,并且如果你的业务数据也是从store
中通过computed
动态获取的,也需要watch
监听数据变化执行相应回调函数,完全可以模仿上面的写法。
17 开发 Floor 组件–父子通信
1. Floor
组件获取mock
数据,发请求的action
书写在哪里?
派发action
应该是在父组件的组件挂载
完毕生命周期函数中书写,因为父组件需要通知 Vuex 发请求,父组件
获取到mock
数据,通过v-for
遍历 生成多个floor
组件,因此达到复用作用。
在 home 中:
mounted(){
this.$store.dispatch("getFloorList")
},
computed:{
...mapState({
floorList: state => state.home.floorList
})
},
2. 组件间通信props:
父子
插槽: 父子
自定义事件: 子父
全局事件总线$bus
: 万能pubsub:
万能Vuex
: 万能$ref
: 父子通信
父子通信
父组件Home/index.vue
:
子组件Floor/index.vue
为什么在 Floor 组件的 mounted 中初始化 Swiper 实例轮播图可以使用
一次书写轮播图的时候,是在组件内部发请求,动态渲染结构【前台至少服务器数据需要回来】,因此当时在mounted
中写轮播不行
因为父组件的mounted
发请求获取 Floor 组件,当父组件的mounted
执行的时候,Floor
组件结构可能没有完整,但是服务器的数据回来以后Floor
组件结构就一定是完成的了,因此v-for
在遍历来自于服务器的数据,如果服务器的数据有了,Floor
结构一定的完整的。
否则,你都看不见 Floor 组件
18 将轮播图模块提取为公共组件
需要注意的是我们要把定义 swiper 对象放在 mounted 中执行,并且还要设置
immediate:true
属性,这样可以实现,无论数据有没有变化,上来立即监听一次。props
实现父组件向子组件传递消息,这里同样也会将轮播图列表传递给子组件,原理相同。
这样方便拆组件,如下引入组件:
<!-- 轮播图 -->
<Carsousel :list="list.carouselList" />
19 getters 使用
getters
是 vuex store 中的计算属性。
0
如果不使用getters
属性,我们在组件获取 state 中的数据表达式为:this.$store.state.子模块.属性
,
就像计算属性一样,getter
的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
仓库中的 getters 是全局属性,是不分模块的。即store
中所有模块的 getter 内的函数都可以通过 $store.getters. 函数名获取
下图为store/search
内容
// 计算属性,在项目中,getters为了简化仓库中的数据
// 可以把我们将来在组件中需要的数据简化一下(将来组件获取数据就方便了)
const getters={
goodsList(state){ //形参state是当前仓库中的state,
// 如果服务器数据回来了,是一个数组,假如网络不稳定,goodsList会变成undefined,以防万一,要加||[]
return stata.searchList.goodsList||[];
},
attrsList(state){
return state.searchList.attrsList||[];
},
trademarkList(state){
return state.searchList.trademarkList||[];
},
};
export default{
namespace:true, // 开启命名空间
state,
mutations,
actions,
getters
}
在Search组件
中使用getters
获取仓库数据
import {mapGetters} from 'vuex'
computed:{
...mapGetters(['goodsList'])
},
20 Object.asign 实现对象拷贝
Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
Object.assign(target, ...sources) 【target:目标对象】,【source:源对象(可多个)】
beforeMount(){
// console.log(this.searchParams)
/*this.searchParams.category1Id= this.$route.query.category1Id;
this.searchParams.category2Id= this.$route.query.category2Id;
this.searchParams.category3Id= this.$route.query.category3Id;
this.searchParams.categoryName= this.$route.query.categoryName;
this.searchParams.keyword= this.$route.query.keyword;*/
// # Object.assign : ES6新增语法
Object.assign(this.searchParams,this.$route.query,this.$route.params)
},
21 面包屑相关操作
1、面包屑删除分类名
<!-- 分类面包屑 -->
<li class="with-x" v-if="searchParams.categoryName">{
{searchParams.categoryName}}
<i @click="removeCategoryName">x</i>
</li>
// 删除分类的名字
removeCategoryName(){
// 带个服务器的参数是可有可无的:如果属性值为空的字符串还是会把相应的字段带给服务器
this.searchParams.categoryName=undefined;
this.searchParams.category1Id=undefined;
this.searchParams.category2Id=undefined;
this.searchParams.category3Id=undefined;
// 重新发请求
this.getData();
// 地址栏也需要改:进行路由跳转
// 严谨:本意是删除query参数,如果路径中出现params参数不应该删除,应该带着
if(this.$route.params)
this.$router.push({name:"search",params:this.$route.params})
},
2、兄弟组件通信
当面包屑中的关键字删除后,需要让兄弟组件 header 中的关键字清除
组件间通信:
这里用全局事件总线$bus
的使用:
① 在 main.js 中配置
new Vue({
render: h => h(App),
// 配置全局事件总线$bus
beforeCreate(){
Vue.prototype.$bus=this;
},
// 注册路由,底下的写法是kv一致,省略v【router小写的】
// 注册路由信息:当这里写router的时候,组件身上都拥有$route,$router属性
router,
// 注册仓库:组件实例对象身上就多了一个$store属性
store
}).$mount('#app')
② 在当前search/index.vue
中用$bus.$emit
通知兄弟组件
第一个参数可以理解为为通信的暗号
,还可以有第二个参数(用于传递数据),我们这里只是用于通知 header 组件进行相应操作,所以没有设置第二个参数。
// 删除关键字
removeKeyword(){
// 给服务器的searchParams中的keyword置空
this.searchParams.keyword=undefined;
this.getData();
// 通知兄弟组件header清除关键字
this.$bus.$emit("clear");
}
③ 在兄弟组件Header/index.vue
中用$bus.$on
接收通信
在挂载的时候就监听到,把 keyword 的值置为空
mounted(){
// 通过全局事件总线清除关键字
// 组件挂载时就监听clear事件,clear事件在search模块中定义
// 当删除关键字面包屑时,触发该事件,同时header的输入框绑定的keyword要删除
this.$bus.$on("clear",()=>{
this.keyword="";
})
},
3、子父组件通信
SearchSelector
子组件向父组件传参,实现面包屑操作
①. 父组件添加自定义事件trademarkInfo
<!--selector 添加自定义事件-->
<SearchSelector @trademarkInfo="trademarkInfo"/>
②. 父组件自定义事件回调
methods:{
// 自定义事件回调
trademarkInfo(trademark){
// console.log(trademark);
// 整理参数,形式 示例: "1:苹果"
this.searchParams.trademark=`${trademark.tmId}:${trademark.tmName}`;
// 再次发请求
this.getData();
}
}
③. 子组件触发$emit
触发事件
<li v-for="(trademark,index) in trademarkList" :key="trademark.tmId" @click="tradeMatkHandler(trademark)">{
{trademark.tmName}}</li>
methods:{
// 品牌的事件处理函数
tradeMatkHandler(trademark){
// 点击品牌,还需要处理参数,向服务器发送请求获取相应的数据
// 在父组件search中发请求,因为父组件中的searchParams参数是带带给服务器的参数,
// 子组件需要把点击的品牌信息,传递给父组件
this.$emit("trademarkInfo",trademark);
// console.log('tmId',trademark.tmId)
}
}
接下来是对面包吧面包屑的配置和删除
<!-- 品牌面包屑 -->
<li class="with-x" v-if="searchParams.trademark">{
{searchParams.trademark.split(":")[1]}}
<i @click="removeTrademark">x</i>
</li>
// 删除品牌的面包屑
removeTrademark(){
this.searchParams.trademark=undefined;
// 重新发请求
this.getData();
}
4、售卖属性面包屑
注意点:
- 展示的时候需要
v-for
因为props
是数组 - 需要判断数组里是否已经有了再
push
- 删除的时候需要传递数组的
index
<!-- 平台售卖属性的面包屑 -->
<li class="with-x" v-for="(attrValue,index) in searchParams.props" :key="index">{
{attrValue.split(":")[1]}}
<i @click="removeAttr(index)">x</i>
</li>
methods:{
// 收集平台售卖属性的回调,(自定义事件)
attrInfo(attr,attrValue){
// console.log("父组件:","attr:",attr,"attrValue:",attrValue)
// 参数按格式整理好["属性ID:属性值:属性名"]
let props = `${attr.attrId}:${attrValue}:${attr.attrName}` // alert( props);
// 判断是否已经存在
if(this.searchParams.props.indexOf(props)==-1)
this.searchParams.props.push(props);
// 再次发请求
this.getData();
}
}
methods:{
// removeAttr删除售卖属性
removeAttr(index){
// 删除数组中的index下标,用splice
this.searchParams.props.splice(index,1);
// 再次发请求
this.getData();
}
}
22 商品排序
1、在 public 文件index.html
引入该 css
<link rel="stylesheet" href="https://at.alicdn.com/t/font_2994457_qqwrvmss9l9.css">
2、点击事件,把当前的flag
传过去
<ul class="sui-nav">
<li :class="{active:isOne}" @click="changeOrder('1')">
<a href="#">综合
<span v-show="isOne" class="iconfont" :class="{'icon-down': isAsc, 'icon-up': isDesc}"></span>
</a>
</li>
<li :class="{active:isTwo}" @click="changeOrder('2')">
<a href="#">价格
<span v-show="isTwo" class="iconfont" :class="{'icon-down': isAsc, 'icon-up': isDesc}"></span>
</a>
</li>
</ul>
3、点击事件,若是当前的flag
则降序变成升序,否则改变flag
,默认降序
// 排序按钮
changeOrder(flag){
// flag形参:用于区分综合、价格,1:综合,2:价格 用户点击的时候传进来的
let orginOrder = this.searchParams.order;
// 获取 起始状态
let orginflag = orginOrder.split(":")[0];
let orginSort = orginOrder.split(":")[1];
let newOrder='';
// 点击的还是当前的flag
if(orginflag == flag){
newOrder =`${orginflag}:${orginSort=="desc"?"asc":"desc"}`;
}else{
// 点击的是价格
newOrder=`${flag}:desc`;
}
// 将新的order给searchParams
this.searchParams.order=newOrder;
// 再次发请求
this.getData();
}
23 分页功能 - 全局组件
在main.js
中引入全局组件
// 引入分页器组件---全局组件
import Pageination from '@/components/Pageination'
Vue.component(Pageination.name,Pageination);
封装分页器组件的时候:需要知道哪些条件?
1: 分页器组件需要知道:一共展示多少条数据 ---- total
【100 条数据】
2: 每一页需要展示几条数据 ------pageSize
【每一页 3 条数据】
3: 需要知道当前在第几页 -------pageNo
【当前在第几页】
4: 需要知道连续页码数 ------ continues
【起始数字、结束数字:连续页码数一般为 5、7、9】奇数,因为对称好看
先用假的数据完成,再用服务器的数据
核心逻辑是获取连续页码的起始
页码和末尾
页码v-for
可以遍历数组 | 数字 | 字符串 | 对象
页面部分代码
<div class="pagination">
<!-- 上 -->
<button :disabled="pageNo==1" @click="$emit('getPageNo',pageNo-=1)">上一页</button>
<button v-if="startNumAndEndNum.start>1" @click="$emit('getPageNo',1)"
:class="{active:pageNo==1}">
1
</button>
<button v-if="startNumAndEndNum.start>2">···</button>
<!-- 中间部分 -->
<button v-for="(page,index) in startNumAndEndNum.end" :key="index" v-if=" page>=startNumAndEndNum.start"
@click="$emit('getPageNo',page)" :class="{active:pageNo==page}">
{
{page}}
</button>
<!-- 下 -->
<button v-if="startNumAndEndNum.end<totalPage-1">...</button>
<button v-if="startNumAndEndNum.end<totalPage" @click="$emit('getPageNo',totalPage)"
:class="{active:pageNo==totalPage}">{
{totalPage}}
</button>
<button :disabled="pageNo==totalPage" @click="$emit('getPageNo',pageNo+=1)">
下一页
</button>
<button style="margin-left: 30px">共{
{total}}条</button>
</div>
1、分页逻辑 js 部分代码
export default {
name: "Pageination",
props:["pageNo","pageSize","total" ,"continues"],
computed:{
// 计算总共多少页
totalPage(){
return Math.ceil(this.total/this.pageSize);
},
// 计算出连续的页码的起始数字和结束数字
startNumAndEndNum(){
// 解构赋值,就不需要this.xxx
const {continues,pageNo,totalPage}=this;
let start =0, end=0;
//如果连续页大于总数,那么起始是1,结束是总页数
if(continues>totalPage){
start =1;
end=totalPage;
}
// 总页数大于连续页数的情况【正常】
else{
start=pageNo-parseInt(continues/2);
end=pageNo+parseInt(continues/2)
if(start<1){
start=1;
end=continues;
}
if(end>totalPage){
start=totalPage-continues+1;
end=totalPage;
}
}
return {start,end};
}
}
};
2、然后把当前的页码给父组件传过去,重新发请求获取数据
// 自定义事件的回调函数,获取当前第几页
getPageNo(pageNo){
// console.log(PageNo);
this.searchParams.pageNo=pageNo;
this.getData();
}
24 商品详情页 - 路由组件
- 在
routes.js
注册路由, 配置路由
// 引入路由文件
import Search from '@/pages/Search'
import Detail from '@/pages/Detail'
//配置路由
export default new VueRouter({
routes:[
{
// 详情页,需要占位
path:"/detail/:stuid?",
component:Detail,
meta:{show:true}
},
]
})
1、从search
中跳转到detail
页,带参数
<router-link :to="`/detail/${good.id}`">
<img :src="good.defaultImg" />
</router-link>
2、每次跳转控制滚动条在最上方
//对外暴露VueRouter类的实例
let router = new VueRouter({
//配置路由
//第一:路径的前面需要有/(不是二级路由)
//路径中单词都是小写的
//component右侧V别给我加单引号【字符串:组件是对象(VueComponent类的实例)】
routes,
//滚动行为
scrollBehavior(to, from, savedPosition) {
//返回的这个y=0,代表的滚动条在最上方
return { y: 0 };
},
});
3、在 api 文件中配置发送请求的
// 获取商品详情信息的接口: url: /api/item/{ skuId },get请求,参数:skuId,必须要
export const reqGoodInfo = (skuid)=> requests({
url:`/item/${skuId}`,
method:"get",
})
4、在store
中新建detail
仓库(Vuex)
在store/index.js
中引入detail
仓库,同样写好仓库中的内容,在detail
中派发获取详情的action
24.1 详情页大图
父亲给数据
<!--放大镜效果-->
<Zoom :skuImageList="skuImageList"/>
computed:{
...mapGetters(['categoryView','skuInfo']),
skuImageList(){
// 假如服务器的数据没有和回来,就是空数组
return this.skuInfo.skuImageList||[];
}
}
儿子接数据 -- 至少是个空对象
export default {
name: "Zoom",
props:['skuImageList'],
computed:{
imgObj(){
//至少是个空对象
return this.skuImageList[0]||{}
}
}
}
在视图中使用
<img :src="imgObj.imgUrl" />
24.2 实现点击的属性为高亮
1、绑定方法,传入spuSaleAttrValue
,spuSaleAttr.spuSaleAttrValueList
<dd changepirce="0" :class="{active:spuSaleAttrValue.isChecked==1}"
v-for="(spuSaleAttrValue,index) in spuSaleAttr.spuSaleAttrValueList"
:key="spuSaleAttrValue.id" @click="changeActive(spuSaleAttrValue,spuSaleAttr.spuSaleAttrValueList)">{
{spuSaleAttrValue.saleAttrValueName}}
</dd>
2、把其余设置 0,当前设置为 1
methods:{
// 切换产品的售卖属性至高亮
changeActive(spuSaleAttrValue,arr){
// 遍历全部的售卖属性的isChecked为0
arr.forEach(item => {
item.isChecked='0'
});
// 点击的售卖属性为1
spuSaleAttrValue.isChecked=1;
}
}
24.3 兄弟组件通信 - 获取索引值
<img :src="slide.imgUrl" :class="{active:currentIndex==index}"
@click="changeCurrentIndex(index)">
methods:{
changeCurrentIndex(index){
// 修改响应式数据
this.currentIndex=index
// 通知自己的兄弟组件,当前的索引值是什么
this.$bus.$emit('getIndex',this.currentIndex)
}
}
data(){
return {
currentIndex:0,
}
},
mounted(){
// 全局事件总线,获取兄弟组件传递过来的索引值
this.$bus.$on('getIndex',(index)=>{
// console.log('接受到索引值:',index)
//修改当前响应式数据
this.currentIndex=index
})
24.4 放大镜功能
<div class="event" @mousemove="handler"></div>
<div class="big">
<img :src="imgObj.imgUrl" ref="big"/>
</div>
methods:{
handler(event){
let mask=this.$refs.mask;
let big= this.$refs.big;
let left= event.offsetX- mask.offsetWidth/2;
let top= event.offsetY- mask.offsetHeight/2;
// 约束范围
if(left<=0) left=0;
//如果大于蒙版的宽度,就是等于蒙版的宽度
if(left>=mask.offsetWidth) left=mask.offsetWidth;
if(top<=0) top=0;
if(top>=mask.offsetHeight) top=mask.offsetHeight;
// 修改元素的left' top值
mask.style.left=left+'px';
mask.style.top=top+'px';
big.style.left= -2*left+'px';
}
25 购物车相关路由
25.1 加入购物车
// 加入购物车的回调函数
async addShopCart(){
// 1;发请求-====将产品加入到数据库,通知服务器
/*
当前这里派发一个action,也向服务器请求,判断加入购物车是成功还是失败,进行相应的操作
这里调用了仓库里的addOrUpdateShopCart,返回的是一个promise
*/
try{
await this.$store.dispatch('addOrUpdateShopCart',{
skuId:this.$route.params.stuid,
skuNum:this.skuNum});
// 进行路由跳转,还需把产品的信息带给下一级路由组件
// 一些简单的数据skuNum,通过query形式给路由组件传递过去
// 产品信息的数据(比较复杂skuInfo),通过会话存储(不持久化,会话结束数据在消失)
// 本地存储和会话存储一般存储 #字符串#
sessionStorage.setItem("SKUINFO",JSON.stringify(this.skuInfo))
this.$router.push({name:'AddCartSuccess',query:{skuNum:this.skuNum}});
}catch(error){
alert(error.message);
}
对应的store
中detail/index.js
// 将商品添加到购物车
async addOrUpdateShopCart({commit},{skuId,skuNum}){
// 加入购物车返回的结果
// 加入购物车以后(发请求),前台将参数带给服务器
// 服务器写入成功并没有返回其他数据,只是返回code=200,代表这次操作成功
// 因此不需要三联环存储数据
let result= await reqAddOrUpdateShopCart(skuId,skuNum);
// console.log(result)
if(result.code==200){
return "ok";
}else{
return Promise.reject(new Error('falid'))
}
}
注册购物车成功的路由组建,在routes.js
中
{
// 购物车成功路由
path:"/AddCartSuccess",
component:AddCartSuccess,
name:"AddCartSuccess",
meta:{show:true}
},
25.2 购物车组件
如果想要获取详细信息,还需要一个用户的uuidToken
,用来验证用户身份。但是该请求函数没有参数,所以我们只能把 uuidToken 加在请求头
中。
创建 utils 工具包文件夹,创建生成 uuid 的 js 文件,对外暴露为函数(导入 uuid => npm install uuid)。
生成临时游客的 uuid(随机字符串), 每个用户的 uuid 不能发生变化,还要持久存储
import {v4 as uuidv4} from 'uuid';
// 要生成一个随机字符串,且每次执行不能发生变化,游客身份持久存储
export const getUUID=()=>{
// 先从本地获取uuid(看一下是否有)
let uuid_token=localStorage.getItem('UUIDTOKEN');
// 如果没有
if(!uuid_token){
// 生成游客临时身份
uuid_token=uuidv4();
// 本地存储一次
localStorage.setItem('UUIDTOKEN',uuid_token);
}
// 要有返回值,否则undefined
return uuid_token;
}
用户的uuid_token
定义在 store 中的 detail 模块
在request.js
中设置请求头
import store from '@/store';
requests.interceptors.request.use(config => {
//config内主要是对请求头Header配置
//1、先判断uuid_token是否为空
if(store.state.detail.uuid_token){
//2、userTempId字段和后端统一
config.headers['userTempId'] = store.state.detail.uuid_token
}
//比如添加token
//开启进度条
nprogress.start();
return config;
})
25.3 购物车数量修改
<a href="javascript:void(0)" class="mins" @click="handler('minus',-1,cart)">-</a>
<input autocomplete="off" type="text" minnum="1" class="itxt"
:value="cart.skuNum" @change="handler('change',$event.target.value*1,cart)">
<a href="javascript:void(0)" class="plus" @click="handler('add',1,cart)">+</a>
type 是区分三个元素,disNum 形参:变化量 1 -1 cart 哪一个产品
注意点:【节流
】【try...catch
】
// 修改某一个产品的个数
/*type是区分三个元素,disNum 形参:变化量 1 -1 cart 哪一个产品 【节流】*/
handler: throttle(async function(type, disNum, cart){
// 目前
switch(type){
case"add":
disNum=1;
break;
case "minus":
// 产品个数大于1,才可以传递给服务器
// 如果出现产品的个数小于等于1,给服务器的个数为0
disNum=cart.skuNum>1?-1:0;
break;
case "change":
// 用户输入的是非法的或负数,服务器是0
if(isNaN(disNum)||disNum<1)
disNum=0;
else
disNum=parseInt(disNum)-cart.skuNum;
break;
}
// 派发action
try{
await this.$store.dispatch('addOrUpdateShopCart',{
skuId:cart.skuId,
skuNum:disNum
});
}catch(error){
alert(error.message)
}
},500),
25.4 购物车删除
接口:
export const reqDeleteCartById = (skuId)=>requests({
url:`cart/deleteCart/${skuId}`,
method:"delete",
})
仓库中:
// 删除购物车某一个产品
async deleteCartListBySkuId({commit},skuId){
let result= await reqDeleteCartById(skuId);
if(result.code==200){
return "ok";
}else{
return Promise.reject(new Error('failed'));
}
}
组件中
<a href="#none" class="sindelet" @click="deleteCartById(cart)">删除</a>
方法:
async deleteCartById(cart){
try{
// 如果删除成功再次发送请求获取新的数据进行展示
await this.$store.dispatch("deleteCartListBySkuId",cart.skuId);
this.getData();
}catch(error){
alert(error.message)
}
}
25.5 购物勾选发请求操作
接口
// 修改产品的选中状态
export const reqUpdateCheckById = (skuId,isChecked)=>requests({
url:`cart/checkCart/${skuId}/${isChecked}`,
method:"get",
})
仓库
// 修改购物车的选中状态
async updateCheckedById({commit},skuId,isChecked){
let result = await reqUpdateCheckById(skuId,isChecked);
if(result.code==200){
return "ok";
}else{
return Promise.reject(new Error('failed'));
}
}
组件中,【//key value 一致 可以省略】
<input type="checkbox"
:checked="cart.isChecked==1" @change="updateChecked(cart,$event)">
//修改某个产品的勾选状态
updateChecked(cart,event){
// 带给服务器的参数isChecked,不是布尔值,是0或者1
// console.log(event.target.checked);
try {
//如果修改数据成功,再次获取服务器数据(购物车)
let isChecked=event.target.checked?"0":"1";
this.$store.dispatch("updateCheckedById",{
skuId:cart.skuId,
isChecked
});//key value一致
this.getData();
} catch (error) {
//如果失败提示
alert(error.message);
}
25.6 删除多个 Promise.all
组件中:
<a @click="deleteAllCheckedCart">删除选中的商品</a>
// 删除全部选中的产品
async deleteAllCheckedCart(){
try{
await this.$store.dispatch("deleteAllCheckedCart");
this.getData();
}catch(error){
alert(error.message)
}
}
仓库中:
// 删除全部勾选的产品
async deleteAllCheckedCart({dispatch,getters}){
// context 小仓库,commit【提交mutation修改state】 getters【计算属性】
// dipatch【派发action】state【当前仓库数据】
// console.log(getters.cartList.cartInfoList)
let PromiseAll=[];
getters.cartList.cartInfoList.forEach(item => {
let promise = item.isChecked==1?dispatch('deleteCartListBySkuId',item.skuId):''
PromiseAll.push( promise);
});
// 只有每一个都成功返回成功
return Promise.all(PromiseAll);
}
25.7 实现全选功能 Promise.all
1、组件中 cartInfoList.length>0 时才会被选择
<input class="chooseAll" type="checkbox"
:checked="isAllChecked && cartInfoList.length>0"
@change="updateAllCartChecked">
<span>全选</span>
2、计算属性computed
:
// 判断底部的复选框是否勾选
isAllChecked(){
// 遍历全部isChecked 如果都是1,返回1
return this.cartInfoList.every(item=>item.isChecked==1);
}
3、方法methods
:派发action
// 修改全部产品的选中状态
updateAllCartChecked(event){
try{
let isChecked= event.target.checked?"1":"0"
// 派发action
this.$store.dispatch("updateAllCartChecked",isChecked);
}catch(error){
alert(error.message);
}
}
4、仓库中调用选择并发请求的函数updateCheckedById
,传入参数skuId,isChecked
// 修改全部产品的状态
updateAllCartChecked({dispatch,state},isChecked){
let PromiseAll=[];
state.cartList[0].cartInfoList.forEach(item=>{
console.log(item.skuId,isChecked)
let promise=dispatch('updateCheckedById',{
skuId:item.skuId,
isChecked
});
PromiseAll.push(promise);
})
// 最终返回的结果
return Promise.all(PromiseAll);
}
26 登录注册
业务逻辑
注册 ----- 通过数据库存储用户信息(名字、密码)
登录 ----- 登录成功的时候,后台为了区分这个用户是谁 - 服务器下发 token【令牌:唯一标识】
登录接口:一般登录成功下发 token,前台持久化存储 token【带着 token 找服务器要用户信息进行展示】Vuex
不是持久化存储,一刷新就没有了
小细节 1:
JS 中可以用@
表示 src(src 的别名),
CSS 中可以用~@
,加波浪号~
background-image: url(~@/assets/images/icons.png);
小细节 2:解构赋值
const {comment,index,deleteComment} = this
上面的这句话是一个简写,最终的含义相当于:
const comment = this.comment
const index = this.index
const deleteComment = this.deleteComment
26.1 注册
组件中:
<input type="text" placeholder="请输入你的手机号" v-model="phone">
<button style="width:100px;height:38px" @click="getCode">获取验证码</button>
<input type="text" placeholder="请输入你的登录密码" v-model="password">
<input type="text" placeholder="请输入确认密码" v-model="password1">
data
中
data(){
return {
// 收集手机号
phone:'',
// 验证码
code:'',
// 密码
password:'',
// 确认密码
password1:'',
// 是否同意
agree:true,
}
},
methods
中 (注意
&& 前面为真, 才执行后面)
// 用户注册
userRegister(){
try{
// 解构赋值
const {phone,code,password,password1} =this;
// && 前面为真,才执行后面
(phone&&code&&password==password1)
&&this.$store.dispatch("userRegister",{phone,code,password,password1});
// 注册成功跳转到登陆
this.$router.push('/login')
}catch(error){
alert(error.message)
}
}
actions
中
// 用户注册
async userRegister({commit},user){
let result = await reqUserRegister(user);
if(result.code==200){
return "ok"
}else{
return Promise.reject(new Error('failed'));
}
}
26.2 登录
组件中methods
:
methods:{
// 登录的回调
userLogin(){
try {
//登录成功
const {phone,password}=this;
phone && password&&(this.$store.dispatch('userLogin',{phone,password}));
this.$router.push("/home")
} catch (error) {
alert(error.message)
}
}
}
仓库中actions
:
// 用户登陆
async userLogin({commit},user){
let result = await reqUserLogin(user);
if(result.code==200){
commit("USERLOGIN",result.data.token);
// localStorage.setItem("TOKEN",result.data.token);
setToke(result.data.token);
return "ok"
}else{
return Promise.reject(new Error('failed'));
}
}
仓库中mutations
和state
:
// 登陆与注册的模块
const state={
code:'',
token:getToke(),
};
const mutations={
GETCODE(state,code){
state.code=code;
},
USERLOGIN(state,token){
state.token=token;
}
};
26.3 获取用户信息
在主页的home
中的mounted
中
mounted(){
// 派发action,获取floor组件中的数据
this.$store.dispatch("getFloorList"),
// 获取用户信息在首页展示
this.$store.dispatch("userInfo")
},
user
仓库里面三连环actions
中
// 获取用户信息
async userInfo({commit}){
let result= await reqUserInfo();
console.log("==",result)
if(result.code==200){
commit("USERINFO",result.data);
console.log("==",result.data)
return "ok"
}else{
return Promise.reject(new Error('failed'));
}
}
state
和mutations
中
const state={
userInfo:{}
};
const mutations={
USERINFO(state,userInfo){
state.userInfo=userInfo;
}
};
26.4 为了持久化存储,把 token 保存到localstorage
// 对外暴露一个函数
// 存储token,获取token
export const setToke=(token)=>{
localStorage.setItem("TOKEN",token);
}
export const getToke=()=>{
localStorage.getItem("TOKEN");
}
26.5 退出登录
1 发请求,需要通知服务器,把现在用户身份 token【销毁】
2 清除仓库数据 + 本地存储数据【都需要清理】
组件中:
logout(){
// 1.需要发请求,通知服务器退出登录【清除一些数据:token】
// 2.清除项目中的数据,【userInfo、token】
try {
this.$store.dispatch("userLogout");
// 如果退出成功,回到首页
this.$router.push('/home')
} catch (error) {
alert(error.message);
}
}
仓库的action
:
async userLogout({commit}){
// 只是向服务器发起请求,通知服务器清除token
let result=await reqLogout();
if(result.code==200){
commit("CLEAR");
return "ok"
}else{
return Promise.reject(new Error('failed'));
}
}
仓库中mutations
:
// 清除本地数据
CLEAR(state){
state.token='',
state.userInfo={}
// 本地存储中的token清空
removeToken();
}
27 导航守卫
导航: 表示路由正在发生改变,进行路由跳转。
守卫: 古代的守门的士兵’守卫
’,守卫可以通过条件判断
路由能不能进行跳转
分类:全局守卫,路由独享守卫,组件内守卫。
全局守卫
是指路由实例($router)上直接操作的钩子函数,触发路由就会触发这些钩子函数。【紫禁城大门的守卫】
在项目中,只要发生路由变化,守卫就能监听到。【按时间不同,分为三种,常用全局前置守卫】
- 全局前置守卫:
router.beforeEach()
- 全局解析守卫:
router.beforeResolve()
- 全局后置钩子:
router.afterEach()
路由守卫
可以在单个路由配置上直接定义beforeEnter
守卫。【相应的皇帝路上守卫】
组件守卫
可以在组件内直接定义以下路由导航守卫:【已经到皇帝物资外面了(进入了)守卫】
beforeRouteEnter
beforeRouteUpdate
(2.2 新增)beforeRouteLeave
router index.js
全局前置守卫代码
// 全局前置守卫
router.beforeEach(async(to,from,next)=>{
// to:可以获取你要跳转的那个路由信息
// from: 可以获取你从哪个路由而来的信息
// next:放行函数; next() 放行; next(path)放行到指定的path路由; next(false)
next();
// 用户登录了才会有token,未登录一定没有token
let token = store.state.user.token;
// 用户信息
let name=store.state.user.userInfo.name;
//1、有token代表登录,全部页面放行
if(token){
// 1.1 用户已经登录,就不能去login,停留在首页
if(to.path=='/login'){
next('/')
}else{
//1.2 登录了,去的不是login
//1.2.1 [判断仓库里是否有用户信息,有放行,没有派发action]
if(name){
next();
}else{
// 1.2.2 如果没有用户信息,则派发actions获取用户信息
try {
// 获取用户信息
await store.dispatch("userInfo");
} catch (error) {
// 1.2.3 获取用户信息失败,原因:token过期
//清除前后端token,跳转到登陆页面
await store.dispatch('userLogout');
next('/login');
}
}
}
}else{
// 2 未登录,首页或者登录页可以正常访问
if(to.path === '/login' || to.path === '/home' || to.path==='/register'){
next();
}
else{
alert('请先登录');
next('/login')
}
}
})
路由守卫,在routes.js
{
path:"/trade",
component:Trade,
meta:{show:true},
// 路由独享组件
beforeEnter:(to,from,next)=>{
if(from.path=='/ShopCart'){
next();
}else{
next(false);
}
}
},
28 交易页面
28.1 修改默认地址
methods:{
// 修改默认地址
changaDefault(address,addressInfo){
// 全部isDefault为0,选中的为1
addressInfo.forEach(item => {
item.isDefault=0
});
address.isDefault=1;
}
}
29 全局引入 api 类似于$bus
在 main.js 中引入
// 统一接口api文件夹里面全部请求函数【统一引入】
import * as API from '@/api'
new Vue({
render: h => h(App),
// 配置全局事件总线$bus
beforeCreate(){
Vue.prototype.$bus=this;
Vue.prototype.$API=API;###########
},
//注册路由,底下的写法是kv一致,省略v【router小写的】
//注册路由信息:当这里写router的时候,组件身上都拥有$route,$router属性
router,
//注册仓库:组件实例对象身上就多了一个$store属性
store
}).$mount('#app')
30 提交订单(直接在组件中发请求)
这次没有使用Vuex
,感觉更方便了
// 提交订单
async submitOrder() {
// console.log(this.$API)
// 交易编码
let { tradeNo } = this.orderInfo;
// console.log(tradeNo);
// 其余六个参数
let data = {
consignee: this.userDefaultAddress.consignee,
consigneeTel: this.userDefaultAddress.phoneNum,
deliveryAddress: this.userDefaultAddress.fullAddress,
paymentWay: "ONLINE",
orderComment: this.msg,
orderDetailList: this.orderInfo.detailArrayList,
}
// 发送请求,带参数(tradeNo,data)
let result= await this.$API.reqSubmitOrder(tradeNo,data);
if(result.code==200){ //提交订单成功
this.orderId=result.data;
// 成功了,路由跳转+传递参数
this.$router.push('/pay?orderId='+this.order);
}else{
alert(result.data);
}
}
注意细节:生命周期函数上不能加 async。可以写成如下方式:
把 async 和 await 放在方法里,在生命周期函数中调用
mounted(){
// 生命周期函数函数不能加async
// await this.$API.reqPayInfo(this.orderId)
this.getPayInfo();########
},
methods:{
async getPayInfo(){####
let result= await this.$API.reqPayInfo(this.orderId);
// 如果成功:组件中存储支付信息
if(result.code==200){
this.payInfo=result.data;
}else{
alert(result.message);
}
}
}
31 二级路由
1.routes.js
中配置(注意默认重定向)
- 组件中引入
32 图片懒加载
进度条nprogress
,二维码qrcode
图片懒加载vue-lazyload
官网
// 引入懒加载 插件
import VueLazyload from 'vue-lazyload'
//引入图片
import atm from '@/assets/1.gif'
// 注册插件
Vue.use(VueLazyload, {
// 懒加载默认的图片
loading: atm,
})
使用
<img v-lazy="good.defaultImg" />
33 表单验证
1.vee-validate
插件vee-validate
插件:Vue 官方提供的一个表单验证的插件【
这个插件很难用:如果你翻看它的文档(看一个月:不保证能看懂),依赖文件很多(文档书写的很难理解)
花大量时间学习,很难搞懂。
哪怕将来工作了,真的使用 vee-validate【老师项目搞出来:改老师代码即可】
第一步:插件安装与引入
cnpm i vee-validate@2 --save 安装的插件安装2版本的
import VeeValidate from 'vee-validate'
import zh_CN from 'vee-validate/dist/locale/zh_CN' // 引入中文 message
Vue.use(VeeValidate)
第二步:提示信息
VeeValidate.Validator.localize('zh_CN', {
messages: {
...zh_CN.messages,
is: (field) => `${field}必须与密码相同` // 修改内置规则的 message,让确认密码和密码相同
},
attributes: { // 给校验的 field 属性名映射中文名称
phone: '手机号',
code: '验证码',
password:'密码',
password1:'确认密码',
isCheck:'协议'
}
})
<span class="error-msg">{
{ errors.first("phone") }}</span>
第三步:基本使用
<input
placeholder="请输入你的手机号"
v-model="phone"
v-validate="{ required: true, regex: /^1\d{10}$/ }"
:class="{ invalid: errors.has('phone') }"
/>
<span class="error-msg">{
{ errors.first("phone") }}</span>
const success = await this.$validator.validateAll(); //全部表单验证
//自定义校验规则
//定义协议必须打勾同意
VeeValidate.Validator.extend('agree', {
validate: value => {
return value
},
getMessage: field => field + '必须同意'
})
34 路由懒加载
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
路由懒加载链接
我还是喜欢下面的写法,不太喜欢官网的写法。
写成import("@/pages/Home"),
代码示例:
{
path:"/home",
component:()=>import("@/pages/Home"),
meta:{show:true}
},
35 打包项目
项目到此基本就完成了,接下来就是打包上线。在项目文件夹下执行npm run build
。会生成dist
打包文件。
dist 文件下的 js 文件存放我们所有的 js 文件,并且经过了加密,并且还会生成对应的map
文件。
map 文件作用:因为代码是经过加密的,如果运行时报错,输出的错误信息无法准确得知时那里的代码报错。有了 map 就可以向未加密的代码一样,准确的输出是哪一行那一列有错。
当然 map 文件也可以去除(map 文件大小还是比较大的)
在 vue.config.js 配置productionSourceMap: false
即可。
注意:vue.config.js 配置改变,需要重启项目
36 购买服务器
- 阿里云 腾讯云等
- 设置安全组,让服务器一些端口打开
- 利用 xshell 登录服务器
cd 打开,ls 查看,mkdir 创建目录,pwd 查看绝对路径
37 nginx 反向代理
Nginx 是一个很强大的高性能 Web 和反向代理服务,它具有很多非常优越的特性
nginx 配置
- xshell 进入根目录 / etc
- 进入 etc 目录,这个目录下有一个 nigix 目录,进入这个目录
- 如果要安装 nginx :sudo yum install nginx
- 安装完 nginx 服务器以后,在 nginx 目录下,多了一个 niginx.conf 文件,在这个文件中 进行配置
- vim ngnx.conf 进入编辑
location / {
root /root/jch/www/shangpihui/dist;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api{
proxy_pass htp://39.98.123.211;
}
- nginx 服务器跑起来
service nigix start