You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Vue3+Vite构建可嵌入单JS文件客服聊天Widget

客服Widget单JS打包与HTML配置读取方案

一、Vite打包配置(生成单JS文件)

修改项目根目录的vite.config.js,配置为库模式打包,输出浏览器可直接运行的单文件:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  build: {
    lib: {
      entry: 'src/main.js', // 项目入口文件路径
      name: 'ChatWidget',
      formats: ['iife'], // 生成自执行脚本格式
      fileName: () => `widget.min.js`
    },
    rollupOptions: {
      external: [], // 不排除Vue依赖,将其打包进单文件
      output: {
        inlineDynamicImports: true,
        manualChunks: undefined // 禁用代码分割,确保单文件输出
      }
    },
    cssCodeSplit: false, // 将样式打包进JS,无需单独引入CSS
    minify: 'terser' // 压缩代码
  }
})

执行npm run build后,dist目录下会生成widget.min.js,即为最终交付的单文件。

二、读取HTML Data属性作为配置

1. 入口文件(main.js)修改

在入口文件中获取当前加载的script标签,读取其data属性并传递给Vue实例:

import { createApp } from 'vue'
import { createStore } from 'vuex'
import App from './App.vue' // 你的容器组件
import websocketModule from './store/modules/websocket' // Vuex模块

// 获取当前加载Widget的script标签
const scriptTag = document.currentScript || document.querySelector('script[src*="widget.min.js"]')
// 解析data属性为配置对象
const widgetConfig = {
  icon: scriptTag.dataset.icon,
  title: scriptTag.dataset.title,
  jwtUrl: scriptTag.dataset.jwtUrl,
  rubikoUrl: scriptTag.dataset.rubikoUrl,
  websocketUrl: scriptTag.dataset.companyWebsocketUrl,
  theme: scriptTag.dataset.theme,
  brandId: scriptTag.dataset.brandId
}

// 创建Vuex Store
const store = createStore({
  modules: {
    websocket: websocketModule
  }
})

// 创建Vue实例,将配置作为props传入容器组件
const app = createApp(App, { widgetConfig })
app.use(store)
// 将Widget挂载到body新增的元素上
app.mount(document.body.appendChild(document.createElement('div')))

2. 容器组件修改

更新容器组件,使用传入的配置替换硬编码地址:

<template>
  <Widget
    :connectionId="retrieveConnectionId"
    :endUserName="name"
    :endUserLastName="lastName"
    :endUserEmail="email"
    :endUserSub="sub"
    :isVip="isVip"
    :categories="categories"
    :isShow="isShow"
  />
  <WidgetTrigger @click="isShow = !isShow" :isOpened="!isShow" :icon="widgetConfig.icon" :title="widgetConfig.title" />
</template>

<script>
import { ref, onMounted } from "vue";
import { Buffer } from "buffer";
import { useActions, useGetters } from "vuex-composition-helpers";
import Widget from "@/components/Widget.vue";
import WidgetTrigger from "@/components/WidgetTrigger.vue";

export default {
  components: { Widget, WidgetTrigger },
  props: {
    widgetConfig: {
      type: Object,
      required: true
    }
  },
  setup(props) {
    const categories = ref([]);
    const email = ref("");
    const error = ref(null);
    const isShow = ref(false);
    const isVip = ref();
    const lastName = ref("");
    const name = ref("");
    const token = ref("");
    const sub = ref("");
    
    const { callWebsocket } = useActions({ callWebsocket: "websocket/processWebsocket" });
    const { retrieveConnectionId } = useGetters({ retrieveConnectionId: "websocket/getConnectionId" });

    const retrieveSignedJWT = async () => {
      // 使用配置中的JWT地址
      fetch(props.widgetConfig.jwtUrl)
        .then(async (response) => {
          const data = await response.text();
          if (!response.ok) throw (data?.message || response.statusText);
          token.value = data;
          decodeToken(data);
          retrieveCategories();
        })
        .catch((err) => {
          error.value = err;
          console.error("获取JWT失败:", err);
        });
    };

    const retrieveCategories = async () => {
      // 使用配置中的API地址
      fetch(`${props.widgetConfig.rubikoUrl}/categories`, {
        method: "GET",
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
          Authorization: `Bearer ${token.value}`,
        },
      })
        .then(async (response) => {
          const data = await response.json();
          if (!response.ok) throw (data?.message || response.statusText);
          categories.value = data;
        })
        .catch((err) => {
          error.value = err;
          console.error("获取分类失败:", err);
        });
    };

    const decodeToken = (jwt) => {
      const base64Url = jwt.split(".")[1];
      const base64 = base64Url.replace("-", "+").replace("_", "/");
      const decodedData = JSON.parse(Buffer.from(base64, "base64").toString("binary"));
      sub.value = decodedData.sub;
      name.value = decodedData.name;
      lastName.value = decodedData.last_name;
      email.value = decodedData.email;
      isVip.value = decodedData.is_vip;
    };

    onMounted(async () => {
      await retrieveSignedJWT();
      // 传递WebSocket地址给Vuex action
      await callWebsocket(props.widgetConfig.websocketUrl);
    });

    return {
      categories,
      decodeToken,
      email,
      error,
      retrieveConnectionId,
      retrieveSignedJWT,
      retrieveCategories,
      isShow,
      isVip,
      lastName,
      name,
      sub,
      token,
      widgetConfig: props.widgetConfig
    };
  },
};
</script>

3. Vuex模块调整

确保WebSocket模块的action接收传入的地址参数:

// store/modules/websocket.js
const state = {
  connectionId: null,
  socket: null
};

const mutations = {
  SET_CONNECTION_ID(state, id) {
    state.connectionId = id;
  },
  SET_SOCKET(state, socket) {
    state.socket = socket;
  }
};

const actions = {
  async processWebsocket({ commit }, websocketUrl) {
    const socket = new WebSocket(websocketUrl);
    socket.onopen = () => {
      const connectionId = socket.id || Math.random().toString(36).substr(2, 9);
      commit('SET_CONNECTION_ID', connectionId);
    };
    // 其他WebSocket逻辑
    commit('SET_SOCKET', socket);
  }
};

const getters = {
  getConnectionId: state => state.connectionId
};

export default {
  namespaced: true,
  state,
  mutations,
  actions,
  getters
};

三、用户使用示例

用户只需在HTML中引入打包后的JS,并通过data属性传递配置:

<script src="https://cdn.xyz.xyz/widget.min.js" 
   data-icon="path_to_icon.jpg"
   data-title="Brand support"
   data-jwt-url="jwt token url"
   data-rubiko-url="another url"
   data-company-websocket-url="websocket url"
   data-theme="default"
   data-brand-id="brand-id">
</script>

内容的提问来源于stack exchange,提问作者Lowtrux

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.08 08:10:29