共计 9520 个字符,预计需要花费 24 分钟才能阅读完成。
前言
因为看到有局部的需要为在页面层,疾速的引入一个包,并且以简略的配置,就能够疾速实现一个聊天窗口,因而尝试以 Vue3 插件的模式开发一个轻量的聊天窗口。
这次简略分享一下此插件的实现思路,以及实现过程,并形容一下本次插件公布 npm 的过程。
技术栈
- Vue3
- pnpm
- Typescript
- Vite
插件外围目录设计
📦 emchat-chatroom-widget | |
┣ 📂 build // 插件打包输入的目录 | |
┣ 📂 demo // 验证插件 demo 相干目录 | |
┣ 📂 scripts // 打包脚本目录 | |
┣ 📂 src // 插件源代码 | |
┃ ┣ 📂 components // 组件目录 | |
┃ ┣ 📂 container // 容器组件目录 | |
┃ ┣ 📂 EaseIM // 环信 IM 相干目录 | |
┃ ┣ 📂 utils // 工具相干目录 | |
┃ ┣ 📜 index.ts // 插件入口文件 | |
┃ ┗ 📜 install.ts // 插件初始化文件 | |
┣ 📜 package.json // 我的项目配置文件 | |
┣ 📜 vite.config.ts // vite 配置文件 | |
┗ 📜 README.md // 我的项目阐明文档 | |
... |
实现过程
确认性能范畴
首先确认本次插件实现的性能范畴,从而围绕要实现的性能着手进行开发筹备。
- Vue3 框架应用
- 轻量配置、仅配置大量参数即可立刻应用聊天性能
- 页面大小自适应,给定容器宽高,插件外部宽高自适应。
- 仅聊天室类型音讯反对根底文本,表情,图片。
临时第一期仅反对这些性能范畴。
着手开发
1、创立空白我的项目
pnpm create vite emchat-chatroom-widget --template vue-ts
2、配置eslint
pretter
等代码校验、以及代码格调工具。
pnpm i eslint eslint-plugin-vue @typescript-eslint/eslint-plugin @typescript-eslint/parser -D
pnpm i prettier eslint-config-prettier eslint-plugin-prettier -D
同时也不要忘了创立对应的 .eslintrc.cjs
和.prettierrc.cjs
这里遇到了一个问题:
这几个文件以 cjs 结尾是因为 package.json 创立时设置了 "type": "module"
后你的所有 js 文件默认应用 ESM 模块标准,不反对 commonjs 标准,所以必须显式的申明成 xxx.cjs 能力标识这个是用 commonjs 标准的,把你的配置都改成.cjs 后缀。
3、配置 scripts 打包脚本
目录下新建一个文件夹命名为scripts
,新加一个 build.js 或者为.ts 文件。
在该文件中引入 vite
进行打包时的配置。因为本次插件编写时应用了 jsx
语法进行编写,因而 vite 打包时也须要引入 jsx 打包插件。
装置 @vitejs/plugin-vue-jsx
插件。
const BASE_VITE_CONFIG = defineConfig({ | |
publicDir: false, // 暂不须要打包动态资源到 public 文件夹 | |
plugins: [vue(), | |
vueJSX(), | |
// visualizer({ | |
// emitFile: true, | |
// filename: "stats.html" | |
// }), | |
dts({ | |
outputDir: './build/types', | |
insertTypesEntry: true, // 插入 TS 入口 | |
copyDtsFiles: true, // 是否将源码里的 .d.ts 文件复制到 outputDir | |
}), | |
], | |
}); |
package.json
中减少 build 脚本执行命令,
"scripts": { | |
"dev": "vite", | |
"build": "vue-tsc && vite build", | |
"preview": "vite preview", | |
"lint": "eslint src --fix", | |
"build:widget": "node ./scripts/build.js" | |
}, |
整体 build.js 代码因为篇幅关系,能够前面查看文末的源码地址。
4、编写 Vue3 插件入口函数
import type {App} from 'vue'; | |
import EasemobChatroom from './container'; | |
import {initEMClient} from './EaseIM'; | |
export interface IEeasemobOptions {appKey: string;} | |
export default {install: (app: App, options: IEeasemobOptions) => { | |
// 在这里编写插件代码 | |
console.log(app); | |
console.log('options', options); | |
if (options && options?.appKey) {initEMClient(options.appKey); | |
} else {throw console.error('appKey 不能为空'); | |
} | |
app.component(EasemobChatroom.name, EasemobChatroom); | |
}, | |
}; |
5、聊天插件入口代码
聊天插件入口组件次要用来接管插件使用者所传递进来的一些必要参数,比方登录用户 id、明码、token、聊天室 id,以及针对初始化插件的初始状态。
import {defineComponent, onMounted} from "vue" | |
import {EMClient} from "../EaseIM" | |
import {useManageChatroom} from "../EaseIM/mangeChatroom" | |
import {manageEasemobApis} from "../EaseIM/imApis" | |
import "./style/index.css" | |
/* components */ | |
import MessageContainer from "./message" | |
import InputBarContainer from "./inputbar" | |
console.log("EMClient", EMClient) | |
export default defineComponent({ | |
name: "EasemobChatroom", | |
props: { | |
username: { | |
type: String, | |
default: "", | |
required: true | |
}, | |
password: { | |
type: String, | |
default: "" | |
}, | |
accessToken: { | |
type: String, | |
default: "" | |
}, | |
chatroomId: { | |
type: String, | |
default: "", | |
required: true | |
} | |
}, | |
setup(props) {const { setCurrentChatroomId} = useManageChatroom() | |
const {loginIMWithPassword, loginIMWithAccessToken} = manageEasemobApis() | |
const loginIM = async (): Promise<void> => {if (!EMClient) return | |
try {if (props.accessToken) {await loginIMWithAccessToken(props.username, props.accessToken) | |
} else {await loginIMWithPassword(props.username, props.password) | |
} | |
} catch (error: any) {throw `${error.data.message}` | |
} | |
} | |
const closeIM = async (): Promise<void> => {console.log(">>>>> 断开连接") | |
// EMClient.close()} | |
onMounted(() => {loginIM() | |
if (props.chatroomId) {setCurrentChatroomId(props.chatroomId) | |
} | |
}) | |
return { | |
loginIM, | |
closeIM | |
} | |
}, | |
render() { | |
return ( | |
<> | |
<div class={"easemob_chatroom_container"}> | |
<MessageContainer /> | |
<InputBarContainer /> | |
</div> | |
</> | |
) | |
} | |
}) |
6、输入框组件代码
次要解决插件输入框性能,实现音讯文本内容,图片内容的发送。
import {defineComponent, ref} from "vue" | |
import {EasemobChat} from "easemob-websdk" | |
import {EMClient} from "../EaseIM" | |
import {useManageChatroom} from "../EaseIM/mangeChatroom" | |
/* compoents */ | |
import InputEmojiComponent from "../components/InputEmojiComponent" | |
import UploadImageComponent from "../components/UploadImageComponent" | |
import "./style/inputbar.css" | |
export enum PLACE_HOLDER_TEXT {TEXT = "Enter 发送输出的内容..."} | |
export default defineComponent({ | |
name: "InputBarContainer", | |
setup() { | |
// 根底文本发送 | |
const inputContent = ref("") | |
const setInputContent = (event: Event) => {inputContent.value = (event.target as HTMLInputElement).value | |
} | |
const {currentChatroomId, loginUserInfo, sendDisplayMessage} = | |
useManageChatroom() | |
const sendMessage = async (event: KeyboardEvent) => {if (inputContent.value.match(/^\s*$/)) return | |
if (event.code === "Enter" && !event.shiftKey) {event.preventDefault() | |
console.log(">>>>>> 调用发送办法") | |
const param: EasemobChat.CreateTextMsgParameters = { | |
chatType: "chatRoom", | |
type: "txt", | |
to: currentChatroomId.value, | |
msg: inputContent.value, | |
from: EMClient.user, | |
ext: {nickname: loginUserInfo.nickname} | |
} | |
try {await sendDisplayMessage(param) | |
inputContent.value = "" | |
} catch (error) {console.log(">>>>> 音讯发送失败", error) | |
} | |
} | |
} | |
const appendEmojitoInput = (emoji: string) => {inputContent.value = inputContent.value + emoji} | |
return () => ( | |
<> | |
<div class={"input_bar_container"}> | |
<div class={"control_strip_container"}> | |
<InputEmojiComponent onAppendEmojitoInput={appendEmojitoInput} /> | |
<UploadImageComponent /> | |
</div> | |
<div class={"message_content_input_box"}> | |
<input | |
class={"message_content_input"} | |
type="text" | |
value={inputContent.value} | |
onInput={setInputContent} | |
placeholder={PLACE_HOLDER_TEXT.TEXT} | |
onKeyup={sendMessage} | |
/> | |
</div> | |
</div> | |
</> | |
) | |
} | |
}) |
7、音讯列表组件代码
渲染聊天室内收发的音讯代码,以及列表滚动。
import {defineComponent, nextTick, watch} from 'vue'; | |
import {useManageChatroom} from '../EaseIM/mangeChatroom'; | |
import {scrollBottom} from '../utils'; | |
import './style/message.css'; | |
import {EasemobChat} from 'easemob-websdk'; | |
const {messageCollect} = useManageChatroom(); | |
const MessageList = () => {const downloadSourceImage = (message: EasemobChat.MessageBody) => {if (message.type === 'img') {window.open(message.url); | |
} | |
}; | |
return ( | |
<> | |
{messageCollect.length > 0 && | |
messageCollect.map((msgItem) => { | |
return (<div class={'message_item_box'} key={msgItem.id}> | |
<div class={'message_item_nickname'}> | |
{msgItem?.ext?.nickname || msgItem.from} | |
</div> | |
{msgItem.type === 'txt' && (<p class={'message_item_textmsg'}>{msgItem.msg}</p> | |
)} | |
{msgItem.type === 'img' && ( | |
<img | |
style={'cursor: pointer;'} | |
onClick={() => {downloadSourceImage(msgItem); | |
}} | |
src={msgItem.thumb} | |
/> | |
)} | |
</div> | |
); | |
})} | |
</> | |
); | |
}; | |
export default defineComponent({ | |
name: 'MessageContainer', | |
setup() {watch(messageCollect, () => {console.log('>>>>>> 监听到音讯列表扭转'); | |
nextTick(() => {const messageContainer = document.querySelector('.message_container'); | |
setTimeout(() => {messageContainer && scrollBottom(messageContainer); | |
}, 300); | |
}); | |
}); | |
return () => { | |
return ( | |
<> | |
<div class='message_container'> | |
<MessageList /> | |
</div> | |
</> | |
); | |
}; | |
}, | |
}); |
8、聊天室内外围办法
聊天室内局部状态治理
import {EasemobChat} from "easemob-websdk" | |
import {reactive, ref} from "vue" | |
import {DisplayMessageType, ILoginUserInfo} from "../types/index" | |
import {manageEasemobApis} from "../imApis/" | |
const messageCollect = reactive<DisplayMessageType[]>([]) | |
const loginUserInfo: ILoginUserInfo = { | |
loginUserId: "", | |
nickname: "" | |
} | |
const currentChatroomId = ref("") | |
export const useManageChatroom = () => {const setCurrentChatroomId = (roomId: string) => {currentChatroomId.value = roomId} | |
const setLoginUserInfo = async (loginUserId: string) => {const { fetchLoginUserNickname} = manageEasemobApis() | |
loginUserInfo.loginUserId = loginUserId | |
try {const res = await fetchLoginUserNickname(loginUserId) | |
loginUserInfo.nickname = res[loginUserId].nickname | |
console.log(">>>>>> 获取到用户属性", loginUserInfo.nickname) | |
} catch (error) {console.log(">>>>>> 获取失败") | |
} | |
} | |
const pushMessageToList = (message: DisplayMessageType) => {messageCollect.push(message) | |
} | |
const sendDisplayMessage = async (payload: EasemobChat.CreateMsgType) => {const { sendTextMessage, sendImageMessage} = manageEasemobApis() | |
return new Promise((resolve, reject) => {if (payload.type === "txt") {sendTextMessage(payload) | |
.then(res => {messageCollect.push(res as unknown as EasemobChat.TextMsgBody) | |
resolve(res) | |
}) | |
.catch(err => {reject(err) | |
}) | |
} | |
if (payload.type === "img") {sendImageMessage(payload) | |
.then(res => {messageCollect.push(res as unknown as EasemobChat.ImgMsgBody) | |
resolve(res) | |
}) | |
.catch(err => {reject(err) | |
}) | |
} | |
}) | |
} | |
return { | |
messageCollect, | |
currentChatroomId, | |
loginUserInfo, | |
setCurrentChatroomId, | |
sendDisplayMessage, | |
pushMessageToList, | |
setLoginUserInfo | |
} | |
} |
实例化 IM SDK
import EaseSDK, {EasemobChat} from "easemob-websdk" | |
import {mountEaseIMListener} from "./listener" | |
export let EMClient = {} as EasemobChat.Connection | |
export const EMCreateMessage = EaseSDK.message.create | |
export const initEMClient = (appKey: string) => { | |
EMClient = new EaseSDK.connection({appKey: appKey}) | |
mountEaseIMListener(EMClient) | |
return EMClient | |
} |
挂载聊天室相干监听监听
import {EasemobChat} from 'easemob-websdk'; | |
import {useManageChatroom} from '../mangeChatroom'; | |
import {manageEasemobApis} from '../imApis'; | |
export const mountEaseIMListener = (EMClient: EasemobChat.Connection) => {const { pushMessageToList, setLoginUserInfo, currentChatroomId} = | |
useManageChatroom(); | |
const {joinChatroom} = manageEasemobApis(); | |
console.log('>>>mountEaseIMListener'); | |
EMClient.addEventHandler('connection', {onConnected: () => {console.log('>>>>>onConnected'); | |
joinChatroom(); | |
setLoginUserInfo(EMClient.user); | |
}, | |
onDisconnected: () => {console.log('>>>>>Disconnected'); | |
}, | |
onError: (error: any) => {console.log('>>>>>>Error', error); | |
}, | |
}); | |
EMClient.addEventHandler('message', {onTextMessage(msg) {if (msg.chatType === 'chatRoom' && msg.to === currentChatroomId.value) {pushMessageToList(msg); | |
} | |
}, | |
onImageMessage(msg) {if (msg.chatType === 'chatRoom' && msg.to === currentChatroomId.value) {pushMessageToList(msg); | |
} | |
}, | |
}); | |
EMClient.addEventHandler('chatroomEvent', {onChatroomEvent(eventData) {console.log('>>>>chatroomEvent', eventData); | |
}, | |
}); | |
}; |
应用形式
npm install emchat-chatroom-widget
import EMChatroom from "emchat-chatroom-widget/emchat-chatroom-widget.esm.js" | |
// 引入插件外部款式 | |
import "emchat-chatroom-widget/style.css" | |
//appKey 需从环信申请 | |
createApp(App) | |
.use(EMChatroom, {appKey: "easemob#XXX"}) | |
.mount("#app") | |
// 模版组件内应用 | |
/** | |
* @param {username} string | |
* @param {password} string | |
* @param {accessToken} string | |
* @param {chatroomId} string | |
*/ | |
<EasemobChatroom | |
:username="'hfp'" | |
:password="'1'" | |
:chatroomId="'208712152186885'" | |
> | |
</EasemobChatroom> |
最终成果
相干代码
Github 源码地址
npm 相干包地址
参考资料
注册环信
环信官网 Web 端相干文档
【前端工程化 - 组件库】从 0-1 构建 Vue3 组件库(组件开发)
应用 TSX 编写 Vue3 组件