我最近写了一篇 文章,解释了如何 使用 HTML、CSS 和 JavaScript 创建倒计时器。现在,让我们看看如何通过将其移植到 Vue 中将其变成一个可复用的组件,使用框架提供的基本功能。
为什么要这样做呢?嗯,有很多原因,但有两个特别突出
- 使 UI 与计时器状态同步:如果您 查看代码,来自 第一篇文章,它都存在于
timerInterval
函数中,最显著的是状态管理。每次它运行(每秒)时,我们需要手动在我们的文档中找到合适的元素——无论是时间标签还是剩余时间路径或任何其他元素——并更改其值或属性。Vue 带有一个基于 HTML 的 模板语法,允许您将渲染的 DOM 声明性地绑定到底层 Vue 实例的数据。这承担了查找和更新正确 UI 元素的所有负担,因此我们可以完全依赖于组件实例的属性。 - 拥有一个高度可复用的组件:原始示例在文档中只有一个计时器时工作良好,但想象一下,您想添加另一个。糟糕!我们依赖元素的 ID 来执行我们的操作,并且在多个实例上使用相同的 ID 将阻止它们独立工作。这意味着我们需要为每个计时器分配不同的 ID。如果我们创建一个 Vue 组件,则其所有逻辑都封装起来并连接到该组件的特定实例。我们可以轻松地在单个文档上创建 10、20、1000 个计时器,而无需更改组件本身的任何一行代码!
这是我们在上一篇文章中一起创建的同一个计时器,但在 Vue 中。
模板和样式
来自 Vue 文档
Vue 使用基于 HTML 的模板语法,允许您将渲染的 DOM 声明性地绑定到底层 Vue 实例的数据。所有 Vue.js 模板都是有效的 HTML,可以由符合规范的浏览器和 HTML 解析器解析。
让我们通过打开一个名为 BaseTimer.vue
的新文件来创建我们的组件。这是我们需要为此提供的基本结构
// Our template markup will go here
<template>
// ...
</template>
// Our functional scripts will go here
<script>
// ...
</script>
// Our styling will go here
<style>
// ...
</style>
在此步骤中,我们将专注于 <template>
和 <style>
部分。让我们将计时器模板移动到 <template>
部分,并将所有 CSS 移动到 <style>
部分。标记主要由 SVG 组成,我们可以使用与第一篇文章中相同的代码。
<template>
// The wrapper for the timer
<div class="base-timer">
// This all comes from the first article
<svg class="base-timer__svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<g class="base-timer__circle">
<circle class="base-timer__path-elapsed" cx="50" cy="50" r="45"></circle>
<path
id="base-timer-path-remaining"
stroke-dasharray="283"
class="base-timer__path-remaining ${remainingPathColor}"
d="
M 50, 50
m -45, 0
a 45,45 0 1,0 90,0
a 45,45 0 1,0 -90,0
"
></path>
</g>
</svg>
// The label showing the remaining time
<span
id="base-timer-label"
class="base-timer__label"
>
${formatTime(timeLeft)}
</span>
</div>
</template>
// "scoped" means these styles will not leak out to other elements on the page
<style scoped>
.base-timer {
position: relative;
width: 100px;
height: 100px;
}
</style>
让我们看看我们刚刚复制的模板,以确定在哪里可以使用我们的框架。有几个部分负责使我们的计时器倒计时并显示剩余时间。
stroke-dasharray
:传递给 SVG<path>
元素的值,该元素负责保存剩余时间。remainingPathColor
:负责更改计时器圆形环的颜色,从而提供一种视觉方式来指示时间即将用完。formatTime(timeLeft)
:负责显示计时器内剩余多少时间的值
我们可以通过操作这些值来控制我们的计时器。
常量和变量
好的,让我们转到 <script>
部分,看看 Vue 为我们提供了哪些开箱即用的功能来简化我们的工作。其中一项功能是让我们能够预先定义常量,这可以使它们的作用域限定在组件内。
在上一篇文章中,我们花了一些时间调整 stroke-dasharray
值,以确保计时器顶层(随着时间的推移动画和颜色变化的环)的动画与其底层(指示过去时间的灰色环)完全一致。我们还定义了顶层应更改颜色的“阈值”(剩余 10 秒时为橙色,剩余 5 秒时为红色)。我们还为这些颜色创建了常量。
我们可以将所有这些直接移动到 <script>
部分
<script>
// A value we had to play with a bit to get right
const FULL_DASH_ARRAY = 283;
// When the timer should change from green to orange
const WARNING_THRESHOLD = 10;
// When the timer should change from orange to red
const ALERT_THRESHOLD = 5;
// The actual colors to use at the info, warning and alert threshholds
const COLOR_CODES = {
info: {
color: "green"
},
warning: {
color: "orange",
threshold: WARNING_THRESHOLD
},
alert: {
color: "red",
threshold: ALERT_THRESHOLD
}
};
// The timer's starting point
const TIME_LIMIT = 20;
</script>
现在,让我们看看我们的变量
let timePassed = 0;
let timeLeft = TIME_LIMIT;
let timerInterval = null;
let remainingPathColor = COLOR_CODES.info.color;
我们可以在这里识别两种不同类型的变量
- 变量的值在我们的方法中直接重新赋值
timerInterval
:当我们启动或停止计时器时会发生变化timePassed
:计时器运行时每秒都会发生变化
- 变量的值在其他变量发生变化时会发生变化
timeLeft
:当timePassed
的值发生变化时会发生变化remainingPathColor
:当timeLeft
的值超过指定的阈值时会发生变化
识别这两种类型之间的差异至关重要,因为它允许我们使用框架的不同功能。让我们分别介绍每种类型。
值直接重新赋值的变量
让我们考虑一下当我们更改 timePassed
值时我们希望发生什么。我们希望计算剩余多少时间,检查是否应该更改顶层环的颜色,并在视图的一部分上使用新值触发重新渲染。
Vue 带有自己的 反应系统,该系统会更新视图以匹配特定属性的新值。要将属性添加到 Vue 的反应系统,我们需要在组件的 data
对象上声明该属性。通过这样做,Vue 将为每个属性创建一个 **getter** 和一个 **setter**,这些属性将跟踪该属性的变化并做出相应的响应。
<script>
// Same as before
export default {
data() {
return {
timePassed: 0,
timerInterval: null
};
}
</script>
我们需要记住两件重要的事情。
- 我们需要预先在
data
对象中声明所有反应式变量。这意味着如果我们知道变量会存在但不知道其值是什么,我们仍然需要使用某个值来声明它。如果我们忘记在data
中声明它,即使它以后被添加,它也不会具有反应性。 - 在声明
data
选项对象时,我们始终需要返回一个新的对象实例(使用return
)。这非常重要,因为如果我们不遵循此规则,则声明的属性将在组件的所有实例之间共享。
您可以在实际操作中看到第二个问题
值在其他变量发生变化时会发生变化的变量
这些变量依赖于另一个变量的值。例如,timeLeft
完全依赖于 timePassed
。在我们 使用原生 JavaScript 的原始示例 中,我们正在负责更改 timePassed
值的间隔中计算该值。使用 Vue,我们可以将该值提取到一个 computed
属性中。
computed
属性是一个返回值的函数。这些值绑定到依赖值,并且仅在需要时更新。更重要的是,computed
属性会被缓存,这意味着它们会记住 computed
属性所依赖的值,并且仅当该依赖属性值发生变化时才会计算新值。如果值没有改变,则返回先前缓存的值。
<script>
// Same as before
computed: {
timeLeft() {
return TIME_LIMIT - this.timePassed;
}
}
}
</script>
传递给 computed
属性的函数必须是 **纯函数**。它不能产生任何副作用并且必须返回值。此外,输出值只能依赖于传递给函数的值。
现在,我们可以将更多逻辑移动到 computed
属性中
circleDasharray
:这返回先前在setCircleDasharray
方法中计算的值。formattedTimeLeft
:这返回formatTime
方法的值。timeFraction
:这是calculateTimeFraction
方法的抽象。remainingPathColor
:这是setRemainingPathColor
方法的抽象。
<script>
// Same as before
computed: {
circleDasharray() {
return `${(this.timeFraction * FULL_DASH_ARRAY).toFixed(0)} 283`;
},
formattedTimeLeft() {
const timeLeft = this.timeLeft;
const minutes = Math.floor(timeLeft / 60);
let seconds = timeLeft % 60;
if (seconds < 10) {
seconds = `0${seconds}`;
}
return `${minutes}:${seconds}`;
},
timeLeft() {
return TIME_LIMIT - this.timePassed;
},
timeFraction() {
const rawTimeFraction = this.timeLeft / TIME_LIMIT;
return rawTimeFraction - (1 / TIME_LIMIT) * (1 - rawTimeFraction);
},
remainingPathColor() {
const { alert, warning, info } = COLOR_CODES;
if (this.timeLeft <= alert.threshold) {
return alert.color;
} else if (this.timeLeft <= warning.threshold) {
return warning.color;
} else {
return info.color;
}
}
}
</script>
我们现在拥有了所需的所有值!但是现在我们需要在我们的模板中使用它们。
在模板中使用数据和计算属性
这是我们模板的结束位置
<template>
<div class="base-timer">
<svg class="base-timer__svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<g class="base-timer__circle">
<circle class="base-timer__path-elapsed" cx="50" cy="50" r="45"></circle>
<path
id="base-timer-path-remaining"
stroke-dasharray="283"
class="base-timer__path-remaining ${remainingPathColor}"
d="
M 50, 50
m -45, 0
a 45,45 0 1,0 90,0
a 45,45 0 1,0 -90,0
"
></path>
</g>
</svg>
<span
id="base-timer-label"
class="base-timer__label"
>
${formatTime(timeLeft)}
</span>
</div>
</template>
让我们从 formatTime(timeLeft)
开始。我们如何将渲染的值动态绑定到我们的 formattedTimeLeftcomputed
属性?
Vue 使用基于 HTML 的 模板语法,允许我们声明性地将渲染的 DOM 绑定到 Vue 实例的底层数据。这意味着所有属性都在模板部分可用。要渲染任何属性,我们使用文本插值,使用“Mustache”语法(双大括号,或 {{ }}
)。
<span
id="base-timer-label"
class="base-timer__label"
>
{{ formattedTimeLeft }}
</span>
接下来是 stroke-dasharray
。我们可以看到我们不想渲染该值。相反,我们想更改 <path>
属性的值。Mustache 不能用于 HTML 属性内部,但别担心!Vue 提供了另一种方法:v-bind
指令。我们可以像这样将值绑定到属性
<path v-bind:stroke-dasharray="circleDasharray"></path>
为了方便使用该指令,我们还可以使用一个简写形式。
<path :stroke-dasharray="circleDasharray"></path>
最后一个是 remainingPathColor
,它会为元素添加一个合适的类。我们可以使用上面相同的 v-bind
指令来实现这一点,但将值分配给元素的 class
属性。
<path :class="remainingPathColor"></path>
让我们看看修改后的模板。
<template>
<div class="base-timer">
<svg class="base-timer__svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<g class="base-timer__circle">
<circle class="base-timer__path-elapsed" cx="50" cy="50" r="45"></circle>
<path
:stroke-dasharray="circleDasharray"
class="base-timer__path-remaining"
:class="remainingPathColor"
d="
M 50, 50
m -45, 0
a 45,45 0 1,0 90,0
a 45,45 0 1,0 -90,0
"
></path>
</g>
</svg>
<span class="base-timer__label">{{ formattedTimeLeft }}</span>
</div>
</template>
我们的模板已经准备好了,我们将所有变量移动到了 data
或 computed
中,并且通过创建相应的 computed
属性摆脱了大多数方法。但是,我们仍然缺少一个至关重要的部分:我们需要启动我们的计时器。
方法和组件生命周期钩子
如果我们查看我们的 startTimer
方法,我们可以看到所有计算、属性更改等都在间隔中发生。
function startTimer() {
timerInterval = setInterval(() => {
timePassed = timePassed += 1;
timeLeft = TIME_LIMIT - timePassed;
document.getElementById("base-timer-label").innerHTML = formatTime(
timeLeft
);
setCircleDasharray();
setRemainingPathColor(timeLeft);
if (timeLeft === 0) {
onTimesUp();
}
}, 1000);
}
由于我们已经将所有这些逻辑移到了 computed
属性中,因此我们在 timerInterval
中需要做的就是在 timePassed
中更改值——其余的将在 computed
属性中神奇地发生。
<script>
// Same as before
methods: {
startTimer() {
this.timerInterval = setInterval(() => (this.timePassed += 1), 1000);
}
}
</script>
我们已经准备好了方法,但我们仍然没有在任何地方调用它。每个 Vue 组件都有一系列钩子,允许我们在组件生命周期的特定时期运行特定的逻辑。这些被称为生命周期钩子。在我们的例子中,因为我们希望在组件加载后立即调用我们的方法。这使得 mounted
成为我们想要的生命周期钩子。
<script>
// Same as before
mounted() {
this.startTimer();
},
// Same methods as before
</script>
就是这样,我们刚刚使用 Vue 将我们的计时器变成了一个一致且可重用的组件!
假设我们现在想在另一个组件中使用此组件。这需要一些操作
// App.vue
import BaseTimer from "./components/BaseTimer"
export default {
components: {
BaseTimer
}
};
总结!
此示例展示了我们如何将组件从原生 JavaScript 移动到基于组件的前端框架,如 Vue。
我们现在可以将计时器视为一个独立的组件,其中所有标记、逻辑和样式都包含在一个不会泄漏到或与其他元素冲突的方式中。组件通常是较大父组件的子组件,该父组件将多个组件组合在一起——例如表单或卡片——其中父组件的属性可以被访问和共享。这里有一个例子,其中计时器组件正在接收来自父组件的指令。
我希望我让你对 Vue 和组件的力量产生了兴趣!我鼓励你访问Vue 文档,以获取我们示例中使用的功能的更详细描述。Vue 可以做的事情太多了!
是的,非常感谢这篇文章,但存在一个问题。
如何在原生 JS 中使用组件?
这可能吗?