无视图的 Vue - 无渲染组件简介
随着组件规模的扩大,维护难度也随之增加。有时,如何将臃肿的组件拆分成更小的组件并不明显。代码会变得更加嘈杂,难以理解。
在这篇文章中,我将介绍“无渲染组件”的概念,它可能会帮助您改进组件。
我的奇妙网站
我们将深入探究“我的神奇网站”的来源。(如果您不想被剧透,请先不要深入 PR。)
Groovy 页脚
看到页面底部那个漂亮的页脚了吗?让我们看看这个页脚的源代码。
<template>
<footer :style="footerStyle">
<div class="text" :style="textStyle">Made with ❤ by Jason Yu © 2019</div>
<label class="insane-mode-label">
<input type="checkbox" v-model="insaneMode"> Insane Mode (new!)
</label>
</footer>
</template>
<script>
import { randomNumber, randomPercentage, randomColor } from '../services/random';
const FOOTER_INTERVAL_MS = 543;
const TEXT_INTERVAL_MS = FOOTER_INTERVAL_MS / 3;
export default {
mounted() {
this.randomFooterStyle();
this.randomTextStyle();
this.footerIntervalId = window.setInterval(this.randomFooterStyle, this.footerIntervalMs);
this.textIntervalId = window.setInterval(this.randomTextStyle, this.textIntervalMs);
},
beforeDestroy() {
window.clearInterval(this.footerIntervalId);
window.clearInterval(this.textIntervalId);
},
data: () => ({
footerStyle: null,
textStyle: null,
insaneMode: false,
}),
computed: {
insaneFactor() {
return this.insaneMode ? 3 : 1;
},
footerIntervalMs() {
return FOOTER_INTERVAL_MS / this.insaneFactor;
},
textIntervalMs() {
return FOOTER_INTERVAL_MS / this.insaneFactor;
},
},
watch: {
insaneMode() {
window.clearInterval(this.footerIntervalId);
window.clearInterval(this.textIntervalId);
this.footerIntervalId = window.setInterval(this.randomFooterStyle, this.footerIntervalMs);
this.textIntervalId = window.setInterval(this.randomTextStyle, this.textIntervalMs);
},
},
methods: {
randomFooterStyle() {
const { insaneFactor } = this;
this.footerStyle = {
borderRadius: `${randomPercentage()} ${randomPercentage()} / ${randomPercentage()} ${randomPercentage()}`,
background: randomColor(),
transitionDuration: `${FOOTER_INTERVAL_MS / insaneFactor}ms`,
};
},
randomTextStyle() {
const { insaneFactor } = this;
this.textStyle = {
transform: `rotate(${randomNumber(
-3 * insaneFactor,
3 * insaneFactor,
)}deg) scale(${randomNumber(0.7 * insaneFactor, 1.3 * insaneFactor)})`,
color: randomColor(),
transitionDuration: `${TEXT_INTERVAL_MS / insaneFactor}ms`,
};
},
},
};
</script>
<style scoped>
footer {
margin-top: 1rem;
padding: 3rem 0;
transition-property: border-radius, background;
text-align: center;
}
footer .text {
transition-property: color, transform;
}
.insane-mode-label {
display: block;
margin-top: 2rem;
}
</style>
注意, 中超过一半的代码<script>
都是用来处理window.setInterval
和 的window.clearInterval
。我们该如何简化这个组件呢?将页脚文本和背景分别移到各自的组件中是没有意义的,因为它们在语义上属于页脚,而不是独立的!
<间隔>
让我们创建一个名为的组件<Interval>
,它将处理与我们相关的所有window.setInterval
事务window.clearInterval
。
src/组件/renderless/Interval.js:
export default {
render: () => null,
};
首先,正如本文标题所示,该render
函数应该不渲染任何内容。因此我们 return null
。
道具
接下来,应该接受什么样的 props <Interval>
?显然我们希望能够控制delay
每个间隔之间的距离。
src/组件/renderless/Interval.js:
export default {
props: {
delay: {
type: Number,
required: true,
},
},
render: () => null,
}
已安装
当<Interval>
安装 时,我们预计它会开始间隔,并会在 处撕开间隔beforeDestroyed
。
src/组件/renderless/Interval.js:
export default {
props: {
delay: {
type: Number,
required: true,
},
},
mounted () {
this.id = window.setInterval(() => /* ... */, this.delay);
},
beforeDestroy () {
window.clearInterval(this.id);
},
render: () => null,
}
我们应该做什么/* ... */
?
setInterval
它接受两个参数:一个回调和一个延迟。那么我们应该接受它callback
作为 prop 吗?这主意很棒,而且效果很好。但我认为更“Vue 式”的做法是触发事件!
src/组件/renderless/Interval.js:
export default {
props: {
delay: {
type: Number,
required: true,
},
},
mounted () {
this.id = window.setInterval(() => this.$emit('tick'), this.delay);
},
beforeDestroy () {
window.clearInterval(this.id);
},
render: () => null,
}
完毕!
尽管它很简单,但它赋予了我们间隔的力量,而无需管理间隔 ID 和间隔的设置/拆除!
重构 Footer.vue!
让我们在 Footer.vue 中分别处理和钩子中的setInterval
和:clearInterval
mounted
beforeDestroy
// ...
mounted() {
// ...
this.footerIntervalId = window.setInterval(this.randomFooterStyle, this.footerIntervalMs);
this.textIntervalId = window.setInterval(this.randomTextStyle, this.textIntervalMs);
},
beforeDestroy() {
window.clearInterval(this.footerIntervalId);
window.clearInterval(this.textIntervalId);
},
// ...
上面的代码现在可以替换为:
<Interval :delay="footerIntervalMs" @tick="randomFooterStyle"></Interval>
<Interval :delay="textIntervalMs" @tick="randomTextStyle"></Interval>
生成的 Footer.vue 将会像这样:
<template>
<footer :style="footerStyle">
<Interval :delay="footerIntervalMs" @tick="randomFooterStyle"></Interval>
<Interval :delay="textIntervalMs" @tick="randomTextStyle"></Interval>
<div class="text" :style="textStyle">Made with ❤ by Jason Yu © 2019</div>
<label class="insane-mode-label">
<input type="checkbox" v-model="insaneMode"> Insane Mode (new!)
</label>
</footer>
</template>
<script>
import { randomNumber, randomPercentage, randomColor } from '../services/random';
import Interval from './renderless/Interval';
const FOOTER_INTERVAL_MS = 543;
const TEXT_INTERVAL_MS = FOOTER_INTERVAL_MS / 3;
export default {
mounted() {
this.randomFooterStyle();
this.randomTextStyle();
},
data: () => ({
footerStyle: null,
textStyle: null,
insaneMode: false,
}),
computed: {
insaneFactor() {
return this.insaneMode ? 3 : 1;
},
footerIntervalMs() {
return FOOTER_INTERVAL_MS / this.insaneFactor;
},
textIntervalMs() {
return FOOTER_INTERVAL_MS / this.insaneFactor;
},
},
watch: {
insaneMode() {
window.clearInterval(this.footerIntervalId);
window.clearInterval(this.textIntervalId);
this.footerIntervalId = window.setInterval(this.randomFooterStyle, this.footerIntervalMs);
this.textIntervalId = window.setInterval(this.randomTextStyle, this.textIntervalMs);
},
},
methods: {
randomFooterStyle() {
const { insaneFactor } = this;
this.footerStyle = {
borderRadius: `${randomPercentage()} ${randomPercentage()} / ${randomPercentage()} ${randomPercentage()}`,
background: randomColor(),
transitionDuration: `${FOOTER_INTERVAL_MS / insaneFactor}ms`,
};
},
randomTextStyle() {
const { insaneFactor } = this;
this.textStyle = {
transform: `rotate(${randomNumber(
-3 * insaneFactor,
3 * insaneFactor,
)}deg) scale(${randomNumber(0.7 * insaneFactor, 1.3 * insaneFactor)})`,
color: randomColor(),
transitionDuration: `${TEXT_INTERVAL_MS / insaneFactor}ms`,
};
},
},
};
</script>
<style scoped>
footer {
margin-top: 1rem;
padding: 3rem 0;
transition-property: border-radius, background;
text-align: center;
}
footer .text {
transition-property: color, transform;
}
.insane-mode-label {
display: block;
margin-top: 2rem;
}
</style>
注意到组件现在看起来漂亮多了?没有像footerIntervalId
or这样荒谬的名字了textIntervalId
,也不用担心忘记删除间隔了!
疯狂模式
疯狂模式由 Footer.vue 中的观察者提供支持:
<template>
<!-- ... -->
<Interval :delay="footerIntervalMs" @tick="randomFooterStyle"></Interval>
<Interval :delay="textIntervalMs" @tick="randomTextStyle"></Interval>
<!-- ... -->
</template>
<script>
// ...
watch: {
insaneMode() {
window.clearInterval(this.footerIntervalId);
window.clearInterval(this.textIntervalId);
this.footerIntervalId = window.setInterval(this.randomFooterStyle, this.footerIntervalMs);
this.textIntervalId = window.setInterval(this.randomTextStyle, this.textIntervalMs);
},
},
// ...
</script>
我们显然想删除这个观察者并将逻辑移到里面<Interval>
。
当触发疯狂模式时, 会<Interval>
收到一个新的delay
prop,因为this.footerIntervalMs
和this.textIntervalMs
发生了变化。然而,<Interval>
尚未被编程来对 的变化做出反应delay
。我们可以添加一个观察器,delay
它会拆除现有的间隔并设置一个新的。
Interval.js
export default {
props: {
delay: {
type: Number,
required: true,
},
},
mounted () {
this.id = window.setInterval(() => this.$emit('tick'), this.delay);
},
beforeDestroy () {
window.clearInterval(this.id);
},
watch: {
delay () {
window.clearInterval(this.id);
this.id = window.setInterval(() => this.$emit('tick'), this.delay);
},
},
render: () => null,
}
现在我们可以删除 Footer.vue 中的观察者:
watch: {
insaneMode() {
window.clearInterval(this.footerIntervalId);
window.clearInterval(this.textIntervalId);
this.footerIntervalId = window.setInterval(this.randomFooterStyle, this.footerIntervalMs);
this.textIntervalId = window.setInterval(this.randomTextStyle, this.textIntervalMs);
},
},
最终的 Footer.vue 如下所示:
<template>
<footer :style="footerStyle">
<Interval :delay="footerIntervalMs" @tick="randomFooterStyle"></Interval>
<Interval :delay="textIntervalMs" @tick="randomTextStyle"></Interval>
<div class="text" :style="textStyle">Made with ❤ by Jason Yu © 2019</div>
<label class="insane-mode-label">
<input type="checkbox" v-model="insaneMode"> Insane Mode (new!)
</label>
</footer>
</template>
<script>
import { randomNumber, randomPercentage, randomColor } from '../services/random';
import Interval from './renderless/Interval';
const FOOTER_INTERVAL_MS = 543;
const TEXT_INTERVAL_MS = FOOTER_INTERVAL_MS / 3;
export default {
mounted() {
this.randomFooterStyle();
this.randomTextStyle();
},
data: () => ({
footerStyle: null,
textStyle: null,
insaneMode: false,
}),
computed: {
insaneFactor() {
return this.insaneMode ? 3 : 1;
},
footerIntervalMs() {
return FOOTER_INTERVAL_MS / this.insaneFactor;
},
textIntervalMs() {
return FOOTER_INTERVAL_MS / this.insaneFactor;
},
},
methods: {
randomFooterStyle() {
const { insaneFactor } = this;
this.footerStyle = {
borderRadius: `${randomPercentage()} ${randomPercentage()} / ${randomPercentage()} ${randomPercentage()}`,
background: randomColor(),
transitionDuration: `${FOOTER_INTERVAL_MS / insaneFactor}ms`,
};
},
randomTextStyle() {
const { insaneFactor } = this;
this.textStyle = {
transform: `rotate(${randomNumber(
-3 * insaneFactor,
3 * insaneFactor,
)}deg) scale(${randomNumber(0.7 * insaneFactor, 1.3 * insaneFactor)})`,
color: randomColor(),
transitionDuration: `${TEXT_INTERVAL_MS / insaneFactor}ms`,
};
},
},
};
</script>
<style scoped>
footer {
margin-top: 1rem;
padding: 3rem 0;
transition-property: border-radius, background;
text-align: center;
}
footer .text {
transition-property: color, transform;
}
.insane-mode-label {
display: block;
margin-top: 2rem;
}
</style>
向你挑战!
希望你觉得这篇文章有趣。如果你想了解更多关于不同类型的无渲染组件的知识,请观看我之前演讲的视频,其中包含更多实时编码示例。
Footer.vue 中的钩子里还有两行代码mounted
。你能想办法扩展一下<Interval>
,把整个钩子都去掉mounted
吗?点击此处查看PR获取灵感。
mounted() {
this.randomFooterStyle();
this.randomTextStyle();
},
为什么?
我们在Attest使用 Vue构建了非常酷的产品。我们发现这种模式在很多方面都很有优势,例如可维护性、正确性、可测试性等等。如果你想加入这个才华横溢的团队,就立即申请吧!
PS 我们喜欢基于功能的 RFC。
文章来源:https://dev.to/ycmjason/vue-without-view-an-introduction-to-renderless-components-23ld