Vue2进阶
vue.config.js中关闭eslint校验
添加第四行
1 | 1. const { defineConfig } = require("@vue/cli-service"); |
安装less以及less-loader
- less-loader指定版本5
1 | npm install --save less less-loader@5 |
- npm版本太高,安装报错
1 | npm install npm@6.14.10 -g // 改为低版本 |
清除默认样式
在public的index.html中引入reset.css
1 | <!-- 引入清除默认的样式 --> |
安装配置路由
安装路由插件
1
npm install --save vue-router
报错:export ‘default’ (imported as ‘VueRouter’) was not found in ‘vue-router’
1
"vue-router": "^4.0.15"
原因:版本过高
指定版本安装
1
npm i vue-router@3.1.3
配置路由
创建router文件夹以及index.js文件
配置index.js文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import Vue from "vue";
import VueRouter from "vue-router";
// 使用插件
Vue.use(VueRouter);
// 引入路由组件
import Home from "@/pages/Home";
// 配置路由
export default new VueRouter({
routes: [
{
path: "/home",
component: Home,
meta: { showFooter: true },
},
]
})配置main.js
1
2
3
4
5
6
7
8
9
10
11
12import Vue from "vue";
import App from "./App.vue";
Vue.config.productionTip = false;
// 引入路由
import router from "@/router";
new Vue({
render: (h) => h(App),
// 注册路由
router,
}).$mount("#app");
$route和$router差别
$route:一般获取路由信息,路径、query、params、meta元信息等
$router:一般进行编程式导航进行路由跳转push、replace
meta配置路由元信息showFooter,判断是否需要显示Footer组件
1 | // 配置路由 |
1 | <!-- 在登录、注册页面隐藏,在首页、搜索页面显示 --> |
路由传参
params:属于路径中的一部分,在配置路由的时候,需要占位
1
path: "/search/:keyWord", // keyWord占位
query:不属于路径中的一部分,不需要占位
props
params、query传参形式:
字符串
1
this.$router.push("/search/" + this.keyWord + "?k=" + this.keyWord);
模板字符串
1
this.$router.push(`/search/${this.keyWord}?k=${this.keyWord}`);
对象
1
2
3
4
5
6{
path: "/search/:keyWord",
component: Search,
meta: { showFooter: true },
name: "search", // 需先给路由起别名
},注意:1
2
3
4
5
6
7
8
9this.$router.push({
name:"search",
params:{
keyWord:this.keyWord
},
query:{
k:this.keyWord
}
});对象的写法可以有name或path,但path不能和params参数一起使用。
params传参问题:
- 如果已经占位了(params参数),但是传参时没有传递params参数,URL会出现问题。
1
2
3
4
5
6
7
8
9this.$router.push({
name: "search",
// params: {
// keyWord: "",
// },
query: {
k: this.keyWord,
},
});问题:
1
2
3
4// 正常
http://localhost:8080/#/search/哎哎哎?k=哎哎哎
// 问题
http://localhost:8080/#/?k=哎哎哎解决:
1
path: "/search/:keyWord?", // 在占位符后面加上?即可
- 如果已经在占位符后面加了?,但是传递的params参数值为空串,会出现同样的URL错误
1
2
3
4
5
6
7
8
9this.$router.push({
name: "search",
params: {
keyWord: "",
},
query: {
k: this.keyWord,
},
});解决:
1
2
3params: {
keyWord: "" || undefined,
},
props传参:
方式一:布尔值写法
1
2
3
4
5
6
7{
path: "/search/:keyWord?",
component: Search,
meta: { showFooter: true },
name: "search",
props: true, // 需设置成true 注意:props接收的是params参数
},方式二:对象写法
1
2
3
4
5
6
7
8
9{
path: "/search/:keyWord?",
component: Search,
meta: { showFooter: true },
name: "search",
props: {
keyWord: "111", // 这时keyWord不是params中的参数,而是自定义的
}
},方式三:函数写法
1
2
3
4
5
6
7
8
9
10
11
12{
path: "/search/:keyWord?",
component: Search,
meta: { showFooter: true },
name: "search",
props: ($route) => { // 可以传递params以及query参数
return {
keyWord: $route.params.keyWord,
k: $route.query.k,
};
},
},
参数接收
1
2
3
4
5
6
7
8
9
10
11
12
13
14<template>
<div>
<h1>params参数:{{ $route.params.keyWord }}</h1>
<h1>query参数:{{ $route.query.k }}</h1>
<h1>props参数1:{{ keyWord }}</h1>
<h1>props参数2:{{ k }}</h1>
</div>
</template>
<script>
export default {
props: ["keyWord", "k"],
};
</script>
注册全局组件
main.js
1 | // 注册三级联动全局组件 |
Postman测试接口
安装axios及配置
安装
1
npm install --save axios
二次封装
创建api文件夹以及request.js
二次封装
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32// 对axios二次封装
import axios from "axios";
// 利用axios中的create方法,创建一个axios实例
const requests = axios.create({
// 路径中自动加上/api
baseURL: "/api",
// 请求超时时间
timeout: 5000,
});
// 请求拦截器(请求发送前做处理)
requests.interceptors.request.use((config) => {
// config:header请求头等属性
return config;
});
// 响应拦截器
requests.interceptors.response.use(
(res) => {
// 成功回调
return res.data;
},
(error) => {
// 失败回调
return Promise.reject(new Error("fail"));
}
);
// 暴露
export default requests;
接口统一管理
使用多个axios相同请求时,可对这些请求进行统一管理,当请求地址改变时便于维护。
在api文件夹新建index.js
1
2
3
4
5
6
7
8
9
10
11
12// API统一管理
import requests from "./request";
// 三级联动接口
// /api/product/getBaseCategoryList get 无参数
export const reqCategoryList = () => {
// 发请求
return requests({
url: "/product/getBaseCategoryList", // 无需加上api,对axios已进行封装
method: "get",
});
};接口调用
1
2import { reqCategoryList } from "@/api";
let data = reqCategoryList();
解决跨域
跨域:协议、域名、端口号不同的请求
配置代理服务器
1
2
3
4
5
6
7
8
9
10
11
12
13
14const { defineConfig } = require("@vue/cli-service");
module.exports = defineConfig({
transpileDependencies: true,
lintOnSave: false, // 关闭eslint
// 代理服务器
devServer: {
proxy: {
"/api": {
target: "http://gmall-h5-api.atguigu.cn",
// pathRewrite: { "^/api": "" },
},
},
},
});
nprogress进度条的使用
可视化请求发送与接收
安装
1
npm install --save nprogress
配置在request.js中的请求、响应拦截器中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23// 引入进度条
import nprogress from "nprogress";
// 引入进度条样式
import "nprogress/nprogress.css";
// 请求拦截器
requests.interceptors.request.use((config) => {
// 进度条开始
nprogress.start();
return config;
});
// 响应拦截器
requests.interceptors.response.use(
(res) => {
// 进度条结束
nprogress.done();
return res.data;
},
(error) => {
return Promise.reject(new Error("fail"));
}
);更改样式
可在node_modules中更改nprogress.css
vuex状态管理库
vuex是官方提供的插件,集中式管理项目中组件公用的数据。
当项目的数据维护费劲时,就可以引入vuex进行管理。
state:仓库存储数据的地方
mutations:修改state的唯一手段
actions:业务逻辑、异步等
getters:state的计算属性
modules:模块化
安装
1
npm install --save vuex
警告
版本过高
1
npm install vuex@3.4.0 // 安装低版本
配置仓库
创建store文件夹及index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
const state = {
count: 1,
};
const mutations = {
ADD(state) {
state.count++;
},
};
const actions = {
// 可以书写业务逻辑,不能修改直接state
add({ commit }) {
commit("ADD");
},
};
const getters = {};
// 暴露store类的实例
export default new Vuex.Store({
state,
mutations,
actions,
getters,
});main.js注册仓库
1
2
3
4
5
6
7
8
9// 引入仓库
import store from "@/store";
new Vue({
render: (h) => h(App),
router,
// 注册仓库
store,
}).$mount("#app");案例
1
2<span>{{ count }}</span>
<button @click="add">点击加1</button>1
2
3
4
5
6
7
8
9
10
11import { mapState } from "vuex";
computed: { // 计算属性获取state中的值
...mapState(["count"]), // 数组写法
},
methods: {
add() {
//派发action
this.$store.dispatch("add");
},
},获取state值的对象写法
1
2
3
4
5
6
7computed: {
...mapState({
categoryList: (state) => {
return state.count;
},
}),
},简写:
1
2
3
4
5computed: {
...mapState({
categoryList: (state) => state.count
}),
},
模块式开发
即在大仓库里封装多个小仓库
创建home、search两个小仓库
配置小仓库
1
2
3
4
5
6
7
8
9
10
11
12
13// home模块小仓库
const state = {};
const mutations = {};
const actions = {};
const getters = {};
export default {
state,
mutations,
actions,
getters,
};1
2
3
4
5
6
7
8
9
10
11
12
13// search模块小仓库
const state = {};
const mutations = {};
const actions = {};
const getters = {};
export default {
state,
mutations,
actions,
getters,
};大仓库引入小仓库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
// 引入小仓库
import home from "./home";
import search from "./search";
// 暴露store类的实例
export default new Vuex.Store({
modules: {
home,
search,
},
});
动态样式
思路:鼠标悬停在哪个一级菜单上,将currentIndex改成对应索引值index。
1 | <h3 @mouseenter="changeCurrentIndex(index)" @mouseleave="changeCurrentIndex(-1)"> // 鼠标移进及移出事件 |
1 | data() { |
1 | <div |
1 | .cur{ |
上面仅仅为了练习动态样式,更简便的方式:
1 | .item:hover{ |
防抖与节流+Lodash引入
防抖节流
函数防抖
如果连续的快速触发函数,则当连续动作结束之后,经过规定的时间才会触发且只会触发一次。
函数节流
设置两次函数触发之间的最小时间间隔。
引入Lodash
Lodash 是一个一致性、模块化、高性能的 JavaScript 实用工具库。
防抖:
1 | _.debounce(function(){ |
节流:
1 | _.throttle(function(){ |
安装:(正常vue项目本身就有lodash存在,因为有些依赖依赖着lodash,如果没有在控制台再进行安装)
引入方式:
1 | // 引入lodash所有功能函数 |
1 | // 按需引入 |
vue中写法:
1 | //节流(防抖同理) |
声明式导航、编程式导航的选择问题
声明式导航:router-link
编程式导航:push|replace
注意:router-link是一个组件,当页面中多处地方使用router-link时,很耗内存。
如果在a标签上都添加单击事件进行编程式导航,页面会加载多个回调函数,也不是最理想的办法。
最好方法:编程式导航 + 事件委派1 | <div @click="goSearch"> |
如何确定点击的是哪个a标签以及是否是a标签?
自定义属性
1
2
3
4
5
6
7<!-- 1.data-一定要加 2.解析出来的属性名为data-后面的值且全都是小写 -->
<div @click="goSearch">
<a :data-index="index1"></a>
<h1></h1>
<a :data-index="index2"></a>
<a :data-index="index3"></a>
</div>event
1
2
3
4
5
6
7goSearch(event) {
let ele = event.target;
let { index } = ele.dataset; // 返回对象,所以得解析对象里的index出来
if (index) {
console.log(index);
}
},
过渡动画
前提
组件或元素务必要有v-if或v-show指令才可以进行过渡动画。
给组件或元素包上transition标签
1
2
3
4
5
6<!-- 过渡动画 -->
<transition name="sort">
<div class="sort" v-show="showSort">
...
</div>
</transition>样式
1
2
3
4
5
6
7
8
9
10
11
12
13//sort过渡动画样式
// 开始状态(进入)
.sort-enter {
height: 0px;
}
// 结束状态 (进入)
.sort-enter-to {
height: 461px;
}
// 定义动画时间、速率
.sort-enter-active {
transition: all 0.5s linear;
}
优化全局组件
问题:
TypeNav全局组件被多个组件使用,而TypeNav挂载完毕会向服务器请求数据,这样导致多次向服务器请求相同的数据。
TypeNav原本挂载后执行请求获取数据的语句
1
2// 通知vuex发送请求,获取商品列表数据,存储于仓库中
this.$store.dispatch("categoryList");将语句移到App.vue的挂载函数里头,因为app组件为跟组件,只会挂载一次
mockjs模拟数据
作用:生成随机数据,拦截 Ajax 请求
安装
1
npm install mockjs
使用
src中创建mock文件夹
准备JSON数据(mock文件夹中创建相应的JSON文件)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18[
{
"id":"1",
"imgUrl":"/images/banner1.jpg"
},
{
"id":"2",
"imgUrl":"/images/banner2.jpg"
},
{
"id":"3",
"imgUrl":"/images/banner3.jpg"
},
{
"id":"4",
"imgUrl":"/images/banner4.jpg"
}
]注意: webpack默认对外暴露:图片、JSON,所以这里无需对外暴露。
把mock数据需要的图片放在public文件夹中
创建mockServer.js
使用mockjs插件实现模拟数据
1
2
3
4
5
6
7
8
9// 引入mockjs
import Mock from "mockjs";
// 引入JSON数据
import banner from "./banner.json";
import floor from "./floor.json";
// mock数据:1、请求的地址 2、请求数据
Mock.mock("/mock/banner",{code:200,data:banner}); // 模拟首页轮播图数据
Mock.mock("/mock/floor",{code:200,data:floor}); // 模拟楼层数据在main.js(入口文件)中引入mockServer.js
1
2// 引入mockServer.js
import "@/mock/mockServer";
swiper使用(watch + nextTick)
安装
1
npm install --save swiper@5
使用
需要用到的组件引包
1
import Swiper from 'swiper';
main.js中引入样式
1
2// 引入swiper样式
import "swiper/css/swiper.css";页面结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<!--banner轮播-->
<div class="swiper-container" id="mySwiper">
<div class="swiper-wrapper">
<div
class="swiper-slide"
v-for="carousel in bannerList"
:key="carousel.id"
>
<img :src="carousel.imgUrl" />
</div>
</div>
<!-- 如果需要分页器 -->
<div class="swiper-pagination"></div>
<!-- 如果需要导航按钮 -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
</div>new Swiper实例
1
2
3
4
5
6
7
8
9
10
11
12
13let mySwiper = new Swiper(".swiper-container",{
loop:true,
// 分页器
pagination:{
el:".swiper-pagination",
clickable:true
},
// 前进后退按钮
navigation:{
nextEl:".swiper-button-next",
prevEl:".swiper-button-prev"
}
});注意:Swiper实例必须在页面中数据全部存在(遍历完)后new
问题:
这里不能在mouted中new Swiper,因为mouted中要发送ajax请求获取需要遍历的数据,而异步请求时数据还没获取到就new Swiper实例会导致轮播不能正常使用。可以在mouted ajax请求语句底下写一个定时器去new Swiper实例,但是这个解决办法不是完美的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14mounted() {
// 获取轮播图数据
this.$store.dispatch("getBannerList");
// new Swiper实例
setTimeout(() => {
new Swiper实例 // 如果这里没有发送ajax请求获取所需数据,则可以直接在mouted中new实例
}, 1000);
},
computed: {
...mapState({
// 拿到bannerList
bannerList: (state) => state.home.bannerList,
}),
},解决办法:watch + nextTick
如果单纯使用watch监听bannerList也不行,因为当handler执行只能保证数据有了,但不能保证v-for已经执行结束了。
1
2
3
4
5
6
7watch:{
bannerList:{
handler(newValue,oldValue){
new Swiper实例
}
}
}所以需要引入nextTick:在下次DOM更新循环结束之后执行延迟回调(相当于定时器,但是更精准)。在修改数据之后立即使用这个方法,获取更新之后的DOM。这样就可以保证v-for遍历结束,即页面中的需要的结构已经有了。
1
2
3
4
5
6
7
8
9
10watch:{
bannerList:{
handler(newValue,oldValue){
this.$nextTick(()=>{
// 当执行的时候,保证数据已经有了,并且v-for已经遍历完了。
new Swiper实例
})
}
}
}小细节
将 id=”mySwiper” 改成 ref=”mySwiper”
new Swiper(“.swiper-container”,{…}); 改成 new Swiper(this.$refs.mySwiper,{…});
父组件获取子组件实例
1 | <Event1 ref="xm" /> |
则 this.$refs.xm 相当于子组件的 this
组件通信
props:父向子组件通信
注意1:当子组件接收到父组件props参数时,watch是监听不到的。
解决:使用watch立即监听属性
1
2
3
4
5
6
7
8
9props: ['floor'],
watch: {
floor: {
immediate: true, // 立即监听
handler() {
console.log("sdf");
},
},
},注意2:如果父组件给子组件传递的是函数,本质就是子组件给父组件传递数据
三种书写方式
数组:[“name”]
对象:{type:String}
对象:{type:String,default:”aaa”}
自定义事件:$on、$emit 可以实现子向父组件通信
使用
1
<Event2 @click="handler2" @xxx="handler3" />
1
2
3
4
5
6
7
8
9<div>
<h2>Event2组件</h2>
<button @click="$emit('click', '触发自定义事件click')">
分发自定义click事件
</button>
<button @click="$emit('xxx', '触发自定义事件xxx')">
分发自定义xxx事件
</button>
</div>深入
问题1:如何给组件标签绑定系统事件?
1
2
3
4
5<!-- 原生DOM绑定系统事件 -->
<button @click="handler">原生</button>
<!-- Event1组件:不是原生DOM节点,绑定的click事件被认为是自定义事件,这时点击Event1组件并不会触发handler1函数 -->
<!-- 只有子组件Event1 $emit触发该事件handler1才会被调用 -->
<Event1 @click="handler1" />解决:加上.native,结果相当于给子组件Event1的根节点绑定了点击事件
1
2<button @click="handler">原生</button>
<Event1 @click.native="handler1" />问题2:如何给原生DOM绑定自定义事件?
解决:没法解决。
全局事件总线:$bus 全能
pubsub-js(发布于订阅):React中使用较多,vue当中几乎不用 全能
插槽:父子之间的通信,一般通信的是结构(即标签)
默认插槽
具名插槽
作用域插槽:子组件数据来源于父组件,子组件决定不了自身结构与外观
场景
实现
父组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42<template>
<div>
<h2>效果一:显示todo列表时,已完成todo为绿色</h2>
<List :todos="todos">
<!-- 接收子组件回传的数据 -->
<template slot-scope="data">
<!-- 给子组件传递的结构 -->
<span :style="{ color: data.todo.isComplete ? 'green' : 'red' }">
{{ data.todo.text }}
</span>
</template>
</List>
<hr />
<h2>效果二:显示todo列表时,带序号,todo颜色为蓝绿搭配</h2>
<List2 :todos="todos">
<!-- 也可以解构 -->
<template slot-scope="{ todo, index }">
<span :style="{ color: todo.isComplete ? 'green' : 'blue' }">
{{ index }}---{{ todo.text }}
</span>
</template>
</List2>
</div>
</template>
<script>
import List from "./List";
import List2 from "./List2";
export default {
components: { List, List2 },
data() {
return {
todos: [
{ id: 1, text: "AAA", isComplete: false },
{ id: 2, text: "BBB", isComplete: true },
{ id: 3, text: "CCC", isComplete: false },
{ id: 4, text: "DDD", isComplete: false },
],
};
},
};
</script>子组件1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<template>
<div style="background: #ccc">
<ul>
<li v-for="(item, index) in todos" :key="index">
<!-- 作用域插槽(将子组件的数据回传给父组件) -->
<slot :todo="item"></slot>
</li>
</ul>
</div>
</template>
<script>
export default {
props: ["todos"],
};
</script>子组件2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<template>
<div style="background: #ccc">
<ul>
<li v-for="(item, index) in todos" :key="index">
<!-- 作用域插槽(将子组件的数据回传给父组件) -->
<slot :todo="item" :index="index"></slot>
</li>
</ul>
</div>
</template>
<script>
export default {
props: ["todos"],
};
</script>
vuex:万能
v-model:父子组件数据同步
v-model实现原理:通过单向绑定+oninput事件(value改变)实现双向绑定
1
<input type="text" :value="msg" @input="msg = $event.target.value" />
实现父子组件数据互通
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26<template>
<div>
<h1>深入v-model</h1>
<input type="text" v-model="msg" />
<Event1 v-model="msg" />
<!-- 相当于 -->
<!-- <Event1 :value="msg" @input="msg = $event" />
:value 则不是单向绑定,而是props传参,子组件得接收value参数
@input 则不是改变事件,而是自定义事件,子组件通过$emit调用input事件,$event获取子组件传递的参数
注意:在原生事件中,$event是事件对象,在自定义事件中,$event是传递过来的数据(参数)
-->
</div>
</template>
<script>
import Event1 from "./Event1";
export default {
components: { Event1 },
data() {
return {
msg: "",
};
},
};
</script>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<template>
<div style="background: #ccc">
<h2>Event1组件</h2>
<input
type="text"
:value="value"
@input="$emit('input', $event.target.value)"
/>
</div>
</template>
<script>
export default {
props: ["value"],
};
</script>属性修饰符sync:父子组件数据同步
1
2<!-- 父组件给子组件传递了props money 并给子组件绑定了一个自定义事件,事件名称为 update:money 回调函数体:money = $event -->
<Event1 :money.sync="money" />1
2
3
4
5
6
7<button @click="$emit('update:money', money - 100)">花100元</button>
<span>还剩{{ money }}元</span>
export default {
props: ["money"],
};$attrs与$listeners:可以获取到父组件给子组件传递的props参数、自定义事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23<template>
<div>
<HintButton
type="success"
icon="el-icon-delete"
size="mini"
title="提示按钮"
@click="handler"
/>
</div>
</template>
<script>
import HintButton from "./HintButton";
export default {
components: { HintButton },
methods: {
handler() {
alert("点击");
},
},
};
</script>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24<template>
<div>
<a :title="title">
<el-button :type="$attrs.type" :icon="$attrs.icon" :size="$attrs.size">
添加
</el-button>
<!-- 或 -->
<el-button v-bind="$attrs" v-on="$listeners">添加</el-button>
</a>
</div>
</template>
<script>
export default {
props: ["title"],
mounted() {
// $attrs属于组件的一个属性,可以获取到父组件传递过来的props数据
// 如果props某个参数被接收了,则$attrs就不会有被接收的参数
console.log(this.$attrs);
// $listeners获取父组件给子组件的自定义事件
console.log(this.$listeners);
},
};
</script>$children与$parent(包含混入mixin的使用)
场景:
父组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45<template>
<div>
<h2>安安,存款{{ money }}</h2>
<button @click="jqFromXM(100)">找小明借100元</button>
<button @click="jqFromXH(150)">找小红借150元</button>
<button @click="jqFromAll(200)">找两个人都借200元</button>
<hr />
<Event1 ref="xm" />
<Event2 ref="xh" />
</div>
</template>
<script>
import Event1 from "./Event1";
import Event2 from "./Event2";
export default {
components: { Event1, Event2 },
data() {
return {
money: 1000,
};
},
methods: {
jqFromXM(val) {
this.money += val;
this.$refs.xm.money -= val;
},
jqFromXH(val) {
this.money += val;
this.$refs.xh.money -= val;
},
jqFromAll(val) {
this.money += 2 * val;
// this.$refs.xm.money -= val;
// this.$refs.xh.money -= val;
// 或者
this.$children.forEach((item) => { // $children:可以获取到当前组件全部子组件
item.money -= val;
});
// 注意:this.$children[0] 别这样书写,如果组件过多,第几项是哪个组件是不确定的
},
},
};
</script>子组件之一、另一同理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<template>
<div style="background: #ccc">
<h2>小明,存款{{ money }}元</h2>
<button @click="give(50)">给安安:50</button>
</div>
</template>
<script>
import myMixin from "../myMixin/myMixin";
export default {
mixins: [myMixin],
data() {
return {
money: 30000,
};
},
};
</script>mixin
1
2
3
4
5
6
7
8
9export default {
// 可以放置组件重复的JS业务逻辑
methods: {
give(val) {
this.money -= val;
this.$parent.money += val;
},
},
};
共用组件carousel
carousel组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49<template>
<div class="swiper-container" ref="mySwiper">
<div class="swiper-wrapper">
<div
class="swiper-slide"
v-for="carousel in carouselList"
:key="carousel.id"
>
<img :src="carousel.imgUrl" />
</div>
</div>
<!-- 如果需要分页器 -->
<div class="swiper-pagination"></div>
<!-- 如果需要导航按钮 -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
</div>
</template>
<script>
import Swiper from "swiper";
export default {
name: "Carousel",
props: ["carouselList"],
watch: {
carouselList: {
immediate: true,
handler() {
this.$nextTick(() => {
let mySwiper = new Swiper(this.$refs.mySwiper, {
loop: true,
// 分页器
pagination: {
el: ".swiper-pagination",
clickable: true,
},
// 前进后退按钮
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
});
});
},
},
},
};
</script>main.js中注册成为全局组件
1
2import Carousel from "@/components/Carousel";
Vue.component(Carousel.name, Carousel);使用
只需传递props参数过去就行
1
<Carousel :carouselList="bannerList" />
getters属性
主要作用就是简化仓库中的数据。
state中searchInfo属性值:
原先获取searchInfo里头的goodList数据需要:
1 | computed: { |
getters简化数据,使更易获取:
1 | const getters = { |
获取数据 :
1 | computed: { |
监听路由
1 | watch:{ |
传参给服务器undefined的使用
1 | searchParams: { |
配置$bus全局事件总线
main.js
1 | new Vue({ |
使用
1 | // 通知兄弟组件Header清楚关键字 |
1 | mounted() { |
自定义事件实现子给父通信
父组件
1 | <SearchSelector @trademarkInfo="trademarkInfo" /> |
1 | methods: { |
子组件
1 | methods: { |
阿里图标库使用
使用:
寻找图标添加到购物车
将购物车中的图标添加至项目中
项目中点击查看在线链接
生成代码
复制Font class代码
index.html引入样式
1
2<!-- 引入阿里图标 -->
<link rel="stylesheet" href="https://at.alicdn.com/t/font_3432904_z2geolzap3l.css"组件使用图标
1
<span class="iconfont icon-down"></span>
动态更改图标样式
1
<span class="iconfont" :class="{ 'icon-down': isDesc, 'icon-up': isAsc }"></span>
1
2
3
4
5
6
7
8computed: {
isAsc() {
return this.searchParams.order.indexOf("asc") != -1;
},
isDesc() {
return this.searchParams.order.indexOf("desc") != -1;
},
},
v-for
v-for:数组|数字|字符串|对象 都可以遍历。
滚动行为
设置路由跳转后滚动条的位置
1 | // 配置路由 |
async await 判断请求成功与否
场景:用户将商品加入购物车,仅需判断是否添加成功。
使用:
actions函数:返回结果没有需要保存的数据,不需要写mutation,可以直接返回结果。
注意:async函数返回的结果一定是promise【要么成功,要么失败】
1
2
3
4
5
6
7
8
9
10// 将产品添加到购物车或者更新购买数量
async addOrUpdateShopCart({ commit }, { skuId, skuNum }) {
let result = await reqAddOrUpdateShopCart(skuId, skuNum);
// 自定义返回信息
if (result.code == 200) {
return "ok";
} else {
return Promise.reject(new Error("加入购物车失败"));
}
},组件methods中对应的方法
1
2
3
4
5
6
7
8
9
10
11
12
13// 加入购物车
async addShopCart() {
try {
await this.$store.dispatch("addOrUpdateShopCart", {
skuId: this.$route.params.skuid,
skuNum: this.skuNum,
});
// 路由跳转
...
} catch (error) {
alert(error.message);
}
},
本地存储和会话存储
本地存储:持久化的(存储上限5MB)
会话存储:非持久化(浏览器关闭则销毁)
本地存储|会话存储一般存储的是字符串。
路由传参结合会话存储
1 | // 路由跳转 |
当路由传递的参数中有对象时,地址栏中会将Object转成字符串显示,能通过this.$route.query获取对象参数,只是地址栏不好看。
1 | http://localhost:8080/#/addcartsuccess?skuInfo=%5Bobject%20Object%5D&skuNum=1 |
像比较复杂的数据【skuInfo】,通过会话存储
1 | // 路由跳转 |
1 | computed: { |
UUID+本地存储 生成临时游客身份
安装uuid,先查看node_modules中是否已经有了再安装
创建uuid_token.js文件
1
2
3
4
5
6
7
8
9
10
11
12import { v4 as uuidv4 } from "uuid";
// 生成随机字符串,且之后每次执行结果都不能改变(游客身份)
export const getUUID = () => {
let uuid_token = localStorage.getItem("UUIDTOKEN");
if(!uuid_token){
// 不存在
uuid_token = uuidv4();
localStorage.setItem("UUIDTOKEN",uuid_token);
}
return uuid_token;
};在vuex中定义uuid_token代表游客身份
1
2
3
4import { getUUID } from "@/utils/uuid_token";
const state = {
uuid_token:getUUID(), // 生成随机字符串(不会再变)
};requests.js中引入仓库并在请求头加上游客id
1
2
3
4
5
6
7
8
9
10
11
12
13// 引入store仓库
import store from "@/store";
// 请求拦截器(请求发送前做处理) config:header请求头等属性
requests.interceptors.request.use((config) => {
// 如果detail仓库中的uuid_token有值,则在请求头加上它
if (store.state.detail.uuid_token) {
config.headers.userTempId = store.state.detail.uuid_token;
}
// 进度条开始
nprogress.start();
return config;
});
actions中的context、以及Promise.all()的使用
1 | const actions = { |
context中存在dispatch属性,可见可以使用context来派发actions中的其它方法。其它属性同理
举例使用:
1 | const actions = { |
prevent取消默认事件
1 | <button class="btn" @click.prevent="login">登录</button> |
导航守卫
导航:表示路由正在发生改变。(进行路由跳转)
守卫:
全局守卫:全局前置守卫、全局解析守卫、全局后置守卫
1 | // 全局守卫:前置守卫(路由跳转之前) |
路由独享守卫
1 | { |
组件内守卫
1 | export default { |
持久化存储token以及通过token获取用户信息(本地存储+导航守卫)
问题1:用户登陆成功后像后台获取token存储在vuex中,并且之后每次请求都在请求头中带上token,以便可以获取用户信息。当首页挂载前发请求获取用户信息,后台通过请求头中的token返回对应的用户信息。之后如果用户刷新页面,此时没有再次向后台获取token(因为token只有点击登录按钮并且登录成功后才会获取到),而且vuex不是持久化存储,这时vuex中的token就会消失。之后每次请求的请求头都不会有token,也就不能获取到用户信息。
解决办法:将获取到的token存储在本地
1 | token: localStorage.getItem("TOKEN"), // state |
记得退出登录后将TOKEN删除
1 | localStorage.removeItem("TOKEN"); |
问题2:此时token已经持久化存在本地,但是要在什么地方发请求获取用户信息。用户信息需要在Header组件中展示,而Header组件在登录之前就会挂载,登录之后除非刷新页面否则不会再挂载。原本是在Home组件挂载前获取用户信息存储在vuex中,Header组件在vuex中获取用户信息就行,但是如果当前为Search组件,当刷新页面,vuex中的用户信息清空,Search组件因为没有在挂载函数中写相应的请求,即使token有了,也没有再发请求获取用户信息。
解决办法:导航守卫
router文件夹的index.js中引入store
1
import store from "@/store"
配置全局前置守卫
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24// 全局守卫:前置守卫
router.beforeEach(async (to, from, next) => {
next();
let token = store.state.user.token;
let name = store.state.user.userInfo.name;
if (token) {
// 用户登录了还想去登录页面则跳到首页
if (to.path == "/login") {
next("/");
} else {
// 判断是否有用户信息
if (name) {
next();
} else {
await store.dispatch("getUserInfo");
next();
}
}
} else if (to.path == "/register") {
next("/register");
} else {
next("/login");
}
});
函数
trim
去除字符串空格。
find
查询数组中符合条件的元素
1 | // 用户最终选择的地址 |
split
将attrValue用:进行分割,拿到分割后的第二个值
1 | {{ attrValue.split(":")[1] }} |
indexOf与splice
indexOf:
1 | // 不添加重复数据 |
splice:
1 | this.searchParams.props.splice(index, 1); // 删除对应index下标的元素 |
every遍历
1 | let arr = [{cur:1},{cur:2},{cur:3}]; |
Math.ceil、parseInt取整
Math.ceil:
1 | mounted() { |
parseInt:
1 | mounted() { |
Object.assign对象合并
ES6新增语法,合并对象。
1 | let searchParams = { |
路由对象参数合并
1 | beforeMount() { |
filter
过滤😂
isNaN
判断文本是否为纯数字
场景:用户输入购买数量,但是输入的不一定是纯数字,需判断。
使用:
1 | // 修改产品个数 |
统一api中的全部请求函数
之前只是在组件或者vuex中引入需要的请求函数。
1 | import { reqGoodsDetail, reqAddOrUpdateShopCart } from "@/api"; |
现在将api中的所有请求函数变成全局
将所有请求函数引入main.js
1
2// 统一api文件夹的全部请求函数
import * as API from "@/api";自定义$API
1
2
3
4
5
6
7
8
9
10new Vue({
render: (h) => h(App),
// 配置全局事件总线以及统一api
beforeCreate() {
Vue.prototype.$bus = this;
Vue.prototype.$API = API;
},
router,
store,
}).$mount("#app");methods中使用
1
let result = await this.$API.reqSubmitOrder(tradeNo, data);
之前如果在api文件夹中有进行分类,形如:
在index.js进行配置,将product文件夹中的js统一暴露
1
2
3
4
5
6
7
8
9
10
11
12
13// 将四个模块请求接口函数统一暴露
import * as trademark from "./product/tradeMark";
import * as attr from "./product/attr";
import * as spu from "./product/spu";
import * as sku from "./product/sku";
// 对外暴露
export default {
trademark,
attr,
sku,
spu,
};然后做1.2步后就这样调用
1
this.$API.trademark.reqDeleteTradeMark(row.id);
尽量别在生命周期函数使用async|await
1 | async mounted() { |
解决:
1 | mounted() { |
elementUI使用+按需引入
安装
1
npm install --save element-ui
按需引入
需借助babel-plugin-component
1
npm install babel-plugin-component -D
配置babel.config.js
1
2
3
4
5
6
7
8
9
10
11
12module.exports = {
presets: ["@vue/cli-plugin-babel/preset"],
plugins: [
[
"component",
{
libraryName: "element-ui",
styleLibraryName: "theme-chalk",
},
],
],
};之后在main.js中按需引入
1
2
3
4
5
6
7
8
9
10
11import { Button, Select } from 'element-ui';
Vue.component(Button.name, Button);
Vue.component(Select.name, Select);
/* 或写为
* Vue.use(Button)
* Vue.use(Select)
*/
// 另一种写法(挂在原型上)
// Vue.prototype.$msgbox = MessageBox
// Vue.prototype.$alert = MessageBox.alert使用
1
2
3
4
5
6// 打开支付窗
open() {
this.$alert("<strong>这是<i>HTML</i></strong>", "HTML片段", {
dangerouslyUseHTMLString: true,
});
},
qrcode二维码插件
安装
1 | npm i qrcode --save |
组件中引入依赖
1 | import QRCode from "qrcode" |
使用
1 | // 生成二维码 |
返回Promise对象,所以需要用到async await
1 | // 打开支付窗 |
路由重定向
场景:默认第一次访问显示的页面
1 | { |
图片懒加载
安装
1 | npm install --save vue-lazyload@1.3.3 |
main.js配置
1 | // 引入懒加载插件及图片 |
使用
1 | <img v-lazy="goods.defaultImg" /> |
自定义插件
src中创建plugins文件夹
定义
1 | let myPlugins = {}; |
modifiers:修饰符,可以自定义属性
1 | <h1 v-upper.erha="msg"></h1> |
main.js配置
1 | // 引入自定义插件 |
使用
1 | <h1 v-upper="msg"></h1> |
1 | data() { |
效果
vee-validate表单验证
安装
1 | npm i vee-validate@2 --save |
配置
main.js引入
1
import "@/plugins/validate";
使用
1 | import Vue from "vue"; |
1 | <!-- 注册内容 --> |
1 | // 注册 |
路由懒加载
当路由被访问的时候才加载对应组件,更高效。
1 | // import Home from "@/pages/Home"; |
打包后的map文件
项目打包后(npm run build),代码都是经过压缩加密的,如果运行时报错,输出的错误信息无法准确得知是哪里的代码报错。
有了map就可以像未加密的代码一样,准确的输出是哪一行哪一列有错。
所以该文件是可以去除的
vue.config.js 配置
productionSourceMap:false
引入JQuery
安装
1 | npm install --save jquery |
版本:”jquery”: “^3.6.0”,
配置:修改vue.config.js文件
1 | const { defineConfig } = require("@vue/cli-service"); |
后台摸板介绍
简洁版:https://github.com/PanJiaChen/vue-admin-template
加强版:https://github.com/PanJiaChen/vue-element-admin
简洁版使用:npm install 以及 npm run dev
浅拷贝与深拷贝
1)格式
浅拷贝形如
1 | this.attrInfo = { ...row }; // es6语法好像,lodash应该也有封装 |
深拷贝形如
1 | // 按需引入lodash当中的深拷贝 |
1 | this.attrInfo = cloneDeep(row); |
2)注意
深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型。
3)实例
1 | // 对象赋值 |
1 | // 浅拷贝 |
1 | // 深拷贝 |
$set
用法:向响应式对象中添加一个属性,并确保这个新属性同样是响应式的(值改变,视图跟着更新),必须用于向响应式对象上添加新属性。
this.Object.newProperty = 'hi'
此时newProperty不是响应式的。
this.$set(Object,'newProperty','hi')
此时newProperty为响应式的。
深度选择器
scoped样式并不是只对当前组件有效,对子组件的根标签同样有效。
需求:给style加上了scoped属性,还想对子组件有效?
解决:深度选择器:可以实现样式穿透。
格式:
1 | 原生css >>> |
实现:
1 | /* 此时就能影响到子组件的h3标签 */ |