自己动手实现聊天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 中的对象或方法时,尽量后引入。比如:

<script src="https://cdn.bootcss.com/vue/2.6.11/vue.min.js"></script>
<script src="https://cdn.bootcss.com/axios/0.19.2/axios.min.js"></script>
<script src="js/mui.min.js"></script>
<script src="js/app.js"></script>

4 尽量使用 v-text 代替 {{ }} 避免插值闪烁。

后端项目

构建

新建maven工程,pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.2.6.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.junan</groupId><artifactId>galiao</artifactId><version>1.0.0-SNAPSHOT</version><name>galiao</name><description>尬聊 v1 服务端</description><properties><java.version>1.8</java.version><fastjson.version>1.2.58</fastjson.version><mybatis-plus.version>3.2.0</mybatis-plus.version><mybatisplus-generation.version>3.2.0</mybatisplus-generation.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.2</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>3.1.0</version></dependency><dependency><groupId>io.netty</groupId><artifactId>netty-all</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>${fastjson.version}</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>${mybatis-plus.version}</version></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-generator</artifactId><version>${mybatisplus-generation.version}</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-freemarker</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>

项目结构

核心

核心功能便是 netty 提供的服务了,在 netty 包下三个类:

GaLiaoServer 表示 Netty 服务的启动类。
GaLiaoServerInitializer 表示 Netty 服务的初始化类。
GaLiaoHandler 表示Netty 服务的处理器类。(这里面包含消息的接受和发送,它就是核心)

至于代码方面大家感兴趣可以去【尬聊开源后端项目】

项目启动页(index.html)

前端代码:

<!DOCTYPE html>
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" /><title>首页</title><link href="css/mui.min.css" rel="stylesheet" /></head><body></body><script src="js/mui.min.js"></script><script type="text/javascript" charset="utf-8">mui.init();mui.plusReady(function() {// 弹到登录页面mui.openWindow({url: 'login.html',id: 'login'});})</script>
</html>

这个页面暂时只是负责打开了登录页面,后期可以做成判断是否登录或自动登录,根据情况跳转到不同页面。

登录(login.html)

架构图

主要还是使用 mvc ,这里暂时不用 netty 。登录成功返回 token,客户端保存 token ,携带 token 访问服务端资源,包含后期使用 netty 发送消息也需要携带 token 。

前端登录页面代码:

<!doctype html>
<html lang="en"><head><meta charset="UTF-8" /><title>登录</title><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" /><link rel="stylesheet" type="text/css" href="css/mui.min.css" /><link rel="stylesheet" type="text/css" href="css/app.css" /><style>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;}</style></head><body><div class="mui-content"><div style="margin-top: 40%; text-align: center; background-color: #FFFFFF;"><h2 class="title">尬聊 v1.0</h2><div style="background-color: #FFFFFF;margin: 30px 0;"><img class="logo" src="img/logo.png" /></div><div class="form"><div><input type="text" class="mui-input-clear galiao-user-id" value="1000" id="galiao_userId" placeholder="尬聊号"></div><div class="mui-input-row"><input type="password" class="mui-input-password galiao-user-password" value="123456" id="galiao_password"placeholder="密码"></div><div><button id="btn_clcik" type="button" class="mui-btn mui-btn-success btn-ok"><img class="btn-img" src="img/ok.png" /></button></div></div></div></div><script type="text/javascript" src="js/config.js"></script><script src="https://cdn.bootcss.com/vue/2.6.11/vue.min.js"></script><script src="https://cdn.bootcss.com/axios/0.19.2/axios.min.js"></script><script src="js/mui.min.js" type="text/javascript" charset="utf-8"></script><script type="text/javascript" src="js/app.js"></script><script type="text/javascript">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').valuelet 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.dataif (data.code != 1) {mui.alert(data.data)return}// 登陆成功// 1 记录 登录信息  (user里面包含token)data.data.token = data.tokenlet 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()}});</script></body>
</html>

登录页面效果:

后端主要代码:

   @Overridepublic 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<User> wrapper = new QueryWrapper<>();wrapper.in("user_id", friendIds);wrapper.select("user_id", "user_avatar", "user_nickname");List<User> users = userMapper.selectList(wrapper);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 页面,这个页面有一个底部导航栏来控制显示其他几个页面。
前端代码如下:

