整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

探秘网页性能提升利器之CSS硬件加速

天我们将为大家介绍一个令网页性能大幅提升的神奇技术——CSS硬件加速。随着移动互联网的蓬勃发展和网页设计越发复杂,如何优化网页性能成为了前端开发者们亟待解决的问题。在这篇文章中,我们将深入了解CSS硬件加速的原理,并通过一个生动的案例来展示它如何帮助我们改善网页的渲染性能。

一、什么是CSS硬件加速

在传统的网页渲染中,浏览器使用中央处理器(CPU)来处理CSS样式和页面渲染。然而,随着网页变得越来越复杂,例如包含大量动画、过渡效果或复杂的变换,CPU可能会承担较重的负担,导致页面加载缓慢或卡顿。CSS硬件加速是一种解决方案,它充分利用了计算机的图形处理单元(GPU)来加快CSS样式的处理和渲染,从而提高页面性能和流畅度。

1.1 CPU

CPU 即中央处理器。

CPU是计算机的大脑,它提供了一套指令集,我们写的程序最终会通过 CPU 指令来控制的计算机的运行。它会对指令进行译码,然后通过逻辑电路执行该指令。整个执行的流程分为了多个阶段,叫做流水线。指令流水线包括取指令、译码、执行、取数、写回五步,这是一个指令周期。CPU会不断的执行指令周期来完成各种任务。

1.2 GPU

GPU 即图形处理器。

GPU,是Graphics ProcessingUnit的简写,是现代显卡中非常重要的一个部分,其地位与CPU在主板上的地位一致,主要负责的任务是加速图形处理速度。GPU是显卡的“大脑”,它决定了该显卡的档次和大部分性能,同时也是2D显示卡和3D显示卡的区别依据。2D显示芯片在处理3D图像和特效时主要依赖CPU的处理能力,称为“软加速”。3D显示芯片是将三维图像和特效处理功能集中在显示芯片内,也即所谓的“硬件加速”功能。

二、CSS硬件加速原理

CSS硬件加速的原理涉及到浏览器的渲染引擎、GPU以及优化渲染的过程。

2.1 浏览器的渲染流程

一个完整的渲染步骤大致可总结为如下:

  • 渲染进程将HTML内容转换为能够读懂的DOM树结构。
  • 渲染引擎将CSS样式表转化为浏览器可以理解的 styleSheets ,计算出DOM节点的样式。
  • 创建布局树,并计算元素的布局信息。
  • 对布局树进行分层,并生成分层树。
  • 为每个图层生成绘制列表,并将其提交到合成线程。
  • 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。
  • 合成线程发送绘制图块命令DrawQuad给浏览器进程。
  • 浏览器进程根据DrawQuad消息生成页面,并显示到显示器上。

2.2 CSS硬件加速触发

在传统的渲染过程中,布局和绘制是由CPU来完成的,而在CSS硬件加速下,GPU参与了渲染的处理,从而提高了性能。

CSS 中的以下几个属性能触发硬件加速:

1.transform属性:该属性用于应用2D或3D变换效果,如旋转、缩放、平移等。当使用transform属性时,浏览器会将变换任务交给GPU处理,从而实现硬件加速。

2.opacity属性:该属性用于设置元素的不透明度。虽然它主要用于控制透明度,但是一个不为1的值(例如0.99)也可以触发硬件加速。

3.will-change属性:will-change属性用于提示浏览器一个元素将要发生的变化,以便浏览器在渲染过程中做出优化。

一旦CSS硬件加速被触发,相关的渲染任务将被GPU处理。GPU在处理图形和动画方面通常比CPU更快和更高效。对于复杂的CSS动画和变换,GPU可以并行处理多个任务,从而提高性能和流畅度。

请注意,CSS硬件加速并不是适用于所有情况。虽然它在许多情况下可以带来显著的性能提升,但有时也可能导致额外的GPU资源占用,从而影响其他应用程序的性能。因此,在使用CSS硬件加速时,我们应该进行性能测试和优化,确保在特定情况下确实能获得性能的提升。

三、CSS硬件加速案例

现在,我们来看一个实际的案例,通过启用CSS硬件加速来改善网页性能。

<!DOCTYPE html>

<html lang="en">

<head>

<meta charset="UTF-8" />

<meta name="viewport" content="width=device-width, initial-scale=1.0" />

<title>Document</title>

<style>

.app {

position: relative;

width: 400px;

height: 400px;

}

.box {

position: absolute;

left: 0;

top: 0;

width: 100px;

height: 100px;

background-color: yellowgreen;

}

