# Vue 知识点
# vue 中 8 种组件通信方式
# props / $emit
父组件通过props
的方式向子组件传递数据,而通过$emit
子组件可以向父组件通信。
# 1. 父组件向子组件传值
demo:
// 父组件
<template>
<div class='section'>
<com-article :articles='articleList'></com-article>
</div>
</template>
<script>
import comArticle from './test/article.vue';
export default {
name: 'HelloWorld',
component: {
comArticle
}
data() {
return {
articleList: [1,2,3]
}
}
}
</script>
<template>
<div>
<span v-for="(item, index) in articles" :key="index">{{item}}</span>
</div>
</template>
<script>
export default {
props: {
articles: {
type: Array
}
}
}
</script>
总结: prop 只可以从上一级组件传递到下一级组件(父子组件), 即所谓的单向数据流。而且 prop 只读,不可修改,所有修改都会失效并且警告。
# 2. 子组件向父组件传值
$emit
绑定一个自定义事件,当这个语句被执行时,就会将参数 arg 传递给父组件,父组件通过 v-on 监听并接受参数。
demo: 在上个例子的基础上, 点击页面渲染出来的 ariticle 的 item, 父组件中显示在数组中的下标
// 父组件
<template>
<div class='section'>
<com-article :articles='articleList' @onEmitIndex="onEmitIndex"></com-article>
</div>
</template>
<script>
import comArticle from './test/article.vue';
export default {
name: 'HelloWorld',
component: {
comArticle
}
data() {
return {
articleList: [1,2,3]
}
}
}
</script>
<template>
<div>
<span v-for="(item, index) in articles" :key="index" @click="emitIndex(index)">{{item}}</span>
</div>
</template>
<script>
export default {
props: {
articles: {
type: Array
}
}
methods: {
emitIndex(index) {
this.$emit('onEmitIndex',index)
}
}
}
</script>
# $chidlren / $parent
通过 $parent
和 $children
就可以访问组件的实例,拿到实例代表什么? 代表就可以访问此组件的所有方法和data
。接下来就是怎么实现拿到指定组件的实例
demo:
// 父组件
<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png" />
<HelloWorld msg="Welcome to Your Vue.js App" />
<button @click="handleChildren">++++</button>
</div>
</template>
<script>
import HelloWorld from "@/components/HelloWorld.vue";
export default {
name: "home",
components: {
HelloWorld
},
data() {
return {
parentText: "父组件数据"
};
},
methods: {
handleChildren() {
console.log(this.$children);
this.$children[0].name = "hanzo";
}
}
};
</script>
// 子组件
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>{{ name }}</p>
</div>
</template>
<script>
export default {
name: "HelloWorld",
props: {
msg: String
},
data() {
return {
name: "ahao"
};
},
created() {
console.log(this.$parent.parentText); // 父组件数据
}
};
</script>
# provide / inject
概念:
provid
/ inject
是 vue2.2.0
新增的 api,简单来说就是父组件中通过provide
来提供变量,然后再子组件中通过inject
来注入变量
注意:这里不论子组件嵌套多深,只要调用了 inject 那么就可以注入 provide 中的数据,而不局限于只能从当前父组件的 props 属性中读取数据
demo:
// 父组件
<template>
<div class="home">
<HelloWorld msg="Welcome to Your Vue.js App" />
</div>
</template>
<script>
import HelloWorld from "@/components/HelloWorld.vue";
export default {
name: "home",
components: {
HelloWorld
},
provide: {
name: "ahao"
}
};
</script>
// 子组件
<template>
<div class="hello">
<p>{{ name }}</p>
</div>
</template>
<script>
export default {
name: "HelloWorld",
inject: ["name"]
};
</script>
# ref / refs
ref
:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例,可以通过实例直接调用组件的方法或访问数据
demo:
// 父组件
<template>
<div class="home">
<HelloWorld ref="HelloWorld" msg="Welcome to Your Vue.js App" />
</div>
</template>
<script>
import HelloWorld from "@/components/HelloWorld.vue";
export default {
name: "home",
components: {
HelloWorld
},
mounted() {
console.log(this.$refs.HelloWorld.name); // i am HelloWorld
this.$refs.HelloWorld.sayHello();
}
};
</script>
// 子组件
<template>
<div class="hello">
<p>{{ name }}</p>
</div>
</template>
<script>
export default {
name: "HelloWorld",
data() {
return {
name: "i am HelloWorld"
};
},
methods: {
sayHello() {
console.log("hello");
}
}
};
</script>
# eventBus
eventBus
又称为事件总线,在 vue 中可以使用它来作为沟通桥梁的概念,就像是所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件,所以组件都可以通知其他组件。
eventBus 也有不方便之处,当项目较大,就容易造成难以维护的灾难
在 Vue 的项目中怎么使用eventBus
来实现组件之间的数据通信呢?具体通过下面几个步骤
- 初始化
首先需要创建一个事件总线并将其导出,以便其他模块可以使用或者监听它;
// 1. 创建eventBus.js
import Vue from "vue";
export const EventBus = new Vue();
- 创建
A
和B
两个组件,以兄弟组件为例
// 父组件
<template>
<div class="home">
<HelloWorld ref="HelloWorld" msg="Welcome to Your Vue.js App"/>
<A></A>
<B></B>
</div>
</template>
<script>
import HelloWorld from '@/components/HelloWorld.vue'
import A from '@/components/A.vue';
import B from '@/components/B.vue';
export default {
name: 'home',
components: {
HelloWorld,
A,
B
},
}
</script>
- 发送事件
// A 组件 发送事件
<template>
<div>
<p>i am A</p>
<button @click='add'>A+</button>
<p>{{num}}</p>
</div>
</template>
<script>
import { EventBus } from './../eventBus';
export default {
name: 'A',
data() {
return {
num: 0,
}
},
methods: {
add() {
EventBus.$emit('addition',{
num: this.num++
})
}
},
}
</script>
- 接收事件
// B 组件 接收事件
<template>
<div>
<p>i am B</p>
<p>{{count}}</p>
</div>
</template>
<script>
import { EventBus } from './../eventBus';
export default {
name: 'B',
data() {
return {
count: 0,
}
},
mounted() {
EventBus.$on('addition', params => {
console.log(params);
this.count = params.num;
})
},
}
</script>
- 移除事件监听者
// 在组件销毁生命周期中 使用 EventBus.off('事件名称')
<template>
<div>
<p>i am B</p>
<button>B+</button>
<p>{{count}}</p>
</div>
</template>
<script>
import { EventBus } from './../eventBus';
export default {
name: 'B',
data() {
return {
count: 0,
}
},
mounted() {
EventBus.$on('addition', params => {
console.log(params);
this.count = params.num;
})
},
destroyed() {
EventBus.$off('addition',{})
}
}
</script>
# Vuex
vuex 就不概述了,最常用的状态管理方法
# LocalStorage / SessionStorage
这种通信比较简单,缺点就是数据和状态比较混乱,不太容易维护。通过 window.localStorage.getItem(key)
获取数据,通过window.localStorage.setIten(key,val)
存储数据
注意用
JSON.parse()
/JSON.stringify()
做数据格式转换localStorage
/SessionStorage
可以结合vuex
,实现数据的持久保存,同时使用 vuex 解决数据和状态混乱问题.
# $attrs 与 $listeners
现在我们来讨论一种情况, 我们一开始给出的组件关系图中 A 组件与 D 组件是隔代关系, 那它们之前进行通信有哪些方式呢?
- 使用
props
绑定来进行一级一级的信息传递,如果 D 组件中状态改变需要传递数据给 A,使用事件系统一级级往上传递. - 使用
eventBus
,这种情况下还是比较适合使用,但是碰到多人合作开发时,代码维护性比较低,可读性也比较低. - 使用
vuex
来进行数据管理,但是如果仅仅是传递数据,而不做中间处理,使用vuex
处理感觉有点大材小用了.
在vue2.4
中,为了解决改需求,引入了$attrs
和$listeners
,新增了inheritAttrs
选项。
在版本 2.4 以前,默认情况下,父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外),将会“回退”且作为普通的 HTML 特性应用在子组件的根元素上。接下来看一个跨级通信的
# Demo:
- 父组件
// 父组件
<template>
<div class="home">
<HelloWorld ref="HelloWorld" msg="Welcome to Your Vue.js App"/>
<Child1
:name='name'
:age='age'
:gender='gender'
></Child1>
</div>
</template>
<script>
import HelloWorld from '@/components/HelloWorld.vue'
import Child1 from '@/components/child1.vue'
export default {
name: 'home',
components: {
HelloWorld,
Child1
},
data() {
return {
name: 'ahao',
age: 24,
gender: '男',
}
}
}
</script>
- Home 组件中的子组件 Child1
<template>
<div>
<p>i am child1</p>
<Child2 v-bind="$attrs"></Child2>
</div>
</template>
<script>
import Child2 from '@/components/child2.vue'
export default {
name: 'child1',
components: {
Child2
},
created() {
console.log(this.$attrs); // {name: "ahao", age: 24, gender: "男"}
}
}
</script>
- Child1 组件中的子组件 Child2
<template>
<div>
<p>i am child2</p>
<p>{{$attrs.name}}</p>
<p>{{$attrs.age}}</p>
<p>{{$attrs.gender}}</p>
</div>
</template>
<script>
export default {
name: 'child2',
created() {
console.log(this.$attrs)
}
}
</script>
# 总结
常见使用场景可以分为三类:
- 父子组件通信:
props
$parent / $children
provide / inject
ref / refs
$attrs / $listeners
- 兄弟组件通信:
eventBus
Vuex
- 跨级通信:
eventBus
Vuex
provide / inject
$attrs / $linsteners
、
# Vue 中 axios 的封装
# 采用 axios 官方推荐的,通过配置项创建 axios 实例的方式进行配置封装。
// http.js
import axios from "axios";
// 创建 axios 实例
const service = axios.create({
// 配置项
});
# 根据环境设置 baseURL
baseURL属性是请求地址前缀,将自动加在 url 前面,除非 url 是个绝对地址。在正常情况下,在开发环境下和生产环境下有着不同的 baseURL,所以,我们需要根据不同的环境切换不同的 baseURL。
在开发模式下,由于有着 devServer 的存在,需要根据固定的 url 前缀进行请求地址重写,所以,在开发环境下,将 baseURL 设为某个固定的值,比如:/apis。 在生产模式下,根据 Java 模块的请求前缀的不同,可以设置不同的 baseURL。
// 根据 process.env.NODE_ENV 区分状态,切换不同的baseURL
const service = axios.create({
baseURL: process.env.NODE_ENV === "production" ? "h5.mao.com" : "m.mao.com",
})
### 统一设置请求头
```js
const service = axios.create({
...
headers: {
get: {
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
// 在开发中,一般还需要单点登录或者其他功能的通用请求头,可以一并配置进来
},
post: {
'Content-Type': 'application/json;charset=utf-8'
// 在开发中,一般还需要单点登录或者其他功能的通用请求头,可以一并配置进来
}
}
})
# 跨域、超时、响应码处理
axios 中,提供是否允许跨域的属性——withCredentials,以及配置超时时间的属性——timeout,通过这两个属性,可以轻松处理跨域和超时的问题。
axios 提供了 validateStatus 属性,用于定义对于给定的 HTTP 响应状态是 resolve 或 reject promise. 所以,正常设置的情况下,我们会将状态 2 系列或者 304 的请求设为 reslove 状态,其余的位 reject 状态. 结果就是,我们可以在业务代码里,使用 catch 统一捕获响应错误的请求,从而进行统一处理。
const service = axios.create({
// 跨域请求时是否需要使用凭证
withCredentials: true
// 请求30s超时
timeout: 30000,
validateStatus: function() {
// 使用async-await,处理reject情况较为繁琐,所以全部返回resolve,在业务代码中处理异常
return true
}
});
# 请求、响应处理
在不使用axios的情况下,每次请求或者接受响应,都需要将请求或响应序列化
而在axios中, transformRequest
允许在向服务器发送请求前,修改请求数据;
transformResponse
在传递给then/catch前,允许修改响应数据.
通过这两个钩子,可以省去大量重复的序列化代码.
const service = axios.create({
// 在向服务器发送请求前,序列化请求数据
transformResquest: [function(data) {
data = JSON.stringify(data)
return data
}],
// 在传递给 then/catch 前,修改响应数据
transformResponse: [function(data) {
if(typeof data === "string" && data.startsWith('{')) {
data = JSON.parse(data)
}
return data
}]
})
# 拦截器
拦截器,分为请求拦截器以及响应拦截器,分别在请求或响应被then或catch处理前拦截它们.
之前提到过,由于 async-await 中 catch 难以处理的问题,所以将出错的情况也作为 resolve 状态进行处理。但这带来了一个问题,请求或响应出错的情况下,结果没有数据协议中定义的 msg 字段(消息)。所以,我们需要在出错的时候,手动生成一个符合返回格式的返回数据。 由于,在业务中,没有需要在请求拦截器中做额外处理的需求,所以,请求拦截器的 resolve 状态,只需直接返回就可以了。
请求拦截器代码如下:
// 请求拦截器
service.interceptors.request.use((config) => {
return config
}, (error) => {
// 错误抛到业务代码
error.data = {}
error.data.msg = '服务器异常,请联系管理员!'
return Promise.resolve(error)
})
再来看看响应拦截器,还是之前的那个问题,除了请求或响应错误,还有一种情况也会导致返回的消息体不符合协议规范,那就是状态码不为 2 系列或 304 时。此时,我们还是需要做一样的处理——手动生成一个符合返回格式的返回数据。但是,有一点不一样,我们还需要根据不同的状态码生成不同的提示信息,以方便处理上线后的问题
响应拦截器代码如下:
// 根据不同的状态码,生成不同的提示信息
const showStatus = (status) => {
let message = ''
// 这一坨代码可以使用策略模式进行优化
switch (status) {
case 400:
message = '请求错误(400)'
break
case 401:
message = '未授权,请重新登录(401)'
break
case 403:
message = '拒绝访问(403)'
break
case 404:
message = '请求出错(404)'
break
case 408:
message = '请求超时(408)'
break
case 500:
message = '服务器错误(500)'
break
case 501:
message = '服务未实现(501)'
break
case 502:
message = '网络错误(502)'
break
case 503:
message = '服务不可用(503)'
break
case 504:
message = '网络超时(504)'
break
case 505:
message = 'HTTP版本不受支持(505)'
break
default:
message = `连接出错(${status})!`
}
return `${message},请检查网络或联系管理员!`
}
// 响应拦截器
service.interceptors.response.use((response) => {
const status = response.status
let msg = '';
if(status < 200 || status >= 300) {
// 处理http错误,抛到业务代码
msg = showStatus(status);
if(typeof response.data === "string") {
response.data = { msg }
} else {
response.data = msg;
}
}
return response
},(error) => {
// 错误抛到业务代码
error.data = {}
error.data.msg = '请求超时或服务器异常,请检查网络或联系管理员!'
return Promise.resolve(err);
})
- 有一些业务相关的需求,可以加在拦截器中,比如:loading、鉴权等~
# 完整代码
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'
const showStatus = (status) => {
let message = ''
switch (status) {
case 400:
message = '请求错误(400)'
break
case 401:
message = '未授权,请重新登录(401)'
break
case 403:
message = '拒绝访问(403)'
break
case 404:
message = '请求出错(404)'
break
case 408:
message = '请求超时(408)'
break
case 500:
message = '服务器错误(500)'
break
case 501:
message = '服务未实现(501)'
break
case 502:
message = '网络错误(502)'
break
case 503:
message = '服务不可用(503)'
break
case 504:
message = '网络超时(504)'
break
case 505:
message = 'HTTP版本不受支持(505)'
break
default:
message = `连接出错(${status})!`
}
return `${message},请检查网络或联系管理员!`
}
const service = axios.create({
// 联调
baseURL: process.env.NODE_ENV === 'production' ? `/` : '/apis',
headers: {
get: {
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
},
post: {
'Content-Type': 'application/json;charset=utf-8'
}
},
// 是否跨站点访问控制请求
withCredentials: true,
timeout: 30000,
transformRequest: [(data) => {
data = JSON.stringify(data)
return data
}],
validateStatus () {
// 使用async-await,处理reject情况较为繁琐,所以全部返回resolve,在业务代码中处理异常
return true
},
transformResponse: [(data) => {
if (typeof data === 'string' && data.startsWith('{')) {
data = JSON.parse(data)
}
return data
}]
})
// 请求拦截器
service.interceptors.request.use((config: AxiosRequestConfig) => {
return config
}, (error) => {
// 错误抛到业务代码
error.data = {}
error.data.msg = '服务器异常,请联系管理员!'
return Promise.resolve(error)
})
// 响应拦截器
service.interceptors.response.use((response: AxiosResponse) => {
const status = response.status
let msg = ''
if (status < 200 || status >= 300) {
// 处理http错误,抛到业务代码
msg = showStatus(status)
if (typeof response.data === 'string') {
response.data = {msg}
} else {
response.data.msg = msg
}
}
return response
}, (error) => {
// 错误抛到业务代码
error.data = {}
error.data.msg = '请求超时或服务器异常,请检查网络或联系管理员!'
return Promise.resolve(error)
})
export default service
最后直接在main.js 写入 Vue.prototype.$http = service
# Vue中封装一个Toast插件
# 实现Vue的MVVM
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app">
<p>{{a.a}}</p>
<p>{{b}}</p>
<input type="text" v-model="b">
</div>
<script src="./vue.js"></script>
<script>
let vm = new Vue({
el: "#app",
data: {
a: {
a: 1
},
b: 'hanzo'
}
})
</script>
</body>
</html>
function Vue(options={}) {
this.$options = options;
let data = this.$data = this.$options.data;
observe(data)
// this代理了this.$data
for(let key in data) {
Object.defineProperty(this,key,{
enumerable: true,
get() {
return this.$data[key]
},
set(newVal) {
this.$data[key] = newVal;
}
})
}
new Compile(this.$options.el,this)
}
function Compile(el,vm) {
// el 表示 dom
vm.$el = document.querySelector(el);
let fragment = document.createDocumentFragment();
while(child = vm.$el.firstChild) { // 将app的内存 移入到内存中
fragment.appendChild(child);
}
console.log(fragment.childNodes)
replace(fragment)
function replace(fragment) {
Array.from(fragment.childNodes).forEach(function(node) {
let text = node.textContent
let reg = /\{\{(.*)\}\}/;
if(node.nodeType === 3 && reg.test(text)) {
let arr = RegExp.$1.split('.');
console.log(arr);
let val = vm;
arr.forEach(function(key) { // 取this.a.a
val = val[key]
});
new Watcher(vm,RegExp.$1,function(newVal) { // 函数里需要接受一个新值
node.textContent = text.replace(reg,newVal);
})
// 替换的逻辑
node.textContent = text.replace(reg,val);
}
if(node.nodeType === 1) {
// 元素节点
let nodeAttrs = node.attributes; // 获取当前dom节点属性
console.log(nodeAttrs)
Array.from(nodeAttrs).forEach(function(attr) {
let name = attr.name;
let exp = attr.value;
if(name.indexOf('v-') == 0) { // v-model
node.value = vm[exp]
}
new Watcher(vm,exp,function(newVal) {
node.value = vm[exp]; // 当wacther触发时 会自动将内容放到输入框内
});
node.addEventListener('input',function(e) {
let newVal = e.target.value;
vm[exp] = newVal;
})
})
}
if(node.childNodes) {
replace(node);
}
})
}
vm.$el.appendChild(fragment);
}
// 观察对象给对象增加Object.defineProperty
function Observe(data) {
let dep = new Dep();
for(let key in data) {
let val = data[key];
observe(val);
Object.defineProperty(data,key,{
enumerable: true,
get() {
Dep.target && dep.addSub(Dep.target);
return val;
},
set(newVal) {
if(newVal === val) {
return;
}
val = newVal;
observe(newVal);
dep.notify(); // 让所有的wacther的update方法执行即可
}
})
}
}
function observe(data) {
if(typeof data != 'object') return;
return new Observe(data)
}
// 发布订阅模式
function Dep() {
this.subs = [];
}
Dep.prototype.addSub = function(sub) { // 订阅
this.subs.push(sub)
}
Dep.prototype.notify = function() {
this.subs.forEach(function(sub) {
sub.update();
})
}
function Watcher(vm,expr,fn) {
this.vm = vm;
this.expr = expr; //添加到订阅中
this.fn = fn;
Dep.target = this;
let val = vm;
let arr = expr.split('.');
arr.forEach(function(k) {
val = val[k];
})
Dep.target = null;
}
Watcher.prototype.update = function() {
let val = this.vm;
let arr = this.expr.split('.');
arr.forEach(function(k) {
val = val[k];
})
this.fn(val);
}