Tuesday, June 7, 2022

SegmentFault 最新的文章

SegmentFault 最新的文章


文字轮播与图片轮播?CSS 不在话下

Posted: 06 Jun 2022 07:30 PM PDT

今天,分享一个实际业务中能够用得上的动画技巧。

巧用逐帧动画,配合补间动画实现一个无限循环的轮播效果,像是这样:

看到上述示意图,有同学不禁会发问,这不是个非常简单的位移动画么?

我们来简单分析分析,从表面上看,确实好像只有元素的 transform: translate() 在位移,但是注意,这里有两个难点:

  1. 这是个无限轮播的效果,我们的动画需要支持任意多个元素的无限轮播切换
  2. 因为是轮播,所以,运行到最后一个的时候,需要动画切到第一个元素

到这里,你可以暂停思考一下,如果有 20 个元素,需要进行类似的无限轮播播报,使用 CSS 实现,你会怎么去做呢?

逐帧动画控制整体切换

首先,我需要利用到逐帧动画效果,也被称为步骤缓动函数,利用的是 animation-timing-function 中,的 steps,语法如下:

{     /* Keyword values */     animation-timing-function: step-start;     animation-timing-function: step-end;     /* Function values */     animation-timing-function: steps(6, start)     animation-timing-function: steps(4, end); }

如果你对 steps 的语法还不是特别了解,强烈建议你先看看我的这篇文章 -- 深入浅出 CSS 动画,它对理解本文起着至关重要的作用。

好的,还是文章以开头的例子,假设我们存在这样 HTML 结构:

<div class="g-container">   <ul>     <li>Lorem ipsum 1111111</li>     <li>Lorem ipsum 2222222</li>     <li>Lorem ipsum 3333333</li>     <li>Lorem ipsum 4444444</li>     <li>Lorem ipsum 5555555</li>     <li>Lorem ipsum 6666666</li>   </ul> </div>

首先,我们实现这样一个简单的布局:

在这里,要实现轮播效果,并且是任意个数,我们可以借助 animation-timing-function: steps()

:root {   // 轮播的个数   --s: 6;   // 单个 li 容器的高度   --h: 36;   // 单次动画的时长   --speed: 1.5s; } .g-container {   width: 300px;   height: calc(var(--h) * 1px); } ul {   display: flex;   flex-direction: column;   animation: move calc(var(--speed) * var(--s)) steps(var(--s)) infinite; } ul li {   width: 100%; } @keyframes move {   0% {     transform: translate(0, 0);   }   100% {     transform: translate(0, calc(var(--s) * var(--h) * -1px));   } }

别看到上述有几个 CSS 变量就慌了,其实很好理解:

  1. calc(var(--speed) * var(--s)):单次动画的耗时 * 轮播的个数,也就是总动画时长
  2. steps(var(--s)) 就是逐帧动画的帧数,这里也就是 steps(6),很好理解
  3. calc(var(--s) * var(--h) * -1px)) 单个 li 容器的高度 * 轮播的个数,其实就是 ul 的总体高度,用于设置逐帧动画的终点值

上述的效果,实际如下:

如果给容器添加上 overflow: hidden,就是这样的效果:

这样,我们就得到了整体的结构,至少,整个效果是循环的。

但是由于只是逐帧动画,所以只能看到切换,但是每一帧之间,没有过渡动画效果。所以,接下来,我们还得引入补间动画。

利用补间动画实现两组数据间的切换

我们需要利用补间动画,实现动态的切换效果。

这一步,其实也非常简单,我们要做的,就是将一组数据,利用 transform,从状态 A 位移到 状态 B。

单独拿出一个来演示的话,大致的代码如下:

<div class="g-container">   <ul style="--s: 6">     <li>Lorem ipsum 1111111</li>     <li>Lorem ipsum 2222222</li>     <li>Lorem ipsum 3333333</li>     <li>Lorem ipsum 4444444</li>     <li>Lorem ipsum 5555555</li>     <li>Lorem ipsum 6666666</li>   </ul> </div>
:root {   --h: 36;   --speed: 1.2s; } ul li {   height: 36px;   animation: liMove calc(var(--speed)) infinite; } @keyframes liMove {   0% {     transform: translate(0, 0);   }   80%,   100%  {     transform: translate(0, -36px);   } }

非常简单的一个动画:

bgg1

基于上述效果,我们如果把一开始提到的 逐帧动画 和这里这个 补间动画 结合一下,ul 的整体移动,和 li 的 单个移动叠在在一起:

