如何用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
相关产品推荐
相关产品推荐

