热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

【uniapp】小程序实现微信在线聊天(私聊/群聊)

之前学习使用uni-app简单实现一个在线聊天的功能,今天记录一下项目核心功能的实现过程。页面UI以及功能逻辑全部来源于微信,即时聊天业务的实现使用so

之前学习使用uni-app简单实现一个在线聊天的功能,今天记录一下项目核心功能的实现过程。页面UI以及功能逻辑全部来源于微信,即时聊天业务的实现使用socket.io,前端使用uni-app开发,后端服务器基于node实现,数据库选择mongoDB。

首先在系统中注册两个用户,将对方添加为好友后,开始正常聊天,先简单看一下聊天功能的效果图,分为私聊和群聊两大部分

一对一聊天效果:

在好友列表中添加群成员创建群后即可群聊,群聊效果:

目录

聊天信息列表的渲染

聊天信息发送的相关问题

实现一对一聊天

关于websocket

建立连接

存储连接的用户

发送聊天信息 

首页新消息提示

实现群聊

加入房间

发送群消息




聊天信息列表的渲染

聊天信息列表区域是一个滚动区,这里使用scroll-view组件,其中对于聊天信息展示,主要分为自己的消息和好友的消息,自己的消息位于右侧,好友的消息位于左侧,所以静态页面阶段要实现是左侧消息和右侧消息的页面布局,以及这些消息类型为文字,图片,语音,位置信息时的布局。

后端接口返回的聊天信息是按照时间顺序排列的,渲染聊天信息时使用v-for遍历接口返回的消息列表的内容即可,需要注意的是,还需要使用条件渲染v-if根据每一条消息的发送者id和当前用户的id判断消息的发送方和接受方,渲染在左右指定的区域,当前用户的id从本地存储localStorage中获取;还有就是使用条件渲染判断消息的类型,是文字,图片,语音或定位,合理展示。


{{handleTime(item.time)}}........................ ........................


聊天信息发送的相关问题

点击发送按钮,正式将信息发送给服务器之前,还有几个问题需要解决,这里面有许多坑,在实现的时候走了不少弯路。

1.scroll-view如何始终定位在最底部?

如下图,当发送了一条聊天信息时,聊天信息列表就会增加这条消息,之所以能够看到这条消息,那是因为scroll-view的滚动条在消息添加时将位置定位到了最底部,这是需要进行一些处理的,默认效果是这样的

是不是很变扭?这样的用户体验很差,滚动条不会自动定位到底部,这里需要给scroll-view组件添加一个scroll-into-view属性,按照官方文档的说法它的值应为某子元素id。设置哪个方向可滚动,则在哪个方向滚动到该元素,也就是说可以动态的修改这个属性的值,从而让scroll-view组件的滚动到想要滚动的页面元素位置。

这里就给每一个scroll-view的子元素(聊天记录item)添加id属性,属性值为 msg + 每条聊天记录的id

......

在发送消息的方法中修改scroll-into-view的值scrollToView,让其为最新一条聊天记录即msg.length - 1的id值,必须使用在$nextTick回调中,这是为了在新的聊天记录渲染完毕后再去定位。

this.$nextTick(function(){this.scrollToView = 'msg' + this.msg[this.msg.length - 1].id;
});

 这样才能实现最终的效果

 

2.如何动态修改scroll-view的高度

如下图,点击 + 按钮发送位置信息时会弹出底部菜单栏,但此时scroll-view内的聊天内容会被覆盖,用户想要看最后一条记录还需操作滚动条,这也是不好的用户体验。

需要做到的是弹出底部菜单栏的同时减小聊天内容区域scroll-view组件的高度,让用户能够完整的看到最后的聊天记录。

需要获取底部菜单栏弹出的高度,随后让scroll-view组件减少这部分高度即可。在uni-app中无法操作dom,获取元素的尺寸使用createSelectorQuery获取页面节点,再用 boundingClientRect查询节点的尺寸。官方文档:uni.createSelectorQuery() | uni-app官网

使用如下代码获取页面节点的尺寸,可能无法及时获取到(得到的可能是undefined),这里需要用定时器包裹,才能拿到菜单栏的高度

