如何将NuxtJS 3组件嵌入外部网站以实现客户支持消息服务?
实现Nuxt3单脚本嵌入式客服消息小部件
核心思路
要实现类似Zendesk的单脚本嵌入小部件,需将Nuxt3项目中的客服组件打包为独立可嵌入脚本,通过动态加载注入客户网站,同时做好样式隔离与跨域交互处理。以下是具体实现步骤:
1. 创建独立无布局的小部件组件
在Nuxt3项目中创建纯客户端渲染的组件,仅包含客服小部件核心UI与逻辑,不依赖Nuxt全局布局或插件。示例文件components/CustomerSupportWidget.client.vue:
<template> <div class="support-widget"> <button class="widget-toggle" @click="toggleWidget">{{ isOpen ? '收起' : '咨询客服' }}</button> <div v-if="isOpen" class="widget-window"> <div class="chat-messages" ref="messagesContainer"> <div v-for="msg in messages" :key="msg.id" :class="`message ${msg.isUser ? 'user' : 'support'}`"> {{ msg.content }} </div> </div> <input v-model="inputText" @keyup.enter="sendMessage" placeholder="输入消息..." /> <button @click="sendMessage">发送</button> </div> </div> </template> <script setup> import { ref, onMounted, watch } from 'vue' const isOpen = ref(false) const messages = ref([]) const inputText = ref('') const messagesContainer = ref(null) const toggleWidget = () => { isOpen.value = !isOpen.value } const sendMessage = () => { if (!inputText.value.trim()) return messages.value.push({ id: Date.now(), content: inputText.value, isUser: true }) inputText.value = '' // 模拟后端回复,实际替换为你的API请求 setTimeout(() => { messages.value.push({ id: Date.now() + 1, content: '收到消息,我们会尽快回复!', isUser: false }) scrollToBottom() }, 1000) scrollToBottom() } const scrollToBottom = () => { messagesContainer.value?.scrollTo({ top: messagesContainer.value.scrollHeight, behavior: 'smooth' }) } onMounted(() => { // 从localStorage恢复聊天记录(可选) const savedMsg = localStorage.getItem('support-chat-history') if (savedMsg) messages.value = JSON.parse(savedMsg) }) // 保存聊天记录到localStorage(可选) watch(messages, (newVal) => { localStorage.setItem('support-chat-history', JSON.stringify(newVal)) }, { deep: true }) </script> <style scoped> .support-widget { position: fixed; bottom: 20px; right: 20px; z-index: 99999; } .widget-toggle { padding: 12px 20px; background: #007bff; color: #fff; border: none; border-radius: 20px; cursor: pointer; } .widget-window { margin-top: 10px; width: 300px; height: 400px; background: #fff; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); display: flex; flex-direction: column; } .chat-messages { flex: 1; padding: 10px; overflow-y: auto; } .message { margin: 8px 0; padding: 8px 12px; border-radius: 16px; max-width: 70%; } .user { background: #007bff; color: #fff; margin-left: auto; } .support { background: #f1f1f1; color: #333; margin-right: auto; } input { padding: 10px; border: none; border-top: 1px solid #eee; } .widget-window button { padding: 10px; background: #007bff; color: #fff; border: none; cursor: pointer; } </style>
2. 编写小部件入口脚本(打包为IIFE格式)
创建components/WidgetEntry.ts作为入口,负责将Vue组件挂载到客户网站DOM,并通过Shadow DOM隔离样式:
import { createApp } from 'vue' import CustomerSupportWidget from './CustomerSupportWidget.client.vue' // 防止重复加载 if (!window.__CustomerSupportWidgetLoaded) { window.__CustomerSupportWidgetLoaded = true // 创建容器并挂载Shadow DOM const widgetContainer = document.createElement('div') const shadowRoot = widgetContainer.attachShadow({ mode: 'open' }) // 创建Vue应用并挂载到Shadow根节点 const app = createApp(CustomerSupportWidget) const rootEl = document.createElement('div') shadowRoot.appendChild(rootEl) app.mount(rootEl) // 补充全局重置样式(可选) const globalStyle = document.createElement('style') globalStyle.textContent = ` * { margin: 0; padding: 0; box-sizing: border-box; font-family: sans-serif; } ` shadowRoot.appendChild(globalStyle) // 将小部件添加到客户页面 document.body.appendChild(widgetContainer) }
3. 配置Nuxt3打包输出独立脚本
修改nuxt.config.ts,配置Vite将小部件入口打包为独立IIFE脚本,输出到public目录:
export default defineNuxtConfig({ vite: { build: { rollupOptions: { input: { widget: './components/WidgetEntry.ts', }, output: { format: 'iife', name: 'CustomerSupportWidget', file: 'public/widget.bundle.js', }, }, }, }, app: { clientOnly: true, }, })
执行npm run build后,public目录会生成widget.bundle.js,这就是客户需要加载的核心脚本。
4. 生成客户嵌入脚本
给客户提供一段简洁的加载脚本,添加到网站的head或body末尾即可:
<script> (function() { const script = document.createElement('script'); script.src = 'https://your-nuxt-domain.com/widget.bundle.js'; script.async = true; // 可选:传递客户唯一标识,用于区分不同客户消息 script.dataset.clientId = 'YOUR_CLIENT_UNIQUE_ID'; document.body.appendChild(script); // 可选:脚本加载失败处理 script.onerror = () => { console.error('客服小部件加载失败'); }; })(); </script>
如需获取客户标识,可在WidgetEntry.ts中通过document.currentScript?.dataset.clientId获取。
5. 跨域与API处理
若小部件需调用后端API发送消息,需在后端配置CORS允许客户网站访问:
// Node.js/Express示例 const cors = require('cors'); app.use(cors({ origin: '*', // 生产环境建议指定客户域名列表 credentials: true, }));
6. 关键注意事项
- 样式隔离:使用Shadow DOM彻底避免小部件与客户网站样式冲突
- 重复加载防护:通过全局变量标记防止客户重复注入脚本
- 响应式适配:确保小部件在移动端、桌面端均能正常显示
- 路由兼容性:挂载到
body上,不受客户单页应用路由切换影响
内容的提问来源于stack exchange,提问作者Mostafiz Rahman
相关产品推荐
相关产品推荐

