原文:Vue’s Darkest Day 作者:Daniel Elkington

译者注:原文写于2019年6月21日

今天,我惊讶的发现,往常积极友好的 VueJS 社区陷入了一场激烈的战争。
两周前,Vue 的创建者尤雨溪发布了一个请求意见稿(RFC),用于在即将发布的 Vue 3.0 中使用基于函数的方式编写 Vue 组件。今天,一个 Reddit 上批评性的帖子Hacker News 上一些类似的批评性的评论,引起大批开发者涌向原本的 RFC 来表达他们的愤怒,其中一些有点侮辱性。在很多地方都有人声称:

  • 所有 Vue 代码都必须以全新的方式重写,因为现有的语法正在被移除,并且被其他东西取代;
  • 人们花在学习 Vue 上的所有时间都被浪费了,因为一切都会改变;
  • 新语法比旧的更糟糕,因为它没有强制的结构,并且会导致意大利面条式代码;
  • Vue 核心团队在没有任何咨询的情况下突然施行一个巨大的破坏性的变化;
  • Vue 要变成 React 了!
  • 不,Vue 要变成 AngularJS/Angular 了!
  • 所有 HTML 都要写在一个超长的字符串里!

看过 Reddit 上成堆的负面评论,你可能会在 RFC 页面上惊讶的发现尤雨溪的 RFC 收到的正面的表情回应的比例比负面的高得多,而且许多早期评论都是相当正面的。实际上,第一条评论就充满了溢美之词。

我就是第一个写评论的人。我碰巧收到新 RFC 的通知,马上读了一下,发现这正是我想从 Vue 3.0 得到的,而且它会给我极大的帮助,于是我在 RFC 发布 15 分钟后留下了第一条评论来表达我的谢意。我希望在这里进一步说明为什么我觉得新提案是一个如此好的主意,但首先,要回应一些批评。

我怀疑很多人在阅读了 Hacker News 或 Reddit 上有着很多误导性评论的帖子之后有点激动,他们在没有阅读原始提案的情况下就表达了自己的愤怒。尤雨溪已经更新了这个提案,通过问答的方式回应了人们的很多问题,总的来说:

  • 如果你不想重写任何代码,那么你就不需要重写——新语法是附加的,而且只要旧语法仍然被广泛使用,它在 Vue 3.0 中依然有效。就算它最终被从核心代码中移除了,也可以很容易地通过插件来使旧语法 100% 有效
  • 学习 Vue 的时间并没有浪费——新组件语法使用的概念与你之前花时间学习的一样,其他概念,例如单文件组件、模板、scoped style 的功用完全一样。
  • 没有经过咨询,就不会改变—— RFC 就是在 咨询。新语法离发布还有很长一段路要走。
  • 不,HTML 代码不需要被写进一个超长字符串。

一个更主观的观点是:新语法不如旧语法,并且会导致结构化程度较低的代码。
我希望通过一个简单的例子来说明为什么我在看到 RFC 时如此兴奋,以及为什么我觉得它更优秀,将会导致结构化 更好 的代码。

考虑一下下面的有趣组件,用户可以输入宠物的详细信息。请注意:

  • 当他们输入完宠物的名字时会显示一条信息;
  • 另一条信息会在他们选择宠物的大小后显示。


你可以在这里尝试组件的demo,也可以在这里查看使用 Vue 2.x 编写的代码(在 components/Vue2.vue)

考虑一下这个组件的 JavaScript:

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
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 数据声明放在 petNameComment 计算属性或者 onPetNameBlur 方法旁边,因为在 Vue 2.x 中,这些选项是按照类型组织的。

当然,对于像这样的小例子来说,这不太重要。但是想象一个更大的例子,它有很多功能,需要 datacomputedmethods、甚至是一两个watcher。目前还 没有好方法 来把相关的东西放一起!有人可能会使用诸如 Mixin 或高阶组件之类的办法,但是它们都有问题——很难辨别一个属性来自哪里,还有命名空间的冲突。(是的,在这种情况下,拆分为多个组件是可能的,但是这个类似的例子就不行)

新提案不是按照选项的类型来组织组件,而是允许我们按照实际功能来组织组件。这类似于你在电脑上整理个人文件的方式——你通常没有“表格”文件夹和“Word 文档”文件夹,相反,你可能有一个”工作“文件夹和一个”假期计划“文件夹。想象一下使用提案里的语法来编写组件(尽我所能,如果你看到了什么 bug 请告诉我):

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
45
46
47
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
};
}
};

注意:

  • 很容易把相关的东西放到一起;
  • 通过查看 setup 函数的返回值,我们可以很容易地知道模板中可以获取什么变量;
  • 我们甚至可以避免暴露模板不需要获取的内部状态(touched)。

除此之外,新语法可以有完整的 Typescript 支持,这在 Vue 2.x 基于对象的语法中很难实现。而且我们可以很轻易地把可重用的逻辑提取为可重用的函数。例如:

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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日更新:

我很快就写好了原文,并没有期望它能得到这样的关注。然后我意识到这个代码示例对于我想要表达的观点来说过于复杂,所以我把它简化了很多。原本的代码示例在这里