【尚品汇】项目笔记
-
1、vue文件目录分析
-
2 项目配置
-
3 路由组件
-
4 footer组件显示与隐藏
-
5 封装axios
-
6 接口统一管理
-
7 nprogress进度条插件
-
8 vuex
-
9 给一级菜单加颜色
-
10 loadsh插件防抖和节流(*重要)
-
11 编程式导航+事件委托实现路由跳转
-
11 TypeNav在Search上隐藏&过渡动画
-
12 Vue路由销毁问题-性能优化
-
13 params和query参数合并
-
14 mock插件使用
-
15 vuex数据存储与使用
-
16 swiper插件实现轮播图
-
17 开发Floor组件–父子通信
-
18 将轮播图模块提取为公共组件
-
19 getters使用
-
20 Object.asign实现对象拷贝
-
21 面包屑相关操作
-
22 商品排序
-
23 分页功能-全局组件
-
24 商品详情页-路由组件
-
25 购物车相关路由
-
26 登录注册
-
27 导航守卫
-
28 交易页面
-
29 全局引入api 类似于`$bus`
-
30 提交订单(直接在组件中发请求)
-
31 二级路由
-
32 图片懒加载
-
33 表单验证
-
34 路由懒加载
-
35 打包项目
-
36 购买服务器
-
37 nginx反向代理
-
38 `Event`相关
-
39 `mixin`混入
-
40 插槽
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翻译成es5
package.json
:看到项目描述、项目依赖、项目运行指令
package-lock.json
: 缓存性文件(各种包的来源)
README.md
:项目说明文件
2 项目配置
-
项目运行,浏览器自动打开
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"
},
-
关闭eslint校验工具、开启自动刷新
(不关闭会有各种规范,不按照规范就会报错)
vue.config.js
,进行配置
module.exports = {
//默认打开地址http://localhost:8080/
devServer: {
host: 'localhost',
port: 8080,
},
//关闭eslint
lintOnSave: false
}
-
src文件夹配置别名,
jsconfig.json
,用
@/
代替
src/
,
exclude
表示不可以使用该别名的文件
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": [
"src/*"
]
}
},
"exclude": [
"node_modules",
"dist"
]
}
-
路由的配置
2个非路由组件,四个路由组件
两个非路由组件:
Header
【首页、搜索页】
Footer
【在首页、搜索页】在登录页是没有的
路由组件:
Home
、
Search
、
Login
(没有底部的Footer组件,带有二维码的)、
Register
(没有底部的Footer组件,带二维码的)
开发一个前端模块可以概括为以下几个步骤:
(1)写静态页面、拆分为静态组件;
(2)发请求(API);
(3)vuex(actions、mutations、state三连操作);
(4)组件获取仓库数据,动态展示;
-
组件页面样式
组件页面的样式使用的是less样式,浏览器不识别该样式,需要下载相关依赖
npm install --save less less-loader@5
如果想让组件识别less样式,则在组件中设置
<script scoped lang="less">
-
清除vue页面默认的样式
vue是单页面开发,我们只需要修改public下的index.html文件
<link rel="stylesheet" href="reset.css">
-
写
footer
组件和
header
非路由组件
非路由组件使用分为几步:
第一步:定义
footer/index.vue
第二步:引入
import myHeader from "./components/Header"
第三步:注册
components: { myHeader}
第四步:使用
<my-header></my-header>
-
安装
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理解为一个a标签,它 也可以加class修饰 - 编程式导航 :声明式导航能做的编程式【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
两个属性可以传递参数
query
参数:不属于路径当中的一部分,类似于get请求,地址栏表现为
/search?k1=v1&k2=v2
query
参数对应的路由信息
path: "/search"
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:''}})
出现的问题和1中的问题相同,地址信息少了/search
解决方法: 加入||undefined,当我们传递的参数为空串时地址栏url也可以保持正常
this.$router.push({name:"Search",query:{keyword:this.keyword},params:{keyword:''||undefined}})
面试题4 路由组件能不能传递props数据
可以,而且有三种写法
//布尔写法
props:true,
// 对象写法
props:{a:1,b:2}
// 函数写法:可以params参数,query参数,通过props传递给路由组件
props:($route)=>{
return {keyword:$route.params.keyword,k:$route.query.k};
}
接收props参数
props:['keyword','k']
params
传参问题
(1)、如何指定params参数可传可不传
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文档:
https://www.kancloud.cn/yunye/axios/234845
安装:
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)=>{
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
})
-
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中的数据
- 例如在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');
}
}
}
-
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、甚至是嵌套子模块——从上至下进行同样方式的分割:
-
在
store
中分别建立
home/index.js
和
search/index.js
-
search/index.js
代码如下:
// search的小仓库
const state ={};
const mutations ={};
const actions={};
const getters={};
export default{
state,
mutations,
actions,
getters
}
-
在
store/index.js
中引入两个仓库,并注册
// 引入小仓库
import home from './home'
import search from "./search";
// 对外暴露Store类的一个实例
export default new Vuex.Store({
# 实现Vuex仓库模块化开发存储数据
modules:{
home,
search
}
})
全局组件放在components下,在mian.js中引入
- 使用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
},
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-categoryName="c1.categoryName" :data-category1Id="c1.categoryId">{{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-categoryName="c2.categoryName" :data-category2Id="c2.categoryId">{{c2.categoryName}}</a>
</dt>
<dd>
<em v-for="c3 in c2.categoryChild" :key="c3.categoryId">
<a :data-categoryName="c3.categoryName" :data-category3Id="c3.categoryId">{{c3.categoryName}}
我们可以通过在函数中传入
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命令,才能进行过渡动画
- 挂载的时侯,不是home就不显示
mounted(){
// 通知Vuex发请求,获取数据,存储在仓库中
this.$store.dispatch('categoryList')
// console.log('挂载完毕',state)
// 如果路由不是home,将typeNav隐藏
if(this.$route.path!='/home'){
this.show = false;
}
},
-
search鼠标进入显示三级菜单,离开的隐藏
// 当鼠标离开的时候,让商品分类隐藏
leaveshow(){
this.currentIndex=-1;
if(this.$route.path!='/home'){
this.show = false;
}
},
// 鼠标进入的时候,显示
entershow(){
if(this.$route.path!='/home'){
this.show = true;
}
}
- 过渡动画的样式
.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()
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(stata){ //形参state是当前仓库中的state,
// 如果服务器数据回来了,是一个数组,假如网络不稳定,goodsList会变成undefined,以防万一,要加||[]
return stata.searchList.goodsList||[];
},
attrsList(stata){
return stata.searchList.attrsList||[];
},
trademarkList(stata){
return stata.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:目标对象】,【souce:源对象(可多个)】
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
的使用
:
1.在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')
2.在当前
search/index.vue
中用
$bus.$emit
通知兄弟组件
第一个参数可以理解为为通信的
暗号
,还可以有第二个参数(用于传递数据),我们这里只是用于通知header组件进行相应操作,所以没有设置第二个参数。
// 删除关键字
removeKeyword(){
// 给服务器的searchParams中的keyword置空
this.searchParams.keyword=undefined;
this.getData();
# 通知兄弟组件header清除关键字
this.$bus.$emit("clear");
}
3.在兄弟组件
Header/index.vue
中用
$bus.$on
接收通信
在挂载的时候就监听到,把keyword的值置为空
mounted(){
// 通过全局事件总线清除关键字
// 组件挂载时就监听clear事件,clear事件在search模块中定义
// 当删除关键字面包屑时,触发该事件,同时header的输入框绑定的keyword要删除
this.$bus.$on("clear",()=>{
this.keyword="";
})
},
3.子父组件通信
SearchSelector
子组件向父组件传参,实现面包屑操作
1. 父组件添加自定义事件
trademarkInfo
<!--selector 添加自定义事件-->
<SearchSelector @trademarkInfo="trademarkInfo"/>
2. 父组件自定义事件回调
methods:{
// 自定义事件回调
trademarkInfo(trademark){
// console.log(trademark);
// 整理参数,形式 示例: "1:苹果"
this.searchParams.trademark=`${trademark.tmId}:${trademark.tmName}`;
// 再次发请求
this.getData();
}
}
3. 子组件触发
$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 商品排序
-
在public文件
index.html
引入该css
<link rel="stylesheet" href="https://at.alicdn.com/t/font_2994457_qqwrvmss9l9.css">
-
点击事件,把当前的
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>
-
点击事件,若是当前的
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>
- 分页逻辑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};
}
}
};
- 然后把当前的页码给父组件传过去,重新发请求获取数据
// 自定义事件的回调函数,获取当前第几页
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}
},
]
})
-
从
search
中跳转到
detail
页,带参数
<router-link :to="`/detail/${good.id}`">
<img :src="good.defaultImg" />
</router-link>
- 每次跳转控制滚动条在最上方
//对外暴露VueRouter类的实例
let router = new VueRouter({
//配置路由
//第一:路径的前面需要有/(不是二级路由)
//路径中单词都是小写的
//component右侧V别给我加单引号【字符串:组件是对象(VueComponent类的实例)】
routes,
//滚动行为
scrollBehavior(to, from, savedPosition) {
//返回的这个y=0,代表的滚动条在最上方
return { y: 0 };
},
});
- 在api文件中配置发送请求的
// 获取商品详情信息的接口: url: /api/item/{ skuId },get请求,参数:skuId,必须要
export const reqGoodInfo = (skuid)=> requests({
url:`/item/${skuId}`,
method:"get",
})
-
在
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 实现点击的属性为高亮
-
绑定方法,传入
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>
- 把其余设置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" name="chk_list"
: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
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
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
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
$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"
name="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
38
Event
相关
Event
在vue中标签可以分为两类:
(1)原生DOM。
<input><button>
等。
(2)自定义组件
<button @click="handle" :msg="msg"></button>
<event1 @click.native="handle"><event1>
这里的
@click
是原生DOM事件,也就是我们传统的点击触发事件。
这里的
:msg
就是绑定组件中的变量。
event1组件:非原生DOM节点,而绑定的click事件并非原生DOM事件,而是自定义事件 @click.native 可以把自定义事件变成原生DOM事件,当原生DOM click事件,其实是给子组件的根节点绑定了点击事件—-利用事件委派
自定义事件
<Event2 @xxx="handler3"></Event2>
<button @click="$emit('xxx','自定义事件')">分发自定义事件xxx</button>
39
mixin
混入
mixin
Vue.mixin
可以对js代码复用
40 插槽
插槽也是可以用来传数据的
子组件HintButton
<template>
<div>
<slot :item1="{'a':1,'b':2}" item2="asd1">e了吗</slot>
</div>
</template>
父组件
<template>
<div>
<HintButton title="提示" icon="el-icon-delete" type="danger" @click="handler">
<template v-slot:default="slopProps" >
<p>{{slopProps}}</p>
<p>{{slopProps.item1}}</p>
<p v-for="(item,index) in slopProps.item1">{{index}}----{{item}}</p>
</template>
</HintButton>
</div>
</template>
插槽的原理就是在子组件(HintButton)内定义一个slot(插槽),父组件可以向该插槽内插入数据。
父组件向子组件传递信息还是通过props传递,这里就不多做赘述。 子组件想父组件传递信息时可以通过插槽传递。
(1)在子组件HintButton的slot内绑定要传递的数据。
(2) 父组件通过v-slot:default="slotProps"可以接收到全部的信息。
箭头所指内容就是子组件通过插槽传递给父组件的信息。接受的数据是键值对的形式。