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 与 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);
// 新增以下
使你的代码可读和可维护的快速提示。
有多少次,你打开一个旧的项目,发现混乱的代码,当你添加一些新的东西时,很容易崩溃?我们都有过这样的经历。
为了减少难以读懂的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界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
在多方合作的项目中,我们需要规划项目的合理落地方案,并在执行过程中和各方有效沟通。结合近期羚珑商家合作测试项目的实践经历,聊聊我在项目中的心得体会。
我们想了解京东首焦banner中,设计因素对点击效果的影响情况。做这件事最大的限制在于投放数据会受多种因素影响,不仅是一个设计因素的选择,还有比如人群、出价、品牌、类目等因素的不同,都能影响到最终的投放结果。因此,从总体样本中取样进行分析的意义不大。为了解决上述问题,我们招募了一些能控制其他因素的品牌广告主,与有能力支持投放的测试投放系统合作,共同完成A/B测试。
不难发现这个需求会涉及到多个项目角色,意味着将有复杂的推进流程,需要提前拆解与规划,才能保证项目进度可控。
△ 测试项目流程与分工
我们可以分三步来进行:
1. 找到项目关键步骤
关键步骤是将一个项目的推进拆解成多个阶段,找到关键步骤,也就知道了每个阶段的主要任务,可以进一步分工来推进项目。
如何找到关键步骤?我们可以从项目目标中提取。测试项目的目标是找到设计因素与投放点击效果的影响关系。这个目标里提取重要的关键词为:设计因素、投放点击效果、影响关系。面对这些关键词我们会有疑问产生如「测试设计因素是什么?有哪些?」、「如何投放」、「如何判断影响关系」,再进行串联梳理,得到完整步骤流程。
2. 明确角色任务与产出
每个阶段的主要任务,可以拆解分工给项目角色,项目角色也就有了各自的任务,角色产出物则是任务执行的结果,将直接推动项目进入下一环节。所以为了顺利推进项目,我们需要根据项目实际情况评估角色任务分工是否合理,产出是否满足项目要求。
比如在「设计测试图片」这个步骤中,有两个分工方案,让广告主或我们自己设计图片。我们从项目时长、沟通成本、潜在风险等维度进行评估,判断广告主把控变量不严谨会导致测试数据无效,且外部合作沟通成本较高时间不够,所以最后将设计图片的任务分给了内部。
3. 预判项目风险
对流程规划得越详细,在后续合作过程中越容易把控项目的节奏,项目风险越低。
再如「设计测试图片」这个关键步骤,详细展开执行流程会发现涉及到交互设计师、两方视觉设计师、广告主四个角色。对于项目执行角色,产出物的质量及按时交付尤为重要,所以重点把控输出准确性与完成时间。对于项目决策角色,也就是决策最终投放哪些图片的广告主,沟通配合尤为重要。所以基于规避风险也对项目流程进行了优化,前置在项目之初,要求广告主提前准备交付内容,规定交付时间及格式,以及充分沟通说明图片设计产出之后不能进行较大改动。
1. 保证数据有效
测试结论来源于测试数据,我们首先应该保证回收数据的有效性。测试项目的关键指标是点击率,即点击量与曝光量的比值。当图片本身曝光量低时,我们认为随着曝光量增加,点击率比值波动范围仍然较大,数据还未稳定在某个区间段,会影响结论准确性,判定为无效数据。发现类似这种问题,我们会和广告主商量继续投放,延长测试时间来增加曝光。
2. 充分测试
要想得到可靠普适的结论,需要对比多组样本的测试结果。对于某个设计因素,我们先进行了单一广告主的投放数据对比,可以找到投放效果最优和最差的设计水平。然后又将多个广告主的结果进行对比,会发现存在不一致的情况,验证了单组样本结论不能作为类目结论输出。如果多个广告主结果一致,或呈现某一趋势,则结论在该类目可以认为较为普适。
3. 差异分析
对于多组样本结果不一致的情况,可以从组间因素差异着手分析。
△ 背景设计因素测试结果
上图是两个广告主分别测试背景设计因素得到的结果。第一组的投放结果为实景设计效果更好,第二组则为普通平面设计效果更好。产生这样差异的原因是什么呢?我们先找出组间可能导致这一结果的因素,分别是颜色和产品。从颜色上看,是否平面+黑色效果更好呢?我们看了其他广告主结果,否定了这一猜想。再从产品图来看,两个产品的识别度是不同的,我们将其他广告主该因素测试图按产品是否容易识别分组,最终得出两组不同的结论:当产品图易识别时,背景设计对效果影响不大;当产品图不易识别时,实景图效果更好。这也就解释了上图结果不一致的原因。
回顾整个项目,我个人认为项目中最重要的工作是沟通。下面我来分别谈谈内外部沟通的经验。
与外部团队、广告主合作推进项目,需要及时有效的沟通,什么是有效的沟通呢?
1. 围绕对方利益谈配合
广告主只看转化结果,我们如果只谈设计不谈转化,广告主是不会想要出钱参与项目的,那么项目也将停滞不前。所以在合作前,我们做了项目及团队包装,用真实的案例让广告主快速理解参与项目所能带来的价值,并用团队以往的作品、能力展示让广告主了解与我们合作的优势。广告主越认同项目价值及我们的专业度,配合度就会越高。
△ 招募PPT中的真实案例介绍
2. 围绕对方目标来「推销」方案
在推进项目这一点上,找到目标一致的点更容易促成各方意见达成一致。
我们合作的广告主很多会选择外包banner设计,所以广告主们习惯了做传说中的「甲方爸爸」,难免会对视觉设计方案有各种主观意见。比如,某电视广告主不喜欢红色图片设计,想要蓝色。对于这样的分歧,我个人喜欢用引导式的沟通方法。首先,不要急于表达自己或否定对方的意见,可以以疑问句式来猜测对方的目的。「喜欢蓝色是希望用沉稳的颜色来表现产品的高级感吗?」,如果猜测正确,对方会因自己的需求被理解而更容易沟通,这时再继续阐述用红色如何表达高级感;如果猜测不正确,对方会顺势说出原因,我们再围绕对方的目标进一步沟通。如果对方毫无理由「不喜欢这个设计」,而设计师并不觉得有设计问题,那么我们需要停止专业角度的沟通,从设计是否满足当前阶段项目需求的角度来沟通,方案则更容易被接受。
内部合作分交互、视觉、用研三种不同的岗位角色,在项目中都发挥着自己重要的作用。
交互擅长统筹与拆解问题,团队的内部沟通能产生多种解题思路,对项目的推进至关重要;在遇到如需要统计学分析、样本设置等专业问题时,用研可以提供更专业的分析方法与帮助,为我们实现科学分析提供有力支持;视觉设计师对banner设计有丰富的经验,可以在各个环节中发挥优势。从项目之初就参与设计因素拆解、测试方案设置,可以补充视觉相关的设计因素,在优化测试方案、风险预期方面都可以提供重要的建议。在结论推导中,对图中的因素差异敏感度高,可以根据数据提出关键猜想。
文章来源:优设 作者:京东设计中心JDC
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
色彩在UI设计中的作用:加深品牌印象与品牌感、引导用户视觉凹增加易读性、区分信息交互的状态、营造氛围传递热度……
不管是做 UI 设计还是画插画,有很多同学觉得自己是因为天赋不够所以对色彩的敏感度不够,其实不然。一个可能是大家总结的太少,从来都是凭感觉和运气去配色,但配色都是有讲究的。
一个设计作品呈现到用户面前,第一眼进入眼帘的就是产品的视觉表现,而产品的色彩在其中起到了举足轻重的作用,毫无疑问色彩搭配对于设计师来说是非常重要的。那么具体到实际项目中该使用什么怎样的色彩,需要怎么做呢?
用户界面是一个设计师用理性思维解决用户感性需求的窗口。如果对色彩的运用不加以克制,界面可能会显得花哨而没有主次;但过于拘谨又容易使界面保守,难以激发用户情绪,下面以Bee express项目的实例来理性推导制定一套色彩系统。
做过设计的同学应该都知道颜色模式:RGB、CMYK、Lab 等等,这里不做过多的解释了。另外每个颜色具有一定的性格特征和表达方式,而且都会有正反两面。虽然每种色彩都有正向性格特征,但是我们在定位主体色之前一定要知道所选择色彩的负面特征对企业是否会带来负面的影响,
开始之前我们需要了解在配色过程中需要避免出现的问题,如果你经常出现下列的问题,保证你在试用期内一次性就能拿到全部薪资,emmm……
虽然设计是相通的,但是在不同的设计领域进行配色时,依然会存在巨大的区别。更换品牌的主体色,都不会是因为设计师自己的决定,而是公司在商业策略上优先做出了调整,然后通过品牌视觉上的变更将这个信息传递给消费者。
Bee Express快递、速递柜业务为主,前期的主色及视觉形象以橙黄色为主,为了避免视觉跳跃性太大,以及后期IP形象(蜜蜂吉祥物)打造,本次品牌色彩升级在原有基础上优化了色调,以保证后期产品的易用性和延展性,并利用最科学、最适用的方式推导出辅助色,以提升应用视觉的丰富性和感官体验。
express原主色:
为了不影响原有色调前期的视觉传播,即在原有主体色的基础上调整SHB的数值,让色彩更具视觉冲击力,在色彩衬托(字体、图标)更清晰。
通过调整后的主体色也能看出,明亮清晰的主体色(品牌色)也更适合在界面中的运用,为信息传递、引导操作、品牌价值带来更大的提升。
同色系为统一的色相,使用中可以加深品牌色的感知,可以让界面更有层次,同时可以让界面保持色彩上的一致性,整体感较强,产生低对比度的和谐美感,给人协调统一的感觉。
具体是指与品牌色 H(色相)一致,通过改变 S(饱和度)与 B(明度)变化产生的色组。分别往浅色/深色方向按均匀数据增减,各产生5个坐标值。
综上能看出,使用同一色系即可完成一个项目,但是对于中大型项目来说实在是过于单调,没有太多的层次感,因此我们需要多色搭配为辅。多色的辅助颜色可设定不同的任务属性和情感表达,再搭配中性色黑白灰,能赋予更多的变化和层次。
根据主体色 H(色相)为基础,不断地递增、递减 15,在 0-360 之间可以得出 24 个颜色,也就是将 360° 色环分割为 24 份,(24份在360°色环上,每一个色相的角度为15°),最终得到下图24色。
辅助色需要满足的两个条件:
和品牌色有明显区分:避免所选辅助色感官上给用户视觉区别与品牌色差距不大,传递的调性太过一致;
不能过于突兀:根据色彩原理,互补色是最能与品牌色本色产生视觉感官对比的颜色,但可能会有些突兀。为了让颜色的辅助起到丰富画面的作用,而不是反而让整个版面显得不和谐,所以选择互补色的邻近色作为辅助色,避免直接使用互补色。
基于品牌色可衍生出 3 个辅助色:一个与品牌色传递调性有明显区分的类似色;两个互补色的邻近色。
类似色搭配:使用色相相近的颜色,页面元素不会相互冲突,更加协调有质感。
互补色搭配:选择使用互补色,最佳搭配是一种作为主色,另一种用于强调。它们有着非常强烈的对比度,用在需要特别强调某个元素时会非常有效。
每一种颜色都有自己的「感官明度」,也就是发光度。根据现有的使用场景,类似色和互补色大都用在同层级的信息展示上,而当我们将最终得到的辅助色摆放在一起之后发现,虽然我们提取出的辅助色明度色值都一致,因为颜色本身自带的感官明度属性有所区别,导致视觉上会有明显的明暗差别。需要通过发光度来进行最终的颜色校正。
校准方式:依次在辅助色上叠加一层纯黑图层,将该纯黑图层颜色模式调整为 Hue(色相),就可以通过无彩色系下的明度色值,进行对比,使色彩视觉感官保持一致(青色和蓝色属冷色调,固需加深)。
根据上面同色系的明度、纯度对比规则,对所有定义的辅助色进行明度和纯度的辅助色彩输出,最终得到辅助色色板。H(色相)一致,通过改变 S(饱和度)与 B(明度)变化产生色组。分别往浅色/深色方向按均匀数据增减,各产生5个坐标值
删除最左侧的3种同色系,因明度过低时,颜色已经非常接近于黑色,色相在肉眼上几乎已经趋于一致。最后得到基于品牌色推导出的全色系色阶色板。
配色常常从确定主色开始,根据行业类型和视觉诉求的需要,选择一种居于支配的色彩作为主调色彩,构成画面的整体色彩倾向。然后选择辅色,添加点缀色,最后按照色彩组合的原则完成设计中的需求。
虽然有了以上的配色方式,但一套标准的色彩系统还会包含中性色规范、颜色的使用规范等等。相信解决了大部分的需求,剩下的工作也难不倒大家了。毕竟以上的方式只是给大家提供了一个理性科学的方法,如果想更加优秀,还需要进一步深入地去学习色彩理论知识,多看优秀的配色作品提升审美,总之要多看、多实践和多思考。
文章来源:优设 作者:能量眼球
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
没有 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 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 也集成到 Vue 的官方调试工具 devtools extension,提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能。采用了全局单例模式,将组件的共享状态抽离出来管理,使得组件树中每一个位置都可以获取共享的状态或者触发行为。
那么什么是状态呢?我把状态理解为在没有使用vuex时,在当前组件中data内需要共用的数据为状态。
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界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
一套 UI 界面当中,核心的 APP 图标是用户每天都要接触、经常使用的视觉组件和交互对象。设计图标的时候,要用到大家最熟悉的元素才能贴合用户认知,要醒目、统一,也要足够「隐形」,避免喧宾夺主。今天这篇文章,来自著名的 Tubik Studio 团队,他们为华为旗下的 EMUI 10 系统设计了核心的图标系统,来看看他们的设计过程吧。
我们总不会低估一个操作系统基础图标,对于产品的可用性和合意性的影响。基础图标虽然小巧,但是对于整个操作系统而言,总是极具影响力的,因为他们是用户界面的核心元素,帮助用户快速直观地在系统中定位、浏览、导航。但是,对于设计师而言,图标的设计始终是挑战,它看起来最为简单,但实则为最为艰难的工作。
图标需要,让人一目了然,但是也要具备良好的可识别性,在传统和创新之间达到平衡。这一次,Tubik Studio 设计团队将要给华为的 EMUI 10 来设计图标,而这篇文章将会为你揭示背后的设计过程。
这次的项目主要是由 Sergii Valiukh 、Arthur Avakyan 和 Polina Taran 来负责。这次的设计项目从最初的探索构思入手,逐渐开始绘制草图,探索样式,一直到最后打磨,完成设计。
为华为 EMUI 10 系统的用户界面设计基础的图标
我们在 2019 年夏季,收到了来自华为的邀约,这次的项目要重新设计 EMUI 这套基于 Android 系统的用户界面基础图标,这套图标会用来适配华为旗下的旗舰手机,这些图标将会随着新版的系统部署到这些手机产品当中。我们的任务,是创建总计 54 款符合当下潮流趋势的图标,这些图标要能够贴合品牌和已有用户的偏好,并且能够吸引新的用户。
这个图标设计项目如今已经在当下的华为旗下手机产品中应用且部署好了,最早使用这套图标的智能手机型号为 Mate 30 ,紧接其后的是 P40。
在整个操作系统中,这些图标是始终位于用户视野以内、最基础的最核心的交互元素,通常用户每天都会同这些核心的基础 APP 进行交互,有时候一天多达几十次,因此它们应当具备良好的功能性,还应该足够美观,给用户带来满足感。同时,这套图标的设计,也应当足够统一,以协调的体验切入到整个 EMUI 的设计系统当中,贴合整体的样式特征。
所以,我们使用了一整套图标网格系统,应对不同需求,在设计的过程中,这套网格有助于确保所有图标外部尺寸的一致性,也保证了内部元素的统一性。
为了发掘全新的视角,我们决定从传统的手绘图标开始,寻找重新塑造图标设计的方法。这些图标所涉及到的元素,早已为全球数百万用户所熟知,要重新设计图标外观,并且还要成套且统一,这本身就是一个巨大的挑战。
比如「电话」图标所对应的听筒元素、「信息」图标所对应的气泡对话框这样的元素,是不可能完全抛弃重新创造的,所以我们的真正的切入点是在形态和色彩上寻求解决方案。
越是简单的东西,重塑起来就越难。
在实际的设计过程中,我们尝试了数以百计的造型变体,从完全放飞、非常规的的外部造型,到极其常规,大家熟知的造型解决方案,我们都逐一试过。而在色彩上的尝试相对会显得更加具有实验性:我们尝试使用明亮的紫罗兰色、栗色和浅绿色来进行混搭。
当然,我们很清楚,这样的实验性的工作是探索过程中的一小步,但是它是必须的,是创造新设计的种子,是寻求正确答案的必经之路。
在设计过程在,有一件有趣的事情发生在设计「相机」图标的过程中。我们尝试过很多不同的图标造型,不同的元素,不同的样式,但是其中始终有一个细节是不变的,那就是右上角的小红点。这是为了暗示用户,华为的摄像头模组来自徕卡,这个红色的小点就是向其致敬的标识。
而下面的概念设计,则是强化了图标之间的几何轮廓的差异,从而提升图标在智能手机屏幕上的对比度和识别度。
下一步,我们基于几何图形外观差异性,设计了多种造型不同但同样优雅的图标。在基于这种风格概念进行延伸的过程中,我们会优先考虑圆形的元素。而「天气」图标明显既不适合圆形也不适合方形来呈现,所以我们使用的是云和太阳两种元素的组合。「钱包」图标使用的是矩形,然后搭配卡片的组合。尽管造型整体上是相对自由的,但是所有的图标都是遵循图标网格,并且在视觉权重上尽可能统一。
在色彩和样式上,我们则更加倾向于渐变。没有色彩渐变,纯扁平的图标显得过于幼稚和「古早」,没有足够的品质感,所以,我们在新的图标设计上,开始加入渐变色彩,提升质感。
不过,在最终版本当中,我们还是保留了图标外部的圆角矩形的外轮廓,然后将图标元素的内径进行了适当地缩减,渐变和核心的简约几何特征依然是整套设计的最高优先级。
这套设计方案展现了在整套 UI 界面中,图标这个小元素的设计上所需要投入的精力和潜在的难度。想要图标足够协调、美观、易用还要贴合品牌特征、还要区别于以往,是一件相当不容易的事情。
细节里藏着魔鬼,任何细小的元素、线条轮廓、色彩的变化都可能会在屏幕上放大、被感知到。
文章来源:优设 作者:Tubik Studio蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
作为公民,我们在用我们擅长的设计去服务社会,在这个过程中形成自己的积淀。这是不可用金钱来衡量的价值。
作者:蓝蓝
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
蓝蓝设计的小编 http://www.lanlanwork.com