<!doctype html>
<html><head><meta charset="utf-8"><title>主页面</title><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" /><link href="css/mui.css" rel="stylesheet" /><style>.mui-bar-tab .mui-tab-item.mui-active {color: #4DB361; /* 这里放你底部导航栏颜色 */}</style></head><body><!-- 底部导航栏 --><nav class="mui-bar mui-bar-tab"><a class="mui-tab-item mui-active" tabindex="0"><span class="mui-icon mui-icon-chat"></span><span class="mui-tab-label">消息</span></a><a class="mui-tab-item" tabindex="1"><span class="mui-icon mui-icon-list"></span><span class="mui-tab-label">联系人</span></a><a class="mui-tab-item" tabindex="2"><span class="mui-icon mui-icon-paperplane"></span><span class="mui-tab-label">发现</span></a><a class="mui-tab-item" tabindex="3"><span class="mui-icon mui-icon-home"></span><span class="mui-tab-label">我的</span></a></nav><div id="app"></div><script src="js/mui.js"></script><script src="https://cdn.bootcss.com/vue/2.6.11/vue.min.js"></script><script type="text/javascript">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);}}});}</script></body>
</html>

该页面效果如下:

该页面难点在于底部导航栏每个 a 标签控制每个页面显示与隐藏,而官网只提供了基于a 标签 href 的代码块的显示与隐藏,这种方式需要将其他页面以 div 的方式写在 main 页面,会导致 main 页面代码过于冗杂。本次使用的切换 html 的方式核心代码在于 init 方法,通过 a 标签上的 tabindex 的值来显示不同页面。

消息页面(message.html)

前端代码:

<!doctype html>
<html><head><meta charset="utf-8"><title>消息页面</title><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" /><link href="css/mui.min.css" rel="stylesheet" /><style>.mui-bar-nav {background-color: #4DB361;}.galiao-title {color: #FFFFFF;}.galiao-message img {border-radius: 5px;}</style></head><body><!-- 头部 --><header class="mui-bar mui-bar-nav"><h1 id="title" class="mui-title galiao-title">消息</h1></header><div class="mui-content" id="app"><div><ul class="mui-table-view"><li v-for="receiver in receivers" :key="receiver.userId" class="mui-table-view-cell mui-media" id="openChat" @click="openChat(receiver)"><div class="galiao-message"><img class="mui-media-object mui-pull-left" :src="receiver.userAvatar"><div class="mui-media-body"><span v-text="receiver.userNickname"></span><p class="mui-ellipsis" v-text="receiver.messages[receiver.messages.length - 1].data"></p></div></div></li></ul></div></div><script src="https://cdn.bootcss.com/vue/2.6.11/vue.min.js"></script><script src="js/config.js"></script><script src="js/mui.min.js"></script><script type="text/javascript">mui.init()document.galiao = {// 后端接口url: config.app.sockUrl}/*** 尬聊app初始化*/function galiao_init() {// 创建 socketif (window.WebSocket) {// 构建socketwindow.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 = thismui.plusReady(function() {// 初始化galiao_init();that.user = JSON.parse(plus.storage.getItem('user'))})}})</script></body>
</html>

消息页面效果:

该页面构建了一个websocket对象供聊天信息的发送和接受,但该对象只能在本页面使用,其他页面(chat.html)需要调用本页面方法来发送消息。该页面收到消息直接传给chat页面,chat页面进行判断后选择显示或忽略。该页面涉及到消息数据的保存功能请参见代码。

联系人页面(friends.html)

前端代码:

