首页

巧用伪元素before和after制作绚丽效果

seo达人

CSS :before 选择器

定义和说明

:before 选择器向选定的元素前插入内容。

使用content 属性来指定要插入的内容。


CSS :after 选择器

定义和说明

:after 选择器向选定的元素之后插入内容。

使用content 属性来指定要插入的内容。


这两个伪元素会在真正页面元素之前和之后插入一个额外的元素,从技术角度上讲,它们与下面的HTML标记是等效的。


1.伪类光圈



<div class="hover-circle">CSS</div>

.hover-circle {

 width: 100%;

 display: flex;

 align-items: center;

 justify-content: center;

 height: 100%;

 font-size: 3rem;

 letter-spacing: 0.3rem;

 font-weight: bold;

 position: relative;

 cursor: pointer;

 color: #666;

}


.hover-circle::before {

 width: 8.5rem;

 height: 8.5rem;

 border: 3px solid pink;

 content: "";

 border-radius: 50%;

 position: absolute;

 opacity: 0;

}


.hover-circle::after {

 width: 7.2rem;

 height: 7.2rem;

 border: 6px solid pink;

 content: "";

 border-radius: 50%;

 position: absolute;

 opacity: 0;

}


.hover-circle:hover::before,

.hover-circle:hover::after {

 animation-duration: 0.8s;

 animation-delay: 0.2s;

 animation: circle 0.8s;

}


@keyframes circle {

 0% {

   opacity: 0;

   scale: 1;

 }


 25% {

   opacity: 0.25;

 }


 50% {

   opacity: 0.5;

   scale: 1.03;

 }


 75% {

   opacity: 0.75;

 }


 100% {

   opacity: 1;

   scale: 1.03;

 }

}

2.伪类括号效果



<div class="hover-text">CSS</div>

.hover-text {

 width: 100%;

 display: flex;

 align-items: center;

 justify-content: center;

 height: 100%;

 font-size: 3rem;

 letter-spacing: 0.3rem;

 font-weight: bold;

 position: relative;

 cursor: pointer;

 color: #666;

}


.hover-text::before {

 content: "[";

 position: absolute;

 left: 0.8rem;

 opacity: 0;

 color: #999;

}


.hover-text::after {

 content: "]";

 position: absolute;

 right: 0.8rem;

 opacity: 0;

 color: #999;

}


.hover-text:hover::before {

 animation-duration: 0.8s;

 animation-delay: 0.2s;

 animation: hovertext1 0.8s;

}


.hover-text:hover::after {

 animation-duration: 0.8s;

 animation-delay: 0.2s;

 animation: hovertext2 0.8s;

}


@keyframes hovertext1 {

 0% {

   opacity: 0;

   left: 0.8rem;

 }


 100% {

   opacity: 1;

   left: 0.5rem;

 }

}


@keyframes hovertext2 {

 0% {

   opacity: 0;

   right: 0.8rem;

 }


 100% {

   opacity: 1;

   right: 0.5rem;

 }

}

3.炫酷丝带效果

双边丝带



<div class="tc">

   <div class="title1"><span>距离结束还有10天</span></div>

</div>

.title1 {

 position: relative;

 display: inline-block;

}


.title1 span {

 position: relative;

 z-index: 2;

 display: inline-block;

 padding: 0 15px;

 height: 32px;

 line-height: 32px;

 background-color: #dc5947;

 color: #fff;

 font-size: 16px;

 box-shadow: 0 10px 6px -9px rgba(0, 0, 0, 0.6);

}


.title1 span::before,

.title1 span::after {

 position: absolute;

 bottom: -6px;

 border-width: 3px 5px;

 border-style: solid;

 content: "";

}


.title1 span::before {

 left: 0;

 border-color: #972f22 #972f22 transparent transparent;

}


.title1 span::after {

 right: 0;

 border-color: #972f22 transparent transparent #972f22;

}


.title1::before,

.title1::after {

 position: absolute;

 top: 6px;

 content: "";

 border-style: solid;

 border-color: #dc5947;

}


.title1::before {

 left: -32px;

 border-width: 16px 26px 16px 16px;

 border-left-color: transparent;

}


.title1::after {

 right: -32px;

 border-width: 16px 16px 16px 26px;

 border-right-color: transparent;

}

右边丝带



<span class="title2">距离结束还有10天</span>

.title2 {

 position: relative;

 display: inline-block;

 padding: 0 15px;

 height: 32px;

 line-height: 32px;

 background-color: #dc5947;

 color: #fff;

 font-size: 16px;

}


.title2::before {

 position: absolute;

 top: -4px;

 left: 0;

 border-width: 2px 4px;

 border-style: solid;

 border-color: transparent #972f22 #972f22 transparent;

 content: "";

}


.title2::after {

 position: absolute;

 top: 0;

 right: -8px;

 border-width: 16px 8px 16px 0;

 border-style: solid;

 border-color: #dc5947 transparent #dc5947 #dc5947;

 content: "";

}

箭头丝带



<span class="title3">距离结束还有10天</span>

.title3 {

 position: relative;

 display: inline-block;

 margin-right: 16px;

 padding: 0 10px;

 height: 32px;

 line-height: 32px;

 background-color: #dc5947;

 color: #fff;

 font-size: 16px;

}


.title3::before {

 position: absolute;

 top: 0;

 left: -16px;

 border-width: 16px 16px 16px 0;

 border-style: solid;

 border-color: transparent #dc5947 transparent transparent;

 content: "";

}


.title3::after {

 position: absolute;

 top: 0;

 right: -16px;

 border-width: 16px 16px 16px 0;

 border-style: solid;

 border-color: #dc5947 transparent #dc5947 #dc5947;

 content: "";

}

多个箭头丝带



<div class="mt30 pl16">

   <span class="title3">距离结束还有10天</span>

   <span class="title3 ml5">距离结束还有10天</span>

   <span class="title3 ml5">距离结束还有10天</span>

</div>

.title4 {

 width: 200px;

 height: 140px;

 position: absolute;

 top: -8px;

 left: -8px;

 overflow: hidden;

}


.title4::before {

 position: absolute;

 left: 124px;

 border-radius: 8px 8px 0 0;

 width: 16px;

 height: 8px;

 background-color: #972f22;

 content: "";

}


.title4::after {

 position: absolute;

 left: 0;

 top: 124px;

 border-radius: 0 8px 8px 0;

 width: 8px;

 height: 16px;

 background-color: #972f22;

 content: "";

}


.title4 span {

 display: inline-block;

 text-align: center;

 width: 200px;

 height: 40px;

 line-height: 40px;

 position: absolute;

 top: 30px;

 left: -50px;

 z-index: 2;

 overflow: hidden;

 -ms-transform: rotate(-45deg);

 -moz-transform: rotate(-45deg);

 -webkit-transform: rotate(-45deg);

 -o-transform: rotate(-45deg);

 transform: rotate(-45deg);

 border: 1px dashed #fff;

 box-shadow: 0 0 0 3px #dc5947, 0 14px 7px -9px rgba(0, 0, 0, 0.6);

 background-color: #dc5947;

 color: #fff;

}

悬挂标签



<div class="pr mt30" style="background-color: #eee; height: 200px;">

   <div class="title4"><span>企业热门动态</span></div>

   <div class="title5"><span>企业热门动态</span></div>

</div>

.title5 {

 width: 140px;

 height: 200px;

 position: absolute;

 top: -8px;

 right: -8px;

 overflow: hidden;

}


.title5::before {

 position: absolute;

 right: 124px;

 border-radius: 8px 8px 0 0;

 width: 16px;

 height: 8px;

 background-color: #972f22;

 content: "";

}


.title5::after {

 position: absolute;

 right: 0;

 top: 124px;

 border-radius: 0 8px 8px 0;

 width: 8px;

 height: 16px;

 background-color: #972f22;

 content: "";

}


.title5 span {

 display: inline-block;

 text-align: center;

 width: 200px;

 height: 40px;

 line-height: 40px;

 position: absolute;

 top: 30px;

 right: -50px;

 z-index: 2;

 overflow: hidden;

 -ms-transform: rotate(45deg);

 -moz-transform: rotate(45deg);

 -webkit-transform: rotate(45deg);

 -o-transform: rotate(45deg);

 transform: rotate(45deg);

 border: 1px dashed #fff;

 box-shadow: 0 0 0 3px #dc5947, 0 14px 7px -9px rgba(0, 0, 0, 0.6);

 background-color: #dc5947;

 color: #fff;

}

4.几何图形

三角形



<div class="triangle"></div>

.triangle {

 width: 0;

 height: 0;

 margin: 50px auto;

 border-bottom: 100px solid #dc5947;

 border-left: 50px solid transparent;

 border-right: 50px solid transparent;

 cursor: pointer;

 transform: scale(1.2);

 transition: 0.5s;

}

五角星



<div class="pentagram"></div>

.pentagram {

 width: 0;

 height: 0;

 margin: 100px auto;

 position: relative;

 border-bottom: 70px solid #dc5947;

 border-left: 100px solid transparent;

 border-right: 100px solid transparent;

 -webkit-transform: rotate(35deg);

 -moz-transform: rotate(35deg);

 -ms-transform: rotate(35deg);

 -o-transform: rotate(35deg);

 transform: rotate(35deg);

 -webkit-transform: scale(1), rotate(35deg);

 -moz-transform: scale(1), rotate(35deg);

 -ms-transform: scale(1), rotate(35deg);

 -o-transform: scale(1), rotate(35deg);

 transform: scale(1), rotate(35deg);

}


.pentagram::after {

 content: "";

 width: 0;

 height: 0;

 border-bottom: 70px solid #dc5947;

 border-left: 100px solid transparent;

 border-right: 100px solid transparent;

 -webkit-transform: rotate(-70deg);

 -moz-transform: rotate(-70deg);

 -ms-transform: rotate(-70deg);

 -o-transform: rotate(-70deg);

 transform: rotate(-70deg);

 position: absolute;

 top: 0px;

 left: -100px;

}


.pentagram::before {

 content: "";

 width: 0;

 height: 0;

 border-bottom: 80px solid #dc5947;

 border-left: 30px solid transparent;

 border-right: 30px solid transparent;

 -webkit-transform: rotate(-35deg);

 -moz-transform: rotate(-35deg);

 -ms-transform: rotate(-35deg);

 -o-transform: rotate(-35deg);

 transform: rotate(-35deg);

 position: absolute;

 top: -45px;

 left: -60px;

}

5.水滴



<div class="drop"></div>

.drop::after {

 content: "";

 position: absolute;

 width: 30px;

 height: 20px;

 border-radius: 50%;

 background-color: #ace3ff;

 margin: 100px auto;

 top: -50px;

 left: 25px;

 box-shadow: 5px 12px 4px #ace3ff, -5px 11px 4px #ace3ff, 0px 14px 4px #4d576e;

 -webkit-transform: rotate(35deg);

}


.drop::before {

 content: "";

 position: absolute;

 width: 0px;

 height: 0px;

 border-style: solid;

 border-width: 0 40px 50px 40px;

 border-color: transparent transparent #ace3ff transparent;

 top: -30px;

 left: 10px;

}


.drop {

 width: 100px;

 height: 100px;

 border-radius: 50%;

 background-color: #ace3ff;

 position: relative;

 margin: 100px auto;

 box-shadow: 0px 6px 0 #3f475a;

}

6 绚丽流动边框





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

.box-line2,

.box-line2::before,

.box-line2::after {

 position: absolute;

 top: 0;

 bottom: 0;

 left: 0;

 right: 0;

}


.box-line2 {

 width: 200px;

 height: 200px;

 margin: auto;

 color: #69ca62;

 box-shadow: inset 0 0 0 1px rgba(105, 202, 98, 0.5);

}


.box-line2::before,

.box-line2::after {

 content: "";

 z-index: 99;

 margin: -5%;

 box-shadow: inset 0 0 0 2px;

 animation: clipMe 8s linear infinite;

}


.box-line2::before {

 animation-delay: -4s;

}


.box-line2:hover::after,

.box-line2:hover::before {

 background-color: rgba(255, 0, 0, 0.3);

}


@keyframes clipMe {


 0%,

 100% {

   clip: rect(0px, 220px, 2px, 0px);

 }


 25% {

   clip: rect(0px, 2px, 220px, 0px);

 }


 50% {

   clip: rect(218px, 220px, 220px, 0px);

 }


 75% {

   clip: rect(0px, 220px, 220px, 218px);

 }

}


@keyframes surround {


 0%,

 100% {

   clip: rect(0px, 220px, 2px, 0px);

 }


 25% {

   clip: rect(0px, 2px, 220px, 0px);

 }


 50% {

   clip: rect(218px, 220px, 220px, 0px);

 }


 75% {

   clip: rect(0px, 220px, 220px, 218px);

 }

}


.box-line1:before,

.box-line1:after {

 position: absolute;

 top: 0;

 left: 0;

 bottom: 0;

 right: 0;

 content: "";

 z-index: 99;

 margin: -5%;

 animation: surround linear infinite 8s;

 box-shadow: inset 0 0 0 2px #69ca62;

}


.box-line1:before {

 animation-delay: -4s;

}


.box-line1 {

 border: 1px solid #69ca62;

 position: absolute;

 left: 500px;

 top: 200px;

 margin: auto;

 width: 200px;

 height: 200px;

 margin: auto;

}

7.Tooltip提示



<div class="tip" data-tip="CSS伪类">CSS伪类</div>

.tip::after {

 content: attr(data-tip);

 display: none;

 position: absolute;

 padding: 5px 10px;

 left: 15%;

 bottom: 100%;

 width: 150px;

 margin-bottom: 12px;

 transform: translateX(-50%);

 font-size: 12px;

 background: #000;

 color: #fff;

 cursor: default;

 border-radius: 4px;

}


.tip::before {

 content: " ";

 position: absolute;

 display: none;

 left: 15%;

 bottom: 100%;

 transform: translateX(-50%);

 margin-bottom: 3px;

 width: 0;

 height: 0;

 border-left: 6px solid transparent;

 border-right: 6px solid transparent;

 border-top: 9px solid #000;

}


.tip:hover::after,

.tip:hover::before {

 display: block;

}

8.CSS 伪类盒子阴影

使用伪元素:before and :after制作出了完美惊艳的相片阴影效果。其中的技巧是使用绝对定位固定伪元素,然后给它们的z-index一个负值,以背景出现。






<div class="box effect2">

   <h3>CSS 伪类盒子阴影</h3>

</div>

.effect2 {

   position: relative;

}


.effect2::before, .effect2::after {

   z-index: -1;

   position: absolute;

   content: "";

   bottom: 15px;

   left: 10px;

   width: 50%;

   top: 80%;

   max-width: 300px;

   background: #777;

   -webkit-box-shadow: 0 15px 10px #777;

   -moz-box-shadow: 0 15px 10px #777;

   box-shadow: 0 15px 10px #777;

   -webkit-transform: rotate(-3deg);

   -moz-transform: rotate(-3deg);

   -o-transform: rotate(-3deg);

   -ms-transform: rotate(-3deg);

   transform: rotate(-3deg);

}

.effect2::after {

   -webkit-transform: rotate(3deg);

   -moz-transform: rotate(3deg);

   -o-transform: rotate(3deg);

   -ms-transform: rotate(3deg);

   transform: rotate(3deg);

   right: 10px;

   left: auto;

}

CSS Box 阴影效果


9.Tabs当前激活状态



   <div class="sm-box flex">

       <div class="menu-tabs active">首页</div>

       <div class="menu-tabs">新闻</div>

       <div class="menu-tabs">视频</div>

       <div class="menu-tabs">图片</div>

   </div>