.box-run1 {

-webkit-animation: run1 4s infinite;

animation: run1 4s infinite;

}

.box-run2 {

-webkit-animation: run2 4s infinite;

animation: run2 4s infinite;

}

@keyframes run1 {

0% {

top: 0;

left: 0;

}

25% {

top: 0;

left: 200px;

}

50% {

top: 200px;

left: 200px;

}

75% {

top: 200px;

left: 0;

}

}

@keyframes run2 {

0% {

transform: translate(0, 0);

}

25% {

transform: translate(200px, 0);

}

50% {

transform: translate(200px, 200px);

}

75% {

transform: translate(0, 200px);

}

}

</style>

</head>

<body>

<div class="app">

<div class="box"></div>

</div>

<button class="btn1">循环转换</button>

<button class="btn2">硬件加速</button>

<script>

let box = document.querySelector(".box");

let btn1 = document.querySelector(".btn1");

let btn2 = document.querySelector(".btn2");

btn1.addEventListener("click", function (e) {

box.classList.remove("box-run2");

box.classList.add("box-run1");

});

btn2.addEventListener("click", function (e) {

box.classList.remove("box-run1");

box.classList.add("box-run2");

});

</script>

</body>

</html>

此时我们可以运行代码,在页面上可以看到,2个按钮均能使box在app当中循环移动。但对于这两种方式的移动,他们的效率却有着很大的差异。我们可以使用开发者工具里的Performance去查看。

当我们点击btn1时,此时box盒子通过定位的left和top进行循环移动时。

此时我们可以看到细节模块的记录详情。

蓝色(Loading):网络通信和HTML解析

黄色(Scripting):Javascript执行

紫色(Rendering):样式计算和布局,即重排

绿色(Painting):重绘

灰色(Other):其他事件花费的时间

白色(Idle):空闲时间

细节模块有4个面板,Summary面板每个事件都会有,其他三个只针对特定事件会有。

当我们点击btn2时,此时box盒子通过transform属性进行css硬件加速后进行循环移动时。

通过对比我们不难发现,当启用硬件加速时,方块的变换会更加流畅,其样式计算和布局、重绘的时间都会减少。因为GPU参与了渲染过程。

总结

CSS硬件加速是一个强大的前端技术,可以显著提高网页的性能和流畅度。通过启用硬件加速,我们可以将一些渲染任务交给GPU来处理,减轻CPU的负担,从而优化网页的渲染性能。然而,我们需要注意不要滥用硬件加速,避免触发不必要的GPU渲染,以确保真正获得性能提升。在日常的网页开发中,我们可以灵活运用CSS硬件加速,为用户带来更好的浏览体验。

篇主要讲websocket的服务端实现,主要基于Tomcat9.0,关于登录,用户列表下篇再讲,

前端可以借鉴上篇文章,地址如下:

自己动手实现基于websocket的聊天web聊天功能高仿仿qq

基于Tomcat9的websocket服务实现

首先创建一个javaweb项目(此处以eclipse开发工具为例)

然后选中项目build path

假如tomcat的依赖jar包

websocket-api.jar

tomcat-websocket.jar

这样就可以了

下面是websocket的简单的实现:

此处“imchat”为访问路径,“{id}”为需传递的参数,是OnOpen时接收的参数这两处变量名需要写一样,如果是多参数可以后面继续追加如/imchat/{id}/{name}

@ServerEndpoint(value="/imchat/{id}")

public class ImSocket {

//静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。

private static int onlineCount = 0;

//concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识

private static CopyOnWriteArraySet<ImSocket> webSocketSet = new CopyOnWriteArraySet<ImSocket>();

//与某个客户端的连接会话,需要通过它来给客户端发送数据

private Session session;

/**

* 连接建立成功调用的方法,只在建立连接时调用

* @param session 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据

*/

@OnOpen

public void onOpen(@PathParam("id") String id,Session session){

this.session = session;

webSocketSet.add(this); //加入set中

addOnlineCount(); //在线数加1

System.out.println("有新连接加入!当前在线人数为" + getOnlineCount());

}

/**

* 连接关闭调用的方法

*/

@OnClose

public void onClose(){

webSocketSet.remove(this); //从set中删除

subOnlineCount(); //在线数减1

System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount());

}

/**

* 收到客户端消息后调用的方法,连接后所有交互数据都在此处理

* @param message 客户端发送过来的消息

* @param session 可选的参数

*/

@OnMessage