:root {   // 轮播的个数   --s: 6;   // 单个 li 容器的高度   --h: 36;   // 单次动画的时长   --speed: 1.5s; } .g-container {   width: 300px;   height: calc(var(--h) * 1px); } ul {   display: flex;   flex-direction: column;   animation: move calc(var(--speed) * var(--s)) steps(var(--s)) infinite; } ul li {   width: 100%;   animation: liMove calc(var(--speed)) infinite; } @keyframes move {   0% {     transform: translate(0, 0);   }   100% {     transform: translate(0, calc(var(--s) * var(--h) * -1px));   } } @keyframes liMove {   0% {     transform: translate(0, 0);   }   80%,   100%  {     transform: translate(0, calc(var(--h) * -1px));   } }

就能得到这样一个效果:

Wow,神奇的化学反应产生了!基于 逐帧动画补间动画 的结合,我们几乎实现了一个轮播效果。

当然,有一点瑕疵,可以看到,最后一组数据,是从第六组数据 transform 移动向了一组空数据:

末尾填充头部第一组数据

实际开发过轮播的同学肯定知道,这里,其实也很好处理,我们只需要在末尾,补一组头部的第一个数据即可:

改造下我们的 HTML:

<div class="g-container">   <ul>     <li>Lorem ipsum 1111111</li>     <li>Lorem ipsum 2222222</li>     <li>Lorem ipsum 3333333</li>     <li>Lorem ipsum 4444444</li>     <li>Lorem ipsum 5555555</li>     <li>Lorem ipsum 6666666</li>     <!--末尾补一个首条数据-->     <li>Lorem ipsum 1111111</li>   </ul> </div>

这样,我们再看看效果:

Beautiful!如果你还有所疑惑,我们给容器加上 overflow: hidden,实际效果如下,通过额外添加的最后一组数据,我们的整个动画刚好完美的衔接上,一个完美的轮播效果:

完整的代码,你可以戳这里:CodePen Demo -- Vertical Infinity Loop

横向无限轮播

当然,实现了竖直方向的轮播,横向的效果也是一样的。

并且,我们可以通过在 HTML 结构中,通过 style 内填写 CSS 变量值,传入实际的 li 个数,以达到根据不同 li 个数适配不同动画:

<div class="g-container">   <ul style="--s: 6">     <li>Lorem ipsum 1111111</li>     <li>Lorem ipsum 2222222</li>     <li>Lorem ipsum 3333333</li>     <li>Lorem ipsum 4444444</li>     <li>Lorem ipsum 5555555</li>     <li>Lorem ipsum 6666666</li>     <!--末尾补一个首尾数据-->     <li>Lorem ipsum 1111111</li>   </ul> </div>

整个动画的 CSS 代码基本是一致的,我们只需要改变两个动画的 transform 值,从竖直位移,改成水平位移即可:

:root {   --w: 300;   --speed: 1.5s; } .g-container {   width: calc(--w * 1px);   overflow: hidden; } ul {   display: flex;   flex-wrap: nowrap;    animation: move calc(var(--speed) * var(--s)) steps(var(--s)) infinite; } ul li {   flex-shrink: 0;   width: 100%;   height: 100%;   animation: liMove calc(var(--speed)) infinite; } @keyframes move {   0% {     transform: translate(0, 0);   }   100% {     transform: translate(calc(var(--s) * var(--w) * -1px), 0);   } } @keyframes liMove {   0% {     transform: translate(0, 0);   }   80%,   100%  {     transform: translate(calc(var(--w) * -1px), 0);   } }

这样,我们就轻松的转化为了横向的效果:

完整的代码,你可以戳这里:CodePen Demo -- Horizontal Infinity Loop

轮播图?不在话下

OK,上面的只是文字版的轮播,那如果是图片呢?

没问题,方法都是一样的。基于上述的代码,我们可以轻松地将它修改一下后得到图片版的轮播效果。

代码都是一样的,就不再列出来,直接看看效果:

完整的代码,你可以戳这里:CodePen Demo -- Horizontal Image Infinity Loop

掌握了这个技巧之后,你可以将它运用在非常多只需要简化版的轮播效果之上。

再简单总结一下,非常有意思的技巧:

  1. 利用 逐帧动画,实现整体的轮播的循环效果
  2. 利用 补间动画,实现具体的 状态A状态B* 的动画效果
  3. 逐帧动画 配合 补间动画 构成整体轮播的效果
  4. 通过向 HTML 结构末尾补充一组头部数据,实现整体动画的衔接
  5. 通过 HTML 元素的 style 标签,利用 CSS 变量,填入实际的参与循环的 DOM 个数,可以实现 JavaScript 与 CSS 的打通

最后

OK,本文到此结束,希望本文对你有所帮助 :)

想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 -- iCSS前端趣闻 😄

更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

2022 前端开发报告:TypeScript 成 84% Web 开发者的“最爱”|无障碍性、边缘渲染成大趋势

Posted: 05 Jun 2022 09:30 PM PDT

近日,The Software House发布了一份"2022 前端开发市场状态调查报告"。

