无缝轮播一直是面试的热门题目,而大部分答案都是复制第一张到最后。诚然,这种方法是非常标准,那么有没有另类一点的方法呢?
第一种方法是需要把所有图片一张张摆好,然后慢慢移动的,
但是我能不能直接不摆就硬移动呢?
如果你使用过vue的transition
,我们是可以通过给每一张图片来添加入场动画和离场动画来模拟这个移动
这样看起来的效果就是图片从右边一直往左移动,但是这个不一样的地方是,我们每一个元素都有这个进场动画和离场动画,我们根本不用关心它是第几个元素,你只管轮播就是。
很简单,我们自己实现一个transtition
的效果就好啦,主要做的是以下两点
xx-enter-active
动画
xx-leave-active
, 注意要让动画播完才消失
function hide(el){
el.className = el.className.replace(' slide-enter-active','')
el.className += ' slide-leave-active' el.addEventListener('animationend',animationEvent)
} function animationEvent(e){
e.target.className = e.target.className.replace(' slide-leave-active','')
e.target.style.display = 'none' e.target.removeEventListener('animationend',animationEvent)
} function show(el){
el.style.display = 'flex' el.className += ' slide-enter-active' }
这里我们使用了animationend
来监听动画结束,注意这里每次从新添加类的时候需要重新添加监听器,不然会无法监听。如果不使用这个方法你可以使用定时器的方式来移除leave-active类。
function hide(el){
el.className = el.className.replace(' slide-enter-active','')
el.className += ' slide-leave-active' setTimeout(()=>
{ //动画结束后清除class el.className = el.className.replace(' slide-leave-active','')
el.style.display = 'none' }, ANIMATION_TIME) //这个ANIMATION_TIME为你在css中动画执行的时间 }
.slide-enter-active{ position: absolute; animation: slideIn ease .5s forwards;
} .slide-leave-active{ position: absolute; animation: slideOut ease .5s forwards;
} @keyframes slideIn {
0%{ transform: translateX(100%);
}
100%{ transform: translateX(0);
}
} @keyframes slideOut {
0%{ transform: translateX(0);
}
100%{ transform: translateX(-100%);
}
}
需要注意的是这里的 forwards
属性,这个属性表示你的元素状态将保持动画后的状态,如果不设置的话,动画跑完一遍,你的元素本来执行了离开动画,执行完以后会回来中央位置杵着。这个时候你会问了,上面的代码不是写了,动画执行完就隐藏元素吗?
如果你使用上面的setTimeout来命令元素执行完动画后消失,那么可能会有一瞬间的闪烁,因为实际业务中,你的代码可能比较复杂,setTimeout没法在那么精准的时间内执行。保险起见,就让元素保持动画离开的最后状态,即translateX(-100%)
。此时元素已经在屏幕外了,不用关心它的表现了
很简单,我们进一个新元素的时候同时移除旧元素即可,两者同时执行进场和离场动画即可。
function autoPlay(){
setTimeout(()=>{
toggleShow(新元素, 旧元素) this.autoPlay()
},DURATION) //DURATION为动画间隔时间 } function toggleShow(newE,oldE){ //旧ele和新ele同时动画 hide(oldE)
show(newE)
}
手机UI中的交互是保持产品鲜活生命力的源动力。好的交互可以帮助用户快速地获得反馈,认知布局,增强体验感和沉浸感。
手机UI中的交互是保持产品鲜活生命力的源动力。好的交互可以帮助用户快速地获得反馈,认知布局,增强体验感和沉浸感。这里为大家整理了一些优秀并富有创意的交互作品,为你的产品设计注入灵感。
--手机appUI设计--
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
更多精彩文章:
写在前面
在平时的设计过程当中,你可能会有这样的疑惑,为什么在大部分APP中,当单个按钮和多个按钮同时存在时,最重要的按钮一般都会放置在页面的右侧呢?如果最重要的按钮放在左侧又有什么问题呢?按钮放在右侧的原因是什么呢?它又有什么理论依据呢?接下来带着这些疑问,开始我们今天所要介绍的内容:交互心理学之古腾堡原则
古腾堡原则的起源
古腾堡原则是由14世纪西方活字印刷术的发明人约翰·古腾堡提出,早在20世纪50年代,他在设计报纸的过程中,提出了一项原则,认为人的阅读方式应该是遵循某种习惯进行的,就像读书一样,由左到右,从上到下。这其中蕴含着什么信息呢?经过研究最终得出被后人所熟知的结论:古腾堡原则,并附上了一张图,名为「古腾堡图」。古腾堡图将画面所呈现的内容分成四个象限:
1、第一视觉区(POA):左上方,用户首先注意到的地方
2、强休息区(SFA):右上方,较少被注意到
3、弱休息区(WFA):左下方,最少被注意到
4、终端视觉区(TA):右下方,视觉流终点
从图中可以看出,用户视线很自然的会从第一视觉区开始,逐渐移动到终端休息区。整个阅读过程视线都会沿着一条方向轴开始从左到右浏览。用户会更容易关注到页面的开始与结束区域,而中间的段落则很少被关注到。古腾堡揭示了一个实用的视觉轨迹规律:阅读引力是从上到下,从左到右。
遵循古腾堡原则把关键信息放在左上角、中间和右下角,能够更好的体现元素的重要性。例如:我们平时所看到的页面弹窗、各种证明文件和合同文件等等。
古腾堡图通过对设计元素的重量与元素布局和组成方式进行调和,指导眼睛的运动轨迹。让用户迅速获取有价值的信息,同时用户对信息的熟悉程度也是影响眼睛运动轨迹的因素之一。
而随着互联网的兴起,古腾堡原则也逐渐被应用到APP设计和网页设计当中。接下来让我们来看看他在界面中的实际应用。
在设计中的应用
1.1 底部单个按钮
这种形式在引导用户操作的页面中最为常见,为了能够保证用户对内容进行阅读,所以将按钮摆放在页面底部,内容放在顶部,这样的摆放即符合用户由上到下的阅读习惯又达到了产品预期的目标。
1.2 底部垂直双按钮
上面我们提到了单个按钮的摆放思路,接下来看一下垂直双按钮的摆放思路是怎么样的。如果一个界面上同时存在两个优先级不同的按钮,并且产品希望用户对每一个按钮都有足够的关注度,那么垂直摆放是最佳选择,虽然垂直双按钮在样式上做了区分,但用户同样会停留一段时间将按钮的内容进行对比思考。
那么,按照古腾堡原则,重要的按钮应该放在页面最底部,原则上它应该是这样的:
仔细观察上图,有没有发现浅色按钮很容易被忽略掉,这样就违背了产品要保证每一个按钮都要有足够关注度的初衷,所以我们要违背古腾堡原则来满足业务需求,正如我们所看到的微信授权页面一样,
为了保证「同意」与「拒绝」这两个独立的按钮能够被用户足够的重视,并且其中的任意一个按钮不会被轻易的忽略掉,这里将「同意」按钮颜色加重,并且放在「拒绝」按钮之上,让眼睛原本垂直向下的运动轨迹产生回流的变化。
小结
原则是设计的基础,并非一成不变,要合理权衡设计原则与产品目标之间的关系。
2、顶部按钮分析
由于顶部导航栏空间有限,导致按钮相对较小,并且不便于点击操作,所以这类顶部按钮适用于修改内容的编辑页面,即可以避免误触,又可以让用户关注内容本身。关键按钮至于顶部,还可以缩短用户眼睛的运动路径,让用户更容易注意到其状态的变化状态。
小结
顶部按钮更关注可编辑的内容区域,并非按钮。而底部按钮则更关注按钮本身。并非内容。
3、水平按钮分析
除了上面提到的顶部按钮和底部按钮,还有水平摆放的按钮,比如淘宝详情页、京东详情页、网易严选详情页的「加入购物车」和「立即购买」按钮,界面中的「立即购买」按钮都放在了右下角,结合古腾堡原则的视觉终点说明,右下角为视觉终端区域,即视觉最终停留的位置,所以他们都将与转化率密切相关的「立即购买」按钮放在了界面的右下角,让用户更容易关注到。
再比如比较常见的「取消」和「确认」弹窗样式,通常是在需要让用户确认某种操作行为时出现,有可能是提交表单、协议授权、获取用户信息等等,为了防止用户误操作,这也是提升产品体验的小细节。
平常我们所看到的弹窗,推荐按钮都是在右侧,那么将推荐按钮放在左侧会怎么样?如下图所示:
不难看出推荐按钮放在右侧后,视觉在水平方向轴上产生了回流。
弹窗的目的是想让用户点击「确认」按钮,如果将「确认」放在左侧,根据古腾堡原则,用户的视线会不由自主的向右侧移动,也就是「取消」按钮的位置,想要回到左侧「确认」按钮位置就需要移动视线,并且眼睛的运动轨迹会在水平方向轴上来回的往复运动,无形中增加了用户选择时长。如果将「确认」放在右侧,「取消」放在左侧则可以为用户提高操作效率。
在实际产品中的应用案例:
小结
当产品想要让用户进行某种操作时,主要按钮放在右边
总结
1、古腾堡图第一视觉区,强休息区,弱休息区,终端视觉区
2、原则是设计的基础,并非一成不变,要合理权衡设计原则与产品目标之间的关系
3、顶部按钮更关注可编辑的内容区域,并非按钮。而底部按钮则更关注按钮本身。并非内容
4、当产品想要让用户进行某种操作时,主要按钮放在右边
文章来源:UI中国 作者:Coldrain1
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
垂直居中基本上是入门 CSS 必须要掌握的问题了,我们肯定在各种教程中都看到过“CSS 垂直居中的 N 种方法”,通常来说,这些方法已经可以满足各种使用场景了,然而当我们碰到了需要使用某些特殊字体进行混排、或者使文字对齐图标的情况时,也许会发现,无论使用哪种垂直居中的方法,总是感觉文字向上或向下偏移了几像素,不得不专门对它们进行位移,为什么会出现这种情况呢?
下图是一个使用各种常见的垂直居中的方法来居中文字的示例,其中涉及到不同字体的混排,可以看出,虽然这里面用了几种常用的垂直居中的方法,但是在实际的观感上这些文字都没有恰好垂直居中,有些文字看起来比较居中,而有些文字则偏移得很厉害。
在线查看:CodePen(字体文件直接引用了谷歌字体,如果没有效果需要注意网络情况)
通过设置vertical-align:middle
对文字进行垂直居中时,父元素需要设置font-size: 0
,因为vertical-align:middle
是将子元素的中点与父元素的baseline + x-height / 2
的位置进行对齐的,设置字号为 0 可以保证让这些线的位置都重合在中点。
我们用鼠标选中这些文字,就能发现选中的区域确实是在父层容器里垂直居中的,那么为什么文字却各有高低呢?这里就涉及到了字体本身的构造和相关的度量值。
这里先提出一个问题,我们在 CSS 中给文字设置了 font-size
,这个值实际设置的是字体的什么属性呢?
下面的图给出了一个示例,文字所在的标签均为 span
,对每种字体的文字都设置了红色的 outline
以便观察,且设有 line-height: normal
。从图中可以看出,虽然这些文字的字号都是 40px,但是他们的宽高都各不相同,所以字号并非设置了文字实际显示的大小。
为了解答这个问题,我们需要对字体进行深入了解,以下这些内容是西文字体的相关概念。首先一个字体会有一个 EM Square(也被称为 UPM、em、em size)[4],这个值最初在排版中表示一个字体中大写 M 的宽度,以这个值构成一个正方形,那么所有字母都可以被容纳进去,此时这个值实际反映的就成了字体容器的高度。在金属活字中,这个容器就是每个字符的金属块,在一种字体里,它们的高度都是统一的,这样每个字模都可以放入印刷工具中并进行排印。在数码排印中,em 是一个被设置了大小的方格,计量单位是一种相对单位,会根据实际字体大小缩放,例如 1000 单位的字体设置了 16pt 的字号,那么这里 1000 单位的大小就是 16pt。Em 在 OpenType 字体中通常为 1000 ,在 TrueType 字体中通常为 1024 或 2048(2 的 n 次幂)。
金属活字,图片来自 http://designwithfontforge.com/en-US/The_EM_Square.html
字体本身还有很多概念和度量值(metrics),这里介绍几个常见的概念,以维基百科的这张图为例(下面的度量值的计量单位均为基于 em 的相对单位):
接下来我们在 FontForge 软件里看看这些值的取值,这里以 Arial
字体给出一个例子:
从图中可以看出,在 General 菜单中,Arial 的 em size 是 2048,字体的 ascent 是1638,descent 是410,在 OS/2 菜单的 Metrics 信息中,可以得到 capital height 是 1467,x height 为 1062,line gap 为 67。
然而这里需要注意,尽管我们在 General 菜单中得到了 ascent 和 descent 的取值,但是这个值应该仅用于字体的设计,它们的和永远为 em size;而计算机在实际进行渲染的时候是按照 OS/2 菜单中对应的值来计算,一般操作系统会使用 hhea(Horizontal Header Table)表的 HHead Ascent 和 HHead Descent,而 Windows 是个特例,会使用 Win Ascent 和 Win Descent。通常来说,实际用于渲染的 ascent 和 descent 取值要比用于字体设计的大,这是因为多出来的区域通常会留给注音符号或用来控制行间距,如下图所示,字母顶部的水平线即为第一张图中 ascent 高度 1638,而注音符号均超过了这个区域。根据资料的说法[5],在一些软件中,如果文字内容超过用于渲染的 ascent 和 descent,就会被截断,不过我在浏览器里实验后发现浏览器并没有做这个截断(Edge 86.0.608.0 Canary (64 bit), MacOS 10.15.6)。
在本文中,我们将后面提到的 ascent 和 descent 均认为是 OS/2 选项中读取到的用于渲染的 ascent 和 descent 值,同时我们将 ascent + descent 的值叫做 content-area。
理论上一个字体在 Windows 和 MacOS 上的渲染应该保持一致,即各自系统上的 ascent 和 descent 应该相同,然而有些字体在设计时不知道出于什么原因,导致其确实在两个系统中有不同的表现。以下是 Roboto 的例子:
Differences between Win and HHead metrics cause the font to be rendered differently on Windows vs. iOS (or Mac I assume) · Issue #267 · googlefonts/roboto
那么回到本节一开始的问题,CSS 中的font-size
设置的值表示什么,想必我们已经有了答案,那就是一个字体 em size 对应的大小;而文字在设置了line-height: normal
时,行高的取值则为 content-area + line-gap,即文本实际撑起来的高度。
知道了这些,我们就不难算出一个字体的显示效果,上面 Arial 字体在line-height: normal
和font-size: 100px
时撑起的高度为(1854 + 434 + 67) / 2048 * 100px = 115px
。
在实验中发现,对于一个行内元素,鼠标拉取的 selection 高度为当前行line-height
最高的元素值。如果是块状元素,当line-height
的值为大于 content-area 时,selection 高度为line-height
,当其小于等于 content-area 时,其高度为 content-area 的高度。
在中间插一个问题,我们应该都使用过 line-height
来给文字进行垂直居中,那么 line-height
实际是以字体的哪个部分的中点进行计算呢?为了验证这个问题,我新建了一个很有“设计感”的字体,em size 设为 1000,ascent 为 800,descent 为 200,并对其分别设置了正常的和比较夸张的 metrics:
上面图中左边是 FontForge 里设置的 metrics,右边是实际显示效果,文字字号设为 100px,四个字母均在父层的 flex 布局下垂直居中,四个字母的 line-height
分别为 0、1em、normal、3em,红色边框是元素的 outline
,黄色背景是鼠标选取的背景。由上面两张图可以看出,字体的 metrics 对文字渲染位置的影响还是很大的。同时可以看出,在设置 line-height
时,虽然 line gap 参与了撑起取值为 normal
的空间,但是不参与文字垂直居中的计算,即垂直居中的中点始终是 content-area 的中点。
我们又对字体进行了微调,使其 ascent 有一定偏移,这时可以看出 1em 行高的文字 outline 恰好在正中间,因此可以得出结论:在浏览器进行渲染时,em square 总是相对于 content-area 垂直居中。
说完了字体构造,又回到上一节的问题,为什么不同字体文字混排的时候进行垂直居中,文字各有高低呢?
在这个问题上,本文给出这样一个结论,那就是因为不同字体的各项度量值均不相同,在进行垂直居中布局时,content-area 的中点与视觉的中点不统一,因此导致实际看起来存在位置偏移,下面这张图是 Arial 字体的几个中线位置:
从图上可以看出来,大写字母和小写字母的视觉中线与整个字符的中线还是存在一定的偏移的。这里我没有找到排版相关学科的定论,究竟以哪条线进行居中更符合人眼观感的居中,以我个人的观感来看,大写字母的中线可能看起来更加舒服一点(尤其是与没有小写字母的内容进行混排的时候)。
需要注意一点,这里选择的 Arial 这个字体本身的偏移比较少,所以使用时整体感觉还是比较居中的,这并不代表其他字体也都是这样。
对于中文字体,本身的设计上没有基线、升部、降部等说法,每个字都在一个方形盒子中。但是在计算机上显示时,也在一定程度上沿用了西文字体的概念,通常来说,中文字体的方形盒子中文字体底端在 baseline 和 descender 之间,顶端超出一点 ascender,而标点符号正好在 baseline 上。
我们已经了解了字体的相关概念,那么如何解决在使用字体时出现的偏移问题呢?
通过上面的内容可以知道,文字显示的偏移主要是视觉上的中点和渲染时的中点不一致导致的,那么我们只要把这个不一致修正过来,就可以实现视觉上的居中了。
为了实现这个目标,我们可以借助 vertical-align
这个属性来完成。当 vertical-align
取值为数值的时候,该值就表示将子元素的基线与父元素基线的距离,其中正数朝上,负数朝下。
这里介绍的方案,是把某个字体下的文字通过计算设置 vertical-align
的数值偏移,使其大写字母的视觉中点与用于计算垂直居中的点重合,这样字体本身的属性就不再影响居中的计算。
具体我们将通过以下的计算方法来获取:首先我们需要已知当前字体的 em-size,ascent,descent,capital height 这几个值(如果不知道 em-size,也可以提供其他值与 em-size 的比值),以下依然以 Arial 为例:
const emSize = 2048; const ascent = 1854; const descent = 434; const capitalHeight = 1467;
// 计算前需要已知给定的字体大小 const fontSize = FONT_SIZE; // 根据文字大小,求得文字的偏移 const verticalAlign = ((ascent - descent - capitalHeight) / emSize) * fontSize; return ( <span style={{ fontFamily: FONT_FAMILY, fontSize }}> <span style={{ verticalAlign }}>TEXT</span> </span> )
由此设置以后,外层 span 将表现得像一个普通的可替换元素参与行内的布局,在一定程度上无视字体 metrics 的差异,可以使用各种方法对其进行垂直居中。
由于这种方案具有固定的计算步骤,因此可以根据具体的开发需求,将其封装为组件、使用 CSS 自定义属性或使用 CSS 预处理器对文本进行处理,通过传入字体信息,就能修正文字垂直偏移。
虽然上述的方案可以在一定程度上解决文字垂直居中的问题,但是在实际使用中还存在着不方便的地方,我们需要在使用字体之前就知道字体的各项 metrics,在自定义字体较少的情况下,开发者可以手动使用 FontForge 等工具查看,然而当字体较多时,挨个查看还是比较麻烦的。
目前的一种思路是我们可以使用 Canvas 获取字体的相关信息,如现在已经有开源的获取字体 metrics 的库 FontMetrics.js。它的核心思想是使用 Canvas 渲染对应字体的文字,然后使用 getImageData
对渲染出来的内容进行分析。如果在实际项目中,这种方案可能导致潜在的性能问题;而且这种方式获取到的是渲染后的结果,部分字体作者在构建字体时并没有严格将设计的 metrics 和字符对应,这也会导致获取到的 metrics 不够准确。
另一种思路是直接解析字体文件,拿到字体的 metrics 信息,如 opentype.js 这个项目。不过这种做法也不够轻量,不适合在实际运行中使用,不过可以考虑在打包过程中自动执行这个过程。
此外,目前的解决方案更多是偏向理论的方法,当文字本身字号较小的情况下,浏览器可能并不能按照预期的效果渲染,文字会根据所处的 DOM 环境不同而具有 1px 的偏移[9]。
CSS Houdini 提出了一个 Font Metrics 草案[6],可以针对文字渲染调整字体相关的 metrics。从目前的设计来看,可以调整 baseline 位置、字体的 em size,以及字体的边界大小(即 content-area)等配置,通过这些可以解决因字体的属性导致的排版问题。
[Exposed=Window] interface FontMetrics {
readonly attribute double width;
readonly attribute FrozenArray<double> advances;
readonly attribute double boundingBoxLeft;
readonly attribute double boundingBoxRight;
readonly attribute double height;
readonly attribute double emHeightAscent;
readonly attribute double emHeightDescent;
readonly attribute double boundingBoxAscent;
readonly attribute double boundingBoxDescent;
readonly attribute double fontBoundingBoxAscent;
readonly attribute double fontBoundingBoxDescent;
readonly attribute Baseline dominantBaseline;
readonly attribute FrozenArray<Baseline> baselines;
readonly attribute FrozenArray<Font> fonts;
};
从 https://ishoudinireadyyet.com/ 这个网站上可以看到,目前 Font Metrics 依然在提议阶段,还不能确定其 API 具体内容,或者以后是否会存在这一个特性,因此只能说是一个在未来也许可行的文字排版处理方案。
文本垂直居中的问题一直是 CSS 中最常见的问题,但是却很难引起注意,我个人觉得是因为我们常用的微软雅黑、苹方等字体本身在设计上比较规范,在通常情况下都显得比较居中。但是当一个字体不是那么“规范”时,传统的各种方法似乎就有点无能为力了。
本文分析了导致了文字偏移的因素,并给出寻找文字垂直居中位置的方案。
由于涉及到 IFC 的问题本身就很复杂[7],关于内联元素使用 line-height
与 vertical-align
进行居中的各种小技巧因为与本文不是强相关,所以在文章内也没有提及,如果对这些内容比较感兴趣,也可以通过下面的参考资料寻找一些相关介绍。
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
ECMAScript模块(简称ES模块)是一种JavaScript代码重用的机制,于2015年推出,一经推出就受到前端开发者的喜爱。在2015之年,JavaScript 还没有一个代码重用的标准机制。多年来,人们对这方面的规范进行了很多尝试,导致现在有多种模块化的方式。
你可能听说过AMD模块,UMD,或CommonJS,这些没有孰优孰劣。最后,在ECMAScript 2015中,ES 模块出现了。
我们现在有了一个“正式的”模块系统。
理论上,ES 模块应该在所有JavaScript环境中。实际上,ES 模块的主要应用还是在浏览器上。
2020年5月,Node.js v12.17.0 增加了在不使用标记前提下对ECMAScript模块的支持。 这意味着我们现在可以在Node.js
中使用import
和export
,而无需任何其他命令行标志。
ECMAScript模块要想在任何JavaScript环境通用,可能还需要很长的路要走,但方向是正确的。
ES 模块是一个简单的文件,我们可以在其中声明一个或多个导出。以下面utils.js
为例:
// utils.js export function funcA() { return "Hello named export!";
} export default function funcB() { return "Hello default export!";
}
这里有两个导出。
第一个是命名导出,后面是export default
,表示为默认导出。
假设我们的项目文件夹中有一个名为utils.js
的文件,我们可以将这个模块提供的对象导入到另一个文件中。
假设我们在项目文中还有一个Consumer.js
的文件。 要导入utils.js
公开的函数,我们可以这样做:
// consumer.js import { funcA } from "./util.js";
这种对应我们的命名导入方式.
如果我们要导入 utils.js
中的默认导出也就是 funcB
方法,我们可以这样做:
// consumer.js import { funcA } from "./util.js";
当然,我们可以导入同时导入命名和默认的:
// consumer.js import funcB, { funcA } from "./util.js";
funcB();
funcA();
我们也可以用星号导入整个模块:
import * as myModule from './util.js';
myModule.funcA();
myModule.default();
注意,这里要使用默认到处的方法是使用 default()
而不是 funcB()
。
从远程模块导入:
import { createStore } from "https://unpkg.com/redux@4.0.5/es/redux.mjs"; const store = createStore(/* do stuff */)
现代浏览器支持ES模块,但有一些警告。 要使用模块,需要在 script
标签上添加属性 type
, 对应值 为 module
。
<html lang="en"> <head> <meta charset="UTF-8"> <title>ECMAScript modules in the browser</title>
</head> <body> <p id="el">The result is:
</p> </body> <script type="module"> import { appendResult } from "./myModule.js"; const el = document.getElementById("el");
appendResult(el);
appendResult(el);
appendResult(el);
appendResult(el);
appendResult(el); </script> </html>
myModule.js
内容如下:
export function appendResult(element) { const result = Math.random();
element.innerText += result;
}
ES 模块是静态的,这意味着我们不能在运行时更改导入。随着2020年推出的动态导入(dynamic imports),我们可以动态加载代码来响应用户交互(webpack早在ECMAScript 2020推出这个特性之前就提供了动态导入)。
考虑下面的代码:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8">
<title>Dynamic imports</title> </head> <body> <button id="btn">Load!</button> </body> <script src="loader.js"></script> </html>
再考虑一个带有两个导出的JavaScript模块
// util.js export function funcA() { console.log("Hello named export!");
} export default function funcB() { console.log("Hello default export!");
}
为了动态导入 util.js 模块,我们可以点击按钮在去导入:
/ loader.js
const btn = document.getElementById("btn");
btn.addEventListener("click", () => { // loads named export import("./util.js").then(({ funcA }) => {
funcA();
});
});
这里使用解构的方式,取出命名导出 funcA
方法:
({ funcA }) => {}
ES模块实际上是JavaScript对象:我们可以解构它们的属性以及调用它们的任何公开方法。
要使用动态导入的默认方法,可以这样做
// loader.js const btn = document.getElementById("btn");
btn.addEventListener("click", () => { import("./util.js").then((module) => { module.default();
});
});
当作为一个整体导入一个模块时,我们可以使用它的所有导出
// loader.js const btn = document.getElementById("btn");
btn.addEventListener("click", () =>
{ // loads entire module // uses everything import("./util.js").then((module) => { module.funcA(); module.default();
});
});
还有另一种用于动态导入的常见样式,如下所示:
const loadUtil = () => import("./util.js"); const btn = document.getElementById("btn");
btn.addEventListener("click", () => { // });
loadUtil
返回的是一个 promise,所以我们可以这样操作
const loadUtil = () => import("./util.js"); const btn = document.getElementById("btn");
btn.addEventListener("click", () => {
loadUtil().then(module => { module.funcA(); module.default();
})
})
动态导入看起来不错,但是它们有什么用呢?
使用动态导入,我们可以拆分代码,并只在适当的时候加载重要的代码。在 JavaScript 引入动态导入之前,这种模式是webpack(模块绑定器)独有的。
像React
和Vue
通过动态导入代码拆分来加载响应事件的代码块,比如用户交互或路由更改。
假设我们项目有一个 person.json
文件,内容如下:
{ "name": "Jules", "age": 43 }
现在,我们需要动态导入该文件以响应某些用户交互。
因为 JSON 文件不是一个方法,所以我们可以使用默认导出方式:
const loadPerson = () => import('./person.json'); const btn = document.getElementById("btn");
btn.addEventListener("click", () => {
loadPerson().then(module => { const { name, age } = module.default; console.log(name, age);
});
});
这里我们使用解构的方式取出 name
和 age
:
const { name, age } = module.default;
因为 import()
语句返回是一个 Promise,所以我们可以使用 async/await
:
const loadUtil = () => import("./util.js"); const btn = document.getElementById("btn");
btn.addEventListener("click", async () => { const utilsModule = await loadUtil();
utilsModule.funcA();
utilsModule.default();
})
使用import()
导入模块时,可以按照自己的意愿命名它,但要调用的方法名保持一致:
import("./util.js").then((module) => { module.funcA(); module.default();
});
或者:
import("./util.js").then((utilModule) => {
utilModule.funcA();
utilModule.default();
});
原文:https://www.valentinog.com/bl...
代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug。
TypeScript 是一种由微软开发的自由和开源的编程语言。它是 JavaScript 的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程。
本文阿宝哥将分享这些年在学习 TypeScript 过程中,遇到的 10 大 “奇怪” 的符号。其中有一些符号,阿宝哥第一次见的时候也觉得 “一脸懵逼”,希望本文对学习 TypeScript 的小伙伴能有一些帮助。
好的,下面我们来开始介绍第一个符号 —— ! 非空断言操作符。
一、! 非空断言操作符
在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符 ! 可以用于断言操作对象是非 null 和非 undefined 类型。具体而言,x! 将从 x 值域中排除 null 和 undefined 。
那么非空断言操作符到底有什么用呢?下面我们先来看一下非空断言操作符的一些使用场景。
1.1 忽略 undefined 和 null 类型
function myFunc(maybeString: string | undefined | null) { // Type 'string | null | undefined' is not assignable to type 'string'. // Type 'undefined' is not assignable to type 'string'. const onlyString: string = maybeString; // Error const ignoreUndefinedAndNull: string = maybeString!; // Ok }
1.2 调用函数时忽略 undefined 类型
type NumGenerator = () => number; function myFunc(numGenerator: NumGenerator | undefined) { // Object is possibly 'undefined'.(2532) // Cannot invoke an object which is possibly 'undefined'.(2722) const num1 = numGenerator(); // Error const num2 = numGenerator!(); //OK }
因为 ! 非空断言操作符会从编译生成的 JavaScript 代码中移除,所以在实际使用的过程中,要特别注意。比如下面这个例子:
const a: number | undefined = undefined; const b: number = a!; console.log(b);
以上 TS 代码会编译生成以下 ES5 代码:
"use strict"; const a = undefined; const b = a; console.log(b);
虽然在 TS 代码中,我们使用了非空断言,使得 const b: number = a!; 语句可以通过 TypeScript 类型检查器的检查。但在生成的 ES5 代码中,! 非空断言操作符被移除了,所以在浏览器中执行以上代码,在控制台会输出 undefined。
二、?. 运算符
TypeScript 3.7 实现了呼声最高的 ECMAScript 功能之一:可选链(Optional Chaining)。有了可选链后,我们编写代码时如果遇到 null 或 undefined 就可以立即停止某些表达式的运行。可选链的核心是新的 ?. 运算符,它支持以下语法:
obj?.prop
obj?.[expr]
arr?.[index] func?.(args)
这里我们来举一个可选的属性访问的例子:
const val = a?.b;
为了更好的理解可选链,我们来看一下该 const val = a?.b 语句编译生成的 ES5 代码:
var val = a === null || a === void 0 ? void 0 : a.b;
上述的代码会自动检查对象 a 是否为 null 或 undefined,如果是的话就立即返回 undefined,这样就可以立即停止某些表达式的运行。你可能已经想到可以使用 ?. 来替代很多使用 && 执行空检查的代码:
if(a && a.b) { } if(a?.b){ } /**
* if(a?.b){ } 编译后的ES5代码
*
* if(
* a === null || a === void 0
* ? void 0 : a.b) {
* }
*/
但需要注意的是,?. 与 && 运算符行为略有不同,&& 专门用于检测 falsy 值,比如空字符串、0、NaN、null 和 false 等。而 ?. 只会验证对象是否为 null 或 undefined,对于 0 或空字符串来说,并不会出现 “短路”。
2.1 可选元素访问
可选链除了支持可选属性的访问之外,它还支持可选元素的访问,它的行为类似于可选属性的访问,只是可选元素的访问允许我们访问非标识符的属性,比如任意字符串、数字索引和 Symbol:
function tryGetArrayElement<T>(arr?: T[], index: number = 0) { return arr?.[index];
}
以上代码经过编译后会生成以下 ES5 代码:
"use strict"; function tryGetArrayElement(arr, index) { if (index === void 0) { index = 0; } return arr === null || arr === void 0 ? void 0 : arr[index];
}
通过观察生成的 ES5 代码,很明显在 tryGetArrayElement 方法中会自动检测输入参数 arr 的值是否为 null 或 undefined,从而保证了我们代码的健壮性。
2.2 可选链与函数调用
当尝试调用一个可能不存在的方法时也可以使用可选链。在实际开发过程中,这是很有用的。系统中某个方法不可用,有可能是由于版本不一致或者用户设备兼容性问题导致的。函数调用时如果被调用的方法不存在,使用可选链可以使表达式自动返回 undefined 而不是抛出一个异常。
可选调用使用起来也很简单,比如:
let result = obj.customMethod?.();
该 TypeScript 代码编译生成的 ES5 代码如下:
var result = (_a = obj.customMethod) === null || _a === void 0 ? void 0 : _a.call(obj);
另外在使用可选调用的时候,我们要注意以下两个注意事项:
如果存在一个属性名且该属性名对应的值不是函数类型,使用 ?. 仍然会产生一个 TypeError 异常。
可选链的运算行为被局限在属性的访问、调用以及元素的访问 —— 它不会沿伸到后续的表达式中,也就是说可选调用不会阻止 a?.b / someMethod() 表达式中的除法运算或 someMethod 的方法调用。
三、?? 空值合并运算符
在 TypeScript 3.7 版本中除了引入了前面介绍的可选链 ?. 之外,也引入了一个新的逻辑运算符 —— 空值合并运算符 ??。当左侧操作数为 null 或 undefined 时,其返回右侧的操作数,否则返回左侧的操作数。
与逻辑或 || 运算符不同,逻辑或会在左操作数为 falsy 值时返回右侧操作数。也就是说,如果你使用 || 来为某些变量设置默认的值时,你可能会遇到意料之外的行为。比如为 falsy 值(''、NaN 或 0)时。
这里来看一个具体的例子:
const foo = null ?? 'default string'; console.log(foo); // 输出:"default string" const baz = 0 ?? 42; console.log(baz); // 输出:0
以上 TS 代码经过编译后,会生成以下 ES5 代码:
"use strict"; var _a, _b; var foo = (_a = null) !== null && _a !== void 0 ? _a : 'default string';
console.log(foo); // 输出:"default string" var baz = (_b = 0) !== null && _b !== void 0 ? _b : 42;
console.log(baz); // 输出:0
通过观察以上代码,我们更加直观的了解到,空值合并运算符是如何解决前面 || 运算符存在的潜在问题。下面我们来介绍空值合并运算符的特性和使用时的一些注意事项。
3.1 短路
当空值合并运算符的左表达式不为 null 或 undefined 时,不会对右表达式进行求值。
function A() { console.log('A was called'); return undefined;} function B() { console.log('B was called'); return false;} function C() { console.log('C was called'); return "foo";} console.log(A() ?? C()); console.log(B() ?? C());
上述代码运行后,控制台会输出以下结果:
A was called
C was called
foo
B was called
false
3.2 不能与 && 或 || 操作符共用
若空值合并运算符 ?? 直接与 AND(&&)和 OR(||)操作符组合使用 ?? 是不行的。这种情况下会抛出 SyntaxError。
// '||' and '??' operations cannot be mixed without parentheses.(5076) null || undefined ?? "foo"; // raises a SyntaxError // '&&' and '??' operations cannot be mixed without parentheses.(5076) true && undefined ?? "foo"; // raises a SyntaxError
但当使用括号来显式表明优先级时是可行的,比如:
(null || undefined ) ?? "foo"; // 返回 "foo"
3.3 与可选链操作符 ?. 的关系
空值合并运算符针对 undefined 与 null 这两个值,可选链式操作符 ?. 也是如此。可选链式操作符,对于访问属性可能为 undefined 与 null 的对象时非常有用。
interface Customer {
name: string;
city?: string;
} let customer: Customer = {
name: "Semlinker" }; let customerCity = customer?.city ?? "Unknown city"; console.log(customerCity); // 输出:Unknown city
前面我们已经介绍了空值合并运算符的应用场景和使用时的一些注意事项,该运算符不仅可以在 TypeScript 3.7 以上版本中使用。当然你也可以在 JavaScript 的环境中使用它,但你需要借助 Babel,在 Babel 7.8.0 版本也开始支持空值合并运算符。
四、?: 可选属性
在面向对象语言中,接口是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类去实现。 TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。
在 TypeScript 中使用 interface 关键字就可以声明一个接口:
interface Person {
name: string;
age: number;
} let semlinker: Person = {
name: "semlinker",
age: 33,
};
在以上代码中,我们声明了 Person 接口,它包含了两个必填的属性 name 和 age。在初始化 Person 类型变量时,如果缺少某个属性,TypeScript 编译器就会提示相应的错误信息,比如:
// Property 'age' is missing in type '{ name: string; }' but required in type 'Person'.(2741) let lolo: Person = { // Error name: "lolo" }
为了解决上述的问题,我们可以把某个属性声明为可选的:
interface Person {
name: string;
age?: number;
} let lolo: Person = {
name: "lolo" }
4.1 工具类型
4.1.1 Partial<T>
在实际项目开发过程中,为了提高代码复用率,我们可以利用 TypeScript 内置的工具类型 Partial<T> 来快速把某个接口类型中定义的属性变成可选的:
interface PullDownRefreshConfig {
threshold: number;
stop: number;
} /**
* type PullDownRefreshOptions = {
* threshold?: number | undefined;
* stop?: number | undefined;
* }
*/ type PullDownRefreshOptions = Partial<PullDownRefreshConfig>
是不是觉得 Partial<T> 很方便,下面让我们来看一下它是如何实现的:
/**
* Make all properties in T optional
*/ type Partial<T> = {
[P in keyof T]?: T[P];
};
4.1.2 Required<T>
既然可以快速地把某个接口中定义的属性全部声明为可选,那能不能把所有的可选的属性变成必选的呢?答案是可以的,针对这个需求,我们可以使用 Required<T> 工具类型,具体的使用方式如下:
interface PullDownRefreshConfig {
threshold: number;
stop: number;
} type PullDownRefreshOptions = Partial<PullDownRefreshConfig> /**
* type PullDownRefresh = {
* threshold: number;
* stop: number;
* }
*/ type PullDownRefresh = Required<Partial<PullDownRefreshConfig>>
同样,我们来看一下 Required<T> 工具类型是如何实现的:
/**
* Make all properties in T required
*/ type Required<T> = {
[P in keyof T]-?: T[P];
};
原来在 Required<T> 工具类型内部,通过 -? 移除了可选属性中的 ?,使得属性从可选变为必选的。
五、& 运算符
在 TypeScript 中交叉类型是将多个类型合并为一个类型。通过 & 运算符可以将现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。
type PartialPointX = { x: number; }; type Point = PartialPointX & { y: number; }; let point: Point = {
x: 1,
y: 1 }
在上面代码中我们先定义了 PartialPointX 类型,接着使用 & 运算符创建一个新的 Point 类型,表示一个含有 x 和 y 坐标的点,然后定义了一个 Point 类型的变量并初始化。
5.1 同名基础类型属性的合并
那么现在问题来了,假设在合并多个类型的过程中,刚好出现某些类型存在相同的成员,但对应的类型又不一致,比如:
interface X {
c: string;
d: string;
} interface Y {
c: number;
e: string } type XY = X & Y; type YX = Y & X; let p: XY; let q: YX;
在上面的代码中,接口 X 和接口 Y 都含有一个相同的成员 c,但它们的类型不一致。对于这种情况,此时 XY 类型或 YX 类型中成员 c 的类型是不是可以是 string 或 number 类型呢?比如下面的例子:
p = { c: 6, d: "d", e: "e" };
q = { c: "c", d: "d", e: "e" };
为什么接口 X 和接口 Y 混入后,成员 c 的类型会变成 never 呢?这是因为混入后成员 c 的类型为 string & number,即成员 c 的类型既可以是 string 类型又可以是 number 类型。很明显这种类型是不存在的,所以混入后成员 c 的类型为 never。
5.2 同名非基础类型属性的合并
在上面示例中,刚好接口 X 和接口 Y 中内部成员 c 的类型都是基本数据类型,那么如果是非基本数据类型的话,又会是什么情形。我们来看个具体的例子:
interface D { d: boolean; } interface E { e: string; } interface F { f: number; } interface A { x: D; } interface B { x: E; } interface C { x: F; } type ABC = A & B & C; let abc: ABC = {
x: {
d: true,
e: 'semlinker',
f: 666 }
}; console.log('abc:', abc);
以上代码成功运行后,控制台会输出以下结果:
由上图可知,在混入多个类型时,若存在相同的成员,且成员类型为非基本数据类型,那么是可以成功合并。
六、| 分隔符
在 TypeScript 中联合类型(Union Types)表示取值可以为多种类型中的一种,联合类型使用 | 分隔每个类型。联合类型通常与 null 或 undefined 一起使用:
const sayHello = (name: string | undefined) => { /* ... */ };
以上示例中 name 的类型是 string | undefined 意味着可以将 string 或 undefined 的值传递给 sayHello 函数。
sayHello("semlinker");
sayHello(undefined);
此外,对于联合类型来说,你可能会遇到以下的用法:
let num: 1 | 2 = 1; type EventNames = 'click' | 'scroll' | 'mousemove';
示例中的 1、2 或 'click' 被称为字面量类型,用来约束取值只能是某几个值中的一个。
6.1 类型保护
当使用联合类型时,我们必须尽量把当前值的类型收窄为当前值的实际类型,而类型保护就是实现类型收窄的一种手段。
类型保护是可执行运行时检查的一种表达式,用于确保该类型在一定的范围内。换句话说,类型保护可以保证一个字符串是一个字符串,尽管它的值也可以是一个数字。类型保护与特性检测并不是完全不同,其主要思想是尝试检测属性、方法或原型,以确定如何处理值。
目前主要有四种的方式来实现类型保护:
6.1.1 in 关键字
interface Admin {
name: string;
privileges: string[];
} interface Employee {
name: string;
startDate: Date;
} type UnknownEmployee = Employee | Admin; function printEmployeeInformation(emp: UnknownEmployee) { console.log("Name: " + emp.name); if ("privileges" in emp) { console.log("Privileges: " + emp.privileges);
} if ("startDate" in emp) { console.log("Start Date: " + emp.startDate);
}
}
6.1.2 typeof 关键字
function padLeft(value: string, padding: string | number) { if (typeof padding === "number") { return Array(padding + 1).join(" ") + value;
} if (typeof padding === "string") { return padding + value;
} throw new Error(`Expected string or number, got '${padding}'.`);
}
typeof 类型保护只支持两种形式:typeof v === "typename" 和 typeof v !== typename,"typename" 必须是 "number", "string", "boolean" 或 "symbol"。 但是 TypeScript 并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护。
6.1.3 instanceof 关键字
interface Padder {
getPaddingString(): string;
} class SpaceRepeatingPadder implements Padder { constructor(private numSpaces: number) {}
getPaddingString() { return Array(this.numSpaces + 1).join(" ");
}
} class StringPadder implements Padder { constructor(private value: string) {}
getPaddingString() { return this.value;
}
} let padder: Padder = new SpaceRepeatingPadder(6); if (padder instanceof SpaceRepeatingPadder) { // padder的类型收窄为 'SpaceRepeatingPadder' }
6.1.4 自定义类型保护的类型谓词(type predicate)
function isNumber(x: any): x is number { return typeof x === "number";
} function isString(x: any): x is string { return typeof x === "string";
}
七、_ 数字分隔符
TypeScript 2.7 带来了对数字分隔符的支持,正如数值分隔符 ECMAScript 提案中所概述的那样。对于一个数字字面量,你现在可以通过把一个下划线作为它们之间的分隔符来分组数字:
const inhabitantsOfMunich = 1_464_301; const distanceEarthSunInKm = 149_600_000; const fileSystemPermission = 0b111_111_000; const bytes = 0b1111_10101011_11110000_00001101;
分隔符不会改变数值字面量的值,但逻辑分组使人们更容易一眼就能读懂数字。以上 TS 代码经过编译后,会生成以下 ES5 代码:
"use strict"; var inhabitantsOfMunich = 1464301; var distanceEarthSunInKm = 149600000; var fileSystemPermission = 504; var bytes = 262926349;
7.1 使用限制
虽然数字分隔符看起来很简单,但在使用时还是有一些限制。比如你只能在两个数字之间添加 _ 分隔符。以下的使用方式是非法的:
// Numeric separators are not allowed here.(6188) 3_.141592 // Error 3._141592 // Error // Numeric separators are not allowed here.(6188) 1_e10 // Error 1e_10 // Error // Cannot find name '_126301'.(2304) _126301 // Error // Numeric separators are not allowed here.(6188) 126301_ // Error // Cannot find name 'b111111000'.(2304) // An identifier or keyword cannot immediately follow a numeric literal.(1351) 0_b111111000 // Error // Numeric separators are not allowed here.(6188) 0b_111111000 // Error
当然你也不能连续使用多个 _ 分隔符,比如:
// Multiple consecutive numeric separators are not permitted.(6189) 123__456 // Error
7.2 解析分隔符
此外,需要注意的是以下用于解析数字的函数是不支持分隔符:
Number()
parseInt()
parseFloat()
这里我们来看一下实际的例子:
Number('123_456') NaN parseInt('123_456') 123 parseFloat('123_456') 123
很明显对于以上的结果不是我们所期望的,所以在处理分隔符时要特别注意。当然要解决上述问题,也很简单只需要非数字的字符删掉即可。这里我们来定义一个 removeNonDigits 的函数:
const RE_NON_DIGIT = /[^0-9]/gu; function removeNonDigits(str) {
str = str.replace(RE_NON_DIGIT, ''); return Number(str);
}
该函数通过调用字符串的 replace 方法来移除非数字的字符,具体的使用方式如下:
removeNonDigits('123_456') 123456 removeNonDigits('149,600,000') 149600000 removeNonDigits('1,407,836') 1407836
八、<Type> 语法
8.1 TypeScript 断言
有时候你会遇到这样的情况,你会比 TypeScript 更了解某个值的详细信息。通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。
通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。类型断言好比其他语言里的类型转换,但是不进行特殊的数据检查和解构。它没有运行时的影响,只是在编译阶段起作用。
类型断言有两种形式:
8.1.1 “尖括号” 语法
let someValue: any = "this is a string"; let strLength: number = (<string>someValue).length;
8.1.2 as 语法
let someValue: any = "this is a string"; let strLength: number = (someValue as string).length;
8.2 TypeScript 泛型
对于刚接触 TypeScript 泛型的读者来说,首次看到 <T> 语法会感到陌生。其实它没有什么特别,就像传递参数一样,我们传递了我们想要用于特定函数调用的类型。
参考上面的图片,当我们调用 identity<Number>(1) ,Number 类型就像参数 1 一样,它将在出现 T 的任何位置填充该类型。图中 <T> 内部的 T 被称为类型变量,它是我们希望传递给 identity 函数的类型占位符,同时它被分配给 value 参数用来代替它的类型:此时 T 充当的是类型,而不是特定的 Number 类型。
其中 T 代表 Type,在定义泛型时通常用作第一个类型变量名称。但实际上 T 可以用任何有效名称代替。除了 T 之外,以下是常见泛型变量代表的意思:
K(Key):表示对象中的键类型;
V(Value):表示对象中的值类型;
E(Element):表示元素类型。
其实并不是只能定义一个类型变量,我们可以引入希望定义的任何数量的类型变量。比如我们引入一个新的类型变量 U,用于扩展我们定义的 identity 函数:
function identity <T, U>(value: T, message: U) : T { console.log(message); return value;
} console.log(identity<Number, string>(68, "Semlinker"));
除了为类型变量显式设定值之外,一种更常见的做法是使编译器自动选择这些类型,从而使代码更简洁。我们可以完全省略尖括号,比如:
function identity <T, U>(value: T, message: U) : T { console.log(message); return value;
} console.log(identity(68, "Semlinker"));
对于上述代码,编译器足够聪明,能够知道我们的参数类型,并将它们赋值给 T 和 U,而不需要开发人员显式指定它们。
九、@XXX 装饰器
9.1 装饰器语法
对于一些刚接触 TypeScript 的小伙伴来说,在第一次看到 @Plugin({...}) 这种语法可能会觉得很惊讶。其实这是装饰器的语法,装饰器的本质是一个函数,通过装饰器我们可以方便地定义与对象相关的元数据。
@Plugin({
pluginName: 'Device',
plugin: 'cordova-plugin-device',
pluginRef: 'device',
repo: 'https://github.com/apache/cordova-plugin-device',
platforms: ['Android', 'Browser', 'iOS', 'macOS', 'Windows'],
}) @Injectable() export class Device extends IonicNativePlugin {}
在以上代码中,我们通过装饰器来保存 ionic-native 插件的相关元信息,而 @Plugin({...}) 中的 @ 符号只是语法糖,为什么说是语法糖呢?这里我们来看一下编译生成的 ES5 代码:
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r;
}; var Device = /** @class */ (function (_super) {
__extends(Device, _super); function Device() { return _super !== null && _super.apply(this, arguments) || this;
}
Device = __decorate([
Plugin({ pluginName: 'Device', plugin: 'cordova-plugin-device', pluginRef: 'device', repo: 'https://github.com/apache/cordova-plugin-device', platforms: ['Android', 'Browser', 'iOS', 'macOS', 'Windows'],
}),
Injectable()
], Device); return Device;
}(IonicNativePlugin));
通过生成的代码可知,@Plugin({...}) 和 @Injectable() 最终会被转换成普通的方法调用,它们的调用结果最终会以数组的形式作为参数传递给 __decorate 函数,而在 __decorate 函数内部会以 Device 类作为参数调用各自的类型装饰器,从而扩展对应的功能。
9.2 装饰器的分类
在 TypeScript 中装饰器分为类装饰器、属性装饰器、方法装饰器和参数装饰器四大类。
9.2.1 类装饰器
类装饰器声明:
declare type ClassDecorator = <TFunction extends Function>(
target: TFunction
) => TFunction | void;
类装饰器顾名思义,就是用来装饰类的。它接收一个参数:
target: TFunction - 被装饰的类
看完第一眼后,是不是感觉都不好了。没事,我们马上来个例子:
function Greeter(target: Function): void {
target.prototype.greet = function (): void { console.log("Hello Semlinker!");
};
} @Greeter class Greeting { constructor() { // 内部实现 }
} let myGreeting = new Greeting();
myGreeting.greet(); // console output: 'Hello Semlinker!';
上面的例子中,我们定义了 Greeter 类装饰器,同时我们使用了 @Greeter 语法糖,来使用装饰器。
友情提示:读者可以直接复制上面的代码,在 TypeScript Playground 中运行查看结果。
9.2.2 属性装饰器
属性装饰器声明:
declare type PropertyDecorator = (target:Object,
propertyKey: string | symbol ) => void;
属性装饰器顾名思义,用来装饰类的属性。它接收两个参数:
target: Object - 被装饰的类
propertyKey: string | symbol - 被装饰类的属性名
趁热打铁,马上来个例子热热身:
function logProperty(target: any, key: string) { delete target[key]; const backingField = "_" + key; Object.defineProperty(target, backingField, {
writable: true,
enumerable: true,
configurable: true }); // property getter const getter = function (this: any) { const currVal = this[backingField]; console.log(`Get: ${key} => ${currVal}`); return currVal;
}; // property setter const setter = function (this: any, newVal: any) { console.log(`Set: ${key} => ${newVal}`); this[backingField] = newVal;
}; // Create new property with getter and setter Object.defineProperty(target, key, { get: getter, set: setter,
enumerable: true,
configurable: true });
} class Person { @logProperty public name: string; constructor(name : string) { this.name = name;
}
} const p1 = new Person("semlinker");
p1.name = "kakuqo";
以上代码我们定义了一个 logProperty 函数,来跟踪用户对属性的操作,当代码成功运行后,在控制台会输出以下结果:
Set: name => semlinker Set: name => kakuqo
9.2.3 方法装饰器
方法装饰器声明:
declare type MethodDecorator = <T>(target:Object, propertyKey: string | symbol,
descriptor: TypePropertyDescript<T>) => TypedPropertyDescriptor<T> | void;
方法装饰器顾名思义,用来装饰类的方法。它接收三个参数:
target: Object - 被装饰的类
propertyKey: string | symbol - 方法名
descriptor: TypePropertyDescript - 属性描述符
废话不多说,直接上例子:
function LogOutput(tarage: Function, key: string, descriptor: any) { let originalMethod = descriptor.value; let newMethod = function(...args: any[]): any { let result: any = originalMethod.apply(this, args); if(!this.loggedOutput) { this.loggedOutput = new Array<any>();
} this.loggedOutput.push({
method: key,
parameters: args,
output: result,
timestamp: new Date()
}); return result;
};
descriptor.value = newMethod;
} class Calculator { @LogOutput double (num: number): number { return num * 2;
}
} let calc = new Calculator();
calc.double(11); // console ouput: [{method: "double", output: 22, ...}] console.log(calc.loggedOutput);
9.2.4 参数装饰器
参数装饰器声明:
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol,
parameterIndex: number ) => void
参数装饰器顾名思义,是用来装饰函数参数,它接收三个参数:
target: Object - 被装饰的类
propertyKey: string | symbol - 方法名
parameterIndex: number - 方法中参数的索引值
function Log(target: Function, key: string, parameterIndex: number) { let functionLogged = key || target.prototype.constructor.name; console.log(`The parameter in position ${parameterIndex} at ${functionLogged} has
been decorated`);
} class Greeter {
greeting: string; constructor(@Log phrase: string) { this.greeting = phrase;
}
} // console output: The parameter in position 0 // at Greeter has been decorated
十、#XXX 私有字段
在 TypeScript 3.8 版本就开始支持 ECMAScript 私有字段,使用方式如下:
class Person {
#name: string; constructor(name: string) { this.#name = name;
}
greet() { console.log(`Hello, my name is ${this.#name}!`);
}
} let semlinker = new Person("Semlinker");
semlinker.#name; // ~~~~~ // Property '#name' is not accessible outside class 'Person' // because it has a private identifier.
与常规属性(甚至使用 private 修饰符声明的属性)不同,私有字段要牢记以下规则:
私有字段以 # 字符开头,有时我们称之为私有名称;
每个私有字段名称都唯一地限定于其包含的类;
不能在私有字段上使用 TypeScript 可访问性修饰符(如 public 或 private);
私有字段不能在包含的类之外访问,甚至不能被检测到。
10.1 私有字段与 private 的区别
说到这里使用 # 定义的私有字段与 private 修饰符定义字段有什么区别呢?现在我们先来看一个 private 的示例:
class Person { constructor(private name: string){}
} let person = new Person("Semlinker"); console.log(person.name);
在上面代码中,我们创建了一个 Person 类,该类中使用 private 修饰符定义了一个私有属性 name,接着使用该类创建一个 person 对象,然后通过 person.name 来访问 person 对象的私有属性,这时 TypeScript 编译器会提示以下异常:
Property 'name' is private and only accessible within class 'Person'.(2341)
那如何解决这个异常呢?当然你可以使用类型断言把 person 转为 any 类型:
console.log((person as any).name);
通过这种方式虽然解决了 TypeScript 编译器的异常提示,但是在运行时我们还是可以访问到 Person 类内部的私有属性,为什么会这样呢?我们来看一下编译生成的 ES5 代码,也许你就知道答案了:
var Person = /** @class */ (function () { function Person(name) { this.name = name;
} return Person;
}()); var person = new Person("Semlinker"); console.log(person.name);
这时相信有些小伙伴会好奇,在 TypeScript 3.8 以上版本通过 # 号定义的私有字段编译后会生成什么代码:
class Person {
#name: string; constructor(name: string) { this.#name = name;
}
greet() { console.log(`Hello, my name is ${this.#name}!`);
}
}
以上代码目标设置为 ES2015,会编译生成以下代码:
"use strict"; var __classPrivateFieldSet = (this && this.__classPrivateFieldSet)
|| function (receiver, privateMap, value) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to set private field on non-instance");
}
privateMap.set(receiver, value); return value;
}; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet)
|| function (receiver, privateMap) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to get private field on non-instance");
} return privateMap.get(receiver);
}; var _name; class Person { constructor(name) {
_name.set(this, void 0);
__classPrivateFieldSet(this, _name, name);
}
greet() { console.log(`Hello, my name is ${__classPrivateFieldGet(this, _name)}!`);
}
}
_name = new WeakMap();
通过观察上述代码,使用 # 号定义的 ECMAScript 私有字段,会通过 WeakMap 对象来存储,同时编译器会生成 __classPrivateFieldSet 和 __classPrivateFieldGet 这两个方法用于设置值和获取值。
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务https://github.com/krasimir/l...
如果你必须在同一个浏览器中从一个标签页发送消息到另一个标签页,你不必用艰难的方式。Local storage bridge在这里让任务变得更简单。
基本使用:
// 发送 lsbridge.send(‘app.message.error’, { error: ‘Out of memory’ });
// 监听 lsbridge.subscribe(‘app.message.error’, function(data) { console.log(data); // { error: ‘Out of memory’ } });
Basil.js统一了session、localStorage和cookie,为你提供了一种处理数据的直接方法。
基本使用:
let basil = new Basil(options);
basil.set(‘name’, ‘Amy’);
basil.get(‘name’);
basil.remove(‘name’);
basil.reset();
https://github.com/marcuswest...
Store.js像其他东西一样处理数据存储。但还有更多的功能,它的一个高级特性是让你更深入地访问浏览器支持。
基本使用:
store.set(‘book’, { title: ‘JavaScript’ }); // Store a book store.get(‘book’);
// Get stored book store.remove(‘book’); // Remove stored book store.clearAll(); // Clear all keys
https://github.com/pamelafox/...
它与localStorage API类似。事实上,它是localStorage的一个封装器,并使用HTML5模拟memcaches函数。在上面的文档中发现更多的功能。
基本使用:
lscache.set(‘name’, ‘Amy’, 5); // 数据将在5分钟后过期 lscache.get(‘name’);
Lockr建立在localStorage API之上。它提供了一些有用的方法来更轻松地处理本地数据。
是什么让你要使用此库而不是localStorage API?
好吧,localStorage API仅允许你存储字符串。如果要存储数字,则需要先将该数字转换为字符串。在Lockr中不会发生这种情况,因为Lockr允许你存储更多的数据类型甚至对象。
基本使用:
Lockr.set(‘name’, ‘Amy’);
Lockr.set(‘age’, 28);
Lockr.set(‘books’, [{title: ‘JavaScript’, price: 11.0}, {title: ‘Python’, price: 9.0}]);
https://github.com/arokor/barn
Barn在localStorage之上提供了一个类似Redis的API。如果持久性很重要,那么你将需要这个库来保持数据状态,以防发生错误。
基本使用:
let barn = new Barn(localStorage); // 原始类型 barn.set(‘name’, ‘Amy’); let name = barn.get(‘name’);
// Amy // List barn.lpush(‘names’, ‘Amy’);
barn.lpush(‘names’, ‘James’); let name1 = barn.rpop(‘names’); // Amy let name2 = barn.rpop(‘names’);
// James
https://github.com/localForag...
这个简单而快速的库将通过IndexedDB或WebSQL使用异步存储来改善Web的脱机体验。它类似于localStorage,但具有回调功能。
基本使用:
localforage.setItem(‘name’, ‘Amy’, function(error, value) { // Do something });
localforage.getItem(‘name’, function(error, value) { if (error) { console.log(‘an error occurs’);
} else { // Do something with the value }
});
很神奇的是它提供中文文档
https://github.com/jas-/crypt.io
crypt.io使用标准JavaScript加密库实现安全的浏览器存储。使用crypto.io时,有三个存储选项:sessionStorage,localStorage或cookie。
基本使用:
let storage = crypto; let book = { title: ‘JavaScript’, price: 13 };
storage.set(‘book’, book, function(error, results) { if (error) { throw error;
} // Do something });
storage.get(‘book’, function(error, results) { if (error) { throw error;
} // Do something });
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
在开始正文前,我们先把本文涉及到的一些内容提前定个基调。
Promise 中只有涉及到状态变更后才需要被执行的回调才算是微任务,比如说 then
、 catch
、finally
,其他所有的代码执行都是宏任务(同步执行)。
上图中蓝色为同步执行,黄色为异步执行(丢到微任务队列中)。
这个问题我们根据 ecma 规范来看:
[[PromiseFulfillReactions]]
和 [[PromiseRejectReactions]]
中。如果你看过手写 Promise 的代码的话,应该能发现有两个数组存储这些回调函数。
了解完以上知识后,正片开始。
Promise.resolve()
.then(() => { console.log("then1"); Promise.resolve().then(() => { console.log("then1-1");
});
})
.then(() => { console.log("then2");
});
以上代码大家应该都能得出正确的答案:then1 → then1-1 → then2
。
虽然 then
是同步执行,并且状态也已经变更。但这并不代表每次遇到 then
时我们都需要把它的回调丢入微任务队列中,而是等待 then
的回调执行完毕后再根据情况执行对应操作。
基于此,我们可以得出第一个结论:链式调用中,只有前一个 then
的回调执行完毕后,跟着的 then
中的回调才会被加入至微任务队列。
大家都知道了 Promise resolve
后,跟着的 then
中的回调会马上进入微任务队列。
那么以下代码你认为的输出会是什么?
let p = Promise.resolve();
p.then(() => { console.log("then1"); Promise.resolve().then(() => { console.log("then1-1");
});
}).then(() => { console.log("then1-2");
});
p.then(() => { console.log("then2");
});
按照一开始的认知我们不难得出 then2
会在 then1-1
后输出,但是实际情况却是相反的。
基于此我们得出第二个结论:每个链式调用的开端会首先依次进入微任务队列。
接下来我们换个写法:
let p = Promise.resolve().then(() => { console.log("then1"); Promise.resolve().then(() => { console.log("then1-1");
});
}).then(() => { console.log("then2");
});
p.then(() => { console.log("then3");
});
上述代码其实有个陷阱,then
每次都会返回一个新的 Promise,此时的 p
已经不是 Promise.resolve()
生成的,而是最后一个 then
生成的,因此 then3
应该是在 then2
后打印出来的。
顺便我们也可以把之前得出的结论优化为:同一个 Promise 的每个链式调用的开端会首先依次进入微任务队列。
以下大家可以猜猜 then1-2
会在何时打印出来?
Promise.resolve()
.then(() => { console.log("then1"); Promise.resolve()
.then(() => { console.log("then1-1"); return 1;
})
.then(() => { console.log("then1-2");
});
})
.then(() => { console.log("then2");
})
.then(() => { console.log("then3");
})
.then(() => { console.log("then4");
});
这题肯定是简单的,记住第一个结论就能得出答案,以下是解析:
resolve
后第一个 then
的回调进入微任务队列并执行,打印 then1
resolve
后内部第一个 then
的回调进入微任务队列,此时外部第一个 then
的回调全部执行完毕,需要将外部的第二个 then
回调也插入微任务队列。
then1-1
和 then2
,然后分别再将之后 then
中的回调插入微任务队列
then1-2
和 then3
,之后的内容就不一一说明了
接下来我们把 return 1
修改一下,结果可就大不相同啦:
Promise.resolve()
.then(() => { console.log("then1"); Promise.resolve()
.then(() => { console.log("then1-1"); return Promise.resolve();
})
.then(() => { console.log("then1-2");
});
})
.then(() => { console.log("then2");
})
.then(() => { console.log("then3");
})
.then(() => { console.log("then4");
});
当我们 return Promise.resolve()
时,你猜猜 then1-2
会何时打印了?
答案是最后一个才被打印出来。
为什么在 then
中分别 return
不同的东西,微任务的执行顺序竟有如此大的变化?以下是笔者的解析。
PS:then
返回一个新的 Promise,并且会用这个 Promise 去 resolve
返回值,这个概念需要大家先了解一下。
根据规范 2.3.2,如果 resolve
了一个 Promise,需要为其加上一个 then
并 resolve
。
if (x instanceof MyPromise) { if (x.currentState === PENDING) {
} else {
x.then(resolve, reject);
} return;
}
上述代码节选自手写 Promise 实现。
那么根据 A+ 规范来说,如果我们在 then
中返回了 Promise.resolve
的话会多入队一次微任务,但是这个结论还是与实际不符的,因此我们还需要寻找其他权威的文档。
根据规范 25.6.1.3.2,当 Promise resolve
了一个 Promise 时,会产生一个NewPromiseResolveThenableJob,这是属于 Promise Jobs 中的一种,也就是微任务。
This Job uses the supplied thenable and its then method to resolve the given promise. This process must take place as a Job to ensure that the evaluation of the then method occurs after evaluation of any surrounding code has completed.
并且该 Jobs 还会调用一次 then
函数来 resolve Promise
,这也就又生成了一次微任务。
这就是为什么会触发两次微任务的来源。
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
轻拟物本身也是拟物,所以它的核心基础和拟物设计师一致的,只是省略了更多复杂的细节。而对于整个拟物的体系来讲,最重要的东西实际上只有2个,形体、光影。
1. 形体表现
形体的表现,就是对图形外轮廓的样式的呈现。在过去我们写的图标分享中,有写过面性图标进阶的设计中,可以包含更多的细节、内部元素,而不是仅仅只有外轮廓。
轻拟物的形体设计就要处于进阶面性图标或者更难的水平之上,即你要把这个图形的内容有明确的示意并画出来,而不是用抽象的图形做填充而已。
比如大众点评的快速入口图标,虽然看起来很复杂,但是那是配色上的复杂,而不是形体轮廓上的具象化。
换句话说,拟物插画的图形基底,类似扁平插画风格图标,不能表现得太抽象,也不能增加过多的细节,需要一种恰到好处的平衡(玄学),这就非常考验设计师的判断和经验了。
并且,在描绘轮廓的时候,新手尽可能的采取正视图来进行绘制,而不要通过俯视图、侧视图、斜视图等方法来呈现图形的多个面,这样难度会大幅度上升,比如下面这种情况。
2. 光影表现
除了形体外,光影就是整个拟物的灵魂了。
当一个完整的图形完成填充色时,它是扁平图案,如果完成光影呈现的时候,它就是三维空间的立体图形,比如下面这个圆的案例:
在拟物的设计中,我们对光影的定义是至关重要的,所以首先就是针对该图形确定光源的方向,是上方、前方、左上还右上,这对后续的设计有一连串的影响。
如果对光影没有正确的解释,那么在制作细节的渐变角度、投影的使用上,就会产生错误的设计,造成光影视觉冲突和矛盾。
在创建了光源以后,物体受到光线的影响就会产生明暗面和投影,可以简单划分成4个部分,高光、亮部、暗部、投影。
这和我们学习的素描有一定的差异,美术中对光影的表现还会包含明暗交界、反光面,这对于轻拟物的来说负担太重,所以我们要去掉它们,接下来重点讲讲高光和暗部。
高光是物体作为受光物对光源的直接反映,比如人像摄影中人眼眸中的高光就是对闪光灯的镜像表现,再或者一拳超人中男主光头上长期存在的高光配饰(多数动画的光头角色都有)……
高光可以非常有效的增加画面的层次和对比性,让物体看上去更有冲击力和观赏性。
而暗部,则完全是为了正常表现物体结构和弧度增加的示意,因为不在受光面,所以颜色会变暗。在实际操作过程中,我们可以通过渐变的方式开控制明暗的表达,但尽量不要直接手动设置一个渐变色出来,而是为它叠加暗部或亮部的黑白透明度渐变。
了解这几个特性以后,下面,我们就通过一个实例来讲解一下轻拟物设计的过程吧。
作为轻拟物的演示,直接画个图标讲一遍怎么操作是没什么用的,我们要从实际场景出发,用它来解决一些真实的问题。比如看看下面的 KFC 官方 APP 首页:
总结它的问题,不难发现首页顶部业务功能太多了,顶部图标就包含30个(加滑动的),虽然每个模块图标单看都没有硬伤,但堆积到一起,就使得顶部缺乏足够的信息层级和对比性。
我们要做的,就是通过轻拟物的方式,调整快速入口最大的三个图标,凸显它们的重要性以及和其它图标的视觉差异性。先从第一个图标开始,讲解一下如何完成轻拟物化设计的升级。
第一步:确认轮廓造型
第一个操作,即确定图标本身的轮廓。根据原有图标的样式我做了一些改动,包括加粗车头,减少高度,增加车灯等。并对每一个模块进行纯色的填充,定义它们的色彩和做出区分。
形体的重要性在于要对图形本身有比较合理的呈现,不要让比例失调和图不达意。
第二步:完善图形细节
这一步,就要在原有基础上,进行下一步的深入。包括对一些细节交代得更清楚一点,增加一些有趣的小元素等等,完善它的具体样式。
第三步:增加基础的暗部表现
在这里,我们就要开始为图标增加高光了,高光从右上角打下来,那么有叠加关系的元素就会产生一个向下的投影。并且反向暗部的表现,让整体的立体感稍强。
这一步在软件中主要使用蒙版功能,通过蒙版在背景上方创建一个图层,然后添加深色的透明度渐变,就可以表现出对暗部和投影的效果。
第四步:增加高光效果
接着,就是最后一步,将高光添加到画面中来,将整个图标的质感进行拉升。
通过上面的演示,我们可以将整个拟物设计流程精简成:
然后,通过这样的步骤,再来完成后续的两个图形,拆解完的效果如下。
然后,再用这三个修改后图标套用进原来的页面,并做出对应的修改,再来看看前后对比:
通过这个对比,我们就可以看出在这个复杂的首页头部中,轻拟物风格可以从一众平面中被凸显出来,且不会显得太突兀和复杂。
而这就是轻拟物在项目设计中的实际作用,当画面元素已经开始超负荷,且容易导致同质化的审美疲劳和主次不清时,就是轻拟物登场的时候了。
最后的总结,学习轻拟物就是增加我们完成界面视觉输出的可能性,为视觉创意增加一些储备弹药,以应对越来越复杂的互联网产品和职业要求。
我们只在这篇罗列了制作的顺序和思路,并没有把软件的操作完全放出来,一方面是因为时间上来不及,另一方面是希望大家不会被软件的使用框住。只要概念清楚了,那么使用 PS、AI、Skecth、Figma 还是 Affinity 等软件都可以做出来。
文章来源:站酷 作者:超人的电话亭
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
文字是界面中最核心的元素,是产品传达给用户的主要内容,它的承载体即是字体。
前半部分从字体的最基本属性(字族、字号、字重、大小写等)说起,熟悉字体的那些特征,了解字体在界面中的作用,以及iOS与Android系统字体的使用规范。
字体是界面设计的基石
字体是排版中最重要的元素,对用户的阅读体验有着至关重要的作用。一般来说,设计师需要了解的字体通常有中文字体和西文字体两种。西文字体由来已久,从最早的罗马字体到现在苹果手机中的SF-UI字体,经历了许多设计上的变革。而中文字体的发展并没有西文字体那么顺利,数量上也远远落后于其他字体。但中国设计正在崛起,我们也看到越来越多的设计团队和设计师加入字体设计的队伍,数量上正在呈指数级别增加。
设计是一门非常严谨的学科,里面蕴含了很多道理,就连最基础的字体选择和排版,都经过了将近千年的发展和演变,有非常多的专业知识。像平面设计一样,在UI设计中字体的使用也有相应的规范,设计师应懂得这些基础知识,才能将字体为自己所用。
本篇就从我们常用的设计软件(sketch、Figma、P hotoshop)字符面板开始,来聊聊有关字体与排版应用方面的知识。
Font 中文翻译为「字型」,是指字的粗细、宽度和样式,是一套具有同样风格和尺寸的字形。例如「Regular_16pt_SF-UI」。
Typeface 中文翻译为「字体」,是指一整套的字形,一个或多个字型的多尺寸的集合,例如「SF-UI」里有不同粗细(Regular、Blod、Light)和不同宽度(12pt、14pt、20pt)。
Glyph 中文翻译为「字形」,是指单个字的形体或是字体的骨骼。 同一字可以有不同的字形,而不影响其表达的意思,例如汉字中的「令」字,第三笔可以是一点或一撇, 最末两笔可以作「ㄗ」或「マ」。
Font和Typeface常常被混淆使用,其实可以这样理解,前者指一种设计,后者指具体的产品。
1. 族类 GenericFamily
族类就是不同字体类型,例如阿里巴巴普惠体、方正新书宋、站酷酷黑体等。
而这些众多字体又可分为「衬线体」和「无衬线体」。
衬线体
宋体就是衬线体,特点就是笔画开始和末端的地方都有额外的装饰,且笔画的粗细有所不同。在传统的正文印刷中,普遍认为衬线字体能带来更加的可读性。常见的衬线体有宋体、Times New Roman、Georgia等。
衬线体一般在APP中比较少见,文字阅读类偏爱这种衬线体,例如「单读」,大标题用的是「華康標宋體」、正文内容用的是「苹方-纤细」而英文用的是「XCross Traditional Bold」
黑体
黑体是无衬线字体,特点是笔画没有额外的装饰,且笔画的粗细差不多。相比严肃的衬线体,简单干净的无衬线体给人一种休闲轻松的感觉。因此大多数App都是使用黑体作为默认字体。如冬青黑体、思源黑体、Myriad等。
2. 字族 FontFamily
一个族类包含不同的字体,然而一个字体又可能有好几种字族。如果电脑安装了Helvetica,在Sketch字体选择器中会发现超过40多个前缀是Helvetica的字族。这是为了协助人们在不同的使用场景下表达合适的意思。
知识点:
基本字族包括细体、标准、粗体、斜体,值得注意的是,斜体字常用在引用文本上,代表「本段文字引用的是另一个著作」的含义。
例如:「若我们能以满怀新鲜的眼神去观照日常,「设计」的意义定会超越技术的层面,为我们的生活观和人生观注入力量。」(引自原研哉的《设计中的设计》)
3. X-height(X字高)
在西文字体中,x高度是指字母的基本高度,就是基线和主线之间的距离。它指一个字体中小写字母的x高度,在现代字体设计领域,x高度代表了一个字体的设计因素,因此在一些场合字母x本身并不完全等于x字高。
除了字母a、c、e、m、n、o等高度一样,还有一些小写字母的字高都比x字高要大,并分为两类:一是含有升部的字母,字母笔画含有向上部分,如字母b、d、h;另一类是含有降部的字母,字母的笔画向下超过了基线,如字母g、p、q。
4. 字号 Font-size
字号就是字体大小,通常在网页端使用px作为字号的单位。移动端兴起后,iOS字体单位是pt,Android是sp。
以iOS为例,正文字号不应小于11pt,这样才能被正常阅读,建议在14-18pt之间。在使用较大的字体来获得更好的易读性的同时,我们也应相应地减小字体的字重,考虑Light、Thin,因为过重的字体会太过醒目,影响其他内容的显示效果。
当字体大小为12-18pt时,建议使用Regular,18-24pt时,使用Light,24-32pt,使用Thin,当字体大小超过32pt时,建议使用Ultralight。
字号大小决定了信息的层级和主次关系,合理有序的字号设置能让界面信息清晰易读、层次分明;相反,糟糕无序的字号使用会让界面混乱不堪,影响阅读体验。
设计中的最小字号
我们都知道在界面设计中最小字号不能低于20px,那是因为,正常情况下,在手机距离眼睛30cm左右,使用视角计算公式,我们能识别到的的文字大小为h= 2*30·tan(0.3/2) ≈ 0.157cm ,拿我们经常使用iPhone7的尺寸1334×750为例。iPhone7的dpi为324,也就是一英寸上显示324个像素,1英寸为2.54cm,那么0.157cm=324*(0.157/2.54cm)= 20px。
字号的基数关系
我们在做设计时,字号的单位最好使用一个基数作为倍增,如2、4、6、8、10 或者3、6、9、12。但其实我们在做移动端设计时,单位需要遵循偶数原则,因为开发中的单位是以一倍图的基数来进行计算。那么其实在制定字体规范中,使用2为单位会导致字号过多,且2号字体的差异化不大。所以在字号方面我们使用4作为单位是比较合适的:一是适配后在@2x跟@3x不会出现半像素,二是使用4为单位,能满足字体大小的均衡。
5. 字重 FontWeight
Weight,中文翻译为「字重」,是指字体笔画的粗细,字体中很重要一个概念,不同字重传递出来视觉感受完全不一样。一般在字体家族名后面注名Thin、Light、Regular、Blod、Black、Heavy等。不同的字体厂商划分字重各有不同,例如「苹方」字体就有6种不同的字重。
一般都有细体、正常、粗体三种基本字族。在应用场景上,通常「细体」多用于超大号字体;「正常」用于正文内容;「粗体」表示强调,多用于标题;
两种字重属性
轻字重:传递出轻盈放松的视觉感受,常配合粗的字重使用,在一些辅助信息,说明文案时候使用;
重字重:视觉感受庄重,很重要,常用在重点强调的文字,页面大标题,数字,引导行动操作点上等;
例如百度网盘「发现」页就用了Regular、Medium、Semibold三种字重以拉开信息层次对比;
知识点:
需要注意的是:在进行界面设计时,不要用软件自带的文本加粗,它不仅破坏了字体本身的美感,还改变了文字原本的字宽,小字体下会模糊不清,合理的方式是使用字体本身的字重来控制粗细。
注意超细体的字体
字重超细的字体要谨慎使用。如果你设计的文本是装饰性倒还好,如果是需要用户能清晰阅读的,就要特别慎重,能不用就不用,否则在部分低分辨率的手机屏幕上看起来会非常糟糕。
6. 字色 FontColor
字色即文字对应的颜色,不做过多解释。需要大家注意的是 远离纯黑色和纯灰色!
纯黑色就像没有生命力的深渊,能吞噬所有细节,使用户陷入冷冰冰的极端情绪中。纯黑色还会与白色产生强烈的对比度,看久了就会感觉疲劳,让用户产生焦虑情绪。
还有就是真实世界中是不存在纯黑色的。尝试在色彩中加入一些色相,这样就不会让页面看上去死气沉沉的。例如iOS系统「设置」页面背景色就是加入了白色的低饱和度蓝色,看上去柔和自然。
7. 字符样式 FontStyle
除了以上几个最常用的文字属性外,还有几个使用频率比较低的字体设置。例如带下划线的、删除线的文本。「下划线文本」一般出现在「文字按钮」或带链接的网址,而「删除线文本」一般会出现在商品橱窗的现价、原价
例如「CCtalk」的课程现价和原价的区分,原价用删除文本,「微信读书」文章底部「加入书架 随时阅读」就是带链接的下划线文本。
8. 字符选项 Text options
Ps和Sketch都有文字(字符)选项一栏,主要针对西文字母大小写格式变换的设置。最常见有默认大小写、全部大写、全部小写和小型大写字母,Ps里面还有「上标」和「下标」。
默认大小写:即正常大小写格式,软件不做干预;
全部大写:如果输入的是小写字母,选择这个选项,软件会强制把小写改为大写;
全部小写:如果输入的是大写字母,或者只是首字母大写,选择这个选项,软件会强制把所大写改为小写;
小型大写字母:这个选项比较特殊,所谓「小型大写」就是,在字号一样的情况下,与小写字母一样高,外形与大写字母保持一致。
注意英文大写
纯大写的字母文本本身不太适合大篇幅阅读,会加大阅读障碍,用的时候注意要额外拉开字母之间的字间距,提升可读性。
9. 全角与半角 Full-width and half-width
全角是指一个字符占用两个标准字符的位置。中文字符、全角的英文字符、国标GB2312-1980中的图形符号、特殊符号都是全角字符。半角是指一个字符占用一个标准字符的位置。
通常情况下,英文字母、数字、符号等都是半角字符。半角和全角主要是针对标点符号来说的,因为正常情况下没有打全角英文的需求。
知识点:
在设计作品时也一定要记得中文搭配全角符号,英文使用半角符号。否则会出现诸如「你好.」或者「t h a n k s。」这样的错误。可按键盘「capslock」键切换全角和半角。这个小知识点虽然非常基础,却也是设计中经常出错的地方。
众所周知,iOS和Android两大阵营都有各自的设计系统,要作出符合平台规范的设计,设计师应熟读各平台的设计规则。因为本篇以讲字体为主,我们就来看看iOS和Android各自字体的规范是什么样的。
1. iOS字体规范
可用字体
在iOS系统规范中,中文字体是「苹方」字体。英文字体是「San Francisco」也简称「SF-UI」,英文还有另外一个衬线体「NewYork」。除了在iOS和Mac OS上,还单独为Watch OS单独对字体进行了调整,命名为 San Francisco Compact。
字体设置
因为在英文字体下,字体环境比较复杂,为了让字体在任何地方看起来都最佳,苹果官方针对不同字号开发了两套「SF-UI Text」和「SF-UI Pro」字体,而每套字体下面又分为Text(文本模式)与Display(展示模式)两种属性,Text只有6个字重,而Display则有9个字重。
这么多类型的字体我们该怎么用呢?iOS的建议是,在字号小于20pt时,使用SF-UI Text,大于或等于20pt时,则使用SF-UI Display。这需要我们在界面设计时手动切换。
对于「NewYork」,小于20点的文本使用小号,20到35点之间的文本使用中号,36到53点之间的文本使用大号,54点或更大的文本使用特大号。
苹方字体提供了6个字重供设计开发者使用。所以从iOS11开始,iOS使用Semibold中粗体、大字号作为界面的标题变的更为流行起来,较为明显的有 iOS 中的一些原生APP,比如App Store、Apple Music…
知识点:
在iOS中,默认字体单位是「pt」,正文字号不应小于11pt,建议在15-18pt之间。在使用较大的字体来获得更好的易读性同时,也应该相应地减小字体的字重,因为过重的字体会太过醒目厚重,影响其他内容的显示效果。
iOS更全面的文字设置
动态类型可以通过让读者选择他们喜欢的文本大小来提供额外的灵活性,除了标准的动态类型大小之外,iOS系统还为有阅读大字体的需求的用户提供了许多字号上的调整(可在系统字体显示大小设置)
iOS「显示与亮度」下设置「文字大小」模式
「苹方」和「SF-UI」字体可在iOS规范网站免费下载
网址:https://developer.apple.com/fonts/
2. Android字体规范
可用字体
在Android设备中,Android始祖Google为了更好的追求视觉效果,联合了Adobe设计发布了「思源黑体」(Noto)来作为中文默认字体,「Roboto」为英文字体。
字体类型
思源黑体,英文名为「NotoSans CJK」。该字体不仅仅在字形上更易于在屏幕阅读,并且拥有7种字重,充分满足了设计的要求。
英文「Roboto」字体,只有6个字重,视觉语言与思源黑体Noto保持一致。该字体具有「现代的」和「平易近人」的气质,是「Material Design」设计风格下的推荐字体。
字体设置
Material Design字体规范,字体类型比例支持的十三种样式的组合。它包含可重用的文本类别,每种类别都有预期的应用程序和含义。
注:Web浏览器根据根元素大小计算REM(根em大小)。 现代网络浏览器的默认值为16px,因此转换为SP_SIZE / 16 = rem。
△Material Design设计类型比例。(字母间距值与Sketch兼容。)
知识点:
值得注意的是,在安卓的字体单位中,不再以px,pt作为单位而是统一的使用了sp,换算方式是:
px = sp*ppi/160 ,sp = px / (ppi / 160)
以iPhone7为例,尺寸是750×1334,密度326ppi 来换算,那么Android的1dp = 1 * 326/160 ≈ 2px
「思源黑体」和「Roboto」字体可在GoogleFonts免费下载,并且可以商用。
3. 话题扩展
值得一提的是,越来越多的手机厂商,为了能够强化自身的品牌形象,推出了定制款的字体。
比如小米的「小米兰亭」:
OPPO的「OPPO Sans」:
三星的「SamsungOne」:
字体基础知识小结
正如开头所说,文字是界面中最核心的元素,字体作为基本语言,是设计中体现品牌很重要一点,字体选择非常重要,字体也是设计中占比(约 80%)最大的内容,所以我们一定要熟练掌握,接下来将从文字行高、字间距、行间距等说起,围绕字体排版继续聊。
设计中,好的排版能让用户愉快地阅读,而不好的排版则会给用户带来糟糕的阅读体验。因此排版的潜在重要性不容忽视。
无论是在西方国家还是亚洲国家,大部分人们的阅读习惯都是从左到右。这种阅读模式已经延续了几百年,因此如无特殊需求,你应该使你的文本左对齐,这样符合人们一贯的阅读习惯(阿拉伯地区除外)。人缺乏耐性,在阅读过程中更似是一种远近不定的跳跃「扫视」。枯燥的文字如果没有经过任何排版处理,会让读者瞬间失去阅读的兴趣,除非非读不可。所以通过改进文字内容的结构和排版来提高阅读性乃至「诱读性」,是一件十分必要的事情。
1. 字间距与字偶间距
字间距,英文名为「spacing」,即字符间的距离,事实上他是字符图形外边界框的尺寸和字符在方框中的位置的距离。
字偶间距,英文名为「Kerning」,也叫做「字距调整」,是在字间距的基础上,为实现不同字偶(一对字符)可以有不同字间距的调整值。我们都知道,不同的字母外形不同,所以只有同样的字间距是不协调的。例如,「NA」间是标准的字间距,而「WA」由于W和A的形状可以重叠,所以需要负字偶间距才能达到协调一致的外观。
在大段落文字排版时,我们一般不需要更改字间距和字偶间距,因为字体设计师已经对他们做过了最优处理。在对一组字符单独设计时,就需要考虑字偶间距,以达到更协调的视觉效果。总的来说,字号越小,字距应当相对越大,行高也应该相对越大。反之亦然。
西文字间距还分为:比例字体和等宽字体
比例字体:根据字符外形特点设置不同字宽的字体,使得字体外形协调,可读性更好;
等宽字体:每个字符设置相同字宽的字体,字符间距较大,它们经常被用于显示计算机代码示例;
2. 字间距的三种形式
标准间距:即默认的字间距,字与字之间的距离不大也不小,在设计中要根据不同的字号设置不同的字间距来排版,往往需要我们根据字号、字重的不同动态调节间距参数,避免千篇一律使用软件默认间距。
紧凑间距:字与字之间的距离向里缩进,在字符工具里的「字间距」数值为负数,一般在-5%~-30%不等,通常用在标题中。
宽松间距:与紧凑型间距相反,字与字之间间距向外扩大,在字符工具里的「字间距」数值为正数,一般在5%~30%不等,通常用在正文中。
知识点:
提示:字间距虽然有以上三种形式,但是在实际工作中也要具体问题具体分析,例如有些中文字体本身「外边框」的距离就比较大,如果再加大字间距,就会显得过于分散。
3. 西文词距
在西文阅读时,视觉上的自然界限是「词距」而不是「字距」。如果排版时需要进行例如「两端对齐」的行内间距调整,中文直接可以动「字距」,把调整量均匀地放到每个字间里;而西文却是动「词距」,只能把调整量加到词距里,而单词内部的字距依然是保持字体设计师预设的原始字距,这是保证西文易读性的关键所在。
4. 标点避头尾
在古代,书籍排版可以做到字间距恒定,原因是古代不存在「标点」,也就没有「标点避头尾」导致的种种问题。而现代汉语存在标点符号,有的标点不能放在行首(如逗号、顿号、句号等),有的不能放在行尾(引号、前括号等)。处理方式叫做「优先推出式」标点避头尾,通过将本行内的标点宽度进行挤压后,腾出了空间给本来排不到的逗号,确保了字间距的恒定(篇幅限制,本文暂不谈文字编排具体调整方法)。更详细的介绍可移步字体设计与排印网站 Type is Beautiful 了解。
5. 文本框
在设计软件中,我们在添加文本时,就会创建一个文本区域,例如Sketch中文本区域有三种类型,自动宽度、自动高度、固定尺寸,而「固定尺寸」可配合「设置文字层垂直对齐方式」使用。
6. 对齐方式
文本的对齐方向有左、中、右三种对齐方式。文本对齐的标准是基于文本区域的边界决定的,只有设置固定的文本区域对齐才有意义。
7. 行高
行高或行距是文字排版的基础参数,也是排版品质的先决要素之一。行高是一行文本垂直方向的高度,这个高度和字高无关,文字内容水平居中,如下图所示:
8. 英文行高
英文的行高指的是一行英文的基线与下一行英文的基线之间的距离,基线(baseline)是英文字体结构中的概念,在css里文字的元素都是按基线来对齐的。西文基本行高是字号的1.2倍左右,字体有上伸部(ascender)和下延部(descender)可来创造行间空隙。
9. 中文行高
中文的结构属于方块字没有基线,所以中文的行高指的是一行中文的最底部与下一行中文最底部之间的距离。中文因为字符密实且高度一致,所以一般行高需要更大,根据不同用户人群(儿童、年轻人、老年人)以及使用环境,可达到1.5~2倍甚至更大。
知识点:
提示:不管是标题、正文还是注释文字,行高都不易过大或过小,会导致阅读困难。总的来说,字号越大行高应该越小,字号越小行高应该越大。
10. 行长
在《中文排版需求》里,明确写明了这项基本要求:
2.3.5 版心设计的注意事项:「一行的行长应为文字尺寸的整数倍,各行的位置尽可能头尾对齐。」
「一行的行长应为文字尺寸的整数倍」,这一基本的、理所应当的需求看似简单,但是在实际操作中,却往往由于单位换算等各个原因没有得到实现。对于后半句提到的「头尾对齐」,将另文讨论,但显然也和本文相关。正因为设计师想实现「头尾对齐」才会盲目地用软件的「两端对齐」功能,大家可以看看身边的印刷品,注意看一下每段的最后一行间距是不是统一,就可以知道设计师有没有按照这个原则排版。
中文的一个字占两个字符,英文一个字占一个字符。正文的行长通常在40到60个字符之间。在行长较宽的区域(例如桌面)中,包含最多120个字符的较长行将需要将行高增大。行长过长易读性就会变差,读者阅读时容易串行,造成阅读困难。合理的行长使用户在行间跳转时感到轻快和愉悦,反之则会使阅读成为一种负担。
11. 行间距
行距是指临近两行之间的距离。合适的行距让用户阅读舒服,阅读效率也高,行距太紧凑会让内容挤成一团,实现无法正常阅读;行距太宽松会让内容松散,产生了我们通常意义上的「河流」,阻断了行的视线,Photoshop中默认行距是1.2倍的字号,例如字号是30px,那么将行距设为36px和默认「自动」的效果一致。1.2倍的行距对中文排版来说通常过小,合适的行间距通常为1.5~2倍之间。文本字体越小,两行之间的行间距应该越大,确保字与行呼吸的空间。
12. 英文行间距
英文的行间距指的是一行英文的底部线与下一行英文的顶部线之间的距离。可以简单的理解为「行与行之间的距离」。另外英文文字底部和顶部都有对应的专有名词,英文顶部的那条叫「升部线」,底部那条叫「降部线」。
13. 中文行间距
中文的行间距就比较好理解了,是指一行文字的最底部与下一行文字的最顶部之间的距离。即行与行之间的距离。
14. 段间距
段间距:段落与段落之间的距离,可保持页面节奏,与字体、行高相互关联。
为保证文章易读性,正文段间距,可以简单地取一个空行(也就是一个行高),这是比较常规也比较合适的做法。举个例子:字号12,行高设定20,段间距 = 行高 + 行间距。行间距越大,段间距就越大;行间距越小,段间距就越小,行距与段间距成正比。段落之间首尾的行之间间距应该大于段内的间距,这时候就应该增加段间距,使得文本的阅读体验得到进一步的提升。
在任何一个设计中都需要把各个元素进行分级,分清主次,这样才能更好地抓住重点。为了能分清各元素的主次,就需要用到CRAP原则。这四个原则分别是对比、重复、对齐、亲密性。
1. 对比 Contrast (增强效果、组织信息)
对比的基本作用是突出重点,增加可读性。附加作用是有效增强视觉效果,打破平淡,吸引读者注意。
一些界面排版混乱,可读性非常差,用户的视线不知道集中在哪,导致这种情况的发生都是因为界面内容对比不明显造成。在同一个视觉区域内的逻辑不同的元素应该有所区别,以避免视觉上的相似,这样就可以有效的分清主次,为了使主要元素更突出,次要元素更弱化,可以尽量使它们的颜色,字体、大小,留白不同。如果两个元素不尽相同,那就让他们截然不同。比如,使用「14 号字」和「15 号字」进行对比,差异就很不明显,而使用「14号字」和「24 号字」,差异就明显得多,一眼就能看到大号的字体。
在这点上,「微信读书」的列表页就做得非常好,它通过标题与描述的字体粗细、大小、颜色进行对比,把最有用的信息直观地呈现在用户面前标题是吸引用户关注的关键,作者和评分只是给用户一种参考,不起决定性作用。因此,如果没有对比原则,标题和描述的字体同样粗细、大小,你就会发现视线总是会情不自禁的被评分所干扰。
大小对比
为了区分文字、图片、图标等元素的重要性,通常采用尺寸的大小来做对比。例如文章的正副标题,副标题一般用来解释主标题的内容,因此副标题的文字应该通过大小和颜色调整变成次级,让用户阅读时分清主次。
颜色对比
在排版中,首先要产生对比效果的就是背景和文字。文字与背景如果在颜色上很接近,那么就不容易区分开来吸引用户注意力,一般来说,人们习惯白纸黑字(也是因为人类有书写需求以来形成的),即白色背景和黑色文字。也有黑纸白字,例如现在APP都在做的DarkMode暗色模式,但其实暗色背景搭配浅色文字并不适合大量阅读。当然这也是为了配合用户使用场景,在夜晚光线较暗的环境下,深色模式或许更利于阅读。「冷知识:暗色模式其实就是厂商为了解决电池耗电量而出的计策,只是换了个噱头而已」总之,不管设计中使用黑白、红绿、蓝黄哪一种配色,一定要注意文字和背景的对比是否清晰便于阅读。
2. 重复 Repeated (统一有秩序)
重复是保持整齐的重要准则。既包括字体、字号的重复,也包括颜色、风格的重复。对于新人来说,要时刻牢记,尽量统一字体、字号、颜色等一系列元素,在统一的基础上,找出需要强调的部分,进行更改,通过对比原则进行强化。
如果相同内容(如标题)属于同一种逻辑关系,则应该使他们的字体、颜色、留白尽量保持一致。这样可以增加内容的条理性,并加强设计的统一性。在重复原则下,用户会因为视觉惯性继续选招设计线索,根据重复性设计线索顺场地浏览下去。
知识点:
重复不是单一的机械式的元素重复,我们可以理解为用统一的重复元素塑造一个新的元素。当然这是在保留基本的元素时所塑造出来的高度统一性的画面,从而增强我们所想要的设计效果。
3. 对齐 Alignment (统一而有条理)
在页面设计上每一元素都应该与页面上的另一个元素存在某种视觉联系,这样才能建立清晰的结构。任何元素内容在在版面上都应该尽量上下左右对齐,对于设计新人来说,最好严格遵循一种对齐方式,不然就会导致混乱,实在不行,至少保证在同一内容版块中遵循一种对齐方式。方法也很简单,就是找到一条明确的对齐线,并用它来对齐。
对齐包括左对齐、居中对齐、右对齐 3 种方式。
左对齐:页面中的元素以左基线对齐。左对齐是最常见的对齐方式,简洁大方,利于阅读;
居中对齐:页面中的元素以中基线对齐。居中对齐给人一种严肃与正式感,不过也会有呆板的感觉;
右对齐:页面中的元素以右基线对齐。相对少见的对齐方式,给人一种人为干预的感觉,加强了形式感,降低了阅读效率;
4. 亲密性 Proximity (实现组织性)
亲密性是实现视觉逻辑化的第一步,它是指关系越近的内容,在视觉上应该靠得越近,反之,关系越疏远的内容,在视觉上应该越远。简单的来讲就是要把画面中的元素分类,把每一个分类做成一个视觉单位,而不是众多的孤立的元素。这有助于组织信息,减少混乱,为读者提供清晰的结构。
那做好亲密性有哪些方法呢,私以为有以下几点:
留白:留白是设计中通用的万金油原则,通过留白建立距离关系进行内容区分;
左图歌曲封面和歌曲名信息间隔比每首歌曲上下间距还大,导致用户的视线流呈垂直方向。
分割:简单来说就是分组,建立组合关系。常见的形式有线条分割,卡片分割等;
色相:通过颜色的对比,不同颜色的信息会暗示这是同一类。常见的日历行程就是通过不同颜色来区分时间和具体事项。
方向:不同的排版方式也可以很好的区分信息;
「信噪比」(Signal-to-Noise Ratio)原本是用在声音和图像领域的概念。在互联网产品中把 「信噪比」概念借用到了用户体验。合理的信噪比可改善与用户的交流。加大信号可以将有用的信息快速准确的传达给用户,减少噪音并使信号脱颖而出。
从人机交互角度,我们应该删除与任务不相关的内容或设计元素。你甚至可以将高信噪比的目标与极简主义联系起来。但是「信号」和「噪音」的确切含义会有所不同,一个人的信号可能是对另一个人的干扰,因此,用户界面的信噪比有低有高,取决于具体的用户和具体的任务。在用户界面中,信噪比所涉及的「信息」可以是任何内容,包括文本内容,视觉元素或动画等。为了提高设计传达信息的效率并帮助用户完成任务,需要提高信噪比。
知识点:
用户始终喜欢清晰、简单、自然、好用的设计和产品。但需要注意的是,除了交流必要信息之外,我们还希望界面在视觉上具备吸引力,以唤起用户的某些情感。有了额外的目标(比如品牌宣传、业务目标等),应该以合理的信噪比为目标,而不是以绝对的方式排除所有「无关」的信息。
例如iOS6到iOS7图标拟物到扁平到改变,让用户可以更快速准确的获取到有效信息。而这一过程,就是典型的放大「信号」。
还有虾米音乐的驾驶模式
我们都知道,在开车的时候操作手机是非常危险的。在40km/h的速度下,看手机3秒,相当于盲驶了35米。但有些情况下又不得不操作手机,比如紧急来电或者导航出错……这时,驾驶模式的界面就显得尤为重要了,让用户能够快速准确的识别信息并进行操作,可以大大提高行车的安全性。
在界面中无论是何种分割方式(分割线、卡片阴影、分割色块),过于浓重的表现都会影响有效信息的获取,成为界面中的「噪音」,因此我们应该让它们细一点、淡一点来降低表现,或者干脆不要(留白分割)。
图版率就是页面中图片面积的所占比。在页面设计中,除了文字之外,通常都会加入图片或是插图等视觉直观性的内容。这种文字和图片所占的比率,对于页面的整体效果和其内容的易读性会产生巨大的影响。当然,除图片本身外,我们也可以通过填充底色,图形叠底等方式来提高界面中的图版率。
图版率高低的区别:同样的设计风格下,图版率高的页面会给人以热闹而活跃的感觉,反之图版率低的页面则会传达出沉稳、安静的效果。提高图版率可以活跃版面,优化版面的视觉度。但完全没有文字的版面也会显得空洞,反而会削弱版面的视觉度。
在没有图像素材的情况下想要呈现出高图版率,可以通过以下几种方式来实现:
通过填充页面底色,取得与提高图版率相似的效果,从而改变页面所呈现出来的视觉效果;
如果素材图像尺寸小,可以通过色块的延伸或是图像的重复来组织页面结构,同样可以提高图版率;
利用排版的节奏感以及跳跃率(文字和图片的跳跃率,是指版面中最大标题和最大的图与最小正文字体和图片大小之间的比率)让无趣的版面充满活力,富有节奏的设计也能间接优化页面的图版率;
增加页面中的图形也可以改善图版率低的问题。无论是数字、序号、图标,甚至是视觉处理后的标题文字,都能提高页面的视觉度,并给用户留下活跃生动的印象;
如果页面中没有图片和插图,那么通过对文字及其颜色的处理,也可以起到提高图版率的作用;
上面的例子中,对于标题文字都进行了视觉加工,起到了整体页面的装饰效果。借助对这种文字大小、颜色、形状的灵活运用,来突出页面的重点,避免视觉上的单调感。
1. 文字在代码中的实现
在开发落地的过程中,文字排版的开发实现是很重要的一个环节,也是经常让设计师和开发小哥哥头疼不已的地方。字体和排版在实现上经常会出现偏差,主要原因在于开发的标注方式和设计软件不一致。因此理解文字开发的实现方式,细节问题的解决方法至关重要。在Android中,文字开发工作是通过一个叫TextView控件来实现的,主要承担文本显示的任务,任何APP都不可避免的会用到它。TextView常用属性如下图:
2. 字体字重对应的font-weight值
在前文聊过,每种字体都对应有好几种字重(Regular、Normal、Medium、Light ),在给开发的 UI 设计稿中,我们给的字体标注通常有 PingFangSC-Regular、PingFangSC-Medium、PingFangSC-Bold,并不会直接给开发 font-weight 的值。虽然这需要开发去熟记,但作为设计师了解它们的对应关系,可以更顺畅的和开发沟通。
在W3C Fonts节章的规范标准中有给具体数值(100至900):
这些有序排列中的每个值,对应字体的字重。其大致符合下列通用重量名称:
当然,并不是每一种字体都有这么多字重,那遇到有些字体只有2、3种字重,该怎么对应font-weight 值呢?W3C Fonts也给出了解决方案,例如字重和400大致符合将会归为Regular、Book、Roman;和700大致符合将会归为Bold。若一个重量所指定的字形不存在,则应当使用相近重量的字形。通常,较重的重量会映射到更重的字形、较轻的重量会映射到更轻的字形。下图所示:灰色表示该重量的字形不存在、使用的是相近重量的字形。
△ 只包含400、700和900重量字形的字体家族的对应字重
△ 只包含300和600重量字形的字体家族的对应字重
3. 文本框行高的问题
我们都知道在设计的时候,可能字体使用的24pt,但其实字体本身占用的距离是包含了升部及降部区域的,这样就导致其占用空间大于24pt,而变成了33pt。每个字体都有相应设定的「字高」比例,可以通过sketch选中字体后的height值来进行查看。线高越大,问题就越大。下面的示例显示文本框之间的距离设置为32px。如你所见,即使你将所有垂直间距都设置为相同的值,它们在视觉上也远大于32px。
△ 虽然标注出来的参数都是一样大,但视觉上间距却是不一样的
4. Leading-Trim:数字排版的未来
去年六月,Microsoft Design赞助了一个新的css规范,称为「Leading-Trim」。这个css方案能很好的解决上面这个问题。
我们常用的UI设计工具,例如Figma和Sketch,似乎已经采用了「half-leading」模式并以此方式渲染文本。因此,我们在设计工具和浏览器中都遇到了这个问题。
设计方面的解决方法相对容易:你可以忽略边界框,而直接根据文本的大写高度和基线来测量空间。这是一个手动过程,因为大多数设计工具没有上限高度和基线的参照目标,尽管设计师将尽一切努力使我们的设计看起来更好!但是,如果采用这种方法,开发人员仍将仅在CSS中实现边界框间距。因此,它们会出现随机的间距值。
为了缓解此随机性问题,开发人员可以在CSS中以负边距「裁剪」文本框。但是负边距将需要手动确定,并且是特定于字体的,因此是「随机的」。任何字体,浏览器,操作系统或语言环境的更改都将不可避免地导致你不小心设置边距。此外,该技术通常不是良好的编码实践,并且可能导致意外的副作用。
Leading-Trim新规范
Leading-trim是CSS工作组正在引入的新CSS属性。顾名思义,它就像文本框剪刀一样工作。你只需使用两行CSS,就可以从你选择的参考点中修剪掉所有多余的空间。
代码示例:
上面的示例首先使用text-edge(也是新属性)来告诉浏览器,所需的文本边缘是大写高度和字母基线。然后,从两侧修剪多余部分。请注意,采用修剪仅会影响文本框。它不会切断其中的文本。这两行简单的CSS创建了一个干净的文本框。这可以帮助你获得更准确的间距并创建更好的视觉层次。
使用后再来对比一下:
△使用新规范对比发现,右图文字上下间距舒服多了,也更合理。
Leading-Trim修复对齐问题
借助Leading-Trim,可以解决在APP上看到的所有奇怪的对齐问题。例如,即使文字位于文本框内,你的文本也不总是在容器中垂直居中。
默认行高中保留的多余空间会导致文本不总是在文本框中居中。使用Leading-Trim修剪,就可以很省心的使文本垂直居中。
知识点:
原因是每种字体的设计也不同。它具有自己的默认行高,并且其中的文本可以具有不同的大小和基线位置,并不都是水平居中对齐的。因此,有时即使字体大小,行高和文本框位置保持不变,更改字体也会改变文本的对齐方式,如下例所示,文字很明显没有对齐。
在第二个示例中,你可以看到Leading-Trim如何防止这种情况并使文本完美对齐。
一致性和工作流程的改进
Leading-trim修整超出了使间距和对齐更准确的范围。它在建立的间距系统,为设计准确性和一致性以及的设计到开发交接铺平道路方面发挥着关键作用。
拥有间距系统有很多好处。设计师可以更快地确定间距,开发人员可以设置相应的间距变量以消除代码中的随机间距值。但是目前,即使我们设置了间距系统,由于文本框中的额外空间,对于文本元素来说也不是很准确。如果我们尝试忽略设计中的边界框并在代码中「裁剪」文本框,则会遇到那些棘手的解决方法问题。
△应用于文本元素且没有Leading-trim修剪的间距系统
借助领先的文字间隔系统,从设计到开发的交接过程也将更加顺畅,因为开发人员将能够建立完全相同的系统,并且避免在布局错误上花费大量时间。最重要的是,领先的微调间距系统将帮助我们提供用户信赖和喜欢的外观更美观的产品。
5. 设计中修改文字行高的方法
上面我们介绍了利用Leading-trim修剪字高的先进方法,但是这个新CSS的规范还在编写中,还未世界范围的推进,不过有「微软」团队的扶持相信国际化也不会太远了。
在这之前,我们想要尽可能的解决字符多出的间距问题,就需要在设计软件里手动修改了,手动把文字行高与客户端系统默认行高保持一致,从而给出准确的文字间距。开发在实现的时候iOS使用系统默认行高,Android系统去掉文字上下padding。
这种方式虽会花费不少时间,但也最,你可以据此设置出最美观合理的间距,而不用担心上线稿的还原度问题,也便于我们后期的页面校对和调整。
△在Sketch中修改文字高度
6. 什么是弹性适配
文字弹性适配一般涉及的是宽度适配,宽度适配普遍使用的是间距适配,即定好左右页边距,中间弹性拉伸。这种方式可以做到较好的适配,也是做快速常用的适配方法。
7. 标注工具
设计师将设计文件交付开发之前,应站在程序员的角度着想,做好前期沟通,提供他们开发所需要的资源。设计文件的标注可以使用Sketch插件或直接上传「蓝湖」,拿Sketch插件「Sketch Measure」为例,它是一款十分智能的标注插件,主要功能包含两大块:标注和规范。
工具栏汇合了Measure所有功能的快捷工具,永远置于画布顶层,有了它就不用再频繁通过菜单栏去使用功能。
做好规范后,点击「导出规范」一键自动生成Html页面,浏览器打开页面点击其中任何元素都可以查看其属性和间距,还包括代码样式,交给开发开发工程师后,不用沟通都能看明白。
△Sketch Measure导出标注的网页界面
产品功能开发完成后,对产品对功能,视觉和交互操作进行测试和验收,确保产品的可用性。一般在功能模块验收完成后,就可以开始具体的视觉设计验收,这也是由主要设计师负责的模块,主要验收颜色、字体、图形、间距、控件和空状态等。
因本文主讲字体与排版,就拿这部分来说,需要检视的就有:
字体:是否用的平台默认字体,如果是内置字体检查字体显示有没问题;
字号:导航栏、栏目名称、分类页签、tab等字号大小是否符合规范;
字重:标题和正文字重是否正确,粗体用的是哪一种,是Medium,还是SemiBold;
字色:标题、正文、注释、提示等文字颜色;
字间距:检查中文间距和英文间距,段落文字标点有无避头尾;
行间距:段落文字行间距,有没有出现多余的行高 ;
……
在检视过程中如发现问题,截图标示问题所在,并出具检视报告。
△ 视觉检视表示例
视觉设计的验收要追求细节上的完美,因为设计上的细节是很容易被挑错的,同时需要耐性和细心,要有像素级的视角,只有这样才能完美的还原设计稿。
原本只是想结合工作积累,写一篇字体应用知识总结,没成想给自己挖了一坑,涉及的知识点真是超级多,很多地方都可以单独拿出来再另写一篇。另外其实在工作中,也建议大家对自己的经验进行总结,对关键信息进行提炼加以沉淀,一方面能让自己的知识更加牢固,另一方面也可以帮助后来者学习成长。字体与排版基础就分享到这里,希望对大家有所帮助。因为篇幅较长,几经修改,有细节不正确的地方,欢迎留言矫正,感谢阅读。
参考文献:
《W3C-CSS字体规范标准》
《从「行长为字号的整数倍」说起》
《Leading-Trim: The Future of Digital Typesetting》
《关于UI设计中字体应用的干货》
《字体与排版》
《深度剖析Baseline设计原理》
文章来源:优设网