public void onMessage(String message, Session session) {

System.out.println("来自客户端的消息:" + message);

//群发消息

for(ImSocket item: webSocketSet){

try {

item.sendMessage(message);

} catch (IOException e) {

e.printStackTrace();

continue;

}

}

}

/**

* 发生错误时调用

* @param session

* @param error

*/

@OnError

public void onError(Session session, Throwable error){

System.out.println("发生错误");

error.printStackTrace();

}

/**

* 这个方法与上面几个方法不一样。没有用注解,是根据自己需要添加的方法。

* @param message

* @throws IOException

*/

public void sendMessage(String message) throws IOException{

this.session.getBasicRemote().sendText(message);

//this.session.getAsyncRemote().sendText(message);

}

public static synchronized int getOnlineCount() {

return onlineCount;

}

public static synchronized void addOnlineCount() {

ImSocket.onlineCount++;

}

public static synchronized void subOnlineCount() {

ImSocket.onlineCount--;

}

}

前端测试代码

<!DOCTYPE html>

<html>

<head>

<title>Java后端WebSocket的Tomcat实现</title>

</head>

<body>

Welcome<br/><input id="text" type="text"/>

<button onclick="send()">发送消息</button>

<hr/>

<button onclick="closeWebSocket()">关闭WebSocket连接</button>

<hr/>

<div id="message"></div>

</body>

<script type="text/javascript">

var websocket = null;

//判断当前浏览器是否支持WebSocket

if ('WebSocket' in window) {

websocket = new WebSocket("ws://localhost:8080/项目名/imchat/id123");

}

else {

alert('当前浏览器 Not support websocket')

}

//连接发生错误的回调方法

websocket.onerror = function () {

setMessageInnerHTML("WebSocket连接发生错误");

};

//连接成功建立的回调方法

websocket.onopen = function () {

setMessageInnerHTML("WebSocket连接成功");

}

//接收到消息的回调方法

websocket.onmessage = function (event) {

setMessageInnerHTML(event.data);

}

//连接关闭的回调方法

websocket.onclose = function () {

setMessageInnerHTML("WebSocket连接关闭");

}

//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。

window.onbeforeunload = function () {

closeWebSocket();

}

//将消息显示在网页上

function setMessageInnerHTML(innerHTML) {

document.getElementById('message').innerHTML += innerHTML + '<br/>';

}

//关闭WebSocket连接

function closeWebSocket() {

websocket.close();

}

//发送消息

function send() {

var message = document.getElementById('text').value;

websocket.send(message);

}

</script>

</html>

以上是简单的websocket后端与前端的连接与交互,以上测试通过后咱们写一下我们聊天的简单实现

基于上面的认识我们已经能够实现简单的前端与websocket服务端的交互,下面我们接着上篇的自己动手实现基于websocket的聊天web聊天功能高仿仿qq

首先我们看一下qq的逻辑,简单的讲就这 三步,登录---》获取好友列表---》发送消息给好友;

第一步登录和获取好友列表,这个我们有两种方式去实现,一种是http请求,一种是websocket去实现,考虑到这样请求websocket会使处理流程变的复杂,所以我们采用http的方式实现,这样我们得websocket主要用来处理消息转发 服务,

首先我们创建一个实体作为消息的承载体

public class ImMsgModel {
	 private boolean system;//消息类型
	 private String key;//事件类型// offline离线消息 online在线消息
	 private String avatar;
	 private String id;
	 private String sign;
	 private String status;
	 private String username;
	 private String name;
	 private String type;
	 private String content;
	 private long timestamp;
	 private String fromid;
	 	
		public boolean isSystem() {
			return system;
		}
		public void setSystem(boolean system) {
			this.system = system;
		}
		public String getKey() {
			return key;
		}
		public void setKey(String key) {
			this.key = key;
		}
		public String getId() {
			return id;
		}
		public void setId(String id) {
			this.id = id;
		}
		public long getTimestamp() {
			return timestamp;
		}
		public void setTimestamp(long timestamp) {
			this.timestamp = timestamp;
		}
		public String getFromid() {
			return fromid;
		}
		public void setFromid(String fromid) {
			this.fromid = fromid;
		}
		public void setAvatar(String avatar) {
	 this.avatar = avatar;
	 }
	 public String getAvatar() {
	 return avatar;
	 }
	 public void setSign(String sign) {
	 this.sign = sign;
	 }
	 public String getSign() {
	 return sign;
	 }
	 public void setStatus(String status) {
	 this.status = status;
	 }
	 public String getStatus() {
	 return status;
	 }
	 public void setUsername(String username) {
	 this.username = username;
	 }
	 public String getUsername() {
	 return username;
	 }
	 public void setName(String name) {
	 this.name = name;
	 }
	 public String getName() {
	 return name;
	 }
	 public void setType(String type) {
	 this.type = type;
	 }
	 public String getType() {
	 return type;
	 }
	 public void setContent(String content) {
	 this.content = content;
	 }
	 public String getContent() {
	 return content;
	 }

第二步我们对websocket进行封装,此处消息实体根据layim前端封装,需要了解详情的请移步layui官网访问layim模块

@ServerEndpoint(value="/imchat/{id}")
public class WebsocketsListener {
	
