头图

从 Vue 2 到 Vue 3 的 Web 组件迁移之旅

原文链接:https://blog.rayberger.org/vue-3-web-components-open-library
作者:Ray Berger
译者:倔强青铜三

前言

大家好,我是倔强青铜三。作为一名热情的软件工程师,我热衷于分享和传播IT技术,致力于通过我的知识和技能推动技术交流与创新。欢迎关注我,微信公众号:倔强青铜三。欢迎点赞、收藏、关注,一键三连!!!

了解 Vue 与 Web 组件

与常见的单页应用(SPA)不同,Open Library 并非通过 Vue 渲染整个应用,而是利用 Vue 在传统 HTML 页面中实现特定的交互元素。这通过集成自定义 HTML 标签(即 Web 组件)来实现,这些标签随后被转换为 Vue 组件。例如,在 Open Library 页面的源代码中,你可能会看到 <ol-barcode-scanner> 标签。这个自定义标签及其 JavaScript 代码允许 Vue 仅渲染该特定元素,而不是管理整个页面。这就好比在传统的 HTML 页面中散布着一个个小型的 Vue 应用。

Vue 3 迁移挑战

虽然 Vue 3 与 Vue 2 大体兼容,且有许多资源可用于从 Vue 2 迁移到 Vue 3,但我们在构建 Vue 3 Web 组件时面临了挑战。Vue 3 文档虽然涵盖了 Web 组件,但其方法并不直接适用于我们的用例,即每个页面构建一个组件,并且脚本相互独立。

Vue 2 构建设置

在 Vue 2 中,构建 Web 组件的过程非常简单。我们只需运行 vue-cli-service build BarcodeScanner.vue,就会生成一个 JS 文件,我们可以在任何页面中使用它。

Vue 3 Web 组件的挑战

下面我将我们的迁移过程分为三个主要挑战。虽然每个挑战在事后看来都有简单的解决方案,但找到这些解决方案却耗费了大量时间。

使用 Vite 构建单个 Web 组件

我们之前用于构建 Vue 2 Web 组件的 Vue CLI 已不再支持 Vue 3 Web 组件,且多年来未更新。因此,我开始研究最新的构建工具:Vite。Vite 的一个主要区别在于它不接受 .vue 文件(单文件组件),而是需要一个配置文件作为输入。这带来了挑战,因为我 1)难以相信它不能直接使用 .vue 文件,而是需要为每个组件新增两个文件;2)试图让它在不新增这么多文件的情况下生成所有组件;3)尝试让一个 Vue 插件工作(稍后会详细说明)。不过,文档中对于单个组件的构建方法相对清晰,我只需要新增两个文件:

// vite.config.js
export default defineConfig({
    plugins: [vue({ customElement: true })], // 因为我们所有的 Vue 组件都是自定义元素
    build: {
        outDir: 'PRODUCTION_DIR',
        emptyOutDir: false, // 保留现有文件,因为我们是单独构建每个组件
        target: 'es2015', // Vite 默认支持的最旧浏览器版本
        rollupOptions: {
            input: join(BUILD_DIR, `BarcodeScanner.js`),
            output: {
                entryFileNames: `ol-barcode-scanner.js`,
                inlineDynamicImports: true, // 使组件能够通过单个 JS 文件工作
                format: 'iife' // 使用 iife 支持不支持 ES 模块的旧浏览器
            },
        },
    },
});
// BarcodeScanner.js
import { defineCustomElement } from 'vue';
import ele from './BarcodeScanner.vue';
customElements.define('ol-barcode-scanner', defineCustomElement(ele));

构建多个 Web 组件

此时,我们的 Vite 配置文件只能有一个输入,即一个 JavaScript 文件,它会生成一个输出,即所需的 JavaScript 文件。这一限制带来了挑战,因为我们有许多组件需要构建。虽然 Vite 配置文件支持多输入和多输出设置,但当我们使用 inlineDynamicImports 选项时,这一功能就不可用,而我们又需要这个选项来让组件独立工作。

我们面临的问题是,我们需要一个配置文件映射到一个 JavaScript 文件,而这个 JavaScript 文件又映射到一个 Vue 文件。这种设置似乎过于复杂。现在每个组件需要三个文件,而不是一个 .vue 文件。这些新增的文件对于每个组件来说基本相同。肯定有更简单的方法,对吧?

左侧是旧的 Vue CLI 设置,右侧是新的 Vite 设置。

试错过程