{{item.text}}
......// 获取指定选择器元素的高度
getHeight(classNa){setTimeout(() => {const query = uni.createSelectorQuery().in(this);query.select(classNa).boundingClientRect(data => {this.$emit('heightChange',data.height);}).exec();},10);
},
// 切换菜单栏显示隐藏
changeMode(){ if(this.showMore){this.showMore = !this.showMore;this.getHeight('.more-view');}
},

拿到底部菜单栏的高度后,使用calc计算并修改行内样式,并修改scroll-view的元素内的子元素定位,这里修改scrollToView的值,一定要置空后再修改,否则会修改无效。

>...... // 弹出菜单栏修改scroll-view高度
handleHeightChange(height){this.scrollView%E5%AE%9E%E7%8E%B0%E4%B8%80%E5%AF%B9%E4%B8%80%E8%81%8A%E5%A4%A9">实现一对一聊天

关于websocket

项目中使用的socket.io底层使用到的是websocket协议,可以实现服务器主动推送消息给客户端,一般应用于实时通信,在线支付等场景,虽然socket.io对其进行了封装,但对其原理的了解还是有必要的。

在websock出现之前,一般使用ajax轮询(设置定时器在相同时间间隔内反复发送请求到服务器拿到服务器最新的数据),长轮询(在指定时间内不让当前请求断开),流化技术等手段进行即时通信,这三者都基于http协议实现,但都非常占用服务器的资源, 显著增加了延时。

websocket协议解决这些缺点,它是一种全双工、双向、单套接字的连接,建立在TCP协议之上,当websocket连接建立后,服务器和客户端可以双向通信,具有以下特点:

1)建立在TCP协议之上,服务端的实现比较容易;

2)于HTTP协议有着良好的兼容性,默认的端口也是80和443,并且握手阶段采用HTTP协议;

3)数据格式轻量,性能开销小,通信高效;

4)可以发送文本,也可以发送二进制数据;

5)没有同源限制



http请求响应图解:

客户端发送请求,服务器响应,至此一次请求响应结束,再次获取服务端最新数据,需要再次重复上述过程;

websocket图解:

黄色部分是握手阶段,客户端给服务端发送请求,该请求基于http协议,服务器返回101状态码,代表成功建立连接,随后客户端和服务器可以开始全双工数据交互,且服务器可以主动推送消息给浏览器,下面是websocket的请求报文:

1.使用websocket请求行的路径是以ws开头,代表使用的是websocket协议

2.请求头Connection:Upgrade代表当前服务器这是一个升级的链接

3.请求头Upgrade:websocket代表需要将当前的链接升级为websocket链接

4.请求头Sec-WebSocket-Key: JnoOq+qL9WP3um80g1Sz3A==是客户端使用base64编码的24位随机字符序列,用户服务器标识当链接的客户端,同时要求服务器响应一个同样加密的Sec-WebSocket-Accept头作为应答;



websocket响应报文如下:

1.服务器响应101状态码代表websocket链接建立成功

2.响应头Sec-WebSocket-Accept: Eu6A8ipjouG1LVFt6xFMSrPFk1E=是对客户端请求头Sec-WebSocket-Key的应答,用于给客户端标识当前的服务器



客户端websocket实现

websocket是HTML5的新特性之一,首先你的浏览器必须支持websocket

1.创建WebSocket实例

const ws = new WebSocket('ws:localhost:8000');

参数url:ws://ip地址:端口号/资源名

2.WebSocket对象包含以下事件


open:连接建立时触发

message:客户端接收服务端数据时触发

error:通信发生错误时触发

close:连接关闭时触发


3.WebSocket对象常用方法


send():使用连接给服务端发送数据


客户端websocket代码模板:

;((doc,WebSocket) => {const msg = doc.querySelector('#msg'); // 获取输入框,需要发送的消息const send = doc.querySelector('#send'); // 发送按钮// 创建websocket实例const ws = new WebSocket('ws:localhost:8000');// 初始化const init = () => {bindEvent();}// 绑定事件function bindEvent () {send.addEventListener('click',handleSendBtnClick,false);ws.addEventListener('open',handleOpen,false);ws.addEventListener('close',handleClose,false);ws.addEventListener('error',handleError,false);ws.addEventListener('message',handleMessage,false);}function handleSendBtnClick () {const message = msg.value;// 将数据发送给服务器ws.send(JSON.stringify({message:message}));msg.value = '';}function handleOpen () {console.log('open');// 当连接建立时,一般做一些页面初始化操作}function handleClose () {console.log('close');// 当连接关闭时}function handleError () {console.log('error');// 当连接出现异常时}function handleMessage (e) {// 在这里获取后端广播的数据,数据通过事件对象e活得,数据存放在e.data中const showMsg = JSON.parse(e.data);}init();
})(document,WebSocket)

