Vue3+webpack实现组件远程加载
Vue3 + webpack 实现组件远程加载
1. 需求背景
最近接到一个运营需求:在页面中植入活动弹窗,且活动内容需要经常替换。
最朴素的方案是把活动组件写在本地、每次发版上线。但对单页应用来说这条路有个硬伤:用户不刷新或重新进入页面,就不会向服务器请求新资源——发版了,在线用户看到的还是旧活动。而产品的要求是:广告发布后,用户进入页面就要看到最新内容,后续还可能随机展示、频繁替换。
要满足这个需求,组件资源必须脱离发版流程,改为运行时远程加载。下面记录我们排除掉两个方案、最终落地的完整过程。
2. 被否掉的两个方案
方案一:lowcode 搭建平台
同事的思路是做一个简化的 lowcode 平台:通过约定的 JSON Schema 描述组件内容,页面按 Schema 渲染——这也是大部分 lowcode 平台的做法。
我反对的理由是:活动页可能有复杂的交互逻辑(联动、动画、多步流程),JSON Schema 能描述结构和样式,却很难表达复杂逻辑。为了低频的运营需求引入一套 lowcode 体系,成本收益不成比例。方案否掉。
方案二:带 compiler 的 Vue runtime + template 字符串
我的第二个思路是使用包含模板编译器的 Vue runtime,运行时编译远程拉取的 template 字符串:

实验之后发现这个方案维护性很差:template 是字符串形式,没有编辑器高亮和校验;样式只能写行内,复杂样式几乎没法维护。写在字符串里的组件,后期的每一次改动都是煎熬。方案否掉。
3. 柳暗花明:直接加载编译后的 SFC
字符串不好维护,那能不能让运营组件保持 .vue 单文件组件的开发体验,构建成 JS 后远程加载?浏览器当然不认识 .vue 文件,但构建工具认识——把 SFC 用构建工具打包成普通 JS,页面运行时拉取并挂载。
接下来的问题只是:用哪个构建工具,怎么打。
尝试 rollup(vite 路线),失败
vite 基于 rollup,我们先用 rollup 测试:

构建成功:

但产物有致命问题:

一个单文件组件被打成了 css 和 js 两个文件,且 css 的 scoped 失效。我们翻遍了相关插件的文档也没找到把它们合成单文件的办法,读源码成本又太高,只能放弃。(补充一句:如果是 Vue2 项目这条路线是可行的,rollup-plugin-vue 的 Vue2 版本默认产出单文件。)
改用 webpack + vue-loader,成功
webpack 的 vue-loader 官方支持 Vue3,且只要不使用 CSS 提取插件,样式就会以 style-loader 的形式内联进 JS——天然单文件。完整配置如下:
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/",
},
},
],
},
],
},
};这份配置里有三个关键点:
externals: { vue: "Vue" }:远程组件和宿主页面共享同一个 Vue 实例,否则会把整个 Vue 打进 bundle(白白增加几百 KB),还可能引发多实例问题library+libraryTarget: "umd":把组件暴露为全局变量,宿主页面用<script>标签加载后直接从window上取- 不用 CSS 提取插件:样式通过
style-loader在运行时注入,保证产物是单个 JS 文件
4. 验证
先验证加载:把产物放到静态服务上访问,library 配置的变量名正确挂载到了全局:

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

组件成功渲染,样式和功能完全正常。方案落地。
5. 总结
最终方案的完整链路:
- 运营组件以标准
.vueSFC 形式开发,保持正常的开发体验 - 独立的 webpack 工程把它构建成单个 UMD 格式的 JS 文件,样式内联
- 产物部署到 CDN/静态服务,运营侧随时替换
- 宿主页面运行时动态插入
<script>加载,从全局变量取出组件并挂载
这套方案的优点是组件资源完全脱离主应用发版流程,每次进入页面都能拉到最新内容,同时保住了 SFC 的开发体验。
实际使用时还有几点值得注意:
- 宿主页面必须以
window.Vue暴露 Vue 实例,与externals配置对应 - 样式隔离要自行约定:
style-loader注入的样式是全局的,组件内建议使用 BEM 或统一类名前缀避免污染宿主页面 - 版本管理要有兜底:远程资源建议带版本号或 hash,避免 CDN 缓存导致新旧版本不一致
探究过程的示例代码放在了 GitHub,可以跑起来自己实验。