Vue3+webpack实现组件远程加载

5 分钟阅读

Vue3 + webpack 实现组件远程加载

1. 需求背景

最近接到一个运营需求:在页面中植入活动弹窗,且活动内容需要经常替换。

最朴素的方案是把活动组件写在本地、每次发版上线。但对单页应用来说这条路有个硬伤:用户不刷新或重新进入页面,就不会向服务器请求新资源——发版了,在线用户看到的还是旧活动。而产品的要求是:广告发布后,用户进入页面就要看到最新内容,后续还可能随机展示、频繁替换。

要满足这个需求,组件资源必须脱离发版流程,改为运行时远程加载。下面记录我们排除掉两个方案、最终落地的完整过程。

2. 被否掉的两个方案

方案一:lowcode 搭建平台

同事的思路是做一个简化的 lowcode 平台:通过约定的 JSON Schema 描述组件内容,页面按 Schema 渲染——这也是大部分 lowcode 平台的做法。

我反对的理由是:活动页可能有复杂的交互逻辑(联动、动画、多步流程),JSON Schema 能描述结构和样式,却很难表达复杂逻辑。为了低频的运营需求引入一套 lowcode 体系,成本收益不成比例。方案否掉。

方案二:带 compiler 的 Vue runtime + template 字符串

我的第二个思路是使用包含模板编译器的 Vue runtime,运行时编译远程拉取的 template 字符串:

运行时编译 template 的写法

实验之后发现这个方案维护性很差:template 是字符串形式,没有编辑器高亮和校验;样式只能写行内,复杂样式几乎没法维护。写在字符串里的组件,后期的每一次改动都是煎熬。方案否掉。

3. 柳暗花明:直接加载编译后的 SFC

字符串不好维护,那能不能让运营组件保持 .vue 单文件组件的开发体验,构建成 JS 后远程加载?浏览器当然不认识 .vue 文件,但构建工具认识——把 SFC 用构建工具打包成普通 JS,页面运行时拉取并挂载。

接下来的问题只是:用哪个构建工具,怎么打。

尝试 rollup(vite 路线),失败

vite 基于 rollup,我们先用 rollup 测试:

rollup 配置

构建成功:

构建产物

但产物有致命问题:

产物问题

一个单文件组件被打成了 css 和 js 两个文件,且 css 的 scoped 失效。我们翻遍了相关插件的文档也没找到把它们合成单文件的办法,读源码成本又太高,只能放弃。(补充一句:如果是 Vue2 项目这条路线是可行的,rollup-plugin-vue 的 Vue2 版本默认产出单文件。)

改用 webpack + vue-loader,成功

webpack 的 vue-loader 官方支持 Vue3,且只要不使用 CSS 提取插件,样式就会以 style-loader 的形式内联进 JS——天然单文件。完整配置如下:

javascript
const { VueLoaderPlugin } = require("vue-loader");
const path = require("path");

module.exports = {
  entry: path.resolve(__dirname, "./app.vue"),
  externals: {
    vue: "Vue", // 关键:不打包 vue 本身,运行时复用宿主页面的 Vue
  },
  output: {
    filename: "bundle.js",
    path: path.resolve(__dirname, "./dist"),
    library: "MyPlugin", // 打包后挂载到全局的变量名
    libraryTarget: "umd", // UMD 格式,兼容各种加载方式
  },
  plugins: [
    new VueLoaderPlugin(),
  ],
  optimization: {
    minimize: false,
  },
  module: {
    rules: [
      // vue 单文件组件
      {
        test: /\.vue/,
        use: ["vue-loader"],
      },
      // JS 语法降级
      {
        test: /\.m?js$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: "babel-loader",
          options: {
            presets: [
              [
                "@babel/preset-env",
                {
                  useBuiltIns: "usage",
                  corejs: {
                    version: 3,
                  }
                },
              ],
            ],
          },
        },
      },
      // CSS 内联进 JS(不提取,保证单文件产物)
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader"],
      },
      // 图片资源
      {
        test: /\.(jpg|png|gif|svg)$/,
        use: [
          {
            loader: "file-loader",
            options: {
              outputPath: "assets/images/",
            },
          },
        ],
      },
      // 字体资源
      {
        test: /\.(ttf|eot|woff|woff2)$/,
        use: [
          {
            loader: "file-loader",
            options: {
              outputPath: "assets/fonts/",
            },
          },
        ],
      },
    ],
  },
};

这份配置里有三个关键点:

  1. externals: { vue: "Vue" }:远程组件和宿主页面共享同一个 Vue 实例,否则会把整个 Vue 打进 bundle(白白增加几百 KB),还可能引发多实例问题
  2. library + libraryTarget: "umd":把组件暴露为全局变量,宿主页面用 <script> 标签加载后直接从 window 上取
  3. 不用 CSS 提取插件:样式通过 style-loader 在运行时注入,保证产物是单个 JS 文件

4. 验证

先验证加载:把产物放到静态服务上访问,library 配置的变量名正确挂载到了全局:

bundle 加载后全局变量可用

再验证渲染:把远程加载的组件挂载到页面上:

远程组件成功渲染

组件成功渲染,样式和功能完全正常。方案落地。

5. 总结

最终方案的完整链路:

  1. 运营组件以标准 .vue SFC 形式开发,保持正常的开发体验
  2. 独立的 webpack 工程把它构建成单个 UMD 格式的 JS 文件,样式内联
  3. 产物部署到 CDN/静态服务,运营侧随时替换
  4. 宿主页面运行时动态插入 <script> 加载,从全局变量取出组件并挂载

这套方案的优点是组件资源完全脱离主应用发版流程,每次进入页面都能拉到最新内容,同时保住了 SFC 的开发体验。

实际使用时还有几点值得注意:

  • 宿主页面必须以 window.Vue 暴露 Vue 实例,与 externals 配置对应
  • 样式隔离要自行约定:style-loader 注入的样式是全局的,组件内建议使用 BEM 或统一类名前缀避免污染宿主页面
  • 版本管理要有兜底:远程资源建议带版本号或 hash,避免 CDN 缓存导致新旧版本不一致

探究过程的示例代码放在了 GitHub,可以跑起来自己实验。

相关文章
前后篇
← NEWER使用Nginx完成动态SEO配置OLDER →手动实现一个输入限制指令
评论