由此可见,使用原生websocket完全可以进行聊天通信,但是它提供的事件和api有限,对于一些复杂的需求实现起来比较困难,socket.io是一个websocket库,它对于websocket进行了很好的封装,提供了许多api,以及自定义事件,使用起来比较灵活。



聊天功能的前后端交互顺序图

需要实现的是客户端A发送消息给客户端B,客户端B能够自动接收并显示,实现私聊的关键是要确定需要将消息发送给谁,所以在进入聊天界面的的时候,每一个连接服务器的客户端就需要将自己的id告诉服务器,服务器会维护一个对象专门用于存放当前已连接的用户id

客户端A进入聊天界面的的时候,还需要存放客户端B的用户id,在发送消息的时候将客户端B的id传递给服务器,让服务器知道当前的这条消息要发送给谁,服务器收到后就会查询存放用户id的对象,如果客户端B连接那么就将A的消息发送给它,这就是私聊的大致思路。


建立连接

能够实现客户端之间的通信首先需要将客户端与服务器建立连接,首先下载依赖,客户端使用weapp.socket.io,服务端使用socket.io

npm i socket.io@2.3.0 --save
npm i express@4.17.1 --save
npm i weapp.socket.io@2.1.0 --save

为了保证能连接正常,建议下载指定版本,前后端版本不匹配会导致连接失败报错。

官方文档英文:Socket.IO

W3Cschool中文文档:socket.io官方文档_w3cschool

客户端:

客户端下载完毕后,可以将weapp.socket.io.js文件单独拿出,其存放的文件位置如下图

将其放在项目指定文件夹下引入,这里放在socket文件下;随后在项目的main.js中引入使用,这里将io挂载在Vue的原型上,供全局使用,连接地址为服务器的地址,端口号需与服务器socket.io监听的端口保持一致;

import io from './socket/weapp.socket.io.js'
Vue.prototype.socket = io('http://localhost:8000');

服务器:

服务器使用node的express框架搭建,在入口js中配置如下,io.on用于绑定事件,connection事件在连接时触发,它是socket.io内置事件之一。

const express = require('express');
const app = express();
let server = app.listen(8000);
let io = require('socket.io').listen(server);io.on('connection',(socket) => { console.log("socket.io连接成功");
});

socket.io建立连接会产生跨域问题,这里直接通过express的方式使用CORS解决跨域:

app.all('*', function(req, res, next) {res.header("Access-Control-Allow-Origin", "*");res.header("Access-Control-Allow-Headers", "Content-Type,Content-Length, Authorization, Accept,X-Requested-With");res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");res.header("X-Powered-By",' 3.2.1')if(req.method=="OPTIONS") res.send(200);/*让options请求快速返回*/else next();
});

当然socket.io也提供了跨域的解决方案,具体可见 Handling CORS | Socket.IO

完成以上配置后,启动项目,客户端便可使用socket.io与服务器正常连接。

观察浏览器network选项卡,请求类型为websocket,响应状态码101,可见socket.io的连接底层走的就是websocket协议


存储连接的用户

用户登陆成功跳转到index主页,每一位用户在注册时都会在数据库生成一个唯一的用户id,这里需要将每一个连接成功的用户id发送给服务器

       =>     

socket.io服务端除了connection(socket连接成功之后触发),message(客户端通过socket.send来传送消息时触发此事件),disconneting(socket失去连接时触发,包括关闭浏览器,主动断开,掉线等任何断开连接的情况) 等内置的默认事件外,还可以使用自定义事件,客户端也类似。

API上,使用emit()触发事件,使用on()绑定事件,进入首页后在客户端onLoad中触发自定义事件login,同时从本地存储中取出用户uid,上传服务器

