Vue核心应用(六)— 手写UI组件

一、前言

仿 element-ui 手写 menu、message、render

安装 npm i element-ui -S

入口文件 main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import Vue from 'vue';
import App from './App';
// 引入element-ui
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';

Vue.use(ElementUI); // 它会将组建注册到全局 Vue.component

new Vue({
el:"#app",
render: h => h(App)
})
// 启动 vue serve
// element-ui 安装 、快速上手 - 全局引入样式

二、Menu 组件

前言

先看下 仿的 element-ui中 menu 组件的效果
App.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
<template>
<div>
<el-menu default-active="2" class="el-menu-vertical-demo">
<el-submenu index="1">
<template slot="title">导航一</template>
<el-submenu index="1-1">
<template slot="title">选项1-1</template>
<el-menu-item index="1-1-1">选项1-1-1</el-menu-item>
<el-menu-item index="1-1-2">选项1-1-2</el-menu-item>
</el-submenu>
<el-menu-item index="1-2">选项1-2</el-menu-item>
</el-submenu>
<el-menu-item index="2">
导航二
</el-menu-item>
<el-menu-item index="3">
导航三
</el-menu-item>
<el-menu-item index="4">
导航四
</el-menu-item>
</el-menu>
</div>
</template>

如下图:

实操

1.1 仿原生组件的代码

App.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
<template>
<div>
<jf-menu>
<!-- jfSubMenu 有2部分 一部分是标题 <template... 一部分是标题所展开的内部<jf-submenu... -->
<jf-subMenu>
<template slot="title">导航一</template>
<jf-subMenu>
<template slot="title">选项1-1</template>
<jf-menu-item>选项1-1-1</jf-menu-item>
<jf-menu-item>选项1-1-2</jf-menu-item>
</jf-subMenu>
<jf-menu-item>选项1-1</jf-menu-item>
<jf-menu-item>选项1-2</jf-menu-item>
</jf-subMenu>
<jf-menu-item>导航二</jf-menu-item>
<jf-menu-item>导航三</jf-menu-item>
</jf-menu>
</div>
</template>
<script>
import jfMenu from './components/jf-menu';
import jfMenuItem from './components/jf-menu-item';
import jfSubMenu from './components/jf-submenu';
export default {
components:{
jfMenu,
jfMenuItem,
jfSubMenu
}
}
</script>

新建 /components/jf-menu.vue

1
2
3
4
5
6
7
<template>
<ul>
<!-- 插槽 默认插槽是没有名字的 这个slot 相当于react里面的children -->
<slot name="default"></slot>
<!-- slot 表示上述 <jf-menu></jf-menu>标签包裹的内容 -->
</ul>
</template>

新建 /components/jf-submenu.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
<template>
<li>
<span @click="change">
<slot name="title"></slot>
</span>
<ul v-if="isShow">
<slot name="default"></slot>
</ul>
</li>
</template>
<script>
export default {
data(){
return {
isShow: false
}
},
methods:{
change(){ // 点击标题 内容显示或隐藏
this.isShow = !this.isShow;
}
}
}
</script>

新建 /components/jf-menu-item.vue

1
2
3
4
5
<template>
<li>
<slot name="default"></slot>
</li>
</template>

效果如下:

1.2 优化(用户调取只需关注数据结构)

如上 App.vue 中 line3-17所示,太过繁琐,希望用户只传递数据就能自动生成上述数据结构
App.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
43
44
<template>
<div>
<Menu :data="data"></Menu>
</div>
</template>
<script>
import Menu from './menu';
export default {
components:{
Menu
},
data(){
return {
data:[
{
name: "导航一",
id: 1,
children: [
{
name: "导航1.1",
id: 1.1,
children: [
{ name: "导航1.1.1", id: 1.3 },
{
name: "导航1.1.2",
id: 1.4,
children: [
{ name: "导航1.1.2.1", id: 1.5 },
{ name: "导航1.1.2.2", id: 1.6 }
]
}
]
},
{ name: "导航1.2", id: 1.2 }
]
},
{ name: "导航二", id: 2 },
{ name: "导航三", id: 3 },
{ name: "导航三", id: 4 }
]
}
}
}
</script>

新建 menu.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<jf-menu>
<!-- 循环根据有没有childrn 来显示是submenu、menu-item -->
<template v-for="menu in data">
<jf-subMenu v-if="menu.children" :key="menu.id">
<template slot="title">{{menu.name}}</template>
<!-- 循环孩子组件 应该是不停的去重复 写一个Resub.vue 组件做重复部分 -->
<!-- 找到重复的部分 做一个递归 -->
</jf-subMenu>
<jf-menu-item v-else :key="menu.id">{{menu.name}}</jf-menu-item>
</template>
<!-- <Resub :data="data"></Resub> -->
</jf-menu>
</template>
  • 全局引入组件 其他组件不必再局部引入

