demo
最近写的IM应用,支持视频通话、多语言、主题切换等;
体验地址:fffuture.top
github地址: github.com/fffuture/IM
准备工作
下载掘金的emoji图片🙈,以便后续使用,就是下边这些:
有142个emoji图片,一个个下载有点麻烦,那就写个爬虫去批量下载。在e0172e9.js文件下有所需的信息:
文件中包含emoji的中文名称和文件名称:
用node把它下载保存到本地, 关键代码:
/**
* @description 获取并保存包含emoji相关信息的js文件,比如emoji名字和地址
*/
function saveJSFile() {
return new Promise(async (resolve, reject) => {
try {
fs.writeFile("./emoji/jsFile.js", "111", err => {
if(!err) console.log("创建jsFile.js成功~");
});
const dest = fs.createWriteStream('./emoji/jsFile.js');
let res = await fetch('https://lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/90efd53.js');
let reader = res.body.pipe(dest);
reader.on('finish', () => {resolve("success");});
} catch (error) {
reject("false");
}
})
}
我期望的目标数据结构:
[
{
"url":string, // emoji原始的图片地址
"CN":string, // emoji中文名称
"EN":string, // emoji英文名称
},
...
]
缺少了英文名称,文件中又不提供,那就写个爬虫实时调用翻译接口,将中文翻译成英文:
const fetch = require('node-fetch');
const cheerio = require('cheerio');
/**
* @description 翻译函数,多个单词使用小写并下划线隔开,例子:微笑你好 -> Smile hello -> smile_hello
* @param {string} word 需要翻译的中文
* @param {number} duration 延长请求时间,防止被识别为爬虫
* @returns {Promise} 翻译完成的英文
*/
function translate(word, duration) {
return new Promise((resolve, reject) => {
if(!word) resolve("empty_input");
setTimeout(() => {
fetch(`https://m.youdao.com/translate?inputtext=${encodeURI(word)}&type=ZH_CN2EN`)
.then(res => res.text())
.then(body => {
const $ = cheerio.load(body);
let name = ($(".generate #translateResult").text() + "").trim();
resolve((name+"").toLowerCase().replace(/ /g, '_').replace(/\,/g, ""));
})
.catch(err => {
console.log("---err: ", err);
// reject(false);
resolve("empty_input")
})
}, duration)
})
}
由于调用有道翻译mobile端接口(只有这个好使),返回的是一个html文件,所以使用cheerio库做dom解析,以便提取翻译结果。
翻译测试:
const {translate} = require('../src/utils/getEmoji');
test('翻译输入 微笑', async () => {
let result = await translate('微笑');
expect(result).toMatch('smile')
});
test('翻译输入 空值', async () => {
let result = await translate('');
expect(result).toMatch('empty_input')
})
test('翻译输入 english', async () => {
let result = await translate(undefined);
expect(result).toMatch('english')
})
到目前为止,对文件内容和翻译接口稍微处理就可以得到想要的数据结果:
最后一步,将emoji图片保存到本地并重属名:
async function saveImg() {
let fetchList = [];
for(let item of imgArr) fetchList.push(fetch(item.url));
return new Promise((resolve, reject) => {
try {
let i = 0;
Promise.allSettled(fetchList)
.then(results => results.forEach((result) => {
if(result.status === "fulfilled") {
let dest = fs.createWriteStream(`./emojiEN/emoji_${imgArr[i].EN}.png`);
result.value.body.pipe(dest);
i++;
console.log(`保存${imgArr[i].CN}${imgArr[i].EN}成功`);
}else {
console.log(`保存${imgArr[i].CN}${imgArr[i].EN}失败`, err);
}
}))
.catch(err=> {
console.log("--保存表情失败: ", err);
});
resolve("saved");
} catch (error) {
reject('---error')
}
});
}
最终获取到的emoji表情:
开始写聊天输入框
先来看看最终效果:
样式方面比较简单,除了输入框关键css,其他就此不做赘述。
.input-container {
box-sizing: border-box;
padding: 0.53rem 0;
display: flex;
justify-content: space-between;
align-items: flex-end;
width: 100%;
min-height: 2.3rem; /* 整个footer的最小高度 */
max-height: 7.1rem; /* 最大高度 */
background-color: var(--Dialogue-footer-bg);
}
.rich-text {
box-sizing: border-box;
padding: .5rem .5rem;
flex-grow: 1;
flex-shrink: 1;
min-height: 1.3rem; /* 输入框的最小高度 */
max-height: 6.04rem; /* 最大高度 */
color: var(--Dialogue-color);
background-color: var(--Dialogue-input-bg);
border-radius: .2rem;
text-align: left;
line-height: 1.5;
word-break: break-all;
outline: none;
overflow: auto;
}
输入框使用的是div的contentEditable属性,由于div没有value属性,但可编辑div有input事件,所以模拟个类似的v-model的功能,关键代码如下:
<div ref="richText" class="rich-text" contentEditable=true @click="inputMsg" @input="textChange"></div>
/**
* @description 当contentEditable中内容发生变化时,将值实时赋值给content:模拟数据双向绑定(v-model)
*/
function textChange(e:any) {
content.value = e.srcElement.innerHTML;
}
/**
* @description 点击表情,将对应字符串推进输入框中 例子: 点击‘微笑表情’ -> 输入框中增加: [微笑]
*/
function pushImg(emoji:Emoji) {
richText.value!.innerHTML += `[${emoji.CN}]`;
content.value += `[${emoji.CN}]`;
}
富文本的处理比较麻烦,在PC端,兼容性和表现都还不错,但在移动端,就有点一言难尽了。也参考过掘金的输入框,在PC端表现良好,但在mobile端,‘表情’选项直接没了。思量了很久都没有一个完美的解决方案,最后妥协采用了:在输入框中,使用‘[emoji中文名称]’来替代表情图片,在对话框中正常显示图片。
为什么不选择input之类的作为输入框:
- input:无法显示多行
- textarea:不能随输入的内容自动控制高度,或者说实现起来很麻烦且不合理
- contentEditable插入表情dom节点:在mobile端,主要是光标问题,在点击表情图片和输入框空白位置时,都会有问题。