自己动手搭建聊天APP

自己动手实现聊天APP
成果
开始
时隔两年多,想再次看看 app 的开发。还记得两年前辛苦使用 andro studio 写 xml 的日子,五味杂陈。网上走了一圈,发现 dcloud 公司推出了 uni-app 和 5 + app 的方式开发 app , 为了知道这些方式和 andro studio 开发 app 的区别。我开始了探寻。
uni-app 和 5 + app
uni-app 最大的特点便是 编写一套代码,可发布到iOS、Android、H5、以及各种小程序(微信/支付宝/百度/头条/QQ/钉钉/淘宝)、快应用 ,并且 基于vue 。听上去方便不少哦。
5 + app 是 HTML5 Plus移动App的意思。HTML5 plus(即HTML5 +)是W3C提供一套规范,属于工信部。扩展了 JavaScript 对象 plus,使得js可以调用摄像头、陀螺仪、文件系统等。HBuilder内置HTML5 + APP开发环境,可以在云端将代码打包为 apk 等文件。
经过测试发现 uni-app 编译过程稍微有点漫长,对于开发来说测试有点费时间。最终我选择 5 + app 的方式开发APP。
mui
打开 hbuilder ,新建项目可以发现有 mui 项目模板。
mui 是 dcloud 公司推出的 5 + app 开发的一款 ui 框架 ,听说性能最接近原生APP。 官网是 【mui】,官网提供了大量示例,可以参考(也可以粘贴复制哦)。但总的来说文档不是很丰富吧,有些功能我还是在百度解决。
尬聊APP
来源
在学习了两天的 mui api后我想尝试写个 app 来锻炼下自己,也为了对比 5 + app 和 android studio 开发上的深层区别和感受,再加上之前对于 即时聊天 app 的 实现感兴趣,想摸索一番,于是取了这个名字便开始了开发。
技术选型
后端大致选择 springboot + netty + mybatisplus 前端大致选择 mui + vue 数据库依旧选择 mysql(当然后边可以换其他数据库)
项目注意点
前端代码需要注意几个问题 : 1 mui 配合 vue 一起玩的时候 @click 会不起作用,只能用 mui 去绑定 tap 事件后调用 this.click() 方法,因为 mui 禁用了 click 事件 ,点击会触发 tap 事件。当然本页面暂未使用到 vue , 其他页面注意。解决代码如下:
mui(document.body).on('tap', '#btn_clcik', function(e) {
this.click()
})
2 mui 配合 vue 一起玩的时候 v-model 双向绑定不起作用。暂时我使用 document 去获取值。比如获取输入框内容时:
document.getElementById('galiao_userId').value
3 如果自定义的 js 文件用到 mui 、vue、axios 中的对象或方法时,尽量后引入。比如:
4 尽量使用 v-text 代替 {{ }} 避免插值闪烁。
后端项目
构建
新建maven工程,pom.xml:
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
项目结构
核心
核心功能便是 netty 提供的服务了,在 netty 包下三个类:
GaLiaoServer 表示 Netty 服务的启动类。 GaLiaoServerInitializer 表示 Netty 服务的初始化类。 GaLiaoHandler 表示Netty 服务的处理器类。(这里面包含消息的接受和发送,它就是核心)
至于代码方面大家感兴趣可以去【尬聊开源后端项目】
项目启动页(index.html)
前端代码:
mui.init();
mui.plusReady(function() {
// 弹到登录页面
mui.openWindow({
url: 'login.html',
id: 'login'
});
})
这个页面暂时只是负责打开了登录页面,后期可以做成判断是否登录或自动登录,根据情况跳转到不同页面。
登录(login.html)
架构图
主要还是使用 mvc ,这里暂时不用 netty 。登录成功返回 token,客户端保存 token ,携带 token 访问服务端资源,包含后期使用 netty 发送消息也需要携带 token 。
前端登录页面代码:
body {
background-color: #FFFFFF;
}
.title {
margin: 0;
background-color: #FFFFFF;
}
.logo {
width: 100px;
height: 100px;
border-radius: 10px;
}
.form {
margin-left: 10%;
margin-right: 10%;
background-color: #FFFFFF;
}
.form .galiao-user-id, .form .galiao-user-password {
margin-bottom: 30px;
border: none;
border-radius: 50px;
background-color: #F3F3F3;
}
.btn-ok {
width: 70px;
height: 70px;
border-radius: 35px;
}
.btn-img {
width: 40px;
height: 40px;
padding-top: 5px;
}
尬聊 v1.0
placeholder="密码">
mui.init()
mui.plusReady(function() {
// 关闭 index 页面
plus.webview.currentWebview().opener().close()
})
// 登录操作
mui(document.body).on('tap', '#btn_clcik', function(e) {
// 关闭软键盘(否则老弹出来)
document.activeElement.blur();
let userId = document.getElementById('galiao_userId').value
let userPwd = document.getElementById('galiao_password').value
// 验证用户名密码是否输入
if (userId === '') {
mui.alert("请输入尬聊号!")
}
if (userId === '') return // 直接在上面if里面return alert就会没反应,一切为了用户体验
if (userPwd === '') {
mui.alert("请输入密码!")
}
if (userPwd === '') return // 直接在上面if里面return alert就会没反应 一切为了用户体验
// 显示加载中动画
mui.showLoading("正在登录....","div")
// 发起登陆请求
axios({
url: config.app.baseUrl + '/user/login',
method: 'post',
headers: {
'Content-Type': 'application/json'
},
data: {
userId: userId,
userPwd: userPwd
}
})
.then(function(res) {
var data = res.data
if (data.code != 1) {
mui.alert(data.data)
return
}
// 登陆成功
// 1 记录 登录信息 (user里面包含token)
data.data.token = data.token
let user = JSON.stringify(data.data)
plus.storage.setItem("user", user)
// 2 跳转页面
mui.openWindow({
url: 'main.html',
id: 'main'
});
// 关闭加载
mui.hideLoading()
})
.catch(function(error) {
// 关闭加载
mui.hideLoading()
mui.alert("网络故障!")
});
})
// 关闭其他页面
window.addEventListener('clearOtherHtml', function(event) {
// 获取当前webview窗口对象
var curr = plus.webview.currentWebview()
//获取所有已经打开的webview窗口
var wvs = plus.webview.all()
for (var i = 0, len = wvs.length; i < len; i++) {
//关闭除当前页面外的其他页面
if (wvs[i].id == curr.id)
//遇到当前页跳过
continue
// 先hide,避免闪一下
wvs[i].hide()
//非当前页执行关闭
wvs[i].close()
}
});
登录页面效果:
后端主要代码:
@Override
public Message login(User user) {
User byId = getById(user.getUserId());
if (byId == null) {
return new Message()
.code(MessageCode.FAIL.value())
.data("用户不存在!");
}
if (!encoder.matches(user.getUserPwd(), byId.getUserPwd())) {
return new Message()
.code(MessageCode.FAIL.value())
.data("用户名或密码错误!");
}
// 密码正确,产生token并返回
user.setUserPwd(null); // 清理掉密码
byId.setUserPwd(null); // 清理掉密码
String token = JwtTokenUtil.createToken(user);
// redis 存储登录的用户的信息 (存储一天)
redisUtil.set(token, FastJsonUtil.toJSONString(byId), 24 * 60 * 60);
// 获取好友
String[] friendIds = byId.getUserFriends().split(",");
QueryWrapper
wrapper.in("user_id", friendIds);
wrapper.select("user_id", "user_avatar", "user_nickname");
List
byId.setFriends(users);
return new Message().code(MessageCode.OK.value()).data(byId).token(token);
}
后端主要用到 spring security 的 BCryptPasswordEncoder 对密码加密和解密,用的 sha256 算法,因此不可逆的,保证密码的安全性。登录成功后会将用户的基本信息(除了密码)放入redis ,key 为用户的token, 暂时默认存一天也是 token 的过期时间。然后把用户信息返回客户端,客户端保存在 localstorage 供页面之间共享。
主页面(main.html)
主页面主要是 main.html 页面,这个页面有一个底部导航栏来控制显示其他几个页面。 前端代码如下:
.mui-bar-tab .mui-tab-item.mui-active {
color: #4DB361; /* 这里放你底部导航栏颜色 */
}
消息
联系人
发现
我的
mui.init()
var vue = new Vue({
el: '#app',
created() {
mui.plusReady(function() {
if(plus.webview.currentWebview().opener()) {
var curr = plus.webview.currentWebview()
var wvs = plus.webview.all()
for (var i = 0, len = wvs.length; i < len; i++) {
if (wvs[i].id == 'login' || wvs[i].id == 'HBuilder') {
// 先hide再关闭 ,避免闪一下
wvs[i].hide()
wvs[i].close()
}
}
}
init();
});
}
})
/**
* main页面初始化
*/
function init() {
var sixinArray = [{
pageUrl: 'message.html',
pageId: 'message'
},
{
pageUrl: 'friends.html',
pageId: 'friends'
},
{
pageUrl: 'found.html',
pageId: 'found'
},
{
pageUrl: 'mine.html',
pageId: 'mine'
}
]
var sixinStyle = {
top: '0px',
bottom: '51px'
}
//获取当前的webview对象
var indexWebview = plus.webview.currentWebview();
//向当前的主页webview追加子页的4张webview对象
for (var i = 0; i < sixinArray.length; i++) {
var sixinPage = plus.webview.create(sixinArray[i].pageUrl,
sixinArray[i].pageId, sixinStyle);
//创建完后不需要马上显示,只有点击的时候才显示,所以隐藏webview窗口
sixinPage.hide();
//追加每一个子页面到当前主页面
indexWebview.append(sixinPage);
}
//设置默认显示页面
plus.webview.show(sixinArray[0].pageId);
//批量绑定tap(点击)事件,展示不同的页面
//通过mui选择器选择唯一的class ,on是表示触发的事件,tap表示手指触摸点击事件类型
//第二个参数表示link的对象,也可以用共同的a标签代替
mui(".mui-bar-tab").on("tap", ".mui-tab-item", function() {
//或者使用a标签选择器 mui(".mui-bar-tab").on("tap", "a", function() {
//在要点击的标签处添加事件名 并且获取到相应的对象
var tabindex = this.getAttribute("tabindex");
//显示tap点击的页面,第一个id,第二个动画效果,第三个延迟时间
plus.webview.show(sixinArray[tabindex].pageId, "none", 400);
//隐藏不需要的页面
for (var i = 0; i < sixinArray.length; i++) {
if (i != tabindex) {
plus.webview.hide(sixinArray[i].pageId, "none", 400);
}
}
});
}
该页面效果如下:
该页面难点在于底部导航栏每个 a 标签控制每个页面显示与隐藏,而官网只提供了基于a 标签 href 的代码块的显示与隐藏,这种方式需要将其他页面以 div 的方式写在 main 页面,会导致 main 页面代码过于冗杂。本次使用的切换 html 的方式核心代码在于 init 方法,通过 a 标签上的 tabindex 的值来显示不同页面。
消息页面(message.html)
前端代码:
.mui-bar-nav {
background-color: #4DB361;
}
.galiao-title {
color: #FFFFFF;
}
.galiao-message img {
border-radius: 5px;
}
消息
mui.init()
document.galiao = {
// 后端接口
url: config.app.sockUrl
}
/**
* 尬聊app初始化
*/
function galiao_init() {
// 创建 socket
if (window.WebSocket) {
// 构建socket
window.sock = new WebSocket(document.galiao.url)
// 刚建立连接时需要mark一下(让服务端把你的channel和尬聊号建立联系)
window.sock.onopen = function(event) {
window.sock.send(JSON.stringify({
action: 'mark',
token: vue.$data.user.token
}))
}
// 服务端有消息推送
window.sock.onmessage = function(event) {
var chat = plus.webview.getWebviewById('chat');
if (chat) {
mui.fire(chat, 'receiveMessage', {
message: event.data
})
// 添加到消息
vue.receiveNewMessage(event.data)
}
}
}
}
/**
* 调用本页面的 socket 发送消息
* @param {Object} message
*/
function sendMessage(message) {
window.sock.send(message)
addNewMessage(JSON.parse(message))
}
/**
* 新增消息栏位
*/
function addReceiver(receiver) {
vue.addReceiver(JSON.parse(receiver))
}
// 打开聊天窗口
mui(document.body).on('tap', '#openChat', function(e) {
this.click()
})
// 添加最新的消息
function addNewMessage(message) {
vue.addNewMessage(message)
}
/**
* vue实例
*/
var vue = new Vue({
el: '#app',
data: {
user: {},
receivers: []
},
methods: {
addReceiver(receiver) {
// 先判断是否已经存在
for(receiver1 of this.receivers) {
if(receiver1.userId === receiver.userId)
this.openChat(receiver1)
return
}
// 添加到消息列表
receiver.messages = []
this.receivers.push(receiver)
this.openChat(receiver)
},
openChat(receiver) {
mui.openWindow({
url: 'chat.html',
id: 'chat',
extras: {
receiver: receiver
}
});
},
// 添加最新消息
addNewMessage(message) {
for(receiver of this.receivers) {
if(receiver.userId === message.receiver) {
receiver.messages.push(message)
}
}
},
// 收到新消息
receiveNewMessage(message) {
let inMessage = JSON.parse(message)
for(receiver of this.receivers) {
if(receiver.userId === inMessage.sender) {
receiver.messages.push(inMessage)
}
}
}
},
created() {
let that = this
mui.plusReady(function() {
// 初始化
galiao_init();
that.user = JSON.parse(plus.storage.getItem('user'))
})
}
})
消息页面效果:
该页面构建了一个websocket对象供聊天信息的发送和接受,但该对象只能在本页面使用,其他页面(chat.html)需要调用本页面方法来发送消息。该页面收到消息直接传给chat页面,chat页面进行判断后选择显示或忽略。该页面涉及到消息数据的保存功能请参见代码。
联系人页面(friends.html)
前端代码:
.mui-bar-nav {
background-color: #4DB361;
}
.galiao-title {
color: #FFFFFF;
}
.galiao-friend img {
border-radius: 5px;
}
#topPopover {
position: fixed;
top: 16px;
right: 6px;
}
#topPopover .mui-popover-arrow {
left: auto;
right: 6px;
}
.mui-popover {
width: 140px;
height: 100px;
}
联系人
mui.init()
// 点击事件
mui(document.body).on('tap', '#open_friend_info', function(e) {
this.click()
})
// 点击扫一扫
mui(document.body).on('tap', '#btn_sao', function(e) {
this.click()
})
// 点击搜索
mui(document.body).on('tap', '#btn_sou', function(e) {
this.click()
})
/**
* vue实例
*/
var vue = new Vue({
el: '#app',
data: {
user: {}
},
methods: {
init() {
this.user = JSON.parse(plus.storage.getItem('user'))
},
openFriendInfo(friend) {
mui.openWindow({
url: 'friend_info.html',
id: 'friend_info',
extras: {
userId: friend.userId
}
});
},
saoClick() {
mui.openWindow({
url: 'sao.html',
id: 'sao'
});
mui('#topPopover').popover('toggle'); // 关闭弹出菜单
},
souClick() {
mui.openWindow({
url: 'sou.html',
id: 'sou'
});
mui('#topPopover').popover('toggle');
}
},
created() {
let that = this
mui.plusReady(function() {
that.init()
})
}
})
页面效果:
联系人页面和消息页面比较相似,可以 copy 一部分。
发现页面 (found.html)
前端代码:
.mui-bar-nav {
background-color: #4DB361 ;
}
.galiao-title {
color: #FFFFFF;
}
.galiao-li {
height: 45px;
}
.galiao-li img {
width: 25px;
padding-bottom: 15px;
}
发现
-
毒鸡汤(每日一毒)
-
代做决定
mui.init()
// 打开毒鸡汤页面
mui(document.body).on('tap', '#dujitang', function(e) {
mui.openWindow({
url: 'du.html',
id:'du'
});
})
// 打开代做决定页面
mui(document.body).on('tap', '#decision', function(e) {
mui.openWindow({
url: 'decision.html',
id:'decision'
});
})
这个页面主要负责打开其他页面,目前主要打开 du.html 和 decision.html,这两个页面目前主要还是 js 在玩,没有 ajax 操作。可在【尬聊开源地址】查看。
我的页面(mine.html)
前端代码:
.mui-bar-nav {
background-color: #4DB361;
}
.galiao-li {
height: 45px;
}
.galiao-title {
color: #FFFFFF;
}
.galiao-mine-header {
width: 100%;
height: 150px;
background-color: #FFFFFF;
padding-top: 16%;
padding-left: 10%;
}
.galiao-mine-header img {
width: 60px;
height: 60px;
border-radius: 6px;
margin-right: 22px;
}
.galiao-mine-header div {
padding-top: 10px;
}
.galiao-mine-header-title {
color: #000000;
font-size: 24px;
}
.galiao-mine-header-desc {
font-size: 14px;
color: #8F8F94;
}
.galiao-mine ul,
.galiao-mine div {
margin-bottom: 20px;
}
.galiao-quit-login {
width: 100%;
height: 40px;
}
我的
mui.init()
// 更换头像
mui(document.body).on('tap', '#btn_replace_avatar', function(e) {
mui.alert("该功能暂无法使用哦!")
})
// 更换昵称
mui(document.body).on('tap', '#btn_replace_nickname', function(e) {
mui.alert("该功能暂无法使用哦!")
})
// 展示二维码
mui(document.body).on('tap', '#btn_show_qrcode', function(e) {
this.click()
})
// 赞赏
mui(document.body).on('tap', '#btn_show_admire', function(e) {
mui.alert("小朋友,省点钱买辣条吃!")
})
// 关于
mui(document.body).on('tap', '#btn_show_about', function(e) {
//打开about页面
mui.openWindow({
url: 'about.html',
id: 'about'
});
})
// 退出登录
mui(document.body).on('tap', '#btn_quit_login', function(e) {
// 清理缓存
clearCache()
//打开login页面
mui.openWindow({
url: 'login.html',
id: 'login'
});
// 调用login的方法清除其他页面
var login = plus.webview.getWebviewById('login');
mui.fire(login, 'clearOtherHtml')
})
var vue = new Vue({
el: '#galiao_mine',
data: {
user: {
userId: '',
userNickname: '',
userAvatar: ''
}
},
methods: {
showQrcode() {
let that = this
mui.openWindow({
url: 'mine_qrcode.html',
id: 'mine_qrcode',
extras: {
userId: that.user.userId
}
});
}
},
created() {
let that = this
mui.plusReady(function() {
that.user = JSON.parse(plus.storage.getItem('user'))
})
}
})
页面效果: 这个页面目前主要实现了我的二维码展示、关于、退出三个功能,后续可以做其他功能。
好友信息页面(friend_info.html)
.mui-bar-nav {
background-color: #4DB361;
}
.galiao-btn-return {
color: #FFFFFF;
}
.galiao-li {
height: 45px;
}
.galiao-title {
color: #FFFFFF;
}
.galiao-mine-header {
width: 100%;
height: 150px;
background-color: #FFFFFF;
padding-top: 16%;
padding-left: 10%;
}
.galiao-mine-header img {
width: 60px;
height: 60px;
border-radius: 6px;
margin-right: 22px;
}
.galiao-mine-header div {
padding-top: 10px;
}
.galiao-mine-header-title {
color: #000000;
font-size: 24px;
}
.galiao-mine ul {
margin-bottom: 20px;
}
.galiao-mine-header-desc {
font-size: 14px;
color: #8F8F94;
}
.galiao-quit-login {
width: 100%;
height: 40px;
}
mui.init()
// 点击发消息按钮
mui(document.body).on('tap', '#btn_send_message', function(e) {
this.click()
})
// 点击设置备注按钮
mui(document.body).on('tap', '#btn_set_remark', function(e) {
mui.alert("该功能暂无法使用哦!")
})
// 点击添加好友
mui(document.body).on('tap', '#btn_add_friend', function(e) {
// this.click()
mui.alert("该功能暂无法使用哦!")
})
/**
* vue实例
*/
var vue = new Vue({
el: '#app',
data: {
user: {
userId: '',
userNickname: '',
userAvatar: '',
isFriend: false
}
},
methods: {
// 打开聊天界面
openChat() {
let that = this
// 不能和自己聊天
if (that.user.userId == JSON.parse(plus.storage.getItem('user')).userId) {
mui.alert("无法和自己聊天!")
return
}
// message 页面新增消息栏
this.messageView.evalJS("addReceiver('" + JSON.stringify(that.user) + "')")
},
// 添加好友
addFriend() {
let that = this
axios.post(config.app.baseUrl + '/user/friend/' + that.user.userId, null, {
params: {
token: JSON.parse(plus.storage.getItem('user')).token
}
})
.then(function(res) {
var data = res.data
if (data.code != 1) {
mui.alert(data.data)
return
}
mui.alert("添加成功!")
that.openChat()
// 更新本地数据
})
.catch(function(error) {
mui.alert("网络异常!")
});
}
},
created() {
let that = this
mui.plusReady(function() {
// 获取上个页面传来的参数
that.user.userId = plus.webview.currentWebview().userId
//获取 message 页面
var wvs = plus.webview.all()
for (var i = 0, len = wvs.length; i < len; i++) {
// 获取建立socket连接的 message 页面
if (wvs[i].id == 'message') {
vue.messageView = wvs[i]
break
}
}
// 获取好友信息
if (that.user.userId) {
axios.get(config.app.baseUrl + '/user/' + that.user.userId, {
params: {
token: JSON.parse(plus.storage.getItem('user')).token
}
})
.then(function(res) {
var data = res.data
if (data.code != 1) {
mui.alert(data.data)
return
}
that.user = data.data
})
.catch(function(error) {
console.log(error)
});
}
})
}
})
页面效果:
这个页面会根据情况判断,如果不是自己的好友,会显示 添加好友 按钮,如果是自己的好友就显示 发消息 按钮。这一块的判断在服务端,主要逻辑如下:
@Override
public Message userById(Long userId, String token) {
// 验证token
if(checkToken(token)) {
QueryWrapper
wrapper.eq("user_id", userId);
wrapper.select("user_id", "user_avatar", "user_nickname");
User user = userMapper.selectOne(wrapper);
if(user == null)
return new Message().code(MessageCode.USER_NOT_FOUND.value()).data("该用户不存在!");
// 用户存在,检查是否是自己的好友
boolean isFriend = false;
// 获取自己的好友列表
String[] split = FastJsonUtil.parseObject(
(String) redisUtil.get(token), User.class)
.getUserFriends()
.split(",");
for (String s : split) {
// 判断是否是自己好友
if(user.getUserId().equals(Long.valueOf(s))) {
isFriend = true;
break;
}
}
user.setIsFriend(isFriend);
return new Message().code(MessageCode.OK.value()).data(user);
}
// token不合法
return new Message().code(MessageCode.FAIL.value()).data("token不合法");
}
聊天页面(chat.html)
前端代码:
body {
background-color: #F2F2F2;
}
.mui-bar-nav {
background-color: #4DB361 ;
}
.galiao-btn-return {
color: #FFFFFF;
}
.galiao-chat-title {
color: #FFFFFF;
}
.galiao-chat-tab {
padding: 7px 12px 3px 12px;
}
.galiao-avator {
width: 50px;
height: 50px;
}
.mui-table-view {
background-color: #F2F2F2;
}
.mui-table-view:after{ height:0}
.mui-table-view:before{ height:0}
.mui-table-view-cell:after{ height:0}
.mui-table-view-cell:before{ height:0}
.galiao-message-bubble-left {
position: relative;
display: inline-block;
padding: 10px 15px 10px 20px;
margin-top: 5px;
background-color: #FFFFFF;
font-size: 14px;
border-radius: 5px;
margin-left: 20px;
max-width: 260px;
}
.galiao-message-bubble-left:before, .galiao-message-bubble-left:after {
content: ""; /*:before和:after必带技能,重要性为满5颗星*/
display: block;
position: absolute; /*日常绝对定位*/
top: 15px;
left: -12px;
border: 6px solid transparent;
border-right-color: #FFFFFF;
width: 0px;
height: 0px;
}
.galiao-message-bubble-right {
position: relative;
display: inline-block;
padding: 10px 15px 10px 20px;
margin-top: 5px;
background-color: #9fe766;
font-size: 14px;
border-radius: 5px;
margin-right: 20px;
max-width: 260px;
}
.galiao-message-bubble-right:before, .galiao-message-bubble-right:after {
content: ""; /*:before和:after必带技能,重要性为满5颗星*/
display: block;
position: absolute; /*日常绝对定位*/
top: 15px;
right: -10px;
border: 6px solid transparent;
border-left-color: #9fe766;
width: 0px;
height: 0px;
}
.galiao-avator{
border-radius: 5px;
}
mui.init()
/**
* 收到消息
* @param {Object} event
*/
window.addEventListener('receiveMessage', function(event) {
//获得事件参数
var message = event.detail.message;
vue.receiveMessage(message)
});
// 按钮点击事件
mui(document.body).on('tap', '#btn_send_message', function(e) {
this.click()
})
/**
* vue实例
*/
var vue = new Vue({
el: '#app',
data: {
messageData: [],
user: {},
receiver: {}
},
methods: {
// 发消息
sendMessage() {
let message = document.getElementById('concurrentMessage').value
if (message != '') {
// 构建传输的消息格式
let msg = {
action: 'out', // 操作类型
receiver: this.receiver.userId, // 接受者尬聊号
data: message, // 信息
token: this.user.token // token
}
// 调用父页面发送消息
this.socketView.evalJS("sendMessage('" + JSON.stringify(msg) + "')")
this.messageData.push(msg)
document.getElementById('concurrentMessage').value = ''
let that = this
setTimeout(function() {
that.movieToBottom()
}, 100)
}
},
// 接收消息
receiveMessage(message) {
let inMessage = JSON.parse(message)
if (inMessage.sender === this.receiver.userId) {
let that = this
that.messageData.push({
action: 'in',
data: inMessage.data
})
// 如果直接调用是没效果的,可能还没挂载吧,等个100ms后已经挂载完成
setTimeout(function() {
that.movieToBottom()
}, 100)
}
},
// 窗口移动到底部(每次发完消息时和初始化时)
movieToBottom() {
// 让页面一直显示最底部
var scroll = mui('.mui-scroll-wrapper').scroll();
scroll.reLayout();
//滚动到底部
scroll.scrollToBottom(100);
}
},
created() {
let that = this
mui.plusReady(function() {
// 获取登录信息
that.user = JSON.parse(plus.storage.getItem('user'))
// 获取接收人信息
that.receiver = plus.webview.currentWebview().receiver
// 渲染最新消息数据
that.messageData = that.receiver.messages != undefined ? that.receiver.messages : []
// 设置scroll
mui('.mui-scroll-wrapper').scroll({
scrollY: true, //是否竖向滚动
scrollX: false, //是否横向滚动
startX: 0, //初始化时滚动至x
startY: 0, //初始化时滚动至y
indicators: true, //是否显示滚动条
deceleration: 0.0005, //阻尼系数,系数越小滑动越灵敏
bounce: true //是否启用回弹
})
//获取 message 页面
var wvs = plus.webview.all()
for (var i = 0, len = wvs.length; i < len; i++) {
// 获取建立socket连接的 message 页面
if (wvs[i].id == 'message') {
// 有socket连接的view
vue.socketView = wvs[i]
break
}
}
})
}
})
页面效果如下:
这个页面的聊天气泡功能稍微有点复杂,可以参考项目中 chat_bubble.html 的页面的简化代码实现。另外,这个页面在发消息时需要调用 message 页面来发,因为只有 message 建立了 websocket 对象。调用其他页面的方法有两种方式,一种是 mui.fire() 方法,另一种是 evalJS() 方法。注意 evalJS() 方式只能传字符串参数,无法直接传对象,可以使用 JSON.stringify() 转为 json 串传参,被调用页面使用JSON.parse() 方法将 json 串转对象。
二维码页面(mine_qrcode.html)
前端代码:
.mui-bar-nav {
background-color: #4DB361;
}
.galiao-qrcode-header {
color: #FFFFFF;
}
.galiao-qrcode {
margin: 30% 0 20% 0;
text-align: center;
}
.qrcode_bg {
width: 340px;
height: 340px;
background-image: url(img/qrcode_bg.png);
background-size: cover;
}
.qrcode {
padding-top: 50%;
}
我的二维码
mui.init()
var vue = new Vue({
el: '#app',
data: {
user: {}
},
created() {
let that = this
mui.plusReady(function() {
that.user = JSON.parse(plus.storage.getItem('user'))
// 设置参数方式
var qrcode = new QRCode('qrcode', {
text: that.user.userId + "",
width: 130,
height: 130,
colorDark: '#000000',
colorLight: '#569362',
correctLevel: QRCode.CorrectLevel.H
});
})
}
})
页面效果:
这个页面主要使用【qrcode.js】对尬聊号生成二维码。
扫一扫页面(sao.html)
前端代码:
.mui-bar-nav {
background-color: #4DB361;
}
.galiao-sao-header {
color: #FFFFFF;
}
.sao-qrcode {
width: 350px;
height: 350px;
margin: 25% auto;
}
扫一扫识别
mui.init()
mui.plusReady(function() {
startRecognize(); //开始扫描
});
// 扫描组件
var scan;
//开启扫描方法
function startRecognize() {
try {
var filter;
//自定义的扫描控件样式
var styles = {
frameColor: "#4DB361",
scanbarColor: "#4DB361",
background: "",
width: '80%',
height: '80%'
}
//扫描控件构造
scan = new plus.barcode.Barcode('saoQrcode', filter, styles);
scan.onmarked = onmarked // 扫描成功
scan.onerror = onerror; // 扫描错误
scan.start();
} catch (e) {
onerror(e)
}
};
// 扫描成功回调 result是返回的结果
function onmarked(type, result, file) {
switch (type) {
case plus.barcode.QR:
type = 'QR';
break;
default:
type = '其它' + type;
break;
}
// 跳转页面
mui.openWindow({
url: 'friend_info.html',
id: 'friend_info',
extras: {
userId: result
}
});
setTimeout(function() {
scan.start()
}, 1000)
};
// 扫描失败回调
function onerror(error) {
mui.alert("扫描失败!")
scan.start()
};
页面效果:
该页面主要使用 plus.barcode.Barcode 开启一个扫一扫的效果,设置回调函数。扫描成功就弹到好友信息页面,这个页面会发 ajax 去查好友信息。
搜索页面(sou.html)
前端代码:
.mui-bar-nav {
background-color: #4DB361;
}
.galiao-sou-header {
color: #FFFFFF;
}
.galiao-sou-search {
margin: 10px 10px 0 10px;
}
搜索
-
用户不存在
注:输入好友的尬聊号搜索添加好友。尬聊号可以在 ‘我的’ -> ‘尬聊号’ 处查看。
mui.init()
// 点击开始搜索
mui(document.body).on('keypress', '#searchValue', function(e) {
this.click();
})
// 点击打开详情
mui(document.body).on('tap', '#btn_open_info', function(e) {
this.click();
})
// vue实例
var vue = new Vue({
el: '#app',
data: {
notFindUser: false,
stranger: {
userId: null,
userAvatar: '',
userNickname: ''
}
},
methods: {
// 搜索
search() {
let that = this
that.notFindUser = false
let searchValue = document.getElementById('searchValue').value
if(searchValue === '') return
axios.get(config.app.baseUrl + '/user/search/' + searchValue, {
params: {
token: JSON.parse(plus.storage.getItem('user')).token
}
})
.then(function(res) {
var data = res.data
if (data.code == 3) {
that.stranger = {
userId: null,
userAvatar: '',
userNickname: ''
}
that.notFindUser = true
return
}
if (data.code != 1) {
mui.alert(data.data)
return
}
that.stranger = data.data
})
.catch(function(error) {
console.log(error)
});
},
// 打开详情页
openInfo() {
mui.openWindow({
url: 'friend_info.html',
id: 'friend_info',
extras: {
userId: this.stranger.userId
}
});
}
},
created() {
mui.plusReady(function() {
});
}
})
页面效果:
这个页面难点在于输入框的回车事件的监听,如果是 vue 的 @keyup.enter.native 就监听不到,需要使用 mui 监听 keypress 事件。输入框 添加 mui-input-speech 的 class ,mui 实现了语音输入。
其他
至于其他页面功能稍微简单一些,可以去项目里面克隆源代码交流。【尬聊前端源码】、【尬聊后端源码】
展望
虽然开发了一些功能,但是还有很多问题需要去发现和解决。比如消息无法保存等 问题。
后边我希望增加有趣的功能,比如过滤 不良消息词汇,用美好的词代替,净化网络暴力。比如 : 你是傻逼 代替为 你是靓仔。让我们的交流更加美好。
当然了,如果你想到了有趣的功能欢迎留言(说不定哪天就去实现了)。要是愿意参与到开源项目的开发当然更好了。走过路过,留个 star 呗。
博客地址:https://www.anlazy.top/index.php/archives/308/ 本人公众号:一只小安仔