此次报告,共有来自全球 125 个国家 / 地区、超 3700+ 名前端开发专业人士填写了调查,同时也融合了来自前端技术开发领域的 19 位专家的观点与分享,并对前端 2020 年和 2022 年的数据并排呈现作对比后最终得出了一份调查结论。

调查结果显示,高达 56% 的受访者正在进行远程工作,其中仅 5% 在办公室工作。大规模远程工作的概念确实比较新颖,以至于 2020 年的调查甚至都没有对这个数据进行调查。

看得出,大多数工程师显然更喜欢远程工作,因为不需要通勤,不用面对随时随刻有人在肩膀上轻拍你而分散注意力的尴尬等等。然而,远程办公状态下,实时分享信息、复制群组消息及自发讨论等问题仍然是一项挑战。

前端开发较易"入门"

此次调查结果中,发现了一个非常有意思的现象:做前端开发的人员,他自身并非仅仅是个前端工程师。据数据显示,在"其他"选项中共享的一些从事前端开发的职位包括:

  • 一个刚开始学习 frontend 的训练班学生,
  • 一位在非技术大学学习的自学成才的开发人员爱上了 frontend,
  • 有时将代码推向生产的产品经理,
  • 开发人员倡导者,时不时帮助前端团队,
  • 前端开发架构师,
  • 设计系统负责人,
  • 一位同时会编码的设计师,
  • 平面设计师和开发人员,
  • 全局负责人:单人负责的开发者商店,自己包揽一切事情,包括前端开发。

虽然这个发现似乎有点不足为奇,但这可以很好地表明:前端技术领域是一个容易进入的领域,即使你此前并没有太多前端背景,但快速学习之后依然能参与进来。

开发者在更大的前端团队工作成常态

据调查数据显示,有 27% 的受访者表示在一家拥有 50 多名前端工程师的公司工作。与此同时,30% 的开发者分享了 5 个或更少的前端开发者在他们公司的工作方式。50% 的受访者在拥有 10 名或以上前端工程师的公司工作。

这个统计数据也显示了一个有趣的现象:在有着大量前端团队公司工作的前端工程师,与在少数人团队或单独工作的公司的工程师数量几乎一样多。当然,这些公司的开发人员经验和期望大不相同:大公司将更多地拥有开发人员经验和前端平台团队。导师制更为常见;在较小的公司里,每个开发人员的责任更大,获得反馈的选项也会更少。

82% 的前端工程师来自科技型公司

数据还显示,有 82% 的受访者被认定为在软件开发公司、开发机构或技术为主或数字为主公司工作。另外,仅 18% 的受访者表示他们在非科技型公司工作。

其中,来自软件开发公司/开发机构的工程师占比为 41.6%,技术为主/数字为主型公司的工程师占比为 41.2%。非技术型公司工程师占比为 12.3%,另外 2.9% 的工程师则来其他领域,1.9% 的工程师来自政府机构。

63% 的开发者关注前端的"无障碍性"

根据调查数据,前端开发的"无障碍性"是今年受访者们普遍关注的重点:有 63% 的人预测它在未来几年会越来越受欢迎。而框架则正倾向于提供不同的方法来解决这个问题,其中就包括 Next/Nuxt Image、HTML validator 和 WebHint。

同时,组件驱动的开发也受到了大多数开发人员的欢迎,考虑到 React、Vue、Svelte 甚至 Web 组件的流行(如今年的独立成功案例——Wordle),这一点很有意义。

渐进式 Web 应用程序也越来越受欢迎,开发人员渴望使用相同的核心代码库充分利用跨平台开发。另外,Headless CMS (无头 CMS)也在不断进步,采用率越来越高,并更多地集成到框架中。

前端"边缘渲染"方案将成大趋势

通过对 2020 年的调查数据与 2022 年今年的调查数据相比对之后,还发现了一个重要的趋势:前端性能优化的方案 —— 边缘渲染。

边缘渲染最初由 CloudFlare 及其 worker 平台驱动。此次调查的期间,大多数部署目标都发布或实现了自己的无服务器或边缘功能,用户很快就会采用这些功能,因此这一趋势并非偶然。

据悉,Nuxt 3、Remix 或 Sveltekit 等框架正朝着这个方向发展,直接在 CDN 级别支持按需渲染。随着服务器呈现的应用程序在减少延迟和降低成本方面的相应收益,由此可以预测这将是 2023 年的一大焦点。

前端开发者正从"Moment.js"转向"Date-FNS"

此次调查结果还显示,在日期处理类库方面,如今的前端开发者们正在从"Moment.js"转向"Date-FNS"。

同时,超过 40% 的人仍然在他们的项目中使用 Moment,尽管该库已经失去了支持,甚至其官方网站上也有创作者留言说"如果你正在考虑使用 Moment,你可能应该寻找替代品"。仅 5% 的受访者希望继续使用该库,看来 Moment 确实正走向衰落。