完整代码如下:
main.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
import Vue from 'vue';
import App from './App';
// 引入element
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import Resub from './Resub';
import Menu from './menu';
import jfMenu from './components/jf-menu';
import jfMenuItem from './components/jf-menu-item';
import jfSubMenu from './components/jf-submenu';

Vue.use(ElementUI); // 它会将组建注册到全局 Vue.component

Vue.component('jf-menu', jfMenu);
Vue.component('Resub', Resub);
Vue.component('jf-menu-item', jfMenuItem);
Vue.component('jf-subMenu', jfSubMenu);
Vue.component('Menu', Menu);

new Vue({
el:"#app",
render: h => h(App)
})
// 启动 vue serve

App.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
<template>
<div>
<Menu :data="data"></Menu>
</div>
</template>
<script>
export default {
data(){
return {
data:[
{
name: "导航一",
id: 1,
children: [
{
name: "导航1.1",
id: 1.1,
children: [
{ name: "导航1.1.1", id: 1.3 },
{
name: "导航1.1.2",
id: 1.4,
children: [
{ name: "导航1.1.2.1", id: 1.5 },
{ name: "导航1.1.2.2", id: 1.6 }
]
}
]
},
{ name: "导航1.2", id: 1.2 }
]
},
{ name: "导航二", id: 2 },
{ name: "导航三", id: 3 },
{ name: "导航三", id: 4 }
]
}
}
}
</script>

menu.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
<template>
<jf-menu>
<!-- 1)这里先循环一次-->
<template v-for="menu in data">
<!-- <jf-subMenu v-if="menu.children" :key="menu.id">
<template slot="title">{{menu.name}}</template>
</jf-subMenu> -->
<!-- 2) 将有可能重复的项 再次循环 -->
<Resub v-if="menu.children" :key="menu.id" :data="menu"></Resub>
<jf-menu-item v-else :key="menu.id">{{menu.name}}</jf-menu-item>
</template>
</jf-menu>
</template>
<script>

export default {
mounted(){
console.log("menu数据:", this.data);
},
props:{
data:{
type: Array,
default:()=>[]
}
}
}
</script>

新建 Resub.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
<template>
<jf-subMenu >
<!-- 标题 -->
<template slot="title">{{data.name}}</template>
<!-- 内容 -->
<template v-for="menu in data.children">
<!-- 3) 递归当前自己-->
<Sub v-if="menu.children" :key="menu.id" :data="menu"></Sub>
<jf-menu-item v-else :key="menu.id">{{menu.name}}</jf-menu-item>
</template>
</jf-subMenu>
</template>
<script>
export default {
name: 'Sub', // 当前组件的名称
mounted(){
console.log("Resub数据:", this.data);
},
props:{
data:{
type: Object, // 传进来的是包含children的对象
default: ()=>({})
}
}
}
</script>

/components/ 下的3个子组件代码不变 如上

最终效果如下:

总结:模板循环,递归组件

三、message组件

前言

先看下 仿的 element-ui中 message 组件的效果
App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<button @click="show">点我</button>
</template>
<!-- 先调用element-ui 的Message组件试试-->
<script>
import { Message } from 'element-ui';
export default {
methods:{
show(){
Message.success({
message: '你好',
duration: 3000
})
}
}
}
</script>

实操

2.1 引入自己的库

思路: 在点击的时候 创建一个组件 把这个组件放到页面body下 新建Message.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<button @click="show">点我</button>
</template>
<script>
import { Message } from './Message'; // 这里引入自己的组件库
export default {
methods:{
show(){
Message.success({ // 这种调取方法参照element-ui中 单独引用
message: '你好',
duration: 3000
})
}
}
}
</script>

新建 Message.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import MessageComponent from './Message.vue';
import Vue from 'vue';

const Message = {
success(options){
// options就是当前弹出来的框
// alert(options);
// $mount() document.body.appendChild
let vm = new Vue({
render: h => h(MessageComponent)
}).$mount();
// 点击弹窗 需要将.vue文件挂载到内存中(挂载到虚拟元素上)
// 这里 vm就是 返回的渲染后的实例
// vm.$el 就是DOM节点
// 将渲染好的内容放到页面中
document.body.appendChild(vm.$el);
}
};
export {
Message
}

新建 Message.vue

1
2
3
4
5
<template>
<div>
弹窗
</div>
</template>

2.2 数据驱动视图

每次用户点击按钮时 都是增加数据 自动渲染到视图上
Message.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
import MessageComponent from './Message.vue';
import Vue from 'vue';

