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

如何将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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 09:57:32