从 Vue 2 迁移到 Vue 3 的技巧

Yehor Pytomets · 关注

发表于 TeamDev Engineering · 阅读 14 分钟 · 2023 年 1 月 26 日

本文是与前端应用程序从 Vue 2 迁移到 Vue 3 生态系统相关的技巧集合。 它包含我们的团队在 Pure Photos 前端应用程序迁移过程中选择的迁移策略、问题的解决方案以及官方指南中未涵盖的一些要遵循的提示。 对于计划将 Vue 2 应用程序迁移为官方和社区指南的补充的开发人员以及开始新的 Vue 3 项目的开发人员来说,它可能很有用。

Vue 3 是 Vue 框架的最新版本。 它通过逻辑关注点提供了更好的代码组织、更好的性能、贡献者的更好支持以及 Vue 2 中不可用的新功能。此外,Vue 2 将于 2023 年底达到生命周期结束。考虑到这一点,这是一个很好的选择 决定将现有代码库迁移到 Vue 3,为更好的维护和新功能开发做好准备。

从哪儿开始?

从广泛的角度来看,我们将整个迁移路径分为几个步骤,每个步骤都是独立且完整的。 这意味着构建应用程序的迁移部分,运行测试,直观地检查结果,修复不一致之处,并将其交付到暂存环境。 如果您有一个大型应用程序,那么从结构上将其划分为较小的部分并逐一迁移它们可能会很有用。 首先,我们迁移了用于构建和测试应用程序的实用程序,然后在应用程序的一小部分上尝试了 Vue 3 的新功能,最后,继续进行其他部分。

为了迁移应用程序,我们遵循了几个指南,其中一些指南如下:

  • 从 Vue CLI v4 到 v5 的迁移指南
  • 从 Vue 2 到 Vue 3 的官方迁移指南
  • 社区开发的 Vue CLI 和 Vue 迁移指南
  • Vue RFC(征求意见)
  • Vue 3 文档
  • Pinia 商店从 Vuex 迁移
  • 从 Vue Router 3 到 Vue Router 4 的迁移指南

整篇文章中提到了用于迁移其他工具的更多链接。

Vue CLI

Vue CLI 是一个基于 Webpack 的官方工具链,用于构建 Vue 应用程序。 还有另一个不依赖 Webpack 的构建工具,推荐用于新的 Vue 3 项目 - Vite。 但我们决定保留 Vue CLI,因为我们在构建脚本中依赖 Webpack。 将配置更改为 Vite 需要重新设计我们解决应用程序中模块和图像导入、路由器中组件延迟加载以及创建 PWA 配置的常用方式。

此步骤的目标是提供所需的最小变更集,以便我们可以构建前端应用程序而无需更新框架本身,因为这需要许多更改和时间。 在这一部分中,我们将 Vue CLI 工具从版本 4 迁移到版本 5 及其插件,以使该工具正常工作并启用对 Vue 3 的支持。我们按照本指南来实现这一目标。

工作箱和服务人员

迁移的 Vue CLI PWA 插件将 workbox-webpack-plugin 从 4.x 更新到 6.x 版本。 Workbox 是一组库,可简化 Service Worker 脚本中的路由和缓存配置。 workbox-webpack-plugin 的 6.x 版本提供了几个重要的功能。

Service Worker 脚本文件和任何嵌套级别的导入文件的导入解析。 这允许为不同前端应用程序的多个服务工作线程脚本提供通用代码库。 每个应用程序的 Service Worker 脚本的配置位于 Vue CLI 配置文件的 pwa 属性中 - vue.config.js :

请在此处查看 InjectManifest 模式的 Workbox 配置选项的完整列表。

预缓存清单不再可通过 self.__precacheManifest 变量获得。 相反,该插件会在顶级服务工作线程文件中查找 self.__WB_MANIFEST 变量,并将其替换为预缓存清单对象:

说明在多个应用程序之间共享服务工作线程脚本的代码是要点。

ESLint

更新 @vue/cli-plugin-eslint 时,我们遇到了 eslinteslint-plugin-vue 软件包,必须将它们更新到最新版本。 babel-eslint 解析器被发现已弃用,然后替换为 @babel/eslint-parser

再生器运行时

迁移 Vue CLI 和其他库后,我们生成了第一个生产版本。 regeneratorRuntime 变量定义由于某种原因在生产代码中消失了。 需要再生器运行时来支持转译的异步函数。 因此,我们必须恢复变量定义。

@babel/plugin-transform-runtime 工具现在负责在生成代码中定义 regeneratorRuntime

