Vue 最黑暗的一天
LongLiveSvelte
今天,我惊讶地看到一向积极友好的 VueJS 社区陷入了一场激烈的争斗。两周前,Vue 的创始人尤雨溪发布了一份征求意见稿(RFC),要求在即将发布的 Vue 3.0 中引入一种基于函数式编写 Vue 组件的新方式。今天,Reddit 上出现了一条批评性的帖子,随后Hacker News上也出现了类似的批评性评论,这导致大量开发者涌向最初的 RFC 页面表达他们的愤怒,其中一些评论甚至近乎辱骂。有人在多个地方声称:
- 所有 Vue 代码都必须以全新的方式重写,因为现有的语法已被删除并替换为其他语法;
- 由于一切都即将改变,人们学习 Vue 所花的所有时间都被浪费了;
- 新的语法比旧的语法更差,没有强制结构,并且会导致意大利面条式代码;
- Vue Core 团队在没有任何协商的情况下突然实施了一项重大变更;
- Vue 正在转变为 React!
- 不,Vue 正在变成 AngularJS/Angular!
- 现在所有 HTML 都需要写成一个巨大的字符串!
Reddit 帖子里充斥着大量负面评论,当你访问RFC 页面时,你可能会惊讶地发现,尤先生的 RFC 中关于表情符号的正面和负面评论比例极高,而且很多初始评论都相当正面。事实上,第一条评论就充满了赞扬。
我是第一个评论的作者。我碰巧收到了新的 RFC 通知,立即阅读了它,发现它正是我想要的 Vue 3.0 版本,而且会非常有帮助,于是在 RFC 发布后的 15 分钟内就留下了第一条评论,表达我的感激之情。我希望在这里详细解释一下为什么我认为这个新提案是一个好主意,但首先,我想先回应一些批评。
我猜很多人在读了 Hacker News 或 Reddit 上一些带有误导性的评论后,会有些激动,甚至没看原始提案就表达了愤怒。Evan You 现在已经更新了提案,并附上了一份问答,解答了大家提出的许多问题。总结一下:
- 如果您不想重写任何代码,则无需重写——新语法是附加的,而旧语法在整个 Vue 3.0 版本中以及在广泛使用的情况下都将保持有效。即使旧语法最终从核心代码中移除,插件也可以轻松地使旧语法仍然 100% 有效。
- 学习 Vue 所花的时间并没有浪费——新的组件语法使用了您花时间学习的相同概念,其他概念(如单文件组件、模板和范围样式)的工作方式完全相同。
- 任何变更都是未经协商而做出的——RFC就是协商结果。新语法距离正式发布还有很长的路要走。
- 不,HTML 代码不需要写成一个巨大的字符串。
稍微主观一点的观点是,新语法不如旧语法,会导致代码结构性较差。我希望用一个简单的例子来解释为什么我看到 RFC 时如此兴奋,以及为什么我认为它更胜一筹,并且会带来更好的结构化代码。
考虑以下有趣的组件,它允许用户输入宠物的详细信息。请注意
- 当他们输入完宠物的名字时会显示一条消息;
- 在他们选择宠物的尺寸后,会显示另一条消息。
您可以在此处尝试该组件的演示,并可以在此处查看使用 Vue 2.x 的完整代码(参见 components/Vue2.vue)。
考虑这个组件的 JavaScript:
export default {
data() {
return {
petName: "",
petNameTouched: false,
petSize: "",
petSizeTouched: false
};
},
computed: {
petNameComment: function() {
if (this.petNameTouched) {
return "Hello " + this.petName;
}
return null;
},
petSizeComment: function() {
if (this.petSizeTouched) {
switch (this.petSize) {
case "Small":
return "I can barely see your pet!";
case "Medium":
return "Your pet is pretty average.";
case "Large":
return "Wow, your pet is huge!";
default:
return null;
}
}
return null;
}
},
methods: {
onPetNameBlur: function() {
this.petNameTouched = true;
},
onPetSizeChange: function() {
this.petSizeTouched = true;
}
}
};
本质上,我们有一些数据、基于这些数据计算出的属性,以及操作这些数据的方法。需要注意的是,在 Vue 2.x 中,没有办法将相关的内容放在一起。我们不能将数据声明放在计算属性或方法petName
旁边,因为在 Vue 2.x 中,内容是按类型分组的。petNameComment
onPetNameBlur
当然,对于像这样的小例子来说,这没什么大不了的。但想象一下一个更大的例子,它包含多个功能,需要data
、computed
、methods
,甚至一watcher
两个。目前还没有好的方法将相关的内容放在一起!人们或许可以使用 Mixins 或高阶组件之类的东西,但它们存在问题——很难看到属性的来源,并且存在命名空间冲突的问题。(是的,在这种情况下,可以将内容拆分成多个组件,但请考虑一下这个类似的例子,它不能拆分。)
新提案不再按选项类型组织组件,而是允许我们按实际功能组织组件。这类似于您在计算机上组织个人文件的方式——通常不会有“电子表格”文件夹和“Word 文档”文件夹,而是可能有“工作”文件夹和“假期计划”文件夹。请考虑使用提案语法编写的上述组件(我尽可能在不查看输出的情况下尽力做到最好——如果您发现任何错误,请告诉我!):
import { state, computed } from "vue";
export default {
setup() {
// Pet name
const petNameState = state({ name: "", touched: false });
const petNameComment = computed(() => {
if (petNameState.touched) {
return "Hello " + petNameState.name;
}
return null;
});
const onPetNameBlur = () => {
petNameState.touched = true;
};
// Pet size
const petSizeState = state({ size: "", touched: false });
const petSizeComment = computed(() => {
if (petSizeState.touched) {
switch (this.petSize) {
case "Small":
return "I can barely see your pet!";
case "Medium":
return "Your pet is pretty average.";
case "Large":
return "Wow, your pet is huge!";
default:
return null;
}
}
return null;
});
const onPetSizeChange = () => {
petSizeState.touched = true;
};
// All properties we can bind to in our template
return {
petName: petNameState.name,
petNameComment,
onPetNameBlur,
petSize: petSizeState.size,
petSizeComment,
onPetSizeChange
};
}
};
注意
- 将相关事物归为一类是极其容易的事情;
- 通过查看设置函数返回的内容,我们可以轻松地看到我们在模板中可以访问的内容;
- 我们甚至可以避免暴露模板不需要访问的内部状态(“接触”)。
最重要的是,新语法可以轻松实现完整的 TypeScript 支持,这在 Vue 2.x 基于对象的语法中很难实现。而且我们可以轻松地将可重用的逻辑提取到可重用的函数中。例如
import { state, computed } from "vue";
function usePetName() {
const petNameState = state({ name: "", touched: false });
const petNameComment = computed(() => {
if (petNameState.touched) {
return "Hello " + petNameState.name;
}
return null;
});
const onPetNameBlur = () => {
petNameState.touched = true;
};
return {
petName: petNameState.name,
petNameComment,
onPetNameBlur
};
}
function usePetSize() {
const petSizeState = state({ size: "", touched: false });
const petSizeComment = computed(() => {
if (petSizeState.touched) {
switch (this.petSize) {
case "Small":
return "I can barely see your pet!";
case "Medium":
return "Your pet is pretty average.";
case "Large":
return "Wow, your pet is huge!";
default:
return null;
}
}
return null;
});
const onPetSizeChange = () => {
petSizeState.touched = true;
};
return {
petSize: petSizeState.size,
petSizeComment,
onPetSizeChange
};
}
export default {
setup() {
const { petName, petNameComment, onPetNameBlur } = usePetName();
const { petSize, petSizeComment, onPetSizeChange } = usePetSize();
return {
petName,
petNameComment,
onPetNameBlur,
petSize,
petSizeComment,
onPetSizeChange
};
}
};
在 Vue 2.x 中,我经常发现自己编写的“庞然大物”组件很难拆分成更小的部分——它无法分解成其他组件,因为基于少量状态发生了太多事情。然而,使用建议的语法,很容易看出如何将大型组件的逻辑分解成更小的可复用部分,并在必要时将其移动到单独的文件中,从而获得小巧、易于理解的函数和组件。
这是 Vue 迄今为止最黑暗的一天吗?看起来是的。此前一直团结在项目方向上的社区如今已经分裂了。但我希望大家能重新审视这个提案,它不会破坏任何东西,仍然允许他们根据选项类型进行分组(如果他们愿意的话),但允许更多——更清晰的代码、更干净的代码、更多有趣的库可能性,以及完整的 TypeScript 支持。
最后,在使用开源软件时,务必记住,维护人员为免费使用的产品付出了大量心血。如今看到的一些近乎辱骂的批评,他们实在不应该容忍。值得庆幸的是,这些不尊重的评论只是少数(尽管数量不小),而且许多人都能够以更尊重的方式表达自己的想法。
2019年6月23日更新:
我写原帖的速度非常快,没想到它能引起如此大的关注。之后我意识到代码示例对于我想要阐述的观点来说太复杂了,所以我对其进行了极大的简化。原始代码示例可以在这里找到。