export default {data() {return {uid:'', // 当前用户id},onLoad() {this.getStroage();this.addUserToSocket(this.uid);},methods:{// 获取本地存储getStroage(){const value = uni.getStorageSync('user');if(value){this.uid = value.id;} else {uni.navigateTo({url:'/pages/login/login'})}},// 添加连接的用户addUserToSocket(uid){this.socket.emit('login',uid);},}
}

在服务端绑定login事件,同时创建对象connectedUsers存放连接的用户, 将用户uid作为key保存,value是socket.id,socket.id是connection回调参数的一个属性,socket.id用于socket.io唯一标识连接的用户

当用户退出应用时触发disconnecting事件,将此用户信息从connectedUsers对象中删除。

let cOnnectedUsers= {};
io.on('connection',(socket) => { console.log("socket.io连接成功");// console.log(socket);// 用户进入主页时获取用户id保存socket.on('login',(id) => {console.log("socket.id:" + socket.id);socket.name = id;connectedUsers [id] = socket.id;});// 用户离开socket.on('disconnecting',() => {console.log('leave:' + socket.id);if(users.hasOwnProperty(socket.name)){delete connectedUsers [socket.name];}});
});

总结:


1)io.on可用来给当前socket连接绑定connection事件,参数socket可以获取这次连接的配置信息,最常用的就是socket.id,它是本次连接的唯一标识

io.on('connection',function(socket){ ...... })

2)on用于绑定事件,用于接收传递的数据

socket.on('自定义事件名',function(参数1,参数2,......,参数n) { ...... });

3)emit用于触发事件,用于传递数据

socket.emit('自定义事件名',参数1,参数2,......,参数n);

4)disconnecting在失去连接时时触发,断开可能是关闭浏览器,主动断开,掉线等导致

socket.on('disconnecting',() => {})



发送聊天信息 

客户端发送消息,将聊天内容加工处理后,触发自定义事件msg,将内容,发送者id和接收者id发送给服务器,代码如下:

客户端chatroom.vue:

// 发送聊天数据
sendSocket(msg){if(this.type === '0'){// 1对1聊天this.socket.emit('msg',msg,this.uid,this.fid);} else {// 群消息this.socket.emit('gmsg',msg,this.uid,this.fid);}
},

服务器绑定msg事件,得到客户端发来数据,首先需要操作数据库完成插入最新的聊天内容,更改最后的通讯时间等操作,如果对方用户在线,则connectedUsers 对象中必然存在该用户的id,使用socket.to(指定接收者的socket.io)将消息发送给指定的用户,同时触发自定义事件backMsg,用法如下:


发送给指定 socketid 的客户端(私密消息)

socket.to().emit('自定义事件名', 参数);


注意:如果不使用socket.to方法直接调用emit,则会发送给所有在线的用户。 

服务器代码:

// 引入数据库文件
let dataBase= require("./dataBase");
// 1对1消息发送
socket.on('msg',(msg,fromId,toId) => {console.log('服务器收到用户' + fromId + '发送给' + toId + '的消息')console.log('发送的消息是:',msg);// 修改好友最后通讯时间dataBase.updateLastMsgTime(fromId,toId);dataBase.updateLastMsgTime(toId,fromId);// 添加消息dataBase.insertMsg(fromId,toId,msg.message,msg.types);console.log('数据库插入成功');// 将获取的消息发送给好友,users[toId]就是好友的socket.idif(connectedUsers[toId]){console.log('将消息发送给',toId,'成功');socket.to(connectedUsers[toId]).emit('backMsg',msg,fromId,0);}
});

这样客户端绑定backMsg事件,就能拿到发送消息了!处理消息展示即可,但需要判断当前用户此时打开的聊天界面是否就是当前发送者聊天对话框即if(fromId === this.fid && type === 0),否则会造成聊天内容的错误展示,比如当前用户可能存在多个好友,客户端A给客户端B发消息时B打开的是和C的聊天对话框,此时就会在C的对话框中错误的收到A发来的消息

客户端chatroom.vue:

this.socket.on('backMsg',(msg,fromId,type) => {// 如果是1对1消息fromId是当前聊天窗口的好友id时执行if(fromId === this.fid && type === 0){......// 一条聊天记录let newMsg = {fromId:fromId,id:msg.id,imgUrl:msg.imgUrl,message:msg.message,types:msg.types, // 0 - 文字信息,1 - 图片信息, 2 - 音频time:new Date(),isFirstPlay:true,};this.msg.push(newMsg); // 如果消息是图片if(msg.types === '1') {this.msgImage.push(msg.message)}this.$nextTick(function(){this.scrollToView = 'msg' + this.msg[this.msg.length - 1].id;});......}
});

测试效果如下: 

 

服务器终端输出结果如下:


首页新消息提示

如下图,用户有新消息会在首页及时显示,并提示未读消息数量

 

需要给首页绑定获取消息的自定义事件backMsg,绑定时机是在生命周期onLoad中,事件一旦触发代表有好友向你发送消息了,会获取服务器传来的消息,在事件回调中要完成两个操作,首先查找发来新消息的好友在首页好友列表数组的索引下标,随后修改指定的数组元素内容,更新这个好友最后消息的时间、最后消息的内容、未读消息数;并将该元素现有位置删除,添加到整个数组的头部,即把这个好友item放到首页列表的最上方,首页index.vue相关代码如下:

{{item.unreadMsg}}{{item.nickName}}{{getTime(item.lastTime)}}{{item.lastMsgUsername ? item.lastMsgUsername : ''}}{{item.lastMsg}}{{item.lastMsgUsername ? item.lastMsgUsername : ''}}[图片]{{item.lastMsgUsername ? item.lastMsgUsername : ''}}[语音]{{item.lastMsgUsername ? item.lastMsgUsername : ''}}[位置]
......onLoad() {this.receiveSocket('backMsg');
}
methods:{// 接收个人/群聊天信息receiveSocket(eventName){this.socket.on(eventName,(msg,fromId,type) => {if(type === 0){let index;if(eventName == 'backMsg') {// 获取有新消息的好友在整个好友数组中的索引下标index = this.friends.findIndex((item) => {return item.id === fromId});}// 修改未读消息数 this.getUnreadMsg(this.friends[index]);// 修改最后聊天时间this.friends[index].lastTime = msg.time;// 修改最后聊天信息this.friends[index].lastMsg = msg.message;// 修改最后聊天信息的类型this.friends[index].lastMsgType = msg.types;// 删除当前item,将其插入到数组的首部,即展示在列表最上方const tempItem = this.friends[index];this.friends.splice(index,1);this.friends.unshift(tempItem);}});},
}

此外还有一个问题就是何时清空未读消息数,清空的操作需要进行两次,一次是用户进入聊天页面时进行清空,在聊天页生命周期onLoad中调用清空消息数的后端接口,清空现有的未读消息;另一次是在点击返回按钮如下图,返回首页时清空,在此按钮事件的回调中调用清空未读消息数的接口,这是为了清空用户和他人聊天时已读的消息,两次操作缺一不可。


实现群聊

群聊的前后端顺序图如下所示:

需要实现的是客户端A在群内发送了消息后,在同一群内的客户端BCD都能同时收到A发送的消息。群聊的大致思路和私聊基本相似,不同点在于群聊中引入了房间的概念,在房间内的成员就是这个群聊的群成员,任何群成员的群内发言就会在这个房间内进行广播所有在线的群成员都能及时够收到。


加入房间

使用socket.join()加入房间,具体使用如下:


socket.join('room',function(){ ...... });

room:房间id,是一个字符串,用户自定义,加入房间会触发参数二回调

socket.leave(room,function(){ ...... })

与join相对应的是leave方法,即退出指定的房间,参数二异常回调函数为可选值。需要注意的是,当与客户端断开连接时,会自动将其从加入的房间中移除


在这个项目里房间id使用的是每一个群聊的群id号,它可以唯一标识一个群聊;

加入房间的操作同样是在用户登录成功进入首页时进行,一个用户可能加入了多个群聊,那么在主页请求用户群聊接口后,需要依次遍历接口返回的群聊列表,为每一个群聊触发addGroup事件,将当前的群id发送给后端,让当前用户加入每个群聊的房间。

index.vue

// 获取当前用户的群消息
getGroup(){uni.request({url:`${this.baseUrl}/index/getGroupList`,method:'POST',data:{uid:this.uid, // 用户id},success: (res) => {let data = res.data.result;// 遍历当前用户的群列表for (var i = 0; i },

 服务器绑定addGroup事件,调用socket.join,让当前用户连接加入房间号为groupId的房间

io.on('connection',(socket) => { // 加入群socket.on('addGroup',(groupId) => {console.log('用户',socket.id,'加入了groupId为',groupId,'的群聊');socket.join(groupId);});
}

效果:例如当前这个用户加入了三个群聊,首页加载后就会触发addGroup三次,依次加入这三个群id标识的房间。

服务器终端输出效果如下:


发送群消息

某一群成员在群内发送消息,会和私聊同样的方式将语音和图片这些静态资源上传服务器,返回服务器存放地址后进行封装,触发gmsg事件将处理后的消息提交服务器

// 发送聊天数据
sendSocket(msg){if(this.type === '0'){// 1对1聊天this.socket.emit('msg',msg,this.uid,this.fid);} else {// 群消息this.socket.emit('gmsg',msg,this.uid,this.fid);}
},

群内广播消息使用到的api是socket.to,具体使用如下:


将内容发送给同在房间名roomName的所有客户端,除了发送者

socket.to(roomName).emit('事件名',参数1,参数2,......参数n);

如果需要包含发送者可以使用

io.in(roomName).emit('事件名',参数1,参数2,......参数n);

也可以同时发送给在多间房间的客户端,使用to链式调用的形式,不包含发送者

socket.to(roomName1).to(roomName2).emit('事件名',参数1,参数2,......参数n);

当然,当前项目中只需要使用第一种方式即可


服务器的gmsg事件回调中,同样需要将获取到的消息插入数据库,同时修改群最后通信时间以及全体成员的未读消息数,最后调用 socket.to方法,触发groupMsg事件,将消息发送给群聊内的其它在线用户。

// 引入数据库文件
let dataBase = require("./dataBase");
// 接收群消息
socket.on('gmsg',(msg,fromId,groupId) => {console.log('服务器接收到来自群',groupId,'的用户',fromId,'的消息',msg);// 修改群的最后通信时间dataBase.updateGroupLastTime(groupId);// 添加群消息dataBase.insertGroupMsg(fromId,groupId,msg.message,msg.types);//将所有成员的未读消息数加一dataBase.changeGroupUnreadMsgNum(groupId);console.log('消息',msg.message,'插入数据库成功')// 获取当前用户的名字和头像dataBase.userDetails(fromId).then((data) => {console.log('查询发送者用户名成功,用户名是:',data[0]);console.log('正在将信息',msg.message,'发送至群',groupId,'内');// 群内广播消息socket.to(groupId).emit('groupMsg',msg,fromId,0,data[0].name,groupId);});
});

客户端在线群成员收到消息,执行groupMsg事件回调中的方法,内部大致逻辑和私聊完全一致,可以将其封装成公共方法使用,需要注意的依旧是要做群id一致性判断,防止获取的消息显示在其它聊天窗口中,即 if(fromId !== this.uid && groupId === this.fid)。

this.socket.on('groupMsg',(msg,fromId,type,friendName,groupId) => {// 判断当前打开的群id和接收消息的群id是否一致,防止消息错误显示if(fromId !== this.uid && groupId === this.fid){......// 模拟服务器数据let newMsg = {fromId:fromId,id:msg.id,imgUrl:msg.imgUrl,message:msg.message,types:msg.types, // 0 - 文字信息,1 - 图片信息, 2 - 音频time:new Date(),isFirstPlay:true,friendName:friendName // 群需显示发送消息用户的名字};this.msg.push(newMsg);// 如果消息是图片if(msg.types === '1') {this.msgImage.push(msg.message)}this.$nextTick(function(){this.scrollToView = 'msg' + this.msg[this.msg.length - 1].id;});......}
});

效果演示:输入一段文字发送到群内

服务器此时终端输出如下

以上就是项目聊天功能难点的全部内容,前端实现实时聊天主要就是对于socket.io提供api的合理使用,剩余的难点就是页面显示的部分逻辑处理,用户体验的优化,还可以在此基础上添加更多的功能,若有不足之处恳请指正!


推荐阅读
  • 本文详细介绍了SQL日志收缩的方法,包括截断日志和删除不需要的旧日志记录。通过备份日志和使用DBCC SHRINKFILE命令可以实现日志的收缩。同时,还介绍了截断日志的原理和注意事项,包括不能截断事务日志的活动部分和MinLSN的确定方法。通过本文的方法,可以有效减小逻辑日志的大小,提高数据库的性能。 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • 如何使用Java获取服务器硬件信息和磁盘负载率
    本文介绍了使用Java编程语言获取服务器硬件信息和磁盘负载率的方法。首先在远程服务器上搭建一个支持服务端语言的HTTP服务,并获取服务器的磁盘信息,并将结果输出。然后在本地使用JS编写一个AJAX脚本,远程请求服务端的程序,得到结果并展示给用户。其中还介绍了如何提取硬盘序列号的方法。 ... [详细]
  • [译]技术公司十年经验的职场生涯回顾
    本文是一位在技术公司工作十年的职场人士对自己职业生涯的总结回顾。她的职业规划与众不同,令人深思又有趣。其中涉及到的内容有机器学习、创新创业以及引用了女性主义者在TED演讲中的部分讲义。文章表达了对职业生涯的愿望和希望,认为人类有能力不断改善自己。 ... [详细]
  • 本文介绍了RPC框架Thrift的安装环境变量配置与第一个实例,讲解了RPC的概念以及如何解决跨语言、c++客户端、web服务端、远程调用等需求。Thrift开发方便上手快,性能和稳定性也不错,适合初学者学习和使用。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • Python瓦片图下载、合并、绘图、标记的代码示例
    本文提供了Python瓦片图下载、合并、绘图、标记的代码示例,包括下载代码、多线程下载、图像处理等功能。通过参考geoserver,使用PIL、cv2、numpy、gdal、osr等库实现了瓦片图的下载、合并、绘图和标记功能。代码示例详细介绍了各个功能的实现方法,供读者参考使用。 ... [详细]
  • 本文介绍了Android 7的学习笔记总结,包括最新的移动架构视频、大厂安卓面试真题和项目实战源码讲义。同时还分享了开源的完整内容,并提醒读者在使用FileProvider适配时要注意不同模块的AndroidManfiest.xml中配置的xml文件名必须不同,否则会出现问题。 ... [详细]
  • Oracle seg,V$TEMPSEG_USAGE与Oracle排序的关系及使用方法
    本文介绍了Oracle seg,V$TEMPSEG_USAGE与Oracle排序之间的关系,V$TEMPSEG_USAGE是V_$SORT_USAGE的同义词,通过查询dba_objects和dba_synonyms视图可以了解到它们的详细信息。同时,还探讨了V$TEMPSEG_USAGE的使用方法。 ... [详细]
  • 本文介绍了Oracle存储过程的基本语法和写法示例,同时还介绍了已命名的系统异常的产生原因。 ... [详细]
  • 本文讨论了在VMWARE5.1的虚拟服务器Windows Server 2008R2上安装oracle 10g客户端时出现的问题,并提供了解决方法。错误日志显示了异常访问违例,通过分析日志中的问题帧,找到了解决问题的线索。文章详细介绍了解决方法,帮助读者顺利安装oracle 10g客户端。 ... [详细]
  • 工作经验谈之-让百度地图API调用数据库内容 及详解
    这段时间,所在项目中要用到的一个模块,就是让数据库中的内容在百度地图上展现出来,如经纬度。主要实现以下几点功能:1.读取数据库中的经纬度值在百度上标注出来。2.点击标注弹出对应信息。3 ... [详细]
  • 浅析Mysql数据回滚错误的解决方法_PHP教程:MYSQL的事务处理主要有两种方法。1、用begin,rollback,commit来实现begin开始一个事务rollback事 ... [详细]
  • Html5-Canvas实现简易的抽奖转盘效果
    本文介绍了如何使用Html5和Canvas标签来实现简易的抽奖转盘效果,同时使用了jQueryRotate.js旋转插件。文章中给出了主要的html和css代码,并展示了实现的基本效果。 ... [详细]
author-avatar
ET
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有