.menu-tabs {

 display: block;

 padding: 0.25rem 1.5rem;

 clear: both;

 font-weight: 400;

 color: #212529;

 text-align: inherit;

 white-space: nowrap;

 background-color: transparent;

 width: 50px;

 border: 0;

 height: 35px;

 justify-content: center;

 display: flex;

 cursor: pointer;

}


.menu-tabs:hover {

 color: #20a884;

 position: relative;

}


.menu-tabs:hover:after {

 position: absolute;

 content: "";

 border: 1px solid #20a884;

 width: 3rem;

 left: 0;

 bottom: 0;

 margin-left: 50%;

 transform: translateX(-50%);

}


.active {

 position: relative;

 color: #20a884;

}


.flex {

 display: flex;

}


.active::after {

 position: absolute;

 content: "";

 border: 1px solid #20a884;

 width: 3rem;

 left: 0;

 bottom: 0;

 margin-left: 50%;

 transform: translateX(-50%);

}

10.伪元素模糊背景



<div class="container">

  <div class="overlay">

     <h1>A blurred overlay</h1>

    <p>... mask or whatever

    <br>that is responsive and could be cross-browser compatible back to IE9</p>

  </div>

</div>

.container {

 width: 100%;

 height: 100%;

 margin: 0;

}


.container,

.overlay:before {

 background: url(https://wow.techbrood.com/assets/landing.jpg) no-repeat fixed 0 0 / cover;

}


.container {

 -webkit-box-align: center;

 -webkit-align-items: center;

 -ms-flex-align: center;

 align-items: center;

 display: -webkit-box;

 display: -webkit-flex;

 display: -ms-flexbox;

 display: flex;

 -webkit-box-pack: center;

 -webkit-justify-content: center;

 -ms-flex-pack: center;

 justify-content: center;

}


.overlay {

 max-height: 200px;

 margin: 0 auto;

 max-width: 768px;

 padding: 50px;

 position: relative;

 color: white;

 font-family: "Lato";

 position: relative;

 text-align: center;

 z-index: 0;

}


.overlay:before {

 content: "";

 -webkit-filter: blur(100px);

 filter: blur(100px);

 height: 100%;

 left: 0;

 position: absolute;

 top: 0;

 width: 100%;

 z-index: -1;

}

11.蓝湖文字



<span class="lanhu_text">

    本站由叫我詹躲躲提供技术支持

</span>

.lanhu_text {

 position: relative;

 color: #2878ff;

}


.lanhu_text::before {

 content: "";

 width: 80px;

 height: 20px;

 position: absolute;

 left: -86px;

 top: 0;

 background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAABCAYAAABJwyn/AAAAjElEQVQoU22NSw7CQAxDX8ahICF2HIDTcf9d1c8kaDpthQSL6CmxHRuk8cZfMxqf6DGh+Y5uCxquaB7xdUCXilZHWvBorEiOaqAKrkRRUEmUOw283TKRRb9b4GnIEpWmGYrA237kDh1w6J5N7zzzZv13gtuvT7t++jefUTYmwvpk7v3fPaCzn//9LfsBvRpHnliu+xMAAAAASUVORK5CYII=) 0 no-repeat;

}


.lanhu_text::after {

 content: "";

 width: 80px;

 height: 20px;

 position: absolute;

 right: -86px;

 top: 0;

 background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAABCAYAAABJwyn/AAAAhElEQVQoU22OSw7CQAxDXzJDWbHiEFyRJQflFggJSjyDph9oC6snx45lO52rrKJqzIyNlkFUPv6vnv+3uW9vrPpBq77/uWFPAXnrnfYVQ16JiWq0stjvxKB9vDc/MeaXWo5cqDEFUkKpEuEoB3ollHvUZ9QV4rFD3R3d9ujwJK5HxAW9AStemAIOwCNlAAAAAElFTkSuQmCC) 100% no-repeat;

}

12 主要标题



<div class="first-title">服务项目</div>

.first-title {

 position: relative;

 color: #a98661;

 font-weight: 400;

 font-size: 30px;

 text-align: center;

}


.first-title::before,

.first-title::after {

 position: absolute;

 content: "";

 width: 110px;

 border-bottom: 1px solid #a98661;

 top: 50%;

 transform: translateY(-50%);

}


.first-title::before {

 left: 100px;

}


.first-title::after {

 right: 100px;

}

13.鼠标浮层遮罩浮层



<div class="black-mask"></div>

.black-mask {

 position: relative;

 height: 100%;

 width: 100%;

 cursor: pointer;

}


.black-mask:hover {

 transition-duration: 1s;

 scale: 1.02;

}


.black-mask:hover:before {

 object-fit: cover;

}


.black-mask:hover:after {

 height: 100%;

 opacity: 1;

 transition-duration: 1s;

 display: flex;

 align-items: flex-end;

 padding: 0 30px 15px;

}


