工作原理
为了说明我们的混合应用程序的架构以及 Vue 2 和 Vue 3 之间的交互,我们有两张图:一张代表整体流程,另一张展示模块关系。
上图展示了应用程序内模块的逐步迁移。它展示了如何使用 Vue 2 或 Vue 3 构建每个模块,从而实现开发灵活性。例如,一个模块可能完全由 Vue 3 构建,展示其全部功能和新功能,而另一个模块可能包含 Vue 2 和 Vue 3 组件的混合,利用现有的遗留代码和新开发的功能。
这种架构支持平稳的迁移路径,促进组件的可重用性和可扩展性。通过允许两个版本协同工作,我们可以逐步实现应用程序的现代化,最大限度地减少中断,同时确保兼容性和性能改进。
createApp 如何桥接 Vue 2 和 Vue 3
在我们的项目中集成 Vue 2 和 Vue 3 的关键在于 createApp 函数。此函数充当同步两个框架的桥梁,使它们能够顺利共享状态和路由处理。让我们分解代码以更好地理解这是如何实现的。
import { createPinia } from "pinia";
import { createApp as createVueApp, reactive } from "vue";
export const createApp = ({ app }) => {
return ({ name = "#pagelet", context = {} }) => {
const appInstance = createVueApp(app);
const route = reactive({});
const state = reactive({});
// provide global access to Vue 2 from Vue 3
appInstance.provide("route", route);
appInstance.provide("vuex", { ...context.$store, state });
appInstance.mount(name);
// Synchronize Vue 2 route and state with Vue 3
context.$watch("$route", (val) => Object.assign(route, val), { deep: true });
context.$watch("$store.state", (val) => Object.assign(state, val), { deep: true });
};
};
createApp
函数
createApp
函数旨在促进 Vue 3 应用程序在 Vue 2 环境中的集成。它由一个外部函数和一个内部函数构成,每个函数都有不同的用途。
外部函数:createApp
外部函数接受一个参数,即包含 app 属性的对象。此 app 表示您要渲染的 Vue 3 应用程序的配置或组件。
此外部函数的主要目的是建立一个可自定义且可重复使用的入口点,用于创建 Vue 3 应用程序。通过接受 app 作为参数,它可以灵活地指定在渲染过程中将使用哪个 Vue 3 组件或配置。
内部函数:返回的函数
createApp 函数返回另一个函数,其中包含初始化 Vue 3 应用程序的核心逻辑。此内部函数接受一个具有两个参数的对象:name 和 context。
name:
此参数的默认值为 #pagelet。它指定由 CSS 选择器标识的 DOM 元素,Vue 3 应用程序将挂载在该元素上。通过提供默认值,该函数可确保如果调用者未指定挂载点,则应用程序将在预定义位置呈现,从而增强可用性。context:
此参数默认为空对象。它旨在将上下文从 Vue 2 应用程序传递到 Vue 3 组件。上下文可能包含重要信息,例如状态管理实例、路由详细信息或 Vue 3 组件与现有 Vue 2 基础架构有效交互所需的任何其他相关数据。
代码解析
1. 创建 Vue 3 上下文(createVueApp): createVueApp 函数负责初始化 Vue 3 应用程序。这是 Vue 3 中与 Vue 2 中的 new Vue() 函数等效的函数。通过调用它,我们生成一个完全独立于 Vue 2 但稍后会连接的应用程序实例。
const appInstance = createVueApp(app);
2. 反应变量来同步数据:在这里,我们声明两个反应对象,路由和状态,它们允许我们在 Vue 2 和 Vue 3 之间保持状态和路由同步。这些反应对象将自动反映两个框架中的变化。
const route = reactive({});
const state = reactive({});
3. 将 Vue 3 中的依赖项注入 Vue 2: Vue 3 提供的 API 允许我们将依赖项全局注入组件层次结构。在本例中,我们将路由状态和 Vuex 状态从 Vue 2 注入到 Vue 3,使两个框架能够共享全局应用程序状态。
appInstance.provide("route", route);
appInstance.provide("vuex", { ...context.$store, state });
route:
这是我们在 Vue 2 和 Vue 3 之间同步路由的地方。vuex:
在这里,我们将 Vue 2 中的 Vuex 状态注入到 Vue 3 中,允许两个框架共享全局存储。
4. 在 Vue 2 容器内安装 Vue 3:通过调用 mount,Vue 3 应用程序将在由名称定义的 DOM 元素内呈现,该元素可以是 Vue 2 HTML 中的 div。这允许 Vue 3 在 Vue 2 管理的 DOM 内呈现其组件。
appInstance.mount(name);
5. 在 Vue 2 和 Vue 3 之间同步路由和状态:为了始终保持路由和状态同步,我们使用 Vue 2 观察器来监听 $route 和 $store.state 中的变化。每当 Vue 2 发生变化时,我们都会使用 Object.assign 更新 Vue 3 中的路由和状态对象,确保一个框架中的更改反映在另一个框架中。
context.$watch("$route", (val) => {
Object.assign(route, val)
}, { deep: true });
context.$watch("$store.state", (val) => {
Object.assign(state, val)
},
{ deep: true });
$route:
监视 Vue 2 中的路线变化并更新 Vue 3 中的反应式路线对象。$store.state:
在 Vue 2 和 Vue 3 之间同步 Vuex 状态。
在子应用程序中使用 createApp
在我们的架构中,子应用程序由其自己的 createApp 函数实例提供支持。这种方法使我们能够在不同的模块之间保持一致的结构,同时利用 Vue 3 的灵活性。通过使用 createApp,我们可以将各种组件无缝集成到我们现有的基础架构中。
以下是付款设置模块如何使用我们自定义的 createApp 函数初始化其自己的 Vue 3 应用程序的示例:
// payment-settings/src/app.ts
import { createApp } from "@org/core";
export const renderApp = createApp({
app: import(() => "./app.vue"),
});
入口点旨在促进新的 Vue 3 组件无缝集成到我们现有的 Vue 2 应用程序中。通过定义清晰的入口点,我们可以管理这些组件的渲染方式和时间,从而确保平稳过渡和增强功能。
这种模块化架构使我们能够独立隔离和管理新功能,从而简化开发并提高整个代码库的可维护性。通过结构化的切入点,我们确保 Vue 2 和 Vue 3 组件和谐共存,为未来发展提供坚实的基础,同时最大限度地降低集成的复杂性。
在内部,app.vue
我们使用 Vue 3 应用程序访问 Vue 2 模态组件并管理新技术堆栈中的表单更新。这种方法不仅保持了 Vue 2 和 Vue 3 之间的兼容性,而且还确保用户上下文中的任何更改都会立即反映出来。我们的集成旨在处理动态更新,使应用程序能够以最小的摩擦进行扩展和发展。
<script lang="ts" setup>
import { useApp } from "shared/composables/use-app";
const { context, refs } = useApp();
</script>
<template>
<div>
<button @click="refs.payments.openModal()">
edit payment settings for {{context.user.name}}
</button>
</div>
</template>
Vue 2 组件设置
在我们的 Vue 2 应用程序中,我们创建了一个组件,作为旧代码和新的 Vue 3 功能之间的桥梁。这个名为Payment operations 的组件负责呈现“编辑付款设置模式”和新的 Vue 3 功能(例如仪表板)。
这是 Vue 2 组件,它作为 Vue 2 和 Vue 3 功能的容器:
import EditPaymentSettingsModal from "../../components/edit-payment-settings-modal.js";
import store from '../../store/store.js';
import { renderApp } from '../../dist/payment-settings.js';
export default {
store,
components: {
"edit-payment-settings-modal": EditPaymentSettingsModal,
},
data() {
return {
user: {},
};
},
mounted() {
renderApp({ context: this, name: "#pagelet" });
},
template: `
<div>
<div id="pagelet"></div>
<edit-payment-settings-modal :user="user" ref="payments" />
</div>
`,
};
支付操作组件不仅仅是一个占位符;它在我们逐步实现应用程序现代化的战略中起着至关重要的作用。通过保留现有的 Vue 2 功能并集成新的 Vue 3 功能,我们可以增强用户体验,而无需承担大规模迁移带来的风险。
在此组件中,保留了旧版 Vue 2 结构,使“编辑付款设置模式”能够利用 Vue 2 的数据绑定通过用户对象管理特定于用户的信息。Vue 3 功能的集成发生在已安装的生命周期钩子中,其中调用了从 payment-settings.js 模块导入的 renderApp 函数。此函数将 Vue 3 组件动态注入到由 #pagelet 标识的 DOM 元素中,从而使 Vue 2 和 Vue 3 能够无缝共存。
随着新功能的逐步引入,这种方法有助于实现平稳过渡。我们不需要完全重写,而是在逐步更新其组件的同时保持现有应用程序的完整性。支付操作组件体现了我们致力于通过利用 Vue 3 的高级功能来增强用户体验,同时保持核心功能的可靠性。
设置捆绑器
我们使用 Vite 来创建模块。此设置可确保 Vue 3 组件可以在同一个 Vue 3 应用程序中共存,从而充分利用两全其美的优势。以下是 Vite 捆绑器的基本配置,它允许模块化构建和优化我们的应用程序交付。
import { defineConfig, Plugin } from "vite";
import vue from "@vitejs/plugin-vue";
import tsconfigPaths from "vite-tsconfig-paths";
import { transform } from "esbuild";
// ...other settings and functions
const { name } = args();
export default defineConfig({
plugins: [vue(), tsconfigPaths()],
build: {
lib: {
entry: `./apps/${name}/src/app.ts`,
name: name,
fileName: name,
formats: ["es"],
},
rollupOptions: {
output: {
assetFileNames: `${name}.[ext]`,
entryFileNames: `${name}.js`,
chunkFileNames: `[name].js`,
dir: `./apps/${name}/build`,
},
},
},
resolve: {
alias: {
shared: `${__dirname}/shared`,
},
},
},
});
为了补充我们的 Vite 配置,我们需要在 package.json 中添加一些脚本来构建特定的应用程序。如下所示:
{
"name": "@organization/app",
"type": "module",
"scripts": {
"dev": "vite",
"build:payment-settings": "vite build -- --name=payment-settings"
},
"dependencies": {
// other dependencies
"vue": "^3.4.37",
},
"devDependencies": {
// other dev dependencies
"@vitejs/plugin-vue": "^5.1.2",
"typescript": "^5.5.3",
"vite": "^5.4.1",
"vite-tsconfig-paths": "^5.0.1",
}
}
此设置确保我们可以有效地构建和管理我们的应用程序,从而实现 Vue 2 和 Vue 3 组件的顺利集成。当我们从旧框架过渡到现代框架时,拥有可靠的捆绑器配置对于维护应用程序的完整性和性能至关重要。
微前端替代品
在从 Vue 2 迁移到 Vue 3 的过程中,采用微前端模式可以显著增强应用程序的架构。以下是一些需要考虑的关键模式:
-
模块联合:此模式允许您在运行时动态共享和加载组件,使不同的团队能够独立处理应用程序的各个部分。通过利用模块联合,Vue 2 和 Vue 3 模块可以共存并进行更新,而无需完全重新部署应用程序。
-
服务器端组合:在此模式下,Vue 2 和 Vue 3 组件可以在服务器端呈现,并作为单个响应传递给客户端。这种方法有助于缩短初始加载时间并增强 SEO 功能,因为用户无需等待客户端 JavaScript 执行即可获得完全呈现的页面。
-
客户端组合:与服务器端组合类似,此方法允许您在单页应用程序上下文中加载 Vue 2 和 Vue 3 组件。使用基于布局的方法,您可以为组件定义不同的入口点,确保根据应用程序状态适当地呈现两个版本。
-
iframe 嵌入:为了实现更显著的分离,您可以将 Vue 3 应用程序嵌入 iframe 中。这样,团队就可以构建和部署新功能,而不必担心与现有 Vue 2 代码库发生冲突。但是,这种方法需要仔细处理主应用程序和 iframe 之间的通信,以确保一致的用户体验。
-
动态导入:使用动态导入,您可以按需加载 Vue 3 组件,这有助于减少初始包大小。此策略可让您的应用程序在根据需要加载新功能的同时保持响应速度,无缝集成新的 Vue 3 组件和现有的 Vue 2 组件。
-
向后兼容性:实现向后兼容层可以简化迁移过程。该层使 Vue 3 组件能够与 Vue 2 的 API 交互,确保两个框架能够和谐地协同工作,直到过渡完成。
好处
- 逐步迁移:您可以逐步重构代码库,从而允许两个框架在过渡期间共存。
- 兼容性:使用 Vue 3 构建新组件时,现有的 Vue 2 组件和状态管理(例如 Vuex)仍可正常运行。
- 现代化和面向未来:利用 Vue 3 的功能(例如 Composition API 和 Pinia),您可以实现应用程序架构的现代化并为未来的更新做好准备。
- 样式隔离:通过为 Vue 3 组件使用自定义 CSS 框架,您可以确保样式被封装并且不会干扰现有的 Vue 2 应用程序。这种方法最大限度地降低了样式冲突的风险,并提供了一种更有条理的样式管理方式。
总结
在大型应用程序中从 Vue 2 迁移到 Vue 3 不一定是一项艰巨的、孤注一掷的任务。通过采用混合方法,您可以利用 Vue 3 的强大功能,同时保持旧代码的功能和兼容性。这种策略不仅可以确保平稳过渡并最大限度地减少中断,还可以让开发团队按照自己的节奏进行现代化改造。
值得注意的是,这一概念并不局限于 Vue;它可以应用于各种框架。无论采用哪种技术堆栈,逐步迁移、集成新功能和利用自动化工具的原则始终保持一致。这种灵活性使团队能够根据特定需求和优先级增强其应用程序,确保它们能够有效地发展和适应。