<!doctype html>
<html><head><meta charset="utf-8"><title>联系人页面</title><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" /><link href="css/mui.min.css" rel="stylesheet" /><style type="text/css">.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;}</style></head><body><!-- 头部 --><header class="mui-bar mui-bar-nav"><h1 id="title" class="mui-title galiao-title">联系人</h1><a href="#topPopover" class="mui-icon mui-icon-plus mui-pull-right galiao-title"></a></header><div class="mui-content" id="app"><div><ul class="mui-table-view"><li v-for="friend in user.friends" v-if="friend.userId != user.userId" @click="openFriendInfo(friend)" :key="friend.userId" class="mui-table-view-cell mui-media"id="open_friend_info"><div class="galiao-friend"><img class="mui-media-object mui-pull-left" :src="friend.userAvatar"><div style="padding-top: 10px;" v-text="friend.userNickname"></div></div></li></ul></div><!--右上角弹出菜单--><div id="topPopover" class="mui-popover"><div class="mui-popover-arrow"></div><div class="mui-scroll-wrapper"><div class="mui-scroll"><ul class="mui-table-view"><li class="mui-table-view-cell" id="btn_sao" @click="saoClick"><a href="javascript:;"><img class="mui-media-object mui-pull-left" style="width: 18px; height: 18px;" src="img/sao.png" />扫一扫</a></li><li class="mui-table-view-cell" id="btn_ssou" @click="souClick"><a href="javascript:;"><img class="mui-media-object mui-pull-left" style="width: 18px; height: 18px;" src="img/sou.png" />搜索好友</a></li></ul></div></div></div></div><script src="https://cdn.bootcss.com/vue/2.6.11/vue.min.js"></script><script src="js/mui.min.js"></script><script type="text/javascript">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 = thismui.plusReady(function() {that.init()})}})</script></body>
</html>

页面效果:

联系人页面和消息页面比较相似,可以 copy 一部分。

发现页面 (found.html)

前端代码:

<!doctype html>
<html><head><meta charset="utf-8"><title>发现页面</title><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" /><link href="css/mui.min.css" rel="stylesheet" /><style type="text/css">.mui-bar-nav {background-color: #4DB361 ;}.galiao-title {color: #FFFFFF;}.galiao-li {height: 45px;}.galiao-li img {width: 25px;padding-bottom: 15px;}</style></head><body><!-- 头部 --><header class="mui-bar mui-bar-nav"><h1 id="title" class="mui-title galiao-title">发现</h1></header><div class="mui-content"><div><ul class="mui-table-view"><!-- 毒鸡汤栏目 --><li id="dujitang" class="mui-table-view-cell mui-media galiao-li"><div class="mui-navigate-right"><img class="mui-media-object mui-pull-left" src="img/du.png"><div style="padding-top: 3px;">毒鸡汤(每日一毒)</div></div></li><!-- 代做决定栏目 --><li id="decision" class="mui-table-view-cell mui-media galiao-li"><div class="mui-navigate-right"><img class="mui-media-object mui-pull-left" src="img/decision.png"><div style="padding-top: 3px;">代做决定</div></div></li></ul></div></div><script src="js/mui.min.js"></script><script type="text/javascript">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'});})</script></body>
</html>

这个页面主要负责打开其他页面,目前主要打开 du.html 和 decision.html,这两个页面目前主要还是 js 在玩,没有 ajax 操作。可在【尬聊开源地址】查看。

我的页面(mine.html)

前端代码:

<!doctype html>
<html><head><meta charset="utf-8"><title>我的页面</title><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" /><link href="css/mui.min.css" rel="stylesheet" /><style type="text/css">.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;}</style></head><body><!-- 头部 --><header class="mui-bar mui-bar-nav"><h1 id="title" class="mui-title galiao-title">我的</h1></header><div class="mui-content"><div class="galiao-mine" id="galiao_mine"><div class="galiao-mine-header"><img style="float: left;" :src="user.userAvatar" /><div><p class="galiao-mine-header-title" v-text="user.userNickname"></p><span class="galiao-mine-header-desc">尬聊号:<span v-text="user.userId">></span></span></div></div><ul class="mui-table-view"><li class="mui-table-view-cell" id="btn_replace_avatar"><a class="mui-navigate-right">头像<img class="mui-pull-right" style="width: 35px; height: 35px; margin-right: 20px;" :src="user.userAvatar" /></a></li><li class="mui-table-view-cell" id="btn_replace_nickname"><a class="mui-navigate-right">昵称<p class="mui-pull-right" style=" margin-right: 20px;" v-text="user.userNickname"></p></a></li><li class="mui-table-view-cell"><a class="mui-navigate-right">尬聊号<p class="mui-pull-right" style=" margin-right: 20px;" v-text="user.userId"></p></a></li><li class="mui-table-view-cell" id="btn_show_qrcode" @click="showQrcode"><a class="mui-navigate-right">我的二维码</a></li></ul><ul class="mui-table-view"><li class="mui-table-view-cell" id="btn_show_admire"><a class="mui-navigate-right">赞赏</a></li><li class="mui-table-view-cell" id="btn_show_about"><a class="mui-navigate-right">关于</a></li></ul><button type="button" id="btn_quit_login" class="mui-btn mui-btn-danger galiao-quit-login">退出登录</button></div></div><script src="https://cdn.bootcss.com/vue/2.6.11/vue.min.js"></script><script src="https://cdn.bootcss.com/axios/0.19.2/axios.min.js"></script><script src="js/mui.min.js"></script><script src="js/app.js"></script><script type="text/javascript">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 = thismui.openWindow({url: 'mine_qrcode.html',id: 'mine_qrcode',extras: {userId: that.user.userId}});}},created() {let that = thismui.plusReady(function() {that.user = JSON.parse(plus.storage.getItem('user'))})}})</script></body>
</html>