还值得注意的是,我们通过在 中将 transpileDependencies 标志设置为 true 来转译所有依赖项 vue.config.js。 在本地提供 Web 应用程序时,禁用此标志不会将 regeneratorRuntime 注入已编译的代码中,即使 @babel/plugin-transform-runtime< /span> 在编译时并行使用。

regenerator-runtime 包不再适用于我们的项目,因此被删除。

组件

组合 API 与选项 API

迁移组件之前要做的第一个决定是选择组件的样式。 Vue 3 为此提供了两个选项——Options API 和 Composition API。 尽管官方文档中对这两者进行了描述,但我们想概述一下为什么我们更喜欢 Composition API 而不是 Options API,并使其成为编写组件的唯一方式。

Composition API 看起来更像是带有函数和变量的传统 JavaScript,而不是带有类和属性的 JavaScript。 这使得小型组件保持简单,并将复杂的结构模式应用于大型组件。

Composition API 建议将逻辑相关的代码组合在一起。 在具有 Options API 的 Vue 2 和 Vue 3 中,我们有一个具有用于定义组件的特定属性的单个对象:反应式状态、计算状态、组件属性、方法、反应式更新的观察者等。通过 Composition API,我们定义反应式和计算式状态 作为变量、方法 - 作为函数,并将回调传递给导入的生命周期挂钩。 这使我们可以自由地在组件中组织代码,并将逻辑相关的变量和函数分组到单独的块中——可组合函数或可组合项,将组件组装在一起。 我们可以将定义某些公共或抽象状态的可组合项导出到单独的文件中,以将它们用作多个组件的构建块。

Composition API 提供了多种定义反应性状态的选项——我们可以深度观察变量、浅层观察变量,或者编写自定义反应性策略。 我们可以为每个单独的变量选择一种策略,以便在组件中做出反应。 在我们的非标准反应策略的用例中,我们应用 shallowRef() 来观察变量引用的变化,而不是深入观察整个对象:

由于可组合项取代了 Composition API 中的 mixins,因此它们让我们可以显式声明某个组件中的给定可组合项将使用哪些响应式状态和函数。 对于 mixin 来说这是不可能的,因为 mixin 中定义的所有内容都成为组件上下文的一部分,即使只需要某些状态或方法。 反对 mixin 的另一个论点是,目前还不清楚在组件内部应用 mixin 后到底能得到什么。 要查看可以使用哪些状态或函数,您必须打开 mixin 实现,找到所需的属性或方法,然后在组件内使用它。 此外,乍一看,如果将两个具有重复属性和方法名称的 mixin 应用于单个组件,那么合并策略是什么? 下面的代码说明了将移动设备上关闭侧边栏导航的 mixin 迁移到可组合项的过程:

支持 Composition API 的另一个原因是我们希望学习一种新的方法来设计和实现看起来更有趣、更灵活的组件。

尽管 Composition API 看起来很完美,但它可能会在设计组件时带来困难。 实现抽象组件变得更加困难——现在不鼓励使用 Class API,因此我们决定放弃将组件定义为类,转而使用 Composition API。 在 Vue 2 中,我们使用 mixin 来定义抽象和通用的状态和功能。 尽管 mixin 可以被重新设计为可组合项,但在组件的脚本块之外定义的内容仍然存在限制。 defineProps()defineEmits()defineExpose() 是编译器 宏只能在 <script setup> 内使用。 一般来说,可以通过为每种情况设计解决方法来解决 - 例如通过在可组合项中手动实现运行时检查来替代属性而不是 props:

Composition API 的另一个主要缺点是 beforeRouteEnter 挂钩和 inheritAttrs 选项仅在 <script> 块。 所以,这就需要我们在某些组件中将Composition API与Options API一起使用。 请参阅 beforeRouteEnter 上的注释:vuejs/rfcs#78 和 vuejs/rfcs#302。

我们决定使用 Composition API 作为默认值以避免混合样式。 选项 API 保留在无法以其他方式编写的地方。 坚持单一风格有助于抵制用其他风格编写某些代码的诱惑。

在 Composition API 中有两个地方可以编写代码 - <script setup> 块和 setup() 函数 <script> 块。 脚本设置方式提供了更好的运行时性能并且更易于阅读。

结构

简单单文件组件的常见结构:

在极少数情况下,大型组件会声明过多的数据(反应变量、计算属性、导入的可组合项中的变量)和函数。 在这种情况下,通过逻辑关注点将此类数据和函数拆分为不同的可组合项非常有用。

