0%

vue组件化

表单组件

简单地自己造一下轮子,实现一下vue的自定组件,这里主要先从form组件开始实践。

form组件需求分析:1、指定数据、校验规则;高内聚、低耦合;

2、这里为了实现form组件的输入功能,我们自定义一个input组件来实现;为保证低耦合性,仅使用并实现双向绑定基本功能。

3、校验的实现跟input分开,再自定义一个KFormItem组件来实现校验的功能。执行校验的组件为KFormItem组件,但通知父组件执行校验的是其子组件KINput,由于事件是谁派发谁监听,不能用this.$emit,需要用this.parent.$emit。之后,在KFormItrm组件上监听校验事件,执行具体校验。

Input

实现功能:维护数据;

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
<template>
<div>
<!-- 自定义组件双向绑定::value @input -->
<!-- v-bind="$attrs"展开$attrs
$attrs/$listeners包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 ( class 和 style 除外)。当一个组件没有
声明任何 prop 时,这里会包含所有父作用域的绑定 ( class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件——在创建高级别的组件时非常有用。-->
<input :type="type" :value="value" @input="onInput" v-bind="$attrs">
</div>
</template>

<script>
export default {
inheritAttrs: false, // 设置为false避免设置到根元素上,
props: {
value: {
type: String,
default: ''
},
type: {
type: String,
default: 'text'
}
},
methods: {
onInput(e) {
// 派发一个input事件即可,相当于在原生input上面再封装一个实现了双向绑定的input
this.$emit('input', e.target.value)

// 通知父级执行校验
this.$parent.$emit('validate')
}
},
}
</script>

<style scoped>

</style>

KFormItem

KformItem组件包裹Input组件,同时在前面添加label标签;

实现功能:1、执行校验2、显示错误信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<template>
<div>
<!-- label -->
<label v-if="label">{{label}}</label>
<slot></slot>

<!-- 校验信息显示 -->
<p v-if="error">{{error}}</p>
</div>
</template>

<script>
// Asyc-validator
import Schema from "async-validator";

export default {
inject: ["form"],
data() {
return {
error: "" // error是空说明校验通过
};
},
props: {
label: {
type: String,
default: ""
},
prop: {
type: String
}
},
mounted() {
this.$on("validate", () => {
this.validate();
});
},
methods: {
validate() {
// 规则
const rules = this.form.rules[this.prop];
// 当前值
const value = this.form.model[this.prop];

// 校验描述对象
const desc = { [this.prop]: rules };
// 创建Schema实例
const schema = new Schema(desc);
return schema.validate({ [this.prop]: value }, errors => {
if (errors) {
this.error = errors[0].message;
} else {
// 校验通过
this.error = "";
}
});
}
}
};
</script>

<style scoped>
</style>

KForm

一层层地往上面的层级进行靠近,在KForm的层级,除了实现slot将下面的层级包裹,同样也要接受数据并处理。

最终目的是在KForm上除了接受数据模型model以外,还要声明校验规则rules,因此声明数据model、rules。

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
<template>
<div>
<slot></slot>
</div>
</template>

<script>
export default {
provide() {
return {
form: this
};//直接将组件自己传递出去,这样就能够在下面子组件拿到model、rules
},
props: {
model: {
type: Object,
required: true
},
rules: {
type: Object
}
},
methods: {
validate(cb) {
// 获取所有孩子KFormItem
// [resultPromise]
const tasks = this.$children
.filter(item => item.prop) // 过滤掉没有prop属性的Item
.map(item => item.validate());

// 统一处理所有Promise结果
Promise.all(tasks)
.then(() => cb(true))
.catch(() => cb(false));
}
}
};
</script>

<style scoped>
</style>

Index

这样,在index层级里面,我们将所有的自定义组件封装起来,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<template>
<div>
<!-- <ElementTest></ElementTest> -->

<!-- KForm -->
<KForm :model="userInfo" :rules="rules" ref="loginForm">
<!-- 用户名 -->
<KFormItem label="用户名" prop="username">
<KInput v-model="userInfo.username" placeholder="请输入用户名"></KInput>
</KFormItem>
<!-- 密码 -->
<KFormItem label="密码" prop="password">
<KInput type="password" v-model="userInfo.password" placeholder="请输入密码"></KInput>
</KFormItem>
<!-- 提交按钮 -->
<KFormItem>
<button @click="login">登录</button>
</KFormItem>
</KForm>
</div>
</template>

<script>
import ElementTest from "@/components/form/ElementTest.vue";
import KInput from "@/components/form/KInput.vue";
import KFormItem from "@/components/form/KFormItem.vue";
import KForm from "@/components/form/KForm.vue";
import Notice from "@/components/Notice.vue";

export default {
data() {
return {
userInfo: {
username: "tom",
password: ""
},
rules: {
username: [{ required: true, message: "请输入用户名称" }],
password: [{ required: true, message: "请输入密码" }]
}
};
},
components: {
ElementTest,
KInput,
KFormItem,
KForm
},
methods: {
login() {
this.$refs["loginForm"].validate(valid => {
const notice = this.$create(Notice, {
title: "",
message: valid ? "请求登录!" : "校验失败!",
duration: 2000
});
notice.show();
// if (valid) {
// alert("submit");
// } else {
// console.log("error submit!");
// return false;
// }
});
}
}
};
</script>

<style scoped>
</style>

VueRouter

需求分析

1、作为一个插件存在:实现VueRouter类和install方法

2、实现两个全局组件:router-view用于显示匹配组件内容,router-link用于跳转

3、监控url变化:监听hashchange或popstate事件

4、响应最新url:创建一个响应式的属性current,当它改变时获取对应组件并显示

实现VueRouter插件

在toyRouter.js里面,我们需要实现一个插件,先创建VueRouter类,并且实现其install方法,该方法会在当前的Vue原型链上挂载$router。使用vue.mixin来混入生命周期中,保证每个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
class KVueRouter {
constructor(options) {
this.$options = options
console.log(this.$options);


KVueRouter.install = function (_Vue) {
// 保存构造函数,在KVueRouter里面使用
Vue = _Vue;

// 挂载$router
// 怎么获取根实例中的router选项,混入生命周期钩子即可,该生命周期钩子会在所有组件都执行一遍
//为什么要用混入方式写?主要原因是Vue.use(VueRouter)代码在前,Router实例创建在后,而install逻辑又需要用到该实例
Vue.mixin({
beforeCreate() {
// 确保根实例的时候才执行,只有根实例有router选项
if (this.$options.router) {
Vue.prototype.$router = this.$options.router
}
}
})
}

export default KVueRouter

为实现功能,将其写成独立的组件,并在vueRouter的install方法中再将其引入。

router-link实现的功能是:解析用户输入的路由值,并重新渲染一个输入路由值对应的a标签。且在纯运行时的环境,不能使用template而需要用render函数来渲染页面,这里我们需要渲染一个a标签。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export default {
props: {
to: {
type: String,
required: true
},
},
render(h) {
// <a href="#/about">abc</a>
// <router-link to="/about">xxx</router-link>,值是用户通过props传进来的,因此直接用this.to,前面拼上#为了使用方便
// h(tag, data, children)
console.log(this.$slots);
return h('a', { attrs: { href: '#' + this.to } }, this.$slots.default)
// return <a href={'#' + this.to}>{this.$slots.default}</a>
}
}

router-view

router-view实现的功能基本上是:根据当前的路由值,获取对应的component并渲染在当前位置。

1、需要监控路由的变化,且为了实现路由改变时,便实时刷新页面,因此需要设置当前路由值为响应式数据。

2、为了确保查找的快速,在新建VueRouter时就应该以用户输入路由配置,初始化一个hash表,这样每次获取当前路由值的component时,只用O(1)时间即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//上述两点应该在VueRouter类中实现,因此VueRouter更新为:
class KVueRouter {
constructor(options) {
this.$options = options
console.log(this.$options);
// 需要创建响应式的current属性,当当前路由路径发生变化时,则页面就会重新渲染,这就是响应式
// 利用Vue提供的defineReactive做响应化
// 这样将来current变化的时候,依赖的组件会重新render
Vue.util.defineReactive(this, 'current', '/')

// this.app = new Vue({
// data() {
// return {
// current: '/'
// }
// }
// })

// 监控url变化,利用bind锁定this,避免你使用的onHashChange函数的this变成后面的window
window.addEventListener('hashchange', this.onHashChange.bind(this))
window.addEventListener('load', this.onHashChange.bind(this))//除了路由修改以外,用户刷新页面也要有重新渲染


// 创建一个路由映射表
this.routeMap = {}
options.routes.forEach(route => {
this.routeMap[route.path] = route
})
}

onHashChange() {
console.log(window.location.hash);
this.current = window.location.hash.slice(1);//由于前面还有#号,因此需要slice切割一下
}
}

而在router-view页面要实现的功能就要简单些了,主要是从路由映射hsahMap中获取对应组件,这里同样要用render函数来渲染。

1
2
3
4
5
6
7
8
9
10
export default {
render(h) {
//获取path对应的component
const {routeMap, current} = this.$router;//从路由映射中找到对应的路由,用hash表来查找,快速找到结果
console.log(routeMap,current);

const component = routeMap[current].component || null;
return h(component)
}
}

Vuex

目的:集中管理数据、可预测的改变数据;类似于整个项目数据的大管家,确保整个程序的状态、数据能够保持同步的状态,维持稳定。

需求分析

1、实现一个插件:声明Store类,挂载$store

2、:创建响应式的state,保存mutations、actions和getters

3、实现commit根据用户传入type执行对应mutation

4、实现dispatch根据用户传入type执行对应action,同时传递上下文

5、实现getters,按照getters定义对state做派生

实现store插件

声明Store、install方法,并创建一个响应式的state。同时利用存取器,避免用户直接去取state

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 保存构造函数引用,避免import
let Vue;

class Store {
constructor(options) {
// this.$options = options;
this._mutations = options.mutations;
this._actions = options.actions;

// 响应化处理state
// this.state = new Vue({
// data: options.state
// })
this._vm = new Vue({
data: {
// 加两个$,Vue不做代理,因此对外部是隐藏的,不能直接去访问。
$$state: options.state
}
})

// 绑定commit、dispatch的上下文问store实例
this.commit = this.commit.bind(this)
this.dispatch = this.dispatch.bind(this)
}

// 存取器, 用户通过store.state的方式来访问,这样避免用户直接修改state。
//官方的实现是:使用一个watch去监听任何修改,一旦用户尝试修改,则直接报错。
get state() {
console.log(this._vm);
return this._vm._data.$$state
}

set state(v) {
console.error('无法修改!');

}

function install(_Vue) {
Vue = _Vue;

Vue.mixin({
beforeCreate() {
if (this.$options.store) {
Vue.prototype.$store = this.$options.store
}
}
})
}

// Vuex
export default {
Store,
install
}

实现commit、dispatch方法

在写方法之前需要将this.commit、this.dispatch绑定为该store实例,在构造函数上加上:this.commit = this.commit.bind(this);this.dispatch = this.dispatch.bind(this)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Store {
/*
在写方法之前需要将this.commit、this.dispatch绑定为该store实例,在构造函数上加上:
this.commit = this.commit.bind(this);
this.dispatch = this.dispatch.bind(this);
*/

// store.commit('add', 1)
// type: mutation的类型
// payload:载荷,是参数
commit(type, payload) {
const entry = this._mutations[type]
if (entry) {
entry(this.state, payload)
}
}

dispatch(type, payload) {
const entry = this._actions[type]
if (entry) {
entry(this, payload)
}
}
}

实现getters

-------------本文结束感谢您的阅读-------------
坚持原创技术分享,您的支持将鼓励我继续创作!