页面效果:

这个页面目前主要实现了我的二维码展示、关于、退出三个功能,后续可以做其他功能。

好友信息页面(friend_info.html)

<!doctype html>
<html><head><meta charset="utf-8"><title>好友信息页面</title><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" /><link href="css/mui.min.css" rel="stylesheet" /><style type="text/css">.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;}</style></head><body><header class="mui-bar mui-bar-nav"><a class="mui-action-back mui-icon mui-icon-left-nav mui-pull-left galiao-btn-return"></a></header><div class="mui-content" id="app"><div class="galiao-mine" id="galiao_mine"><div class="galiao-mine-header"><img style="float: left;" :src="user.userAvatar" /><div><p class="galiao-mine-header-title" v-text="user.userNickname"></p><span class="galiao-mine-header-desc">尬聊号:<span v-text="user.userId"></span> </span></div></div><!-- 已经是好友显示 --><ul class="mui-table-view" v-if="user.isFriend"><li class="mui-table-view-cell" id="btn_set_remark"><a class="mui-navigate-right">设置备注和标签</a></li></ul><ul class="mui-table-view" v-if="user.isFriend"><li class="mui-table-view-cell" id="btn_send_message" @click="openChat"><a style="text-align: center; color: dimgray;"><span class="mui-icon mui-icon-chatbubble" style="width: 25px; height: 25px;"></span>发消息</a></li></ul><!-- 不是好友显示 --><ul class="mui-table-view" v-if="user.isFriend == false" style="margin-top: 20px;"><li class="mui-table-view-cell" id="btn_add_friend" @click="addFriend"><a style="text-align: center; color: dimgray;"><img src="img/add_friend.png" class="mui-icon mui-icon-chatbubble" style="width: 20px; height: 20px; margin-bottom: -3px;"></img>添加好友</a></li></ul></div></div><script type="text/javascript" src="js/config.js"></script><script src="https://cdn.bootcss.com/vue/2.6.11/vue.min.js"></script><script src="https://cdn.bootcss.com/axios/0.19.2/axios.min.js"></script><script src="js/mui.min.js"></script><script type="text/javascript">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 = thisaxios.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.dataif (data.code != 1) {mui.alert(data.data)return}mui.alert("添加成功!")that.openChat()// 更新本地数据}).catch(function(error) {mui.alert("网络异常!")});}},created() {let that = thismui.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.dataif (data.code != 1) {mui.alert(data.data)return}that.user = data.data}).catch(function(error) {console.log(error)});}})}})</script></body>
</html>

页面效果:

这个页面会根据情况判断,如果不是自己的好友,会显示 添加好友 按钮,如果是自己的好友就显示 发消息 按钮。这一块的判断在服务端,主要逻辑如下:

    @Overridepublic Message userById(Long userId, String token) {// 验证tokenif(checkToken(token)) {QueryWrapper<User> wrapper = new 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)

前端代码:

<!doctype html>
<html lang="en"><head><meta charset="UTF-8" /><title>尬聊聊天页面</title><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" /><link rel="stylesheet" type="text/css" href="css/mui.min.css" /><style>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;}</style></head><body><div id="app"><header class="mui-bar mui-bar-nav"><a class="mui-action-back mui-icon mui-icon-left-nav mui-pull-left galiao-btn-return"></a><h1 class="mui-title galiao-chat-title" v-text="receiver.userNickname"></h1></header><div class="mui-scroll-wrapper" style="margin: 50px 0 70px 0;"><div class="mui-scroll"><!-- 左气泡 --><ul class="mui-table-view" class="galiao-chat-message"><li v-for="(message, index) in messageData" :key="index" class="mui-table-view-cell"><img v-if="message.action != 'out'" class="galiao-avator mui-pull-left" :src="receiver.userAvatar" /><div v-if="message.action != 'out'" class="galiao-message-bubble-left mui-pull-left" v-text="message.data"></div><img v-if="message.action == 'out'" class="galiao-avator mui-pull-right" :src="user.userAvatar" /><div v-if="message.action == 'out'" class="galiao-message-bubble-right mui-pull-right" v-text="message.data"></div></li></ul></div></div><nav class="mui-bar mui-bar-tab galiao-chat-tab"><div><span class="mui-icon mui-icon-pengyouquan" style="width: 10%; padding-right: 3px;"></span><input id="concurrentMessage" type="text" style="width: 60%;border: none;"><span class="mui-icon mui-icon-star" style="width: 10%; padding-left: 7px;"></span><button type="button" id="btn_send_message" @click="sendMessage" class="mui-btn mui-btn-success" style="width: 14%;">发送</button></div></nav></div><script src="https://cdn.bootcss.com/vue/2.6.11/vue.min.js"></script><script src="js/mui.min.js" type="text/javascript" charset="utf-8"></script><script type="text/javascript">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').valueif (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 = thissetTimeout(function() {that.movieToBottom()}, 100)}},// 接收消息receiveMessage(message) {let inMessage = JSON.parse(message)if (inMessage.sender === this.receiver.userId) {let that = thisthat.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 = thismui.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 : []// 设置scrollmui('.mui-scroll-wrapper').scroll({scrollY: true, //是否竖向滚动scrollX: false, //是否横向滚动startX: 0, //初始化时滚动至xstartY: 0, //初始化时滚动至yindicators: 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连接的viewvue.socketView = wvs[i]break}}})}})</script></body>
</html>

页面效果如下:

这个页面的聊天气泡功能稍微有点复杂,可以参考项目中 chat_bubble.html 的页面的简化代码实现。另外,这个页面在发消息时需要调用 message 页面来发,因为只有 message 建立了 websocket 对象。调用其他页面的方法有两种方式,一种是 mui.fire() 方法,另一种是 evalJS() 方法。注意 evalJS() 方式只能传字符串参数,无法直接传对象,可以使用 JSON.stringify() 转为 json 串传参,被调用页面使用JSON.parse() 方法将 json 串转对象

二维码页面(mine_qrcode.html)

前端代码:

<!doctype html>
<html><head><meta charset="utf-8"><title>二维码页面</title><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" /><link href="css/mui.min.css" rel="stylesheet" /><style type="text/css">.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%;}</style></head><body><header class="mui-bar mui-bar-nav"><a class="mui-action-back mui-icon mui-icon-left-nav mui-pull-left galiao-qrcode-header"></a><h1 class="mui-title galiao-qrcode-header">我的二维码</h1></header><div class="mui-content galiao-qrcode" id="app"><div class="mui-card"><!--页眉,放置标题--><div class="mui-card-header"><img :src="user.userAvatar" /> <span v-text="user.userNickname"></span></div><div class="mui-card-content" style="padding: 10px;"><div class="qrcode_bg" id="qrcode_bg"><div class="qrcode" id="qrcode"></div></div></div></div></div><script src="https://cdn.bootcss.com/vue/2.6.11/vue.min.js"></script><script src="js/qrcode.min.js"></script><script src="js/mui.min.js"></script><script type="text/javascript">mui.init()var vue = new Vue({el: '#app',data: {user: {}},created() {let that = thismui.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});})}})</script></body>
</html>

页面效果:

这个页面主要使用【qrcode.js】对尬聊号生成二维码。

扫一扫页面(sao.html)

前端代码:

<!doctype html>
<html><head><meta charset="utf-8"><title>扫一扫页面</title><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" /><link href="css/mui.min.css" rel="stylesheet" /><style type="text/css">.mui-bar-nav {background-color: #4DB361;}.galiao-sao-header {color: #FFFFFF;}.sao-qrcode {width: 350px;height: 350px;margin: 25% auto;}</style></head><body><header class="mui-bar mui-bar-nav"><a class="mui-action-back mui-icon mui-icon-left-nav mui-pull-left galiao-sao-header"></a><h1 class="mui-title galiao-sao-header">扫一扫识别</h1></header><div class="mui-content"><div id="saoQrcode" class="sao-qrcode"><!--盛放扫描控件的div--></div></div><script src="js/mui.min.js"></script><script type="text/javascript">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()};</script></body>
</html>

页面效果:

该页面主要使用 plus.barcode.Barcode 开启一个扫一扫的效果,设置回调函数。扫描成功就弹到好友信息页面,这个页面会发 ajax 去查好友信息。

搜索页面(sou.html)

前端代码:

<!doctype html>
<html><head><meta charset="utf-8"><title>搜索页面</title><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" /><link href="css/mui.min.css" rel="stylesheet" /><style type="text/css">.mui-bar-nav {background-color: #4DB361;}.galiao-sou-header {color: #FFFFFF;}.galiao-sou-search {margin: 10px 10px 0 10px;}</style></head><body><header class="mui-bar mui-bar-nav"><a class="mui-action-back mui-icon mui-icon-left-nav mui-pull-left galiao-sou-header"></a><h1 class="mui-title galiao-sou-header">搜索</h1></header><div class="mui-content" id="app"><div class="mui-input-row mui-search galiao-sou-search"><input type="search" id="searchValue" class="mui-input-clear mui-input-speech" placeholder="请输入尬聊号" @click="search"></div><ul v-show="stranger.userId != null" class="mui-table-view"><li class="mui-table-view-cell" id="btn_open_info" style="height: 60px; text-align: center;" @click="openInfo"><img class="mui-media-object mui-pull-left" :src="stranger.userAvatar" style="border-radius: 5px;"><div class="mui-media-body mui-pull-left" style="padding-top: 10px;" v-text="stranger.userNickname"></div></li></ul><ul v-show="notFindUser" class="mui-table-view"><li class="mui-table-view-cell" style="height: 60px; text-align: center; padding-top: 20px;">用户不存在</li></ul><p class="galiao-sou-search">注:输入好友的尬聊号搜索添加好友。尬聊号可以在 ‘我的’ &nbsp; -> &nbsp;‘尬聊号’ 处查看。</p></div><script type="text/javascript" src="js/config.js"></script><script src="https://cdn.bootcss.com/vue/2.6.11/vue.min.js"></script><script src="https://cdn.bootcss.com/axios/0.19.2/axios.min.js"></script><script src="js/mui.min.js"></script><script type="text/javascript">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 = thisthat.notFindUser = falselet searchValue = document.getElementById('searchValue').valueif(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.dataif (data.code == 3) {that.stranger = {userId: null,userAvatar: '',userNickname: ''}that.notFindUser = truereturn}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() {});}})</script></body>
</html>

页面效果:

这个页面难点在于输入框的回车事件的监听,如果是 vue 的 @keyup.enter.native 就监听不到,需要使用 mui 监听 keypress 事件。输入框 添加 mui-input-speech 的 class ,mui 实现了语音输入。

其他

至于其他页面功能稍微简单一些,可以去项目里面克隆源代码交流。【尬聊前端源码】、【尬聊后端源码】

展望

虽然开发了一些功能,但是还有很多问题需要去发现和解决。比如消息无法保存等 问题。

后边我希望增加有趣的功能,比如过滤 不良消息词汇,用美好的词代替,净化网络暴力。比如 : 你是傻逼 代替为 你是靓仔。让我们的交流更加美好。

当然了,如果你想到了有趣的功能欢迎留言(说不定哪天就去实现了)。要是愿意参与到开源项目的开发当然更好了。走过路过,留个 star 呗。

博客地址:https://www.anlazy.top/index.php/archives/308/
本人公众号:一只小安仔

自己动手搭建聊天APP相关推荐

  1. 实践:动手搭建聊天机器人

    什么是聊天机器人? 聊天机器人是一种人工智能系统,可以用文字或者语音和人类交流互动.简单的如询问现在的天气怎么样.最新的新闻是什么,复杂一点的如手机出问题了询问一下要如何解决等等. 不过聊天机器人现 ...

  2. 仿微信H5聊天系统即时通讯社交完整优化版搭建,IM聊天APP聊天交友客服,带安卓苹果端APP源码+视频教程

    H5聊天系统即时通讯社交完整优化版,IM聊天APP聊天交友客服仿微信 带安卓苹果端APP源码+教程

  3. 在线聊天App研发说明(android学习总结)

    初识android 确定学习android之后,开始动手,跟之前做过android的同事,要了开发环境和一本电子书<深入浅出Android--Google手持设备应用程序设计>.书很薄只有 ...

  4. 动手搭建自己的本地测试服务器

    动手搭建自测服务器,加快本地开发进度,最近自己在开发过程中有这么一个需求,在开发本地应用模块过程中,需要请求服务器测试服务器,但是服务器老出问题,于是就有自己本地搭建一个服务器的一个想法,然后配置接口 ...

  5. 从零搭建uniapp app,适合小白,傻瓜化,页面搭积木可视化创建

    分享一: 从零搭建uniapp app 分享二: 一个 uniapp uview ui 可视化,完全自由拖拽,一键生成flex代码网站: http://aicode.shagua.wiki/uni/i ...

  6. native聊天界面 react_ReactNative 仿微信聊天 App 实例分享|RN 仿朋友圈

    今天给大家分享的是 RN 聊天室项目,基于 react-native+react-navigation+react-redux+react-native-image-picker+rnPop 等技术实 ...

  7. native聊天界面 react_ReactNative 聊天 App 实战|RN 仿微信界面群聊|朋友圈

    前言 这次要给大家分享的是基于ReactNative开发的聊天APP实战项目RN_ChatRomm,运用react-native+react-navigation+react-redux+react- ...

  8. 用sockets打造自己的Android聊天app(安卓篇)

    用sockets打造自己的Android聊天app(安卓篇) 翻译自http://www.androidhive.info/2014/10/android-building-group-chat-ap ...

  9. native聊天界面 react_ReactNative仿微信聊天APP实战项目|RN输入框表情

    本帖最后由 xiaoyan2015 于 2019-9-4 13:35 编辑 今天给大家分享的是 RN 聊天室项目,基于 react-native+react-navigation+react-redu ...

  10. 【免费福利】零AI基础,如何搭建聊天机器人:技术架构剖析

    作为人工智能领域最为重要的技术,自然语言处理的应用在工业界无处不在.从网页公开数据的分析和抽取.情感分析.机器翻译.智能客服.问答系统到聊天机器人,它的重要性不言而喻. 今天我们来探讨一下自然语言处理 ...

最新文章

  1. 赠书 | 发自暗处的光:你不知道的暗数据
  2. 单片机18b20c语言程序,AVR单片机控制DS18B20的示例C程序
  3. C#调用ORACLE存储过程返回结果集及函数
  4. RocketMQ集成SpringBoot
  5. python 环境常用指令(updating...)
  6. 《趣学JavaScript——教孩子学编程》——1.5 本章小结
  7. 贝叶斯网络在疾病预测诊断中的应用与优化
  8. Cadence orcad cis数据库搭建及access元器件数据库下载
  9. 智能语音翻译APP——腾讯翻译君
  10. LabVIEW编程LabVIEW开发 Kepco ABC程控电源 例程与相关资料
  11. python导入openpyxl_python系列之(6)Python使用Openpyxl操作Excel
  12. 内存泄漏的原因及解决方法
  13. scylladb集群管理
  14. 【MySQL】浅谈MySQL中索引的基本操作以及背后的数据结构
  15. 苹果开发者:如何将准备好的应用上传到iTunes Connect
  16. IMX8开发板Ubuntu20系统下外设界面功能测试-迅为i.MX8MM
  17. 如何旋转树莓派的显示屏幕
  18. Vue中引入字体并解决字体文件过大问题
  19. 基于C#+WinForm+SQL Server2016+Visual Studion2019的仓库管理系统
  20. linux查看自动周期性任务,Linux自学笔记——Linux周期性计划任务

热门文章

  1. 读《许三观卖血记》----余华
  2. poj 2567 code thr tree
  3. 【npm】伙计,给我来一杯package.json!不加糖
  4. TOEFL wordlist 16
  5. ECCV 2020 论文大盘点-图像增强与图像恢复篇
  6. 【Java】Maven使用笔记
  7. 《从零开始的RPG游戏制作教程》第一期:制作基础场景
  8. 最新的中国风歌曲——《颜如玉》
  9. python 嵌入式webserver 服务器 状态监控
  10. android 以太网 热插拔,android_8.1 hdmi设备热插拔事件