一些相互引用和调用的数据和函数组应该形成一个可组合项,而其他数据和函数应该形成另一个可组合项。 每个可组合项应仅公开用于 或作为其他可组合项的参数提供的数据和函数。 这种分组避免了组件成为相互引用的函数、属性和变量的可靠列表,但并没有带来任何组件实际功能的意义。

如果多个可组合项使用从外部导入的相同变量或函数,那么它们应该接受该变量或函数作为参数。

这种复杂组件结构的一个示例是 UserTemplateView 组件。 请注意组件中的可组合项定义如何遵循通用组件结构。 这种分组的灵感来自 Composition API 文档中的一个示例组件。

这些是在迁移多个大型组件时采用的做法,但最好还是坚持适用于大多数组件的通用结构。

反应状态

我们更喜欢使用 ref() 而不是 reactive() 来声明反应状态,原因如下:

  • ref 可以包装任何基元和对象类型的值,而 reactive 仅包装对象;
  • ref 包裹的对象可以在不失去反应性的情况下解构,而用 reactive 包裹的对象 不能。
  • ref 值可以是反应性,而反应性变量则不能 - 它将失去反应性。< /里>

对此的更多想法:

  • 对 Vue 3 Composition API 的思考 - reactive() 被认为是有害的
  • Vue 3 组合 API:引用与响应式
  • Vue 3 组合 API:ref()reactive()
  • Vue 3 中的
  • ref()reactive() 比较?

全局属性

在 Vue 2 中,我们曾经通过访问该对象来拥有所有组件中可用的属性。 这些属性引用了有用的常量和服务,例如视图之间导航的常用路线或翻译功能:

在 Composition API 中,建议将这些常量和服务设为全局,并将它们导入到需要它们的组件中。 此外,脚本设置块中导入的任何内容都可以按原样在模板中使用。 作为替代方案,您可以在组件内部使用 getCurrentInstance() 来访问其实例对象或依赖注入机制,但与传统导入相比,它会产生额外的开销:

程序化组件创建

在 Vue 2 中,我们可以使用编程方式创建组件 $mount():

在Vue 3中,没有这样的方法,所以我们使用动态组件来代替:

$children replacement

随着 Vue 3 中 $children 属性的删除,没有正式的方法可以访问通过脚本块中的槽提供的组件。 在 Vue 2 中,此属性允许访问模板中使用的或通过槽提供的当前组件的子组件:

我们提出了自己的解决方案,因为迁移指南没有任何方法来替换此 API。 在模板中,可以使用 ref 属性来标记特定的子组件。 然后,它可以通过以标签命名的变量在脚本设置块中使用:

If you need to access a list of components provided via slots, you can use the following migration strategy that combines the usage of template refs, dynamic components, and components props. Instead of providing components via slots, provide an array of objects via props, where each object contains two fields — a component to render and the object of properties to pass to the rendered component. The provided array should be iterated in the template of the current component to render its objects as dynamic components. This strategy allows to migrate such cases with minimal changes if your old component relied on the $children property:

Client-side models

Keep models simple and immutable. Do not reference the original object of the model anywhere in the app if it is intended to be reactive and displayed on the UI. When creating the reactive reference to the object, Vue 3 creates a proxy, that is a deep copy of the original object and triggers reactive updates when its state changes. By referencing the original object somewhere in the app and changing its state, the reactive updates will not trigger, as these are different objects:

In Vue 2 referring to the original object would not break reactivity, as this version of the framework just overwrites getters and setters of the original object to make it reactive. In Vue 3, the object should be updated by referring to its reactive variable.

Limit interactions with the original non-reactive object. Make it reactive or set to the store as soon as the object is created or fetched from the server, and then refer to the reactive variable:

Do not start asynchronous updates of the object from inside the constructor as the object’s this refers to the initial object, but not to the reactive proxy:

Extract asynchronous updates into a separate function/service/store action and keep the model immutable:

Vue 3 reactivity system encourages developers to keep models immutable or plain (with getters and setters) — depending on your preference, with complex modification logic extracted to functions, services, and store actions.

State Management

The official state-management library in Vue 2 is Vuex. In Vue 3, the official state-management library is Pinia. It is built on top of the Composition API and requires less code to implement a single store. Also, it requires less code to access the store inside your application.

Defining a store

Pinia comes with two options to define a single store — options object and setup function, which are similar to Vue’s Options API and Composition API styles respectively.

With options objects we have separate blocks for defining state, getters, and actions:

With the setup function, we use Composition API features for defining a store:

The official documentation doesn’t tell which style is more beneficial in terms of performance. But we prefer options objects for their structure and simplicity.

State reactivity

A store state acts like it was defined with refs. If you need another reactivity strategy for state, apply custom versions of ref() function to state fields:

Store destructuring

A single store can be used in any component and destructured to simplify access to its state and actions:

Filtering actions in store plugins

Sometimes, we may need to observe the effects of a specific action, defined in a certain store. Store plugins come in for such tasks. The store object involved in filtration has an inconvenience in its structure when it comes to filtering the right action in a plugin — we cannot access the action name from the store object:

For example, given the Auth store with the setUser action:

Inside a plugin, it could be useful to find the setUser action by its name from the store object, like we do when filtering the right store by ID:

From the example above it is clear that we must use string literals for referring to store actions in such cases. To avoid producing string literals in plugins, declare the action names used in filtering in $onAction observers near the store definition. Maintaining the store structure outside the store is one of the disadvantages of Vuex and a reason why Pinia is created:

The proposal to maintain such action names on the Pinia side was discarded. More on that request: vuejs/pinia#1408.

Getter function caching

In case you need to pass arguments to a store getter, it’s better to rework the getter into the store action or function outside the store. Another approach would be to return a function from a getter. But with this approach, the getter is not cached anymore, so it performs like a regular action or function. Unless you use some reactive data in the getter body, outside of the returned function, like in the example below. In this case, the getter is justified in terms of performance benefits:

Inside a component, use such a getter directly or pass an argument to it inside a computed property or function:

Router

With the release Vue 3, the Vue Router team has introduced a new major version of a vue-router— 4.x. This is the current version of Vue Router used in our app.

Catch-all routes

Catch-all routes regex has changed:

More thoughts on that:

  • Catch all / 404 Not found Route
  • Removed * (star or catch-all) routes

Libraries migration

In this part of the article, we provide the list of third-party NPM libraries to which we migrated because their Vue 2 counterparts no longer work in Vue 3 projects. At first, when migrating a single library, try to check if it has an upgraded version for Vue 3. If it doesn’t, look for the library fork or its closest alternative that ports the original library to Vue 3 to migrate your codebase with the least effort.

I18n

@panter/vue-i18next for Vue 2 -> i18next/i18next-vue for Vue 3.

Provides internationalization of Vue components using i18next framework. See the migration guide. The library has the following issues that were fully or partially resolved during migration:

  • i18next object retrieved from is not reactive: i18next/i18next-vue#6
  • t() function retrieved from useTranslation() cannot be restricted to a certain namespace and key prefix. Limit the scope of a SFC component to some namespace and key prefix: i18next/i18next-vue#7
  • New <TranslationComponent> for inserting HTML and Components as interpolation values for localization terms: i18next/i18next-vue#8

The policy of choosing between $t() and t() functions in a template is to always choose t() returned from the useTranslation() composable to strive for consistency.

VueUse

VueUse is a handy library of composable utilities for Vue 3 that is actively maintained and provides common solutions that used to be separate packages in Vue 2.

Vuelidate

vuelidate for Vue 2 -> @vuelidate/core and @vuelidate/validators for Vue 3.
Used for validation of forms in the application. See the migration guide.

Clipboard

vue-clipboard2 for Vue 2 -> @vueuse/core for Vue 3.
Use useClipboard() composable for accessing Clipboard API.

Touch events

vue2-touch-events for Vue 2 -> vue3-touch-events for Vue 3.
Provides Touch Events API for Vue 3. The community-developed library that took precedence over its ancestor and became its replacement in Vue 3 components.

Datepicker

vuejs-datepicker for Vue 2 -> vuejs-datepickervuejs-datepicker for Vue 3.
Provides the Datepicker component for Vue 3. The community-developed library that has almost the same API as its ancestor and became its replacement in Vue 3 components.

Template compiler

Removed vue-template-compiler as it is being shipped with the vue package.

Slides

vueperslides 1.x -> 3.x.
The library that implements a slideshow of images.

Upload component

vue-upload-component 2.x -> 3.x.
The library that covers low-level details of uploading a file and provides an upload component with drag-and-drop mode.

Conclusion

In this article, we provided you with a collection of tips that can help you migrate a frontend application from Vue 2 to Vue 3 ecosystem.

Most of these tips are not covered by the official guides, so this article may serve as a handy reference for some uncommon migration solutions.

   |    备案号:京ICP备09015132号-1044