之前在公司花了大约一个星期的时间把负责开发的会员卡模块从 Vue1 升级到了 Vue2,中间经历了不少坑,其中大部分是因为原本的代码偏离 Vue2 的最佳实践比较多(主要是数据流是双向还是单向),另一部分是因为 Vue2 底层的变化(引入了 Virtual DOM)。

升级的缘由及考量

升级的考量无非两个方面,是否满足现在和以后的需求,升级的好处和代价分别有多大。

需求

  • 首屏服务端渲染的需求,由于存在两套技术栈,一套是 PHP 模板,一套是前后端分离的 Vue 和 Play for Scala,当从旧有页面切换到 Vue 编写的页面时,会有明显的空白加载时间
  • 组件校验的需求,当时还是官方推荐的 vue-validator 还不支持这一点,但在它 3.0 的计划中,并且只支持 Vue2。 这也是一个很大的考量因素,即 Vue 周边的一些东西最后可能都会根据 Vue2 来开发和迭代。
  • 一些已有的 bug,但在 Vue2 中修复了。比如 v-model 的 number 修饰下, input 为空时会变成 0(后面看到这个 bug 在 Vue1 中也修复了,但并不能排除一些小 bug 可能不会再考虑 Vue1)。 好处

除了解决上述需求和问题之外,我暂时没有找到一个让我从 Vue1 升级到 Vue2 决定性的优势。

代价

  • 迁移所需要的时间,这个时间不真的开始改代码其实很难估计,当时我用 vue-migrate-tool 检测出大约几百处需要修改(这里不包括 Vuex),我预计的时间是两到三天,实际花了一个星期。而且在升级过程中,项目是无法完整运行的(为什么说完整?因为只要一个模块一个模块改,就能保证有一部分是可运行可测试的)。以及包括后续升级其他也用 Vue1 开发中或开发完的项目所需时间。
  • 项目迁移完成之后的风险,即使有良好的单元测试也无法保证和之前 Vue1 版本表现是真的一致了
  • 学习成本,这个比起 Angular 是好多了,大多数变化都在 API 改名字之类的语法层面,而且大多数变化是在做减法,在减去一些不常用的或是累赘的 API,但其实 Vue2 改动了一些底层的东西(主要是 Virtual DOM),导致语义风格上其实有一些变化了(我认为是开始偏向 React 了)
  • 服务端渲染方面,就不仅仅是前端的问题了,整个前后端的架构也要动,涉及到后端和运维方面的改变。要考虑是不是加一层 Node 作所谓后端的前端,可行性和整个项目进度是否允许等等。(最终这个项目没有用 Vue 的服务端渲染,而是用 Play 直出一部分公共的组件(侧边栏、顶部导航等),再加载 Vue 的 JS 文件去渲染剩下的部分)

总结一下,如果没有找到一个促使你升级的决定性因素,升级要谨慎,可以考虑还在开发中的以及后续项目尽量遵循 Vue2 一些最佳实践,少用 Vue2 废弃的东西。

Vue2 VS Vue1

回到正题上,Vue2 相比 Vue1 有了哪一些比较重要的变化?

  1. 单向还是双向?

    知乎上有个问题问 Vue 所谓的渐进式框架是什么意思,答案大多都说渐进式是主张最少的意思。那我想在单向还是双向这个问题上,Vue 还是主张了。在 Vue1 中,子组件可以修改父组件传给它的 prop,而在 Vue2 中不行。而我在用 Vue1 开发的过程中,大量使用了双向数据流,把之前的 sync 改成自定义事件这部分工作占据了我迁移的大多数时间。

    我在哪些地方用了 sync ?

    • 通用基础组件

      比如 Popup, Sidepage 这类组件都需要一个变量来控制是否显示。双向数据流下,这个变量可以由父组件置为打开,当需要关闭的时候由子组件置为关闭。单向数据流下,需要关闭时,由子组件触发自定义事件,父组件监听该事件并在捕获到事件后将变量置为关闭状态。(其实 Vue1 下还有一种做法,就是这个变量可以是子组件的本地数据,父组件触发子组件的事件来完成操作,但是在 Vue2 中事件流只能向上不能向下)

      这个场景下,简单地比较一下两者的差异,双向简单(写起来觉得爽),单向略为复杂但是仍有好处。扩展一下这个简单场景,如果关闭不仅仅是关闭,而是在关闭的同时要做一些其他事情,那么双向情况下,控制权在子组件,你需要传一个回调函数给子组件,而单向情况下,控制权在父组件,只要在原本的事件回调中增加逻辑。所以其实最终要考虑的是,数据的控制权究竟归属于父组件还是子组件,还是说比较中立,无论谁来操作都没有影响。

    • 多层嵌套组件

      组件需要嵌套多层,有一个祖先组件的数据,在它的后代中层层传递,很多个组件都需要它,最终的数据修改发生在比较深的地方。这种情况其实不太需要多讨论,双向情况下,如 Vue 文档里说的,考虑数据的安全性,任意子组件都可能修改这份数据,容易出现未知的错误。而单向情况下,操作起来就比上面那种两层的情况更麻烦了,最终我把这个情况全部改成了用 Vuex 来做。(之前在 Vue1,我只有在非父子的横向关系共享数据上使用 Vuex)

    注: 文档提及 Vue2 中有一个例外,就是如果是引用类型的数据,就没法阻止子组件去改变父组件的数据。

  2. 渲染函数与 Virtual DOM

    我觉得这是 Vue2 中最重要的改变,即在 watch 和 依赖收集之上,修改真实 DOM 之下,加了一个通过渲染函数计算新的 Virtual DOM 的过程。模板最终都会被编译成渲染函数,以及这个渲染函数是组件级别的,这意味着要重新审视模板了。

    模板中可能出现的有:

    • 普通的数据绑定
    • 动态绑定的 DOM 属性
    • props
    • 事件回调
    • filter
    • directive

    模板中上述任一部分都可能会出现副作用,而且这些副作用会随着渲染函数的计算一遍遍地重复。比如在 props 中使用了引用类型的字面量,会导致每次渲染都新建对象,涉及的问题有内存和性能开销以及可能出现引用类型之间比较引起的未知错误。再比如,在 filter 中做输出操作,会觉得明明没有修改 filter 作用的数据却仍然调用了 filter 。(因为渲染函数是组件级别的,组件的任意数据更改都会响应式地去调用渲染函数)如果不重新看待模板,不改变旧有的编码风格,就可能出现意想不到的错误。

以上两个重要变化,单向数据流(数据向下,事件向上)与渲染函数(Virtual DOM),使我觉得 Vue2 在语义风格层面在向 React 靠近。可能未来各个框架之间,差异会一直变小,框架之间会一直相互借鉴。这是一件好事,因为框架最终都是拿来解决问题的,而不是拿来划分领地的。但不禁又想,前端下一个类似 Virtual DOM 引领潮流的新功能或是设计会是什么?