.black-mask::before {

 position: absolute;

 content: "";

 background: url(https://dcdn.it120.cc/2019/11/14/f17c5848-6d1f-4254-b3ba-64d3969d16b6.jpg) no-repeat;

 background-size: 100% 100%;

 width: 100%;

 height: 100%;

}


.black-mask::after {

 position: absolute;

 content: "雾在微风的吹动下滚来滚去,像冰峰雪山,似蓬莱仙境,如海市蜃楼,使人觉得飘然欲仙。山河景色在雾的装点下,变得更加美丽。远处的七连山巍峨挺拔,它们仿佛成了神仙住的宝山,令人神往。近处池塘边时时飘来雾气,在初升阳光的照耀下,呈现出赤、橙、黄、绿、青、蓝、紫七种色彩。......";

 width: 90%;

 height: 0%;

 bottom: 0;

 right: 0;

 z-index: 32;

 background: rgba(0, 0, 0, 0.3);

 opacity: 1;

 color: #fff;

 opacity: 0;

 padding: 0 30px 0;

}

14.绚丽光圈



<div class="aperture">光圈</div>

.aperture {

 width: 136px;

 height: 136px;

 background-color: #dc5947;

 border-radius: 50%;

 line-height: 136px;

 text-align: center;

 color: #fff;

 font-size: 24px;

 cursor: pointer;

 position: relative;

}


.aperture::before {

 border: 3px dashed #a0ff80;

 content: "";

 width: 144px;

 height: 144px;

 position: absolute;

 border-radius: 50%;

 left: -8px;

 top: -6px;

 animation: clockwise 5s linear infinite;

}


@keyframes clockwise {

 100% {

   transform: rotate(360deg);

 }

}

15.彩色流动边框



<div class="rainbow"></div>

.rainbow {

 position: relative;

 z-index: 0;

 width: 400px;

 height: 300px;

 border-radius: 10px;

 overflow: hidden;

 padding: 2rem;

}


.rainbow::before {

 content: '';

 position: absolute;

 z-index: -2;

 left: -50%;

 top: -50%;

 width: 200%;

 height: 200%;

 background-color: #399953;

 background-repeat: no-repeat;

 background-size: 50% 50%, 50% 50%;

 background-position: 0 0, 100% 0, 100% 100%, 0 100%;

 background-image: linear-gradient(#399953, #399953), linear-gradient(#fbb300, #fbb300), linear-gradient(#d53e33, #d53e33), linear-gradient(#377af5, #377af5);

 -webkit-animation: rotate 4s linear infinite;

 animation: rotate 4s linear infinite;

}


.rainbow::after {

 content: '';

 position: absolute;

 z-index: -1;

 left: 6px;

 top: 6px;

 width: calc(100% - 12px);

 height: calc(100% - 12px);

 background: white;

 border-radius: 5px;

}


@keyframes rotate {

 100% {

   -webkit-transform: rotate(1turn);

   transform: rotate(1turn);

 }

}

16.炫酷伪类边框



<div class="corner-button">CSS3</div>

.corner-button::before, .corner-button::after {

 content: '';

 position: absolute;

 background: #2f2f2f;

 z-index: 1;

 transition: all 0.3s;

}

.corner-button::before {

 width: calc(100% - 3rem);

 height: calc(101% + 1rem);

 top: -0.5rem;

 left: 50%;

 -webkit-transform: translateX(-50%);

 transform: translateX(-50%);

}

.corner-button::after {

 height: calc(100% - 3rem);

 width: calc(101% + 1rem);

 left: -0.5rem;

 top: 50%;

 -webkit-transform: translateY(-50%);

 transform: translateY(-50%);

}



.corner-button:hover {

 color: pink;

}

.corner-button {

 font-family: 'Lato', sans-serif;

 letter-spacing: .02rem;

 cursor: pointer;

 background: transparent;

 border: 0.5rem solid currentColor;

 padding: 1.5rem 2rem;

 font-size: 2.2rem;

 color: #06c17f;

 position: relative;

 transition: color 0.3s;

 text-align: center;

 margin: 5rem 12rem;

}

.corner-button:hover::after {

 height: 0;

}


.corner-button:hover::before {

 width: 0;

}

.bg-f2{

 background: #2f2f2f;

}

17.伪类美化文字



<div class="beautify-font" data-text='躲躲'>躲躲</div>

<div class="beautify-font2" data-text='躲躲'>躲躲</div>

.beautify-font{

 position: relative;

 font-size: 12rem;

 color: #0099CC

}

.beautify-font::before{

 position: absolute;

 font-size: 12rem;

 color: #333;

 content: attr(data-text);

 white-space:nowrap;

 width: 50%;

 display: inline-block;

 overflow: hidden;

 transition:1s ease-in-out 0s;

}

.beautify-font2{

 position: relative;

 font-size: 6rem;

 color: #0099CC

}

.beautify-font2::before{

 position: absolute;

 font-size: 6rem;

 color: #333;

 content: attr(data-text);

 white-space:nowrap;

 height: 50%;

 display: inline-block;

 overflow: hidden;

 transition:1s ease-in-out 0s;

}


.beautify-font:hover::before{

 width:0;

}

.beautify-font2:hover::before{

 height: 0;

}

18.照片堆叠效果

只使用一张图片来创造出一堆图片叠摞在一起的效果,能做到吗?当然,关键是要使用伪元素:before和:after来帮助呈现。把这些伪元素的z-index设置成负值,让它们以背景方式起作用。




<div class="stackthree"><img src="./images/city.jpg"></div>

.stackthree::before {

 background: #eff4de;

}


.stackthree, .stackthree::before, .stackthree::after {

 border: 6px solid #fff;

 height: 200px;

 width: 200px;

 -webkit-box-shadow: 2px 2px 5px rgba(0,0,0,0.3);

 -moz-box-shadow: 2px 2px 5px rgba(0,0,0,0.3);

 box-shadow: 2px 2px 5px rgba(0,0,0,0.3);

}


.stackthree::before {

 top: 5px;

 left: -15px;

 z-index: -1;

 -webkit-transform: rotate(-10deg);

 -moz-transform: rotate(-10deg);

 -o-transform: rotate(-10deg);

 -ms-transform: rotate(-10deg);

 transform: rotate(-10deg);

}

.stackthree::after {

 top: -2px;

 left: -10px;

 -webkit-transform: rotate(-5deg);

 -moz-transform: rotate(-5deg);

 -o-transform: rotate(-5deg);

 -ms-transform: rotate(-5deg);

 transform: rotate(-5deg);

}


.stackthree::before, .stackthree::after {

 background: #768590;

 content: "";

 position: absolute;

 z-index: -1;

 height: 0px\9;

 width: 0px\9;

 border: none\9;

}

.stackthree {

 float: left;

 position: relative;

 margin: 50px;

}

为元素的兼容性

不论你使用单冒号还是双冒号语法,浏览器都能识别。因为IE8只支持单冒号的语法,所以,如果你想兼容IE8,保险的做法是使用单冒号。

蓝蓝设计www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务

你不知道的 WebSocket

seo达人

在最后的 阿宝哥有话说 环节,阿宝哥将介绍 WebSocket 与 HTTP 之间的关系、WebSocket 与长轮询有什么区别、什么是 WebSocket 心跳及 Socket 是什么等内容。


下面我们进入正题,为了让大家能够更好地理解和掌握 WebSocket 技术,我们先来介绍一下什么是 WebSocket。


一、什么是 WebSocket

1.1 WebSocket 诞生背景

早期,很多网站为了实现推送技术,所用的技术都是轮询。轮询是指由浏览器每隔一段时间向服务器发出 HTTP 请求,然后服务器返回的数据给客户端。常见的轮询方式分为轮询与长轮询,它们的区别如下图所示:




为了更加直观感受轮询与长轮询之间的区别,我们来看一下具体的代码:




这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而 HTTP 请求与响应可能会包含较长的头部,其中真正有效的数据可能只是很小的一部分,所以这样会消耗很多带宽资源。


比较新的轮询技术是 Comet)。这种技术虽然可以实现双向通信,但仍然需要反复发出请求。而且在 Comet 中普遍采用的 HTTP 长连接也会消耗服务器资源。


在这种情况下,HTML5 定义了 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。Websocket 使用 ws 或 wss 的统一资源标志符(URI),其中 wss 表示使用了 TLS 的 Websocket。如:


ws://echo.websocket.org

wss://echo.websocket.org

WebSocket 与 HTTP 和 HTTPS 使用相同的 TCP 端口,可以绕过大多数防火墙的限制。默认情况下,WebSocket 协议使用 80 端口;若运行在 TLS 之上时,默认使用 443 端口。


1.2 WebSocket 简介

WebSocket 是一种网络传输协议,可在单个 TCP 连接上进行全双工通信,位于 OSI 模型的应用层。WebSocket 协议在 2011 年由 IETF 标准化为 RFC 6455,后由 RFC 7936 补充规范。


WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。


介绍完轮询和 WebSocket 的相关内容之后,接下来我们来看一下 XHR Polling 与 WebSocket 之间的区别:




1.3 WebSocket 优点

较少的控制开销。在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。

更强的实时性。由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于 HTTP 请求需要等待客户端发起请求服务端才能响应,延迟明显更少。

保持连接状态。与 HTTP 不同的是,WebSocket 需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。

更好的二进制支持。WebSocket 定义了二进制帧,相对 HTTP,可以更轻松地处理二进制内容。

可以支持扩展。WebSocket 定义了扩展,用户可以扩展协议、实现部分自定义的子协议。

由于 WebSocket 拥有上述的优点,所以它被广泛地应用在即时通信、实时音视频、在线教育和游戏等领域。对于前端开发者来说,要想使用 WebSocket 提供的强大能力,就必须先掌握 WebSocket API,下面阿宝哥带大家一起来认识一下 WebSocket API。


二、WebSocket API

在介绍 WebSocket API 之前,我们先来了解一下它的兼容性:




(图片来源:https://caniuse.com/#search=W...)


从上图可知,目前主流的 Web 浏览器都支持 WebSocket,所以我们可以在大多数项目中放心地使用它。


在浏览器中要使用 WebSocket 提供的能力,我们就必须先创建 WebSocket 对象,该对象提供了用于创建和管理 WebSocket 连接,以及可以通过该连接发送和接收数据的 API。


使用 WebSocket 构造函数,我们就能轻易地构造一个 WebSocket 对象。接下来我们将从 WebSocket 构造函数、WebSocket 对象的属性、方法及 WebSocket 相关的事件四个方面来介绍 WebSocket API,首先我们从 WebSocket 的构造函数入手:


2.1 构造函数

WebSocket 构造函数的语法为:


const myWebSocket = new WebSocket(url [, protocols]);

相关参数说明如下:


url:表示连接的 URL,这是 WebSocket 服务器将响应的 URL。

protocols(可选):一个协议字符串或者一个包含协议字符串的数组。这些字符串用于指定子协议,这样单个服务器可以实现多个 WebSocket 子协议。比如,你可能希望一台服务器能够根据指定的协议(protocol)处理不同类型的交互。如果不指定协议字符串,则假定为空字符串。

当尝试连接的端口被阻止时,会抛出 SECURITY_ERR 异常。


2.2 属性

WebSocket 对象包含以下属性:




每个属性的具体含义如下:


binaryType:使用二进制的数据类型连接。

bufferedAmount(只读):未发送至服务器的字节数。

extensions(只读):服务器选择的扩展。

onclose:用于指定连接关闭后的回调函数。

onerror:用于指定连接失败后的回调函数。

onmessage:用于指定当从服务器接受到信息时的回调函数。

onopen:用于指定连接成功后的回调函数。

protocol(只读):用于返回服务器端选中的子协议的名字。

readyState(只读):返回当前 WebSocket 的连接状态,共有 4 种状态:


CONNECTING — 正在连接中,对应的值为 0;

OPEN — 已经连接并且可以通讯,对应的值为 1;

CLOSING — 连接正在关闭,对应的值为 2;

CLOSED — 连接已关闭或者没有连接成功,对应的值为 3。

url(只读):返回值为当构造函数创建 WebSocket 实例对象时 URL 的绝对路径。

2.3 方法

close([code[, reason]]):该方法用于关闭 WebSocket 连接,如果连接已经关闭,则此方法不执行任何操作。

send(data):该方法将需要通过 WebSocket 链接传输至服务器的数据排入队列,并根据所需要传输的数据的大小来增加 bufferedAmount 的值 。若数据无法传输(比如数据需要缓存而缓冲区已满)时,套接字会自行关闭。

2.4 事件

使用 addEventListener() 或将一个事件监听器赋值给 WebSocket 对象的 oneventname 属性,来监听下面的事件。


close:当一个 WebSocket 连接被关闭时触发,也可以通过 onclose 属性来设置。

error:当一个 WebSocket 连接因错误而关闭时触发,也可以通过 onerror 属性来设置。

message:当通过 WebSocket 收到数据时触发,也可以通过 onmessage 属性来设置。

open:当一个 WebSocket 连接成功时触发,也可以通过 onopen 属性来设置。

介绍完 WebSocket API,我们来举一个使用 WebSocket 发送普通文本的示例。


2.5 发送普通文本



在以上示例中,我们在页面上创建了两个 textarea,分别用于存放 待发送的数据 和 服务器返回的数据。当用户输入完待发送的文本之后,点击 发送 按钮时会把输入的文本发送到服务端,而服务端成功接收到消息之后,会把收到的消息原封不动地回传到客户端。


// const socket = new WebSocket("ws://echo.websocket.org");

// const sendMsgContainer = document.querySelector("#sendMessage");

function send() {

 const message = sendMsgContainer.value;

 if (socket.readyState !== WebSocket.OPEN) {

   console.log("连接未建立,还不能发送消息");

   return;

 }

 if (message) socket.send(message);

}

当然客户端接收到服务端返回的消息之后,会把对应的文本内容保存到 接收的数据 对应的 textarea 文本框中。


// const socket = new WebSocket("ws://echo.websocket.org");

// const receivedMsgContainer = document.querySelector("#receivedMessage");    

socket.addEventListener("message", function (event) {

 console.log("Message from server ", event.data);

 receivedMsgContainer.value = event.data;

});

为了更加直观地理解上述的数据交互过程,我们使用 Chrome 浏览器的开发者工具来看一下相应的过程:




以上示例对应的完整代码如下所示:


<!DOCTYPE html>

<html>

 <head>

   <meta charset="UTF-8" />

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

   <title>WebSocket 发送普通文本示例</title>

   <style>

     .block {

       flex: 1;

     }

   </style>

 </head>

 <body>

   <h3>阿宝哥:WebSocket 发送普通文本示例</h3>

   <div style="display: flex;">

     <div class="block">

       <p>即将发送的数据:<button onclick="send()">发送</button></p>

       <textarea id="sendMessage" rows="5" cols="15"></textarea>

     </div>

     <div class="block">

       <p>接收的数据:</p>

       <textarea id="receivedMessage" rows="5" cols="15"></textarea>

     </div>

   </div>


   <script>

     const sendMsgContainer = document.querySelector("#sendMessage");

     const receivedMsgContainer = document.querySelector("#receivedMessage");

     const socket = new WebSocket("ws://echo.websocket.org");


     // 监听连接成功事件

     socket.addEventListener("open", function (event) {

       console.log("连接成功,可以开始通讯");

     });


     // 监听消息

     socket.addEventListener("message", function (event) {

       console.log("Message from server ", event.data);

       receivedMsgContainer.value = event.data;

     });


     function send() {

       const message = sendMsgContainer.value;

       if (socket.readyState !== WebSocket.OPEN) {

         console.log("连接未建立,还不能发送消息");

         return;

       }

       if (message) socket.send(message);

     }

   </script>

 </body>

</html>

其实 WebSocket 除了支持发送普通的文本之外,它还支持发送二进制数据,比如 ArrayBuffer 对象、Blob 对象或者 ArrayBufferView 对象:


const socket = new WebSocket("ws://echo.websocket.org");

socket.onopen = function () {

 // 发送UTF-8编码的文本信息

 socket.send("Hello Echo Server!");

 // 发送UTF-8编码的JSON数据

 socket.send(JSON.stringify({ msg: "我是阿宝哥" }));

 

 // 发送二进制ArrayBuffer

 const buffer = new ArrayBuffer(128);

 socket.send(buffer);

 

 // 发送二进制ArrayBufferView

 const intview = new Uint32Array(buffer);

 socket.send(intview);


 // 发送二进制Blob

 const blob = new Blob([buffer]);

 socket.send(blob);

};

以上代码成功运行后,通过 Chrome 开发者工具,我们可以看到对应的数据交互过程:




下面阿宝哥以发送 Blob 对象为例,来介绍一下如何发送二进制数据。


Blob(Binary Large Object)表示二进制类型的大对象。在数据库管理系统中,将二进制数据存储为一个单一个体的集合。Blob 通常是影像、声音或多媒体文件。在 JavaScript 中 Blob 类型的对象表示不可变的类似文件对象的原始数据。

对 Blob 感兴趣的小伙伴,可以阅读 “你不知道的 Blob” 这篇文章。


2.6 发送二进制数据



在以上示例中,我们在页面上创建了两个 textarea,分别用于存放 待发送的数据 和 服务器返回的数据。当用户输入完待发送的文本之后,点击 发送 按钮时,我们会先获取输入的文本并把文本包装成 Blob 对象然后发送到服务端,而服务端成功接收到消息之后,会把收到的消息原封不动地回传到客户端。


当浏览器接收到新消息后,如果是文本数据,会自动将其转换成 DOMString 对象,如果是二进制数据或 Blob 对象,会直接将其转交给应用,由应用自身来根据返回的数据类型进行相应的处理。


数据发送代码


// const socket = new WebSocket("ws://echo.websocket.org");

// const sendMsgContainer = document.querySelector("#sendMessage");

function send() {

 const message = sendMsgContainer.value;

 if (socket.readyState !== WebSocket.OPEN) {

   console.log("连接未建立,还不能发送消息");

   return;

 }

 const blob = new Blob([message], { type: "text/plain" });

 if (message) socket.send(blob);

 console.log(`未发送至服务器的字节数:${socket.bufferedAmount}`);

}

当然客户端接收到服务端返回的消息之后,会判断返回的数据类型,如果是 Blob 类型的话,会调用 Blob 对象的 text() 方法,获取 Blob 对象中保存的 UTF-8 格式的内容,然后把对应的文本内容保存到 接收的数据 对应的 textarea 文本框中。


数据接收代码


// const socket = new WebSocket("ws://echo.websocket.org");

// const receivedMsgContainer = document.querySelector("#receivedMessage");

socket.addEventListener("message", async function (event) {

 console.log("Message from server ", event.data);

 const receivedData = event.data;

 if (receivedData instanceof Blob) {

   receivedMsgContainer.value = await receivedData.text();

 } else {

   receivedMsgContainer.value = receivedData;

 }

});

同样,我们使用 Chrome 浏览器的开发者工具来看一下相应的过程:




通过上图我们可以很明显地看到,当使用发送 Blob 对象时,Data 栏位的信息显示的是 Binary Message,而对于发送普通文本来说,Data 栏位的信息是直接显示发送的文本消息。


以上示例对应的完整代码如下所示:


<!DOCTYPE html>

<html>

 <head>

   <meta charset="UTF-8" />

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

   <title>WebSocket 发送二进制数据示例</title>

   <style>

     .block {

       flex: 1;

     }

   </style>

 </head>

 <body>

   <h3>阿宝哥:WebSocket 发送二进制数据示例</h3>

   <div style="display: flex;">

     <div class="block">

       <p>待发送的数据:<button onclick="send()">发送</button></p>

       <textarea id="sendMessage" rows="5" cols="15"></textarea>

     </div>

     <div class="block">

       <p>接收的数据:</p>

       <textarea id="receivedMessage" rows="5" cols="15"></textarea>

     </div>

   </div>


   <script>

     const sendMsgContainer = document.querySelector("#sendMessage");

     const receivedMsgContainer = document.querySelector("#receivedMessage");

     const socket = new WebSocket("ws://echo.websocket.org");


     // 监听连接成功事件

     socket.addEventListener("open", function (event) {

       console.log("连接成功,可以开始通讯");

     });


     // 监听消息

     socket.addEventListener("message", async function (event) {

       console.log("Message from server ", event.data);

       const receivedData = event.data;

       if (receivedData instanceof Blob) {

         receivedMsgContainer.value = await receivedData.text();

       } else {

         receivedMsgContainer.value = receivedData;

       }

     });


     function send() {

       const message = sendMsgContainer.value;

       if (socket.readyState !== WebSocket.OPEN) {

         console.log("连接未建立,还不能发送消息");

         return;

       }

       const blob = new Blob([message], { type: "text/plain" });

       if (message) socket.send(blob);

       console.log(`未发送至服务器的字节数:${socket.bufferedAmount}`);

     }

   </script>

 </body>

</html>

可能有一些小伙伴了解完 WebSocket API 之后,觉得还不够过瘾。下面阿宝哥将带大家来实现一个支持发送普通文本的 WebSocket 服务器。


三、手写 WebSocket 服务器

在介绍如何手写 WebSocket 服务器前,我们需要了解一下 WebSocket 连接的生命周期。




从上图可知,在使用 WebSocket 实现全双工通信之前,客户端与服务器之间需要先进行握手(Handshake),在完成握手之后才能开始进行数据的双向通信。


握手是在通信电路创建之后,信息传输开始之前。握手用于达成参数,如信息传输率,字母表,奇偶校验,中断过程,和其他协议特性。 握手有助于不同结构的系统或设备在通信信道中连接,而不需要人为设置参数。


既然握手是 WebSocket 连接生命周期的第一个环节,接下来我们就先来分析 WebSocket 的握手协议。


3.1 握手协议

WebSocket 协议属于应用层协议,它依赖于传输层的 TCP 协议。WebSocket 通过 HTTP/1.1 协议的 101 状态码进行握手。为了创建 WebSocket 连接,需要通过浏览器发出请求,之后服务器进行回应,这个过程通常称为 “握手”(Handshaking)。


利用 HTTP 完成握手有几个好处。首先,让 WebSocket 与现有 HTTP 基础设施兼容:使得 WebSocket 服务器可以运行在 80 和 443 端口上,这通常是对客户端唯一开放的端口。其次,让我们可以重用并扩展 HTTP 的 Upgrade 流,为其添加自定义的 WebSocket 首部,以完成协商。


下面我们以前面已经演示过的发送普通文本的例子为例,来具体分析一下握手过程。


3.1.1 客户端请求

GET ws://echo.websocket.org/ HTTP/1.1

Host: echo.websocket.org

Origin: file://

Connection: Upgrade

Upgrade: websocket

Sec-WebSocket-Version: 13

Sec-WebSocket-Key: Zx8rNEkBE4xnwifpuh8DHQ==

Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

备注:已忽略部分 HTTP 请求头

字段说明


Connection 必须设置 Upgrade,表示客户端希望连接升级。

Upgrade 字段必须设置 websocket,表示希望升级到 WebSocket 协议。

Sec-WebSocket-Version 表示支持的 WebSocket 版本。RFC6455 要求使用的版本是 13,之前草案的版本均应当弃用。

Sec-WebSocket-Key 是随机的字符串,服务器端会用这些数据来构造出一个 SHA-1 的信息摘要。把 “Sec-WebSocket-Key” 加上一个特殊字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后计算 SHA-1 摘要,之后进行 Base64 编码,将结果做为 “Sec-WebSocket-Accept” 头的值,返回给客户端。如此操作,可以尽量避免普通 HTTP 请求被误认为 WebSocket 协议。

Sec-WebSocket-Extensions 用于协商本次连接要使用的 WebSocket 扩展:客户端发送支持的扩展,服务器通过返回相同的首部确认自己支持一个或多个扩展。

Origin 字段是可选的,通常用来表示在浏览器中发起此 WebSocket 连接所在的页面,类似于 Referer。但是,与 Referer 不同的是,Origin 只包含了协议和主机名称。

3.1.2 服务端响应

HTTP/1.1 101 Web Socket Protocol Handshake ①

Connection: Upgrade ②

Upgrade: websocket ③

Sec-WebSocket-Accept: 52Rg3vW4JQ1yWpkvFlsTsiezlqw= ④

备注:已忽略部分 HTTP 响应头

① 101 响应码确认升级到 WebSocket 协议。

② 设置 Connection 头的值为 "Upgrade" 来指示这是一个升级请求。HTTP 协议提供了一种特殊的机制,这一机制允许将一个已建立的连接升级成新的、不相容的协议。

③ Upgrade 头指定一项或多项协议名,按优先级排序,以逗号分隔。这里表示升级为 WebSocket 协议。

④ 签名的键值验证协议支持。

介绍完 WebSocket 的握手协议,接下来阿宝哥将使用 Node.js 来开发我们的 WebSocket 服务器。


3.2 实现握手功能

要开发一个 WebSocket 服务器,首先我们需要先实现握手功能,这里阿宝哥使用 Node.js 内置的 http 模块来创建一个 HTTP 服务器,具体代码如下所示:


const http = require("http");


const port = 8888;

const { generateAcceptValue } = require("./util");


const server = http.createServer((req, res) => {

 res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });

 res.end("大家好,我是阿宝哥。感谢你阅读“你不知道的WebSocket”");

});


server.on("upgrade", function (req, socket) {

 if (req.headers["upgrade"] !== "websocket") {

   socket.end("HTTP/1.1 400 Bad Request");

   return;

 }

 // 读取客户端提供的Sec-WebSocket-Key

 const secWsKey = req.headers["sec-websocket-key"];

 // 使用SHA-1算法生成Sec-WebSocket-Accept

 const hash = generateAcceptValue(secWsKey);

 // 设置HTTP响应头

 const responseHeaders = [

   "HTTP/1.1 101 Web Socket Protocol Handshake",

   "Upgrade: WebSocket",

   "Connection: Upgrade",

   `Sec-WebSocket-Accept: ${hash}`,

 ];

 // 返回握手请求的响应信息

 socket.write(responseHeaders.join("\r\n") + "\r\n\r\n");

});


server.listen(port, () =>

 console.log(`Server running at http://localhost:${port}`)

);

在以上代码中,我们首先引入了 http 模块,然后通过调用该模块的 createServer() 方法创建一个 HTTP 服务器,接着我们监听 upgrade 事件,每次服务器响应升级请求时就会触发该事件。由于我们的服务器只支持升级到 WebSocket 协议,所以如果客户端请求升级的协议非 WebSocket 协议,我们将会返回 “400 Bad Request”。


