随着大数据产业的蓬勃发展,很多企业都开始应用数据可视化。所以数据可视化设计,绝对是热门的设计之一。很多 UI 设计师突然会接到公司数据可视化设计的需求,如果不了解数据可视化设计,肯定是一头雾水,不知从何入手。本文结合最近设计案例,分享大屏可视化设计过程中遇到的一些问题以及设计思路,供大家一起交流与学习。
△ 最终动态效果图
首先放的是项目改版前的页面:
1. 需求介绍
某某应用云,分为五大云平台模块:云端综合调度、数据查询通道、数据应用处理、数据存储管理、管理运行维护。每个大模块下⾯有若干个子系统。
可视化⼤屏首页需要展示的内容包括:
2. 需求分析
分析大屏可视化的一些共性:
结合大屏的一些共性特点针对看到的线上旧版本设计,分析存在的问题。
3. 布局
整合数据,分析出主要数据、次要数据、总量数据、细分数据、各数据的维度等等。首先优化页面布局,可以先在纸上画一画,然后脑子里有大概思路以后再用电脑绘制,如下图:
采用栅格化对称布局,让整体视觉左右平衡。
4. 风格
一提到数据可视化大家往往能想到科技、数据、蓝色等一些普遍关键词。
了解到客户是想做一个科技感强、炫酷的视觉效果。可以在网上找一些效果图或是自己曾经做过的案例供客户选择,确定一个大致的风格,然后结合具体的业务场景进行设计。
5. 颜色
颜色上结合产品使用场景,以及整个产品调性还是以蓝色为主,背景选用深色调,让视觉更好聚焦,内容部分采用比较透亮的蓝色系,保证内容与背景有一定的对比关系,便于业务信息传达。
6. 主体地图
地图为大屏的主要展示内容,首先分析展示的目的是为了看清各个城市间的不同分布情况,和城市数据的汇集效果。
如图:
改版前:地图过于单薄,没有立体感,太平缺乏层次,颜色黄色不符合产品调性。
改版后:主色调改为科技蓝,在原有地图上增加外发光和多层阴影叠加,增加地图的立体感,地图上增加科技线条上升的效果代表每个城市数据变化的提升,地图背景采用比较弱化的转动线条圆形,衬托地图主体,使得画面更加丰富。
7. 地图效果的实现方法
首先用 ps 拉框助手新建一个山东的地图(拉框助手的使用获取方法可以参照上篇文章)。
完成后会得到一个叫 map 文件夹的地图分层文件,如图所示。这里需要对每个城市的颜色进行调整,为了区分每个城市之间的数据不同关系。
调整完块之间的颜色后,就需要给地图整体增加立体效果。
首先,是整体给地图加了一个描边和外发光。描边是为了强化地图边缘,外发光是为了地图与背景有一个区分。
其次为了增加地图立体感,需要给地图增加多层阴影叠加的效果。复制现有形状层,拼合成一个山东省的地图,如下图:
最后,把拼合好的图层移动到 map 文件夹下面,阴影可以添加多层,这里针对每一层进行不同颜色大小的调整,就是下面的这种效果了,地图的体积厚度感也就出来了。这里只是提供一个大概的思路,大家可以多去尝试。
整体地图效果调整完成后,就是给地图增加些纹理,和上升线条这些细节上的效果了。纹理可以用图案叠加,或者找一张纹理图剪切蒙版实现,最后再添加上升线条的效果,地图的效果就完成了。
最后加上线条上升的动态效果:
8. 数据图表拆分
在选定数据图表之前,首先要确定图表之间的关系,可以从以下四个维度进行思考分析:
可以参照下面这个图:
△ 图片来自于网络,侵删
当确定好分析维度后,事实上我们所能选用的图表类型也就基本确定了。接下来我们只需要从少数几个图表里筛选出最能体现我们设计意图的那个就好了。
传统的图表比如 echarts 图表在视觉上展示可能不是很美观好看,可根据选择的图表在其基础之上进行美化设计,总之选定图表最重要的两个点就是:易理解、可实现。
易理解:就是要考虑最终用户,可视化结果应该是一看就懂,不需要思考和过度理解,因而选定图表时要理性,避免为了视觉上的效果而选择一些对用户不太友好的图形及元素。
可实现:主要是跟开发前期沟通好实现方式,一般都采用开源组件库的居多,比如 echarts 组件库,我们设计的图形图表,要开发能够实现。实际工作中,一些可视化效果开发用代码写很容易实现,效果也不错,但这些效果设计师用 Ps/Ai/Ae 这些工具模拟时会发现比较困难。同样的,某些效果设计师用设计工具可以轻易实现,但开发要用代码落地却非常困难,所以大屏设计中跟开发常沟通非常重要,我们需要明确哪些地方设计师可以尽情发挥,哪些地方需要谨慎设计。一个项目总有周期与预算限制,不会无限期的修改迭代,所以设计师在这里需要抓住重点,有取舍,不钻牛角尖、死磕不放。
案例中在图表选择上,强化科技感元素,条形图打破传统条形图的展示形式,采用电池晶格的展示形式,在保持图表功能的同时更加凸显科技感。
从页面的整体看,已经有两处用到了条形图、柱状图,如果这里还是条形图,那么页面看起来会很单调,图表也没有表现出多样性,所以现在设计要体现图表的多样性也能够有排名的直观呈现。以下图表采用科技圆盘的形式,运用科技线条的上升状态代表排名的先后顺序,所有图表都采用数据降序来展示排名更加直观。
改版前的图标样式比较单一,改版后针对每组数据不同的对比形式,采用比较贴合的图表进行展示,篇幅原因就不一一做展示了。
附上最终视觉效果图:
大屏设计需要注意的点:
以上是我对数据可视化大屏的案例总结,希望能帮助到你。除此之外还有很多地方没有涉及到,包括具体设计的操作方式、选取图形元素的具体方法,以及在各种大屏中所需要的相对应的组件等,在庞大的可视化大屏设计系统中,还有很多值得学习参考和优化的知识,欢迎沟通交流,大家一起努力。
文章来源:优设
有句话叫:「设计无小事」,很多看似不起眼的东西却起着至关重要的作用,比如这期要说的线条,很多人对于线条的理解有局限性,比如:线条的形态可以是曲线、直线、折线、粗线、细线、实线、虚线等等。其实已经牵扯到了点、线、面的知识,这也是很多科班生在学校必学的知识点,但是这期所说的线与点线面中的线还是有所不同的,点线面中的线可以是线条、可以是文字或者是看不到的视线,而是今天着重说的是设计中很直观的线条。下面我们还是通过实际的案例逐一分析:
设计类的知识很多都和日常生活息息相关,尝试着把设计类的知识点与日常生活想结合,对于记忆和理解来说会更加得心应手,例如:
图中的闪电可以视作为设计中的线条,给人的视觉感受是通过闪电把天与地连接为一个整体,而闪电在图中的作用就是串联整体,那么回到这里的正题:线条有引导视觉的作用该怎么理解呢?再举一个现实生活中的案例:
我们选择从北京到拉萨开车去,出发之前可能需要在地图上看下路线,知道途径哪些省市,规划好行程路线,这里绿色的虚线就起到了引导视觉的作用。回归设计中道理是一样的,线条可以引导用户把原本杂乱无章的视觉点规整为有次序的视觉元素,例如:
当看到左侧这张海报时我们视觉次序会出现很多变化,比如:1>A>3>B>4>C>2 或者 A>2>C>4>B>3>1 等等 N 多种顺序,这时给人的感觉就是杂乱无章的,毫无视觉次序而言;而看右侧的海报给人的感觉却是条例清晰的,相比而言只是多了两条线,但是却在整个海报中起到了引导视觉的作用,它可以给与用户阅读海报时视觉辅助的作用,让用户以右>左>右的视觉次序欣赏、阅读,看似很不起眼,其作用却至关重要。
前面也说了,线的形态可以有很多种,例如:
这里是以真实的可口可乐吸管作为设计中的线条,同样起到了视觉引导的作用,但是我们不难发现,这里的线条不仅仅只有一个作用,也牵扯到另一个作用:线条有串联整体的作用。
在排版时我们有分组原则,即把互想关联的元素彼此靠近,无关联的相互疏远。在页面中我们会把同一色块上的元素视作为一个整体;下面我们说下线条所带来的串联整体的作用是如何体现的,比如:
△ 图一
△ 图二
图一因为大面积的留白能使得用户很容易分辨出自行车与文案是一个整体,但是相较于图二而言,其整体性略显不足,而且给人的感觉太过单薄、重心不稳;图二的整体性更强,这里的矩形线条就起到了串联主题的作用,类似的还有:
不难看出,这些案例中的线条都有串联主题的作用,线条使得主题元素整体感更强、画面板式更加严谨;对于整体的视觉传达也起到了串联、引导的作用;在文字排版中,也有类似的线条,比如:
同样是通过线条把文案更加整体化,也起到了串联的作用。
突出主题的方式有很多种,像我们之前所说到的留白、对比。接下来继续说下另一个可以突出主题的方式—线条,下面看个案例:
通过对比观察我们发现,右侧海报整体感更强,主题文案信息更加清晰,主体更明确。其中的原理可以理解为:因为线条的存在,使得主题信息有了一定的范围,在视觉上等于是在海报中划定了视觉焦点,从而起到了突出主题的作用。当然还有其他的表现形式,比如:
很好的诠释了线条的作用——突出标题序号。在进行创作时,作品的每个元素都要做到有理有据,否则只是一味的抄袭,到再创作时脑袋里还是一片空白,只有明白了其中的设计原理,才能做到活学活用。再看几个案例:
突出主题也许一个线条就可以表现的淋漓尽致,因设计目的的不同,线条所发挥的作用也不尽相同。下面继续分析:
前面说了线条有串联整体的作用,而这里又说可以分割整体,是否存在矛盾呢?下面举个简单的例子:
在小文案的区域中间我加了两个线条,看似很小的改变,其目的是把文案很准确、严谨地分割为三个小整体,希望能给用户带来识别性更强的阅读性,再举个例子:
这里的线条把月份和日期分割、英文和中文分割开,使得用户对于信息的捕捉能力以及可辨识性都提升了很多,而线条的作用就起到了分割的作用。
线条也能起到修饰、衬托的作用,很多小伙伴会忽视这一点,其实线条也可以成为海报中衬托画面、修饰主题的元素,例如:
海报中的线条起到了衬托、修饰主题的作用,假如把这些线条都删除,画面整体会显得相对单薄。
更多设计中线条的应用:
线条的作用我们分为四个逐一分析,其实它们之间也存在着相辅相成的作用,不能以一概全,线条所起到的作用可以是一种,也可以是多种,比如:我们前面「可口可乐」的案例,即有串联整体的作用,又有引导视觉的作用。只要我们在使用的时候能明确目的,而不是机械式的抄袭,那么最终一定会得心应手。
文章来源:优设 作者:美工美邦
未来荧黑是一个基于思源黑体、Fira Sans和Raleway的开源字体项目,支持简体中文、繁体中文与日文。
思源黑体的7种字重被扩展为9种字重,并增加了5种字体宽度,全家族共包含44款字体。
相比于思源黑体,未来荧黑的造型更加简明现代,版面效果清新轻快。未来荧黑的中宫与字面更加收敛,重心在字重之间经过了重新配置;笔画细节上处理得更加干练,简洁而平直化。
开发者:Celestial Phineas
字体文件以SIL Open Font License 1.1发布,构建字体开发的代码以MIT License发布。
发布地址:github.com/welai/glow-sans
网盘地址:https://pan.baidu.com/s/1f2UuFO8ZxWa8v5XXYUEmig 提取碼 2e8w
备份下载链接:https://pan.baidu.com/s/1E1woRsZX91bCrq5FT1SAzg 提取码: 92t2
文章来源:优设 作者:GrayDesign
微信在3.23号上线了传闻已久的 「暗黑版」,用来适配 iOS 的深色模式,相信不少同学已经安装并体验上了,如果还没安装的可以看看下面图例。
微信每有一次大动作都会引发全网性的讨论,而针对设计上的调整,往往只会迎来一片骂声。比如我们看看知乎中讨论的内容,感觉民愤都快压抑不住了。
但我们先别急着参与网上的声讨,现在站在设计师的角度,来评价一下迟到的微信深色模式。
很多人会把深色模式和夜间模式搞混,这里必须强调这是两种不同的模式,所以我们要对还没搞清楚状况的同学先做一个扫盲(最近扫盲好像搞的比较多…)。
先说夜间模式,是一个专门针对夜晚环境适配的设计版本。腾讯的 ISUX 团队之前做过调研,有 71.1% 的用户习惯在夜间不开灯看手机。
如果在深夜漆黑的房间中看手机屏幕,对我们的健康有非常大的损害,不仅表现在对视力的伤害上面,视网膜和神经元容易受损,同时主流的研究项目还表明会这会抑制我们褪黑素的分泌造成失眠。
所以,夜间模式,是一个用来降低屏幕对用户伤害,提升夜间使用体验的特殊模式。
通常,夜间模式会采取 降低尼特值、减少对比度、降低色彩明度、增加深色遮罩的方法,比如之前 QQ 官方的夜间模式示意图,大家感受一下,是不是有内味儿了?
这里提一下尼特这个概念,尼特是用来说明亮度的术语,1nit=1坎德拉/平方米。是现在各大手机发布会中介绍屏幕的时候都要强调的参数之一,因为尼特值越高,证明在户外大白天的环境中屏幕内容可以越清晰,而夜间模式做的就是用来降低显示亮度尼特值的。
然后再解释一下苹果的深色模式,苹果的 DarkMode,并不是一个专门面向深夜环境的模式。官方对此版本的解释,详见我们翻译的 iOS 官方文档中 112 页。
这是一个面向全天候的视觉风格,同时通过深色背景的对比,来加凸显图片、文字内容。包括上面那种官方的配图,大家应该就能感觉到主体元素比白色模式下更凸出,更激烈。
所以了解了这两个模式的区别,我们才能好好展开对微信深色模式的讨论。
接下来,我们先来总结一遍微信的深色模式。首先是分析一遍它使用的背景色。
背景色纯灰色,有3个等级的灰度。熟悉我的都了解,看色彩的奥秘,靠16进制代码和 RGB 那是分析不出个什么所以然的。如果我们把它们转化成 HSB 一看,规律性就来了。
背景色从深到浅色的灰度值 B 值分为 10、14、18,是不是朗朗上口。应用的层级虽然和官方规范一样使用了三级,但是设置和官方的不同。
然后再看看其中使用的其它配色,其中主色保持了不变,其它应用到图标色彩,都进行了明度的调整,比如下图案例。
再看看文字的用色,也是纯灰色,标题使用 B:85,正文使用 B:65,注释使用 B:35(主要用色)。
而官方使用的文字色彩中,却并不是纯灰色,除了第一级的白色以外,其它灰阶的文字是由带有蓝色色相的色彩通过降低透明度来呈现的。
对中性色增加蓝色色值是一个常规操作,可以让这种灰色看起来有一点活力,不会像纯灰色那么单调,不过这次微信明显在文字的应用上更保守,一点色相也没有给。
这次微信被大面积吐槽的,就是颜色的应用上和官方的规范不一致,作为从业人员直接开喷是相当不专业的(最起码要先走个形式),这就是我要分析的重点了。
要说微信的 UED 团队,专业性肯定是国内最顶尖的,你们网上所有看过有关交互的方法论、可用性测试的分享, 他们肯定都有做过,而且执行得更专业。
直接用官方规范的黑底白字模式,微信团队不可能没有出过这样的方案。但很明显,这个方案最后被毙了,上了我们看见的这个版本,虽然不知道以后会不会变。
再看看下面官方发的一条微博。
其中已经提及了,是和苹果 「共同探索」 出来的方案,这是非常耐人寻味的。也就是说,这个不用官方的模式是苹果团队也通过的。
那么为什么要做的不一样呢?没有内幕消息,就根据自己的经验来判断一下。
我自己认为的一个非常重要的原因,就是对于 「夜间模式「 的兼容。前面不是讲暗黑模式和夜间模式不一样嘛?为什么微信的暗黑模式又去兼容夜间模式了。
这里面有几个小彩蛋,首先就是官方对深色模式的态度,在我看来越来越暧昧了。比如在显示与亮度设置页面里,有一个自动设置外观 —— 日出前保持深色外观的功能。这样,就等于默认将深色模式和夜间模式挂钩。
还有,如果过去我们没有整理 iOS13 的翻译,就不会发现,上面我们展示的那句专注于内容的解释,现在在官网已经被删掉了(你品,你细品)。
再说关于用户认知的问题上面,在 UI 群体中,能了解暗黑模式和夜间模式是不一样的可能就只占 10 分之一,那么对于普通用户来说,这个情况就更不乐观,能有 1% 的用户了解这个概念就不错了。所以,绝大多数用户会直接认为暗黑模式就是夜间模式。
如果暗黑模式直接当成夜间模式用,在深夜的环境里,观感会特别差,因为 —— 明暗对比度过高。
如果在黑底中直接用白字,那么可以说屏幕的文字和背景的对比度就是 100(HSB中 B 的差值),在一个漆黑的环境中会非常应验 「让内容脱颖而出」 的效果,刺激性会非常强烈,文字将变得非常尖锐,比如 QQ 暗黑模式,大家可以在被窝里打开下面这张图感受一下。
新的深色模式版本中,文字和背景的对比度基本控制在 50 左右,降低了一半。
并且,中英文字形在正负形上的差异(简单理解就是中文笔划更复杂),这个感觉会更强烈。比如我们用一个公众号页面举例,使用纯黑底白字,采用相同字号的中英文,看看显示的效果。
还有,纯黑背景色和白色的对比度,会根据屏幕的类型和参数不同而有不一样的感受,比如苹果从 X 后旗舰机型使用 OLED 屏幕,纯黑色区域将不会发光,进一步扩大对比度,使得文字变得更尖锐,更让人难以接受。
如果不是使用 OLED 屏幕的同学光看一个案例可能很难受,所以我们用纯黑的案例来对比一下现在的状态。
是不是发现明显在夜间的情况下黑白模式并不如另一个模式看起来舒适?而这种不舒适的差别并不会随着屏幕亮度降低而变化。
所以色彩并没有符合官方的原因,我的判断就在大环境中无法割裂夜间和深色模式的区别,同时也要让深色模式更适应夜间环境,做出了调整。而又因为它不是真正的夜间模式,所以对比度也不能像 QQ 之前的夜间版本一样将整体环境完全压暗。
你看,真是一个让人矛盾的过程……
最后再简单讨论一个问题,为什么深色模式来得这么晚。很多用户一直嘲讽,不就是换一套皮肤的事嘛,为什么就是不上线。
外行可以看热闹,但是如果是从业人员就应该知道,微信这种体量的应用,上线深色模式绝对不是一个非常容易的事情。
适配黑暗模式首先需要使用苹果新的 iOS 13 SDK(开发者工具)进行编译,等于应用中有大量的代码需要调整,而这种升级调整的结果还会导致沉重的测试压力。有经历过 Darkmode 开发的团队应该都知道这绝对不是改改颜色就能上线的皮肤。
再看到知乎另一个回答中提到的:
另一方面点大家随便听听。使用 iOS 13 SDK 之后,Apple 要求 VoIP 推送必须使用 CallKit,否则应用程序会被终止。而由于众所周知的原因,CallKit 在中国大陆是无法使用的,这样的改动会降低微信语音电话的体验。
原文地址:https://www.zhihu.com/question/378027349/answer/1069072154
再者,抛去大量用户体验调研相关的工作,微信整个生态对于暗黑模式的不友好可以说是无解的。比如说公众号,有大量公众号内部的标题、分割线、引用语句是用图片做上去的,而图片还用的是白底(透明底黑字的也有),于是现在就产生了灾难性的阅读体验。
比如我的公众号:超人的电话亭,其中文章展示的截图。
而且因为公众号发出去是不能修改的,只能删除,那么这部分存量文章将无法更改,体验也无法扭转。而且公众号还支持文字色彩等自定义,那么你在白色背景下添加的颜色,可不会直接适配深色模式,尤其是官方也不可能轻易直接给你们 「适配」 掉。
而在夜间模式,正常访问的文章网页,也和公众号会很像,但是打开以后是白色背景的话,统一的体验在哪里?
再者还有小程序,小程序虽然也可以通过微信官方提供小程序的深色模式适配文档,对应的 SDK,但是小程序不是 APP,其中有大量小程序开发后是缺少维护的。
因为线下门店通过外包方做好一个小程序上线以后,没特殊的原因不会直接去更新它,那么这部分小程序的升级适配无从谈起,会出现打开小程序一个白一个黑的窘境。
最后,再讲一个微信里最高频使用的功能 —— 发表情。深色模式直接造成大量自定义表情报废,无法正常显示的问题,比如看看下面我自己发的表情。
前面提到的,都是不能解决的问题,这就是做深色模式的挑战,因为用户 UGC 内容是不可控的,官方不可能通过算法直接帮用户强行 「适配」。
而这些,就是做深色版的难点。
以上总结内容多数为主观分析,纯粹站在 UI 设计师角度进行专业解读,不带入个人立场。而一定要我自己评价的话,那就是 :赶紧把这模式给我移除!!
再顺便提一点小感想,一个有数亿用户的产品,每一个小调整分量都不轻,都要慎之又慎。同时,你做的每一个决策,都意味着要站在其中一部分用户的对立面,因为你满足不了所有用户的需求。所以,这就是设计师的压力与挑战。
文章来源:优设 作者:超人的电话亭
网上对于这两个的区别解释都是统一口径的,一个是开发依赖,一个是线上依赖,打包发布需要用到的要添加到线上依赖,一模一样的回答,误导了很多人。今天自己测试一下这两个命令,记录一下。
–save-dev,会在devDependencies里面添加依赖
-D,会在devDependencies里面添加依赖
–save,会在dependencies里面添加依赖
-S,会在dependencies里面添加依赖
devDependencies和dependencies可以同时存在同一个包的依赖。
如果npm install xxx后面没有输入要保存到哪个里面,devDependencies和dependencies都没有。
我这边直接npm install jquery,node_modules下有jQuery。然后我删除node_modules,执行npm install,node_modules下并没有下载jQuery。
所以,安装依赖的时候如果没有加上要依赖到开发还是线上,只是临时的在node_modules里面帮你下载,而devDependencies和dependencies的依赖都会帮你下载。
然后我在devDependencies下安装依赖:
"devDependencies": {
"html-webpack-plugin": "^4.0.3",
"jquery": "^3.4.1",
"webpack": "^4.42.1",
"webpack-cli": "^3.3.11"
}
在入口文件引用和打印jQuery:
import $ from 'jquery'
console.log($)
打包之后,可以使用jQuery。
然后我在dependencies下安装依赖:
"dependencies": {
"html-webpack-plugin": "^4.0.3",
"jquery": "^3.4.1",
"webpack": "^4.42.1",
"webpack-cli": "^3.3.11"
}
在入口文件引用和打印jQuery:
import $ from 'jquery'
console.log($)
打包之后,可以使用jQuery。
测试的结果就是,无论是–save还是–save-dev,对于打包都没有任何影响。devDependencies和dependencies两种情况,打包出来的main.js都把jQuery打包进去。这两种情况,如果都没有引用jQuery的情况下,也都不会把jQuery打包。
接着在一个空白的项目里面下载axios,npm install axios -S,打开node_modules文件夹:
发现多出了另外三个依赖,查看axios下的package.json:
"dependencies": {
"follow-redirects": "1.5.10"
}
查看follow-redirects下的package.json:
"dependencies": {
"debug": "=3.1.0"
}
查看debugs下的package.json:
"dependencies": {
"ms": "2.0.0"
}
最后ms的package.json没有dependencies。
而这几个包的devDependencies依赖的包没有一个下载。
接着我在node_modules把follow-redirects、debugs、ms都删了,把axios里面的package.js的dependencies给删了,然后执行npm install,发现没有下载follow-redirects、debugs、ms这几个,也证明了如果node_modules里面有下载的包,是不会重新去下载的。我把node_modules删除,执行npm install,这几个包又都下载下来了。
最后得出 的结论是,–save-dev和–save在平时开发的时候,对于打包部署上线是没有任何影响的。如果你是发布一个包给别人用,而你开发的包依赖第三方的包,那么你如果是–save,那么别人安装你开发的包,会默认下载你依赖的包,如果你是–save-dev,那么别人安装你开发的包,是不会默认帮忙下载你依赖的包。
其实发布的包如果没有必要,很少会默认帮你下载,比如bootstrap,依赖jQuery,怕你原本就下载了引起冲突,也不会在dependencies里面安装jQuery而是:
"peerDependencies": {
"jquery": "1.9.1 - 3",
"popper.js": "^1.16.0"
}
表示bootstrap依赖于这两个包,你必须安装,版本不固定,但是一定要安装这两个包,安装的时候会有警告:
peerDependencies WARNING bootstrap@ requires a peer of jquery@1.9.1 - 3 but none was installed
peerDependencies WARNING bootstrap@ requires a peer of popper.js@^1.16.0 but none was installed
当你引用了然后打包,报错:
ERROR in ./node_modules/_bootstrap@4.4.1@bootstrap/dist/js/bootstrap.js
Module not found: Error: Can't resolve 'jquery' in 'C:\Users\wade\Desktop\savedev\node_modules_bootstrap@4.4.1@bootstrap\dist\js'
@ ./node_modules/_bootstrap@4.4.1@bootstrap/dist/js/bootstrap.js 7:82-99
@ ./src/index.js
ERROR in ./node_modules/_bootstrap@4.4.1@bootstrap/dist/js/bootstrap.js
Module not found: Error: Can't resolve 'popper.js' in 'C:\Users\wade\Desktop\savedev\node_modules_bootstrap@4.4.1@bootstrap\dist\js'
@ ./node_modules/_bootstrap@4.4.1@bootstrap/dist/js/bootstrap.js 7:101-121
@ ./src/index.js
以上就是对–save和–save-dev的一些测试,想更快的得出结论其实是自己发布一个包。至于本人的答案是不是存在错误,欢迎指出,因为只是自己简单测试的结果。
用户体验地图(Customer Journey Map)是什么?
用户体验地图是从用户的视角出发,去理解用户、产品或者服务交互的一个重要的设计工具。
也可以说是以可视化的形式,来表现一个用户使用产品或者接受服务的体验情况,从体验的过程中来发现用户在整个体验过程中的问题点与情绪点,以此来从中提取出产品的优化点,方便对产品进行迭代,从而保证良好的用户体验。
经典案例
Chris Risdon绘制的欧洲铁路购票的体验地图
上图中是欧洲铁路公司整个体验地图的一部分。欧洲铁路公司是一家美国经销商,为北美旅客提供一个独立预订火车票去欧洲各地的平台,而无需用户去网站预定。他们已经拥有了一个良好体验的网站和一个屡获殊荣的咨询中心,但他们希望通过所有接触点来优化用户使用过程,这样可以让他们更全面地了解,他们应该专注的投资,设计和技术资源。整体的“诊断”评价系统,包含一系列的重点举措,体验地图只是其中派生的一部分。体验地图帮助建立同理心图,来理解随着时间和空间的推移,用户与欧洲铁路公司服务系统交互时接触点的变化。
在这张体验地图中采用了五个关键组成一个体验地图,一个体验地图可以直观的表示用户操作流、期望、特定的目标、用户情绪状态和整体的体验点,做到整体把控和评估产品体验。
作用 :
用户体验地图能帮助我们创造出一个有大局观的用户体验,更好的帮助我们理解用户的痛点和需求,帮助Team达成共识,非常有利于跨团队合作。
用户体验地图包含的内容 :
其中包括,人群(产品的用户是哪一类人)、 用户的需求(用户想得到什么)、 路径(在某特定的场景下体验的整体过程) 、接触点 (产品与人或人与服务接触的关键点)、行为(用户的行为是什么样的?)、情绪 (体验过程中的感受心情) 、机会点 (过程中可以突破的点,可以成为特色的地方)、 解决方案 (解决用户在体验过程的痛点)、 问题 (解决用户在体验过程的痛点)。
用户画像 :
在准备开始绘制用户体验地图的时候,我们应该要确立用户群体 / 确定产品目标 / 了解用户目标,并作出用户画像。
视觉设计师怎么使用
举例(一):
那我们看看作为一名视觉设计师应该关注哪部分的流程。
视觉设计师的用户体验地图 :
我们的聚焦点应在上图的这几个部分。
所以当绘制完用户体验地图后,应该再绘制一份视觉设计师看的版本,我们设计师主要关注的视觉的触点。
定量方法(产品方向):
我们在行为和情绪上一般会使用问卷法、后台数据分析法;而在需求和问题上一般会使用焦点小组、访谈法、观察法、日志法和田野调查,下面就为大家来解释下这些方法。
焦点小组:是指从研究产品中所确定的全部用户群(总体)中抽取一定数量的用户来组成样本,根据样本信息推断用户群总体特征的一种调查方法。
访谈法:访谈,就是以口头形式向用户进行询问,根据被询用户的答复搜集客观的、不带偏见的事实信息,以准确地说明样本所要代表的总体的一种方式。
观察法:观察法是指研究者根据一定的研究目的、研究提纲或观察表,用自己的感官和辅助工具去直接观察用户,从而获得资料的一种方法。
日志法:是由用研人员按时间顺序,详细记录自己在一段时间内使用产品的过程,经过归纳、分析,达到分析产品目的的一种工作分析方法。
田野调查:在日常生活中,在一个有一个严格定义的空间和时间的范围内,体验特定用户群的日常生活与思想境界,通过记录自己的生活的方方面面,来展示不同阶段用户群的基本需求。
注意事项(5要点)
1. 在制作地图前,应理清楚产品的前期规划和需求,并且与同事达成共识。
2. 避免以自己的经验或者认知来确定用户体验地图中的接触点,应当真正的从用户的行为中去提取。
3. 不要将一些落后的信息加入到用户体验地图中。
4. 最好先在Team内部脑暴一份地图,再去与所制作的地图进行对比。
5. 用户体验地图不会涉及到实现方案和现实机制,只涉及用户的体验。
团队人员的合理搭配 :
将公司或者团队的PM、RD、运营、Leader等过来,详细的描述这一份用户体验地图,聆听他们的反馈。
在分析用户问题上 :
分为四个等级:ABCD,在对优先级进行排列的同时应该,考虑到产品在每个阶段的侧重点,根据不同的进度和情况,来对优先级进行排列,帮助我们整理问题和提炼最核心的一些体验问题,区分问题还能帮助我们更好的把握产品的优化方向。
视觉设计师应该关注的点 :
视觉设计师的任务是什么?是有效的传达出产品的信息、简洁并且优雅的传达、通过视觉设计制造出愉悦的用户体验。用户在很多的场景下都可能接触到企业的产品或者是服务,这个服务接触带给用户的感受更多是偏向于视觉感知方面的。所以我们需要尽可能的列举出企业的产品或者服务与用户可能产生接触的场景、服务触点,再根据服务触点延伸出相关的“视觉触点”,用来梳理出我们需要输出的视觉产物,做出相对应的查漏补缺和优化,输出指导企业的品牌建设工作。而用户体验地图就很适合作为这样的工具。
“体验地图”对于优化视觉体验的意义 :
整体性:系统性地规划品牌的视觉统一化工作,提升品牌建设工作的全面性和完整度。也可以避免未来工作中不同的品牌 / UI / 运营设计师对于品牌概念的理解不同而带来的设计出入。
品牌设计,是用户对于公司产品的直接印象,所以在品牌设计的要求就是:建立特征、保持特征、推广特征、美化特征、对于以上的要求,来提供完整且匹配的设计方案。
运营设计,运营设计的目标就是让用户尽可能的感知到产品的好,把产品的特点通过设计包装传递给用户,一个好的运营设计,应该是在用户看到你的设计作品后,会产生足够好的兴趣和好感,并愿意去关注你的产品。
UI设计,这是产品与用户接触过程中,频率最高、最直观的部分,目的是为了让用户认识到产品的相貌和气质,UI设计需要注意界面视觉层次的强弱、信息划分、用户的视线轨迹、色彩的表达、质感、舒适度等,来让用户觉得这个产品设计真好。
例如 :
OFO,以年轻人为主的共享骑行产品,无论是在品牌/运营/UI的设计上,都能让人感觉时尚、年轻、阳光、且有亲和力。
品牌设计 :
UI设计 :
运营设计 :
UI设计 :
运营设计 :
设计师的进阶 :
在一开始的初级设计师阶段(也就是1.0阶段),我们需要从交互设计师手中接过交互设计稿,来对它进行气质进行改造,做出独特的视觉设计,也就是将其翻译为高保真稿,然后再与开发同学进行对接,也要保持视觉走查,以防实际效果与预期效果的不符;在这个1.0阶段我们的表现力和创造力,是最为主要的,如何去做出差异化?这是这个阶段的设计师需要考虑的,在这个APP设计趋同的大浪潮下,你如果能够做出不一样的设计,那么你则可以一鸣惊人,从众多水平相当的设计师中脱颖而出,这时你便可以考虑进入下一个阶段,也就是2.0。
在高级设计阶段(即2.0阶段),这时候你就需要拥有更好的产品思维和逻辑能力,不仅仅只是从交互设计师拿到交互设计稿,直接上手开做,在这之前,你需要开始了解产品的业务定位、用户人群、产品目标、当前的问题、未来的迭代等,需求方这时候就成你的主要对接对象,需要你具备拆解需求、采集用户的需求、扩展业务、能进行设计验证的能力,能将产品的气质和品牌贯穿于整个产品(UI/运营/品牌),设计是怎么推导的,现在就不是仅仅只在停留在好看的层面上了,毕竟设计师不是画师,而是解决问题的,我们在做了某个设计后,就要去关注它的变化了,看看用户的反馈、商业转化率等等,这都是为你的下一次设计迭代做的参考。
从
分享到脉脉
转自:脉脉
原文链接:https://maimai.cn/article/detail?fid=988630001&efid=N-uHKNnf7vXGBmaFd3lZHA&use_rn=1
本文讲述,在使用VUE的移动端实现类似于iPhone的悬浮窗的效果。
相关知识点
touchstart 当在屏幕上按下手指时触发
touchmove 当在屏幕上移动手指时触发
touchend 当在屏幕上抬起手指时触发
mousedown mousemove mouseup对应的是PC端的事件
touchcancel 当一些更高级别的事件发生的时候(如电话接入或者弹出信息)会取消当前的touch操作,即触发touchcancel。一般会在touchcancel时暂停游戏、存档等操作。
效果图
实现步骤
1.html
总结了一下评论,好像发现大家都碰到了滑动的问题。就在这里提醒一下吧。可将该悬浮 DIV 同你的 scroller web 同级。 —- (log: 2018-08-21)
html结构: <template> <div>你的web页面</div> <div>悬浮DIV</div> </template>
<template> <div id="webId"> ... <div>你的web页面</div> <!-- 如果碰到滑动问题,1.1 请检查这里是否属于同一点。 --> <!-- 悬浮的HTML --> <div v-if="!isShow" class="xuanfu" id="moveDiv" @mousedown="down" @touchstart="down" @mousemove="move" @touchmove="move" @mouseup="end" @touchend="end" > <div class="yuanqiu"> {{pageInfo.totalPage}} </div> </div> ... </div> </template>
2.JS
<script> data() { return { flags: false, position: { x: 0, y: 0 }, nx: '', ny: '', dx: '', dy: '', xPum: '', yPum: '', } } methods: { // 实现移动端拖拽 down(){ this.flags = true; var touch; if(event.touches){ touch = event.touches[0]; }else { touch = event; } this.position.x = touch.clientX; this.position.y = touch.clientY; this.dx = moveDiv.offsetLeft; this.dy = moveDiv.offsetTop; }, move(){ if(this.flags){ var touch ; if(event.touches){ touch = event.touches[0]; }else { touch = event; } this.nx = touch.clientX - this.position.x; this.ny = touch.clientY - this.position.y; this.xPum = this.dx+this.nx; this.yPum = this.dy+this.ny; moveDiv.style.left = this.xPum+"px"; moveDiv.style.top = this.yPum +"px"; //阻止页面的滑动默认事件;如果碰到滑动问题,1.2 请注意是否获取到 touchmove document.addEventListener("touchmove",function(){ event.preventDefault(); },false); } }, //鼠标释放时候的函数 end(){ this.flags = false; }, } </script>
3.CSS
<style> .xuanfu { height: 4.5rem; width: 4.5rem; /* 如果碰到滑动问题,1.3 请检查 z-index。z-index需比web大一级*/ z-index: 999; position: fixed; top: 4.2rem; right: 3.2rem; border-radius: 0.8rem; background-color: rgba(0, 0, 0, 0.55); } .yuanqiu { height: 2.7rem; width: 2.7rem; border: 0.3rem solid rgba(140, 136, 136, 0.5); margin: 0.65rem auto; color: #000000; font-size: 1.6rem; line-height: 2.7rem; text-align: center; border-radius: 100%; background-color: #ffffff; } </style>
实现好JS逻辑,基本上,问题不大。
本文链接 http://www.luyixian.cn/javascript_show_166242.aspx
再加一点
css之display:inline-block布局
1.解释一下display的几个常用的属性值,inline , block, inline-block
两个图可以看出,display:inline-block后块级元素能够在同一行显示,有人这说不就像浮动一样吗。没错,display:inline-block的效果几乎和浮动一样,但也有不同,接下来讲一下inline-block和浮动的比较。
2.inline-block布局 vs 浮动布局
a.不同之处:对元素设置display:inline-block ,元素不会脱离文本流,而float就会使得元素脱离文本流,且还有父元素高度坍塌的效果
b.相同之处:能在某程度上达到一样的效果
我们先来看看这两种布局:
图一:display:inline-block
图二:
对两个孩子使用float:left,我在上一篇浮动布局讲过,这是父元素会高度坍塌,所以要闭合浮动,对box使用overflow:hidden,效果如下:
>>乍一看两个都能做到几乎相同的效果,(仔细看看display:inline-block中有间隙问题,这个留到下面再讲)
c.浮动布局不太好的地方:参差不齐的现象,我们看一个效果:
图三:
图四:
>>从图3,4可以看出浮动的局限性在于,若要元素排满一行,换行后还要整齐排列,就要子元素的高度一致才行,不然就会出现图三的效果,而inline-block就不会。
3.inline-block存在的小问题:
a.上面可以看到用了display:inline-block后,存在间隙问题,间隙为4像素,这个问题产生的原因是换行引起的,因为我们写标签时通常会在标签结束符后顺手打个回车,而回车会产生回车符,回车符相当于空白符,通常情况下,多个连续的空白符会合并成一个空白符,而产生“空白间隙”的真正原因就是这个让我们并不怎么注意的空白符。
b.去除空隙的方法:
1.对父元素添加,{font-size:0},即将字体大小设为0,那么那个空白符也变成0px,从而消除空隙
现在这种方法已经可以兼容各种浏览器,以前chrome浏览器是不兼容的
图一:
c.浏览器兼容性:ie6/7是不兼容 display:inline-block的所以要额外处理一下:
在ie6/7下:
对于行内元素直接使用{dislplay:inline-block;}
对于块级元素:需添加{display:inline;zoom:1;}
4.总结:
display:inline-block的布局方式和浮动的布局方式,究竟使用哪个,我觉得应该根据实际情况来决定的:
a.对于横向排列东西来说,我更倾向与使用inline-block来布局,因为这样清晰,也不用再像浮动那样清除浮动,害怕布局混乱等等。
b.对于浮动布局就用于需要文字环绕的时候,毕竟这才是浮动真正的用武之地,水平排列的是就交给inline-block了。
Node 的os模块是操作系统的
Node 的内置模块 fs
内置模块在下载node的时候就自带的,使用 require()方法来导入
语法 :require(‘模块fs’)
在内置模块中的方法
1 fs.readFile() —》用来专门 异步 读取文件的方法 三个参数
语法 :fs.readFile(‘要读取的文件’,读取文件的格式,读取成功的回调函数)
Eg : fs.readFIle(‘a’,’utf8’,’function(err,data){ })
2 fs.readFileSync()-– 专门用来 同步 读取的方法, 两个参数
语法: fs.readFileSync(‘要读取的文件’,读取格式)
3 fs.writeFIle() —>用来写入 异步 文件的方法 三个参数
语法: fs.writeFile(‘写入到哪个文件’,写入的内容,成功的回调函数)
Eg: fs.writeFile(‘./text.tex’,”内容”, function(){ })
注意:再次写入的内容会完全覆盖 。如果文件夹没有 会自动创建一个文件夹
4 fs.writeFileSync() --> 同步写入的方法
语法: fs.writeFileSync(‘写入到文件’,“写入的内容”)
Node的http模块
这个模块专门用来创建服务的
只能支持http协议。
也是使用require()方法
Const http= require(“http”)
方法
1 http.createServer(function(req,res){ }) 两个形参
Req=request 代表每次的请求信息
Res=response 代表每次请求的响应
返回值是一个服务,当服务监听端口号的时候,就变成了服务器。
2 监听端口号
创建的服务.listen(监听的端口号,监听成功的回调函数(选填))
server.listen(8080,function(){ 端口号0-65535 建议0-1023不使用 })
此时浏览器就可以执行localhost进行访问了
自定义模块
每一个js文件都是一个独立的模块,他们都自带一个 module 是一个对象,
其中 module里面的 exports,是一个对象 这个 module.exports 就是这个文件向外导出的内容,也就是说,只有导出,才能导入
Eg: function fn1(){console.log() }
Module.exports.fn1=fn1
这样,才能是另一个js文件到入这个文件 同样也是require(‘./js’)方法
想要学会这个漂亮的烟花吗?快来跟着学习吧~
<div class="container"></div>
我们只需要一个盒子表示烟花爆炸范围就可以了
fire是烟花 注意添加绝对定位
<style> .container{ margin: 0 auto; height: 500px; width: 1200px; background: black; position: relative; overflow: hidden; } .fire{ width: 10px; background: white; height: 10px; /* border-radius: 50%; */ position: absolute; bottom: 0; } </style>
需要用到一个鼠标点击的位置,一个div选择器,一个爆炸样式
function Firework(x,y,selector,type){ //此处获取对象的方式为单例的思想,避免重复获取相同的元素 if(Firework.box && selector === Firework.box.selector){ this.box = Firework.box.ele; }else{ Firework.box = { ele:document.querySelector(selector), selector:selector } this.box = Firework.box.ele; } this.type = type; this.init(x,y) }
function animation(ele,attroptions,callback){ for(var attr in attroptions){ attroptions[attr] ={ target:attroptions[attr], inow:parseInt(getComputedStyle(ele)[attr]) } } clearInterval(ele.timer); ele.timer = setInterval(function(){ for(var attr in attroptions ){ var item = attroptions[attr] var target = item.target; var inow = item.inow; var speed = (target - inow)/10; speed = speed>0?Math.ceil(speed):Math.floor(speed); if(Math.abs(target - inow) <= Math.abs(speed)){ ele.style[attr] = target+"px"; delete attroptions[attr]; for(var num in attroptions){ return false; } clearTimeout(ele.timer); if(typeof callback === "function")callback(); }else{ attroptions[attr].inow += speed; ele.style[attr] = attroptions[attr].inow+"px"; } } },30) }
Firework.prototype = { constructor:Firework, //初始化 init:function(x,y){ //创建一个烟花 this.ele = this.createFirework(); //xy为鼠标落点 this.x = x ; this.y = y; //maxXy为最大运动范围 this.maxX = this.box.offsetWidth - this.ele.offsetWidth; this.maxY = this.box.offsetHeight - this.ele.offsetHeight; //初始化结束后 烟花随机颜色 this.randomColor(this.ele); //烟花升空 this.fireworkUp(this.ele); }, //创造烟花 createFirework:function(){ var ele = document.createElement("div"); ele.className = "fire"; this.box.appendChild(ele); return ele; }, //烟花升空 fireworkUp:function(ele){ ele.style.left = this.x + "px"; //此处用到刚刚封装的运动方法 animation(ele,{top:this.y},function(){ ele.remove(); this.fireworkBlast() }.bind(this)); }, //烟花爆炸 fireworkBlast:function(){ for(var i = 0 ; i < 20; i++){ var ele = document.createElement("div"); ele.className = "fire"; ele.style.left = this.x + "px"; ele.style.top = this.y + "px"; this.box.appendChild(ele); ele.style.borderRadius = "50%"; this.randomColor(ele); //判定一下输入的爆炸方式是原型烟花 还是散落烟花 由此更改获取的烟花位置 animation(ele,this.type === "circle"?this.circleBlast(i,20): this.randomPosition(),function(cale){ cale.remove(); }.bind(this,ele)) } }, //圆形爆炸位置 circleBlast:function(i,total){ var r = 200; var reg = 360 / total *i; var deg = Math.PI / 180 *reg; return { left:r * Math.cos(deg) + this.x , top:r * Math.sin(deg) + this.y } }, //随机颜色 randomPosition:function(){ return { left : Math.random()*this.maxX, top : Math.random()*this.maxY } }, randomColor:function(ele){ var color = "#" + parseInt(parseInt("ffffff",16)*Math.random()).toString(16).padStart(6,0); return ele.style.backgroundColor = color; } }
document.querySelector(".container").addEventListener("click",function(evt){ var e = evt||event; new Firework(e.offsetX,e.offsetY,".container","circle") new Firework(e.offsetX,e.offsetY,".container") })
全部代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> <style> .container{ margin: 0 auto; height: 500px; width: 1200px; background: black; position: relative; overflow: hidden; } .fire{ width: 10px; background: white; height: 10px; /* border-radius: 50%; */ position: absolute; bottom: 0; } </style> </head> <body> <div class="container"></div> <script src="./utils.js"></script> <script> function animation(ele,attroptions,callback){ for(var attr in attroptions){ attroptions[attr] ={ target:attroptions[attr], inow:parseInt(getComputedStyle(ele)[attr]) } } clearInterval(ele.timer); ele.timer = setInterval(function(){ for(var attr in attroptions ){ var item = attroptions[attr] var target = item.target; var inow = item.inow; var speed = (target - inow)/10; speed = speed>0?Math.ceil(speed):Math.floor(speed); if(Math.abs(target - inow) <= Math.abs(speed)){ ele.style[attr] = target+"px"; delete attroptions[attr]; for(var num in attroptions){ return false; } clearTimeout(ele.timer); if(typeof callback === "function")callback(); }else{ attroptions[attr].inow += speed; ele.style[attr] = attroptions[attr].inow+"px"; } } },30) } function Firework(x,y,selector,type){ if(Firework.box && selector === Firework.box.selector){ this.box = Firework.box.ele; }else{ Firework.box = { ele:document.querySelector(selector), selector:selector } this.box = Firework.box.ele; } this.type = type; this.init(x,y) } Firework.prototype = { constructor:Firework, //初始化 init:function(x,y){ this.ele = this.createFirework(); this.x = x ; this.y = y; this.maxX = this.box.offsetWidth - this.ele.offsetWidth; this.maxY = this.box.offsetHeight - this.ele.offsetHeight; this.randomColor(this.ele); this.fireworkUp(this.ele); }, //创造烟花 createFirework:function(){ var ele = document.createElement("div"); ele.className = "fire"; this.box.appendChild(ele); return ele; }, fireworkUp:function(ele){ ele.style.left = this.x + "px"; animation(ele,{top:this.y},function(){ ele.remove(); this.fireworkBlast() }.bind(this)); }, //烟花爆炸 fireworkBlast:function(){ for(var i = 0 ; i < 20; i++){ var ele = document.createElement("div"); ele.className = "fire"; ele.style.left = this.x + "px"; ele.style.top = this.y + "px"; this.box.appendChild(ele); ele.style.borderRadius = "50%"; this.randomColor(ele); animation(ele,this.type === "circle"?this.circleBlast(i,20): this.randomPosition(),function(cale){ cale.remove(); }.bind(this,ele)) } }, circleBlast:function(i,total){ var r = 200; var reg = 360 / total *i; var deg = Math.PI / 180 *reg; return { left:r * Math.cos(deg) + this.x , top:r * Math.sin(deg) + this.y } }, randomPosition:function(){ return { left : Math.random()*this.maxX, top : Math.random()*this.maxY } }, randomColor:function(ele){ var color = "#" + parseInt(parseInt("ffffff",16)*Math.random()).toString(16).padStart(6,0); return ele.style.backgroundColor = color; } } document.querySelector(".container").addEventListener("click",function(evt){ var e = evt||event; new Firework(e.offsetX,e.offsetY,".container","circle") new Firework(e.offsetX,e.offsetY,".container") }) </script> </body> </html>
———————————————— 版权声明:本文为CSDN博主「SpongeBooob」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/qq_41383900/article/details/105026768
许多人都有这样一种映像,NodeJS比较快; 但是因为其是单线程,所以它不稳定,有点不安全,不适合处理复杂业务; 它比较适合对并发要求比较高,而且简单的业务场景。
在Express的作者的TJ Holowaychuk的 告别Node.js一文中列举了以下罪状:
Farewell NodeJS (TJ Holowaychuk)
• you may get duplicate callbacks
• you may not get a callback at all (lost in limbo)
• you may get out-of-band errors
• emitters may get multiple “error” events
• missing “error” events sends everything to hell
• often unsure what requires “error” handlers
• “error” handlers are very verbose
• callbacks suck
其实这几条主要吐嘈了两点: node.js错误处理很扯蛋,node.js的回调也很扯蛋。
事实上NodeJS里程确实有“脆弱”的一面,单线程的某处产生了“未处理的”异常确实会导致整个Node.JS的崩溃退出,来看个例子, 这里有一个node-error.js的文件:
var http = require('http'); var server = http.createServer(function (req, res) { //这里有个错误,params 是 undefined var ok = req.params.ok; res.writeHead(200, {'Content-Type': 'text/plain'}); res.end('Hello World '); }); server.listen(8080, '127.0.0.1'); console.log('Server running at http://127.0.0.1:8080/');
启动服务,并在地址栏测试一下发现 http://127.0.0.1:8080/ 不出所料,node崩溃了
$ node node-error Server running at http://127.0.0.1:8080/ c:githubscript ode-error.js:5 var ok = req.params.ok; ^ TypeError: Cannot read property 'ok' of undefined at Server.<anonymous> (c:githubscript ode-error.js:5:22) at Server.EventEmitter.emit (events.js:98:17) at HTTPParser.parser.onIncoming (http.js:2108:12) at HTTPParser.parserOnHeadersComplete [as onHeadersComplete] (http.js:121:23) at Socket.socket.ondata (http.js:1966:22) at TCP.onread (net.js:525:27)
其实Node.JS发展到今天,如果连这个问题都解决不了,那估计早就没人用了。
我们可以uncaughtException来全局捕获未捕获的Error,同时你还可以将此函数的调用栈打印出来,捕获之后可以有效防止node进程退出,如:
process.on('uncaughtException', function (err) { //打印出错误 console.log(err); //打印出错误的调用栈方便调试 console.log(err.stack); });
这相当于在node进程内部进行守护, 但这种方法很多人都是不提倡的,说明你还不能完全掌控Node.JS的异常。
我们还可以在回调前加try/catch,同样确保线程的安全。
var http = require('http'); http.createServer(function(req, res) { try { handler(req, res); } catch(e) { console.log(' ', e, ' ', e.stack); try { res.end(e.stack); } catch(e) { } } }).listen(8080, '127.0.0.1'); console.log('Server running at http://127.0.0.1:8080/'); var handler = function (req, res) { //Error Popuped var name = req.params.name; res.writeHead(200, {'Content-Type': 'text/plain'}); res.end('Hello ' + name); };
这种方案的好处是,可以将错误和调用栈直接输出到当前发生的网页上。
标准的HTTP响应处理会经历一系列的Middleware(HttpModule),最终到达Handler,如下图所示:
这 些Middleware和Handler在NodeJS中都有一个特点,他们都是回调函数,而回调函数中是唯一会让Node在运行时崩溃的地方。根据这个 特点,我们只需要在框架中集成一处try/catch就可以相对完美地解决异常问题,而且不会影响其它用户的请求request。
事实上现在的NodeJS WEB框架几乎都是这么做的,如 OurJS开源博客所基于的 WebSvr
就有这么一处异常处理代码:
Line: 207 try { handler(req, res); } catch(err) { var errorMsg = ' ' + 'Error ' + new Date().toISOString() + ' ' + req.url + ' ' + err.stack || err.message || 'unknow error' + ' ' ; console.error(errorMsg); Settings.showError ? res.end('<pre>' + errorMsg + '</pre>') : res.end(); }
那么不在回调中产生的错误怎么办?不必担心,其实这样的node程序根本就起不起来。
此外node自带的 cluster 也有一定的容错能力,它跟nginx的worker很类似,但消耗资源(内存)略大,编程也不是很方便,OurJS并没有采用此种设计。
现 在已经基本上解决了Node.JS因异常而崩溃的问题,不过任何平台都不是100%可靠的,还有一些错误是从Node底层抛出的,有些异常 try/catch和uncaughtException都无法捕获。之前在运行ourjs的时侯,会偶尔碰到底层抛出的文件流读取异常,这就是一个底层 libuv的BUG,node.js在0.10.21中进行了修复。
面对这种情况,我们就应该为nodejs应用添加守护进程,让NodeJS遭遇异常崩溃以后能马上复活。
另外,还应该把这些产生的异常记录到日志中,并让异常永远不再发生。
node-forever 提供了守护的功能和LOG日志记录功能。
安装非常容易
[sudo] npm install forever
使用也很简单
$ forever start simple-server.js $ forever list [0] simple-server.js [ 24597, 24596 ]
还可以看日志
forever -o out.log -e err.log my-script.js
使用node来守护的话资源开销可能会有点大,而且也会略显复杂,OurJS直接在开机启动脚本来进程线程守护。
如在debian中放置的 ourjs 开机启动文件: /etc/init.d/ourjs
这个文件非常简单,只有启动的选项,守护的核心功能是由一个无限循环 while true; 来实现的,为了防止过于密集的错误阻塞进程,每次错误后间隔1秒重启服务
WEB_DIR='/var/www/ourjs' WEB_APP='svr/ourjs.js' #location of node you want to use NODE_EXE=/root/local/bin/node while true; do { $NODE_EXE $WEB_DIR/$WEB_APP config.magazine.js echo "Stopped unexpected, restarting " } 2>> $WEB_DIR/error.log sleep 1 done
错误日志记录也非常简单,直接将此进程控制台当中的错误输出到error.log文件即可: 2>> $WEB_DIR/error.log 这一行, 2 代表 Error。
蓝蓝设计的小编 http://www.lanlanwork.com