孔乙己从鲁镇的破落小屋一觉醒来,竟然发现自己来到了现代世界,成为了一个辛苦的打工人,好不容易适应了写前端的辛苦工作生活,一天下班回到出租屋时,突然感到一阵突来的孤独。
想着最近AI如火如荼,自己又在学React,于是孔乙己打算自给自足,写一个基于AI的聊天软件。
说干就干,孔乙己想的是做一个Web版本的聊天软件,这样如果其他人想使用也比较方便,于是就打开笔记本,开始构思起来。
- 工具链方面直接就用的Vite,写个人项目还是足够的,没什么疑虑
- 至于是用JS还是TS,孔乙己思考了挺长时间,但最终孤独打败了他,用TS写项目有时候会陷入因为要避免anyscript风格而转向类型体操的困境,这会花费挺多时间不注重在功能实现本身,为了能快些用到聊天软件,孔乙己最终决定使用JS
决定好了工具链和语言后,孔乙己很干脆地新建了项目并放到Github上,因为UI有些参考微信,因此他将项目的名称起为ourchat。
注意上述的 ourchat 已经不使用了,改成了Electron应用版本的 ourchat-app
项目有了,孔乙己便开始规划功能,起初的规划是很简单:
- 首先,当然需要可以聊天,可以发文字、小表情和图片等,后续做复杂了可以更进一步地实现在线表情包、文件等
- 其次,需要有好友管理功能,可以添加好友和删除好友
- 再次,需要朋友圈的功能,光是聊天是不丰富的,最好可以看好友的朋友圈,也可以自己写朋友圈给他们看
- 最后,当然是好友可以聊天、发朋友圈的功能,因为朋友很少,因此还需要引入AI,并且为了让AI成为知心好友,还可以设定好友的基本性格
有了设想,孔乙己开始画简略的UI和设计功能了,实际上UI和功能都分为几大块:
- 聊天
- 通讯录
- 朋友圈
- 设置
有了功能分块后,为了组织页面,孔乙己使用了react-router,然后就需要写每一块功能了。
然而,当孔乙己准备写具体实现的时候,才发现没有数据支撑的情况下写页面比较不直观,因此,他稍缓一步,开始实现聊天软件的数据储存和Mock数据那一块,作为聊天软件,当然不能一刷新页面就重置了,因此,他决定采取浏览器本地储存作为数据容器。
- 聊天数据使用indexedDB储存,比如用户信息、用户关系信息、聊天信息等
- 其它简单数据采用localStorage储存,比如页面布局拖拽信息、可拖拽窗体位置等
- 如果有更临时的简短数据,就用sessiongStorage储存,比如当前的聊天好友ID等
使用localStorage和sessiongStorage比较简单,但直接操作indexedDB就比较繁琐,因此孔乙己就使用dexie做一层包装,顿时就简单多了,由于不会数据库,孔乙己直接将数据大概分为几张表:
- users表,储存独立的用户信息,包含一些基本信息,username、nickname等
- relations表,储存用户好友信息,具体有fromId、targetId、alias、character、status这些信息,alias是用户添加好友时另起的别名,character是给好友设定的基本性格,status是简单的状态,比如说是否显示在聊天界面
- messages表,储存用户间的聊天内容
- fileStore表,储存聊天时上传的图片、文件等
有了基本的表结构,孔乙己便可以开始后续操作了。
生成用户
生成用户是个比较难做好的点,主要就是用户名的生成,常见的fake库大都包含英文名的生成,但中文随机姓名就比较少,为此,孔乙己打算自己写一个简单的,就从常见姓氏中选择一个姓,然后从古诗词中选一两个字作为名就行,具体的实现他放在了另外的库,见random-names。
除了用户名,用户的头像也是麻烦的点,想了挺多办法,最终孔乙己决定用最原始的法子,用Stable Diffusion生成200个头像,然后随机选择一个,因为Stable Diffusion的模型用的是counterfeit,生成的图属二次元少女比较好看,因此,暂时的头像都是二次元少女。
聊天服务器
为了方便后续聊天服务器可能会被替换成线上的或者别的,孔乙己将indexedDB包装成一个server,在server上实现具体的接口作为页面中真正调用的部分。
class Server {
getUser(id) {}
getUsers() {}
getActiveContacts(fromId) {}
getContacts(fromId) {}
...
}
聊天
聊天是最主要的功能,但也不复杂,UI上看就是左侧的聊天好友目录和右侧的聊天内容:
实现的过程中,孔乙己记录下了以下几点:
- 为了避免原生的滚动条不好看,使用了OverlayScrollbars
- 为了实现拖拽改变两边的宽度,使用了splitjs
- 为了输入小表情,使用了emoji-mart
- 为了显示Markdown格式的聊天内容,使用了react-markdown
- 为了实现搜索中文名,使用了pinyin-engine
- 上传的图片直接存在indexedDB里,聊天内容存的是图片的key,避免多次上传相同图片
通讯录
通讯录会列出所有的好友,并且可以设定好友的性格,这个相比聊天就简单些。
当然可以删除和添加好友:
朋友圈
孔乙己写这篇帖子的时候,朋友圈只有个基本的雏形,用户需要在通讯录“拍一拍”好友他就会发一篇简单的朋友圈。
设置
语言设置
语言设置使用的是react-i18next,没什么特殊之处。
AI设置
AI这一块,孔乙己遇到了很大的困难:
- 孔乙己最开始用的是Kimi的Moonshot接口,但它对请求频率和Token限制比较严格,经常会无响应或者说请求太频繁,于是孔乙己打算换成通义千问试一试
- 换成通义千问,结果发现它在浏览器端请求会跨域,为了最快解决问题,孔乙己在本地搭了个简单的Node后台作为代理,这的确是没啥问题,但需要搭一个本地代理就很麻烦
- 为了看看还有没有别的选择,孔乙己又用了一下智谱,然而智谱的情况和Moonshot几乎一样
- 想了挺久,孔乙己觉得明明用的是Web应用,却还要本地搭一个服务器就太麻烦了(孔乙己太穷,没有买云服务器),况且普通用户如果也想用这个应用,那也要搭一个代理服务器就太麻烦了,还不如直接做成Electron应用,这样虽然要下载安装一下,但比搭建代理服务器还是要简单的,于是,孔乙己打算推翻过去的设想,将Web应用改成Electron应用
最终,孔乙己将应用改成了Electron版本,这样,AI设置对于用户来说就只需要填一个Key了。
Electron改造
Electron基础框架孔乙己选了个Electron Vite,这样改动量比较小些,不过为了适配本地应用,还做了以下的修改:
- 修改AI的请求结构,请求部分全部放在Electron主进程中,因此通义千问不会跨域了
- i18n不使用Backend版本了,没必要专门去请求翻译文件
- react router改成hash路由
- 稍微改了些样式,增加了应用栏等
最后,新版本孔乙己放在了和Web版本不同的位置,见ourchat-app。
后续
至此,孔乙己终于可以和许多知心好友聊天了,然而,他还没聊几句,更大的孤独却迎面而来,在这样的现代社会。