当服务器接收到升级为 WebSocket 的握手请求时,会先从请求头中获取 “Sec-WebSocket-Key” 的值,然后把该值加上一个特殊字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后计算 SHA-1 摘要,之后进行 Base64 编码,将结果做为 “Sec-WebSocket-Accept” 头的值,返回给客户端。


上述的过程看起来好像有点繁琐,其实利用 Node.js 内置的 crypto 模块,几行代码就可以搞定了:


// util.js

const crypto = require("crypto");

const MAGIC_KEY = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";


function generateAcceptValue(secWsKey) {

 return crypto

   .createHash("sha1")

   .update(secWsKey + MAGIC_KEY, "utf8")

   .digest("base64");

}

开发完握手功能之后,我们可以使用前面的示例来测试一下该功能。待服务器启动之后,我们只要对 “发送普通文本” 示例,做简单地调整,即把先前的 URL 地址替换成 ws://localhost:8888,就可以进行功能验证。


感兴趣的小伙们可以试试看,以下是阿宝哥本地运行后的结果:




从上图可知,我们实现的握手功能已经可以正常工作了。那么握手有没有可能失败呢?答案是肯定的。比如网络问题、服务器异常或 Sec-WebSocket-Accept 的值不正确。


下面阿宝哥修改一下 “Sec-WebSocket-Accept” 生成规则,比如修改 MAGIC_KEY 的值,然后重新验证一下握手功能。此时,浏览器的控制台会输出以下异常信息:


WebSocket connection to 'ws://localhost:8888/' failed: Error during WebSocket handshake: Incorrect 'Sec-WebSocket-Accept' header value

如果你的 WebSocket 服务器要支持子协议的话,你可以参考以下代码进行子协议的处理,阿宝哥就不继续展开介绍了。


// 从请求头中读取子协议

const protocol = req.headers["sec-websocket-protocol"];

// 如果包含子协议,则解析子协议

const protocols = !protocol ? [] : protocol.split(",").map((s) => s.trim());


// 简单起见,我们仅判断是否含有JSON子协议

if (protocols.includes("json")) {

 responseHeaders.push(`Sec-WebSocket-Protocol: json`);

}

好的,WebSocket 握手协议相关的内容基本已经介绍完了。下一步我们来介绍开发消息通信功能需要了解的一些基础知识。


3.3 消息通信基础

在 WebSocket 协议中,数据是通过一系列数据帧来进行传输的。为了避免由于网络中介(例如一些拦截代理)或者一些安全问题,客户端必须在它发送到服务器的所有帧中添加掩码。服务端收到没有添加掩码的数据帧以后,必须立即关闭连接。


3.3.1 数据帧格式

要实现消息通信,我们就必须了解 WebSocket 数据帧的格式:


0                   1                   2                   3

0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1

+-+-+-+-+-------+-+-------------+-------------------------------+

|F|R|R|R| opcode|M| Payload len |    Extended payload length    |

|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |

|N|V|V|V|       |S|             |   (if payload len==126/127)   |

| |1|2|3|       |K|             |                               |

+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +

|     Extended payload length continued, if payload len == 127  |

+ - - - - - - - - - - - - - - - +-------------------------------+

|                               |Masking-key, if MASK set to 1  |

+-------------------------------+-------------------------------+

| Masking-key (continued)       |          Payload Data         |

+-------------------------------- - - - - - - - - - - - - - - - +

:                     Payload Data continued ...                :

+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +

|                     Payload Data continued ...                |

+---------------------------------------------------------------+

可能有一些小伙伴看到上面的内容之后,就开始有点 “懵逼” 了。下面我们来结合实际的数据帧来进一步分析一下:




在上图中,阿宝哥简单分析了 “发送普通文本” 示例对应的数据帧格式。这里我们来进一步介绍一下 Payload length,因为在后面开发数据解析功能的时候,需要用到该知识点。


Payload length 表示以字节为单位的 “有效负载数据” 长度。它有以下几种情形:


如果值为 0-125,那么就表示负载数据的长度。

如果是 126,那么接下来的 2 个字节解释为 16 位的无符号整形作为负载数据的长度。

如果是 127,那么接下来的 8 个字节解释为一个 64 位的无符号整形(最高位的 bit 必须为 0)作为负载数据的长度。

多字节长度量以网络字节顺序表示,有效负载长度是指 “扩展数据” + “应用数据” 的长度。“扩展数据” 的长度可能为 0,那么有效负载长度就是 “应用数据” 的长度。


另外,除非协商过扩展,否则 “扩展数据” 长度为 0 字节。在握手协议中,任何扩展都必须指定 “扩展数据” 的长度,这个长度如何进行计算,以及这个扩展如何使用。如果存在扩展,那么这个 “扩展数据” 包含在总的有效负载长度中。


3.3.2 掩码算法

掩码字段是一个由客户端随机选择的 32 位的值。掩码值必须是不可被预测的。因此,掩码必须来自强大的熵源(entropy),并且给定的掩码不能让服务器或者代理能够很容易的预测到后续帧。掩码的不可预测性对于预防恶意应用的作者在网上暴露相关的字节数据至关重要。


掩码不影响数据荷载的长度,对数据进行掩码操作和对数据进行反掩码操作所涉及的步骤是相同的。掩码、反掩码操作都采用如下算法:


j = i MOD 4

transformed-octet-i = original-octet-i XOR masking-key-octet-j

original-octet-i:为原始数据的第 i 字节。

transformed-octet-i:为转换后的数据的第 i 字节。

masking-key-octet-j:为 mask key 第 j 字节。

为了让小伙伴们能够更好的理解上面掩码的计算过程,我们来对示例中 “我是阿宝哥” 数据进行掩码操作。这里 “我是阿宝哥” 对应的 UTF-8 编码如下所示:


E6 88 91 E6 98 AF E9 98 BF E5 AE 9D E5 93 A5

而对应的 Masking-Key 为 0x08f6efb1,根据上面的算法,我们可以这样进行掩码运算:


let uint8 = new Uint8Array([0xE6, 0x88, 0x91, 0xE6, 0x98, 0xAF, 0xE9, 0x98,

 0xBF, 0xE5, 0xAE, 0x9D, 0xE5, 0x93, 0xA5]);

let maskingKey = new Uint8Array([0x08, 0xf6, 0xef, 0xb1]);

let maskedUint8 = new Uint8Array(uint8.length);


for (let i = 0, j = 0; i < uint8.length; i++, j = i % 4) {

 maskedUint8[i] = uint8[i] ^ maskingKey[j];

}


console.log(Array.from(maskedUint8).map(num=>Number(num).toString(16)).join(' '));

以上代码成功运行后,控制台会输出以下结果:


ee 7e 7e 57 90 59 6 29 b7 13 41 2c ed 65 4a

上述结果与 WireShark 中的 Masked payload 对应的值是一致的,具体如下图所示:




在 WebSocket 协议中,数据掩码的作用是增强协议的安全性。但数据掩码并不是为了保护数据本身,因为算法本身是公开的,运算也不复杂。那么为什么还要引入数据掩码呢?引入数据掩码是为了防止早期版本的协议中存在的代理缓存污染攻击等问题。


了解完 WebSocket 掩码算法和数据掩码的作用之后,我们再来介绍一下数据分片的概念。


3.3.3 数据分片

WebSocket 的每条消息可能被切分成多个数据帧。当 WebSocket 的接收方收到一个数据帧时,会根据 FIN 的值来判断,是否已经收到消息的最后一个数据帧。


利用 FIN 和 Opcode,我们就可以跨帧发送消息。操作码告诉了帧应该做什么。如果是 0x1,有效载荷就是文本。如果是 0x2,有效载荷就是二进制数据。但是,如果是 0x0,则该帧是一个延续帧。这意味着服务器应该将帧的有效负载连接到从该客户机接收到的最后一个帧。


为了让大家能够更好地理解上述的内容,我们来看一个来自 MDN 上的示例:


Client: FIN=1, opcode=0x1, msg="hello"

Server: (process complete message immediately) Hi.

Client: FIN=0, opcode=0x1, msg="and a"

Server: (listening, new message containing text started)

Client: FIN=0, opcode=0x0, msg="happy new"

Server: (listening, payload concatenated to previous message)

Client: FIN=1, opcode=0x0, msg="year!"

Server: (process complete message) Happy new year to you too!

在以上示例中,客户端向服务器发送了两条消息。第一个消息在单个帧中发送,而第二个消息跨三个帧发送。


其中第一个消息是一个完整的消息(FIN=1 且 opcode != 0x0),因此服务器可以根据需要进行处理或响应。而第二个消息是文本消息(opcode=0x1)且 FIN=0,表示消息还没发送完成,还有后续的数据帧。该消息的所有剩余部分都用延续帧(opcode=0x0)发送,消息的最终帧用 FIN=1 标记。


好的,简单介绍了数据分片的相关内容。接下来,我们来开始实现消息通信功能。


3.4 实现消息通信功能

阿宝哥把实现消息通信功能,分解为消息解析与消息响应两个子功能,下面我们分别来介绍如何实现这两个子功能。


3.4.1 消息解析

利用消息通信基础环节中介绍的相关知识,阿宝哥实现了一个 parseMessage 函数,用来解析客户端传过来的 WebSocket 数据帧。出于简单考虑,这里只处理文本帧,具体代码如下所示:


function parseMessage(buffer) {

 // 第一个字节,包含了FIN位,opcode, 掩码位

 const firstByte = buffer.readUInt8(0);

 // [FIN, RSV, RSV, RSV, OPCODE, OPCODE, OPCODE, OPCODE];

 // 右移7位取首位,1位,表示是否是最后一帧数据

 const isFinalFrame = Boolean((firstByte >>> 7) & 0x01);

 console.log("isFIN: ", isFinalFrame);

 // 取出操作码,低四位

 /**

  * %x0:表示一个延续帧。当 Opcode 为 0 时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片;

  * %x1:表示这是一个文本帧(text frame);

  * %x2:表示这是一个二进制帧(binary frame);

  * %x3-7:保留的操作代码,用于后续定义的非控制帧;

  * %x8:表示连接断开;

  * %x9:表示这是一个心跳请求(ping);

  * %xA:表示这是一个心跳响应(pong);

  * %xB-F:保留的操作代码,用于后续定义的控制帧。

  */

 const opcode = firstByte & 0x0f;

 if (opcode === 0x08) {

   // 连接关闭

   return;

 }

 if (opcode === 0x02) {

   // 二进制帧

   return;

 }

 if (opcode === 0x01) {

   // 目前只处理文本帧

   let offset = 1;

   const secondByte = buffer.readUInt8(offset);

   // MASK: 1位,表示是否使用了掩码,在发送给服务端的数据帧里必须使用掩码,而服务端返回时不需要掩码

   const useMask = Boolean((secondByte >>> 7) & 0x01);

   console.log("use MASK: ", useMask);

   const payloadLen = secondByte & 0x7f; // 低7位表示载荷字节长度

   offset += 1;

   // 四个字节的掩码

   let MASK = [];

   // 如果这个值在0-125之间,则后面的4个字节(32位)就应该被直接识别成掩码;

   if (payloadLen <= 0x7d) {

     // 载荷长度小于125

     MASK = buffer.slice(offset, 4 + offset);

     offset += 4;

     console.log("payload length: ", payloadLen);

   } else if (payloadLen === 0x7e) {

     // 如果这个值是126,则后面两个字节(16位)内容应该,被识别成一个16位的二进制数表示数据内容大小;

     console.log("payload length: ", buffer.readInt16BE(offset));

     // 长度是126, 则后面两个字节作为payload length,32位的掩码

     MASK = buffer.slice(offset + 2, offset + 2 + 4);

     offset += 6;

   } else {

     // 如果这个值是127,则后面的8个字节(64位)内容应该被识别成一个64位的二进制数表示数据内容大小

     MASK = buffer.slice(offset + 8, offset + 8 + 4);

     offset += 12;

   }

   // 开始读取后面的payload,与掩码计算,得到原来的字节内容

   const newBuffer = [];

   const dataBuffer = buffer.slice(offset);

   for (let i = 0, j = 0; i < dataBuffer.length; i++, j = i % 4) {

     const nextBuf = dataBuffer[i];

     newBuffer.push(nextBuf ^ MASK[j]);

   }

   return Buffer.from(newBuffer).toString();

 }

 return "";

}

创建完 parseMessage 函数,我们来更新一下之前创建的 WebSocket 服务器:


server.on("upgrade", function (req, socket) {

 socket.on("data", (buffer) => {

   const message = parseMessage(buffer);

   if (message) {

     console.log("Message from client:" + message);

   } else if (message === null) {

     console.log("WebSocket connection closed by the client.");

   }

 });

 if (req.headers["upgrade"] !== "websocket") {

   socket.end("HTTP/1.1 400 Bad Request");

   return;

 }

 // 省略已有代码

});

更新完成之后,我们重新启动服务器,然后继续使用 “发送普通文本” 的示例来测试消息解析功能。以下发送 “我是阿宝哥” 文本消息后,WebSocket 服务器输出的信息。


Server running at http://localhost:8888

isFIN:  true

use MASK:  true

payload length:  15

Message from client:我是阿宝哥

通过观察以上的输出信息,我们的 WebSocket 服务器已经可以成功解析客户端发送包含普通文本的数据帧,下一步我们来实现消息响应的功能。


3.4.2 消息响应

要把数据返回给客户端,我们的 WebSocket 服务器也得按照 WebSocket 数据帧的格式来封装数据。与前面介绍的 parseMessage 函数一样,阿宝哥也封装了一个 constructReply 函数用来封装返回的数据,该函数的具体代码如下:


function constructReply(data) {

 const json = JSON.stringify(data);

 const jsonByteLength = Buffer.byteLength(json);

 // 目前只支持小于65535字节的负载

 const lengthByteCount = jsonByteLength < 126 ? 0 : 2;

 const payloadLength = lengthByteCount === 0 ? jsonByteLength : 126;

 const buffer = Buffer.alloc(2 + lengthByteCount + jsonByteLength);

 // 设置数据帧首字节,设置opcode为1,表示文本帧

 buffer.writeUInt8(0b10000001, 0);

 buffer.writeUInt8(payloadLength, 1);

 // 如果payloadLength为126,则后面两个字节(16位)内容应该,被识别成一个16位的二进制数表示数据内容大小

 let payloadOffset = 2;

 if (lengthByteCount > 0) {

   buffer.writeUInt16BE(jsonByteLength, 2);

   payloadOffset += lengthByteCount;

 }

 // 把JSON数据写入到Buffer缓冲区中

 buffer.write(json, payloadOffset);

 return buffer;

}

创建完 constructReply 函数,我们再来更新一下之前创建的 WebSocket 服务器:


server.on("upgrade", function (req, socket) {

 socket.on("data", (buffer) => {

   const message = parseMessage(buffer);

   if (message) {

     console.log("Message from client:" + message);

     // 新增以下

停止犯下这5个JavaScript风格错误,使你的代码可读和可维护的快速提示

seo达人

使你的代码可读和可维护的快速提示。


有多少次,你打开一个旧的项目,发现混乱的代码,当你添加一些新的东西时,很容易崩溃?我们都有过这样的经历。


为了减少难以读懂的javascript的数量,我提供了以下示例。这些都是我过去所犯过的错误。


对具有多个返回值的函数使用数组解构

假设我们有一个返回多个值的函数。一种可能的实现是使用数组解构,如下所示:


const func = () => {

 const a = 1;

 const b = 2;

 const c = 3;

 const d = 4;

 return [a,b,c,d];

}

const [a,b,c,d] = func();

console.log(a,b,c,d); // 1,2,3,4

尽管上面的方法很好用,但确实引入了一些复杂性。


当我们调用函数并将值分配给 a,b,c,d 时,我们需要注意返回数据的顺序。这里的一个小错误可能会成为调试的噩梦。


此外,无法确切指定我们要从函数中获取哪些值,如果我们只需要 c 和 d 怎么办?


相反,我们可以使用对象解构。


const func = () => {

 const a = 1;

 const b = 2;

 const c = 3;

 const d = 4;

 return {a,b,c,d};

}

const {c,d} = func();

现在,我们可以轻松地从函数中选择所需的数据,这也为我们的代码提供了未来的保障,允许我们在不破坏东西的情况下增加额外的返回变量。


不对函数参数使用对象分解

假设我们有一个函数,该函数将一个对象作为参数并对该对象的属性执行一些操作。一种幼稚的方法可能看起来像这样:


// 不推荐

function getDaysRemaining(subscription) {

 const startDate = subscription.startDate;

 const endDate = subscription.endDate;

 return endDate - startDate;

}

上面的方法按预期工作,但是,我们创建了两个不必要的临时引用 startDate 和 endDate。


一种更好的实现是对 subscription 对象使用对象解构来在一行中获取 startDate 和 endDate。


// 推荐

function getDaysRemaining(subscription) {

 const { startDate, endDate } = subscription;

 return startDate - endDate;

}

我们可以更进一步,直接对参数执行对象析构。


// 更好

function getDaysRemaining({ startDate, endDate }) {

 return startDate - endDate;

}

更优雅,不是吗?


在不使用扩展运算符的情况下复制数组

使用 for循环遍历数组并将其元素复制到新数组是冗长且相当丑陋的。


可以以简洁明了的方式使用扩展运算符来达到相同的效果。


const stuff = [1,2,3];


// 不推荐

const stuffCopyBad = []

for(let i = 0; i < stuff.length; i++){

 stuffCopyBad[i] = stuff[i];

}


// 推荐

const stuffCopyGood = [...stuff];

使用var

使用 const 保证不能重新分配变量。这样可以减少我们代码中的错误,并使其更易于理解。


// 不推荐

var x = "badX";

var y = "baxY";


// 推荐

const x = "goodX";

const y = "goodX";

果你确实需要重新分配变量,请始终选择 let 而不是 var。


这是因为 let 是块作用域的,而 var 是函数作用域的。


块作用域告诉我们,只能在定义它的代码块内部访问变量,尝试访问块外部的变量会给我们提供ReferenceError。


for(let i = 0; i < 10; i++){

 //something

}

print(i) // ReferenceError: i is not defined

函数作用域告诉我们,只能在定义其的函数内部访问变量。


for(var i = 0; i < 10; i++){

 //something

}

console.log(i) // 10

let 和 const 都是块范围的。


不使用模板字面值

手动将字符串连接在一起相当麻烦,而且输入时可能会造成混淆。这是一个例子:


// 不推荐

function printStartAndEndDate({ startDate, endDate }) {

 console.log('StartDate:' + startDate + ',EndDate:' + endDate)

}

模板文字为我们提供了一种可读且简洁的语法,该语法支持字符串插值。


// 推荐

function printStartAndEndDate({ startDate, endDate }) {

 console.log(`StartDate: ${startDate}, EndDate: ${endDate}`)

}

模板文字也提供了嵌入新行的简便方法,你所需要做的就是照常按键盘上的Enter键。


// 两行打印

function printStartAndEndDate({ startDate, endDate }) {

 console.log(`StartDate: ${startDate}

 EndDate: ${endDate}`)

}

蓝蓝设计www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务

如何推进多方合作的A/B测试项目?

涛涛

在多方合作的项目中,我们需要规划项目的合理落地方案,并在执行过程中和各方有效沟通。结合近期羚珑商家合作测试项目的实践经历,聊聊我在项目中的心得体会。

项目背景

我们想了解京东首焦banner中,设计因素对点击效果的影响情况。做这件事最大的限制在于投放数据会受多种因素影响,不仅是一个设计因素的选择,还有比如人群、出价、品牌、类目等因素的不同,都能影响到最终的投放结果。因此,从总体样本中取样进行分析的意义不大。为了解决上述问题,我们招募了一些能控制其他因素的品牌广告主,与有能力支持投放的测试投放系统合作,共同完成A/B测试。

需求拆解与规划

不难发现这个需求会涉及到多个项目角色,意味着将有复杂的推进流程,需要提前拆解与规划,才能保证项目进度可控。

如何推进多方合作的A/B测试项目?来看京东的实战案例!

△ 测试项目流程与分工

我们可以分三步来进行:

1. 找到项目关键步骤

关键步骤是将一个项目的推进拆解成多个阶段,找到关键步骤,也就知道了每个阶段的主要任务,可以进一步分工来推进项目。

如何找到关键步骤?我们可以从项目目标中提取。测试项目的目标是找到设计因素与投放点击效果的影响关系。这个目标里提取重要的关键词为:设计因素、投放点击效果、影响关系。面对这些关键词我们会有疑问产生如「测试设计因素是什么?有哪些?」、「如何投放」、「如何判断影响关系」,再进行串联梳理,得到完整步骤流程。

2. 明确角色任务与产出

每个阶段的主要任务,可以拆解分工给项目角色,项目角色也就有了各自的任务,角色产出物则是任务执行的结果,将直接推动项目进入下一环节。所以为了顺利推进项目,我们需要根据项目实际情况评估角色任务分工是否合理,产出是否满足项目要求。

比如在「设计测试图片」这个步骤中,有两个分工方案,让广告主或我们自己设计图片。我们从项目时长、沟通成本、潜在风险等维度进行评估,判断广告主把控变量不严谨会导致测试数据无效,且外部合作沟通成本较高时间不够,所以最后将设计图片的任务分给了内部。

3. 预判项目风险

对流程规划得越详细,在后续合作过程中越容易把控项目的节奏,项目风险越低。

再如「设计测试图片」这个关键步骤,详细展开执行流程会发现涉及到交互设计师、两方视觉设计师、广告主四个角色。对于项目执行角色,产出物的质量及按时交付尤为重要,所以重点把控输出准确性与完成时间。对于项目决策角色,也就是决策最终投放哪些图片的广告主,沟通配合尤为重要。所以基于规避风险也对项目流程进行了优化,前置在项目之初,要求广告主提前准备交付内容,规定交付时间及格式,以及充分沟通说明图片设计产出之后不能进行较大改动。

结论推导的重要前提与方法

1. 保证数据有效

测试结论来源于测试数据,我们首先应该保证回收数据的有效性。测试项目的关键指标是点击率,即点击量与曝光量的比值。当图片本身曝光量低时,我们认为随着曝光量增加,点击率比值波动范围仍然较大,数据还未稳定在某个区间段,会影响结论准确性,判定为无效数据。发现类似这种问题,我们会和广告主商量继续投放,延长测试时间来增加曝光。

2. 充分测试

要想得到可靠普适的结论,需要对比多组样本的测试结果。对于某个设计因素,我们先进行了单一广告主的投放数据对比,可以找到投放效果最优和最差的设计水平。然后又将多个广告主的结果进行对比,会发现存在不一致的情况,验证了单组样本结论不能作为类目结论输出。如果多个广告主结果一致,或呈现某一趋势,则结论在该类目可以认为较为普适。

3. 差异分析

对于多组样本结果不一致的情况,可以从组间因素差异着手分析。

如何推进多方合作的A/B测试项目?来看京东的实战案例!

△ 背景设计因素测试结果

上图是两个广告主分别测试背景设计因素得到的结果。第一组的投放结果为实景设计效果更好,第二组则为普通平面设计效果更好。产生这样差异的原因是什么呢?我们先找出组间可能导致这一结果的因素,分别是颜色和产品。从颜色上看,是否平面+黑色效果更好呢?我们看了其他广告主结果,否定了这一猜想。再从产品图来看,两个产品的识别度是不同的,我们将其他广告主该因素测试图按产品是否容易识别分组,最终得出两组不同的结论:当产品图易识别时,背景设计对效果影响不大;当产品图不易识别时,实景图效果更好。这也就解释了上图结果不一致的原因。

回顾整个项目,我个人认为项目中最重要的工作是沟通。下面我来分别谈谈内外部沟通的经验。

双赢是外部合作沟通的基石

与外部团队、广告主合作推进项目,需要及时有效的沟通,什么是有效的沟通呢?

1. 围绕对方利益谈配合

广告主只看转化结果,我们如果只谈设计不谈转化,广告主是不会想要出钱参与项目的,那么项目也将停滞不前。所以在合作前,我们做了项目及团队包装,用真实的案例让广告主快速理解参与项目所能带来的价值,并用团队以往的作品、能力展示让广告主了解与我们合作的优势。广告主越认同项目价值及我们的专业度,配合度就会越高。

如何推进多方合作的A/B测试项目?来看京东的实战案例!

△ 招募PPT中的真实案例介绍

2. 围绕对方目标来「推销」方案

在推进项目这一点上,找到目标一致的点更容易促成各方意见达成一致。

我们合作的广告主很多会选择外包banner设计,所以广告主们习惯了做传说中的「甲方爸爸」,难免会对视觉设计方案有各种主观意见。比如,某电视广告主不喜欢红色图片设计,想要蓝色。对于这样的分歧,我个人喜欢用引导式的沟通方法。首先,不要急于表达自己或否定对方的意见,可以以疑问句式来猜测对方的目的。「喜欢蓝色是希望用沉稳的颜色来表现产品的高级感吗?」,如果猜测正确,对方会因自己的需求被理解而更容易沟通,这时再继续阐述用红色如何表达高级感;如果猜测不正确,对方会顺势说出原因,我们再围绕对方的目标进一步沟通。如果对方毫无理由「不喜欢这个设计」,而设计师并不觉得有设计问题,那么我们需要停止专业角度的沟通,从设计是否满足当前阶段项目需求的角度来沟通,方案则更容易被接受。

内部合作各尽所能发挥优势

内部合作分交互、视觉、用研三种不同的岗位角色,在项目中都发挥着自己重要的作用。

交互擅长统筹与拆解问题,团队的内部沟通能产生多种解题思路,对项目的推进至关重要;在遇到如需要统计学分析、样本设置等专业问题时,用研可以提供更专业的分析方法与帮助,为我们实现科学分析提供有力支持;视觉设计师对banner设计有丰富的经验,可以在各个环节中发挥优势。从项目之初就参与设计因素拆解、测试方案设置,可以补充视觉相关的设计因素,在优化测试方案、风险预期方面都可以提供重要的建议。在结论推导中,对图中的因素差异敏感度高,可以根据数据提出关键猜想。

文章来源:优设    作者:京东设计中心JDC

蓝蓝设计www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务

如何为你的UI制定一套色彩系统?

涛涛

色彩在UI设计中的作用:加深品牌印象与品牌感、引导用户视觉凹增加易读性、区分信息交互的状态、营造氛围传递热度……

前言

不管是做 UI 设计还是画插画,有很多同学觉得自己是因为天赋不够所以对色彩的敏感度不够,其实不然。一个可能是大家总结的太少,从来都是凭感觉和运气去配色,但配色都是有讲究的。

一个设计作品呈现到用户面前,第一眼进入眼帘的就是产品的视觉表现,而产品的色彩在其中起到了举足轻重的作用,毫无疑问色彩搭配对于设计师来说是非常重要的。那么具体到实际项目中该使用什么怎样的色彩,需要怎么做呢?

用户界面是一个设计师用理性思维解决用户感性需求的窗口。如果对色彩的运用不加以克制,界面可能会显得花哨而没有主次;但过于拘谨又容易使界面保守,难以激发用户情绪,下面以Bee express项目的实例来理性推导制定一套色彩系统。

切勿直奔主题

做过设计的同学应该都知道颜色模式:RGB、CMYK、Lab 等等,这里不做过多的解释了。另外每个颜色具有一定的性格特征和表达方式,而且都会有正反两面。虽然每种色彩都有正向性格特征,但是我们在定位主体色之前一定要知道所选择色彩的负面特征对企业是否会带来负面的影响,

开始之前我们需要了解在配色过程中需要避免出现的问题,如果你经常出现下列的问题,保证你在试用期内一次性就能拿到全部薪资,emmm……

  • 高饱和度的色彩会造成我们的视觉疲劳及视幻;
  • 灰部使用过多的配色会使界面有一种脏兮兮、雾蒙蒙的感觉,甚至心情低落;
  • 没有规律且过多的配色。如果你不是做五彩斑斓的黑,建议6、3、1的色彩配比,辅助色不超过3种;
  • 荧光色。使用这种色彩的,建议跟色彩对视,看谁坚持的更久,除非是你赢了;
  • 太轻柔的颜色-没有重点且轻飘飘的感觉;
  • 现在很火的新拟物化设计对于部分(没有绝对)产品可能会造成信息识别性很差;
  • 不要将对抗色重叠,否则你会很浮躁。

定位品牌色

虽然设计是相通的,但是在不同的设计领域进行配色时,依然会存在巨大的区别。更换品牌的主体色,都不会是因为设计师自己的决定,而是公司在商业策略上优先做出了调整,然后通过品牌视觉上的变更将这个信息传递给消费者。

Bee Express快递、速递柜业务为主,前期的主色及视觉形象以橙黄色为主,为了避免视觉跳跃性太大,以及后期IP形象(蜜蜂吉祥物)打造,本次品牌色彩升级在原有基础上优化了色调,以保证后期产品的易用性和延展性,并利用最科学、最适用的方式推导出辅助色,以提升应用视觉的丰富性和感官体验。

express原主色:

如何为你的UI制定一套色彩系统?来看这个实战案例!

为了不影响原有色调前期的视觉传播,即在原有主体色的基础上调整SHB的数值,让色彩更具视觉冲击力,在色彩衬托(字体、图标)更清晰。

  • H(Hue:色相)
  • S(Saturation:饱和度)
  • B(Brightness:明度)

如何为你的UI制定一套色彩系统?来看这个实战案例!

通过调整后的主体色也能看出,明亮清晰的主体色(品牌色)也更适合在界面中的运用,为信息传递、引导操作、品牌价值带来更大的提升。

  • 信息传递:产品的首要目的是传递用户所需要的信息,这就需要界面有清晰的层级关系,明确、舒适的阅读体验。
  • 引导操作:清晰合理的操作引导,让用户能够准确地根据引导进行下一步操作。
  • 品牌价值:很多同学会忽略这一点,导致产品的界面与品牌关联性差,整体界面没有品牌感。

根据主体色推理同色系

同色系为统一的色相,使用中可以加深品牌色的感知,可以让界面更有层次,同时可以让界面保持色彩上的一致性,整体感较强,产生低对比度的和谐美感,给人协调统一的感觉。

具体是指与品牌色 H(色相)一致,通过改变 S(饱和度)与 B(明度)变化产生的色组。分别往浅色/深色方向按均匀数据增减,各产生5个坐标值。

如何为你的UI制定一套色彩系统?来看这个实战案例!

综上能看出,使用同一色系即可完成一个项目,但是对于中大型项目来说实在是过于单调,没有太多的层次感,因此我们需要多色搭配为辅。多色的辅助颜色可设定不同的任务属性和情感表达,再搭配中性色黑白灰,能赋予更多的变化和层次。

提取24色-铺垫辅助色

根据主体色 H(色相)为基础,不断地递增、递减 15,在 0-360 之间可以得出 24 个颜色,也就是将 360° 色环分割为 24 份,(24份在360°色环上,每一个色相的角度为15°),最终得到下图24色。

如何为你的UI制定一套色彩系统?来看这个实战案例!

选取辅助色

辅助色需要满足的两个条件:

和品牌色有明显区分:避免所选辅助色感官上给用户视觉区别与品牌色差距不大,传递的调性太过一致;

不能过于突兀:根据色彩原理,互补色是最能与品牌色本色产生视觉感官对比的颜色,但可能会有些突兀。为了让颜色的辅助起到丰富画面的作用,而不是反而让整个版面显得不和谐,所以选择互补色的邻近色作为辅助色,避免直接使用互补色。

  • 邻近色:色相差值 15° 以内的颜色为邻近色;
  • 类似色:色相差值 30° 以内的颜色为类似色;
  • 互补色:色相差值 180° 的颜色为互补色。

基于品牌色可衍生出 3 个辅助色:一个与品牌色传递调性有明显区分的类似色;两个互补色的邻近色。

如何为你的UI制定一套色彩系统?来看这个实战案例!

类似色搭配:使用色相相近的颜色,页面元素不会相互冲突,更加协调有质感。

互补色搭配:选择使用互补色,最佳搭配是一种作为主色,另一种用于强调。它们有着非常强烈的对比度,用在需要特别强调某个元素时会非常有效。

视觉统一感官校准

每一种颜色都有自己的「感官明度」,也就是发光度。根据现有的使用场景,类似色和互补色大都用在同层级的信息展示上,而当我们将最终得到的辅助色摆放在一起之后发现,虽然我们提取出的辅助色明度色值都一致,因为颜色本身自带的感官明度属性有所区别,导致视觉上会有明显的明暗差别。需要通过发光度来进行最终的颜色校正。

校准方式:依次在辅助色上叠加一层纯黑图层,将该纯黑图层颜色模式调整为 Hue(色相),就可以通过无彩色系下的明度色值,进行对比,使色彩视觉感官保持一致(青色和蓝色属冷色调,固需加深)。

如何为你的UI制定一套色彩系统?来看这个实战案例!

全色系输出

根据上面同色系的明度、纯度对比规则,对所有定义的辅助色进行明度和纯度的辅助色彩输出,最终得到辅助色色板。H(色相)一致,通过改变 S(饱和度)与 B(明度)变化产生色组。分别往浅色/深色方向按均匀数据增减,各产生5个坐标值

如何为你的UI制定一套色彩系统?来看这个实战案例!

删除最左侧的3种同色系,因明度过低时,颜色已经非常接近于黑色,色相在肉眼上几乎已经趋于一致。最后得到基于品牌色推导出的全色系色阶色板。

如何为你的UI制定一套色彩系统?来看这个实战案例!

如何为你的UI制定一套色彩系统?来看这个实战案例!

总结

配色常常从确定主色开始,根据行业类型和视觉诉求的需要,选择一种居于支配的色彩作为主调色彩,构成画面的整体色彩倾向。然后选择辅色,添加点缀色,最后按照色彩组合的原则完成设计中的需求。

虽然有了以上的配色方式,但一套标准的色彩系统还会包含中性色规范、颜色的使用规范等等。相信解决了大部分的需求,剩下的工作也难不倒大家了。毕竟以上的方式只是给大家提供了一个理性科学的方法,如果想更加优秀,还需要进一步深入地去学习色彩理论知识,多看优秀的配色作品提升审美,总之要多看、多实践和多思考。

文章来源:优设    作者:能量眼球

蓝蓝设计www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务

你可能不需要在 JavaScript 使用 switch 语句!

seo达人

没有 switch 就没有复杂的代码块

switch很方便:给定一个表达式,我们可以检查它是否与一堆case子句中的其他表达式匹配。 考虑以下示例:


const name = "Juliana";


switch (name) {

 case "Juliana":

   console.log("She's Juliana");

   break;

 case "Tom":

   console.log("She's not Juliana");

   break;

}

当 name 为“Juliana”时,我们将打印一条消息,并立即中断退出该块。 在switch函数内部时,直接在 case 块使用 return,就可以省略break。


当没有匹配项时,可以使用 default 选项:


const name = "Kris";


switch (name) {

 case "Juliana":

   console.log("She's Juliana");

   break;

 case "Tom":

   console.log("She's not Juliana");

   break;

 default:

   console.log("Sorry, no match");

}

switch在 Redux reducers 中也大量使用(尽管Redux Toolkit简化了样板),以避免产生大量的if。 考虑以下示例:


const LOGIN_SUCCESS = "LOGIN_SUCCESS";

const LOGIN_FAILED = "LOGIN_FAILED";


const authState = {

 token: "",

 error: "",

};


function authReducer(state = authState, action) {

 switch (action.type) {

   case LOGIN_SUCCESS:

     return { ...state, token: action.payload };

   case LOGIN_FAILED:

     return { ...state, error: action.payload };

   default:

     return state;

 }

}

这有什么问题吗?几乎没有。但是有没有更好的选择呢?


从 Python 获得的启示

来自 Telmo 的这条 Tweet引起了我的注意。 他展示了两种“switch”风格,其中一种非常接近Python中的模式。


Python 没有开关,它给我们一个更好的替代方法。 首先让我们将代码从 JavaScript 移植到Python:


LOGIN_SUCCESS = "LOGIN_SUCCESS"

LOGIN_FAILED = "LOGIN_FAILED"


auth_state = {"token": "", "error": ""}



def auth_reducer(state=auth_state, action={}):

   mapping = {

       LOGIN_SUCCESS: {**state, "token": action["payload"]},

       LOGIN_FAILED: {**state, "error": action["payload"]},

   }


   return mapping.get(action["type"], state)

在 Python 中,我们可以使用字典来模拟switch 。 dict.get() 可以用来表示 switch 的 default 语句。


当访问不存在的key时,Python 会触发一个 KeyError 错误:


>>> my_dict = {

   "name": "John",

   "city": "Rome",

   "age": 44

   }


>>> my_dict["not_here"]


# Output: KeyError: 'not_here'

.get()方法是一种更安全方法,因为它不会引发错误,并且可以为不存在的key指定默认值:


>>> my_dict = {

   "name": "John",

   "city": "Rome",

   "age": 44

   }


>>> my_dict.get("not_here", "not found")


# Output: 'not found'

因此,Pytho n中的这一行:


return mapping.get(action["type"], state)

等价于 JavaScript中的:


function authReducer(state = authState, action) {

 ...

   default:

     return state;

 ...

}

使用字典的方式替换 switch

再次思考前面的示例:


const LOGIN_SUCCESS = "LOGIN_SUCCESS";

const LOGIN_FAILED = "LOGIN_FAILED";


const authState = {

 token: "",

 error: "",

};


function authReducer(state = authState, action) {

 switch (action.type) {

   case LOGIN_SUCCESS:

     return { ...state, token: action.payload };

   case LOGIN_FAILED:

     return { ...state, error: action.payload };

   default:

     return state;

 }

}

如果不使用 switch 我们可以这样做:


function authReducer(state = authState, action) {

 const mapping = {

   [LOGIN_SUCCESS]: { ...state, token: action.payload },

   [LOGIN_FAILED]: { ...state, error: action.payload }

 };


 return mapping[action.type] || state;

}

这里我们使用 ES6 中的计算属性,此处,mapping的属性是根据两个常量即时计算的:LOGIN_SUCCESS 和 LOGIN_FAILED。

属性对应的值,我们这里使用的是对象解构,这里 ES9((ECMAScript 2018)) 出来的。


const mapping = {

 [LOGIN_SUCCESS]: { ...state, token: action.payload },

 [LOGIN_FAILED]: { ...state, error: action.payload }

}

你如何看待这种方法?它对 switch 来说可能还能一些限制,但对于 reducer 来说可能是一种更好的方案。


但是,此代码的性能如何?


性能怎么样?

switch 的性能优于字典的写法。我们可以使用下面的事例测试一下:


console.time("sample");

for (let i = 0; i < 2000000; i++) {

 const nextState = authReducer(authState, {

   type: LOGIN_SUCCESS,

   payload: "some_token"

 });

}

console.timeEnd("sample");

测量它们十次左右,


for t in {1..10}; do node switch.js >> switch.txt;done

for t in {1..10}; do node map.js >> map.txt;done

clipboard.png


人才们的 【三连】 就是小智不断分享的最大动力,如果本篇博客有任何错误和建议,欢迎人才们留言,最后,谢谢大家的观看。


原文:https://codeburst.io/alternat...


代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug。

蓝蓝设计www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务



vuex管理状态仓库详解

seo达人

一.什么是Vuex?

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 也集成到 Vue 的官方调试工具 devtools extension,提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能。采用了全局单例模式,将组件的共享状态抽离出来管理,使得组件树中每一个位置都可以获取共享的状态或者触发行为。
那么什么是状态呢?我把状态理解为在没有使用vuex时,在当前组件中data内需要共用的数据为状态。
vuex使得状态或行为成为了共享的状态,所共享的状态或行为可以在各个组件中都可以访问到,省去了子父或子子之间传递变量,提高了开发效率。

二.不使用vuex时与使用vuex时的差别

当我们不使用vuex时,对于组件之间传递信息会较为麻烦。

不使用vuex时

父子之间传递信息:

App.vue文件中:

<template>
  <div id="app">
      <Fruits :fruitList="fruitList"/>
  </div>
</template> 
<script> import Goods from './components/Goods'; export default { name: 'App',
  components:{
    Fruits,
    Goods
  }, data(){
    return{ goodList:[
      {
        name:'doll',
        price:12 },
      { name:'glass',
        price:10 }
    ],
    }
  }
}
</script>
<style>
</style>

Good.vue文件中:

<template>
  <div class="hello">
      <ul>
        <li v-for="(good,index) in goodList" :key="index"> name:{{good.name}} number: {{good.number}} {{index}}
        </li>
      </ul>
  </div>
</template>

<script> export default { props:['goodList'],
}
</script>
<style>

</style>

兄弟之间传递信息:

首先先创建一个js文件作为两兄弟之间传输的纽扣,这里起名为msg.js

//创建并暴露vue import Vue from 'vue';
export default new Vue

兄弟组件Goods:

<template>
  <div>
        <button @click="deliver">点击</button>
  </div>
</template>

<script> import MSG from '../msg';
export default {
  data(){ return{
      msg:'hahah' }
  },
  methods:{
    deliver() {
        MSG.$emit('showMsg',this.msg)
    }
  }

}
</script>
<style>

</style>

兄弟组件Fruits:

<template>
  <div>
      <div class="fruit">
          {{text}}
      </div>
  </div>
</template>
<script> import MSG from '../msg';
export default {
    data(){ return{
        text:'' }
    },
    created(){ this.getMsg()
    },
    methods:{
      getMsg(){
        MSG.$on('showMsg',(message)=>{ this.text = message
        })
      }
    }
}
</script>
<style>
</style>

在App组件中的代码:
在这里插入图片描述
点击按钮:


蓝蓝设计www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务

Tubik Studio 是怎么为华为定制整套 UI 图标的?

涛涛

一套 UI 界面当中,核心的 APP 图标是用户每天都要接触、经常使用的视觉组件和交互对象。设计图标的时候,要用到大家最熟悉的元素才能贴合用户认知,要醒目、统一,也要足够「隐形」,避免喧宾夺主。今天这篇文章,来自著名的 Tubik Studio 团队,他们为华为旗下的 EMUI 10 系统设计了核心的图标系统,来看看他们的设计过程吧。

Tubik Studio 是怎么为华为定制整套 UI 图标的?

我们总不会低估一个操作系统基础图标,对于产品的可用性和合意性的影响。基础图标虽然小巧,但是对于整个操作系统而言,总是极具影响力的,因为他们是用户界面的核心元素,帮助用户快速直观地在系统中定位、浏览、导航。但是,对于设计师而言,图标的设计始终是挑战,它看起来最为简单,但实则为最为艰难的工作。

图标需要,让人一目了然,但是也要具备良好的可识别性,在传统和创新之间达到平衡。这一次,Tubik Studio 设计团队将要给华为的 EMUI 10 来设计图标,而这篇文章将会为你揭示背后的设计过程。

这次的项目主要是由 Sergii Valiukh 、Arthur Avakyan 和 Polina Taran 来负责。这次的设计项目从最初的探索构思入手,逐渐开始绘制草图,探索样式,一直到最后打磨,完成设计。

Tubik Studio 是怎么为华为定制整套 UI 图标的?

项目内容

为华为 EMUI 10 系统的用户界面设计基础的图标

客户介绍

我们在 2019 年夏季,收到了来自华为的邀约,这次的项目要重新设计 EMUI 这套基于 Android 系统的用户界面基础图标,这套图标会用来适配华为旗下的旗舰手机,这些图标将会随着新版的系统部署到这些手机产品当中。我们的任务,是创建总计 54 款符合当下潮流趋势的图标,这些图标要能够贴合品牌和已有用户的偏好,并且能够吸引新的用户。

这个图标设计项目如今已经在当下的华为旗下手机产品中应用且部署好了,最早使用这套图标的智能手机型号为 Mate 30 ,紧接其后的是 P40。

Tubik Studio 是怎么为华为定制整套 UI 图标的?

设计过程

在整个操作系统中,这些图标是始终位于用户视野以内、最基础的最核心的交互元素,通常用户每天都会同这些核心的基础 APP 进行交互,有时候一天多达几十次,因此它们应当具备良好的功能性,还应该足够美观,给用户带来满足感。同时,这套图标的设计,也应当足够统一,以协调的体验切入到整个 EMUI 的设计系统当中,贴合整体的样式特征。

所以,我们使用了一整套图标网格系统,应对不同需求,在设计的过程中,这套网格有助于确保所有图标外部尺寸的一致性,也保证了内部元素的统一性。

Tubik Studio 是怎么为华为定制整套 UI 图标的?

为了发掘全新的视角,我们决定从传统的手绘图标开始,寻找重新塑造图标设计的方法。这些图标所涉及到的元素,早已为全球数百万用户所熟知,要重新设计图标外观,并且还要成套且统一,这本身就是一个巨大的挑战。

Tubik Studio 是怎么为华为定制整套 UI 图标的?

比如「电话」图标所对应的听筒元素、「信息」图标所对应的气泡对话框这样的元素,是不可能完全抛弃重新创造的,所以我们的真正的切入点是在形态和色彩上寻求解决方案。

越是简单的东西,重塑起来就越难。

在实际的设计过程中,我们尝试了数以百计的造型变体,从完全放飞、非常规的的外部造型,到极其常规,大家熟知的造型解决方案,我们都逐一试过。而在色彩上的尝试相对会显得更加具有实验性:我们尝试使用明亮的紫罗兰色、栗色和浅绿色来进行混搭。

当然,我们很清楚,这样的实验性的工作是探索过程中的一小步,但是它是必须的,是创造新设计的种子,是寻求正确答案的必经之路。

Tubik Studio 是怎么为华为定制整套 UI 图标的?

在设计过程在,有一件有趣的事情发生在设计「相机」图标的过程中。我们尝试过很多不同的图标造型,不同的元素,不同的样式,但是其中始终有一个细节是不变的,那就是右上角的小红点。这是为了暗示用户,华为的摄像头模组来自徕卡,这个红色的小点就是向其致敬的标识。

Tubik Studio 是怎么为华为定制整套 UI 图标的?

而下面的概念设计,则是强化了图标之间的几何轮廓的差异,从而提升图标在智能手机屏幕上的对比度和识别度。

Tubik Studio 是怎么为华为定制整套 UI 图标的?

下一步,我们基于几何图形外观差异性,设计了多种造型不同但同样优雅的图标。在基于这种风格概念进行延伸的过程中,我们会优先考虑圆形的元素。而「天气」图标明显既不适合圆形也不适合方形来呈现,所以我们使用的是云和太阳两种元素的组合。「钱包」图标使用的是矩形,然后搭配卡片的组合。尽管造型整体上是相对自由的,但是所有的图标都是遵循图标网格,并且在视觉权重上尽可能统一。

Tubik Studio 是怎么为华为定制整套 UI 图标的?

在色彩和样式上,我们则更加倾向于渐变。没有色彩渐变,纯扁平的图标显得过于幼稚和「古早」,没有足够的品质感,所以,我们在新的图标设计上,开始加入渐变色彩,提升质感。

Tubik Studio 是怎么为华为定制整套 UI 图标的?

不过,在最终版本当中,我们还是保留了图标外部的圆角矩形的外轮廓,然后将图标元素的内径进行了适当地缩减,渐变和核心的简约几何特征依然是整套设计的最高优先级。

Tubik Studio 是怎么为华为定制整套 UI 图标的?

这套设计方案展现了在整套 UI 界面中,图标这个小元素的设计上所需要投入的精力和潜在的难度。想要图标足够协调、美观、易用还要贴合品牌特征、还要区别于以往,是一件相当不容易的事情。

Tubik Studio 是怎么为华为定制整套 UI 图标的?

细节里藏着魔鬼,任何细小的元素、线条轮廓、色彩的变化都可能会在屏幕上放大、被感知到。

文章来源:优设    作者:Tubik Studio

蓝蓝设计www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务

中央气象台近十年设计精品集

前端达人

今天整理网站类作品,看到给中央气象台近十年设计的诸多网页和系统。这几天每天关注鄱阳湖洪水状况,2018年改版时,有一版的设计方案,我设计了洪水到来时的专题,图片视频,动画提醒居民注意事项。同事们也花了许多精力时间做了大量工作。

作为公民,我们在用我们擅长的设计去服务社会,在这个过程中形成自己的积淀。这是不可用金钱来衡量的价值。

微信图片_20200721174610.png


微信图片_20200721174606.jpg


微信图片_20200721174603.png

微信图片_20200721174559.png

微信图片_20200721174555.png

微信图片_20200721174551.png

微信图片_20200721174547.png

微信图片_20200721174543.png

微信图片_20200721174539.png

微信图片_20200721174614.png







作者:蓝蓝

  蓝蓝设计www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服

2020-2021 设计趋势ISUX报告 · 用户体验篇

鹤鹤

It is ultra experience



ISUX Design Trend Report

前言

——————————

身为用户体验设计师,无时无刻不被世界上的新事物冲刷着认知——互联网红利下降带来变化莫测的商业动向、循着摩尔定律野蛮生长日新月异的新技术、各类亚文化群体催生出多元复杂的圈层文化、脑洞口味越来越独特的年轻人,甚至眼下席卷全球的黑天鹅事件......


任何一个新事物的悄悄冒头,都有可能在未知的将来影响着用户体验设计师。我们能做的是,在起初感受到微微震幅时,便沿着震感逐步寻找源头,并思考未来的发展走向。赶在变化降临前先拥抱变化。


本文通过研究近一两年科技、社会文化以及自身用户体验领域的变化,从用户体验领域关键的用户、媒介(设备与应用)、交互行为、信息与场景的五个角度出发,探索用户体验设计未来的趋势,希望能带来启发。


随着人工时代到来,过去机械的单向交互方式逐渐被打破,机器渐渐演化成了会主动“观察”真实场景,“感受”用户情感,预判用户意图并自动完成任务的贴心小棉袄。机器如何为人们提供更智能便捷的服务,未来还有非常大的想象空间。 





随着人工时代到来,过去机械的单向交互方式逐渐被打破,机器渐渐演化成了会主动“观察”真实场景,“感受”用户情感,预判用户意图并自动完成任务的贴心小棉袄。机器如何为人们提供更智能便捷的服务,未来还有非常大的想象空间。



1-1

——————————

基于真实场景推理用户意图

随着AI技术的发展,智能设备可以越来越无缝地将数字世界和物理世界嫁接起来,主动感知用户所处情境并智能提供相应服务。


在2019的 Google I/O 大会上,Google Lens 展示的AR点菜功能可以智能识别用户扫描的菜单并将美食网站上的相关推荐直接呈现在屏幕上。


当用 Google Lens 识别到小票信息时,可快速提取小票上的金额,且可自动弹起计算器快速帮助用户计算人均消费,节省人工计算的时间成本。


随着信息入口从数字空间延伸到周围的物理空间中,未来万物皆可为用户体验的媒介,设计师未来在设计的时候需要注意:


寻找适合的打通真实世界的切入点:在陌生语言、信息复杂或者难以处理等苛刻的环境下,充分发挥智能设备对信息智能读取、批量识别与翻译等强大能力,帮助用户完成任务;


将用户旅程的上下游串联:根据生活常识和经验预判用户行为目的,前置推荐服务;


更加系统细心地考量干扰因素:真实场景是动态变化的,需要更全方位考虑光线的强弱、多源的噪音、实体的可视性、人员和事件的打断等因素。



1-2

——————————

任务自动化,简化用户旅程


为了完成一项任务,用户往往需要借助多个应用来回切换配合,使用起来琐碎麻烦。如今应用越做越强大也越复杂,过去仅仅解决单一场景的解决方案不再适应于用户对于完成任务的诉求。


Google Assistant 的新能力 Duplex on the web 可以通过自动跨应用任务处理来简化用户旅程。只需要用户发出语音指令“预定一辆去某地的车”,助手便可自动跨邮件、日历、付款等应用调取信息、自动根据使用习惯做选择,并自动填写信息,而用户全程需要的只是在关键节点轻敲“确认”即可。


2019年随着 iOS 13 的更新,“快捷指令”推出了“自动化”能力,用户通过“if...then...”语法便可为自己的App设计一套程序,实现如:“当我回到公司时提醒我打卡”、“每天早上10点给我的女朋友发送一条表白短信”等能力,将不相关的场景动作串联字一起自动化执行,大大节省人工操作成本。


提升使用效率是用户体验设计孜孜不倦努力的方向之一。在利用新技术进一步简化用户旅程时,设计师可以充分利用以下因素:


借助语音输入:比起界面触控操作,语音交互的直达性可以“穿透”复杂界面,让设备第一时间明确用户目标;


基于用户行为形成习惯记忆:对用户长期重复的行为做分析处理,构建用户习惯模型并主动提供服务;


适当考虑专家级用户:随着部分用户的智能设备使用水平越来越高,可以考虑为专家用户提供自定义操作脚本,满足其自身的独特需求。



1-3

——————————

基于情感感知,主动理解用户需求


随着人脸识别、表情识别、肢体跟踪等技术的提升,机器逐渐学会感性语言,主动感知用户内在情感和心理需求。


2019年1月的CES展上起亚亮相的互动式“情感驾驶空间”技术,可通过传感器读取用户的面部表情、心率等反应,调整驾驶空间内的灯光、影片类型、音乐风格等,舒缓舱内乘客心情,由此提供更人性化的出行体验。

用户总是会期待更贴心的服务,设计师未来对同理心的情感嗅觉更加敏锐:


利用感性线索定位用户情绪:需要通过面部表情、特殊时间节点或者识别到的关键词,对用户情绪进行理解和定位,判断用户情感理解用户内心诉求是自由探索、趣味娱乐、或者静谧修行并提供符合用户当下心境的服务。


综合使用感性元素进行设计:通过使用线条、色彩、声音和动作等传达并唤起相对应的情感,提供更加人性化的体验。


小结

更智能的服务提供方式会让人们生活拥有更多可能性,但一旦火候把握不得当,可能就会造成对人们生活的野蛮入侵。关于如何让科技更好造福于人们,早在上个世纪,施乐帕克研究中心提出了宁静技术(Calm Technology)的愿景,认为影响最深远的技术应该是隐匿不见的,它们如纤维般融入日常生活,丝丝入扣,直至不可分辨。


随着科技的发展,设计师对新技术不应是不加克制地应用,而应该润物细无声般地提供服务,帮助人们从繁杂喧嚣的数字世界中解脱出来,将宝贵的注意力资源投放在让生活更美好的事物上。



回顾人类和机器的交流语言,从命令行界面、图形用户界面到自然用户界面,人机交互方式越来越贴合人与人之间更自然的交流方式,其背后是心智模型与实现模型的高度拟合的趋势。


在自然用户界面中,为满足新形态智能硬件对新接口的需求,以及人们对更丰富强大的交互方式的自然诉求,越来越多的自然用户界面被开发出来。语音交互和隔空手势交互便是近几年迅速发展并落地的两种交互方式。



2-1

——————————

隔空手势交互:更自由、更灵动


为了让机器更好地读懂用户的身体语言,能够感知深度信息的摄像头走进了日常手机。2019年国内外手机厂商的发布大会上,LG 手机 G8 ThinQ 以及华为发布 Mate 30 系列推出的隔空手势,可实现一些简单的诸如滑动、切歌、截屏等效果。



除此以外,隔空手势支持更加细微的手势,如旋转、揉搓等,可以更直观、更灵活的方式操控界面,让用户获得一种像魔术师用意念控制事物运作的快感。



对于隔空手势操作网上的言论褒贬不一,其中争议性最大的就是隔空手势宛如“杀鸡用牛刀”,明明可以用更加精准的手势触控,为什么还要用看似很酷炫其实精准度更低的隔空手势操作?


隔空手势并不是要替代触控手势成为主流的人机交互方式,更多是对情境式障碍场景的补充。在某些场景下,用户使用设备的条件可能是充满干扰的。想想看当你边看手机食谱边炒菜的时候、边煲剧边剥小龙虾的时候、疫情期间出门佩戴橡胶手套无法正常触控手机屏幕时.....隔空手势是不是特别好用?


每个人在特殊的场景下都有可能面临感官障碍,未来的设计也应该更多地考虑情境式障碍的场景,让用户无论身处何时何地依旧能一如既往无障碍地使用设备。




2-2

——————————

语音交互:更精准、更好玩

语音交互作为更趋近于人与人之间最自然的交流方式,近些年有许多发展的突破点。


在发展主线上,语音交互趋向更自然、更人性化、更个性化。过去反人类的一些沟通方式慢慢被“调教”。此外,多人会话场景下的技术方案日渐增多。


2019的 Google I/O 大会展示了一个视频片段,视频中的两位嘉宾相继吐槽,经常出现针锋相对难以听清的时候,这时用户可以调节音源音量选择性增强自己关注的人物声音,让另一个人“静音”。


  滑动选择音源


此外,语音交互除了在智能音箱领域广泛应用以外,也逐渐应用在广告等更多的传播媒介中,刷新人们日常使用体验。2020年2月索尼提交了一项广告播放新专利。当用户在观看电视节目时,如果出现广告,只要站起来大喊广告中对应品牌的名字,便可直接跳过这个广告。


设计师在语音交互场景下,需要留意以下几个比较容易被忽视的因素:


用户语音交互习惯培养:如今还处于培养用户语音交互使用习惯阶段,设计师需要更多地考虑应用的语音交互规则如何才能更趋近于人们日常的沟通习惯,并进一步为人们的社会习俗所接纳。


真实场景下的多人音源:在现实情境中, 在多人对话场景下将面临音源不清、穿插停顿、噪音过多等影响体验的情况,由于计算机听觉分析能力开始从单人音源拓宽到了多人音源,多人对话解决方案上还有很大想象空间。


改变传统的视听体验:在使用场景上,语音交互接口也将逐渐运用到更多的媒介上,更全面地刷新用户体验。



小结

人类拥有双手、眼睛、耳朵和发声的嘴巴,但是并不总是在每个使用场景下都能自如地使用:在安静的自习室下声音收到限制,在驾驶场景下注意力受到限制,在双手拎着东西场景下双手受到限制......但目前许多产品设计都建立在用户能完整使用感官功能这一理想化的基础上。


未来的发展趋势倾向于将视、听、触、嗅等多通道信息完美整合起来,综合使用多种输入通道和输出通道,根据用户使用场景用最恰当的方式传递服务,满足用户多方位的需求。




尽管乔布斯曾断言3.5英寸是手机的黄金尺寸,但作为人们日常内容消费与娱乐的窗口,手机屏幕毫无疑问地变得越来越大,甚至超出传统物理限制。人们对大屏享受的追求与设备携带便捷性之间的矛盾由来已久,硬件形态的变化对旧有的用户体验设计思路带来的新的挑战。


3-1

——————————

大屏幕:单手持握新挑战

屏幕横纵比越来越大,而人类的手部具有先天限制,曾经惯用的界面布局方式在高横纵比的屏幕上可能无法被大拇指无障碍全覆盖,使得越来越多的设计更加重视利用移动屏幕下半部分。


操作与信息进一步下移


高德地图、苹果地图的搜索框下移,方便单手操作用户快速激活输入框;


影视资讯平台IMDB强化底部标签栏功能,双击“搜索”tab即可激活输入框,无须艰难地触摸顶部。


即时战斗类手游皇室战争的说明卡片主要展示在下半部分,方便用户进行卡片上的相关操作。



底部导航被赋予更多能力

Pocket的底部标签栏现在兼任汉堡菜单功能,在激活状态下再次点击主页icon可选择主页上须展示的内容。


利用下滑手势代替点击

Snapchat的许多表示前后进退关系的页面都不是”返回“按钮,而是向下箭头,用户可下滑退出当前页面。







3-2

——————————

折叠屏:新形态的交互方式

为了解决设备形态和人类手部先天限制之间的矛盾,折叠屏诞生浏览并颠覆旧有的界面设计方式。


更灵活的信息布局


过去在单屏设计下,考虑到用户注意力由上到下纵向衰减,因此信息布局更多是按照优先级从上往下排序。而折叠屏中,屏幕展开后便可以开辟出更大的可利用空间,将次级页面或者较为重要的内容曝光在第二屏,对信息的布局将带来全新的变化。设计师为保证大小屏下顺畅的阅读体验,需要对信息模块在不同空间布局下的流动性有更强的把控能力。


更便捷的多任务操作


在过去的单屏体验中,用户只能将注意力完全集中在当前的界面中,一次只做一件事。但在实际生活中,用户面临的情景往往是主线任务和支线任务的频繁交错,并且根据会任务不同的性质自由调动自己的注意力重心,如边看视频边聊天、边看直播边逛街等等。在折叠屏中,设计师可以探索更多主线和支线交错进行的场景,利用折叠屏带来的更大的屏幕空间,可以让用户在不离开主线场景的基础上进行支线任务的处理,大大节约了在不同App上来回切换的操作成本。



更直观的拖拽交互


此外,随着多任务处理越来越广泛使用,拖拽交互将成为重要的交互模式之一。文本、表情包、图片、视频等交互对象,不再需要经过复杂的分享转发流程才能在不同App中流转,通过拖拽的方式可以更直观地进行交互。




双面屏互动玩法


外折叠屏在折叠状态下可转为双面屏,等于是给用户增加多一个观看视角。例如华为 Mate X 的镜像拍摄可以让被拍摄者即时获知自己的镜头影像是否满意,这一拍女友神器有望成为直男拍摄终结者。在未来更多的多人观看和互动玩法将被开拓出来。


 华为Mate X 的镜像拍摄


未来随着5G通讯技术的成长,越来越多的设备可以同时加入物联网,人们的生活将被各种智能设备围绕,设计师需要参与更多屏幕外的设计,让不同设备串联在一起协同合作,让用户能更加自在地享受科技的便利。



席卷全球的新冠疫情让数十亿用户乖乖待在家里。过去需要花费大量精力去教育的用户使用习惯因为疫情纷纷转变。云购物、云蹦迪、云赏樱、云监工......人们足不出户便可还原许多线下场景。随着用户线上和线下生活的界限进一步模糊,用户对于应用的效率和情感诉求也发生了变化。



4-1

——————————

更关注效率导向

疫情让远程办公学习需求剧增,多人协作场景越来越频繁,许多企业随之升级了电话、视频会议、文档制作等多人协作效率软件。过去仅仅考虑少人场景协作的方式不适用,设计师需要比以往更多地考虑多人协作场景下,如何对海量密集的信息进行分析处理和展示。


在学习方式上,由于线下学习转移至线上,学生群体对于娱乐向软件也有了效率诉求。为了顺应用户诉求变化,2020年5月QQ推出学习模式,屏蔽娱乐性的内容推送,让学生更专注在学习上。



4-2

————————————

更注重缓解社交疏离感


除了效率诉求急剧提升以外,随着长时间的线上学习与办公所产生社交疏离感和缺失感,人们对于线上学习工作的情感化诉求也进一步增强。


2020年推出的plagi远程办公软件支持设置每个人的avartar形象,让大家在远程办公时依旧能时刻感受到彼此的存在。在完成任务时还可以放鞭炮庆祝,让员工能感受到亲密无间的线上办公体验。


设计师需要更加关注如何让线上生活进一步与现实生活圈和时间线接轨,通过拓展真实社交下的更多伴生行为让线上也能还原线下的真实场景细节和互动体验,以弥补用户对真实社交的缺失感。



4-3

——————————

加速人和信息的强连接


疫情的发生加速了人与信息之间的连接。人们越来越习惯将自身的身体资料、心情状态等信息沉淀在智能设备上。


为了做好广大市民群众的健康监测服务,辅助疫情防控工作,微信和支付宝在2020年年初都上线了健康码服务,不同颜色的健康码代表人们不同的健康情况,市民出入特定场所都需初始健康码。



随着人的数据化越来越深入,个人身份信息的线上化在各平台上将成为更加通用的能力。设计师需要考虑如何更自然更低成本地将线下动态变化的资料信息线上化,更有效地对用户信息进行加工处理,以及记忆用户的使用习惯和行为,以便帮助用户更地完成任务。


疫情的出现加速了线下生活线上化,短短时间内我们看到日常习以为常的应用为响应疫情下的特殊需求纷纷出现改造,钉钉、QQ群被改造成上网课、批改作业的地方,医疗卫生公众号开辟了实时疫情播报与辟谣通道,无接触设计和服务需求异常突出......这也启发了设计师需要保持对突发事件的敏感力以及应急能力,在日常生活中留心思考,为日后突发事件提供充足的场景支撑。




在汹涌的资本语境下,互联网设计师裹挟在商业驱动的结果导向中狂奔,对设计的伦理和责任鲜有发声,但伴随着互联网红利退潮,发展放缓,狂奔之下的人本问题也逐渐浮出水面。在大趋势下,UX设计师需要培养自身设计对伦理和责任的敏感度,在满足商业目的外,重拾节操,为多群体,为大社会设计,更加注重“以人为本”。




5-1

——————————

更包容性的设计

包容性设计师指在做设计产品的时候,考虑到各类用户的诉求,输出具有包容性的设计方案。包容性设计依旧是2020年设计主题之一,伴随着互联网产品全球化,在通用性和包容性上也提出了新的要求。




为身障人士设计


三星在2019年针对东南亚市场推出了一款让聋盲人士和健全人实时交流的app:Good Vibes,盲聋人轻击屏幕输入摩斯电码,预先连线好的另一台手机就会显示从盲聋人发来的短信。健全人用普通的文字输入回复,在盲聋人这一端就会翻译成摩斯电码、以手机振动的方式读出短信内容。

   GOOD VIBES宣传视频


饿了么:在饿了么送货骑手中,约8%受色盲色弱的困扰(全国男性群体中红绿色盲色弱占比达8%-9%,饿了么骑手男性占比90%),为此饿了么设计团队在2019年对app的进行了重新设计,包括使用WCAG无障碍色彩对比度,以及无障碍色盘,以及调整字阶,使用辅助图形等设计手段来解决部分骑手在送货途中使用APP的痛点问题。


饿了么UED:《为骑士创造平等 — 配送 App 的包容性设计》



跨年龄段设计


谷歌助手礼貌功能 ( Google Pretty Please ) :开启谷歌助手礼貌功能后,如果使用者在下达指令的语句中包括“Please”,谷歌助手会对礼貌的请求表示感谢,以此培养孩子的礼貌言行。

Google Pretty Please功能宣传



Swift Playground:当10后小学生VITA君的编程课被“可敬的发量”刷满弹幕时,Swift playgrounds功不可没,这款为儿童新手学习编程的软件,用趣味的游戏方式为4岁以上低龄用户提供了一个学习编程的低门槛平台。



为性别平等而设计


苹果emoji回看历年苹果emoji的更新,从肤色平等,到性别、性向平等,再到为残疾人设计,2020年再为跨性别者增加新表情,性别平等依旧是包容性设计中重要一环。



Airbnb插画:爱彼迎在插画系统中,也为不同肤色,不同职业,不同性别,以及身障人士进行了人物的绘制。






5-2

——————————

关注用户隐私

2019是互联网科技隐私问题沉浮的一年,国外有Facebook因泄露隐私收到史上最大罚单,国内则打响了“人脸识别第一案”。笼罩在隐私信任危机下,个人信息和数据立法突飞猛进,美国推动《加州消费者隐私法案》,国内也将在2020年出台《个人信息保护法》和《数据安全法》。


MIUI12推出隐匿面具功能


Android开放生态导致的权限隐私问题一直被用户所诟病,某些APP存在用户不授权就无法使用情况,针对这一情况,MIUI12推出了隐匿面具功能。当用户在开启某些APP要求授权权限时,可以选择空白通行证进行授权,从而保护用户真实信息。


   在MIUI12的更新中,还推出了照明弹、拦截网两项隐私保护功能


iOS 14剪贴板提醒


在iOS 14的更新中,保护用户隐私方面进一步升级。

其中剪贴板提醒设计很贴心,当用户打开应用,如果该应用读取了你剪贴板的内容,会在系统顶部弹出提示,用户能在第一时间意识到剪贴板内容被读取,帮助用户更好的保护自己的隐私内容。




5-3

——————————

健康的数码生活方式


科技的发展是一把双刃剑,互联网产品的发展给用户带来便捷和沉浸体验的同时,也使得用户沉溺于科技所带来的惰性和投食之下,逐渐丧失了对真实生活的把控权,被科技绑架。


数字福祉(digital wellbeing)近年被频频提起,指科技产品需要权衡好数码产品和真实生活之间的平衡,防止数码产品过渡分散用户的注意力而影响生活质量。


Android Q 专注模式  Google Android Q Focus Mode


Android Q的更新加入了专注模式,用户在专注模式下,可以在系统层面快捷地关闭使你分心的应用,让你聚焦于更重要的事情。



防沉迷系统升级


推荐技术的进步,产品体验的升级,给用户带来了更合胃口的菜式和沉浸体验,但同时也被冠上了“电子海洛因”的称号。游戏或者内容产品的防沉迷系统依旧会是数字福祉下不可避免的趋势。


王者荣耀在2020年升级防沉迷系统,对青少年的娱乐时间和点券充值的限制进行了进一步升级。承接话。B站在2019年推出青少年模式,在该模式下,使用时长和内容推荐等做了定制化处理。






2020年的UI设计趋势,一方面是对往年风格的衍变和细化,另一方面,在扁平克制的界面风格盛行后,设计师们向往更自由、更突破的视觉表达。



6-1

——————————

深色模式


2019年iOS 13深色模式姗姗来迟,紧接着大厂APP相继推出此功能。在2020年,深色模式会继续普及外,也会在可视性和实现成本方面有更多细节打磨和研究。





6-2

——————————

新拟态

设计趋势的发展是螺旋式上升的,在扁平化设计流行之后,对物体的拟真再一次回归设计圈,新拟态以一种对旧拟物风格的再创新,重新流行起来。


新拟物风格(Neumorphism)缘起于设计师Alexander Plyuto发布在dribbble的一组作品,以投影重新对扁平界面进行了塑造,模仿出类似浮雕的视觉效果,感受耳目一新,引起大量设计师相尽模仿。


新拟态的实用性和可落地性有待商榷,但是作为一种新的风格受到设计师拥趸,也不失为下一波风潮到来前的灵感缪斯。



WWDC2020对mac OS的更新也重新定义了新拟态设计语言,在mac OS新系统Big Sur中,图标的设计增添了轻微的渐变、投影、高光,以此来营造图标内元素之间的纵深关系。




6-3

——————————

多彩配色


在扁平简洁UI风格盛行之后,丰富的色彩依旧是设计趋势之一,大面积色块,碰撞配色,带来更具冲击感的视觉体验。





6-4

——————————

字体装饰化


UI界面逐渐扁平,色块图标弱化,为突出页面重心和内容,iOS 11在界面标题上使用更大的字号,更粗的字重。近年在大标题的风格衍变下,文字在传达信息外,也开始有了装饰性作用,采用超大字体,成为页面排版美化的一部分。





6-5

————————

更大圆角

大圆角的风格会继续延续,相较以往,卡片的处理圆角会更大,随之带来的是多的留白处理,结合大字号,带来更透气通透的视觉感受。



Mac OS Big Sur的界面相对旧版本采用了更大的圆角;系统图标的设计统一成圆角矩形。





6-6

——————————

更丰富的插图


UI插图的丰富体现在样式和内容上,样式上开始3D化,内容上更注重插图叙事的表达。


3D插图


3D图形往年更多运用在动态影像或运营类设计中,随着3D的普及运用,UI插图也会迎来3D化,给用户带来更立体,更新鲜的视觉感受。




讲求叙事表意


相较于往年追求形式的UI插图,新趋势下的插图更讲求功能性,每一副插图都承载一定的作用——传达功能信息或透传品牌情感;同时插图更讲求画面表意和情节,给用户叙事性的视觉体验,增进用户和产品之间的情感联系。




插图组件化


插画的流行,随之而来的是成本的水涨船高——一套系列插图为保持风格统一,往往由唯一设计师绘制,同时为兼容各类场景,设计师往往要绘制多张。


为解决插图的成本和效率,插图开始以组件化的方式进行绘制——插图设计师将插画进行拆分绘制——不同人物,不同场景,不同物件等,再通过组件化的拼接合成,使用组件的设计师可以根据需求场景自由组合,也避免了风格不统一问题。


设计师Pablo Stanley将日常绘制的插画制成一套矢量插图组件库,将人物分为:半身、全身和坐姿3大类。通过不同表情、发型和服装可自由搭配出近60万种组合。


Pablo Stanley人物插画系统




6-7

————————

多维度动画表现


新趋势下,动画一方面回溯复古线描手绘风格,另一方面追求更三维的体验,同时帧率进一步提升,追求更流畅的视觉感受。


手绘动画


手绘插图是往年的热门,其随性自然的笔触,能给用户带来亲切的感受,在新的趋势下,动画的加入赋予手绘插图一份灵性和趣味。


3D运动


Google Material Design通过卡片投影层级和二维动画规律,赋予扁平界面Z轴的纵深感。随着3D的普及流行,新趋势下的界面,界面的运动从二维走向三维,表现出3D场景下透视感。



高帧率动画


高帧率影视从线下电影院移步到线上流媒体,手机高帧率屏幕从90Hz到120Hz逐步升级,用户对画面流畅的定义一再刷新,UI动画的帧率升级也会是新的一轮趋势。


Telegram的表情采用了高帧率动画,给用户更流畅的视觉感受。








体验的持续升级,产品的高速迭代,对UX设计师的设计师效率提出了更高的要求。的设计方式是一个永恒的趋势。




7-1

——————————

从本地文件到云端协作


传统的文件交接方式效率低下,导致设计师之间信息不对称,最终影响产品的一致性体验。近些年在线设计协同工具发展迅速,从UI设计、 设计交付以及组件协同等环节上给设计师提供更加实时的协作体验,获得大量UX设计师的簇拥。在2019 uxtool的设计工具调研中,在线设计协同工具佼佼者figma以其协作和性能优势,大有追赶sketch之势。


随着团队对设计效率要求的提高,设计文档从本地走向云端协作是不可逆趋势。不过设计工具的迭代是需要成本的,尤其在大型设计团队,设计工具需要渡过阵痛期来完成迭代,进而提升设计效率和体验一致性。



7-2

——————————

科学有效的设计系统


UX的发展,从早期的静态规范到当下的动态设计系统,是为解决产品迭代增速后带来的设计效率和产品体验问题。商业驱动下的产品迭代速度有增无减,设计系统依旧会是未来几年的设计趋势之一。


这里说的设计系统不是广义上的设计系统,而是在互联网设计的发展中,对组件化设计逐步迭代升华的一套设计协作方法:


“设计系统(Design systems)是一组为了共同目标而服务的内在相互联系的设计模式和多人协同执行的方法。”(引自《Design systems》,Alla Kholmatova,C7210翻译)。


设计系统历程衍变


组件化的发展历经规范文档到UI组件,再到设计系统,形态从最初对设计一致性的指导规范,到对产品研发流程的规范,以及产品设计价值观的输出,当下的设计系统以集大成者形式影响整个产品的设计形态。


设计系统的结构见下图:



设计系统的求同存异


设计系统并非一成不变的,他是一个动态进化的系统,会根据团队性质、产品特性在内容上有所区分——比如大团队更应该大而全,小团队更倾向小而精;成熟产品的设计系统更倾向于打造完整闭环的合作流程机制,新产品的设计系统应该以小为始,快速迭代……


随着产品的垂直化,细分化,设计系统的趋势会是在趋势大同之下找到适合产品和团队自身的形态和节奏。


Material Design是一个包含了指导规范、组件,以及设计开发工具的自适应性设计系统。


它作为平台型性设计系统,更为大而全的规范了整个生态系统的设计风格,以及提供工具让研发者能快速产出符合规范的产品。


  Google生态庞大繁杂,Material Design更为全面


Ant Design作为一个为to B产品提供解决方案的平台,更多从设计可用性和完整性考虑设计系统的搭建。


Ant Design通过模块化解决方案,降低冗余的生产成本,让设计者专注于更好的用户体验


QQ作为一款面向95后的2C社交产品,其设计系统Q语言从风格调性上对设计进行规范,同时给予设计师一定的自由度;也考虑到QQ内兼顾多个产品,以及界面主题样式,对基础组件的使用场景和代码进行了规范,方便设计和开发敏捷开发。


Q语言,给予产品的自由调性之外,也针对主题和基础组件进行了规范


每个产品和团队都有自身的特征,设计系统的建设也应该有的放矢,没有可照搬的标准答案,在大方向下找到适合自身的解决方案才是的可行之道,将效率最大化。


科学有效的优化迭代


组件是设计系统中的重要组成部分,但是以往静态的、孤立的协作方式使得组件的更新迭代滞后和阻塞。随着设计系统的发展,设计师组件化思维的普及,组件的更新需要更科学的方式进行管理。


Figma在2019年推出的Design System Analytics功能,组件设计师可以借此查看组件的使用情况,包括引用次数,解组次数等,并可以生成组件使用情况的曲线趋势图,以数据的形式,科学地推动组件的优化迭代。


 1.选择分析的时间段;2.组件使用的次数曲线图;3.团队使用情况;4.所有组件使用情况



后记

未来的用户体验会出现什么新趋势?人工智能等算法的发展、5G技术普及、新的智能设备形态、新的信息处理技术、新一代用户的喜好和口味......这些往后或将影响用户体验发展的走向。未来用户对体验的要求只会越来越高。


用户体验设计师需要了解更多的技术动向,但安身立命之本还是让用户真正受益:立足于用户真实使用场景,在理性价值层面上,打造可用、易用、的设计;在感性需求层上赋予情感上的愉悦性,在反思层面赋予意义价值。




文章来源:站酷    作者:百度MEUX

蓝蓝设计www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务



日历

链接

个人资料

蓝蓝设计的小编 http://www.lanlanwork.com

存档