通过阅读文档、不断试错以及借助 ChatGPT,我们痛苦地接受了确实需要一个 Vue 文件、一个指向 Vue 文件的 JavaScript 文件,以及一个指向 JavaScript 文件的配置文件(希望有人能证明我是错的)。如果我们按照传统方式处理,每个组件都需要一个 Vue 文件、一个 JavaScript 文件和一个 Vite 配置文件。目前我们有五个组件,这意味着我们需要新增十个几乎相同的文件。肯定有更简单的方法,对吧?

解决方案

虽然这种方法看起来有些“黑客”风格,但它确实有效,而且没有在代码库中新增十个文件。它分为两部分。

首先,设置一个环境变量 COMPONENT_NAME,让 vite.config.js 读取它,这样我们就不需要为每个组件单独创建一个配置文件:

const COMPONENT_NAME = process.env.COMPONENT;
export default defineConfig({
    build: {
        // 省略部分代码以突出关键修改
        rollupOptions: {
            input: join(BUILD_DIR, `vue-tmp-${COMPONENT_NAME}.js`),
            output: { entryFileNames: `ol-${COMPONENT_NAME}.js`, },
        },
    },
});

其次,我们需要处理这些烦人的输入文件。因此,我们直接在配置文件中从字符串生成它们:

generateViteEntryFile(COMPONENT_NAME);

export default defineConfig({...})

function generateViteEntryFile(componentName) {
    const template = `
import { defineCustomElement } from 'vue';
import ele from './${componentName}.vue';
customElements.define('${kebabCase(componentName)}', defineCustomElement(ele));
`;

    try {
        writeFileSync(join(BUILD_DIR, `vue-tmp-${componentName}.js`), template);
    } catch (error) {
        // eslint-disable-next-line no-console
        console.error(`Failed to generate Vite entry file: ${error.message}`);
        process.exit(1);
    }
}

总的来说,这个解决方案简单且避免了维护大量几乎相同的文件。不过,我真的很难相信这些“黑客”手段(环境变量和生成输入文件)是最佳方案。

插件支持

除了一个使用了 Vue 插件 vue-async-computed 的组件外,其他所有组件都运行良好。我们之前考虑过移除这个插件,因为它不支持 Vue 3,但最终没有这么做,因为它有助于保持代码结构的清晰。Drini 几年前就提交了支持 Vue 3 的 PR,但由于该插件缺乏更新,也许我们应该考虑切换到更新的替代方案,如 computedAsync。但无论如何,我决心完成升级到 Vue 3 的任务。此外,Drini 也指出,升级后我们应该考虑使用一些不错的插件。

我尝试了各种方法将插件集成到我们的 Vue 组件中。我找到的指南通常深入到 Vue 的内部,特别是如何为组件集成插件。我们的团队并非由 Vue 专家组成,我们只是希望在网站的部分页面上增加一些交互性。

最终,我决定使用 vue-web-component-wrapper 库。说实话,我之前就看到过它,但不想为了本应简单的事情新增一个依赖。无论如何,这个库通过允许我们将所需的插件与组件一起传递,简化了整个过程。它简洁明了,我还为唯一需要该插件的组件添加了条件判断。

总结

从 Vue 2 Web 组件迁移到 Vue 3 Web 组件的过程中,我遇到了许多挑战。但现在我们已经找到了解决方案,这些挑战似乎并不那么难了。

我确信在迁移过程中遇到的困难是有技术原因的。但如果我有一根魔法棒,我希望做出以下改变:

  1. Vite(底层使用 Rollup)应该允许在使用 inlineDynamicImports 的同时支持多输入和多输出。仅此一项功能就能大大简化整个过程。如果无法实现,那么 Web 组件指南应该更新一个合理的解决方案。
  2. Web 组件指南应该增加关于插件的信息。解释如何在不引入额外依赖的情况下添加插件,或者至少指出 vue-web-component-wrapper 是一个可行的解决方案。
  3. Vite 应该能够在 vite.config.js 中直接接受 .vue 文件。

总的来说,这次迁移过程大约耗费了我 20 个小时(不包括前几年其他人的尝试)。在整个过程中,我没有找到一个能够涵盖所有这些挑战的示例。我希望这篇文章能对那些在 2025 年及以后从 Vue 2 迁移到 Vue 3 的开发者有所帮助。也许 Vue 和 Vite 团队会注意到这一点,并改进官方指南,甚至可能改进工具。祝开源顺利!

最后感谢阅读!欢迎关注我,微信公众号倔强青铜三。欢迎点赞收藏关注,一键三连!!!

倔强青铜三
41 声望0 粉丝