调查结果中,Axios 网络请求库以超过 60% 的高"得票率",进入了稳定阶段。该库在前端市场已经有很长一段时间了,人们对此很清楚,它更像是一种"标准"而非"趋势"。

另外,由于 Apollo 用于与 GraphQL 的无缝连接,因此它在"使用过的和喜欢的"类别中得票也较高:40% 的开发人员希望在未来学习 Apollo ,这意味着 Apollo 社区正在稳步增长。

TypeScript 成 84% Web 开发者的"最爱"

据调查数据显示,相比 2030 年, 2022 年也就是今年使用 TypeScript 的人数上升了 7 个百分点以上,已经达到了惊人的 84%!


看来大家都知道,TypeScript 如今已受到了广大开发人员的普遍欢迎,人们经常称赞"TypeScript 如何在 bug 发生之前就阻止了一整类 bug",这反过来又使得开发速度更快,应用程序更可靠。

那么,什么让这么多开发人员喜欢 TypeScript?

在经历了多年的 Web 开发之后,前端开发人员早就不想重复多次在代码编辑器和浏览器之间来回切换的经历,不用再猜测为什么"未定义不是功能"。

所以,TypeScript 不仅赢得了开发人员的心,而且还努力成为前端行业标准,它让 web 开发方式变得不再像以前那么让人沮丧了。

关于本次调查结果的更多详情,可查看完整报告。
参考链接:https://tsh.io/state-of-front...

一文搞懂四种 WebSocket 使用方式,建议收藏!!!

Posted: 06 Jun 2022 01:19 AM PDT

在上家公司做IM消息系统的时候,一直是使用 WebSocket 作为收发消息的基础组件,今天就和大家聊聊在 Java 中,使用 WebSocket 所常见的四种姿势,如果大家以后或者现在碰到有要使用 WebSoocket 的情况可以做个参考。

image.png

上面的思维导图已经给大家列出了三种使用 WebSocket 的方式,下文会对它们的特点进行一一解读,不同的方式具有不同的特点,我们先按下不表。

在这里,我想让大家思考一下我在思维导图中列举的第四种做 WebScoket 支持的方案可能是什么?不知道大家能不能猜对,后文将会给出答案。


本文代码:以下仓库中 spring-websocket 模块,拉整个仓库下来后可在 IDEA Maven 工具栏中单独编译此模块。

  • Github
  • Gitee

    WS简介

    在正式开始之前,我觉得有必要简单介绍一下 WebSocket 协议,引入任何一个东西之前都有必要知道我们为什么需要它?

    在 Web 开发领域,我们最常用的协议是 HTTP,HTTP 协议和 WS 协议都是基于 TCP 所做的封装,但是 HTTP 协议从一开始便被设计成请求 -> 响应的模式,所以在很长一段时间内 HTTP 都是只能从客户端发向服务端,并不具备从服务端主动推送消息的功能,这也导致在浏览器端想要做到服务器主动推送的效果只能用一些轮询和长轮询的方案来做,但因为它们并不是真正的全双工,所以在消耗资源多的同时,实时性也没理想中那么好。

    既然市场有需求,那肯定也会有对应的新技术出现,WebSocket 就是这样的背景下被开发与制定出来的,并且它作为 HTML5 规范的一部分,得到了所有主流浏览器的支持,同时它还兼容了 HTTP 协议,默认使用 HTTP 的80端口和443端口,同时使用 HTTP header 进行协议升级。

    和 HTTP 相比,WS 至少有以下几个优点:

  1. 使用的资源更少:因为它的头更小。
  2. 实时性更强:服务端可以通过连接主动向客户端推送消息。
  3. 有状态:开启链接之后可以不用每次都携带状态信息。

除了这几个优点以外,我觉得对于 WS 我们开发人员起码还要了解它的握手过程和协议帧的意义,这就像学习 TCP 的时候需要了解 TCP 头每个字节帧对应的意义一样。

像握手过程我就不说了,因为它复用了 HTTP 头只需要在维基百科(阮一峰的文章讲的也很明白)上面看一下就明白了,像协议帧的话无非就是:标识符、操作符、数据、数据长度这些协议通用帧,基本都没有深入了解的必要,我认为一般只需要关心 WS 的操作符就可以了。

WS 的操作符代表了 WS 的消息类型,它的消息类型主要有如下六种:

  1. 文本消息
  2. 二进制消息
  3. 分片消息(分片消息代表此消息是一个某个消息中的一部分,想想大文件分片)
  4. 连接关闭消息
  5. PING 消息
  6. PONG 消息(PING的回复就是PONG)

那我们既然知道了 WS 主要有以上六种操作,那么一个正常的 WS 框架应当可以很轻松的处理以上这几种消息,所以接下来就是本文的中心内容,看看以下这几种 WS 框架能不能很方便的处理这几种 WS 消息。

J2EE 方式