const Message = {
success(options){
// options就是当前弹出来的框
// alert(options);
// $mount() document.body.appendChild
let vm = new Vue({
render: h => h(MessageComponent)
}).$mount();
// 点击弹窗 需要将.vue文件挂载到内存中(挂载到虚拟元素上)
// 这里 vm就是 返回的渲染后的实例
// vm.$el 就是DOM节点
// 将渲染好的内容放到页面中
document.body.appendChild(vm.$el);

// 现在希望通过数据来驱动, 而不是每次点都通过append渲染,点一次渲染一次点一次渲染一次,性能不好,
// 如 Message.vue改成数据驱动视图
vm.$children[0].add(options); // 这里的vm.$children[0]就是添加的这个 MessageComponent,因为只添加了一个,所以这里是$children[0]

}
};
export {
Message
}

Message.vue

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>
<div v-for="(layer,index) in layers" :key="index">
{{layer.message}}
</div>
</div>
</template>

<script>
export default {
// 每次用户点击按钮时 都是增加数据 自动渲染到视图上
data(){
return {
layers:[]
}
},
methods:{ // 提供方法 供外界去调取 $children
add(options){
this.layers.push(options);
}
}
}
</script>

现在情况是 一点击 创建一个实例(vm),给实例添加options属性,没有累加的效果,每次点击都产生新的
需要将new Vue操作只做一次 ,不能重复new
用单例模式

2.3 单例模式

Message.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let instance;
let getVueInstance = () =>{
// 此 instance 是全局的
instance = new Vue({
render: h => h(MessageComponent)
}).$mount();
// 把生成的结果放到页面中
document.body.appendChild(instance.$el);
}

import MessageComponent from './Message.vue';
import Vue from 'vue';

const Message = {
success(options){
!instance && getVueInstance();
instance.$children[0].add(options);
}
};
export {
Message
}

如图可以看到每次都只在body中添加一个div,在这个div里面产生多个message组件,而不是一直在body中新增由div包裹的组件一直append

2.4 设置定时 时间到销毁

增加序号,时间到了,需要根据序号删除
Message.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
<template>
<div>
<div v-for="(layer,index) in layers" :key="index">
{{layer.message}} {{layer.id}}
</div>
</div>
</template>

<script>
export default {
// 每次用户点击按钮时 都是增加数据 自动渲染到视图上
data(){
return {
layers:[]
}
},
mounted(){
this.id = 0;
},
methods:{ // 提供方法 供外界去调取 $children
add(options){ // 增加序号,时间到了,需要根据序号删除
let layer = {...options, id: ++this.id};
layer.timer = setTimeout(() => {
this.remove(layer);
}, options.duration);

this.layers.push(layer);
},
remove(layer){
clearTimeout(layer.timer);
this.layers = this.layers.filter(item=> item.id !== layer.id);

}
}
}
</script>

2.5 全局引入的方式

上面是单独引入Message的方式调取 这里参考element-ui 中

原文: 全局方法 Element 为 Vue.prototype 添加了全局方法 $message。因此在 vue instance 中可以采用本页面中的方式调用 Message。

即可以通过 this.$message 调取 如下:
Message.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
33
34
35
36
37
38
39
let instance;
let getVueInstance = () =>{
instance = new Vue({
render: h => h(MessageComponent)
}).$mount();
document.body.appendChild(instance.$el);
}

import MessageComponent from './Message.vue';
import Vue from 'vue';

const Message = {
success(options){
!instance && getVueInstance();
instance.$children[0].add({
...options,
type: 'success'
});
},
info(){},
warn(){},
};
export {
Message
}
// 现在如App.vue line6调取方式可知 走的是如下的export
export default{
// _Vue是当前的构造函数,默认Vue.use(line7) 就会使用调用这个方法
install(_Vue){
// 当前的这个_Vue是 App.vue中line5引入的Vue
// 这个install里面主要做什么: 1) 注册全局组件(Vue.component); 2) 注册全局指令; 3)往原型上添加方法、属性
let $message = {};
Object.keys(Message).forEach(key=>{
$message[key] = Message[key];
})
// 一般使用新对象时 采用拷贝的方式
Vue.prototype.$message = $message;
}
}

App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<button @click="show">点我</button>
</template>
<script>
import Vue from 'vue'; // 模块之间是独立的,因为下面要用到Vue,所以这里需要引入Vue
import jfUi from './Message';
Vue.use(jfUi);
export default {
methods:{
show(){
this.$message.success({
message: '你好',
duration: 3000
})
}
}
}
</script>

执行 还是跟之前的使用效果一样的 和 图 设置定时时间到销毁 一样