@yeez-tech/chat-tool
v0.3.3
Published
Embeddable OpenIM chat window for Vue apps, powered by ChatUI and OpenIM WASM SDK
Readme
@yeez-tech/chat-tool
可嵌入 Vue 应用的 OpenIM 聊天能力,UI 基于 ChatUI,IM 基于 OpenIM WASM SDK。
集成方式对齐 dianshu / Rocket.Chat:在 App 根节点挂载一次 全局聊天壳,业务页通过 useOpenIMContact() 打开会话(如「联系卖家」),而不是每个页面各自 mount 聊天组件。

安装
npm install @yeez-tech/chat-tool vue @openim/wasm-client-sdk0.2.0 起:@openim/wasm-client-sdk 为 peerDependency,本包不再把 SDK 打进 chat-tool.mjs;宿主须自行安装 SDK,并部署浏览器静态资源(对齐 OpenIM Browser Quickstart)。
宿主必须提供的静态资源
| 文件 | 建议路径 | 说明 |
| ---- | -------- | ---- |
| wasm_exec.js | /wasm_exec.js | 在 index.html / Nuxt app.head 用 <script src="/wasm_exec.js"> 先于聊天组件加载 |
| worker.js | /@openim/wasm-client-sdk/lib/worker.js | SDK 在 Vite/Nuxt dev 下会把预构建路径改写为此目录,见下方同步脚本 |
| worker-legacy.js | 同上目录 | 不支持 Module Worker 的浏览器 |
| openIM.wasm | /openIM.wasm 或 CDN | 可通过 core-wasm-path 覆盖 |
| sql-wasm.wasm | /sql-wasm.wasm 或 CDN | 可通过 sql-wasm-path 覆盖 |
一键从 node_modules 同步到宿主 public/(不修改 OpenIM SDK 源码):
node node_modules/@yeez-tech/chat-tool/scripts/sync-openim-static.mjs public生产构建(Nuxt/Vite)还可把 worker.js 复制到客户端 chunk 同级目录(/_nuxt/worker.js),可使用包内 Vite 插件。
默认(推荐 SSR 主站同源):不传插件参数、不传组件 props 时,Worker 仍走 public/@openim/...(与 sync-openim-static.mjs 一致)。
Nuxt 打包到 _nuxt/(可选,需与 openimWorkerCopyPlugin 成对使用):
openimViteSdkPatchPlugin({ workerBasePath: 'bundled' }),
openimWorkerCopyPlugin(),'bundled' 会根据 Vite base + build.assetsDir 自动解析(Nuxt 默认 /_nuxt/)。也可在组件上传 worker-base-path 覆盖。
// nuxt.config.ts → vite.plugins
import {
openimViteSdkPatchPlugin,
openimWorkerCopyPlugin,
} from "@yeez-tech/chat-tool/scripts/openim-vite-plugin.mjs";
import { chatToolSafariRegexpPlugin } from "@yeez-tech/chat-tool/scripts/chat-tool-safari-regexp-plugin.mjs";
export default defineNuxtConfig({
vite: {
plugins: [
openimViteSdkPatchPlugin(), // 默认 public/@openim;bundled 模式见上
chatToolSafariRegexpPlugin(),
openimWorkerCopyPlugin(),
],
optimizeDeps: {
exclude: ["@yeez-tech/chat-tool"],
include: ["@openim/wasm-client-sdk"],
},
},
app: {
head: {
script: [{ src: "/wasm_exec.js" }],
},
},
});Vue CLI / Webpack 宿主(如典枢 website)请使用 openim-webpack-worker-loader.cjs 在打包时 patch SDK(不修改 node_modules),并复制 worker 到 public/@openim/wasm-client-sdk/lib/。0.2.2 起 loader 使用 CommonJS patch(修复生产构建 ERR_REQUIRE_ESM);可通过 loader options.workerBasePath 或组件 worker-base-path 覆盖 Worker 目录。详见下方 Safari 兼容。
ChatUI 工具栏/导航图标已随包内置(ensureChatuiIcons),宿主无需再引入 CDN 图标脚本。
Nuxt 等使用 vite-plugin-node-polyfills 的宿主:包内会在初始化前自动补齐 process.pid 等字段(ensureGoProcessShim),无需再写 openim-process-shim 一类插件。
Safari 兼容(含 Safari 16.1 / iOS 16.x)
Safari 上常见问题来自三层,0.2.0 分别在包内与宿主侧处理:
| 层级 | 现象 | 处理方式 |
| ---- | ---- | -------- |
| ChatUI bundle | 页面白屏 / Invalid regular expression: invalid group specifier name | 本包 构建时 自动 patch(见下) |
| OpenIM WASM | commonEventFunc is not a function、WASM 不启动 | 宿主静态资源 + Webpack loader / CDN MIME |
| 宿主业务代码 | 首页 SSR 脚本解析失败 | 宿主自行避免 lookbehind 等 Safari 不支持的语法(与 chat-tool 无关) |
1. ChatUI RegExp 探针(本包构建时自动处理)
@chatui/core 依赖链会在模块加载阶段探测 (?<a>b) 命名捕获;Safari < 16.4(含 16.1)会直接抛 SyntaxError,导致聊天 chunk 无法加载。
npm run build 末尾会自动运行 scripts/sanitize-regexp-ncg.mjs,改写 dist/chat-tool.mjs / dist/chat-tool.cjs:
- 将命名捕获探针 stub 为
true - 注入
__chatToolRegExpFails,避免 Safari 上 feature 检测本身抛错 - patch 函数追加在 bundle 末尾(不插在
import前,避免 Safari ESM 报错)
Nuxt 开发模式若使用 file:../chatui-openim-demo 且 dist 未及时重建,可在 nuxt.config.ts 启用 dev 兜底插件(见上方示例中的 chatToolSafariRegexpPlugin())。
2. OpenIM WASM / Worker(宿主集成)
Safari 还需注意 OpenIM SDK 运行时行为(不修改 @openim/wasm-client-sdk 磁盘源码):
| 问题 | Safari 行为 | 宿主应对 |
| ---- | ----------- | -------- |
| Module Worker | 部分版本不稳定 | Safari 使用 worker-legacy.js(sync-openim-static.mjs 已同步) |
| Nuxt cdnURL 跨域 | Failed to construct 'Worker'(CDN /_nuxt/worker.*.js ≠ 页面域名) | 必须 openimViteSdkPatchPlugin(),Worker 走同源 /@openim/wasm-client-sdk/lib/ |
| instantiateStreaming | CDN / MIME 不符时易失败 | Webpack 宿主用 loader 改为 arrayBuffer + instantiate |
| WASM 404 / MIME | commonEventFunc is not a function | CDN 返回 200 且 Content-Type: application/wasm(勿用 SPA fallback 返回 HTML) |
| Webpack 5 打包 worker | Worker 内 require is not defined | 使用 openim-webpack-worker-loader.cjs 改为从 public/@openim/wasm-client-sdk/lib/ 加载 |
Vue CLI 示例(vue.config.js → chainWebpack):
const openimSdkEntry = path.resolve(
__dirname,
"node_modules/@openim/wasm-client-sdk/lib/index.es.js"
);
const openimWebpackWorkerLoader = path.resolve(
__dirname,
"node_modules/@yeez-tech/chat-tool/scripts/openim-webpack-worker-loader.cjs"
);
config.module
.rule("openim-sdk-webpack-worker")
.test(/index\.es\.js$/)
.include.add(openimSdkEntry)
.end()
.use("openim-webpack-worker-loader")
.loader(openimWebpackWorkerLoader);
config.resolve.alias
.set(
"@yeez-tech/chat-tool$",
path.resolve(__dirname, "node_modules/@yeez-tech/chat-tool/dist/chat-tool.mjs")
)
.set("@openim/wasm-client-sdk$", openimSdkEntry);loader 还会在打包时修正 sqlWasmPath 初始化顺序,并导出 __chatToolResetSDKSingleton(WASM 加载失败后可重置单例重试)。
WASM 路径: 生产环境建议通过 props / 环境变量指向 CDN,例如:
VUE_APP_OPENIM_CORE_WASM_PATH=https://cdn.example.com/web/im.wasm
VUE_APP_OPENIM_SQL_WASM_PATH=https://cdn.example.com/web/sql.wasmNetwork 面板应能看到 im.wasm(约 34MB)返回 200 且 Type 为 wasm;若出现 ERR_CONTENT_LENGTH_MISMATCH,清除站点 Cache Storage 后重试。
3. 宿主自检清单
- [ ]
postinstall或 CI 中执行sync-openim-static.mjs public - [ ]
index.html/ Nuxtapp.head已加载/wasm_exec.js(先于聊天组件) - [ ] Nuxt:
openimViteSdkPatchPlugin(cdnURL 跨域必填)+chatToolSafariRegexpPlugin;ViteoptimizeDeps.exclude含@yeez-tech/chat-tool - [ ] Webpack:
openim-webpack-worker-loader+@yeez-tech/chat-toolalias 到dist/chat-tool.mjs - [ ] WASM CDN
Content-Type: application/wasm - [ ] 联系卖家等业务入口传递
uniqueUserId(OpenIM userID 与店铺 ID 映射)
推荐集成(dianshu 模式)
1. App 根节点挂载一次
<!-- App.vue -->
<script setup lang="ts">
import { OpenIMChatApp } from "@yeez-tech/chat-tool";
import "@yeez-tech/chat-tool/style.css";
const apiAddr = "...";
const wsAddr = "...";
const userID = "...";
const token = "...";
</script>
<template>
<router-view />
<OpenIMChatApp
:apiAddr="apiAddr"
:wsAddr="wsAddr"
:userID="userID"
:token="token"
:platformID="5"
auto-login
/>
</template>OpenIMChatApp = provide 聊天控制能力 + 浮层壳 + 内部 OpenIMChat(对标 dianshu 的 provide(chatControls) + FloatingCustomerService)。
2. 业务页「联系卖家」
<script setup lang="ts">
import { useOpenIMContact } from "@yeez-tech/chat-tool";
const { handleContact } = useOpenIMContact();
async function onContactSeller() {
// 卖家 OpenIM userID(由业务后端映射,对标 dianshu 的 createUser → roomId)
await handleContact({ userID: sellerOpenIMUserId });
}
</script>
<template>
<button type="button" @click="onContactSeller">联系卖家</button>
</template>| dianshu | @yeez-tech/chat-tool |
| ------------------------------------ | ------------------------------------------- |
| handleContact({ createUser }) | handleContact({ userID }) |
| POST /chat/conversation → roomId | SDK getOneConversation → conversationID |
| iframe go /direct/{roomId} | 内部切会话 + 拉历史 |
| FloatingCustomerService 全局单例 | OpenIMChatApp 全局单例 |
其他入口
const { openInbox, openChat, closeChat } = useOpenIMContact();
openInbox(); // 打开会话列表(对标「我的消息」)
openChat(); // 仅显示浮层
closeChat(); // 关闭浮层补充说明(弹窗壳 shellMode="popup"):
openInbox():标准会话模式,显示左侧会话列表 + 右侧聊天区。handleContact({ userID | groupID }):单会话模式,仅显示目标会话聊天区(隐藏左侧会话列表),适合“联系卖家”入口。- 可在两种入口之间切换:调用
openInbox()会回到标准会话模式。
架构
App.vue
└── <OpenIMChatApp /> ← provide(openIMChatControlsKey)
└── OpenIMChatHost ← 壳层(popup 浮窗 / fullpage 全页面)
└── OpenIMChat ← React + ChatUI + WASM SDK
商品页 / 订单页
└── useOpenIMContact()
└── openConversation({ userID }) → SDK + 切换会话宿主接入注意(Nuxt / SSR)
- 全屏聊天页(
shell-mode="fullpage"):使用默认layout(auto)即可;勿写layout="wide",否则窄屏无法切换列表/会话分屏。 - 输入区变高:需设置
composer-height/compact-composer-height(外框);仅改*-textarea-min-height可能无可见效果(见下方输入区高度说明)。 - 表情面板:点击笑脸应在输入框上方弹出 emoji 网格并插入文本(非
sendFace直发);若只见空白条请升级到 ≥ 0.3.3。 - 弹窗(联系卖家 / 客服):
OpenIMChatHost会自动维护host-chat-open;宿主需auto-login且在userID+token就绪后再挂载组件(避免空凭证触发设置面板)。 - 过早点击「联系卖家」:组件会在登录 / WASM 就绪后自动重试打开会话,并显示加载蒙层,无需宿主额外处理。
Props(传给 OpenIMChatApp / OpenIMChat)
Vue 模板请使用 camelCase 对应的 kebab-case(如 compactComposerHeight → compact-composer-height)。
连接类 prop 请用 :userID、:platformID 等,勿写成 :user-id / :platform-id(Vue 会映射成错误的属性名)。
连接与登录
| Prop | 类型 | 默认 | 说明 |
| ---- | ---- | ---- | ---- |
| apiAddr | string | — | OpenIM HTTP API 根地址 |
| wsAddr | string | — | OpenIM WebSocket 地址 |
| userID | string | — | 当前用户 OpenIM userID |
| token | string | — | 登录 token(由业务鉴权接口下发) |
| platformID | number | 5 | 平台 ID(Web 一般为 5) |
| autoLogin | boolean | false | 有 userID + token 时自动调用 SDK login |
| persistConfig | boolean | true | 是否将连接设置写入 localStorage |
| storageKey | string | openim-chatui-config | persistConfig 为 true 时的存储键 |
| debug | boolean | false | 开启 OpenIM WASM SDK 调试日志 |
壳层与布局
| Prop | 类型 | 默认 | 说明 |
| ---- | ---- | ---- | ---- |
| shellMode | 'popup' \| 'fullpage' | popup | popup:全局浮窗;fullpage:占满父容器(如 /chat 新标签页) |
| layout | 'auto' \| 'compact' \| 'wide' | auto | 见下方 布局说明 |
| compactBreakpoint | number | 768 | layout='auto' 且 shellMode='fullpage' 时,容器宽度 ≤ 此值则切换为紧凑分屏 |
| hostChatOpen | boolean | — | 是否应对外同步会话/历史:fullpage 应为 true;popup 在浮层打开时为 true。使用 OpenIMChatHost 时会自动注入,一般无需手写 |
| initialPeerUserID | string | — | 登录并拉取列表后,自动打开指定对方 userID 的会话(常用于全屏页深链);fullpage 仍保留左侧列表 |
弹窗尺寸(仅 shellMode='popup')
| Prop | 类型 | 默认 | 说明 |
| ---- | ---- | ---- | ---- |
| chatWindowWidth | number | 800 | 浮窗宽度(px),由 OpenIMChatHost 读取 |
| chatWindowMinWidth | number | 400 | 浮窗最小宽度(px);拖拽缩放下限与 CSS min-width |
| chatWindowHeight | number | 600 | 浮窗高度(px) |
| chatMinHeight | number | 700 | 弹窗壳 min-height(px) |
| compactChatMinHeight | number | — | 紧凑布局壳最小高度;未传时回退 chatMinHeight |
输入区高度
通过 CSS 变量注入,无需 :deep() 覆盖样式。默认值见包内 DEFAULT_* 常量。
| Prop | 类型 | CSS 默认 | 说明 |
| ---- | ---- | -------- | ---- |
| composerHeight | number | 140 | 宽屏布局下整条输入区外框高度(px) |
| compactComposerHeight | number | 100 | 紧凑布局下输入区外框高度(px);未传时回退 composerHeight |
| composerTextareaMinHeight | number | 80 | 宽屏下文本框 min-height(px) |
| compactComposerTextareaMinHeight | number | 50 | 紧凑布局下文本框 min-height(px);未传时回退 composerTextareaMinHeight |
注意: 外框 .im-composer 使用固定 height,仅增大 *TextareaMinHeight 而外框高度不变时,视觉上可能无变化。需要整块输入区变高时,请同时(或至少)设置 composerHeight / compactComposerHeight。
WASM 与对象存储
| Prop | 类型 | 默认 | 说明 |
| ---- | ---- | ---- | ---- |
| coreWasmPath | string | /openIM.wasm | 主 WASM 路径(常用 CDN URL) |
| sqlWasmPath | string | /sql-wasm.wasm | SQL WASM 路径 |
| wasmExecPath | string | /wasm_exec.js | Go WASM runtime 脚本路径 |
| workerBasePath | string | /@openim/wasm-client-sdk/lib/ | Worker 目录(可传 /_nuxt/ 等);运行时覆盖打包默认值 |
| objectStorageProxy | object | — | 内网 MinIO / OpenIM 对象 URL 改写;需配合 installOpenIMObjectStorageFetchProxy() 做上传代理 |
objectStorageProxy 字段:internalOrigin、publicOrigin、urlRewriteRules(见 OpenIMObjectStorageProxyConfig 类型)。
布局(layout)
仅 shellMode='fullpage' 时生效;popup 浮窗始终为桌面双栏,不随容器变窄切换分屏。
| 值 | 行为 |
| -- | ---- |
| auto(推荐) | 根据 组件容器宽度(ResizeObserver,非 window.innerWidth)与 compactBreakpoint 自动切换宽屏双栏 / 紧凑「列表 ⇄ 会话」分屏 |
| compact | 始终紧凑分屏 |
| wide | 始终桌面双栏;窄屏下左右栏会被挤在一起,不适合移动端全屏聊天页 |
全屏聊天页请使用 shell-mode="fullpage" 且 不要 传 layout="wide"(或显式省略 layout,使用默认 auto)。
autoLogin 与加载态
autoLogin 为 true 且当前应同步数据时(fullpage 或 popup 已打开),在 会话列表首次拉取完成前 会显示全区域加载蒙层(转圈 +「加载中…」),避免 WASM 初始化 / 登录期间闪出「请先登录」「暂无对话」等占位 UI。
蒙层在以下情况隐藏:列表拉取成功(含空列表)、目标会话已打开、同步流程结束,或登录明确失败(connection === 'failed')。
联系卖家若在 autoLogin / WASM 尚未完成时触发,会进入待打开队列并在登录完成后自动重试,期间保持蒙层(单会话模式且尚未选中 peer 时同样显示蒙层)。
Events
| 事件 | 载荷 | 说明 |
| ---- | ---- | ---- |
| login-start | — | 开始调用 SDK login |
| login-success | — | IM 登录成功 |
| login-fail | error | IM 登录失败 |
| connect-success | — | WebSocket 连接成功 |
| connect-fail | error | 连接失败 |
| token-expired | — | Token 无效或过期 |
| unread-change | { total } | 全部会话未读之和;宿主用于消息入口角标 |
| new-message | { peerKey, conversationID?, userID?, groupID?, sendID?, isSelf } | 收到他人消息;宿主自行 toast / 桌面通知 |
| visit-seller-profile | sellerID | 弹窗会话栏「店铺」图标点击;由宿主跳转卖家主页 |
| close | — | 弹窗壳点击关闭(shellMode='popup') |
高级导出
import {
OpenIMChat, // 仅内核,自行 provide + 壳
OpenIMChatHost,
useOpenIMChatControls,
openIMChatControlsKey,
initIMSDK,
getIMSDK,
installOpenIMObjectStorageFetchProxy,
DEFAULT_COMPOSER_HEIGHT,
DEFAULT_CHAT_WINDOW_WIDTH,
} from "@yeez-tech/chat-tool";若不用 OpenIMChatApp,需自行:
provide(openIMChatControlsKey, useOpenIMChatControls());并挂载 OpenIMChatHost。
本地 Demo
npm i
npm run dev # predev 会自动 sync OpenIM 静态资源到 public/.env 示例:
VITE_API_URL=http://your-openim-host:10002
VITE_WS_URL=ws://your-openim-host:10001
VITE_OPENIM_USER_ID=
VITE_OPENIM_TOKEN=
VITE_DEMO_SELLER_USER_ID= # 演示「联系卖家」的对方 userIDDemo 页:OpenIMChatApp 全局挂载 + 按钮调用 useOpenIMContact(),与宿主集成方式一致。
npm run build # 构建 npm 包 → dist/功能
- 会话列表、文本/图片/文件/名片/表情、历史消息、单聊已读
- 容器宽度
ResizeObserver紧凑布局(非window.innerWidth) autoLogin下首次进入的加载蒙层,直至会话列表就绪- 可配置的弹窗尺寸与输入区高度(props,见上表)
- Safari < 16.4:构建时 RegExp 探针 sanitize;Webpack 宿主可选 loader 处理 WASM / worker
版本记录
| 版本 | 说明 |
| ---- | ---- |
| 0.3.3 | 修复固定高度布局下表情面板被 overflow: hidden 裁切导致空白的问题;表情层挂载到 im-composer 顶层 |
| 0.3.2 | 修复滚动时输入区底部白条(ChatFooter 背景与 padding-bottom) |
| 0.3.1 | 新增 chatWindowMinWidth,弹窗窄屏不再被硬编码 400px min-width 撑破 |
| 0.3.0 | 推荐宿主用固定 composer-height + MessageContainer flex:1 控制消息区高度(替代易失效的 ratio 方案) |
| 0.2.8+ | 稳定 OpenIMComposer 组件;表情插入输入框;隐藏 ChatUI 重复 .Composer |
| 0.2.2 | Webpack worker loader(CJS patch);workerBasePath 可配置 |
| 0.2.0 | @openim/wasm-client-sdk 改为 peerDependency;静态资源同步脚本 |
发布
npm run build
npm pack # 生成 yeez-tech-chat-tool-0.3.3.tgz,可供 file: 或私有源安装
npm publish --access publicLicense
MIT
