同域下单点登录分析 | 单点登录讲解(2)

本项目主要讲解的是单点登录系统的原理及其实现。相关代码github链接。本章主要讲解的是同域下单点登录分析。同域下SSO分析与设计流程图虽然看着复杂,但大家不要被吓到啦^ _ ^,请大家参照着流程路,听我下面细细道来~~详细流程分析项目结构在github的代码中,我建立了三个项目,分别是服务端SSOServer、客户端SSOClient、以及两个集成了客户端的业务系统app1与aPP2。SSO流程分析将SSOServer,app1,app2启动后,开始SSO流程分析。1)未登录访问业务系统未登录访问业务系统app1的index页面:请求被客户端的Filter拦截。由于没有token,客户端Filter控制其进行登录操作,并将原始的URL作为请求的参数。2)用户执行登录操作进入服务端的UserLoginServlet进行登录操作。进入登录页时,获取URL中的参数——原始的origRUL,将其作为request对象的属性,方便后人获取,并且跳转到服务端的登录页面。用户提交表单,服务端获取表单中信息后,到数据库中进行查询。若登录失败,则返回原来的登录页面,并携带原来的URL,将原始的URL作为表单的隐藏属性。若登录成功则:1.生成token。2.将token与其对应的user放到全局唯一数据结构中,方便所有人进行获取。3.给该用户设置一个cookie,值为token,用户在下次访问的时候就会携带此cookie,服务端也就可以通过该cookie对其身份进行验证。4.判断原始URL是否为空,若不为空则跳转到原始URL页面,否则跳转到成功登录页面。在用户要跳转到原始URL页面的时候,被客户端的Filter拦截,进行有无token的验证,由于经过登录操作在cookie中已经生成了token,故Filter发送Http通信请求服务端进行token有效性的验证,并将token作为请求参数。服务端的TokenValidateServlet获取参数中的token后,到全局唯一数据结构中查找有无该token对应的user。若没有,则证明该token可能已经失效或是伪造的,则向客户端返回空字符串,否则返回查询到的user信息。拦截器接收到服务端的返回信息,若为空字符串则返回原始登录页面,并携带原始URL,否则通过传来的用户信息,对user对象进行还原,方便下个人获取,拦截操作结束,成功进入了业务系统的index页面,并将request对象携带的user信息显示出来。3)业务系统增加自己的拦截器在通过了客户端的拦截器之后,业务系统还可以自定义拦截器,从而根据用户信息获取与本系统相关的用户业务信息,或者是对用户的权限进行进一步的验证,如:“猪猪"不可访问index页面等QAQ。4)单点退出服务端从request对象的cookie中获取token的值,将这个token从全局数据结构中移除,并且将用户保存有该token的cookie设置为无效的。关于代码部分,我将会在下一章节中进行详细解释~~~大家多多关照!^ _ ^

March 9, 2019 · 1 min · jiezi

活动 Web 页面人机识别验证的探索与实践

在电商行业,线上的营销活动特别多。在移动互联网时代,一般为了活动的快速上线和内容的即时更新,大部分的业务场景仍然通过 Web 页面来承载。但由于 Web 页面天生“环境透明”,相较于移动客户端页面在安全性上存在更大的挑战。本文主要以移动端 Web 页面为基础来讲述如何提升页面安全性。活动 Web 页面的安全挑战对于营销活动类的 Web 页面,领券、领红包、抽奖等活动方式很常见。此类活动对于普通用户来说大多数时候就是“拼手气”,而对于非正常用户来说,可以通过直接刷活动 API 接口的“作弊”方式来提升“手气”。这样的话,对普通用户来说,就变得很不公平。对于活动运营的主办方来说,如果风控措施做的不好,这类刷接口的“拼手气”方式可能会对企业造成较大的损失。如本来计划按 7 天发放的红包,在上线 1 天就被刷光了,活动的营销成本就会被意外提升。主办方想发放给用户的满减券、红包,却大部分被黄牛使用自动脚本刷走,而真正想参与活动的用,却无法享受活动优惠。终端用户到底是人还是机器,网络请求是否为真实用户发起,是否存在安全漏洞并且已被“羊毛党”恶意利用等等,这些都是运营主办方要担心的问题。安全防范的基本流程为了提升活动 Web 页面的安全性,通常会引入专业的风控服务。引入风控服务后,安全防护的流程大致如图所示。Web 前端:用户通过 Web 页面来参与活动,同时 Web 前端也会收集用于人机识别验证的用户交互行为数据。由于不同终端(移动端 H5 页面和 PC 端页面)交互形式不同,收集用户交互行为数据的侧重点也会有所不同。风控服务:一般大公司都会有专业的风控团队来提供风控服务,在美团内部有智能反爬系统来基于地理位置、IP地址等大数据来提供频次限制、黑白名单限制等常规的基础风控拦截服务。甚至还有依托于海量的全业务场景的用户大数据,使用贝叶斯模型、神经网络等来构建专业度较深的服务。风控服务可以为 Web 前端提供通用的独立验证 SDK:验证码、滑块验证等区分人机的“图灵验证”,也可以为服务端提供 Web API 接口的验证等。后端业务服务:负责处理活动业务逻辑,如给用户发券、发红包,处理用户抽奖等。请求需要经过风控服务的验证,确保其安全性,然后再来处理实际业务逻辑,通常,在处理完实际业务逻辑时,还会有针对业务本身的风控防范。对于活动 Web 页面来说,加入的风控服务主要为了做人机识别验证。在人机识别验证的专业领域上,我们可以先看看业界巨头 Google 是怎么做的。Google 如何处理人机验证Google 使用的人机验证服务是著名的 reCAPTCHA(Completely Automated Public Turing Test To Tell Computers and Humans Apart,区分人机的全自动图灵测试系统),也是应用最广的验证码系统。早年的 reCAPTCHA 验证码是这样的:如今的 reCAPTCHA 已经不再需要人工输入难以识别的字符,它会检测用户的终端环境,追踪用户的鼠标轨迹,只需用户点击“我不是机器人”就能进行人机验证(reCAPTCHA骗用户进行数据标注而进行AI训练的验证另说)。reCAPTCHA 的验证方式从早先的输入字符到现在的轻点按钮,在用户体验上,有了较大的提升。而在活动场景中引入人机识别验证,如果只是简单粗暴地增加验证码,或者只是像 reCAPTCHA 那样增加点击“我不是机器人”的验证,都会牺牲用户体验,降低用户参加活动的积极性。Google 的普通 Web 页面的浏览和有强交互的活动 Web 页面虽是不同的业务场景,但对于活动 Web 页面来说,强交互恰好为人机识别验证提供了用户交互行为数据收集的契机。人机识别验证的技术挑战理想的方案是在用户无感知的情况下做人机识别验证,这样既确保了安全又对用户体验无损伤。从实际的业务场景出发再结合 Web 本身的环境,如果想实现理想的方案,可能会面临如下的技术挑战:(1)需要根据用户的使用场景来定制人机识别验证的算法:Web 前端负责收集、上报用户交互行为数据,风控服务端校验上报的数据是否符合正常的用户行为逻辑。(2)确保 Web 前端和风控服务端之间通信和数据传输的安全性。(3)确保上述两大挑战中提到的逻辑和算法不会被代码反编译来破解。在上述的三个挑战中,(1)已经实现了人机识别验证的功能,而(2)和(3)都是为了确保人机识别验证不被破解而做的安全防范。接下来,本文会分别针对这三个技术挑战来说明如何设计技术方案。挑战一:根据用户使用场景来定制人机识别验证算法先来分析一下用户的使用场景,正常用户参与活动的步骤是用户进入活动页面后,会有短暂的停留,然后点击按钮参与活动。这里所说的“参与活动”,最终都会在活动页面发起一个接口的请求。如果是非正常用户,可以直接跳过以上的实际动作而去直接请求参与活动的接口。那么区别于正常用户和非正常用户就是那些被跳过的动作,对实际动作进一步归纳如下:进入页面。短暂的停留。滚动页面。点击按钮。以上的动作又可以分为必需的操作和可选的操作。对这一连串动作产生的日志数据进行收集,在请求参与活动的接口时,将这些数据提交至后端,验证其合法性。这就是一个简单的人机识别验证。在验证动作的合法性时,需要考虑到这些动作数据是不是能被轻易模拟。另外,动作的发生应该有一条时间线,可以给每个动作都增加一个时间戳,比如点击按钮肯定是在进入页面之后发生的。一些特定的动作的日志数据也会有合理的区间,进入页面的动作如果以 JS 资源加载的时间为基准,那么加载时间可能大于 100 毫秒,小于 5 秒。而对于移动端的按钮点击,点击时记录的坐标值也会有对应的合理区间,这些合理的区间会根据实际的环境和情况来进行设置。除此之外,设备环境的数据也可以进行收集,包括用户参与活动时使用的终端类型、浏览器的类型、浏览器是否为客户端的容器等,如果使用了客户端,客户端是否会携带特殊的标识等。最后,还可以收集一些“无效”的数据,这些数据用于障人耳目,验证算法会将其忽略。尽管收集数据的动作是透明的,但是验证数据合法性不是透明的,攻击者无法知道,验证的算法中怎么区分哪些是有效、哪些是无效。这已经有点“蜜罐数据”的意思了。挑战二:确保通信的安全性收集的敏感数据要发送给风控服务端,进而确保通信过程的安全。Web API 接口不能被中途拦截和篡改,通信协议使用 HTTPS 是最基本的要求;同时还要让服务端生成唯一的 Token,在通信过程中都要携带该 Token。接口携带的敏感数据不能是明文的,敏感数据要进行加密,这样攻击者无法通过网络抓包来详细了解敏感数据的内容。Token 的设计Token 是一个简短的字符串,主要为了确保通信的安全。用户进入活动 Web 页面后,请求参与活动的接口之前,会从服务端获取 Token。该 Token 的生成算法要确保 Token 的唯一性,通过接口或 Cookie 传递给前端,然后,前端在真正请求参与活动的接口时需要带上该 Token,风控服务端需要验证 Token 的合法性。也就是说,Token 由服务端生成,传给前端,前端再原封不动的回传给服务端。一旦加入了 Token 的步骤,攻击者就不能直接去请求参与活动的接口了。Token 由风控服务端基于用户的身份,根据一定的算法来生成,无法伪造,为了提升安全等级,Token 需要具有时效性,比如 10 分钟。可以使用 Redis 这类缓存服务来存储 Token,使用用户身份标识和 Token 建立 KV 映射表,并设置过期时间为 10 分钟。虽然前端在 Cookie 中可以获取到 Token,但是前端不能对 Token 做持久化的缓存。一旦在 Cookie 中获取到了 Token,那么前端可以立即从 Cookie 中删除该 Token,这样能尽量确保 Token 的安全性和时效性。Token 存储在 Redis 中,也不会因为用户在参与活动时频繁的切换页面请求,而对服务造成太大的压力。另外,Token 还可以有更多的用处:标识参与活动用户的有效性。敏感数据对称加密时生成动态密钥。API 接口的数字签名。敏感数据加密通信时,传递的敏感数据可以使用常见的对称加密算法进行加密。为了提升加密的安全等级,加密时的密钥可以动态生成,前端和风控服务端约定好动态密钥的生成规则即可。加密的算法和密钥也要确保不被暴露。通过对敏感数据加密,攻击者在不了解敏感数据内容的前提下就更别提模拟构造请求内容了。挑战三:化解纸老虎的尴尬有经验的 Web 开发者看到这里,可能已经开始质疑了:在透明的前端环境中折腾安全不是白折腾吗?这就好比费了很大的劲却只是造了一个“纸老虎”,质疑是有道理的,但是且慢,通过一些安全机制的加强是可以让“纸老虎”尽可能的逼真。本文一再提及的 Web 环境的透明性,是因为在实际的生产环境中的问题:前端的代码在压缩后,通过使用浏览器自带的格式化工具和断点工具,仍然具备一定的可读性,花点时间仍然可以理解代码的逻辑,这就给攻击者提供了大好的代码反编译机会。如果要化解“纸老虎”的尴尬,就要对前端的代码进行混淆。前端代码混淆前端的 JS 代码压缩工具基本都是对变量、函数名称等进行缩短,压缩对于混淆的作用是比较弱。除了对代码进行压缩,还需要进行专门的混淆。对代码进行混淆可以降低可读性,混淆工具有条件的话最好自研,开源的工具要慎用。或者基于 Uglify.js 来自定义混淆的规则,混淆程度越高可读性就越低。代码混淆也需要把握一个度,太复杂的混淆可能会让代码无法运行,也有可能会影响本身的执行效率。同时还需要兼顾混淆后的代码体积,混淆前后的体积不能有太大的差距,合理的混淆程度很重要。断点工具的防范会更麻烦些。在使用断点工具时通常都会导致代码延迟执行,而正常的业务逻辑都会立即执行,这是一个可以利用的点,可以考虑在代码执行间隔上来防范断点工具。通过代码混淆和对代码进行特殊的处理,可以让格式化工具和断点工具变得没有用武之地。唯一有些小遗憾,就是处理后的代码也不能正常使用 Source Map 的功能了。有了代码混淆,反编译的成本会非常高,这样“纸老虎”已经变得很逼真了。技术方案设计在讲解完如何解决关键的技术挑战后,就可以把相应的方案串起来,然后设计成一套可以实施的技术方案了。相对理想的技术方案架构图如下:下面会按步骤来讲解技术方案的处理流程:Step 0 基础风控拦截基础风控拦截是上面提到的频次、名单等的拦截限制,在 Nginx 层就能直接实施拦截。如果发现是恶意请求,直接将请求过滤返回 403,这是初步的拦截,用户在请求 Web 页面的时候就开始起作用了。Step 1 风控服务端生成 Token 后传给前端Step 0 可能还没进入到活动 Web 页面,进入活动 Web 页面后才真正开始人机识别验证的流程,前端会先开始获取 Token。Step 2 前端生成敏感数据敏感数据应包含用户交互行为数据、设备环境数据、活动业务逻辑数据以及无效数据。Step 3 使用 HTTPS 的签名接口发送数据Token 可以作为 Authorization 的值添加到 Header 中,数据接口的签名可以有效防止 CSRF 的攻击。Step 4 数据接口的校验风控服务端收到请求后,会先验证数据接口签名中的 Token 是否有效。验证完 Token,才会对敏感数据进行解密。数据解密成功,再进一步对人机识别的数据合法性进行校验。Step 5 业务逻辑的处理前面的步骤为了做人机识别验证,这些验证不涉及到业务逻辑。在所有这些验证都通过后,后端业务服务才会开始处理实际的活动业务逻辑。处理完活动业务逻辑,最终才会返回用户参与活动的结果。总结为了提升活动 Web 页面的安全性,使用了各种各样的技术方案,我们将这些技术方案组合起来才能发挥安全防范的作用,如果其中某个环节处理不当,都可能会被当作漏洞来利用,从而导致整个验证方案被攻破。为了验证技术方案的有效性,可以持续观察活动 API 接口的请求成功率。从请求成功率的数据中进一步分析“误伤”和“拦截”的数据,以进一步确定是否要对方案进行调优。通过上述的人机识别验证的组合方案,可以大幅提升活动 Web 页面的安全性。在活动 Web 页面应作为一个标准化的安全防范流程,除了美团,像淘宝和天猫也有类似的流程。由于活动运营的环节和方法多且复杂,仅仅提升了 Web 页面也不敢保证就是铁板一块,安全需要关注的环节还很多,安全攻防是一项长期的“拉锯升级战”,安全防范措施也需要持续地优化升级。参考资料https://www.google.com/recaptcha/intro/v3.htmlhttps://segmentfault.com/a/1190000006226236https://www.freebuf.com/articles/web/102269.html作者简介益国,美团点评 Web 前端开发工程师。2015年加入美团,曾先后负责过风控前端SDK和活动运营平台的研发,现负责大数据平台的研发工作。 ...

March 8, 2019 · 1 min · jiezi

单点登录系统SSO概述 | 单点登录讲解(1)

本项目主要讲解的是单点登录系统的原理及其实现。本章主要讲解的是单点登录系统的概述部分。单点登录单点登录顾名思义就是从一个系统进行登录操作,就可以访问其他附近的系统。单点登录避免了用户重复的登录过程,在整个核心业务中起到了一个基层的辅助作用。关键步骤当用户对业务系统发起访问请求的时候,我们将其拦截下来,进行授权验证,验证其是否有访问业务系统的权限。若用户有权限,则进行访问操作。若用户没有权限,则提供一个接口,让用户进行授权登录的操作。单点登录体系结构1.认证中心认证中心的作用:(1)验证用户是否有访问的权限。(2)若无权限,则提供页面或接口,让用户进行授权登录操作。2.用户与账号管理系统需要提供用户数据获取的接口,让业务系统在需要获取用户的数据的时候通过访问接口获得。3.客户端模块完成用户请求时的授权验证与访问拦截。4.令牌权限的验证。登录流程登录流程的三个组成部分:(1)权限的验证1.1 token是否存在的判断1.2 token是否有效的判断2)认证中心的认证界面3)token的生成

March 8, 2019 · 1 min · jiezi

编写chameleon跨端组件的正确姿势(上篇)

在chameleon项目中我们实现一个跨端组件一般有两种思路:使用第三方组件封装与基于chameleon语法统一实现。本篇是编写chameleon跨端组件的正确姿势系列文章的上篇,以封装一个跨端的indexlist组件为例,首先介绍如何优雅的使用第三方库封装跨端组件,然后给出编写chameleon跨端组件的建议。使用chameleon语法统一实现跨端组件请关注文章《编写chameleon跨端组件的正确姿势(下篇)》依靠强大的多态协议,chameleon项目中可以轻松使用各端的第三方组件封装自己的跨端组件库。基于第三方组件可以利用现有生态迅速实现需求,但是却存在很多缺点,例如多端第三方组件本身的功能与样式差异、组件质量得不到保证以及绝大部分组件并不需要通过多态组件差异化实现,这样反而提升了长期的维护成本;使用chameleon语法统一实现则可以完美解决上述问题,并且扩展一个新的端时现有组件可以直接运行。本文的最后也会详细对比一下两种方案的优劣。 因此,建议将通过第三方库实现跨端组件库作为临时方案,从长期维护的角度来讲,建议开发者使用chameleon语法统一实现绝大部分跨端组件,只有一些特别复杂并且已有成熟第三方库或者框架能力暂时不支持的组件,才考虑使用第三方组件封装成对应的跨端组件。由于本文介绍的是使用第三方库封装跨端组件, 因此示例的indexlist组件采用第三方组件封装来实现, 通过chameleon统一实现跨端组件的方法可以看《编写chameleon跨端组件的正确姿势(下篇)》。最终实现的indexlist效果图:前期准备使用各端第三方组件实现chameleon跨端组件需要如下前期准备:项目初始化创建一个新项目 cml-demo cml init project 进入项目cd cml-demo组件设计开发一个模块时我们首先应该根据功能确定其输入与输出,对应到组件开发上来说,就是要确定组件的属性和事件,其中属性表示组件接受的输入,而事件则表示组件在特定时机对外的输出。 为了方便说明,本例暂时实现一个具备基础功能的indexlist。一个indexlist组件至少应该在用户选择某一项时抛出一个onselect事件,传递用户当前所选中项的数据;至少应该接受一个datalist,作为其渲染的数据源,这个datalist应该是一个类似于以下结构的对象数组: const dataList = [ { name: ‘阿里’, pinYin: ‘ali’, py: ‘al’ }, { name: ‘北京’, pinYin: ‘beijing’, py: ‘bj’ }, ….. ]寻找第三方组件库由于本文介绍的是如何使用第三方库封装跨端组件,因此在确定组件需求以及实现思路后去寻找符合要求的第三方库。在开发之前,作者调研了目前较为流行的各端组件库,推荐如下:web端:cube-uivuxmint-uivantwx端:iview weappvant weappweuiweex端:weex-ui除了上述组件库之外,开发者也可以根据自己的实际需求去寻找经过包装之后符合预期的第三方库。截止文章编写时,作者未找到较成熟的支付宝及百度小程序第三方库,因此暂时先实现web、微信小程序以及weex端,这也体现出了使用第三方库扩展跨端组件的局限性:当没有成熟的对应端第三方库时,无法完成该端的组件开发;而使用chameleon语法统一实现.md)则可以解决上述问题,扩展新的端时已有组件能够直接运行,无需额外扩展。 本文在实现indexlist组件时分别使用了cube-ui, iview weapp以及weex-ui, 以下会介绍具体的开发过程.组件开发初始化创建多态组件cml init component选择“多态组件”, 并输入组件名字“indexlist”, 完成组件的创建, 创建之后的组件位于src/components/indexlist文件夹下。接口校验多态组件中的.interface文件利用接口校验语法对组件的属性和事件进行类型定义,保证各端的属性和事件一致。确定了组件的属性与事件之后就开始编写.interface文件, 修改src/components/indexlist/indexlist.interface: type eventDetail = { name: String, pinYin: String, py: String}type arrayItem = { name: String, pinYin: String, py: String}type arr = [arrayItem];interface IndexlistInterface { dataList: arr, onselect(eventDetail: eventDetail): void}具体的interface文件语法可以参考此处, 本文不再赘述。web端组件开发安装cube-ui npm i cube-ui -S在src/components/indexlist/indexlist.web.cml的json文件中引入cube-ui的indexlist组件"base": { “usingComponents”: { “cube-index-list”: “cube-ui/src/components/index-list/index-list” }}修改src/components/indexlist/indexlist.web.cml中的模板代码,引用cube-ui的indexlist组件: <view class=“index-list-wrapper”> <cube-index-list :data=“list” @select=“onItemSelect”/></view>修改src/components/indexlist/indexlist.web.cml中的js代码, 根据cube-ui文档将数据处理成符合其组件预期的结构, 并向上抛出onselect事件:const words = [“A”,“B”,“C”,“D”,“E”,“F”,“G”,“H”,“I”,“J”,“K”,“L”,“M”,“N”,“O”,“P”,“Q”,“R”,“S”,“T”,“U”,“V”,“W”,“X”,“Y”,“Z”];class Indexlist implements IndexlistInterface {props = { dataList: { type: Array, default() { return [] } }}data = { list: [],}methods = { initData() { const cityData = []; words.forEach((item, index) => { cityData[index] = {}; cityData[index].items = []; cityData[index].name = item; }); this.dataList.forEach((item) => { let firstName = item.pinYin.substring(0, 1).toUpperCase(); let index = words.indexOf(firstName); cityData[index].items.push(item) }); this.list = cityData; }, onItemSelect(item) { this.$cmlEmit(‘onselect’, item); }}mounted() { this.initData();}}export default new Indexlist();编写必要的样式: .index-list-wrapper { width: 750cpx; height: 1200cpx;}以上便使用cube-ui完成了web端indexlist组件的开发,效果如下: weex端组件开发安装weex-uinpm i weex-ui -S在src/components/indexlist/indexlist.weex.cml的json文件中引入weex-ui的wxc-indexlist组件:“base”: { “usingComponents”: { “wex-indexlist”: “weex-ui/packages/wxc-indexlist” } }修改src/components/indexlist/indexlist.weex.cml中的模板代码,引用weex-ui的wxc-indexlist组件: <view class=“index-list-wrapper”> <wex-indexlist :normal-list=“list” @wxcIndexlistItemClicked=“onItemSelect” /> </view>修改src/components/indexlist/indexlist.weex.cml中的js代码:class Indexlist implements IndexlistInterface { props = { dataList: { type: Array, default() { return [] } } } data = { list: [], } mounted() { this.initData(); } methods = { initData() { this.list = this.dataList; }, onItemSelect(e) { this.$cmlEmit(‘onselect’, e.item); } }}export default new Indexlist();编写必要样式,此时发现weex端与web端有部分重复样式,因此将样式抽离出来创建indexlist.less,在web端与weex端的cml文件中引入该样式<style lang=“less”> @import ‘./indexlist.less’;</style> indexlist.less文件内容:.index-list-wrapper { width: 750cpx; height: 1200cpx;}以上便使用weex-ui完成了weex端indexlist组件的开发,效果如下: wx端组件编写根据iview weapp文档, 首先到Github下载iview weapp代码,将dist目录拷贝到项目的src目录下,然后在src/components/indexlist/indexlist.wx.cml的json文件中引入iview的index与index-item组件:“base”: { “usingComponents”: { “i-index”:"/iview/index/index", “i-index-item”: “/iview/index-item/index” }},修改src/components/indexlist/indexlist.wx.cml中的模板代码,引用iview的index与index-item组件: <view class=“index-list-wrapper”> <i-index height=“1200rpx” > <i-index-item wx:for="{{cities}}" wx:for-index=“index” wx:key="{{index}}" wx:for-item=“item” name="{{item.key}}" > <view class=“index-list-item” wx:for="{{item.list}}" wx:for-index=“in” wx:key="{{in}}" wx:for-item=“it” c-bind:tap=“onItemSelect(it)” > <text>{{it.name}}</text> </view> </i-index-item> </i-index> </view>修改src/components/indexlist/indexlist.wx.cml中的js代码, 根据iview weapp文档将数据处理成符合其组件预期的结构, 并向上抛出onselect事件:const words = [“A”,“B”,“C”,“D”,“E”,“F”,“G”,“H”,“I”,“J”,“K”,“L”,“M”,“N”,“O”,“P”,“Q”,“R”,“S”,“T”,“U”,“V”,“W”,“X”,“Y”,“Z”];class Indexlist implements IndexlistInterface { props = { dataList: { type: Array, default() { return [] } } } data = { cities: [] } methods = { initData() { let storeCity = new Array(26); words.forEach((item,index)=>{ storeCity[index] = { key: item, list: [] }; }); this.dataList.forEach((item)=>{ let firstName = item.pinYin.substring(0,1).toUpperCase(); let index = words.indexOf(firstName); storeCity[index].list.push(item); }); this.cities = storeCity; }, onItemSelect(item) { this.$cmlEmit(‘onselect’, item); } } mounted() { this.initData(); }}export default new Indexlist();编写必要样式:@import ‘indexlist.less’;.index-list { &-item { height: 90cpx; padding-left: 20cpx; justify-content: center; border-bottom: 1cpx solid #F7F7F7 }}以上便使用iview weapp完成了wx端indexlist组件的开发, 效果如下: 组件使用修改src/pages/index/index.cml文件里面的json配置,引用创建的indexlist组件"base": { “usingComponents”: { “indexlist”: “/components/indexlist/indexlist” }},修改src/pages/index/index.cml文件中的模板部分,引用创建的indexlist组件 <view class=“page-wrapper”> <indexlist dataList="{{dataList}}" c-bind:onselect=“onItemSelect” /> </view>其中dataList是一个对象数组,表示组件要渲染的数据源。具体结构为:const dataList = [ { name: ‘阿里’, pinYin: ‘ali’, py: ‘al’ }, { name: ‘北京’, pinYin: ‘beijing’, py: ‘bj’ }, ….. ]开发总结根据上述例子可以看出,chameleon项目可以轻松结合第三方库封装自己的跨端组件库。使用第三方组件封装跨端组件库的步骤大致如下:跨端组件设计根据实际需求引入合适的第三方组件根据第三方组件文档,将数据处理成符合预期的结构,并在适当时机抛出事件编写必要样式一些思考理解*.[web|wx|weex].cml根据组件多态文档, 像indexlist.web.cml、indexlist.wx.cml与indexlist.weex.cml的这些文件是灰度区, 它们是唯一可以调用下层端能力的CML文件,这里的下层端能力既包含下层端组件,例如在web端和weex端的.vue文件等;也包含下层端的api,例如微信小程序的wx.pageScrollTo等。这一层的存在是为了调用下层端代码,各端具体的逻辑实现应该在下层来实现, 这种规范的好处是显而易见的: 随着业务复杂度的提升,各个下层端维护的功能逐渐变多,其中通用的部分又可以通过普通cml文件抽离出来被统一调用,这样可以保证差异化部分始终是最小集合,灰度区是存粹的;如果将业务逻辑都放在了灰度区,随着功能复杂度的上升,三端通用功能/组件就无法达到合理的抽象,导致灰度层既有相同功能,又有差异化部分,这显然不是开发者愿意看到的场景。在灰度区的模板、逻辑、样式和json文件中分别具有如下规则:模板调用下层组件时,既可以使用chameleon语法,也可以使用各端原生语法;在灰度区chameleon编译器不会编译各个端原生语法,例如v-for,bindtap等。建议在模板部分仍然使用chameleon模板语法,只有在实现对应平台不支持的语法(例如web端v-html等)时才使用原生语法。引用下层全局组件时需要添加origin-前缀,这样可以“告诉”chameleon编译器是在引用下层的原生组件,chameleon编译器就不会对其进行处理了。这种做法同时解决了组件命名冲突问题,例如在微信小程序端引用<origin-button>表示调用小程序原生的button组件而不是chameleon内置的button组件。逻辑在script逻辑代码中,除了编写普通cml逻辑代码之外,开发者还可以使用下层端的全局变量和任意方法,包括生命周期函数。这种机制保证开发者可以灵活扩展各端特有功能,而不需要依赖多态接口。样式既可以使用cmss语法也可以使用下层端的css语法。json文件*web.cml:base.usingComponents可以引入普通cml组件和任意.vue扩展名组件,路径规则见组件配置。*wx.cml:base.usingComponents可以引入普通cml组件和普通微信小程序组件,路径规则见组件配置。*weex.cml:base.usingComponents可以引入普通cml组件和任意.vue扩展名组件,路径规则见组件配置。在各端对应的灰度区文件中均可以根据上述规范使用各端的原生语法,但是为了规范仍然建议使用chameleon体系的语法规则。总体来说,灰度区可以认为是chameleon体系与各端原生组件/方法的衔接点,向下使用各端功能/组件,向上通过多态协议提供各端统一的调用接口。 ...

March 7, 2019 · 2 min · jiezi

编写chameleon跨端组件的正确姿势(下篇)

在chameleon项目中我们实现一个跨端组件一般有两种思路:使用第三方组件封装与基于chameleon语法统一实现。在《编写chameleon跨端组件的正确姿势(上篇)》中, 我们介绍了如何使用第三方库封装跨端组件,但是绝大多数组件并不需要那样差异化实现,绝大多数情况下我们推荐使用chameleon语法统一实现跨端组件。本篇是编写chameleon跨端组件的正确姿势系列文章的下篇,与上篇给出的示例相同,本篇也以封装一个跨端的indexlist组件为例,首先介绍如何使用chameleon语法统一实现一个跨端组件,然后对比两种组件开发方式并给出开发建议。最终效果以下效果依此为weex端、web端、支付宝小程序端、微信小程序端以及百度小程序端: 开发项目初始化创建一个新项目 cml-democml init project 进入项目cd cml-demo组件创建cml init component选择“普通组件”, 并并输入组件名字“indexlist”, 完成组件的创建, 创建之后的组件位于src/components/indexlist文件夹下。组件设计为了方便说明,本例暂时实现一个具备基础功能的indexlist组件。从功能方面讲,indexlist组件主要由两部分组成,主列表区域和索引区域。在用户点击组件右侧索引时,主列表能够快速定位到对应区域;在用户滑动组件主列表时,右侧索引跟随滑动不停切换当前索引项。从输入输出方面讲,组件至少应该在用户选择某一项时抛出一个onselect事件,传递用户当前所选中项的数据;至少应该接受一个datalist,作为其渲染的数据源,这个datalist应该是一个类似于以下结构的对象数组: const dataList = [ { name: ‘阿里’, pinYin: ‘ali’, }, { name: ‘北京’, pinYin: ‘beijing’, }, ….. ]主要数据结构设计根据设计的组件功能与输入输出, 我们开始设计数据结构。 indexlist组件右侧的索引列对应的数据结构为一个数组,其中的每一项表示一个索引,具体结构如下: this.shortcut = [ ‘A’, ‘B’, ‘C’, ….]indexlist组件的主列表区域对应的数据结构也是一个数组,其中的每一项表示一个子列表区域(例如以首字母a开头的子列表)。下面我们考虑每一个子列表区域中至少应该包含的字段:一个name字段,表示该子列表区域的名称;一个items字段,该字段也是一个数组,数组中的每一项表示该子列表区域的每一项;一个offsetTop, 表示该子列表区域距离主列表顶部的距离,通过该字段实现点击右侧索引时能够通过滚动相应距离快速定位到该子列表;一个totalHeight字段,表示该子列表区域的所占的高度,通过该字段与offsetTop字段可以确定每个子列表所在的高度范围, 以此实现右侧索引跟随滑动不停切换当前索引项由上面分析可得主列表区域数据结构如下: this.list = [ { name: “B”, items:[ { name: “北京”, pinYin: “beijing” }, { name: “包头”, pinYin: “baotou” } … ], offsetTop: 190, totalHeight: 490 }, …. ]功能实现从前文可知,输入组件的datalist具有如下结构: const dataList = [ { name: ‘阿里’, pinYin: ‘ali’, }, { name: ‘北京’, pinYin: ‘beijing’, }, ….. ]可以发现该datalist结构是扁平并且缺乏很多信息(例如totalHeight等)的,因此首先要从输入数据中整理出来所需的数据结构,修改src/components/indexlist/indexlist.cml的js部分: initData() { // get shortcut this.dataList.forEach(item => { if (item.pinYin) { let firstName = item.pinYin.substring(0, 1); if (item.pinYin && this.shortcut.indexOf(firstName.toUpperCase()) === -1) { this.shortcut.push(firstName.toUpperCase()); }; }; }); // handle input data const cityData = this.shortcut.map(item => ({items:[], name: item})); this.dataList.forEach((item) => { let firstName = item.pinYin.substring(0, 1).toUpperCase(); let index = this.shortcut.indexOf(firstName); cityData[index].items.push(item); }); // calculate item offsetTop && totalHeight cityData.forEach((item, index) => { let arr = cityData.slice(0, index); item.totalHeight = this.itemNameHeight + item.items.length * this.itemContentHeight; item.offsetTop = arr.reduce((total, cur) => (total + this.itemNameHeight + cur.items.length * this.itemContentHeight), 0); }); this.list = cityData; },这样我们就拿到了主列表数组this.list与索引列表数组this.shortcut, 然后根据数组结构编写模板内容。模板内容分为两大部分,一个是主列表区域,修改src/components/indexlist/indexlist.cml文件模板部分: <scroller height="{{-1}}" class=“index-list-wrapper” scroll-top="{{offsetTop}}" c-bind:onscroll=“handleScroll” > <view c-for="{{list}}" c-for-item=“listitem” class=“index-list-item” > <view class=“index-list-item-name” style="{{compItemNameHeight}}"> <text class=“index-list-item-name-text”>{{listitem.name}}</text> </view> <view c-for="{{listitem.items}}" c-for-item=“subitem” class=“index-list-item-content” style="{{compItemContentHeight}}" c-bind:tap=“handleSelect(subitem)” > <text class=“index-list-item-content-text”> {{subitem.name}}</text> </view> </view> </scroller>其中scroller是一个chameleon提供的内置滚动组件,其属性值scrolltop表示当前滚动的距离,onscroll表示滚动时触发的事件。在主列表这一部分,我们要实现如下功能:在滚动时,右侧索引不停切换当前索引项的功能点击列表中的每一项时,向外抛出onselect事件修改src/components/indexlist/indexlist.cml文件js部分: handleScroll(e) { let { scrollTop } = e.detail; scrollTop = Math.ceil(scrollTop); this.activeIndex = this.list.findIndex(item => scrollTop >= item.offsetTop && scrollTop < item.totalHeight + item.offsetTop ) }, handleSelect(e) { this.$cmlEmit(‘onselect’, e) }当前激活的索引(this.activeIndex)经过计算得到,规则为:如果当前scroller滚动的距离在对应子列表所在的高度范围内,则认为该索引是激活的。另一部分是索引区域,修改src/components/indexlist/indexlist.cml文件模板部分,增加索引区域模板内容: <view class=“short-cut-wrapper” style="{{compScwStyle}}" > <view c-for="{{shortcut}}" class=“short-cut-item” c-bind:tap=“scrollToItem(item)” > <text class=“short-cut-item-text” style="{{activeIndex === index ? ‘color:orange’ : ‘’}}">{{item}}</text> </view> </view>在索引区域,我们要实现点击索引值主列表能够快速定位到对应区域,修改src/components/indexlist/indexlist.cml文件js部分: scrollToItem(shortcut) { let { offsetTop } = this.list.find(item => item.name === shortcut); this.offsetTop = offsetTop; }索引区域应该定位在视窗右侧并且上下居中。由于chameleon暂时不支持在css中使用百分比,因此我们通过chameleon-api提供的对外接口获取屏幕视窗高度,然后使用js计算得到位置, 配合部分css来实现索引区域定位在视窗右侧居中。修改src/components/indexlist/indexlist.cml文件js部分: // computed compScwStyle() { return top:${this.viewportHeight / 2}cpx } // method async getViewportHeight() { let res = await cml.getSystemInfo(); this.viewportHeight = res.viewportHeight; },至此便通过chameleon语法统一实现了一个跨端indexlist组件,该组件直接可以在web、weex、微信小程序、支付宝小程序与百度小程序五个端运行。为了方便描述,上述代码只是简单介绍了组件实现的核心代码,跳过了样式和一些逻辑细节。组件使用修改src/pages/index/index.cml文件里面的json配置,引用创建的indexlist组件"base": { “usingComponents”: { “indexlist”: “/components/indexlist/indexlist” }},修改src/pages/index/index.cml文件中的模板部分,引用创建的indexlist组件 <view class=“page-wrapper”> <indexlist dataList="{{dataList}}" c-bind:onselect=“onItemSelect” /> </view>其中dataList是一个对象数组,表示组件要渲染的数据源一些思考本篇文章主要介绍了如何通过chameleon语法实现跨端组件。对比编写chameleon跨端组件的正确姿势(上篇).md)介绍的通过第三方库封装的方法可以发现,两种方式是完全不同的,现详细对比一下这两种实现方式的优势与劣势, 并给出开发建议: 优势 劣势 开发建议 基于第三方组件库实现 - 可利用已有生态迅速完成跨端组件 - 组件的实现依赖第三方库,如果没有成熟的对应端第三方库则无法完成该端组件开发 - 由于各端第三方组件存在差异,封装的跨端组件样式与功能存在差异 - 第三方组件升级时,要对应调整跨端组件的实现,维护成本较大 - 第三方组件库质量不能得到保证 - 将基于各端第三方组件封装跨端组件库的方法作为临时方案 - 对于特别复杂并且已有成熟第三方库或者框架能力暂时不支持的组件,可以考虑使用第三方组件封装成对应的跨端组件,例如图表组件、地图组件等等 基于chameleon统一实现 - 新的端接入时,能够直接运行 - 一般情况下,不存在各端样式与功能差异 - 绝大部分组件不需要各端差异化实现,使用chameleon语法实现开发与维护成本更低- 能够导出原生组件供多端使用 - 从零搭建时间与技术成本较高 从长期维护的角度来讲,建议使用chameleon生态来统一实现跨端组件库 如果仅仅是各端api层面的不同,建议使用多态接口抹平差异,而不使用多态组件 ...

March 7, 2019 · 2 min · jiezi

重新学习web后端开发-004-了解http响应

一个人必须知道该说什么,一个人必须知道什么时候说,一个人必须知道对谁说,一个人必须知道怎么说。——现代管理之父德鲁克1. http 响应"/hello"接口的响应内容如下:HTTP/1.1 200 OKContent-Type: text/plain; charset=utf-8Date: Tue, 15 Jan 2019 02:56:59 GMTContent-Length: 12hello, world其中,第1行是状态行,第2-4行都是响应头,第5行是一个空行,第6行是响应的消息体。一个http响应包括三个部分:状态行响应头消息体(body)[可选]1.1 状态行HTTP/1.1 200 OK包括三个部分:http版本,这里是1.1状态码,这里是200,代表成功状态码简短描述,这里是ok上面的状态行行,简单理解就是:采用http 1.1协议,向GET /hello请求进行响应,响应状态为成功。这里,我们要重点了解下状态码。1.1.1 状态码客户端处理响应的结果优先根据状态码,其次根据响应头部来进行的。状态吗主要分成5个部分:1xx (Informational response),以1开头的状态码,表明服务端已收到请求,告诉客户端等待请求的结果。2xx (Success),以2开头的状态码,表明服务端已收到并接收了请求。例如常见的200,代表请求被成功处理。3xx (Redirection),以3开头的状态码,表明服务端需要进行额外的处理来完成该请求。很多3xx的状态码被用在url重定向中。4xx (Client errors),以4开头的状态码,表明客户端引起的错误。例如常见的404,表示客户端访问了一个不存在的资源。5xx (Server errors),以5开头的状态码,表明服务端处理请求时发生了错误。例如常见的500错误,表示服务端处理请求时发生了内部错误。状态码包括标准定义的官方状态码,还有非官方的状态码。非官方的状态码主要是由第三方软件系统自己定义的。比如Nginx服务器定义的494,表示客户端请求头数据太多。1.1.1.1 常见的2xx标准状态码状态码描述备注200ok最常见的状态码,表示服务端处理请求成功201Created请求处理成功,同时新建了一个新的资源204No Content请求处理成功,但是不返回任何主体数据(body)205Reset Content与204类似,但是要求客户端重置表单数据1.1.1.2 常见的3xx标准状态码状态码描述备注301Moved Permanently请求永久重定向到给定的url304Not Modified根据请求头If-Modified-Since或If-None-Match判断,资源未发生改变,客户端可以使用之前请求的资源副本。1.1.1.3 常见的4xx标准状态码状态码描述备注400Bad Request客户端请求错误,例如:请求参数不对,数据太大,参数格式错误等401Unauthorized类似403,未通过验证,或者是未提供必须的用户信息,比如用户名和密码等403Forbidden服务端拒绝请求,比如用户没有方法该资源的权限404Not Found访问的资源不存在405Method Not Allowed请求方法不支持。比如,客户端对某个url发起了一个GET请求,而服务端对于该url必须使用POST请求。429Too Many Requests请求数量超过了服务端的限制1.1.1.4 常见的5xx标准状态码状态码描述备注500Internal Server Error服务端内部错误502Bad Gateway服务端器作为网关时或代理时,从上游收到了不正确的响应503Service Unavailable服务暂时不可用。这是一个临时状态。504Gateway Timeout服务端器作为网关时或代理时,从一定的时间内未收到上游的响应1.2 响应头响应头位于状态行之后,与请求头一样,使用key-value的格式,并以CRLF结尾。也就是说每行都是一个键值对。响应头可以包括多个键值对。最后使用一个空行,来表明整个响应头的结束。标准响应头字段有76个,非标准的有十多个。作为后端开发人员,需要了解一些常见的响应头。我们首先介绍下’hello,world’中的响应头,后续出现其它的响应头字段时再一一说明。1.2.1 hello, world 响应头Content-Type: text/plain; charset=utf-8Date: Tue, 15 Jan 2019 02:56:59 GMTContent-Length: 12字段描述例子备注Content-Type媒体类型(MIME type)Content-Type: text/plain; charset=utf-8表示响应的消息体内容格式是纯文本,采用utf-8编码 Date响应时间Date: Tue, 15 Jan 2019 02:56:59 GMT Content-Length消息体(body)内容的长度,以字节为单位Content-Length: 12比如:hello, world正是12个字节长度 1.3 消息体-body部分hello, world该消息体返回了文本"hello, world",其长度为12个字节。在正式的项目中,就是返回该接口对应的数据。通常这些数据都是来自于数据库或其它存储中,根据业务需求,处理后返回给客户端。实际上,作为web后端开发人员,熟悉了解业务需求,可以有助于提高项目质量和降低项目周期风险。在作者带团队时,宁愿多花点时间和成员沟通业务需求和相关流程,在此基础上,后续的开发会变得顺畅多了。2. 小结本节主要介绍了http响应的主要内容,介绍了响应格式,常见的状态码等。下节将介绍前后端分离的开发模式和实践中经常会出现的问题,以及如何处理这些问题的经验和方法。本文为作者原创作品,属于《重新学习web后端开发》专辑中的一篇,转载时请备注作者信息及来源。本文原文地址:https://www.donnyzhang.com/20… ...

March 4, 2019 · 1 min · jiezi

利用lighttpd Web引擎在Ubuntu 16.04系统中搭建网站系统

我们在Linux服务器中搭建建站系统较为多见的是利用Nginx或者是Apache,这个应该是占用大部分网站站长使用的WEB引擎。但是,也有很多网友会选择其他引擎环境的,比如我们熟知的还有Litespeed、Lighttpd,以及其他多种引擎方式。其实这些引擎方式都是可以建站使用的,而且各有优点。比如lighttpd占用资源小,适合在资源不足且需要节省资源的服务器中运行。Lighttpd提供了一个轻量级的Web服务器,它能够在比Apache等服务器使用更少内存的情况下为大型负载提供服务。 在这篇文章中将介绍如何在Ubuntu 16.04上安装和配置lighttpd Web服务器。 如果我们有喜欢的也可以参考使用到生产环境中。利用lighttpd Web引擎在Ubuntu 16.04系统中搭建网站系统第一、升级软件源和系统apt-get update && apt-get upgrade -y如果我们有必要的话可以也将当前服务器别名更换成需要的,一般我们就默认。第二、安装Lighttpd和设置apt-get install lighttpd -y直接执行脚本安装,一旦安装之后我们可以直接在浏览器输入当前服务器的IP地址,可以看到默认的界面。/etc/lighttpd/lighttpd.conf配置文件位于这里,我们可以根据实际需要开启和设置参数。在server.modules里我们可以看到列出的模块,如果是#表示禁止的,我们删除他表示开启。server.max-connections 是并发数的设置,可以调节参数。第三、创建WEB网站环境实例1、创建网站环境lighty-enable-mod simple-vhost2、重启Lighttpd生效systemctl restart lighttpd.service3、修改配置/etc/lighttpd/conf-available/10-simple-vhost.conf在当前文件设置。simple-vhost.server-root = “/var/www/html"simple-vhost.document-root = “htdocs"simple-vhost.default-host = “bandwagonhoster.com"根据需要修改成我们配置网站的参数。网站目录位于/var/www/html中。systemctl restart lighttpd.service配置完毕后重启生效。第四、虚拟机设置过程1、启动lighty-enable-mod evhost2、重启lighttpdsystemctl restart lighttpd.service3、配置文件/etc/lighttpd/conf-available/10-evhost.conf修改配置文件:evhost.path-pattern = “/var/www/html/%0/htdocs/“server.document-root = “/var/www/html/bandwagonhoster.com/htdocs"修改对应网站域名和目录。4、重启生效systemctl restart lighttpd.service第五、创建网站目录既然我们上面配置完毕文件目录后,我们还没有创建网站文件夹,这里来创建。mkdir -p /var/www/html/bandwagonhoster.com/htdocs/根据实际的域名创建,我们如果有多个域名可以一并创建多个。mkdir -p /var/www/html/{example.net/htdocs,example.org/htdocs}我们再创建软连接。ln -s /home/example-user/bandwagonhoster.com/ /var/www/html/bandwagonhoster.com第六、配置FastCGI1、安装Pythonapt-get install python2、安装Rubyapt-get install ruby3、为CGI安装PHP7apt-get install php7.0-cgi4、检查配置文件/etc/lighttpd/conf-enabled/15-fastcgi-php.conf检查:fastcgi.server += ( “.php” =>((“bin-path” => “/usr/bin/php-cgi”,“socket” => “/var/run/lighttpd/php.socket”,“max-procs” => 1,“bin-environment” => (“PHP_FCGI_CHILDREN” => “4”,“PHP_FCGI_MAX_REQUESTS” => “10000”),“bin-copy-environment” => (“PATH”, “SHELL”, “USER”),“broken-scriptfilename” => “enable”)))最后,是不是比较麻烦?如果我们只是建站应用就没有必要这样操作,确实浪费时间也没有必要。我们平时自己使用还是安装网站环境,常规的就可以。本文来自:https://bandwagonhoster.com/6… ...

March 4, 2019 · 1 min · jiezi

WEB网站开发

前端浏览器: 浏览器品牌: Google Chrome , Mozilla Firefox ,Microsoft Edge , Safari,QQ浏览器,360浏览器,IE浏览器。

February 26, 2019 · 1 min · jiezi

某熊的技术之路指北 ☯

某熊的技术之路指北 ☯当我们站在技术之路的原点,未来可能充满了迷茫,也存在着很多不同的可能;我们可能成为 Web/(大)前端/终端工程师、服务端架构工程师、测试/运维/安全工程师等质量保障、可用性保障相关的工程师、大数据/云计算/虚拟化工程师、算法工程师、产品经理等等某个或者某几个角色。某熊的技术之路系列文章/书籍/视频/代码即是笔者蹒跚行进于这条路上的点滴印记,包含了笔者作为程序员的技术视野、知识管理与职业规划,致力于提升开发者的学习能力与实际研发效能。本指北就是对笔者不同领域方面沉淀下的知识仓库的导航与索引,便于读者快速地寻找到自己需要的内容。我们也可以在笔者的个人主页,或者公众号(WIP)中,或者使用 alfred-sg 这样的本地工具进行关键字检索。路漫漫其修远兮,吾正上下而求索,也希望能给所有遇见过笔者痕迹的同学些许帮助,在浩瀚银河间能顺利达到一个又一个彼岸。Just Coder,Travel in Galaxy,欢迎关注某熊的技术之路公众号,让我们一起前行。0.阅读,笔记与编码博观而约取,厚积而薄发。在这个知识爆炸与终身学习/碎片化学习为主的时代,我们面临的问题之一就是如何进行有效学习,不仅能有效平衡广度与深度,并且能真正的积淀下来,提升自己的研发效能。于笔者而言,常常郁结于胸的就是以下三个问题:应该学习什么?这是怎样的一个技术世界?存在着怎样的高峰与路径?如何克服遗忘带来的无效学习?如何不再碎片化地学习?究其根本,也就是需要拓展自己的知识广度,精进自己的知识深度,锤炼自己的编程能力。所谓知识广度,即是为实际问题选择合适的解决方案的能力,广义来说也是眼界与格局的表现。它并不拘泥于某个技术方向或者行业领域,而需要对传统/流行的各类语言、工具、框架、库、服务等有一定的认识;能够明晰各个方案的优劣,并在较高的层次(High Level)描述相关原理。知识广度的拓展与保持需要建立在庞大的阅读量与知识沉淀能力上。Awesome Lists 就为我们准备了精而全的技术开发学习与实践资料索引,去芜存菁,去重留一;譬如其中的 Awesome WebSites 一文就为我们推荐了值得阅读的资讯、博客等站点列表。知识广度的拓展也并非一蹴而就之事,需得循序渐进,从初窥门径,到登堂入室,最后融会贯通,当我们感觉乱花渐欲迷人眼,太多的碎片化知识反而使自己迷失方向之际,就可以前往 Awesome CS Books Warehouse,去深入地阅读学习各个领域的精选书籍、课程等系统化的内容。俗话说,好记性不如烂笔头,当我们阅读的多了,自然也要开始记录;而笔者认为记录的开始就要有自己的知识体系。在自己的知识体系下随看随记、定期整理。唯有建立符合自己认知方式的知识图谱,才能有效地沉淀知识,明晰知识边界并进行不断地探索。上车伊始,笔者即致力于构建自己的 MindMap, IT 技术图谱与知识架构,提供了软件工程通用、前端、后端、DevOps、测试、架构师、人工智能工程师等多领域的知识图谱、学习成长路线与面试必备内容,并在数年来不断维护与刷新。笔者目前选择的是以 MarkDown 格式记录,并且将所有的笔记存放于 Github-文档札记以 Git 方式进行版本管理;编辑器是直接使用的 VSCode,移动端编辑的话也是用的 GitGo/WorkCopy 这样的 Git 应用。这些笔记即是笔者自身技术视野与认知的外化,也类比于外设之于内存,在需要的时候分页加载到脑海中使用,以应对这知识爆炸的时代。其中的典型代表,Awesome CheatSheets,对于日常开发中用到的相关知识的备忘录/清单进行总结, 适合快速掌握或者回顾某个语言/框架/工具的语法或使用要点。Tech Road, 我的技术之路是对于笔者多年学习与认知变迁的总结。先贤有云,知行合一,知是行之始,行是知之成,Linus Torvalds 也曾提到: ‘Talk is cheap. Show me the code.’,在阅读与笔记之后,就是要开始实践编码。所谓编程能力,并不仅仅是编写代码(Write Code)的能力,而是包含了阅读、编写、理解、重构、抽象等多个方面,是所谓的代码管理/掌控。其外在表现之一即是能够随时随地用合适的语言无阻塞地实现某些功能需求,对于常见的语法,接口,算法,设计模式等能够做到心随意动,信手拈来。编程能力是提升研发效能的重要保障,于笔者而言也是毕生应该追求的目标与爱好之一。笔者的编程能力较弱,日常开发,特别是在多语言多框架并用的场景下,往往会需要不断地中断,查找以继续工作,也是令我颇为苦恼。前文重在讨论如何拓宽技术视野、追寻技术的广度,但是需要铭记的是,技术深度才是技术广度的基石,正如中国自古以来常用道术之辩,知其然,也要知其所以然;亦如 Richard Feynman 所述:”What I cannot create, I do not understand.”。所谓知识深度,即是能够对某个方面做到深入了解,并且达到融会贯通,洞若观火,可以随心所欲地加以扩展、优化、创新等改造或变换。这方面则更加的见仁见智,不同的领域与方向对于深度的定义与挖掘方向也是千差万别。我们需要自己去从零开始造些轮子,才能深刻理解使用的框架/库/平台的内部原理,才能在碰到故障时快速地修复;在下文的几乎每个章节中,我们都会提到某些笔者自造的轮子。1.编程语言编程语言是一切的基础,正如 Steve McConnell 在 Code Complete 一书中提及,我们应该 Program into a language 而不是 Program in a language,针对不同的需要选择合适的编程语言来实现,而不是受制于自己所会的语言。在知识图谱中也包含了跨编程语言的公共知识杂谈,笔者与编程语言相关的文章存放在 Programming Language Series | 编程语言语法基础与工程实践仓库中,其涵盖了 C/C++、Go、Java、JavaScript、Python、Rust、Swift 等常见的语言,以及通用的编程语言理论。编程能力锻炼的基础,首要的就是关于数据结构与算法,以及面向对象的设计模式,其对应的代码分别存放在了 coding-snippets, algorithm-snippets, design-pattern-snippets 中。此外,我们还可以从零编写一些类似于 Guava & Lodash 这样的自己的通用工具库,笔者自身是整合在了 Guash 中。在编程语言之上,我们就需要考虑如何去实现真正的软件系统,譬如 软件工程基础 系列中的开发工具/Git 漫谈、软件系统架构、软件质量保障等内容,我们也可以自己去实现一些自己的工具,譬如笔者的 Soogle 是构建自身搜索、外部服务访问能力的工具集合;而 xCompass 是包含个人主页在内的多端阅读能力的源代码仓库。2.Web 与大前端工程师如果您对于 JavaScript 基础语法尚不完全了解,那么建议您首先浏览现代 JavaScript 语法基础与工程实践或者 JavaScript-CheatSheet 以了解基础的 JavaScript 语法及实践应用。如果您想快速地了解 Web 开发实践,或者是想查阅某些清单,那么建议您前往 Awesome-CheatSheets/Web;或者从导论篇开始阅读,它会包含 Web 开发简史与变迁、数据流驱动的界面、模块化与组件化、工具化与工程化、前后端分离与全栈架构、微前端与大前端、运行机制与性能优化等内容。接下来,您可以选择以下章节中感兴趣的模块进行深度阅读:基础篇: 对于 HTML、CSS、DOM 等 Web 开发中涉及的基础知识与理念的总结介绍。工程实践篇: 构建工具,测试,安全,WebAssembly。架构优化篇: 组件化,状态管理,性能优化,PWA。React 篇:近年来前端领域百花齐放,各种技术方案争妍斗艳,各领风骚。本书立足于其中的佼佼者 React,深入浅出的介绍 React, Webpack, ES6, Redux, MobX 等常见前端开发工具与开发库的用法,帮助初学者能够迅速成为一名合格前端工程师。而本书也不仅局限于工具使用的层面,探寻各种技术方案背后蕴含的设计思想与架构模式,从前端工程化的角度讨论前端开发者在进阶过程中需要掌握的工程实践、模块化与组件化、质量保障、性能优化等知识要点。最终帮助开发者在前端开发中能够因地制宜的指定合理方案,以尽可能快的速度实现可信赖的产品。在阅读之外,我们同样需要进行大量的代码实践,不仅仅是熟悉常用的框架,还需要去积累自己的组件、框架等功能库:fe-boilerplates 是笔者对于日常工作中的基于 React/Vue.js 技术栈与实践的收集与沉淀;为了方便不同级别/熟练程度的开发者使用,笔者将模板尽可能地泛化为多个项目,包含了从入门级到生产环境,微前端等多个不同层次/复杂度的模板项目。fractal-components 则是笔者日常工作中总结出来的应用、组件库以及组件开发模式,为了保证其独立性与复用性,笔者以不同的方式实现了组件。Ueact 旨在从零开始实现自定义的组件系统,多调和策略与数据流响应方式,同时能够被渲染/编译到多种组件。Legoble 则承载了自己实现一款可视化的应用构建工具的念想。Pudding 是有关于 Web Automation、多维度记录、回放、优化工具等集合。3.服务端架构工程师这是全栈的时代,我们更多地以业务来划分而非单纯地前后端,Backend Series | 服务端应用程序开发与系统架构/微服务架构与实践承载了笔者在服务端的总结与经验,其包含了服务端应用程序开发基础,深入浅出 Node.js 全栈架构,Spring Boot 5 与 Spring Cloud 微服务实践等内容。Backend-Boilerplates is Boilerplate for Your Server Side(Backend) Application, Java | Spring(Boot, Cloud) | Node.js(Express, Koa, Egg) | Go | Python | DevOps.winter-boot is Another boot for your Java applications like Spring Boot, but Winter is coming.4.测试/运维/安全工程师软件系统的质量保障是服务端运维不可绕过的部分,其包含了软件测试基础以及 DevOps 与 SRE 实战,信息安全与渗透测试必知必会等相关内容。在实践方面,我们还可以参考:Chaos-Scanner 混沌守望者(扫描器),半自动化分布式智能网络空间测绘、管理与安全探测。xe-crawler 是遵循声明式、可监测理念的分布式爬虫,其计划提供 Node.js、Go、Python 多种实现,能够对于静态 Web 页面、动态 Web 页面、关系型数据库、操作系统等异构多源数据进行抓取。5.大数据/云计算/虚拟化工程师前文讨论的更多是应用层的知识,而对于更底层的操作系统、数据库、大数据处理等分布式基础架构相关内容,都存放在了 Distributed-Infrastructure-Series | 深入浅出分布式基础架构系列中,主要包含分布式计算、分布式系统、数据存储、虚拟化、网络、操作系统等几个部分。如上文所述,我们需要在重构轮子中成长:Reinvent-MQ 即是 Multiple home-made Message Queues, LocalMQ(akin RocketMQ), PongoMQ(akin Kafka), etc.Reinvent-DB 即是 Multiple home-made Databases, Godis(akin Redis), HiSQL(akin MySQL), MemDB, DataGo(akin ETL) etc. Understanding DBs by Reinventing It.Focker 是从零开始自定义的类 Docker 简化版容器实现。SparkChain 即是在区块链方面的实验探索的积累。6.算法工程师前文我们讨论过数据结构与算法的相关内容,而在人工智能火热的现在,AIDL-Series | 人工智能与深度学习实战系列包含了数学原理篇、机器学习篇、深度学习篇、自然语言篇、工程实践篇、人工智能与深度学习课程篇等内容。在实践方面,代码主要存放于 Artificial Intelligence & Deep Learning Workbench 中。7.产品经理笔者选择了产品经理作为压轴之篇,也是希望能表述自己关于产品的观点,无论是我们创造的库、框架、应用还是平台,乃至于我们的文章、整理的系列书籍,都当以产品视之,跳出上帝视角,从用户的角度去考量。我们首先可以阅读些产品经理/用户体验方面的书籍。笔者目前积累不多,主要在 Product Series | 产品迷思中,其首先会关注产品经理的基础素养、用户交互体验、文档处理等方面。其次会讨论有关于项目管理、通用的领域能力构建(流程引擎、CRM 等)以及对于经典产品的分析。最后,该系列还会关注于具体的行业观点,譬如电子商务、智能制造等。很多时候,自己动手做些小产品也是有趣的事情,譬如 MushiChat 这样的聊天平台与聊天机器人、IoTable 这样在 IoT 领域的一些探索。 ...

February 23, 2019 · 2 min · jiezi

前端项目如何管理

前端项目如何管理前端项目的管理分为两个维度:项目内的管理与多项目之间的管理。1. 项目内的管理在一个项目内,当有多个开发者一起协作开发时,或者功能越来越多、项目越来越庞大时,保证项目井然有序的进行是相当重要的。一般会从下面几点来考证一个项目是否管理得很好:可扩展性:能够很方便、清晰的扩展一个页面、组件、模块组件化:多个页面之间共用的大块代码可以独立成组件,多个页面、组件之间共用的小块代码可以独立成公共模块可阅读性:阅读性良好(包括目录文件结构、代码结构),能够很快捷的找到某个页面、组件的文件,也能快捷的看出项目有哪些页面、组件可移植性:能够轻松的对项目架构进行升级,或移植某些页面、组件、模块到其他项目可重构性:对某个页面、组件、模块进行重构时,能够保证在重构之后功能不会改变、不会产生新 bug开发友好:开发者在开发某一个功能时,能够有比较好的体验(不好的体验比如:多个文件相隔很远)协作性:多人协作时,很少产生代码冲突、文件覆盖等问题可交接性:当有人要离开项目时,交接给其他人是很方便的1.1 可扩展性对于前端项目而言,可扩展性是并不难的,因为很多时候前端的代码、文件分块都是按照页面来的,所以天然就是一块一块的。但这里还是要提一下,因为有些开发者不喜欢分块,把应该分块的东西杂揉在一起,比如:- src/ - main/ # main 目录 - css/ # css 集合 - alpha.css - beta.css - … - js/ # js 集合 - alpha.js - beta.js - … - alpha.html # alpha 页面 - beta.html # beta 页面 - …更好的方式:- src/ - main/ # main 目录 - alpha/ # alpha 页面 - index.css # css 入口文件 - index.js # js 入口文件 - index.html # html 入口文件 - … - beta/ # beta 页面 - index.css - index.js - index.html - … - …使前端项目具有高可扩展性,一般从目录文件结构入手,可以参考:目录结构优化。1.2 组件化这里的组件化是项目内的组件化,我们可以把多个页面之间共用的大块代码独立成组件,多个页面、组件之间共用的小块代码独立成公共模块。这样做的目的是为了提高代码的可重用性,避免重复造轮子。另外,也有利于代码之间的解耦。比如:- src/ - data/ # 常量、静态数据目录 - data1.js - data2.js - … - components/ # 组件目录 - componnet1/ - componnet2/ - … - utils/ # 工具函数目录 - util1.js - util2.js - … - … 可以参考:组件化。1.3 可阅读性这里的可阅读性有两个方面:目录文件结构、代码结构。1.3.1 目录文件结构目录文件结构可阅读性的好与否除了跟开发者有关系外,跟项目的搭建者也有很大的关系,因为如果搭建者在最初就定义好整个项目的目录结构,对后期的开发者是一个很好的约束。可阅读性比较差的目录文件结构:- src/ - css/ # css 集合 - main/ # main 目录 - alpha.css - beta.css - … - js/ # js 集合 - main/ # main 目录 - alpha.js - beta.js - … - html/ # html 集合 - main/ # main 目录 - alpha.html # alpha 页面 - beta.html # beta 页面 - …可阅读性比较好的目录文件结构:- src/ - main/ # main 目录 - alpha/ # alpha 页面 - index.css # css 入口文件 - index.js # js 入口文件 - index.html # html 入口文件 - … - beta/ # beta 页面 - index.css - index.js - index.html - … - …关于目录文件结构的高可读性,可以参考:目录结构优化。1.3.2 代码结构代码结构的可阅读性大部分取决于开发者的水平,但我们可以使用工具帮助开发者书写规范、格式良好的代码。主要有下面的工具:.editorconfig: 统一每个开发人员的编辑器配置eslint: 检查 js 语法(包括 jsx 语法),然后最大程度的矫正不符合规范的代码stylelint: 检查 css 语法(包括 less, scss 语法),然后最大程度的矫正不符合规范的代码prettier: 代码格式优化husky + lint-staged: 强制开发人员对代码进行检查、自动矫正与优化上面的具体用法可以参考:怎样提升代码质量搭建自己的前端脚手架1.4 可移植性可能的情况下,让项目具有一定的伸缩性,可以在未来轻松的对项目进行架构升级。让项目能够轻松的移植某些页面、组件、模块到其他项目,需要对整个项目代码尽量的解耦与模块化。另外,也与后面会讲到的“项目之间的统一性”有关。1.5 可重构性对页面、组件的重构是常有的事,但怎样保证在重构之后功能不会改变、不会产生新 bug,这就得靠测试用例了。js 模块:jest / mocha / tape / avaReact 组件:enzyme + jest,另外可以使用 react-testing-library 代替 react-dom/test-utilsVue 组件:vue-test-utils + jest / mocha / tape / ava可以参考:react 前端项目技术选型、开发工具、周边生态vue 前端项目技术选型、开发工具、周边生态1.6 开发友好这主要是从目录结构优化着手,比如:像下面这种目录结构,如果要编辑一个页面,需要到处找页面相关的文件,编辑器上就会形成一个很长的目录树,一点不友好:- src/ - css/ # css 集合 - main/ # main 目录 - alpha.css - beta.css - … # 中间有 30 个页面 - js/ # js 集合 - main/ # main 目录 - alpha.js - beta.js - … # 中间有 30 个页面 - html/ # html 集合 - main/ # main 目录 - alpha.html # alpha 页面 - beta.html # beta 页面 - … # 中间有 30 个页面而像下面这种目录结构,所有的文件都在一个目录下,找文件就很方便,而且很清晰:- src/ - main/ # main 目录 - alpha/ # alpha 页面 - index.css # css 入口文件 - index.js # js 入口文件 - index.html # html 入口文件 - … - beta/ # beta 页面 - index.css - index.js - index.html - … - …1.7 协作性当项目变大、多人协作时,我们就需要管理好哪些是正在开发的代码、哪些是提交测试的代码、哪些是已经上线的代码、如何避免代码冲突与线上新代码被旧代码覆盖等等。具体可以参考:web 项目如何进行 git 多人协作开发。1.8 可交接性当有人要离开项目时,就需要把他负责的代码交接给别人,但怎么样才能使交接是轻松愉快的?那就是文档,包括注释文档、接口文档等。想想,如果没有文档,该怎样交接呢?可以参考:api 接口管理工具。2. 多项目之间的管理多个项目之间,如何管理好项目之间联系,比如共用组件、公共模块等,保证快捷高效开发、不重复造轮子,也是很重要的。一般会从下面几点来考证多个项目之间是否管理得很好:组件化:多个项目共用的代码应当独立出来,成为一个单独的组件项目版本化:组件项目与应用项目都应当版本化管理,特别是组件项目的版本应当符合 semver 语义化版本规范统一性:多个项目之间应当使用相同的技术选型、UI 框架、脚手架、开发工具、构建工具、测试库、目录规范、代码规范等,相同功能应指定使用固定某一个库文档化:组件项目一定需要相关的文档,应用项目在必要的时候也要形成相应的文档2.1 组件化这里的组件化是项目之间的组件化,我们可以把多个项目共用的代码独立出来,成为一个单独的组件项目。这样做的目的也是为了提高代码的可重用性,避免重复造轮子。另外,也便于版本化管理组件。- project1/ # 项目一 - package.json - src/ - … - project2/ # 项目二 - package.json - src/ - … - component1/ # 组件一 - package.json - src/ - dist/ - … - component2/ # 组件二 - package.json - src/ - dist/ - … 在 project1 中使用 component1、component2:# package.json{ “dependencies”: { “component1”: “^0.0.1”, “component2”: “^0.0.1” }}import component1 from ‘component1’;import component2 from ‘component2’;常用组件有:@yourCompany/utils: 工具类@yourCompany/shortcut.css: 快捷 css 类@yourCompany/data: 常用静态数据…组件化一般会与私有 npm 仓库一起使用。可以参考:组件化私有 npm 仓库2.2 版本化如果应用项目使用 npm 来管理依赖,就是版本化管理了。组件项目更不用说了,值得提一下的是组件项目的版本号应当符合 semver 语义化版本规范。版本格式:主版本号.次版本号.修订号,版本号递增规则如下:主版本号:当你做了不兼容的 API 修改,次版本号:当你做了向下兼容的功能性新增,修订号:当你做了向下兼容的问题修正。先行版本号及版本编译元数据可以加到“主版本号.次版本号.修订号”的后面,作为延伸。可以参考:从 1 到完美,写一个 js 库、node 库、前端组件库从 0 到 1 再到 100, 搭建、编写、构建一个前端项目2.3 统一性多个项目之间应当使用相同的技术选型、UI 框架、脚手架、开发工具、构建工具、测试库、目录规范、代码规范等,相同功能应指定使用固定某一个库。这样做的目的是减少项目之间的环境差异,有利于项目之间的代码移植,也更有利于组件化、代码重用。可以参考:react 前端项目技术选型、开发工具、周边生态vue 前端项目技术选型、开发工具、周边生态搭建自己的前端脚手架通用、封装、简化 webpack 配置2.4 文档化完善的文档,不管是对组件项目还是应用项目,都是很重要的。组件项目需要用文档告诉开发者组件怎么用、有哪些接口;应用项目需要用文档来做小组交流、项目交接等。后续更多博客,查看 https://github.com/senntyou/blogs作者:深予之 (@senntyou)版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证) ...

February 19, 2019 · 3 min · jiezi

自底向上的web数据操作指南

简介本篇文章主要探讨JavaScript中的数据操作.JavaScript一直以来给人一种比较低能的感觉,例如无法读取系统上的文件,不能做一些底层的操作.所以在页面上操作数据会交由服务器处理也就成了主流的做法.但是很多人没有发现,实际上JavaScript以及在逐步增强这些功能,现在我们就已经可以放心的在web端进行文件操作了.起因N个月前我去新浪面试实习,我提到了原来我做过一个页面配合上传Excel可以完成一些功能.我的这番话勾起了面试官在实际编码中遇到了一些问题,就是如何不通过服务器来操作数据,我和她讨论了一番,最后不了了之了(当然也没过).N个月后实习被坑,成了无业游民闲来无事正好也好奇这个问题然后就研究了一下.涉及的内容没有非必要的内容,对于文件操作来说以下API都是必须了解的,本文也会渐进式的讨论这些内容.BlobArrayBufferTypedArrayDataViewFileReaderFileURL兼容性我没有详细考证API的兼容性,不过从MDN提供的数据来看IE10以上的浏览器大部分都是兼容的.总览一般来说操作一个文件都要经历如下的步骤:知道文件的地址(存放的位置)读取保存到Buffer中,重复上步骤直至结束进行数据编辑知道要写入的地址获取要写入的数据,从Buffer中获取还是所有数据写入写入完成API名称以及对应的职责:名称职责URL制造文件地址FileReader读取文件的接口Blob用于在JavaScript表示文件File用于表示文件对象ArrayBuffer表示Buffer(仅仅提供一片内存空间)TypedArray基于数组操作Buffer上的数据(操作的最小单位是数组元素)DataView基于字节操作Buffer上的数据上面描述的内容之间的关系很复杂,这里我们逐步来进行分析.ArrayBufferhttps://developer.mozilla.org…ArrayBuffer对象用于表示一段缓冲区域(可以理解为一段可控的内存区域),它仅仅表示这片被开辟的区域但是不提供操作方式.const arraybuffer = new ArrayBuffer(8) // 创建一个长度为8字节大小的Buffer默认ArrayBuffer中每一个字节都被填充了0.利用这个对象我们可以完成如下的操作:获取该Buffer的大小(字节)该Buffer的副本(范围)修改该Buffer的大小判断给定的数据是否是操作视图(实例方法)异常当创建的Buffer长度超过Number.MAX_SAFE_INTEGER的大小会产生错误const arraybuffer = new ArrayBuffer(8);console.log(arraybuffer.byteLength); // 获取长度console.log(arraybuffer.slice(4,8)); // 获取副本// 截止到2019年2月12日 20:11:05没有浏览器实现该功能console.log(arraybuffer.transfer(arraybuffer,16));// 修改原有Bufferconsole.log(ArrayBuffer.isView({})) // false 是否是视图DataViewhttps://developer.mozilla.org…DataView用于操作ArrayBuffer中的数据,这也是它构造函数中接受一个ArrayBuffer的原因:const arraybuffer = new ArrayBuffer(8);const dataview = new DataView(arraybuffer); // 默认的视图大小就是buffer的大小const offset = new DataView(arraybuffer, 0, arraybuffer.byteLength); // 默认的偏移量以及长度利用这个对象我们可以完成如下的操作:获取被该视图引入的Buffer(只读)该视图从Buffer中读取的自己长度(只读)该视图从Buffer中读取的偏移量(只读)异常如果由偏移(byteOffset)和字节长度(byteLength)计算得到的结束位置超出了 buffer 的长度.写入使用xxx类型写入(见下方)读取使用xxx类型读取可以使用的类型:类型名称对应的方法Int8getInt8,setInt8Uint8getUint8,setUint8Int16getInt16,setInt16Uint16getUint16,setUint16Int32getInt32,setInt32Uint32getUint32,setUint32Float32getFloat32,setFloat32Float64getFloat64,setFloat64简单实例: const arraybuffer = new ArrayBuffer(1); // 一个字节 const dataview = new DataView(arraybuffer); // 默认的视图大小就是buffer的大小 dataview.setInt8(0,127) // 从0开始写入一个int8(8位无符号整形,一个字节) dataview.getInt8(0) // 从偏移0开始读取一个int8(8位无符号整形,一个字节) console.log(dataview.getInt8(0)); dataview.setInt16(0,65535); // 错误超出了ArrayBuffer的空间 int16占两个字节字节序简单来讲-使用DataView:在读写时不用考虑平台字节序问题。https://developer.mozilla.org…https://zh.wikipedia.org/wiki…可以利用这个函数来进行判断:var littleEndian = (function() { var buffer = new ArrayBuffer(2); new DataView(buffer).setInt16(0, 256, true /* 设置值时使用小端字节序 /); // Int16Array 使用系统字节序,由此可以判断系统是否是小端字节序 return new Int16Array(buffer)[0] === 256;})();console.log(littleEndian); // true or falseTypedArrayhttps://developer.mozilla.org…在上面一节中我们使用get和set的方式基于数据类型来读写内存(ArrayBuffer)中的数据.而所谓的TypedArray就是使用类似于操作数组的方式来操作我们的Buffer可以理解为数组中的每一个元素都是不同类型的数据,这样一来我们可以使用数组上的很多方法,相较于干巴巴的使用get和set更加灵活一些,少掉点头发.名字叫做TypedArray的这个对象或者全局构造函数并不存在于JavaScript中.因为类型数组并不只有一个,但是TypedArray代指的这些内容拥有统一的构造函数,统一的属性统一的方法,不同的只是他们的名字以及所对应的数据类型.TypedArray()指的是以下的其中之一: Int8Array(); Uint8Array(); Uint8ClampedArray();Int16Array(); Uint16Array();Int32Array(); Uint32Array(); Float32Array(); Float64Array();看到这里我们立马联想到了之前DataView上不同的Get和Set,概念是一样的,不同于ArrayBuffer的是,这里的最小数据单位是数组中的元素,不同类型元素所占用的空间是不同的,但是我们不需要考虑在字节层面上进行控制.接下来我们利用Int8Array来进行讨论:构造函数传入一个数值来表示类型数组中元素的数量传入任意一个类型数组在保留其原有的长度上进行数据类型转换方法(静态)Int8Array.from()通过可迭代对象创建一个类型数组Int8Array.of()通过可变参数创建一个类型数组例子:// 32无符号能表示的最大的数值 占4个字节const int32 = new Int32Array(1); // 使用lengthint32[0] = 4294967295;// 8位无符号能表示最大的内容是127 占1个字节const int8 = new Int8Array(int32); // 使用另外一个类型数组console.log(int8[0]) // -1 32位转8位要确保,32位的值在8位的范围内否则无法保证精度const from = Int8Array.from([0,127]);console.log(from.length === 2) // trueconst of = Int8Array.of(0,127);console.log(of.length === 2)// true属性(静态)TypedArray.BYTES_PER_ELEMENTTypedArray.lengthTypedArray.nameget TypedArray[@@species]TypedArray.prototype属性(实例)TypedArray.prototype.bufferTypedArray.prototype.byteLengthTypedArray.prototype.byteOffsetTypedArray.prototype.length方法(实例)方法是在是太多了Array上的方法TypedArray基本都有,例举太多都是照搬MDN,给个贴上大家自行查阅吧.方法列表例子(类数组操作):const int8 = new Int8Array(2);int8[0] = 0;int8[1] = 127;int8.forEach((value)=>console.log(value));for (const elem of int8) { console.log(elem);}Array.isArray(int8) // false 类数组而不是真的数组Blobhttps://developer.mozilla.org…Blob` 对象表示一个不可变、原始数据的类文件对象。Blob 表示的不一定是JavaScript原生格式的数据这说明了什么意思,类似于ArrayBuffer一样,ArrayBuffer本身没有为了达到某种目的而提供具体的操作方法,他的存在就类似于一个占位符一样,Blob对象也是类似的概念,在JavaScript中我们使用Blob对象来表示一个文件,当这个文件需要进行操作的时候我们在利用其他途径对这个Blob对象进行操作.(个人理解)Blob的API和ArrayBuffer非常相似,因为他们有着非常密切的联系,创建Blob对象有两种方式,对应着两种具体的需求:直接调用构造函数传入JavaScript中的数据结构使用File对象创建,用于表示文件这里我们不讨论由File对象创建的情况,这部分留到下节中讨论.构造函数你可以利用现有的JavaScript数据结构来创建一个Blob对象你可以选择这个Blob对象的MIME类型你可以控制这个Blob对象中的换行符在系统中表现的行为具体参考属性(实例)size - Blob对象所包含的数据大小type - Blob对象所描述的MIME类型方法(实例)slice()类似于ArrayBuffer.slice()从原有的Blob中分离出一部分组成新的Blob对象例子: const blob1 = new Blob([JSON.stringify({ content: ‘success’ })], { type: ‘application/json’ }); const blob2 = new Blob([’<a id=“a”><b id=“b”>hey!</b></a>’],{ type:’text/html’ });注意:Blob对象接受的第一个参数是一个数组.Blob对象还可以根据其他数据结构进行创建:ArrayBufferArrayBufferView(TypedArray)Blobhttps://developer.mozilla.org…乍一看Blob对象看似很鸡肋,不过在JavaScript中能装载数据还可以指定MIME类型,这种情况多半都是用于和外部进行交互.回顾前面的内容,我们知道了如何创建一片内存中的区域,还知道了如何利用不同的工具来对这篇内存进行操作,最重要的一个用于描述文件Blob对象接受ArrayBuffer和TypedArray,那么还能玩出什么花样呢?File文件(File)接口提供有关文件的信息,并允许网页中的 JavaScript 访问其内容。https://developer.mozilla.org…File对象用于描述文件,这个对象虽然可以利用构造函数自行创建,但是大多数情况下都是利用浏览器上的<input>元素或者拖拽API来获取的.File对象继承Blob对象,所以继承了Blob对象上的原型方法和属性,和Blob纯粹表示文件不同,File更加接地气一点,他还拥有了我们操作系统上常见的一些特征:属性(实例)lastModified 最后修改时间name 文件名称size 文件大小type MIME类型详细介绍构造函数详细介绍例子: // 创建buffer const buffer = new Int8Array(2); console.log(buffer.byteLength); // 2 buffer[0] = 0; buffer[1] = 127 console.log(buffer[0]); // 127 // 利用buffer创建一个file对象 const file = new File([buffer],’text.txt’,{ type:’text/plain’, lastModified:Date.now() }); // file继承blob所以可以使用slice方法,返回一个blob对象 const blob = file.slice(1,2,’text/plain’); console.log(blob.size); //1File对象目前看来依然扮演者’载体’的角色,不过在将他交由其他的API的时候才是他真正发挥威力的地方.FileReaderFileReader一看名字我就有一种想喊JavaScript(浏览器端)永不为奴的冲动.前面铺垫了那么多终于可以看到真正可以实际利用的内容了.FileReader 对象允许Web应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 File 或 Blob 对象指定要读取的文件或数据。https://developer.mozilla.org…FileReader和前面的所提到的内容不同的地方在于,这个API有事件,你可以使用onXXX和addEventListener进行监听.基本工作流程:获取用户提供的文件对象(通过input或者拖拽)或者自己创建File或者(Blob)对象新建一个FileReader()实例监听对应的方法来获取读取内容完成后的回调利用不同的方法读取文件内容读取为fileReader.ArrayBuffer()读取为DataURLfileReader.readAsDataURL()读取为字符串fileReader.readAsText()示例1读取计算机上的文件:<!DOCTYPE html><html><head> <meta charset=“utf-8”> <meta http-equiv=“X-UA-Compatible” content=“IE=edge”> <title>blob</title> <meta name=“viewport” content=“width=device-width, initial-scale=1”></head><body> <!– 建议选中一个文本 –> <label for=“file”>读取文件<input id=“file” type=“file” ></label> <script type=“text/javascript”> document.getElementById(‘file’).addEventListener(‘change’,(event)=>{ const files = event.srcElement.files; if(files.length === 0){ return console.log(‘没有选择任何内容’); } const file = files[0]; console.log(file instanceof File); // true console.log(file instanceof Blob); // true const reader = new FileReader(); reader.addEventListener(‘abort’,()=>console.log(‘读取中断时候触发’)); reader.addEventListener(’error’,()=>console.log(‘读取错误时候触发’)); reader.addEventListener(’loadstart’,()=>console.log(‘开始读取的时候触发’)); reader.addEventListener(’loadend’,()=>console.log(‘读取结束触发’)); reader.addEventListener(‘progress’,()=>console.log(‘读取过程中触发’)); // 当内容读取完成后再load事件触发 reader.addEventListener(’load’,(event)=>{ // 输出文本文件的内容 console.log(event.target.result) }); // 读取一个文本文件 reader.readAsText(file); }); </script></body></html>如果一切顺利,你就可以从计算机上读取一个文件,并且以文本的形式展现在了控制台中.而且不仅如此,利用:reader.readAsArrayBuffer(file)我们可以读取任何类型的数据,然后再内存中进行修改,剩下的就差保存了.FileReaderSync这个API是FileReader的同步版本,这意味着代码执行到读取的时候会等待文件的读取,所以这个API只能在workers里面使用,如果在主线程中调用它会阻塞用户界面的执行.由于是同步读取,所以没有回调掉必要存在,也就不需要监听事件了.https://developer.mozilla.org…URL前面我们讨论完成了数据的读取,在FileReader中我们已经可以获取ArrayBuffer然后使用DateView和TypedArray就可以修改ArrayBuffer完成文件的修改,接下来我们旅行中的最后一程.https://developer.mozilla.org…在JavaScript(浏览器端)中我们可以使用URL来创建一个URL对象:new URL(‘https://www.xxx.com?q=10’)他返回的对象包含如下的内容:// 控制台new URL(‘https://www.xxx.com?q=10’)URLhash: ““host: “www.xxx.com"hostname: “www.xxx.com"href: “https://www.xxx.com/?q=10"origin: “https://www.xxx.com"password: ““pathname: “/“port: ““protocol: “https:“search: “?q=10"searchParams: URLSearchParams { }username: ““可见该对象是一个工具对象用于帮助我们更加容易的处理URL.例子(来自MDN):var a = new URL(”/”, “https://developer.mozilla.org”); // Creates a URL pointing to ‘https://developer.mozilla.org/‘var b = new URL(“https://developer.mozilla.org”); // Creates a URL pointing to ‘https://developer.mozilla.org’var c = new URL(’en-US/docs’, b); // Creates a URL pointing to ‘https://developer.mozilla.org/en-US/docs’var d = new URL(’/en-US/docs’, b); // Creates a URL pointing to ‘https://developer.mozilla.org/en-US/docs’var f = new URL(’/en-US/docs’, d); // Creates a URL pointing to ‘https://developer.mozilla.org/en-US/docs’var g = new URL(’/en-US/docs’, “https://developer.mozilla.org/fr-FR/toto”); // Creates a URL pointing to ‘https://developer.mozilla.org/en-US/docs’var h = new URL(’/en-US/docs’, a); // Creates a URL pointing to ‘https://developer.mozilla.org/en-US/docs’var i = new URL(’/en-US/docs’, ‘’); // Raises a SYNTAX ERROR exception as ‘/en-US/docs’ is not validvar j = new URL(’/en-US/docs’); // Raises a SYNTAX ERROR exception as ‘about:blank/en-US/docs’ is not validvar k = new URL(‘http://www.example.com’, ‘https://developers.mozilla.com’); // Creates a URL pointing to ‘https://www.example.com’var l = new URL(‘http://www.example.com’, b); // Creates a URL pointing to ‘https://www.example.com’实际上这和Node中的URL对象十分相似:// 终端> Node> new URL(‘https://www.xxx.com/?q=10’)URL { href: ‘https://www.xxx.com/?q=10’, origin: ‘https://www.xxx.com’, protocol: ‘https:’, username: ‘’, password: ‘’, host: ‘www.xxx.com’, hostname: ‘www.xxx.com’, port: ‘’, pathname: ‘/’, search: ‘?q=10’, searchParams: URLSearchParams { ‘q’ => ‘10’ }, hash: ’’ }它和我们讨论的文件下载有什么关系呢,在我们在浏览器中一切可以利用的资源都有唯一的标识符那就是URL.而我们自定义或者读取的文件需要通过URL对象创建一个指向我们定义资源的链接.那么URL对象上提供了两个静态方法:URL.createObjectURL() 创建根据URL或者Blob创建一个URLURL.revokeObjectURL() 销毁之前已经创建的URL实例那么生成的这个URL,可以被用在任何使用URL的地方,在这个例子中我们读取一个图片,然后将它赋值给img标签的src属性,这会在你的浏览器中打开一张图片.<!DOCTYPE html><html><head> <meta charset=“utf-8”> <meta http-equiv=“X-UA-Compatible” content=“IE=edge”> <title>blob</title> <meta name=“viewport” content=“width=device-width, initial-scale=1”></head><body> <label for=“file”>读取文件<input id=“file” accept=“image/” type=“file” ></label> <img id=“img” src=”” alt=”"> <script type=“text/javascript”> document.getElementById(‘file’).addEventListener(‘change’,(event)=>{ const files = event.srcElement.files; if(files.length === 0){ return console.log(‘没有选择任何内容’); } const file = files[0]; document.getElementById(‘img’).src = URL.createObjectURL(file); }); </script></body></html>我们的图片被如下格式的URL所描述:blob:http://127.0.0.1:5500/b285f19f-a4e2-48e7-b8c8-5eae11751593导出文件实践主要是利用浏览器在解析到MIME为application/octet-stream类型的内容会弹出下载对话框的特性.我们有如下对策:创建一个File对象修改他的type为application/octet-stream使用这个File利用URL.createObjectURL()创建一个URL重定向到这个URL,让浏览器自动弹出下载框const buffer = new ArrayBuffer(1024), array = new Int8Array(buffer);array.fill(1);const blob = new Blob(array), file = new File([blob],’test.txt’,{ lastModified:Date.now(), type:‘application/octet-stream’ });saveAs(file,’test.txt’)const url = window.URL.createObjectURL(file);window.location.href = url;上面这种方式简单粗,不过导出的文件你得修改文件名称.我们只需要稍稍利用利用a标签就可以优雅的完成这项任务:const buffer = new ArrayBuffer(1024), array = new Int8Array(buffer);array.fill(1);const blob = new Blob(array), file = new File([blob],’test.txt’,{ lastModified:Date.now(), type:’text/plain;charset=utf-8’ });const url = window.URL.createObjectURL(file), a = document.createElement(‘a’);a.href = url;a.download = file.name; // see https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/a#%E5%B1%9E%E6%80%A7a.click();大功告成,利用HTML5的API我们终于可以愉快的在WEB上操作数据啦!MDN上几篇不错的指引分别是:在web应用程序中操作文件指南JavaScript 类数组对象Base64的编码与解码参考https://github.com/SheetJS/js…https://github.com/eligrey/Fi… ...

February 16, 2019 · 3 min · jiezi

在Shell中进行独立的集成测试

翻译:疯狂的技术宅原文:https://zachholman.com/posts/…本文首发微信公众号:jingchengyideng欢迎关注,每天都给你推送新鲜的前端技术文章我在开发 during.com 时创建了一系列的微服务,它们被用来做一些同步、导入和单调繁重数据处理之类的工作。如果你对微服务不熟悉,那么它只是一个花哨的名词而已,意思就是“让我们把这些该死的业务逻辑散落的到处都是!”不管怎样,我的微服务到处都是,嗯,的确是“微”。不过我绝对不是一个逗逼,我已经多次重写了自己的web服务,从Rails中的一个目录开始,然后迁移到Ruby,接着是Crystal,之后是Go,现在又回到了Ruby。这并不是在浪费时间,这只是为了以防万一而尝试新的方法。最后我又把这些服务迁移回了Ruby。这段时间Ruby的表现真是没得说,它能很轻松的进行扩展来应对用户的请求。不过目前这个应用还没有进入beta测试阶段,在你还没有用户的时候,它的确容易扩展。实际上如果在没有用户使用的前提下,几乎任何关于软件开发的一切问题都不算什么,当然除了赚钱(当然了这也并没有成为硅谷任何一家公司的障碍)。好吧我跑题了,我一直都很享受用Shell来测试这些服务的过程。在POSIX shell环境下测试, 或者 UBIQUITOUSIX shell 环境也可以我已经用Shell脚本为这些服务编写了测试,很不错。首先,不需要为基本环境操心。无论是我的AWS实例,还是我的持续集成服务器,还有我自己的开发机上都有Shell环境。所以不需要安装任何东西,也不必运行什么Docker实例(实际上用它肯定也没什么坏处)。不过最重要的一点是,我的测试是独立的,独立于将来可能会使用的任何语言。我可以在不同的语言和框架之间进行切换,而不需要对测试脚本做任何改变。这一点非常重要,因为如果你的v1版本中有一个微妙的bug,而测试却通过了,当你开始重写v2版本的服务时,如果在无意中修正了这个bug,测试将可能失败。这意味着你暴露给其它服务的API不会因此而意外中断,你可以使用其它服务来暂时顶替,为修复bug争取时间,而不是在部署到生产环境后大吃一惊。这些测试的工具也是相当不错的,这些年我一直在用我的好友Blake Mizerany写的一个Shell环境下的小工具roundup。最近我一直在使用Sam Stephenson的 bats,现在它已经形成了一个十分活跃的社区(哈,谁能想到呢,仅仅是一个shell测试工具而已)。我的Shell测试看起来就像这样,用bats:@test “Responds with events within the given timespan” { url_params="?starts_at=2017-05-01T00:00:00-00:00&ends_at=2017-05-31T00:00:00-00:00" run curl “$URL$url_params” –silent -H “Authorization: Bearer:$bearer” assert_output –partial “Test Event 0” assert_output –partial “Test Event 2” refute_output –partial “Test Event 5” refute_output –partial “No location data” refute_output –partial “Not included in the date span”}测试非常简单,也容易理解。基本上就是运行curl然后检查输出结果,完成。整合周围的一切最后一点,这些微服务非常之小,我完全可以不用为它们写任何其它的测试,只需要写集成测试即可。全栈测试(full-stack)真的非常有趣,但是人们对此很谨慎,不知道它会成为下一个好主意还是成为世界上最差劲的想法。对于它的价值,GitHub的主旨是随时愉快地运行在零单元测试的生产环境下。总的来说我正在实践这种悬而未决的理论,不过我会悬崖勒马。如果你感兴趣的话可以阅读关于这个话题更多的文章。但是我要说的是在这种情况下,哇,一股新鲜空气袭来。我们的测试是可移植的,如果我重写了服务,不必为它们重写新的测试。我只需要通过自己的基于 shell 的测试即可。本文首发微信公众号:jingchengyideng欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

February 13, 2019 · 1 min · jiezi

从零到亿:WEB后台的变迁史

第一阶段 - 项目成立随便在阿里云买了一台虚拟机。在万网买了一个不是很满意的域名。使用免费的Gitlab创建了自己的代码项目。通过朋友介绍认识设计师小A。第二阶段 - 开始运营另外多买了一台虚拟机。购买了阿里云的负载均衡服务,并且将80,443端口指向这一新一旧两台服务器。考虑到数据库安全,购买了阿里云的RDMS服务,不再担心数据丢失与备份问题。给服务器创建了一些短信告警。第三阶段 - 天使在望注册用户竟然达到了惊人的10万。一些调皮的用户开始对你的系统进行业务测试,你开始关注防火墙与SQL注入等安全问题。为队列系统购买了数台服务器充当RabbitMQ Cluster与Worker Cluster角色。将一些非实时性与高消耗任务迁移到队列。发生短信/邮件。计算用户行为。统计各项数据报表。同时购买了阿里云的RDMS从库,使用KingShard等中间件进行透明读写分离。CPU不在持续在80%以上。开始开发全端客户端,使用Access Token权限机制代替Session。开始处理当初写下得烂SQL,优化系统性能。招聘了程序员小B,小C。第三阶段 - 数据量的盛宴你开始从别人的口中听到你的产品消息。好的或者不好的。系统数据量开始以超过60度角的水平递增,你坐在电脑前 tail -f /var/log/nginx.access.log 享受着qps带来的荣耀感。你开始在办公室挂了2块屏幕显示当前系统的平均响应时间和异常总数。几张极度增长的MySQL数据表开始让你担忧,开始设计下一代系统。引入了分表分库,将搜索功能迁移到了ElasticSearch。你在Google上疯狂的搜索SQL优化技巧。开始将一些独立的服务从主系统剥离,使用RPC进行构建。权限系统存储系统多媒体处理系统验证码系统你的日PV达到了千万级。内部系统的hits达到了数亿。你从几篇博客里学习了几篇DDD的知识,要求程序员小B、小C开始写单元测试。直接使得小C的不满,导致小C的离职。小C认为小单元测试是对他技术的不信任。你没有多说什么,招聘了程序员小D,同时将小B提拔为技术总监,你开始将精力放到了你不擅长的商务领域。第四阶段 - 全球覆盖你终于迎来了你的第一个海外客户。但是你的客户将他那高达2300ms的ping截图甩你脸上令你羞愧不已。下定决心开始构建全球系统。将静态资源存储在Amazon S3,并且使用七牛回源到中国大陆。使用阿里云的智能域名解析,将域名的ip解析到离客户最快的海外节点。海外节点与主系统构建VPN系统相连接。开始引入多语言系统,对系统的错误码,文案进行多语言翻译。你开始拥有了6位程序员,你将小B和小E分为系(加)统(班)保(不)障(给)组(钱).其余人分为前端后端两大组。小B和小E完善了你的Bug Worker程序,引入了完善的ACK机制和异常处理,解决了偶尔充值不到账的问题,客服小U对小E心生爱意。终于,你拥有了一个现代化的全球Web后台系统。

February 13, 2019 · 1 min · jiezi

用一张图总结web缓存策略

1 浏览器缓存浏览器缓存机制有四个方面,它们按照获取资源时请求的优先级依次排列如下:1.Memory Cache2.Service Worker Cache3.HTTP Cache4.Push Cache1.1 Memory CacheMemoryCache,是指存在内存中的缓存。从优先级上来说,它是浏览器最先尝试去命中的一种缓存。从效率上来说,它是响应速度最快的一种缓存。不过当页面关闭时,内存里的数据也就没有了。资源存不存内存,浏览器秉承的是“节约原则”。我们发现,Base64 格式的图片,几乎永远可以被塞进 memory cache,这可以视作浏览器为节省渲染开销的“自保行为”;此外,体积不大的 JS、CSS 文件,也有较大地被写入内存的几率——相比之下,较大的 JS、CSS 文件就没有这个待遇了,内存资源是有限的,它们往往被直接甩进磁盘。1.2 Service Worker CacheService Worker 是一种独立于主线程之外的 Javascript 线程。它可以帮我们实现离线缓存、消息推送和网络代理等功能。通常我们如果要使用 Service Worker 基本就是以下几个步骤:首先我们需要在页面的 JavaScript 主线程中注册 Service Worker。注册成功后后台开始安装步骤, 通常在安装的过程中需要缓存一些静态资源。安装成功后开始激活 Service Worker激活成功后 Service Worker 可以控制页面了(监听 fetch 和 message 事件),但是只针对在成功注册了 Service Worker 后打开的页面。在页面发起 http 请求时,service worker 可以通过 fetch 事件拦截请求,并且给出自己的响应。页面和 serviceWorker 之间可以通过 posetMessage() 方法发送消息,发送的消息可以通过 message 事件接收到。Service Worker 必须以 https 协议为前提。1.3 HTTP 缓存HTTP 缓存分为强缓存和协商缓存。优先级较高的是强缓存,在命中强缓存失败的情况下,才会走协商缓存。1.3.1 强缓存强缓存指的是向浏览器缓存查找该请求的结果,并根据该结果的缓存规则来决定是否使用该缓存结果的过程强缓存是利用http响应头中的 Expires 和 Cache-Control 两个字段来控制的。1.3.1.1 Expires实现强缓存,过去我们一直用 expires。在服务器的响应头里,会将过期时间写入 expires 字段:那么,当我们试图再次向服务器请求资源时,浏览器就会先对比本地时间和 expires 的时间,如果本地时间小于 expires 设定的过期时间,就直接去缓存中取这个资源。不过expires依赖于本地时间,如果服务端和客户端的时间设置不同,那么expires 将无法达到我们的预期。1.3.1.2 Cache-Control考虑到 expires 的局限性,HTTP1.1 新增了 Cache-Control 字段来完成 expires 的任务。当 Cache-Control 与 expires 同时出现时,我们以 Cache-Control 为准。Cache-Control 包含以下几个值:(1)max-agecache-control: max-age=31536000max-age 会等于一个时间长度(以秒为单位)。在本例中,max-age 是 31536000 秒,它意味着该资源在 31536000 秒以内都是有效的,完美地规避了时间戳带来的潜在问题。在代理服务器中,我们使用 s-maxage 来执行 max-age 的功能。(2)public 与 private如果我们为资源设置了 public,那么它既可以被浏览器缓存,也可以被代理服务器缓存(也就是多个用户可以共享这个缓存);如果我们设置了 private,则该资源只能被浏览器缓存。private 为默认值。但多数情况下,public 并不需要我们手动设置,因为设置了 max-age 就表示响应是可以缓存的。(3)no-store 与 no-cache如果我们为资源设置了 no-cache,浏览器会对响应进行缓存,但是需要到服务器去确认这个缓存是否能用。即走我们下文即将讲解的协商缓存的路线。如果设置了 no-store ,所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存1.3.2 协商缓存协商缓存指的是强制缓存失效后,浏览器向服务器询问缓存的相关信息,进而判断是重新发起请求还是从本地拿缓存的过程。如果服务端提示缓存资源未改动(Not Modified),资源会被重定向到浏览器缓存,这种情况下网络请求对应的状态码是 304(如下图)。同样,协商缓存的标识也是在响应报文的HTTP头中和请求结果一起返回给浏览器的,控制协商缓存的字段分别有:Last-Modified / If-Modified-Since和Etag / If-None-Match,其中Etag / If-None-Match的优先级比Last-Modified / If-Modified-Since高。1.3.2.1 Last-Modified / If-Modified-Since如果我们启用了协商缓存,Last-Modified 会在首次请求时随着响应头返回:Last-Modified: Fri, 27 Oct 2017 06:35:57 GMT随后我们每次请求时,会带上一个叫 If-Modified-Since 的时间戳字段,它的值正是上一次 response 返回给它的 last-modified 值:If-Modified-Since: Fri, 27 Oct 2017 06:35:57 GMT服务器接收到这个时间戳后,会比对该时间戳和资源在服务器上的最后修改时间是否一致,从而判断资源是否发生了变化。如果发生了变化,就会返回一个完整的响应内容,并在响应头中添加新的 Last-Modified 值;否则,返回如上图的 304 响应,响应头不会再添加 Last-Modified 字段。1.3.2.2 Etag / If-None-MatchEtag 是由服务器为每个资源生成的唯一的标识字符串,这个标识字符串是基于文件内容编码的,只要文件内容不同,它们对应的 Etag 就是不同的。当首次请求时,我们会在响应头里获取到一个最初的标识符字符串:ETag: W/“2a3b-1602480f459"那么下一次请求时,请求头里就会带上一个值相同的、名为 if-None-Match 的字符串供服务端比对:If-None-Match: W/“2a3b-1602480f459"不过 Etag 的生成过程需要服务器额外付出开销,会影响服务端的性能。1.3.3 HTTP 缓存决策指南根据上文所说的 HTTP 缓存知识点,我们在面对一个具体的缓存需求时,可以根据下图的路线来决策:当我们的资源内容不可复用时,直接为 Cache-Control 设置 no-store,拒绝一切形式的缓存;否则考虑是否每次都需要向服务器进行缓存有效确认,如果需要,那么设 Cache-Control 的值为 no-cache;否则考虑该资源是否可以被代理服务器缓存,根据其结果决定是设置为 private 还是 public;然后考虑该资源的过期时间,设置对应的 max-age 和 s-maxage 值;最后,配置协商缓存需要用到的 Etag、Last-Modified 等参数。1.4 Push CachePush Cache 是指 HTTP2 在 server push 阶段存在的缓存。2 服务器缓存2.1 CDNCDN (Content Delivery Network,即内容分发网络)指的是一组分布在各个地区的服务器。这些服务器存储着数据的副本,因此服务器可以根据哪些服务器与用户距离最近,来满足数据的请求。 CDN 提供快速服务,较少受高流量影响。CDN 的核心点有两个,一个是缓存,一个是回源。“缓存”就是说我们把资源 copy 一份到 CDN 服务器上这个过程,“回源”就是说 CDN 发现自己没有这个资源(一般是缓存的数据过期了),转头向根服务器(或者它的上层服务器)去要这个资源的过程。CDN 往往被用来存放静态资源,就是像 JS、CSS、图片等不需要业务服务器进行计算即得的资源。3 HTML5缓存3.1 Web StorageWeb Storage 是 HTML5 专门为浏览器存储而提供的数据存储机制。存储容量可以达到 5-10M 之间。它又分为 Local Storage 与 Session Storage。3.1.1 Local Storage 与 Session Storage 的区别两者的区别在于生命周期与作用域的不同。生命周期:存储在Local Storage的数据是永远不会过期的,使其消失的唯一办法是手动删除;而 Session Storage 是临时性的本地存储,当会话结束(页面被关闭)时,存储内容也随之被释放。作用域:Local Storage、Session Storage 和 Cookie 都遵循同源策略。但 Session Storage 特别的一点在于,即便是相同域名下的两个页面,只要它们不在同一个浏览器窗口中打开,那么它们的 Session Storage 内容便无法共享。3.1.2 Web Storage 核心 API 使用示例(1)存储数据localStorage.setItem(‘user_name’, ‘xiuyan’)sessionStorage.setItem(‘key’, ‘value’);Web Storage只能存字符串。(2)读取数据localStorage.getItem(‘user_name’)var data = sessionStorage.getItem(‘key’);(3)删除某一键名对应的数据localStorage.removeItem(‘user_name’)sessionStorage.removeItem(‘key’);(4)清除所有数据localStorage.clear()sessionStorage.clear();3.1.3 应用场景Local Storage 的特点之一是持久,有时我们更倾向于用它来存储一些内容稳定的资源。比如图片内容丰富的电商网站会用它来存储 Base64 格式的图片字符串,有的网站还会用它存储一些不经常更新的 CSS、JS 等静态资源。Session Storage 更适合用来存储生命周期和它同步的会话级别的信息。这些信息只适用于当前会话,当你开启新的会话时,它也需要相应的更新或释放。比如微博的 Session Storage 就主要是存储你本次会话的浏览足迹。3.2 IndexDBIndexDB 是一个运行在浏览器上的非关系型数据库。4 参考文章前端性能优化原理与实践Service Worker 生命周期Service Worker初体验彻底理解浏览器的缓存机制 ...

February 11, 2019 · 2 min · jiezi

基于 React & TS & Webpack 的微前端应用模板

m-fe/react-ts-webpack在 Web 开发导论/微前端与大前端一文中,笔者简述了微服务与微前端的设计理念以及微前端的潜在可行方案。微服务与微前端,都是希望将某个单一的单体应用,转化为多个可以独立运行、独立开发、独立部署、独立维护的服务或者应用的聚合,从而满足业务快速变化及分布式多团队并行开发的需求。如康威定律(Conway’s Law)所言,设计系统的组织,其产生的设计和架构等价于组织间的沟通结构;微服务与微前端不仅仅是技术架构的变化,还包含了组织方式、沟通方式的变化。微服务与微前端原理和软件工程,面向对象设计中的原理同样相通,都是遵循单一职责(Single Responsibility)、关注分离(Separation of Concerns)、模块化(Modularity)与分而治之(Divide & Conquer)等基本的原则。fe-boilerplates 是笔者的前端项目模板集锦,包含了单模块单页面、单模块多页面、(伪)多模块单页面、微前端项目等不同类型的模板,其中微前端项目模块 m-fe/react-ts-webpack 与前者的区别即在于微前端中的各个模块能够独立开发,独立版本发布,独立部署,独立加载。分布式协作势必会带来协同以及开发流程上的挑战,在设计微前端项目架构的时候开发易用性也是非常重要的考量点。在年度总结中我也讨论了使用 TS 面向重构编程的意义,欢迎参考 Backend-Boilerplates/node 中的 ts-* 项目,使用 TS 进行全栈开发。当我们考量项目框架、模板或者脚手架的时候,首先想到的点就是希望尽可能对上层屏蔽细节,但是对于长期维护的、多人协作的中大型项目而言,如果项目的主导者直接使用了部分抽象的脚手架,不免会给未来的更新、迭代带来一定的技术负债;同时,目前也有很多成熟的工程化脚手架,因此笔者选择以项目模板的形式抽象出微前端中所需要的部分。尽可能地遵循简约、直观的原则,减少抽象/Magic Function 等;大型项目可能会抽象出专用的开发工具流,但是对于大部分项目而言,在现有框架/工具链的基础上进行适当封装会是较优选择。# 拉取并且提取出子项目git clone https://github.com/wxyyxc1992/fe-boilerplatecp fe-boilerplate/micro-frontend/react-ts-webpack ../# 添加全局的依赖更新工具$ yarn global add npm-check-updates# 为各个子项目安装依赖,以及链接各个子项目$ npm run bootstrap && npm run build# 执行预编译操作$ npm run build# 以基础模式运行 Host APP,此时 Host APP 作为独立应用启动$ cd packages/rtw-host-app & npm run dev:sa# 以标准模式运行子应用$ cd packages/rtw-mobx-app & npm run dev# 返回根目录$ cd .. & npm start值得说明的是,微前端作为概念对于不同人承载了不同的考量,其实现方式、落地路径也是见仁见智,若有不妥,敬请指教。Features非 APP 类可单独发布,APP 类可单独运行,与发布。发布版本可包含 ES, CJS, UMD 等,dist 目录下包含 ES/CJS 模块,build 目录下包含 APP 完整资源以及 UMD 模块。版本控制: 子应用资源不使用 Hash 方式,而是使用语义化版本,/[cdnHost]/[projectName]/[subAppName]/[x.y.z]/index.{js,css}样式,LESS 文件支持 CSS Modules,CSS/SCSS 使用标准 CSS状态管理,灵活支持 Redux/MobX/Dva 等不同的状态管理框架,对于 Redux 提供全局统一的 Store 声明Structure | 项目结构完整的微前端应用,可能会包含以下组成部分:Module | 模块: 模块是可单独编译、发布的基础单元,基础模式下可直接打包入主应用,标准模式下多项目共用时可单独打包为 AMD/UMD 格式,通过 SystemJS 引入Page | 页面: 页面不可单独编译,使用 Webpack SplitChunk 或其他机制进行异步加载App | 应用: 应用是对模块的扩展,是实际用户可见的部分Widget | 控件: 控件是特殊的模块,譬如通用的无业务组件等Extension | 扩展: 扩展是特殊的应用,提供了跨模块的通用功能,类似于 Chrome Extension 的定位基于此,我们可以将某个微前端应用抽象为如下不同的模块组:基础模块:rtw: 根目录,public 目录下包含了部分跨模块集成测试的代码核心模块:rtw-core/rtw-sdk/rtw-shared: 暴露给子应用可用的通用基础类、模型定义、部分无界面独立模块等。rtw-core 建议不放置界面相关,使用 Jest UT 方式进行功能验证。rtw-bootstrap: 完整项目级别编译与启动入口,包含项目的运行时配置、依赖配置消息总线、注册中心、核心模块加载机制等。rtw-host-app: 提供界面基础容器,譬如应用标准的 Layout,Menu 等组件;提供 Redux 核心 Store。子业务应用:rtw-mobx-app: MobX 示例应用rtw-redux-app: Redux 示例应用扩展模块:rtw-widgets: 包含部分业务型控件,提供给所有的子应用使用,提取通用业务逻辑、对上屏蔽部分第三方依赖关系,类似于完整的 OSS 文件上传控件等。rtw-extensions: 包含部分业务无关的通用型插件,类似于 Chrome Extension 的定位。rtw-worker: 包含通用的 Web Worker & WASM 计算模块,子应用内也可以通过 Buffer 方式直接引入自定义的 Worker如果希望在子应用 A 中加载子应用 B 的实例,则应该使用类似于依赖注入的方式,从统一的注册中心中获取该实例对象。所有各个模块共享的基础库,都必须以 UMD 模式加载到全局;rtw-host-app 中声明与使用需要展示哪些模块,rtw-bootstrap 中注册可提供的 UMD 子模块。开发模式笔者一直推崇渐进式的工程架构,因此该模板对于复杂度要求较低的项目而言,可以直接从基础模式启动,与其他 TS 项目并无太大区别。基础模式基础模式类似于(伪)多模块单页面,仅有唯一的 Host APP 作为编译与运行的入口,其他包体(譬如 rtw-core)直接打包进主包体中,不使用 SystemJS 进行独立加载。rtw-corertw-core 及相似的库承载了公共的结构定义、工具类等,在该包体目录下运行 npm run build 命令即可以生成 ES/CJS/UMD 等多种类型文件,以及 types 类型定义;可以直接通过 npm publish 来发布到公共/私有的 NPM 仓库中。其他包体通过 NPM 安装 rtw-core 并使用,如果以标准模式运行,则需要首先加载该库到全局作用域,利用 RequireJS/SystemJS 等工具遵循 AMD 规范来注入到其他依赖的库/应用中。值得一提的是,对于子应用中,如果存在需要共享组件/类型的情景。对于类型信息,建议是将子应用同样编译打包发布到 NPM 仓库中,纯组件可以直接引入,对于业务组件建议通过全局的注册中心来获取。rtw-host-app在 rtw-host-app 包下,执行 npm run dev:sa 命令,会从 src/index.sa 文件启动应用;如上文所述,该模式仅会基于 Webpack Splitted Chunk 进行异步加载,其开发流程与标准的单模块应用并无区别。标准模式rtw-bootstrap & rtw-host-apprtw-bootstrap 是微前端应用的实际启动点,其核心功能是执行依赖与子应用的注册。在启动时,其会根据传入的 HOST_APP 与 DEV_APP 等变量信息完成应用的顺序加载与启动。在标准模式下,rtw-host-app 的入口是 src/index 文件,该模式下,index 文件会对外暴露 render 函数,该函数会由 rtw-bootstrap 注入 importApp 函数来执行子应用加载:export function render(_importApp: Function) { importApp = _importApp; ReactDOM.render( … );}换言之,rtw-bootstrap 提供了应用加载的能力,而 rtw-host-app 决定了应该加载哪些应用;在实际案例中,我们应该将用户权限控制、菜单与子应用信息获取等业务操作放置在 rtw-host-app 中。rtw-redux-app & rtw-mobx-app这里以 rtw-mobx-app 为例介绍如何进行子应用开发,如果是项目已经发布上线,那么我们可以通过 Resource Overrides 等在线资源请求转发的工具将线上资源请求转发到本地服务器。在进行本地开发时,因为子应用本身并不会包含 ReactDOM.render 或者类似的将 Virtual DOM 渲染到界面的函数,因此在运行 npm run dev 之后,本地会开启生成 UMD 文件的 Webpack Dev Server。参考子应用的 public/index.html 文件:<script src="./bootstrap/static.js" type=“text/javascript”></script><script src="./bootstrap/runtime.js" type=“text/javascript”></script><script src="./bootstrap/vendors.js" type=“text/javascript”></script><script> // 联调环境 // window.HOST_APP = { // id: ‘host’, // name: ‘HOST APP’, // module: ‘http://0.0.0.0:8081/index.js’, // css: ‘http://0.0.0.0:8081/index.css’ // }; // 正式开发环境 window.HOST_APP = { title: ‘HOST APP’, module: ‘/release/rtw-host-app/index.js’, css: ‘/release/rtw-host-app/index.css’ }; window.DEV_APP = { id: ‘dev’, name: ‘DEV APP’, module: ‘/index.js’ };</script><script src="./bootstrap/index.js" type=“text/javascript”></script>可以看出子应用的启动需要依赖于 rtw-bootstrap 以及 rtw-host-app,如果项目已经发布上线,那么建议是直接从 CDN 加载资源;否则可以将资源放置到 public/release 目录下。如果本地需要同时调试 Host APP,则直接也将 Host APP 以开发方式运行(npm run dev),然后直接引入 Webpack Dev Server 生成的资源地址即可。延伸阅读如果希望实践掌握 Web 开发,可以阅读 JavaScript CheatSheet/ProgrammingLanguage-Series/JavaScript,DOM CheatSheet/CSS CheatSheet,React CheatSheet/Vue CheatSheet,现代 Web 开发基础与工程实践/Web Tuning CheatSheet 等。 ...

January 29, 2019 · 2 min · jiezi

WebGL 纹理颜色原理

本文由云+社区发表作者:ivweb qcyhust导语WebGL绘制图像时,往着色器中传入颜色信息就可以给图形绘制出相应的颜色,现在已经知道顶点着色器和片段着色器一起决定着向颜色缓冲区写入颜色信息并最终呈现出来,那么这个过程是什么样,如果图形的颜色需要用现有图片来渲染那么又该如何操作?颜色缓冲区在绘制开始前,经常见到调用函数清空画布的代码gl.clear(gl.COLOR_BUFFER_BIT),清空画布的绘图区实际上就是用之前定义好的背景颜色将颜色缓冲的的颜色清除。颜色缓冲区中存放着需要显示到画布上的像素的颜色数据,它属于帧缓存的一部分,与深度缓存、模板缓存等一起决定着最终画布上图像的显示信息。可以将颜色缓存区看成图像颜色存储器,在缓存区中以RGB或RGBA的格式存储着画布上每一个像素的颜色信息,各个像素点组合起来就构成了颜色缓存的矩形阵列。这个定义看起来与图片存储器是很相似的,颜色缓存为RGB或是RGBA每一个通道分配存放位数,其中RGB就是颜色数据,A表示alpha也就是该像素的透明度信息,颜色占用的位数值就是颜色深度,比如颜色深度为24位,表示每一个像素24位,一般24位的分配方案就是红色、蓝色、绿色各占8位,如果需要透明效果的话,可以采用32位颜色深度为alpha通道分配8位。这里可以总结得出,画布上各个像素点呈现的颜色就是存放在颜色缓冲区的颜色信息所决定的,而绘制图形的颜色缓冲区的信息又是由顶点着色器决定。要知道颜色如何渲染就要深入分析着色器的工作过程。图形装配要绘制一个三角形,我们是这样定义着色器的:// 顶点着色器const VSHADER_SOURCE = attribute vec4 a_Position; void main() { gl_Position = a_Position; };// 片段着色器const FSHADER_SOURCE = void main() { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); };之后通过gl.program将顶点position坐标传入顶点着色器,这就相当于在画布上确定了几个点的坐标信息,这些点需要用线条连接起来才能构成图形,这个由顶点坐标装配成几何图形的过程就叫做图形装配。被装配的基本图形被称作图元,它包含点、线、面等基本几何图形。在调用WebGL的drawArrays或drawElements方法时作为参数传入,从而指定图元类型。一个三角形的绘制过程拆分来看就是执行三次顶点着色器,将三个点坐标都传入装配区,根据绘制函数的图元参数gl.TRIANGLES将三个点装配成三角形,然后进入下一个过程——光栅化。光栅化简单来说,光栅化就是将图形转化成片元,可以理解成一个个像素。只有将图形转化成像素后才能交由片段着色器处理。光栅化结束后,WebGL执行片段着色器。每执行一次片段着色器就处理一个片元,将该片元的颜色写入颜色缓冲区中,等到图形中所有的片元处理完毕画布上就得到了最后的图像。如上面的例子,每一个片元都会被执行成红色,由这一个个红色像素组成的三角形也就是红色的。如果要绘制一个多颜色三角图形又是一个什么过程呢?首先需要修改着色器的定义,也许可以这样:// 顶点着色器const VSHADER_SOURCE = attribute vec4 a_Position; attribute vec4 a_Color; varying vec4 v_Color; void main() { gl_Position = a_Position; v_Color = a_Color; };// 片段着色器const FSHADER_SOURCE = varying vec4 v_Color; void main() { gl_FragColor = v_Color; };向顶点着色器传入顶点坐标和颜色两个数据,执行三次后得到三角形三个顶点的坐标和颜色,接下来通过图元装配得到一个三角形的图元,到了关键的光栅化这一步,该如何定义片元的颜色呢?WebGL采用一个叫做内插的过程来计算颜色的值。以一条线为例来解释内插,两个端点分别为(1.0,0.0,0.0)和(0.0,1.0,0.0),从一端到另一端,R的值从1.0降到0.0,G的值由0.0升到1.0,线上的所有点颜色值都这样计算出来,实现了平滑的颜色渐变,这就是内插。经过内插,图形的每一个片元都指定了自己的颜色,写入颜色缓冲区后呈现出来。纹理贴图如果要为WebGL创建更加复杂更加自然的现实效果,就需要采用贴图来将现成的图片贴到图形上。图片容器中存放的也是一个个RGB或RGBA的像素,将图片的信息读取后存放在纹理对象或者说纹理图像中,纹理图像有自己的坐标系,坐标中每一个单元格就存放的纹理图像的像素信息,也被称作纹素。 将纹理图像的坐标转换到画布上图形的坐标的映射过程就是纹理映射,这个过程中,为图形顶点指定了纹理坐标,剩下的颜色由内插计算得出,写入颜色缓冲区后,图形的表面就被贴上了图像的颜色。 用一个案例来实现纹理贴图,现在要做的是:加载好需要的纹理图像设置纹理坐标对纹理进行配置片段着色器抽出纹素并赋值给片元在这个例子中我选择提前加载图片。在这里要注意有的浏览器不允许访问本地文件,可以考虑自己搭建server或是开启浏览器访问本地文件。function main() { const canvas = document.getElementById(‘webgl’); const webgl = getWebGLContext(canvas); webgl.images = {}; // 初始化之前先加载图片 loadImage([ src/images/0.jpeg, ], webgl).then((gl) => { if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) { console.log(‘Failed to intialize shaders.’); return; } gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); const n = initVertexBuffers(gl); initTextures(gl, n, 0); });}loadImage的实现很简单,用一个promise来处理异步加载图片,传入数组为了之后支持多张图片。在initVertexBuffers中创建数据buffer,将图形顶点和纹理图像坐标一起传入着色器。function initVertexBuffers(gl) { // 顶点坐标和纹理图像坐标 const vertices = new Float32Array([ -0.3, 0.7, 0.0, 0.0, 1.0, -0.3, -0.7, 0.0, 0.0, 0.0, 0.3, 0.7, 0.0, 1.0, 1.0, 0.3, -0.7, 0.0, 1.0, 0.0, ]); const FSIZE = vertices.BYTES_PER_ELEMENT; const vertexBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); const a_Position = gl.getAttribLocation(gl.program, ‘a_Position’); const a_TexCoord = gl.getAttribLocation(gl.program, ‘a_TexCoord’); gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 5, 0); gl.enableVertexAttribArray(a_Position); gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, FSIZE * 5, FSIZE * 3); gl.enableVertexAttribArray(a_TexCoord); return 4;}然后看看最主要的initTextures,在这里配置纹理:function initTextures(gl, n, index) { // 创建纹理对象 const texture = gl.createTexture(); const u_Sampler = gl.getUniformLocation(gl.program, ‘u_Sampler’); const image = gl.images[index]; // WebGL纹理坐标中的纵轴方向和PNG,JPG等图片容器的Y轴方向是反的,所以先反转Y轴 gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); // 激活纹理单元,开启index号纹理单元 gl.activeTexture(gl[TEXTURE${index}]); // 绑定纹理对象 gl.bindTexture(gl.TEXTURE_2D, texture); // 配置纹理对象的参数 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); // 将纹理图像分配给纹理对象 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); // 将纹理单元编号传给着色器 gl.uniform1i(u_Sampler, index); // 绘制 gl.drawArrays(gl.TRIANGLE_STRIP, 0, n);}这里又遇到两个概念:纹理对象配置参数 texParameteri方法用来配置纹理对象参数,函数第二个参数传入配置参数名,第三个参数传入配置参数值,可以配置的参数有:伸展(gl.TEXTURE_MAX_FILTER): 绘制图形比纹理图像大的时候怎么取纹素,默认值gl.LINEAR收缩(gl.TEXTURE_MIN_FILTER): 绘制图形比纹理图像小的时候怎么取纹素, 默认值gl.NEAREST_MIP_LINEAR水平填充(gl.TEXTURE_WRAP_S): 定义绘制图形水平方向如何填充,默认值gl.REPEAT垂直填充(gl.TEXTURE_WRAP_T): 定义绘制图形垂直方向如何填充,默认值gl.REPEAT详细参考texParameteri纹理单元 如果需要使用多张图片就要管理多个纹理图片,WebGL为了使用多个纹理,用纹理单元来处理纹理图像。WebGL的实现至少支持8个纹理单元,分别用gl.TEXRTRUE0,gl.TEXRTRUE1,…,gl.TEXRTRUE7来表示。最后是着色器代码,在调用gl.drawArrays传入图元类型TRIANGLE_STRIP后执行:const VSHADER_SOURCE = attribute vec4 a_Position; attribute vec2 a_TexCoord; varying vec2 v_TexCoord; void main() { gl_Position = a_Position; v_TexCoord = a_TexCoord; };const FSHADER_SOURCE = precision mediump float; uniform sampler2D u_Sampler; varying vec2 v_TexCoord; void main() { gl_FragColor = texture2D(u_Sampler, v_TexCoord); };顶点着色器中传入纹理图像的顶点坐标,将它传递给片段着色器,在片段着色器中声明了一个专用于纹理对象的数据类型sampler2D,指向一个纹理单元编号(接下来解释),着色器获取纹素由函数texture2D完成,传入参数纹理单元编号和纹理图像坐标。 多纹理实现要使用多个纹理就要用到更多的纹理单元,多个纹理可以组合也可以单独渲染,利用前面的代码,可以很容易扩展成一起多纹理的案例,加上一些3D效果和动画,就可以组合成一个轮播图片。此文已由腾讯云+社区在各渠道发布获取更多新鲜技术干货,可以关注我们腾讯云技术社区-云加社区官方号及知乎机构号 ...

January 28, 2019 · 2 min · jiezi

每个JavaScript开发人员应阅读的书【1】 - JavaScript: The Good Parts

JavaScript: The Good Parts作者: Douglas Crockford出版社: Yahoo Press副标题: The Good Parts出版年: 2008-5页数: 172定价: GBP 23.99装帧: PaperbackISBN: 9780596517748内容简介 · · · · · ·大多数编程语言都包含好的和坏的部分,但是JavaScript有很多不好的部分,在它被改进之前已经匆忙开发和发布。这本权威书籍详细解释了使JavaScript成为一种出色的面向对象编程语言的功能,并向您发出有关不良部分的警告。在这个过程中,JavaScript:Good Parts定义了一个JavaScript的子集,它比整个语言更可靠,可读和可维护。作者Douglas Crockford是ECMA的JavaScript 2.0委员会成员,被开发社区的许多人认为是JavaScript专家。他解释说,一种美丽,优雅,轻盈,富有表现力的语言隐藏在一堆热情好客和失误之中。非常好的想法包括函数,松散类型,动态对象和富有表现力的对象文字符号。可怕的想法包括基于全局变量的编程模型。使用JavaScript:Good Parts,您可以从旧shell中释放这种优雅的编程语言,并创建更易于维护,可扩展和高效的代码。该书的主题包括:SyntaxObjectsFunctionsInheritanceArraysRegular expressionsMethodsStyleBeautiful features附录总结了JavaScript的不良部分和可怕的部分。但研究好部件的最大好处是可以避免忘记坏部件的需要。如果您想了解有关坏部件以及如何严重使用它们的更多信息,请参阅任何其他JavaScript书籍。JavaScript是Web的语言 - 所有浏览器中唯一的语言 - 因此完全避免使用它不是一种替代方法。但是,无论您是管理对象库还是只是试图让Ajax快速运行,Crockford在JavaScript中的指导:好的部件将帮助您创建真正有效的JavaScript代码。作者简介 · · · · · ·Douglas Crockford是Yahoo!的高级JavaScript架构师。 他是JSON格式的维护者,并且是高级JavaScript主题会议的常规发言人。 他也是ECMA的JavaScript 2.0委员会成员。

January 27, 2019 · 1 min · jiezi

每天一本电子书 - Eloquent Javascript, 3rd Edition

Eloquent Javascript, 3rd Edition作者: Marijn Haverbeke出版社: No Starch Press副标题: A Modern Introduction to Programming出版年: 2018-10-30页数: 472定价: GBP 30.04装帧: PaperbackISBN: 9781593279509内容简介 · · · · · ·经过全面修订和更新,这本畅销的JavaScript编程入门专注于编写实际应用程序。Eloquent JavaScript深入研究JavaScript语言,向程序员展示如何编写优雅,有效的JavaScript代码。 像任何好的编程书一样,Eloquent JavaScript从基础 - 变量,控制结构,函数和数据结构 - 开始,然后转向复杂的主题,如面向对象的编程和正则表达式。 第三版介绍了2017版JavaScript的新功能,例如类符号,箭头函数,迭代器,异步函数,模板字符串和黑色范围。 作者Marijn Haverbeke保留了友好的语气和易于理解的解释,使原作成为热门,他为读者添加了新的练习来测试他们的技能。 雄辩的JavaScript将让读者能够立刻熟练掌握网络语言。作者简介 · · · · · ·Marijn Haverbeke是一名编程语言爱好者和多语言。 他从事过广泛的软件系统工作,从数据库到编译器再到编辑器。 他围绕他的开源项目经营一家小企业。更多信息文档资源搜索Maning参考12 Books Every JavaScript Developer Should Read

January 26, 2019 · 1 min · jiezi

Vue:scoped与module的使用与利弊

一个web应用是离不开html、css与js,其中css充斥的整个web项目中。css它有一个特定,它是全局的。这样的特性导致的结果是,一旦你在不同的地方定义了相同的css命名,那么它们的样式就会相互覆盖,最终导致的style错乱,从而影响整个网页布局。我相信对于每一个前端开发者都遇到过这种css样式覆盖的情况,值得庆幸的是,这些问题前辈都已经给出了解决方案。在Vue中我们通过Scoped与Module来解决。下面我会分别对scoped与module解决方案进行说明,最后在分析它们的利弊与选择。如果你还未使用过或者说对它们之间的利弊与选择存在疑问的,相信这篇文章能够帮你解惑。Scoped假设我们有如下一段代码:index.vue<template> <div class=“content”> <div class=“title-wrap”>我是红色的</div> <green-title></green-title> </div></template> <style lang=“scss”>.content { .title-wrap { font-size: 20px; color: red; }}</style>GreenTitle.vue<template> <div class=“content”> <div class=“title-wrap”>我是绿色的</div> </div></template> <style lang=“scss”>.content { .title-wrap { font-size: 20px; color: green; }}</style>最终这屏幕上展示的是两行红色的文字,这就是父组件与子组件都定义了title-wrap的样式,导致子组件的样式被父组件所覆盖。遇到这种情况,可以在style标签中添加scoped属性<style lang=“scss” scoped>.content { .title-wrap { font-size: 20px; color: green; }}</style>scoped作用的阻止上层的css样式传递到下层,限制当前css作用域,使其只对当前组件生效。知道了它的作用,下面我们在开深入看下它的实现。前面的是没有添加scoped的源码,后面是添加了scoped的源码。我们进行一一对比,发现前面的两个div标签都使用了title-wrap样式,自然导致样式覆盖;而后面的两个div标签,第一个增加了data-v-67e6b31f的前缀,这就是父组的style中增加scoped的效果,区别与第二个div中的title-wrap样式。scoped的实现是借助了PostCSS实现的,一旦增加了scoped,他会将之前覆盖的样式转换成下面的样式<style lang=“scss”>.content[data-v-67e6b31f] { .title-wrap[data-v-67e6b31f] { font-size: 20px; color: green; }}</style>通过这种转换方式,间接的改变了原有的css命名。防止上层组件样式覆盖下层组件样式。特性细心的读者可能会发现上面的后一张源码图中第二个div的content中也有data-v-67e6b31f,可能会疑问,第二个content不是子组件中的css吗?子组件中未添加scoped,为什么还会添加data-v-67e6b31f前缀?这是scoped的一个特性,使用 scoped 后,父组件的样式将不会渗透到子组件中。不过一个子组件的根节点会同时受其父组件有作用域的 CSS 和子组件有作用域的 CSS 的影响。这样设计是为了让父组件可以从布局的角度出发,调整其子组件根元素的样式。所以如果我们将子组件做如下修改<template> <!– <div class=“content”> –> <div class=“title-wrap”>我是绿色的</div> <!– </div> –></template>由于父组件scoped特性,所以会影响到子组件的title-wrap,也会添加data-v-67e6b31f前缀那么又有个疑问,增加了scoped是否就一定不能传递的下层组件呢?毕竟我们可能有需要个别样式传递到下层的需求。别急,接着看,这个也能很方便的解决。深度作用如果你希望scoped中的某个样式能够作用的更深,影响到子组件,你可以使用>>>操作符<style scoped>.content >>> .title-wrap { font-size: 20px; color: red;}</style>注意看我将style中的lang=“scss"去掉了,因为加了预处理器后无法正确解析>>>,这种情况可以使用/deep/代替,本质是>>>的别名<style lang=“scss” scoped>.content { /deep/ { .title-wrap { font-size: 20px; color: red; } }}</style>将会编译成.content[data-v-67e6b31f] .title-wrap { font-size: 20px; color: red;}通过 v-html 创建的 DOM 内容不受作用域内的样式影响,但是你仍然可以通过深度作用选择器来为他们设置样式Module针对上面的覆盖问题,还可以通过设置module来解决<template> <div :class="$style.content”> <div :class="$style[’title-wrap’]">我是红色的</div> <green-title></green-title> </div></template> <style lang=“scss” module>.content { .title-wrap { font-size: 20px; color: red; }}</style>module的用法也很简单,只要在style中增加module属性即可。不同之处是它在布局中的引用,都需要添加前缀$style。因为通过module作用的style都被保存到$style对象中。我可以通过console查看它的具体引用名。mounted() { console.log(this.$style) console.log(this.$style[’title-wrap’])}通过观察,发现引用名有一定的规律。都是已index开头,后面再接着style中定义的命名,最后再接个后缀。这里的index是父组件的文件名index.vue。所以通过module作用的style将会重新命名为:文件名_原style名_不定后缀。这么命名又有什么好处呢?我们再来看下展示的效果当我们在浏览的控制台查看Elements时,优点显而易见。相对于scoped的方式,module的方式能够一眼知道该元素时属于哪个文件组件中。在大型项目中能够帮助我们迅速定位到要查找的组件。除了上述的快速定位,由于module会将所有的style都归入$style中,所以我们可以很灵活的将任意的父组件样式传递到任意深层的子组件中。例如,将父组件中的title-wrap通过props传递到子组件中<template> <div :class="$style.content"> <div :class="$style[’title-wrap’]">我是红色的</div> <green-title :styleTitle="$style[’title-wrap’]"></green-title> </div></template><template> <div class=“content”> <div :class=“styleTitle”>我是绿色的</div> </div></template><script> export default { props: { styleTitle: String, },}</script>module还有一个特性非常不错,它可以导出定义的变量,将变量归入$style中,例如:<template> <div :class="$style.content"> <div :class="$style[’title-wrap’]">我是红色的</div> <green-title :styleTitle="$style[’title-wrap’]"></green-title> <div>{{$style.titleColor}}</div> </div></template> <style lang=“scss” module>$title-color: red;:export { titleColor: $title-color}.content { .title-wrap { font-size: 20px; color: $title-color; }}</style>更多module相关操作可以点击查看总结scoped与module都非常简单、易用,那么又该如何选择呢?通过上面的使用对比,发现scoped不需要额外的知识,只要在style中定义scoped属性即可,使用非常简便。但它的局限性是适用于中小项目中。因为scoped作用的style对于我们来说不直观,对于快速查找定位,module更加合适,同时module对于style向下传递的控制权也非常灵活;额外的还有变量导出等便捷功能。所以如果你是小项目中且低成本的使用,scoped更加适合;而对大项目module更加合适,虽然有一点学习成本,但对于用更好的控制权、可观性与定位速度来说也就不值一提。公众号感觉不错的可以来一波关注,扫描下方二维码,关注公众号:怪谈时间,及时获取最新知识技巧与互联网新动态。 ...

January 24, 2019 · 1 min · jiezi

自开发Web应用和SAP Customer Data Cloud Identity服务的集成

今天的文章继续由SAP成都研究院的云时代女王,Aviva给大家分享关于SAP Customer Data Cloud的一些使用经验。Aviva之前的文章可以在本文末尾处获得。下面是她的正文。*大家好,我是Aviva。本人在SAP不负责Gigya的开发工作,只是出于个人兴趣,在业余时间阅读了SAP官网上Gigya的帮助文档后,就Gigya提供的网站登录接入功能做了一些非常简单的Hello World级别的例子,在此分享给大家。2017年SAP收购了以色列创业公司Gigya, 现在Gigya又被称作SAP Customer Data Cloud,是SAP C/4HANA的五朵云之一。所以下文在不同的上下文出现的SAP Customer Data Cloud和Gigya,大家可以认为这两组词表达的是同一个意思。文章分为两部分,第一部分,简单介绍SAP Customer Data Cloud的业务,第二部分用一个demo介绍如何在自己的网站中使用SAP Customer Data Cloud中的某些服务。Customer Data Cloud从功能上分为三大模块:SAP Customer IdentitySAP Customer ConsentSAP Customer Profile我们首先来简单了解一下这三大功能模块。SAP Customer Identity提供跨渠道和跨设备的用户身份识别,在Web,移动设备和物联网设备上提供统一的注册,身份验证,登录等用户体验。除此之外,SAP Customer Identity还提供单点登录,无密码的身份验证,能够安全地识别来自任何接入端的在线访问者。通过抓取客户授权的身份数据,SAP Customer Identity可以推动个性化、即时营销、销售和服务,同时尊重消费者隐私并满足数据保护法规。Gigya 在安全上做了很多工作,确保用户数据不被窃取和攻击。SAP Customer Consent提供了开箱即用的工作流程,帮助企业明确提出各项服务条款,隐私政策,营销沟通,以及其他需要用户授权的同意请求等等。每次企业更新服务条款和隐私政策,SAP Customer Consent会自动触发新的用户同意请求,并记录每次用户的同意选项和授权的时间。企业管理员可以访问用户整个使用周期内每一次授权的历史记录,从而有效地解决政府对隐私的监管和审计。在企业的数字生态系统中,通过将用户的配置文件与企业的应用程序和服务同步,在每个渠道上强制执行用户的隐私许可,满足关键数据隐私要求。SAP Customer Consent提供了跨平台和跨设备的用户隐私设置服务中心,使用户在整个使用产品的生命周期内,都能透明地管理自己的偏好设置,控制企业对自己的隐私数据的访问规则,从而帮助企业和客户建立透明可信任的关系。SAP Customer Profile通过抓取用户授权后的第一手数据,SAP Customer Profile为用户建立丰富的用户档案,让企业的每个应用程序和服务都可以无缝的使用它们。企业各种应用和服务的用户资料通过统一的平台对管理员开放,同时也能为营销人员提供各种用户数据的分析报告,以及为客户细分和个性化的营销方案提供数据支撑。下面通过一个简单的demo,向大家介绍如何将SAP Customer Identity集成到我们自己的Web应用中。我开发了一个基于nodejs的Web应用。后台使用nodejs + express框架,前端使用SAP UI5。登陆SAP Customer Data Cloud的RAAS(Registration-as-a-Service)平台。首先在RAAS平台上创建site和应用。本地开发和测试使用的Site Domain可以填成localhost:创建一个新的Application:创建了Site 和Application之后,Site会自动生成对应的API Key:Application会产生User Key和Secret。其次,在Web应用的index.html中引入Gigya Web SDK:<script src=“https://cdns.gigya.com/js/gigya.js?apikey=YOUR-API-KEY-HERE"></script>然后使用SAP UI5开发Web应用的登录页面 login.view.xml和控制器login.controller.js。Gigya 提供了一个默认的用户登录注册页面,只需两行代码就可以在我们的Web应用里使用。这种便捷的使用方式体现了RAAS的含义。在xml视图中嵌入一个div标签:在控制器实现的初始化函数中加入:gigya.accounts.showScreenSet({ screenSet: ‘Default-RegistrationLogin’, containerID: me.byId(‘LoginGigya’).sId });Gigya的登录和注册服务就加入我们自己的Web应用中去了。除了使用默认的登录屏幕设置之外,我们当然可以直接在Gigya平台上定制登录页面和注册流程。Gigya提供了UI Builder,在UI Builder里可以使用默认的控件,通过拖拽就可以生成不同的登录页面(类似SAP Cloud for Customer UI Designer),也可以直接修改html和css,修改登录和注册流程等等。Gigya定义了许多不同类型的事件,这些事件由用户交互来触发,例如用户登录,按钮点击等。应用程序可以注册监听感兴趣的事件,并在收到这些事件时执行代码。下面是简单的监听用户登录和注销事件的代码:var me = this;gigya.accounts.addEventHandlers({ onLogin: me.login, onLogout: me.onLogout, context: me });登录后,在控制台简单地打印一些字符串。login: function (response) { console.log(“LOGIN!!!!!!!!!!!!!!!!!”); console.log(response);**},关于Gigya支持的所有事件和事件相关参数,可以参考用户手册:https://developers.gigya.com/…在Web应用后台,我们还可以使用Rest API的方式访问Gigya的相关服务。举个例子,在后台获取用户的Account信息,调用Rest API 需要用到Site 的API Key 和Application的User Key和Secret。以上只是基于Gigya提供的服务进行的一些最简单的练习。关于Gigya更多的功能介绍,请移步官网上去查看,有很详细的介绍:https://developers.gigya.com/ 感谢阅读。Aviva另外两篇文章:Hyperledger Fabric on SAP Cloud PlatformSAP C/4HANA与人工智能和增强现实(AR)技术结合的又一个创新案例要获取更多Jerry的原创文章,请关注公众号"汪子熙”: ...

January 22, 2019 · 1 min · jiezi

安装指定版本(老版本)的PrimeNG

由于兼容问题,有时候还是要安装老版本的PrimeNG的,ng-zorro-antd也可以参照本方法卸载已有版本:npm uninstall primeng –save清理缓存:npm cache verify安装老版本:npm install primeng@x.x.x –save,其中的x.x.x是版本号截止2019年1月22日PrimeNG是7.0.4,前一个稳定的大版本号是6.1.7安装兼容版本的PrimeNG同时可以解决:node_modules/primeng/components/table/table.d.ts(15,86)诸如此类的错误提示,缺少括号或者分号的错误,都是版本兼容问题

January 22, 2019 · 1 min · jiezi

web开发中,必须要了解的HTTP相关知识

本文已同步到github, web开发中,必须要了解的HTTP相关知识,欢迎收藏,欢迎start。本文主要记录与HTTP相关的具体概念和知识,关于HTTP协议的诞生和历史发展,不多做介绍,自己但是既然是写HTTP,顺带说两句,上下文也能衔接的上。CERN(欧洲核子研究组织)的蒂姆 • 伯纳斯 - 李(Tim BernersLee)博士提出了一种能让远隔两地的研究者们共享知识的设想,于是HTTP慢慢的诞生了。另外,HTTP协议是无状态可以,于是为了保存用户的状态,cookie诞生了。HTTP协议是建立在TCP连接之上的,当浏览器输入URL进行访问,浏览器冲URL中解析出主机名和端口,浏览器建立一条与web服务器的连接,然后才进行http请求。TCP连接的建立与终止建立TCP连接(三次握手)在客户端与服务端进行http通信之前,需要建立TCP连接,这时需要三次握手(1) 请求新的TCP连接,客户端发送一个小的TCP分组,这个分组设置一个特殊的SYN标记,表明是一个客户端请求。(2) 如果服务器接受这个连接,就会对一些连接参数进行计算,并向客户端回送一个TCP分组,发送SY和ACK标记,表明连接请求已经被接受(3) 最后,客户端向服务器回送一条确认消息,通知服务器连接已经建立。断开TCP连接(四次断开)建立一个连接需要三次握手,而终止一个连接要经过4次握手。这由TCP的半关闭(half-close)造成的。既然一个TCP连接是全双工(即数据在两个方向上能同时传递),因此每个方向必须单独地进行关闭。这原则就是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向连接。当一端收到一个FIN,它必须通知应用层另一端几经终止了那个方向的数据传送。发送FIN通常是应用层进行关闭的结果。(1) 客户端发送FIN标记到服务器,表明客户端发起关闭连接(2) 服务器接收客户端的FIN标记并,向客户端发送FIN的ACK确认标记(3) 服务器发送FIN到客户端,服务器关闭连接(4) 服务器端发送一个FIN的ACK确认标记,确认连接关闭建立持久连接的请求和响应交互:使用wireshark进行数据抓包:这里向大家推荐一款抓包软件Wireshark,可以用来分析TCP连接的建立和断开过程,以及抓取HTTP请求和相应的信息等,下面是我进行一次客户端和服务端通信的抓包数据截图:HTTP报文HTTP协议报文是应用程序之间发送的数据块,也就是客户端和服务端用于交互的信息。客户端的报文叫做请求报文,服务器端的报文叫做响应报文。HTTP报文组成HTTP报文由起始行、首部和实体的主体(也称报文主体或主体)组成。起始行和首部以一个回车符和换行符作为结束,主体部分可以是二进制数据,也可以为空。1. 起始行请求报文起始行:请求报文起始行说明了要做什么,由请求方法 、请求URI和协议版本构成。GET /index.html HTTP/1.1响应报文起始行:响应报文的起始行,由协议版本、状态码和原因短语构成。HTTP/1.1 200 OK // OK就是原因短语2. 首部首部字段分类1.通用首部客户端和服务端都可以使用的首部通用首部字段表:2.请求首部请求报文特有的首部,为服务器提供了一些额外的信息,补充了请求的附加内容、客户端信息、响应内容相关的优先级等信息。请求首部字段3.响应首部响应报文特有的字段响应首部字段表:4.实体首部用于针对请求报文和响应报文主体部分使用的首部5.扩展首部扩展首部是非标准的首部,由应用程序开发者创建,但还未添加到已批准的HTTP标准中去。http状态码状态码的职责是当客户端向服务器端发送请求时,描述返回的请求结果。借助状态码,用户可以知道服务器端是正常处理了请求,还是出现了错误。状态码分类:状态码区间类别100~199信息性状态码200~299成功状态码300~399重定向状态码400~499客户端错误状态码500~599服务器错误状态码常用状态码列表:状态码原因短语含义200OK表示从客户端发来的请求在服务器端被正常处理了204No Content该状态码代表服务器接收的请求已成功处理,但在返回的响应报文中不含实体的主体部分。另外,也不允许返回任何实体的主体。301Moved Permanently永久重定向,该状态码表示请求的资源已被分配了新的 URI,以后应使用资源现在所指的 URI302Found临时性重定向,该状态码表示请求的资源已被分配了新的 URI,希望用户(本次)能使用新的 URI 访问303See Other303 状态码和 302 Found 状态码有着相同的功能,但 303 状态码明确表示客户端应当采用 GET 方法获取资源,这点与 302 状态码有区别304Not Modified缓存307Temporary Redirect临时重定向,和302一样400Bad Request该状态码表示请求报文中存在语法错误。当错误发生时,需修改请求的内容后再次发送请求。另外,浏览器会像 200 OK 一样对待该状态码401Unauthorized该状态码表示发送的请求需要有通过 HTTP 认证(BASIC 认证、DIGEST 认证)的认证信息403Forbidden该状态码表明对请求资源的访问被服务器拒绝了404Not Found该状态码表明服务器上无法找到请求的资源500Internal Server Error该状态码表明服务器端在执行请求时发生了错误。也有可能是 Web应用存在的 bug 或某些临时的故障502Bad Gateway网关错误503Service Unavailable该状态码表明服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。如果事先得知解除以上状况需要的时间,最好写入RetryAfter 首部字段再返回给客户端HTTP中不同场景下,首部字段的作用1. CORS 跨域资源共享跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的Web应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。 –MDN下面使用nodejs来搭建一个简单的服务器,来介绍一个跨域问题的解决方法// index.html<!DOCTYPE html><html><head> <meta charset=“utf-8” /> <title>CORS</title></head><body> Hello World<script> fetch(‘http://127.0.0.1:8081’)</script></body></html>// server.jsconst http = require(‘http’)http.createServer(function(req, res) { res.writeHead(‘200’, { ‘Access-Control-Allow-Origin’: ‘http://localhost:8082’ })}).listen(8081)在源地址为 http://localhost:8082 下,请求http://localhost:8081,是跨域请求,浏览器会自动在request Header中发送Origin首部字段,并把值设置为来自哪个源,本例为http://localhost:8081。服务器需要在响应头中设置Access-Control-Allow-Origin,来告知浏览器可以处理返回的数据。如果响应头中不设置Access-Control-Allow-Origin则会报错,但是返回状态码为200,跨域实际上是浏览器本身的一个安全机制。// server2.js// 启动8082端口服务,在浏览器中访问http://127.0.0.1:8082,会返回index.html内容const http = require(‘http’)const fs = require(‘fs’)http.createServer(function(req, res) { var page = fs.readFileSync(‘index.html’, ‘utf-8’) res.writeHead(200, { ‘Content-Type’: ’text/html’ }) res.end(page)}).listen(8082)关于CORS跨域请求的分类:1.简单请求:需要同时满足以下的条件就是简单请求(1)请求方法:GET、POST、HEAD(2)请求头不能为以下其他字段之外AcceptAccept-LanguageContent-LanguageContent-Type的值必须为application/x-www-form-urlencoded、multipart/form-data、text/plain之一2.非简单请求:非简单请求是当请求信息不满足简单请求的条件,浏览器就发送方法为OPTIONS的预请求,包含自己请求的方法及需要使用的请求头字段,在得到服务器响应允许之后,浏览器会按照想要使用的请求方法及头信息再发一次请求。现在修改以下上面的例子:// index.html<!DOCTYPE html><html><head> <meta charset=“utf-8” /> <title>CORS</title></head><body> Hello World<script> fetch(‘http://127.0.0.1:8081’, { method: ‘PUT’, headers: { X-Coustom-Head: ‘abc’ } })</script></body></html>// server.jsconst http = require(‘http’)http.createServer(function(req, res) { res.writeHead(‘200’, { ‘Access-Control-Allow-Origin’: ‘http://localhost:8082’ })}).listen(8081)如果服务端不进行相应的设置告诉浏览器允许跨域访问则会报错但是预请求返回状态码为200// server2.js// 启动8082端口服务,在浏览器中访问http://127.0.0.1:8082,会返回index.html内容const http = require(‘http’)const fs = require(‘fs’)http.createServer(function(req, res) { var page = fs.readFileSync(‘index.html’, ‘utf-8’) res.writeHead(200, { ‘Content-Type’: ’text/html’ }) res.end(page)}).listen(8082)现在我们修改以下 server.js// server.jsconst http = require(‘http’)http.createServer(function(req, res) { res.writeHead(‘200’, { ‘Access-Control-Allow-Origin’: ‘http://localhost:8082’, ‘Access-Control-Allow-Headers’: ‘X-Coustom-Head’, ‘Access-Control-Allow-Methods’: ‘PUT’ })}).listen(8081)重新启动node服务,访问http://locaohost:8082,可以看到在发送预请求后,浏览器会继续发送PUT请求关于CORS的其他设置这里就不多做介绍了,这里主要是用一个例子来说明以下http不同字段在跨域场景下的作用。2. 缓存 (Cache-Control的作用)本例依旧用node服务来讲解一下Cache-Control的作用,新建三个文件// index.html<!DOCTYPE html><html><head> <meta charset=“utf-8” /> <meta http-equiv=“X-UA-Compatible” content=“IE=edge”> <title>Cache-Control</title> <meta name=“viewport” content=“width=device-width, initial-scale=1”></head><body> <script src="/script.js"></script></body></html>// script.jsconsole.log(‘script.js’)// server.jsconst http = require(‘http’)const fs = require(‘fs’)http.createServer(function(req, res) { if (req.url === ‘/’) { let page = fs.readFileSync(‘index2.html’, ‘utf-8’) res.writeHead(200, { ‘Content-Type’: ’text/html’ }) res.end(page) } if (req.url === ‘/script.js’) { let page = fs.readFileSync(‘script.js’, ‘utf-8’) res.writeHead(200, { ‘Content-Type’: ’text/javascript’, ‘Cache-Control’: ‘max-age=10’ }) res.end(page) }}).listen(8082)在第一次请求script.js资源时,向服务器发送请求由于服务器返回响应时,设置Cache-Control: ‘max-age=10’时,修改script.js后,在10秒内继续请求script.js资源,则从缓存中读取,而打印信息依旧是’script.js’// script.jsconsole.log(‘script-modify.js’)更多关于缓存的知识在这里也不多介绍了,贴两张cache-control字段在请求和响应时可以设置的值和其表示含义:1. Cache-Control 缓存请求指令:2. Cache-Control 缓存响应指令:3. cookie指某些网站为了辨别用户身份、进行 session 跟踪而储存在用户本地终端上的数据(通常经过加密),当下次再访问时浏览器会将该网站的cookie发回给服务器端。cookie如果不设置过期时间,随浏览器关闭而失效,如果有需要可以设置过期时间,继续上代码例子????,新建两个文件如下// index.html<!DOCTYPE html><html><head> <meta charset=“utf-8” /> <title>Cookie</title></head><body> Cookie<script> console.log(document.cookie)</script></body></html>// server.jsconst http = require(‘http’)const fs = require(‘fs’)http.createServer(function(req, res) { if (req.url === ‘/’) { let page = fs.readFileSync(‘index.html’, ‘utf-8’) res.writeHead(200, { ‘Content-Type’: ’text/html’, ‘Set-Cookie’: [‘a=1;max-age:5’, ‘b=2;HTTPOnly’] }) res.end(page) }}).listen(8082)启动node服务,访问localhost:8082,可以看到成功设置了cookie并在响应头信息中设置了Set-Cookie字段另外关注以下打印信息,发现只有a=1,因为给b=2设置了HttpOnly属性,不允许JavaScript通过脚本来获取到cookie信息由于当再次请求时,cookie会在请求头中发送到服务器,由于cookie a=1设置了5秒后过期,在5秒后刷新页面,请求头中的cookie只有a=1在5秒内发送二次请求,cookie a=1没有失效,在请求头中cookie a=1;b=2都会发送到服务器另外对于cookie的其他设置如expires、domain等在这里也不多做介绍了4. 重定向当服务端返回301、302、307等状态码都代表资源已经被重定向到其他位置,301表示永久改变URI,302和307表示临时重定向到某个URI本例举一个服务器返回302状态码的例子,直接上代码:// server.jsconst http = require(‘http’);const fs = require(‘fs’)http.createServer((req, res) => { if (req.url === ‘/’) { res.writeHead(302, { ‘Location’: ‘/redirect’ }) res.end() } if (req.url === ‘/redirect’) { res.end(‘redirect’) }}).listen(8082);访问localhost:8082, 服务器返回302状态码时,在相应头中设置Location首部字段,浏览器会继续发送请求到重定向的地址HTTP与HTTPS的区别首先说一下什么是HTTPSHTTPS(全称:Hyper Text Transfer Protocol over Secure Socket Layer 或 Hypertext Transfer Protocol Secure,超文本传输安全协议),是以安全为目标的HTTP通道,简单讲是HTTP的安全版。即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。 –百度百科HTTPS = HTTP+ 加密 + 认证 + 完整性保护最主要是在应用层和传输层中间加了一个SSL(安全套阶层),通常,HTTP 直接和 TCP 通信。当使用 SSL 时,则演变成先和 SSL 通信,再由 SSL 和 TCP 通信。HTTP与HTTPS的区别:(1) HTTP是明文传输,HTTPS是经过SSL加密后进行传输,只有客户端和服务端根据公钥和私钥进行加密和解密能看到,中间任何传输环节无法获取传输信息,所以HTTPS比HTTP安全(2) HTTPS需要到数字证书认证机构进行购买(3) HTTP服务器默认端口是80,HTTPS服务器默认端口是443本文主要介绍HTTP,关于HTTPS主要就介绍这么多吧。HTTP2本想说点HTTP2的知识,奈何自己是小白,放个百度百科的链接吧 HTTP2。等后续随着不断的学习,再回来更新本文。另外放一个HTTP1.1与HTTP2请求与相应对比的demo的链接HTTP/2 is the future of the Web, and it is here!最后,本文主要介绍了一些HTTP在web开发中的基础知识,关于概念和图解流程的截图基本上都是来自《TCP/IP详解 卷1:协议》、《图解HTTP》、《HTTP权威指南》,可放心参考。笔者功力实在有限,如有问题,请大家多多指出,相互学习和进步,也希望通过我的学习与实践过程,整理出的笔记能对大家有所帮助,谢谢。本文参考链接:TCP/IP详解 卷1:协议图解HTTPHTTP权威指南跨域资源共享 CORS 详解–阮一峰 ...

January 22, 2019 · 2 min · jiezi

web开发中,必须要了解的HTTP相关知识

本文已同步到github, web开发中,必须要了解的HTTP相关知识,欢迎收藏,欢迎start。本文主要记录与HTTP相关的具体概念和知识,关于HTTP协议的诞生和历史发展,不多做介绍,自己但是既然是写HTTP,顺带说两句,上下文也能衔接的上。CERN(欧洲核子研究组织)的蒂姆 • 伯纳斯 - 李(Tim BernersLee)博士提出了一种能让远隔两地的研究者们共享知识的设想,于是HTTP慢慢的诞生了。另外,HTTP协议是无状态可以,于是为了保存用户的状态,cookie诞生了。HTTP协议是建立在TCP连接之上的,当浏览器输入URL进行访问,浏览器冲URL中解析出主机名和端口,浏览器建立一条与web服务器的连接,然后才进行http请求。TCP连接的建立与终止建立TCP连接(三次握手)在客户端与服务端进行http通信之前,需要建立TCP连接,这时需要三次握手(1) 请求新的TCP连接,客户端发送一个小的TCP分组,这个分组设置一个特殊的SYN标记,表明是一个客户端请求。(2) 如果服务器接受这个连接,就会对一些连接参数进行计算,并向客户端回送一个TCP分组,发送SY和ACK标记,表明连接请求已经被接受(3) 最后,客户端向服务器回送一条确认消息,通知服务器连接已经建立。断开TCP连接(四次断开)建立一个连接需要三次握手,而终止一个连接要经过4次握手。这由TCP的半关闭(half-close)造成的。既然一个TCP连接是全双工(即数据在两个方向上能同时传递),因此每个方向必须单独地进行关闭。这原则就是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向连接。当一端收到一个FIN,它必须通知应用层另一端几经终止了那个方向的数据传送。发送FIN通常是应用层进行关闭的结果。(1) 客户端发送FIN标记到服务器,表明客户端发起关闭连接(2) 服务器接收客户端的FIN标记并,向客户端发送FIN的ACK确认标记(3) 服务器发送FIN到客户端,服务器关闭连接(4) 服务器端发送一个FIN的ACK确认标记,确认连接关闭建立持久连接的请求和响应交互:使用wireshark进行数据抓包:这里向大家推荐一款抓包软件Wireshark,可以用来分析TCP连接的建立和断开过程,以及抓取HTTP请求和相应的信息等,下面是我进行一次客户端和服务端通信的抓包数据截图:HTTP报文HTTP协议报文是应用程序之间发送的数据块,也就是客户端和服务端用于交互的信息。客户端的报文叫做请求报文,服务器端的报文叫做响应报文。HTTP报文组成HTTP报文由起始行、首部和实体的主体(也称报文主体或主体)组成。起始行和首部以一个回车符和换行符作为结束,主体部分可以是二进制数据,也可以为空。1. 起始行请求报文起始行:请求报文起始行说明了要做什么,由请求方法 、请求URI和协议版本构成。GET /index.html HTTP/1.1响应报文起始行:响应报文的起始行,由协议版本、状态码和原因短语构成。HTTP/1.1 200 OK // OK就是原因短语2. 首部首部字段分类1.通用首部客户端和服务端都可以使用的首部通用首部字段表:2.请求首部请求报文特有的首部,为服务器提供了一些额外的信息,补充了请求的附加内容、客户端信息、响应内容相关的优先级等信息。请求首部字段3.响应首部响应报文特有的字段响应首部字段表:4.实体首部用于针对请求报文和响应报文主体部分使用的首部5.扩展首部扩展首部是非标准的首部,由应用程序开发者创建,但还未添加到已批准的HTTP标准中去。http状态码状态码的职责是当客户端向服务器端发送请求时,描述返回的请求结果。借助状态码,用户可以知道服务器端是正常处理了请求,还是出现了错误。状态码分类:状态码区间类别100~199信息性状态码200~299成功状态码300~399重定向状态码400~499客户端错误状态码500~599服务器错误状态码常用状态码列表:状态码原因短语含义200OK表示从客户端发来的请求在服务器端被正常处理了204No Content该状态码代表服务器接收的请求已成功处理,但在返回的响应报文中不含实体的主体部分。另外,也不允许返回任何实体的主体。301Moved Permanently永久重定向,该状态码表示请求的资源已被分配了新的 URI,以后应使用资源现在所指的 URI302Found临时性重定向,该状态码表示请求的资源已被分配了新的 URI,希望用户(本次)能使用新的 URI 访问303See Other303 状态码和 302 Found 状态码有着相同的功能,但 303 状态码明确表示客户端应当采用 GET 方法获取资源,这点与 302 状态码有区别304Not Modified缓存307Temporary Redirect临时重定向,和302一样400Bad Request该状态码表示请求报文中存在语法错误。当错误发生时,需修改请求的内容后再次发送请求。另外,浏览器会像 200 OK 一样对待该状态码401Unauthorized该状态码表示发送的请求需要有通过 HTTP 认证(BASIC 认证、DIGEST 认证)的认证信息403Forbidden该状态码表明对请求资源的访问被服务器拒绝了404Not Found该状态码表明服务器上无法找到请求的资源500Internal Server Error该状态码表明服务器端在执行请求时发生了错误。也有可能是 Web应用存在的 bug 或某些临时的故障502Bad Gateway网关错误503Service Unavailable该状态码表明服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。如果事先得知解除以上状况需要的时间,最好写入RetryAfter 首部字段再返回给客户端HTTP中不同场景下,首部字段的作用1. CORS 跨域资源共享跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的Web应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。 –MDN下面使用nodejs来搭建一个简单的服务器,来介绍一个跨域问题的解决方法// index.html<!DOCTYPE html><html><head> <meta charset=“utf-8” /> <title>CORS</title></head><body> Hello World<script> fetch(‘http://127.0.0.1:8081’)</script></body></html>// server.jsconst http = require(‘http’)http.createServer(function(req, res) { res.writeHead(‘200’, { ‘Access-Control-Allow-Origin’: ‘http://localhost:8082’ })}).listen(8081)在源地址为 http://localhost:8082 下,请求http://localhost:8081,是跨域请求,浏览器会自动在request Header中发送Origin首部字段,并把值设置为来自哪个源,本例为http://localhost:8081。服务器需要在响应头中设置Access-Control-Allow-Origin,来告知浏览器可以处理返回的数据。如果响应头中不设置Access-Control-Allow-Origin则会报错,但是返回状态码为200,跨域实际上是浏览器本身的一个安全机制。// server2.js// 启动8082端口服务,在浏览器中访问http://127.0.0.1:8082,会返回index.html内容const http = require(‘http’)const fs = require(‘fs’)http.createServer(function(req, res) { var page = fs.readFileSync(‘index.html’, ‘utf-8’) res.writeHead(200, { ‘Content-Type’: ’text/html’ }) res.end(page)}).listen(8082)关于CORS跨域请求的分类:1.简单请求:需要同时满足以下的条件就是简单请求(1)请求方法:GET、POST、HEAD(2)请求头不能为以下其他字段之外AcceptAccept-LanguageContent-LanguageContent-Type的值必须为application/x-www-form-urlencoded、multipart/form-data、text/plain之一2.非简单请求:非简单请求是当请求信息不满足简单请求的条件,浏览器就发送方法为OPTIONS的预请求,包含自己请求的方法及需要使用的请求头字段,在得到服务器响应允许之后,浏览器会按照想要使用的请求方法及头信息再发一次请求。现在修改以下上面的例子:// index.html<!DOCTYPE html><html><head> <meta charset=“utf-8” /> <title>CORS</title></head><body> Hello World<script> fetch(‘http://127.0.0.1:8081’, { method: ‘PUT’, headers: { X-Coustom-Head: ‘abc’ } })</script></body></html>// server.jsconst http = require(‘http’)http.createServer(function(req, res) { res.writeHead(‘200’, { ‘Access-Control-Allow-Origin’: ‘http://localhost:8082’ })}).listen(8081)如果服务端不进行相应的设置告诉浏览器允许跨域访问则会报错但是预请求返回状态码为200// server2.js// 启动8082端口服务,在浏览器中访问http://127.0.0.1:8082,会返回index.html内容const http = require(‘http’)const fs = require(‘fs’)http.createServer(function(req, res) { var page = fs.readFileSync(‘index.html’, ‘utf-8’) res.writeHead(200, { ‘Content-Type’: ’text/html’ }) res.end(page)}).listen(8082)现在我们修改以下 server.js// server.jsconst http = require(‘http’)http.createServer(function(req, res) { res.writeHead(‘200’, { ‘Access-Control-Allow-Origin’: ‘http://localhost:8082’, ‘Access-Control-Allow-Headers’: ‘X-Coustom-Head’, ‘Access-Control-Allow-Methods’: ‘PUT’ })}).listen(8081)重新启动node服务,访问http://locaohost:8082,可以看到在发送预请求后,浏览器会继续发送PUT请求关于CORS的其他设置这里就不多做介绍了,这里主要是用一个例子来说明以下http不同字段在跨域场景下的作用。2. 缓存 (Cache-Control的作用)本例依旧用node服务来讲解一下Cache-Control的作用,新建三个文件// index.html<!DOCTYPE html><html><head> <meta charset=“utf-8” /> <meta http-equiv=“X-UA-Compatible” content=“IE=edge”> <title>Cache-Control</title> <meta name=“viewport” content=“width=device-width, initial-scale=1”></head><body> <script src="/script.js"></script></body></html>// script.jsconsole.log(‘script.js’)// server.jsconst http = require(‘http’)const fs = require(‘fs’)http.createServer(function(req, res) { if (req.url === ‘/’) { let page = fs.readFileSync(‘index2.html’, ‘utf-8’) res.writeHead(200, { ‘Content-Type’: ’text/html’ }) res.end(page) } if (req.url === ‘/script.js’) { let page = fs.readFileSync(‘script.js’, ‘utf-8’) res.writeHead(200, { ‘Content-Type’: ’text/javascript’, ‘Cache-Control’: ‘max-age=10’ }) res.end(page) }}).listen(8082)在第一次请求script.js资源时,向服务器发送请求由于服务器返回响应时,设置Cache-Control: ‘max-age=10’时,修改script.js后,在10秒内继续请求script.js资源,则从缓存中读取,而打印信息依旧是’script.js’// script.jsconsole.log(‘script-modify.js’)更多关于缓存的知识在这里也不多介绍了,贴两张cache-control字段在请求和响应时可以设置的值和其表示含义:1. Cache-Control 缓存请求指令:2. Cache-Control 缓存响应指令:3. cookie指某些网站为了辨别用户身份、进行 session 跟踪而储存在用户本地终端上的数据(通常经过加密),当下次再访问时浏览器会将该网站的cookie发回给服务器端。cookie如果不设置过期时间,随浏览器关闭而失效,如果有需要可以设置过期时间,继续上代码例子????,新建两个文件如下// index.html<!DOCTYPE html><html><head> <meta charset=“utf-8” /> <title>Cookie</title></head><body> Cookie<script> console.log(document.cookie)</script></body></html>// server.jsconst http = require(‘http’)const fs = require(‘fs’)http.createServer(function(req, res) { if (req.url === ‘/’) { let page = fs.readFileSync(‘index.html’, ‘utf-8’) res.writeHead(200, { ‘Content-Type’: ’text/html’, ‘Set-Cookie’: [‘a=1;max-age:5’, ‘b=2;HTTPOnly’] }) res.end(page) }}).listen(8082)启动node服务,访问localhost:8082,可以看到成功设置了cookie并在响应头信息中设置了Set-Cookie字段另外关注以下打印信息,发现只有a=1,因为给b=2设置了HttpOnly属性,不允许JavaScript通过脚本来获取到cookie信息由于当再次请求时,cookie会在请求头中发送到服务器,由于cookie a=1设置了5秒后过期,在5秒后刷新页面,请求头中的cookie只有a=1在5秒内发送二次请求,cookie a=1没有失效,在请求头中cookie a=1;b=2都会发送到服务器另外对于cookie的其他设置如expires、domain等在这里也不多做介绍了4. 重定向当服务端返回301、302、307等状态码都代表资源已经被重定向到其他位置,301表示永久改变URI,302和307表示临时重定向到某个URI本例举一个服务器返回302状态码的例子,直接上代码:// server.jsconst http = require(‘http’);const fs = require(‘fs’)http.createServer((req, res) => { if (req.url === ‘/’) { res.writeHead(302, { ‘Location’: ‘/redirect’ }) res.end() } if (req.url === ‘/redirect’) { res.end(‘redirect’) }}).listen(8082);访问localhost:8082, 服务器返回302状态码时,在相应头中设置Location首部字段,浏览器会继续发送请求到重定向的地址HTTP与HTTPS的区别首先说一下什么是HTTPSHTTPS(全称:Hyper Text Transfer Protocol over Secure Socket Layer 或 Hypertext Transfer Protocol Secure,超文本传输安全协议),是以安全为目标的HTTP通道,简单讲是HTTP的安全版。即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。 –百度百科HTTPS = HTTP+ 加密 + 认证 + 完整性保护最主要是在应用层和传输层中间加了一个SSL(安全套阶层),通常,HTTP 直接和 TCP 通信。当使用 SSL 时,则演变成先和 SSL 通信,再由 SSL 和 TCP 通信。HTTP与HTTPS的区别:(1) HTTP是明文传输,HTTPS是经过SSL加密后进行传输,只有客户端和服务端根据公钥和私钥进行加密和解密能看到,中间任何传输环节无法获取传输信息,所以HTTPS比HTTP安全(2) HTTPS需要到数字证书认证机构进行购买(3) HTTP服务器默认端口是80,HTTPS服务器默认端口是443本文主要介绍HTTP,关于HTTPS主要就介绍这么多吧。HTTP2本想说点HTTP2的知识,奈何自己是小白,放个百度百科的链接吧 HTTP2。等后续随着不断的学习,再回来更新本文。另外放一个HTTP1.1与HTTP2请求与相应对比的demo的链接HTTP/2 is the future of the Web, and it is here!最后,本文主要介绍了一些HTTP在web开发中的基础知识,关于概念和图解流程的截图基本上都是来自《TCP/IP详解 卷1:协议》、《图解HTTP》、《HTTP权威指南》,可放心参考。笔者功力实在有限,如有问题,请大家多多指出,相互学习和进步,也希望通过我的学习与实践过程,整理出的笔记能对大家有所帮助,谢谢。本文参考链接:TCP/IP详解 卷1:协议图解HTTPHTTP权威指南跨域资源共享 CORS 详解–阮一峰 ...

January 21, 2019 · 2 min · jiezi

前端性能监控

web-monitoring[功能列表][x] 允许用户创建多个监测站点[x] 从不同维度统计用户访问情况[x] 自定义查询时间[x] 多种图表展示[x] 支持自定义上报(js错误,api请求)[ ] 自定义阈值[ ] 自动报警功能[技术支持][x] 前端:Angular5+,ant-design[x] 后端:Nodejs+Express[x] 数据库:MondoDB前端监控平台专注于 Web 端体验数据监控。监测 Web 页面的健康度的三个方面:页面打开速度(测速)页面稳定性(JS Error)外部服务调用成功率(API)然后从不同纬度去分析用户体验。访问页面访问速度API请求地理终端参考:http://fex.baidu.com/blog/201…阿里云前端监控页面打开速度网络耗时数据可以借助前面提到 Navigation Timing 接口获取,与之类似的还有Resource Timing,可以获取页面所有静态资源的加载耗时。通过此接口可以轻松获取 DNS、TCP、首字节、html 传输等耗时,Navigation Timing 的接口示意图如下所示:API请求默认情况下,使用XMLHTTP拦截用户请求,在请求成功/失败后,统计时间,上报请求。用户可使用__ml.api(success, time, code, msg)手动上报。 success:上传是否成功(true/false ) time:耗时(ms) code:返回码 msg:消息(string/object)JS错误默认情况下,使用window.onError去监听用户错误脚本,自动上报。用户使用的有些前端框架会捕获js错误,错误信息不会抛至window.onError,这种情况需用户手动调用。例如在Angular2+,在你的框架全局捕获错误的地方调用__ml.error(errorObj) export class MyErrorHandler implements ErrorHandler { handleError(error) { console.error(error); window.__ml && window.__ml.error && window.__ml.error(error.stack || error); } } @NgModule({ declarations: [], imports: [], providers: [{ provide: ErrorHandler, useClass: MyErrorHandler }], bootstrap: [] }) export class AppModule { }在Vue:import Vue from ‘vue’const errorHandler = (error, vm)=>{ console.error(error); window.__ml && window.__ml.error && window.__ml.error(error);}Vue.config.errorHandler = errorHandler;Vue.prototype.$throw = (error)=> errorHandler(error,this);如何使用(easy!!!)网页地址:WEB-MONITOR第一步:在监控站点中创建一个站点。第二步:复制应用配置中的探针到你需要监控的站点(index.html)页面。大功告成! ...

January 18, 2019 · 1 min · jiezi

vultr vps 限时优惠,充值即送$50测试金额

限时优惠充值即送 $50 的一个月测试金额vultr 今天开始推出了一个限时优惠活动,充值即送50美元的测试金额(注:有效期30天,相当于可以免费随便使用一个月了)。附上优惠链接 点我进入需要注意的是,次优惠只有通过以上链接打开官网再进行注册有效。此优惠是限时优惠,需要的赶紧注册抓紧时间呦亲测有效,附上亲测图:可以看到,充值金额最低从 10美元 开始,最高不限。充值的时候会直接多上 $50美元,官方主要是为了让新人更好的了解vultr主机,所以这50美元是有30天的有效期。因为vultr主机的性价比很高,所以这50美元的账户一个月里还是可以干很多事情的。vultr主机特点简单说明vultr主机的费用是按照上面标注的每月价格分摊到每个小时,然后按照实际使用的小时数来算的,销毁主机不适用的话是不收费的。有包括东京、新加坡美国硅谷等全球机房可以选择随时可以创建新的主机、摧毁支持IPV6等支持支付宝、微信、以及信用卡等充值方式,方便国人使用建站、搭梯子等首选方案

January 17, 2019 · 1 min · jiezi

Don't Make Me Think - 简单至上的交互设计书摘

Don’t Make Me Think. A Common Sense Approach to Web Usability.早些年从后端逐步迈入前端,也没有什么艺术细菌,也多是从技术角度考虑而忽略了产品、交互以及视觉。在产品经理必读书单以及年度总结中,都会有想要逐步培养些产品思维,无论是自己平日工作中拧紧的螺丝钉还是手工做的小产品,都想赋予些设计而不仅仅是功能的堆叠。Don’t Make Me Think 成书较早,提供的一些交互设计事例或者具体的交互方式可能有些成就,但是贯穿全书的简单至上的理念也是让我在某些方面有了更深刻的认识;也可以前往 Awesome-CS-Books-Warehouse 浏览更多产品、交互、软件工程方面的书籍。Here are a few things you won’t find in this book:Hard and fast usability rulesPredictions about the future of technology and the WebBad-mouthing of poorly designed sites and apps.You’ll find a lot of different definitions of usability often breaking it down into attributes like:Useful: Does it do something people need done?Learnable: Can people figure out how to use it?Memorable: Do they have to relearn it each time they useEffective: Does it get the job done?Efficient: Does it do it with a reasonable amount of time and effort?Desirable: Do people want it?Delightful: Is using it enjoyable, or even fun?Person of average (or even below average) ability and experience can figure out how to use the thing [i.e., it’s learnable] to accomplish something[effective] without it being more trouble than it’s worth [efficient].Chapter 1. Dont make me thinkWhat’s the most important thing I should do if i want to make sure my site or app is easy to use? It’s not “Nothing important should ever be more than two clicks away” or “Speak the user’ s language” or “Be consistent.” It’s Dont make me think.All kinds of things on a Web page can make us stop and think unnecessarily. Your goal should be for each page or screen to be self-evident, so that just by looking at it the average user. Here’s the rule: If you can t make something self-evident, you at least need to make it self-explanatory.On a self-explanatory page, it takes a little thought to get it, but only a little. The appearance of things (like size, color, and layout), their well-chosen names, and the small amounts of carefully crafted text should all work together to create a sense of nearly effortless understanding.Chapter 2. How we really use the WebWhen we’re creating Sites, we act as though people are going to pore over each page, reading all of our carefully crafted text, figuring out how we’ve organized things, and weighing their options before deciding which link to click.What they actually do most of the time (if were lucky is glance at each new page, scan some of the text, and click on the first link that catches their interest or vaguely resembles the thing they’re looking for.FACT OF LIFE #1: We dont read pages. We scan themFACT OF LIFE #2: We dont make optimal choices. We satisfice.FACT OF LIFE #3: We don’t figure out how things work. We muddle through.Chapter 3. Billboard Design 101DESIGNING FOR SCANNING, NOT READINGTake advantage of conventionsLike Stop signs, Given how crucial it is that drivers see and recognize them at a glance at a distance, in all kinds of weather and lighting conditions, it’s a really good thing that all stop signs look the same.Create effective visual hierarchiesThe more important something is, the more prominent it is.Things that are related logically are related visually.Things are"nested visually to show what’s part of whatBreak pages up into clearly defined areasWeb page scanning suggest that users decide very quickly in their initial glances which parts of the page are likely to have useful information and then rarely look at the other parts.Banner blindness-the ability of users to completely ignore areas they think willcontain ads–is just the extreme case.Make it obvious what’s clickableAs we scan a page, we’re looking for a variety of visual cues that identify things as clickable(or tappable?‘on touch screens )-things like shape(buttons, tabs, etc.), location(in a menu bar, for instance), and formatting(color and underlining).Eliminate distractions/noiseOne of the great enemies of easy-to-grasp pages is visual noise. There are really three different kinds of noise:Shouting: Automated slideshows, animation, pop-ups, and the never-ending array of new attention-grabbing ad formats! Shouting is usually the result of a failure to make tough decisions about which elements are really the most important and then create a visual hierarchy that guides users to them first.Disorganization: This is a sure sign that the designer doesn’t understand the importance of using grids to align the elements on a page.Clutter: It’s hard to find and focus on the messages you actually care about.A good idea to start with the assumption that everything is visual noise (the “presumed guilty until proven innocent” approach) and get rid of anything that’s not making a real contribution.Format content to support scanningUse plenty of headings.Keep paragraphs short.Use bulleted lists.Highlight key terms.Chapter 4. Animal, Vegetable, or Mineral? WHY USERS LIKE MINDLESS CHOICESIt doesn’t matter how many times I have to click, as long as each click is a mindless, unambiguous. But over time I’ve come to think that what really counts is not the number of clicks it takes me to get to what I want (although there are limits), but rather how hard each click is—the amount of thought required and the amount of uncertainty about whether I’m making the right choice.Some assistance may be requiredThis guidance works best when it’sBrief: The smallest amount of information that will help meTimely: Placed so I encounter it exactly when I need itUnavoidable: Formatted in a way that ensures that I’ll notice itChapter 5. Omit needless wordsGet rid of half the words on each page, then get rid of half of what’s left.Happy talk must die: It’s the introductory text that’s supposed to welcome us to the site and tell us how great it is or to tell us what we’re about to see in the section we’ve just entered. Happy talk is like small talk—content-free, basically just a way to be sociable. But most Web users don’t have time for small talk; they want to get right to the point. You can—and should—eliminate as much happy talk as possible.Instructions must die: Your objective should always be to eliminate instructions entirely by making everything self-explanatory, or as close to it as possible.Chapter 6. Street signs and BreadcrumbsWeb Navigation 101In many ways, you go through the same process when you enter a Web site.You’re usually trying to find something.You decide whether to ask first or browse first. The difference is that on a Web site there’s no one standing around who can tell you where things are. The Web equivalent of asking directions is searching—typing a description of what you’re looking for in a search box and getting back a list of links to places.Some people (Jakob Nielsen calls them “search-dominant” users) will almost always look for a search box as soon as they enter a site. Other people (Nielsen’s “link-dominant” users) will almost always browse first, searching only when they’ve run out of likely links to click or when they have gotten sufficiently frustrated by the site.Web experience is missing many of the cues we’ve relied on all our lives to negotiate spaces. Consider these oddities of Web space:No sense of scale.No sense of direction.No sense of location.This lack of physicality is both good and bad. On the plus side, the sense of weightlessness can be exhilarating and partly explains why it’s so easy to lose track of time on the Web—the same as when we’re “lost” in a good book.On the negative side, If you look up navigation in a dictionary, it’s about doing two things: getting from one place to another, and figuring out where you are.Two of the purposes of navigation are fairly obvious: to help us find whatever it is we’re looking for and to tell us where we are.Persistent navigationJust having the navigation appear in the same place on every page with a consistent look gives you instant confirmation that you’re still in the same site.Persistent navigation should include the four elements you most need to have on hand at all times:On pages where a form needs to be filled in, the persistent navigation can sometimes be an unnecessary distraction.One of the ways navigation can counteract the Web’s inherent “lost in space” feeling is by showingme where I am in the scheme of things.PagesThere are four things you need to know about page names:Every page needs a name. Just as every corner should have a street sign, every page should have a name.The name needs to be in the right place. In the visual hierarchy of the page, the page name should appear to be framing the content that is unique to this pageThe name needs to be prominent.The name needs to match what I clicked.BreadcrumbsLike “You are here” indicators, Breadcrumbs show you where you are. They’re called Breadcrumbs because they’re reminiscent of the trail of crumbs Hansel dropped in the woods so he and Gretel could find their way back home.Breadcrumbs show you the path from the Home page to where you are and make it easy to move back up to higher levels in the hierarchy of a site.Put them at the top.Use > between levels.Boldface the last item.TabsTabs are one of the very few cases where using a physical metaphor in a user interface actually works. Here’s why I like them:They’re self-evident.They’re hard to miss.They’re slick.If the page is well designed, when your vision clears you should be able to answer these questions without hesitation:What site is this? (Site ID)What page am I on? (Page name)What are the major sections of this site? (Sections)What are my options at this level? (Local navigation)Where am I in the scheme of things? (“You are here” indicators)How can I search?Here’s how you perform the trunk test:Step 1: Choose a page anywhere in the site at random, and print it.Step 2: Hold it at arm’s length or squint so you can’t really study it closely.Step 3: As quickly as possible, try to find and circle each of these items:Site IDPage nameSections (Primary navigation)Local navigation“You are here” indicator(s)SearchChapter 7. The Big Bang Theory of Web DesignTHE IMPORTANCE OFGETTING PEOPLE OFFON THE RIGHT FOOT.As quickly and clearly as possible, the Home page needs to answer the four questions I have in myhead when I enter a new site for the first time:This is what I call the Big Bang Theory of Web Design. Like the Big Bang Theory, it’s based on the idea that the first few seconds you spend on a new Web site or Web page are critical.Everything on the Home page can contribute to our understanding of what the site is. But there arethree important places on the page where we expect to find explicit statements of what the site isabout.The tag lineOne of the most valuable bits of real estate is the space right next to the Site ID. When we see a phrase that’s visually connected to the ID, we know it’s meant to be a tagline, and so we read it as a description of the whole site. We’ll look at taglines in detail in the next section.The Welcome blurb.The Welcome blurb is a terse description of the site, displayed in aprominent block on the Home page, usually at the top left or center of the content space so it’s the first thing that catches your eye.The “Learn more.”Innovative products and business models tend to require a fair amount ofexplanation, often more than most people have the patience for. But people have become accustomed to watching short videos on their computers and mobile devices. As a result, people have now come to expect a short explanatory video on most sites and are often willing to watch them.Chapter 8. “The Farmer and the Cowman Should Be Friends” | 论内部协作Left to their own devices, Web teams aren’t notoriously successful at making decisions about usability questions. Most teams end up spending a lot of precious time rehashing the same issues over and over.The point is, it’s not productive to ask questions like “Do most people like pull-down menus?” The right kind of question to ask is “Does this pull-down, with these items and this wording in this context on this page create a good experience for most people who are likely to use this site?”And there’s really only one way to answer that kind of question: testing. You have to use the collective skill, experience, creativity, and common sense of the team to build some version of the thing (even a crude version), then watch some people carefully as they try to figure out what it is andhow to use it.Chapter 9. Usability testing on 10 cents a dayUsability testing has been around for a long time, and the basic idea is pretty simple: If you want to know whether something is easy enough to use, watch some people while they try to use it and note where they run into problems. Testing one user is 100 percent better than testing none. Testing one user early in the project is better than testing 50 near the end.Repeat after me: Focus groups are not usability testsHere’s the difference in a nutshell:In a focus group, a small group of people (usually 5 to 10) sit around a table and talk about things, like their opinions about products, their past experiences with them, or their reactions to new concepts. Focus groups are good for quickly getting a sampling of users’ feelings and opinions about things.Usability tests are about watching one person at a time try to use something (whether it’s a Web site, a prototype, or some sketches of a new design) to do typical tasks so you can detect and fix the things that confuse or frustrate them.The main difference is that in usability tests, you watch people actually use things, instead of justlistening to them talk about them.Do-it-yourself usability testingThe purpose of this kind of testing isn’t to prove anything. You don’t need to find all of the problems. In fact, you’ll never find all of the problems in anything you test. And it wouldn’t help if you did, because of this fact:You can find more problems in half a day than you can fix in a month.A typical one-hour test would be broken down something like this:Welcome (4 minutes). You begin by explaining how the test will work so the participant knows what to expect.The questions (2 minutes). Next you ask the participant a few questions about themselves. This helps put them at ease and gives you an idea of how computer-savvy and Web-savvy they are.The Home page tour (3 minutes). Then you open the Home page of the site you’re testing and ask the participant to look around and tell you what they make of it. This will give you an ideaof how easy it is to understand your Home page and how much the participant already knows your domain.The tasks (35 minutes). This is the heart of the test: watching the participant try to perform a series of tasks (or in some cases, just one long task). Again, your job is to make sure the participant stays focused on the tasks and keeps thinking aloud.If the participant stops saying what they’re thinking, prompt them by saying—wait for it — “What are you thinking?” (For variety, you can also say things like “What are you looking at?” and “What are you doing now?”)During this part of the test, it’s crucial that you let them work on their own and don’t do or say anything to influence them. Don’t ask them leading questions, and don’t give them any clues or assistance unless they’re hopelessly stuck or extremely frustrated. If they ask for help, just say something like “What would you do if I wasn’t here?”Probing (5 minutes). After the tasks, you can ask the participant questions about anything that happened during the test and any questions that the people in the observation room would likeyou to ask.Wrapping up (5 minutes). Finally, you thank them for their help, pay them, and show them to the door.The debriefing: Deciding what to fixHere are some of the types of problems you’re going to see most often:Users are unclear on the concept.The words they’re looking for aren’t there.There’s too much going on.FOCUS RUTHLESSLY ON FIXING THE MOST SERIOUS PROBLEMS FIRSTChapter 10. Mobile: It’s not just a city in Alabama anymoreSo, what’s different about usability when you’re designing for use on a mobile device?In one sense, the answer is: Not much. The basic principles are still the same. If anything, people aremoving faster and reading even less on small screens. Most of the challenges in creating good mobile usability boil down to making good tradeoffs.Which parts do you leave out? One approach was Mobile First. Instead of designing a full-featured (and perhaps bloated) version of your Web site first and then paring it down to create the mobile version, you design the mobile version first based on the features and content that are most important to your users. Then you add on more features and content to create the desktop/full versionBreeding chameleonsIf there are two things I can tell you about scalable design (a/k/a dynamic layout, fluid design, adaptive design, and responsive design), they’re these:It tends to be a lot of work.It’s very hard to do it well.In the meantime, here are three suggestions:Allow zooming.Don’t leave me standing at the front door. You tap on a link in an email or a social media site and instead of taking you to the article in question it takes you to the mobile Home page, leaving you to hunt for the thing yourself.Always provide a link to the “full” Web site.Be careful that your responsive design solutions aren’t loading up pages with huge amounts of code and images that are larger than necessary for the user’s screen.Don’t hide your affordances under a bushelAffordances are visual clues in an object’s design that suggest how we can use it.[](https://ww1.sinaimg.cn/large/...This is not to say that all affordances need to hit you in the face. They just have to be visible enoughthat people can notice the ones they need to get their tasks done.Flat design has a tendency to take along with it not just the potentially distracting decoration but also the useful information that the more textured elements were conveying.Delightful & LearnableDelightful apps usually come from marrying an idea about something people would really enjoy being able to do, but don’t imagine is possible, with a bright idea about how to use some new technology to accomplish it.One of the biggest problems with apps is that if they have more than a few features they may not be very easy to learn.That’s not to say that no one in the real world learns how to use it. It gets great reviews and isconsistently a best seller. But I have to wonder how many people who bought it have never masteredit, or how many more sales they could make if it were easier to learn.There’s one more attribute that’s important: memorability. Once you’ve figured out how to use an app,will you remember how to use it the next time you try or will you have to start over again fromscratch?Usability testing on mobile devicesHere are some of the issues you have to deal with:Do you need to let the participants use their own devices?Do they need to hold the device naturally, or can it be sitting on a table or propped up on a stand?What do the observers need to see (e.g., just the screen, or both the screen and the participant’s fingers so they can see their gestures)? And how do you display it in the observation room?How do you create a recording?Until better technology-based solutions come along, here’s what I’d lean toward:Use a camera pointed at the screen instead of mirroring.Attach the camera to the device so the user can hold it naturally.Don’t bother with a camera pointed at the participant.Chapter 11. Usability as common courtesy(绅士)Here are a few of the things that tend to make users feel like the people publishing a site don’t have their best interests at heart(避免做以下的事):Hiding information that I want. The most common things to hide are customer support phone numbers, shipping rates, and prices.Punishing me for not doing things your way. I should never have to think about formatting data: whether or not to put dashes in my Social Security number, spaces in my credit card number, or parentheses in my phone number.Asking me for information you don’t really need.Shucking and jiving me. We’re always on the lookout for faux sincerity, and disingenuous attempts to convince me that you care about me can be particularly annoying.Putting sizzle in my way. Having to wade through pages bloated with feel-good marketing photos makes it clear that you don’t understand—or care—that I’m in a hurry.Your site looks amateurish. You can lose goodwill if your site looks sloppy, disorganized, or unprofessional, like no effort has gone into making it presentable.The good news is that even if you make mistakes, it’s possible to restore my goodwill by doing things that convince me that you do have my interests at heart. Most of these are just the flip side of the other list:Know the main things that people want to do on your site and make them obvious andeasTell me what I want to know.Save me steps wherever you can. ...

January 16, 2019 · 17 min · jiezi

Web事件总结

本文首发于公众号:符合预期的CoyPanweb中的事件事件并不是JavaScript的核心部分,他们是在浏览器的Web Api中定义的。下面列举的几种情况,都属于发生了事件。用户在某个元素上点击鼠标或悬停光标。用户在键盘中按下某个按键。用户调整浏览器的大小或者关闭浏览器窗口。一个网页停止加载。提交表单。播放、暂停、关闭视频。发生错误。我们可以在代码中使用事件处理器来处理各种事件。事件模型假设我们有这么一段html代码:<html> <body> <table> <tbody> <tr> <td>Shady Grove</td> <td>Aeolain</td> </tr> <tr> <td>Over the River, Charile</td> <td>Dorian</td> </tr> </tbody> </table> </body></html>如果我们点击over the Rive, Charile,整个事件流程如下:事件会经历三个阶段,上图中分别由红色、蓝色、绿色标出。第一阶段为红色,事件流从根元素一直走到点击的目标元素,这个过程称为捕获。第二阶段为蓝色。这个阶段中,会处理点击事件,为事件加上各种属性等。第三阶段为绿色,事件又回重新回到根元素,这个过程称为冒泡。在整个事件流中,我们在事件流经过的任何元素上,都能监听到该事件,从而进行处理。一般建议在冒泡阶段处理事件,这样可以最大限度的兼容各种浏览器。注意:blur、focus、load、unload 等几个事件不会冒泡。原因是在于:这些事件仅发生于自身上,而它的任何父节点上的事件都不会产生,所有不会冒泡。我们可以查看事件的bubbles属性,来判断该事件是否可以冒泡。事件处理EventTargetEventTarget是一个由可以接收事件的对象实现的接口,并且可以为它们创建侦听器。Web中的所有事件处理器都是由EventTarget"提供"的。addEventListener该方法将指定的监听器注册到 EventTarget 上,当该对象触发指定的事件时,指定的回调函数就会被执行。 事件目标可以是一个文档上的元素 Element,Document和Window或者任何其他支持事件的对象 (比如 XMLHttpRequest)。其标准语法如下:target.addEventListener(type, listener[, options]);target.addEventListener(type, listener[, useCapture]);type: 字符串。表示事件类型,比如: click。listener:函数。事件触发时的回调函数。这个函数会接受一个Event事件对象。这个Event事件对象中,包含了以下重要的属性和方法(这里只列举出常用的)属性值bubbles只读。一个布尔值,表示该事件是否能够在DOM中冒泡。cancelBubble通过在一个事件处理程序返回前设置这个属性的值为真,来阻止事件冒泡。cancelable只读。一个布尔值,用来表示这个事件是否可以取消。currentTarget只读。注册事件监听的对象。target只读。对事件起源目标的引用。方法作用preventDefault取消默认事件stopPropagation阻止事件冒泡options :对象。指定一个listener的配置参数。属性值capture布尔值。如果为true,表示该listener会在捕获过程中执行。如果为false,listener会在冒泡过程中执行。默认为false。once布尔值。默认为false。如果为true,listener只会执行一次,并且执行后会被自动移除。passive布尔值。如果为true,则无法调用preventDefault来阻止默认事件。默认为false。这个属性有一个需要注意的地方。在safari浏览器的页面滚动事件中,这个值是默认的true。所以要阻止safari中的页面滚动,需要手动将这个值设为false。useCapture:布尔值,可选。默认为false,事件在冒泡过程中触发listener。removeEventListener删除使用addEventListener注册到target上的事件。标准语法:target.removeEventListener(type, listener[, options]);target.removeEventListener(type, listener[, useCapture]);为了提高页面性能,我们在处理完某事件,并且不用继续监听该事件时,可以将之前注册的事件监听函数移除。需要注意的是,如果注册事件时,在捕获或冒泡阶段均进行了监听,那么移除时需要分别移除。currentTarget 与 target的区别在事件处理函数中,我们经常会使用到事件的这两个属性。currentTarget表示注册事监听的对象。target表示事件起源的对象。举个例子:<div id=“father”> <div id=“child1”>child1</div> <div id=“child2”>child2</div> <div id=“child3”>child3</div></div>document.getElementById(‘father’).addeventListener(‘click’, function(e){ console.log(e.currentTarget); console.log(e.target);});我们将事件处理函数绑定在father上。现在,如果我们点击的是child1,由于child1是事件源,那么e.target就是child1。而我们的事件处理函数是绑定在father上的,所以,e.currentTarget就是father。这一点在开发过程中需要特别注意。事件代理这个概念依赖于这样一个事实,如果你想要在大量子元素中单击任何一个都可以运行一段代码,您可以将事件监听器设置在其父节点上,并将事件监听器气泡的影响设置为每个子节点,而不是每个子节点单独设置事件监听器。举个例子:<ul> <li>1<li> <li>2<li> <li>3<li> <li>4<li> <li>5<li> <li>6<li> <li>7<li> <li>8<li> <li>9<li></ul>我们要实现点击每一个li的时候,输出li内对应的数字,我们当然可以直接在所有的li上分别绑定一个事件。但是这样会造成内存的浪费。我们可以只在ul上绑定一个事件,根据事件的target来获取当前点击的li,拿到该li内对应的数字。document.querySelector(‘ul’).addEventListener(‘click’, function(e){ if(e.target && e.target.nodeName === “LI”) { console.log(e.target.innerHTML); }});自定义事件最新的DOM标准允许我们自定义事件。直接看下面的例子。var fakeNode = document.createElement(‘Coy’); // 创建一个自定义元素var evt = document.createEvent(‘Event’); // 创建一个自定义事件var evtType = ’test’; // 自定义事件的类型// 事件监听函数fakeNode.addEventListener(evtType, function(e){ console.log(e); // e.type === ’test’;}, false);// 初始化事件。// initEvent用法:event.initEvent(type, bubbles, cancelable);evt.initEvent(evtType, false, false); // 向fakeNode派发evt事件fakeNode.dispatchEvent(evt); 写在后面事件,是前端开发中的一个基础。虽然简单,但是十分重要。欢迎关注我的公众号: 符合预期的CoyPan做一名符合预期的FE ...

January 14, 2019 · 1 min · jiezi

从一次重写原生方法遇到的坑,总结一下Web中的事件系统

写在前面前段时间,我写过一篇文章前端开发中的Error以及异常捕获。 在文章中,我提到了这个问题:经过不断探索(不想再喷自己了),我找到了原因。下面一一道来。本文主要讲解自己找问题原因的思路,如果想看结论和总结,请直接跳到文末。问题复现我是在自己以前的项目中测试addEventListener的重写的。这里直接上精简后的问题代码:import React from ‘react’;import ReactDOM from ‘react-dom’;const nativeAddEventListener = EventTarget.prototype.addEventListener;EventTarget.prototype.addEventListener = function (type, func, options) { const wrappedFunc = function (…args) { try { return func.apply(this, args); } catch (e) { const errorObj = { error_msg: e.message || ‘’, error_stack: e.stack || (e.error && e.error. error_native: e }; } } return self.nativeAddEventListener.call(this, type, wrappedFunc, options);};const App = function() { return <div>11111</div>};ReactDOM.render(<App/>, document.body);运行这段代码,浏览器上一片空白,但是却没有任何报错。我一脸懵逼。问题初探索删掉那一点重写addEventListener的代码后,表现符合预期了。应该是重写那儿的问题。但是仔细看了过后,那段代码并没有什么问题。并且这段代码我在其他地方也试过,表现一直是正常的。是不是和React哪里冲突了?我使用的React版本是我搜索了react-dom源码中的addEventListener关键字,总共出现了四次。初步看了一下,并没有什么问题,只是注册了一些事件而已。没有具体分析这些代码的含义,我选择了先更换React的版本试一试,于是,我换成了15.6.2的版本。令人吃惊的是,表现符合预期了。难道真的和React的版本有关系? 在我的认知中,两个版本中最大的不同就是:React v16采用了全新的Fiber架构,而我对Fiber的理解大概就是:重新设计了react node的数据结构,模拟实现了自己的任务堆栈,结合时间分片来进行任务的调度,从而更新整个系统。另外,React有自己的一套任务系统,addEventListener和任务也是紧密相关的,难道影响到了这个?继续探索我决定从ReactDOM.render()这个方法入手,调试一下ReactDOM的源代码。之前并没有研究过React的源码,压力有点大。调试了一翻之后,我并没有发现什么问题,并且已经有点懵逼了。我准备同时调试react v15和react v16的代码,看看有什么不同。为了方便,我将问题代码全部抽了出来,全部写到了一个html文件中,并且直接引用React的cdn地址。这个时候,我发现了一个神奇的问题:直接引用cdn地址后,不管React是什么版本,就算是v16版本,也不会出现之前问题,表现都是符合预期的。我更加懵逼了。发现问题静下心来仔细观察后,我发现了,我cdn引用的都是react的production版本,而我在项目中使用的react代码,却是development版本的,难道是development和production的diff代码,导致了上面的问题。于是我重新仔细看了一下v16的development的代码,找到了代码中一段长长的注释:大意就是:在开发版本中,react不会采用try{}catch(){}的方式来捕获错误,而是会把所有开发者定义的callback用一个叫做invokeGuardedCallback的函数包裹起来,然后使用一个假的dom,监听、触发自定义事件来执行invokeGuardedCallback,并且通过一个全局的错误捕捉函数来捕获错误。在这段注释的下面,就是注释中提到的invokeGuardedCallback的代码。我仔细研究了这个invokeGuardedCallback的代码,其核心就是:function invokeGuardedCallback(name, func, context, a, b, c, d, e, f){ … var fakeNode = document.createElement(‘react’); var evt = document.createEvent(‘Event’); var evtType = ‘react-’ + (name ? name : ‘invokeguardedcallback’); var callCallback = function(){ … fakeNode.removeEventListener(evtType, callCallback, false); // 这里很重要!!! … func.apply(context, funcArgs); // 这里是真正执行react中的逻辑代码 } fakeNode.addEventListener(evtType, callCallback, false); evt.initEvent(evtType, false, false); fakeNode.dispatchEvent(evt); …} react将所有容易出错的函数,都用这个invokeGuardedCallback包了起来。每一次都重新造一个虚拟的element,然后监听其自定义事件,并且立即触发这个自定义事件。调试了这个invokeGuardedCallback后,我发现在react v16中,发现很多函数被多次执行。为什么会多次执行呢? 终于,我找到了问题的原因:我重写了addEventListener, 在函数外包了一层try{}catch(){},返回的是一个新的函数,所以,最终注册在事件监听器上的,并不是我传入的那个函数。这个时候,调用removeEventListener时,无法移除我传入addEventListener的函数。在invokeGuardedCallback中,removeEventListener的逻辑相当于并没有生效。于是,在Fiber的调度中,某个函数被多次重复执行了,而被重复执行的函数并不是幂等的,问题便产生了。问题的总结与思考问题终于定位了,一句总结,就是:重写了addEventListener,却并没有考虑到与之对应的removeEventListener,导致removeEventListener无法正常工作。下面是一些思考:一开始,如果我仔细看一下react源码中addEventListener周围的代码,或许能更早发现这个问题,就不用绕这么大一个圈了。自己对于第三方库的development版本和production版本,并没有一个很强烈的认知、意识,以前上线的不少项目,线上竟然还是用的第三方库的development版本,这个毛病,一定得改掉。分析问题的能力还很欠缺,不够敏感。考虑问题的全面性需要提高。真的不要随便重写原生方法。。。写在后面在探索这个问题的过程中,我看到了react巧妙应用自定义事件来捕获错误。于是,我全面总结一下了Web中的事件系统,也算是对基础的巩固。由于篇幅已经不够了,这里就直接放文章链接吧:谈一谈web中的事件谈一谈web中的事件欢迎关注我的公众号: 符合预期的CoyPan,这里只有干货,符合你的预期。 ...

January 14, 2019 · 1 min · jiezi

腾讯 Tars-Go 服务 Hello World——从 HTTP 开始

引言本人上一篇文章《腾讯 Tars 基础框架手动搭建》简单介绍了 Tars 框架及其搭建方法。在我们的实际应用中,目前基于 Taf / Tars,主要采用 Node.js 和 C++ 进行开发。对于 C++ 程序员来说,目前最热门的后台开发语言莫过于 Google 的 Go。Tars 框架最新的版本已经把内部的 Taf-Go 开源为 Tars-Go。作为与时俱进的程序员,当然要尝鲜啦。本文中的代码均可以在 我的 GitHub repo 中查阅。本系列文章:腾讯 Tars 基础框架手动搭建——填掉官方 Guide 的坑腾讯 Tars-Go 服务 Hello World——从 HTTP 开始腾讯 Tars-Go 服务 Hello World——RPC 通信环境准备Go 环境开发环境显然要安装好 Go 了。请注意的是,TarsGo 要求 Go 版本 1.9 以上。最新稳定版已经是 1.11 了。安装最新版即可。Go 安装好之后,请注意配置好 $GOPATH 和 $GOROOT 环境变量,建议配置为 $HOME/go 目录。尽管在 Go 1.8 之后,go 命令的运行已经不再需要程序员配置上述变量(go 会自动配置,可执行 $ go env 查看),但是 TarsGo 的脚本在执行的时候还是需要依赖。TarsGo 包执行 go 安装命令并编译:$ go get github.com/TarsCloud/TarsGo/tars$ cd $GOPATH/src/github.com/TarsCloud/TarsGo/tars/tools/tars2go && go build .$ sudo cp tars2go $GOPATH/bin上述命令会把 TarsGo 下载下来,并且将比较重要的一个命令 tars2go 安装好。代码设计TarsGo 的官方 Quick Start 文档 的第一个例子,就是使用 tars 协议进行 server-client 的通信。不过我个人觉得,要说后台服务程序的 hello world 的话,第一个应该是 http 服务嘛,毕竟程序一运行就可以看到效果,这才是 hello world 嘛。给服务命名Tars 实例的名称,有三个层级,分别是 App(应用)、Server(服务)、Servant(服务者,有时也称 Object)三级。在前文我们已经初步接触到了:比如 Tars 基础框架中的 tarsstat,其服务的完整名称即为:tars.tarsstat.StatObj。Tars 实例的名称其中一个非常重要的作用就是用于服务间名字服务寻址。而对于 HTTP 这样的直接对外提供服务的实例而言,其实这块相对不是很重要,我们更多的是以描述服务功能的角度去命名。这里我把我的 HTTP 服务命名为 amc.GoWebServer.GoWebObj创建基础框架和 TarsCpp 一样,TarsGo 也提供了一个 create_tars_server.sh 脚本用于生成 tars 服务,但却没有提供 create_http_server.sh 生成 HTTP 服务。所以这里我们就直接用它就行了:$ cd $GOPATH/src/github.com/TarsCloud/TarsGo/tars/tools$ chmod +x create_tars_server.sh$ ./create_tars_server.sh amc GoWebServer GoWeb执行后我们可以查看生成的文件,清除不需要的:$ cd $GOPATH/src/amc/GoWebServer$ rm -rf GoWeb.tars client debugtool$ chmod +x start.sh$ ls -ltotal 44-rw-rw-r– 1 centos centos 303 Jan 5 22:09 GoWebImp.go-rw-rw-r– 1 centos centos 964 Jan 5 22:09 GoWebServer.conf-rw-rw-r– 1 centos centos 422 Jan 5 22:09 GoWebServer.go-rw-rw-r– 1 centos centos 252 Jan 5 22:09 makefile-rw-rw-r– 1 centos centos 59 Jan 5 22:09 start.shdrwxrwxr-x 2 centos centos 4096 Jan 5 22:09 vendor其实留下的,各文件里的内容,实际上我们都要完全替换掉的……首先是修改 makefile,自动生成的 makefile 内容是这样的:$ cat makefile APP := amcTARGET := GoWebServerMFLAGS :=DFLAGS :=CONFIG := clientSTRIP_FLAG:= NJ2GO_FLAG:= libpath=${subst :, ,$(GOPATH)}$(foreach path,$(libpath),$(eval -include $(path)/src/github.com/TarsCloud/TarsGo/tars/makefile.tars))我们把 “CONFIG := client” 行去掉就行了。代码修改GoWebServer.go接着是修改代码了。首先是 GoWebServer.go,这里参照官方 Guide 的写法就好了,TarsGo 的 HTTP 实现用的是 Go 原生的组件。我稍微调整了一下,把回调函数放在 GoWebImp.go 中(“imp” 是 implementation,我以前一直以为是小恶魔的意思……),将 GoWebServer.go 简化为:package mainimport ( “github.com/TarsCloud/TarsGo/tars”)func main() { mux := &tars.TarsHttpMux{} mux.HandleFunc("/", HttpRootHandler) cfg := tars.GetServerConfig() tars.AddHttpServant(mux, cfg.App+"."+cfg.Server+".GoWebObj") //Register http server tars.Run()}代码还是比较简单的,无需多言。GoWebImp.goGoWebServer.go 中的 HTTPRootHandler 回调函数定义在业务的主要实现逻辑 GoWebImp.go 文件中:package mainimport ( “fmt” “time” “net/http”)func HttpRootHandler(w http.ResponseWriter, r *http.Request) { time_fmt := “2006-01-02 15:04:05” local_time := time.Now().Local() time_str = local_time.Format(time_fmt) ret_str = fmt.Sprintf("{"msg":"Hello, Tars-Go!", "time":"%s"}", time_str) w.Header().Set(“Content-Type”, “application/json;charset=utf-8”) w.Write([]byte(ret_str)) return}部署发布编译打包编译打包上面的工程:$ cd $GOPATH/src/amc/GoWebServer$ make && make tar成功后,会在目录下生成目标文件 GoWebServer.tgz,后文部署发布时需要上传这个包。部署发布创建服务在 Tars 管理平台主页中,点击 “运维管理”,界面如下:Tars 管理平台没有专门的 “新增应用” 功能,所有 app、server、object 的新增都在这个界面中配置。输入一个不存在的对象,就相当于新增操作。所以我们新增 “amc.GoWebServer.GoWebObj”,就是在各项中如下填写:应用:amc服务名称:GoWebServer服务类型:tars_go模板:tars.default节点:填写你打算部署的 IP 地址OBJ:GoWebObj端口类型:TCP协议:非TARS端口可以自定义,也可以填好信息后点 “获取端口” 来生成。各项填写完毕后,点 “确定”,然后刷新界面,重新进入 Tars 管理平台主页,可以看到界面左边的列表就多了上面的配置:发布服务点击 “GoWebServer”,显示 “发布管理” 子标签。在 “服务列表” 中选中需要发布的节点,然后点击 “发布选中节点” 按钮:再点击 “上传发布包”,进入如下界面:点击 “发布包” 右边的 “确定” 按钮,在弹出的对话框中选择前面提到的 GoWebServer.tgz 文件。给这个发布包写好描述之后,点击确认,开始上传发布包:发布成功后,回到 “发布管理” 界面,在该界面中,选择刚才发布的包,然后点击发布,一切正常情况下,即可发布成功。服务验证假设前面获取到的 servant 端口为 10008,那么可以在机器上执行 curl 命令(比如我的机器 IP 是 10.0.4.11):$ curl 10.0.4.11:10008{“msg”:“Hello, Tars-Go!”,“unix”:1546747070,“time”:“2019-01-06 11:57:50”,“client”:":-1"}这就验证 OK 啦,同时也说明了 Tars 管理平台的配置值配置正确了。错误示范此外,本人开始的时候用的是 localhost 地址,但是却错误了:$ curl 127.0.0.1:10008curl: (52) Empty reply from server这里让我误以为服务没有发布成功,折腾了好久。究其原因,是因为在 Tars 中对 servant 自动生成的配置是这样的(以我的为例,在 “服务管理” 中点击 ”管理Servant“):留意在 “绑定地址” 中,线程监听的 IP 地址是 10.0.4.11,所以 localhost 自然就访问不到了。这里不建议修改,如果要修改的话,还需要修改 “服务配置”。这歌内容相对比较深入,本文就不详述了。本文章采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。原文发布于:https://cloud.tencent.com/developer/article/1381300。 ...

January 12, 2019 · 2 min · jiezi

自定义a标签的不可用状态

a标签设置为不可用a标签没有disabled属性,所以要自己手动制作不可用状态设置颜色为灰色停用事件响应设置鼠标样式为默认样式 - 选择用的箭头样式使用ngClass为a标签设定样式<a (click)=“insertNodeSibling()” [ngClass]="{‘disableA’: brotherNodeDisabled}">新增同级</a>上面代码中disableA表示CSS中的样式类,当后面的变量brotherNodeDisabled为true时前面的样式disableA生效相反则不生效样式代码:.disableA{ pointer-events: none; cursor: default; color:gray;}更改样式:使用代码this.brotherNodeDisabled=true;修改变量的true和false,则会触发或者关闭CSS中的样式

January 12, 2019 · 1 min · jiezi

web前端开发中,需掌握的linux概念及常用命令

本文已同步到github web前端开发中,需掌握的linux概念及常用命令,欢迎收藏,欢迎start前一段时间阅读了《鸟哥的linux私房菜》的部分章节,也做了一些笔记,就在我再次打开笔记时,发现很多命令这是啥东东,有的都忘了,不过从头看一遍自己的笔记,就很快想起来了,从我个人的角度,平时学习过程中,记笔记确实是必要的。所以想把自己整理出的这些笔记整理一下,也分享给大家。那从何说起呢?既然是讲linux笔记,那就先说一句吧,在linux中,一切皆文件。1. 环境变量环境变量(environment variables) 一般是指在操作系统中用来指定操作系统运行环境的一些参数,如:临时文件夹位置和系统文件夹位置等。环境变量是在操作系统中一个具有特定名字的对象,它包含了一个或者多个应用程序所将使用到的信息。查看环境变量的配置格式, 如查看$PS1(操作系统及当前用户的相关信息)的配置echo $PS1临时设置直接给$PS1赋值即可$PS1=’[\u@\h \w]$‘如需永久生效,则需要修改/etc/profile文件的配置export PS1="[\u@\h \t]$“然后执行,source命令即可(通常用于重新执行刚修改的初始化文件,使之立即生效,而不必注销并重新登录)source /etc/profilePATH环境变量命令存放的路径, 是系统创建好的, 供用户使用, 随时随地使用echo $PATH // 查看PATH环境变量在执行命令如 ls 的时候, 会在PATH中查找, 没有提示command not found命令软件可安装在如下目录中/bin/sbin/usr/bin/usr/sbin/usr/local/bin/usr/local/sbin2. 硬链接与软链接2.1 硬链接(Hard Link)在同一分区中, inode号码相同的文件互为硬链接, 硬链接也就是文件的入口, 一般都是系统默认创建的防止误删除, 创建硬链接只能给文件创建硬链接, 不能给目录创建硬链接创建硬链接:ln 源文件入口 创建的硬链接入口例如: 给 /test/a.txt 创建一个硬链接2.2 软连接(Symbolic Link)存放的源文件的位置(inode节点号与源文件不同),相当于windows的快捷方式创建软链接:ln -s 源文件入口 创建的软链接入口例如: 给 /test/a.txt 创建一个软链接2.3 硬链接与软连接的区别互为硬链接的inode号相同软连接与源文件的inode的节点号不同,硬链接必须再同一个分区中, 软链接可跨文件系统不能对目录创建硬链接, 软链接可以(经常用)2.4 彻底删除一个文件硬链接数为0(rm -f 硬链接)进程调用数为0如何查看文件被哪个进程调用lsof | grep 文件名如果硬链接数为0, 但文件被进程调用, 重启对应的软件或服务即可3. vim编辑文件时的快捷键及编辑、保存、退出操作3.1 vim快捷键G : 文件的最后一行(非编辑INSERT状态下)gg : 文件的第1行(非编辑INSERT状态下)o : 当前行下面一行插入一个空行并进入编辑模式u : 撤销(非编辑INSERT状态下)C : 删除光标所在位置到行尾内容,并进入到编辑模式A : 快速到达当前行的结尾, 并今日到编辑模式3.2 编辑、保存、退出操作1.按键盘 i 进入编辑模式, 进行文件配置2.按键盘 esc 退出编辑模式3.输入:wq 保存并退出4.如果文件无改动时退出 :q4.如果文件有改动, 不想保存修改的文件, 输入:q!注意: vim编辑文件, 输入法必须在英文模式下4. /etc目录下的配置文件目录功能/etc/profile一般进行系统全局环境变量永久生效或别名的配置文件/etc/selinux/configselinux防火墙的配置文件/etc/init.d/iptablesiptables防火墙的配置文件/etc/sysconfig/i18n字符集的配置文件/etc/sysconfig/network-scripts/ifcfg-eth0网卡配置文件/etc/sysconfig/network可配置主机名/etc/hosts解析主机名与ip地址的对应关系/etc/bashrc别名配置文件5. 网卡(网络适配器)配置文件网卡配置文件的位置在/etc/sysconfig/network-scripts/ifcfg-eth0, 默认配置项包括:1. DEVICE: 网卡的名称2.TYPE: 网络类型3.UUID: 系统中给每个设备分配的标识符号, 在系统中唯一4.HWADDR: HardWare Address 网卡的硬件地址/mac地址/物理地址, 全世界唯一, 在生产网卡时就已经确定5.ONBOOT: 开机或重启网络的时候是否自启动6.NM_CONTROLLED: 网卡的配置文件是否受系统newwork服务这个软件控制, 如果系统或网络重新连接, 网卡就重启了7.BOOTPROTO: 网卡获取ip的方法1.static/none: 固定ip,需手动设置2.dhcp: 系统默认自动获取8.IPADDR: 网卡的ip地址9.NETMASK: 子网掩码, 决定了一个局域网中最多可以有多少台机器(255.255.255.0相当于PREFIX=24)10.GATEWAY: 网关 ?<span style=“color: red”>不太理解</span>6. 别名 alias6.1 查看linux中默认的别名设置执行alias命令即可alias输出如下: 6.2 别名设置6.2.1 临时生效临时设置命令的别名, 命令行直接alias + 命令 = “自定义命令"即可, 如alias rm=“echo xxx"在命令行中执行:rm /a.txt // 输出xxx a.txt注意: 1.当重新登录服务器后, 别名设置失败2.在设置别名后, 如与原来命令功能不同, 但还想使用的话, 则在执行命令前加’ ‘即可\rm a.txt //这时rm原有功能生效6.2.2 永久生效想要让别名无论是否重启服务器还是重新登录服务器都生效, 则需在 /etc/profile中,进行配置 alias rm=“echo xxx”, 然后执行命令:source /etc/profile这样,rm的别名设置将永久生效6.3 取消别名设置unalias rm7. linux下面安装软件方式7.1 yum是一个在Fedora和RedHat以及CentOS中的Shell前端软件包管理器, 在linux环境下, 就可以使用yum命令,前提是需要网络。特点:自动解决需要安装的软件的依赖需要网络yum install tree -y7.2 rpm在linux环境下, 就可以使用yum命令,不需要网络, 但是需要挂在光盘, 来提供软件包的库。1.把设备挂在到服务器的目录下mount /dev/chrom /mnt2.查看设备是否挂在成功df -h3. 执行rpm命令安装需要的软件包,如:rpm -ivh /mnt/Packages/lrzsz-0.12.20-27.1.el6.x86_64.rpm4.检查是否安装成功lrzsz软件包rmp -qa lrzsz5.可以查看安装的软件包中的内容(可执行命令):rpm -ql lrzsz6.rpm 删除安装的软件包:rpm -e lrzsz7.3 编译安装默认安装到/usr/local目录下, 需要自己安装依赖8. tar 打包压缩8.1 创建压缩包(1). 打包/etc目录到 /tmp/etc.tar.gztar zcvf /tmp/etc.tar.gz /etc// z 通过gzipg工具进行压缩// c create 创建压缩包文件// v verbose 显示压缩过程// f 指定文件(2). 打包/etc目录到 /tmp/etc.tar.gz 并排除掉/etc/servicestar zcvf /tmp/etc.tar.gz /etc –exclude=/etc/services8.2 查看压缩包内容tar tf /tmp/etc.tar.gz8.3 解压压缩包8.3.1 解压到当前路径tar xf /tmp/etc.tar.gz// 默认解压到当前路径8.3.2 解压到指定目录下tar xf /tmp/etc.tar.gz -C /ttt// -C 解压到指定目录下9. no space left on device 磁盘空间不足9.1. 查看磁盘各分区占用情况df -h9.2. 进入分区使用率最高的查看该分区下各文件block大小du -sh .| sort -h //一层一层排查,确认好后再删除9.3. 查看系统中inode占用磁盘情况df -ih // 主要是查看系统中的小文件// -i 显示inode号// -h 人可以阅读懂得显示9.4. 文件没有被彻底删除(硬链接数为0,但是文件被系统进程所调用)常用命令1. manformat and display the on-line manual pages 使用手册想查看哪个命令如何使用,如查看ls命令如何使用:man ls2. ipshow / manipulate routing, devices, policy routing and tunnels 显示网卡ip地址信息知识点:1. ip a // ip address 缩写3. whichshows the full path of (shell) commands 展示命令的完整路径查看madir命令的完整路径which mkdir4. pwdprint name of current/working directory 显示当前工作目录5. lslist directory contents 列出目录的所有内容5.1 列出/root目录下的所有目录及文件cd /rootls5.2 列出/root目录下的所有目录及文件,并显示文件的相关信息cd /rootls -l // 相当于ll, -l : use a long listing format, 展示文件详细信息6. mkdir-make directories 创建目录6.1. 创建 /data文件夹mkdir /data6.2. 用一条命令完成创建目录/my/test,既创建/my目录及/my/test目录mkdir -p /my/test知识点:-p –parents no error if existing, make parent directories as needed如果文件已经存在,新建时不报错如果需要创建多级文件夹,会一直创建到最底层的父级文件夹7. touchchange file timestamps 创建文件,修改文件时间戳在data下面建一个文件test.txttouch /data/test.txt8. cdcd -change direcory 切换目录8.1 进入/data/test目录cd /data/test8.2 返回上一层目录cd ..9. cp-copy files and directories 拷贝文件或目录9.1 把/data/test.txt文件拷贝到/tmp下cp /data/test.txt /tmp9.2 拷贝/data目录到/tmp目录下cp -r /data /tmp知识点:1. -r 参数表示递归复制目录,一层一层的复制2. -p 拷贝时,保持文件属性不变2. -a 相当于 -pdr9.3 拷贝/data目录到/tmp目录下, 保持文件属性不变cp -p /data /tmp10. mv-move (rename) files 移动或重命名文件10.1 把/data目录移动到/root下mv /data /root10.2 把/data/a.txt 重命名为b.txtcd /datamv a/txt b.txt11. rm-remove files or directories 删除文件或目录11.1 进入/root目录下的data目录,删除test.txt文件cd /root/datarm -f test.txt // -f “force”, 表示强制删除11.2 删除目录,删除/data/test目录rm -r /data/test // -r 删除目录12. echo-display a line of text 显示文本行12.1 为/data/test.txt增加内容为“I am studying linux.”echo “I am studing linux” > /data/test.txt知识点:“>” , 重定向,先清空文件内容,然后再追加到文件结尾”>>” ,追加重定向 把内容追加到文件结尾13. catconcatenate files and print on the standard output 连接文件并打印输出文件内容13.1 显示/test.txt的文件内容cat /test.txt13.2 与<<配合,追加多行内容到文件/a.txt中cat >>/a.txt<<EOFaaaabbbbccccEOF14. history命令可以用来显示曾执行过的命令history 15. df查看磁盘空间df -h // 查看blockdf -i //查看inode16. du查看文件目录下文件大小du -sh / //查看根目录下的所有文件大小17. sort排序sort -参数 文件-n // 按照数字顺序排序-r // 逆序-k // 根据第几列进行排序-h // 人可以读懂的信息, 根据文件大小 T>G>M>K 18. psprocess status 进程状态18.1查看所有node进程ps -ef | grep node19. findsearch for files in a directory hierarchy 在目录层次结构中, 查找文件19.1 在/data目录下查找a.txt文件find /data -type f -name “a.txt"知识点:-type 查找内容的类型f 表示要查找的内容为文件类型19.2 find与 管道符 “|“配合使用 在/data目录下查找a.txt文件,并删除find /data -type f name “a.txt” | xargs rm -rf20. grep-print lines matching a pattern 根据匹配的规则过滤文本内容20.1 输出test.txt文件中,包含aaa字符串的行grep “aaa” test.txt20.2输出test.txt文件中,不包含aaa字符串的行grep -v “aaa” test.txt // -v参数: –invert-match 过滤掉不匹配的行20.3 与管道符号配合使用,如过滤出命令历史记录中的所有cd命令history | grep cd21. awk21.1 已知文件/data目录下的test.txt文件内容为:aaabbbcccddd输出test.txt文件内容时,不包含bbb字符串:awk ‘!/bbb/’ /data/test.txt21.2 输出/data/text.txt的第二行awk ‘NR ==20’ /data/text.txt21.3 输出/data/text.txt的第二行到第三行awk ‘NR ==2, NR==3’ /data/text.txt22. sed用于过滤和转换文本的流编辑器22.1 /data/test.txt中内容为aaa,把/data/a.txt的内容替换为bbbsed -i ’s#aaa#bbb#g’ /data/test.txt 知识点:-i –in-place 替换固定用法, ’s#要被替换的内容#替换后的内容#g’. ‘#‘也可以使用‘@’等等符号,一般常用‘#’22.2 使用sed命令输出行,如输出/data目录下test.txt的第20行,第20到30行sed -n ‘20p’ /data/test.txtsed -n ‘20, 30p’ /data/test.txt23. pingsend ICMP ECHO_REQUEST packets to network hosts 检查网络是否连通本周根据记录的笔记,整理出了在web前端开发中,几个linux中的相关概念和常用的命令,用法也都比较基础,主要是方便查阅复习,同时分享给大家,更全的信息那当然是去查手册了。如有问题,请各位大神多多指出,谢谢。最后,让我们一起来ping一下百度吧,ping!! ping! ping!ping www.baidu.com ...

January 11, 2019 · 3 min · jiezi

web前端开发中,需掌握的linux概念及常用命令

本文已同步到github web前端开发中,需掌握的linux概念及常用命令,欢迎收藏,欢迎start前一段时间阅读了《鸟哥的linux私房菜》的部分章节,也做了一些笔记,就在我再次打开笔记时,发现很多命令这是啥东东,有的都忘了,不过从头看一遍自己的笔记,就很快想起来了,从我个人的角度,平时学习过程中,记笔记确实是必要的。所以想把自己整理出的这些笔记整理一下,也分享给大家。那从何说起呢?既然是讲linux笔记,那就先说一句吧,在linux中,一切皆文件。1. 环境变量环境变量(environment variables) 一般是指在操作系统中用来指定操作系统运行环境的一些参数,如:临时文件夹位置和系统文件夹位置等。环境变量是在操作系统中一个具有特定名字的对象,它包含了一个或者多个应用程序所将使用到的信息。查看环境变量的配置格式, 如查看$PS1(操作系统及当前用户的相关信息)的配置echo $PS1临时设置直接给$PS1赋值即可$PS1=’[\u@\h \w]$‘如需永久生效,则需要修改/etc/profile文件的配置export PS1="[\u@\h \t]$“然后执行,source命令即可(通常用于重新执行刚修改的初始化文件,使之立即生效,而不必注销并重新登录)source /etc/profilePATH环境变量命令存放的路径, 是系统创建好的, 供用户使用, 随时随地使用echo $PATH // 查看PATH环境变量在执行命令如 ls 的时候, 会在PATH中查找, 没有提示command not found命令软件可安装在如下目录中/bin/sbin/usr/bin/usr/sbin/usr/local/bin/usr/local/sbin2. 硬链接与软链接2.1 硬链接(Hard Link)在同一分区中, inode号码相同的文件互为硬链接, 硬链接也就是文件的入口, 一般都是系统默认创建的防止误删除, 创建硬链接只能给文件创建硬链接, 不能给目录创建硬链接创建硬链接:ln 源文件入口 创建的硬链接入口例如: 给 /test/a.txt 创建一个硬链接2.2 软连接(Symbolic Link)存放的源文件的位置(inode节点号与源文件不同),相当于windows的快捷方式创建软链接:ln -s 源文件入口 创建的软链接入口例如: 给 /test/a.txt 创建一个软链接2.3 硬链接与软连接的区别互为硬链接的inode号相同软连接与源文件的inode的节点号不同,硬链接必须再同一个分区中, 软链接可跨文件系统不能对目录创建硬链接, 软链接可以(经常用)2.4 彻底删除一个文件硬链接数为0(rm -f 硬链接)进程调用数为0如何查看文件被哪个进程调用lsof | grep 文件名如果硬链接数为0, 但文件被进程调用, 重启对应的软件或服务即可3. vim编辑文件时的快捷键及编辑、保存、退出操作3.1 vim快捷键G : 文件的最后一行(非编辑INSERT状态下)gg : 文件的第1行(非编辑INSERT状态下)o : 当前行下面一行插入一个空行并进入编辑模式u : 撤销(非编辑INSERT状态下)C : 删除光标所在位置到行尾内容,并进入到编辑模式A : 快速到达当前行的结尾, 并今日到编辑模式3.2 编辑、保存、退出操作1.按键盘 i 进入编辑模式, 进行文件配置2.按键盘 esc 退出编辑模式3.输入:wq 保存并退出4.如果文件无改动时退出 :q4.如果文件有改动, 不想保存修改的文件, 输入:q!注意: vim编辑文件, 输入法必须在英文模式下4. /etc目录下的配置文件目录功能/etc/profile一般进行系统全局环境变量永久生效或别名的配置文件/etc/selinux/configselinux防火墙的配置文件/etc/init.d/iptablesiptables防火墙的配置文件/etc/sysconfig/i18n字符集的配置文件/etc/sysconfig/network-scripts/ifcfg-eth0网卡配置文件/etc/sysconfig/network可配置主机名/etc/hosts解析主机名与ip地址的对应关系/etc/bashrc别名配置文件5. 网卡(网络适配器)配置文件网卡配置文件的位置在/etc/sysconfig/network-scripts/ifcfg-eth0, 默认配置项包括:1. DEVICE: 网卡的名称2.TYPE: 网络类型3.UUID: 系统中给每个设备分配的标识符号, 在系统中唯一4.HWADDR: HardWare Address 网卡的硬件地址/mac地址/物理地址, 全世界唯一, 在生产网卡时就已经确定5.ONBOOT: 开机或重启网络的时候是否自启动6.NM_CONTROLLED: 网卡的配置文件是否受系统newwork服务这个软件控制, 如果系统或网络重新连接, 网卡就重启了7.BOOTPROTO: 网卡获取ip的方法1.static/none: 固定ip,需手动设置2.dhcp: 系统默认自动获取8.IPADDR: 网卡的ip地址9.NETMASK: 子网掩码, 决定了一个局域网中最多可以有多少台机器(255.255.255.0相当于PREFIX=24)10.GATEWAY: 网关 ?<span style=“color: red”>不太理解</span>6. 别名 alias6.1 查看linux中默认的别名设置执行alias命令即可alias输出如下: 6.2 别名设置6.2.1 临时生效临时设置命令的别名, 命令行直接alias + 命令 = “自定义命令"即可, 如alias rm=“echo xxx"在命令行中执行:rm /a.txt // 输出xxx a.txt注意: 1.当重新登录服务器后, 别名设置失败2.在设置别名后, 如与原来命令功能不同, 但还想使用的话, 则在执行命令前加’ ‘即可\rm a.txt //这时rm原有功能生效6.2.2 永久生效想要让别名无论是否重启服务器还是重新登录服务器都生效, 则需在 /etc/profile中,进行配置 alias rm=“echo xxx”, 然后执行命令:source /etc/profile这样,rm的别名设置将永久生效6.3 取消别名设置unalias rm7. linux下面安装软件方式7.1 yum是一个在Fedora和RedHat以及CentOS中的Shell前端软件包管理器, 在linux环境下, 就可以使用yum命令,前提是需要网络。特点:自动解决需要安装的软件的依赖需要网络yum install tree -y7.2 rpm在linux环境下, 就可以使用yum命令,不需要网络, 但是需要挂在光盘, 来提供软件包的库。1.把设备挂在到服务器的目录下mount /dev/chrom /mnt2.查看设备是否挂在成功df -h3. 执行rpm命令安装需要的软件包,如:rpm -ivh /mnt/Packages/lrzsz-0.12.20-27.1.el6.x86_64.rpm4.检查是否安装成功lrzsz软件包rmp -qa lrzsz5.可以查看安装的软件包中的内容(可执行命令):rpm -ql lrzsz6.rpm 删除安装的软件包:rpm -e lrzsz7.3 编译安装默认安装到/usr/local目录下, 需要自己安装依赖8. tar 打包压缩8.1 创建压缩包(1). 打包/etc目录到 /tmp/etc.tar.gztar zcvf /tmp/etc.tar.gz /etc// z 通过gzipg工具进行压缩// c create 创建压缩包文件// v verbose 显示压缩过程// f 指定文件(2). 打包/etc目录到 /tmp/etc.tar.gz 并排除掉/etc/servicestar zcvf /tmp/etc.tar.gz /etc –exclude=/etc/services8.2 查看压缩包内容tar tf /tmp/etc.tar.gz8.3 解压压缩包8.3.1 解压到当前路径tar xf /tmp/etc.tar.gz// 默认解压到当前路径8.3.2 解压到指定目录下tar xf /tmp/etc.tar.gz -C /ttt// -C 解压到指定目录下9. no space left on device 磁盘空间不足9.1. 查看磁盘各分区占用情况df -h9.2. 进入分区使用率最高的查看该分区下各文件block大小du -sh .| sort -h //一层一层排查,确认好后再删除9.3. 查看系统中inode占用磁盘情况df -ih // 主要是查看系统中的小文件// -i 显示inode号// -h 人可以阅读懂得显示9.4. 文件没有被彻底删除(硬链接数为0,但是文件被系统进程所调用)常用命令1. manformat and display the on-line manual pages 使用手册想查看哪个命令如何使用,如查看ls命令如何使用:man ls2. ipshow / manipulate routing, devices, policy routing and tunnels 显示网卡ip地址信息知识点:1. ip a // ip address 缩写3. whichshows the full path of (shell) commands 展示命令的完整路径查看madir命令的完整路径which mkdir4. pwdprint name of current/working directory 显示当前工作目录5. lslist directory contents 列出目录的所有内容5.1 列出/root目录下的所有目录及文件cd /rootls5.2 列出/root目录下的所有目录及文件,并显示文件的相关信息cd /rootls -l // 相当于ll, -l : use a long listing format, 展示文件详细信息6. mkdir-make directories 创建目录6.1. 创建 /data文件夹mkdir /data6.2. 用一条命令完成创建目录/my/test,既创建/my目录及/my/test目录mkdir -p /my/test知识点:-p –parents no error if existing, make parent directories as needed如果文件已经存在,新建时不报错如果需要创建多级文件夹,会一直创建到最底层的父级文件夹7. touchchange file timestamps 创建文件,修改文件时间戳在data下面建一个文件test.txttouch /data/test.txt8. cdcd -change direcory 切换目录8.1 进入/data/test目录cd /data/test8.2 返回上一层目录cd ..9. cp-copy files and directories 拷贝文件或目录9.1 把/data/test.txt文件拷贝到/tmp下cp /data/test.txt /tmp9.2 拷贝/data目录到/tmp目录下cp -r /data /tmp知识点:1. -r 参数表示递归复制目录,一层一层的复制2. -p 拷贝时,保持文件属性不变2. -a 相当于 -pdr9.3 拷贝/data目录到/tmp目录下, 保持文件属性不变cp -p /data /tmp10. mv-move (rename) files 移动或重命名文件10.1 把/data目录移动到/root下mv /data /root10.2 把/data/a.txt 重命名为b.txtcd /datamv a/txt b.txt11. rm-remove files or directories 删除文件或目录11.1 进入/root目录下的data目录,删除test.txt文件cd /root/datarm -f test.txt // -f “force”, 表示强制删除11.2 删除目录,删除/data/test目录rm -r /data/test // -r 删除目录12. echo-display a line of text 显示文本行12.1 为/data/test.txt增加内容为“I am studying linux.”echo “I am studing linux” > /data/test.txt知识点:“>” , 重定向,先清空文件内容,然后再追加到文件结尾”>>” ,追加重定向 把内容追加到文件结尾13. catconcatenate files and print on the standard output 连接文件并打印输出文件内容13.1 显示/test.txt的文件内容cat /test.txt13.2 与<<配合,追加多行内容到文件/a.txt中cat >>/a.txt<<EOFaaaabbbbccccEOF14. history命令可以用来显示曾执行过的命令history 15. df查看磁盘空间df -h // 查看blockdf -i //查看inode16. du查看文件目录下文件大小du -sh / //查看根目录下的所有文件大小17. sort排序sort -参数 文件-n // 按照数字顺序排序-r // 逆序-k // 根据第几列进行排序-h // 人可以读懂的信息, 根据文件大小 T>G>M>K 18. psprocess status 进程状态18.1查看所有node进程ps -ef | grep node19. findsearch for files in a directory hierarchy 在目录层次结构中, 查找文件19.1 在/data目录下查找a.txt文件find /data -type f -name “a.txt"知识点:-type 查找内容的类型f 表示要查找的内容为文件类型19.2 find与 管道符 “|“配合使用 在/data目录下查找a.txt文件,并删除find /data -type f name “a.txt” | xargs rm -rf20. grep-print lines matching a pattern 根据匹配的规则过滤文本内容20.1 输出test.txt文件中,包含aaa字符串的行grep “aaa” test.txt20.2输出test.txt文件中,不包含aaa字符串的行grep -v “aaa” test.txt // -v参数: –invert-match 过滤掉不匹配的行20.3 与管道符号配合使用,如过滤出命令历史记录中的所有cd命令history | grep cd21. awk21.1 已知文件/data目录下的test.txt文件内容为:aaabbbcccddd输出test.txt文件内容时,不包含bbb字符串:awk ‘!/bbb/’ /data/test.txt21.2 输出/data/text.txt的第二行awk ‘NR ==20’ /data/text.txt21.3 输出/data/text.txt的第二行到第三行awk ‘NR ==2, NR==3’ /data/text.txt22. sed用于过滤和转换文本的流编辑器22.1 /data/test.txt中内容为aaa,把/data/a.txt的内容替换为bbbsed -i ’s#aaa#bbb#g’ /data/test.txt 知识点:-i –in-place 替换固定用法, ’s#要被替换的内容#替换后的内容#g’. ‘#‘也可以使用‘@’等等符号,一般常用‘#’22.2 使用sed命令输出行,如输出/data目录下test.txt的第20行,第20到30行sed -n ‘20p’ /data/test.txtsed -n ‘20, 30p’ /data/test.txt23. pingsend ICMP ECHO_REQUEST packets to network hosts 检查网络是否连通本周根据记录的笔记,整理出了在web前端开发中,几个linux中的相关概念和常用的命令,用法也都比较基础,主要是方便查阅复习,同时分享给大家,更全的信息那当然是去查手册了。如有问题,请各位大神多多指出,谢谢。最后,让我们一起来ping一下百度吧,ping!! ping! ping!ping www.baidu.com ...

January 10, 2019 · 3 min · jiezi

PWA--未来式app

本文是PWA科普文,不涉及技术,望大佬勿喷。什么是PWA应用PWA(Progressive Web Apps 的简称,译作渐进式 Web App),是 Google 在 2015 年推出的一个项目,旨在将 Web 网页服务具备类似原生 Apps 的使用体验。PWA应用兼有网页应用和原生app的优点,能给用户带来原生应用一样的体验,同时又能避免原生应用体积过大,滥用权限,频繁更新等问题。PWA应用通过网页加载,同时也能使用service worker实现离线存储和使用。用户无需像原生app那样下载安装PWA,只需打开相应网页即可通过浏览器一键添加到桌面。下次可通过桌面图标进入应用,操作逻辑和原生app一样,只是不用下载,安装。同时PWA应用也是跨平台的,无论是在iOS,安卓还是windows phone下都有一致的用户体验。如何添加PWA应用 iOS: 使用Safari打开相应app网页,点击分享按钮,选择添加到主屏幕即可。 安卓:目前只有chrome(谷歌)浏览器完全支持PWA,点击右上角三个点的按钮,然后选择添加到主屏幕即可。 哪里能找到PWA应用 国外有很多PWA应用商店,这里推荐一个https://pwa.rocks/ 国内有https://www.pwaappstore.cn ,需要使用手机打开这个站点,电脑打开是404

January 3, 2019 · 1 min · jiezi

深度介绍:也许你对 Fetch 了解得不是那么多

编者按:除创宇前端与作者博客外,本文还在语雀发布。前言本篇主要讲述 Fetch 的一些基本知识点以及我们在生产开发中怎么去使用。为了能够更好的了解 Fetch,我们希望你对以下知识点有所了解,如果有相关的开发经验,那是最好不过的了。XHRPromiseHTTP本文中对有些关键词提供了相应的链接,如果你对该关键词不够了解或想要了解更多,你可以通过点击它充实自己。文中有些知识点在 MDN Fetch 上已经写的很详细,因此有略过,希望同学们在阅读本文章时能够同时对照阅读。本文行文思路首先从规范入手,目的是让大家了解的更透彻,达到知其然知其所以然。为了更好的掌握 Fetch,文章中还提供了一些示例代码供大家学习使用。在使用该示例代码前,我们希望你对 node.js 有一些了解,如果没有的话,你可以根据示例中的友情提示完成你的这次学习体验。读完本篇文章后你将了解到以下内容:什么是 FetchFetch 的一些基本概念如何使用 FetchFetch 的一些不足以及我们如何“优雅”的使用它希望你通过读完本篇文章后,对 Fetch 有一个基本的了解。Fetch 简介Fetch 是一种新的用于获取资源的技术,它被用来代替我们已经吐槽了很久的技术(XHR)。Fetch 使用起来很简单,它返回的是一个 Promise,即使你没有 XHR 的开发经验也能快速上手。说了那么多,我们还是先睹为快吧,让我们快快下面的示例代码。fetch(‘https://github.com/frontend9/fe9-library', {method: ‘get’}).then(function(response) {}).catch(function(err) {// Error});是不是简单的不能再简单了?好,既然我们 Fetch 有了简单的认识之后,那我们再来了解下 Fetch 的基本概念。Fetch 基本概念在 Fetch 中有四个基本概念,他们分别是 Headers、Request 、Response 和 Body。为了更好的理解 Fetch,我们需要对这些概念做一个简单的了解。在一个完整的 HTTP 请求中,其实就已经包含了这四个概念。请求中有请求头和请求体,响应中有响应头和响应体。所以我们有必要了解这些概念。Headers为了实现头部的灵活性,能够对头部进行修改是一个非常重要的能力。Headers 属于 HTTP 中首部的一份子,它是一个抽象的接口,利用它可以对 HTTP 的请求头和响应头做出添加、修改和删除的操作。下面我们先看一下它具有哪些接口:typedef (sequence<sequence<ByteString>> or record<ByteString, ByteString>) HeadersInit;[Constructor(optional HeadersInit init), Exposed=(Window,Worker)]interface Headers { void append(ByteString name, ByteString value); void delete(ByteString name); ByteString? get(ByteString name); boolean has(ByteString name); void set(ByteString name, ByteString value); iterable<ByteString, ByteString>;};interface Headers { void append(ByteString name, ByteString value); void delete(ByteString name); ByteString? get(ByteString name); boolean has(ByteString name); void set(ByteString name, ByteString value); iterable<ByteString, ByteString>;};// 来自 https://fetch.spec.whatwg.org/#headers-class规范中定义的接口我们可以对应着 MDN 进行查看,你可以点击这里更直观的看看看看它有哪些方法供我们使用。这里我们对 Headers 的构造参数做个解释。首先参数类型为 HeadersInit,我们再看下这个类型支持哪些类型的值。我们从规范中可以看到的定义是:typedef (sequence<sequence<ByteString>> or record<ByteString, ByteString>) HeadersInit;这里我们对应到 JavaScript 这门语言,意思就是说这个对象可以是数组或者是键值对(即对象)。关于如何初始化这些参数,我们可以看下规范中定义的流程。To fill a Headers object (headers) with a given object (object), run these steps:If object is a sequence, then for each header in object:If header does not contain exactly two items, then throw a TypeError.Append header’s first item/header’s second item to headers.Otherwise, object is a record, then for each key → value in object, append key/value to headers.这里我需要对这个做个说明,后面对 fetch 的用法会涉及到一点以及我们看 polyfill 都会有所帮助。第一种:即数组,当数据每项如果不包含两项时,直接抛出错误。然后数组第一项是 header 名,第二项是值。,最后直接通过 append 方法添加。第二种:即键值对(这里指对象),我们通过循环直接取到键值对,然后通过 append 方法添加。示例示例代码地址:https://github.com/GoDotDotDo…打开浏览器输入:http://127.0.0.1:4000/headers那么我们该如何使用它呢?首先我们需要通过 new Headers() 来实例化一个 Headers 对象,该对象返回的是一个空的列表。在有了对象实例后,我们就可以通过接口来完成我们想要的操作,我们来一起看看下面的示例: function printHeaders(headers) { let str = ‘’; for (let header of headers.entries()) { str += &lt;li&gt;${header[0]}: ${header[1]}&lt;/li&gt; ; console.log(header[0] + ‘: ’ + header[1]); } return &lt;ul&gt; ${str} &lt;/ul&gt;; } const headers = new Headers(); // 我们打印下看看是否返回的是一个空的列表 const before = printHeaders(headers); // 发现这里没有任何输出 document.getElementById(‘headers-before’).innerHTML = before; // 我们添加一个请求头 headers.append(‘Content-Type’, ’text/plain’); headers.append(‘Content-Type’, ’text/html’); headers.set(‘Content-Type’, [‘a’, ‘b’]); const headers2 = new Headers({ ‘Content-Type’: ’text/plain’, ‘X-Token’: ‘abcdefg’, }); const after = printHeaders(headers); // 输出:content-type: 如果你觉得每次都要 append 麻烦的话,你也可以通过在构造函数中传入指定的头部,例如:const headers2 = new Headers({ ‘Content-Type’: ’text/plain’,‘X-Token’: ‘abcdefg’});printHeaders(headers2);// 输出:// content-type: text/plain// x-token: abcdefg这里我添加了一个自定义头部 X-Token,这在实际开发中很常见也很有实际意义。但是切记在 CORS 中需要满足相关规范,否则会产生跨域错误。你可以通过append 、 delete 、set 、get 和has 方法修改请求头。这里对 set 和 append 方法做个特殊的说明:set: 如果对一个已经存在的头部进行操作的话,会将新值替换掉旧值,旧值将不会存在。如果头部不存在则直接添加这个新的头部。append:如果已经存在该头部,则直接将新值追加到后面,还会保留旧值。为了方便记忆,你只需要记住 set 会覆盖,而 append 会追加。GuardGuard 是 Headers 的一个特性,他是一个守卫者。它影响着一些方法(像 append 、 set 、delete)是否可以改变 header 头。它可以有以下取值:immutable、request、request-no-cors、response 或 none。这里你无需关心它,只是为你让你了解有这样个东西在影响着我们设置一些 Headers。你也无法去操作它,这是代理的事情。举个简单的例子,我们无法在 Response Headers 中插入一个 Set-Cookie。如果你想要了解更过的细节,具体的规范请参考 concept-headers-guard 和 MDN Guard注意我们在给头部赋值的时候需要满足可接受的首部字段集合否则将会报 TypeError 。BodyBody 准确来说这里只是 mixin,代表着请求体或响应体,具体由 Response 和 Request 来实现。下面我们来看看它具有哪些接口:interface mixin Body { readonly attribute ReadableStream? body; readonly attribute boolean bodyUsed; [NewObject] Promise<ArrayBuffer> arrayBuffer(); [NewObject] Promise<Blob> blob(); [NewObject] Promise<FormData> formData(); [NewObject] Promise<any> json(); [NewObject] Promise<USVString> text();};// 来自 https://fetch.spec.whatwg.org/#body规范中定义的接口我们可以对应着 MDN 进行查看,你可以点击这里更直观的看看它有哪些属性和方法供我们使用。这里需要注意看这些方法返回的都是 Promise,记住这在基于 fetch 进行接口请求中很重要。记住了这个,有利于我们在后面的文章中理解 fetch 的用法。示例范例将在 Response 中体现。RequestRequest 表示一个请求类,需要通过实例化来生成一个请求对象。通过该对象可以描述一个 HTTP 请求中的请求(一般含有请求头和请求体)。既然是用来描述请求对象,那么该请求对象应该具有修改请求头(Headers)和请求体(Body)的方式。下面我们先来看下规范中 Request 具有哪些接口:typedef (Request or USVString) RequestInfo;[Constructor(RequestInfo input, optional RequestInit init), Exposed=(Window,Worker)]interface Request { readonly attribute ByteString method; readonly attribute USVString url; [SameObject] readonly attribute Headers headers; readonly attribute RequestDestination destination; readonly attribute USVString referrer; readonly attribute ReferrerPolicy referrerPolicy; readonly attribute RequestMode mode; readonly attribute RequestCredentials credentials; readonly attribute RequestCache cache; readonly attribute RequestRedirect redirect; readonly attribute DOMString integrity; readonly attribute boolean keepalive; readonly attribute boolean isReloadNavigation; readonly attribute boolean isHistoryNavigation; readonly attribute AbortSignal signal; [NewObject] Request clone();};Request includes Body;dictionary RequestInit { ByteString method; HeadersInit headers; BodyInit? body; USVString referrer; ReferrerPolicy referrerPolicy; RequestMode mode; RequestCredentials credentials; RequestCache cache; RequestRedirect redirect; DOMString integrity; boolean keepalive; AbortSignal? signal; any window; // can only be set to null};enum RequestDestination { “”, “audio”, “audioworklet”, “document”, “embed”, “font”, “image”, “manifest”, “object”, “paintworklet”, “report”, “script”, “sharedworker”, “style”, “track”, “video”, “worker”, “xslt” };enum RequestMode { “navigate”, “same-origin”, “no-cors”, “cors” };enum RequestCredentials { “omit”, “same-origin”, “include” };enum RequestCache { “default”, “no-store”, “reload”, “no-cache”, “force-cache”, “only-if-cached” };enum RequestRedirect { “follow”, “error”, “manual” };// 来自 https://fetch.spec.whatwg.org/#request-class规范中定义的接口我们可以对应着 MDN 进行查看,你可以点击这里更直观的看看它有哪些属性和方法供我们使用,这里不做一一解释。注意这里的属性都是只读的,规范中我们可以看到构造函数的第一个参数为 Request 对象或字符串,我们一般采取字符串,即需要访问的资源地址( HTTP 接口地址)。第二个参数接收一个 RequestInit 可选对象,而这个对象是一个字典。在 javascript 中,我们可以理解为一个对象({})。RequestInit 里面我们可以配置初始属性,告诉 Request 我们这个请求的一些配置信息。这里我们需要对以下几个属性特别注意下。mode 是一个 RequestMode 枚举类型,可取的值有 navigate, same-origin, no-cors, cors。它表示的是一个请求时否使用 CORS,还是使用严格同源模式。当处于跨域情况下,你应当设置为 cors。该值的默认值在使用 Request 初始化时,默认为 cors。当使用标记启动的嵌入式资源,例如 <link>、 <script>标签(未手动修改 crossorigin 属性),默认为 no-cors。详细信息请参考 whatwg 规范或 MDN 。credentials 是一个 RequestCredentials 枚举类型,可取的值有 omit, same-origin, include。它表示的是请求是否在跨域情况下发送 cookie。看到这,如果对 XHR 了解的同学应该很熟悉。这和 XHR 中的 withCredentials 很相似。但是 credentials 有三个可选值,它的默认值为 same-origin。当你需要跨域传递 cookie 凭证信息时,请设置它为 include。注意这里有一个细节,当设置为 include 时,请确保 Response Header 中 Access-Control-Allow-Origin 不能为 ,需要指定源(例如:http://127.0.0.1:4001),否则会你将会在控制台看到如下错误信息。详细信息请参考 whatwg 规范或 MDN 。The value of the ‘Access-Control-Allow-Origin’ header in the response must not be the wildcard ‘’ when the request’s credentials mode is ‘include’.你可以使用文章中提供的代码中启动 cors 示例代码,然后在浏览器中输入 http://127.0.0.1:4001/request,如果不出意外的话,你可以在控制台中看到上面的错误提示。body 是一个 BodyInit 类型。它可取的值有 Blob,BufferSource , FormData , URLSearchParams , ReadableStream , USVString。细心的同学不知道有没有发现,我们常见的 json 对象却不在其中。因此,我们如果需要传递 json 的话,需要调用 JSON.stringify 函数来帮助我们转换成字符串。下面将给出一段示例代码。示例示例代码地址:https://github.com/GoDotDotDo…打开浏览器输入:http://127.0.0.1:4000/request // 客户端 const headers = new Headers({ ‘X-Token’: ‘fe9’, }); const request = new Request(’/api/request’, { method: ‘GET’, headers, }); console.log(request); // Request {method: “GET”, url: “http://127.0.0.1:4000/api/request”, headers: Headers, destination: “”, referrer: “about:client”, …} console.log(request.method); // GET console.log(request.mode); // cors console.log(request.credentials); // same-origin // 如果你想打印headers信息,可以调用 printHeaders(request.headers)这里我们先以 GET 简单请求作为示例,我们传递了一个自定义的 Headers,指定了请求方法 method 为 GET(默认为 GET)。在上面的接口规范中,我们可以通过 Request 对象拿到一些常用的属性,比如 method、url、headers 、body 等等只读属性。ResponseResponse 和 Request 类似,表示的是一次请求返回的响应数据。下面我们先看下规范中定义了哪些接口。[Constructor(optional BodyInit? body = null, optional ResponseInit init), Exposed=(Window,Worker)]interface Response { [NewObject] static Response error(); [NewObject] static Response redirect(USVString url, optional unsigned short status = 302); readonly attribute ResponseType type; readonly attribute USVString url; readonly attribute boolean redirected; readonly attribute unsigned short status; readonly attribute boolean ok; readonly attribute ByteString statusText; [SameObject] readonly attribute Headers headers; readonly attribute Promise<Headers> trailer; [NewObject] Response clone();};Response includes Body;dictionary ResponseInit { unsigned short status = 200; ByteString statusText = “”; HeadersInit headers;};enum ResponseType { “basic”, “cors”, “default”, “error”, “opaque”, “opaqueredirect” };// 来自 https://fetch.spec.whatwg.org/#response-class规范中定义的接口我们可以对应着 MDN 进行查看,你可以点击这里更直观的看看它有哪些属性和方法供我们使用,这里不做一一解释。其中 status, headers 属性最为常用。通过 status 状态码我们可以判断出服务端请求处理的结果,像 200, 403 等等常见状态码。这里举个例子,当 status 为 401 时,可以在前端进行拦截跳转到登录页面,这在现如今 SPA(单页面应用程序)中尤为常见。我们也可以利用 headers 来获取一些服务端返回给前端的信息,比如 token。仔细看上面的接口的同学可以发现 Response includes Body; 这样的标识。在前面我们说过 Body 由 Request 和 Response 实现。所以 Body 具有的方法,在 Response 实例中都可以使用,而这也是非常重要的一部分,我们通过 Body 提供的方法(这里准确来说是由 Response 实现的)对服务端返回的数据进行处理。下面我们将通过一个示例来了解下简单用法:示例示例代码位置:https://github.com/GoDotDotDo… // 客户端 const headers = new Headers({ ‘X-Token’: ‘fe9-token-from-frontend’, }); const request = new Request(’/api/response’, { method: ‘GET’, headers, }); // 这里我们先发起一个请求试一试 fetch(request) .then(response => { const { status, headers } = response; document.getElementById(‘status’).innerHTML = ${status}; document.getElementById(‘headers’).innerHTML = headersToString(headers); return response.json(); }) .then(resData => { const { status, data } = resData; if (!status) { window.alert(‘发生了一个错误!’); return; } document.getElementById(‘fetch’).innerHTML = data; });这里我们先忽略 fetch 用法,后面的章节中会进行详细介绍。我们先关注第一个 then 方法回调里面的东西。可以看到返回了一个 response 对象,这个对象就是我们的 Response 的实例。示例中拿了 status 和 headers ,为了方便,这里我将其放到 html 中。再看看该回调中最后一行,我们调用了一个 response.json() 方法(这里后端返的数据是一个 JSON 对象,为了方便直接调用 json()),该方法返回一个 Promise,我们将处理结果返给最后一个 then 回调,这样就可以获得最终处理过后的数据。打开浏览器,输入 http://127.0.0.1:4000/response,如果你的示例代运行正常,你将会看到以下页面:(查看 Response 返回的数据)Fetch 与 XHR 比较Fetch 相对 XHR 来说具有简洁、易用、声明式、天生基于 Promise 等特点。XHR 使用方式复杂,接口繁多,最重要的一点个人觉得是它的回调设计,对于实现 try…catch 比较繁琐。但是 Fetch 也有它的不足,相对于 XHR 来说,目前它具有以下劣势:不能取消(虽然 AbortController 能实现,但是目前兼容性基本不能使用,可以使用 polyfill )不能获取进度不能设置超时(可以通过简单的封装来模拟实现)兼容性目前比较差(可以使用 polyfill 间接使用 XHR 来优雅降级,这里推荐使用 isomorphic-fetch )在了解 Fetch 和 XHR 的一些不同后,还是需要根据自身的业务需求来选择合适的技术,因为技术没有永远的好坏,只有合不合适。下面章节我们将介绍如何“优雅”的使用 Fetch 以及如何尽量避免掉劣势。如何使用Fetch前面了解了这么多基础知识,现在终于到了介绍如何使用 Fetch 了。老规矩,我们先来看下规范定义的接口。partial interface mixin WindowOrWorkerGlobalScope { [NewObject] Promise<Response> fetch(RequestInfo input, optional RequestInit init);};规范中定义的接口我们可以对应着 MDN 进行查看,你可以点击这里更直观的看看它的用法。从规范中我们可以看到 fetch 属于 WindowOrWorkerGlobalScope 的一部分,暴露在 Window 或 WorkerGlobalScope 对象上。所以在浏览器中,你可以直接调用 fetch。规范中定义了 fetch 返回一个 Promise,它最多可接收两个参数( input 和 init )。为了能够对它的使用方法有个更全面的了解,下面来讲一下这两个参数。input 参数类型为 RequestInfo,我们可以回到前面的 Request 部分,来回顾一下它的定义。typedef (Request or USVString) RequestInfo;发现它是一个 Request 对象或者是一个字符串,因此你可以传 Request 实例或者资源地址字符串,这里一般我们推荐使用字符串。init 参数类型为 RequestInit,我们回顾前面 Requst 部分,它是一个字典类型。在 JavaScript 中你需要传递一个 Object 对象。dictionary RequestInit { ByteString method; HeadersInit headers; BodyInit? body; USVString referrer; ReferrerPolicy referrerPolicy; RequestMode mode; RequestCredentials credentials; RequestCache cache; RequestRedirect redirect; DOMString integrity; boolean keepalive; AbortSignal? signal; any window; // can only be set to null };在本小节之前我们都没有介绍 fetch 的使用方式,但是在其他章节中或多或少出现过它的容貌。现在,我们终于可以在这里正式介绍它的使用方式了。fetch 它返回一个 Promise,意味着我们可以通过 then 来获取它的返回值,这样我们可以链式调用。如果配合 async/await 使用,我们的代码可读性会更高。下面我们先通过一个简单的示例来熟悉下它的使用。示例示例代码位置:https://github.com/GoDotDotDo… // 客户端 const headers = new Headers({ ‘X-Token’: ‘fe9’, }); setTimeout(() => { fetch(’/data?name=fe’, { method: ‘GET’, // 默认为 GET,不写也可以 headers, }) .then(response => response.json()) .then(resData => { const { status, data } = resData; if (!status) { window.alert(‘发生了一个错误!’); return; } document.getElementById(‘fetch’).innerHTML = data; }); }, 1000);上面的示例中,我们自定义了一个 headers 。为了演示方便,这里我们设定了一个定时器。在请求成功时,服务器端会返回相应的数据,我们通过 Response 实例的 json 方法来解析数据。细心的同学会发现,这里 fetch 的第一个参数我们采用的是字符串,在第二个参数我们提供了一些 RequestInit 配置信息,这里我们指定了请求方法(method)和自定义请求头(headers)。当然你也可以传递一个 Request 实例对象,下面我们也给出一个示例。代码位置:https://github.com/GoDotDotDo… const headers = new Headers({ ‘X-Token’: ‘fe9’, }); const request = new Request(’/api/request’, { method: ‘GET’, headers, }); setTimeout(() => { fetch(request) .then(res => res.json()) .then(res => { const { status, data } = res; if (!status) { alert(‘服务器处理失败’); return; } document.getElementById(‘fetch-req’).innerHTML = data; }); }, 1200);在浏览器中打开:http://127.0.0.1:4000/, 如果上面的示例运行成功,你将会看到如下界面:好,在运行完示例后,相信你应该对如何使用 fetch 有个基本的掌握。在上一章节,我们讲过 fetch 有一定的缺点,下面我们针对部分缺点来尝试着处理下。解决超时当网络出现异常,请求可能已经超时,为了使我们的程序更健壮,提供一个较好的用户 体验,我们需要提供一个超时机制。然而,fetch 并不支持,这在上一小节中我们也聊到过。庆幸的是,我们有 Promise ,这使得我们有机可趁。我们可以通过自定义封装来达到支持超时机制。下面我们尝试封装下。const defaultOptions = { headers: { ‘Content-Type’: ‘application/json’, },};function request(url, options = {}) { return new Promise((resolve, reject) => { const headers = { …defaultOptions.headers, …options.headers }; let abortId; let timeout = false; if (options.timeout) { abortId = setTimeout(() => { timeout = true; reject(new Error(’timeout!’)); }, options.timeout || 6000); } fetch(url, { …defaultOptions, …options, headers }) .then((res) => { if (timeout) throw new Error(’timeout!’); return res; }) .then(checkStatus) .then(parseJSON) .then((res) => { clearTimeout(abortId); resolve(res); }) .catch((e) => { clearTimeout(abortId); reject(e); }); });}上面的代码中,我们需要注意下。就是我们手动根据超时时间来 reject 并不会阻止后续的请求,由于我们并没有关闭掉此次连接,属于是伪取消。fetch 中如果后续接受到服务器的响应,依然会继续处理后续的处理。所以这里我们在 fetch 的第一个 then 中进行了超时判断。取消 const controller = new AbortController(); const signal = controller.signal; fetch(’/data?name=fe’, { method: ‘GET’, signal, }) .then(response => response.json()) .then(resData => { const { status, data } = resData; if (!status) { window.alert(‘发生了一个错误!’); return; } document.getElementById(‘fetch-str’).innerHTML = data; }); controller.abort();我们回过头看下 fetch 的接口,发现有一个属性 signal, 类型为AbortSignal,表示一个信号对象( signal object ),它允许你通过 AbortController 对象与DOM请求进行通信并在需要时将其中止。你可以通过调用 AbortController.abort 方法完成取消操作。当我们需要取消时,fetch 会 reject 一个错误( AbortError DOMException ),中断你的后续处理逻辑。具体可以看规范中的解释。由于目前 AbortController 兼容性极差,基本不能使用,但是社区有人帮我们提供了 polyfill(这里我不提供链接,因为目前来说还不适合生产使用,会出现下面所述的问题),我们可以通过使用它来帮助我们提前感受新技术带来的快乐。但是你可能会在原生支持 Fetch 但是又不支持 AbortController 的情况下,部分浏览器可能会报如下错误:Chrome: “Failed to execute ‘fetch’ on ‘Window’: member signal is not of type AbortSignal.“Firefox: “‘signal’ member of RequestInit does not implement interface AbortSignal.“如果出现以上问题,我们也无能为力,可能原因是浏览器内部做了严格验证,对比发现我们提供的 signal 类型不对。但是我们可以通过手动 reject 的方式达到取消,但是这种属于伪取消,实际上连接并没有关闭。我们可以通过自定义配置,例如在 options 中增加配置,暴露出 reject,这样我们就可以在外面来取消掉。这里本人暂时不提供代码。有兴趣的同学可以尝试一下,也可以在下面的评论区评论。前面提到过的获取进度目前我们还无法实现。拦截器示例代码位置:https://github.com/GoDotDotDo…下面我们讲一讲如何做一个简单的拦截器,这里的拦截器指对响应做拦截。假设我们需要对接口返回的状态码进行解析,例如 403 或者 401 需要跳转到登录页面,200 正常放行,其他报错。由于 fetch 返回一个 Promise ,这就使得我们可以在后续的 then 中做些简单的拦截。我们看一下示例代码:function parseJSON(response) { const { status } = response; if (status === 204 || status === 205) { return null; } return response.json();}function checkStatus(response) { const { status } = response; if (status >= 200 && status < 300) { return response; } // 权限不允许则跳转到登陆页面 if (status === 403 || status === 401) { window ? (window.location = ‘/login.html’) : null; } const error = new Error(response.statusText); error.response = response; throw error;}/** * @description 默认配置 * 设置请求头为json /const defaultOptions = { headers: { ‘Content-Type’: ‘application/json’, }, // credentials: ‘include’, // 跨域传递cookie};/* * Requests a URL, returning a promise * * @param {string} url The URL we want to request * @param {object} [options] The options we want to pass to “fetch” * * @return {object} The response data */function request(url, options = {}) { return new Promise((resolve, reject) => { const headers = { …defaultOptions.headers, …options.headers }; let abortId; let timeout = false; if (options.timeout) { abortId = setTimeout(() => { timeout = true; reject(new Error(’timeout!’)); }, options.timeout || 6000); } fetch(url, { …defaultOptions, …options, headers }) .then((res) => { if (timeout) throw new Error(’timeout!’); return res; }) .then(checkStatus) .then(parseJSON) .then((res) => { clearTimeout(abortId); resolve(res); }) .catch((e) => { clearTimeout(abortId); reject(e); }); });}从上面的 checkStatus 代码中我们可以看到,我们首先检查了状态码。当状态码为 403 或 401 时,我们将页面跳转到了 login 登录页面。细心的同学还会发现,我们多了一个处理方法就是 parseJSON,这里由于我们的后端统一返回 json 数据,为了方便,我们就直接统一处理了 json 数据。总结本系列文章整体阐述了 fetch 的基本概念、和 XHR 的差异、如何使用 fetch 以及我们常见的解决方案。希望同学们在读完整篇文章能够对 fetch 的认识有所加深。建议:在整体了解了 fetch 之后,希望同学们能够读一下 github polyfill 源码。在读代码的同时,可以同时参考 Fetch 规范。参考:MDN FetchFetch 规范示例代码文 / GoDotDotDotLess is more.编 / 荧声作者其他文章:优秀前端必知的话题:我们应该做些力所能及的优化本文由创宇前端作者授权发布,版权属于作者,创宇前端出品。欢迎注明出处转载本文。本文链接:https://blog.godotdotdot.com/…想要订阅更多来自知道创宇开发一线的分享,请搜索关注我们的微信公众号:创宇前端(KnownsecFED)。欢迎留言讨论,我们会尽可能回复。欢迎点赞、收藏、留言评论、转发分享和打赏支持我们。打赏将被完全转交给文章作者。感谢您的阅读。 ...

January 3, 2019 · 8 min · jiezi

分享10个免费H5模版(主题)资源网站

并非所有程序员都是伟大的设计师(实际上我认识的大部分程序员审美都很要命)。所以即使你心血来潮想为自己开发的网站做一把设计,但显然并不是所有用户想为此付钱。。所以在找不到靠谱的美工的时候,还是老老实实的看看有没有现成的资源可以利用吧。以下是各种网站主题的绝佳资源,从通用主题、后台管理到电商系统。1. HTML5 UpHTML5 Up 有40~50个非常好的简单但优雅的HTML5 / CSS3主题。他们的大多数主题都是通用的,你可以将它们用于任何类型的网站或应用程序。这些主题有很好的响应式设计,在移动设备上看起来很棒,并且完全可定制,并且在Creative Common License下完全免费。每个主题都提供了在线演示,顶部有一个导航栏,可以显示桌面,平板电脑和移动设备的外观。我喜欢这些主题简洁的设计,很易于使用。相比而言,有些付费主题却更加复杂难用。程序员都喜欢简单!2. Start BootstrapStart Bootstrap当然是Bootstrap主题。里面都是最新的bootstrap4主题,很好的入门主题。提供了最精简的模版让你你可以轻松自定义。他们还提供非常好看的后台管理的模版。因此,如果要构建类似博客或CMS的东西,那么你可以找到非常好看的前端界面。我在使用PHP Codeigniter以及Node和Express.js构建的项目中使用了admin主题。如果免费选择不够,他们还提供更好看的高级主题。像大多数此类网站一样,你可以在下载之前预览主题。3. Templated.coTemplated.co是免费HTML5主题的另一个重要资源。拥有近900个免费主题的集合,这些主题在Creative Commons License下是免费的。主题都非常通用,支持在线预览。他们有一些不错的网格库类型主题,适合作为图像分享网站的主题。4. Free HTML5freehtml5.co有付费的高级主题,但是也有很多很棒的免费主题。有许多通用主题以及从律师事务所到摄影主题。许多主题也有很好的动画。有些是纯HTML,有些是Bootstrap的。5. ColorlibColorlib是一个很棒的网站,他们有很多免费主题。有一些独特的类别,如医疗和旅行为主题。也有一些不错的房地产主题,其中一个我几乎用于我的Django课程的房地产应用程序,但最终我决定创建自己的。他们有大量标准的HTML5主题以及一些非常酷的Wordpress主题。他们的实时预览浏览器易于使用,你可以快速测试每个主题。6. Zero ThemeZeroTheme有120多个免费主题可供选择,它们都是响应式的。其中87个是HTML,37个是Bootstrap 4,都提供了桌面版和移动版的预览。在我看来,很多设计看起来非常相似,但你可能会找到你喜欢的东西。7. StyleshoutStyleshout有一些非常好看的响应式主题。他们有一些单页主题,这使它们很容易自定义。他们甚至有“敬请期待”和“404页面”的主题,这真的很酷。他们的许多主题都用JavaScript实现了一些动态的效果,如倒计时和滑块。他们还提供定制化服务,如果您需要定制,您可以支付费用并让他们这样做,这对非开发人员来说非常好。8. HTML5xCSS3html5xcss3.com有不同类别的有大约500个免费主题。其中许多都是基于Bootstrap的。他们也有一些Wordpress主题。这些设计并不具有超级创意,但它们看起来非常简单易用,可以自定义满足你的大部分需求。他们也有一些付费的高级主题。9. 后台管理主题Athemes有一篇文章展示了20多个非常好的后台管理系统主题。这些管理面板设计得很好,很多都具有动态功能,如图表和弹窗。大多数是基于Bootstrap的,有一些基于Material Design和常规HTML5。10. 电商主题Themewagon有一篇文章推荐了很多的HTML5和Bootstrap 4电商主题。范围从服装、家具到电子产品。也有很多是通用的,可用于任何网上店铺。无论您是从头开始构建电商系统还是使用Magento之类的东西,这些主题或多或少都可以帮助你搭建出一个漂亮的电商网站。提示以上资源国内访问较慢,翻墙比较快。

January 2, 2019 · 1 min · jiezi

可视化开发脚手架

项目地址https://github.com/xinglie/re…界面预览设计需求1. 可视化工具越来越多的成为标配,针对web开发,各种框架都有自己的可视化搭建工具2. 依赖web操控、展示、通信的行业会越来越多,为传统行业提供快速搭建的功能3. 大数据时代,除数据可视化,其它各方面的可视化需求也会增多设计实现1. 脚手架只做基础的布局控制与界面展现2. 编辑、可视哪些元素由开发者提供,包括有哪些元素,元素可编辑的属性,界面展示,逻辑控制等3. 输入输出使用JSON数据,可视化产出描述的数据,可用其它任意技术进行界面构建还原4. 尽量适应任何的可视化场景:报表,物联网,页面搭建等

January 2, 2019 · 1 min · jiezi

【转】2019年Web开发指南

文章总结自视频Web Development In 2019 - A Practical Guide眨眼2018过去了,还有很多计划学习的东西恐怕都还没有完成,时间不等人,我们要开始看看2019年有什么要关注学习的了。视频大纲:0:28 - What Is In This Guide?(指南主要内容介绍)1:24 - Basic Software & Tools(基本开发软件和工具)3:43 - HTML & CSS(HTML和CSS)5:06 - Responsive Layout(响应式布局)5:55 - Basic Deployment(部署介绍)7:35 - Sass Pre-Processor(Sass预处理器)8:38 - Vanilla JavaScript(原生Javascript)10:08 - Basic Front-End Web Developer(前端开发介绍)11:13 - What To Learn Next(学些什么)11:53 - HTML / CSS Framework(HTML/CSS框架)13:21 - Git & Tooling(Git和相关工具)16:58 - Front-End Framework(前端框架)19:10 - State Management(状态管理器)20:29 - Full Fledged Front-End Web Developer(优秀的前端开发者)21:24 - Server Side Language(服务端语言)24:16 - Server Side Framework(服务端框架)27:52 - Database(数据库介绍)29:34 - Server Rendered Pages(服务端渲染)30:41 - CMS(内容管理系统)31:44 - DevOps, Deployment & More(部署等)34:40 - Full Stack Badass(全栈)34:57 - Mobile Development(移动端开发)35:58 - Desktop Apps With Electron(Electron的桌面应用)36:33 - GraphQL & Apollo(GraphQL和Apollo)37:28 - TypeScript(TypeScript)38:15 - Serverless Architecture(无服务器架构)38:52 - AI & Machine Learning(智能和机器学习)39:23 - Blockchain Technology(区块链技术)40:07 - PWA(渐进式页面应用)40:42 - Web Assembly(不知道如何解释)我就几个重点来介绍一下:基本开发软件和工具编辑器:VSCode,这两年来,它的Web开发的使用比例急速上升,如果你是一名前端,非常推荐使用哦。另外对我非常有帮助的VSCode插件Settings Sync,我也是强烈推荐的,使用方法可以阅读我曾经写的Visual Studio Code 设置同步到github的插件介绍及使用方法(Settings Sync)浏览器:Chrome是我目前用的最顺手的了,开发调试也是非常强大,作为一名Web开发者,还在使用360,或许有点显得太不专业了????其他:Windows下的终端强烈推荐Git Bash,至少我是极度反感每次按完ctrl+C还要Y一下的,如果使用VSCode,可以修改以下设置(默认git安装路径的话)“terminal.integrated.shell.windows”: “C:\Program Files\Git\bin\bash.exe”,如果有用到设计相关的,可以考虑学习XD,PS,Sketch…基础知识掌握HTMT5, CSS3, Javascript:这三个依旧是需要熟练的!HTML5:面世很久了,其实很多时候我们并未熟练掌握各个标签的使用,以及一些高效API还是有必要进一步学习的。CSS3:最多的最复杂的应该是transform和flex这块了,了解他们有哪些功能的前提下,没事多看看文档,是不是可以更快的提高工作效率呢Javascript:ES6趋势越来越明显,各类构建工具配合Babel强大到了简单的配置即可兼容大部分浏览器,因此使用ES6+进行JS开发实在是会轻松一些,因此,请多阅读阮一峰的ES6文档。响应式开发可以考虑放弃使用px,如果需要做响应式的Web应用,Rem或许是更好的选择,当然你也可以使用VW单位,还有设置网格,Viewport,媒体查询等等方式让你的响应式应用更加完美。Sass,PostCSS手写CSS,真的很慢,如果可以的话,非常推荐在开发环境下使用Sass和PostCSS,最大的便利之处是代码更加好维护和管理了。前端框架三大开发框架,Angular,React,Vue,各有特点!很有必要去了解,即便只会其中一个,也推荐去了解其他的。这里就不细说了。UI框架:ElementUI,Ant Desgin,等等,太多了。也是各有特点,大家请多多尝试。CSS框架:BootStrap,Bulma(我也没用过)等等,我认为熟悉这些框架对于规范化话CSS是有较大的帮助的。服务端语言前端工程师还是需要熟悉Node.js及相关主流框架,比如:Express,KOA,Egg.js等等。而其他Web开发者如有需要可能会使用到Java,PHP(Laravel,ThinkPHP),Python(DJango),Go等等。数据存储关系型数据库:MySQL,PostgreSQLNoSQL:MongoDB云:Firebase,AWS,LeanCloud(比较推荐看看)轻存储:SQLite,Redis服务端渲染三大框架对应的三套:Augular Universal,Next.js,Nuxt.js(使用Vue的同学,可以试试这个,以前问题挺多的,不过最近除了新版也是挺强大的。)网站部署不仅仅是运维需要熟悉的这些Linux,SSH,Git,Nginx等等。其他开发人员也有必要了解。国内比较有名的平台,阿里云,腾讯云,华为云等,都有比较完善的方案,不过这里有一个国外的Digital Ocean,不熟悉的可以多去看看,很多比较好的关于服务器维护管理等知识。我以前经常阅读,受益匪浅,强烈推荐。Docker也是越发流行,互不干扰的环境非常适合很多个项目。趋势及总结这里对2019的趋势做了简单的预测,很多其实并不是新知识了,但是他们依然有着极大的Web开发地位,依旧要反复学习,下面就几个重点,关键词有兴趣的可以了解一套代码实现多端应用的最佳方案,Ionic,React Native,Flutter等,他们也是各有利弊,有必要学习了解下。TypeScript;GraphQL & Apollo;AI和机器学习;区块链技术;PWA(渐进式Web应用)等等好了,其实没有什么太多干货,更多的是对视频内的一些总结,其实这几天我也看过不少Web开发的2018和2019,基本上大同小异。就我个人而言,我今天最大的目标就是更加熟练的掌握ES6,将Vue和React玩到飞起,Node.js也更进一步,再小试TS。好了,差不多就这些了,那你的2019目标是什么呢,欢迎讨论哦~其他视频参考(需要梯子):10 Predictions about 2019 for DevelopersTop 8 Web Development Trends 2019 ...

January 2, 2019 · 1 min · jiezi

2018-某熊的技术之路: 做些有趣的产品

2018-某熊的技术之路: 做些有趣的产品年初的时候,我就在想,今年的主题词是什么;上半年考虑的较多的是所谓研发效能的提升,下半年却渐渐发现自己更多的会在想产品这两个字。从代码出发,在写代码的时候会想到模块/库/框架/平台等等概念,但是突兀地发现好像从未想过,做的任何东西都是产品。印象深刻的 Case 就是浏览了 Don’t Make Me Think 及其他书籍之后,慢慢发现自己之前无论是写页面(譬如个人主页)、PPT 还是论文,都推崇草蛇灰线,却是极大地违反了受众的习惯;为了勉励自己能在 2019 年更加的下沉到产品思考,就将本文重定位为,做些有趣的产品。Github,知识检索与知识图谱今年投入精力较多的算是对于笔记/代码的重整合,使得各个模块、脉络更为清晰,也方便外化。偶尔会瞅瞅 Github 的总 Star 数,不知不觉终于要破万了,这种行为没啥大意义,但是会告诉我生活不止眼前的苟且或磅礴,还有属于自己的路。阅读,写作,编程本身就是乐趣,而不仅仅是他们的结果。生,活不难,生活却艰难,给人生多几个坐标,几个赛道也是舒缓压力不错的方式。我的 Github 知识类仓库,大概分为如下几部分:前几年喜欢强调对于资讯的掌握程度,以逛各种聚合阅读、博客为主,虽然想着要系统化学习,却不可否认地在这里逡巡还是只能窥冰山一角,还是需要阅读大部头的书籍,完整地学习某些课程或者自己从零开始造些玩具轮子。笔者开始降低每日的阅读量,而是尝试改为专题式的学习,每周可以定一个 Topic,专注地,不贪多贪全地去学习。这里 Awesome-CS-Books-Warehouse 存放了笔者阅读过的书籍以及书摘,而 InfraS-Wheels 则是希望能够在重造轮子的过程中,提升自己的基础掌握与代码实践能力。为了方便检索,我也特地重构了个人主页,把知识图谱、知识架构与知识检索结合到了一起:不得不说,这个个人主页做的极烂,极大地违反了 Don’t Make Me Think 这个原则。交互并不限于界面,应该是完整的用户流程;虽然笔者用了 PWA/Web Worker 等些许的优化手段,网页搜索的响应,包括每次需要打开浏览器,而无法 One Stop 一键直达等性质,让我选择自建了 alfred-sg 这个工具:可以先在 MAC 上安装效率神器 Alfred,然后直接使用 NPM 安装即可:$ npm install -g alfred-sg欢迎使用,欢迎 ISSUE。做了许久的 MD 工程师,不知道何时能晋升到高级 MD 工程师。数据浪潮之间的前端工程师现在是专门做前端的工程师,却感觉自己离前端愈来愈远;今年唯一与前端相关的总结就是数据浪潮之间的前端工程师了吧,写完自己却觉得索然无味。数据浪潮之后,有 Web 前端、数据富集与处理、人工智能,自己却更像 API 调用工程师,针对不同的业务场景选择合适的 API,选择合适的模型。这里不再赘述,笔者还是想随意说些其他的前端感想:No Warning,在编写 JS/TS 项目时习惯了不放过任一的 ESLint/TSLint Warning, 很多 Bug 就隐藏在 Warning 之下。面向重构编程,使用 ts;拥抱变化,项目之处的很多规划、设计、业务可能都会改变,不畏变化,随时重构。圆角,阴影,边距,良好的交互源于细节,产品不仅仅可用就好。恰到好处地主动优化,平衡用户体感,业务价值与自我排期;过度/提前优化也是万恶之源。用产品思维做技术,用技术赋能产品。GraphQL 并非银弹,不建议直接替换客户端的状态管理,详情参看 GraphQL CheatSheet。杂谈今年年初的时候完成了跳槽,换了个吃饭的地方。即有意料之中,也有意料之外。不过自我的主观情绪确实是有很大的波动,工作的压力,自我的迷茫,更加地勤奋与分秒必争。也会要去关注主要矛盾,不能为,那些重要但是仍为支路,或者无法以主观愿望为转移的事务,过多的倾入情感,平滑这些压力或者挫折;不断地自我学习,关注行业变化。这个冬天真的很冷,都冻掉了几层皮,希望来年能温暖一些吧。几年前从创业开始踏入社会,称兄道弟者多,能互称 SD 者少,确实很开心能在新的岗位碰到一群 SD。下半年在新工作岗位上愈发忙碌,写字的时间都少了;有时候也会感慨,愈忙碌,越懒惰,越不能专注与坚持,忙忙碌碌,碌碌无为。阅读、记录、整理、编码,在工作的主航道之外还有些通幽曲径,让自己休憩释怀。竞争意识会降低竞争力,使你局限在某个困境或者泥潭中,从而忘记自己的规划与目标。思考与执行分离,充分讨论,坚决执行。使用番茄工作法 过多的碎片化信息反而会带来副作用;求全求杂,过多的碎片化工作也很难带来可感的成就感或者可度量的成长。我的编程能力不行,所以希望提升编程能力。编程能力是提升研发效能的重要保障,对于笔者而言,良好编程能力的外在表现是能够随时随地用合适的语言无阻塞地实现某些功能需求。今年恰好是上一个三年的结束,得失兼有;希望下一个三年,不负韶华不负卿。希望明年,我能继续前行,不再懒惰,能做些有趣的事;有目的的,批量地处理事情,无论是工作,娱乐(刷朋资讯,看漫画)都应该专注地去做。 ...

December 31, 2018 · 1 min · jiezi

从前端界面开发谈微信小程序体验

本文由云+社区发表作者介绍:练小习,2011年加入搜狐,负责搜狐相册的产品策划与前端开发。2015年后加入腾讯 ISUX (社交用户体验设计部),目前主要负责腾讯云的UI开发工作,专注于人机交互,有丰富的UI开发经验。这段时间有幸加入了一个关于微信小程序的项目开发组,从无到有的根据文档自行学习了小程序的开发过程,前面已经有几位前辈的文章珠玉在前,我这里就先从前端界面的开发方面谈一谈小程序以及我所遇到的问题吧。在结构和样式方面,小程序提供了一些常用的标签与控件,比如:view,小程序主要的布局元素,类似于html标签的div,你也完全可以像控制div那样去控制view。scroll-view,你要滚动内容的话,没必要用view去做overflow,scroll-view提供了更为强大的功能,通过参数的调整,你可以控制滚动方向,触发的事件等等swiper,滑块视图容器,对于新手来说,再也不用为选用哪个滚动插件伤脑筋了icon,小程序提供了多种图标供你直接使用text,文本,唯一可以通过长按被选中内容的一个组件。progress,进度条button 按钮,尽量使用小程序提供给你的几种样式参数表单以及常用表单组件 :form,input,checkbox,label,picker,radio,slider,switch各种操作反馈,消息提示框:action-sheet,modal,toast,loading以及一些媒体组件,video、audio,image,canvas等等这些东西在几位前辈的文章里,以及微信的开发文档里都有更详细的介绍,我这里就不再一一介绍。那么我们就快速的跑通一个小程序的demo先。在开发之前你要有微信开发者工具。这里我要假设大家有已经拿到了内测或者公测的资格,因为没有拿到的话下面的步骤是没法进行的。打开以后你会看到这样的界面,我们选择小程序进入这时候就可以创建项目了填写你拿到的appid,和自己的项目名称以及目录,然后就可以打开自己的项目了。界面如下:左侧菜单栏就不说了,中间是编译后的预览界面,新的开发者工具已经可以做到实时更新,不需要每次都去点编译了。右侧是你的项目目录,其中pages就是你的页面结构目录了,每个页面下面必须要有 js,wxml,wxss这三个文件,缺少任何一个的话都无法上传预览。然后就是那个当前态的app.json文件,app.json 是对整个小程序的全局配置。我们可以在这个文件中配置小程序是由哪些页面组成,配置小程序的窗口背景色,配置导航条样式,配置默认标题。注意该文件不可添加任何注释。,我们简单的配置一下:这里比较好的一点是,navigationbar的背景颜色支持自定义任意颜色了,不再有只能黑白透明的限制,还是很不错的。然后我们在页面p里简单的写个Hello World保存以后预览界面已经立即刷新出来如果想真机测试(个人建议一定要真机测试,特别是给上下游预览的时候,pc上的样式还原程度较差,包括字体等等,毕竟系统不同。),只需要选中项目选项然后在界面上点击预览下面的三个选项都比较实用,可以根据需要点选。然后就会弹出可以用注册过的微信号真机预览的二维码,如图:这样一个简单的小程序demo就完全跑通了。上面和配置文件app.json平级的还有一个app.js文件,是小程序的脚本代码。我们可以在这个文件中监听并处理小程序的生命周期函数、声明全局变量,在每个page目录里的js做当前页面的业务操作。但是小程序的页面的脚本逻辑是在JsCore中运行,JsCore是一个没有窗口对象的环境,所以不能在脚本中使用window,也无法在脚本中操作组件,所以我们常用的zepto/jquery 等类库也是无法使用的。另一个app.wxss文件,这个是全局的样式,所有的页面都会调用到,每个项目目录下面的wxss是局部样式文件,不会和其他目录产生污染,可以放心使用样式名。他提供的WXSS(WeiXin Style Sheets)是一套样式语言,具有 CSS 大部分特性,可以看作一套简化版的css。同时为了更适合开发微信小程序,还对 CSS 进行了扩充以及修改,直接帮我们把适配的一部分工作都做了,比如他的rpx(responsive pixel),可以根据屏幕宽度进行自适应,规定屏幕宽为750rpx。如在 iPhone6 上,屏幕宽度为375px,共有750个物理像素,则750rpx = 375px = 750物理像素,1rpx = 0.5px = 1物理像素。这个很赞,很方便。你可以简单的理解为就是你平时按照750设计稿开发的流程,只不过你不需要再去做rem的转换和适配工作了,所以小程序的视觉稿,最好也是按照750来出。但是!在不同的屏幕上多多少少会有一些差异,只能根据大家的经验去规避和解决,通过媒体查询也好,还是其他方法也好。而且,在wxss里不能引用本地资源,说起来这个坑,满眼都是泪。那天晚上写小程序demo的时候,没有仔细的去查去问,自己闷头边写边预览,突然真机预览就不行了,毫无预兆,我也完全不会想到是一个背景图造成的问题,折腾了大半夜终于知道了问题所在,于是很开心的把图片都转换成base64,心里想着这下没问题了吧?结果预览上传又失败了,继续折腾了下半夜,才知道小程序对整个包的大小有严格要求,不可以超过1M,最后把所有的静态资源都放到了腾讯云的cdn,才算解决了这个问题。所以如果你要写背景图,那么需要引用一个线上的图片在这里极不推荐使用base64!极不推荐使用base64!极不推荐使用base64!另外一个应用同时只能打开5个页面,当已经打开了5个页面之后,wx.navigateTo不能正常打开新页面。请避免多层级的交互方式。在开发的过程中也不可避免的遇到了一些小坑,举个例子,比如一个简单的switch控件,你可以通过查看元素的方式轻易得知他的自身样式那么我要做一个简单的和文本垂直剧中对齐,从以往的css经验,只要vertical-align: middle就可以轻松解决了,在本地预览的时候也是这样好好的可是在真机测试的时候,各种设备就开始出现偏差了然后简单的审查元素之后发现问题在于这个控件是存在空白区域的,根据设备,屏幕大小的不一,空白区域大小也不一致。受于时间紧迫,可翻阅文档有限,感觉是因为默认的行高原因,于是我只好发挥老司机的狡猾本质,可以通过行高或者overflow的控制,干掉多余的部分,最终真机界面显示还算统一如果你要按照像素级别设计稿来做小程序开发的话,控件的小差异还是需要自己来做一些控制(也有可能从根本上就是我个人用错了方法或者理解错了,鉴于文档太少,以后开发者多了大家会有更清晰的认识。)还有另一个遇到的问题,就是小程序对 image 的默认渲染,这是通过工具查看默认图像的样式经过多方打听发现小程序的image是按照background-image来实现的,所以所有图像会得到一个初始宽高320 240,而且无法通过auto重置,只可以通过具体的值来重写。好在微信提供了3种缩放模式,9种裁剪模式,在大多数场景可以满足我们对图片的控制:例如原图:scaleToFill 模式不保持纵横比缩放图片,使图片完全适应aspectFit保持纵横比缩放图片,使图片的长边能完全显示出来aspectFill保持纵横比缩放图片,只保证图片的短边能完全显示出来top不缩放图片,只显示图片的顶部区域bottom不缩放图片,只显示图片的底部区域center不缩放图片,只显示图片的中间区域left不缩放图片,只显示图片的左边区域right不缩放图片,只显示图片的右边边区域top left不缩放图片,只显示图片的左上边区域top right不缩放图片,只显示图片的右上边区域bottom left不缩放图片,只显示图片的左下边区域bottom right不缩放图片,只显示图片的右下边区域如果你有更严格的图片设计展示方式,那么可以尝试用一些特殊的方式去控制图像的宽高吧。还有小程序的button控件,他的初始样式里并没有border,所以我费尽心思也没能把他重写为一个无边无背景的设计形式,最终为了满足设计稿,个别语义化为按钮的元素,我是用其他更可控的元素来实现的,比如这个界面的发送图片按钮但是到后来才知道button是通过after来写的样式,开发者工具的调试里完全看不到这个after(┬_┬)…..除了这些UI开发上的体会,大家也都知道,小程序诞生就不是为了展示,他不适合做纯展示型的东西,主要是做一些功能型的应用。而微信所提供的小程序现有的SDK和DEMO,缺乏对服务端的支持,依赖开发者逐个模块搭建服务;而且必须通过HTTPS完成与服务端通信,依赖开发者自行完成证书申请部署;鉴权流程安全性要求高,开发者高效安全的完成会话管理难度会比较大;提供了WebSocket长连接通信的客户端API,但缺乏服务端配套支持,开发者自行实现难度还是较大的。并且具备快速传播,流量突增的特点,要求架构具备弹性伸缩能力。此文已由作者授权腾讯云+社区发布 腾讯云祝您元旦快乐~

December 29, 2018 · 1 min · jiezi

也许你对 Fetch 了解得不是那么多(上)

编者按:除创宇前端与作者博客外,本文还在语雀发布。前言本篇主要讲述 Fetch 的一些基本知识点以及我们在生产开发中怎么去使用。为了能够更好的了解 Fetch,我们希望你对以下知识点有所了解,如果有相关的开发经验,那是最好不过的了。XHRPromiseHTTP本文中对有些关键词提供了相应的链接,如果你对该关键词不够了解或想要了解更多,你可以通过点击它充实自己。文中有些知识点在 MDN Fetch 上已经写的很详细,因此有略过,希望同学们在阅读本文章时能够同时对照阅读。本文行文思路首先从规范入手,目的是让大家了解的更透彻,达到知其然知其所以然。为了更好的掌握 Fetch,文章中还提供了一些示例代码供大家学习使用。在使用该示例代码前,我们希望你对 node.js 有一些了解,如果没有的话,你可以根据示例中的友情提示完成你的这次学习体验。读完本篇文章后你将了解到以下内容:什么是 FetchFetch 的一些基本概念如何使用 FetchFetch 的一些不足以及我们如何“优雅”的使用它希望你通过读完本篇文章后,对 Fetch 有一个基本的了解。Fetch 简介Fetch 是一种新的用于获取资源的技术,它被用来代替我们已经吐槽了很久的技术(XHR)。Fetch 使用起来很简单,它返回的是一个 Promise,即使你没有 XHR 的开发经验也能快速上手。说了那么多,我们还是先睹为快吧,让我们快快下面的示例代码。fetch(‘https://github.com/frontend9/fe9-library', {method: ‘get’}).then(function(response) {}).catch(function(err) {// Error});是不是简单的不能再简单了?好,既然我们 Fetch 有了简单的认识之后,那我们再来了解下 Fetch 的基本概念。Fetch 基本概念在 Fetch 中有四个基本概念,他们分别是 Headers、Request 、Response 和 Body。为了更好的理解 Fetch,我们需要对这些概念做一个简单的了解。在一个完整的 HTTP 请求中,其实就已经包含了这四个概念。请求中有请求头和请求体,响应中有响应头和响应体。所以我们有必要了解这些概念。Headers为了实现头部的灵活性,能够对头部进行修改是一个非常重要的能力。Headers 属于 HTTP 中首部的一份子,它是一个抽象的接口,利用它可以对 HTTP 的请求头和响应头做出添加、修改和删除的操作。下面我们先看一下它具有哪些接口:typedef (sequence<sequence<ByteString>> or record<ByteString, ByteString>) HeadersInit;[Constructor(optional HeadersInit init), Exposed=(Window,Worker)]interface Headers { void append(ByteString name, ByteString value); void delete(ByteString name); ByteString? get(ByteString name); boolean has(ByteString name); void set(ByteString name, ByteString value); iterable<ByteString, ByteString>;};interface Headers { void append(ByteString name, ByteString value); void delete(ByteString name); ByteString? get(ByteString name); boolean has(ByteString name); void set(ByteString name, ByteString value); iterable<ByteString, ByteString>;};// 来自 https://fetch.spec.whatwg.org/#headers-class规范中定义的接口我们可以对应着 MDN 进行查看,你可以点击这里更直观的看看看看它有哪些方法供我们使用。这里我们对 Headers 的构造参数做个解释。首先参数类型为 HeadersInit,我们再看下这个类型支持哪些类型的值。我们从规范中可以看到的定义是:typedef (sequence<sequence<ByteString>> or record<ByteString, ByteString>) HeadersInit;这里我们对应到 JavaScript 这门语言,意思就是说这个对象可以是数组或者是键值对(即对象)。关于如何初始化这些参数,我们可以看下规范中定义的流程。To fill a Headers object (headers) with a given object (object), run these steps:If object is a sequence, then for each header in object:If header does not contain exactly two items, then throw a TypeError.Append header’s first item/header’s second item to headers.Otherwise, object is a record, then for each key → value in object, append key/value to headers.这里我需要对这个做个说明,后面对 fetch 的用法会涉及到一点以及我们看 polyfill 都会有所帮助。第一种:即数组,当数据每项如果不包含两项时,直接抛出错误。然后数组第一项是 header 名,第二项是值。,最后直接通过 append 方法添加。第二种:即键值对(这里指对象),我们通过循环直接取到键值对,然后通过 append 方法添加。示例示例代码地址:https://github.com/GoDotDotDo…打开浏览器输入:http://127.0.0.1:4000/headers那么我们该如何使用它呢?首先我们需要通过 new Headers() 来实例化一个 Headers 对象,该对象返回的是一个空的列表。在有了对象实例后,我们就可以通过接口来完成我们想要的操作,我们来一起看看下面的示例: function printHeaders(headers) { let str = ‘’; for (let header of headers.entries()) { str += &lt;li&gt;${header[0]}: ${header[1]}&lt;/li&gt; ; console.log(header[0] + ‘: ’ + header[1]); } return &lt;ul&gt; ${str} &lt;/ul&gt;; } const headers = new Headers(); // 我们打印下看看是否返回的是一个空的列表 const before = printHeaders(headers); // 发现这里没有任何输出 document.getElementById(‘headers-before’).innerHTML = before; // 我们添加一个请求头 headers.append(‘Content-Type’, ’text/plain’); headers.append(‘Content-Type’, ’text/html’); headers.set(‘Content-Type’, [‘a’, ‘b’]); const headers2 = new Headers({ ‘Content-Type’: ’text/plain’, ‘X-Token’: ‘abcdefg’, }); const after = printHeaders(headers); // 输出:content-type: 如果你觉得每次都要 append 麻烦的话,你也可以通过在构造函数中传入指定的头部,例如:const headers2 = new Headers({ ‘Content-Type’: ’text/plain’,‘X-Token’: ‘abcdefg’});printHeaders(headers2);// 输出:// content-type: text/plain// x-token: abcdefg这里我添加了一个自定义头部 X-Token,这在实际开发中很常见也很有实际意义。但是切记在 CORS 中需要满足相关规范,否则会产生跨域错误。你可以通过append 、 delete 、set 、get 和has 方法修改请求头。这里对 set 和 append 方法做个特殊的说明:set: 如果对一个已经存在的头部进行操作的话,会将新值替换掉旧值,旧值将不会存在。如果头部不存在则直接添加这个新的头部。append:如果已经存在该头部,则直接将新值追加到后面,还会保留旧值。为了方便记忆,你只需要记住 set 会覆盖,而 append 会追加。GuardGuard 是 Headers 的一个特性,他是一个守卫者。它影响着一些方法(像 append 、 set 、delete)是否可以改变 header 头。它可以有以下取值:immutable、request、request-no-cors、response 或 none。这里你无需关心它,只是为你让你了解有这样个东西在影响着我们设置一些 Headers。你也无法去操作它,这是代理的事情。举个简单的例子,我们无法在 Response Headers 中插入一个 Set-Cookie。如果你想要了解更过的细节,具体的规范请参考 concept-headers-guard 和 MDN Guard注意我们在给头部赋值的时候需要满足可接受的首部字段集合否则将会报 TypeError 。BodyBody 准确来说这里只是 mixin,代表着请求体或响应体,具体由 Response 和 Request 来实现。下面我们来看看它具有哪些接口:interface mixin Body { readonly attribute ReadableStream? body; readonly attribute boolean bodyUsed; [NewObject] Promise<ArrayBuffer> arrayBuffer(); [NewObject] Promise<Blob> blob(); [NewObject] Promise<FormData> formData(); [NewObject] Promise<any> json(); [NewObject] Promise<USVString> text();};// 来自 https://fetch.spec.whatwg.org/#body规范中定义的接口我们可以对应着 MDN 进行查看,你可以点击这里更直观的看看它有哪些属性和方法供我们使用。这里需要注意看这些方法返回的都是 Promise,记住这在基于 fetch 进行接口请求中很重要。记住了这个,有利于我们在后面的文章中理解 fetch 的用法。示例范例将在 Response 中体现。RequestRequest 表示一个请求类,需要通过实例化来生成一个请求对象。通过该对象可以描述一个 HTTP 请求中的请求(一般含有请求头和请求体)。既然是用来描述请求对象,那么该请求对象应该具有修改请求头(Headers)和请求体(Body)的方式。下面我们先来看下规范中 Request 具有哪些接口:typedef (Request or USVString) RequestInfo;[Constructor(RequestInfo input, optional RequestInit init), Exposed=(Window,Worker)]interface Request { readonly attribute ByteString method; readonly attribute USVString url; [SameObject] readonly attribute Headers headers; readonly attribute RequestDestination destination; readonly attribute USVString referrer; readonly attribute ReferrerPolicy referrerPolicy; readonly attribute RequestMode mode; readonly attribute RequestCredentials credentials; readonly attribute RequestCache cache; readonly attribute RequestRedirect redirect; readonly attribute DOMString integrity; readonly attribute boolean keepalive; readonly attribute boolean isReloadNavigation; readonly attribute boolean isHistoryNavigation; readonly attribute AbortSignal signal; [NewObject] Request clone();};Request includes Body;dictionary RequestInit { ByteString method; HeadersInit headers; BodyInit? body; USVString referrer; ReferrerPolicy referrerPolicy; RequestMode mode; RequestCredentials credentials; RequestCache cache; RequestRedirect redirect; DOMString integrity; boolean keepalive; AbortSignal? signal; any window; // can only be set to null};enum RequestDestination { “”, “audio”, “audioworklet”, “document”, “embed”, “font”, “image”, “manifest”, “object”, “paintworklet”, “report”, “script”, “sharedworker”, “style”, “track”, “video”, “worker”, “xslt” };enum RequestMode { “navigate”, “same-origin”, “no-cors”, “cors” };enum RequestCredentials { “omit”, “same-origin”, “include” };enum RequestCache { “default”, “no-store”, “reload”, “no-cache”, “force-cache”, “only-if-cached” };enum RequestRedirect { “follow”, “error”, “manual” };// 来自 https://fetch.spec.whatwg.org/#request-class规范中定义的接口我们可以对应着 MDN 进行查看,你可以点击这里更直观的看看它有哪些属性和方法供我们使用,这里不做一一解释。注意这里的属性都是只读的,规范中我们可以看到构造函数的第一个参数为 Request 对象或字符串,我们一般采取字符串,即需要访问的资源地址( HTTP 接口地址)。第二个参数接收一个 RequestInit 可选对象,而这个对象是一个字典。在 javascript 中,我们可以理解为一个对象({})。RequestInit 里面我们可以配置初始属性,告诉 Request 我们这个请求的一些配置信息。这里我们需要对以下几个属性特别注意下。mode 是一个 RequestMode 枚举类型,可取的值有 navigate, same-origin, no-cors, cors。它表示的是一个请求时否使用 CORS,还是使用严格同源模式。当处于跨域情况下,你应当设置为 cors。该值的默认值在使用 Request 初始化时,默认为 cors。当使用标记启动的嵌入式资源,例如 <link>、 <script>标签(未手动修改 crossorigin 属性),默认为 no-cors。详细信息请参考 whatwg 规范或 MDN 。credentials 是一个 RequestCredentials 枚举类型,可取的值有 omit, same-origin, include。它表示的是请求是否在跨域情况下发送 cookie。看到这,如果对 XHR 了解的同学应该很熟悉。这和 XHR 中的 withCredentials 很相似。但是 credentials 有三个可选值,它的默认值为 same-origin。当你需要跨域传递 cookie 凭证信息时,请设置它为 include。注意这里有一个细节,当设置为 include 时,请确保 Response Header 中 Access-Control-Allow-Origin 不能为 ,需要指定源(例如:http://127.0.0.1:4001),否则会你将会在控制台看到如下错误信息。详细信息请参考 whatwg 规范或 MDN 。The value of the ‘Access-Control-Allow-Origin’ header in the response must not be the wildcard ‘’ when the request’s credentials mode is ‘include’.你可以使用文章中提供的代码中启动 cors 示例代码,然后在浏览器中输入 http://127.0.0.1:4001/request,如果不出意外的话,你可以在控制台中看到上面的错误提示。body 是一个 BodyInit 类型。它可取的值有 Blob,BufferSource , FormData , URLSearchParams , ReadableStream , USVString。细心的同学不知道有没有发现,我们常见的 json 对象却不在其中。因此,我们如果需要传递 json 的话,需要调用 JSON.stringify 函数来帮助我们转换成字符串。下面将给出一段示例代码。示例示例代码地址:https://github.com/GoDotDotDo…打开浏览器输入:http://127.0.0.1:4000/request // 客户端 const headers = new Headers({ ‘X-Token’: ‘fe9’, }); const request = new Request(’/api/request’, { method: ‘GET’, headers, }); console.log(request); // Request {method: “GET”, url: “http://127.0.0.1:4000/api/request”, headers: Headers, destination: “”, referrer: “about:client”, …} console.log(request.method); // GET console.log(request.mode); // cors console.log(request.credentials); // same-origin // 如果你想打印headers信息,可以调用 printHeaders(request.headers)这里我们先以 GET 简单请求作为示例,我们传递了一个自定义的 Headers,指定了请求方法 method 为 GET(默认为 GET)。在上面的接口规范中,我们可以通过 Request 对象拿到一些常用的属性,比如 method、url、headers 、body 等等只读属性。ResponseResponse 和 Request 类似,表示的是一次请求返回的响应数据。下面我们先看下规范中定义了哪些接口。[Constructor(optional BodyInit? body = null, optional ResponseInit init), Exposed=(Window,Worker)]interface Response { [NewObject] static Response error(); [NewObject] static Response redirect(USVString url, optional unsigned short status = 302); readonly attribute ResponseType type; readonly attribute USVString url; readonly attribute boolean redirected; readonly attribute unsigned short status; readonly attribute boolean ok; readonly attribute ByteString statusText; [SameObject] readonly attribute Headers headers; readonly attribute Promise<Headers> trailer; [NewObject] Response clone();};Response includes Body;dictionary ResponseInit { unsigned short status = 200; ByteString statusText = “”; HeadersInit headers;};enum ResponseType { “basic”, “cors”, “default”, “error”, “opaque”, “opaqueredirect” };// 来自 https://fetch.spec.whatwg.org/#response-class规范中定义的接口我们可以对应着 MDN 进行查看,你可以点击这里更直观的看看它有哪些属性和方法供我们使用,这里不做一一解释。其中 status, headers 属性最为常用。通过 status 状态码我们可以判断出服务端请求处理的结果,像 200, 403 等等常见状态码。这里举个例子,当 status 为 401 时,可以在前端进行拦截跳转到登录页面,这在现如今 SPA(单页面应用程序)中尤为常见。我们也可以利用 headers 来获取一些服务端返回给前端的信息,比如 token。仔细看上面的接口的同学可以发现 Response includes Body; 这样的标识。在前面我们说过 Body 由 Request 和 Response 实现。所以 Body 具有的方法,在 Response 实例中都可以使用,而这也是非常重要的一部分,我们通过 Body 提供的方法(这里准确来说是由 Response 实现的)对服务端返回的数据进行处理。下面我们将通过一个示例来了解下简单用法:示例示例代码位置:https://github.com/GoDotDotDo… // 客户端 const headers = new Headers({ ‘X-Token’: ‘fe9-token-from-frontend’, }); const request = new Request(’/api/response’, { method: ‘GET’, headers, }); // 这里我们先发起一个请求试一试 fetch(request) .then(response => { const { status, headers } = response; document.getElementById(‘status’).innerHTML = ${status}; document.getElementById(‘headers’).innerHTML = headersToString(headers); return response.json(); }) .then(resData => { const { status, data } = resData; if (!status) { window.alert(‘发生了一个错误!’); return; } document.getElementById(‘fetch’).innerHTML = data; });这里我们先忽略 fetch 用法,后面的章节中会进行详细介绍。我们先关注第一个 then 方法回调里面的东西。可以看到返回了一个 response 对象,这个对象就是我们的 Response 的实例。示例中拿了 status 和 headers ,为了方便,这里我将其放到 html 中。再看看该回调中最后一行,我们调用了一个 response.json() 方法(这里后端返的数据是一个 JSON 对象,为了方便直接调用 json()),该方法返回一个 Promise,我们将处理结果返给最后一个 then 回调,这样就可以获得最终处理过后的数据。打开浏览器,输入 http://127.0.0.1:4000/response,如果你的示例代运行正常,你将会看到以下页面:(查看 Response 返回的数据)编者注:本文未完待续。文 / GoDotDotDotLess is more.编 / 荧声作者其他文章:优秀前端必知的话题:我们应该做些力所能及的优化本文已由作者授权发布,版权属于创宇前端。欢迎注明出处转载本文。本文链接:https://blog.godotdotdot.com/…想要订阅更多来自知道创宇开发一线的分享,请搜索关注我们的微信公众号:创宇前端(KnownsecFED)。欢迎留言讨论,我们会尽可能回复。欢迎点赞、收藏、留言评论、转发分享和打赏支持我们。打赏将被完全转交给文章作者。感谢您的阅读。新年快乐 :) ...

December 29, 2018 · 5 min · jiezi

web开发的跨域问题详解

本文由云+社区发表做过 web 开发的同学,应该都遇到过跨域的问题,当我们从一个域名向另一个域名发送 Ajax 请求的时候,打开浏览器控制台就会看到跨域错误,今天我们就来聊聊跨域的问题。1. 浏览器的同源策略同源的定义是:如果两个页面的协议,端口(如果有指定)和域名都相同,则两个页面具有相同的源。同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。2. 跨域错误信息产生的原因为了说明问题,我们可以做如下实验,我们在本地搭建了开发环境, 由客户端 http://localhost:3001 向服务器 http://localhost:3000 发送两个请求,一个使用 javascript 异步请求数据,另一个使用 img 标签请求数据,服务器收到请求后,打印接收到请求的日志,如下图所示:客户端发送两个请求服务端打印日志并处理请求代开客户端浏览器的控制台,可以看到发出了两个请求,并且都收到了状态码为 200 的响应,同时控制台报了一个错误,即 xhr 请求报错。由此我们可以知道,之所以产生跨域错误信息,原因有以下三条:浏览器端的限制(服务端收到了请求并正确返回)发送的是 XMLHttpRequest 请求(使用 img 标签发送的请求为 json 类型,并不会报错)请求了不同域的资源只有同时满足了这三个条件,浏览器才会产生跨域错误。3. 解决跨域的思路既然我们知道了跨域错误产生的原因,那么解决思路就很直观了,针对出错的三个原因进行相应的处理即可,相应的解决思路也有三个方向:打破浏览器的限制不发送 XHR 请求解决跨域下文将分别进行阐述。3.1 打破浏览器的限制由上面分析结论可知,之所以出现跨域的错误,实际上是客户端浏览器所做的限制,服务器并未进行限制,因此我们可以通过设置浏览器,使其不进行跨域检查。实际上浏览器也提供了对应的设置选项。以 MacOS 下的 Chrome 浏览器为例,在终端中使用命令open -n /Applications/Google\ Chrome.app/ –args –disable-web-security –user-data-dir=/Users/your-computer-account/MyChromeDevUserData/打开浏览器,即可禁用 Chrome 浏览器的安全检查功能,同时也会禁用跨域安全检查功能,这样再次拿前面的例子进行测试,发现此时不会报错,同时也可以正确拿到服务端返回的数据。禁用浏览器安全检查功能这种方式虽然可以实现跨域,但是需要每个用户都对浏览器进行设置,同时可能导致潜在的安全隐患,正常情况下不实用。但这个例子充分说明了,跨域错误是前端浏览器所做的限制,与后台服务无关。3.2 JSONP实现跨域根据思路2,既然跨域问题产生的原因是因为客户端发送了 Ajax 请求,那么我们打破这个条件即可。具体实现方式就是使用 JSONP 来进行跨域请求。JSONP,是 JSON with Padding 的简称,它是 json 的一种补充使用方式,利用 script 标签来解决跨域问题。JSONP 是非官方协议,他只是前后端一个约定,如果请求参数带有约定的参数,则后台返回 javascript 代码而非 json 数据,返回代码是函数调用形式,函数名即约定值,函数参数即要返回的数据。JSONP 的实现原理如下图所示:JSONP实现原理首先在客户端 js 中定义一个函数(假设名为 handler),然后动态创建一个 script 标签插入页面中,script 标签的 src 属性即要调用的地址,同时,在调用的 url 中加入一个服务端约定的参数(假设名为 callback,参数值为已定义的函数名 handler),服务端收到请求,如果发现请求的 url 中带有约定的参数,那么就返回一段函数调用形式的 javascript 代码,该段代码的函数名即为 callback 参数的值 handler,函数的参数即为待返回的数据。这样,客户端拿到返回结果后就会执行 handler 函数,对返回的数据进行处理。我们使用 jquery 向服务端发送一个 JSONP 格式的请求,从浏览器控制台可以看到请求和对应的响应,如下图所示:JSONP请求JSONP请求的响应由上图可以看到,发送JSONP请求时,请求的 Type 为 script 类型而非 xhr 类型,这样就打破了跨域报错的三个必要条件,不会产生跨域错误,同时也验证了服务端返回的数据格式为 javascript 代码调用的形式,其中 Jquery331045** 这一长串函数名是 jquery 自动生成的。由于 JSONP 的原理是使用 script 标签来加载数据,所以它的兼容性很好,但是使用 JSONP 来解决跨域问题存在以下缺陷:只能发送 GET 请求发送的不是 XHR 请求,这样导致 XHR 请求中的很多事件都无法进行处理服务端需要改动3.3 跨域资源共享CORSCORS 是一个 W3C 标准,全称是"跨域资源共享"(Cross-origin resource sharing)。它允许浏览器向跨源服务器,发出 XMLHttpRequest 请求,从而克服了 AJAX 只能同源使用的限制。CORS 基于 http 协议关于跨域方面的规定,使用时,客户端浏览器直接异步请求被调用端服务端,在响应头增加响应的字段,告诉浏览器后台允许跨域。跨域错误回到文章开始的这个跨域错误信息,可以看到错误的具体信息是:服务端没有设置Access-Control-Allow-Origin 这个响应头从而导致报错,通过设置 Access-Control-Allow-Origin: * 这个响应头,我们可以解决问题。但是,这种设置能满足所有情况吗? 更进一步,使用 CORS 时浏览器如何检查跨域错误? 前面我们有讲到,虽然浏览器报错,但是在这之前服务端已经接受了请求,那么,浏览器总是先发出请求后再进行判断吗?下面我们一一讨论。3.3.1 浏览器如何检查跨域错误浏览器检查跨域错误的基本原理是:浏览器检测到 ajax 请求的域与当前域不一致,会在请求头中增加 Origin 字段,然后检查服务端响应头 Access-Control-Allow-Origin,如果不存在或不匹配,则报跨域错误。浏览器检查跨域错误原理3.3.2 浏览器总是先发出请求,然后根据是否有 Access-Control-Allow-Origin 响应头来判断吗答案是,对于简单请求,是;而对于非简单请求,不是。非简单请求的情况下,浏览器并不是直接请求所需资源,而是会先发出一个预检请求,预检请求通过后才会对所需资源进行请求。非简单请求预检请求这里涉及到的简单请求和非简单请求的概念,那么简单请求和非简单请求有什么区别呢?MDN 对非简单请求进行了定义,满足下列条件之一,即为非简单请求:使用了下列 HTTP 方法:PUT、DELETE、CONNECT、OPTIONS、TRACE、PATCH使用了除以下首部之外的其他首部:Accept、Accept-Language、Content-Language、Content-TypeContent-Type首部的值不属于下列其中一个: application/x-www-form-urlencoded、 multipart/form-data、 text/plain请求中的 XMLHttpRequestUpload 对象注册了任意多个事件监听器请求中使用了ReadableStream对象简单来说,除了我们平时使用最多的 GET 和 POST 方法,以及最常使用的 Accept、Accept-Language、Content-Language 和 类型为 application/x-www-form-urlencoded、 multipart/form-data、 text/plain 的 Content-Type 请求头,其他基本都是非简单请求。对于这些非简单请求,浏览器会发出两个请求,第一个为 OPTIONS 遇见请求,遇见请求的响应检查通过后才会发出对资源的请求。非简单请求过程生产环境下,如果需要发送非简单跨域请求,每次两个请求会增加响应时间,为此,W3C 标准中增加了另一个响应头 Access-Control-Max-Age 参数,该响应头表明了对于非简单请求的预检请求浏览器的缓存时间,在缓存有效期内,非简单请求可以不发送预检请求,另外,实际开发中,可以在服务端设置接收到的请求方法是 OPTIONS 时,直接返回 200,这样也能加快响应。3.3.3 设置 Access-Control-Allow-Origin: * 就行吗带cookie的跨域当我们需要发送带 cookie 的请求时,Access-Control-Allow-Origin 直接设置为通配符 * 时是无法通过浏览器的检查的,此时该响应头的值必须与发出请求的域完全匹配才行,另外,还需要设置 Access-Control-Allow-Credentials 响应头的值为 true,表示支持带 cookie 的跨域请求。3.3.4 CORS请求头和响应头总结请求头:Origin: 浏览器发出 Ajax 跨域请求之前会添加此头部,值为发送请求的域Access-Control-Request-Method:使用了除 GET、POST 请求方法之外的方法,浏览器会添加此头部,值为当前请求方法Access-Control-Request-Headers:使用了自定义头部或除了Accept、Accept-Language、Content-Language、Content-Type 之外的头部,浏览器会添加此头部,值为当前的请求方法响应头:Access-Control-Allow-Origin: 表示服务端允许哪些域请求资源Access-Control-Allow-Methods: 当客户端包含 Access-Control-Request-Method 请求头时,服务端需要响应该头部,值通常由 Reauest 的 header 中 Access-Control-Request-Method 取得Access-Control-Allow-Headers: 当客户端包含 Access-Control-Request-Headers 请求头时,服务端需要响应该头部,值通常由 Reauest 的 header 中 Access-Control-Request-Headers 取得Access-Control-Expose-Headers: 指出客户端通过 XHR 对象的 getResponseHeaders 方法可以获取的响应头有哪些Access-Control-Allow-Credentials: 允许带 cookie 的跨域请求Access-Control-Max-Age: 预检请求的缓存时间4. 总结本文介绍了跨域的原因,重点介绍了使用 JSONP 和 CORS 解决跨域问题的方法。除此之外,实际开发中还其他各种解决跨域问题的思路,本质上,这些方法都是打破跨域错误的三个条件,大家可以自行查资料了解一下。此文已由作者授权腾讯云+社区在各渠道发布 ...

December 28, 2018 · 2 min · jiezi

如何在web实现连接linux终端

1.使用开源框架GateOne(1)查看服务器python的版本和安装pip$python -V由上图,我服务器的python版本是2.7.5由上图,当python2的版本是2.7.9+或是python3的版本是3.4+时会自带pip,那我的python版本是2.7.5就需要另外安装pip。pip下载由上图,官网的最新版本是18.1。以wget的方式安装pip$wget https://bootstrap.pypa.io/get-pip.py$python get-pip.py(2)安装tornado和Pillow#查看tornado的版本$python -c “import tornado;print(tornado.version)"#上传tornado-2.4.1.tar.gz后进行解压缩安装$tar -zxvf tornado-2.4.1.tar.gz$cd tornado-2.4.1$python setup.py build$python setup.py install#查看pip安装的模块及对应的版本号,显示的Pillow版本是2.0.0$pip listtornado下载(3)安装GateOneGateOne下载#上传GateOne-1.1.tar.gz,解压缩,进行安装$tar -zxvf gateone-1.1.tar.gz$cd GateOne$python setup.py install由上图,安装路径在/opt/gateone启动gateone:第一次启动前,在/opt/gateone目录下不存在server.conf文件,第一次启动之后才会生成。修改配置文件/opt/gateone/server.conf,注意这里添加的origin地址是https:xxxx再次启动打开浏览器进行连接2.使用开源矿建websshwebssh的github地址403 Forbidden处理3.用saltstack实现参考资料:https://pip.pypa.io/en/stable… pip文档http://www.laozuo.org/10703.html CentOS安装配置GateOne实现Web终端SSH功能https://www.cnblogs.com/frank… 【webssh】网页上的SSH终端

December 22, 2018 · 1 min · jiezi

SAP CRM settype的创建,背后发生了什么

来自我的同事Sara。当我们在CRM系统里创建一个settype之后,其实系统后台悄悄的帮我们创建了很多ABAP对象,比如对应的database tables, other ABAP Dictionary objects, function groups, function modules, and screens等等。Create set types:DB check:自动创建了一个和settype ID同名的数据库表问题:How this DB generate?How to check what function groups, function modules, and screens are created? Especially which screens?When we assign the attributes to the set type, I guess it will insert lines into the table.Jerry的解答:当你创建一个新的settype时,product框架会自动生成针对该settype的structure和存储Table, 以及对应的用于CRUD的function module。但是了解框架本身如何实现我前面说的这些事情,对我们接下来做的co deployment没有直接帮助。你的这个问题可以转换成:假设我只知道product的description字段是通过某个settype实现的,我想不问别人,自己弄清楚该settype的名字和访问该settype的CRUD的function module名字,该如何做?I got one information from my colleague, the Product Description function is implemented by the SAP standard set type. Then I want to make clear:(1) Which standard set type implement Product Description and it’s DB table?(2) Which function module (API) implement the CRUD of Product Description?写在前面:本文提及的方法只限一种思路,不排除个别Assignment Block 不适用的情景,请大家广开思路,积极探索更多的方法来研究,希望能总结出更多的具体API,简化每一个功能的实现。Product ID: SZIPC (in QHD)After this self study, I can get a general knowledge of how the product fields implemented in the SAP System.Then I will start this self study step-by-step by question driven.(1) Which standard set type implement Product Description?Try Google it first. Wow, got some information. set type name COMM_PR_SHTEXT.(2) Is it right? How can I display set type?One way, Google.Second way, I would like to use SE93 to find transaction code by transaction description, got it!TCODE: COMM_SETTYPEThere is some issue, you can not display set type by TCODE: COMM_SETTYPE directly, you can only use COMM_ATTRSET input the set type COMM_PR_SHTEXT first the sue TCODE: COMM_SETTYPE to dispaly.(3) Then how can I find the DB for this set type and the CRUD function model?There is 2 ways.The first way, is to use TCODE: ST05 trace.I update the description from Material_Sara to Material_SaraZhang. I search with key words ‘Material_SaraZhang’, then I find the DB–COMM_PORTEXT.The second way, I would like to use TCODE: SE93 find the package of TCODE COMM_SETTYPE.–Assume,I do not know, Most of the Set Type name = DB Table name.There must be a table include all the Set Type name and DB Table names, how can I find it?This is an important thinking point, for every TCODE in SAP, it’s a collection of multiple functions /tables/views. Which should be packaged in a package.In the package, there is multiple DB, from the DB description, you can know COMC_SETTYPE is the admin table which include all information we want.–Admin table and header table always as the start and base point of DB relationship.Filter by the set type name COMM_PR_SHTEXT, we found the set type DB COMM_PRSHTEXT and related function module.(4) How the function module work when I read the product description?TCODE: SE37 find COM_COMM_PR_SHTEXT_READ_WITH_P and set breakpoint.search Product ID: SZIPCin WebClient UI.Follow the call stack, find the key API for read product set types.(5) How the function module work when I Update the product description?Function Module: COM_COMM_PR_SHTEXT_MAINTAIN_UPFollow the call stack, find the key API for update product set types.(6) I will try to simplify this function module, pick this function into a report for confirming how does this CALL FUNCTION ‘CRM_PRODUCT_UI_GETDETAIL’ work?This is the key function for processing product set types.We’d better to understand it’s indeed input valueOne, is product guid in a structure.Two, is the set type name.It means that, any set type can read by this function.要获取更多Jerry的原创文章,请关注公众号"汪子熙": ...

December 21, 2018 · 3 min · jiezi

WEB-UI自动化实践

1.设计背景随着IT行业的发展,产品愈渐复杂,web端业务及流程更加繁琐,目前UI测试仅是针对单一页面,操作量大。为了满足多页面功能及流程的需求及节省工时,设计了这款UI 自动化测试程序。旨在提供接口,集成到蜗牛自动化测试框架,方便用例的设计。整个程序是基于 selenium 设计的。程序对 selenium 提供的接口进行了二次封装以满足日常的用例设计,二次封装后的接口解决了元素加载,元素定位解析等问题,可以让用例设计变得更加简捷。之所以采用 Selenium 的模式。原因一,对于用户来说这是一个开源框架,很想窥探一二; 原因二,Selenium 可无缝接入。这是一个用于Web应用程序测试的工具,支持多平台、多浏览器、多语言去实现自动化测试,Selenium2将浏览器原生的API封装成WebDriver API,可以直接操作浏览器页面里的元素,甚至操作浏览器本身(截屏,窗口大小,启动,关闭之类的),所以就像真正的用户在操作一样。目前支持:Mac、Windows操作系统,chrome、Firefox、IE浏览器。2.工作原理• 在蜗牛管理后台添加测试用例。• 蜗牛管理后台测试用例执行调用任务执行接口,传送任务id及测试数据的JSON格式字符串给程序。• 程序根据获取数据,解析并处理。• 启动浏览器后,selenium-webdriver会将目标浏览器绑定到特定的端口,启动后的浏览器则作为webdriver的server。• 客户端(也就是测试脚本),借助ComandExecutor发送HTTP请求给server端(通信协议:The WebDriver Wire Protocol,在HTTP request的body中,会以WebDriver Wire协议规定的JSON格式的字符串来告诉Selenium,我们希望浏览器接下来做什么事情)。• Server端需要依赖原生的浏览器组件,转化Web Service的命令为浏览器native的调用来完成操作。• 最后将处理结果及任务id通过JSON字符串的格式返回给蜗牛,通过蜗牛的管理后台可查看每条用例执行结果。3.框架介绍3.1.工程结构按照实际的业务流程调用对应接口来实现 WEB-UI 自动化测试用例。case 层可调用 service 层和 pageObject 层的接口,pageObject 是对每一个页面元素的一个封装,service 是对一个常用的业务模块功能的封装。比如一个查询企业信息的测试用例,需要依赖登入,这个业务功能就可以直接调用 service 中的接口。企业查询的创建就可以调用 pageObject 中的接口,然后按照查询的业务流程,在测试用例中把这些接口串起来就形成了一个 UI 自动化测试用例,详细细节接下去会举例说明。如企业查询。查询之前,需要登入管理后台,登入操作已封装到业务层,直接调用 service 层的接口,不需要在意这个步骤的细节;登入之后要指定一个路径,找到对应的空间,直接调用 model 层的接口,不需要在意这个步骤的细节;接着是创建查询,创建查询的所有定位方法也封装到业务层,这就是个企业查询的实现,也是用例设计中最主要的环节。整个工程基于 selenium,采用 pageObject 模式搭建。下面对工程中的几个重要模块做介绍。3.1.1 driver — 接口层对 web 页面所有元素的操作都是在driver定义接口并实现的。driver 对 selenium 提供的接口做了二次封装,对外提供封装后的接口。pageObject 实现了一些公共方法,比如给输入框赋值等,目前 pageObject封装的方法不多,大多功能都可以通过 selenium 实现。driver 层对开源工具接口做了二次封装,想要驱动一个浏览器还有一个必不可少的工具 —— 浏览器驱动,这个驱动放在 Referenced Libraries 里,驱动的版本必须与被测浏览器版本相匹配。3.1.2 model — 数据模型创建数据模型为了实现测试数据和测试用例分离而采取的一种方法,具体的测试数据初始化。可以对一个业务流程中需要测试数据的元素在一个 model 中定义出来,方便管理和代码阅读。3.1.3 pageObject — 业务层pageObject 模式,采用接口形式封装每一个页面需要用到的元素,实现封装只要做两步:1、确定元素的定位方式;2、调用 driver 中对应的操作接口。driver 的接口实现包含了一定的容错能力,但并不是全面的,部分页面或者组件具有独特性,单纯调用 driver 的接口并不能保证测试用例的稳定性,此时就需要在 pageObject 的接口实现中加入一些容错算法,确保用例稳定性。3.1.4 service — 提供业务功能一个业务流程很多时候依赖其他业务模块功能,为了方便设计一个测试用例,也为了避免重复造轮子,service 层就提供了一些常用的业务功能,比如登入、企业查询等。依赖方只需要在 service 层调用即可。3.2.功能优化对selenium 做二次封装的同时也对接口做了优化,框架的初衷是使UI 用例的设计尽可能的易设计、易读、易维护。3.2.1 接口优化直接调用 selenium 的接口经常会遇到些令人头疼的问题,比如网络问题使页面 loading 太慢,需要操作的元素还没展示出来,这种情况就会经常报元素找不到的 error,导致用例执行失败,但实际上这种报错并不是一个 bug,其测试结果是无效的。为了减少误报率 driver 层接口设计了等待元素加载的功能,使用的关键方法:cf.searchForElementVisibleXpath(TestStartQuitwd.wd, “//[text()=‘运营平台登录’]”, id, 200, 100L)。参考代码:在 click、input 等操作接口中加入循环查找的判断可最大限度的等待一个元素的加载从而提高测试用例的稳定性。3.2.2 元素定位统一入口接触过 UI 自动化用例设计的测试人员会比较清楚,想通过 selenium 操作一个元素,其中必不可少的就是对元素定位的描述,通俗的讲就是要通知接口在当前页面操作哪个位置上的元素。定位一个元素的方法很多,常用的有 id,name,css,xpath 等,对应不同的定位方法selenium 在处理上也给出了不同接口,这从维护角度上来考虑显然不是最好的。最好的做法就是用例设计者只管元素定位和操作事件的调用,而事件在实现上走了哪种渠道最好是无感知,无需维护的。对此框架封装了一个方法供 driver 调用,主要功能就是解析描述元素的字符串自动判断是 id、css 还是 xpath。3.3.元素定位UI自动化用例其实可以分成两部分,1. 定位元素;2. 调用接口操作该元素。其中定位一个元素的方法很多,常用的有 id,name,css,xpath。实际设计中选择哪种定位方法一般会在维护角度上考虑的会多一些,因为现在的服务器性能配置等都很优秀,所以跑一个WEB-UI用例可以不用考虑性能问题。从维护成本上考虑会优先选择 id、name,其次 css,最后用 xpath。我们不能保证每一个 web 系统的所有元素都能提供一个唯一 id 或 name,当然如果能和前端开发达成合作,这就是一件很美好的事情了。一般情况下我们都需要面对没有 id 和 name 这两个属性的情况。这时我们就可以使用 css 样式,很多时候 css 样式是能满足我们的定位需求。当然在这些都不提供给我们的情况下就只能选择 xpath,使用 xpath 的优点易获取,主流浏览器只要打开“查看”就可以通过 copy 轻松获取到;页面上的元素都可以用 xpath 来描述;缺点,不稳定,大量使用会给用例维护产生很大的负担。xpath 一般只要前端在页面上做一下小调整,用例就必须重新维护,在不得不使用 xpath 的情况下,为了减少今后的维护量,可对 xpath 做一些优化,可以减少 xpath 的路径长度提高稳定性。以下是实践过程中最长用到的几种类型:1.依靠自己的属性文本定位,如 //input[@value=‘XXXXX’]2.包含指示性字符,如 //input[contains(text(),’指示性字符’)]3.巧妙使用content,如 //[@id=‘app-container’]4.常见报错使用过程中经常会遇到问题,这里做下总结方便 debug。1.某些页面弹窗,有时候定位不到弹窗元素。理论上 selenium 在一个页面中查找一个元素是可以定位到,但有些时候出现弹窗,此时就需要在重新定位弹窗。解决方法:2.有些输入框不能被 input 接口正常操作。实践过程中在日历控件中遇到过,元素定位什么的都对,但就是不能正常被操作。解决方法:判断元素是否是select类型,之后再赋值。解决代码:3.发现 selenium 的某些接口不能 work 了,此时最大的可能就是浏览器升级了。解决方法:重新下载低版本浏览器。4.元素不可见。有一种元素能在页面上正常展示,但对于工具来说它是不可见的,这是因为在一般情况下,元素可见需要满足以下几个条件:visibility!=hidden ; display!=none; opacity!=0; height、width都大于0;对于 input 标签,没有 hidden 属性。如截图就是只读的实例。解决方法:调用接口TestStartQuitwd.js.executeScript(“var txtN = document.getElementsByName("timeRange"); txtN[0].readOnly = false;”); 5.结束语UI自动化是在开源工具的基础上做了些优化,在 driver 层,数据层、业务层以及用例层的解决方案还有很大的提升空间。WEB-UI自动化还不完美,后期还需继续努力。感谢一直以来支持研究的小伙伴。来源:宜信技术学院作者:颜博莲 ...

December 20, 2018 · 1 min · jiezi

2018博客回顾

8012年马上过去了,整理一下今年的博客内容。canvas烟花锦集时间:2018-01-15内容:了解canvas知识,不同烟火效果实现方式。链接:https://www.bestvist.com/p/49Skeleton Screen – 骨架屏时间:2018-01-19内容:用户体验一直是前端开发需要考虑的重要部分,在数据请求时常见到锁屏的loading动画,而现在越来越多的产品倾向于使用Skeleton Screen Loading(骨架屏)替代,以优化用户体验。链接:https://www.bestvist.com/p/50Fetch – http请求的另一种姿势时间:2018-02-05内容:传统Ajax是利用XMLHttpRequest(XHR)发送请求获取数据,不注重分离的原则。而Fetch API是基于Promise设计,专为解决XHR问题而出现。链接:https://www.bestvist.com/p/51JS中的async/await – 异步隧道尽头的亮光时间:2018-02-07内容:JS中的异步操作从最初的回调函数演进到Promise,再到Generator,都是逐步的改进,而async函数的出现仿佛看到了异步方案的终点,用同步的方式写异步。链接:https://www.bestvist.com/p/52mvvm-simple双向绑定简单实现时间:2018-04-17内容:mvvm模式解放DOM枷锁链接:https://www.bestvist.com/p/53JavaScript Promise查缺补漏时间:2018-04-23内容:Promise 对象代表了未来将要发生的事件,用来传递异步操作的消息。链接:https://www.bestvist.com/p/54鼠标移入移出效果 – jQuery/Vue版时间:2018-06-21内容:根据鼠标hover的方向弹出、收回遮罩层,分为jQuery和Vue版。链接:https://www.bestvist.com/p/56后管模版整理时间:2018-08-15内容:整理一些后管模版,包含bootstrap、vue、react、angular等主流框架。链接:https://www.bestvist.com/p/57花样形状 – CSS时间: 2018-08-27内容:只使用一个html元素绘制图形,部分图形需要浏览器支持。链接:https://www.bestvist.com/p/58UI酷炫交互效果时间:2018-10-08内容:多端交互效果,UI设计。链接:https://www.bestvist.com/p/59vue组件从开发到发布时间:2018-10-15内容:梳理一篇vue组件从开发到发布托管流程。链接:https://www.bestvist.com/p/60JWT - just what?时间:2018-12-07内容:初见JWT,不知所云,赶紧Google(百度)一下,原来是跨域身份验证解决方案。链接:https://www.bestvist.com/p/62原文地址:https://www.bestvist.com/p/63

December 18, 2018 · 1 min · jiezi

React中那些纠结你的地方(一)

React太灵活了特别是配上了es6之后,它就像泥沙里的泥鳅,越想抓住越是抓不住;除此之外我在使用react的时候时不时的会有些纠结1.组件中有很多事件处理,到底是使用一个方法+switch呢?还是每个事件写一个方法?//1. one function + switch patternclass OneFunSwitch extends Component { handleChange = (e)=>{ let {type} = e.target.dataset; /** switch–case–default / } render(){ return( <div> <button onClick={this.handleChange} data-type=“name”>修改名称</button> <button onClick={this.handleChange} data-type=“age”>修改年龄</button> <button onClick={this.handleChange} data-type=“submit”>提交</button> {/ …more evenet /} </div> ) }}// 2. multiple functon patternclass MultipleFun extends Component { handleNameChange = ()=>{} handleAgeChange = ()=>{} handleSubmit = ()=>{} render(){ return( <div> <button onClick={this.handleNameChange} >修改名称</button> <button onClick={this.handleAgeChange} >修改年龄</button> <button onClick={this.handleSubmit} >提交</button> {/ …more evenet /} </div> ) }}上面2钟方式,你在开发中经常使用哪一种?还是说看心情来,<_>2. 通过那种方式进行事件绑定和参数传递?主要有以下几种方式// 1. 使用 es6 arrow function class ArrowFun extends React.Component { click(){ / click… / } render(){ return( <div> <button onClick={()=>this.click()}>click</button> <button onClick={this.click.bind(this,{})}>click</button> </div> ) }}// 以上2种方法等效,看上去这种写法还是很不错的,但是因为函数无法像数据那样走diff算法,所以组件会做浪费的渲染// 2. 通过构造函数进行this绑定class ArrowFun extends React.Component { constructor(props){ super(props); this.click = this.click.bind(this); } click(){ / click… **/ } render(){ return( <div> <button onClick={this.click}>click</button> </div> ) }}// 相对第一种,是解决了多余render问题,但是好像没啥好的办法进行参数的传递,同时还多了一行代码。// 3. 使用class-propertiesclass ArrowFun extends React.Component { constructor(props){ super(props); } click =()=>{ } render(){ return( <div> <button onClick={this.click}>click</button> </div> ) }}// 相对前2种,貌似这种最好了;但是需要注意的是1. 类属性还不是es规范,2.同样面临参数传递的问题好吧,该用哪一种呢?任意用吗?~~~未完待续 ...

December 18, 2018 · 1 min · jiezi

JWT - just what?

初见JWT,不知所云,赶紧Google(百度)一下,原来是跨域身份验证解决方案。JWT只是缩写,全拼则是 JSON Web Tokens ,是目前流行的跨域认证解决方案,一种基于JSON的、用于在网络上声明某种主张的令牌(token)。JWT 原理jwt验证方式是将用户信息通过加密生成token,每次请求服务端只需要使用保存的密钥验证token的正确性,不用再保存任何session数据了,进而服务端变得无状态,容易实现拓展。加密前的用户信息,如:{ “username”: “vist”, “role”: “admin”, “expire”: “2018-12-08 20:20:20”}客户端收到的token:7cd357af816b907f2cc9acbe9c3b4625JWT 结构一个token分为3部分:头部(header)载荷(payload)签名(signature)3个部分用“.”分隔,如:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c 头部JWT的头部分是一个JSON对象,描述元数据,通常是:{ “typ”: “JWT”, “alg”: “HS256”}typ 为声明类型,指定 “JWT"alg 为加密的算法,默认是 “HS256"也可以是下列中的算法:JWS算法名称描述HS256HMAC256HMAC with SHA-256HS384HMAC384HMAC with SHA-384HS512HMAC512HMAC with SHA-512RS256RSA256RSASSA-PKCS1-v1_5 with SHA-256RS384RSA384RSASSA-PKCS1-v1_5 with SHA-384RS512RSA512RSASSA-PKCS1-v1_5 with SHA-512ES256ECDSA256ECDSA with curve P-256 and SHA-256ES384ECDSA384ECDSA with curve P-384 and SHA-384ES512ECDSA512ECDSA with curve P-521 and SHA-512载荷载荷(payload)是数据的载体,用来存放实际需要传递的数据信息,也是一个JSON对象。JWT官方推荐字段:iss: jwt签发者sub: jwt所面向的用户aud: 接收jwt的一方exp: jwt的过期时间,这个过期时间必须要大于签发时间nbf: 定义在什么时间之前,该jwt都是不可用的.iat: jwt的签发时间jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。也可以使用自定义字段,如:{ “username”: “vist”, “role”: “admin”}签名签名部分是对前两部分(头部,载荷)的签名,防止数据篡改。按下列步骤生成:1、先指定密钥(secret)2、把头部(header)和载荷(payload)信息分别base64转换3、使用头部(header)指定的算法加密最终,签名(signature) = HMACSHA256(base64UrlEncode(header) + “.” + base64UrlEncode(payload),secret)客户端得到的签名:header.payload.signature也可以对JWT进行再加密。JWT 使用1、服务端根据用户登录状态,将用户信息加密到token中,返给客户端2、客户端收到服务端返回的token,存储在cookie中3、客户端和服务端每次通信都带上token,可以放在http请求头信息中,如:Authorization字段里面4、服务端解密token,验证内容,完成相应逻辑JWT 特点JWT更加简洁,更适合在HTML和HTTP环境中传递JWT适合一次性验证,如:激活邮件JWT适合无状态认证JWT适合服务端CDN分发内容相对于数据库Session查询更加省时JWT默认不加密使用期间不可取消令牌或更改令牌的权限JWT建议使用HTTPS协议来传输代码原文:https://www.bestvist.com/p/62

December 10, 2018 · 1 min · jiezi

前站 - 前端导航,搜索社区,阅读文章,提升技术

推荐一个前端导航网站,记录了各种关于前端的网址,可以直接在对应社区查找问题,还可以查看github热门项目喜欢的话赶快收藏一波吧 ^^网址:https://www.frontendjs.com/

December 4, 2018 · 1 min · jiezi

动手做个聊天室,前端工程师百无聊赖的人生

本项目服务端基于node.js技术,使用了koa框架,所有数据存储在mongodb中。客户端使用react框架,使用redux和immutable.js管理状态,APP端基于react-native和expo开发。本文需要对JavaScript较为熟悉,讲解核心功能点的设计思路。服务端架构服务端负责两件事:1.提供基于WebSocket的接口2.提供index.html响应服务端使用了koa-socket这个包,它集成了socket.io并实现了socket中间件机制,服务端基于该中间件机制,自己实现了一套接口路由每个接口都是一个async函数,函数名即接口名,同时也是socket事件名async login(ctx) { return ’login success’}然后写了个route中间件,用来完成路由匹配,当判断路由匹配时,以ctx对象作为参数执行路由方法,并将方法返回值作为接口返回值function noop() {}/** * 路由处理 * @param {IO} io koa socket io实例 * @param {Object} routes 路由 /module.exports = function (io, _io, routes) { Object.keys(routes).forEach((route) => { io.on(route, noop); // 注册事件 }); return async (ctx) => { // 判断路由是否存在 if (routes[ctx.event]) { const { event, data, socket } = ctx; // 执行路由并获取返回数据 ctx.res = await routes[ctx.event]({ event, // 事件名 data, // 请求数据 socket, // 用户socket实例 io, // koa-socket实例 _io, // socket.io实例 }); } };};还有一个重要catchError中间件是,它负责捕获全局异常,业务流程中大量使用assert判断业务逻辑,不满足条件时会中断流程并返回错误消息,catchError将捕获业务逻辑异常,并取出错误消息返回给客户端const assert = require(‘assert’);/* * 全局异常捕获 /module.exports = function () { return async (ctx, next) => { try { await next(); } catch (err) { if (err instanceof assert.AssertionError) { ctx.res = err.message; return; } ctx.res = Server Error: ${err.message}; console.error(‘Unhandled Error\n’, err); } };};这些就是服务端的核心逻辑,基于该架构下定义接口组成业务逻辑另外,服务端还负责提供index.html响应,即客户端首页。客户端的其他资源是放在CDN上的,这样可以缓解服务端带宽压力,但是index.html不能使用强缓存,因为会使得客户端更新不可控,因此index.html放在服务端客户端架构客户端使用socket.io-client连接服务端,连接成功后请求接口尝试登录,如果localStorage没有令牌或者接口返回令牌过期,将会以游客身份登录,登录成功会返回用户信息以及群组,好友列表,接着去请求各群组,好友的历史消息客户端需要监听connect / disconnect / message三个消息1.connect:socket连接成功2.disconnect socket连接断开3.message 接收到新消息客户端使用redux管理数据,需要被组件共享的数据放在redux中,只有自身使用的数据还是放在组件的state中,客户端存储的redux数据结构如下:用户用户信息_id用户id用户名用户名linkmans联系人列表,包括群组,好友以及临时会话isAdmin是否是管理员焦点当前聚焦的联系人id,既对话中的目标连接连接状态ui客户端UI相关和功能开关客户端的数据流,主要有两条线路1.用户操作=>请求接口=>返回数据=>更新redux =>视图重新渲染2.监听新消息=>处理数据=>更新redux =>视图重新渲染用户系统用户架构定义:const UserSchema = new Schema({ createTime: { type: Date, default: Date.now }, lastLoginTime: { type: Date, default: Date.now }, username: { type: String, trim: true, unique: true, match: /^([0-9a-zA-Z]{1,2}|[\u4e00-\u9eff]){1,8}$/, index: true, }, salt: String, password: String, avatar: { type: String, },});createTime:创建时间lastLoginTime:最后一次登录时间,用来清理僵尸号用username:用户昵称,同时也是账号salt:加密盐password:用户密码avatar:用户头像URL地址用户注册注册接口需要username/ password两个参数,首先做判空处理const { username, password} = ctx.data;assert(username, ‘用户名不能为空’);assert(password, ‘密码不能为空’);然后判断用户名是否已存在,同时获取默认群组,新注册用户要加入到默认群组const user = await User.findOne({ username });assert(!user, ‘该用户名已存在’);const defaultGroup = await Group.findOne({ isDefault: true });assert(defaultGroup, ‘默认群组不存在’);存密码明文肯定是不行的,生成随机盐,并使用盐加密密码const salt = await bcrypt.genSalt$(saltRounds);const hash = await bcrypt.hash$(password, salt);给用户一个随机默认头像,保存用户信息到数据库let newUser = null;try { newUser = await User.create({ username, salt, password: hash, avatar: getRandomAvatar(), });} catch (err) { if (err.name === ‘ValidationError’) { return ‘用户名包含不支持的字符或者长度超过限制’; } throw err;}将用户添加到默认群组,然后生成用户令牌令是用来免费码登录的凭证,存储在客户端localStorage,令牌里带带用户id,过期时间,客户端信息三个数据,用户id和过期时间容易理解,客户端信息是为了防止令牌盗用,之前也试过验证客户端ip一致性,但是ip可能会有经常改变的情况,搞得用户每次自动登录都被判定为盗用了……defaultGroup.members.push(newUser);await defaultGroup.save();const token = generateToken(newUser._id, environment);将用户id与当前 socket 连接关联, 服务端是以 ctx.socket.user 是否为 undefined 来判断登录态的 更新 Socket 表中当前 socket 连接信息, 后面获取在线用户会取 Socket 表数据ctx.socket.user = newUser._id;await Socket.update({ id: ctx.socket.id }, { user: newUser._id, os, // 客户端系统 browser, // 客户端浏览器 environment, // 客户端环境信息});最后将数据返回客户端return { _id: newUser._id, avatar: newUser.avatar, username: newUser.username, groups: [{ _id: defaultGroup._id, name: defaultGroup.name, avatar: defaultGroup.avatar, creator: defaultGroup.creator, createTime: defaultGroup.createTime, messages: [], }], friends: [], token,}用户登录fiora是不限制多登陆的,每个用户都可以在无限个终端登录登录有三种情况:游客登录令牌登录用户名/密码登录游客登录仅能查看默认群组消息,并且不能发消息,主要是为了降低第一次来的用户的体验成本令牌登录是最常用的,客户端首先从localStorage取令牌,令牌存在就会使用令牌登录首先对令牌解码取出负载数据,判断令牌是否过期以及客户端信息是否匹配let payload = null;try { payload = jwt.decode(token, config.jwtSecret);} catch (err) { return ‘非法token’;}assert(Date.now() < payload.expires, ’token已过期’);assert.equal(environment, payload.environment, ‘非法登录’);从数据库查找用户信息,更新最后登录时间,查找用户所在的群组,并将socket添加到该群组,然后查找用户的好友const user = await User.findOne({ _id: payload.user }, { _id: 1, avatar: 1, username: 1 });assert(user, ‘用户不存在’);user.lastLoginTime = Date.now();await user.save();const groups = await Group.find({ members: user }, { _id: 1, name: 1, avatar: 1, creator: 1, createTime: 1 });groups.forEach((group) => { ctx.socket.socket.join(group._id); return group;});const friends = await Friend .find({ from: user._id }) .populate(’to’, { avatar: 1, username: 1 });更新socket信息,与注册相同ctx.socket.user = user._id;await Socket.update({ id: ctx.socket.id }, { user: user._id, os, browser, environment,});最后返回数据用户名/密码与令牌登录仅一开始的逻辑不同,没有解码令牌验证数据这步先验证用户名是否存在,然后验证密码是否匹配const user = await User.findOne({ username });assert(user, ‘该用户不存在’);const isPasswordCorrect = bcrypt.compareSync(password, user.password);assert(isPasswordCorrect, ‘密码错误’);接下来逻辑就与令牌登录一致了消息系统发送消息sendMessage接口有三个参数:to:发送的对象,群组或者用户type:消息类型content:消息内容因为群聊和私聊共用这一个接口,所以首先需要判断是群聊还是私聊,获取群组id或者用户户ID,群聊/私聊通过参数区分群聊时到是相应的群组id,然后获取群组信息私聊时到是发送者和接收者二人id拼接的结果,去掉发送者id就得到了接收者id,然后获取接收者信息let groupId = ‘’;let userId = ‘’;if (isValid(to)) { const group = await Group.findOne({ _id: to }); assert(group, ‘群组不存在’);} else { userId = to.replace(ctx.socket.user, ‘’); assert(isValid(userId), ‘无效的用户ID’); const user = await User.findOne({ _id: userId }); assert(user, ‘用户不存在’);}部分消息类型需要做些处理,文本消息判断长度并做xss处理,邀请消息判断邀请的群组是否存在,然后将邀请人,群组id,群组名等信息存储到消息体中let messageContent = content;if (type === ’text’) { assert(messageContent.length <= 2048, ‘消息长度过长’); messageContent = xss(content);} else if (type === ‘invite’) { const group = await Group.findOne({ name: content }); assert(group, ‘目标群组不存在’); const user = await User.findOne({ _id: ctx.socket.user }); messageContent = JSON.stringify({ inviter: user.username, groupId: group._id, groupName: group.name, });}将新消息存入数据库let message;try { message = await Message.create({ from: ctx.socket.user, to, type, content: messageContent, });} catch (err) { throw err;}接下来构造一个不包含敏感信息的消息数据, 数据中包含发送者的id、用户名、头像, 其中用户名和头像是比较冗余的数据, 以后考虑会优化成只传一个id, 客户端维护用户信息, 通过id匹配出用户名和头像, 能节约很多流量 如果是群聊消息, 直接把消息推送到对应群组即可 私聊消息更复杂一些, 因为 fiora 是允许多登录的, 首先需要推送给接收者的所有在线 socket, 然后还要推送给自身的其余在线 socketconst user = await User.findOne({ _id: ctx.socket.user }, { username: 1, avatar: 1 });const messageData = { _id: message._id, createTime: message.createTime, from: user.toObject(), to, type, content: messageContent,};if (groupId) { ctx.socket.socket.to(groupId).emit(‘message’, messageData);} else { const sockets = await Socket.find({ user: userId }); sockets.forEach((socket) => { ctx._io.to(socket.id).emit(‘message’, messageData); }); const selfSockets = await Socket.find({ user: ctx.socket.user }); selfSockets.forEach((socket) => { if (socket.id !== ctx.socket.id) { ctx._io.to(socket.id).emit(‘message’, messageData); } });}最后把消息数据返回给客户端,表示消息发送成功。客户端为了优化用户体验,发送消息时会立即在页面上显示新信息,同时请求接口发送消息。如果消息发送失败,就删掉该条消息获取历史消息getLinkmanHistoryMessages接口有两个参数:linkmanId:联系人id,群组或者俩用户id拼接existCount:已有的消息个数详细逻辑比较简单,按创建时间倒序查找已有个数+每次获取个数数量的消息,然后去掉已有个数的消息再反转一下,就是按时间排序的新消息const messages = await Message .find( { to: linkmanId }, { type: 1, content: 1, from: 1, createTime: 1 }, { sort: { createTime: -1 }, limit: EachFetchMessagesCount + existCount }, ) .populate(‘from’, { username: 1, avatar: 1 });const result = messages.slice(existCount).reverse();返回给客户端接收推送消息客户端订阅消息事件接收新消息 socket.on(‘message’)接收到新消息时,先判断状态中是否存在该联系人,如果存在则将消息存到对应的联系人下,如果不存在则是一条临时会话的消息,构造一个临时联系人并获取历史消息,然后将临时联系人添加到州中。如果是来自自己其它终端的消息,则不需要创建联系人const state = store.getState();const isSelfMessage = message.from._id === state.getIn([‘user’, ‘_id’]);const linkman = state.getIn([‘user’, ’linkmans’]).find(l => l.get(’_id’) === message.to);let title = ‘’;if (linkman) { action.addLinkmanMessage(message.to, message); if (linkman.get(’type’) === ‘group’) { title = ${message.from.username} 在 ${linkman.get('name')} 对大家说:; } else { title = ${message.from.username} 对你说:; }} else { // 联系人不存在并且是自己发的消息, 不创建新联系人 if (isSelfMessage) { return; } const newLinkman = { _id: getFriendId( state.getIn([‘user’, ‘_id’]), message.from._id, ), type: ’temporary’, createTime: Date.now(), avatar: message.from.avatar, name: message.from.username, messages: [], unread: 1, }; action.addLinkman(newLinkman); title = ${message.from.username} 对你说:; fetch(‘getLinkmanHistoryMessages’, { linkmanId: newLinkman.id }).then(([err, res]) => { if (!err) { action.addLinkmanMessages(newLinkman.id, res); } });}如果当前聊天页是在后台的,并且打开了消息通知开关,则会弹出桌面提醒if (windowStatus === ‘blur’ && state.getIn([‘ui’, ’notificationSwitch’])) { notification( title, message.from.avatar, message.type === ’text’ ? message.content : [${message.type}], Math.random(), );}如果打开了声音开关,则响一声新消息提示音if (state.getIn([‘ui’, ‘soundSwitch’])) { const soundType = state.getIn([‘ui’, ‘sound’]); sound(soundType);}如果打开了语言播报开关并且是文本消息,将消息内的url和#过滤掉,排除长度大于200的消息,然后推送到消息朗读队列中if (message.type === ’text’ && state.getIn([‘ui’, ‘voiceSwitch’])) { const text = message.content .replace(/https?://(www.)?[-a-zA-Z0-9@:%.+~#=]{2,256}.[a-z]{2,6}\b([-a-zA-Z0-9@:%+.~#?&//=])/g, ‘’) .replace(/#/g, ‘’); // The maximum number of words is 200 if (text.length > 200) { return; } const from = linkman && linkman.get(’type’) === ‘group’ ? ${message.from.username}在${linkman.get('name')}说 : ${message.from.username}对你说; if (text) { voice.push(from !== prevFrom ? from + text : text, message.from.username); } prevFrom = from;}更多中间件限制未登录请求大多数接口是只允许已登录用户访问的,如果接口需要登录且socket连接没有用户信息,则返回“未登录”错误/** * 拦截未登录请求 /module.exports = function () { const noUseLoginEvent = { register: true, login: true, loginByToken: true, guest: true, getDefalutGroupHistoryMessages: true, getDefaultGroupOnlineMembers: true, }; return async (ctx, next) => { if (!noUseLoginEvent[ctx.event] && !ctx.socket.user) { ctx.res = ‘请登录后再试’; return; } await next(); };};限制调用频率为了防止刷接口的情况,减轻服务器压力,限制同一插座连接每分钟内最多请求30次接口const MaxCallPerMinutes = 30;/* * Limiting the frequency of interface calls */module.exports = function () { let callTimes = {}; setInterval(() => callTimes = {}, 60000); // Emptying every 60 seconds return async (ctx, next) => { const socketId = ctx.socket.id; const count = callTimes[socketId] || 0; if (count >= MaxCallPerMinutes) { return ctx.res = ‘接口调用频繁’; } callTimes[socketId] = count + 1; await next(); };};小黑屋管理员账号可以将用户添加到小黑屋,被添加到小黑屋的用户无法请求任何接口,10分钟后自动解禁/ / * Refusing to seal user requests */module.exports = function () { return async (ctx, next) => { const sealList = global.mdb.get(‘sealList’); if (ctx.socket.user && sealList.has(ctx.socket.user.toString())) { return ctx.res = ‘你已经被关进小黑屋中, 请反思后再试’; } await next(); };};其它表情表情是一张雪碧图,点击表情会向输入框插入格式为#(xx)的文本,例如#(滑稽)。在渲染消息时,通过正则匹配将这些文本替换为<img>,并计算出该表情在雪碧图中的位置,然后渲染到页面上不设置src会显示一个边框,需要将src设置为一张透明图function convertExpression(txt) { return txt.replace( /#(([\u4e00-\u9fa5a-z]+))/g, (r, e) => { const index = expressions.default.indexOf(e); if (index !== -1) { return class="expression-baidu" src="${transparentImage}" style="background-position: left ${-30 * index}px;" onerror="this.style.display='none'" alt="${r}"&gt;; } return r; }, );}表情包搜索的爬https://www.doutula.com上的搜…const res = await axios.get(https://www.doutula.com/search?keyword=${encodeURIComponent(keywords)});assert(res.status === 200, ‘搜索表情包失败, 请重试’);const images = res.data.match(/data-original="[^ “]+"/g) || [];return images.map(i => i.substring(15, i.length - 1));桌面消息通知效果如上图,不同系统/浏览器在样式上会有区别经常有人问到这个是怎么实现的,其实是HTML5增加的功能Notification粘贴发图监听paste事件,获取粘贴内容,如果包含Files类型内容,则读取内容并生成Image对象。注意:通过该方式拿到的图片,会比原图片体积大很多,因此最好压缩一下再使用@autobindhandlePaste(e) { const { items, types } = (e.clipboardData || e.originalEvent.clipboardData); // 如果包含文件内容 if (types.indexOf(‘Files’) > -1) { for (let index = 0; index < items.length; index++) { const item = items[index]; if (item.kind === ‘file’) { const file = item.getAsFile(); if (file) { const that = this; const reader = new FileReader(); reader.onloadend = function () { const image = new Image(); image.onload = () => { // 获取到 image 图片对象 }; image.src = this.result; }; reader.readAsDataURL(file); } } } e.preventDefault(); }}后话想把前端学好,js真的真的很重要!!!我的web前端学习q.u.n【731771211】,学习资源免费分享,五年资深前端攻城狮在线课堂讲解实战技术。欢迎新手,进阶点击:加入 ...

November 23, 2018 · 6 min · jiezi

css实现动态阴影、蚀刻文本、渐变文本

css实现动态阴影创建与类似的阴影box-shadow 而是基于元素本身的颜色。代码实现:<div class=“dynamic-shadow-parent”> <div class=“dynamic-shadow”></div></div><style>.dynamic-shadow-parent { position: relative; z-index: 1;}.dynamic-shadow { position: relative; width: 10rem; height: 10rem; background: linear-gradient(75deg, #6d78ff, #00ffb8);}.dynamic-shadow::after { content: ‘’; width: 100%; height: 100%; position: absolute; background: inherit; top: 0.5rem; filter: blur(0.4rem); opacity: 0.7; z-index: -1;}</styel>效果如下:说明代码片段需要一些复杂的情况来正确堆叠上下文,这样伪元素将定位在元素本身的下面,同时仍然可见。position: relative 在父元素上为子元素建立笛卡尔定位上下文。z-index: 1 建立新的堆叠内容。position: relative 在子级上建立伪元素的定位上下文。::after 定义伪元素。position: absolute 从文档流中取出伪元素,并将其相对于父元素定位。width: 100% 和height: 100%调整伪元素的大小以填充其父元素的尺寸,使其大小相等。background: inherit 使伪元素继承在元素上指定的线性渐变。top: 0.5rem 将伪元素从其父元素稍微向下偏移。filter: blur(0.4rem) 将模糊伪元素以在下面创建阴影的外观。opacity: 0.7 使伪元素部分透明。z-index: -1 将伪元素定位在父元素后面。浏览器支持91.7 %,需要前缀才能获得完全支持蚀刻文本创建文本显示为“蚀刻”或刻在背景中的效果。代码实现:<p class=“etched-text”>I appear etched into the background.</p></styel>.etched-text { text-shadow: 0 2px white; font-size: 1.5rem; font-weight: bold; color: #b8bec5;}</styel>效果如下:说明text-shadow: 0 2px white 创建白色阴影偏移0px 水平和2px 垂直于原点位置。背景必须比阴影暗,效果才能发挥作用。文字颜色应该稍微褪色,使其看起来像是刻在背景上的。浏览器支持98.1 %,没有警告。渐变文本为文本提供渐变颜色。代码实现:<p class=“gradient-text”>Gradient text</p></styel>.gradient-text { background: -webkit-linear-gradient(pink, red); -webkit-text-fill-color: transparent; -webkit-background-clip: text;}</styel>效果如下:说明background: -webkit-linear-gradient(…) 为文本元素提供渐变背景。webkit-text-fill-color: transparent 使用透明颜色填充文本。webkit-background-clip: text 用文本剪辑背景,用渐变背景作为颜色填充文本。浏览器支持91.5 %,使用非标准属性。web前端开发新手进阶q.u.n:731.771.211 ...

November 22, 2018 · 1 min · jiezi

程序员35岁后,不拼体力了还不能拼什么?

IT真是一个吃青春饭的行业吗?IT真有年龄槛吗?35岁的IT工作者真的不能再做技术了吗?在IT行业,新技术、新概念和新思维如洪水般不断涌来,让人目不暇接。为了不至于落伍,IT人的全部时间基本都花在了学习和应用技术上。每天高效率的工作,人就像上了一辆高速列车,身不由己,想停也无法马上歇下来。业内专家认为,一方面,从人的生理条件看,一个30多岁的程序员和一个20出头的年轻程序员在一起编程,从工作心情上、反应速度上、作业效率上都存在一定差距。公司为了降低开发成本,会倾向于对经验少的年轻开发者支付较低的报酬。另一方面,从社会角度看,大多数IT人在35岁已经成家立业,肩负担子远非以前那种一人吃饱全家吃饱的状态可以相比,这多少也牵制了大家对于工作的精力投入。当然并不是每个人都能做管理或是适合做管理,调查显示还有32%的IT人选择了在35岁时进行创业。给老板打工不如给自己打工,这是很多人最朴素的想法。35岁前拼的是体力,进行各方面创业条件的准备,比如资金的积累、人脉关系的培养、财务知识和公司经营管理理念的学习等,到了35岁,就能万事具备,大干一番,开创人生的另外一个高峰。业内专家认为,35岁的人虽然精力比不上年轻人,但是他凭借沉淀的经验和知识,正是开始职业生涯新的起点的好时光。有12%的IT人并不认为年龄是一个槛,调查显示他们选择了一条不同于常人思维的路。他们认为,做技术牛人不是神话!IT老人杨先生告诉笔者,一个35岁的IT人所积累的经验肯定要比20岁的人多。在实际开发中,经验要比技术重要,行业经验并不能简单地靠年轻人的拼搏来获得的,时间是这场“算法”中的“种子”。一个IT人大学毕业后往往会有一段时期的不确定期(会有2-3年的跳槽频繁期,以期找到合适自己的专业领域),当最后确认自己的未来发展时已经二十七八了,此时开始在专业领域内修炼,一般到35岁左右才可能在自己的领域内有所成就。其实有很多技术专家都是30好几还在写程序,当然这样的专家并不只是会熟练使用几门语言而已。微软的底层的员工都是四五十岁的样子,但他们依然能够站在整个行业的前端。为什么他们没有年龄的限制呢?学习技术的最高境界是什么?掌握各门技术的相通性,这样才能一步一步跟着技术进步(当然这是很难的),水平越高,看的也就越远,思维也就愈开阔,这时年龄不会是个问题。首先,在职业目标的确定中,30岁以下的IT人虽然首选是管理(36.89%),但与位列第二的创业(34.09%)差距非常相近,显然想把自己创业作为目标是这个年龄段人的一大特点。随着年龄的增长,到了30-35岁这个年龄段,对于想自己创业的热情明显减弱,这时却有16.34%的IT人愿意做一辈子的技术牛人,相对比率要高于30岁以下年龄段的人。对于35岁以上的IT老兵来讲,或许时间磨平了昔日的梦想,管理成为他们最为务实的方向,还想创业的比率仅占23.47%。另外有一个数字值得注意,有8.16%的人想教书育人,比率甚至于高过了去做市场和销售。其次,对于“如果35岁面临失业”这个假设性的问题,大部分30岁以下的人选择自己创业(53.11%),只有21.12%的人认为自己会做回老本行。而同样的问题在30岁以上的人看来,情况又有所不同。选择自己创业的比重随着年龄的增长而减少,做回老本行的想法却随着的岁月的流逝愈来愈强烈。不同年龄段对于“35岁找工作的关键因素”也有不同的理解。30岁以下的人对薪资福利的关注度是最高的,其次才是发展前途和工作类型。30岁以上的人则把发展前途看成最主要的因素,薪资福利退后到第二位。此外,30岁左右的人更关注工作本身的类型,而到了35岁以上,企业类型变得更为重要些。业内专家指出,35岁以上的IT高级人才在选择工作时,更看重企业文化对个人的适合性,因为他们认为只有在适合自己的企业中才能如鱼得水地开展工作。行业变数太多,选择走一步算一步的朋友是可以理解的。我们都有不知所措迷茫的时候,不知道做什么,也不知道怎么做,更不会去争取做得最好。于是人云亦云,别人抱怨IT难做,你也就抱怨,别人讲现在IT没发展前途,然后就想要转行。于是在人生的舞台上你永远只能是观众,观看别人的成功。其实IT人35岁的调查包含的不仅仅是35岁以后做什么?而是提醒IT人重新审视自己的职业发展之路。做IT是很辛苦,但反过来想,难道做其他工作就不辛苦了?压力就不大了?这个世界没有不辛苦的工作。不要像坐在井底的青蛙,抱怨世界就这么小。这口井,束缚了我们思想观念,而观念决定我们的发展格局。一个观念的转变,可以改变我们的一生。对于IT人来说,技术是安身立命之本,但往更高层次发展的决定因素,往往不是技术本身,而是你本身有没有目标和你达成这个目标的能力。我的学习,交流q.u.n:731771211,里面都是学习前端的,全栈发展!互联网时代趋势-全栈工程师

November 21, 2018 · 1 min · jiezi

node.js中常用的fs文件系统

fs文件系统模块对于系统文件及目录进行一些读写操作。模块中的方法均有异步和同步版本,例如读取文件内容的函数有异步的 fs.readFile() 和同步的 fs.readFileSync()。异步的方法函数最后一个参数为回调函数,回调函数的第一个参数包含了错误信息(error)。建议大家使用异步方法,比起同步,异步方法性能更高,速度更快,而且没有阻塞。创建文件 fs.writeFile(filename ,data,[options],function(err){})如果文件存在,写入的内容会覆盖旧文件内容filename (String) 文件名称data (String | Buffer) 将要写入的内容,可以使字符串 或 buffer数据。options (Object) option数组对象,包含callback {Function} 回调,传递一个异常参数err。追加文件 fs.appendFile(path,data,[options],function(err){})name : 文件名str : 添加的字段encode : 设置编码callback : 回调函数 ,传递一个异常参数err读取文件 fs.readFile(path,options,function(err,data){}) filename 具体的文件保存路径地址 [options] 具体选项配置,包括数据的编码方式,callback为具体的回调函数,进行相应的错误捕捉及提示。文件是否存在fs.exists(path, function(exists){})path 欲检测的文件路径callback 回调注意此回调的参数和其他 Node.js 回调的参数不一致。不推荐在调用 fs.open,fs.readFile(),fs.writeFile() 之前使用 fs.exists()检测文件是否存在。这样做会引起竞争条件,因为在两次调用之间,其他进程可能修改文件。作为替代,用户应该直接开/读取/写入文件,当文件不存在时再处理错误。删除文件 fs.unlink(path,function(err){})path - 文件路径callback - 回调函数,err创建文件夹fs.mkdir(name,function(err){})path - 文件路径。callback - 回调函数,err,异步地创建目录。 完成回调只有一个可能的异常参数。删除文件夹fs.rmdir(path,function(err){})path - 文件路径。callback - 回调函数,没有参数。读取文件夹fs.readdir(path,function(err,files){})path - 文件路径。callback - 回调函数,回调函数带有两个参数err, files,err 为错误信息,files 为 目录下的文件数组列表改名字fs.rename(oldname,newname,function(err){})修改文件名称,可更改文件的存放路径。

November 21, 2018 · 1 min · jiezi

程序员练级攻略(2018):前端性能优化和框架

这个是我订阅 陈皓老师在极客上的专栏《左耳听风》,我整理出来是为了自己方便学习,同时也分享给你们一起学习,当然如果有兴趣,可以去订阅,为了避免广告嫌疑,我这就不多说了!以下第一人称是指陈皓老师。前端性能优化首先是推荐几本前端性能优化方面的图书。Web Performance in Action ,这本书目前国内没有卖的。你可以看电子版本,我觉得是一本很不错的书,其中有 CSS、图片、字体、JavaScript 性能调优等。Designing for Performance ,这本在线的电子书很不错,其中讲了很多网页优化的技术和相关的工具,可以让你对整体网页性能优化有所了解。High Performance JavaScript ,这本书在国内可以买到,能让你了解如何提升各方面的性能,包括代码的加载、运行、DOM 交互、页面生存周期等。雅虎的前端工程师尼古拉斯·扎卡斯(Nicholas C. Zakas)和其他五位 JavaScript 专家介绍了页面代码加载的最佳方法和编程技巧,来帮助你编写更为高效和快速的代码。你还会了解到构建和部署文件到生产环境的最佳实践,以及有助于定位线上问题的工具。High Performance Web Sites: Essential Knowledge for Front-End Engineers ,这本书国内也有卖,翻译版为《高性能网站建设指南:前端工程师技能精髓》。作者给出了 14 条具体的优化原则,每一条原则都配以范例佐证,并提供了在线支持。全书内容丰富,主要包括减少 HTTP 请求、Edge Computing 技术、Expires Header 技术、gzip 组件、CSS 和 JavaScript 最佳实践、主页内联、Domain 最小化、JavaScript 优化、避免重定向的技巧、删除重复 JavaScript 的技巧、关闭 ETags 的技巧、Ajax 缓存技术和最小化技术等。除了上面这几本书之外,Google 的 Web Fundamentals 里的 Performance 这一章节也有很多非常不错的知识和经验。接下来是一些最佳实践性的文档。Browser Diet ,前端权威性能指南(中文版)。这是一群为大型站点工作的专家们建立的一份前端性能的工作指南。PageSpeed Insights Rules ,谷歌给的一份性能指南和最佳实践。Best Practices for Speeding Up Your Web Site ,雅虎公司给的一份 7 个分类共 35 个最佳实践的文档。接下来,重点推荐一个性能优化的案例学习网站 WPO Stats 。WPO 是 Web Performance Optimization 的缩写,这个网站上有很多很不错的性能优化的案例分享,一定可以帮助你很多。然后是一些文章和案例。A Simple Performance Comparison of HTTPS, SPDY and HTTP/2 ,这是一篇比较浏览器的 HTTPS、SPDY 和 HTTP/2 性能的文章,除了比较之外,还可以让你了解一些技术细节。7 Tips for Faster HTTP/2 Performance ,对于 HTTP/2 来说,Nginx 公司给出的 7 个增加其性能的小提示。Reducing Slack’s memory footprint ,Slack 团队减少内存使用量的实践。Pinterest: Driving user growth with performance improvements ,Pinterest 关于性能调优的一些分享,其中包括了前后端的一些性能调优实践。其实也是一些比较通用的玩法,这篇文章主要是想让前端的同学了解一下如何做整体的性能调优。10 JavaScript Performance Boosting Tips ,10 个提高 JavaScript 运行效率的小提示,挺有用的。Getting started with the Picture Element ,这篇文章讲述了 Responsive 布局所带来的一些负面的问题。主要是图像适配的问题,其中引出了一篇文章"Native Responsive Images" ,值得一读。Improve Page Load Times With DNS Prefetching ,这篇文章教了你一个如何降低 DNS 解析时间的小技术——DNS prefetching。Jank Busting for Better Rendering Performance ,这是一篇 Google I/O 上的分享,关于前端动画渲染性能提升。JavaScript Memory Profiling ,这是一篇谷歌官方教你如何使用 Chrome 的开发工具来分析 JavaScript 内存问题的文章。接下来是一些性能工具。在线性能测试分析工具太多,这里只推荐比较权威的。PageSpeed ,谷歌有一组 PageSpeed 工具来帮助你分析和优化网站的性能。Google 出品的,质量相当有保证。YSlow ,雅虎的一个网页分析工具。GTmetrix ,是一个将 PageSpeed 和 YSlow 合并起来的一个网页分析工具,并且加上一些 Page load 或是其它的一些分析。也是一个很不错的分析工具。Awesome WPO ,在 GitHub 上的这个 Awesome 中,你可以找到更多的性能优化工具和资源。另外,中国的网络有各种问题(你懂的),所以,你不能使用 Google 共享的 JavaScript 链接来提速,你得用中国自己的。你可以到这里看看中国的共享库资源,Forget Google and Use These Hosted JavaScript Libraries in China 。前端框架接下来,要学习的是 Web 前端的几大框架。目前而言,前端社区有三大框架 Angular.js、React.js 和 Vue.js。我认为,React 和 Vue 更为强劲一些,所以,我这里只写和 React 和 Vue 相关的攻略。关于两者的比较,网上有好多文章。我这里推荐几篇我觉得还不错的,供你参考。Angular vs. React vs. Vue: A 2017 comparisonReact or Vue: Which JavaScript UI Library Should You Be Using?ReactJS vs Angular5 vs Vue.js - What to choose in 2018?其实,比较这些框架的优缺点还有利弊并不是要比出个输赢,而是让你了解一下不同框架的优缺点。我觉得,这些框架都是可以学习的。而在我们生活工作中具体要用哪个框架,最好还是要有一些出发点,比如,你是为了找份好的工作,为了快速地搭一个网站,为了改造一个大规模的前端系统,还是纯粹地为了学习……不同的目的会导致不同的决定。我并不希望上述的这些比较会让你进入"二选一"或是"三选一"的境地。我只是想通过这些文章让你知道这些框架的设计思路和实现原理,这些才是让你受益一辈子的事。React.js 框架下面先来学习一下 React.js 框架。入门React 学起来并不复杂,就看 React 官方教程 和其文档就好了( React 的中文教程 )。然后,下面的文章会带你了解一下 React.js 的基本原理。All the fundamental React.js concepts ,这篇文章讲了所有的 React.js 的基本原理。Learn React Fundamentals and Advanced Patterns ,这篇文章中有几个短视频,每个视频不超过 5 分钟,是学习 React 的一个很不错的地方。Thinking in React,这篇文章将引导你完成使用 React 构建可搜索产品数据表的思考过程。提高学习一个技术最重要的是要学到其中的思想和方法。下面是一些我觉得学习 React 中最重要的东西。状态,对于富客户端来说是非常麻烦也是坑最多的地方,这里有几篇文章你可以一读。Common React.js mistakes: Unneeded state ,React.js 编程的常见错误——不必要的状态。State is an Anti-Pattern ,关于如何做一个不错的组件的思考,很有帮助。Why Local Component State is a Trap ,一些关于 “Single state tree” 的想法。Thinking Statefully ,几个很不错的例子让你对声明式的有状态的技术有更好的理解。传统上,解决 React 的状态问题一般用 Redux。在这里推荐 Tips to learn React + Redux in 2018 。Redux 是一个状态粘合组件,一般来说,我们会用 Redux 来做一些数据状态和其上层 Component 上的同步。这篇教程很不错。最后是 “State Architecture Patterns in React " 系列文章,非常值得一读。Part 1: A ReviewPart 2: The Top-Heavy Architecture, Flux and PerformancePart 3: Articulation Points, zine and An Overall StrategyPart 4: Purity, Flux-duality and Dataflow 函数式编程。从 jQuery 过来的同学一定非常不习惯 React,而从 Java 等后端过来的程序员就会很习惯了。所以,我觉得 React 就是后端人员开发的,或者说是做函数式编程的人开发的。对此,你需要学习一下 JavaScript 函数式编程的东西。这里推荐一本免费的电子书 《Professor Frisby’s Mostly Adequate Guide to Functional Programming》,其中译版为《JS 函数式编程指南中文版》。下面有几篇文章非常不错。前两篇和函数式编程有关的文章非常值得一读。后三篇是一些比较实用的函数式编程和 React 结合的文章。Master the JavaScript Interview: What is Functional Programming?The Rise and Fall and Rise of Functional Programming (Composing Software)Functional UI and Components as Higher Order FunctionsFunctional JavaScript: Reverse-Engineering the HypeSome Thoughts on Function Components in React设计相关。接下来是学习一些 React 的设计模式。React Pattern 是一个不错的学习 React 模式的地方。除此之外,还有如下的一些不错的文章也会对你很有帮助的。React Higher Order Components in depthPresentational and Container ComponentsControlled and uncontrolled form inputs in React don’t have to be complicatedFunction as Child ComponentsWriting Scalable React Apps with the Component Folder PatternReusable Web Application StrategiesCharacteristics of an Ideal React Architecture实践和经验还有一些不错的实践和经验。9 things every React.js beginner should knowBest practices for building large React applicationsClean Code vs. Dirty Code: React Best PracticesHow to become a more productive React Developer8 Key React Component Decisions资源列表最后就是 React 的资源列表。Awesome React ,这是一些 React 相关资源的列表,很大很全。React/Redux Links ,这也是 React 相关的资源列表,与上面不一样的是,这个列表主要收集了大量的文章,其中讲述了很多 React 知识和技术,比上面的列表好很多。React Rocks ,这个网站主要收集各种 React 的组件示例,可以让你大开眼界。Vue.js 框架Vue 可能是一个更符合前端工程师习惯的框架。不像 React.js 那样使用函数式编程方式,是后端程序员的思路。通过文章 “Why 43% of Front-End Developers want to learn Vue.js” ,你可以看出其编程方式和 React 是大相径庭的,符合传统的前端开发的思维方式。通过文章 Replacing jQuery With Vue.js: No Build Step Necessary ,我们可以看到,从 jQuery 是可以平滑过渡到 Vue 的。另外,我们可以通过 “10 things I love about Vue” ,了解 Vue 的一些比较优秀的特性。最令人高兴的是,Vue 的作者是我的好朋友尤雨溪(Evan You),最近一次对他的采访 “Vue on 2018 - Interview with Evan You” 当中有很多故事以及对 Vue 的展望。(注意:Vue 是完全由其支持者和用户资助的,这意味着它更接近社区而不受大公司的控制。)要学习 Vue 并不难,我认为上官网看文档( Vue 官方文档( 中文版)),照着搞一搞就可以很快上手了。Vue.js screencasts 是一个很不错的英文视频教程。另外,推荐 新手向:Vue 2.0 的建议学习顺序 ,这是 Vue 作者写的,所以有特殊意义。Vue 的确比较简单,有 Web 开发经验的人上手也比较快,所以这里也不会像 React 那样给出很多的资料。下面是一些我觉得还不错的内容,推荐给你。How not to Vue ,任何技术都有坑,了解 Vue 的短板,你就能扬长避短,就能用得更好。Vue.js Component Communication Patterns4 AJAX Patterns For Vue.js AppsHow To (Safely) Use A jQuery Plugin With Vue.js7 Ways To Define A Component Template in Vue.jsUse Any Javascript Library With Vue.jsDynamic and async components made easy with Vue.js当然,最后一定还有 Awesome Vue ,Vue.js 里最为巨大最为优秀的资源列表。小结总结一下今天的内容。我先介绍的是前端性能优化方面的内容,推荐了图书、最佳实践性的文档、案例,以及一些在线性能测试分析工具。随后重点讲述了 React 和 Vue 两大前端框架,给出了大量的文章、教程和相关资源列表。我认为,React.js 使用函数式编程方式,更加符合后端程序员的思路,而 Vue 是更符合前端工程师习惯的框架。因此,两者比较起来,Vue 会更容易上手一些。以上是陈皓老师分享的,结合上一篇其实内容是很多的,这个不是一时就能看完的,如果你不想当一辈子的码农,不只只是搬砖的,那我们目标是更具有创造的工程师,架构师,这些内容是值得我们花10年、20年,甚至一身要去学习的,希望大家有好东西也分享出来一起学习哈!ps: 如果你想成为一名高级的程序员(工程师),英文能力是不可缺少的,平时也需要加强英文的学习!你的点赞是我持续分享好东西的动力,欢迎点赞!一个笨笨的码农,我的世界只能终身学习! ...

November 21, 2018 · 3 min · jiezi

Web 端的测试 Selenium 用法必备

大家都知道,基于Web端的测试的基础框架是需要Selenium做主要支撑的,这里边给大家介绍下Web测试核心之基于 Python 的 Selenium一、简单介绍Selenium 是用于测试 Web 应用程序用户界面 (UI) 的常用框架。它是一款用于运行端到端功能测试的超强工具。您可以使用多个编程语言编写测试,并且 Selenium 能够在一个或多个浏览器中执行这些测试。二、环境安装1.安装 python,这个。。。忽略,建议 python2.7。2.安装基于 python 的 selenium 依赖包,命令:pip install selenium==2.53.6,你懂的。3.安装浏览器驱动包,推荐使用 chrome 浏览器的 chromedriver.exe,对应 chrome 版本一定要对哦,不然运行不起来的,安装在哪?想放哪放哪,不过一般是放在 python 的根目录下。下载地址:chromedriver.storage.googleapis.com/index.html4.安装 PyCharm 2.7 左右版本,这个无脑安装~然后可自定义界面 UI 及编码风格,这个。。。忽略三、牛刀小试1.控制浏览器 #coding=utf-8from selenium import webdriverdriver = webdriver.Chrome()driver.get(“http://www.baidu.com”)driver.maximize_window() #将浏览器最大化显示driver.set_window_size(480, 800) #设置浏览器宽480、高800显示" driver.back() #后退driver.forward() #前进 driver.close() #关闭chromedriver.quit() # 退出chrome对Python开发感兴趣可以加705673780,群内会有不定期的发放免费的资料链接,这些资料都是从各个技术网站搜集、整理出来的,如果你有好的学习资料可以私聊发我,我会注明出处之后分享给大家。2.对象的定位通过 id 定位:find_element_by_id()通过 name 定位:find_element_by_name()通过 class 定位:find_element_by_class_name()通过 tag 定位:find_element_by_tag_name()通过 link 定位:find_element_by_link_text()通过 partial_link 定位:find_element_by_partial_link_text()通过 xpath 定位:find_element_by_xpath()通过 css 定位:find_element_by_css_selector()以上几种定位是常规操作,应该就基本够用了,但是有的时候就是会出现一些诡异的定位失效或者定位到了点击失效的问题,这个时候如果用js进行直接执行该事件,接下来介绍下非常规操作:id 定位:document.getElementById()name 定位:document.getElementsByName()tag 定位:document.getElementsByTagName()class 定位:document.getElementsByClassName()css 定位:document.querySelectorAll() search_js = “document.getElementsByName(‘wd’)[0].value=‘selenium’;”# 通过name定位,然后赋值“selenium” search_js2 = “document.querySelectorAll(’.s_ipt’)[0].value=‘selenium’;”# 通过css定位,然后赋值“selenium” button_js = “document.getElementById(‘su’).click();”# 通过id定位,然后执行单击操作 button_js2 = “document.getElementsByClassName(’s_btn’)[0].click()”# 通过className定位,然后执行单击操作 driver.execute_script(search_js2)#执行,execute_script(script, *args)以上几种定位是可以再度升级,可以利用 jQuery 定位一波,这里可参看之前总结的 JQ 选择器中的思维导图知识点(www.cnblogs.com/aoaoao/arti… JS,便忽略3.操作测试对象 #coding=utf-8from selenium import webdriverdriver = webdriver.Chrome()driver.get(“http://passport.kuaibo.com/login/")driver.find_element_by_id("user_name").clear() #清除输入框的默认内容driver.find_element_by_id(“user_name”).send_keys(“username”)driver.find_element_by_id(“user_pwd”).clear()driver.find_element_by_id(“user_pwd”).send_keys(“password”) #输入输入框的内容为“password”driver.find_element_by_id(“dl_an_submit”).click() #通过 submit() 来提交操作driver.find_element_by_id(“dl_an_submit”).submit()size=driver.find_element_by_id(“kw”).size #返回百度输入框的宽高 text=driver.find_element_by_id(“cp”).text #返回百度页面底部备案信息#返回元素的属性值,可以是 id、name、type 或元素拥有的其它任意属性attribute=driver.find_element_by_id(“kw”).get_attribute(’type’) #返回元素的结果是否可见,返回结果为 True 或 Falseresult=driver.find_element_by_id(“kw”).is_displayed()driver.quit() #退出4.鼠标键盘事件from selenium import webdriver #引入 Keys 类包from selenium.webdriver.common.keys import Keys #引入 ActionChains 类from selenium.webdriver.common.action_chains import ActionChains… #鼠标事件 #定位到要操作的元素right =driver.find_element_by_xpath(“xx”) #对定位到的元素执行鼠标右键操作ActionChains(driver).context_click(right).perform() #对定位到的元素执行鼠标双击操作ActionChains(driver).double_click(right).perform() #对定位到的元素执行鼠标移动到上面的操作ActionChains(driver).move_to_element(right).perform() #对定位到的元素执行鼠标左键按下的操作ActionChains(driver).click_and_hold(right).perform() #定位元素的原位置element = driver.find_element_by_name(“xxx”) #定位元素要移动到的目标位置target = driver.find_element_by_name(“xxx”) #执行元素的移动操作ActionChains(driver).drag_and_drop(element, target).perform() #键盘事件 #删除多输入的一个 值driver.find_element_by_id(“kw”).send_keys(Keys.BACK_SPACE) #输入空格键+“教程”driver.find_element_by_id(“kw”).send_keys(Keys.SPACE)driver.find_element_by_id(“kw”).send_keys(u"教程”) #ctrl+x 剪切输入框内容driver.find_element_by_id(“kw”).send_keys(Keys.CONTROL,‘x’) #其余的键盘操作类似5.等待时间 #coding=utf-8from selenium import webdriver #导入 WebDriverWait 包from selenium.webdriver.support.ui import WebDriverWaitfrom selenium.webdriver.support import expected_conditions as EC #导入 time 包import timedriver = webdriver.Chrome()driver.get(“http://www.baidu.com”) #WebDriverWait()方法使用,显示等待,WebDriverWait(driver,超时时长,调用频率,忽略异常).until(可执行方法,超时返回的信息),这里可以调用EC来实现可执行方法is_disappeared = WebDriverWait(driver, 30, 1, (ElementNotVisibleException)).until_not(lambda x: x.find_element_by_id(“kw”).is_displayed()) #until(method, message=’ ’),调用该方法提供的驱动程序作为一个参数,直到返回值不为 Falseelement.send_keys(“selenium”) #添加智能等待,隐时等待driver.implicitly_wait(30)driver.find_element_by_id(“su”).click() #添加固定休眠时间,强制等待time.sleep(5)driver.quit()6.组对象定位及层级定位,呃,忽略7.多窗口处理 #coding=utf-8from selenium import webdriver import timedriver = webdriver.Chrome()driver.get(“http://www.baidu.com/") #获得当前窗口nowhandle=driver.current_window_handle #打开注册新窗口driver.find_element_by_name(“tj_reg”).click()allhandles=driver.window_handles #循环判断窗口是否为当前窗口for handle in allhandles: if handle != nowhandle: driver.switch_to_window(handle) print ’now register window!’#切换到邮箱注册标签driver.find_element_by_id(“mailRegTab”).click()time.sleep(5)driver.close() #回到原先的窗口driver.switch_to_window(nowhandle)driver.find_element_by_id(“kw”).send_keys(u"注册成功!")time.sleep(3) #ifrome处理 #这里会自动识别id,name,如果没有则可以将元素通过选择器找到,然后输入该元素即可driver.switch_to_frame(“f1”)element = driver.find_element_by_id(“kw”)driver.switch_to_frame(element)driver.quit()&emsp;8.提示窗口处理 #coding=utf-8from selenium import webdriver import timedriver = webdriver.Chrome()driver.get(“http://www.baidu.com/") #点击打开搜索设置driver.find_element_by_name(“tj_setting”).click()driver.find_element_by_id(“SL_1”).click() #点击保存设置driver.find_element_by_xpath(”//div[@id=‘gxszButton’]/input”).click() #获取网页上的警告信息alert=driver.switch_to_alert() #接收警告信息alert.accept() #取消对话框(如果有的话)alert.dismiss() #输入值(如果有的话)alert.send_keys(“xxx”)9.控制浏览器滚动条,这个运用之前提示的jq语句即可实现10.cookie处理,主要用途在于处理验证码问题 #coding=utf-8from selenium import webdriver import timedriver = webdriver.Chrome()driver.get(“http://www.youdao.com”) #向 cookie 的 name 和 value 添加会话信息。driver.add_cookie({’name’:‘key-aaaaaaa’, ‘value’:‘value-bbbb’}) #遍历 cookies 中的 name 和 value 信息打印,当然还有上面添加的信息for cookie in driver.get_cookies(): print “%s -> %s” % (cookie[’name’], cookie[‘value’]) ##### 下面可以通过两种方式删除 cookie ##### # 删除一个特定的 cookiedriver.delete_cookie(“CookieName”) # 删除所有 cookiedriver.delete_all_cookies()time.sleep(2)driver.close()四、小结对Python开发感兴趣可以加705673780,群内会有大佬答疑,学习交流,免费学习资料可以领取。在熟悉了selenium常见的API基本操作后,这里便可以开展实际测试用例的设计了,一个良好的自动化测试用例起码符合一下五个条件:1、一个脚本是一个完整的场景,从用户登陆操作到用户退出系统关闭浏览器。2、一个脚本脚本只验证一个功能点,不要试图用户登陆系统后把所有的功能都进行验证再退出系统3、尽量只做功能中正向逻辑的验证,不要考虑太多逆向逻辑的验证,逆向逻辑的情况很多(例如手 号输错有很多种情况),验证一方面比较复杂,需要编写大量的脚本,另一方面自动化脚本本身比较脆弱, 很多非正常的逻辑的验证能力不强。(我们尽量遵循用户正常使用原则编写脚本即可)4、脚本之间不要产生关联性,也就是说编写的每一个脚本都是独立的,不能依赖或影响其他脚本。5、如果对数据进行了修改,需要对数据进行还原。6、在整个脚本中只对验证点进行验证,不要对整个脚本每一步都做验证。最后配合unittest或者testNG单元测试框架,实现分层、数据驱动、断言、截图、日志等全方位功能,得心应手的开展自动化测试工作。 ...

November 20, 2018 · 2 min · jiezi

只知道ajax?你已经out了

欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~本文由前端林子发表于云+社区专栏随着前端技术的发展,请求服务器数据的方法早已不局限于ajax、jQuery的ajax方法。各种js库已如雨后春笋一般,蓬勃发展,本文主要想介绍其中的axios和fetch。0.引入ajax(Asynchronous JavaScript and XML–异步JavaScript 和 XML),是一种客户端向服务器请求数据的方式,并且不需要去刷新整个页面;它依赖的是XMLHttpRequest对象。在我之前的文章中,介绍过ajax的创建过程,可以移步这次,我们聊聊ajax的创建过程。当然项目中我们一般没有直接使用原生的ajax,而是使用javascript的各种库,例如jQuery。jQuery对原生的XHR对象进行了封装,还增添了对JSONP的支持,且经过多年维护,各种文档资料非常丰富,非常适合学习和上手。不过随着前端技术的快速发展,react、vue框架的兴起,XHR对象都有了替代的方案(fetch)。另外如果为了要使用$.ajax方法,就导入整个jQuery这个大而全的库,也未免显得臃肿了些。所以本文将介绍两个目前常用的获取服务器数据的js库:axios和fetch。1.axios是一个基于 Promise 的 HTTP 库,可以用在浏览器和 node.js 中。随着 vuejs 作者尤雨溪发布消息,不再继续维护vue-resource,并推荐大家使用 axios 开始,axios 进入了很多人的目光。axios本质也是对原生的XHR的封装,不过它是Promise 的实现版本,符合最新的ES规范,axios的几条特性:(1)从浏览器中创建XHR;(2)从node.js创建http请求;(3)支持Promise API;(4)客户端支持防御CSRF(5)提供了一些并发请求的接口使用npm安装: npm install axios示例–执行GET请求://axiosaxios.get(’/user’, { params: { ID: 12345 }}).then(function (response) { console.log(response);}).catch(function (error) { console.log(error);});axios的优点:体积较小、使用简单、还可以执行多个并发请求,并且可以直接得到返回结果,不会像fetch需要自己去转换,个人还是比较喜欢使用axios。2.fetchfetch API脱离了XHR,是基于Promise设计。旧浏览器不支持Promise,需要使用polyfill es6-promise。2.1 使用使用npm安装:npm install whatwg-fetch –save示例–执行GET请求://use ‘whatwg-fetch’import ‘whatwg-fetch’var result = fetch(url, { credentials: ‘include’,//跨域请求带上cookie headers: { ‘Accept’: ‘application/json, text/plain, /’ }//设置http请求的头部信息 }).then(res => { return res.text() //将请求来的数据转化成 文本形式 // return res.json() //将数据转换成 json格式}).then(text => { console.log(text)}).catch(e => { throw (e)})可以在这个代码的基础上,增加一些操作,比如说在对请求数据处理前,先检查下返回结果的状态。对状态非200的结果,增加对应状态码的错误提示;在得到请求数据后,转换成需要的文本格式,或者json格式;另外,还可以对转换后的数据进行进一步的处理,比如请求的数据返回的是下划线类型的数据,可以处理成驼峰形式。2.2 fetch的优点及需要注意的地方为什么要使用fetch呢?直接使用jQuery和axios也能满足我们的开发需要。看了些文章,提及到使用fetch的好处:脱离的XHR,是ES规范里新的实现方式,支持async/await;更加底层,提供了丰富的API(request,response);符合关注分离,没有将输入、输出和用事件来跟踪的状态混杂在一个对象里;更好更方便的写法;需要注意的是:兼容性;当服务器返回400、500等错误码时并不会reject,只有网络错误等导致请求不能完成时,fetch才会被reject;fetch不支持abort,不支持超时控制,使用setTimeout及Promise.reject的实现的超时控制,并不能阻止请求过程继续在后台运行,造成了流量的浪费;fetch没有办法原生监测请求的进度,而XHR可以;fetch跨域请求时,默认不会带cookie,如果需要带cookie,则要指定:credentials:’include’,例如:var result = fetch(url, { credentials: ‘include’,});3.小结本文简单地分别介绍了axios和fetch的使用方法和特点。如果要详细了解fetch的应用,推荐阅读 MDN Fetch 教程和WHATWG Fetch 规范。如有问题,欢迎指正。相关阅读【每日课程推荐】机器学习实战!快速入门在线广告业务及CTR相应知识 ...

November 20, 2018 · 1 min · jiezi

附实例!实现iframe父窗体与子窗体的通信

欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~本文由前端林子发表于云+社区专栏本文主要会介绍如何基于MessengerJS,实现iframe父窗体与子窗体间的通信,传递数据信息。同时本文会提供一个可运行的实例代码,实现在父窗体中,获取到来自子窗体的数据的效果。0.背景介绍(1)需要在当前的前端项目中,使用iframe嵌套别的站点页面。(2)当子窗体触发了一个事件后,要给父窗体传一个跳转地址的url。父窗体监听到这个事件后,根据接收到的url,来更新当前父窗体的url,实现页面的跳转。1.采用方案1.1 MessengerJS方案可以采用MessengerJS方案,该方案可以实现父窗体与iframe之间的通信、多个iframe之间的通信。不过要前提是要确保对不同域的页面有修改权限,并且父窗体、子窗体页面都要同时加载这个MessengerJS。1.2 使用方法(1) 在需要通信的父窗体、和子窗体的文档中,都需要引入MessengerJS。(2) 父窗体和子窗体各自的文档(document)中,都需要自己的Messenger与其他文档通信,父窗体和子窗体的window对象都对应着有且仅有一个Messenger对象,该Messenger对象会负责当前window的所有通信任务。因此,每个Messenger对象都需要唯一的名字,这样它们之间才可以知道是在跟谁通信。另外,MessengerJS方案推荐指定项目名称,(类似命名空间的作用),以增强代码健壮性与组件复用性,避免未来与其他项目冲突。(注意: 项目名称应使用字符串类型)父窗体与子窗体初始化Messenger对象: // 父窗口中 - 初始化Messenger对象 // 推荐指定项目名称, 避免Mashup类应用中, 多个开发商之间的冲突 var messenger = new Messenger(‘Parent’, ‘projectName’); // iframe中 - 初始化Messenger对象 // 注意! Messenger之间必须保持项目名称一致, 否则无法匹配通信 var messenger = new Messenger(‘iframe1’, ‘projectName’); // 多个iframe, 使用不同的名字 var messenger = new Messenger(‘iframe2’, ‘projectName’);(3) 在发现消息前,目标文档要确保已经监听了消息事件: messenger.listen(function(msg){ alert(“收到消息: " + msg); });(4) 父窗体想给子窗体发信息,要添加消息对象,明确告知当前的父窗体,要发送消息的子窗体的window引用与messenger对象的名字:// 父窗口中 - 添加消息对象, 明确告诉父窗口iframe的window引用与名字 messenger.addTarget(iframe1.contentWindow, ‘iframe1’); // 父窗口中 - 可以添加多个消息对象 messenger.addTarget(iframe2.contentWindow, ‘iframe2’);(5) 发消息时,要指定messenger的名字和消息,例如父窗体要给子窗体发消息: // 父窗口中 - 向单个iframe发消息 messenger.targets[‘iframe1’].send(msg1); messenger.targets[‘iframe2’].send(msg2); // 父窗口中 - 向所有目标iframe广播消息 messenger.send(msg);2.实例基于上面的介绍,下面要实现开篇提出的需求了(实例代码只是示例如何传递数据,没有更改父窗体url)。这里分别是父窗体和子窗体的代码实现,可直接在浏览器中打开查看效果,其中messenger.js可以在这里下载,放到项目目录下。父窗体:<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <title>父窗体</title> <style type=“text/css”></style> <!– 这个messenger.js可下载放到项目目录下 –> <script type=“text/javascript” src=”./messenger.js"></script></head><body> <div>这是父窗体</div> <div id=“msg”></div> <iframe id=“iframe1” name=“iframe1” src="./child.html" width=“600px” height=“316px” style=“z-index: 100000;position: absolute;"> </iframe></body><script type=“text/javascript”> //父页面中,注册一个messager到一个统一的项目中,第一个参数为自己页面的名称,第二个参数为项目名称 var messenger = new Messenger(‘parent’, ‘monitor’), iframe1 = document.getElementById(‘iframe1’); //父页面中绑定监听消息事件,当接受到iframe1发来的消息后执行 messenger.listen(function (msg) { // alert(msg); var oDiv = document.getElementById(“msg”); oDiv.innerHTML += msg; //todo });</script></html>子窗体:<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <title>子窗体</title> <style type=“text/css”></style> <!– 这个messenger.js可下载放到项目目录下 –> <script type=“text/javascript” src=”./messenger.js"></script></head><body> <div style=“background: #8CB08B;height:300px;"> <div>这是子窗体</div> <input type=“button” onclick=“sendMessage(‘这是一条来自子窗体的消息!’)” value=“按钮” /> </div></body><script type=“text/javascript”> //子页面中,注册一个messager到一个统一的项目中,第一个参数为自己页面的名称,第二个参数为项目名称 var messenger = new Messenger(‘iframe1’, ‘monitor’); //添加消息对象, 明确告诉子窗口iframe的window引用与名字 messenger.addTarget(window.parent, “parent”); function sendMessage(msg) { messenger.targets[“parent”].send(msg); }</script></html>代码解释:父窗体中嵌入iframe,要先引入messenger.js,同时初始化messenger到一个统一的项目中,其中第一个参数为自己页面messenger对象的名字,第二个参数为项目名称;然后父窗体要绑定监听消息事件,当接收到iframe子窗体发来的消息后执行。子窗体也要先引入messenger.js,同时初始化一个messenger到一个统一的项目中,其中第一个参数为自己页面messenger对象的名字,第二个参数为项目名称;然后添加消息对象,告知子窗体的window引用与messenger对象的名字。然后在触发onclick事件时,向父窗口传递消息。发消息时,要指定接收消息的父窗体的messenger的名字,以及传递的消息。3.小结本文主要是介绍了一个MessengerJS方案及其使用方法,来解决父窗体与子窗体的通信问题。同时提供了一个完整的实例,可以实现子窗体向父窗体传递消息,父窗体通过监听消息事件,来获取子窗体消息的目的。如有问题,欢迎指正。相关阅读【每日课程推荐】机器学习实战!快速入门在线广告业务及CTR相应知识 ...

November 16, 2018 · 1 min · jiezi

纯css实现加号效果

实现下图的加号效果:若想实现这个效果, 只需一个div元素即可搞定。需要用到css的为了before和after, 以及border特性。先设置一个div便签<div class=“add”></div>再设置一个边框:.add { border: 1px solid; width: 100px; height: 100px; color: #ccc; transition: color .25s; position: relative;}此时边框是这样的:我们可以利用伪类before和其border-top来设置一个“横”:.add::before{ content: ‘’; position: absolute; left: 50%; top: 50%; width: 80px; margin-left: -40px; margin-top: -5px; border-top: 10px solid;}注意我们使了绝对定位。 此时变成了这样:参照上面, 我们可以使用after伪类和border-bottom设置一个“竖”:.add::after { content: ‘’; position: absolute; left: 50%; top: 50%; height: 80px; margin-left: -5px; margin-top: -40px; border-left: 10px solid;}在加上hover伪类,设置鼠标悬浮上去的颜色:最终的样式:当鼠标悬浮上去是, 会变色:web前端技术分享点击:加入

November 16, 2018 · 1 min · jiezi

如何给localStorage设置一个过期时间?

引言 这个话题其实在上次分享<小程序填坑记里讲过了>已经讲过(大佬可绕过哦~),但后来群里/评论都有些同学,提到了一些疑问,问能否单独整理一篇更为详细的分享,讲解一下细节和完善提到的不足,如是有了下文????。 —— 「 用心分享 做有温度的攻城狮,我是首席填坑官——苏南 」各位大佬早安,这里是@IT·平头哥联盟,我是首席填坑官∙苏南,用心分享 做有温度的攻城狮。公众号:honeyBadger8,群:912594095思考点 从我们接触前端起,第一个熟悉的存储相关的Cookie或者来分析我们生活中密切相关的淘宝、物流、闹钟等事物来说起吧,Cookie从你设置的时候,就会给个时间,不设置默认会话结束就过期;淘宝购物 从你下单付款起,就会给这件货物设置一个收货期限时间,过了这个时间自动认为你收货(即订单结束);闹钟 你设置的提醒时间,其实也就是它的过期时间;再比如与您每天切身相关的产品需求,过完需求,你给出的上线时间,也就是这个需求的过期时间;再通俗点讲,您今年的生日过完到明年生日之间也是相当于设置了有效期时间;以上种种,我们能得出一个结论任何一件事、一个行为动作,都有一个时间、一个节点,甚至我们可以黑localStorage,就是一个完善的API,为什么不能给一个设置过期的机制,因为sessionStorage、Cookie并不能满足我们实际的需求。实现思路 抱歉,黑localStorage不完善,有点夸张了,综合上述的总结,问题就简单了,给localStorage一个过期时间,一切就都so easy ?到底是不是,来看看具体的实现吧:简单回顾//示例一:localStorage.setItem(’test’,1234567);let test = localStorage.getItem(’test’);console.log(typeof test, test); //示例二:localStorage[’name’] = ‘苏南’;console.log(localStorage[’name’]);/输出:“1234567” ,‘苏南’,这里要注意,1234567 存进去时是number 取出来就成string了/重写 set(存入) 方法:首先有三个参数 key、value、expired ,分别对应 键、值、过期时间,过期时间的单位可以自由发挥,小时、分钟、天都可以,注意点:存储的值可能是数组/对象,不能直接存储,需要转换 JSON.stringify,这个时间如何设置呢?在这个值存入的时候在键(key)的基础上扩展一个字段,如:key+’expires’,而它的值为当前 时间戳 + expired过期时间具体来看一下代码 :set(key, value, expired) { /* * set 存储方法 * @ param {String} key 键 * @ param {String} value 值, * @ param {String} expired 过期时间,以分钟为单位,非必须 * @ 由@IT·平头哥联盟-首席填坑官∙苏南 分享,交流:912594095 / let source = this.source; source[key] = JSON.stringify(value); if (expired){ source[${key}__expires__] = Date.now() + 100060expired }; return value;}重写 get(获取) 方法:获取数据时,先判断之前存储的时间有效期,与当前的时间进行对比;但存储时expired为非必须参数,所以默认为当前时间+1,即长期有效;如果存储时有设置过期时间,且在获取的时候发现已经小于当前时间戳,则执行删除操作,并返回空值;注意点:存储的值可能是数组/对象,取出后不能直接返回,需要转换 JSON.parse,具体来看一下代码 :get(key) { / * get 获取方法 * @ param {String} key 键 * @ param {String} expired 存储时为非必须字段,所以有可能取不到,默认为 Date.now+1 * @ 由@IT·平头哥联盟-首席填坑官∙苏南 分享,交流:912594095 / const source = this.source, expired = source[${key}__expires__]||Date.now+1; const now = Date.now(); if ( now >= expired ) { this.remove(key); return; } const value = source[key] ? JSON.parse(source[key]) : source[key]; return value;}重写 remove(删除) 方法:删除操作就简单了,;remove(key) { const data = this.source, value = data[key]; //首席填坑官∙苏南的专栏 delete data[key]; delete data[${key}__expires__]; return value;}优化点:记得上次有个同学,是这么评论的:「 删除缓存能放到constructor里面执行么,放到get里面 不取就一直在那是不是不太好?」;所以本次优化做一个初始化删除操作,清除已经过期的数据;为什么不用for in而是 for ? for in循环遍历对象的属性时,原型链上的所有属性都将被访问,解决方案:使用hasOwnProperty方法过滤或Object.keys会返回自身可枚举属性组成的数组;class storage { constructor(props) { this.props = props || {} this.source = this.props.source || window.localStorage this.initRun(); } initRun(){ / * set 存储方法 * @ param {String} key 键 * @ param {String} value 值,存储的值可能是数组/对象,不能直接存储,需要转换 JSON.stringify * @ param {String} expired 过期时间,以分钟为单位 * @ 由@IT·平头哥联盟-首席填坑官∙苏南 分享,交流:912594095 */ const reg = new RegExp("expires"); let data = this.source; let list = Object.keys(data); if(list.length > 0){ list.map((key,v)=>{ if( !reg.test(key )){ let now = Date.now(); let expires = data[${key}__expires__]||Date.now+1; if (now >= expires ) { this.remove(key); }; }; return key; }); }; }}总结: 以上就是今天为大家总结的分享,您GET到了吗?小程序、sessionStorage、localStorage,都适用,做些许调整即可哦,希望今天的分享能给您带来些许成长,如果觉得不错,记得关注下方公众号哦,每周第一时间为您推最新分享????????。更多文章:easy-mock 最好的备胎没有之一immutability因React官方出镜之使用总结分享!面试踩过的坑,都在这里了~你应该做的前端性能优化之总结大全!如何给localStorage设置一个过期时间?动画一点点 - 如何用CSS3画出懂你的3D魔方?作者:苏南 - 首席填坑官链接:https://blog.csdn.net/weixin_…交流:关注公众号邀请您加入交流群 honeyBadger8本文原创,著作权归作者所有。商业转载请联系@IT·平头哥联盟获得授权,非商业转载请注明原链接及出处。 ...

November 15, 2018 · 2 min · jiezi

读了这篇文章,你将变身web分析大师

欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~本文由shirishiyue发表于云+社区专栏1、工具介绍 这是一个非常详细且专业的web页面性能分析工具,而且开源的!如果你打不开其官网,或者担心安全问题,你可以自己拿源码搭建这个平台工具。 性能分析极其详细,包含你所知道的所有新能参数指标,还有一些这个工具自己的指标参数,比如speed index,更能从人的角度看待一个页面体验是否良好。提供了多种主流浏览器的访问性能,提供了全球多个地点的测试性能,还有视频录像功能可以返回体验。这个视频功能极好的用于直观的对比演示。总之,优点非常之多,没有理由不尝试一下。 入口在 这里,长这样, 上面三输入框,,,第一行,输入你的页面url。 第二行,选择你的访问点,比如你想测一下北京的用户访问速度,那么你选择北京。 第三行,选择什么浏览器访问。 右边,点击START TEST,,,,耐心等待。 分析的结果时存下来的,比如,我的三个url分析结果如下,可以反复进入查阅。页面1:https://test.igame.qq.com/pvp…,分析结果。页面2:http://134.175.16.24/vuessr/a…,分析结果。页面3:http://134.175.16.24/newcss/a…,分析结果。2、结果使用和分析 分析完后,结果长这样, 总共跑了三次。每一次都是First View(就是相当于新用户,首次打开页面,没有任何缓存)。 Performance Result 就是指标总览,列举了一些主要的新能指标的平均值。指标解析: 从输入url按enter开始的,达到下面节点的时间。▲ Load Time 页面完全加载完时间(不等待图片下载,iframe下载,css更新完),此时,window.onload 事件此时触发。同 Document complete time.▲ DOM Content Loaded HTML DOM 骨架完全下载和解析,包括<script>元素。(等待图像下载,css更新,iframe更新等)。此时,DOMContentLoaded 事件触发。(你在预加载数据的时候,可以在这个事件时会更合适些,而不是上面的dom ready)▲ Time to First Byte 首字节时间。浏览器接收到第一个字节的时间。包括服务器处理以及网络传输,DNS寻址时间+建立连接时间(Socket) + SSL认证时间等。▲ Start Render 白屏后首次出现内容的时间。▲ Speed Index 速度指数,页面呈现出来的平均时间。比Start Render更人性化的指标。详细计算方式参考:Speed Index 。主要▲ Time to Interactive 首次可交互时间。页面可以开始响应用户输入事件。(因为页面呈现过程中,其实还是不可交互的。)▲ Requests 浏览器针对页面上的内容(图片,javascript,css等)发出的请求数。▲ Bytes In 浏览器加载页面下载的数据量。它通常也被称为“页面大小”。过程详细:▲ 中间的Waterfall可以看到瀑布流图,点进去,跟你使用chrome tool一样。不多介绍。▲ Screen Shot,网页快照,保存了从输入url到页面fully loaded的全称快照。▲ Video 整个过程的视频。这个非常好,你可以和几个视频放在一起,非常直观的看哪一个快哪一个慢。▲ 各种 breakdown 拆分统计,包括 Contents,Processing,Domains等等,,,,,比如,Contents 拆分统计如下,▲ domains breakdown,and, request map,,,可统计请求域名情况,域名越多,明显对时间要求更多。▲ 性能表现总量,可以优化性能的指标完成情况。 这里面列出了可使用的性能点,如:First Byte Time (back-end processing): 51/100Use persistent connections (keep alive): 100/100Use gzip compression for transferring compressable responses: 42/100Compress Images: 100/100Leverage browser caching of static assets: 14/100Use a CDN for all static assets: 0/100相关阅读【每日课程推荐】机器学习实战!快速入门在线广告业务及CTR相应知识 ...

November 14, 2018 · 1 min · jiezi

程序员新人面临最尴尬的事:需要工作积累经验,需要有经验才能找到工作!到底怎么办?

你需要一份工作来积累经验,但你需要有经验才能找到工作……大学刚毕业,或者转换职业方向后新入一个职场领域,你会发现求职之路举步维艰。大多数的公司职位都要求工作经验,单这一点就足以将你拒之门外。前几天,一位即将从大学毕业的读者征求我的建议,问我该怎样才能找到一份开发岗位的工作。他投入了大量的时间浏览求职网站,海投求职信,但却没有收到一封回复。我完全理解他的这种痛苦。现实情境也确实如他所遭遇的那般,大学毕业生以及那些没有多少经验的求职者很难找到一份工作。如果我们问那些雇主,为什么他们不想雇用没有太多工作经验的人,我们得到的通常也是相同的回答,“我们想找经验丰富的人”。这种感觉糟透了。你需要一份工作来积累经验,但你有需要经验才能找到一份工作……这就是其中的矛盾之处所在。幸运的是,在编程职业领域,有一个方法可以解决这一难题。你所要做的就是找到愿意聘请经验不足程序员的雇主,听上去似乎不可能?其实并不是这样,信不信由你,确实有这样的雇主存在。他们正想尽办法,只要能找到人来解决他们的问题,他们就很愿意为你的付出支付酬劳。自由职业和临时工作小项目自由职业?全职工作都没有人愿意雇用我,作为自由职业者又怎么会有人愿意雇用?自由职业到底有什么特别之处?我再坚持一段时间,继续找工作岂不是更好?在你抛出所有的这些疑虑之前,在你投出更多没有多少希望的简历之前,请往下看在开发岗位工作领域内,我暂且将自由职业划分为几个不同的类别,其中分别是顾问、自由职业雇员和自由职业临时项目从业者。顾问通常是非常有经验的专家开发人员,他们大多负责处理一些棘手的技术问题,并能给出自己独特的见解,展现高水准的专业知识。既然你是初入编程领域的新手,显然这一类别并不适合你。自由职业雇员自由职业雇员与全职正式雇员非常相似,只是自由职业雇员不是像正式员工那样直接为公司所雇用,而是通过职业介绍所得到的这份工作。这通常是由于政治原因,在这里无法详细解释,只能说你也并不属于这一类。自由职业临时性项目从业者。临时性项目自由职业者填补了软件工程领域一块特殊的空白区。他们通常所负责的往往都是要么项目太小、要么太专业或者太具试验性质而无法聘请全职员工的那些工作。雇主之所以雇用他们是因为他们需要有人去完成这些临时性工作,而临时工作者可以并且愿意去做这些工作。以下是一些可能会雇用临时性工作者的雇主案例:企业家—希望对某一个想法进行概念验证内部创业者—在公司内部致力于创新的企业家,他们也在寻求进行概念验证小企业—通常寻找一些召之即来的人负责安装或设置某样东西遇到编程问题的个人—可能是需要你帮他一起完成编程任务或者其他类似事情的某个人这些客户可以提供的编程工作的统一特征是具体、零碎并且具有临时性。这类工作肯定也有它的缺陷所在,我不会将这类工作推荐给你作为长期职业生涯的发展方向。但对于那些刚开始进入编程领域的人来说,这是发展个人技能、创建人脉联系以及可信度的最快捷途径之一。除此之外,这也是一种很有趣的体验。为什么职业生涯刚开始适合接这种零碎的小活儿?你可能不敢相信,这类工作很容易找到,它们通常都是些小活儿,可能只需要你投入几个小时或者最多一个星期的时间即可完成。这类工作报酬并不会太高,所以竞争自然也不是很激烈。除此之外,这类工作并不需要你有多少的经验,大多是要求你完成类似“编写一个简单的网站”这样的工作,如果是让你做一个京东啊,支付宝啊。。请你三思而后行。。对于雇主而言,提供这类工作的成本要低得多,不需要做出长期承诺、签订长期雇佣合同,不需要打印员工身份徽章,也不需要人力资源部门的参与。如果这份工作没有做好,那也并不会造成多大的损失。上述这些因素就意味着要找到这样的工作其实相当容易,你只要尽量表现出自己的专业水准,干活麻利,不要让别人觉得自己不好合作即可。做到这些,即便你缺乏经验,也可以得到这一类的工作机会。找到适合你自己的工作类型当你得到这样的临时工作机会之后,你可以利用这一机会来探索自己适合怎样的编程工作。这种零碎的临时工作可以让你有机会为许多不同的雇主工作,并参与到许多不同的项目之中。这是一个很好的机会,你可以借此找到自己喜欢的职业方向。我自己就是用这种方法尝试了编程领域各种不同的工作,在这过程中,我有机会尝试的项目类别包括:网站、自定义视频播放器、音频播放器、微控制器编程、直播和视频会议、简单的游戏等等……不胜枚举。利用这些临时工作机会来找到自己喜欢以及不喜欢的程序工作方向。这一探索和学习的过程非常有价值,相比于你从事一份自己并不喜欢的工作而赚到的钱来说,更有价值。临时工作能够磨练你的个人技能,提升专业水平当你处于编程职业生涯早期阶段之时,你可能还没有做好参与大项目的准备,因为这些项目往往都十分复杂,可能会涉及到你尚未掌握的一些技能(例如读取他人的代码等)。对于这些临时性的小项目,你可能需要自己去负责所有的事情,你必须创建整个系统,除此之外别无选择,这样你就可以了解到各个方面都是如何运作的。我们开始接受一些临时性工作时,其实根本就没有对它们抱多大的期待,但神奇的事情确实会发生。例如,今天我用了两个小时的时间完成了一位客户交给我的临时性工作,并且做到了让客户满意。第二天,这位客户又来找我,问我是否有时间再做另一个项目。我创建的每一个项目都可以添加到我的项目之中,这每一个项目也都拓展了我的人脉网络。我之前服务过的客户会陆续把他们的朋友和同事介绍给我,随着时间的推移,我甚至能为一些非常知名的公司工作。最终,我甚至不必再自己去寻找新的工作,各种新工作会直接来找我。“细分”市场地理区域:一些人才市场专门面向当地社区,当雇主想与当地人才合作时,他们往往会通过这些网站来进行招聘。技术:一些市场会侧重某项特定的技术。如果你曾花时间掌握了某项特定技术,那这些方向的求职板块将成为你发挥优势的地方。特定受众:一些市场并不是以技术为侧重点,而是专注于特定的受众。每个社区都有软件项目,如果你是该社区成员,那你就可以访问其他开发人员看不到的这些内容。选择一个合适的市场能够大大增加你找到合适临时工作的几率。在这种细分市场,你与雇主之间将更具相关性,与普通的自由职业求职网站相比,你面临的竞争压力也更小。最重要的是,专注于细分市场能够让你有机会专注积累自己某个方面的专业优势,从而让你在以后的求职过程中更有优势。

November 14, 2018 · 1 min · jiezi

程序员的你还沉浸在大公司就是螺丝钉?小公司锻炼人?错了!看完即懂

刚毕业那会经历过很多所谓创业公司,和很多朋友经历过画大饼,洗脑以及公司上市原始股这样的承诺。当你正在趟过这些谎言你就会发现,在这个世界上能信这些鬼话的也只有涉世未深的毕业生了。小公司里真的就是十几二十几个精英带你一路向前?没有办公室政治?呵呵,金庸说过有人的地方就有江湖。在经济下滑的今天小公司的老板能不拖欠的工资你就烧高香吧。作为一个曾经毕业就信了大公司是螺丝钉,小公司锻炼人的鬼话的过来人,希望才毕业的朋友不要再去趟这个坑。记住:名企的经历就是名校学历!大公司就真的是个螺丝钉?要知道大公司是由N多个子公司/部门组成,每个部门下面有n多个TEAM,每个team就是一个个小公司。缺人的时候你指望别的部门的人给你干活?做梦?加班去吧,看书去,学新技术去。该折磨你的磨难你一个都少不了。关于创业,全team都知道我会接私活,然后呢?大公司谁会关心你做一个小app,或是赚了什么小钱。随便接一个单子都爆你家几套房子,你那破玩意谁不会做。一句话你要想混哪儿都能混,你要想学大公司还能拦着你不让你进步?你周围的这些人的生活就决定着你未来的模样。大公司里名校出身,硕士博士遍地爬。富二代不想接班在公司学经验也不在少数,技术大牛哪个部门没有几个坐镇的?这些是小公司想都不敢想的事情。完善的制度和庞大高质量的人脉网络,比起一大堆老公是老板,老婆是产品经理,小姨子是销售总监的家庭小作坊,大公司是完完全全的正规军,制度先放一边,不说别的,该有国家福利你一毛钱都不会少,让你走的时候该赔的钱也够你在家宅半年。小公司呢??人少大牛更少。你可以说你们那公司里谁谁曾经是哪里出来的大牛。这句话就已经暴露出你的大公司的情节,因为他是XX公司出来的,所以他是大牛。如果一个家伙没有名企经验,几十年就在各个小公司里打转但是就是各种牛逼,说实话,这话你自个信吗?你要是个小公司出身,假设你技术牛逼/人脉广泛。对不起,纵使有几个识货的猎头愿意挖你,但是也有HR因你的出身压低你的入职工资,不要觉得欺负人,你要不是名校毕业压的更低。如果你阿里巴巴/腾讯这样的公司出身,你只要把简历挂网上猎头就能把你的电话打到爆,即使在知乎这样的平台上你回答个问题也会有很多人在后面点赞,没人关心你是谁?没人听你说自己有多牛逼,只是因为你鹅厂/猫厂的人。社会价值取向就是这么势利。你承认也好不承认也好,这就是现实。选公司一.大公司VS大公司如果手里有几个大公司的offer该怎么PK?比如狼厂的,鹅厂的,美团的,360的,小米的,京东的,这个之间改怎么选?1.核心业务优先因为大公司好但不是什么都好,每个巨头都会有自己的核心业务和专属领域,这就是我们所说的基因。比如说阿里的电商基因,腾讯的社交基因,百度的搜索和数据基因。这些都是大公司的看家本事,是他们市值的主要来源,都会死死看住,并投入最主要的资源。如果你手头有两个大公司的offer,一个是核心业务,一个是非核心业务。那么通常要选择去核心业务,因为这意味着钱多、资源多、内部话语权强、上升通道通畅等等。非核心业务部门就比较不好说,比如说像当年的腾讯微博和搜搜,百度的百伯和爱乐活,阿里的阿里云和来往。今天的阿里云和当年的阿里云已经完全不一样了,当年的阿里云是解散过的。像我上面所说的这些业务,如今很多大公司都已经不在做了。所以,在进行这样的业务选择的时候,一定要搞清楚大公司的业务有几个战略走向,什么是他们吃饭的家伙,什么是阻击性业务,什么是常识性业务。我们说大公司资源多钱也多,当方向看不清的时候,他们可能会同时孵化几个十几个方向的业务。这是一个试错的过程,和天使投项目没什么区别,万一哪个成功了呢。但是,大公司孵化这样的项目,老板通常给你的时间不会太长。如果数据不错,接着投,如果不行,立刻裁撤。而且在这样的项目中,因为不是大公司的核心业务,他们的人员储备也不多,所以很多职位会通过社招来解决。这时候如果你觉得这是馅饼砸中了你,那真的就有点一厢情愿了。2.看核心职能其实和业务有基因是一样的,大公司的职能也是有基因的。我们说互联网的三大职能,技术产品和运营。其中百度的技术、腾讯的产品、阿里的运营这些口口相传的说法,都是有一定道理的,也是主营业务的一种必然需求。百度的搜索需要技术比较多,电商就会要求运营强,社交要求产品体验好。这些都不是谁拍脑袋就能定的,而是在企业长期发展中磨合出来的基本倾向。一旦这种格局形成,我们对于组织文化、对于绩效文化就会产生相应的影响。它决定了在内部运作中的话语权和资源的分配,这就是我们所说的产品导向、技术导向还是运营导向。如果说你拿到了几个大公司的offer也可以根据自己的职能领域来做一个比较,毕竟谁都希望自己所在的部门是公司的主导部门。小公司VS小公司那么我们接下来说说第二个场景,如果说都是小公司或者都是创业公司的offer该怎么选。其实在目前的情况之下,创业市场真的是鱼龙混杂、泥沙俱下。不光你难选,其实投资人也难选。这个部分靠眼力,部分靠人品。人品我真的没有办法教大家,所以还是练练眼力吧,这个也叫因上努力,果上随缘。1.选团队相比上面的因素,这个其实最难选,也最难讲清楚。毕竟模式太多,变量也太多,所以在这里只能给出一些大致的标准。2.选创始人在选择团队的时候,我觉得首先需要看创始人。我们做过非常多样本的一个统计,在c类公司,创始人对于公司的重要性超过8成,这丝毫不为过,也算是荣辱成败系于一身。所以对创始人的选择要点有以下几个:(1).传统行业出身的创始人通常不选因为互联网的玩法和传统行业的差别太大,等创始人自己转过身来,公司早就黄了。(2).想趁创业浪潮起把哄的创始人不选我们看过一个市调,采访了超过500个创始人,结果是相当的触目惊心。对于这些创始人来讲,当能够拿到20万到30万的稳定年薪时,有19.2%的创业者会选择放弃创业。当这个数字上升到30万到50万的时候,又会有36%的人选择放弃创业去打工。真正到百万年薪还不改初衷的,只有0.2%的人,而这个0.2%大致也吻合创业公司成功的概率。因为拿不到50万年薪而出来创业的人,我们通常称之为互联网的loser,也就是失败者,如果你跟着这样的创始人,结果肯定是不言自明的。(3).投资人兼创始人的通常不选因为他外面事情太多了,不会只做这一个事情,而对于互联网创业来讲,如果没有全情投入,事情基本是做不成的。(4).有过连续创业经历的会加分因为即使之前的创业并不成功,但一则他有着过人的勇气,二则之前的失败经历会让他少走许多弯路,你跟着他去做这个事情,自己的试错成本会小。如果是之前有过成功创业经历的,那就更好了。(5).有过大公司背景的创始人可以谨慎加分他之前的人脉、资源和训练,都是创业的重要条件。当然从大公司出来的眼高手低的也有,但总的来讲,有大公司的背景再出来创业是利大于弊的。(6).选择有情怀的创始人因为互联网创业确实是需要一点情怀的,需要有一点改变世界的勇气。如果只为名和利而创业,通常是很难坚持下去的。当然情怀这个东西不能当饭吃,有情怀还要有实现情怀的脚踏实地。那么大家会说怎么去判断情怀,其实判断这个东西也并不太难。你看一下创始人在关键利益点上的选择就知道了。比如说这样的关键利益点包括公司的期权值、公司的股权结构,了解一下老板在创业过程当中自己实际拿多少工资,在b轮的融资之前有没有套现,老板自己有没有往里投了像样的钱等等等等。这样的判断其实还是需要有一定的经验,如果你实在不知道,那你只能找我来单独咨询了。3.选商业模式相比上面的因素,这个其实最难选,也最难讲清楚。毕竟模式太多,变量也太多,所以在这里只能给出一些大致的标准。1.首先这个模式必须要解决痛点,比如滴滴,痛点切得就很直接,就是解决大中城市出行难的问题。比如团购,C端痛点切得还可以,就是价格和便利,但B端痛点就很难说了,牺牲利润的方式得来的大部分是价格敏感的客户,一旦价格优势不在,立刻作鸟兽散,形不成有效回头客流,这无异于是饮鸩止渴,除非做成全产业链平台,否则团购业务单独规模化盈利的可能性是非常渺茫的,这也是最近为什么美团和大众点评合并。2.如果有巨头已经深度介入的领域就算了,比如现在的大搜索,整合的电商平台,范社交领域都有巨头深度介入,在这些领域创业通常成功的概率是非常低的。3.屡次被实践证明不可行的就不要蹚浑水了,比如职业化社交,这么多年起起伏伏,没有一家公司能做出来。包括进入中国的LinkedIn,在中国的发展我就极度不看好。4.传统公司办的互联网分支就算了,因为鸡是没法同鸭讲的。现在有很多房地产公司,传统的物流公司,包括服务类公司,一直在做互联网的分支,但我对他们的前景一直并不特别看好。5.商业模式太大太空,动不动就要建平台的,需要非常谨慎的选择。中国互联网发展到今天,除非技术上有重大突破,否则平台型的模式很难做起来,即使有这样的机会,也是从一个细分领域慢慢摸索出来的,一上来就说要建入口级、平台级模式的,要么是忽悠,要么是梦游,你可以给他鼓鼓掌,然后拍拍屁股离开。最后大家理性面对创业小公司,或许你听到的鸡汤,为你编制的梦想,都是精心策划的谎言,真正想创业,靠谱的公司是不会找应届生的。你拿青春赌明天,但你的青春是回不来的。能去大公司的,还是优先选择大公司吧,如果暂时的技术问题,拿不到大公司的offer。也要正视中小型公司的选择!努力把自己的技能提高,才能把选择掌握在自己的手中!这里推荐一下我的前端学习交流q.u.n.:731771211,里面都是学习前端的,如果你想制作酷炫的网页,想学习编程。自己整理了一份2018最全面前端学习资料,从最基础的HTML+CSS+JS【炫酷特效,游戏,插件封装,设计模式】到移动端HTML5的项目实战的学习资料都有整理,送给每一位前端小伙伴,有想学习web前端的,或是转行,或是大学生,还有工作中想提升自己能力的,正在学习的小伙伴欢迎加入学习。点击:加入

November 11, 2018 · 1 min · jiezi

浅谈easy-mock 最好的备胎没有之一

引言 今天我们来聊聊Mock,随着互联网发展,这两年前后端分离的开发模式兴起,Mock也从以住的幕后走上了台面,让更多的人而得知,以前传统的开发方式Mock大多局限在后端人员接触较多一些。 Mock已经是老生常谈了,网上一搜索就很多,各位前辈们都讲的很到位,但今天我只讲它——easy-mock。 为什么会突然来聊它,这个就说来话长了,个人简介里就说过,专注于分享工作中遇到的坑,但这一次不是我的坑,来源于QQ群友(# 如果您有想知道的故事,而正好我也会,那么就由我为您讲出来吧,欢迎留言哦 # ),请看下图:这里是@IT·平头哥联盟,我是首席填坑官—苏南,用心分享 做有温度的攻城狮。什么是Mock 什么是Mock?? Mock其实就是真实数据存在之前,即调试期间的代替品,是个虚拟的存在,用人话讲它就是个备胎,如女生长的好看,追她的人多,但又不是很满意但也不拒绝,在自己心仪的小哥哥出现之前,一直吊着你????!如何Mock数据?不要告诉我 new 一个哦,对象可以 new,备胎可new不出来呢????;方法一:最low的方式将 Mock 数据写在代码里、json文件里;方法二:利用 Charles 、Fiddler等代理工具,将 URL 映射到本地文件;方法三:本地起 Mock Server,即mockjs,有点麻烦每次修改了后还要重启服务,nodemon能解决,但开的东西多了,电脑卡出翔,维护也麻烦;方法四:规范些的公司自己已经集成了一套mock数据体系;重点来了:easy-mock一个在线 Mock 平台,活儿好又性感是你备胎的最佳选择。当然优秀的你可能还有很多其他的方式,欢迎补充。//mock 基本使用示例import Mock from “mockjs”;Mock.mock({ “code”: 0, “message”: “请求成功”, “data|20”: [{ “name”: “@cname”,//cname 中文,name 英文 “userId”: “@id”, “lastDate”: “@datetime” }]})什么是easy-mock,能给我们带来什么?Easy Mock 是一个可视化,并且能快速生成 模拟数据 的持久化服务,Easy Mock 支持基于 Swagger 创建项目,以节省手动创建接口的时间;简单点说:Easy Mock就是一个在线创建mock的服务平台,帮你省去你 配置、安装、起服务、维护、多人协作Mock数据不互通等一系列繁琐的操作, 它能在不用1秒钟的时间内给你所要的一切,呼之即来、挥之即去的2018最优秀备胎没有之一,完全不用担心负任何责任哦。更多优点它在等你去发现哦……深入浅出 - 简介就跟人一样长的再好看,了解过后才懂,一样东西也是如何,谁用谁知道;Easy Mock支持团队协作,也可以是个人项目,以下以个人项目为例,与多人协作没有区别,只是少了成员邀请;深入浅出 - Mock语法回顾@ip -> 随机输出一个ip;@id -> 随机输出长度18的字符,不接受参数;“array|1-10” -> 随机输出1-10长度的数组,也可以直接是固定长度;“object|2” -> 输出一个两个key值的对象,"@image()" 返回一个占位图url,支持size, background, foreground, format, text;等等,这里就不再一一介绍。深入浅出 - 创建一个接口它的写法,跟Mock.js一模一样,上面代码已经展示过,更多示例使用Easy Mock创建一个接口,请看下图:深入浅出 - 高阶用法 Function在线编辑,它也能支持 function ,是不是很优秀,能获取到全部请求头,可以让我们像写在js里一样写逻辑,写运算,当然它肯定是还有很多局限性的,如并不支持ES6,有一点需要注意的是 function 里要写传出Mock对象,不能直接@…,来看示例:对象描述MockMock 对象_req.url获得请求 url 地址_req.method获取请求方法_req.params获取 url 参数对象_req.querystring获取查询参数字符串(url中?后面的部分),不包含 ?_req.query将查询参数字符串进行解析并以对象的形式返回,如果没有查询参数字字符串则返回一个空对象_req.body当 post 请求以 x-www-form-urlencoded 方式提交时,我们可以拿到请求的参数对象…_req.cookies、ip、host等等,我只是一个代码的搬运,更详细请看这里//简单模拟登录,根据用户传入的参数,返回不同逻辑数据{ defaultName:function({_req}){ return _req.query.name; }, code: function({_req}){ return this.defaultName ? 0 : -97; }, message: function({_req}) { return this.defaultName ? “登录成功” : “参数错误”; }, data: function({req,Mock}){ return this.defaultName ? { token: Mock.mock("@guid()"), userId: Mock.mock("@id(5)"), cname: Mock.mock("@cname()"), name: Mock.mock("@name()"), avatar: Mock.mock("@image(200x100, #FF6600)"), other:"@IT·平头哥联盟-首席填坑官∙苏南 带你再谈Mock数据之easy-mock" }:{} }}深入浅出 - 在线调试再优秀的工程师写出的代码也一样要测试,没有人敢说自己的代码无Bug,Easy Mock 它是真的懂你的,已经为你准备好了,接口编写好后,立马就能让你测试,是不是觉得很棒棒呢??如果是你自己本地写mock数据,你需要重启服务、手动写参数、可能还需要整个测试页,知道你已经非常饥渴迫切的想要知道,具体的调试方式了:看不清吗??已经为你备好在线调试链接,支持POST、GET、PUT等方式,因gif图加载比较大,就不一一演示了结尾: 天下无不散之宴席,又到说再见的时候了,以上就是今天苏南为大家带来的分享,您GET到了吗?Easy Mock更多强大之处自己去折腾吧,#用心分享 做有温度的攻城狮#,希望今天的分享能给您带来些许成长,如果觉得不错记得点个赞哦,,顺便关注下方公众号就更棒了呢,每周为您推最新分享????????。更多文章:immutability因React官方出镜之使用总结分享!小程序项目之做完项目老板给我加了6k薪资~小程序项目之填坑小记面试踩过的坑,都在这里了~你应该做的前端性能优化之总结大全!如何给localStorage设置一个过期时间?动画一点点 - 如何用CSS3画出懂你的3D魔方?动画一点点 - 手把手教你如何绘制一辆会跑车SVG Sprites Icon的使用技巧作者:苏南 - 首席填坑官链接:https://blog.csdn.net/weixin…交流群:912594095、公众号:honeyBadger8本文原创,著作权归作者所有。商业转载请联系@IT·平头哥联盟获得授权,非商业转载请注明原链接及出处。 ...

November 7, 2018 · 1 min · jiezi

小程序项目之填坑小记

作者:首席填坑官∙苏南公众号:honeyBadger8,本文原创,著作权归作者所有,转载请注明原链接及出处。简诉 是的,真的,你没有看错,我就是上次那个加薪的,但是现在问题来了,最近又搞了个小程序的需求,又填了不少坑,其中的辛酸就不说了,说多了都是泪????????,此处省略三千字 ………^……,说重点吧,反正最后就是差点这让老板叫走人了,你说优秀不优秀~。 前段时间网上一直说的“<u>你可以骂那些中年人,尤其是有车有房的……</u>”,虽然我没有房、也没有车,但也坚决不做那个可以随便骂的中年人(人到中年不如狗??),不存在的啦,这个仇宝宝已经记下了????,先分享一下最近遇到的几个坑吧。 —— 我是首席填坑官——苏南,早上好,新的一周加油。填坑一,canvas遮挡问题:随着小程序的API调整,很多东西都要用户手动授权,不能直接调用后,toast、弹窗这种提示的场景越来越多了,下图就是公司活动的canvas合成,现在微信API不让直接调用授权了,某些场景要多一个弹窗来提示用户开启设置,但当遇上canvas API这个大佬后,一切都变了,谁都只能站在它后面,场景一 :如之前拒绝授权了,后续引导用户打开设置页,即 wx.openSetting,下图就是:坑一 小结 :当遇上这种情况,我的第一思路是 设置样式:visibility: hidden;opacity:0;,但是结果是让人失望的,canvas 大佬就是大佬,这两属性在手机上失效了,该显示还是显示,你阻挡不了它的光辉,真的,不信可以去测试!解决思路:canvas 图片合成,获取到图片的地址后,隐藏canvas,改用image标签显示,这种场景有局限性,如果你的需求是echart交互的,显示挂了;cover-view 貌似也是有局限,<cover-view /> 内只能嵌套 <cover-view /> 和 <cover-image />,view 标签的子节点树在真机上都会被忽略,这是我测试后的浏览器给出的警告,如果自定modal,要加button按钮让用户点击后授权某功能,肯定也就挂了 ;当弹窗出现的时候,隐藏canvas,这种比较暴力,但覆盖面广,任何场景都能照顾到,却也影响体验;把canvas定位移动到屏幕之外绘制内容;有同学可能说直接使用原生的 wx.showModal,但官方目前,button还不支持设置open-type属性;等微信小程序官方修复????,好吧,看到这里你肯定笑了~,这不是一个方法,估计还没等到老板真叫你走人了,欢迎大佬们补充!!!填坑二,Maximum call stack size exceeded发现这个bug,要从最近换了个手机说起,用了3年的5S终于歇菜了(再也买不起iphone了~),换了个android vivo x23, 以为从此走上人生巅峰了,现实却给了我一个响亮的耳光,又是一个记忆深刻的梗~,被组里的同事笑话好久!!话说,堆栈溢出,是怎么造成的呢?—— 循环引用;同时我又有些疑惑了,为什么其他手机都正常,就vivo 报了这个错,同样的代码,希望有大神看到能给于解惑!先来看个示例,简单演示一下 :let sum = 20; (function test(){ sum–; console.log(sum); test(); /* if( sum > 0 ){ test(); }*/ })()# 而项目中的报错是这样的 #: //fetch.js import wepy from ‘wepy’ import login from ‘./login’; ……省略N行 //login.js import {fetch} from “./fetch.js”; import Storage from “./storage.js”; ……省略N行 //更改后 login.js ,避免了循环引用 loginFn = ()=>{ require("./fetch.js").fetch({ url:"/wabApi/login/login", }); }坑二 小结 :循环引用,可以理解为 a.js内调用了b.js,b.js里又引用了a.js,所以在项目开发中要注意一下,看了下网上的讨论,这个问题需要等官方解决,貌似h5里是可以这样写的。填坑三,canvasGetImageData、canvasToTempFilePath这两个方法,之间的调用,要做一定的延时,不明白是为什么,如果不做延时,也不会报错,也不提示,方法执行完,canvas上还是空白的;但是让人尴尬的是,此在写总结的我,又验证了一下,不加setTimeout,调试器上可以,真机挂了!目测跟绘制的目标对象大小有关系!其他微信API的调整,如:「 wx.getUserInfo」「 wx.getSetting」「 wx.openSetting」「 wx.getPhoneNumber」等这些现在需要添加按钮,用户手动来点击,带来的不便大家都知道了,就不再多说;字体文件 ,加载报错,但也能正常显示,而且只有第一次报错哦;其他还有待发现的坑…… @font-face { font-family: ’test’; src: url(“https://cdn-xx.xxx.com/common/font/font.ttf") format(’truetype’); font-weight: normal; font-style: normal; }扯淡段子 小明公司之前上线的小程序项目,好久没有迭代了,产品说有个需求要改一下,很快,就一点点东西,比如一个按钮UI调整一下,改了赶紧发上去,嗯,最好今天就发了审核吧; 这话,是你会怎么接呢??小明说要一天,产品就惊呆了????,这家伙没有发烧吧??小明后来经过半天的努力,终于让产品知道了小程序API更新后,再发布的相关流程都要改的; 有谁能理解小明的痛苦?有谁能理解小程序的API更新机制?更新过的API没有向下兼容的余地,已经发布过的就放过你了,但是你再改动,所有它改过的流程,你都要改一遍。结尾 开心一笑,给自己找点乐,为今天的分享画上圆满的句号,以上就是我最近的一次小小填坑记整理,希望能给其他同学带来些许帮助,文中如有理解不足之处,请指正????。作者:苏南 - 首席填坑官链接:https://blog.csdn.net/weixin_…交流群:912594095、公众号:honeyBadger8本文原创,著作权归作者所有。商业转载请联系@IT·平头哥联盟获得授权,非商业转载请注明原链接及出处。 ...

November 5, 2018 · 1 min · jiezi

immutability因React官方出镜之使用总结分享!

引言 之前项目中遇到数据拷贝、引用之间数据层级嵌套过深,拷贝的值相互之间影响的问题,后来引入了immutability-helper,使用过程中的一些总结,跟大家分享下,至于为什么不是immutable,请看下文分解,这里是@IT·平头哥联盟,我是首席填坑官——苏南。 相信大家在面试/工作中都遇到过js对象/数组的拷贝问题,面试官问你,你一般怎么做??在现在ES6盛行的当下,不会一点ES6都不好意思说自己是前端(其实我一般都说自己是攻城狮、切图崽????),我们想的大多第一想法,如下:Object.assign - 最方便;[…] - 最有逼格;JSON.parse、JSON.stringify - 完美组合;$.extend() - jQuery时代的引领潮流时尚前沿的API;最后想到的才是自己递归实现一个; 但是通常我们使用的Object.assign属于浅拷贝,当数据嵌套层级较深时,就……呵呵了;而JSON.parse、stringify它应该是创建一个临时可能很大的字符串,然后又访问解析器,性能是比较慢的。于是后来发现了 immutable「不可变数据」,曾经我也一度特别喜欢它,但时间久了,慢慢发现,它过于有个性了些、凡事都都没有任何商量的余地,所有的数据,从创建、变更、插入、删除等操作,都要按它的套路来,对于我这种一生放荡不羁爱自由的人来说,长时间的约束,是不能忍的;都说两人如果三观不合,是无法长久下去的,可能也是缘份吧,在后来的某一天偶然的闲逛中邂逅了新欢 ————Immutability Helpers。 嗯,今天的主题就是给大家分享一下,Immutability Helpers的一些用法,会介绍API的使用操作和小技巧,如有不理解不对,请纠正: 太兴奋了,差点忘了,补充一下,一个简单的拷贝: //实现一个简单的递归数据拷贝 let customClone = (rawObj)=>{ let copyObj = {}; for (var key in rawObj) { if( typeof rawObj[key] === ‘object’ && Object.prototype.toString.call(rawObj[key]) !== ‘[object Array]’){ copyObj[key] = customClone(rawObj[key]); }else{ copyObj[key] = rawObj[key]; }; }; return copyObj; }; let objA = {“name”:“苏南”,“sex”:“男”,“height”:“176”}; let objB = customClone(objA); objB.signature = “宝剑锋从磨砺出,梅花香自苦寒来,做有温度的攻城狮”; console.log(objA); console.log(objB);补充一个 Object.assign 的坑 : let data = { a:1, b:2, children:{ name:“苏南”, organization:"@IT·平头哥联盟", job:“首席填坑官”, address:“ShenZhen”, age:18 } }; let data2 = Object.assign({},data); data2.children.age = 28; data2.children.job = “首席甩锅官”; data2.b = 666; console.log(“我是原始数据 data:",data); console.log(“我是复制后的数据 data2:",data2);immutable 最后的一次回顾 都说有了新欢,忘了旧爱,但我不是那种无情无义的人,最后正式介绍一下 immutable,为我俩的……画上一个圆满的句号: 再次强调,并不是觉得immutable不好,不够强大,只是自己个人观点,有些不喜欢而已,各位immutable粉勿喷,想了解更多的同学可以点击这里Immutable data encourages pure functions (data-in, data-out) and lends itself to much simpler application development and enabling techniques from functional programming such as lazy evaluation.使用示例: const list1 = List([ 1, 2, 3 ]); const list2 = List([ 4, 5, 6 ]); const array = [ 7, 8, 9 ]; const list3 = list1.concat(list2, array); console.log(list3) // List {size: 9, _origin: 0, _capacity: 9, _level: 5, _root: null, …} 是不能直接获取到数据的,须使用get,– list3.get(0) let data = fromJS({ obj:{} }); let data1 = { a:1, b:2, children:{ name:“苏南”, } }; let data2 = data.mergeIn([‘obj’],data1,{c:666}); console.log(“获取的数据:",data2.getIn([‘obj’,‘c’])); console.log(“这里是由formJS创建的数据:",data2.getIn([‘obj’,‘children’,’name’]));//使用immutable后,所有数据都要类似选择器,一个一个往下选择,并不是说它不好、功能不够强大,只是自己有些不喜欢它类似JQuery选择器一样的语法,get、getIn、set、List等的使用方式,当然它也是可以使用 toJS方法转回来的。Immutability Helpers出场gitHub上它对自己的介绍很简单:Mutate a copy of data without changing the original source —— 在不更改原始源的情况下改变数据副本。 与它结缘,是因为它在react官方文档中出镜,而被我所宠幸,真的 ,只是因为在人群中多看了它一眼再也没能忘掉, 它跟immutable不一样,不会有那么多条条框框约束你,给你自由、给你独立的空间、给你独立的思想,让你想用即用、用之即走~~(泥马,怎么有点像张小龙说它的小程序一样????),但您放心,它的坑真的比小程序少,API也很简洁,接下来来看一下,它的基本用法:$push —— 数组;$unshift —— 数组;$splice —— 数组;$set —— 替换/覆盖/合并原数据;$toggle —— array of strings ,toggles a list of boolean fields from the target object;$unset —— remove the list of keys in array from the target object;$merge —— 合并对象;$apply —— passes in the current value to the function and updates it with the new returned value;$add —— 新增;$remove —— 删除。以上基本就是它全部的API了,下面一起来看看,具体用法吧:$push 的使用 :看名字就知道它的作用了啦,跟原生的push一样,不过写法有一点点不一样; let arr = [1,2,3,4,5,66]; let arr2 = update(arr,{ $push : [“a”,“b”,“c”], //一定要 []号的形式哦,不可以 “a”; [4]:{ // !!index ,可以指定修改下标的值 $set:“我是替换过的” } }); console.log(arr2);$unshift 的使用 :一样,跟原生的unshift,在原数组开头处插入,同样写法是以一个数组的形式; let arr = [1,2,3,4,5,66]; let arr2 = update(arr,{ $unshift : [“a”,“b”,“c”], [4]:{ $set:“我是首席填坑官∙苏南” //这里需要注意,它的操作是在 unshift之前执行的,也就是在原 arr 上查找 第4个下标 } }); console.log(“原始数组”,arr);// [1, 2, 3, 4, 5, 66] 相互之间并不会影响 console.log(arr2); //[“a”, “b”, “c”, 1, 2, 3, 4, “我是首席填坑官∙苏南”, 66]$splice 的使用 :注意 :数组套数组,start,end, 插入的数据……,; let arr = [1,2,3,4,5,66]; let arr2 = update(arr,{ $splice : [[1,2,[66788,99],{a:123,b:“苏南”}]], // or [0,1,“从我开始是插入的内容”,88,89,90,“后面可以很多,是数组、对象、字符串都行”] }); console.log(arr2); //复杂一些的用法: let obj={ name:“immutable”, list :[1,2,[90,55,44,3,22,55],3,4,6,7,8] }; let obj2 = update(obj,{ list:{ [2]:value=>update(value,{ $splice:[[0,2]] // [90,55,44,3,22,55] => [44, 3, 22, 55] }) } });$set 的使用 :上面已经演示过了,其实有点替换的意思,当有重复的值时,就会覆盖,没有就新增,来展示复杂一点的场景,层级深的数据,也不会相互影响; let obj={ name:“immutable”, children:{ address:“ShenZhen”, hobby:"@IT·平头哥联盟-前端开发” } }; let obj2 = update(obj,{ $set : {name:“immutability-helper”,other:“其他字段,如微信公众号:honeyBadger8,每周为你带来最新分享”} }); let obj3 = update(obj,{ name:{ $set : “苏南” }, children:{ hobby:{ $set:“首席填坑官 - javascript” } } }); console.log(“原始数据:",obj); console.log(“obj2:",obj2); console.log(“obj3”,obj3); $toggle 的使用:听名字,应该就能猜出来,开关切换的意思;Boolean 布尔值的切换,如果你是强制要 Number 类型 的 0、1,那么使用引方法的时候就要注意了; let obj={ name:“immutable”, a:false, b:true, c:1, d:0 }; let obj2 = update(obj,{ $toggle:[‘b’,‘a’,“c”,“d”], }); console.log(“原始数据:",obj); console.log(“obj2:",obj2);$unset 的使用:它跟$set相反,有点remove的味道,但又貌似有不同的之处,当操作的对象为object时key是删除了;而数组array中它的值没有了,却保留了下标,不改变数组的长度,删除数组建议还是用$splice;请看下图: let arr = [1,2,3,4,5,6]; let obj={ name:“immutable”, children:{ address:“ShenZhen”, hobby:“写博客” } }; let obj2 = update(obj,{ $unset : [“name”], children:{ $unset:[“address”] } }); console.log(“原始数据:",obj); console.log(“obj2:",obj2); let arr2 = update(arr,{ $unset : [1] }); console.log(“arr2:",arr2,arr2.length);$merge 的使用:$merge 跟我们最爱的Object.assign一样,做合并操作的,但它比assign优秀很多,深层次拷贝,不会相互影响 : let arr = [1,2,3,4,5,6]; let obj={ name:“immutable”, children:{ address:“ShenZhen”, hobby:“写博客”, array:[“我不是程序员”,“切图崽了解一下”], } }; let obj2 = update(obj,{ $merge:{ arr }, children:{ array:{ $merge:{items:[“从前有坐山”,“山里有个庙”]}, $splice:[[3,0,“住着一个小和尚”]] } } }); console.log(“原始数据:",obj); console.log(“obj2:",obj2);$apply 的使用:$apply 基于当前值进行一个函数运算,从而得到新的值 :注意 :它必须是一个 function 哦! let obj={ name:“immutable”, children:{ items:[“从前有一坐山”], array: [1,2,3,4,5,6], } }; let obj2 = update(obj,{ name:{ $apply:(val)=>(“首席填坑官”) }, children:{ items:{ $apply:(val)=>{ console.log(“旧值”,val); return [3,0,“住着一个小和尚”] } }, array:{ $apply:(val)=>(val.reverse()) //必须是一个函数 } } }); console.log(“原始数据:",obj); console.log(“obj2:",obj2);$remove 的使用:$remove 一定一定 要是使用Set、Map 创建的数组:要删除的值,必须是数组成存在的,如值不存在则忽略,$remove:[2,666],2会删除,6则会被忽略;这个api有点奇怪,正常普通的数组 [],这样的删除不了!!;常见错误如下图: let obj={ name:“immutable”, children:{ array:new Set([1, 2, 3, 4, 4]), } }; let obj2 = update(obj,{ children:{ array:{ $remove:[2], }, } }); console.log(“原始数据:",obj); console.log(“obj2:",obj2);$add 的使用:$add 跟刚才的 $remove 一样要使用Map/Set,$add方法也跟 es6 Map/Set的 add方法一致:只是写的时候也要注意一些, [ [] ] ,嵌套! let obj={ name:“immutable”, array:new Map([[“a”,1],[“b”,2]]), }; let obj2 = update(obj,{ array:{ $add:[[“66”,56]], }, }); console.log(“原始数据:",obj); console.log(“obj2:",obj2); console.log(“获取key a:",obj2.array.get(‘a’));Immutability Helpers的高阶用法:还可以自定义方法,如 定义一个 $trinocular 方法,来判断数组中的值;只是一个简单的示例,更多复杂的用法,可以自己去探索哦 去官方 github ???? update.extend(’$trinocular’, function(proportion, original) { return original > 88 ? (original/proportion ): (proportion+original); }); let array =[56,33,55,777,322,444,61,12,34,52,245]; let array2 = array.map((k,v)=>update(k,{ $trinocular:2 })) console.log(“原始数据:",array); console.log(“array2:",array2);总结/结尾: 以上就是基础 API 的用法 ,添加了一些官方示例,没有讲到的组合使用,以及使用过程中,可能出现的一些错误,需要留意的地方,更多定制高级用法,有兴趣的同学可以自行了解一下。 以上就是今天为大家带来的分享,它可能没有 immutable 那么多功能,但贵在简洁,不会有太多的约束,如理解有误之处,欢迎各位大佬纠正,毕竟我还只是个宝宝——新手上路中!????。 下方是我弄的一个公众号,欢迎关注,以后文章会第一时间,在公众号上更新,原因是之前分享的有两篇文章,竟然被其他公众号抄袭了????,前些天去更新发表的时候,微信提示我文章已经不是原创了检测到相同的文章,宝宝心里那个凉啊~,果断申诉告了对方(是一个培训学校公众号,好气哦),补了掘金发布的链接和截图日期,万幸最后胜诉了????!????????更多文章:做完小程序项目、老板给我加了6k薪资~面试踩过的坑,都在这里了~你应该做的前端性能优化之总结大全!如何给localStorage设置一个过期时间?手把手教你如何绘制一辆会跑车如何用CSS3画出懂你的3D魔方?SVG Sprites Icon的使用技巧作者:苏南 - 首席填坑官链接:https://honeybadger8.github.i…交流:912594095、公众号:honeyBadger8本文原创,著作权归作者所有。商业转载请联系@IT·平头哥联盟获得授权,非商业转载请注明原链接及出处。 ...

November 4, 2018 · 4 min · jiezi

几个非常实用的JQuery代码片段

jQuery是一个兼容多浏览器的javascript库,核心理念是write less,do more(写得更少,做得更多)。jQuery使用户能更方便地处理HTML(标准通用标记语言下的一个应用)、events、实现动画效果,并且方便地为网站提供AJAX交互。jQuery还有一个比较大的优势是,它的文档说明很全,而且各种应用也说得很详细,同时还有许多成熟的插件可供选择。jQuery能够使用户的html页面保持代码和html内容分离,也就是说,不用再在html里面插入一堆js来调用命令了,只需要定义id即可,jQuery已经成为最流行的javascript库,下面给大家推荐几款常用的JQuery代码。1、管理搜索框的值现在各大网站都有搜索框,而搜索框通常都有默认值,当输入框获取焦点时,默认值消失。而一旦输入框失去焦点,而输入框里又没有输入新的值,输入框里的值又会恢复成默认值,如果往输入框里输入了新值,则输入框的值为新输入的值。这种特效用JQuery很容易实现:$("#searchbox") .focus(function(){$(this).val(’’)}) .blur(function(){ var $this = $(this); // ‘请搜索…‘为搜索框默认值 ($this.val() === ‘’)? $this.val(‘请搜索…’) : null; });2、反序访问JQuery对象里的元素在某些场景下,我们可能需要反序访问通过JQuery选择器获取到的页面元素对象,这个怎么实现呢?看下面代码://要掌握JQuery对象的get方法 以及数组的reverse方法即可var arr = $(’#nav’).find(’li’).get().reverse();$.each(arr,function(index,ele){ …. … });3、克隆table header到表格的最下面为了让table具有更好的可读性,我们可以将表格的header信息克隆一份到表格的底部,这种特效通过JQuery就很容易实现:var $tfoot = $(’<tfoot></tfoot>’); $($(’thead’).clone(true, true).children().get().reverse()).each(function(){ $tfoot.append($(this));});$tfoot.insertAfter(’table thead’);4、使用JQuery重绘图片的大小关于图片大小的重绘,你可以在服务端来实现,也可以通过JQuery在客户端实现。$(window).bind(“load”, function() { // IMAGE RESIZE $(’#product_cat_list img’).each(function() { var maxWidth = 120; var maxHeight = 120; var ratio = 0; var width = $(this).width(); var height = $(this).height(); if(width > maxWidth){ ratio = maxWidth / width; $(this).css(“width”, maxWidth); $(this).css(“height”, height * ratio); height = height * ratio; } var width = $(this).width(); var height = $(this).height(); if(height > maxHeight){ ratio = maxHeight / height; $(this).css(“height”, maxHeight); $(this).css(“width”, width * ratio); width = width * ratio; } }); //$("#contentpage img").show(); // IMAGE RESIZE});5、滚动时动态加载页面内容有些网站的网页内容不是一次性加载完毕的,而是在鼠标向下滚动时动态加载的,这是怎么做到的呢?看下面代码:var loading = false;$(window).scroll(function(){ if((($(window).scrollTop()+$(window).height())+250)>=$(document).height()){ if(loading == false){ loading = true; $(’#loadingbar’).css(“display”,“block”); $.get(“load.php?start="+$(’#loaded_max’).val(), function(loaded){ $(‘body’).append(loaded); $(’#loaded_max’).val(parseInt($(’#loaded_max’).val())+50); $(’#loadingbar’).css(“display”,“none”); loading = false; }); } }});$(document).ready(function() { $(’#loaded_max’).val(50);}); ...

November 3, 2018 · 1 min · jiezi

你可能不知道的14个JavaScript调试技巧

了解你的工具可以在完成任务的过程中发挥重大作用。尽管传言 JavaScript 难以调试,但是如果你掌握了一些调试技巧,那么你将会花费更少的时间来解决这些错误。我们已经列出了14个你可能不知道的调试技巧,但可能要记住,这样下次你需要调试 JavaScript 代码时就可以马上使用了!现在就马上开始。1. ‘debugger;’除了 console.log , debugger; 是我们最喜欢、快速且肮脏的调试工具。一旦执行到这行代码,Chrome 会在执行时自动停止。 你甚至可以使用条件语句加上判断,这样可以只在你需要的时候运行。愚人码头注:本人实在觉得这种调试方面很不好,因为后续的调试步骤和断点调试没什么区别。而且调试完成后,还要记住删掉这行代码。确实有点肮脏。JavaScript 代码:if (thisThing) {debugger;}2. 将 objects 显示为表格有时,你有一个复杂的对象要查看。你可以用 console.log 查看并滚动浏览该对象,或者使用console.table展开,更容易看到正在处理的内容!JavaScript 代码:var animals = [{ animal: ‘Horse’, name: ‘Henry’, age: 43 },{ animal: ‘Dog’, name: ‘Fred’, age: 13 },{ animal: ‘Cat’, name: ‘Frodo’, age: 18 }];console.table(animals);输出:3. 尝试所有的屏幕尺寸虽然在桌面设备上安装不同移动设备模拟器非常棒,但在现实世界中并不可行。 应该是调整你的可视窗口,而不是替换移动设备? Chrome为你提供所需的一切。 进入Chrome 开发者调试工具,然后点击 ‘toggle device mode(切换设备模式)’ 按钮。 实时观察窗口变化即可!4. 如何快速找到DOM元素在 Elements(元素) 面板中标记 DOM 元素,并可以在 console(控制台) 中使用它。Chrome 检测器会保留其历史记录中的最后 5 个元素,以便最终标记的元素显示 $0 ,倒数第二个标记元素 $1 ,依此类推。如果你按照“item-4”,“item-3”,“item-2”,“item-1”,“item-0”的顺序标记下列项,则可以在控制台中像这样访问DOM节点:5. 使用 console.time() 和 console.timeEnd() 来标记循环耗时要确切地知道某段代码需要执行多长时间,尤其是在调试慢循环时,可能会非常有用。您甚至可以通过为该方法分配标签来设置多个定时器。让我们看看它是如何工作的:JavaScript 代码:console.time(‘Timer1’);var items = [];for(var i = 0; i < 100000; i++){items.push({index: i});}console.timeEnd(‘Timer1’);运行产生了如下结果:6. 获取函数的堆栈跟踪信息你可能知道JavaScript框架,会引入大量代码。它创建视图触发事件,而且你最终会想知道函数调用是怎么发生的。因为 JavaScript 不是一个很结构化的语言,有时候很难完整的了解到底 发生了什么 以及 什么时候发生 的。 使用 console.trace((仅仅只是在控制台中跟踪) 可以方便地调试JavaScript 。假设你现在想看 car 实例在第24行调用 funcZ 函数的完整堆栈轨迹信息:JavaScript 代码:var car; var func1 = function() {func2();} var func2 = function() {func4();}var func3 = function() {} var func4 = function() {car = new Car();car.funcX();}var Car = function() {this.brand = ‘volvo’;this.color = ‘red’;this.funcX = function() {this.funcY();}this.funcY = function() {this.funcZ();}this.funcZ = function() {24行将输出:现在我们可以看到 func1 调用 func2, func2 调用 func4。 Func4 创建了一个 Car 的实例,然后调用函数 car.funcX,依此类推。即使你认为非常了解自己的代码,这种分析仍然可以让你感到很方便。假如你想改进你的代码。获取跟踪信息和所有涉及的函数名单,每一项都可以点击,你可以在他们之间来回切换。这就像一个特地为你准备的菜单。7. 美化代码使调试 JavaScript 变得简单有时你可能在生产环境中遇到问题,但是你的source maps没有部署在服务器上。 不要害怕 。Chrome 可以将你的 Javascript 文件美化为更易阅读的格式。虽然代码不会像你的真实代码那样有用 – 但至少你可以看到发生了什么。点击检查器中源代码查看器下方的 {} 美化按钮即可。8. 快速查找要调试的函数假设你想在一个函数中设置一个断点。最常见的两种方法是:1.在源代码查看器查找到相应的行,并添加一个断点2.在代码中添加debugger在这两个解决方案中,您必须在文件中单击以调试特定行。使用控制台打断点可能不太常见。在控制台中使用 debug(funcName),当到达传入的函数时,代码将停止。这个调试方法很快, 但缺点是不适用于私有函数或匿名函数。但除了私有和匿名函数, 这可能是找到调试函数的最快方法。(注意:这个函数和console.debug 函数是不同的东西。)JavaScript 代码:var func1 = function() {func2();};var Car = function() {this.funcX = function() {this.funcY();}this.funcY = function() {this.funcZ();}}var car = new Car();在控制台中输入 debug(car.funcY) ,当调用 car.funcY 时,脚本将以调试模式停止:9. 屏蔽不相关的代码现在,我们经常在应用中引入多个库或框架。其中大多数都经过良好的测试且相对没有陷阱。 但是,调试器仍然会进入与调试任务无关的文件。解决方案是屏蔽不需要调试的脚本。当然也可以包括你自己的脚本。10. 在复杂的调试过程中寻找重点在更复杂的调试中,我们有时希望输出很多行。你可以做的事情就是使用更多控制台函数来保持良好的输出结构,例如, console.log, console.debug, console.warn, console.info, console.error等等。然后,可以在控制台中快速浏览。但有时候,某些 JavaScrip 调试信息并不是你需要的。现在,可以自己美化调试信息了。在调试 JavaScript 时,可以使用 CSS 并自定义控制台信息:JavaScript 代码:console.todo = function(msg) {console.log(‘ % c % s % s % s‘, ‘color: yellow; background - color: black;’, ‘–‘, msg, ‘–‘);}console.important = function(msg) {console.log(‘ % c % s % s % s’, ‘color: brown; font - weight: bold; text - decoration: underline;’, ‘–‘, msg, ‘–‘);}console.todo(“This is something that’ s need to be fixed”);console.important(‘This is an important message’);输出:例如:在 console.log() 中, 可以用 %s 设置字符串,%i 设置数字,%c 设置自定义样式等等,还有很多更好的 console.log() 使用方法。 如果使用的是单页应用框架,可以为视图(view)消息创建一个样式,为模型(models),集合(collections),控制器(controllers)等创建另一个样式。也许还可以像 wlog,clog 和 mlog 一样发挥想象力!11. 观察特定函数的调用及其参数在 Chrome 控制台中,您可以关注特定的函数。 每次调用该函数时,都会对传入的参数值进行记录。JavaScript 代码:var func1 = function(x, y, z) {//….};输出:这是查看哪些参数传递给函数的好方法。 但是我必须说,如果控制台可以告诉我们需要多少参数,那将是一件好事。 在上面的例子中,func1 期望 3个参数,但是只有 2 个参数被传入。如果在代码中没有处理这个参数,它可能导致一个可能的 bug 。12. 在控制台中快速访问元素在控制台中执行 querySelector 一种更快的方法是使用美元符。$(‘css-selector’) 将会返回CSS选择器的第一个匹配项。$$(‘css-selector’) 将会返回所有匹配项。如果多次使用一个元素,可以把它保存为一个变量。13. Postman 很棒(但Firefox更快)许多开发人员使用 Postman 查看ajax请求。Postman真的很优秀。但打开一个新的浏览器窗口,新写一个请求对象来测试,这确实显得很麻烦。有时使用浏览器更容易。当你使用浏览器查看时,如果请求一个密码验证页面,你不需要担心身份验证的cookie。下面看,在Firefox中如何编辑并重新发送请求。打开检查员并转到网络选项卡。 右键单击所需的请求,然后选择编辑并重新发送。 现在你可以改变任何你想要的。 更改标题并编辑您的参数并点击重新发送。下面我用不同的属性提出两次请求:14. 节点变化时中断DOM 是一个有趣的东西。 有时候它会被修改,但是你并不知道为什么。 但是,当您需要调试 JavaScript 时,Chrome会让您在DOM元素发生更改时暂停。 你甚至可以监视它的属性。 在Chrome 检查器中,右键单击该元素,然后在设置中选择一个中断就可以了:这里推荐一下我的前端学习交流群:784783012,自己整理了一份2018最全面前端学习资料,从最基础的HTML+CSS+JS【炫酷特效,游戏,插件封装,设计模式】到移动端HTML5的项目实战的学习资料都有整理,送给每一位前端小伙伴 ...

October 30, 2018 · 2 min · jiezi

如何用CSS3画出懂你的3D魔方?

作者:首席填坑官∙苏南公众号:honeyBadger8,群:912594095,本文原创,著作权归作者所有,转载请注明原链接及出处。前言 最近在写《每周动画点点系列》文章,上一期分享了< 手把手教你如何绘制一辆会跑车 >,本期给大家带来是结合CSS3画出来的一个立体3d魔方,结合了js让你随心所欲想怎么转,就怎么转,这里是 @IT·平头哥联盟,我是首席填坑官∙苏南(South·Su),我们先来看看效果,然后再分解它的实现过程吧绘制过程: 好吧,gif图看着好像有点不是很清晰,想在线预览的同学,可点击在线预览 ????,废话不多扯了,先来分析一下,看如何实现这个功能吧。∙ API预热 :本次示例是一个立体的正方形,既然有立体效果,肯定少不了CSS3中的 -webkit-perspective-透视、preserve-3d-三维空间,这个两个是重点哦,当然还有transform-origin、transition、transform等,先来回故一下 API 怎么是讲的吧:perspective 取值 :none :不指定透视 ;length :指定观察者与「z=0」平面的距离,使具有三维位置变换的元素产生透视效果。「z>0」的三维元素比正常大,而「z<0」时则比正常小,大小程度由该属性的值决定,不允许负值。transform-style 取值 :flat :指定子元素位于此元素所在平面内;preserve-3d :指定子元素定位在三维空间内,当该属性值为 preserve-3d时,元素将会创建局部堆叠上下文;小结 :决定一个变换元素看起来是处在三维空间还是平面内,需要该元素的父元素上定义 <’ transform-style ‘> 属性,也就是说想某元素有三维效果,需要设定它的父级有 preserve-3d 。transform-origin 取值 :percentage:用百分比指定坐标值。可以为负值;length:用长度值指定坐标值。可以为负值;left:指定原点的横坐标为left;center①:指定原点的横坐标为center;right:指定原点的横坐标为right;top:指定原点的纵坐标为top;center②:指定原点的纵坐标为center;bottom:指定原点的纵坐标为bottom;transform、transition等,就不介绍了/* perspective 使用示例:*/div{ -webkit-perspective:600px; perspective:600px;}/transform-style 使用示例:/.preserve{ transform-style:preserve-3d; -webkit-transform-style:preserve-3d;} /transform-origin 使用示例:/.preserve{ -webkit-transform-origin:50% 50% -100px; or -webkit-transform-origin:bottom; or -webkit-transform-origin:top; …………} ∙ 绘制6个面 :是的,我没有说错,就是6个面:上、正面、下、背面、左、右,上面API讲了这么多,来实践试一下吧,写6个div,结构大概是这样的,也是接下来的魔方需要的结构:<div class=“cube”> <div class=“cube-inner running”> <p class=“single-side s1”><span>最</span></p> <p class=“single-side s2”><span>懂</span></p> <p class=“single-side s3”><span>你</span></p> <p class=“single-side s4”><span>的</span></p> <p class=“single-side s5”><span>魔</span></p> <p class=“single-side s6”><span>方</span></p> </div></div>!!!发生了什么??是不是很吃惊??说好的值越大,透视效果越强的呢?后面明明藏了个妹子,怎么看没有透视出来?开始我也是跟你一样吃惊的,但瞬间就悟透了,少了rotate,加个它再来看看效果吧:.cube{ width:200px; height:200px; margin:10px auto; padding:260px; position:relative; -webkit-perspective:600px; perspective:600px; transition: .5s ;}.cube-inner{ width:200px; height:200px; position:relative; -webkit-transform-style:preserve-3d; transition:.3s; -webkit-transform-origin:50% 50% -100px; transform: rotateX(45deg);}.cube:hover{ /鼠标经过时,把 perspective 过渡到100 / -webkit-perspective:100px; perspective:100px;}既然API有效,那么拉下来我们就画出6个面吧,按:上、正面、下、背面、左、右,这个顺序来设置吧;首先,我们要指定它们是在三维空间内的preserve-3d,也就是6个面的父级要设置 transform-style 样式;以上都设置好后,再来看看6个面吧,为了便于区分,给它们每个都设置了不同颜色(用了css3的渐变 radial-gradient)——不想手写的同学推荐一个网站可在线设置你要的效果,复制样式即可,先来一睹风采,为了便于观察,整体角度旋转了10deg:说到渐变,偶然之间发现了一个有意思的东西hue-rotate,它能在你初始的颜色基础上旋转元素的色调及其内容,从而达到不同的效果。了解更多hue-rotate : The hue-rotate() CSS function rotates the hue of an element and its contents. Its result is a <filter-function>. 上 - “最”:.cube-inner .single-side.s1{ /s1顶部/ left:0;top:-200px; background: radial-gradient(circle, rgba(255,255,255,.88), #00adff); background: -webkit-radial-gradient(circle, rgba(255,255,255,.88), #00adff); transform-origin:bottom; -webkit-transform-origin:bottom; transform:rotateX(90deg); -webkit-transform:rotateX(90deg);} 正面 - “懂”:下面就是默认的,什么都不用设置,所以就不展示了 ; 下面 - “你”:即底部,底部的设置,正好跟顶部它是相反的,一个origin 以 bottom为基准为坐标,一个以top为基准为坐标;.cube-inner .single-side.s3{ /s3底部/ left:0;top:200px; background: radial-gradient(circle, rgba(255,255,255,.88), #100067); background: -webkit-radial-gradient(circle, rgba(255,255,255,.88), #100067); transform-origin:top; -webkit-transform-origin:top; transform:rotateX(-90deg); -webkit-transform:rotateX(-90deg);} 背面 - “的”:即正面的后边,整体旋转了 135deg,让背面更直观能看到;translateZ 、rotateX 同时移动,形成透视的关系,让它看起来,在正面面的后面;下图二,把默认的正面,设置了透明度,可以看出,背面的透视效果;.cube-inner .single-side.s4{ /s4背部/ z-index:2; left:0;top:0; background: radial-gradient(circle, rgba(255,255,255,.88), #F0C); background: -webkit-radial-gradient(circle, rgba(255,255,255,.88), #F0C); transform:translateZ(-200px) rotateX(180deg) ; -webkit-transform:translateZ(-200px) rotateX(180deg) ; /rotateZ(-180deg) 左右旋转的时候,Z轴旋转180°,因为字是倒着的/} 左侧面 - “魔”:origin以right为基准,left负元素的宽度,rotateY轴旋转90deg;.cube-inner .single-side.s5{ /s5左侧/ left:-200px;top:0; background: radial-gradient(circle, rgba(255,255,255,.88),rgba(33,33,33,1)); background: -webkit-radial-gradient(circle, rgba(255,255,255,.88),rgba(33,33,33,1)); transform-origin:right; -webkit-transform-origin:right; transform:rotateY(-90deg) -webkit-transform:rotateY(-90deg)} 右侧面 - “方”:同理右侧,与左侧正好相反;.cube-inner .single-side.s6{ /s6右侧/ right:-200px;top:0; transform-origin:left; -webkit-transform-origin:left; background: radial-gradient(circle, rgba(255,255,255,.88), #f00); background: -webkit-radial-gradient(circle, rgba(255,255,255,.88), #f00); transform:rotateY(90deg); -webkit-transform:rotateY(90deg);}小结 : 嗯,以上魔方的6个面的绘制过程,基本已经完成,主要在在于transform-origin、rotate、translate等属性的应用,但为了让它更炫酷一些,我们还要给边角加一些光感。∙ 添加高光 :细心的宝宝,前面的布局应该已经发现了,每一行布局的p标签里,都多套了一层span,就是为高光光感,埋下的伏笔,一个平面正方形有四个边,after、before只有两,那么肯定要再套一层,当然方法很多,比如直接用border也是可以的,但比较麻烦,我就选择了现在要讲的这种:after、before设置1px的边框,设置一个线性渐变,中间是白色,两断是过渡到透明的,这样高光就有了,来看一组图吧:∙ CSS 360°旋转 :上面是一个鼠标经过的过渡动画,可以看出立体效果是已经有了,接下来就写一个CSS animation的动画,让它360度旋转,每个角都能看到,这样会显的很666;animation 配合 keyframes 使用,请看代码示例:.cube .cube-inner{ /-webkit-transform:rotateX(180deg) rotateY(0deg) ;/ animation: elfCube 10s infinite ease-in-out; -webkit-animation: elfCube 10s infinite ease alternate;}@keyframes elfCube { 0% { transform: rotateX(0deg) rotateY(0deg); } 50% { transform: rotateX(360deg) rotateY(360deg); } 100% { transform: rotateX(0deg) rotateY(0deg); }}@-webkit-keyframes elfCube { 0% { -webkit-transform: rotateX(0deg) rotateY(0deg); } 50% { -webkit-transform: rotateX(360deg) rotateY(360deg); } 100% { transform: rotateX(0deg) rotateY(0deg); }}∙ 跟随鼠标旋转 :说好的随着鼠标旋转呢??别慌,接下来就是带你装逼,带你飞的时候,首先我们要了解,鼠标在容器内所在的位置,X = e.pageX - ele.offsetLeft, Y = e.pageY - ele.offsetTop;同时要知道元素内的中心点:centerX = width/2,centerY =height/2;然后得出值:axisX = X - centerX,axisY = Y - centerY;PS : 开始尝试想的是鼠标从哪个方向进入,得到它的角度,但发现旋转效果不明显 ,有兴趣的同学可以尝试一下:(((Math.atan2(Y, X) * (180 / Math.PI)) + 180) / 90),参考司徒大神的JS判断鼠标从什么方向进入一个容器;最后,给容器绑上事件:mouseover、mousemove、mouseout,鼠标进入时,暂停css的动画,不然会相互打架哦! ……getAxisX(e){ let left = this.cubeEle.offsetLeft; return e.pageX - left - (this.cubeW/2) * (this.cubeW>this.cubeH ? this.cubeH/this.cubeW : 1);}getAxisY(e){ let top = this.cubeEle.offsetTop; return e.pageY - top - (this.cubeH/2) * (this.cubeH>this.cubeW ? this.cubeW/this.cubeH : 1);} ………… …………run(){ this.cubeEle.addEventListener(‘mouseover’,(e)=>this.hoverOut(e),false); this.cubeEle.addEventListener(‘mousemove’,(e)=>this.move(e),false); this.cubeEle.addEventListener(‘mouseout’,(e)=>this.hoverOut(e),false);}hoverOut(e){ //进入/离开 e.preventDefault(); this.axisX = this.getAxisX(e), this.axisY = this.getAxisY(e); if(e.type == ‘mouseout’){ //离开 this.axisX=0; this.axisY = 0; console.log(“离开”) this.cubeInner.className=“cube-inner running”; }else{ this.cubeInner.className=“cube-inner”; console.log(“进入”) }; let rotate = rotateX(${-this.axisY}deg) rotateY(${-this.axisX}deg); this.cubeInner.style.WebkitTransform = this.cubeInner.style.transform = rotate;} ……结尾:-webkit-perspective,-webkit-transform-style,-webkit-transform-origin,radial-gradient、linear-gradient,transform:rotate、translate、scale,transition,animation;以上就是今天为大家带来的分享,以及使用到的知识点API,如文章中有不对之处,烦请各位大神斧正,想学习更多前端知识,记得关注我的公众号哦文章源码获取-> blog-resource ????想直接在线预览 ????作者:苏南 - 首席填坑官交流群:912594095,公众号:honeyBadger8本文原创,著作权归作者所有。商业转载请联系@IT·平头哥联盟获得授权,非商业转载请注明原链接及出处。 ...

October 29, 2018 · 2 min · jiezi

一文盘点直播技术中的编解码、直播协议、网络传输与简单实现

本文节选自 Live CheatSheet | 直播技术理论基础与实践概论,很多内容非作者原创,而是对 Live Links 中列举出的多篇文章的盘点总结,更多直播相关内容可以前往 xCompass 交互式检索或 MushiChat 查看代码。Live CheatSheet | 直播技术理论基础与实践概论音视频直播的基本流程都是采集 → 编码推流 → 网络分发 → 解码 → 播放这五大环节,其中又会涉及平台硬件、编解码、网络传输、服务并发、数字信号处理、在线学习等多方面技术。从交互模式上,又可以泛分为单对单模式与会议模式两大类;从实时性要求上,直播又可以分为伪实时、准实时与真实时三个等级:伪实时:视频消费延迟超过 3 秒,单向观看实时,通用架构是 CDN + RTMP + HLS,譬如很多的直播平台准实时: 视频消费延迟 1 ~ 3 秒,能进行双方互动但互动有障碍;可以通过 TCP/UDP + FLV 已经实现了这类技术,譬如 YY 直播等真实时:视频消费延迟 < 1 秒,平均 500 毫秒,譬如 QQ、微信、Skype 和 WebRTC 等编解码视频封装格式就是我们通常所说的 .mp4,.flv,.ogv,.webm 等,它其实就是一个盒子,用来将实际的视频流以一定的顺序放入,确保播放的有序和完整性。视频压缩格式(视频编码)就是指能够对数字视频进行压缩或者解压缩(视频解码)的程序或者设备。通常这种压缩属于有损数据压缩。视频压缩格式和视频格式具体的区别就是,它是将原始的视频码流变为可用的数字编码。首先,由原始数码设备提供相关的数字信号流,然后经由视频压缩算法,大幅度的减少流的大小,然后交给视频盒子,打上相应的 dts,pts 字段,最终生成可用的视频文件。视频编码也可以指通过过特定的压缩技术,将某个视频格式转换成另一种视频格式。视频封装格式常见的视频封装格式(简称:视频格式)包括了 AVI,MPEG,VOB 等,即相当于一种储存视频信息的容器,由相应的公司开发出来的。AVIAVI 格式(后缀为.AVI):它的英文全称为 Audio Video Interleaved,即音频视频交错格式。它于 1992 年被 Microsoft 公司推出。这种视频格式的优点是图像质量好。由于无损 AVI 可以保存 alpha 通道,经常被我们使用。缺点太多,体积过于庞大,而且更加糟糕的是压缩标准不统一,最普遍的现象就是高版本 Windows 媒体播放器播放不了采用早期编码编辑的 AVI 格式视频,而低版本 Windows 媒体播放器又播放不了采用最新编码编辑的 AVI 格式视频,所以我们在进行一些 AVI 格式的视频播放时常会出现由于视频编码问题而造成的视频不能播放或即使能够播放,但存在不能调节播放进度和播放时只有声音没有图像等一些莫名其妙的问题。DV-AVIDV-AVI 格式(后缀为.AVI):DV 的英文全称是 Digital Video Format,是由索尼、松下、JVC 等多家厂商联合提出的一种家用数字视频格式。数字摄像机就是使用这种格式记录视频数据的。它可以通过电脑的 IEEE 1394 端口传输视频数据到电脑,也可以将电脑中编辑好的的视频数据回录到数码摄像机中。这种视频格式的文件扩展名也是 avi。电视台采用录像带记录模拟信号,通过 EDIUS 由 IEEE 1394 端口采集卡从录像带中采集出来的视频就是这种格式。MOVQuickTime File Format 格式(后缀为.MOV):美国 Apple 公司开发的一种视频格式,默认的播放器是苹果的 QuickTime。具有较高的压缩比率和较完美的视频清晰度等特点,并可以保存 alpha 通道。大家可能注意到了,每次安装 EDIUS,我们都要安装苹果公司推出的 QuickTime。安装其目的就是为了支持 JPG 格式图像和 MOV 视频格式导入。MPEGMPEG 格式(文件后缀可以是 .MPG .MPEG .MPE .DAT .VOB .ASF .3GP .MP4 等):它的英文全称为 Moving Picture Experts Group,即运动图像专家组格式,该专家组建于 1988 年,专门负责为 CD 建立视频和音频标准,而成员都是为视频、音频及系统领域的技术专家。MPEG 文件格式是运动图像压缩算法的国际标准。MPEG 格式目前有三个压缩标准,分别是 MPEG-1、MPEG-2、和 MPEG-4。MPEG-1、MPEG-2 目前已经使用较少,着重介绍 MPEG-4,其制定于 1998 年,MPEG-4 是为了播放流式媒体的高质量视频而专门设计的,以求使用最少的数据获得最佳的图像质量。目前 MPEG-4 最有吸引力的地方在于它能够保存接近于 DVD 画质的小体积视频文件。你可能一定注意到了,怎么没有 MPEG-3 编码,因为这个项目原本目标是为高分辨率电视(HDTV)设计,随后发现 MPEG-2 已足够 HDTV 应用,故 MPEG-3 的研发便中止。WMVWMV 格式(后缀为.WMV .ASF):它的英文全称为 Windows Media Video,也是微软推出的一种采用独立编码方式并且可以直接在网上实时观看视频节目的文件压缩格式。WMV 格式的主要优点包括:本地或网络回放,丰富的流间关系以及扩展性等。WMV 格式需要在网站上播放,需要安装 Windows Media Player(简称 WMP),很不方便,现在已经几乎没有网站采用了。Real VideoReal Video 格式(后缀为.RM .RMVB):Real Networks 公司所制定的音频视频压缩规范称为 Real Media。用户可以使用 RealPlayer 根据不同的网络传输速率制定出不同的压缩比率,从而实现在低速率的网络上进行影像数据实时传送和播放。RMVB 格式:这是一种由 RM 视频格式升级延伸出的新视频格式,当然性能上有很大的提升。RMVB 视频也是有着较明显的优势,一部大小为 700MB 左右的 DVD 影片,如果将其转录成同样品质的 RMVB 格式,其个头最多也就 400MB 左右。大家可能注意到了,以前在网络上下载电影和视频的时候,经常接触到 RMVB 格式,但是随着时代的发展这种格式被越来越多的更优秀的格式替代,著名的人人影视字幕组在 2013 年已经宣布不再压制 RMVB 格式视频。FLVFlash Video 格式(后缀为.FLV):由 Adobe Flash 延伸出来的的一种流行网络视频封装格式。随着视频网站的丰富,这个格式已经非常普及。MKVMatroska 格式(后缀为.MKV):是一种新的多媒体封装格式,这个封装格式可把多种不同编码的视频及 16 条或以上不同格式的音频和语言不同的字幕封装到一个 Matroska Media 档内。它也是其中一种开放源代码的多媒体封装格式。Matroska 同时还可以提供非常好的交互功能,而且比 MPEG 的方便、强大。视频编解码视频实际上就是一帧一帧的图片,拼接起来进行播放;标准的图像格式使用 RGB 三字节描述像素颜色值,会占用较大的存储空间与带宽。视频编解码器会根据前后图像的变化做运动检测,通过各种压缩把变化的结果发送到对方。实时视频编码器需要考虑两个因素:编码计算量和码率带宽,实时视频会运行在移动端上,需要保证实时性就需要编码足够快,码率尽量小。基于这个原因现阶段一般认为 H.264 是最佳的实时视频编码器,而且各个移动平台也支持它的硬编码技术;譬如 1080P 进行过 H.264 编码后带宽也就在 200KB/S ~ 300KB/S 左右。编码基础总的来说,常用的编码方式分为三种:变换编码:消除图像的帧内冗余。涉及到图像学里面的两个概念:空域和频域。空域就是我们物理的图片,频域就是将物理图片根据其颜色值等映射为数字大小。而变换编码的目的是利用频域实现去相关和能量集中。常用的正交变换有离散傅里叶变换,离散余弦变换等等。运动估计和运动补偿:消除帧间冗余。视频压缩还存在时间上的关联性。例如,针对一些视频变化,背景图不变而只是图片中部分物体的移动,针对这种方式,可以只对相邻视频帧中变化的部分进行编码。熵编码:提高压缩效率,熵编码主要是针对码节长度优化实现的。原理是针对信源中出现概率大的符号赋予短码,对于概率小的符号赋予长码,然后总的来说实现平均码长的最小值。编码方式(可变字长编码)有:霍夫曼编码、算术编码、游程编码等。I,B,P 实际上是从运动补偿中引出来的,这里为了后面的方便先介绍一下。I 帧(I-frame): 学名叫做: Intra-coded picture。也可以叫做独立帧。该帧是编码器随机挑选的参考图像,换句话说,一个 I 帧本身就是一个静态图像。它是作为 B,P 帧的参考点。对于它的压缩,只能使用熵 和 变化编码 这两种方式进行帧内压缩。所以,它的运动学补偿基本没有。P 帧(P‑frame): 又叫做 Predicted picture–前向预测帧。即,他会根据前面一张图像,来进行图片间的动态压缩,它的压缩率和 I 帧比起来要高一些。B 帧(B‑frame): 又叫做 Bi-predictive picture– 双向预测。它比 P 帧来说,还多了后一张图像的预测,所以它的压缩率更高。考虑到不同帧传输的无序性,我们还需要引入 PTS 与 DTS 来进行控制,使用 DTS 来解码,PTS 来进行播放。PTS(presentation time stamps): 显示时间戳,显示器从接受到解码到显示的时间。DTS(decoder timestamps): 解码时间戳。也表示该 sample 在整个流中的顺序H.26XH.26X 系列由 ITU 国际电传视讯联盟主导包括, H.261、H.262、H.263、H.264、H.265 等:H.261:主要在老的视频会议和视频电话产品中使用。H.263:主要用在视频会议、视频电话和网络视频上。H.264:H.264/MPEG-4 第十部分,或称 AVC(Advanced Video Coding,高级视频编码),是一种视频压缩标准,一种被广泛使用的高精度视频的录制、压缩和发布格式。H.265:高效率视频编码(High Efficiency Video Coding,简称 HEVC)是一种视频压缩标准,H.264/MPEG-4 AVC 的继任者。HEVC 被认为不仅提升图像质量,同时也能达到 H.264/MPEG-4 AVC 两倍之压缩率(等同于同样画面质量下比特率减少了 50%),可支持 4K 分辨率甚至到超高画质电视,最高分辨率可达到 8192×4320(8K 分辨率),这是目前发展的趋势。直至 2013 年,Potplayer 添加了对于 H.265 视频的解码,尚未有大众化编码软件出现。H.264 是由 ITU 和 MPEG 两个组织共同提出的标准,整个编码器包括帧内预测编码、帧间预测编码、运动估计、熵编码等过程,支持分层编码技术(SVC)。单帧 720P 分辨率一般 PC 上的平均编码延迟 10 毫秒左右,码率范围 1200 ~ 2400kpbs,同等视频质量压缩率是 MPEG4 的 2 倍,H.264 也提供 VBR、ABR、CBR、CQ 等多种编码模式,各个移动平台兼容性好。H.264 为了防止丢包和减小带宽还引入一种双向预测编码的 B 帧,B 帧以前面的 I 或 P 帧和后面的 P 帧为参考帧。H.264 为了防止中间 P 帧丢失视频图像会一直错误它引入分组序列(GOP)编码,也就是隔一段时间发一个全量 I 帧,上一个 I 帧与下一个 I 帧之间为一个分组 GOP。在实时视频当中最好不要加入 B 帧,因为 B 帧是双向预测,需要根据后面的视频帧来编码,这会增大编解码延迟。MPGA 系列MPEG 系列由 ISO 国际标准组织机构下属的 MPEG 运动图象专家组开发视频编码方面主要有:MPEG-1 第二部分(MPEG-1 第二部分主要使用在 VCD 上,有些在线视频也使用这种格式。该编解码器的质量大致上和原有的 VHS 录像带相当。)MPEG-2 第二部分(MPEG-2 第二部分等同于 H.262,使用在 DVD、SVCD 和大多数数字视频广播系统和有线分布系统(cable distribution systems)中。)MPEG-4 第二部分(MPEG-4 第二部分标准可以使用在网络传输、广播和媒体存储上。比起 MPEG-2 和第一版的 H.263,它的压缩性能有所提高。)MPEG-4 第十部分(MPEG-4 第十部分技术上和 ITU-T H.264 是相同的标准,有时候也被叫做“AVC”)最后这两个编码组织合作,诞生了 H.264/AVC 标准。ITU-T 给这个标准命名为 H.264,而 ISO/IEC 称它为 MPEG-4 高级视频编码(Advanced Video Coding,AVC)。音频编码器实时音视频除了视频编码器以外还需要音频编码器,音频编码器只需要考虑编码延迟和丢包容忍度,所以一般的 MP3、AAC、OGG 都不太适合作为实时音频编码器。从现在市场上来使用来看,Skype 研发的 Opus 已经成为实时音频主流的编码器。Opus 优点众多,编码计算量小、编码延迟 20ms、窄带编码-silk、宽带编码器 CELT、自带网络自适应编码等。同视频编码类似,将原始的音频流按照一定的标准进行编码,上传,解码,同时在播放器里播放,当然音频也有许多编码标准,例如 PCM 编码,WMA 编码,AAC 编码等等。直播协议常用的直播协议包括了 HLS, RTMP 与 HTTP-FLV 这三种,其对比如下:协议优势缺陷延迟性HLS支持性广延时巨高10s 以上RTMP延时性好,灵活量大的话,负载较高1s 以上HTTP-FLV延时性好,游戏直播常用只能在手机 APP 播放2s 以上HLSHLS, HTTP Live Streaming 是 Apple 提出的直播流协议,其将整个流分成一个个小的块,并基于 HTTP 的文件来下载;HLS 由两部分构成,一个是 .m3u8 文件,一个是 .ts 视频文件;每一个 .m3u8 文件,分别对应若干个 ts 文件,这些 ts 文件才是真正存放视频的数据,m3u8 文件只是存放了一些 ts 文件的配置信息和相关路径,当视频播放时,.m3u8 是动态改变的,video 标签会解析这个文件,并找到对应的 ts 文件来播放,所以一般为了加快速度,.m3u8 放在 web 服务器上,ts 文件放在 CDN 上。 HLS 协议视频支持 H.264 格式的编码,支持的音频编码方式是 AAC 编码。.m3u8 文件,其实就是以 UTF-8 编码的 m3u 文件,这个文件本身不能播放,只是存放了播放信息的文本文件:#EXTM3U m3u文件头#EXT-X-MEDIA-SEQUENCE 第一个TS分片的序列号#EXT-X-TARGETDURATION 每个分片TS的最大的时长#EXT-X-ALLOW-CACHE 是否允许cache#EXT-X-ENDLIST m3u8文件结束符#EXTINF 指定每个媒体段(ts)的持续时间(秒),仅对其后面的URI有效mystream-12.tsHLS 协议的使用也非常便捷,将 m3u8 直接写入到 src 中然后交与浏览器解析,也可以使用 fetch 来手动解析并且获取相关文件:<video controls autoplay> <source src=“http://devimages.apple.com/iphone/samples/bipbop/masterplaylist.m3u8" type=“application/vnd.apple.mpegurl” /> <p class=“warning”>Your browser does not support HTML5 video.</p></video>HLS 详细版的内容比上面的简版多了一个 playlist,也可以叫做 master。在 master 中,会根据网络段实现设置好不同的 m3u8 文件,比如,3G/4G/wifi 网速等。比如,一个 master 文件中为:#EXTM3U#EXT-X-VERSION:6#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2855600,CODECS=“avc1.4d001f,mp4a.40.2”,RESOLUTION=960x540live/medium.m3u8#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=5605600,CODECS=“avc1.640028,mp4a.40.2”,RESOLUTION=1280x720live/high.m3u8#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1755600,CODECS=“avc1.42001f,mp4a.40.2”,RESOLUTION=640x360live/low.m3u8#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=545600,CODECS=“avc1.42001e,mp4a.40.2”,RESOLUTION=416x234live/cellular.m3u8以 high.m3u8 文件为例,其内容会包含:#EXTM3U#EXT-X-VERSION:6#EXT-X-TARGETDURATION:10#EXT-X-MEDIA-SEQUENCE:26#EXTINF:9.901,http://media.example.com/wifi/segment26.ts#EXTINF:9.901,http://media.example.com/wifi/segment27.ts#EXTINF:9.501,http://media.example.com/wifi/segment28.ts该二级 m3u8 文件也可以称为 media 文件,其有三种类型:live playlist: 动态列表。顾名思义,该列表是动态变化的,里面的 ts 文件会实时更新,并且过期的 ts 索引会被删除。默认,情况下都是使用动态列表。event playlist: 静态列表。它和动态列表主要区别就是,原来的 ts 文件索引不会被删除,该列表是不断更新,而且文件大小会逐渐增大。它会在文件中,直接添加 #EXT-X-PLAYLIST-TYPE:EVENT 作为标识。VOD playlist: 全量列表。它就是将所有的 ts 文件都列在 list 当中。如果,使用该列表,就和播放一整个视频没有啥区别了。它是使用 #EXT-X-ENDLIST 表示文件结尾。显而易见,HLS 的延时包含了 TCP 握手、m3u8 文件下载与解析、ts 文件下载与解析等多个步骤,可以缩短列表的长度和单个 ts 文件的大小来降低延迟,极致来说可以缩减列表长度为 1,并且 ts 的时长为 1s,但是这样会造成请求次数增加,增大服务器压力,当网速慢时回造成更多的缓冲,所以苹果官方推荐的 ts 时长时 10s,所以这样就会大改有 30s 的延迟。RTMPRTMP,Real-Time Messaging Protocol 是由 Adobe 推出的音视频流传递协议;它通过一种自定义的协议,来完成对指定直播流的播放和相关的操作。在 Web 上可以通过 MSE(MediaSource Extensions)来接入 RTMP,基本思路是根据 WebSocket 直接建立长连接进行数据的交流和监听。RTMP 协议根据不同的套层,也可以分为:纯 RTMP: 直接通过 TCP 连接,端口为 1935RTMPS: RTMP + TLS/SSL,用于安全性的交流。RTMPE: RTMP + encryption。在 RTMP 原始协议上使用,Adobe 自身的加密方法RTMPT: RTMP + HTTP。使用 HTTP 的方式来包裹 RTMP 流,这样能直接通过防火墙。不过,延迟性比较大。RTMFP: RMPT + UDP。该协议常常用于 P2P 的场景中,针对延时有变态的要求。RTMP 内部是借由 TCP 长连接协议传输相关数据,所以,它的延时性非常低。并且,该协议灵活性非常好(所以,也很复杂),它可以根据 message stream ID 传输数据,也可以根据 chunk stream ID 传递数据。两者都可以起到流的划分作用。流的内容也主要分为:视频,音频,相关协议包等。HTTP-FLVRTMP 是直接将流的传输架在 RTMP 协议之上,而 HTTP-FLV 是在 RTMP 和客户端之间套了一层转码的过程,即:每个 FLV 文件是通过 HTTP 的方式获取的,所以,它通过抓包得出的协议头需要使用 chunked 编码:Content-Type:video/x-flvExpires:Fri, 10 Feb 2017 05:24:03 GMTPragma:no-cacheTransfer-Encoding:chunked网络传输单对单模式主要是怎么通过路由路径优化手段达到两点之间最优,这方面 SKYPE 首先提出基于 P2P 的 Real-time Network 模型。而 单对多模式是一个分发树模型,各个客户端节点需要就近接入离自己最近的服务器,然后在服务器与服务器构建一个实时通信网络。基础推流所谓推流,就是将我们已经编码好的音视频数据发往视频流服务器中。实时音视频系统都是一个客户端到其他一个或者多个客户端的通信行为,这就意味着需要将客户端编码后的音视频数据传输到其他实时音视频系统都是一个客户端到其他一个或者多个客户端的通信行为,这就意味着需要将客户端编码后的音视频数据传输到其他客户端上,一般做法是先将数据实时上传到服务器上,服务器再进行转发到其他客户端,客户端这个上传音视频数据行为称为推流。我们可以通过 Nginx 的 RTMP 扩展方便地搭建推流服务器:rtmp { server { listen 1935; #监听的端口 chunk_size 4000; application hls { #rtmp推流请求路径 live on; hls on; hls_path /usr/local/var/www/hls; hls_fragment 5s; } }}推流会受到客户端网络的影响,例如:wifi 信号衰减、4G 弱网、拥挤的宽带网络等。为了应对这个问题,实时音视频系统会设计一个基于拥塞控制和 QOS 策略的推流模块。WebRTCWebRTC 是一个开源项目,旨在使得浏览器能为实时通信(RTC)提供简单的 JavaScript 接口。说的简单明了一点就是让浏览器提供 JS 的即时通信接口。这个接口所创立的信道并不是像 WebSocket 一样,打通一个浏览器与 WebSocket 服务器之间的通信,而是通过一系列的信令,建立一个浏览器与浏览器之间(peer-to-peer)的信道,这个信道可以发送任何数据,而不需要经过服务器。并且 WebRTC 通过实现 MediaStream,通过浏览器调用设备的摄像头、话筒,使得浏览器之间可以传递音频和视频。WebRTC 有三个重要的部分:MediaStream、RTCPeerConnection、RTCDataChannel:MediaStream:通过设备的摄像头及话筒获得视频、音频的同步流PeerConnection: 用于构建点对点之间稳定、高效的流传输的组件DataChannel:能够使得浏览器之间(点对点)简历一个高吞吐量、低延时的信道,用于传输任何数据实时网络传输优化TCP 与 UDP在大规模实时多媒体传输网络中,TCP 和 RTMP 都不占优势。TCP 是个拥塞公平传输的协议,它的拥塞控制都是为了保证网络的公平性而不是快速到达,我们知道,TCP 层只有顺序到对应的报文才会提示应用层读数据,如果中间有报文乱序或者丢包都会在 TCP 做等待,所以 TCP 的发送窗口缓冲和重发机制在网络不稳定的情况下会造成延迟不可控,而且传输链路层级越多延迟会越大。在实时传输中使用 UDP 更加合理,UDP 避免了 TCP 繁重的三次握手、四次挥手和各种繁杂的传输特性,只需要在 UDP 上做一层简单的链路 QoS 监测和报文重发机制,实时性会比 TCP 好,这一点从 RTP 和 DDCP 协议可以证明这一点,我们正式参考了这两个协议来设计自己的通信协议。UDP 不可避免地存在抖动、乱序、丢包问题,视频必须按照严格是时间戳来播放,否则的就会出现视频动作加快或者放慢的现象,如果我们按照接收到视频数据就立即播放,那么这种加快和放慢的现象会非常频繁和明显。也就是说网络抖动会严重影响视频播放的质量,一般为了解决这个问题会设计一个视频播放缓冲区,通过缓冲接收到的视频帧,再按视频帧内部的时间戳来播放既可。UDP 除了小范围的抖动以外,还是出现大范围的乱序现象,就是后发的报文先于先发的报文到达接收方。乱序会造成视频帧顺序错乱,一般解决的这个问题会在视频播放缓冲区里做一个先后排序功能让先发送的报文先进行播放。UDP 在传输过程还会出现丢包,丢失的原因有多种,例如:网络出口不足、中间网络路由拥堵、socket 收发缓冲区太小、硬件问题、传输损耗问题等等。在基于 UDP 视频传输过程中,丢包是非常频繁发生的事情,丢包会造成视频解码器丢帧,从而引起视频播放卡顿。这也是大部分视频直播用 TCP 和 RTMP 的原因,因为 TCP 底层有自己的重传机制,可以保证在网络正常的情况下视频在传输过程不丢。基于 UDP 丢包补偿方式一般有以下几种:报文冗余,报文冗余很好理解,就是一个报文在发送的时候发送 2 次或者多次。这个做的好处是简单而且延迟小,坏处就是需要额外 N 倍(N 取决于发送的次数)的带宽。FEC, Forward Error Correction,即向前纠错算法,常用的算法有纠删码技术(EC),在分布式存储系统中比较常见。最简单的就是 A B 两个报文进行 XOR(与或操作)得到 C,同时把这三个报文发往接收端,如果接收端只收到 AC,通过 A 和 C 的 XOR 操作就可以得到 B 操作。丢包重传,丢包重传有两种方式,一种是 push 方式,一种是 pull 方式。Push 方式是发送方没有收到接收方的收包确认进行周期性重传,TCP 用的是 push 方式。pull 方式是接收方发现报文丢失后发送一个重传请求给发送方,让发送方重传丢失的报文。丢包重传是按需重传,比较适合视频传输的应用场景,不会增加太对额外的带宽,但一旦丢包会引来至少一个 RTT 的延迟。拥塞控制要评估一个网络通信质量的好坏和延迟一个重要的因素就是 Round-Trip Time(网络往返延迟),也就是 RTT。评估两端之间的 RTT 方法很简单,大致如下:发送端方一个带本地时间戳 T1 的 ping 报文到接收端;接收端收到 ping 报文,以 ping 中的时间戳 T1 构建一个携带 T1 的 pong 报文发往发送端;发送端接收到接收端发了的 pong 时,获取本地的时间戳 T2,用 T2 – T1 就是本次评测的 RTT。因为客户端有可能在弱网环境下进行推流,音视频数据如果某一时刻发多了,就会引起网络拥塞或者延迟,如果发少了,可能视频的清晰不好。在实时音视频传输过程会设计一个自动适应本地网络变化的拥塞控制算法,像 QUIC 中的 BBR、webRTC 中 GCC 和通用的 RUDP。思路是通过 UDP 协议反馈的丢包和网络延迟(RTT)来计算当前网络的变化和最大瞬时吞吐量,根据这几个值调整上层的视频编码器的码率、视频分辨率等,从而达到适应当前网络状态的目的。QoS 策略客户端推流除了需要考虑网络上传能力以外,还需要考虑客户端的计算能力。如果在 5 年前的安卓机上去编码一个分辨率为 640P 的高清视频流,那这个过程必然会产生延迟甚至无法工作。为此需要针对各个终端的计算能力设计一个 QoS 策略,不同计算能力的终端采用不同的视频编码器、分辨率、音频处理算法等,这个 QoS 策略会配合拥塞控制做一个状态不可逆的查找过程,直到找到最合适的 QoS 策略位置媒体处理技术回声消除在实时音视频系统中,回声消除是一个难点,尽管 webRTC 提供了开源的回声消除模块,但在移动端和一些特殊的场景表现不佳。专业的实时音视频系统会进行回声消除的优化。回声消除的原理描述很简单,就是将扬声器播放的声音波形和麦克风录制的波形进行抵消,达到消除回声的作用。因为回声的回录时间不确定,所以很难确定什么时间点进行对应声音数据的抵消。在专业的回声消除模块里面通常会设计一个逼近函数,通过不断对输出和输入声音波形进行在线学习逼近,确定回声消除的时间差值点。简单 Web 实验本部分的代码实验参考 MushiChat。Media Source ExtensionMSE 全称就是 Media Source Extensions。它是一套处理视频流技术的简称,里面包括了一系列 API:Media Source,Source Buffer 等。在没有 MSE 出现之前,前端对 video 的操作,仅仅局限在对视频文件的操作,而并不能对视频流做任何相关的操作。现在 MSE 提供了一系列的接口,使开发者可以直接提供 media stream。const vidElement = document.querySelector(‘video’);if (window.MediaSource) { const mediaSource = new MediaSource(); vidElement.src = URL.createObjectURL(mediaSource); mediaSource.addEventListener(‘sourceopen’, sourceOpen);} else { console.log(‘The Media Source Extensions API is not supported.’);}function sourceOpen(e) { URL.revokeObjectURL(vidElement.src); const mime = ‘video/webm; codecs=“opus, vp9”’; const mediaSource = e.target; const sourceBuffer = mediaSource.addSourceBuffer(mime); const videoUrl = ‘droid.webm’; fetch(videoUrl) .then(function(response) { return response.arrayBuffer(); }) .then(function(arrayBuffer) { sourceBuffer.addEventListener(‘updateend’, function(e) { if (!sourceBuffer.updating && mediaSource.readyState === ‘open’) { mediaSource.endOfStream(); } }); sourceBuffer.appendBuffer(arrayBuffer); });}其中 MediaSource 只是一系列视频流的管理工具,它可以将音视频流完整的暴露给 Web 开发者来进行相关的操作和处理。所以,它本身不会造成过度的复杂性。 ...

October 26, 2018 · 4 min · jiezi

深拷贝的终极探索(99%的人都不知道)

划重点,这是一道面试必考题,我靠这道题刷掉了多少面试者✧(≖ ◡ ≖✿)嘿嘿首先这是一道非常棒的面试题,可以考察面试者的很多方面,比如基本功,代码能力,逻辑能力,而且进可攻,退可守,针对不同级别的人可以考察不同难度,比如漂亮妹子就出1☆题,要是个帅哥那就得上5☆了,(^__^) 嘻嘻……无论面试者多么优秀,漂亮的回答出问题,我总能够潇洒的再抛出一个问题,看着面试者露出惊异的眼神,默默一转身,深藏功与名本文我将给大家破解深拷贝的谜题,由浅入深,环环相扣,总共涉及4种深拷贝方式,每种方式都有自己的特点和个性深拷贝 VS 浅拷贝再开始之前需要先给同学科普下什么是深拷贝,和深拷贝有关系的另个一术语是浅拷贝又是什么意思呢?如果对这部分部分内容了解的同学可以跳过其实深拷贝和浅拷贝都是针对的引用类型,JS中的变量类型分为值类型(基本类型)和引用类型;对值类型进行复制操作会对值进行一份拷贝,而对引用类型赋值,则会进行地址的拷贝,最终两个变量指向同一份数据// 基本类型var a = 1;var b = a;a = 2;console.log(a, b); // 2, 1 ,a b指向不同的数据// 引用类型指向同一份数据var a = {c: 1};var b = a;a.c = 2;console.log(a.c, b.c); // 2, 2 全是2,a b指向同一份数据对于引用类型,会导致a b指向同一份数据,此时如果对其中一个进行修改,就会影响到另外一个,有时候这可能不是我们想要的结果,如果对这种现象不清楚的话,还可能造成不必要的bug那么如何切断a和b之间的关系呢,可以拷贝一份a的数据,根据拷贝的层级不同可以分为浅拷贝和深拷贝,浅拷贝就是只进行一层拷贝,深拷贝就是无限层级拷贝var a1 = {b: {c: {}};var a2 = shallowClone(a1); // 浅拷贝a2.b.c === a1.b.c // truevar a3 = clone(a3); // 深拷贝a3.b.c === a1.b.c // false浅拷贝的实现非常简单,而且还有多种方法,其实就是遍历对象属性的问题,这里只给出一种,如果看不懂下面的方法,或对其他方法感兴趣,可以看我的这篇文章function shallowClone(source) { var target = {}; for(var i in source) { if (source.hasOwnProperty(i)) { target[i] = source[i]; } } return target;}最简单的深拷贝深拷贝的问题其实可以分解成两个问题,浅拷贝+递归,什么意思呢?假设我们有如下数据var a1 = {b: {c: {d: 1}};只需稍加改动上面浅拷贝的代码即可,注意区别function clone(source) { var target = {}; for(var i in source) { if (source.hasOwnProperty(i)) { if (typeof source[i] === ‘object’) { target[i] = clone(source[i]); // 注意这里 } else { target[i] = source[i]; } } } return target;}大部分人都能写出上面的代码,但当我问上面的代码有什么问题吗?就很少有人答得上来了,聪明的你能找到问题吗?其实上面的代码问题太多了,先来举几个例子吧没有对参数做检验判断是否对象的逻辑不够严谨没有考虑数组的兼容(⊙o⊙),下面我们来看看各个问题的解决办法,首先我们需要抽象一个判断对象的方法,其实比较常用的判断对象的方法如下,其实下面的方法也有问题,但如果能够回答上来那就非常不错了,如果完美的解决办法感兴趣,不妨看看这里吧function isObject(x) { return Object.prototype.toString.call(x) === ‘[object Object]’;}函数需要校验参数,如果不是对象的话直接返回function clone(source) { if (!isObject(source)) return source; // xxx}关于第三个问题,嗯,就留给大家自己思考吧,本文为了减轻大家的负担,就不考虑数组的情况了,其实ES6之后还要考虑set, map, weakset, weakmap,/(ㄒoㄒ)/~~其实吧这三个都是小问题,其实递归方法最大的问题在于爆栈,当数据的层次很深是就会栈溢出下面的代码可以生成指定深度和每层广度的代码,这段代码我们后面还会再次用到function createData(deep, breadth) { var data = {}; var temp = data; for (var i = 0; i < deep; i++) { temp = temp[‘data’] = {}; for (var j = 0; j < breadth; j++) { temp[j] = j; } } return data;}createData(1, 3); // 1层深度,每层有3个数据 {data: {0: 0, 1: 1, 2: 2}}createData(3, 0); // 3层深度,每层有0个数据 {data: {data: {data: {}}}}当clone层级很深的话就会栈溢出,但数据的广度不会造成溢出clone(createData(1000)); // okclone(createData(10000)); // Maximum call stack size exceededclone(createData(10, 100000)); // ok 广度不会溢出其实大部分情况下不会出现这么深层级的数据,但这种方式还有一个致命的问题,就是循环引用,举个例子var a = {};a.a = a;clone(a) // Maximum call stack size exceeded 直接死循环了有没有,/(ㄒoㄒ)/~~关于循环引用的问题解决思路有两种,一直是循环检测,一种是暴力破解,关于循环检测大家可以自己思考下;关于暴力破解我们会在下面的内容中详细讲解一行代码的深拷贝有些同学可能见过用系统自带的JSON来做深拷贝的例子,下面来看下代码实现function cloneJSON(source) { return JSON.parse(JSON.stringify(source));}其实我第一次简单这个方法的时候,由衷的表示佩服,其实利用工具,达到目的,是非常聪明的做法下面来测试下cloneJSON有没有溢出的问题,看起来cloneJSON内部也是使用递归的方式cloneJSON(createData(10000)); // Maximum call stack size exceeded既然是用了递归,那循环引用呢?并没有因为死循环而导致栈溢出啊,原来是JSON.stringify内部做了循环引用的检测,正是我们上面提到破解循环引用的第一种方法:循环检测var a = {};a.a = a;cloneJSON(a) // Uncaught TypeError: Converting circular structure to JSON破解递归爆栈其实破解递归爆栈的方法有两条路,第一种是消除尾递归,但在这个例子中貌似行不通,第二种方法就是干脆不用递归,改用循环,当我提出用循环来实现时,基本上90%的前端都是写不出来的代码的,这其实让我很震惊举个例子,假设有如下的数据结构var a = { a1: 1, a2: { b1: 1, b2: { c1: 1 } }}这不就是一个树吗,其实只要把数据横过来看就非常明显了 a / \ a1 a2 | / \ 1 b1 b2 | | 1 c1 | 1 用循环遍历一棵树,需要借助一个栈,当栈为空时就遍历完了,栈里面存储下一个需要拷贝的节点首先我们往栈里放入种子数据,key用来存储放哪一个父元素的那一个子元素拷贝对象然后遍历当前节点下的子元素,如果是对象就放到栈里,否则直接拷贝function cloneLoop(x) { const root = {}; // 栈 const loopList = [ { parent: root, key: undefined, data: x, } ]; while(loopList.length) { // 深度优先 const node = loopList.pop(); const parent = node.parent; const key = node.key; const data = node.data; // 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素 let res = parent; if (typeof key !== ‘undefined’) { res = parent[key] = {}; } for(let k in data) { if (data.hasOwnProperty(k)) { if (typeof data[k] === ‘object’) { // 下一次循环 loopList.push({ parent: res, key: k, data: data[k], }); } else { res[k] = data[k]; } } } } return root;}改用循环后,再也不会出现爆栈的问题了,但是对于循环引用依然无力应对破解循环引用有没有一种办法可以破解循环应用呢?别着急,我们先来看另一个问题,上面的三种方法都存在的一个问题就是引用丢失,这在某些情况下也许是不能接受的举个例子,假如一个对象a,a下面的两个键值都引用同一个对象b,经过深拷贝后,a的两个键值会丢失引用关系,从而变成两个不同的对象,o(╯□╰)ovar b = 1;var a = {a1: b, a2: b};a.a1 === a.a2 // truevar c = clone(a);c.a1 === c.a2 // false如果我们发现个新对象就把这个对象和他的拷贝存下来,每次拷贝对象前,都先看一下这个对象是不是已经拷贝过了,如果拷贝过了,就不需要拷贝了,直接用原来的,这样我们就能够保留引用关系了,✧(≖ ◡ ≖✿)嘿嘿但是代码怎么写呢,o(╯□╰)o,别急往下看,其实和循环的代码大体一样,不一样的地方我用// ==========标注出来了引入一个数组uniqueList用来存储已经拷贝的数组,每次循环遍历时,先判断对象是否在uniqueList中了,如果在的话就不执行拷贝逻辑了find是抽象的一个函数,其实就是遍历uniqueList// 保持引用关系function cloneForce(x) { // ============= const uniqueList = []; // 用来去重 // ============= let root = {}; // 循环数组 const loopList = [ { parent: root, key: undefined, data: x, } ]; while(loopList.length) { // 深度优先 const node = loopList.pop(); const parent = node.parent; const key = node.key; const data = node.data; // 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素 let res = parent; if (typeof key !== ‘undefined’) { res = parent[key] = {}; } // ============= // 数据已经存在 let uniqueData = find(uniqueList, data); if (uniqueData) { parent[key] = uniqueData.target; break; // 中断本次循环 } // 数据不存在 // 保存源数据,在拷贝数据中对应的引用 uniqueList.push({ source: data, target: res, }); // ============= for(let k in data) { if (data.hasOwnProperty(k)) { if (typeof data[k] === ‘object’) { // 下一次循环 loopList.push({ parent: res, key: k, data: data[k], }); } else { res[k] = data[k]; } } } } return root;}function find(arr, item) { for(let i = 0; i < arr.length; i++) { if (arr[i].source === item) { return arr[i]; } } return null;}下面来验证一下效果,amazingvar b = 1;var a = {a1: b, a2: b};a.a1 === a.a2 // truevar c = cloneForce(a);c.a1 === c.a2 // true接下来再说一下如何破解循环引用,等一下,上面的代码好像可以破解循环引用啊,赶紧验证一下惊不惊喜,(^__^) 嘻嘻……var a = {};a.a = a;cloneForce(a)看起来完美的cloneForce是不是就没问题呢?cloneForce有两个问题第一个问题,所谓成也萧何,败也萧何,如果保持引用不是你想要的,那就不能用cloneForce了;第二个问题,cloneForce在对象数量很多时会出现很大的问题,如果数据量很大不适合使用cloneForce性能对比上边的内容还是有点难度,下面我们来点更有难度的,对比一下不同方法的性能我们先来做实验,看数据,影响性能的原因有两个,一个是深度,一个是每层的广度,我们采用固定一个变量,只让一个变量变化的方式来测试性能测试的方法是在指定的时间内,深拷贝执行的次数,次数越多,证明性能越好下面的runTime是测试代码的核心片段,下面的例子中,我们可以测试在2秒内运行clone(createData(500, 1)的次数function runTime(fn, time) { var stime = Date.now(); var count = 0; while(Date.now() - stime < time) { fn(); count++; } return count;}runTime(function () { clone(createData(500, 1)) }, 2000);下面来做第一个测试,将广度固定在100,深度由小到大变化,记录1秒内执行的次数深度clonecloneJSONcloneLoopcloneForce500351212338372100017410417514315001166711282200092508869将上面的数据做成表格可以发现,一些规律随着深度变小,相互之间的差异在变小clone和cloneLoop的差别并不大cloneLoop > cloneForce > cloneJSON我们先来分析下各个方法的时间复杂度问题,各个方法要做的相同事情,这里就不计算,比如循环对象,判断是否为对象clone时间 = 创建递归函数 + 每个对象处理时间cloneJSON时间 = 循环检测 + 每个对象处理时间 * 2 (递归转字符串 + 递归解析)cloneLoop时间 = 每个对象处理时间cloneForce时间 = 判断对象是否缓存中 + 每个对象处理时间cloneJSON的速度只有clone的50%,很容易理解,因为其会多进行一次递归时间cloneForce由于要判断对象是否在缓存中,而导致速度变慢,我们来计算下判断逻辑的时间复杂度,假设对象的个数是n,则其时间复杂度为O(n2),对象的个数越多,cloneForce的速度会越慢1 + 2 + 3 … + n = n^2/2 - 1关于clone和cloneLoop这里有一点问题,看起来实验结果和推理结果不一致,其中必有蹊跷接下来做第二个测试,将深度固定在10000,广度固定为0,记录2秒内执行的次数宽度clonecloneJSONcloneLoopcloneForce013400327214292989排除宽度的干扰,来看看深度对各个方法的影响随着对象的增多,cloneForce的性能低下凸显cloneJSON的性能也大打折扣,这是因为循环检测占用了很多时间cloneLoop的性能高于clone,可以看出递归新建函数的时间和循环对象比起来可以忽略不计下面我们来测试一下cloneForce的性能极限,这次我们测试运行指定次数需要的时间var data1 = createData(2000, 0);var data2 = createData(4000, 0);var data3 = createData(6000, 0);var data4 = createData(8000, 0);var data5 = createData(10000, 0);cloneForce(data1)cloneForce(data2)cloneForce(data3)cloneForce(data4)cloneForce(data5)通过测试发现,其时间成指数级增长,当对象个数大于万级别,就会有300ms以上的延迟总结尺有所短寸有所长,无关乎好坏优劣,其实每种方法都有自己的优缺点,和适用场景,人尽其才,物尽其用,方是真理下面对各种方法进行对比,希望给大家提供一些帮助 clonecloneJSONcloneLoopcloneForce难度☆☆☆☆☆☆☆☆☆☆兼容性ie6ie8ie6ie6循环引用一层不支持一层支持栈溢出会会不会不会保持引用否否否是适合场景一般数据拷贝一般数据拷贝层级很多保持引用关系本文的灵感都来自于@jsmini/clone,如果大家想使用文中的4种深拷贝方式,可以直接使用@jsmini/clone这个库// npm install –save @jsmini/cloneimport { clone, cloneJSON, cloneLoop, cloneForce } from ‘@jsmini/clone’;本文为了简单和易读,示例代码中忽略了一些边界情况,如果想学习生产中的代码,请阅读@jsmini/clone的源码@jsmini/clone孵化于jsmini,jsmini致力于为大家提供一组小而美,无依赖的高质量库jsmini的诞生离不开jslib-base,感谢jslib-base为jsmini提供了底层技术感谢你阅读了本文,相信现在你能够驾驭任何深拷贝的问题了,如果有什么疑问,欢迎和我讨论最后推荐下我的新书《React状态管理与同构实战》,深入解读前沿同构技术,感谢大家支持京东:https://item.jd.com/12403508.html当当:http://product.dangdang.com/25308679.html最后最后招聘前端,后端,客户端啦!地点:北京+上海+成都,感兴趣的同学,可以把简历发到我的邮箱: yanhaijing@yeah.net原文网址:http://yanhaijing.com/javascr… ...

October 14, 2018 · 4 min · jiezi

Gin 框架的路由结构浅析

Gin 是 go 语言的一款轻量级框架,风格简单朴素,支持中间件,动态路由等功能。gin项目github地址路由是web框架的核心功能。在没有读过 gin 的代码之前,在我眼里的路由实现是这样的:根据路由里的 / 把路由切分成多个字符串数组,然后按照相同的前子数组把路由构造成树的结构;寻址时,先把请求的 url 按照 / 切分,然后遍历树进行寻址。比如:定义了两个路由 /user/get,/user/delete,则会构造出拥有三个节点的路由树,根节点是 user,两个子节点分别是 get delete。上述是一种实现路由树的方式,且比较直观,容易理解。对 url 进行切分、比较,时间复杂度是 O(2n)。Gin的路由实现使用了类似前缀树的数据结构,只需遍历一遍字符串即可,时间复杂度为O(n)。当然,对于一次 http 请求来说,这点路由寻址优化可以忽略不计。EngineGin 的 Engine 结构体内嵌了 RouterGroup 结构体,定义了 GET,POST 等路由注册方法。Engine 中的 trees 字段定义了路由逻辑。trees 是 methodTrees 类型(其实就是 []methodTree),trees 是一个数组,不同请求方法的路由在不同的树(methodTree)中。最后,methodTree 中的 root 字段(*node类型)是路由树的根节点。树的构造与寻址都是在 *node的方法中完成的。UML 结构图trees 是个数组,数组里会有不同请求方法的路由树。nodenode 结构体定义如下type node struct { path string // 当前节点相对路径(与祖先节点的 path 拼接可得到完整路径) indices string // 所以孩子节点的path[0]组成的字符串 children []*node // 孩子节点 handlers HandlersChain // 当前节点的处理函数(包括中间件) priority uint32 // 当前节点及子孙节点的实际路由数量 nType nodeType // 节点类型 maxParams uint8 // 子孙节点的最大参数数量 wildChild bool // 孩子节点是否有通配符(wildcard)}path 和 indices关于 path 和 indices,其实是使用了前缀树的逻辑。举个栗子:如果我们有两个路由,分别是 /index,/inter,则根节点为 {path: “/in”, indices: “dt”…},两个子节点为{path: “dex”, indices: “"},{path: “ter”, indices: “"}handlershandlers里存储了该节点对应路由下的所有处理函数,处理业务逻辑时是这样的:func (c *Context) Next() { c.index++ for s := int8(len(c.handlers)); c.index < s; c.index++ { c.handlersc.index }}一般来说,除了最后一个函数,前面的函数被称为中间件。如果某个节点的 handlers为空,则说明该节点对应的路由不存在。比如上面定义的根节点对应的路由 /in 是不存在的,它的 handlers就是[]。nTypeGin 中定义了四种节点类型:const ( static nodeType = iota // 普通节点,默认 root // 根节点 param // 参数路由,比如 /user/:id catchAll // 匹配所有内容的路由,比如 /article/key)param 与 catchAll 使用的区别就是 : 与 * 的区别。 会把路由后面的所有内容赋值给参数 key;但 : 可以多次使用。比如:/user/:id/:no 是合法的,但 /user/*id/:no 是非法的,因为 * 后面所有内容会赋值给参数 id。wildChild如果孩子节点是通配符(*或者:),则该字段为 true。一个路由树的例子定义路由如下:r.GET(”/”, func(context *gin.Context) {})r.GET("/index", func(context *gin.Context) {})r.GET("/inter", func(context *gin.Context) {})r.GET("/go", func(context *gin.Context) {})r.GET("/game/:id/:k", func(context *gin.Context) {})得到的路由树结构图为:附一篇前缀树的文章,前缀树和后缀树以上。 ...

October 12, 2018 · 1 min · jiezi

一个完整Java Web项目背后的密码

前言最近自己做了几个Java Web项目,有公司的商业项目,也有个人做着玩的小项目,写篇文章记录总结一下收获,列举出在做项目的整个过程中,所需要用到的技能和知识点,带给还没有真正接触过完整Java Web项目的同学一个比较完整的视角,提供一个所谓的“大局观”,也以便于同学们更有针对性地学习。当然,这里所用到的例子项目是非常初级,简单的项目,所以大神们就可以不用往下看了。首先我们从网站的架构谈起。一般来说,我们将网站分为前端和后端。前端主要负责页面的展示,后端则是业务逻辑的实现。由于html5的兴起,前端领域已经越来越火热,前端技术发展极快,今天我们不做过多介绍,因为现在的互联网公司,前端工程师和Java工程师是完全不同的两种技术岗位。所以我们还是以Java的角度去看待一个项目。在前端没有那么火的前几年,或者说在经典的Java Web的开发模式中,我们使用Jsp技术来作为展现层的实现,其实也就是所谓的前端。当然只懂得Jsp是不够的还需要懂html,css,js,ajax等一些前端的基础技术,Jsp技术在其中扮演外层包装的角色。那么后端呢?后端是由于一些实现了业务逻辑Java代码和数据库组成。说到这,就可以推出Web开发中经典的MVC模式,Model-View-Controller。View,,就是指表现层,Model,是用来承载数据的抽象结构,而Controller则是View和Model的桥梁。View存在与前端代码中,Controller,Model存在与后端代码中。在后端代码中,为了保证代码的整洁,易读性,一般会采用分层的办法,自顶向下分为controller层,service层,dao层,数据层或者叫持久层(直接与数据库打交道)。有时候,为了达到解耦的目的,会在上述基层中间加入响应的接口层,以使得接口与实现分离。在更加大型的网站中,会出现更加复杂的架构,比如dao层与数据层之间要有缓存层,或者访问压力增大后,需要使用集群,负载均衡等高级技术,但在这里,我们就不深入讨论复杂架构了。所以以上我们看到,一个Web 项目前端需要表现层,后端有controller层,service层,dao层,持久层。在表现层,除了刚才提到的html,css,js,ajax,jsp等基础知识,在实际开发中,我们还会用到许多框架技术,比如tiles,velocity,freemarker等模板技术来简化表现层的开发。在持久层,除了jdbc外,还有Mybatis,Hibernate等框架来提高开发效率。在Java Web中最耀眼的当属Spring了,Spring作为一个贯穿整个项目的框架,为项目开发带来依赖注入,面向切面编程的功能。除了这些,我们还需要熟练掌握一种关系型数据库的使用,如MySQL,Oracle等,当然更好地话还需要掌握一种非关系型数据库,MongoDB,Redis。 掌握了以上知识点,恭喜你,你已经可以开始写Java Web项目了。但是只会写而不会部署,我们的项目仍然不能拿出来用。所以说到这里,我们的项目代码已经写好,接下来该怎么办呢?当然是需要找一个容器来运行我们的代码。这里的容器是当然不是指Java里的数据类型,而是指应用服务器,此处要特别区分应用服务器与Web服务器,至于Web服务器是什么,稍后会介绍。(如果有同学不是很清楚服务器的概念,可以先看下一段,再回来看)目前,市面主要流行的应用服务器有Tomcat,Jetty,JBoss等。很多人会问我们为什么需要应用服务器?顾名思义,应用服务器就是用来运行我们的应用代码的。这里需要特别提到的是,Java Web用到了像Jsp,servlet这样的动态web技术,而这些技术的代码是必须运行中应用服务器中的。所以当我们写好应用代码后,需要把自己的应用部署到应用服务器上。应用部署好后,那么用户们该怎么访问呢?直接访问应用服务器吗?这个时候就需要web服务器出场了。在互联网上,最强大的应用层协议当属http协议了,人们访问网站就是通过http协议来进行访问的,而Web服务器就是支持http协议的服务器,所以就叫http服务器。Web服务器接收http请求,然后再将请求转交给应用服务器。有人会问用户直接访问应用服务器不好吗?为什么要给web服务器,然后再到应用服务器?从功能实现上来说,是可以的。许多应用服务器,比如Tomcat是具有web服务器的功能,所以直接访问也可以。但是由于在实际的生产环境中,由于负载均衡,cdn加速等原因,我们还是需要在应用服务器的前端再加一个web服务器来提高访问效率,常用的有Nginx,Apache这样的服务器。 之前老是在讲这个服务器,那个服务器,不知道有没有同学听懵了。其实服务器这个概念,我们应该在真正接触计算机专业领域之前早就听说过,比如以前打游戏觉得卡的时候大家都时不时会用到服务器这个词汇。但是我真正理解并研究服务器是在学习tomcat, jetty之后才开始的。那么服务器到底什么呢?服务器这个概念其实很简单,就是一台电脑,那它和我们日常用的电脑有什么区别?第一,它一般没有显示器,它只有主机。第二,它的操作系统不同于我们常用的windows , Mac OS。更多的是nix系统。第三,它运行了一些服务器端软件。比如说,我们上文提到的Tomcat , Jetty , Nginx,Apache,其实这些都是服务器软件,只是主机运行了这些软件,所以有时候大家就混淆了叫法。所以所谓的数据库服务器,大家也知道了,其实就是运行了数据库的主机。说到服务器,我们所需要掌握的重点知识就是服务器操作系统,也就是nix系统,比如CentOS , Ubuntu等。说到这里,其实一个简单的完整的Java Web项目就差不多了。我们简单回顾一下,首先,我们需要利用各种框架和开发技术写出应用代码。接下来,我们需要一个台安装了*nix系统的主机,在上面安装好Web服务器软件,应用服务器软件,再把我们的应用代码部署到应用服务器上。现在我们只需要获取到主机的IP地址,就能够远程访问应用了。知识点列表:开发:1、视图层技术——HTML,CSS,JS,AJAX,Tiles,Velocity,FreeMarker2、持久层技术——MyBatis,Hibernate3、Spring , Spring MVC4、项目构建工具Maven5、日志Log4j6、版本控制 Git数据库技术:1、SQL语句2、参数调优操作系统:1、熟练掌握一种Linux系统,原理,Shell命令服务器技术:1、熟练使用并理解一个应用服务器技术的原理(Tomcat)2、熟练使用并理解一个Web服务器技术的原理(Nginx)附加:缓存技术:1、熟练使用并理解一种缓存技术(Redis,Memcache,EhCache)非关系型数据库熟练使用并理解一种非关系型数据库(MongoDB)中间件技术:1、JMS:activeMQ和kafka/2、RPC: Dubbo设计模式:1、了解并能够使用几种最主要的设计模式网络:1、熟练使用并理解一个网络开发技术(Netty)2、熟悉http,TCP协议Java虚拟机:1、熟悉jvm运行原理,内存分布2、jvm参数调优原文:blog.csdn.net/JasonLiuLJX/article/details/51494048阅读更多有关Android插件化思考Java并发面试,幸亏有点道行,不然又被忽悠了Android酷炫实用的开源框架(UI框架)(Android)面试题级答案(精选版)相信自己,没有做不到的,只有想不到的![微信图片_20180703092352.jpg(https://upload-images.jianshu…

September 1, 2018 · 1 min · jiezi