	 private static final Set<WebsocketsListener> connections = new CopyOnWriteArraySet<WebsocketsListener>();
	 private Session session;
	 private String userid;
	
	Logger log = null;
	public WebsocketsListener() {
		log = Logger.getGlobal();
	}
	
	 @OnOpen
	public void start(@PathParam(value="id") String id,Session session) {
		// TODO Auto-generated method stub
		//log.log(Level.INFO, "打开监听onOpen");
		this.session=session;
		this.userid=id;
		connections.add(this);
//		Redis.use().hmset("userid="+id, hash);
		System.out.println(id+"用户session:"+session);
	}
	@OnMessage
 public void incoming(String message) {
		 log.info("------------------"+message); 
	 try { 
	 	 ImMsgModel m = JSON.parseObject(message, ImMsgModel.class);
	 	 System.out.println(m.getFromid()+"发送给"+m.getId());
	 	
	 	 Session s = getSessionByID(m.getId());//接收者id
	 	
	 	 //消息接收方掉线
				if(s==null){
					 Session s1 = getSessionByID(m.getFromid());
					 //发送方也不在线
					 if(s1==null) {
						 log.info( "用户已掉线线");
					 }else {
						 //发送消息给消息发送方提示消息接收方掉线
						 ImMsgModel msg = new ImMsgModel();
						 msg.setKey("offline");
						 msg.setSystem(true);
						 msg.setId(m.getId());
						 msg.setType("friend");
						 msg.setContent("对方已掉线");
						 log.info( "对方已掉线");
						 send2user(JSON.toJSONString(msg), s1);
					 }
				}else{
					//发送消息给消息接收方
					 m.setId(m.getFromid());
					 m.setKey("online");
					 send2user(JSON.toJSONString(m), s);
				}
				
	 } catch (Exception e) { 
	 // TODO Auto-generated catch block 
	 e.printStackTrace(); 
	 } 
	 
	 } 
	private Session getSessionByID(String id) {
		System.out.println("连接用户数"+connections.size());
		for (WebsocketsListener wb : connections) {
			 //接收对象id对应的session
 		if(id.equals(wb.userid)) {
 			return wb.session;
 		}
 	}
		return null;
	}
	@OnClose
 public void end() {
		connections.remove(this);
	 }
	
	@OnError
	 public void onError(Throwable t) throws Throwable {
		log.info( "发生错误onError");
		t.printStackTrace();
	}
	
	private void send2user(String msg,Session session){
			 try {
					 session.getBasicRemote().sendText(msg); 
			} catch (IOException e) {
				e.printStackTrace();
			}
		
	}
	 
	 public static void sendAll(String string) {
			 for (WebsocketsListener wb : connections) {
	 	try {
	 			wb.session.getBasicRemote().sendText(string);
					} catch (Exception e) {
						e.printStackTrace();
						 connections.remove(wb);
						try {
						 wb.session.close();
						} catch (IOException e1) {
							e1.printStackTrace();
						}
					}
	 	
	 }
		}
	}

以上完成了websocket的服务前端消息转发代码(数据持久化与 redis后面继续进行补充)

下面我贴一下上篇中讲的前端代码的完整版

前端代码

