Vue 及其所有组件都只不过是 JavaScript.
如果不认同这句话,可以看下接下来的vue组件基础。
一、 Vue组件本质
观察一下下面这个单文件组件,相信很多人都写过类似的代码。
<template>
<div class="mood">
{{ todayIsSunny ? 'Makes me happy' : 'Eh! Doesn't bother me' }}
</div>
</template>
<script>
export default {
data: () => ({ todayIsSunny: true })
}
</script>
<style>
.mood:after {
content: '🎉🎉';
}
</style>
Vue 试图简化样式和其他资源的管理,只是 Vue 并没有直接做这些事情,而是把这些工作留给了构建过程,比如 webpack。
webpack 在处理.vue 文件时,会对其执行一个转换。在转换过程中,CSS 被从组件中提取出来,并放到单独的文件中,剩余部分则被转换为 JavaScript。例如:
export default {
template: `
<div class="mood">
{{ todayIsSunny ? 'Makes me happy' : 'Eh! Doesn't bother me' }}
</div>`,
data: () => ({ todayIsSunny: true })
}
当模板编辑器遇到这段代码,它将 template 属性提取出来,并将它的内容编译为 JavaScript,然后将一个渲染函数添加到组件对象中。这个渲染函数将返回已转换为 JavaScript 的 template 属性的内容。
对JSX熟悉的人对这个代码就十分眼熟了。
Vue提供了方法可以自己写渲染函数,有关渲染函数的更多信息,请参阅官方文档
render(h) {
return h(
'div',
{ class: 'mood' },
this.todayIsSunny ? 'Makes me happy' : 'Eh! Doesn't bother me'
)
}
现在,当组件对象被传给 Vue 时,组件的渲染函数会经过一些优化并变成 VNode(虚拟节点),然后 VNode 被传给 snabbdom(Vue 内部用于管理虚拟 DOM 的库)。Vue 通过 VNode 的方式来渲染组件。
二、Slot基本功
Vue中使用slot
的一个重要原因,就是为了达到组件的复用,子组件的某些元素直接由调用他的父组件决定。
不能免俗,先记一下目前的三种插槽。
单个插槽
其实就是最简单的配置,不额外写任何东西,直接拿官网的例子
子组件 <navigation-link>
<a v-bind:href="url" class="nav-link">
<slot></slot>
</a>
父组件调用
<navigation-link url="/profile">
<!-- 添加一个 Font Awesome 图标 -->
<span class="fa fa-user"></span>
Your Profile
</navigation-link>
最终会渲染为
<a v-bind:href="url" class="nav-link">
<!-- 添加一个 Font Awesome 图标 -->
<span class="fa fa-user"></span>
Your Profile
</a>
具名插槽
其实就是指定了名字的插槽,代码会被渲染到指定的位置。
继续上面的例子:
子组件指定具名插槽和默认插槽,则父节点中slot的元素会按位置渲染:
<template>
<a v-bind:href="url" class="nav-link">
<slot name="up"></slot>
<p>我是分割线</p>
<slot></slot>
</a>
</template>
<script>
export default {
props: {
url: {
type: String
}
},
data() {
return { specData: '我的内容来自子节点'};
}
};
</script>
父组件:
<template>
<navigation-link url="/profile">
<!-- 添加一个 Font Awesome 图标 -->
<span class="fa fa-user"></span>
Your Profile
<span>{{specData}}</span>
<p slot="up">
<span>我是up</span>
</p>
</navigation-link>
</template>
<script>
import navigationLink from './child.vue';
export default {
created(){
},
data() {
return { specData: '我必须由父节点来传递,我的内容来自父节点};
},
components: { navigationLink },
}
</script>
最终渲染为:
<a href="/profile" class="nav-link">
<p>
<span>我是up</span>
</p>
<p>我是分割线</p>
<span class="fa fa-user"></span>
Your Profile
<span>我必须由父节点来传递,我的内容来自父节点</span>
</a>
可见,多了一个name,其实我们就可以定制显示的位置,和默认的匿名插槽混用也不会有影响,这个在需要渲染多个插槽时十分有效。
在提到作用域插槽之前,必须确保已经了解Vue的编译作用域。
父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译。
其实仔细看上面的例子,父子组件都赋值了specData
,但是渲染的是父组件的内容便了解了,其实写错了也没事,Vue会给你及时提示的。
作用域插槽
子组件通过给slot上绑定参数,达到给父组件传递进来的模板赋值的效果。
子组件:
<template>
<a v-bind:href="url" class="nav-link">
<slot name="up"></slot>
<p>我是分割线</p>
<slot></slot>
<p>我是分割线</p>
<slot name="list" :list="nameList"></slot>
</a>
</template>
<script>
export default {
props: {
url: {
type: String
}
},
data() {
return {
specData: "我的内容来自子节点",
nameList: ['小王', '小张', '隔壁大爷']
};
}
};
</script>
父组件:(这里slot-scope="{ list }"
直接用了结构)
<template>
<navigation-link url="/profile">
<!-- 添加一个 Font Awesome 图标 -->
<span class="fa fa-user"></span>
Your Profile
<span>{{specData}}</span>
<p slot="up">
<span>我是up</span>
</p>
<template slot="list" slot-scope="{ list }">
<p>
<span v-for="(item, index) in list" :key="index">{{item}}</span>
</p>
</template>
</navigation-link>
</template>
<script>
import "@mfelibs/base-css";
import navigationLink from "./child.vue";
export default {
created() {},
data() {
return {
specData: "我必须由父节点来传递"
};
},
components: { navigationLink }
};
</script>
最终渲染为
三、无渲染组件
我们可以这样理解无渲染组件:为一个组件创建通用功能抽象,然后通过扩展这个组件来创建更好更健壮的组件,或者说,遵循 S.O.L.I.D 原则。
根据 S.O.L.I.D 的单一责任原则:
一个类应该只有一个用途。
我们将这个概念移植到 Vue 开发中,让每个组件只提供一个用途。
例如我们实现一个评论组件,当另一个需求过来,修改了样式,修改了交互时,就不得不去修改组件代码来实现这个需求。而我们之前做的和接口的通信方式,和客户端的通信方式,可能又需要重新copy一份。
这打破了 S.O.L.I.D 的开放封闭原则,这个原则规定:
类或组件应该为扩展而开放,为修改而封闭。
也就是说,你应该扩展它,而不是直接修改组件的源代码。
Vue 遵循了 S.O.L.I.D.
原则,让组件拥有 prop、event、slot 和 scoped slot,这些东西让组件的交互和扩展变得轻而易举。我们可以构建具备所有特性的组件,而无需修改任何样式或标记。这对于可重用性和高效代码来说非常重要。
在设计时需要仔细思考的是
表现和行为分离
由于无渲染组件只处理状态和行为,所以它们不会对设计或布局强加任何的决策。这意味着,如果你能够找到一种方法,将所有的行为从UI组件中移出,将将其转换成一个无组件的组件,那么你就可以重用无渲染组件来实现任何布局效果的标签输入控件。
简单的一个toogle组件
这个组件完成一个简单的功能,传递给子组件一个状态on
(也可以不传,例子中就没传),通过调用子组件暴露出的方法,达到利用子组件的状态,影响父组件的渲染的目的。这样,父组件的结构可以随便改,样式随便让产品设计去折腾,我们的基本功能是不变的,达到一定层度的代码复用。
父组件
注意slot-scope
不能直接用在子组件标签<toogle>
上
<template>
<toogle>
<div slot-scope="{ on, setOn, setOff }" class="container">
<button @click="click(setOn)" class="button">Blue pill</button>
<button @click="click(setOff)" class="button isRed">Red pill</button>
<div v-if="buttonPressed" class="message">
<span v-if="on">It's all a dream, go back to sleep.</span>
<span v-else>I don't know how far the rabbit hole goes, I'm not a rabbit, neither do I measure holes.</span>
</div>
</div>
</toogle>
</template>
<script>
import toogle from "./toogle";
export default {
data() {
return {
buttonPressed: false
};
},
components: {
toogle
},
methods: {
click(fn) {
this.buttonPressed = true;
fn && fn();
}
}
};
</script>
子组件
<template>
<div>
<slot :on="currentState" :setOn="setOn" :setOff="setOff" :toogle="toggle"></slot>
</div>
</template>
<script>
export default {
props: {
on: { type: Boolean, default: false }
},
data() {
return { currentState: this.on };
},
methods: {
setOn() {
this.currentState = true;
},
setOff() {
this.currentState = false;
},
toggle() {
this.currentState = !this.currentState;
}
}
};
</script>
如果要用render函数,子组件可以省去template部分,在script部分添加
render: function(createElement) {
return createElement("div", [
this.$scopedSlots.default({
on: this.currentState,
setOn: this.setOn,
setOff: this.setOff,
toggle: this.toggle
})
]);
},
关于这部分可参考 官网api
2018.11.12更新
发现了一篇好文,讲解的不错
Vue中的无渲染组件 (原文 70%付费)
直接搜题目 就能搜到有人买过了 粘出来的文章,这里不再贴了。