先来 J2EE,一般我把 javax 包里面对 JavaWeb 的扩展都叫做 J2EE,这个定义是否完全正确我觉得没必要深究,只是一种个人习惯,而本章节所介绍的 J2EE 方式则是指 Tomcat 为 WS 所做的支持,这套代码的包名前缀叫做:javax.websocket

这套代码中定义了一套适用于 WS 开发的注解和相关支持,我们可以利用它和 Tomcat 进行WS 开发,由于现在更多的都是使用 SpringBoot 的内嵌容器了,所以这次我们就来按照 SpringBoot 内嵌容器的方式来演示。

首先是引入 SpringBoot - Web 的依赖,因为这个依赖中引入了内嵌式容器 Tomcat:

        <dependency>             <groupId>org.springframework.boot</groupId>             <artifactId>spring-boot-starter-web</artifactId>         </dependency>

接着就是将一个类定义为 WS 服务器,这一步也很简单,只需要为这个类加上@ServerEndpoint注解就可以了,在这个注解中比较常用的有三个参数:WS路径、序列化处理类、反序列化处理类。

@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) public @interface ServerEndpoint {     String value();      String[] subprotocols() default {};      Class<? extends Decoder>[] decoders() default {};      Class<? extends Encoder>[] encoders() default {};      Class<? extends Configurator> configurator() default Configurator.class; }

接下来我们来看具体的一个 WS 服务器类示例:

@Component @ServerEndpoint("/j2ee-ws/{msg}") public class WebSocketServer {      //建立连接成功调用     @OnOpen     public void onOpen(Session session, @PathParam(value = "msg") String msg){         System.out.println("WebSocketServer 收到连接: " + session.getId() + ", 当前消息:" + msg);     }      //收到客户端信息     @OnMessage     public void onMessage(Session session, String message) throws IOException {         message = "WebSocketServer 收到连接:" + session.getId() +  ",已收到消息:" + message;         System.out.println(message);         session.getBasicRemote().sendText(message);     }      //连接关闭     @OnClose     public void onclose(Session session){         System.out.println("连接关闭");     }  }

在以上代码中,我们着重关心 WS 相关的注解,主要有以下四个:

  1. @ServerEndpoint : 这里就像 RequestMapping 一样,放入一个 WS 服务器监听的 URL。
  2. @OnOpen :这个注解修饰的方法会在 WS 连接开始时执行。
  3. @OnClose :这个注解修饰的方法则会在 WS 关闭时执行。
  4. @OnMessage :这个注解则是修饰消息接受的方法,并且由于消息有文本和二进制两种方式,所以此方法参数上可以使用 String 或者二进制数组的方式,就像下面这样:

     @OnMessage  public void onMessage(Session session, String message) throws IOException {   }   @OnMessage  public void onMessage(Session session, byte[] message) throws IOException {   }

    除了以上这几个以外,常用的功能方面还差一个分片消息、Ping 消息 和 Pong 消息,对于这三个功能我并没有查到相关用法,只在源码的接口列表中看到了一个 PongMessage 接口,有知道的读者朋友们有知道的可以在评论区指出。
    细心的小伙伴们可能发现了,示例中的 WebSocketServer 类还有一个 @Component 注解,这是由于我们使用的是内嵌容器,而内嵌容器需要被 Spring 管理并初始化,所以需要给 WebSocketServer 类加上这么一个注解,所以代码中还需要有这么一个配置:

    @Configuration public class WebSocketConfig {   @Bean  public ServerEndpointExporter serverEndpointExporter() {      return new ServerEndpointExporter();  } }

    Tips:在不使用内嵌容器的时候可以不做以上步骤。
    最后上个简陋的 WS 效果示例图,前端方面直接使用 HTML5 的 WebScoket 标准库,具体可以查看我的仓库代码:
    image.png

    Spring 方式

    第二部分来说 Spring 方式,Spring 作为 Java 开发界的老大哥,几乎封装了一切可以封装的,对于 WS 开发呢 Spring 也提供了一套相关支持,而且从使用方面我觉得要比 J2EE 的更易用。

    使用它的第一步我们先引入 SpringBoot - WS 依赖,这个依赖包也会隐式依赖 SpringBoot - Web 包:

         <dependency>          <groupId>org.springframework.boot</groupId>          <artifactId>spring-boot-starter-websocket</artifactId>      </dependency>

    第二步就是准备一个用来处理 WS 请求的 Handle了,Spring 为此提供了一个接口—— WebSocketHandler,我们可以通过实现此接口重写其接口方法的方式自定义逻辑,我们来看一个例子:

    @Component public class SpringSocketHandle implements WebSocketHandler {   @Override  public void afterConnectionEstablished(WebSocketSession session) throws Exception {      System.out.println("SpringSocketHandle, 收到新的连接: " + session.getId());  }   @Override  public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {      String msg = "SpringSocketHandle, 连接:" + session.getId() +  ",已收到消息。";      System.out.println(msg);      session.sendMessage(new TextMessage(msg));  }   @Override  public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {      System.out.println("WS 连接发生错误");  }   @Override  public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {      System.out.println("WS 关闭连接");  }   // 支持分片消息  @Override  public boolean supportsPartialMessages() {      return false;  } }

    上面这个例子很好的展示了 WebSocketHandler 接口中的五个函数,通过名字我们就应该知道它具有什么功能了:

  5. afterConnectionEstablished:连接成功后调用。
  6. handleMessage:处理发送来的消息。
  7. handleTransportError:WS 连接出错时调用。
  8. afterConnectionClosed:连接关闭后调用。
  9. supportsPartialMessages:是否支持分片消息。

以上这几个方法重点可以来看一下 handleMessage 方法,handleMessage 方法中有一个 WebSocketMessage 参数,这也是一个接口,我们一般不直接使用这个接口而是使用它的实现类,它有以下几个实现类:

  1. BinaryMessage:二进制消息体
  2. TextMessage:文本消息体
  3. PingMessage:Ping 消息体
  4. PongMessage:Pong 消息体

但是由于 handleMessage 这个方法参数是WebSocketMessage,所以我们实际使用中可能需要判断一下当前来的消息具体是它的哪个子类,比如这样:

    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {         if (message instanceof TextMessage) {             this.handleTextMessage(session, (TextMessage)message);         } else if (message instanceof BinaryMessage) {             this.handleBinaryMessage(session, (BinaryMessage)message);         }     }

但是总这样写也不是个事,为了避免这些重复性代码,Spring 给我们定义了一个 AbstractWebSocketHandler,它已经封装了这些重复劳动,我们可以直接继承这个类然后重写我们想要处理的消息类型:

    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {     }      protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception {     }      protected void handlePongMessage(WebSocketSession session, PongMessage message) throws Exception {     }

上面这部分都是对于 Handle 的操作,有了 Handle 之后我们还需要将它绑定在某个 URL 上,或者说监听某个 URL,那么必不可少的需要以下配置:

@Configuration @EnableWebSocket public class SpringSocketConfig implements WebSocketConfigurer {      @Autowired     private SpringSocketHandle springSocketHandle;      @Override     public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {         registry.addHandler(springSocketHandle, "/spring-ws").setAllowedOrigins("*");     } }

这里我把我的自定义 Handle 注册到 "/spring-ws" 上面并设置了一下跨域,在整个配置类上还要打上@EnableWebSocket 注解,用于开启 WS 监听。

Spring 的方式也就以上这些内容了,不知道大家是否感觉 Spring 所提供的 WS 封装要比 J2EE 的更方便也更全面一些,起码我只要看 WebSocketHandler 接口就能知道所有常用功能的用法,所以对于 WS 开发来说我是比较推荐 Spring 方式的。

最后上个简陋的 WS 效果示例图,前端方面直接使用 HTML5 的 WebScoket 标准库,具体可以查看我的仓库代码:
image.png

SocketIO 方式

SocketIO 方式和上面两种有点不太一样,因为 SocketIO 诞生初就是为了兼容性作为考量的,前端的读者们应该对它更熟悉,因为它是一个 JS 库,我们先来看一下维基百科对它的定义:

Socket.IO 是一个面向实时 web 应用的 JavaScript 库。它使得服务器和客户端之间实时双向的通信成为可能。他有两个部分:在浏览器中运行的客户端库,和一个面向Node.js的服务端库,两者有着几乎一样的API。
Socket.IO 主要使用WebSocket协议。但是如果需要的话,Socket.io可以回退到几种其它方法,例如Adobe Flash Sockets,JSONP拉取,或是传统的AJAX拉取,并且在同时提供完全相同的接口。

所以我觉得使用它更多是因为兼容性,因为 HTML5 之后原生的 WS 应该也够用了,然而它是一个前端库,所以 Java 语言这块并没有官方支持,好在民间大神已经以 Netty 为基础开发了能与它对接的 Java 库: netty-socketio

不过我要先给大家提个醒,不再建议使用它了,不是因为它很久没更新了,而是因为它支持的 Socket-Client 版本太老了,截止到 2022-04-29 日,SocketIO 已经更新到 4.X 了,但是 NettySocketIO 还只支持 2.X 的 Socket-Client 版本。

说了这么多,该教大家如何使用它了,第一步还是引入最新的依赖:

        <dependency>             <groupId>com.corundumstudio.socketio</groupId>             <artifactId>netty-socketio</artifactId>             <version>1.7.19</version>         </dependency>

第二步就是配置一个 WS 服务:

@Configuration public class SocketIoConfig {      @Bean     public SocketIOServer socketIOServer() {         com.corundumstudio.socketio.Configuration config = new com.corundumstudio.socketio.Configuration();          config.setHostname("127.0.0.1");         config.setPort(8001);         config.setContext("/socketio-ws");         SocketIOServer server = new SocketIOServer(config);         server.start();         return server;     }      @Bean     public SpringAnnotationScanner springAnnotationScanner() {         return new SpringAnnotationScanner(socketIOServer());     } }

大家在上文的配置中,可以看到设置了一些 Web 服务器参数,比如:端口号和监听的 path,并将这个服务启动起来,服务启动之后日志上会打印这样一句日志:

[ntLoopGroup-2-1] c.c.socketio.SocketIOServer : SocketIO server started at port: 8001

这就代表启动成功了,接下来就是要对 WS 消息做一些处理了:

@Component public class SocketIoHandle {      /**      * 客户端连上socket服务器时执行此事件      * @param client      */     @OnConnect     public void onConnect(SocketIOClient client) {         System.out.println("SocketIoHandle 收到连接:" + client.getSessionId());     }      /**      * 客户端断开socket服务器时执行此事件      * @param client      */     @OnDisconnect     public void onDisconnect(SocketIOClient client) {         System.out.println("当前链接关闭:" + client.getSessionId());     }      @OnEvent( value = "onMsg")     public void onMessage(SocketIOClient client, AckRequest request, Object data) {         System.out.println("SocketIoHandle 收到消息:" + data);         request.isAckRequested();         client.sendEvent("chatMsg", "我是 NettySocketIO 后端服务,已收到连接:" + client.getSessionId());     } }

我相信对于以上代码,前两个方法是很好懂的,但是对于第三个方法如果大家没有接触过 SocketIO 就比较难理解了,为什么@OnEvent( value = "onMsg")里面这个值是自定义的,这就涉及到 SocketIO 里面发消息的机制了,通过 SocketIO 发消息是要发给某个事件的,所以这里的第三个方法就是监听 发给onMsg事件的所有消息,监听到之后我又给客户端发了一条消息,这次发给的事件是:chatMsg,客户端也需要监听此事件才能接收到这条消息。

最后再上一个简陋的效果图:
image.png
由于前端代码不再是标准的 HTML5 的连接方式,所以我这里简要贴一下相关代码,具体更多内容可以看我的代码仓库:

    function changeSocketStatus() {         let element = document.getElementById("socketStatus");         if (socketStatus) {             element.textContent = "关闭WebSocket";             const socketUrl="ws://127.0.0.1:8001";             socket = io.connect(socketUrl, {                 transports: ['websocket'],                 path: "/socketio-ws"             });             //打开事件             socket.on('connect', () => {                 console.log("websocket已打开");             });             //获得消息事件             socket.on('chatMsg', (msg) => {                 const serverMsg = "收到服务端信息:" + msg;                 pushContent(serverMsg, 2);             });             //关闭事件             socket.on('disconnect', () => {                 console.log("websocket已关闭");             });             //发生了错误事件             socket.on('connect_error', () => {                 console.log("websocket发生了错误");             })         }     }

第四种方式?

第四种方式其实就是 Netty 了,Netty 作为 Java 界大名鼎鼎的开发组件,对于常见协议也全部进行了封装,所以我们可以直接在 Netty 中去很方便的使用 WebSocket,接下来我们可以看看 Netty 怎么作为 WS 的服务器进行开发。

注意:以下内容如果没有 Netty 基础可能一脸蒙的进,一脸蒙的出,不过还是建议大家看看,Netty 其实很简单。

第一步需要先引入一个 Netty 开发包,我这里为了方便一般都是 All In:

        <dependency>             <groupId>io.netty</groupId>             <artifactId>netty-all</artifactId>             <version>4.1.75.Final</version>         </dependency>

第二步的话就需要启动一个 Netty 容器了,配置很多,但是比较关键的也就那几个:

public class WebSocketNettServer {     public static void main(String[] args) {          NioEventLoopGroup boss = new NioEventLoopGroup(1);         NioEventLoopGroup work = new NioEventLoopGroup();          try {             ServerBootstrap bootstrap = new ServerBootstrap();             bootstrap                     .group(boss, work)                     .channel(NioServerSocketChannel.class)                     //设置保持活动连接状态                     .childOption(ChannelOption.SO_KEEPALIVE, true)                     .localAddress(8080)                     .handler(new LoggingHandler(LogLevel.DEBUG))                     .childHandler(new ChannelInitializer<SocketChannel>() {                         @Override                         protected void initChannel(SocketChannel ch) throws Exception {                             ch.pipeline()                                     // HTTP 请求解码和响应编码                                     .addLast(new HttpServerCodec())                                     // HTTP 压缩支持                                     .addLast(new HttpContentCompressor())                                     // HTTP 对象聚合完整对象                                     .addLast(new HttpObjectAggregator(65536))                                     // WebSocket支持                                     .addLast(new WebSocketServerProtocolHandler("/ws"))                                     .addLast(WsTextInBoundHandle.INSTANCE);                         }                     });              //绑定端口号,启动服务端             ChannelFuture channelFuture = bootstrap.bind().sync();             System.out.println("WebSocketNettServer启动成功");              //对关闭通道进行监听             channelFuture.channel().closeFuture().sync();          } catch (InterruptedException e) {             e.printStackTrace();         } finally {             boss.shutdownGracefully().syncUninterruptibly();             work.shutdownGracefully().syncUninterruptibly();         }      } }

以上代码我们主要关心端口号和重写的 ChannelInitializer 就行了,里面我们定义了五个过滤器(Netty 使用责任链模式),前面三个都是 HTTP 请求的常用过滤器(毕竟 WS 握手是使用 HTTP 头的所以也要配置 HTTP 支持),第四个则是 WS 的支持,它会拦截 /ws 路径,最关键的就是第五个了过滤器它是我们具体的业务逻辑处理类,效果基本和 Spring 那部门中的 Handle 差不多,我们来看看代码:

@ChannelHandler.Sharable public class WsTextInBoundHandle extends SimpleChannelInboundHandler<TextWebSocketFrame> {      private WsTextInBoundHandle() {         super();         System.out.println("初始化 WsTextInBoundHandle");     }      @Override     public void channelActive(ChannelHandlerContext ctx) throws Exception {         System.out.println("WsTextInBoundHandle 收到了连接");     }      @Override     protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {          String str = "WsTextInBoundHandle 收到了一条消息, 内容为:" + msg.text();          System.out.println(str);          System.out.println("-----------WsTextInBoundHandle 处理业务逻辑-----------");          String responseStr = "{\"status\":200, \"content\":\"收到\"}";          ctx.channel().writeAndFlush(new TextWebSocketFrame(responseStr));         System.out.println("-----------WsTextInBoundHandle 数据回复完毕-----------");     }      @Override     public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {          System.out.println("WsTextInBoundHandle 消息收到完毕");         ctx.flush();     }      @Override     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {         System.out.println("WsTextInBoundHandle 连接逻辑中发生了异常");         cause.printStackTrace();         ctx.close();     } }

这里面的方法我都不说了,看名字就差不多知道了,主要是看一下这个类的泛型:TextWebSocketFrame,很明显这是一个 WS 文本消息的类,我们顺着它的定义去看发现它继承了 WebSocketFrame,接着我们去看它的子类:

image.png

一图胜千言,我想不用多说大家也都知道具体的类是处理什么消息了把,在上文的示例中我们是一定了一个文本 WS 消息的处理类,如果你想处理其他数据类型的消息,可以将泛型中的 TextWebSocketFrame 换成其他 WebSocketFrame 类就可以了
至于为什么没有连接成功后的处理,这个是和 Netty 的相关机制有关,可以在 channelActive 方法中处理,大家有兴趣的可以了解一下 Netty。

最后上个简陋的 WS 效果示例图,前端方面直接使用 HTML5 的 WebScoket 标准库,具体可以查看我的仓库代码:
image.png

总结

洋洋洒洒五千字,有了收获别忘赞。

在上文中,我总共介绍了四种在 Java 中使用 WS 的方式,从我个人使用意向来说我感觉应该是这样的:Spring 方式 > Netty 方式 > J2EE 方式 > SocketIO 方式,当然了,如果你的业务存在浏览器兼容性问题,其实只有一种选择:SocketIO。

最后,我估计某些读者会去具体拉代码看代码,所以我简单说一下代码结构:

├─java │  └─com │      └─example │          └─springwebsocket │              │  SpringWebsocketApplication.java │              │  TestController.java │              │ │              ├─j2ee │              │      WebSocketConfig.java │              │      WebSocketServer.java │              │ │              ├─socketio │              │      SocketIoConfig.java │              │      SocketIoHandle.java │              │ │              └─spring │                      SpringSocketConfig.java │                      SpringSocketHandle.java │ └─resources     └─templates             J2eeIndex.html             SocketIoIndex.html             SpringIndex.html

代码结构如上所示,应用代码分成了三个文件夹,分别放着三种方式的具体示例代码,在资源文件夹下的 templates 文件夹也有三个 HTML 文件,就是对应三种示例的 HTML 页面,里面的链接地址和端口我都预设好了,拉下来直接单独编译此模块运行即可。

我没有往里面放 Netty 的代码,是因为感觉 Netty 部分内容很少,文章示例中的代码直接复制就能用,后面如果写 Netty 的话会再开一个 Netty 模块用来放 Netty 相关的代码。

好了,今天的内容就到这了,希望对大家有帮助的话可以帮我文章点点赞,GitHub 也点点赞,大家的点赞与评论都是我更新的不懈动力,下期见。

No comments:

Post a Comment