 <!DOCTYPE html>
<html>
<head>
 <meta charset="utf-8">
 <title>layui</title>
 <meta name="renderer" content="webkit">
 <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
 <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
 <link rel="stylesheet" href="js/css/modules/layui.css" media="all">
 <!-- 注意:如果你直接复制所有代码到本地,上述css路径需要改成你本地的 -->
</head>
<body>
 
 
<script src="js/layui.js" charset="utf-8"></script>
<!-- 注意:如果你直接复制所有代码到本地,上述js路径需要改成你本地的 -->
<style>
/* img */
i{font-style:normal;}
.qq-login{width:430px;height:330px;margin:0 0 -165px -215px;bottom:50%;left:50%;position:fixed;z-index:9999;border-radius:3px;overflow:hidden;box-shadow:0 0 5px #333;background:#ebf2f9 url(js/images/bj/qq-login-bg.jpg) center top no-repeat;display:block;}
.login-menu{width:90px;height:30px;top:0;right:0;position:absolute;}
.login-menu span{float:left;width:30px;height:30px;background-image:url(js/images/bj/qq-login-bg.jpg);}
.login-menu span:hover{background-color:#3a95de;}
.login-menu span:nth-child(1){background-position:left center;}
.login-menu span:nth-child(2){background-position:-30px center;}
.login-menu span:nth-child(3){background-position:-90px center;}
.login-menu span:nth-child(3):hover{background-color:#ea4848;}
.login-ner{margin-top:182px;float:left;width:100%;height:148px;}
.login-left{float:left;width:133px;height:148px;}
.login-head{float:left;width:80px;height:80px;border-radius:50%;border:1px solid #ccc;overflow:hidden;margin:12px 11px 0 40px;}
.login-head img{width:80px;height:80px;}
.login-on{width:194px;height:148px;float:left;}
.login-txt{float:left;margin-top:12px;height:60px;width:100%;}
.login-txt input{border:1px solid #d1d1d1;float:left;height:30px;padding:0 7px;font-size:12px;width:100%;}
.login-txt input:nth-child(1){border-radius:4px 4px 0 0;}
.login-txt input:nth-child(2){border-radius:0 0 4px 4px;margin-top:-1px;}
.login-xuan{width:100%;float:left;height:14px;line-height:14px;margin-top:8px;}
.login-xuan input{width:14px;height:14px;float:left;}
.login-xuan i{float:left;padding-left:4px;}
.login-right{width:103px;height:60px;float:left;margin-top:12px;}
.login-right a{float:left;padding-left:10px;width:90%;color:#2786e4;line-height:30px;text-indent:10px;}
.login-but{width:100%;height:30px;margin:13px 0;float:left;background:#09a3dc;color:#fff;text-align:center;line-height:30px;border-radius:4px;font-size:14px; cursor:context-menu;}
.login-menu span {
 float: left;
 width: 30px;
 height: 30px;
 background-image: url(js/images/bj/wins.png);
}
.login-tips{line-height:40px;width:300px;padding:10px;color: white;top:0;left:0;position:absolute;}
</style>
<script id="login_html" type="text/html">
<div class="qq-login">
 <div class="login-tips" id="login-tips"></div>
<div class="login-menu">
 <span></span><span></span><span class="login-close"></span>
</div>
<div class="login-ner">
 <div class="login-left">
 <div class="login-head"><img src="js/css/modules/layim/skin/4.jpg"></div>
 </div>
 <div class="login-on">
 <div class="login-txt"><input type="text" id="username" placeholder="QQ号码/手机/邮箱"><input id="password" type="password" placeholder="密码"></div>
 <div class="login-xuan"><span class="fl"><input type="checkbox"><i>记住密码</i></span><span class="fr"><input type="checkbox"><i>自动登录</i></span></div>
 <div class="login-but" id="login-but">安全登录</div>
 </div>
 <div class="login-right">
 <a href="http://zc.qq.com/chs/index.html" target="_blank">注册账号</a><a href="https://aq.qq.com/cn2/findpsw/pc/pc_find_pwd_input_account?pw_type=0&aquin=" target="_blank">找回密码</a>
 </div>
</div>
</div>
</script>
<script>
layui.use('layim', function(){
 var layim = layui.layim,id;
 $ = layui.jquery,
 
 layer.open({
	 title:false
 ,type: 1
 ,offset: 'auto' //具体配置参考:http://www.layui.com/doc/modules/layer.html#offset
 ,content: login_html.innerHTML
 ,btn: false
 ,shadeClose: false
 ,closeBtn: 0
 ,moveType: 0
 ,move: '.login-head'
 ,btnAlign: 'r' //按钮居中
 ,shade: 0 //不显示遮罩
 });
 //绑定登陆事件
 $(document).on('click', '#login-but', function(data) {
 login();
 });
 
 var tips= $('#login-tips');	
 
 function login(){
	 tips.html("正在登陆……");
	 var un= $("#username").val();
	 var ps= $("#password").val();
	 var d={"m":"login","username":un,"password":ps}
	 $.ajax({
 type:"POST",
 url:"../chat",
 dataType:"json",
 data:d,
 success:function(data){
 if(data.code==20000){
 	 id=data.data.id
 	 tips.html("登陆成功");
 	 chartSetting();
 	 connection();
 }else{
 	 tips.html("登陆失败请重试!"+data.msg); 
 }
 },
 error:function(jqXHR){
 	 tips.html("发生错误"+jqXHR.status); 
 }
 });
 }
 
 function chartSetting(){
	//基础配置
	 layim.config({
	 //初始化接口
	 init: {
	 url: 'chat?m=list&id='+id
	 ,data: {}
	 }
	 //查看群员接口
	 ,members: {
	 url: 'chat?m=getMembers&id='+id
	 ,data: {}
	 }
	 
	 ,uploadImage: {
	 url: 'uploadv2?filepath=' //(返回的数据格式见下文)
	 ,type: '' //默认post
	 }
	 ,uploadFile: {
	 url: 'uploadv2?filepath=' //(返回的数据格式见下文)
	 ,type: '' //默认post
	 }
	 
	 ,isAudio: true //开启聊天工具栏音频
	 ,isVideo: true //开启聊天工具栏视频
	 
	 //扩展工具栏
	 ,tool: [{
	 alias: 'code'
	 ,title: '代码'
	 ,icon: ''
	 }]
	 
	 ,brief: false //是否简约模式(若开启则不显示主面板)
	 
	 ,title: '消息' //自定义主面板最小化时的标题
	 ,right: '10px' //主面板相对浏览器右侧距离
	 ,minRight: '90px' //聊天面板最小化时相对浏览器右侧距离
	 ,initSkin: '3.jpg' //1-5 设置初始背景
	 ,skin: ['js/css/modules/layim/skin/6.jpg',
	 'js/css/modules/layim/skin/1.jpg'] //新增皮肤
	 ,isfriend: true //是否开启好友
	 ,isgroup: true //是否开启群组
	 ,min: false //是否始终最小化主面板,默认false
	 ,notice: true //是否开启桌面消息提醒,默认false
	 ,voice: true //声音提醒,默认开启,声音文件为:default.mp3
	 ,msgbox: 'msgbox.html' //消息盒子页面地址,若不开启,剔除该项即可
	 ,find: 'find.html' //发现页面地址,若不开启,剔除该项即可
	 ,chatLog: 'chatlog.html' //聊天记录页面地址,若不开启,剔除该项即可
	 });
 }
 //监听在线状态的切换事件
 layim.on('online', function(status){
 layer.msg(status);
 });
 //演示自动回复
 var autoReplay = [
 '您好,我现在有事不在,一会再和您联系。', 
 '你没发错吧?face[微笑] ',
 '洗澡中,请勿打扰,偷窥请购票,个体四十,团体八折,订票电话:一般人我不告诉他!face[哈哈] ',
 '你好,我是主人的美女秘书,有什么事就跟我说吧,等他回来我会转告他的。face[心] face[心] face[心] ',
 'face[威武] face[威武] face[威武] face[威武] ',
 '<(@ ̄︶ ̄@)>',
 '你要和我说话?你真的要和我说话?你确定自己想说吗?你一定非说不可吗?那你说吧,这是自动回复。',
 'face[黑线] 你慢慢说,别急……',
 '(*^__^*) face[嘻嘻] ,是贤心吗?'
 ];
 
 
 //监听在线状态的切换事件
 layim.on('online', function(status){
 layer.msg(status);
 });
 
 //监听签名修改
 layim.on('sign', function(value){
 layer.msg(value);
 });
 //监听自定义工具栏点击,以添加代码为例
 layim.on('tool(code)', function(insert){
 layer.prompt({
 title: '插入代码 - 工具栏扩展示例'
 ,formType: 2
 ,shade: 0
 }, function(text, index){
 layer.close(index);
 insert('[pre class=layui-code]' + text + '[/pre]'); //将内容插入到编辑器
 });
 });
 
 //监听layim建立就绪
 layim.on('ready', function(res){
 //console.log(res.mine);
 layim.msgbox(5); //模拟消息盒子有新消息,实际使用时,一般是动态获得
 });
 //监听发送消息
 layim.on('sendMessage', function(data){
 var To = data.to;
 var Me = data.mine;
 if(To.type === 'friend'){
 layim.setChatStatus('<span style="color:#FF5722;">对方正在输入。。。</span>');
 }
 if(To.id==Me.id){
 	alert("无法和自己发起聊天");
 	return;
 }else{
 	
 var data={
			 username: Me.username //消息来源用户名
				 ,avatar: Me.avatar //消息来源用户头像
				 ,id: To.id //消息的来源ID(如果是私聊,则是用户id,如果是群聊,则是群组id)
				 ,type:To.type //聊天窗口来源类型,从发送消息传递的to里面获取
				 ,content: Me.content //消息内容
				 ,cid: 0 //消息id,可不传。除非你要对消息进行一些操作(如撤回)
				 ,mine: false //是否我发送的消息,如果为true,则会显示在右方
				 ,fromid:Me.id //消息的发送者id(比如群组中的某个消息发送者),可用于自动解决浏览器多窗口时的一些问题
				 ,timestamp:new Date().getTime() //服务端时间戳毫秒数。注意:如果你返回的是标准的 unix 时间戳,记得要 *1000
	 };
 //模拟系统消息
	 websocket.send(JSON.stringify(data));
	 layim.setChatStatus('<span style="color:#FF5722;">在线</span>');
 }
 
 });
 //监听查看群员
 layim.on('members', function(data){
 //console.log(data);
 });
 
 //监听聊天窗口的切换
 layim.on('chatChange', function(res){
 var type = res.data.type;
 console.log(res.data.id)
 if(type === 'friend'){
 //模拟标注好友状态
 layim.setChatStatus('<span style="color:#FF5722;">在线</span>');
 } else if(type === 'group'){
 //模拟系统消息
 layim.getMessage({
 system: true
 ,id: res.data.id
 ,type: "group"
 ,content: '模拟群员'+(Math.random()*100|0) + '加入群聊'
 });
 }
 });
 
 
 
 
 function connection(){
	 tips.html("开始连接服务……");
 	 if('WebSocket' in window){
 websocket = new WebSocket("ws://"+sy()+"/imchat/"+id);
 
 }else{
 	tips.html("不支持websocket");
 
 }
 //连接发生错误的回调方法
 websocket.onerror = function(ev,data){
 	
 	tips.html("连接发生错误的回调方法");
 };
 	 //连接成功建立的回调方法
 websocket.onopen = function(e){
 	
 	 tips.html("");
 };
 
 //接收到消息的回调方法
 websocket.onmessage = function(event){
 	// layim.getMessage(event.data);
 	var json=JSON.parse(event.data);
 	 console.log("接收信息:");
 	 console.log(event.data);
 	 if(json.key=="offline"){
 		 //用户离线 
 		 layim.setFriendStatus(json.id, 'offline');
 		 layim.setChatStatus('<span style="color:gray;">离线</span>');
 layer.msg(json.content+"无法接收到消息", {
 icon: 1
 });
 	 }else if(json.key=="online"){ //接收在线消息
 		 //制造好友消息
 		 layim.setFriendStatus(json.id, 'online');
 		 layim.setChatStatus('<span style="color:#FF5722;">在线</span>');
 layim.getMessage(json);	 
 	 }
 	 
 		 
 	
 };
 //连接关闭的回调方法
 websocket.onclose = function(event){
 	 //alert('连接关闭的回调方法');
 	 tips.html("连接已关闭,尝试重连……");
 disConnect();
 };
 //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
 window.onbeforeunload = function(){
 websocket.close();
 };
 }
//检查链接,短线重连
 var disConnect = function(){
 setTimeout(function(){
 	connection();
 },5000);
 }
 //关闭连接
 function closeWebSocket(){
	 tips.html("关闭closeWebSocket");
 websocket.close();
 }
 
	function sy(){
		var curWwwPath = window.document.location.href;
		var pathName = window.document.location.pathname;
		var pos = curWwwPath.indexOf(pathName);
		var localhostPaht = curWwwPath.substring(0,pos);
		var projectName = pathName.substring(0,pathName.substr(1).indexOf('/')+1);
		var ip=window.location.host;
		var prot=window.location.port;
		return (ip + projectName);
		}
	
});
</script>
</body>
</html>

结合以上服务端代码和前端代码可以实现基本的聊天功能,本篇没涉及到登录接口和用户列表接口,下篇再做补充。需要的同学关注一下下篇。有问题欢迎留言指正

门的HTML - tabindex 的作用

HTML 的 tabindex 属性开发过程中一般不会使用到,最近开发中有个需求兼顾富交互,便总结了一下。本篇文章同时收录在我的【前端知识点】中,Github链接请点击阅读原文直达,欢迎 Star

兼容性:Safari不支持!

阅读本文您将收获

  • tabindex的作用
  • tabindex的使用
  • 如何利用 tabindex 创造更好的用户体验

前言

在我们日常使用网页的过程中,可以通过键盘控制一些元素的聚焦,从而达到便捷访问的目的

element 分为 focusable 和 非focusable ,如果使用了tabindex就可以改变相关的行为

在HTML中有6个元素默认支持聚焦:

  • 带 href 属性的 <a> 标签
  • 带 href 属性的 <link> 标签
  • <button></button> 标签
  • <input /> 标签 (排除带有 type="hidden" 属性的)
  • <select></select> 标签
  • <textarea></textarea> 标签

以上的元素默认都可以使用 Tab 键,以及 JS focus() 方法聚焦

document.querySelector("a").focus();

使用 tab键 进行聚焦元素时,聚焦的顺序等于元素在代码中的出现先后顺序,当我们进行富交互优化时,就需要用到 tabindex 这个属性来帮助我们进行更好用户体验的优化了

tabindex的作用

①元素是否能聚焦:通过键盘这类输入设备,或者通过 JS focus() 方法

②元素什么时候能聚焦:在用户通过键盘与页面交互时

通俗来说:就是当用户使用键盘时,tabindex用来定位html元素,即使用tab键时焦点的顺序。

tabindex的范围

tabindex理论上可以使用在几乎所有元素上

  • tabindex 理论上可以用在几乎所有元素上,不管这个元素默认是否支持聚焦

tabindex 有三个值:0,-N(通常是-1),N(正值)

  • tabindex=0,该元素可以用tab键获取焦点
    • 且访问的顺序是按照元素在文档中的顺序来focus,即使采用了浮动改变了页面中显示的顺序,依然是按照html文档中的顺序来定位
  • tabindex<=-1,该元素用tab键获取不到焦点,但是可以通过js获取
    • 这样就便于我们通过js设置上下左右键的响应事件来focus
    • 取值 -1~-999 之间没有区别,但为了可读性和一致性考虑,推荐使用 -1
  • tabindex>=1,该元素可以用tab键获取焦点,而且优先级大于tabindex=0
    • 不过在tabindex>=1时,数字越小,越先定位到;
    • 如果多个元素拥有相同的 tabindex ,他们的相对顺序按照他们在当前DOM中的先后顺序决定

tabindex的使用

tabindex 决定聚焦顺序

  • 可聚焦元素中,正整数数值越大,顺序越往后,正整数数值的节点顺序比0值的节点靠前
  • 代码:
// HTML
<button type="button" tabindex="1">tabindex === 1</button>
<button type="button" tabindex="999">tabindex === 999</button>
<button type="button" tabindex="0">tabindex === 0</button>
  • 效果:
  • 可聚焦元素中,相同 tabindex 数值的节点,根据 DOM节点 先后顺序决定聚焦顺序
  • 代码:
// HTML
<button type="button" tabindex="0">tabindex === 0</button>
<button type="button" tabindex="1">tabindex === 1</button>
<button type="button" tabindex="999">tabindex === 999</button>
<button type="button" tabindex="0">tabindex === 0</button>
  • 效果:

tabindex 决定是否聚焦

  • 节点的 tabindex 设置为 -1 时,当前节点使用 tab键 不能聚焦
  • 代码:
// HTML
<button type="button">未设置tabindex</button>
<button type="button" tabindex="-1">tabindex === -1</button>
<button type="button" tabindex="0">tabindex === 0</button>
<button type="button" tabindex="1">tabindex === 1</button>
  • 效果:

tabindex 与JS编程聚焦

  • 通过 tabindex 结合JS可以让默认不支持聚焦的节点进行聚焦,tabindex 为不超出范围的任何整数值都可以
  • 代码:
// HTML
<button type="button" @click="clickBtn()">点击让DIV聚焦</button>
<div id="FocusDiv" ref="FocusDiv" tabindex="-1">这是一个div</div>

// JS
clickBtn: function() {
    document.getElementById('FocusDiv').focus();
}
  • 效果:

如何利用 tabindex 创造更好的用户体验

针对自定义标签进行富交互优化

  • 我们在创建一个自定义的标签时,如果默认行为中不包含聚焦事件,我们可以使用 tabindex 为它增加聚焦功能,从而可以像很多可聚焦节点一样进行顺次焦点聚焦了

针对特定节点禁止聚焦操作

  • 某些浮层及上层节点,如 toast组件、模态框、侧边弹出信息等,我们不希望节点被用户聚焦捕获,可以将节点的 tabindex 设置为 -1,就能避免这一问题

复杂列表控制聚焦顺序

  • 一些复杂的树形结构或者列式结构,如果需要用户操作顺序按照我们预想的书序进行聚焦,可以利用tabindex 值的大小来进行处理。