
前端,编程语言相关技术专家
北京的夕阳,伴随淡淡的霾殇。从写字楼望去,光线是那么昏黄。没有孤雁,也没有霞光,遥想当年,还是 jQuery 独霸一方。那时的我们,写程序都习惯了使用 $,至少在对美元符号的喜爱上,与 PHP 达成了一致。 当然,我并不讨论语言,我只说前端。 在 React 大行其道的如今,很少再看到 jQuery 的身影,是它离开了我们吗,还是我们选择了不挽留。总之,我们返璞归真,重新写起了原生的 JavaScript,这无疑是原教主义者们的胜利并且值得庆祝的时代。 使用 jQuery,对于 DOM 操作毫不费力。没了 jQuery,好多小伙伴像断臂杨过,生活只能靠姑姑处理。倒不是说原生不能处理,只是方法很繁琐: document.getElementById document.getElementsByClass document.getElementsByName document.getElementsByTagName 方法都很全,像牙科医生的工具包。 好在后来又加上了类 jQuery 的选择方式, document.querySelector document.querySelectorAll 那这样呢我们又能愉快地使用单一的方法进行多种类型的 DOM 选择了。 即使这样,还是给我们留下了一些不爽,那就是名字太长。大家应该都知道电影里反派的统一死法吧————死于话多。所以本着能省则省,能少敲几个字母就绝不多敲的原则,我们很是需要对这些方法进行一次包装,或者说取个别名。对,最好就用熟悉的 $ 。 于是我们说干就干,在不到四分之一柱香的时间,我们撸出了如下代码: var $ = document.querySelectorAll; 以及测试代码: console.debug($('body')); 通过只有少数人才知道的快捷键组合 ⌘+⌥+j,我们娴熟地唤出了浏览器控制台进行测试。 但是测试之后,我们开始怀疑人生。这便是本文存在的意义。它帮妳拨开云雾见日升,拥有不再怀疑的人生。 这里报错的原因是 querySelectorAll 所需的执行上下文必需是 document,而我们赋值到 $ 调用后上下文变成了全局 window。 明白了这个道理后,我们再花不到四分之一柱香的时间,就改写了之前的版本,释出了正确的版本,这个版本里面,我们用正确的姿势去 alias。 var $ = document.querySelectorAll.bind(document); 然后我们再测试,本来这次测试是没有必要的,至少应该像一个信心满满的程序员那样去喝杯咖啡了。 对于 querSelector 同理,它的上下文也是 document。 为了使用方便,我们可以将其他一系列的 DOM 选择方法都给上简写。 var query = document.querySelector.bind(document); var queryAll = document.querySelectorAll.bind(document); var fromId = document.getElementById.bind(document); var fromClass = document.getElementsByClassName.bind(document); var fromTag = document.getElementsByTagName.bind(document); 需要注意的地方是,这些方法返回的要么是单个 Node 节点,要么是 NodeList 而 NodeLis 是类数组的对象,但并不是真正的数组,所以拿到之后不能直接使用 map,forEach 等方法。 正确的操作姿势应该是: Array.prototype.map.call(document.querySelectorAll('button'),function(element,index){ element.onclick = function(){ } }) 相关链接 https://stackoverflow.com/questions/13383886/making-a-short-alias-for-document-queryselectorall
现如今好多浏览器都有「隐身模式」,Safari 管这叫「Private Browing」,国内各种牌子的套壳浏览器叫「无痕浏览」。私以为从命名上来说,倒是国内更中文一些。 这种模式下浏览网页踏雪无痕,雁过不留声。具体来说,与正常模式的区别是浏览器不会保存历史记录,没有页面缓存,所有本地数据也都是临时的,页面关闭后无法还原。譬如本文下面要讲到的 localStorage。 并不是说这种模式下绝对安全,服务器仍然对用户的浏览是有感知的。所以 IP 什么的依然可以追踪。这世界并不如我们天真设想般烂漫。--------- LOG --------- 00:01:00 - 一位不具名用户在零点零一分进行了访问 00:02:00 - 一位不愿透露姓名的用户在零点零二分打开了你丢弃在服务器 `社会科学/东方艺术鉴赏/东瀛国浮世绘` 中的资源 `ae2bx86.jpg` 从功能上来说,普通用户大概鲜有人知道这一功能(产品情怀就这样被用户无视,PM 们默默泪目),而开发者则利用其干净的特点来开发调试,排除程序之外的因素导致 bug 的可能。 因为所有本地数据都是临时的,那么问题来了,如果网页代码中还使用了诸如 localStorage 的本地存储,还能生效吗? 答案是肯定的,但只针对本次访问。这个肯定只限于桌面浏览器。 而手机端则不然。 iOS 上 Safari private 模式下浏览器假装支持 localStorage,并在全局 window 上暴露了该方法。但是当你在调用 localStorage.setItem 进行保存的时候就会报 QUOTA_EXCEEDED_ERR 错。 QUOTA_EXCEEDED_ERR:DOM Exception 22:An attempt was made to add something to storage... 考察下面的测试代码: <button class="setValue">SET</button> <hr> <button class="getValue">GET</button> <script> var q = document.querySelector; document.querySelector('.setValue').onclick = function () { try { var time = new Date().getTime(); localStorage.setItem('time', time); alert('set '+time); } catch (error) { alert(JSON.stringify(error)); } } document.querySelector('.getValue').onclick = function () { var content = localStorage.getItem('time', new Date().getTime()); alert('got '+content); } </script> 我在页面放了两个按钮,一个用于向浏览器保存值,一个用于获取。 下面是测试结果: iOS Safari 隐私模式设置值 iOS Safari 隐私模式获取值 iOS Chrome 隐私模式设置值 iOS Chrome 隐私模式获取值 这表明在 iOS 上,不仅是 Safari 在隐私模式中不能使用 localStorage, Chrome 也不行也不行。这不禁让人怀疑跟系统平台的策略有关。 博主是谷粉,很早就入手了 Nexus。本着严谨的做事态度,那肯定也得拿来测试一下丫。而安卓机上的测试则让人无法接受。 安卓 Chrome 隐私模式下设置值 安卓 Chrome 隐私模式下获取值 是的,安卓上面并没有表现出假装支持 localStorage,而是真正的支持,能存能取,能取能用!再次证实了上面的怀疑,这种假装的支持应该是 iOS 的设计哲学。 回过头来想,隐私模式主要的功能不就是让用户的数据不被追踪吗,如果能够存取数据的话,反而没那么隐私了。从这点来说,localStorage 设置不成功倒也考量了些许人文情怀在里面。 问题想当于回到了开发者手中,我们在开发过程中使用 loaclStorage 就需要对这种情况进行兼容,以避免 js 报错后影响整个页面的功能。 下面是兼容代码示例: function isLocalStorageSupport(){ try { var isSupport = 'localStorage' in window && window['localStorage'] !== null; if (isSupport) { localStorage.setItem('__test', '1'); localStorage.removeItem('__test'); } return isSupport; } catch (e) { return false; } } 为此,我们可以考虑提取一个辅助类来封装 localStorage,这样就可以随时随地放心使用。
数据没有用,我们需要的是数据所反映出来的东西。增长率,排名,占比等。而这些结果是通过分析数据得到的。 从网上搜集到数据后,导入到表格程序中便可以进行方便地分析处理了。下面介绍将网页中的表格数据导入到 Google Sheet 中的操作。 当我尝试去 Google 相关方法的时候,对于这个搜索结果相当的满意。不仅给出了来自 Youtube 的视频教程,还给出了建议观看的位置。这样的产品细节让很多竞品难以匹敌。 Import HTML in Google Docs,你可以自行观看也可以继续阅读本文。 利用 importHTML 公式可以轻松实现将网页中的数据导入到我们的工作表当中。 该公式需要三个入参,分别是: url 导入数据的网页地址 query 指定数据的类型,是页面中的列表(ul,ol)还是表格(table) index 指定需要导入的索引,如果页面中不止一个数据源,则可以通过这个来指明导入第几个 我们以 [List of countries by GDP](https://en.wikipedia.org/wiki/List_of_countries_by_population_(United_Nations)这个来自 Wikipedia 的国家 GDP 排名页面为列,将其中的表格数据进行导入。 在需要导入的单元格里输入以下公式: =importHTML("https://en.wikipedia.org/wiki/List_of_countries_by_GDP_(nominal)","table",3) 然后执行。数据加载完成后,页面中第三个 table 的数据就被导入了。 回顾上面的参数,第一个 url 没问题,就是浏览器地址栏里的,直接复制粘贴。 第二个参数自不必多说,我们需要导入的不是列表,而是table。 而最后个参数为什么是3?因为如果是1的话导入的数据并不正常,所以页面的 HTML 代码中有隐藏的用于布局的 table,我们需要跳过,尝试到3的时候有数据了。 对于没有网页编程相关经验的人来说,总之可以从1开始试,通过导入的结果便可知道是否是想要的数据。 当数据在专业的表格程序中的时候,分析处理起来就很得心应手了。譬如我们觉得表格数据不够直观,可以快速简单点两下就能插入一个地区图。 假设我们想要观察 GDP 排名前20的国家在地图上的分布。首先选中所需数据。 选择Insert->Chart... 在弹出的图表编辑框中,指定图表类型为Geo chart。 数据一下子就直观起来了! 但通过图片看出问题来了,也就是少了些很重要的经济体,譬如兔子,战斗民族。 回头看表格中的数据,China 的名字似乎不对,将数据复制一分出来到 Sheet2,将名字更正一下,再重复上面的步骤。 这是完工后的工作表,前往参观。
话说当时做 APP 时,三月不知肉味,再次将眼光投放前端,有种天上一天,地下一年的感觉。 Flux 是一种思想 了解的最好方式当然是看Flux官方文档了。React 中文站点也能找到对应的翻译版本,但及时性可能无法保证。 Flux不算框架,它是一种编程思想,抑或是一种程序设计范式(Design Pattern),应用架构(Application Architecture),我更习惯称它为一种思想,与前端组件化的编程思想 react 相辅相成。 It's more of a pattern rather than a formal framework, 既然只是一种范式,可以看作是程序编写过程中的一种指导,具体的实现就因人而异了。 现在世面上各种牌子的 Flux 实现都有,选择的空间很大,Which Flux implementation should I use?这个 issue 里倒是列出了一些并附上了 npm 的下载量。 我们注意到 Redux 在其中是比较瞩目的一个,之一。 虽然从最近更新的 Redux 文档来看, Can Redux be considered a Flux implementation? Yes, and no. --- Redux Doc 1.3-Prior Art 对于它是否是 Flux 的一种实现还有争论,但了解 Flux 对于我们搞清楚时下前沿技术,保持技术人员的先进性,使用 Redux,也是有帮助的。但老实说,Flux 早在2014就出现在公众视野了,到现在已算不得有多新。 Flux 与 React React 将界面组件化,并实现了由数据驱动的层叠式更新。这过程中数据成为了程序中最为关键的一环。在传统的 MVC 模式下,React 可以看作是 View 层面的东西,而其他方面,逻辑及状态管理,则需要 React 之外的东西来接管,Flux 应运而生。 单向数据流,这些一核心理念可以和 React 很好地补充。当然也有另一层意思,Flux 不一定与 React 搭配才能用。用户在 React 组件上进行交互,视图将操作通过 Action 的形式由 Dispatcher 进行分发,各个Store 注册并接收,处理自己所负责的 Action,然后将更新重新反映到视图上。 整个流程下来,负责程序某一组件的 Store 只需要发送更新,而不用关心视图怎样根据状态来变更。这种做法很符合 React 的声明式编程风格(Declarative programming style)。 强势插入科普环节 与声明式编程相对应的是命令式编程(Imperative programming),具体来说, 声明式编程是告诉计算机你想要的结果,具体过程由计算机自动完成 命令式编程则是事先告诉计算机操作步骤,你期待的输出则可能会在程序执行完后出现 了解更多可以参见 StackOverflow 的这个提问Difference between declarative and imperative in React.js? 但是为什么说 React 是声明式的编程风格呢?想想用 React 编程的时候,你通过掌控流程和状态,告诉程序此刻应该是什么样子,而根据当前状态各视图要怎样切换,你无需多管,反正最终组合后的结果,就是你想要的样子。 Flux 的组成 是时候祭出这张图了。 Flux 的理念里,包含三个重要组成部分。Actions,Dispatcher 和 Stores。如果有第四个的话,我想可能是 Controller Views。 各部分的关系可从上图看出一二。用户的操作触发 Action,由统一的 Dispatcher 来分发,Store 接收到 Action 后各自进行处理,更新自己的数据和所负责的 View。 Actions “我其实就一普通对象”,面对我们的镜头,Action 耸了耸肩。 是的,Action 就是一个普通的 JS 对象,里面包含了这次动作所携带的新数据。约定Action 对象里包含一个唯一标识该动作的字段(一般用常量表示),这样在 Store 接收到该 Action 时可以用来判断是否需要处理该 Action。 代码来自官方 TODO 示例里面TodoActions.js https://github.com/facebook/flux/blob/master/examples/flux-todomvc/js/actions/TodoActions.js#L20-L25 ... /** * @param {string} text */ create: function(text) { AppDispatcher.dispatch({ actionType: TodoConstants.TODO_CREATE, text: text }); }, ... 可以说,Actions 连接了视图与 Dispathcer,负责发起一个动作来触发数据更新。这个动作可以是来自界面用户的操作,也可以是异步请求的返回。 Action 只是个普通对象,只有将它发送到 Dispathcer 才会发光发热。像上面代码中,创建一个 ToDo Item 的create方法,便是一个封装我们的 Action,将它发送到 Dispathcer 的方法。我们称之为 Action creator。而 TodoActions.js 则是集合了很多 Action creator 的对象。这个 Action 会在用户点击「添加」按钮时调用。 我们当然可以在点击事件里直接调用 Dispather 来发送 Action,但当程序庞大的时候,这样做不利于维护。View 层面不应该直接与 Dispather 打交道,它只需要完成分内的事情:响应交互,从 Store 获取数据渲染自己。而且,这样包装的另一好处是代码更加语义化,一看便知道是创建操作,将 Actoin 的创建融合在一起,也方便管理。 Dispatcher “你一定学过计算机原理,所以你一定知道集成总线(bus)。” 面对我们的镜头,Dispather 侃侃而谈,没有 Action 的拘谨,“对,我就是那根总线。” 一个 Flux 程序中只有唯一一个 Dispatcher。 Store 会在它上面注册回调,它将 Action 分发给所有注册过回调的 Store。它充当中央调度员的角色,管理所有的 Action 分发。 按照官方的解释,之所以 Actions 需要经过一个统一的 Dispather 进行派发,是因为大型项目下,可以方便地管理 Action 之间的依赖。实际使用中,一些操作依赖于另一个操作的完成。当我们的 Action 都经过一个地方处理后,可以很容易实现这样的依赖,Dispather 则会提供waitFor方法供我们使用。 Dispatch 中的依赖管理示例 Store 在向 Dispatcher 注册回调时,会得到一个返回值,这个值是该回调在 Dispatcher 中的索引值,能够唯一标识该回调。 代码来自 Flux 官方文档 PrependedTextStore.dispatchToken = Dispatcher.register(function (payload) { // ... }); 拿到这个索引值,我们便可以在waitFor方法中指定需要等待的操作了。 case 'TODO_CREATE': Dispatcher.waitFor([ PrependedTextStore.dispatchToken, YetAnotherStore.dispatchToken ]); TodoStore.create(PrependedTextStore.getText() + ' ' + action.text); break; 上面的示例中,会在TODO_CREATE的操作会在PrependedTextStore和YetAnotherStore执行完成后才开始执行。 Stores Store 中包含了程序的状态和逻辑。可以类比 MVC 模式中的 Model。但一如官方文档所解释的那样,MVC 中的 Model 更多的是一个单独ORM对象,是对现实世界个体的抽象,比如 Person,Cat。 而 Store 则是可以看成是多种对象的组合,每个对象又只取需要的部分,它负责的是程序中一个组件中状态的管理,所以它里面的状态数据是和所负责的程序区域相关的,而并不是以 ORM 对象为单位的。 如此说来我倒觉得有点像MVVM中的 View Model。 譬如聊天框,它可能包含联系人列表中的数据,用于在输入@的时候进行提示,也会包含富文本对象用于插入种类媒体信息,与此同时,它本身还有一个输入值的模型。总之,这个InputStore里组装了所需的状态。 Store 向 Dispatcher 注册回调,这个回调接收 Action 作为入参。前面说道,Dispatcher接收到 Action 后会分发给所有注册过回调的 Store,所以在 Store 里,一般会有switch语句去与 Action 中的类型进行比较,以判断是否是这个 Store 关心的 Action,Store 只对自己关心的 Action 作出反应,更新状态。 Store 更新自己后,向外派发 change 事件,controller-views 监听change 事件,从 store 获取到新数据,然后派发给所有 View 上的子节点。 关于 Store, 另一个重要的点是:Store 接收 Action 后自己处理内部的逻辑并更新相应的状态数据,一轮更新下来后所有 Store 各自井然有序,内部的状态没有对外暴露,改变的唯一方法就是通过 Action。 状态只在各自的 store 里管理,程序各组件状态的分离,可以达到高度解耦的目的。由单向数据流来驱动,一目了然。而双向绑定及不够细化的各状态,会导致一处变动,很多地方跟着变动,而这些变化都由开发者自己维护,程序复杂后便不太好掌控全局了。 关于 Controller-Views 视图很好理解,如果是用 React,视图便是组件调用形成的树上面的各个绿叶,它们是用户看到的视图。而Controller-Views, 可以结合 React 的Controlled components来理解。虽然是两样东西,但都可以理解为绑定了数据后被数据所控制,所以叫 'controller','controlled'。 严格来说,数据驱动的情况下,谁又不是被数据所绑定和控制的呢,这里 controll-views 更强调的是作为根结点,与数据源打交道的这么一个角色,这么一层视图。它监听来自 Store 的change事件,然后向 Store 获取最新的数据,接下来把数据沿着组件树向下派发,通知各个组件更新。这里就完全进入 React 了,组件内部通过调用 setSate()或者forceUpdate()来重新触发render()方法,以达到视图重新渲染的目的。 Flux 学习资料 Awesome 系列! Github 上的 awesome react 仓库里面,flux 部分。 顺便说一句,如果你要找资料,哪都不用去,直接在 github 上找该技术的 awesome 系列准没错。这个系列的特点是,内容多而全,大都还免费,缺点是在纷繁的世界里需要自己去甄别好与坏,awesome 的不一定都 awesome,适合的才是对味的。 Happy coding :) 参考 Overview from Flux Official Doc Getting To Know Flux, the React.js Architecture
考察下面的 HTML 代码片段: <div> <section>section 1</section> <section>section 2</section> <ul> <li>item 1</li> <li> <ul> <li>sub item 1</li> <li>sub item 2</li> <li>sub item 3</li> </ul> </li> <li>item 3</li> <li>item 4</li> <li>item 5</li> <li>item 6</li> <li>item 7</li> <li>item 8</li> <li>item 9</li> </ul> <section>section 3</section> <section>section 4</section> <section>section 5</section> </div> 单凭 section 可以让我们选中所有的<section> 标签,what if we wanna specific ones? 譬如只选中第一个。 那你可能已经知道:first-child伪类选择器了,所以选中第一个也不是什么麻烦事情。类似地可以用:last-child选中最后一个指定的元素。 section:first-child,section:last-child { color: red; } here comes out the result: 当场景再复杂一些的时候,譬如选中第2个,第3个,第基数个,很自然地,我们会想到引入一个变量来完成任务。 nth 系列荣誉登场 CSS3中的 nth 系列选择器便是这样一种支持变量计算的选择器,可以完成上述复杂的选择需求。 譬如高亮前面示例 HTML 片段中第基数个 section 和 li 标签可以这样做: section:nth-child(2n+1),li:nth-child(2n+1){ color:red; } and here comes out the result again: :nth-child完整的语法为:nth-child(an+b),它匹配父容器下面中第an+b个子元素。例如:nth-child(3n+1)将会选中位置位于第1(3*0+1),4(3*1+1),7(3*2+1)...的元素。 像:nth-child这样厉害的选择器还有3个!它们分别是: :nth-last-child(an+b) 原理同:nth-child,只不过方向相反,从满足条件的兄弟子节点后面开始计数 :nth-of-type(an+b) 匹配第 an+b 个相同标签的元素 :nth-last-of-type(an+b) 同 nth-of-type ,只不过方向相反,从最后开始计数。 借助于这样灵活的选择器,在编写样式时使我们更加得心应手,甚至有了很多花样玩法。 :nth-child :nth-child(an+b) 会匹配所有兄弟节点中位置位于an+b位置的元素。 其中 n 是从0开始的正整数。 除了像前面所说的可以通过完整的表达式匹配到连续规律位置的元素外,如果我们将 a 设为0的话,就可以匹配指定的单个元素。 譬如考察下面的 HTML 片段: <div> <p>foo</p> <p>bar</p> <p>baz</p> </div> 高亮第二个元素: p:nth-child(2){ color: red; } 同理,:nth-child(3) 会选中第三个元素。 这个示例中,也可以用:nth-last-child: p:nth-last-child(2){ color: red; } 效果当然是一样的,因为:nth-last-child(2)从后面开始数第二个,正好与顺位数第二个是同一元素。 :nth-of-type :nth-of-type(an+b)用法上没有区别,但它只会匹配相同标签的兄弟元素。也就是在:nth-child的基础上加了一条限制:标签要一致。 还是考察刚刚的 HTML 片段,我们要选中第二个<p> 标签,仍然是指定位置为2即可: p:nth-of-type(2){ color: red; } 但情况变一下,我们在第2个<p>标签前面加上另外一个元素譬如<section>,考察更新后的 HTML 片段: <div> <p>foo</p> <section>quz</section> <p>bar</p> <p>baz</p> </div> 此时我们仍然想要选中第2个<p>标签。 p:nth-child(2){ color: red;/*会匹配失败,因为第二个子元素不是 p 标签*/ } p:nth-of-type(2){ color: red;/*仍然匹配成功*/ } 此时用:nth-child(2)不会选中任何元素,因为它的意思是选中div下面子元素中的第2个元素,并且这个元素是一个<p> 标签。而上面 HTML 片段中,第二个子元素明显不是<p>标签,所以匹配失败。 而通过:nth-of-type(2)来选择则仍然生效。因为不管第2个<p>元素前面插几个<section>标签,此时内容为bar的<p>标签仍然是父容器所有子节点中顺位第二个类型为<p>的标签。 :nth-child与:nth-of-type的区别 通过前面的示例可以看出,:nth-of-type在你始终需要选择第 N 个特定类型的元素时更为可靠,它首先会提取出所限定的元素类型,然后再从这个没有杂质的集合中去匹配顺序。 因此:nth-of-type在大多数时候可能更满足你的需要,毕竟很多时候需求是选中第3个<span>,第5个<p>。而不是第7个元素,无论是什么类型的节点。 这里有一个页面:nth Tester可以方便地把玩 nth 系列的四大金钢。通过可视化操作应该能够更好地理解。 扩展的花样玩法 前面说道,表达式an+b可以将 a 取值为0,这样就可以选中第 b 个元素。如果将 a 取值为1的话,我们就可以选中从第 b 个元素开始的所有元素。 譬如选中从第三个元素开始的所有<p>标签: <div> <p>1</p> <p>2</p> <p>3</p> <p>4</p> <p>5</p> </div> p:nth-child(n+3){ color: red; } 虽然 n 是从0开始的正整数,但 a 其实可以取负值的。当我们将 a 取值为-1的时候,可以达到只选取前 b 个元素的目的。 示例:选中前3个元素 <div> <p>1</p> <p>2</p> <p>3</p> <p>4</p> <p>5</p> </div> p:nth-child(-n+3){ color: red; } 另外,选取基数和偶数元素时,可以通过指定值为odd与even来完成,这和2n+1与2n效果是一样的。 css 选偶数元素 p:nth-child(2n){ color: red; } /*或者*/ p:nth-child(even){ color: red; } css 选基数元素 p:nth-child(2n+1){ color: red; } /*或者*/ p:nth-child(odd){ color: red; } https://css-tricks.com/useful-nth-child-recipies/ 需要注意的地方 与 class 的搭配 博主在使用过程中刚好遇到一个问题,可以拿出来分享一下。 那就是 nth 系列对元素的类名是不生效的,也就是说它只对标签名起作用,如果你使用时指定为 class 名则不会生效。 譬如考察下面的 HTML 片段与 CSS: <div> <p>1</p> <p class="foo">2</p> <p class="foo">3</p> <p class="foo">4</p> <p class="foo">5</p> </div> /*从带 class 为'foo'的 p 标签中选取第2个将字体设为红色*/ p.foo:nth-of-type(2){ color: red; } /*从带 class 为'foo'的 p 标签中选取第3个将字体设为绿色*/ p.foo:nth-child(3){ color: green; } 上述 CSS 中,我们只希望对带 class 且值为foo的<p>标签进行操作,于是使用了类选择器进行限制,但最终结果其实是这样的: 我们预期值为3的应该为红色,因为它是带 class 且值为foo这种类型里面的第二个,但其实值为2的 <p>标签被选中了。因为第一个不带 class 的 <p> 标签其实也参与了进来,证明 class 选择器其实没有生效,并没有起到限制的作用。 对于:nth-child同理。 推而广之,其实对于其他嵌套 CSS 语法组合(arbitrary selector),譬如属性选择[type=text],:nth-child,:nth-of-type 都是会忽略的。 对于:nth-child,纳入考量的永远是同属同一个父容器下同一级所有的兄弟元素。而对于:nth-of-type来说,则是同一父容器下,同一级所有兄弟元素中标签类型相同的元素。 与 querySelector 的搭配 既然是伪类选择器,所以就无法使用 querySelector 来进行选择。我想你已经读出另外一层意思,即所有伪类选择器在 querySelector 中都不起作用,而不只是 nth 系列。原因见W3C Spec。 浏览器兼容性 拿 :nth-child 为例,nth 系列的浏览器支持情况还是蛮理想的。可以放心使用。 更多资料 MDN :nth-child doc MDN :nth-of-type doc :nth Tester The Difference Between :nth-child and :nth-of-type Useful :nth-child Recipes nth-child doesn't respond to class Can I combine :nth-child() or :nth-of-type() with an arbitrary selector
this 虐我千百遍,看完此文效立见!不得不说,这篇文章的总结很地道很全面,适合收藏之用。 原文:all this 习惯了高级语言的你或许觉得JavaScript中的this跟Java这些面向对象语言相似,保存了实体属性的一些值。其实不然。将它视作幻影魔神比较恰当,手提一个装满未知符文的灵龛。 以下内容我希望广大同行们能够了解。全是掏箱底的干货,其中大部分占用了我很多时间才掌握。 全局this 浏览器宿主的全局环境中,this指的是window对象。 <script type="text/javascript"> console.log(this === window); //true </script> 示例 浏览器中在全局环境下,使用var声明变量其实就是赋值给this或window。 <script type="text/javascript"> var foo = "bar"; console.log(this.foo); //logs "bar" console.log(window.foo); //logs "bar" </script> 示例 任何情况下,创建变量时没有使用var或者let(ECMAScript 6),也是在操作全局this。 <script type="text/javascript"> foo = "bar"; function testThis() { foo = "foo"; } console.log(this.foo); //logs "bar" testThis(); console.log(this.foo); //logs "foo" </script> 示例 Node命令行(REPL)中,this是全局命名空间。可以通过global来访问。 > this { ArrayBuffer: [Function: ArrayBuffer], Int8Array: { [Function: Int8Array] BYTES_PER_ELEMENT: 1 }, Uint8Array: { [Function: Uint8Array] BYTES_PER_ELEMENT: 1 }, ... > global === this true 在Node环境里执行的JS脚本中,this其实是个空对象,有别于global。 console.log(this); console.log(this === global); $ node test.js {} false 当尝试在Node中执行JS脚本时,脚本中全局作用域中的var并不会将变量赋值给全局this,这与在浏览器中是不一样的。 var foo = "bar"; console.log(this.foo); $ node test.js undefined ...但在命令行里进行求值却会赋值到this身上。 > var foo = "bar"; > this.foo bar > global.foo bar 在Node里执行的脚本中,创建变量时没带var或let关键字,会赋值给全局的global但不是this(译注:上面已经提到this和global不是同一个对象,所以这里就不奇怪了)。 foo = "bar"; console.log(this.foo); console.log(global.foo); $ node test.js undefined bar 但在Node命令行里,就会赋值给两者了。 译注:简单来说,Node脚本中global和this是区别对待的,而Node命令行中,两者可等效为同一对象。 函数或方法里的this 除了DOM的事件回调或者提供了执行上下文(后面会提到)的情况,函数正常被调用(不带new)时,里面的this指向的是全局作用域。 <script type="text/javascript"> foo = "bar"; function testThis() { this.foo = "foo"; } console.log(this.foo); //logs "bar" testThis(); console.log(this.foo); //logs "foo" </script> 示例 foo = "bar"; function testThis () { this.foo = "foo"; } console.log(global.foo); testThis(); console.log(global.foo); $ node test.js bar foo 还有个例外,就是使用了"use strict";。此时this是undefined。 <script type="text/javascript"> foo = "bar"; function testThis() { "use strict"; this.foo = "foo"; } console.log(this.foo); //logs "bar" testThis(); //Uncaught TypeError: Cannot set property 'foo' of undefined </script> 示例 当用调用函数时使用了new关键字,此刻this指代一个新的上下文,不再指向全局this。 <script type="text/javascript"> foo = "bar"; function testThis() { this.foo = "foo"; } console.log(this.foo); //logs "bar" new testThis(); console.log(this.foo); //logs "bar" console.log(new testThis().foo); //logs "foo" </script> 示例 通常我将这个新的上下文称作实例。 原型中的this 函数创建后其实以一个函数对象的形式存在着。既然是对象,则自动获得了一个叫做prototype的属性,可以自由地对这个属性进行赋值。当配合new关键字来调用一个函数创建实例后,此刻便能直接访问到原型身上的值。 function Thing() { console.log(this.foo); } Thing.prototype.foo = "bar"; var thing = new Thing(); //logs "bar" console.log(thing.foo); //logs "bar" 示例 当通过new的方式创建了多个实例后,他们会共用一个原型。比如,每个实例的this.foo都返回相同的值,直到this.foo被重写。 function Thing() { } Thing.prototype.foo = "bar"; Thing.prototype.logFoo = function () { console.log(this.foo); } Thing.prototype.setFoo = function (newFoo) { this.foo = newFoo; } var thing1 = new Thing(); var thing2 = new Thing(); thing1.logFoo(); //logs "bar" thing2.logFoo(); //logs "bar" thing1.setFoo("foo"); thing1.logFoo(); //logs "foo"; thing2.logFoo(); //logs "bar"; thing2.foo = "foobar"; thing1.logFoo(); //logs "foo"; thing2.logFoo(); //logs "foobar"; 示例 在实例中,this是个特殊的对象,而this自身其实只是个关键字。你可以把this想象成在实例中获取原型值的一种途径,同时对this赋值又会覆盖原型上的值。完全可以将新增的值从原型中删除从而将原型还原为初始状态。 function Thing() { } Thing.prototype.foo = "bar"; Thing.prototype.logFoo = function () { console.log(this.foo); } Thing.prototype.setFoo = function (newFoo) { this.foo = newFoo; } Thing.prototype.deleteFoo = function () { delete this.foo; } var thing = new Thing(); thing.setFoo("foo"); thing.logFoo(); //logs "foo"; thing.deleteFoo(); thing.logFoo(); //logs "bar"; thing.foo = "foobar"; thing.logFoo(); //logs "foobar"; delete thing.foo; thing.logFoo(); //logs "bar"; 示例 ...或者不通过实例,直接操作函数的原型。 function Thing() { } Thing.prototype.foo = "bar"; Thing.prototype.logFoo = function () { console.log(this.foo, Thing.prototype.foo); } var thing = new Thing(); thing.foo = "foo"; thing.logFoo(); //logs "foo bar"; 示例 同一函数创建的所有实例均共享一个原型。如果你给原型赋值了一个数组,那么所有实例都能获取到这个数组。除非你在某个实例中对其进行了重写,实事上是进行了覆盖。 function Thing() { } Thing.prototype.things = []; var thing1 = new Thing(); var thing2 = new Thing(); thing1.things.push("foo"); console.log(thing2.things); //logs ["foo"] 示例 通常上面的做法是不正确的(译注:改变thing1的同时也影响了thing2)。如果你想每个实例互不影响,那么请在函数里创建这些值,而不是在原型上。 function Thing() { this.things = []; } var thing1 = new Thing(); var thing2 = new Thing(); thing1.things.push("foo"); console.log(thing1.things); //logs ["foo"] console.log(thing2.things); //logs [] 示例 多个函数可以形成原型链,这样this便会在原型链上逐步往上找直到找到你想引用的值。 function Thing1() { } Thing1.prototype.foo = "bar"; function Thing2() { } Thing2.prototype = new Thing1(); var thing = new Thing2(); console.log(thing.foo); //logs "bar" 示例 很多人便是利用这个特性在JS中模拟经典的对象继承。 注意原型链底层函数中对this的操作会覆盖上层的值。 function Thing1() { } Thing1.prototype.foo = "bar"; function Thing2() { this.foo = "foo"; } Thing2.prototype = new Thing1(); function Thing3() { } Thing3.prototype = new Thing2(); var thing = new Thing3(); console.log(thing.foo); //logs "foo" 示例 我习惯将赋值到原型上的函数称作方法。上面某些地方便使用了方法这样的字眼,比如logFoo方法。这些方法中的this同样具有在原型链上查找引用的魔力。通常将最初用来创建实例的函数称作构造函数。 原型链方法中的this是从实例中的this开始住上查找整个原型链的。也就是说,如果原型链中某个地方直接对this进行赋值覆盖了某个变量,那么我们拿到 的是覆盖后的值。 function Thing1() { } Thing1.prototype.foo = "bar"; Thing1.prototype.logFoo = function () { console.log(this.foo); } function Thing2() { this.foo = "foo"; } Thing2.prototype = new Thing1(); var thing = new Thing2(); thing.logFoo(); //logs "foo"; 示例 在JavaScript中,函数可以嵌套函数,也就是你可以在函数里面继续定义函数。但内层函数是通过闭包获取外层函数里定义的变量值的,而不是直接继承this。 function Thing() { } Thing.prototype.foo = "bar"; Thing.prototype.logFoo = function () { var info = "attempting to log this.foo:"; function doIt() { console.log(info, this.foo); } doIt(); } var thing = new Thing(); thing.logFoo(); //logs "attempting to log this.foo: undefined" 示例 上面示例中,doIt 函数中的this指代是全局作用域或者是undefined如果使用了"use strict";声明的话。对于很多新手来说,理解这点是非常头疼的。 还有更奇葩的。把实例的方法作为参数传递时,实例是不会跟着过去的。也就是说,此时方法中的this在调用时指向的是全局this或者是undefined在声明了"use strict";时。 function Thing() { } Thing.prototype.foo = "bar"; Thing.prototype.logFoo = function () { console.log(this.foo); } function doIt(method) { method(); } var thing = new Thing(); thing.logFoo(); //logs "bar" doIt(thing.logFoo); //logs undefined 示例 所以很多人习惯将this缓存起来,用个叫self或者其他什么的变量来保存,以将外层与内层的this区分开来。 function Thing() { } Thing.prototype.foo = "bar"; Thing.prototype.logFoo = function () { var self = this; var info = "attempting to log this.foo:"; function doIt() { console.log(info, self.foo); } doIt(); } var thing = new Thing(); thing.logFoo(); //logs "attempting to log this.foo: bar" 示例 ...但上面的方式不是万能的,在将方法做为参数传递时,就不起作用了。 function Thing() { } Thing.prototype.foo = "bar"; Thing.prototype.logFoo = function () { var self = this; function doIt() { console.log(self.foo); } doIt(); } function doItIndirectly(method) { method(); } var thing = new Thing(); thing.logFoo(); //logs "bar" doItIndirectly(thing.logFoo); //logs undefined 示例 解决方法就是传递的时候使用bind方法显示指明上下文,bind方法是所有函数或方法都具有的。 function Thing() { } Thing.prototype.foo = "bar"; Thing.prototype.logFoo = function () { console.log(this.foo); } function doIt(method) { method(); } var thing = new Thing(); doIt(thing.logFoo.bind(thing)); //logs bar 示例 同时也可以使用apply或call 来调用该方法或函数,让它在一个新的上下文中执行。 function Thing() { } Thing.prototype.foo = "bar"; Thing.prototype.logFoo = function () { function doIt() { console.log(this.foo); } doIt.apply(this); } function doItIndirectly(method) { method(); } var thing = new Thing(); doItIndirectly(thing.logFoo.bind(thing)); //logs bar 示例 使用bind可以任意改变函数或方法的执行上下文,即使它没有被绑定到一个实例的原型上。 function Thing() { } Thing.prototype.foo = "bar"; function logFoo(aStr) { console.log(aStr, this.foo); } var thing = new Thing(); logFoo.bind(thing)("using bind"); //logs "using bind bar" logFoo.apply(thing, ["using apply"]); //logs "using apply bar" logFoo.call(thing, "using call"); //logs "using call bar" logFoo("using nothing"); //logs "using nothing undefined" 示例 避免在构造函数中返回作何东西,因为返回的东西可能覆盖本来该返回的实例。 function Thing() { return {}; } Thing.prototype.foo = "bar"; Thing.prototype.logFoo = function () { console.log(this.foo); } var thing = new Thing(); thing.logFoo(); //Uncaught TypeError: undefined is not a function 示例 但,如果你在构造函数里返回的是个原始值比如字符串或者数字什么的,上面的错误就不会发生了,返回语句将被忽略。所以最好别在一个将要通过new来调用的构造函数中返回作何东西,即使你是清醒的。如果你想实现工厂模式,那么请用一个函数来创建实例,并且不通过new来调用。当然这只是个人建议。 诚然,你也可以使用Object.create从而避免使用new。这样也能创建一个实例。 function Thing() { } Thing.prototype.foo = "bar"; Thing.prototype.logFoo = function () { console.log(this.foo); } var thing = Object.create(Thing.prototype); thing.logFoo(); //logs "bar" 示例 这种方式不会调用该构造函数。 function Thing() { this.foo = "foo"; } Thing.prototype.foo = "bar"; Thing.prototype.logFoo = function () { console.log(this.foo); } var thing = Object.create(Thing.prototype); thing.logFoo(); //logs "bar" 示例 正因为Object.create没有调用构造函数,这在当你想实现一个继承时是非常有用的,随后你可能想要重写构造函数。 function Thing1() { this.foo = "foo"; } Thing1.prototype.foo = "bar"; function Thing2() { this.logFoo(); //logs "bar" Thing1.apply(this); this.logFoo(); //logs "foo" } Thing2.prototype = Object.create(Thing1.prototype); Thing2.prototype.logFoo = function () { console.log(this.foo); } var thing = new Thing2(); 示例 对象中的this 可以在对象的任何方法中使用this来访问该对象的属性。这与用new得到的实例是不一样的。 var obj = { foo: "bar", logFoo: function () { console.log(this.foo); } }; obj.logFoo(); //logs "bar" 示例 注意这里并没有使用new,也没有用Object.create,更没有函数的调用来创建对象。也可以将函数绑定到对象,就好像这个对象是一个实例一样。 var obj = { foo: "bar" }; function logFoo() { console.log(this.foo); } logFoo.apply(obj); //logs "bar" 示例 此时使用this没有向上查找原型链的复杂工序。通过this所拿到的只是该对象身上的属性而以。 var obj = { foo: "bar", deeper: { logFoo: function () { console.log(this.foo); } } }; obj.deeper.logFoo(); //logs undefined 示例 也可以不通过this,直接访问对象的属性。 var obj = { foo: "bar", deeper: { logFoo: function () { console.log(obj.foo); } } }; obj.deeper.logFoo(); //logs "bar" 示例 DOM 事件回调中的this 在DOM事件的处理函数中,this指代的是被绑定该事件的DOM元素。 function Listener() { document.getElementById("foo").addEventListener("click", this.handleClick); } Listener.prototype.handleClick = function (event) { console.log(this); //logs "<div id="foo"></div>" } var listener = new Listener(); document.getElementById("foo").click(); 示例 ...除非你通过bind人为改变了事件处理器的执行上下文。 function Listener() { document.getElementById("foo").addEventListener("click", this.handleClick.bind(this)); } Listener.prototype.handleClick = function (event) { console.log(this); //logs Listener {handleClick: function} } var listener = new Listener(); document.getElementById("foo").click(); 示例 HTML中的this HTML标签的属性中是可能写JS的,这种情况下this指代该HTML元素。 <div id="foo" onclick="console.log(this);"></div> <script type="text/javascript"> document.getElementById("foo").click(); //logs <div id="foo"... </script> 示例 重写this 无法重写this,因为它是一个关键字。 function test () { var this = {}; // Uncaught SyntaxError: Unexpected token this } 示例 eval中的this eval 中也可以正确获取当前的 this。 function Thing () { } Thing.prototype.foo = "bar"; Thing.prototype.logFoo = function () { eval("console.log(this.foo)"); //logs "bar" } var thing = new Thing(); thing.logFoo(); 示例 这里存在安全隐患。最好的办法就是避免使用eval。 使用Function关键字创建的函数也可以获取this: function Thing () { } Thing.prototype.foo = "bar"; Thing.prototype.logFoo = new Function("console.log(this.foo);"); var thing = new Thing(); thing.logFoo(); //logs "bar" 示例 使用with时的this 使用with可以将this人为添加到当前执行环境中而不需要显示地引用this。 function Thing () { } Thing.prototype.foo = "bar"; Thing.prototype.logFoo = function () { with (this) { console.log(foo); foo = "foo"; } } var thing = new Thing(); thing.logFoo(); // logs "bar" console.log(thing.foo); // logs "foo" 示例 正如很多人认为的那样,使用with是不好的,因为会产生歧义。 jQuery中的this 一如HTML DOM元素的事件回调,jQuery库中大多地方的this也是指代的DOM元素。页面上的事件回调和一些便利的静态方法比如$.each 都是这样的。 <div class="foo bar1"></div> <div class="foo bar2"></div> <script type="text/javascript"> $(".foo").each(function () { console.log(this); //logs <div class="foo... }); $(".foo").on("click", function () { console.log(this); //logs <div class="foo... }); $(".foo").each(function () { this.click(); }); </script> 示例 传递 this 如果你用过underscore.js或者lo-dash你便知道,这两个库中很多方法你可以传递一个参数来显示指定执行的上下文。比如_.each。自ECMAScript 5 标准后,一些原生的JS方法也允许传递上下文,比如forEach。事实上,上文提到的bind,apply还有call 已经给我们手动指定函数执行上下文的能力了。 function Thing(type) { this.type = type; } Thing.prototype.log = function (thing) { console.log(this.type, thing); } Thing.prototype.logThings = function (arr) { arr.forEach(this.log, this); // logs "fruit apples..." _.each(arr, this.log, this); //logs "fruit apples..." } var thing = new Thing("fruit"); thing.logThings(["apples", "oranges", "strawberries", "bananas"]); 示例 这样可以使得代码简洁些,不用层层嵌套bind,也不用不断地缓存this。 一些编程语言上手很简单,比如Go语言手册可以被快速读完。然后你差不多就掌握这门语言了,只是在实战时会有些小的问题或陷阱在等着你。 而JavaScript不是这样的。手册难读。非常多缺陷在里面,以至于人们抽离出了它好的部分(The Good Parts)。最好的文档可能是MDN上的了。所以我建议你看看他上面关于this的介绍,并且始终在搜索JS相关问题时加上"mdn" 来获得最好的文档资料。静态代码检查也是个不错的工具,比如jshint。 欢迎勘误及讨论,我的推特@bjorntipling。
本文前戏较多,务实的同学可以直接跳到结论。 由「钢的琴」网友脑洞大开延伸出了吉的他二的胡琵的琶,以及后来许嵩的「苏格拉没有底」,是否可以再拓展一下,得到哥本不爱吃哈根,哈根爱达斯等剧情乱入的关系。 上面跟本文要讨论的主题有什么关系? 没关系。 缘起 有用户反馈内部MIS系统慢,页面加载耗时长。前端同学们开组会提及此事,如何解决慢的问题。 最致命的是:偶发!你不能准确知道它抽风的时间点,无法在想要追查问题的时候必现它。 这只是一方面,另外,慢的可能实在太多了,那么问题来了,是前端导致的还是后端的问题? 对慢的定义也有待商榷,多久算慢?如果这个页面加载大量数据耗时增加那我认为这是正常的。但这个时限超过了一个合理的自然值,就变得不那么正常了,比如四五十秒,一分多钟。 最奇葩的是,如此久的耗时居然不会报超时错误,而是拿到正确返回后将页面呈现了出来! 可能的原因 初步猜测 初步的猜测可能是后端迟迟未返回造成浏览器处于等待状态。这个猜测是很合乎逻辑的,至少能够很合理地解释Chrome Dev Tool 网络面板中我们看到的状态pending。 但我们不能停留在猜测阶段,要用事实说话,数据才不会骗人。这也正是本文将要展开的。 下面是另外一些被提出来的可能性。 Angular Angular首当其冲。为什么,因为这个问题出现在后台MIS系统中,且这些系统多用Angular开发。 Angular :怪我咯。 因为问题多出现在基于Angular的MIS系统中,并且Angular的性能一直是被诟病的,所以听到不少的声音将矛头指向Angular。这似乎没什么好庇护的。Angular在整个项目中的前端部分扮演了很重的角色。树大招风,理所当然。 这让我想起初次接触到这个问题时,那是在七月份,芙蓉的爱马仕平台用户反馈了慢的问题,报到前端,顺便看了下,一看Pending状态,觉得是后端未返回。只是情深缘浅当时也没有深入,同时洪堂大神负责去追查了。当时那个系统,很负责地说,没有用Angular。 所以这里可以为Angular正身,将其排除。 内部封装的commonResource库 内部对Angular原生的resource进行了封装,做了些数据的转换处理。既然上面Angular都被正身了,那么这里的怀疑也是站不住脚的。 Chrome插件 经查,网上好多呼声有说是Adblock等与网络有关的Chrome插件。可我不使用它已经很多年,那玩意儿太重,后来找到了算法更高级体量更轻便的µBlock。关键是后者也在我使用一段时间后放弃了,因为个人觉悟提高了,免费内容是需要广告支撑的,如果你不希望付费变成强制的话。所以现在一直是处于未开这类插件的状态。那么在未开广告屏蔽插件的情况下重现了问题,可以排除这类插件的影响了。 关于插件,此刻我的Chrome里唯一还会接管Chrome网络的便是代理插件SwitchSharp, 升级之后这货叫Switchy哦卖喝(与时俱进的我当然使用的是后者)。 Chrome独家? 因为内部MIS只兼容了Chrome开发,所以不会有在除了Chrome之外的浏览器上使用的场景,并且其他浏览器上面追查问题也是很痛苦的事情。这里仅在火狐里进行了少量尝试,未复现。同时接到反馈,Safari里也未复现。但也不能肯定就只有Chrome存在问题。似乎这个对于问题的解决还不那么重要,所以先不管。 杀毒软件 后面会看到,在追查错误号ERR_CONNECTION_RESET时引出了杀毒软件可能会导致Chrome工作不正常的情况,但这个可能也在稍后被排除人。 并且,我厂使用Mac的同学并没有安装杀软,依然是可以复现的。 重现 第一件事情便是重现。虽然是偶发,为了尽可能保存现场,还是想要手动将它刷出来。天不灭我,经过良久尝试,该问题被复现。于是各种截图,保存请求数据。这个时候还没有开启chrome://net-internals/#events页面来捕获事件日志。 为以后引用方便,这里留下版本信息: OS: Windows 7 Ultimate Chrome:Version 39.0.2171.95 m 这是请求Pending时的请求信息: 这是请求成功返回后: 可以看到Stalled了1分多钟。神奇的是竟然不报超时错误而是成功返回了。 同时保存了请求头,响应头,还将本次问题请求保存成了CURL等。现场已经留下,感觉Bug不会存活太久了。 接下来就是对比正常请求跟这次异常请求的不同,一轮比较下来,未发现多少异常。 常态与变态的对比 请求头对比: 请求头的对比已丢失,但除了时间外,其余无差别。 响应头对比: 返回结果对比: 上面的对比意义不大,但还是要做的,万一发现有价值的情报了呢。 一次失败的尝试 解决问题时,习惯性地看有没有人已经碰过到类似问题,这样做的好处很明显: 如果有,站在巨人的肩上轻松地牛逼着; 如果没有,这是个机会。 于是信心满满地出发了,因为根据一条互联网准则,70%的问题已经有人解决过了,那些没有被解决的要么是现有技术达不到,要么是未被人发现。所以能够搜索出问题答案的概率还是蛮大的。 经过旷日持久的搜索,有价值的参考寥寥无几。可能是问题本身太过奇葩,遇到的人太少;也有可能问题过于晦涩,无法表述;抑或我搜索的关键词不够精准。 倒也不是说一个都没找到,但一般涉及网络日志的情况就无人问津了,无人问津了! 比如这个,一年多前被人问的,现在还没有一个回答。 还比如这个 Chrome stalls when making multiple requests to same resource? 是后来作为参考的,也是无人问津了…… 甚至自己也去问了一个,依然无人问津了…… 神秘的CACHE LOCK 上面提到,Stackoverflow上找到一个问题,跟现在需要解决一有些类似点: 偶发,并不是必然出现的。这里我们的问题也是偶发,很难复现,需要反复刷。 也是请求被Pending了很久,从请求的时间线来看,体现在Stalled上。 这一刻,有一种感觉大概是这样的: 伟大的意大利的左后卫!他继承了意大利的光荣的传统。法切蒂、卡布里尼、马尔蒂尼在这一刻灵魂附体!格罗索一个人他代表了意大利足球悠久的历史和传统,在这一刻他不是一个人在战斗,他不是一个人! 突然看到了希望。该提问到没有给出什么建设性的意见,但它后面的追加编辑却给出了答案。过程是查看Chrome的网络日志,在事件里面发现有一个超时错误: t=33627 [st= 5] HTTP_CACHE_ADD_TO_ENTRY [dt=20001] --> net_error = -409 (ERR_CACHE_LOCK_TIMEOUT) 耗时20秒之久!而且写得非常明显是ERR_CACHE_LOCK_TIMEOUT。根据提问者贴出来的链接,了解到Chrome有一个缓存锁的机制。 具体源于一个今年6月分实现的一个补丁,加入了这么个机制,而这个机制的引入又源于2010年的一个issue。具体信息可以通过这个这里查看,下面引用如下。 Basically here is the situation: The site author has a long-lived XHR being used to stream a slow response from the server. This XHR response is cachable (it is just really slow). They kick off the XHR asynchronously, and as data slowly arrives on it, update the progressive load of the webpage. Cool. Now what happens if you try to load this page in multiple tabs of Chrome is: The first page starts to load just fine, but the second one does nothing. What has happened, is the background XHR of the first page load has acquired an exclusive lock to the cache entry, and the background XHR of the second page is stalled at "Waiting for cache..." trying to get a reader access to the cache entry. Since the first request can takes minutes, this is a problem. eroman 同学指出了这么一个事实: 浏览器对一个资源发起请求前,会先检查本地缓存,此时这个请求对该资源对应的缓存的读写是独占的。那么问题来了,试想一下,当我新开一个标签尝试访问同一个资源的时候,这次请求也会去读取这个缓存,假设之前那次请求很慢,耗时很久,那么后来这次请求因为无法获取对该缓存的操作权限就一直处于等待状态。这样很不科学。于是有人建议优化一下。也就是上面所描述的那样。 随着问题的提出,还出了两种可能的实现方案。 (a) [Flexible but complicated] Allow cache readers WHILE writing is in progress. This way the first request could still have exclusive access to the cache entry, but the second request could be streamed the results as they get written to the cache entry. The end result is the second page load would mirror the progress of the first one. (a) [Naive but simpler] Have a timeout on how long we will block readers waiting for a cache entry before giving up and bypassing the cache. 我猜上面第二个(a)应该是(b)。简单说第一种优化方案更加复杂但科学。之前的请求对缓存仍然是独占的,但随着前一次请求不断对缓存进行更新,可以把已经更新的部分拿给后面的请求读取,这样就不会完全阻塞后面的请求了。 第二种方案则更加简单暴力。给后来的请求设定一个读取缓存超时的时限,如果超过了这个时限,我认为缓存不可用或者本地没有缓存,忽略这一步直接发请求。 于是Chromium的开发者们选择了后者简单的实现。也就是Issue 345643003: Http cache: Implement a timeout for the cache lock 这个提交里的实现。 这个提交的描述如下: The cache has a single writer / multiple reader lock to avoid downloading the same resource n times. However, it is possible to block many tabs on the same resource, for instance behind an auth dialog. This CL implements a 20 seconds timeout so that the scenario described in the bug results in multiple authentication dialogs (one per blocked tab) so the user can know what to do. It will also help with other cases when the single writer blocks for a long time. The timeout is somewhat arbitrary but it should allow medium size resources to be downloaded before starting another request for the same item. The general solution of detecting progress and allow readers to start before the writer finishes should be implemented on another CL. 于是就产生了上面题主遇到的情况。 所以他的解决方法就很明朗了,对请求加个时间戳让其变得唯一,或者服务器响应头设置为无缓存。Both will work! 那么我们的问题也会是这样的么?我幻想由于某种未知的原因造成之前的请求不正常(虽然网络面板里没有数据证明这样的阻塞请求在问题请求之前存在),然后我们的MIS里打开页面时读取不到缓存,卡了,一会儿缓存好了,正常了,于是在等待了几十秒后请求成功发出去了。 似乎不太可能。因为恰好内部MIS系统的响应头里已经加了缓存控制了 Cache-Control: no-cache。 以下是一次问题请求的响应头: HTTP/1.1 200 OK Date: Wed, 31 Dec 2014 11:47:21 GMT Content-Type: application/json; charset=UTF-8 Transfer-Encoding: chunked Connection: keep-alive Expires: Thu, 19 Nov 1981 08:52:00 GMT Pragma: no-cache Cache-Control: no-cache tracecode: 28410188240979065866123119 tracecode: 28410188240506537994123119 Server: Apache 并且开多个标签也是无法进行有效重现的。 因此可以排除缓存的干扰。那么似乎这里的缓存锁并不是导致问题的原因,只能另寻他路。不得不说,高兴过后有点失望。 八卦时间 可喜的是,在细细口味了上面缓存机制引入的过程后,真是耐人寻味。这里不妨八卦一下。相信你也注意到了,上面提到,该缓存问题的提出是在2010年,确切地说是Jun 8, 2010。是的,2010年6月8日由eroman 同学提出。但最后针对该问题进行修复的代码提交却是在今年6月份,2014年6月24日,提交时间摆在那里我会乱说? 于是好奇为什么会拖了这么久,遂跟了一下该问题下面的回复看看发生了什么。简直惊呆了。 同月14号,有了首次对这个问题的回复,那是将该问题指派给了rvargas同学。 一个月过去了,也就是7月15号,rvargas同学指出了与该问题关联的另外一个issue「issue 6697」 接下来是8月5日,rvargas同学为该问题贴上了标签-Mstone-7 Mstone-8,表明将会在里程碑7或者8里面进行修复。但在后面的10月7日,这个日程又被推到了-Mstone-8 Mstone-9。 再接下来11月5日,有人表示以目前的速度及bug数量,还没有时间来修复它,重点在处理优先级为p1的问题上。于是此问题又成功被顺延了,来到-mstone-9 Mstone-10,同时优级降为p2。Chromium人手也不够啊,看来。 时间来到12月9日,因为优先级为p2的issue如果没有被标为开始状态的话又自动推到下一个里程碑了,于是顺利来到 -Mstone-10 MovedFrom-10 Mstone-11。次年2月来到-Mstone-11 Mstone-12。完成了一次跨年! ………… 上面省略N步。如此反复,最后一次被推到了-Mstone-16,那是在2011年10月12日。 时间一晃来到2013年,这一年很平静,前面的几个月都没有人对此问题进行回复。直到11月27日,有人看不下去了,评论道: This bug has been pushed to the next mstone forever...and is blocking more bugs (e.g https://code.google.com/p/chromium/issues/detail?id=31014)and use-cases same video in 2 tags on one page, and adaptive bit rate html5 video streaming whenever that will kick in. Any chance this will be prioritized? 由于这个bug的无限后延也阻塞了另外一些同类问题,看来是时候解决了。这不,最初的owner 当天就进行了回复: ecently there was someone looking at giving it another try... I'd have to see if there was any progress there. If not, I may try to fix it in Q1. 最后一句亮瞎。敢情这之前owner就没有想过要去真正解决似的,因为有其他人在看这个问题了,所以就没管了,如果Q1还没人解决的话,我会出手的!嗯,就是这个意思。 ………… 最后,也就是上文提到的,2014年6月,还是rvargas同学对这个问题进行了修复,实现了对缓存读取20秒超时的控制。 该问题就是这样从2010来到2014的。我怀疑Chrome是如何成为版本帝的。 阶段总结 仅有的希望到此似乎都没有了。不过前面的努力也不是没有作何收获,至少我得到了以下有价值的信息: 谷歌的神坛光环不再那么耀眼,他们的产品也是有Bug的 Chrome 处理issue的效率,当然不排除这种大型项目bug数量跟人力完全不匹配的情况 受上面Stackoverflow问题的启发,接下来我将重点转移到了针对出问题请求的日志分析上,并且取得了突破 开始新的征程 虽然上面的努力没能定位到问题,但作为这次对解决这次问题的尝试,还是将它记录了下来,估且称作「旧的回忆」吧。 下面开始「新的征程」。 再次重现 这次受到上面的启发,开启chrome://net-internals/#events页面来捕获事件日志。看是否有错误或异常发生。 再次经过旷日持久的机械操作,重现了!这次,日志在手,天下我有。感觉Bug不会存活多久了。 Chrome Dev Tools 网络面板截图: 由上面的截图看到,本次出问题的请求总耗时42.74秒。 问题请求的时间线信息截图: 可以预见,通过捕获的日志完全可以看到Stalled那么久都发生了些什么鬼。 话不多说,切换到事件捕获页面,定位到出问题的请求,查看其详情。同时将该日志导出,永久保存!作为纪念,也方便以后再次导入查看。有兴趣的同学可以访问下方下载后进行导入,就可以清晰地查看到现场了,就好像你亲历了整个犯罪现场一样。 日志还原 下载该日志文件 在Chrome新开一个标签输入chrome://net-internals/#events 切换到Import,选择刚才下载的JSON文件进行导入 切换到Events,定位到http://qa.tieba.baidu.com/release/getReleaseHistory?projectId=fum1.0.593 这个请求 此刻右边出现的便是该问题请求的详细日志。 日志解读 下面不妨把日志文件贴出来先: 193486: URL_REQUEST http://qa.tieba.baidu.com/release/getReleaseHistory?projectId=fum1.0.593 Start Time: 2015-01-02 17:51:05.323 t= 1 [st= 0] +REQUEST_ALIVE [dt=42741] t= 1 [st= 0] URL_REQUEST_DELEGATE [dt=0] t= 1 [st= 0] +URL_REQUEST_START_JOB [dt=42740] --> load_flags = 339804160 (BYPASS_DATA_REDUCTION_PROXY | MAYBE_USER_GESTURE | REPORT_RAW_HEADERS | VERIFY_EV_CERT) --> method = "GET" --> priority = "LOW" --> url = "http://qa.tieba.baidu.com/release/getReleaseHistory?projectId=fum1.0.593" t= 2 [st= 1] URL_REQUEST_DELEGATE [dt=0] t= 2 [st= 1] HTTP_CACHE_GET_BACKEND [dt=0] t= 2 [st= 1] HTTP_CACHE_OPEN_ENTRY [dt=0] t= 2 [st= 1] HTTP_CACHE_ADD_TO_ENTRY [dt=0] t= 2 [st= 1] HTTP_CACHE_READ_INFO [dt=0] t= 2 [st= 1] URL_REQUEST_DELEGATE [dt=0] t= 2 [st= 1] +HTTP_STREAM_REQUEST [dt=2] t= 4 [st= 3] HTTP_STREAM_REQUEST_BOUND_TO_JOB --> source_dependency = 193488 (HTTP_STREAM_JOB) t= 4 [st= 3] -HTTP_STREAM_REQUEST t= 4 [st= 3] +HTTP_TRANSACTION_SEND_REQUEST [dt=0] t= 4 [st= 3] HTTP_TRANSACTION_SEND_REQUEST_HEADERS --> GET /release/getReleaseHistory?projectId=fum1.0.593 HTTP/1.1 Host: qa.tieba.baidu.com Connection: keep-alive Accept: application/json, text/plain, */* User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36 Referer: http://qa.tieba.baidu.com/project/ Accept-Encoding: gzip, deflate, sdch Accept-Language: en-US,en;q=0.8 Cookie: [268 bytes were stripped] t= 4 [st= 3] -HTTP_TRANSACTION_SEND_REQUEST t= 4 [st= 3] +HTTP_TRANSACTION_READ_HEADERS [dt=21301] t= 4 [st= 3] HTTP_STREAM_PARSER_READ_HEADERS [dt=21301] --> net_error = -101 (ERR_CONNECTION_RESET) t=21305 [st=21304] HTTP_TRANSACTION_RESTART_AFTER_ERROR --> net_error = -101 (ERR_CONNECTION_RESET) t=21305 [st=21304] -HTTP_TRANSACTION_READ_HEADERS t=21305 [st=21304] +HTTP_STREAM_REQUEST [dt=3] t=21307 [st=21306] HTTP_STREAM_REQUEST_BOUND_TO_JOB --> source_dependency = 193494 (HTTP_STREAM_JOB) t=21308 [st=21307] -HTTP_STREAM_REQUEST t=21308 [st=21307] +HTTP_TRANSACTION_SEND_REQUEST [dt=3] t=21308 [st=21307] HTTP_TRANSACTION_SEND_REQUEST_HEADERS --> GET /release/getReleaseHistory?projectId=fum1.0.593 HTTP/1.1 Host: qa.tieba.baidu.com Connection: keep-alive Accept: application/json, text/plain, */* User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36 Referer: http://qa.tieba.baidu.com/project/ Accept-Encoding: gzip, deflate, sdch Accept-Language: en-US,en;q=0.8 Cookie: [268 bytes were stripped] t=21311 [st=21310] -HTTP_TRANSACTION_SEND_REQUEST t=21311 [st=21310] +HTTP_TRANSACTION_READ_HEADERS [dt=21304] t=21311 [st=21310] HTTP_STREAM_PARSER_READ_HEADERS [dt=21304] --> net_error = -101 (ERR_CONNECTION_RESET) t=42615 [st=42614] HTTP_TRANSACTION_RESTART_AFTER_ERROR --> net_error = -101 (ERR_CONNECTION_RESET) t=42615 [st=42614] -HTTP_TRANSACTION_READ_HEADERS t=42615 [st=42614] +HTTP_STREAM_REQUEST [dt=12] t=42627 [st=42626] HTTP_STREAM_REQUEST_BOUND_TO_JOB --> source_dependency = 193498 (HTTP_STREAM_JOB) t=42627 [st=42626] -HTTP_STREAM_REQUEST t=42627 [st=42626] +HTTP_TRANSACTION_SEND_REQUEST [dt=2] t=42627 [st=42626] HTTP_TRANSACTION_SEND_REQUEST_HEADERS --> GET /release/getReleaseHistory?projectId=fum1.0.593 HTTP/1.1 Host: qa.tieba.baidu.com Connection: keep-alive Accept: application/json, text/plain, */* User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36 Referer: http://qa.tieba.baidu.com/project/ Accept-Encoding: gzip, deflate, sdch Accept-Language: en-US,en;q=0.8 Cookie: [268 bytes were stripped] t=42629 [st=42628] -HTTP_TRANSACTION_SEND_REQUEST t=42629 [st=42628] +HTTP_TRANSACTION_READ_HEADERS [dt=112] t=42629 [st=42628] HTTP_STREAM_PARSER_READ_HEADERS [dt=112] t=42741 [st=42740] HTTP_TRANSACTION_READ_RESPONSE_HEADERS --> HTTP/1.1 200 OK Date: Fri, 02 Jan 2015 09:51:48 GMT Content-Type: application/json; charset=UTF-8 Transfer-Encoding: chunked Connection: keep-alive Cache-Control: no-cache tracecode: 31079600320335034634010217 tracecode: 31079600320537995786010217 Server: Apache t=42741 [st=42740] -HTTP_TRANSACTION_READ_HEADERS t=42741 [st=42740] HTTP_CACHE_WRITE_INFO [dt=0] t=42741 [st=42740] HTTP_CACHE_WRITE_DATA [dt=0] t=42741 [st=42740] HTTP_CACHE_WRITE_INFO [dt=0] t=42741 [st=42740] URL_REQUEST_DELEGATE [dt=0] t=42741 [st=42740] -URL_REQUEST_START_JOB t=42741 [st=42740] URL_REQUEST_DELEGATE [dt=0] t=42741 [st=42740] HTTP_TRANSACTION_READ_BODY [dt=0] t=42741 [st=42740] HTTP_CACHE_WRITE_DATA [dt=0] t=42741 [st=42740] HTTP_TRANSACTION_READ_BODY [dt=0] t=42741 [st=42740] HTTP_CACHE_WRITE_DATA [dt=0] t=42742 [st=42741] -REQUEST_ALIVE 首先,日志显示的总耗时与上面网络面板截图的总耗时是吻合的,都是42.74秒,说明我们定位正确。 以下时间均以毫秒计 日志第一列为时间线,自请求发起时算。 第二列为每步操作所逝去的时间,时间差的概念,与第三列里面的dt不同,它会积累前面的耗时。 第三列为具体的事件,以及相应事件的耗时dt,此耗时为绝对耗时。 +号对应事件开始,-号对应事件结束,也就是说他们必然成对出现。住里是展开后更加详细的子事件。直到不能再细分。 如果说一开始接触到这个日志时手足无措的话,我们来看一下正常情况下的日志是怎样的,有对比才有发现。 以下随便摘取一次正常请求的日志,如下: 384462: URL_REQUEST http://qa.tieba.baidu.com/release/getReleaseHistory?projectId=fum1.0.593 Start Time: 2015-01-03 20:23:54.698 t=1556 [st= 0] +REQUEST_ALIVE [dt=172] t=1556 [st= 0] URL_REQUEST_DELEGATE [dt=0] t=1556 [st= 0] +URL_REQUEST_START_JOB [dt=171] --> load_flags = 335609856 (BYPASS_DATA_REDUCTION_PROXY | MAYBE_USER_GESTURE | VERIFY_EV_CERT) --> method = "GET" --> priority = "LOW" --> url = "http://qa.tieba.baidu.com/release/getReleaseHistory?projectId=fum1.0.593" t=1557 [st= 1] +URL_REQUEST_DELEGATE [dt=4] t=1557 [st= 1] DELEGATE_INFO [dt=4] --> delegate_info = "extension Tampermonkey" t=1561 [st= 5] -URL_REQUEST_DELEGATE t=1561 [st= 5] HTTP_CACHE_GET_BACKEND [dt=0] t=1561 [st= 5] HTTP_CACHE_OPEN_ENTRY [dt=1] --> net_error = -2 (ERR_FAILED) t=1562 [st= 6] HTTP_CACHE_CREATE_ENTRY [dt=0] t=1562 [st= 6] HTTP_CACHE_ADD_TO_ENTRY [dt=0] t=1562 [st= 6] URL_REQUEST_DELEGATE [dt=0] t=1562 [st= 6] +HTTP_STREAM_REQUEST [dt=2] t=1564 [st= 8] HTTP_STREAM_REQUEST_BOUND_TO_JOB --> source_dependency = 384467 (HTTP_STREAM_JOB) t=1564 [st= 8] -HTTP_STREAM_REQUEST t=1564 [st= 8] +HTTP_TRANSACTION_SEND_REQUEST [dt=1] t=1564 [st= 8] HTTP_TRANSACTION_SEND_REQUEST_HEADERS --> GET /release/getReleaseHistory?projectId=fum1.0.593 HTTP/1.1 Host: qa.tieba.baidu.com Connection: keep-alive Accept: application/json, text/plain, */* User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36 Referer: http://qa.tieba.baidu.com/project/ Accept-Encoding: gzip, deflate, sdch Accept-Language: en-US,en;q=0.8 Cookie: [2642 bytes were stripped] t=1565 [st= 9] -HTTP_TRANSACTION_SEND_REQUEST t=1565 [st= 9] +HTTP_TRANSACTION_READ_HEADERS [dt=161] t=1565 [st= 9] HTTP_STREAM_PARSER_READ_HEADERS [dt=160] t=1725 [st=169] HTTP_TRANSACTION_READ_RESPONSE_HEADERS --> HTTP/1.1 200 OK Date: Sat, 03 Jan 2015 12:23:54 GMT Content-Type: application/json; charset=UTF-8 Transfer-Encoding: chunked Connection: keep-alive Cache-Control: no-cache tracecode: 14346880480340800522010320 tracecode: 14346880480253893130010320 Server: Apache t=1726 [st=170] -HTTP_TRANSACTION_READ_HEADERS t=1726 [st=170] HTTP_CACHE_WRITE_INFO [dt=0] t=1726 [st=170] HTTP_CACHE_WRITE_DATA [dt=0] t=1726 [st=170] HTTP_CACHE_WRITE_INFO [dt=0] t=1726 [st=170] +URL_REQUEST_DELEGATE [dt=1] t=1726 [st=170] DELEGATE_INFO [dt=1] --> delegate_info = "extension Tampermonkey" t=1727 [st=171] -URL_REQUEST_DELEGATE t=1727 [st=171] -URL_REQUEST_START_JOB t=1727 [st=171] URL_REQUEST_DELEGATE [dt=0] t=1727 [st=171] HTTP_TRANSACTION_READ_BODY [dt=0] t=1727 [st=171] HTTP_CACHE_WRITE_DATA [dt=1] t=1728 [st=172] HTTP_TRANSACTION_READ_BODY [dt=0] t=1728 [st=172] HTTP_CACHE_WRITE_DATA [dt=0] t=1728 [st=172] -REQUEST_ALIVE 针对上面正常的请求,我们主要关注两部分,如下面的截图: 发送请求头 +HTTP_TRANSACTION_SEND_REQUEST [dt=1] 读取响应头 +HTTP_TRANSACTION_READ_HEADERS [dt=161] 这是正常的情况下,没有什么问题。并且日志里可以清晰地看到发送的请求头是什么,然后解析出来的响应头是什么。这跟在网络面板看到的是一致的。 再回到出问题的请求日志上来,同样我们只关注这两部分。如下面的截图: 与正常相比,最后一次发送请求和读取响应头无异常,时间就多在了前面还有再次发送和请求的过程,细看时间都花在了以下两个事件中: HTTP_STREAM_PARSER_READ_HEADERS [dt=21301] HTTP_STREAM_PARSER_READ_HEADERS [dt=21304] 该事件的名称已经自我解读,意思是解析读取的响应头。但问题是紧接着下面报错了, --> net_error = -101 (ERR_CONNECTION_RESET) 读取响应头时发生了链接重置的错误,有理由认为本次链接是不成功的,没拿到正确的响应头,于是解析不成功。时间都花在了这里,足足21秒之久,两个21秒造就了上面看到的Stalled了42秒之久。 问题似乎已经很明朗了。链接被重置。 在第三次尝试的时候正常了,于是正确返回,我们才看到了被解析的响应头被展示在了下面。也就是说在出问题的时候要么响应头未拿到,要么响应头非法导致解析不成功。而原因就是链接被重置。 那么接下来的工作就是对ERR_CONNECTION_RESET这个错误的追查了。 官方关于 ERR_CONNECTION_RESET 错误的解释 未找到官方相应的资料,Chrome官网上唯一关于此错误的描述是在安装Chrome时出现Error 101。我估计文档的撰写人员没想到谁会这么蛋疼想要看这些生涩的东西,除了开发者。既然你都是开发者了,那为什么不去看Chromium的源码。 好吧,唯一的途径似乎只能从源码中寻找了。作为只精JS的前端人员,现在要从C,C++代码中找答案了。估计追完这个问题,我会尝试为Chromium贡献代码。 慢着,在这之前,还是搜到一些关于这个错误的信息的。但似乎都不怎么靠谱。 比如这里提到,是因为ISP网络问题,实在无太可能。还有这是神马居然一个硬件网站但提到了这个错误,并且怀疑是杀软导致Chrome出问题,但杀软已经在上文被我们排除了。 Chromium 源码 那么这个错误究竟是什么。能不能找到点靠谱的解释。当然能,让我们进入到Chromium的源码中去。 ERR_CONNECTION_RESET被唤起的地方 在Chromium的源码中搜索该常量名,确实出现很多结果。联系到我们查看日志发现问题的上下文,是在解析响应头报的。所以我们定位到http_stream_parser.cc文件,同时注意到有一个文件叫net_errors_win.cc,所以猜测他是定义所有错误常量用的,也顺便打开之。 经过观察src/net/base/net_errors_win.cc 其路径和代码得知其中多为系统级别的错误,似乎跟我们的问题不是很关联,忽略该文件。 http_stream_parser.cc文件中,ERR_CONNECTION_RESET仅出现一次。这给我们定位带来了极大的便利。 cpp [chromium]//src/net/base/net_errors_win.cc https://code.google.com/p/chromium/codesearch#chromium/src/net/http/http_stream_parser.cc&q=ERR_CONNECTION_RESET&sq=package:chromium&dr=C http_stream_parser.cc // Returns true if |error_code| is an error for which we give the server a // chance to send a body containing error information, if the error was received // while trying to upload a request body. bool ShouldTryReadingOnUploadError(int error_code) { return (error_code == ERR_CONNECTION_RESET); } 这里定义了一个ShouldTryReadingOnUploadError 的方法,注释耐人寻味,这个时候,这样的情景,能否正确解读注释成为了比读懂代码更重要(这是我在看JS代码时永远无法体味到的感觉),下面尽可能对它进行理解: 在尝试发送一个请求体的时候,让服务器尝试发送一个带错误的响应体,如果我们接收到了该错误则返回true 我承认被上面的复杂从句打败! 那么我们来看这个方法被调用的场景。 现在我们点击上面的ShouldTryReadingOnUploadError方法,代码下方出现调用了该方法的地方,一共有两处。 分别点击进行查看。 cpp 459行DoSendHeadersComplete方法里进行了调用 int HttpStreamParser::DoSendHeadersComplete(int result) { if (result < 0) { // In the unlikely case that the headers and body were merged, all the // the headers were sent, but not all of the body way, and |result| is // an error that this should try reading after, stash the error for now and // act like the request was successfully sent. if (request_headers_->BytesConsumed() >= request_headers_length_ && ShouldTryReadingOnUploadError(result)) { upload_error_ = result; return OK; } return result; } 虽然不太可能,但也不排除头部和请求体合并的情况,当所有头部发送完毕,请求体不一定,此时result便是需要稍后处理的一种错误,这里暂且先返回OK。 cpp 516行另一个DoSendBodyComplete方法里进行了调用 int HttpStreamParser::DoSendBodyComplete(int result) { if (result < 0) { // If |result| is an error that this should try reading after, stash the // error for now and act like the request was successfully sent. if (ShouldTryReadingOnUploadError(result)) { upload_error_ = result; return OK; } return result; } 跟上面类似,如果result出错,稍后处理,先返回正常 这也与我们在日志中看到的情况相符,在前面再次错误后,这次请求并没有终止结束,而是尝试到了第三次并且以成功结束的。 但不管怎样,从这两个方法,一个DoSendHeadersComplete, 另一个DoSendBodyComplete,身上能体现出请求确实已经发出去。 TCP RST 另外,在net_error_list.h这个文件的109行,可以准确找到我们在日志中得到的101号错误。它的定义如下: // A connection was reset (corresponding to a TCP RST). NET_ERROR(CONNECTION_RESET, -101) 从括号中的进一步解释可以知道,它代表TCP连接重置。 TCP 那么问题来了,什么是TCP连接重置?什么会引发TCP连接重置。从这篇文章中有比较详细的解答。 想要完全解释,本文似乎是不可能的了。但根据上面的文章,这里可以简单转述一下。 什么是TCP连接 它是一种协议。当网络上一个节点想与另一个节点通信时,双方需要选建立连接。而这个连接过程需要大家都懂的一种约定,TCP就是事先定好的一种约定,于是我们采用它吧,于是其中一个节点按照这个约定发起一建立连接的请求,另一节点收到后,根据该约定,便能读懂这个请求里各字段的意思:哦,丫这是想约我呢。 三次握手 继续上面的例子。A想与B通信,并且使用TCP。 首先A发起一个报文,其中包含自己的地址,想要连接的目标地址,自己用来连接的端口及目标机器的端口,etc. B收到邀约,并且愿意付约。此刻B需要回传一个报文,告诉A我愿意跟你连接。 A收到B的肯定应答,到此A与B经历了三次通信或者说是握手,双方都没有异议,连接建立。 而连接断开的过程也颇为类似。双方中的一方比如说A先发起一个断开连接的报文FIN,B收到并确认,然后回传一个可以断开的报文FIN给A。此刻A收到并确认。此刻双方都确认后,连接可以安全断开,但还会保持一个等待断开的状态,大概持续4分钟,用于之前连接通路上未传输完成的数据进行善后。 什么是重置 上面提到了4分钟的等待时间,而重置RESET便是立即断开连接的手段。 发生重置的情况 到此重置的作用已然明了。也就是说,重置甚至算不上一个错误,它是TCP连接中的一种正常情况。但什么时候会发生重置,如何引起的。 上文列出了三种情况。 SMB Reset 简单举例来说,服务器提供了两个端口445,139进行服务,客户端同时去请求与这两个端口连接,服务器返回了两个端口可以被连接,此刻客户端择优选择一个进行连接,而重置另一个。 Ack, Reset 报文重置发生主要有以下情况: 服务器没有监听被请求的端口,无法建立连接 服务器此刻无法比如没有充裕的资源用来连接连接 TCP Reset due to no response 由于没有响应而被重置。当发起连接的一方连续发送6次请求未得到回应,此刻默认他们之间已经通过三次握手建立了连接并且通信有问题,发起的一方将连接重置。 Application Reset 除了上面的情况,找不到TCP内部自己发送的重置,则归为了这一类。程序内将连接重置。此种情况包含了所有你想得到想不到将连接断开的情况。有可能是程序内部逻辑重置的,所以不能完全认为此时发生了错误。 值得注意的是,上面列出的情况服务器的不确定性导致连接重置的可能性要合理些。Chrome 主动发起URL请求不太可能自己又重置掉,并且没有理由重置掉后又去重连。 Chrome Dev Tool 中时间线各阶段代表的意义 另附注一下Chrome Dev Tool 中请求的时间线各阶段代表的意义。 以下内容扒自Chrome 开发者文档页,然后我将它本地化了一下下。 Stalled/Blocking 在请求能够被发出去前的等等时间。包含了用于处理代理的时间。另外,如果有已经建立好的连接,那么这个时间还包括等待已建立连接被复用的时间,这个遵循Chrome对同一源最大6个TCP连接的规则。 「拿我们的情况来说,上面出错所有的耗时也是算在了这部分里面。网络面板中显示的其余时间比如DNS查找,连接建立等都是属于最后那次成功请求的了」 Proxy Negotiation 处理代理的时间。 DNS Lookup 查找DNS的时间。页面上每个新的域都需要一次完整的寻路来完成DNS查找。 Initial Connection / Connecting 用于建立链接的时间,包括TCP握手及多次尝试握手,还有处理SSL。 SSL 完成SSL握手的时间。 Request Sent / Sending Time spent issuing the network request. Typically a fraction of a millisecond. 发起请求的时间,通常小到可以忽略。 Waiting (TTFB) 等待响应的时间,具体来说是等待返回首个字节的时间。包含了与服务器之间一个来回响应的时间和等待首个字节被返回的时间。 Content Download / Downloading 用于下载响应的时间 结论 我相信很多同学是直接跳到这里来了的。事实上我给不出什么解决方案,只能证明前端代码没有问题,请求已发。请RD同学接着排查。 另外,利用好Chrome的内部网络日志chrome://net-internals/#events进行网络分析是个非常好的选择,特别是在处理这些调试面板上无法体现的细节问题时。 参考及引用 #1 Chrome stalls when making multiple requests to same resource? #2 What does “pending” mean for request in Chrome Developer Window? #3 Evaluating network performance / Resource network timing #4 Provisional headers are shown #5 “CAUTION: provisional headers are shown” in Chrome debugger #6 Chrome 里的请求报错 "CAUTION: Provisional headers are shown" 是什么意思? #7 Issue 345643003: Http cache: Implement a timeout for the cache lock #8 Issue 46104: Pages can get blocked in "Waiting for Cache" for a very long time #9 Providing Network Details for bug reports #10 从FE的角度上再看输入url后都发生了什么 #11 ERR_CONNECTION_RESET 的Chromium 源码 #12 Chromium Network Stack #13 Where do resets come from? (No, the stork does not bring them.)
CSS中存在一个神秘的变量,少有人知自然也不怎么为人所用。它就是crrentColor变量(或者说是CSS关键字,但我觉得称为变量好理解些)。 初识 它是何物?具有怎样的功效?它从哪里来?带着这些疑问我们继续。 下面是来自MDN的解释: currentColor代表了当前元素被应用上的color颜色值。 使用它可以将当前这个颜色值应用到其他属性上,或者嵌套元素的其他属性上。 你这可以这么理解,CSS里你可以在任何需要写颜色的地方使用currentColor这个变量,这个变量的值是当前元素的color值。如果当前元素没有在CSS里显示地指定一个color值,那它的颜色值就遵从CSS规则,从父级元素继承而来。 到此似乎解决了上面三个哲学式的提问,但依然有些模糊。程序员之间的交流,还是上码才好。 场景1 <p>约么?</p> p{ color: red; } 此时,<p>标签currentColor的值为red。 场景2 <div class="container"> <p>约么?</p> </div> .container{ color: #00ff00; } 现在,我们没有给<p>标签指定颜色,它的color从父级容器也就是class为container的div继承而来,换句话说此时p标签的color为#00ff00,currentColor又是直接去取元素的color值,所以此时p标签的currentColor值也为#00ff00。 场景3 如果父级元素也没有写color呢?其实这里都还是CSS规则的范畴,跟本文的主角关系不太大。但本着不啰嗦会死的原则,就展开了讲。 如果父级元素也没有指定颜色,那它的父级元素就会从父级的父级去继承,直到文档的根结点html标签都还没显示指定一个颜色呢,就应用上浏览器默认的颜色呗~ <!doctype html> <html> <head> <title>我来组成头部</title> </head> <body> <p>约么?</p> </body> <footer>战神金钢,宇宙的保护神!</footer> </html> /** * 无CSS */ 那,这个时候的黑色其实是浏览器默认给的。此时p标签的currentColor自然也跟color值一样,为黑色,纯黑的#000。 如何用? 了解它是怎样的物品后,下面问题来了,如何用?有额外的buff效果么,耗蓝多么,CD时间长么。。。 前面说道,它就是一个CSS变量,存储了颜色值,这个值来自当前元素的colorCSS属性。当你需要为该元素其他属性指定颜色的时候,它就可以登上舞台了。 <div class="container"> 好好说话,有话好好说 </div> .container{ color: #3CAADB; border: 4px solid currentColor; } 这里我们第一次领略了currentColor的奇效。在指定边框颜色的时候,我们直接使用currentColor变量,而没有写一个传统的颜色值。 你似乎也知道了该如何用了。不只是border,其他能够使用颜色的地方,比如background,box-shadow等等。 带你装逼带你飞 新技能就是如此炫酷。大开脑洞任性地去使用吧! 与渐变混搭 你可能无法想象到的是,除了可以将currentColor用到普通需要颜色的场景,它同样可以被用在渐变中。 <div class="container"> </div> .container{ height:200px; color: #3CAADB; background-image: linear-gradient(to right, #fff, currentColor 100%); } 甚至也可用于填充svg,下面会有相应示例。 与CSS动画结合 当与CSS animation结合,可以得到更加有创意的效果,比如这个来自codepen的示例 See the Pen currentColor by Scott Kellum (@scottkellum) on CodePen. 更加简洁的CSS 其实,新技能不只是装逼那么单纯,合理的使用currentColor 变量会让你的CSS代码变得简洁。这才是我们想要达到的目的。以炫技为目的技能是没有生产意义的。 看下面这个例子(这个示例灵感来自这里) 我们在按钮中使用了一个svg图标。你是一个负责任的FE,所以,对这个按钮的各种状态:focus,:hover,:active都作了样式上的处理。同时,为了让图标也跟着保持一致的姿态变更,需要把对<a>标签的样式处理同样就到到<svg>标签上。于是你的CSS代码看起来就是下面这样的了。 /*a 标签*/ .button { color: #117B6F; font-size: 1.2em; } .button:hover, .button:focus { color: #01B19A; } .button:active { color: #02D7BB; } /*svg 标签*/ .button svg { height: 17px; width: 17px; fill: #117B6F; } .button:hover svg, .button:focus svg { fill: #01B19A; } .button:active svg { fill: #02D7BB; } 你也发现了,代码有点冗余。接下来,我们用currentColor来将它简化一下。于是成了下面这样: /*a 标签*/ .button { color: #117B6F; font-size: 1.2em; } .button:hover, .button:focus { color: #01B19A; } .button:active { color: #02D7BB; } /*svg 标签*/ .button svg { height: 17px; width: 17px; fill: currentColor; } 更好维护的CSS 仔细想想不难发现,当使用currentColor后,我们的CSS也变得更加好维护了。 还拿上面的按钮示例来说,优化之前不但代码冗余,而且哪天PM来劲了说这颜色饱看,给换个其他色。于是你得把<a>标签和<svg>一起换了。 但优化后就不一样了,因为<svg>使用的填充是currentColor,你只需要改变<a>标签的颜色,它也就跟着变了。真正做到了牵一发而不动全身。这不正是众码友们毕生所追求的理想编程境界么。 浏览器兼容性 一提到浏览器兼容性,FE同学们或许就不敢那么任性了。之前你可能是这样的: 当听到IE传来的噩耗,你可能是这样的: 经查,can i use 没有关于它的数据。 经测, 本机Win7搭载的IE8不支持 本机安装的火狐31发来战报表示支持 Chrome,你猜? 本机Safari 5.1.7也表示支持 本机Opera 26 同样表示支持 根据这篇文章的描述,它是可以很好地工作在在所有现代浏览器和IE9+上的,甚至是各浏览器对应的移动版本。所以,在IE不是主要客户对象的情况下,还是可以放心使用的。 参考 css-tricks currentColor Keeping CSS short with currentColor The First CSS Variable: currentColor
HTML5 中新增的<details>标签允许用户创建一个可展开折叠的元件,让一段文字或标题包含一些隐藏的信息。 用法 一般情况下,details用来对显示在页面的内容做进一步骤解释。其展现出来的效果和jQuery手风琴插件差不多。 其大致写法如下: <details> <summary>Google Nexus 6</summary> <p>商品详情:</p> <dl> <dt>屏幕</dt> <dd>5.96” 2560x1440 QHD AMOLED display (493 ppi)</dd> <dt>电池</dt> <dd>3220 mAh</dd> <dt>相机</dt> <dd>13MP rear-facing with optical image stabilization 2MP front-facing</dd> <dt>处理器</dt> <dd>Qualcomm® Snapdragon™ 805 processor</dd> </dl> </details> 首先是<details>标签,里面接着是标题<summary>,这里面的内容一般简短,具有总结性,会展示在页面。接着可以跟任意类型的HTML元素作为详情内容,这些内容需要在点击<summary>才会呈现。 上面代码呈现出来的效果会是下面这样的: 最开始详情是隐藏的,当点击时都会展现。 open 属性 当然,你也可以通过给<details>标签设置open属性让它默认为展开状态。 <details open> <summary>Google Nexus 6</summary> <p>商品详情:</p> <dl> <dt>屏幕</dt> <dd>5.96” 2560x1440 QHD AMOLED display (493 ppi)</dd> <dt>电池</dt> <dd>3220 mAh</dd> <dt>相机</dt> <dd>13MP rear-facing with optical image stabilization 2MP front-facing</dd> <dt>处理器</dt> <dd>Qualcomm® Snapdragon™ 805 processor</dd> </dl> </details> 此时默认会把详情展开,而点击标题后会折叠起来。 示例 示例如上面那样,预览在线版本可点击此处。 浏览器兼容性 由于是HTML5新标签,浏览器支持情况不是很理想。从来自caniuse的数据来看,目前仅Chrome, Safari 8+ 和Opera 26+支持此标签。 可喜的是,如果你在caniuse开启了「显示来自UC浏览器的结果」 选项的话,会发现,国产的UC浏览器也支持了此标签。 Polyfill 既然支持情况如此不理解,那么使用垫片(polyfill)就很有必要了。 垫片就是在那些不支持此特性的浏览器上使用JavaScript来手动模拟,看起来好像是浏览器支持了一样。 chemerisuk给出了他的一个实现,源码在GitHub上,具体的实现思路也写成了博文发到了Smashing Magazine,用法可参见GitHub。 参考 HTML <details> Tag HTML <dt> Tag Making A Complete Polyfill For The HTML5 Details Element better-details-polyfill
「注释」作者在本文里没有说明这么一个事实: 目前的版本Lo-Dash v2.4.1并没有引入延迟求值的特性,Lo-Dash 3.0.0-pre中部分方法进行了引入,比如filter(),map(),reverse()。 原文 我时常觉得像Lo-Dash这样优秀的库已经无法再优化了。它整合了各种奇技淫巧已经将JavaScript的性能开发到了极限。它使用了最快速的语句,优化的算法,甚至还会在发版前做性能测试以保证回归没问题。 延迟求值 但似乎我错了-还可以让Lo-Dash有明显的提升。只需将关注点从细微的优化转移到算法上来。譬如,在一次循环中我们往往会去优化循环体: var len = getLength(); for(var i = 0; i < len; i++) { operation(); // <- 10ms - 如何做到 9ms?! } 但针对循环体的优化往往很难,很多时候已经到极限了。相反,优化getLength() 函数尽量减少循环次数变得更有意义了。你想啊,这个数值越小,需要循环的10ms就越少。 这便是Lo-Dash实现延迟求值的大致思路。重要的是减少循环次数,而不是每次循环的时间。让我们考察下面的例子: function priceLt(x) { return function(item) { return item.price < x; }; } var gems = [ { name: 'Sunstone', price: 4 }, { name: 'Amethyst', price: 15 }, { name: 'Prehnite', price: 20}, { name: 'Sugilite', price: 7 }, { name: 'Diopside', price: 3 }, { name: 'Feldspar', price: 13 }, { name: 'Dioptase', price: 2 }, { name: 'Sapphire', price: 20 } ]; var chosen = _(gems).filter(priceLt(10)).take(3).value(); 我们只想取出3个价格低于10元的小球。通常情况下我们先过滤整个数据源,最后从所有小于10的元素里返回前面三个即可。 但这种做法并不优雅。它处理了全部8个数据,但其实只需要处理前面5个我们就能拿到结果了。同样为了得到正确的结果,延迟求值则只处理最少的元素。优化后如下图所示: 一下子就获得了37.5%的性能提升。很容易找出提升X1000+的例子。比如: var phoneNumbers = [5554445555, 1424445656, 5554443333, … ×99,999]; // 取出100个含 `55` 的号码 function contains55(str) { return str.contains("55"); }; var r = _(phoneNumbers).map(String).filter(contains55).take(100); 这个例子中map和filter 将遍历99999 个元素,但很有可能我们只需处理到1000个元素的时候就已经拿到想要的结果了。这回性能的提升就太明显了(benchmark): 流水线 延迟求值同时带来了另一个好处,我称之为“流水线”。要旨就是避免产生中间数组,而是对一个元素一次性进行完所有操作。下面用代码说话: var result = _(source).map(func1).map(func2).map(func3).value(); 上面看似优雅的写法在原始的Lo-Dash里会转换成下面的样子(直接求值): var result = [], temp1 = [], temp2 = [], temp3 = []; for(var i = 0; i < source.length; i++) { temp1[i] = func1(source[i]); } for(i = 0; i < source.length; i++) { temp2[i] = func2(temp1[i]); } for(i = 0; i < source.length; i++) { temp3[i] = func3(temp2[i]); } result = temp3; 当引入了延迟求值后,代码大致就成这样的了: var result = []; for(var i = 0; i < source.length; i++) { result[i] = func3(func2(func1(source[i]))); } 减少不必要的中间变量多少会带来性能上的提升,特别是在数据源特别巨大,内存又吃紧的情况下。 延迟执行 延迟求值带来的另一个好处是延迟执行。无论何时你写了段链式代码,只有在显式地调用了.value()后才会真正执行。这样一来,在数据源需要异步去拉取的情况下,可以保证我们处理的是最新的数据。 var wallet = _(assets).filter(ownedBy('me')) .pluck('value') .reduce(sum); $json.get("/new/assets").success(function(data) { assets.push.apply(assets, data); // 更新数据源 wallet.value(); // 返回的结果是最新的 }); 而且这种机制在某些情况下也会提高执行效果。我们可以老早发送一个请求获取数据,然后指定一个精确的时间来执行。 后记 延迟求值并且不算什么新技术。在一些库中已经在使用了,比如LINQ,Lazy.js还有其他等等。那么问题来了,Lo-Dash存在的意义是啥?我想就是你仍然可以使用你熟悉的Underscore 接口但享受一个更高效的底层实现,不需要额外的学习成本,代码上面也不会有大的变动,只需稍加修改。
原文:How to make a simple HTML5 Canvas game 想要快速上手HTML5 Canvas小游戏开发?下面通过一个例子来进行手把手教学。(如果你怀疑我的资历, A Wizard's Lizard这个游戏的半数以上开发是由我完成的) 我们直接来看源码里的game.js,当然你也可以在线体验一下游戏先。 游戏截图 创建画布 // Create the canvas var canvas = document.createElement("canvas"); var ctx = canvas.getContext("2d"); canvas.width = 512; canvas.height = 480; document.body.appendChild(canvas); 首先我们需要创建一张画布作为游戏的舞台。这里通过JS代码而不是直接在HTML里写一个<canvas>元素目的是要说明代码创建也是很方便的。有了画布后就可以获得它的上下文来进行绘图了。然后我们还设置了画布大小,最后将其添加到页面上。 准备图片 // 背景图片 var bgReady = false; var bgImage = new Image(); bgImage.onload = function () { bgReady = true; }; bgImage.src = "images/background.png"; 游戏嘛少不了图片的,所以我们先加载一些图片先。简便起见,这里仅创建简单的图片对象,而不是专门写一个类或者Helper来做图片加载。bgReady这个变量用来标识图片是否已经加载完成从而可以放心地使用了,因为如果在图片加载未完成情况下进行绘制是会报错的。 整个游戏中需要用到的三张图片:背景,英雄及怪物我们都用上面的方法来处理。 游戏对象 // 游戏对象 var hero = { speed: 256, // 每秒移动的像素 x: 0, y: 0 }; var monster = { x: 0, y: 0 }; var monstersCaught = 0; 现在定义一些对象将在后面用到。我们的英雄有一个speed属性用来控制他每秒移动多少像素。怪物游戏过程中不会移动,所以只有坐标属性就够了。monstersCaught则用来存储怪物被捉住的次数。 处理用户的输入 // 处理按键 var keysDown = {}; addEventListener("keydown", function (e) { keysDown[e.keyCode] = true; }, false); addEventListener("keyup", function (e) { delete keysDown[e.keyCode]; }, false); 现在开始处理用户的输入(对初次接触游戏开发的前端同学来说,这部分开始可能就需要一些脑力了)。在前端开发中,一般是用户触发了点击事件然后才去执行动画或发起异步请求之类的,但这里我们希望游戏的逻辑能够更加紧凑同时又要及时响应输入。所以我们就把用户的输入先保存下来而不是立即响应。 为此,我们用keysDown这个对象来保存用户按下的键值(keyCode),如果按下的键值在这个对象里,那么我们就做相应处理。 开始一轮游戏 // 当用户抓住一只怪物后开始新一轮游戏 var reset = function () { hero.x = canvas.width / 2; hero.y = canvas.height / 2; // 将新的怪物随机放置到界面上 monster.x = 32 + (Math.random() * (canvas.width - 64)); monster.y = 32 + (Math.random() * (canvas.height - 64)); }; reset方法用于开始新一轮和游戏,在这个方法里我们将英雄放回画布中心同时将怪物放到一个随机的地方。 更新对象 // 更新游戏对象的属性 var update = function (modifier) { if (38 in keysDown) { // 用户按的是↑ hero.y -= hero.speed * modifier; } if (40 in keysDown) { // 用户按的是↓ hero.y += hero.speed * modifier; } if (37 in keysDown) { // 用户按的是← hero.x -= hero.speed * modifier; } if (39 in keysDown) { // 用户按的是→ hero.x += hero.speed * modifier; } // 英雄与怪物碰到了么? if ( hero.x <= (monster.x + 32) && monster.x <= (hero.x + 32) && hero.y <= (monster.y + 32) && monster.y <= (hero.y + 32) ) { ++monstersCaught; reset(); } }; 这就是游戏中用于更新画面的update函数,会被规律地重复调用。首先它负责检查用户当前按住的是中方向键,然后将英雄往相应方向移动。 有点费脑力的或许是这个传入的modifier 变量。你可以在main 方法里看到它的来源,但这里还是有必要详细解释一下。它是基于1开始且随时间变化的一个因子。例如1秒过去了,它的值就是1,英雄的速度将会乘以1,也就是每秒移动256像素;如果半秒钟则它的值为0.5,英雄的速度就乘以0.5也就是说这半秒内英雄以正常速度一半的速度移动。理论上说因为这个update 方法被调用的非常快且频繁,所以modifier的值会很小,但有了这一因子后,不管我们的代码跑得快慢,都能够保证英雄的移动速度是恒定的。 现在英雄的移动已经是基于用户的输入了,接下来该检查移动过程中所触发的事件了,也就是英雄与怪物相遇。这就是本游戏的胜利点,monstersCaught +1然后重新开始新一轮。 渲染物体 // 画出所有物体 var render = function () { if (bgReady) { ctx.drawImage(bgImage, 0, 0); } if (heroReady) { ctx.drawImage(heroImage, hero.x, hero.y); } if (monsterReady) { ctx.drawImage(monsterImage, monster.x, monster.y); } // 计分 ctx.fillStyle = "rgb(250, 250, 250)"; ctx.font = "24px Helvetica"; ctx.textAlign = "left"; ctx.textBaseline = "top"; ctx.fillText("Monsterrs caught: " + monstersCaught, 32, 32); }; 之前的工作都是枯燥的,直到你把所有东西画出来之后。首先当然是把背景图画出来。然后如法炮制将英雄和怪物也画出来。这个过程中的顺序是有讲究的,因为后画的物体会覆盖之前的物体。 这之后我们改变了一下Canvas的绘图上下文的样式并调用fillText来绘制文字,也就是记分板那一部分。本游戏没有其他复杂的动画效果和打斗场面,绘制部分大功告成! 主循环函数 // 游戏主函数 var main = function () { var now = Date.now(); var delta = now - then; update(delta / 1000); render(); then = now; // 立即调用主函数 requestAnimationFrame(main); }; 上面的主函数控制了整个游戏的流程。先是拿到当前的时间用来计算时间差(距离上次主函数被调用时过了多少毫秒)。得到modifier后除以1000(也就是1秒中的毫秒数)再传入update函数。最后调用render 函数并且将本次的时间保存下来。 关于游戏中循环更新画面的讨论可参见「Onslaught! Arena Case Study」。 关于循环的进一步解释 // requestAnimationFrame 的浏览器兼容性处理 var w = window; requestAnimationFrame = w.requestAnimationFrame || w.webkitRequestAnimationFrame || w.msRequestAnimationFrame || w.mozRequestAnimationFrame; 如果你不是完全理解上面的代码也没关系,我只是觉得拿出来解释一下总是极好的 为了循环地调用main函数,本游戏之前用的是setInterval。但现今已经有了更好的方法那就是requestAnimationFrame。使用新方法就不得不考虑浏览器兼容性。上面的垫片就是出于这样的考虑,它是Paul Irish 博客原版的一个简化版本。 启动游戏! // 少年,开始游戏吧! var then = Date.now(); reset(); main(); 总算完成了,这是本游戏最后一段代码了。先是设置一个初始的时间变量then用于首先运行main函数使用。然后调用 reset 函数来开始新一轮游戏(如果你还记得的话,这个函数的作用是将英雄放到画面中间同时将怪物放到随机的地方以方便英雄去捉它)。 到此,相信你已经掌握了开发一个简单H5小游戏需要的基本功了。玩玩这个游戏或者下载代码自己研究研究吧 :)
HTML5有无限可能,总是在释出一些新鲜实用的功能,让原生的web环境更加炫酷。 今天看到datalist 这个元素,可以用来预先定义一个输入框的潜在选项,也就是我们在平时项目中经常用jQuery插件或者自己写JS来实现的autocomplete「自动补全,但似乎自动提示更贴切一些」功能。 具体来说,页面上的input还是原来的input,只是在它的下面定义一下新的datalist在其中填充触发提示的文本,同时在该input元素上指定list属性指向这个list。一个大概的例子大概是像下面这样 你最喜欢的浏览器是: <input list="browsers"> <datalist id="browsers"> <option value="Internet Explorer"> <option value="Firefox"> <option value="Chrome"> <option value="Opera"> <option value="Safari"> </datalist> 最后出来的效果又差不多是这样的: 在线查看效果请点击这里 没什么特别之处,简单得发指~ 但相信大家在看了效果后跟我一样,发现了一个不足之处,在input右边会有向下的箭头,这让它看起来就像一个dropdown 或者select 「下拉框」,解决办法是多加两句CSS代码来将它隐藏,但此方法只是针对webkit内核的浏览器进行的优化: input::-webkit-calendar-picker-indicator { display: none; -webkit-appearance: none; } 这样之后出来的效果差不多成了这样: 浏览器兼容性 下面的数据来自caniuse。 可以看出,遥遥领先的依然是风采依旧的Chrome,对该元素的支持全线飘绿; 同时Firefox也是毫不示弱,紧随版本帝之后; 而其他浏览器情况则各不相同,正所谓性福的人都相似,不幸的人各有不幸。 Opera在边缘浏览器中表现强劲,绿得很耀眼; 值得注意的是,在这场不算较量的较量中,苹果太子Safari则是黑马般拿到了垫底的位置,全线飘红。这直接一举打破IE在主流浏览器的各种评测中常年垫底的记录。 而IE虽然摆脱了末位的阴影,但即使是最新的IE11也只是对Datalist元素进行了部分支持,所以要与各强劲对手比肩而受到前端开发者的青睐还有些工作要做。但留给IE翻盘的时间已经不多了,正如留给中国队的时间一样~ REFERENCE Can I Use Datalist element How to create Autocomplete Textbox using Datalist in HTML5
Chrome的开发者工具已经强大到没朋友的地步了,特别是其功能丰富界面友好的console,使用得当可以有如下功效: 更高「逼格」更快「开发调试」更强「进阶级的Frontender」 Bug无处遁形「Console大法好」 console.log 大家都会用log,但鲜有人很好地利用console.error , console.warn 等将输出到控制台的信息进行分类整理。 他们功能区别不大,意义在于将输出到控制台的信息进行归类,或者说让它们更语义化。 各个所代表的语义如下: console.log:普通信息 console.info:提示类信息 console.error:错误信息 console.warn:警示信息 当合理使用上述log方法后,可以很方便地在控制台选择查看特定类型的信息。 console.log('一颗红心向太阳','吼吼~'); console.info('楼上药不能停!'); console.warn('楼上嘴太贱!'); console.error('楼上关你毛事?'); 如果再配合console.group 与console.groupEnd,可以将这种分类管理的思想发挥到极致。这适合于在开发一个规模很大模块很多很复杂的Web APP时,将各自的log信息分组到以各自命名空间为名称的组里面。 console.group("app.foo"); console.log("来自foo模块的信息 blah blah blah..."); console.groupEnd(); console.group("app.bar"); console.log("来自bar模块的信息 blah blah blah..."); console.groupEnd(); 而关于console.log,早已被玩儿坏了。一切都源于Chrome提供了这么一个API:第一个参数可以包含一些格式化的指令比如%c。 比如给hello world 做件漂亮的嫁衣再拉出来见人: console.log('%chello world','font-size:25px;color:red;'); 如果你觉得不够过瘾,那就把你能写出来的最华丽的CSS样式都应用上吧,比如渐变。于是你可以得到如下华丽丽的效果: console.log('%chello world', 'background-image:-webkit-gradient( linear, left top, right top, color-stop(0, #f22), color-stop(0.15, #f2f), color-stop(0.3, #22f), color-stop(0.45, #2ff), color-stop(0.6, #2f2),color-stop(0.75, #2f2), color-stop(0.9, #ff2), color-stop(1, #f22) );color:transparent;-webkit-background-clip: text;font-size:5em;'); 各种招大招的节奏啊~ 看着上面密集的代码不用惊慌,上面console.log()第二个参数全是纯CSS用来控制样式的,你不会陌生。而第一个参数里可以带用百分号开头的转义指令,如上面输出带样式的文字时使用的%c指令。更详细的指令参见官方API文档的这个表格。 如果还不够过瘾,那咱们来log一些图片吧,甚至。。。动图? 对,你得先有图,我们拿这张图为例。 console.log("%c", "padding:50px 300px;line-height:120px;background:url('http://wayou.github.io/2014/09/10/chrome-console-tips-and-tricks/rabbit.gif') no-repeat;"); 看着上面摇摆的豆比兔是不是有种抽它一脸的冲动。 除此,console.table 更是直接以表格的形式将数据输出,不能赞得太多! 借用之前写过的一篇博文里的例子: var data = [{'品名': '杜雷斯', '数量': 4}, {'品名': '冈本', '数量': 3}]; console.table(data); 另外,console.log() 接收不定参数,参数间用逗号分隔,最终会输出会将它们以空白字符连接。 console.log('%c你好','color:red;','小明','你知道小红被妈妈打了么'); console.assert 当你想代码满足某些条件时才输出信息到控制台,那么你大可不必写if或者三元表达式来达到目的,cosole.assert便是这样场景下一种很好的工具,它会先对传入的表达式进行断言,只有表达式为假时才输出相应信息到控制台。 var isDebug=false; console.assert(isDebug,'开发中的log信息。。。'); console.count 除了条件输出的场景,还有常见的场景是计数。 当你想统计某段代码执行了多少次时也大可不必自己去写相关逻辑,内置的console.count可以很地胜任这样的任务。 function foo(){ //其他函数逻辑blah blah。。。 console.count('foo 被执行的次数:'); } foo(); foo(); foo(); console.dir 将DOM结点以JavaScript对象的形式输出到控制台 而console.log是直接将该DOM结点以DOM树的结构进行输出,与在元素审查时看到的结构是一致的。不同的展现形式,同样的优雅,各种体位任君选择反正就是方便与体贴。 console.dir(document.body); console.log(document.body); console.time & console.timeEnd 输出一些调试信息是控制台最常用的功能,当然,它的功能远不止于此。当做一些性能测试时,同样可以在这里很方便地进行。 比如需要考量一段代码执行的耗时情况时,可以用console.time与 console.timeEnd来做此事。 这里借用官方文档的例子: console.time("Array initialize"); var array= new Array(1000000); for (var i = array.length - 1; i >= 0; i--) { array[i] = new Object(); }; console.timeEnd("Array initialize"); 当然,我们也可以选择自己写代码来计时: var start=new Date().getTime(); var array= new Array(1000000); for (var i = array.length - 1; i >= 0; i--) { array[i] = new Object(); }; console.log(new Date().getTime()-start); 相信你也看到了,用内置的console.time是多么地方便,省去了自己写代码来计算的工作量。另外值得一提的是,通过调用内置的console.time得到的结果要比自己手动计算的时间差更精确可靠。 console.profile & console.timeLime 当想要查看CPU使用相关的信息时,可以使用console.profile配合 console.profileEnd来完成这个需求。 这一功能可以通过UI界面来完成,Chrome 开发者工具里面有个tab便是Profile。 与此类似的功能还有console.timeLine配合 console.timeLineEnd,它的作用是开始记录一段时间轴,同样可以通过Chrome开发者工具里的Timeline 标签来进行相应操作。 所以在我看来这两个方法有点鸡肋,因为都可以通过操作界面来完成。但至少他提供了一种命令行方式的交互,还是多了种姿势供选择吧。 console.trace 堆栈跟踪相关的调试可以使用console.trace。这个同样可以通过UI界面完成。当代码被打断点后,可以在Call Stack面板中查看相关堆栈信息。 上面介绍的都是挂在window.console这个对象下面的方法,统称为Console API,接下来的这些方法确切地说应该叫命令,是Chrome内置提供,在控制台中使用的,他们统称为Command Line API。 $ 似乎美刀总是被程序员及各种编程语言所青睐「你看看PHP代码就知道PHPer有多爱钱了」,在Chrome的控制台里,$用处还真是蛮多且方便的。$_命令返回最近一次表达式执行的结果,功能跟按向上的方向键再回车是一样的,但它可以做为一个变量使用在你接下来的表达式中: 2+2//回车,再 $_+1//回车得5 上面的$_需要领悟其奥义才能使用得当,而$0~$4则代表了最近5个你选择过的DOM节点。 什么意思?在页面右击选择审查元素,然后在弹出来的DOM结点树上面随便点选,这些被点过的节点会被记录下来,而$0会返回最近一次点选的DOM结点,以此类推,$1返回的是上上次点选的DOM节点,最多保存了5个,如果不够5个,则返回undefined。 另外值得一赞的是,Chrome 控制台中原生支持类jQuery的选择器,也就是说你可以用$加上熟悉的css选择器来选择DOM节点,多么滴熟悉。 $('body') $(selector)返回的是满足选择条件的首个DOM元素。 剥去她伪善的外衣,其实$(selector)是原生JavaScript document.querySelector() 的封装。 同时另一个命令$$(selector)返回的是所有满足选择条件的元素的一个集合,是对document.querySelectorAll() 的封装。 $$('div') copy 通过此命令可以将在控制台获取到的内容复制到剪贴板。 copy(document.body) 然后你就可以到处粘了: 看完此条命令行,机智的你是不是跟脑洞全开的我一样,冒出了这样一个想法:那就是通过这个命令可以在JavaScript里进行复制操作从而不用依赖Flash插件了。 But现实是残酷的,如之前所述的,这里的控制台命令只能在控制台中环境中执行,因为他不依附于任何全局变量比如window,所以其实在JS代码里是访问不了这个copy方法的,所以从代码层面来调用复制功能也就无从谈起。但愿有天浏览器会提供相应的JS实现吧~ keys & values 这是一对基友。前者返回传入对象所有属性名组成的数据,后者返回所有属性值组成的数组。具体请看下面的例子: var tboy={name:'wayou',gender:'unknown',hobby:'opposite to the gender'}; keys(tboy); values(tboy); monitor & unmonitor monitor(function),它接收一个函数名作为参数,比如function a,每次a被执行了,都会在控制台输出一条信息,里面包含了函数的名称a及执行时所传入的参数。 而unmonitor(function)便是用来停止这一监听。 function sayHello(name){ alert('hello,'+name); } monitor(sayHello); sayHello('wayou'); unmonitor(sayHello); sayHello('wayou'); debug & undebug debug同样也是接收一个函数名作为参数。当该函数执行时自动断下来以供调试,类似于在该函数的入口处打了个断点,可以通过debugger来做到,同时也可以通过在Chrome开发者工具里找到相应源码然后手动打断点。 而undebug 则是解除该断点。 而其他还有好些命令则让人没有说的欲望,因为好些都可以通过Chrome开发者工具的UI界面来操作并且比用在控制台输入要方便。 REFERENCE Styled console logging in the Chrome DevTools (Canary) Chrome Console API Chrome Console Command Line API
sublime text很赞,windows上最接近mac逼格的轻量编辑器,对于我这样比较喜欢格调的人来说,简直不二之选啊。 美中不足的是,看久了觉得它的图标似乎不是很上心。现在都流行扁平化了而它还停留在拟物的阶段,拟物也就算了还带一点立体感把整个平面内顷,于是乎想自己换个图标,换个好心情。 如果你有同样的审美那我们继续。 step1. 选择喜欢的图片 首先你需要选择一个中意的图片做为新的图标,这里拿我喜欢的章鱼猫为例。 当然你不喜欢章鱼猫,随便谷歌一下还是有很多正常的ST图标的,比如下面这些 step2. 转为ico格式 网上找的图片大多为png或jpg格式的,这里我们需要ico, so 需要转换一下下。 同样,转ico格式的网站也是蛮多的,比如这个,进去后把图片上传,完了下下来后你得到的就是一个.ico 格式的图片啦~ step3. ResEdit ResEdit是一个Windows下的资源编辑器,可以直接编辑exe文件,更改替换其中的资源,这里我们就用它来更改exe 程序的图标。 如果你手头没有,可以点击上面的链接进入官方页面选择下载。 step4. 用ResEdit打开SublimeText 将SublimeText安装目录下的sublime_text.exe复制一分放到比如桌面什么的。 运行ResEdit, File->Open Project..., 打开刚才复制的sublime_text.exe。 step5. 替换图标 打开后差不多就像上面截图一样,你会看到左边Resources里第一个便是Icon, 在这个文件夹上面右击选择Add resource...->Icon,如下图 之后在弹出的对话框里选择Create from an existing file 之后去选中我们先前准备好的ico文件,将其加载进来。 完了Icon文件夹下多了我们自己的icon文件,现在把原来的图标删除,右击103[English (Australia)] 选择Remove from project。 最后点击File->Save。 step final. 替换exe 最后,将更改后的sublime_text.exe考回SublimeText安装目录下将原来的文件覆盖,当然,如果你以后可能想要恢复原来的图标的话,建议你覆盖前将原来的sublime_text.exe文件备份一下。 All done!
来自bjorn的一篇吐槽文. C 是经久不衰的M1半自动来复,虽然有些时日了但稳定压倒一切。 c++ 是威力强大的双截棍,看看李小龙使它的时候那鼓威风劲你就领悟了。但问题是掌握它需要很长段时间,而在这段时间内经常是把自己打得鼻青脸肿而不是敌人。 Perl是***,偶尔会很有用,但现今用的人已少。 Java是架240发全自动冲锋枪,扫起来爽翻天,前提是弹夹没空。一旦弹夹空了会发生NullPointerException异常,表明这枪就报废了然后你就挂了。 Scala跟Java的冲锋枪没差,唯一的不同是他的使用说明是用你看不懂的方言写成的天书,而且里面大部分都还是在瞎B。 JavaScript是把没有手柄的双刃剑,不多说。 Go 就自制的拥有if err != nil 检查特性的短枪,每次射完你都要执行一次以确定是不是射成功了,并且它只射Tab不射空格。 Rust 纯属3D打印的产物,兴许哪天能派上用场。 bash是被下了诅咒的锤子,挥起来的时候全世界都是钉子,包括你自己的手指。 Python是牛逼哄哄的双管枪,但一次只射一管,另一管不知何时射。或许曾经我用过工具来把它启用。 Ruby「红宝石」是把镶嵌了红宝石的宝剑,你使用它的原因正是那闪闪的宝石可以亮瞎众人。 PHP像一根管子,你把一头插入汽车的排气管,然后另一头通过车窗插到车内,再接着你坐进了车里发动了引擎。 Mathematica 是一个可以发射低轨地球卫星的发射器,非常的华丽与强劲,前提是你也土豪到用得起它。 C#是搭配在一头驴上的激光步枪,效果可想而知。但将它从这头驴上拿下来后,似乎也不能工作。 Prolog 是高级的AI智能武器,你告诉他怎么做做哪些,但最后他还会多生成一些终结者把制造他的人干掉。 Lisp 精巧如剃刀,使用者往往非常疯狂且危险。 原文:If programming languages were weapons
有前端题目大概是这样的:考虑到性能问题,如何快速从一个巨大的数组中随机获取部分元素。 比如有个数组有100K个元素,从中不重复随机选取10K个元素。 为了演示方便我们将数据简化,先给出方案最后再用大点的数据来测试性能的对比。 常规解法 常规做法倒也不难,生成一个0到数组长度减1的随机数,这个数也就是被选中元素在原数组中的下标,获得该元素后将值保存到另一个数组同时通过数组的splice方法将该元素从原数组中删除,以保证下次不会重复取到。 按以上思路,代码大概就是这样的: //元素总数,为了方便演示这里取个小一点的数目比如5,表示总共5个元素 var TOTAL_NUM = 5, //要取得的个数,表示我们要从原数组中随机取3个元素 COUNT = 3, //用随机字符串初始化原数组 arr = new Array(TOTAL_NUM + 1).join('0').split('').map(function() { return Math.random().toString(36).substr(2); }), //保存结果的数组 result = []; console.log('原数组:', arr); //开始我们的选取过程 for (var i = COUNT - 1; i >= 0; i--) { //从原数组中随机取一个元素出来 var index = Math.floor(Math.random() * arr.length); //压入结果数组 result.push(arr[index]); //将该元素从原数组中删除 arr.splice(index, 1); }; console.log('结果数组:', result); 运行结果如下图: 当然上面例子中为了便于演示,将题目要求的100 000 大数目简化为总数为5,同时只取3个。 由测试结果看这种做法是完全可行的。 但存在一个问题:为了下次随机时不重复选取已经选择过的元素,我们将选择过的元素从原数组中通过splice方法进行删除,但这个splice方法操作的过程本身就是数组重新维护其元素索引的过程,这意味着被选择的元素之后的所有元素需要前移一个位置来重新生成一个紧凑的数组,可以想象如果我们取走了原数组中的第1个元素,那么之后的99 999个元素都需要发生变动来完成重组数组的操作,无疑有点耗时。 利用洗牌算法 另一个思路可以是这样的,既然要随机选取,那我可以先把数组的元素打乱先,然后要多少就从开始取多少就行了。一提到随机,自然想到洗牌算法,而关于洗牌算法已经有一个非常经典且高效的Fisher-Yates算法了,这个算法我之前有写过一篇博客介绍过。 这个想法较之前的方法有点逆行的感觉,前面着重点是随机,所以每次都产生一个随机下标到原数组去取,现在是先将数组元素随机打乱,再去正常取。由于洗牌算法非常高效且省去了数组的重组,较之前性能应该有所提升。 照这个思路最后实现的代码大概就是这个样子的: //元素总数,为了方便演示这里取个小一点的数目比如5,表示总共5个元素 var TOTAL_NUM = 5, //要取得的个数,表示我们要从原数组中随机取3个元素 COUNT = 3, //用随机字符串初始化原数组 arr = new Array(TOTAL_NUM + 1).join('0').split('').map(function() { return Math.random().toString(36).substr(2); }), //保存结果的数组 result = []; console.log('原数组:', arr); //随机化原数组 arr = shuffle(arr); //选取元素 result = arr.slice(0, COUNT); console.log('结果数组:', result); function shuffle(array) { var m = array.length, t, i; // 如果还剩有元素… while (m) { // 随机选取一个元素… i = Math.floor(Math.random() * m--); // 与当前元素进行交换 t = array[m]; array[m] = array[i]; array[i] = t; } return array; } 上面代码中包含了经典的洗牌算法Fisher-Yates Shuffle算法,即shuffle函数。具体可参见我的另一篇博客。 运行结果: 从结果来看,此种方法也是可行的。 细想还是存在问题,对于一个比较大的数组来说,不管你的洗牌算法多么高效(即使上面Fisher-Yates算法时间复杂度为O(n)),要随机整个数组也还是很庞大的工程的吧。 所以对于这个题目的探索还没有完。当我在stackoverflow上面发问后,虽然没得到什么惊人的回答,但有个回答却提醒我可以将上面的方法再次改进。 只取所需 那就是我们没有必要随机掉整个数组,在我们取完需要数量的元素后,可以将Fisher-Yates乱序方法中止掉! 思路是非常明显的了, 这样可以省下不少无意义的操作。 所以最后的实现大概成了这样子: //元素总数,为了方便演示这里取个小一点的数目比如5,表示总共5个元素 var TOTAL_NUM = 5, //要取得的个数,表示我们要从原数组中随机取3个元素 COUNT = 3, //用随机字符串初始化原数组 arr = new Array(TOTAL_NUM + 1).join('0').split('').map(function() { return Math.random().toString(36).substr(2); }), //保存结果的数组 result = []; console.log('原数组:', arr); //此段代码由Fisher-Yates shuflle算法更改而来 var m = arr.length, t, i; while (m && result.length < COUNT) { // 随机选取一个元素… i = Math.floor(Math.random() * m--); t = arr[m]; arr[m] = arr[i]; arr[i] = t; result.push(arr[m]); } console.log('结果数组:', result); 上面代码将Fisher-Yates算法略做修改,在取得满足要求的元素之后便停止了,所以较前面的做法更加科学。 运行结果: 性能比较 最后给出上面三个方法耗时的比较,这里将需要操作的数组元素个数回归到题目中要求的100 000来。 下图是jsperf上运行测试的结果,详情可点测试页面重新运行。数值越大越好。由上到下依次是本文中介绍的三种方法。 总结 目前PO主只能想到这些,更优的做法还有待进一步探究。 REFERNCE 由乱序播放说开了去-数组的打乱算法Fisher–Yates Shuffle http://www.cnblogs.com/Wayou/p/fisher_yates_shuffle.html
.Net开发者一定熟悉下面这个画面: 这就是宇宙第一IDE Visual Studio的启动画面,学名叫Splash Screen(或者Splash Window)。同样,Javar们一定对Eclipse的启动画面不会陌生。不只是IDE,很多桌面程序都会有这个Splash 窗口,在程序进行初始化时显示。 这方面做得最赞的非Adobe旗下的设计类软件莫数了,毕竟是搞艺术出身的啊。博主从PS 8.0用起,每次升级新版本激动的不是新功能,首先是激动新的启动画面。下图是最新CC版PS的Splash Screen。视觉效果震撼的一逼。。张牙舞爪的,无出其右。 启动画面也不是桌面程序所独有,完全可以在我们的网页中实现。并且随着时间的推移,现在Web应用越来纷繁复杂,加载也是很费时的,一个Splash Screen就显得很有必要了不是么。 比如谷哥的Gmail,要是全屏运行,就一个原生App的感觉。 下面我们就来为我们的Web应用加上Gmail一样的Splash Screen。程序可以很渣,若表面功夫到位了同样可以显得高端大气上档次。 效果预览请点我 浏览代码请点我 进度的获取 展示静态图片还好,如果你的启动界面要显示程序进行的进度的话,一个很棘手的问题来了,如何获取进度。经过大量的调研(写过论文的同学都知道,类似'经过大量实验表明…'的表述其实很有可能是只做了一次实验就开始写结论了)我发现,没有办法获取一个页面的实际下载进度!当然,不排除我孤陋寡闻,如果你知道这样的方法请告诉我。 对于页面中的异步操作,倒是可以监听到进度的。但也得分情况。HTML5规范中,Ajax多了个progress事件,通过它可以获取异步操作的完成情况,但前提是event.lengthComputable属性为真是才管用。也就是说有些请求的结果我们是可以知道大小的,但更多时候服务器返回的内容的大小是不确定的,这种情况下即使你监听了progress事件也无法获取真实的操作进度。 既然如此,那我们就不要那么死磕,具体进行到百分之几意义不大,我们的目的是提高用户体验,在用户等待的这个过程中有东西可看,或者有一个活着的会动的东西表明程序还在跑而不是出错了卡死了。所以给用户展示一个会动的进度条即可(我相信大多数带进度条的程序也是这么干的),直到页面全部加载完成时把进度条托到100%。 插曲:在我探索如何获取页面真实下载进度的过程中,研究了pace.js的代码,一个做得非常棒的页面加载进度插件,发现他内部也是这么干的,页面上显示的进度并不真实返回页面加载的实现进度,只是不断的增加而以,等页面加载完了再拖到100%。当然该库写得比较完善,处理了各种情况比如ajax,web socket等。另外就是Gmail,经过大量(也有可能只有两三次,请不必太认真)的刷新页面尝试之后,我发现个规律,它的进度条会一路跑到一个点然后停下来,然后再一路跑到终点!之前的结论。(不过谷歌拥有牛逼烘烘的工程师,不排除他用了啥高科技算法在里面能够精确地返回页面加载的进度。Anyway, 如果我这里的结论错,请别太认真找我麻烦) Nprogress 方便起见,这里使用nprogress这个JS库来显示动画。它提供了很方便的几个API可供我们使用。 NProgress.start() — 启动进度条 NProgress.set(0.4) — 将进度设置到具体的百分比位置 NProgress.inc() — 少量增加进度 NProgress.done() — 将进度条标为完成状态 定义我们的Splash Screen 好的,思路出来了下面我们就开始毫无技术含量的施工。 具体来说,首先页面只显示我们预先定义好的Splash Screen,它要占满整个屏幕且z-index设为页面中最高。 这里直接借用Juri 发表在他博客中的代码,不过我们使用我之前一篇博文《前端冷知识集锦》可提到的技巧,将HTML代码存放在一个script标签中。 <script type="text" id="splash-template"> <div class="splash card"> <div role="spinner"> <div class="spinner-icon"></div> </div> <p class="lead" style="text-align:center">不要回来,马上走开...</p> <div class="progress"> <div class="mybar" role="bar"> </div> </div> </div> </script> 这个splash screen会在HTML加载好之后第一时间显示。接下来就可以这样做了,在页面最开始调用 Nprogress.start()启动进度条,而在这个splash screen下方遮住的页面中,继续我们程序的初始化,做其他一些非常耗时的操作等。比如你想象一下Gmail,最开始可能页面只有显示进度条那些基本的HTML和JS代码,然后需要向服务器请求大量的邮件信息,数据接收完后,再组织成HTML生成邮件列表append到页面,但这个过程因为被进度条挡住了,所以我们看不见。等一切就绪,再调用Nprogress.done()将进度条隐藏。这时你看到的就是一个完整的页面了。 事件的顺序 window.onload事件是在整个页面加载完成,包括其中所有图片,iframe等。所以,可以确定在这个事件里面把进度搞到100%是没有问题且逻辑正确的。 确定了何时结束再来看何时开始。既然我们一开始就要显示Splash Window且操作之前定义好的splash screen模板,意思就是说再怎么早开始也得等我们splash screen部分的HTML加载完成之后再进行吧。所以,得到的结论就是把进度条开始的代码放在这部分HTML代码之后,但这种HTML中插JS的做法很不好,所以最后决定还是放在$(document).ready()里面,因为这个事件是在页面HTML加载完后触发的,但只是DOM,不包括其他比如上面提到的图片,iframe等,所以它是比window.onload先触发的。 所以在页面的head标签里面加入以下代码: $(function() { NProgress.configure({ template: $('#splash-template').html() }); NProgress.start(); }); $(window).load(function() { NProgress.done(); }) 实际应用中更科学的做法其实应该是这样的,页面只有关于进度条的代码,程序的内容全部通过Ajax填充到页面,然后在页面中监视所有Ajax的返回情况。 模拟耗时的操作 一切就绪了,但需要解决一个事情就是如何模拟耗时的操作。我们现在弄的这个例子它不费时,无法看到缓慢的加载效果,并且本地测试,放上几十张图片都会很快就完事。 当然可以用setTimeout来达到目的,但不太科学吧,还是要弄得真实点。于是我们在页面放一个iframe,从其他网站引用页面,这样多少会有些加载的时间。 所以这个例子最后的代码差不多是这样的了: HTML: <!doctype html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta name="description" content=""> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>splash screen example</title> <link rel="stylesheet" href="nprogress.css"> <link rel="stylesheet" href="main.css"> <script src="jquery.min.js"></script> <script src="nprogress.js"></script> <script type="text/javascript"> $(function(){ NProgress.configure({ template: $('#splash-template').html() }); NProgress.start(); }); $(window).load(function(){ NProgress.done(); }) </script> </head> <body> <script type="text" id="splash-template"> <div class="splash card"> <div role="spinner"> <div class="spinner-icon"></div> </div> <p class="lead" style="text-align:center">不要回来,马上走开...</p> <div class="progress"> <div class="mybar" role="bar"> </div> </div> </div> </script> <iframe id="iframe" style="width: 100%; height: 660px;" src="http://wayou.github.io/SlipHover/" frameborder="0"></iframe> </body> </html> 加入些美化的样式: CSS: html,body,iframe{ margin: 0; padding: 0; } #nprogress{ position: fixed; top: 0; right: 0; bottom: 0; left: 0; background-color: #f7f7f7; z-index: 999; } .spinner-icon{ display: none!important; } .splash { position:absolute; top:40%; left:0; right:0; margin: auto; } .splash img { display: block; margin-left: auto; margin-right: auto; height: 100px; width: 100px; } .card { background-color: #f7f7f7; padding: 20px 25px 15px; margin: 0 auto 25px; width: 380px; } .mybar { background: #29d; height:10px; } .progress { height: 10px; overflow: hidden; } 现在可以运行页面查看效果了。好了,就这么多。效果预览请点我 Reference Intercept Page Load: https://developer.mozilla.org/en-US/Add-ons/Overlay_Extensions/XUL_School/Intercepting_Page_Loads Gmail-style progress bar when page is loading http://stackoverflow.com/questions/8020929/gmail-style-progress-bar-when-page-is-loading http://www.gayadesign.com/scripts/queryLoader/ https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Monitoring_progress http://api.jquery.com/jQuery.ajax/ http://stackoverflow.com/questions/15328275/show-progressbar-while-loading-pages-using-jquery-ajax-in-single-page-website http://www.dave-bond.com/blog/2010/01/JQuery-ajax-progress-HMTL5/ http://github.hubspot.com/pace/ http://onextrapixel.com/examples/youtube-like-ajax-loading-bar/
因为觉得博客园自带的代码高亮样式很单一,不符合作为前端的我的审美习惯,于是下定决心要想办法折腾出一个方法来应用上另外一套代码高亮样式。 虽然探索的过程是很痛苦的,但最后还是成功了,但也不枉付出的那些努力。近来有网友问及如何实现,现分享出来,看完本文后你也可以把自己博客的代码整得漂亮些,让别人看着舒服些了。 方法其实是很麻烦的,即使是写了好几篇博客了对这个过程我已经娴熟了,但其实也还是挻麻烦的。不过谁叫我有是个偏执狂呢,为了让页面漂亮我愿麻烦自己,舒服大家。如果你有更好的方法那当然更好。 安装sublimehighlight 我在博客里应用的样式是SublimeText编辑器里面的主题,这跟我用它来编写代码有关。其实如果ST支持复制为富文本形式的话,事情就要方便得多,直接copy然后paste到word里就把样式带上了,包括缩进,代码高亮等。遗憾的是它不支持。所以出路便是找一个可用的ST插件让它支持富文本复制。 好在ST流行度大,社区活跃,插件众多,还真有款能够完成我需求的插件--n1k0/SublimeHighlight。更详细的关于如何安装的问题等可见它的项目页面。 简单点其实跟安装其他ST插件是一样的,先Ctrl+Shift+P调出control panel,然后输入install package,不用输完,当输入了Install后便出来了,然后回车等待插件列表的显示,这个过程大概有个几秒钟的样子。 然后输入插件名称sublimehighlight,选中并进行安装。如果这一步进行顺利,则跳到下一节。 当你进行到上面一步发现搜不出该插件时,需要手动添加该插件的repo到本地。 具体做法是退出刚才的界面重新输入Ctrl+Shift+P调出control panel,输入add repository 选中并回车。 这时界面下方会出现输入repo地址的地方,将https://github.com/n1k0/SublimeHighlight/tree/python3输入后回车确定。 当提示添加成功后再次进行上面安装插件的步骤来到插件列表,输入sublimehighlight,选中该插件进行安装,如果一切顺利,恭喜你万里长征第一步走完! 设置喜欢的代码样式 安装完成后,可以设置你喜欢的样式,这个样式是你复制出来的样式,跟你在ST里面用的代码样式是没有关系的。也就是说最终复制出来的代码的样式以这个插件的设置为准。 可选的样式可以在插件的GitHub主页看到,下图直接来自其项目页面,图中包括了主题的名称和预览: 设置方法是依次点开preferences=>package settings=>sublimehighlight=>settings - user 会调出一个设置页面,输入喜欢的样式的名字,像下面这样: 当然,还可以指定要使用的字体等其他设置选项,同样,请前往插件主页进行参考。 将代码复制为HTML 像上面那样弄好后,最后一步,就是把代码复制成HTML形式放到博客里去啦! 依次点开edit=>highlight=>convert to html,此命令会将代码转成HTML形式。 之后,在新出来的标签中,请将class为highlight的div直接ctrl+c复制。这一块便是我们需要的东西。 最后,在写博客的时候,以HTML方式编辑博客,将刚才的内容进行粘贴。噢啦! 确定之后的效果: 过程是有点麻烦吧,如果代码量大的话,这个工作将会很恼人。Anyway, 完全看你愿不愿意了。Enjoy!
本文基于lukehoban/es6features ,同时参考了大量博客资料,具体见文末引用。 ES6(ECMAScript 6)是即将到来的新版本JavaScript语言的标准,代号harmony(和谐之意,显然没有跟上我国的步伐,我们已经进入中国梦版本了)。上一次标准的制订还是2009年出台的ES5。目前ES6的标准化工作正在进行中,预计会在14年12月份放出正式敲定的版本。但大部分标准已经就绪,且各浏览器对ES6的支持也正在实现中。要查看ES6的支持情况请点此。 目前想要运行ES6代码的话,可以用google/traceur-compiler将代码转译。点此访问traceur-compiler 在线版本时实编辑ES6代码并查看转换后的结果,代码运行结果会在console显示。 另外,关于Google Traceur,业界大神Addy Osmani利用前者写了个Chrome插件ES6 Tepl,安装后也可以进行ES6的测试。 当然,并不是所有ES6新特性都被实现了,所以上面的方法可以测试大部分,有一些还是无法测试的。 虽然ES6都还没真正发布,但已经有用ES6重写的程序了,各种关于ES789的提议已经开始了,这你敢信。潮流不是我等大众所能追赶的。 潮流虽然太快,但我们不停下学习的步伐,就不会被潮流丢下的,下面来领略下ES6中新特性,一堵新生代JS的风采。 箭头操作符 如果你会C#或者Java,你肯定知道lambda表达式,ES6中新增的箭头操作符=>便有异曲同工之妙。它简化了函数的书写。操作符左边为输入的参数,而右边则是进行的操作以及返回的值Inputs=>outputs。 我们知道在JS中回调是经常的事,而一般回调又以匿名函数的形式出现,每次都需要写一个function,甚是繁琐。当引入箭头操作符后可以方便地写回调了。请看下面的例子。 var array = [1, 2, 3]; //传统写法 array.forEach(function(v, i, a) { console.log(v); }); //ES6 array.forEach(v = > console.log(v)); 大家可以打开文章开头提到的traceur在线代码转译页面输入代码来查看效果。 类的支持 ES6中添加了对类的支持,引入了class关键字(其实class在JavaScript中一直是保留字,目的就是考虑到可能在以后的新版本中会用到,现在终于派上用场了)。JS本身就是面向对象的,ES6中提供的类实际上只是JS原型模式的包装。现在提供原生的class支持后,对象的创建,继承更加直观了,并且父类方法的调用,实例化,静态方法和构造函数等概念都更加形象化。 下面代码展示了类在ES6中的使用。再次啰嗦一句,你可以将代码贴到traceur自己查看运行结果。 //类的定义 class Animal { //ES6中新型构造器 constructor(name) { this.name = name; } //实例方法 sayName() { console.log('My name is '+this.name); } } //类的继承 class Programmer extends Animal { constructor(name) { //直接调用父类构造器进行初始化 super(name); } program() { console.log("I'm coding..."); } } //测试我们的类 var animal=new Animal('dummy'), wayou=new Programmer('wayou'); animal.sayName();//输出 ‘My name is dummy’ wayou.sayName();//输出 ‘My name is wayou’ wayou.program();//输出 ‘I'm coding...’ 增强的对象字面量 对象字面量被增强了,写法更加简洁与灵活,同时在定义对象的时候能够做的事情更多了。具体表现在: 可以在对象字面量里面定义原型 定义方法可以不用function关键字 直接调用父类方法 这样一来,对象字面量与前面提到的类概念更加吻合,在编写面向对象的JavaScript时更加轻松方便了。 //通过对象字面量创建对象 var human = { breathe() { console.log('breathing...'); } }; var worker = { __proto__: human, //设置此对象的原型为human,相当于继承human company: 'freelancer', work() { console.log('working...'); } }; human.breathe();//输出 ‘breathing...’ //调用继承来的breathe方法 worker.breathe();//输出 ‘breathing...’ 字符串模板 字符串模板相对简单易懂些。ES6中允许使用反引号 ` 来创建字符串,此种方法创建的字符串里面可以包含由美元符号加花括号包裹的变量${vraible}。如果你使用过像C#等后端强类型语言的话,对此功能应该不会陌生。 //产生一个随机数 var num=Math.random(); //将这个数字输出到console console.log(`your num is ${num}`); 解构 自动解析数组或对象中的值。比如若一个函数要返回多个值,常规的做法是返回一个对象,将每个值做为这个对象的属性返回。但在ES6中,利用解构这一特性,可以直接返回一个数组,然后数组中的值会自动被解析到对应接收该值的变量中。 var [x,y]=getVal(),//函数返回值的解构 [name,,age]=['wayou','male','secrect'];//数组解构 function getVal() { return [ 1, 2 ]; } console.log('x:'+x+', y:'+y);//输出:x:1, y:2 console.log('name:'+name+', age:'+age);//输出: name:wayou, age:secrect 参数默认值,不定参数,拓展参数 默认参数值 现在可以在定义函数的时候指定参数的默认值了,而不用像以前那样通过逻辑或操作符来达到目的了。 function sayHello(name){ //传统的指定默认参数的方式 var name=name||'dude'; console.log('Hello '+name); } //运用ES6的默认参数 function sayHello2(name='dude'){ console.log(`Hello ${name}`); } sayHello();//输出:Hello dude sayHello('Wayou');//输出:Hello Wayou sayHello2();//输出:Hello dude sayHello2('Wayou');//输出:Hello Wayou 不定参数 不定参数是在函数中使用命名参数同时接收不定数量的未命名参数。这只是一种语法糖,在以前的JavaScript代码中我们可以通过arguments变量来达到这一目的。不定参数的格式是三个句点后跟代表所有不定参数的变量名。比如下面这个例子中,…x代表了所有传入add函数的参数。 //将所有参数相加的函数 function add(...x){ return x.reduce((m,n)=>m+n); } //传递任意个数的参数 console.log(add(1,2,3));//输出:6 console.log(add(1,2,3,4,5));//输出:15 拓展参数 拓展参数则是另一种形式的语法糖,它允许传递数组或者类数组直接做为函数的参数而不用通过apply。 var people=['Wayou','John','Sherlock']; //sayHello函数本来接收三个单独的参数人妖,人二和人三 function sayHello(people1,people2,people3){ console.log(`Hello ${people1},${people2},${people3}`); } //但是我们将一个数组以拓展参数的形式传递,它能很好地映射到每个单独的参数 sayHello(...people);//输出:Hello Wayou,John,Sherlock //而在以前,如果需要传递数组当参数,我们需要使用函数的apply方法 sayHello.apply(null,people);//输出:Hello Wayou,John,Sherlock let与const 关键字 可以把let看成var,只是它定义的变量被限定在了特定范围内才能使用,而离开这个范围则无效。const则很直观,用来定义常量,即无法被更改值的变量。 for (let i=0;i<2;i++)console.log(i);//输出: 0,1 console.log(i);//输出:undefined,严格模式下会报错 for of 值遍历 我们都知道for in 循环用于遍历数组,类数组或对象,ES6中新引入的for of循环功能相似,不同的是每次循环它提供的不是序号而是值。 var someArray = [ "a", "b", "c" ]; for (v of someArray) { console.log(v);//输出 a,b,c } 注意,此功能google traceur并未实现,所以无法模拟调试,下面有些功能也是如此 iterator, generator 这一部分的内容有点生涩,详情可以参见这里。以下是些基本概念。 iterator:它是这么一个对象,拥有一个next方法,这个方法返回一个对象{done,value},这个对象包含两个属性,一个布尔类型的done和包含任意值的value iterable: 这是这么一个对象,拥有一个obj[@@iterator]方法,这个方法返回一个iterator generator: 它是一种特殊的iterator。反的next方法可以接收一个参数并且返回值取决与它的构造函数(generator function)。generator同时拥有一个throw方法 generator 函数: 即generator的构造函数。此函数内可以使用yield关键字。在yield出现的地方可以通过generator的next或throw方法向外界传递值。generator 函数是通过function*来声明的 yield 关键字:它可以暂停函数的执行,随后可以再进进入函数继续执行 模块 在ES6标准中,JavaScript原生支持module了。这种将JS代码分割成不同功能的小块进行模块化的概念是在一些三方规范中流行起来的,比如CommonJS和AMD模式。 将不同功能的代码分别写在不同文件中,各模块只需导出公共接口部分,然后通过模块的导入的方式可以在其他地方使用。下面的例子来自tutsplus: // point.js module "point" { export class Point { constructor (x, y) { public x = x; public y = y; } } } // myapp.js //声明引用的模块 module point from "/point.js"; //这里可以看出,尽管声明了引用的模块,还是可以通过指定需要的部分进行导入 import Point from "point"; var origin = new Point(0, 0); console.log(origin); Map,Set 和 WeakMap,WeakSet 这些是新加的集合类型,提供了更加方便的获取属性值的方法,不用像以前一样用hasOwnProperty来检查某个属性是属于原型链上的呢还是当前对象的。同时,在进行属性值添加与获取时有专门的get,set 方法。 下方代码来自es6feature // Sets var s = new Set(); s.add("hello").add("goodbye").add("hello"); s.size === 2; s.has("hello") === true; // Maps var m = new Map(); m.set("hello", 42); m.set(s, 34); m.get(s) == 34; 有时候我们会把对象作为一个对象的键用来存放属性值,普通集合类型比如简单对象会阻止垃圾回收器对这些作为属性键存在的对象的回收,有造成内存泄漏的危险。而WeakMap,WeakSet则更加安全些,这些作为属性键的对象如果没有别的变量在引用它们,则会被回收释放掉,具体还看下面的例子。 正文代码来自es6feature // Weak Maps var wm = new WeakMap(); wm.set(s, { extra: 42 }); wm.size === undefined // Weak Sets var ws = new WeakSet(); ws.add({ data: 42 });//因为添加到ws的这个临时对象没有其他变量引用它,所以ws不会保存它的值,也就是说这次添加其实没有意思 Proxies Proxy可以监听对象身上发生了什么事情,并在这些事情发生后执行一些相应的操作。一下子让我们对一个对象有了很强的追踪能力,同时在数据绑定方面也很有用处。 以下例子借用自这里。 //定义被侦听的目标对象 var engineer = { name: 'Joe Sixpack', salary: 50 }; //定义处理程序 var interceptor = { set: function (receiver, property, value) { console.log(property, 'is changed to', value); receiver[property] = value; } }; //创建代理以进行侦听 engineer = Proxy(engineer, interceptor); //做一些改动来触发代理 engineer.salary = 60;//控制台输出:salary is changed to 60 上面代码我已加了注释,这里进一步解释。对于处理程序,是在被侦听的对象身上发生了相应事件之后,处理程序里面的方法就会被调用,上面例子中我们设置了set的处理函数,表明,如果我们侦听的对象的属性被更改,也就是被set了,那这个处理程序就会被调用,同时通过参数能够得知是哪个属性被更改,更改为了什么值。 Symbols 我们知道对象其实是键值对的集合,而键通常来说是字符串。而现在除了字符串外,我们还可以用symbol这种值来做为对象的键。Symbol是一种基本类型,像数字,字符串还有布尔一样,它不是一个对象。Symbol 通过调用symbol函数产生,它接收一个可选的名字参数,该函数返回的symbol是唯一的。之后就可以用这个返回值做为对象的键了。Symbol还可以用来创建私有属性,外部无法直接访问由symbol做为键的属性值。 以下例子来自es6features (function() { // 创建symbol var key = Symbol("key"); function MyClass(privateData) { this[key] = privateData; } MyClass.prototype = { doStuff: function() { ... this[key] ... } }; })(); var c = new MyClass("hello") c["key"] === undefined//无法访问该属性,因为是私有的 Math,Number,String,Object 的新API 对Math,Number,String还有Object等添加了许多新的API。下面代码同样来自es6features,对这些新API进行了简单展示。 Number.EPSILON Number.isInteger(Infinity) // false Number.isNaN("NaN") // false Math.acosh(3) // 1.762747174039086 Math.hypot(3, 4) // 5 Math.imul(Math.pow(2, 32) - 1, Math.pow(2, 32) - 2) // 2 "abcde".contains("cd") // true "abc".repeat(3) // "abcabcabc" Array.from(document.querySelectorAll('*')) // Returns a real Array Array.of(1, 2, 3) // Similar to new Array(...), but without special one-arg behavior [0, 0, 0].fill(7, 1) // [0,7,7] [1,2,3].findIndex(x => x == 2) // 1 ["a", "b", "c"].entries() // iterator [0, "a"], [1,"b"], [2,"c"] ["a", "b", "c"].keys() // iterator 0, 1, 2 ["a", "b", "c"].values() // iterator "a", "b", "c" Object.assign(Point, { origin: new Point(0,0) }) Promises Promises是处理异步操作的一种模式,之前在很多三方库中有实现,比如jQuery的deferred 对象。当你发起一个异步请求,并绑定了.when(), .done()等事件处理程序时,其实就是在应用promise模式。 //创建promise var promise = new Promise(function(resolve, reject) { // 进行一些异步或耗时操作 if ( /*如果成功 */ ) { resolve("Stuff worked!"); } else { reject(Error("It broke")); } }); //绑定处理程序 promise.then(function(result) { //promise成功的话会执行这里 console.log(result); // "Stuff worked!" }, function(err) { //promise失败会执行这里 console.log(err); // Error: "It broke" }); 总结 总结就是一句话,前后端差异越来越小了。 REFERENCE Google traceur online compiler http://google.github.io/traceur-compiler/demo/repl.html array destruction http://ariya.ofilabs.com/2013/02/es6-and-destructuring-assignment.html class http://www.joezimjs.com/javascript/javascript-prototypal-inheritance-and-what-es6-classes-have-to-say-about-it/ enhanced object literal http://maximilianhoffmann.com/posts/object-based-javascript-in-es6 parameters http://globaldev.co.uk/2013/10/es6-part-4/ let keyword http://globaldev.co.uk/2013/09/es6-part-2/ for of iterator https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of#Browser_compatibility the Iterator protocol https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/The_Iterator_protocol generators https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function* ES6 Iterators, Generators, and Iterables http://domenic.me/2013/09/06/es6-iterators-generators-and-iterables/ proxies http://ariya.ofilabs.com/2013/07/es6-and-proxy.html symbols http://tc39wiki.calculist.org/es6/symbols/ promise http://www.html5rocks.com/en/tutorials/es6/promises/ 8 cool features in ES6 http://code.tutsplus.com/tutorials/eight-cool-features-coming-in-es6--net-33175 (此文章错误较多)
前端已经被玩儿坏了!像console.log()可以向控制台输出图片等炫酷的玩意已经不是什么新闻了,像用||操作符给变量赋默认值也是人尽皆知的旧闻了,今天看到Quora上一个帖子,瞬间又GET了好多前端技能,一些属于技巧,一些则是闻所未闻的冷知识,一时间还消化不过来。现分类整理出来分享给大家,也补充了一些平时的积累和扩展了一些内容。 HTML篇 浏览器地址栏运行JavaScript代码 这个很多人应该还是知道的,在浏览器地址栏可以直接运行JavaScript代码,做法是以javascript:开头后跟要执行的语句。比如: javascript:alert('hello from address bar :)'); 将以上代码贴到浏览器地址栏回车后alert正常执行,一个弹窗神现。 需要注意的是如果是通过copy paste代码到浏览器地址栏的话,IE及Chrome会自动去掉代码开头的javascript:,所以需要手动添加起来才能正确执行,而Firefox中虽然不会自动去掉,但它根本就不支持在地址栏运行JS代码,sigh~ 这一技术在我的另一篇博文《让Chrome 接管邮件连接,收发邮件更方便了》中有使用到,利用在浏览器地址栏中执行JavaScript代码将Gmail设置为系统的邮件接管程序。 浏览器地址栏运行HTML代码 如果说上面那条小秘密知道的人还算多的话,这条秘笈知道的人就要少一些了,在非IE内核的浏览器地址栏可以直接运行HTML代码! 比如在地址栏输入以下代码然后回车运行,会出现指定的页面内容。 data:text/html,<h1>Hello, world!</h1> 你造么,可以把浏览器当编辑器 还是浏览器地址栏上做文章,将以下代码贴到地址栏运行后浏览器变成了一个原始而简单的编辑器,与Windows自带的notepad一样,吼吼。 data:text/html, <html contenteditable> 归根结底多亏了HTML5中新加的contenteditable属性,当元素指定了该属性后,元素的内容成为可编辑状态。 推而广之,将以下代码放到console执行后,整个页面将变得可编辑,随意践踏吧~ document.body.contentEditable='true'; 利用a标签自动解析URL 很多时候我们有从一个URL中提取域名,查询关键字,变量参数值等的需要,而万万没想到可以让浏览器方便地帮我们完成这一任务而不用我们写正则去抓取。方法就在JS代码里先创建一个a标签然后将需要解析的URL赋值给a的href属性,然后就得到了一切我们想要的了。 var a = document.createElement('a'); a.href = 'http://www.cnblogs.com/wayou/p/'; console.log(a.host); 利用这一原理,稍微扩展一下,就得到了一个更加健壮的解析URL各部分的通用方法了。下面代码来自James的博客。 function parseURL(url) { var a = document.createElement('a'); a.href = url; return { source: url, protocol: a.protocol.replace(':',''), host: a.hostname, port: a.port, query: a.search, params: (function(){ var ret = {}, seg = a.search.replace(/^\?/,'').split('&'), len = seg.length, i = 0, s; for (;i<len;i++) { if (!seg[i]) { continue; } s = seg[i].split('='); ret[s[0]] = s[1]; } return ret; })(), file: (a.pathname.match(/\/([^\/?#]+)$/i) || [,''])[1], hash: a.hash.replace('#',''), path: a.pathname.replace(/^([^\/])/,'/$1'), relative: (a.href.match(/tps?:\/\/[^\/]+(.+)/) || [,''])[1], segments: a.pathname.replace(/^\//,'').split('/') }; } 页面拥有ID的元素会创建全局变量 在一张HTML页面中,所有设置了ID属性的元素会在JavaScript的执行环境中创建对应的全局变量,这意味着document.getElementById像人的阑尾一样显得多余了。但实际项目中最好老老实实该怎么写就怎么写,毕竟常规代码出乱子的机会要小得多。 <div id="sample"></div> <script type="text/javascript"> console.log(sample); </script> 加载CDN文件时,可以省掉HTTP标识 现在很流行的CDN即从专门的服务器加载一些通用的JS和CSS文件,出于安全考虑有的CDN服务器使用HTTPS方式连接,而有的是传统的HTTP,其实我们在使用时可以忽略掉这个,将它从URL中省去。 <script src="//domain.com/path/to/script.js"></script> 这一点在之前一篇译文博客《jQuery编程最佳实践》中也有提到。 利用script标签保存任意信息 将script标签设置为type='text'然后可以在里面保存任意信息,之后可以在JavaScript代码中很方便地获取。 <script type="text" id="template"> <h1>This won't display</h1> </script> var text = document.getElementById('template').innerHTML CSS篇 关于CSS的恶作剧 相信你看完以下代码后能够预料到会出现什么效果。 *{ cursor: none!important; } 简单的文字模糊效果 以下两行简单的CSS3代码可达到将文字模糊化处理的目的,出来的效果有点像使用PS的滤镜,so cool! p { color: transparent; text-shadow: #111 0 0 5px; } 垂直居中 有好多次博主都有这样的需求,垂直居中显示某个DIV,我们知道CSS中天然有水平居中的样式text-align:center。唯独这个垂直居中无解。 当然你可以将容器设置为display:table,然后将子元素也就是要垂直居中显示的元素设置为display:table-cell,然后加上vertical-align:middle来实现,但此种实现往往会因为display:table而破坏整体布局,那还不如直接用table标签了呢。 下面这个样式利用了translate来巧妙实现了垂直居中样式,需IE9+。 .center-vertical { position: relative; top: 50%; transform: translateY(-50%); } 相比而言,水平居中要简单得多,像上面提到的text-align:center,经常用到的技巧还有margin:0 auto。但对于margin大法也只在子元素宽度小于容器宽度时管用,当子元素宽度大于容器宽度时此法失效。 如法炮制,利用left和transform同样可实现水平居中,不过意义不大,毕竟text-align和margin差不多满足需求了。 .center-horizontal { position: relative; left: 50%; transform: translateX(-50%); } 多重边框 利用重复指定box-shadow来达到多个边框的效果 在线演示 /*CSS Border with Box-Shadow Example*/ div { box-shadow: 0 0 0 6px rgba(0, 0, 0, 0.2), 0 0 0 12px rgba(0, 0, 0, 0.2), 0 0 0 18px rgba(0, 0, 0, 0.2), 0 0 0 24px rgba(0, 0, 0, 0.2); height: 200px; margin: 50px auto; width: 400px } 实时编辑CSS 通过设置style标签的display:block样式可以让页面的style标签显示出来,并且加上contentEditable属性后可以让样式成为可编辑状态,更改后的样式效果也是实时更新呈现的。此技巧在IE下无效。拥有此技能者,逆天也! <!DOCTYPE html> <html> <body> <style style="display:block" contentEditable> body { color: blue } </style> </body> </html> 创建长宽比固定的元素 通过设置父级窗口的padding-bottom可以达到让容器保持一定的长度比的目的,这在响应式页面设计中比较有用,能够保持元素不变形。 <div style="width: 100%; position: relative; padding-bottom: 20%;"> <div style="position: absolute; left: 0; top: 0; right: 0; bottom: 0;background-color:yellow;"> this content will have a constant aspect ratio that varies based on the width. </div> </div> CSS中也可以做简单运算 通过CSS中的calc方法可以进行一些简单的运算,从而达到动态指定元素样式的目的。 .container{ background-position: calc(100% - 50px) calc(100% - 20px); } JavaScript篇 生成随机字符串 利用Math.random和toString生成随机字符串,来自前一阵子看到的一篇博文。这里的技巧是利用了toString方法可以接收一个基数作为参数的原理,这个基数从2到36封顶。如果不指定,默认基数是10进制。略屌! function generateRandomAlphaNum(len) { var rdmString = ""; for (; rdmString.length < len; rdmString += Math.random().toString(36).substr(2)); return rdmString.substr(0, len); } 整数的操作 JavaScript中是没有整型概念的,但利用好位操作符可以轻松处理,同时获得效率上的提升。 |0和~~是很好的一个例子,使用这两者可以将浮点转成整型且效率方面要比同类的parseInt,Math.round 要快。在处理像素及动画位移等效果的时候会很有用。性能比较见此。 var foo = (12.4 / 4.13) | 0;//结果为3 var bar = ~~(12.4 / 4.13);//结果为3 顺便说句,!!将一个值方便快速转化为布尔值 !!window===true 。 重写原生浏览器方法以实现新功能 下载的代码通过重写浏览器的alert让它可以记录弹窗的次数。 (function() { var oldAlert = window.alert, count = 0; window.alert = function(a) { count++; oldAlert(a + "\n You've called alert " + count + " times now. Stop, it's evil!"); }; })(); alert("Hello World"); 关于console的恶作剧 关于重写原生方法,这里有个恶作剧大家可以拿去寻开心。Chrome的console.log是支持对文字添加样式的,甚至log图片都可以。于是可以重写掉默认的log方法,把将要log的文字应用到CSS的模糊效果,这样当有人试图调用console.log()的时候,出来的是模糊不清的文字。好冷,我表示没有笑。 是从这篇G+帖子的评论里看到的。使用之后的效果是再次调用log会输出字迹模糊不清的文字。 var _log = console.log; console.log = function() { _log.call(console, '%c' + [].slice.call(arguments).join(' '), 'color:transparent;text-shadow:0 0 2px rgba(0,0,0,.5);'); }; 不声明第三个变量的值交换 我们都知道交换两个变量值的常规做法,那就是声明一个中间变量来暂存。但鲜有人去挑战不声明中间变量的情况,下面的代码给出了这种实现。蛮有创意 的。 var a=1,b=2;a=[b,b=a][0]; 万物皆对象 在JavaScript的世界,万物皆对象。除了null和undefined,其他基本类型数字,字符串和布尔值都有对应有包装对象。对象的一个特征是你可以在它身上直接调用方法。对于数字基本类型,当试图在其身上调用toString方法会失败,但用括号括起来后再调用就不会失败了,内部实现是用相应的包装对象将基本类型转为对象。所以(1).toString()相当于new Number(1).toString()。因此,你的确可以把基本类型数字,字符串,布尔等当对象使用的,只是注意语法要得体。 同时我们注意到,JavaScript中数字是不分浮点和整形的,所有数字其实均是浮点类型,只是把小数点省略了而以,比如你看到的1可以写成1.,这也就是为什么当你试图1.toString()时会报错,所以正确的写法应该是这样:1..toString(),或者如上面所述加上括号,这里括号的作用是纠正JS解析器,不要把1后面的点当成小数点。内部实现如上面所述,是将1.用包装对象转成对象再调用方法。 If语句的变形 当你需要写一个if语句的时候,不妨尝试另一种更简便的方法,用JavaScript中的逻辑操作符来代替。 var day=(new Date).getDay()===0; //传统if语句 if (day) { alert('Today is Sunday!'); }; //运用逻辑与代替if day&&alert('Today is Sunday!'); 比如上面的代码,首先得到今天的日期,如果是星期天,则弹窗,否则什么也不做。我们知道逻辑操作存在短路的情况,对于逻辑与表达式,只有两者都真才结果才为真,如果前面的day变量被判断为假了,那么对于整个与表达式来说结果就是假,所以就不会继续去执行后面的alert了,如果前面day为真,则还要继续执行后面的代码来确定整个表达式的真假。利用这点达到了if的效果。 对于传统的if语句,如果执行体代码超过了1 条语句,则需要加花括号,而利用逗号表达式,可以执行任意条代码而不用加花括号。 if(conditoin) alert(1),alert(2),console.log(3); 上面if语句中,如果条件成立则执行三个操作,但我们不需要用花括号将这三句代码括起来。当然,这是不推荐的,这里是冷知识课堂:) 禁止别人以iframe加载你的页面 下面的代码已经不言自明了,没什么好多说的。 if (window.location != window.parent.location) window.parent.location = window.location; console.table Chrome专属,IE绕道的console方法。可以将JavaScript关联数组以表格形式输出到浏览器console,效果很惊赞,界面很美观。 //采购情况 var data = [{'品名': '杜雷斯', '数量': 4}, {'品名': '冈本', '数量': 3}]; console.table(data); REFERENCE What are the most interesting HTML/JS/DOM/CSS hacks that most web developers don't know about? 45 Useful JavaScript Tips, Tricks and Best Practices 10 Small Things You May Not Know About Javascript
GitHub官方有个表情项目,旨在丰富文字信息。意味着你可以在提交代码的时候,在提交信息里面添加表情,同时也可以在项目的ReadMe.md文件里面使用表情。除此之外,当然还有项目在GitHub上的wiki页面,总之在GitHub的页面上,都可以使用。 GitHub官方表情项目地址:github / gemoji 效果预览 项目README.md 项目wiki页面 代码提交时 可用表情清单 可以访问这个页面查看所有支持的表情以及对应的代码 使用方法 使用方法为前后冒号包围表情代号的句法。 :blush: 会显示成 所以你在提交代码的时候可以这样写提交信息: git commit –m 'commit some changes :blush:' 支持的网站 上面例出的表情不仅适用于GitHub,也适用于其他地方。下面引用原文表述: A one pager listing the different emoji emoticons supported on Campfire, GitHub, Basecamp Next, Teambox,Plug.dj, Flowdock, Sprint.ly, GitLab, Kandan, andbang, Trello, Hall, Qiita, Trello, Zendesk, Ruby-China, Grove,Idobata, NodeBB Forums, Slack, Streamup, Quip, OrganisedMinds, and Hackpad. 上面例出的网站均支持这种代码式的表情插入。 同样,你可以让自己的项目支持这种表情,具体参见: Ruby – github.com/github/gemoji, github.com/jsw0528/rails_emoji Javascript – github.com/kof/emoticons Javascript – github.com/hassankhan/emojify.js Alt. JS version (+node.js) - github.com/henrikjoreteg/emoji-images.js Objective-C – https://github.com/diy/nsstringemojize Java - https://github.com/pepibumur/emojize Reference https://github.com/arvida/emoji-cheat-sheet.com https://github.com/github/gemoji
HTML5草案里面其实有原生的字幕标签(<track> Tag)的,但使用的是vtt格式的文件,非常规的字幕(.sub, .srt)或歌词文件(.lrc)。 用法如下(代码来自W3School): <video width="320" height="240" controls> <source src="forrest_gump.mp4" type="video/mp4"> <source src="forrest_gump.ogg" type="video/ogg"> <track src="subtitles_en.vtt" kind="subtitles" srclang="en" label="English"> <track src="subtitles_no.vtt" kind="subtitles" srclang="no" label="Norwegian"> </video> 但遗憾的是,使用起来还有不便之处。一是浏览器支持情况不太理想,连强大的FireFox(目前28.0)都还没支持,这你敢信!?。二是格式不兼容现有字幕或歌词文件,至少得需要个转换工具吧。 所以在它流行起来之前,考虑另外的实现还是有必要的。 效果预览 效果预览页面:http://wayou.github.io/selected/ 如果你网速流畅的话,尽情欣赏我精选的这些歌曲吧(不时更新),只是别忘了star,也可以fork后添加自己喜欢的歌曲。 项目GitHub地址:https://github.com/wayou/selected 具体实现可以前往项目的GitHub页面下载代码进行查看,下面介绍思路和简单的实现。 歌词文件的格式 实现之前,当然得了解一下歌词文件的格式了。常规歌词文件的格式基本是一句一行,每行由两部分组成,前面是中括号括起来的时间轴,后面紧跟歌词,像下面这样: [ar:文筱芮] [by:airplay] [00:00.00]那个 [00:03.00]作词:文筱芮 作曲:文筱芮 [00:06.00]编曲:于韵非 [00:09.00]制作人:胡海泉 秦天 [00:12.00]演唱:文筱芮 这样挺有规律的,用正则可以很方便地将时间与歌词提取分离。 但凡事得多个心眼啊。事后发生的事情证明这句话有多正确。我在整理歌词时还发现了另外一种形式,像下面这样: [ar:庭竹] [al:爱的九宫格] [by:airplay] [00:00.17]庭竹 - 公主的天堂 [00:05.40]作曲:陈嘉唯、Skot Suyama 陶山、庭竹 [00:07.33]作词:庭竹 [00:15.59]风铃的音谱 在耳边打转 [00:18.62]城堡里 公主也摆脱了黑暗的囚禁 [00:22.82]她一点点地 无声悄悄地慢慢长大 [00:26.36]期待着 深锁木门后的世界 [01:38.72][00:29.76] [01:51.48][00:30.32]树上 小鸟的轻响 在身边打转 [01:55.35][00:34.09]公主已 忘记木制衣橱背后的惆怅 [01:59.65][00:38.35]她跳舞唱歌天真无邪地寻找属于自己的光亮和快乐 [02:06.98][00:45.76] [02:07.41][00:46.06]树叶一层层拨开了伪装 [02:11.29][00:50.25]彩虹一步步露出美丽脸庞 无限的光亮 这种形式的歌词把歌词内容相同但时间不同的部分合并,节省了篇幅。 所以,现在知道的歌词其实有两种写法了,不过都还算规律,用正则可以搞定,只是对于第二种,处理时得将时间再次分割。 具体思路 首先将LRC文件读取为文本 用String.prototype.split('\n');将整个文本以换行符为单位分隔成一行一行的文本,保存到一个数组中 然后将开头部分不属于歌词的文本去掉,得到只有时间与歌词的干净文件 对于每一行,匹配出时间与文字,分别存入数组[time,text],然后将每行得到的这样的数组存入一个大的数组[[time,text],[time,text]…] 利用Audio标签的ontimeupdate事件,不断比较当然播放时间audio.currentTime与数组中每个元素中时间,如果当前时间大于某个歌词中的时间,则显示该歌词 文件读取 在具体处理歌词前,需要解决一个问题就是如何把歌词文件读取到代码中。对于文件读取,JavaScript中可以用FileReader,但它需要手动选择文件,也就是你得在页面放一个file类型的input或者实现文件拖拽操作,显示不可能让用户听歌的时候自己去找歌词然后上传,多麻烦。但JavaScript是没有办法操作本地文件的能力的,那就只能通过XMLHttpRequest(Ajax)发起一个到服务器的请求来获得文件了,这样一来,我们的程序就必需得运程在服务器上面。所以当你从GitHub下载了本文的源码后是无法直接运行的,请挂到本地服务器上观看效果。 下面展示了如何发起一个Ajax请求来获得歌词文件。 function getLyric(url) { //建立一个XMLHttpRequest请求 var request = new XMLHttpRequest(); //配置, url为歌词地址,比如:'./content/songs/foo.lrc' request.open('GET', url, true); //因为我们需要的歌词是纯文本形式的,所以设置返回类型为文本 request.responseType = 'text'; //一旦请求成功,但得到了想要的歌词了 request.onload = function() { //这里获得歌词文件 var lyric = request.response; }; //向服务器发送请求 request.send(); } 通过上面的代码就可以LRC文件读取成文本,然后就可以进行下一步处理了。 提取分离 因为时间我歌词的分隔是很有规律的,先通过\n将所有文字分隔成一行行存入数组,然后根据文章开始分析的思路一步一步提取分离。为此写一个解析歌词的函数。 function parseLyric(text) { //将文本分隔成一行一行,存入数组 var lines = text.split('\n'), //用于匹配时间的正则表达式,匹配的结果类似[xx:xx.xx] pattern = /\[\d{2}:\d{2}.\d{2}\]/g, //保存最终结果的数组 result = []; //去掉不含时间的行 while (!pattern.test(lines[0])) { lines = lines.slice(1); }; //上面用'\n'生成生成数组时,结果中最后一个为空元素,这里将去掉 lines[lines.length - 1].length === 0 && lines.pop(); lines.forEach(function(v /*数组元素值*/ , i /*元素索引*/ , a /*数组本身*/ ) { //提取出时间[xx:xx.xx] var time = v.match(pattern), //提取歌词 value = v.replace(pattern, ''); //因为一行里面可能有多个时间,所以time有可能是[xx:xx.xx][xx:xx.xx][xx:xx.xx]的形式,需要进一步分隔 time.forEach(function(v1, i1, a1) { //去掉时间里的中括号得到xx:xx.xx var t = v1.slice(1, -1).split(':'); //将结果压入最终数组 result.push([parseInt(t[0], 10) * 60 + parseFloat(t[1]), value]); }); }); //最后将结果数组中的元素按时间大小排序,以便保存之后正常显示歌词 result.sort(function(a, b) { return a[0] - b[0]; }); return result; } 这一步,我们便得到 了一个总的数组,它的元素是一些小的数组,这些小数组包含两个元素,一个是时间,并且这个时间已经由分:秒的形式转化为了秒,一个是时间对应的歌词[['秒数','歌词'], ['秒数','歌词']…]。 歌词同步 接下来就是先把全部歌词显示到页面,进行滚动式显示,或者也可以不全部显示,像电影字幕一样,唱一句显示一句。 下面看如何同步。当歌曲播放时,监听audio标签的ontimeupdate事件,即时更新显示歌词到页面即可。 //获取页面上的audio标签 var audio = document.getElementsByTagName('audio'), //显示歌词的元素 lyricContainer = document.getElementById('lyricContainer'); //监听ontimeupdate事件 audio.ontimeupdate = function(e) { //遍历所有歌词,看哪句歌词的时间与当然时间吻合 for (var i = 0, l = lyric.length; i < l; i++) { if (this.currentTime /*当前播放的时间*/ > lyric[i][0]) { //显示到页面 lyricContainer.textContent = that.lyric[i][1]; }; }; }; 我在selected项目中使用的是滚动显示的形式,但显示形式是可以变的,关键是同步的方法,可以多理解一下。 总结 上面的做法处理了多时间共处一行的情况,所以对于大多数歌词文件来说都是可行的,目前还没有发现另外形式的歌词文件。上面介绍的方法同样适用于video标签在播放视频时同步字幕,只是用于匹配的正则表达式需要更改,因为字幕文件的格式较歌词又不同了。同时字幕文件也分很多种后缀,但实现起来同样是利用media tag的ontimeupdate事件。 REFERENCE http://www.html5rocks.com/en/tutorials/track/basics/ http://www.w3schools.com/tags/ref_av_dom.asp http://www.w3schools.com/tags/tag_track.asp
前端一直是一块充满惊喜的土地,不仅是那些富有创造性的页面,还有那些惊赞的效果及不断推出的新技术。像node.js这样的后端开拓者直接将前端人员的能力扩大到了后端。瞬间就有了一统天下的感觉,来往穿梭于前后端之间代码敲得飞起,从此由前端晋升为'前后端'。 图片来自G+ 本文将使用Node.js加web socket协议打造一个网页即时聊天程序,取名为HiChat,中文翻过来就是'嗨聊',听中文名有点像是专为寂寞单身男女打造的~ 其中将会使用到express和socket.io两个包模块,下面会有介绍。 源码&演示 在线演示 (heroku服务器网速略慢且免费套餐是小水管,建议下载代码本地运行) 源码可访问项目的GitHub页面下载 本地运行方法: 命令行运行npm install 模块下载成功后,运行node server启动服务器 打开浏览器访问localhost 下图为效果预览: 准备工作 本文示例环境为Windows,Linux也就Node的安装与命令行稍有区别,程序实现部分基本与平台无关。 Node相关 你需要在本机安装Node.js(废话) 多少需要一点Node.js的基础知识,如果还未曾了解过Node.js,这里有一篇不错的入门教程 然后我们就可以开始创建一个简单的HTTP服务器啦。 类似下面非常简单的代码,它创建了一个HTTP服务器并监听系统的80端口。 //node server example //引入http模块 var http = require('http'), //创建一个服务器 server = http.createServer(function(req, res) { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.write('hello world!'); res.end(); }); //监听80端口 server.listen(80); console.log('server started'); 将其保存为一个js文件比如server.js,然后从命令行运行node server或者node server.js,服务器便可启动了,此刻我们可以在浏览器地址栏输入localhost进行访问,也可以输入本机IP127.0.0.1,都不用加端口,因为我们服务器监听的是默认的80端口。当然,如果你机子上面80端口被其他程序占用了,可以选择其他端口比如8080,这样访问的时候需要显示地加上端口号localhost:8080。 Express 首先通过npm进行安装 在我们的项目文件夹下打开命令行(tip: 按住Shift同时右击,可以在右键菜单中找到'从此处打开命令行'选项) 在命令行中输入 npm install express 回车进行安装 然后在server.js中通过require('express')将其引入到项目中进行使用 express是node.js中管理路由响应请求的模块,根据请求的URL返回相应的HTML页面。这里我们使用一个事先写好的静态页面返回给客户端,只需使用express指定要返回的页面的路径即可。如果不用这个包,我们需要将HTML代码与后台JavaScript代码写在一起进行请求的响应,不太方便。 //返回一个简单的HTML内容 server = http.createServer(function(req, res) { res.writeHead(200, { 'Content-Type': 'text/html' //将返回类型由text/plain改为text/html }); res.write('<h1>hello world!</h1>'); //返回HTML标签 res.end(); }); 在存放上一步创建的server.js文件的地方,我们新建一个文件夹名字为www用来存放我们的网页文件,包括图片以及前端的js文件等。假设已经在www文件夹下写好了一个index.html文件(将在下一步介绍,这一步你可以放一个空的HTML文件),则可以通过以下方式使用express将该页面返回到浏览器。可以看到较最开始,我们的服务器代码简洁了不少。 //使用express模块返回静态页面 var express = require('express'), //引入express模块 app = express(), server = require('http').createServer(app); app.use('/', express.static(__dirname + '/www')); //指定静态HTML文件的位置 server.listen(80); 其中有四个按钮,分别是设置字体颜色,发送表情,发送图片和清除记录,将会在下面介绍其实现 socket.io Node.js中使用socket的一个包。使用它可以很方便地建立服务器到客户端的sockets连接,发送事件与接收特定事件。 同样通过npm进行安装 npm install socket.io 。安装后在node_modules文件夹下新生成了一个socket.io文件夹,其中我们可以找到一个socket.io.js文件。将它引入到HTML页面,这样我们就可以在前端使用socket.io与服务器进行通信了。 <script src="/socket.io/socket.io.js"></script> 同时服务器端的server.js里跟使用express一样,也要通过require('socket.io')将其引入到项目中,这样就可以在服务器端使用socket.io了。 使用socket.io,其前后端句法是一致的,即通过socket.emit()来激发一个事件,通过socket.on()来侦听和处理对应事件。这两个事件通过传递的参数进行通信。具体工作模式可以看下面这个示例。 比如我们在index.html里面有如下JavaScript代码(假设你已经在页面放了一个ID为sendBtn的按钮): <script type="text/javascript"> var socket=io.connect(),//与服务器进行连接 button=document.getElementById('sendBtn'); button.onclick=function(){ socket.emit('foo', 'hello');//发送一个名为foo的事件,并且传递一个字符串数据‘hello’ } </script> 上述代码首先建立与服务器的连接,然后得到一个socket实例。之后如果页面上面一个ID为sendBtn的按钮被点击的话,我们就通过这个socket实例发起一个名为foo的事件,同时传递一个hello字符串信息到服务器。 与此同时,我们需要在服务器端写相应的代码来处理这个foo事件并接收传递来的数据。 为此,我们在server.js中可以这样写: //服务器及页面响应部分 var express = require('express'), app = express(), server = require('http').createServer(app), io = require('socket.io').listen(server); //引入socket.io模块并绑定到服务器 app.use('/', express.static(__dirname + '/www')); server.listen(80); //socket部分 io.on('connection', function(socket) { //接收并处理客户端发送的foo事件 socket.on('foo', function(data) { //将消息输出到控制台 console.log(data); }) }); 现在Ctrl+C关闭之前启动的服务器,再次输入node server启动服务器运行新代码查看效果,一切正常的话你会在点击了页面的按扭后,在命令行窗口里看到输出的'hello'字符串。 一如之前所说,socket.io在前后端的句法是一致的,所以相反地,从服务器发送事件到客户端,在客户端接收并处理消息也是显而易见的事件了。这里只是简单介绍,具体下面会通过发送聊天消息进一步介绍。 基本页面 有了上面一些基础的了解,下面可以进入聊天程序功能的开发了。 首先我们构建主页面。因为是比较大众化的应用了,界面不用多想,脑海中已经有大致的雏形,它有一个呈现消息的主窗体,还有一个输入消息的文本框,同时需要一个发送消息的按钮,这三个是必备的。 另外就是,这里还准备实现以下四个功能,所以界面上还有设置字体颜色,发送表情,发送图片和清除记录四个按钮。 最后的页面也就是先前截图展示的那们,而代码如下: www/index.html <!doctype html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta name="author" content="Wayou"> <meta name="description" content="hichat | a simple chat application built with node.js and websocket"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>hichat</title> <link rel="stylesheet" href="styles/main.css"> <link rel="shortcut icon" href="favicon.ico" type="image/x-icon"> <link rel="icon" href="favicon.ico" type="image/x-icon"> </head> <body> <div class="wrapper"> <div class="banner"> <h1>HiChat :)</h1> <span id="status"></span> </div> <div id="historyMsg"> </div> <div class="controls" > <div class="items"> <input id="colorStyle" type="color" placeHolder='#000' title="font color" /> <input id="emoji" type="button" value="emoji" title="emoji" /> <label for="sendImage" class="imageLable"> <input type="button" value="image" /> <input id="sendImage" type="file" value="image"/> </label> <input id="clearBtn" type="button" value="clear" title="clear screen" /> </div> <textarea id="messageInput" placeHolder="enter to send"></textarea> <input id="sendBtn" type="button" value="SEND"> <div id="emojiWrapper"> </div> </div> </div> <div id="loginWrapper"> <p id="info">connecting to server...</p> <div id="nickWrapper"> <input type="text" placeHolder="nickname" id="nicknameInput" /> <input type="button" value="OK" id="loginBtn" /> </div> </div> <script src="/socket.io/socket.io.js"></script> <script src="scripts/hichat.js"></script> </body> </html> 样式文件 www/styles/main.css html, body { margin: 0; background-color: #efefef; font-family: sans-serif; } .wrapper { width: 500px; height: 640px; padding: 5px; margin: 0 auto; background-color: #ddd; } #loginWrapper { position: fixed; top: 0; right: 0; bottom: 0; left: 0; background-color: rgba(5, 5, 5, .6); text-align: center; color: #fff; display: block; padding-top: 200px; } #nickWrapper { display: none; } .banner { height: 80px; width: 100%; } .banner p { float: left; display: inline-block; } .controls { height: 100px; margin: 5px 0px; position: relative; } #historyMsg { height: 400px; background-color: #fff; overflow: auto; padding: 2px; } #historyMsg img { max-width: 99%; } .timespan { color: #ddd; } .items { height: 30px; } #colorStyle { width: 50px; border: none; padding: 0; } /*custom the file input*/ .imageLable { position: relative; } #sendImage { position: absolute; width: 52px; left: 0; opacity: 0; overflow: hidden; } /*end custom file input*/ #messageInput { width: 440px; max-width: 440px; height: 90px; max-height: 90px; } #sendBtn { width: 50px; height: 96px; float: right; } #emojiWrapper { display: none; width: 500px; bottom: 105px; position: absolute; background-color: #aaa; box-shadow: 0 0 10px #555; } #emojiWrapper img { margin: 2px; padding: 2px; width: 25px; height: 25px; } #emojiWrapper img:hover { background-color: blue; } .emoji{ display: inline; } footer { text-align: center; } 为了让项目有一个良好的目录结构便于管理,这里在www文件夹下又新建了一个styles文件夹存放样式文件main.css,然后新建一个scripts文件夹存放前端需要使用的js文件比如hichat.js(我们前端所有的js代码会放在这个文件中),而我们的服务器js文件server.js位置不变还是放在最外层。 同时再新建一个content文件夹用于存放其他资源比如图片等,其中content文件夹里再建一个emoji文件夹用于存入表情gif图,后面会用到。最后我们项目的目录结构应该是这样的了: ├─node_modules └─www ├─content │ └─emoji ├─scripts └─styles 此刻打开页面你看到的是一个淡黑色的遮罩层,而接下来我们要实现的是用户昵称的输入与服务器登入。这个遮罩层用于显示连接到服务器的状态信息,而当连接完成之后,会出现一个输入框用于昵称输入。 上面HTML代码里已经看到,我们将www/scripts/hichat.js文件已经引入到页面了,下面开始写一些基本的前端js开始实现连接功能。 定义一个全局变量用于我们整个程序的开发HiChat,同时使用window.onload在页面准备好之后实例化HiChat,调用其init方法运行我们的程序。 www/scripts/Hichat.js window.onload = function() { //实例并初始化我们的hichat程序 var hichat = new HiChat(); hichat.init(); }; //定义我们的hichat类 var HiChat = function() { this.socket = null; }; //向原型添加业务方法 HiChat.prototype = { init: function() {//此方法初始化程序 var that = this; //建立到服务器的socket连接 this.socket = io.connect(); //监听socket的connect事件,此事件表示连接已经建立 this.socket.on('connect', function() { //连接到服务器后,显示昵称输入框 document.getElementById('info').textContent = 'get yourself a nickname :)'; document.getElementById('nickWrapper').style.display = 'block'; document.getElementById('nicknameInput').focus(); }); } }; 上面的代码定义了整个程序需要使用的类HiChat,之后我们处理消息显示消息等所有业务逻辑均写在这个类里面。 首先定义了一个程序的初始化方法,这里面初始化socket,监听连接事件,一旦连接到服务器,便显示昵称输入框。当用户输入昵称后,便可以在服务器后台接收到然后进行下一步的处理了。 设置昵称 我们要求连接的用户需要首先设置一个昵称,且这个昵称还要唯一,也就是不能与别人同名。一是方便用户区分,二是为了统计在线人数,同时也方便维护一个保存所有用户昵称的数组。 为此在后台server.js中,我们创建一个名叫users的全局数组变量,当一个用户设置好昵称发送到服务器的时候,将昵称压入users数组。同时注意,如果用户断线离开了,也要相应地从users数组中移除以保证数据的正确性。 在前台,输入昵称点击OK提交后,我们需要发起一个设置昵称的事件以便服务器侦听到。将以下代码添加到之前的init方法中。 www/scripts/hichat.js //昵称设置的确定按钮 document.getElementById('loginBtn').addEventListener('click', function() { var nickName = document.getElementById('nicknameInput').value; //检查昵称输入框是否为空 if (nickName.trim().length != 0) { //不为空,则发起一个login事件并将输入的昵称发送到服务器 that.socket.emit('login', nickName); } else { //否则输入框获得焦点 document.getElementById('nicknameInput').focus(); }; }, false); server.js //服务器及页面部分 var express = require('express'), app = express(), server = require('http').createServer(app), io = require('socket.io').listen(server), users=[];//保存所有在线用户的昵称 app.use('/', express.static(__dirname + '/www')); server.listen(80); //socket部分 io.on('connection', function(socket) { //昵称设置 socket.on('login', function(nickname) { if (users.indexOf(nickname) > -1) { socket.emit('nickExisted'); } else { socket.userIndex = users.length; socket.nickname = nickname; users.push(nickname); socket.emit('loginSuccess'); io.sockets.emit('system', nickname); //向所有连接到服务器的客户端发送当前登陆用户的昵称 }; }); }); 需要解释一下的是,在connection事件的回调函数中,socket表示的是当前连接到服务器的那个客户端。所以代码socket.emit('foo')则只有自己收得到这个事件,而socket.broadcast.emit('foo')则表示向除自己外的所有人发送该事件,另外,上面代码中,io表示服务器整个socket连接,所以代码io.sockets.emit('foo')表示所有人都可以收到该事件。 上面代码先判断接收到的昵称是否已经存在在users中,如果存在,则向自己发送一个nickExisted事件,在前端接收到这个事件后我们显示一条信息通知用户。 将下面代码添加到hichat.js的inti方法中。 www/scripts/hichat.js this.socket.on('nickExisted', function() { document.getElementById('info').textContent = '!nickname is taken, choose another pls'; //显示昵称被占用的提示 }); 如果昵称没有被其他用户占用,则将这个昵称压入users数组,同时将其作为一个属性存到当前socket变量中,并且将这个用户在数组中的索引(因为是数组最后一个元素,所以索引就是数组的长度users.length)也作为属性保存到socket中,后面会用到。最后向自己发送一个loginSuccess事件,通知前端登陆成功,前端接收到这个成功消息后将灰色遮罩层移除显示聊天界面。 将下面代码添加到hichat.js的inti方法中。 www/scripts/hichat.js this.socket.on('loginSuccess', function() { document.title = 'hichat | ' + document.getElementById('nicknameInput').value; document.getElementById('loginWrapper').style.display = 'none';//隐藏遮罩层显聊天界面 document.getElementById('messageInput').focus();//让消息输入框获得焦点 }); 在线统计 这里实现显示在线用户数及在聊天主界面中以系统身份显示用户连接离开等信息。 上面server.js中除了loginSuccess事件,后面还有一句代码,通过io.sockets.emit 向所有用户发送了一个system事件,传递了刚登入用户的昵称,所有人接收到这个事件后,会在聊天窗口显示一条系统消息'某某加入了聊天室'。同时考虑到在前端我们无法得知用户是进入还是离开,所以在这个system事件里我们多传递一个数据来表明用户是进入还是离开。 将server.js中login事件更改如下: server.js socket.on('login', function(nickname) { if (users.indexOf(nickname) > -1) { socket.emit('nickExisted'); } else { socket.userIndex = users.length; socket.nickname = nickname; users.push(nickname); socket.emit('loginSuccess'); io.sockets.emit('system', nickname, users.length, 'login'); }; }); 较之前,多传递了一个login字符串。 同时再添加一个用户离开的事件,这个可能通过socket.io自带的disconnect事件完成,当一个用户断开连接,disconnect事件就会触发。在这个事件中,做两件事情,一是将用户从users数组中删除,一是发送一个system事件通知所有人'某某离开了聊天室'。 将以下代码添加到server.js中connection的回调函数中。 server.js //断开连接的事件 socket.on('disconnect', function() { //将断开连接的用户从users中删除 users.splice(socket.userIndex, 1); //通知除自己以外的所有人 socket.broadcast.emit('system', socket.nickname, users.length, 'logout'); }); 上面代码通过JavaScript数组的splice方法将当前断开连接的用户从users数组中删除,这里我们看到之前保存的用户索引被使用了。同时发送和用户连接时一样的system事件通知所有人'某某离开了',为了让前端知道是离开事件,所以发送了一个'logout'字符串。 下面开始前端的实现,也就是接收system事件。 在hichat.js中,将以下代码添加到init方法中。 www/scripts/hichat.js this.socket.on('system', function(nickName, userCount, type) { //判断用户是连接还是离开以显示不同的信息 var msg = nickName + (type == 'login' ? ' joined' : ' left'); var p = document.createElement('p'); p.textContent = msg; document.getElementById('historyMsg').appendChild(p); //将在线人数显示到页面顶部 document.getElementById('status').textContent = userCount + (userCount > 1 ? ' users' : ' user') + ' online'; }); 现在运行程序,打开多个浏览器标签,然后登陆离开,你就可以看到相应的系统提示消息了。 发送消息 用户连接以及断开我们需要显示系统消息,用户还要频繁的发送聊天消息,所以可以考虑将消息显示到页面这个功能单独写一个函数方便我们调用。为此我们向HiChat类中添加一个_displayNewMsg的方法,它接收要显示的消息,消息来自谁,以及一个颜色共三个参数。因为我们想系统消息区别于普通用户的消息,所以增加一个颜色参数。同时这个参数也方便我们之后实现让用户自定义文本颜色做准备。 将以下代码添加到的我的HiChat类当中。 www/scripts/hichat.js //向原型添加业务方法 HiChat.prototype = { init: function() { //此方法初始化程序 //... }, _displayNewMsg: function(user, msg, color) { var container = document.getElementById('historyMsg'), msgToDisplay = document.createElement('p'), date = new Date().toTimeString().substr(0, 8); msgToDisplay.style.color = color || '#000'; msgToDisplay.innerHTML = user + '<span class="timespan">(' + date + '): </span>' + msg; container.appendChild(msgToDisplay); container.scrollTop = container.scrollHeight; } }; 在_displayNewMsg方法中,我们还向消息添加了一个日期。我们也判断了该方法在调用时有没有传递颜色参数,没有传递颜色的话默认使用#000即黑色。 同时修改我们在system事件中显示系统消息的代码,让它调用这个_displayNewMsg方法。 www/scripts/hichat.js this.socket.on('system', function(nickName, userCount, type) { var msg = nickName + (type == 'login' ? ' joined' : ' left'); //指定系统消息显示为红色 that._displayNewMsg('system ', msg, 'red'); document.getElementById('status').textContent = userCount + (userCount > 1 ? ' users' : ' user') + ' online'; }); 现在的效果如下: 有了这个显示消息的方法后,下面就开始实现用户之间的聊天功能了。 做法也很简单,如果你掌握了上面所描述的emit发送事件,on接收事件,那么用户聊天消息的发送接收也就轻车熟路了。 首先为页面的发送按钮写一个click事件处理程序,我们通过addEventListner来监听这个click事件,当用户点击发送的时候,先检查输入框是否为空,如果不为空,则向服务器发送postMsg事件,将用户输入的聊天文本发送到服务器,由服务器接收并分发到除自己外的所有用户。 将以下代码添加到hichat.js的inti方法中。 www/scripts/hichat.js document.getElementById('sendBtn').addEventListener('click', function() { var messageInput = document.getElementById('messageInput'), msg = messageInput.value; messageInput.value = ''; messageInput.focus(); if (msg.trim().length != 0) { that.socket.emit('postMsg', msg); //把消息发送到服务器 that._displayNewMsg('me', msg); //把自己的消息显示到自己的窗口中 }; }, false); 在server.js中添加代码以接收postMsg事件。 server.js io.on('connection', function(socket) { //其他代码。。。 //接收新消息 socket.on('postMsg', function(msg) { //将消息发送到除自己外的所有用户 socket.broadcast.emit('newMsg', socket.nickname, msg); }); }); 然后在客户端接收服务器发送的newMsg事件,并将聊天消息显示到页面。 将以下代码显示添加到hichat.js的init方法中了。 this.socket.on('newMsg', function(user, msg) { that._displayNewMsg(user, msg); }); 运行程序,现在可以发送聊天消息了。 发送图片 上面已经实现了基本的聊天功能了,进一步,如果我们还想让用户可以发送图片,那程序便更加完美了。 图片不同于文字,但通过将图片转化为字符串形式后,便可以像发送普通文本消息一样发送图片了,只是在显示的时候将它还原为图片。 在这之前,我们已经将图片按钮在页面放好了,其实是一个文件类型的input,下面只需在它身上做功夫便可。 用户点击图片按钮后,弹出文件选择窗口供用户选择图片。之后我们可以在JavaScript代码中使用FileReader来将图片读取为base64格式的字符串形式进行发送。而base64格式的图片直接可以指定为图片的src,这样就可以将图片用img标签显示在页面了。 为此我们监听图片按钮的change事件,一但用户选择了图片,便显示到自己的屏幕上同时读取为文本发送到服务器。 将以下代码添加到hichat.js的init方法中。 www/scripts/hichat.js document.getElementById('sendImage').addEventListener('change', function() { //检查是否有文件被选中 if (this.files.length != 0) { //获取文件并用FileReader进行读取 var file = this.files[0], reader = new FileReader(); if (!reader) { that._displayNewMsg('system', '!your browser doesn\'t support fileReader', 'red'); this.value = ''; return; }; reader.onload = function(e) { //读取成功,显示到页面并发送到服务器 this.value = ''; that.socket.emit('img', e.target.result); that._displayImage('me', e.target.result); }; reader.readAsDataURL(file); }; }, false); 上面图片读取成功后,调用_displayNImage方法将图片显示在自己的屏幕同时向服务器发送了一个img事件,在server.js中,我们通过这个事件来接收并分发图片到每个用户。同时也意味着我们还要在前端写相应的代码来接收。 这个_displayNImage还没有实现,将会在下面介绍。 将以下代码添加到server.js的socket回调函数中。 server.js //接收用户发来的图片 socket.on('img', function(imgData) { //通过一个newImg事件分发到除自己外的每个用户 socket.broadcast.emit('newImg', socket.nickname, imgData); }); 同时向hichat.js的init方法添加以下代码以接收显示图片。 this.socket.on('newImg', function(user, img) { that._displayImage(user, img); }); 有个问题就是如果图片过大,会破坏整个窗口的布局,或者会出现水平滚动条,所以我们对图片进行样式上的设置让它最多只能以聊天窗口的99%宽度来显示,这样过大的图片就会自己缩小了。 #historyMsg img { max-width: 99%; } 但考虑到缩小后的图片有可能失真,用户看不清,我们需要提供一个方法让用户可以查看原尺寸大小的图片,所以将图片用一个链接进行包裹,当点击图片的时候我们打开一个新的窗口页面,并将图片按原始大小呈现到这个新页面中让用户查看。 所以最后我们实现的_displayNImage方法应该是这样的。 将以下代码添加到hichat.js的HiChat类中。 www/scripts/hichat.js _displayImage: function(user, imgData, color) { var container = document.getElementById('historyMsg'), msgToDisplay = document.createElement('p'), date = new Date().toTimeString().substr(0, 8); msgToDisplay.style.color = color || '#000'; msgToDisplay.innerHTML = user + '<span class="timespan">(' + date + '): </span> <br/>' + '<a href="' + imgData + '" target="_blank"><img src="' + imgData + '"/></a>'; container.appendChild(msgToDisplay); container.scrollTop = container.scrollHeight; } 再次启动服务器打开程序,我们可以发送图片了。 发送表情 文字总是很难表达出说话时的面部表情的,于是表情就诞生了。 前面已经介绍过如何发送图片了,严格来说,表情也是图片,但它有特殊之处,因为表情可以穿插在文字中一并发送,所以就不能像处理图片那样来处理表情了。 根据以往的经验,其他聊天程序是把表情转为符号,比如我想发笑脸,并且规定':)'这个符号代码笑脸表情,然后数据传输过程中其实转输的是一个冒号加右括号的组合,当每个客户端接收到消息后,从文字当中将这些表情符号提取出来,再用gif图片替换,这样呈现到页面我们就 看到了表情加文字的混排了。 你好,王尼玛[emoji:23]------>你好,王尼玛 上面形象地展示了我们程序中表情的使用,可以看出我规定了一种格式来代表表情,[emoji:xx],中括号括起来然后'emoji'加个冒号,后面跟一个数字,这个数字表示某个gif图片的编号。程序中,如果我们点击表情按扭,然后呈现所有可用的表情图片,当用户选择一个表情后,生成对应的代码插入到当前待发送的文字消息中。发出去后,每个人接收到的也是代码形式的消息,只是在将消息显示到页面前,我们将表情代码提取出来,获取图片编号,然后用相应的图片替换。 首先得将所有可用的表情图片显示到一个小窗口,这个窗口会在点击了表情按钮后显示如下图,在HTML代码中已经添加好了这个窗口了,下面只需实现代码部分。 我们使用兔斯基作为我们聊天程序的表情包。可以看到,有很多张gif图,如果手动编写的话,要花一些功夫,不断地写<img src='xx.gif'/>,所以考虑将这个工作交给代码来自动完成,写一个方法来初始化所有表情。 为此将以下代码添加到HiChat类中,并在init方法中调用这个方法。 www/scripts/hichat.js _initialEmoji: function() { var emojiContainer = document.getElementById('emojiWrapper'), docFragment = document.createDocumentFragment(); for (var i = 69; i > 0; i--) { var emojiItem = document.createElement('img'); emojiItem.src = '../content/emoji/' + i + '.gif'; emojiItem.title = i; docFragment.appendChild(emojiItem); }; emojiContainer.appendChild(docFragment); } 同时将以下代码添加到hichat.js的init方法中。 www/scripts/hichat.js this._initialEmoji(); document.getElementById('emoji').addEventListener('click', function(e) { var emojiwrapper = document.getElementById('emojiWrapper'); emojiwrapper.style.display = 'block'; e.stopPropagation(); }, false); document.body.addEventListener('click', function(e) { var emojiwrapper = document.getElementById('emojiWrapper'); if (e.target != emojiwrapper) { emojiwrapper.style.display = 'none'; }; }); 上面向页面添加了两个单击事件,一是表情按钮单击显示表情窗口,二是点击页面其他地方关闭表情窗口。 现在要做的就是,具体到某个表情被选中后,需要获取被选中的表情,然后转换为相应的表情代码插入到消息框中。 为此我们再写一个这些图片的click事件处理程序。将以下代码添加到hichat.js的inti方法中。 www/scripts/hichat.js document.getElementById('emojiWrapper').addEventListener('click', function(e) { //获取被点击的表情 var target = e.target; if (target.nodeName.toLowerCase() == 'img') { var messageInput = document.getElementById('messageInput'); messageInput.focus(); messageInput.value = messageInput.value + '[emoji:' + target.title + ']'; }; }, false); 现在表情选中后,消息输入框中可以得到相应的代码了。 之后的发送也普通消息发送没区别,因为之前已经实现了文本消息的发送了,所以这里不用再实现什么,只是需要更改一下之前我们用来显示消息的代码,首先判断消息文本中是否含有表情符号,如果有,则转换为图片,最后再显示到页面。 为此我们写一个方法接收文本消息为参数,用正则搜索其中的表情符号,将其替换为img标签,最后返回处理好的文本消息。 将以下代码添加到HiChat类中。 www/scripts/hichat.js _showEmoji: function(msg) { var match, result = msg, reg = /\[emoji:\d+\]/g, emojiIndex, totalEmojiNum = document.getElementById('emojiWrapper').children.length; while (match = reg.exec(msg)) { emojiIndex = match[0].slice(7, -1); if (emojiIndex > totalEmojiNum) { result = result.replace(match[0], '[X]'); } else { result = result.replace(match[0], '<img class="emoji" src="../content/emoji/' + emojiIndex + '.gif" />'); }; }; return result; } 现在去修改之前我们显示消息的_displayNewMsg方法,让它在显示消息之前调用这个_showEmoji方法。 _displayNewMsg: function(user, msg, color) { var container = document.getElementById('historyMsg'), msgToDisplay = document.createElement('p'), date = new Date().toTimeString().substr(0, 8), //将消息中的表情转换为图片 msg = this._showEmoji(msg); msgToDisplay.style.color = color || '#000'; msgToDisplay.innerHTML = user + '<span class="timespan">(' + date + '): </span>' + msg; container.appendChild(msgToDisplay); container.scrollTop = container.scrollHeight; } 下面是实现后的效果: 主要功能已经完成得差不多了,为了让程序更加人性与美观,可以加入一个修改文字颜色的功能,以及键盘快捷键操作的支持,这也是一般聊天程序都有的功能,回车即可以发送消息。 文字颜色 万幸,HTML5新增了一个专门用于颜色选取的input标签,并且Chrome对它的支持非常之赞,直接弹出系统的颜色拾取窗口。 IE及FF中均是一个普通的文本框,不过不影响使用,只是用户只能通过输入具体的颜色值来进行颜色设置,没有Chrome里面那么方便也直观。 之前我们的_displayNewMsg方法可以接收一个color参数,现在要做的就是每次发送消息到服务器的时候,多加一个color参数就可以了,同时,在显示消息时调用_displayNewMsg的时候将这个color传递过去。 下面是修改hichat.js中消息发送按钮代码的示例: document.getElementById('sendBtn').addEventListener('click', function() { var messageInput = document.getElementById('messageInput'), msg = messageInput.value, //获取颜色值 color = document.getElementById('colorStyle').value; messageInput.value = ''; messageInput.focus(); if (msg.trim().length != 0) { //显示和发送时带上颜色值参数 that.socket.emit('postMsg', msg, color); that._displayNewMsg('me', msg, color); }; }, false); 同时修改hichat.js中接收消息的代码,让它接收颜色值 this.socket.on('newMsg', function(user, msg, color) { that._displayNewMsg(user, msg, color); }); 这只是展示了发送按钮的修改,改动非常小,只是每次消息发送时获取一下颜色值,同时emit事件到服务器的时候也带上这个颜色值,这样前端在显示时就可以根据这个颜色值为每个不两只用户显示他们自己设置的颜色了。剩下的就是按相同的做法把发送图片时也加上颜色,这里省略。 最后效果: 按键操作 将以下代码添加到hichat.js的inti方法中,这样在输入昵称后,按回车键就可以登陆,进入聊天界面后,回车键可以发送消息。 document.getElementById('nicknameInput').addEventListener('keyup', function(e) { if (e.keyCode == 13) { var nickName = document.getElementById('nicknameInput').value; if (nickName.trim().length != 0) { that.socket.emit('login', nickName); }; }; }, false); document.getElementById('messageInput').addEventListener('keyup', function(e) { var messageInput = document.getElementById('messageInput'), msg = messageInput.value, color = document.getElementById('colorStyle').value; if (e.keyCode == 13 && msg.trim().length != 0) { messageInput.value = ''; that.socket.emit('postMsg', msg, color); that._displayNewMsg('me', msg, color); }; }, false); 部署上线 最后一步,当然就是将我们的辛勤结晶部署到实际的站点。这应该是最激动人心也是如释重负的一刻。但在这之前,让我们先添加一个node.js程序通用的package.json文件,该文件里面可以指定我们的程序使用了哪些模块,这样别人在获取到代码后,只需通过npm install命令就可以自己下载安装程序中需要的模块了,而不用我们把模块随源码一起发布。 添加package.json文件 将以下代码保存为package.json保存到跟server.js相同的位置。 { "name": "hichat", "description": "a realtime chat web application", "version": "0.4.0", "main": "server.js", "dependencies": { "express": "3.4.x", "socket.io": "0.9.x" }, "engines": { "node": "0.10.x", "npm": "1.2.x" } } 云服务选择与部署 首先我们得选择一个支持Node.js同时又支持web socket协议的云服务器。因为只是用于测试,空间内存限制什么的都无所谓,只要免费就行。Node.js在GitHub的Wiki页面上列出了众多支持Node.js环境的云服务器,选来选去满足条件的只有heroku。 如果你之前到heroku部署过相关Node程序的话,一定知道其麻烦之处,并且出错了非常不容易调试。不过当我在写这篇博客的时候,我发现了一个利器codeship,将它与你的github绑定之后,你每次提交了新的代码它会自动部署到heroku上面。什么都不用做! 代码更新,环境设置,编译部署,全部自动搞定,并且提供了详细的log信息及各步骤的状态信息。使用方法也是很简单,注册后按提示,两三步搞定,鉴于本文已经够长了,应该创纪录了,这里就不多说了。 已知问题 部署测试后,发现一些本地未出现的问题,主要有以下几点: 首次连接过慢,有时会失败出现503错误,这个查了下heroku文档,官方表示程序首次接入时受资源限制确实会很慢的,这就是用免费套餐注定被鄙视的结果,不过用于线上测试这点还是能够忍受的 发送表情时,Chrome会向服务器重新请求已经下载到客户端的gif图片,而IE和FF都无此问题,导致在Chrome里表情会有延迟,进而出现聊天主信息窗口滚动也不及时的现象 用户未活动一定时间后会与服务器失连,socket自动断开,不知道是socket.io内部机制还是又是heroku捣鬼 总结展望 经过上面一番折腾,一个基本的聊天程序便打造完毕。可以完善的地方还有许多,比如利用CSS3的动画,完全可以制作出窗口抖动功能的。听起来很不错是吧。同时利用HTML5的Audio API,要实现类似微信的语音消息也不是不可能的,够震撼吧。甚至还有Geolocaiton API我们就可以联想到实现同城功能,利用Webcam可以打造出视频对聊,但这方面WebRTC已经做得很出色了。 PS:做程序员之前有两个想法,一是写个播放器,一是写个聊天程序,现在圆满了。 REFERENCE HOW TO SEND IMAGES THROUGH WEB SOCKETS WITH NODE.JS AND SOCKET.IO Simple Chat - Node.js + WebSockets Hosting compatible with Node What is a good tool to export a directory structure heroku codeship
简介 HTML5向Web API新引入了document.querySelector以及document.querySelectorAll两个方法用来更方便地从DOM选取元素,功能类似于jQuery的选择器。这使得在编写原生JavaScript代码时方便了许多。 用法 两个方法使用差不多的语法,都是接收一个字符串参数,这个参数需要是合法的CSS选择语法。 element = document.querySelector('selectors'); elementList = document.querySelectorAll('selectors'); 其中参数selectors 可以包含多个CSS选择器,用逗号隔开。 element = document.querySelector('selector1,selector2,...'); elementList = document.querySelectorAll('selector1,selector2,...'); 使用这两个方法无法查找带伪类状态的元素,比如querySelector(':hover')不会得到预期结果。 querySelector 该方法返回满足条件的单个元素。按照深度优先和先序遍历的原则使用参数提供的CSS选择器在DOM进行查找,返回第一个满足条件的元素。 element = document.querySelector('div#container');//返回id为container的首个div element = document.querySelector('.foo,.bar');//返回带有foo或者bar样式类的首个元素 querySelectorAll 该方法返回所有满足条件的元素,结果是个nodeList集合。查找规则与前面所述一样。 elements = document.querySelectorAll('div.foo');//返回所有带foo类样式的div 但需要注意的是返回的nodeList集合中的元素是非实时(no-live)的,想要区别什么是实时非实时的返回结果,请看下例: <div id="container"> <div></div> <div></div> </div> //首先选取页面中id为container的元素 container=document.getElementById('#container'); console.log(container.childNodes.length)//结果为2 //然后通过代码为其添加一个子元素 container.appendChild(document.createElement('div')); //这个元素不但添加到页面了,这里的变量container也自动更新了 console.log(container.childNodes.length)//结果为3 通过上面的例子就很好地理解了什么是会实时更新的元素。document.getElementById返回的便是实时结果,上面对其添加一个子元素后,再次获取所有子元素个数,已经由原来的2个更新为3个(这里不考虑有些浏览器比如Chrome会把空白也解析为一个子节点)。 关于转义 我们知道反斜杠是用来转义用的,比如在字符串里我们想表示空字符就使用'\b',换行'\n'。同样,在提供给querySelector和querySelectorAll的参数也支持转义,了解这点非常重要。 先看个例子,比如我们有个div它的样式类为'foo:bar',当然我知道你一般不会这样写。当我们需要选择它的时候,就需要将其中的冒号进行转义,否则抛错。 <div class="foo:bar"></div> 浏览器报怨表示不是一个合法的选择语句。 同时,有趣的事情来了,或许你以为将冒号直接转义就解决问题了。 同样,也表示非法。原因就在于反斜杠在字符串中本身就表示转义的意思,它于冒号结合转不出东西来,于是抛错。 所以正确的做法是将反斜杠转义后'.foo\\:bar'再传递给querySelector,后者在接收到'.foo\\:bar'这个参数后,字符串将两个反斜杠转义成一个,然后querySelector前面得到的一个反斜杠与冒号结合进行转义得到正确结果。 也就是说经历两次转义,一次是字符串当中,一次是querySelector解析参数时。 理解这点后,可以来看一个更有趣的例子了。比如我们要选择类名里面含反斜杠的元素。是的,我们需要一共使用四个反斜杠!才能正常工作。 <div class="foo\bar"></div> 浏览器兼容性 目前各主流浏览器对此API提供了良好支持,IE需8+。详情见caniuse。 4.0+ 3.5+ 8+ 10.0+ 3.1+ REFERENCE 本文主要参考了MDN上的文档 document.querySelectorAll document.querySelector NodeList
好像是feedly订阅里看到的文章,读完后觉得非常不错,译之备用,多看受益。 加载jQuery 1.坚持使用CDN来加载jQuery,这种别人服务器免费帮你托管文件的便宜干嘛不占呢。点击查看使用CDN的好处,点此查看一些主流的jQuery CDN地址。 <script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script> <script>window.jQuery || document.write('<script src="js/jquery-1.11.0.min.js" type="text/javascript"><\/script>')</script> 2.安全起见,最好还是提供一个本地备份以便在无法从远程CDN服务器获取jQuery时网站也能工作,如上面代码所示。详情见此。 3.使用裸协议的URL(也就是说去掉http:或者https:),如上面代码展示的那样。 4.如果可能,尽量将你的JavaScript和jQuery代码放到页面底部。详情移步这里,或者查看一个HTML5页面标准模板。 5.该使用哪个版本? 如果你想兼容IE678请表用2.x的版本 针对极少数不用考虑兼容性的幸运儿,极力推荐使用最新版本的jQuery 当从CDN服务器加载jQuery时,最好把版本写全(比如1.11.0而不是1.11或者直接写个1) 千万莫重复加载 6.如果你同时还使用了其他JS框架诸如Prototype, MooTools, Zepto云云,因为他们也使用了$符号,所以你就表再用美刀符号来进行jQuery 编码了,而请用'jQuery'代替。并且调用$.noConflict()保证不会有冲突出现。 7.要检测浏览器对一些新特性是否支持,请用Modernizr。插播广告:论为毛不检测浏览器 关于变量 1.jQuery类型的变量最好加个$前缀。 2.时常将jQuery选择器返回的内容存进变量以便重用 var $products = $("div.products"); // 慢 var $products = $(".products"); // 快 3.使用驼峰命名 关于选择器 1.尽量ID选择器。其背后机理其实是调用原生的document.getElementById(),所以速度较其他选择器快。 2.使用类选择器时表指定元素的类型。不信你看这个性能比较 var $products = $("div.products"); // 慢 var $products = $(".products"); // 快 3.ID父亲容器下面再查找子元素请用.find()方法。这样做快的原因是通过id选择元素不会使用Sizzle引擎。详情看这里 4.多级查找中,右边尽量指定得详细点而左边则尽量简单点。了解更多 // 丑陋 $("div.data .gonzalez"); // 优化后 $(".data td.gonzalez"); 5.避免冗余。详情或者查看性能比较 $(".data table.attendees td.gonzalez"); // 好的方式:去掉了中间的冗余 $(".data td.gonzalez"); 6.指定选择的上下文。 // 劣质的代码:因为需要遍历整个DOM来找到.class $('.class'); // 高品代码:因为只需在指定容器范围内进行查找 $('.class', '#class-container'); 7.表使用万能选择器。查看具体阐释 $('div.container > *'); // 差 $('div.container').children(); // 棒 8.警惕隐式的万能选择器。省略的情况下其实使用的就是*号通配符。更多信息 $('div.someclass :radio'); // 差 $('div.someclass input:radio'); // 棒 9.ID已经表示唯一了,背后使用的是document.getElementById(),所以表跟其他选择器混搭了。 $('#outer #inner'); // 脏 $('div#inner'); // 乱 $('.outer-container #inner'); // 差 $('#inner'); // 干净利落,后台只需调用document.getElementById() DOM操作相关 1.操作任何元素前先将其从文档卸载,完了再贴回去。这事儿还能说细点 var $myList = $("#list-container > ul").detach(); //...一大堆对$myList的处理 $myList.appendTo("#list-container"); 2.代码里将HTML组织好后再一次性贴到DOM中去。具体来说,性能比较 // 这样不好 var $myList = $("#list"); for(var i = 0; i < 10000; i++){ $myList.append("<li>"+i+"</li>"); } // 这样好 var $myList = $("#list"); var list = ""; for(var i = 0; i < 10000; i++){ list += "<li>"+i+"</li>"; } $myList.html(list); // 但这样更好 var array = []; for(var i = 0; i < 10000; i++){ array[i] = "<li>"+i+"</li>"; } $myList.html(array.join('')); 3.不要处理不存在的元素。详情 // 无良的做法:jQuery后台要跑完三个函数后才会知道这个元素其实根本不存在 $("#nosuchthing").slideUp(); // 应该这样 var $mySelection = $("#nosuchthing"); if ($mySelection.length) { $mySelection.slideUp(); } 事件相关 1.一个页面只写一个文档ready事件的处理程序。这样代码既清晰好调试,又容易跟踪代码的进程。 2.表用匿名函数来做事件的回调。匿名函数不易调试维护测试和复用。或许你想较真,看看这里吧 $("#myLink").on("click", function(){...}); // 表这样 // 这样 function myLinkClickHandler(){...} $("#myLink").on("click", myLinkClickHandler); 3.处理文档ready事件的回调也表用匿名函数,匿名函数不易调试维护测试和复用:( $(function(){ ... }); // 糟糕的做法:无法利用此函数也无法为其写测试用例 // 好的做法 $(initPage); // 抑或 $(document).ready(initPage); function initPage(){ // 这里你可以进行程序的初始化了 } 4.进一步,最好也将ready事件的处理程序放到外部文件中引入到页面,而页面中内嵌的代码只需调用即可。 <script src="my-document-ready.js"></script> <script> // 初始化一些必要的全局变量 $(document).ready(initPage); // 抑或 $(initPage); </script> 5.千万表写内联到HTML的JS代码,这是调试的梦魇!应该总是用jQuery来绑定事件自带程序,这样也方便随时动态地取消绑定。 <a id="myLink" href="#" onclick="myEventHandler();">my link</a> <!--不好 --> $("#myLink").on("click", myEventHandler); // GOOD 6.如果可能尽量在绑定事件处理程序时使用一个命名空间,这样可以方便地取消绑定而不会影响其他绑定。 $("#myLink").on("click.mySpecialClick", myEventHandler); // 不错 // 之后,让我们优雅地解除绑定 $("#myLink").unbind("click.mySpecialClick"); 异步操作 1.直接用$.ajax()而表去用.getJson() 或 .get(),因为jQuery内部还是将其转为前者 2.表对HTTPS站点使用HTTP去发起请求,最好干脆就表指定(将HTTP或者HTTPS从你的URL中移除) 3.表在链接里面嵌参数,请使用专门的参数设置来传递 // 不易阅读的代码... $.ajax({ url: "something.php?param1=test1&param2=test2", .... }); // 更易阅读... $.ajax({ url: "something.php", data: { param1: test1, param2: test2 } }); 4.尽量指明数据类型以便你自己清楚要处理什么样的数据(见下方会提到的Ajax模板) 5.对于异步动态加载的内容,最好使用代理来绑定事件处理程序。这样的好处是对于之后动态加载的元素事件同样有效。你或许想了解更多 $("#parent-container").on("click", "a", delegatedClickHandlerForAjax); 6.使用Promise模式。更多例子 $.ajax({ ... }).then(successHandler, failureHandler); // 抑或 var jqxhr = $.ajax({ ... }); jqxhr.done(successHandler); jqxhr.fail(failureHandler); 7.标准的Ajax模板一分。追寻根源 var jqxhr = $.ajax({ url: url, type: "GET", // 默认为GET,你可以根据需要更改 cache: true, // 默认为true,但对于script,jsonp类型为false,可以自行设置 data: {}, // 将请求参数放这里. dataType: "json", // 指定想要的数据类型 jsonp: "callback", // 指定回调处理JSONP类型的请求 statusCode: { // 如果你想处理各状态的错误的话 404: handler404, 500: handler500 } }); jqxhr.done(successHandler); jqxhr.fail(failureHandler); 动画与特效 1.保持一个始终如一风格统一的动画实现 2.紧遵用户体验,表滥用动画特效 使用简洁的显示隐藏,状态切换,滑入滑出等效果来展示元素 使用预设值来设置动画的速度'fast','slow',或者400(中等速度) 插件相关 1.始终选择一个有良好支持,完善文档,全面测试过并且社区活跃的插件 2.注意所用插件与当前使用的jQuery版本是否兼容 3.一些常用功能应该写成jQuery插件。一分jQuery插件的编写模板 链式句法 1.除了用变量将jQuery选择器返回的结果保存,还可以利用好链式调用。 $("#myDiv").addClass("error").show(); 2.当链式调用多达3次以上或代码因绑定回调略显复杂时,使用换行和适当的缩进来提高代码的可读性。 $("#myLink") .addClass("bold") .on("click", myClickHandler) .on("mouseover", myMouseOverHandler) .show(); 3.对于特别长的调用最好还是用变量保存下中间结果来简化代码。 其他 1.使用对象字面量来传递参数 $myLink.attr("href", "#").attr("title", "my link").attr("rel", "external"); // 糟糕:调用了三次attr // 不错,只调用了一次attr $myLink.attr({ href: "#", title: "my link", rel: "external" }); 2.表将CSS与jQuery杂揉 $("#mydiv").css({'color':red, 'font-weight':'bold'}); // 不好 .error {/* 不错 */ color: red; font-weight: bold; } $("#mydiv").addClass("error"); 3.时刻关注官方Changelog,表使用摒弃了的方法。点此查看所有废弃的方法 4.适时地使用原生JavaScript。一些与此有关的性能比较 $("#myId"); // 多少还是会逊色于... document.getElementById("myId"); REFERENCE 原文:Coding Standards & Best Practices http://lab.abhinayrathore.com/jquery-standards/ 原文的reference jQuery Performance: http://learn.jquery.com/performance/ jQuery Learn: http://learn.jquery.com jQuery API Docs: http://api.jquery.com/ jQuery Coding Standards and Best Practice: http://www.jameswiseman.com/blog/2010/04/20/jquery-standards-and-best-practice/ jQuery Plugin Boilerplate: http://stefangabos.ro/jquery/jquery-plugin-boilerplate-revisited/
继续玩味之前写的音乐频谱作品,将原来在Canvas标签上的 作图利用Three.js让它通过WebGL呈现,这样就打造出了一个全立体感的频谱效果了。 项目详情及源码 项目GitHub地址:https://github.com/Wayou/3D_Audio_Spectrum_VIsualizer/tree/master 在线演示地址:http://wayou.github.io/3D_Audio_Spectrum_VIsualizer 如果你想的话,可以从这里下载示例音乐:http://pan.baidu.com/s/1eQqqSfS Note: 可以直接点击'play default' 播放自带的音乐,神探夏洛克插曲,如果你也看了的话,听着应该会有感的 支持文件拖拽进行播放,将音频文件拖拽到页面即可 也可以通过文件上传按钮选择一个音频文件进行播放 鼠标拖拽可以移动镜头变换视野 鼠标滚轮可以进行缩放 右上角的控制面板可以进行一些外观及镜头上的设置,可以自己探索玩玩 利用Three.js呈现 关于音频处理方面的逻辑基本和之前介绍HTML5 Audio API那篇博客里讲的差不多,差别只在这个项目里面将频谱的展示从2d的canvas换成3d的WebGL进行展示,使用的是Three.js。所以只简单介绍关于3d场景方面的构建,具体实现可以访问项目GitHub页面下载源码。 构建跃动的柱条 每根绿色柱条是一个CubeGeometry,柱条上面的盖子也是CubeGeometry,只是长度更短而以,同时使用的是白色。 //创建绿色柱条的形状 var cubeGeometry = new THREE.CubeGeometry(MWIDTH, 1, MTHICKNESS); //创建绿色柱条的材质 var cubeMaterial = new THREE.MeshPhongMaterial({ color: 0x01FF00, ambient: 0x01FF00, specular: 0x01FF00, shininess: 20, reflectivity: 5.5 }); //创建白色盖子的形状 var capGeometry = new THREE.CubeGeometry(MWIDTH, 0.5, MTHICKNESS); //创建白色盖子的材质 var capMaterial = new THREE.MeshPhongMaterial({ color: 0xffffff, ambient: 0x01FF00, specular: 0x01FF00, shininess: 20, reflectivity: 5.5 }); 上面只是创建了形状及材质,需要将这两者组合在一起形成一个模型,才是我们看到的实际物体。下面通过一个循环创建了一字排开的柱条和对应的盖子,然后添加到场景中。 //创建一字排开的柱条和盖子,并添加到场景中 for (var i = METERNUM - 1; i >= 0; i--) { var cube = new THREE.Mesh(cubeGeometry, cubeMaterial); cube.position.x = -45 + (MWIDTH + GAP) * i; cube.position.y = -1; cube.position.z = 0.5; cube.castShadow = true; cube.name = 'cube' + i; scene.add(cube); var cap = new THREE.Mesh(capGeometry, capMaterial); cap.position.x = -45 + (MWIDTH + GAP) * i; cap.position.y = 0.5; cap.position.z = 0.5; cap.castShadow = true; cap.name = 'cap' + i; scene.add(cap); }; 注意到我们为每个物体指定了名称以方便之后获取该物体。 添加动画 动画部分同时是使用requestAnimation,根据传入的音频分析器(analyser)的数据来更新每根柱条的长度。 var renderAnimation = function() { if (analyser) { //从音频分析器中获取数据 var array = new Uint8Array(analyser.frequencyBinCount); analyser.getByteFrequencyData(array); var step = Math.round(array.length / METERNUM); //更新每根柱条的高度 for (var i = 0; i < METERNUM; i++) { var value = array[i * step] / 4; value = value < 1 ? 1 : value; var meter = scene.getObjectByName('cube' + i, true); meter.scale.y = value; } }; //重新渲染画面 render.render(scene, camera); requestAnimationFrame(renderAnimation); }; requestAnimationFrame(renderAnimation); 对于白色盖子的处理稍微不同,因为它是缓慢下落的,不能使用及时送达的音频数据来更新它。实现的方式是每次动画更新中检查当前柱条的高度与前一时刻盖子的高度,看谁大,如果柱条更高,则盖子使用新的高度,否则盖子高度减1,这样就实现了缓落的效果。 var renderAnimation = function() { if (analyser) { var array = new Uint8Array(analyser.frequencyBinCount); analyser.getByteFrequencyData(array); var step = Math.round(array.length / METERNUM); for (var i = 0; i < METERNUM; i++) { var value = array[i * step] / 4; value = value < 1 ? 1 : value; var meter = scene.getObjectByName('cube' + i, true), cap = scene.getObjectByName('cap' + i, true); meter.scale.y = value; //计算柱条边沿尺寸以获得高度 meter.geometry.computeBoundingBox(); height = (meter.geometry.boundingBox.max.y - meter.geometry.boundingBox.min.y) * value; //将柱条高度与盖子高度进行比较 if (height / 2 > cap.position.y) { cap.position.y = height / 2; } else { cap.position.y -= controls.dropSpeed; }; } }; //重新渲染画面 render.render(scene, camera); requestAnimationFrame(renderAnimation); }; requestAnimationFrame(renderAnimation); 镜头控制 镜头的控制使用的是与Three.js搭配的一个插件ObitControls.js,如果你下载了Three.js的源码可以在里面找到。只需获取一个鼠标拖动的前后时间差,然后在动画循环中调用插件进行画面更新即可。 var orbitControls = new THREE.OrbitControls(camera); orbitControls.minDistance = 50; orbitControls.maxDistance = 200; orbitControls.maxPolarAngle = 1.5; var renderAnimation = function() { var delta = clock.getDelta(); orbitControls.update(delta); if (analyser) { var array = new Uint8Array(analyser.frequencyBinCount); analyser.getByteFrequencyData(array); var step = Math.round(array.length / METERNUM); for (var i = 0; i < METERNUM; i++) { var value = array[i * step] / 4; value = value < 1 ? 1 : value; var meter = scene.getObjectByName('cube' + i, true), cap = scene.getObjectByName('cap' + i, true); meter.scale.y = value; //计算柱条边沿尺寸以获得高度 meter.geometry.computeBoundingBox(); height = (meter.geometry.boundingBox.max.y - meter.geometry.boundingBox.min.y) * value; //将柱条高度与盖子高度进行比较 if (height / 2 > cap.position.y) { cap.position.y = height / 2; } else { cap.position.y -= controls.dropSpeed; }; } }; //重新渲染画面 render.render(scene, camera); requestAnimationFrame(renderAnimation); }; requestAnimationFrame(renderAnimation); 注意到在实例化一个ObitControls后,进行了一些角度和镜头伸缩方面的设置,限制了用户把画面翻转到平面的底部,也保证了镜头在伸缩时不会太远及太近。 参数控制 右上角的控制面板可以进行画面的一些参数更改,使用的是谷歌员工创建的一个插件dat.gui.js 。 首先需要定义一个包含全部需要控制的参数的对象: var controls = new function() { this.capColor = 0xFFFFFF; this.barColor = 0x01FF00; this.ambientColor = 0x0c0c0c; this.dropSpeed = 0.1; this.autoRotate = false; }; 然后实例化一个控制器,将这个对象及相应参数进行绑定: var gui = new dat.GUI(); //添加盖子下降速度的控制 gui.add(controls, 'dropSpeed', 0.1, 0.5); //盖子颜色控制 gui.addColor(controls, 'capColor').onChange(function(e) { scene.children.forEach(function(child) { if (child.name.indexOf('cap') > -1) { child.material.color.setStyle(e); child.material.ambient = new THREE.Color(e) child.material.emissive = new THREE.Color(e) child.material.needsUpdate = true; } }); }); //柱条颜色控制 gui.addColor(controls, 'barColor').onChange(function(e) { scene.children.forEach(function(child) { if (child.name.indexOf('cube') > -1) { child.material.color.setStyle(e); child.material.ambient = new THREE.Color(e) child.material.emissive = new THREE.Color(e) child.material.needsUpdate = true; } }); }); //镜头自动移动控制 gui.add(controls, 'autoRotate').onChange(function(e) { orbitControls.autoRotate = e; }); 总结 完成了主要功能,但没达到我预期的效果,我想的是把柱条做成发光的,调研了一下,需要用更复杂的材质,同时也不能用WebGL来渲染画面了,性能是一方面,同时也还没研究得那么深入,所以就先出了这个版本先。以后或许弄个水波效果。 REFERENCE Offical Documentation: http://threejs.org/docs/ A Demo: http://srchea.com/blog/2013/05/experimenting-with-web-audio-api-three-js-webgl/ Another Example: https://github.com/arirusso/three-audio-spectrum A Working Demo: http://badassjs.com/post/27056714305/plucked-html5-audio-editor-and-threeaudio-js Dat GUI plugin: https://code.google.com/p/dat-gui/
之前用HTML5的Audio API写了个音乐频谱效果,再之后又加了个播放列表就成了个简单的播放器,其中弄了个功能是'Shuffle'也就是一般播放器都有的列表打乱功能,或者理解为随机播放。 但我觉得随机播放绝对要好实现些,用Math.random()产生一个介于1到歌曲数目之间的随机数便可,然后player.play(随机数)。 而列表的打乱情况要不一样点,一是要呈现到界面,歌曲顺序要随机排,二是播放顺序不变,该哪是哪,只是该位置上的歌曲可能已经变成其他曲目了。抽象出来就是数组元素的重排,那么具体算法就值得探究了。 面对一个新问题时,我首先想到的是前人是否已经给出了问题的答案。正如所料于是发现了这个成熟的Fisher-Yates乱序算法,这是公认经典的洗牌算法了。但事情到此并没有结束。 原文给出了三个循序渐近的例子,下面来看。 一般化方法 原文引入的现实情境是这样的,假如你要洗牌,那么最随机的做法无疑是从牌堆里随便抽一张出来,然后放在一边,之后从剩下的牌里重复之前的操作,直到所有牌都被抽出来放到了另一堆中。抽象到代码世界,按相同的做法,就是随机从数组里取出一个元素,保存到另一个数组,然后重复之,直到原数组中所有元素都处理掉。 原文给出的演示我觉得不够直观,下方是我自己写的动画用以演示此算法过程(也可在这里查看:http://sandbox.runjs.cn/show/1hylhpck ): 下面是按这个思路的一个实现: function shuffle(array) { var copy = [], n = array.length, i; // 如果还剩有元素则继续。。。 while (n) { // 随机抽取一个元素 i = Math.floor(Math.random() * array.length); // 如果这个元素之前没有被选中过。。 if (i in array) { copy.push(array[i]); delete array[i]; n--; } } return copy; } 我们创建了一个copy数组,然后遍历目标数组,将其元素复制到copy数组里,同时将该元素从目标数组中删除,这样下次遍历的时候就可以跳过这个序号。而这一实现的问题正在于此,即使一个序号上的元素已经被处理过了,由于随机函数产生的数是随机的,所有这个被处理过的元素序号可能在之后的循环中不断出现,一是效率问题,另一个就是逻辑问题了,存在一种可能是永远运行不完! Note:Math.random()产生[0,1)的小数delete 操作只将数组元素的值删除,但不影响数组长度,删除后原来位置的值变为undefined 改进的做法 上面的分析已经看出问题的所在了,所以改进的做法就是处理完一个元素后,我们用Array的splice()方法将其从目标数组中移除同时也更新了目标数组的长度,如此一来下次遍历的时候是从新的长度开始,不会重复处理的情况了。 动画演示(http://sandbox.runjs.cn/show/v6a7gq0f) function shuffle(array) { var copy = [], n = array.length, i; // 如果还剩有元素。。 while (n) { // 随机选取一个元素 i = Math.floor(Math.random() * n--); // 移动到新数组中 copy.push(array.splice(i, 1)[0]); } return copy; } 再次优化的最终版本 上面的做法已经可以了,但上面的改进依然还有提升空间。因为调用splice来删除数组元素会导致删除位置之后的所有元素要做shift操作来向前补充,从而达到将数组长度减小的目的,当然这是在后台自动完成的,但这无疑增加了算法的复杂度。 注意到我们要做的仅仅是将数组元素重新排序,已经取出来的元素和剩下的元素之和一定是等于数组原来的总元素个数的。所以可以考虑不创建新的数组来保存已经抽取的元素,可以这样,随机从数组中抽出一个元素,然后与最后个元素交换,相当于把这个随机抽取的元素放到了数组最后面去,表示它已经是被随机过了,同时被换走的那个元素跑到前面去了,会在后续的重复操作中被随机掉。一轮操作过后,下一轮我们只在剩下的n-1个元素也就是数组的前n-1个元素中进行相同的操作,直到进行到第一个。 动画演示(http://sandbox.runjs.cn/show/jabgttzr): function shuffle(array) { var m = array.length, t, i; // 如果还剩有元素… while (m) { // 随机选取一个元素… i = Math.floor(Math.random() * m--); // 与当前元素进行交换 t = array[m]; array[m] = array[i]; array[i] = t; } return array; } 更加简洁的版本 上面介绍的便是在各语言中都广为实现的Fisher-Yates乱序算法。但具体到JavaScript,我们其实可以结合数组自带的sort()方法编写出更简洁的代码来达到目的。中间变量以及值交换什么的都省了,虽然后台实现肯定还是会进行值交换的,但我们不关心,一切交给sort()让它自己处理。但这种方法也只是简洁而以,效果是不如上面介绍的算法的,因为随着数组元素越多,其随机性会变差。 function shuffle(array) { return array.sort(function() { return Math.random() - 0.5 }); } REFERENCE Fisher–Yates Shuffle: http://bost.ocks.org/mike/shuffle/ Why the Fisher-Yates Shuffle is the best algorithm from Quora: http://www.quora.com/Algorithms/Are-there-any-better-shuffling-algorithms-than-Fisher%E2%80%93Yates-shuffle 45 Useful JavaScript Tips, Tricks and Best Practices http://flippinawesome.org/2013/12/23/45-useful-javascript-tips-tricks-and-best-practices/
我们将要达到的是如下的效果(若效果未出现请刷新): 分析 主要还是运用CSS3的transition, animation, transform还有渐变背景等特性。 由于按钮在鼠标进入时有不同的样式,所以要对其:hover状态运用另外的背景样式 通过对按钮的:after状态添加一个内容为空的元素,并给其一个边框,这样在鼠标进入后我们让这个隐藏的空元素变大直到淡出,就得到我们看到的效果了 构建基本按钮样式 做为例子,我们的页面会很简单,就放一个a标签作为按钮,然后对其写样式让它看起来更像一个按钮。并定义好:after元素。 <style type="text/css"> .button{ cursor: pointer; text-decoration: none; padding: 10px; color: #fff; border-radius: 10px; position: absolute; top: 100px; left: 48%; background: linear-gradient(#93c, #50c); border:1px solid purple; } .button:after{ content: ""; position: absolute; top: 0; left: 0; width: 100%; height: 100%; border-radius: 10px; opacity: 0; border:1px solid purple; } .button:hover{ background: linear-gradient(#b5e, #93c); } </style> <body> <a class="button" href="javascript:void(0);" >Fake Button</a> </body> 添加动画 首先用keyframes定义动画 @-webkit-keyframes boom { 0% { opacity: 0 } 5% { opacity: 1 } 100% { -webkit-transform: scale(1.3); transform: scale(1.3); opacity: 0 } } @keyframes boom { 0% { opacity: 0 } 5% { opacity: 1 } 100% { transform: scale(1.3); transform: scale(1.3); opacity: 0 } } 再将其运用到按钮后面隐藏的元素上 .button:hover:after { -webkit-animation: boom 0.5s ease; animation: boom 0.5s ease; } 扩展 另外,我还发现一个jQuery插件jQuery.twinkle专门做这样的效果,因为是通过js做的,所以原理跟上面的完全不一样,但这个插件提供的效果丰富,且很炫很有创意,大家可以去欣赏下。下面是一个效果的截图。 代码下载 度娘盘:http://pan.baidu.com/s/1kT7c8gJ REFERENCE A working button on CodePen: https://codepen.io/signup/plans/ A collection of the shining effect by using a jQuery plugin : http://larsjung.de/twinkle/ jQuery twinkle plugin: http://larsjung.de/twinkle/
带平行视差效果的星星 先看效果: 如果下方未出现效果也可前往这里查看 http://sandbox.runjs.cn/show/0lz3sl9y 下面我们利用CSS3的animation写出这样的动画来,要点就是: 用动画不停改变背景图片位置; 动画高为无限循环; 在页面放三个DIV,首先将他们大小铺满整个窗口,这点是通过将其position设为absolute然后分别设置上左下右四个方面距离为0px达到的。 <!doctype html> <html> <head> <title>Moving stars</title> <style type="text/css"> html,body{ margin: 0; } .wall{ position: absolute; top: 0; left: 0; bottom: 0; right: 0; } div#background{ background: black url('img/background.png') repeat-x 5% 0%; } div#midground{ background:url('img/midground.png')repeat 20% 0%; z-index: 1; } div#foreground{ background:url('img/foreground.png')repeat 35% 0%; z-index: 2; } </style> </head> <body> <div id="background" class="wall"></div> <div id="midground" class="wall"></div> <div id="foreground" class="wall"></div> </body> </html> 然后定义的们的动画,让背景图片的位置从开始的0%变化到600%,注意我们只改变x方向的位置,y保持0%不变,因为我们想要的效果是水平移动,所以y方向无变化。 @-webkit-keyframes STAR-MOVE { from { background-position:0% 0% } to { background-position: 600% 0% } } @keyframes STAR-MOVE { from { background-position: 0% 0% } to { background-position: 600% 0% } } 最后一步便是将动画关键帧应用到三个充当背景的DIV上。 div#background { background: black url('img/background.png') repeat-x 5% 0%; -webkit-animation: STAR-MOVE 200s linear infinite; -moz-animation: STAR-MOVE 200s linear infinite; -ms-animation: STAR-MOVE 200s linear infinite; animation: STAR-MOVE 200s linear infinite; } div#midground { background: url('img/midground.png')repeat 20% 0%; z-index: 1; -webkit-animation: STAR-MOVE 100s linear infinite; -moz-animation: STAR-MOVE 100s linear infinite; -ms-animation: STAR-MOVE 100s linear infinite; animation: STAR-MOVE 100s linear infinite; } div#foreground { background: url('img/foreground.png')repeat 35% 0%; z-index: 2; -webkit-animation: STAR-MOVE 50s linear infinite; -moz-animation: STAR-MOVE 50s linear infinite; -ms-animation: STAR-MOVE 50s linear infinite; animation: STAR-MOVE 50s linear infinite; } 飘动的浮云 如果把上面的星星图片换成云彩,那就成了飘动的浮云了。 代码需要小的改动,就是背景层需要设置background-size为cover,这样才能让蓝天铺满窗口。 div#background { background: black url('img/background.png') repeat-x 5% 0%; background-size: cover; -webkit-animation: STAR-MOVE 200s linear infinite; -moz-animation: STAR-MOVE 200s linear infinite; -ms-animation: STAR-MOVE 200s linear infinite; animation: STAR-MOVE 200s linear infinite; } 下面嵌入的貌似效果不太好,还是打开链接全屏查看吧,http://sandbox.runjs.cn/show/zgvynqhj。 代码下载 度娘盘:http://pan.baidu.com/s/1sj0KHmX REFERENCE http://css-tricks.com/examples/StarryNightCSS3/ http://www.techumber.com/2013/12/amazing-glitter-star-effect-using-pure-css3.html
fartscroll.js,为放屁而生 你知道么,有了这个js库,你的页面就可以——————————放屁勒! 打开下面的演示地址,然后滚动页面。 在线演示:http://theonion.github.io/fartscroll.js/ jShaker晃到你眼睛生疼 点击页面元素,然后就闪个不停 在线演示:http://www.ajaxblender.com/script-sources/jshaker/demo/index.html?inp1=&inp2=# jQuery.spritely 任意摆布的小鸟 很有创意的一个效果,手动手柄控制飞行方向及速度,小鸟就由你掌控了! 看,那小鸟的表情是不是很二的样子,特别是那对眼睛,我去这。。不自然啊也不科学啊~ 在线演示:http://spritely.net/ Wanker.js 重磅推荐!前端中枪没? 作者知道一些前端人员已经养成习惯会调整窗口大小来看别人网页是不是响应式的了,所以写了这么个二不啦机的插件,动窗口你就中招。 插件是挺有创意的这倒没什么,重点官方给出的Example!太没品太无节操了!!不信你拿自己去试试。。。话说我在博客里也启用了,但没那么无节操。 在线演示:http://mig.io/makes/wanker/
《如何优雅地》系列在知乎火得不行了,以至于有机智网友在微博吐槽道:'知乎如何优雅地系列应该单独开一个站叫逼乎'。 早在这个噱头之前,看过一篇如何优雅地使用Windows的文章,无非就是介绍些快捷键,用快捷键就显得高端大气上档次。这个观念已经在大家脑海中根深蒂固了。从我个人的角度出发,快捷键于我的意义更多的是将双手从鼠标解放出来了,时刻紧贴键盘。我说双手解放出来了不是要证明什么,身经网络江湖的你千万表多想。 使用键盘不仅省去了移动鼠标的劳累,同时也提高了操作流畅度,对整个操作系统有更好的掌控力。缺点是学习成本高,如果快捷键使用不熟练的话,反而降低操作流畅度,毫无快感可言!但习惯了的话,各种按键Comb连技如心经一般了然无心无需强记,需要时随按即出得心应手,在各窗口间切换自如,各种操作在行云流水的几套连招之下无缝完成,游刃有余(妈蛋这里有卖弄成语的嫌疑)。 倒不是提倡有鼠标不用,非显摆,到办公室上班把鼠标优雅一拔扔一边,然后开始双手不停手操,屏幕闪烁着命令行工具。这不是拍电影,莫装逼,遭雷霹。应该以实用性原则为准,还有就是个人爱好。 比如在写博客时没鼠标是极其不方便的,给文字设样式,插表格,etc。没啥好解释的,此刻的我正是因为无线鼠标没电了才来吐槽快捷键如何有用武之地。注意,出于无奈而开的快捷键是没有装逼嫌疑的。 话说无线鼠标就是爽,但爽完后麻烦随之而来了,很规律地个把月就得换次电池不说,而且个把月就要换次电池! AutoHotKey,给键盘右边整一个回车键吧 但在继续之前这里请让我推荐另一个于我来说用处颇大的软件AutoHotkey,它非常之强大以至于它创造了自己的语法你可以进行编程来将键盘上的按键进行自定义绑定,功能类似Dota玩家用于技术施放的改键工具,但它远不止于此,他还可以绑定一些特定的操作到按键上,这不是我想要的,我用它只是用来解决苦恼我良久的一个问题:为什么键盘左边不放一个回车键! 不知道自键盘随电脑诞生这久以来有没有人会跟我有同样的这么一个需求,如果没有那只能证明我比较奇葩。这个需求的产生不是因为没有鼠标,恰恰是右手在握着鼠标的时候左手需要跑很远的距离去按那个回车!而且回车键是常用的按键!! 所以我的解决方案是,将下面的代码复制到文本文件保存成随便的名字后缀为ahk运行之。前提是你电脑装了上面说的这个软件。 $CapsLock::Enter LAlt & Capslock::SetCapsLockState, % GetKeyState("CapsLock", "T") ? "Off" : "On" 然后,左边的大小写锁定键就被替换成了回车键了,当你需要将切换到大写输入状态时,只需按Alt+CapsLk。 之所以选择它,原因有二: 我们不经常使用大小写锁定键,所以它平时就是个摆设,并且在需要输入大写的时候(虽然情况很少),我一般都是按住Shift来完成的 它的位置绝佳,与键盘上正常位置的回车键位置对称 从此,我在用鼠标操作的时候,可以很优雅地完成回车键的确定操作了。 Vimium,体验别样的网页浏览------的操作快感 平时上网浏览网页则更顷向于使用键盘操作即使是在有鼠标的情况下,包括页面导航刷新前进后退标签切换查看历史进入全屏,etc。一些浏览器默认提供的快捷键足以完成刷新后退标签切换查看历史进入全屏,他们分别是F5/Ctrl+R,,Backspace,Ctrl+Tab,Ctrl+H和F11。要完成更高级的页面导航打开连接等,不借助触摸板那必需得使用下面介绍的这款神器,口号是"像黑客一样浏览网页!!"。 在此,不管你是不是用过Vim编辑器,我要向你隆重推荐一款Chrome扩展插件:Vimium。它的诞生源自Vim编辑器,快捷键相似,如果你已经使用Vim的话,那基本没学习成本。 常用的操作主要有以下几条,需要注意的是它的快捷键是大小写敏感的,就是说x和X是不同的快捷键: yt创建当前页面的一个副本 x关闭当前标签,X恢复最近关闭的标签 j向下滚动页面,k 向上滚动页面, G(shilft+g)到页面底部,gg定位到页面顶部 f显示页面所有链接的按键分配,点击相应按键后打开连接 你只需记上面这几个操作,便可以畅通无阻浏览网页勒! 最后,任何时候你需要帮助的时候可以按?(shilft+/)来调出帮助菜单,同时显示了所有快捷键的说明。 同类的插件在Firefox里叫VimFx。 编辑器快捷键 都说SublimeText好,自从做前端后我也开始用起来。漂亮简单干净,编辑器中后起之秀,婉如林妹下凡,但此女只因天上有,习惯了其他复杂编辑器的朋友或许不太能驾驭,各种文件操作上下文菜单没有,还得装个sidebar插件才有更好的项目管理能力,貌似一些快捷键与其他编辑器通用的不一样。其实任何一个编辑器的快捷键都是需要学习成本的,所以这里没什么好责难的。 说话编辑器,我倒是挻期待GitHub最近推出的Atom的,基于Chromium,势头应该会很猛! 关于SublimeText的快捷键,木北同学的这篇博文罗列得十分详尽,下面直接摘抄。 选择类 Ctrl+D 选中光标所占的文本,继续操作则会选中下一个相同的文本。 Alt+F3 选中文本按下快捷键,即可一次性选择全部的相同文本进行同时编辑。举个栗子:快速选中并更改所有相同的变量名、函数名等。 Ctrl+L 选中整行,继续操作则继续选择下一行,效果和Shift+↓效果一样。 Ctrl+Shift+L 先选中多行,再按下快捷键,会在每行行尾插入光标,即可同时编辑这些行。 Ctrl+Shift+M 选择括号内的内容(继续选择父括号)。举个栗子:快速选中删除函数中的代码,重写函数体代码或重写括号内里的内容。 Ctrl+M 光标移动至括号内结束或开始的位置。 Ctrl+Enter 在下一行插入新行。举个栗子:即使光标不在行尾,也能快速向下插入一行。 Ctrl+Shift+Enter 在上一行插入新行。举个栗子:即使光标不在行首,也能快速向上插入一行。 Ctrl+Shift+[ 选中代码,按下快捷键,折叠代码。 Ctrl+Shift+] 选中代码,按下快捷键,展开代码。 Ctrl+K+0 展开所有折叠代码。 Ctrl+← 向左单位性地移动光标,快速移动光标。 Ctrl+→ 向右单位性地移动光标,快速移动光标。 shift+↑ 向上选中多行。 shift+↓ 向下选中多行。 Shift+← 向左选中文本。 Shift+→ 向右选中文本。 Ctrl+Shift+← 向左单位性地选中文本。 Ctrl+Shift+→ 向右单位性地选中文本。 Ctrl+Shift+↑ 将光标所在行和上一行代码互换(将光标所在行插入到上一行之前)。 Ctrl+Shift+↓ 将光标所在行和下一行代码互换(将光标所在行插入到下一行之后)。 Ctrl+Alt+↑ 向上添加多行光标,可同时编辑多行。 Ctrl+Alt+↓ 向下添加多行光标,可同时编辑多行。 编辑类 Ctrl+J 合并选中的多行代码为一行。举个栗子:将多行格式的CSS属性合并为一行。 Ctrl+Shift+D 复制光标所在整行,插入到下一行。 Tab 向右缩进。 Shift+Tab 向左缩进。 Ctrl+K+K 从光标处开始删除代码至行尾。 Ctrl+Shift+K 删除整行。 Ctrl+/ 注释单行。 Ctrl+Shift+/ 注释多行。 Ctrl+K+U 转换大写。 Ctrl+K+L 转换小写。 Ctrl+Z 撤销。 Ctrl+Y 恢复撤销。 Ctrl+U 软撤销,感觉和Gtrl+Z一样。 Ctrl+F2 设置书签 Ctrl+T 左右字母互换。 F6 单词检测拼写 搜索类 Ctrl+F 打开底部搜索框,查找关键字。 Ctrl+shift+F 在文件夹内查找,与普通编辑器不同的地方是sublime允许添加多个文件夹进行查找,略高端,未研究。 Ctrl+P 打开搜索框。举个栗子:1、输入当前项目中的文件名,快速搜索文件,2、输入@和关键字,查找文件中函数名,3、输入:和数字,跳转到文件中该行代码,4、输入#和关键字,查找变量名。 Ctrl+G 打开搜索框,自动带:,输入数字跳转到该行代码。举个栗子:在页面代码比较长的文件中快速定位。 Ctrl+R 打开搜索框,自动带@,输入关键字,查找文件中的函数名。举个栗子:在函数较多的页面快速查找某个函数。 Ctrl+: 打开搜索框,自动带#,输入关键字,查找文件中的变量名、属性名等。 Ctrl+Shift+P 打开命令框。场景栗子:打开命名框,输入关键字,调用sublimetext或插件的功能,例如使用package安装插件。 Esc 退出光标多行选择,退出搜索框,命令框等。 显示类 Ctrl+Tab 按文件浏览过的顺序,切换当前窗口的标签页。 Ctrl+PageDown 向左切换当前窗口的标签页。 Ctrl+PageUp 向右切换当前窗口的标签页。 Alt+Shift+1 窗口分屏,恢复默认1屏(非小键盘的数字) Alt+Shift+2 左右分屏-2列 Alt+Shift+3 左右分屏-3列 Alt+Shift+4 左右分屏-4列 Alt+Shift+5 等分4屏 Alt+Shift+8 垂直分屏-2屏 Alt+Shift+9 垂直分屏-3屏 Ctrl+K+B 开启/关闭侧边栏。 F11 全屏模式 Shift+F11 免打扰模式 Windows快捷键 Windows的快捷键就太多了,相关的介绍也是一搜一大堆,这里我只说说我平时常用的(本机windows 8.1)。 >Win+D这个就不用多说了,简直妇孺皆知名声在外享誉四海啊,功能为显示桌面 >Win+E打开资源管理器,所以我的桌面从来不放我的电脑那个图标的 >键盘上的上下文按键是经常用的,相当于鼠标右键,如果我表述不够清楚,它是位于右边Alt按键和右边Ctrl按键中间的 >新建文件夹Ctrl+Shilt+N,这个操作我经常在桌面进行,为了不让桌面显得凌乱不堪我通常将暂时性的东西扔进新建文件夹 >Win+B,将焦点移到任务栏托盘区,Win8中获得焦点的图标会有相应UI提示,而在之前的Windows版本中选中的和没选中的完全没有区别。之后你就可以用方向键自由选择要操作的托盘图标了 > Ctrl+Shift+Alt+Tab,这个comb有点大招的感觉,按键多,单手操作需要反复练习方能完成。功能是打开所有活动窗口的切换栏,如果我表述不够清楚你可以亲自试下。他与Alt+Tab的区别在于后者按键松开后窗口会消失,而前者则不会 >Win++++,是的你没看错,四个加号,第一个当然是正常的连接,后面三个表示连按三次键盘上的加号键。功能是打开Windows自带放大镜工具,将屏幕放大,在精细到像素的取色时我会用到 >Ctrl+W或许只对标签类应用起效,作用就是关闭当前标签,比如在浏览器里,而Alt+F4则是在任何时候都起效的关闭窗口的命令,同时另一个重要得体的用途是关机!没错,Windows8的关机操作改得极为隐蔽极为不友好,而其实你只需在桌面按下这个连击就可以调出关机对话框 >键盘上的Alt键,它除了被用在各种Comb组合之中外,单独发挥时功能也尚好。多数情况下我用来显示菜单上的热操作,这在大多数Windows程序中通用,按下之后,所有可用的操作会显示出一个热键,典型的是Office系列。 >关于在Windows的某个文件夹里快速打开命令行,只需按住Shilft加鼠标右键或者键盘上的上下文键,另外如果要以管理员身份打开命令行的话,详见我另篇博文《如何方便快速在指定文件夹打开命令行》 > Ctrl+L定位到地址栏并选中其中的文本,这个在Windows的资源管理器和浏览器里是通用的。有同学反应Win7中无效,有个替代按键是Alt+D 具体其他组合快捷键见下面,内容来自微软官网,可能没有江湖坊间盛传的版本全面彻底。 Windows 系统组合键 F1:帮助 CTRL+ESC:打开"开始"菜单 ALT+TAB:在打开的多个程序之间切换 ALT+F4:退出程序 SHIFT+DELETE:永久删除项目 Windows 徽标+L:锁定计算机 (不使用 CTRL+ALT+DELETE) Windows 程序组合键 CTRL+C:Copy CTRL+X:剪切 CTRL+V:粘贴 CTRL+Z:撤消 CTRL+B:加粗 CTRL+U:下划线 CTRL+I:倾斜 外壳对象的鼠标单击/键盘修改键组合 SHIFT+右键单击:显示包含可选命令的快捷菜单 SHIFT+双击:运行备用的默认命令(菜单上的第二个项目) ALT+双击:显示属性 SHIFT+DELETE:立即删除项目,不将其放到回收站中 常用的仅使用键盘的命令 F1:启动 Windows 帮助 F10:激活菜单栏选项 SHIFT+F10 打开对应于选定项目的快捷菜单(这与右键单击对象等效) CTRL+ESC:打开"开始"菜单(使用箭头键可选择项目) CTRL+ESC 或 ESC:选择"开始"按钮(按 TAB 键选择任务栏,或者按 SHIFT+F10 显示上下文菜单) CTRL+SHIFT+ESC:打开 Windows 任务管理器 ALT+下箭头:打开下拉列表框 ALT+TAB:切换到另一个正在运行的程序(按住 ALT 键,然后按 TAB 键可查看任务切换窗口) SHIFT:插入 CD-ROM 时按住 SHIFT 键可跳过自动运行功能 ALT+空格键:显示主窗口的"系统"菜单(从系统菜单中,您可以还原、移动、最大化、最小化或关闭窗口) ALT+-(ALT+短划线):显示多文档界面 (MDI) 子窗口的系统菜单(从 MDI 子窗口的"系统"菜单中,您可以还原、移动、最大化、最小化或关闭子窗口) CTRL+TAB:切换到多文档界面 (MDI) 程序的下一个子窗口 ALT+underlined letter in menu:打开菜单 ALT+F4:关闭当前窗口 CTRL+F4:关闭当前多文档界面 (MDI) 窗口 ALT+F6:在同一程序的多个窗口之间切换(例如,当显示记事本的查找对话框时,按 ALT+F6 可在"查找"对话框和记事本主窗口之间切换) 外壳对象和常用文件夹/Windows 资源管理器快捷键 对于选定对象: F2:重命名对象 F3:查找所有文件 CTRL+X:剪切 CTRL+C:复制 CTRL+V:粘贴 SHIFT+DELETE:立即删除选定项目,不将项目移动到回收站 ALT+ENTER:打开选定对象的属性 复制文件 按住 CTRL 键将文件拖到另一文件夹。 创建快捷方式 按住 CTRL+SHIFT 将文件拖到桌面或文件夹中。 常用文件夹/快捷键控件 F4:选择"转到不同的文件夹"框并沿框中的项向下移动(如果工具栏在 Windows 资源管理器中是活动的) F5:刷新当前窗口。 F6:在 Windows 资源管理器中的窗格之间移动 CTRL+G:打开"转到文件夹"工具(仅限于 Windows 95 中的 Windows 资源管理器) CTRL+Z:撤消上一命令 CTRL+A:选择当前窗口中的所有项目 BACKSPACE:切换到父文件夹 SHIFT+ 单击 +关闭按钮:对于文件夹,关闭当前文件夹和所有父文件夹 Windows 资源管理器树控件 数字键盘 *:展开当前选项下的所有内容 数字键盘 +:展开当前选项 数字键盘 -:折叠当前选项。 右箭头:如果当前选项未展开,则展开它;否则转到第一个子节点 左箭头:如果当前选项已展开,则折叠它;否则转到父节点 属性控件 CTRL+TAB/CTRL+SHIFT+TAB:在属性选项卡中移动 辅助功能快捷键 按 SHIFT 键五次:打开和关闭粘滞键 按住右 SHIFT 键八秒钟:打开和关闭筛选键 按住 NUM LOCK 键五秒钟:打开和关闭切换键 左 ALT+左 SHIFT+NUM LOCK:打开和关闭鼠标键 左 ALT+左 SHIFT+PRINT SCREEN:打开和关闭高对比度 Microsoft Natural Keyboard 键 Windows 徽标:"开始"菜单 Windows 徽标+R:"运行"对话框 Windows 徽标+M:全部最小化 SHIFT+Windows 徽标+M:撤消全部最小化 Windows 徽标+F1:帮助 Windows 徽标+E:Windows 资源管理器 Windows 徽标+F:查找文件或文件夹 Windows 徽标+D:最小化所有打开的窗口并显示桌面 CTRL+Windows 徽标+F:查找计算机 CTRL+Windows 徽标+TAB:将焦点从"开始"菜单移动到"快速启动"工具栏或系统任务栏(使用右箭头键或左箭头键可将焦点移动到"快速启动"工具栏和系统任务栏中的项目) Windows 徽标+TAB:在任务栏按钮之间循环 Windows 徽标+Break:"系统属性"对话框 应用程序键:显示对应于选定项目的快捷菜单 安装有 IntelliType 软件的 Microsoft Natural Keyboard Windows 徽标+L:注销 Windows Windows 徽标+P:启动打印管理器 Windows 徽标+C:打开控制面板 Windows 徽标+V:启动剪贴板 Windows 徽标+K:打开"键盘属性"对话框 Windows 徽标+I:打开"鼠标属性"对话框 Windows 徽标+A:启动辅助功能选项(如果已安装) Windows 徽标+空格键:显示 Microsoft IntelliType 快捷键的列表 Windows 徽标+S:打开和关闭 CAPS LOCK 对话框键盘命令 TAB:移动到对话框中的下一个控件 SHIFT+TAB:移动到对话框中的上一个控件 空格键:如果当前控件是一个按钮,将单击此按钮。如果当前控件是一个复选框,将选中或清除该复选框。如果当前控件是一个选项,将选择该选项。 ENTER:与单击选定的按钮(带有轮廓线的按钮)等效 ESC:与单击"取消"按钮等效 ALT+underlined letter in dialog box item:移动到相应的项目 Reference http://www.cnblogs.com/mubei/p/3570857.html http://support.microsoft.com/kb/126449/zh-cn http://www.autohotkey.com/
相信很多刚踏入软件这个行业的小伙伴一如当初的我,对开源软件的各种协议不甚了解被搞昏了头脑。毕竟对于一个新生程序员来说,如何写好代码才是亟待解决的问题,无暇了解这些。随着你项目做得多了代码写得多了,你会发现编码过程中会不时用到其他人的成果,一个项目下来多少会引入一些优秀的库,别人放在公网上开源的DLL,以及一些算法等等。细心的你会注意到即使只是一小段代码,优秀的作者都在最开始会简单地附上一段关于许可的声明,或者说是协议比如"Licensed under the MIT license",并且一些博客也会标明"此文章发表在CC协议下"。而如果我们Copy了别人的代码或者文字同时没注意这些的话,在国外法律意识特别强的环境下,我们的作品会因触犯别人的权益而违法。因为好多开源协议最低要求是使用者需要保留原作者对代码的声明,不声不响地就拿来用了必然导致恶果。 所以开源不等于免费,开源也不等于没有约束。 何为License License是软件的授权许可,里面详尽表述了你获得代码后拥有的权利,可以对别人的作品进行何种操作,何种操作又是被禁止的。软件协议可分为开源和商业。当然本文要讨论的当然是开源协议。 对于商业协议,或者叫法律声明、许可协议,每个软件会有自己的一套行文,由软件作者或专门律师撰写。这是什么惊为天人的东西嘛还得请专门的律师。因为涉及到以后侵权打官司这种事情,这种商业条款的行文是非常严谨而讲究的,记得以前看到句调侃的话:'如果法律文件不写得那么生涩难懂,律师们就没饭吃了',就是说任何文字一旦上升到法律的层次,不要说你接受完了九年义务教育,就是考了个专八也会觉得英语白学了,直接的法律协议什么的那不是给常人看的。而至于法律条款缘何会晦涩难懂,这个偏题有点偏远了,可以查看这里了解。看累了?下面是欢乐时刻,奉上一个协议相关的Joke(笑崩!苹果iOS7升级协议条款中员工神吐槽)。 你丫知道么?这已经是46页,肯定没人读的。我敢打赌大概只有5个人点了"条款和协议",所以我们想扯点啥就扯点啥吧。 Apple总部5楼的那个tony总是浑身一股沙丁鱼味 有人给我们寄点啥2b向的邮件,我们都得很文艺的用"i笑了"的方式回复。这是我们的工作规定 还记得当年关于Apple Studio的版权合法性争议么?想知道我们怎么摆平的?我们把披头士乐队买下来了。他们中活着的现在没事来给我们唱两曲解解闷,死了的,我们想办法像Miku的3D投影那样,设法在二次元给他们来个复活 我们餐厅永远只卖苹果东西:苹果,苹果汁,苹果煎饼,苹果棒糖。。。不想丢工作的话我们只能吃这些,而哥恰好对苹果过敏,哥现在正处于饿的神志不清乱敲键盘的节奏。 我们伪造了登月真相。其实美国人登月是2008年的事情,我们向你们洗脑它发生在1969,我们绝对有这种洗脑的本事。如果有人发现我知道的太多了,我就会被查水表,但是没关系,没人会看这页。 所以对于大多数人来说,不用自己花大把时间去写许可协议,选择一分广为流传的开源协议是个不错的选择,如果你的作品是开源的话,这样省时又省心。 选择一分协议的好处 你的作品如果不是定性为全商业性质,可以考虑选择一分流行度比较高的开源协议。具体来说的话,你肯定希望作品能够被多数人分享查阅吧,不但提高自己业界的知名度,同时也方便了需要的人为开源做出了贡献。换句话说,你不分享出来的话你的作品的意义何在呢(当然,自己捣腾的私人东西还是自己保留吧)?可是一旦你把你的代码贴出来,这就表示任何人都可以看到并获取,之后发生的事情你无法控制,有的人或许稍微修改一下放进自己的代码中,有的把你的软件改个名字拿去贩卖,有的甚至会拿去把作者名字改为自己然后拿去找工作什么的,而不会有人知道这个作品的原作者,背后辛勤付出了的人。所以为了公开分享你的代码,同时又让你对代码保留一定权利,在作品中声明一个许可协议是非常有必要的,这是很多新人所忽略的问题,同时很多人在使用别人的劳动成果时也会忽视协议的存在,这样不好。所以你会看到我的博客里面时不时会给出连接指向来源页面,同时文末也会列出所有参考过的文章。我相信我做到了这点,别人在转载我的文章的时候,也可以做到这点,这样营造出来的氛围一定会非常和谐,互相尊重/Show Respect。 多说一句,一个事实让你了解国外开发者在尊重他人劳动成果方面做得是如何的到位,如果A的作品是因为B的作品的启发而来,A甚至都没有使用B任何一句代码,但A会在他的作品里面指明是受到了B的启发"Inspired by XXX link :http://www.blah.com"。 当然有人会觉得,有了一分协议声明在那里,我就需要鸟你么,我拿来用了把作者名字去掉同时还要加上我的名字,你咬我?!这是后话,只是在利益很小的情况下,或者作者不知情的情况下,作者不会追究什么责任,但如果你的产品做成功了,那就不一定了。另外就是,有协议和没声明协议的裸代码是有非常重要区别的,一般作品当中没声明协议的默认为Copy right的,也就是版权保留。此种情况表明他人没有任何授权,不得复制分发修改使用等等,但一如上面所讨论的,这样的话还何来开源,何来分享呢。有了协议的声明,在未来你的维权上面会方便很多,让你的作品在分享的同时保留了自身的一些权利。 快速选择 目前流行的开源协议有很多,并且同一款协议有很多变种,比如你或许看到过' CC Attribution-NoDerivs',' CC Attribution-NonCommercial'同属CC协议(后面会有介绍)。如此纷繁的协议该如何选择?协议太宽松会导致作者丧失对作品的很多权利,太严格又不便于使用者使用及作品的传播。所以除了协议多之外,你还要考虑你对作品想保留哪些权利,放开哪些限制。 如果你不想了解太多,只是想要一个简直直接的答案,下面给出的建议或许适合你。下方关于协议的选择及表格来自GitHub choosealicence项目。 简单宽松的协议 如果你只想要一个简单点的协议不想太麻烦的话。 MIT协议相对宽松但还是抓住了要点的。此协议允许别人以任何方式使用你的代码同时署名原作者,但原作者不承担代码使用后的风险,当然也没有技术支持的义务。jQuery和Rails就是MIT协议。 考虑有专利的情况 如果你的作品中涉及到专利相关。 Apache协议也是个相对宽松与MIT类似的协议,但它简单指明了作品归属者对用户专利上的一些授权(我的理解是软件作品中含有专利,但它授权你可以免费使用)。Apache服务器,SVN还有NuGet等是使用的Apache协议。 代码分享与促进 如果你在乎作品的传播和别人的修改,希望别人也以相同的协议分享出来。 GPL(V2或V3)是一种版本自由的协议(可以参照copy right来理解,后者是版本保留,那copyleft便是版权自由,或者无版权,但无版权不代表你可以不遵守软件中声明的协议)。此协议要求代码分发者或者以此代码为基础开发出来的衍生作品需要以同样的协议来发布。此协议的版本3与版本2相近,只是多3中加了条对于不支持修改后代码运行的硬件的限制(没太明白此句话的内涵)。 各协议授权详情 下面是更多开源协议的一个表格任君选择,总有一款是你的菜。 不过先来了解一些下方表格中出现的用词的解释: 协议和版权信息(License and copyright notice):在代码中保留作者提供的协议和版权信息 声明变更(State Changes):在代码中声明对原来代码的重大修改及变更 公开源码(Disclose Source):代码必需公开。如果是基于LGPL协议 下,则只需使用的开源代码公开,不必将整个软件源码公开 库引用(Library usage):该库可以用于商业软件中 责任承担(Hold Liable):代码的作者承担代码使用后的风险及产生的后果 商标使用(Use Trademark):可以使用作者的姓名,作品的Logo,或商标 附加协议(Sublicensing):允许在软件分发传播过程中附加上原来没有的协议条款等 协议 描述 要求 允许 禁止 Apache 一个较宽松且简明地指出了专利授权的协议。 协议和版权信息 声明变更 商用 分发 修改 专利授权 私用 附加协议 责任承担(禁止让作者承担责任,可以理解为免责) 商标使用 GPL 此协议是应用最为广泛的开源协议,拥有较强的版权自由( copyleft )要求。衍生代码的分发需开源并且也要遵守此协议。此协议有许多变种,不同变种的要求略有不同。 公开源码 协议和版权信息 声明变更 商用 分发 修改 专利授权 私用 责任承担 附加协议 MIT 宽松简单且精要的一个协议。在适当标明来源及免责的情况下,它允许你对代码进行任何形式的使用。 协议和版权信息 商用 分发 修改 私用 附加协议 责任承担 Artistic Perl社区尤为钟爱此协议。要求更改后的软件不能影响原软件的使用。 协议和版权信息 声明变更 商用 分发 修改 私用 附加协议 责任承担 商标使用 BSD 较为宽松的协议,包含两个变种BSD 2-Clause 和BSD 3-Clause,两者都与MIT协议只存在细微差异。 协议和版权信息 商用 分发 修改 私用 附加协议 责任承担 Eclipse 对商用非常友好的一种协议,可以用于软件的商业授权。包含对专利的优雅授权,并且也可以对相关代码应用商业协议。 公开源码 协议和版权信息 商用 分发 修改 专利授权 私用 附加协议 责任承担 LGPL 主要用于一些代码库。衍生代码可以以此协议发布(言下之意你可以用其他协议),但与此协议相关的代码必需遵循此协议。 公开源码 库引用 协议和版权信息 商用 分发 修改 专利授权 私用 附加协议 责任承担 Mozilla Mozilla Public License(MPL 2.0)是由Mozilla基金创建维护的。此协议旨在较为宽松的BSD协议和更加互惠的GPL协议中寻找一个折衷点。 公开源码 协议和版权信息 商用 分发 修改 专利授权 私用 附加协议 责任承担 商标使用 No license 你保留所有权利,不允许他人分发,复制或者创造衍生物。当你将代码发表在一些网站上时需要遵守该网站的协议,此协议可能包含了一些对你劳动成果的授权许可。比如你将代码发布到GitHub,那么你就必需同意别人可以查看和Fork你的代码。 协议和版权信息 商用 私用 分发 修改 附加协议 Public domain dedication 在许多国家,默认版权归作者自动拥有,所以Unlicense协议提供了一种通用的模板,此协议表明你放弃版权,将劳动成果无私贡献出来。你将丧失对作品的全部权利,包括在MIT/X11中定义的无担保权利。 N/A 商用 分发 修改 私用 责任承担 非代码类作品的协议 上面各协议只是针对软件或代码作品,如果你的作品不是代码,比如视频,音乐,图片,文章等,共享于公众之前,也最好声明一下协议以保证自己的权益不被侵犯。针对非代码的数字作品的协议,最通用的莫过于Creative Commons(也是你经常在别人博客下面可以看到的CC协议)协议。所以现在你见到博客园别人文章下面的签名就不会感到陌生了。 无协议 你没有义务也没人非要你必需在自己的代码作品里面加上一个开源协议。但一如上文所讨论过的优点,如果你想把代码分享出来,最好还是选择一个适合的开源协议,这样别人用着放心。 Reference https://github.com/github/choosealicense.com http://choosealicense.com/ http://www.smashingmagazine.com/2011/06/14/understanding-copyright-and-licenses/
每次打开谷歌浏览器的About页面更新的时候,总是期待着一个新版本的到来,新的东西总是让人感到Amazing。这样久了之后心中不免产生一个疑问,什么时候该发布一个新版本了,有什么规律么?平时的小更新总是版本号后面无关仅要的数字的增长,当这个数字增长到何时可以让主版本号加1? 带着这个疑惑到StackOverflow造访了一下,良久无回音。再加上自己写点小东西时也需要正确地命名版本号来管理发布,看来是有必要补充一下这方面的知识了。 语义化版本号 当我在发布jQuery插件时,发现其官方页面上提供了一个帮助我们更好地命名软件版本号的概念"semver",即Semantic Versioning语义化的版本。看了下其规则觉得很nice。 关于软件的版本号,向来没有统一或者严格的规定,对于大型软件产品,其开发团队内部或许维护了自己的一套规则来界定软件开发到何时可以发布新版本,何时又只是增加次版本号,也或许在遵循一些现成的大家认可的规范;更多情况是对个人开发者而言,在自己捣腾一些小东西时,这样的版本号规则就更自由了,完全视软件作者的水平而良莠不齐。有的作者或许学习过版本相关的知识,知道遵循一些现成的规范,更多的新手比如像我这样,完全是随毫无规则地在使用版本号。今天开发完一个功能,那就发布一个版本叫做0.1吧,下午发现个bug并修复之再发个版本0.2吧。如此显然不好,无规矩不成方圆啊,我们已经饱受各浏览器不完全遵循W3C规范而带来的各种跨浏览器前端问题了,血的教训,历史告诉我们,不能让悲剧重演,所以迫切需要一个好的准则来指导大家更好地使用软件版本号。 语义化版本号的作者正是抱着这样的希望创造了它。 语义化版本号是由Tom Preston-Werner 发起的一个关于软件版本号的命名规范,关于这个规范详细的说明可以在官网查看,也可访问其GitHub项目页面,而关于该规范的中文版本,可以访问我fork的版本,由官网繁体中文转换而来,并稍加修改以更符合大陆用语。顺便提句,该规范的作者是Gravatars创办者同时是GitHub联合创始人。你或许不知道gravatar但作为程序员你肯定知道GitHub。 基本规则 顾名思义,语义化的版本就是让版本号更具语义化,可以传达出关于软件本身的一些重要信息而不只是简单的一串数字。 版本格式:主版本号.次版本号.修订号,版本号递增规则如下: 主版本号:当你做了不兼容的API 修改 次版本号:当你做了向下兼容的功能性新增 修订号:当你做了向下兼容的问题修正 先行版(预览版)版本号及版本编译信息可以加到"主版本号.次版本号.修订号"的后面,作为延伸。 具体规范 具体详尽的规范可以参见其官网,当然也可以访问中文版本。这里简单总结一下。 版本号是以点隔开的形式'X.Y.Z' 且XYZ为正整数,数字前面不加0, 也就是说v0.1.0不能写成v0.01.0 一般软件开发过程中以0.1.0 版本开始,开发过程中不断增加新功能,则增加次版本号比如变成0.2.0,然后期间的问题及bug修复体现在修订号上,比如版本号变成0.1.12。这一阶段的版本视为不稳定版本,一般也未对外发布 主版本号表示正式版的形成,也即如果你开发的是供大家使用的软件或插件,那就标致本软件公共API的形成,比如新浪微博API v1.0.0发布,大家就可以在自己网站上调用了,这是个正式而稳定的版本。所以这里有个规定,版本一旦发布,不允许对软件做任何修改。任何改过之后的代码都应标记新的版本号在下次发布中体现 主版本号的增加可以是次版本号以有修订号增加到一定数量后的结果,也可以是有不兼容旧版的新功能或API加入的结果,并且主版本增加后次版本号和修订号归零 次版本号表示有兼容旧版本的功能或API增加,而修订号表示bug修复并且这种修复一般是对代码结果不正确的修复而且一定是兼容旧版本的,如果你修复bug越改越大结果不兼容旧版本了,则需要增加主版本号 其他信息比如预览版,先行版或者软件编译信息可以跟随在修订号之后。示例:1.0.0-alpha+001、1.0.0+20130313144700、 1.0.0-beta+exp.sha.5114f85 使用语义化版本号的好处 也即原规范中对为何要使用语义化版本号的描述。在我看来,无非就是在遵循了本规范后,透过版本号,你可以非常清楚地了解到你手头拿到的软件版本相比于上一个版本发生了怎样的变化,所以你在使用的时候可以更注意一下这些变化,以免出现不兼容的情况。 比如如果主版本号升级了,可以知道软件新增了功能且该功能或者重大问题修复,且都是与旧版本不兼容的。好比大家热切推崇的文本编辑器Sublimetext2和3,他的很多插件在这两个版本间无法兼容使用,所以一般要标明插件是使用在Sublimetext2还是3中。同时主版本号的更新也可以表明是次版本号更新到了一定程序,比如新增功能数量达到了一定指标,我们可以认为可以升级一下主版本号了,毕竟一个可以copy as rtf,带项目文件管理sidebar,更换主题的文本编辑器和Windows自带文本编辑器在功能上还是有质的区别的。 如果次版本更新了,我们可以知道有小部分新功能添加,或者修订号更新,有小部分bug被修复,而在获取这些信息时完全还没有查看change log。这正是语义化的好处,版本号就告诉你大部分信息了,当然更具体的参见change log吧。 另外个好处就是当大家都在遵循一个规范的时候,无疑扫清了一些认知上的障碍,将事情简单化,大家也心照不宣地能看懂每个人代码中的版本号的意思,初学者也很容易掌握这方面的知识。 一些问题 各版本优先级 也即如何判定哪个版本版次更高。下面是来自原规范的解释,已经够详尽就不另外阐述。 判断优先层级时,必须把版本依序拆分为主版本号、次版本号、修订号及先行版本号后进行比较(版本编译信息不在这份比较的列表中)。由左到右依序比较每个标识符号,第一个差异值用来决定优先层级:主版本号、次版本号及修订号以数值比较,例如1.0.0 < 2.0.0 < 2.1.0 < 2.1.1。当主版本号、次版本号及修订号都相同时,改以优先层级比较低的先行版本号决定。例如:1.0.0-alpha < 1.0.0。有相同主版本号、次版本号及修订号的两个先行版本号,其优先层级必须透过由左到右的每个被句点分隔的标识符号来比较,直到找到一个差异值后决定:只有数字的标识符号以数值高低比较,有字母或连接号时则逐字以ASCII的排序来比较。数字的标识符号比非数字的标识符号优先层级低。若开头的标识符号都相同时,栏 位比较多的先行版本号优先层级比较高。范例:1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0- rc.1 < 1.0.0。 如何界定正式版1.0.0的发布 这个正是我在开发自己的jQuery插件时面临的问题。如上面规范所述,软件最初开发阶段一般以0.1.0开始。当软件基本正式功能全部完成且测试通过,对外公共API完成可以用于实际线上环境了,则可以形成1.0.0的正式版了。 更多关于本规范的常见问题还是请查看文档,上面的FAQ列出的问题很实在,可以解决使用本版本号命名中的疑惑。 Reference: Semver 简体中文版本:https://github.com/Wayou/semver_zh_CN/blob/master/semver_zh_CN.md Semver GitHub项目地址:https://github.com/mojombo/semver semver 官方文档页:http://semver.org/
将之前捣腾的音乐频谱效果加上一个播放列表就成了现在的喵喵播放器(Meow meow Player,额知道这名字很二很装萌~),全HTML5打造的网页程序,可本地运行也可以挂服务器上用。 在线Demo及源码 你可以访问下面的地址打开在线demo: http://wayou.github.io/MeowmeowPlayer/ 项目github地址: https://github.com/Wayou/MeowmeowPlayer/ 示例音乐下载,提供给硬盘里没有Music的同学:http://pan.baidu.com/s/1eQqqSfS 欢迎喜欢的朋友前去星(star)我叉(fork)我或者下载代码自己研究。 Note 支持文件拖拽上传,同时你也可以点击页面上的'Add'来添加音乐文件 对文件大小做了限制,只处理30M以下的文件,这是为了防止内存上涨把浏览器爆掉 同时在对文件解码时对页面上可进行的操作进行了屏蔽,为是防止一些非法操作导致逻辑出错 程序可能报bug,但娱乐基本够用 目前,列表的打乱功能还没做好,即'Shuffle'(Update:此功能已实现) 缺憾是没有实现暂停功能,因为web audio api没有提供相关接口 浏览器兼容性 工作于支持Web Audio API 的浏览器,开发测试的浏览器为Chrome 33, Firefox 28. Chrome Firefox 技术点 基本上来说,用到了以下一些技术: Web Audio API HTML5 canvas绘图 HTML5 requstAnimation绘制动画 CSS3 anmation, transition, transform ,etc. FileReader JavaScript中用于文件获取 其中,有些代码参考甚至直接来自网上现成的代码: 带有渐变且半透明效果的遮罩用于频谱图的镜像,参考了代码引用1的代码 用于播放列表的Win8 进度效果的CSS3实现来自codepen上的代码,地址见引用2 问题及交流 欢迎技术爱好者,极客朋友,前端开发人员,喜欢捣腾新东西以及广大喜欢炫酷新鲜玩意儿的用户朋友们反馈问题或者提建议。 你可以访问项目的GitHub 页面 获取代码,star我fork我或者open an issue. 你可以访问在线demo 查看效果,获得至尊感受 你还可以访问之前一篇博客获得关于实现的详细信息 Reference http://hammerspace.co.uk/2012/02/css3-gradients-with-transparency http://codepen.io/jameswyse/pen/uisvk http://www.cnblogs.com/Wayou/p/html5_audio_api_visualizer.html
在看了一些关于HTML5 Audio API的资源后,一下子热情高涨顺势写了个音乐频谱效果的作品来玩味,也就是上上篇博文里介绍的。今天在测试的时候发现三个问题处理得不是很好: 当歌曲正在播放的时候再选择另一首,页面顶部的标题没有很好地更新 对于有些音频文件,比如在上篇博文中提供的示例音乐里面,bbc_sherlock_openning.mp3这个文件在播放完毕后频谱居然不会归零! 另一个问题就是重点了,歌曲播放完了后后台代码仍在不停地跑 结论 鉴于后面的内容是跟具体程序有关的(上上篇博文中介绍的Audio Visualizer),所以不知道源码的你或许不太感兴趣,于是把结论提前。 我想说的就是, 在JavaScript中递归使用了setTimeout或者setInterval,或者requestAnimationFrame(这个方法本身要求你递归使用),如果你的代码有结束条件,最好在递归满足结束条件后调用clearTimeout/clearInterval,cancleAnimationFrame来结束这些方法的运行。 就比如在问题三当中,我用的requestAnimationFrame来写的动画,当歌曲播放完毕,调用cancleAnimationFrame来清理之前设置的requestAnimationFrame以释放内存。 下面是个关于取消requestAnimationFrame的简单例子,来自css-tricks: var globalID; function repeatOften() { $("<div />").appendTo("body"); globalID = requestAnimationFrame(repeatOften); } $("#start").on("click", function() { globalID = requestAnimationFrame(repeatOften); }); $("#stop").on("click", function() { cancelAnimationFrame(globalID); }); Question 1 第一个问题是用户体验的问题,很好解决,所以对于页面顶部的infobar作了如下调整: 程序开始显示程序名称"HTML5 Audio API showcase | An Audio Viusalizer" 用户选择文件或者拖拽文件到页面后,显示相应的后台操作,比如"文件上传中。。。","文件解码中。。。",etc. 途中出错则显示相应错误信息,比如"!解码失败",如果一切顺利,则音乐开始播放并显示当前播放的文件名,同时将文字淡下去,让主题频谱更好地呈现 歌曲顺利结束,顶部信息恢复正常显示,同时更新标题到最开始状态,也就是显示程序名 如果歌曲播放过程中用户选择了另外的文件,顶部信息恢复正常显示并且转到步骤2 Question 2 第二个问题,出乎我的意料,同时也想不通,不妨先来重现一下。 下图便是播放bbc_sherlock_openning.mp3完毕后的画面,程序就这样停留在了这样的一个画面,有三根频谱条没有归零。我硬盘里大部分歌曲都被我跑过了都没问题,但才看了<神探夏洛克>的我执意要拿它的片头曲来爽一把,然后就发现问题了。 我不认为是程序的问题,又无奈想不出这文件有什么问题,于是只能来硬的了。进行人工干预,在每首歌曲播放完毕后手动将从歌曲里面获取的值也就是analyser设为0,这样所有频谱条都会没有问题了。算是个笨拙的解决方法吧,因为没有打到问题的根源。 具体代码可以去下载最新版本的源码查看。之前的代码都有注释,后来加的这些注释不详细,所以我自认为有点晦涩难懂。 Question 3 对于第三个问题,其实原因我知道,也知道如何解决,只是在创建程序时没有好的方案来组织代码避免其他问题。今天再次进行编码时得到了解决。 解决之前,不妨先来看开发者工具中显示的信息。 选择文件进行播放 F12打开高度工具,切到时间轴(Timeline)面板,选择内存(memory) 点击'记录按钮(Record)' 开始监视代码执行过程中的内存及运行状况 这是歌曲播放过程中,还看不出什么端倪。等待歌曲结束继续观测。 从上图可以看出,即使歌曲结束,内存还是呈现规律地起伏,我们期望的是它稳定,并且上方的记录数随时间推进也在增加,说明后台代码正在运行。 原因是代码中使用了requestAnimationFrame设置动画,并且每选择一首歌后,都又会重新建立新的requestAnimationFrame来进行动画,而前面的requestAnimationFrame没有消失仍然存在,这样一来,一首接一首的歌曲后可以预见到浏览器崩溃的情况。 解决方法也就是前面说的适时地调用cancelAnimationFrame来清理之前设置的动画。 下图展示了改进后的情况。可以看到,歌曲播放完毕后,记录数停止了增加,说明后台代码没有再运行了,并且等待了一段时间后图中的内存也处于稳定状态,没有起伏。
Source: StatCounter Global Stats - Browser Market Share
话说HTML5的炫酷真的是让我爱不释手,即使在这个提到IE就伤心不完的年代。但话又说回来,追求卓越Web创造更美世界这样高的追求什么时候又与IE沾过边儿呢?所以当你在看本文并且我们开始讨论HTML5等前沿东西的时候,我们默认是把IE排除在外的。本文的例子可以工作在最新的Chrome及Firefox浏览器下,其他浏览器暂未测试。 若下方未出现演示页面请刷新。 你也可以点此全屏演示 或者前往GitHub进行代码下载然后本地运行。 你还可以 下载示例音乐(如果你手头没有音频文件的话) 文件列表:bbc_sherlock_openning.mp3Neptune Illusion Dennis Kuo .mp3单曲Remix ┃ 爱上这个女声 放进专辑里私藏 夜电播音员.mp3爱啦啦.mp3 最后,喜欢的朋友可以去GitHub星我(star me)叉我(fork me)。 这里将要介绍的HTML5 音频处理接口与Audio标签是不一样的。页面上的Audio标签只是HTML5更语义化的一个表现,而HTML5提供给JavaScript编程用的Audio API则让我们有能力在代码中直接操作原始的音频流数据,对其进行任意加工再造。 展示HTML5 Audio API 最典型直观的一个例子就是跟随音乐节奏变化的频谱图,也称之为可视化效果。本文便是以此为例子展示JavaScript中操作音频数据的。 文中代码仅供参考,实际代码以下载的源码为准。 了解Audio API 一段音频到达扬声器进行播放之前,半路对其进行拦截,于是我们就得到了音频数据了,这个拦截工作是由window.AudioContext来做的,我们所有对音频的操作都基于这个对象。通过AudioContext可以创建不同各类的AudioNode,即音频节点,不同节点作用不同,有的对音频加上滤镜比如提高音色(比如BiquadFilterNode),改变单调,有的音频进行分割,比如将音源中的声道分割出来得到左右声道的声音(ChannelSplitterNode),有的对音频数据进行频谱分析即本文要用到的(AnalyserNode)。 浏览器中的Audio API 统一前缀 JavaScript中处理音频首先需要实例化一个音频上下文类型window.AudioContext。目前Chrome和Firefox对其提供了支持,但需要相应前缀,Chrome中为window.webkitAudioContext,Firefox中为mozAudioContext。所以为了让代码更通用,能够同时工作在两种浏览器中,只需要一句代码将前缀进行统一即可。 window.AudioContext = window.AudioContext || window.webkitAudioContext || window.mozAudioContext || window.msAudioContext; 这是一种常见的用法,或者操作符'||' 连接起来的表达式中,遇到真值即返回。比如在Chrome中,window.AudioContext为undefined,接着往下走,碰到window.webkitAudioContext不为undefined,表达式在此判断为真值,所以将其返回,于是此时window.AudioContext =window.webkitAudioContext ,所以代码中我们就可以直接使用window.AudioContext 而不用担心具体Chrome还是Firefox了。 var audioContext=new window.AudioContext(); 考虑浏览器不支持的情况 但这还只是保证了在支持AudioContext的浏览器中能正常工作,如果是在IE中,上面实例化对象的操作会失败,所以有必要加个try catch语句来避免报错。 try { var audioContext = new window.AudioContext(); } catch (e) { Console.log('!Your browser does not support AudioContext'); } 这样就安全多啦,妈妈再不担心浏览器报错了。 组织代码 为了更好地进行编码,我们创建一个Visualizer对象,把所有相关属性及方法写到其中。按照惯例,对象的属性直接写在构造器里面,对象的方法写到原型中。对象内部使用的私有方法以短横线开头,不是必要但是种好的命名习惯。 其中设置了一些基本的属性将在后续代码中使用,详细的还请参见源码,这里只简单展示。 var Visualizer = function() { this.file = null, //要处理的文件,后面会讲解如何获取文件 this.fileName = null, //要处理的文件的名,文件名 this.audioContext = null, //进行音频处理的上下文,稍后会进行初始化 this.source = null, //保存音频 }; Visualizer.prototype = { _prepareAPI: function() { //统一前缀,方便调用 window.AudioContext = window.AudioContext || window.webkitAudioContext || window.mozAudioContext || window.msAudioContext; //这里顺便也将requestAnimationFrame也打个补丁,后面用来写动画要用 window.requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame; //安全地实例化一个AudioContext并赋值到Visualizer的audioContext属性上,方便后面处理音频使用 try { this.audioContext = new AudioContext(); } catch (e) { console.log('!妳的浏览器不支持AudioContext:('); console.log(e); } }, } 加载音频文件 不用说,你肯定得先在代码中获取到音频文件,才能够对其进一步加工。 文件获取的方法 读取文件到JavaScript可以有以下三种方法: 1.新开一个Ajax异步请求来获取文件,如果是本地测试需要关掉浏览器的同源安全策略才能获取成功,不然只能把网页放到服务器上才能正常工作。 具体说来,就是先开一个XMLHttpRequest请求,将文件路径作为请求的URL,并且设置请求返回类型为'ArrayBuffer',这种格式方便我们后续的处理。下面是一个例子。 loadSound("sample.mp3"); //调用 // 定义加载音频文件的函数 function loadSound(url) { var request = new XMLHttpRequest(); //建立一个请求 request.open('GET', url, true); //配置好请求类型,文件路径等 request.responseType = 'arraybuffer'; //配置数据返回类型 // 一旦获取完成,对音频进行进一步操作,比如解码 request.onload = function() { var arraybuffer = request.response; } request.send(); } 2.通过文件类型的input来进行文件选择,监听input的onchnage事件,一担文件选中便开始在代码中进行获取处理,此法方便,且不需要工作在服务器上 3.通过拖拽的形式把文件拖放到页面进行获取,比前面一种方法稍微繁杂一点(要监听'dragenter','dragover','drop'等事件)但同样可以很好地在本地环境下工作,无需服务器支持。 更多在JavaScript中获取及处理文件的方法可以见这里 不用说,方法2和3方便本地开发与测试,所以我们两种方法都实现,既支持选择文件,也支持文件拖拽。 通过选择获取 在页面放一个file类型的input。然后在JavaScript中监听它的onchange事件。此事件在input的值发生变化时触发。 对于onchange事件,在Chrome与Firefox中还有一点小的区别,如果你已经选择了一个文件,此时Input就有值了,如果你再次选择同一文件,onchange事件不会触发,但在Firefox中该事件会触发。这里只是提及一下,关系不大。 <label for="uploadedFile">Drag&drop or select a file to play:</label> <input type="file" id="uploadedFile"></input> 当然,这里同时也把最后我们要画图用的canvas也一起放上去吧,后面就不用多话了。所以下面就是最终的HTML了,页面基本不会变,大量的工作是在JavaScript的编写上。 <div id="wrapper"> <div id="fileWrapper" class="file_wrapper"> <div id="info"> HTML5 Audio API showcase | An Audio Viusalizer </div> <label for="uploadedFile">Drag&drop or select a file to play:</label> <input type="file" id="uploadedFile"></input> </div> <div id="visualizer_wrapper"> <canvas id='canvas' width="800" height="350"></canvas> </div> </div> 再稍微写一点样式 #fileWrapper { transition: all 0.5s ease; } #fileWrapper: hover { opacity: 1!important; } #visualizer_wrapper { text-align: center; } 向Visualizer对象的原型中新加一个方法,用于监听文件选择既前面讨论的onchange事件,并在事件中获取选择的文件。 _addEventListner: function() { var that = this, audioInput = document.getElementById('uploadedFile'), dropContainer = document.getElementsByTagName("canvas")[0]; //监听是否有文件被选中 audioInput.onchange = function() { //这里判断一下文件长度可以确定用户是否真的选择了文件,如果点了取消则文件长度为0 if (audioInput.files.length !== 0) { that.file = audioInput.files[0]; //将文件赋值到Visualizer对象的属性上 that.fileName = that.file.name; that._start(); //获取到文件后,开始程序,这个方法会在后面定义并实现 }; }; } 上面代码中,我们假设已经写好了一个进一步处理文件的方法_start(),在获取到文件后赋值给Visualizer对象的file属性,之后在_start()方法里我们就可以通过访问this.file来得到该文件了,当然你也可以直接让_start()方法接收一个file参数,但将文件赋值到Visualizer的属性上的好处之一是我们可以在对象的任何方法中都能获取该文件 ,不用想怎么用参数传来传去。同样,将文件名赋值到Visualizer的fileName属性当中进行保存,也是为了方便之后在音乐播放过程中显示当前播放的文件。 通过拖拽获取 我们把页面中的canvas作为放置文件的目标,在它身上监听拖拽事件'dragenter','dragover','drop'等。 还是在上面已经添加好的_ addEventListner方法里,接着写三个事件监听的代码。 dropContainer.addEventListener("dragenter", function() { that._updateInfo('Drop it on the page', true); }, false); dropContainer.addEventListener("dragover", function(e) { e.stopPropagation(); e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; //设置文件放置类型为拷贝 }, false); dropContainer.addEventListener("dragleave", function() { that._updateInfo(that.info, false); }, false); dropContainer.addEventListener("drop", function(e) { e.stopPropagation(); e.preventDefault(); that.file = e.dataTransfer.files[0]; //获取文件并赋值到Visualizer对象 that.fileName = that.file.name; that._start(); }, false); 注意到上面代码中我们在'dragover'时设置文件拖放模式为'copy',既以复制的形式获取文件,如果不进行设置无法正确获取文件 然后在'drop'事件里,我们获得文件以进行一下步操作。 用FileReader读取文件为ArrayBuffer 下面来看这个_start()方法,现在文件得到 了,但首先需要将获取的文件转换为ArrayBuffer格式,才能够传递给AudioContext进行解码,所以接下来_start()方法中要干的事情就是实例化一个FileReader来将文件读取为ArrayBuffer格式。 _start: function() { //read and decode the file into audio array buffer var that = this, //当前this指代Visualizer对象,赋值给that以以便在其他地方使用 file = this.file, //从Visualizer对象上获取前面得到的文件 fr = new FileReader(); //实例化一个FileReader用于读取文件 fr.onload = function(e) { //文件读取完后调用此函数 var fileResult = e.target.result; //这是读取成功得到的结果ArrayBuffer数据 var audioContext = that.audioContext; //从Visualizer得到最开始实例化的AudioContext用来做解码ArrayBuffer audioContext.decodeAudioData(fileResult, function(buffer) { //解码成功则调用此函数,参数buffer为解码后得到的结果 that._visualize(audioContext, buffer); //调用_visualize进行下一步处理,此方法在后面定义并实现 }, function(e) { //这个是解码失败会调用的函数 console.log("!哎玛,文件解码失败:("); }); }; //将上一步获取的文件传递给FileReader从而将其读取为ArrayBuffer格式 fr.readAsArrayBuffer(file); } 注意这里我们把this赋值给了that,然后再 audioContext.decodeAudioData的回调函数中使用that来指代我们的Visualizer对象。这是因为作用域的原因。我们知道JavaScript中无法通过花括号来创建代码块级作用域,而唯一可以创建作用域的便是函数。一个函数就是一个作用域。函数内部的this指向的对象要视情况而定,就上面的代码来说,它是audioContext。所以如果想要在这个回调函数中调用Visualizer身上方法或属性,则需要通过另一个变量来传递,这里是that,我们通过将外层this(指向的是我们的Viusalizer对象)赋值给新建的局部变量that,此时that便可以传递到内层作用域中,而不会与内层作用域里面原来的this相冲突。像这样的用法在源码的其他地方也有使用,细心的你可以下载本文的源码慢慢研究。 所以,在 audioContext.decodeAudioData的回调函数里,当解码完成得到audiobuffer文件(buffer参数)后,再把audioContext和buffer传递给Visualizer的_visualize()方法进一步处理:播放音乐和绘制频谱图。当然此时_visualize()方法还没有下,下面便开始实现它。 创建Analyser分析器及播放音频 上面已经将获取的文件进行解码,得到了audio buffer数据。接下来是设置我们的AudioContext以及获取频谱能量信息的Analyser节点。向Visualizer对象添加_visualize方法,我们在这个方法里完成这些工作。 播放音频 首先将buffer赋值给audioContext。AudioContext只相当于一个容器,要让它真正丰富起来需要将实际的音乐信息传递给它的。也就是将audio buffer数据传递给它的BufferSource属性。 其实到了这里你应该有点晕了,不过没关系,看代码就会更明白一些,程序员是理解代码优于文字的一种生物。 var audioBufferSouceNode = audioContext.createBufferSource(); audioBufferSouceNode.buffer = buffer; 就这么两名,把音频文件的内容装进了AudioContext。 这时已经可以开始播放我们的音频了。 audioBufferSouceNode.start(0); 这里参数是时间,表示从这段音频的哪个时刻开始播放。 注意:在旧版本的浏览器里是使用onteOn()来进行播放的,参数一样,指开始时刻。 但此时是听不到声音的,因为还差一步,需要将audioBufferSouceNode连接到audioContext.destination,这个AudioContext的destination也就相关于speaker(扬声器)。 audioBufferSouceNode.connect(audioContext.destination); audioBufferSouceNode.start(0); 此刻就能够听到扬声器传过来动听的声音了。 _visualize: function(audioContext, buffer) { var audioBufferSouceNode = audioContext.createBufferSource(); audioBufferSouceNode.connect(audioContext.destination); audioBufferSouceNode.buffer = buffer; audioBufferSouceNode.start(0); } 创建分析器 创建获取频谱能量值的analyser节点。 var analyser = audioContext.createAnalyser(); 上面一步我们是直接将audioBufferSouceNode与audioContext.destination相连的,音频就直接输出到扬声器开始播放了,现在为了将音频在播放前截取,所以要把analyser插在audioBufferSouceNode与audioContext.destination之间。明白了这个道理,代码也就很简单了,audioBufferSouceNode连接到analyser,analyser连接destination。 audioBufferSouceNode.connect(analyser); analyser.connect(audioContext.destination); 然后再开始播放,此刻所有音频数据都会经过analyser,我们再从analyser中获取频谱的能量信息,将其画出到Canvas即可。 假设我们已经写好了画频谱图的方法_drawSpectrum(analyser); _visualize: function(audioContext, buffer) { var audioBufferSouceNode = audioContext.createBufferSource(), analyser = audioContext.createAnalyser(); //将source与分析器连接 audioBufferSouceNode.connect(analyser); //将分析器与destination连接,这样才能形成到达扬声器的通路 analyser.connect(audioContext.destination); //将上一步解码得到的buffer数据赋值给source audioBufferSouceNode.buffer = buffer; //播放 audioBufferSouceNode.start(0); //音乐响起后,把analyser传递到另一个方法开始绘制频谱图了,因为绘图需要的信息要从analyser里面获取 this._drawSpectrum(analyser); } 绘制精美的频谱图 接下来的工作,也是最后一步,也就是实现_drawSpectrum()方法,将跟随音乐而灵动的柱状频谱图画出到页面。 绘制柱状能量槽 首先你要对数字信号处理有一定了解,吓人的,不了解也没多大关系。频谱反应的是声音各频率上能量的分布,所以叫能量槽也没有硬要跟游戏联系起来的嫌疑,是将输入的信号经过傅里叶变化得到的(大学里的知识终于还是可以派得上用场了)。但特么我知道这些又怎样呢,仅仅为了装逼显摆而以。真实的频谱图是频率上连续的,不是我们看到的最终效果那样均匀分开锯齿状的。 通过下面的代码我们可以从analyser中得到此刻的音频中各频率的能量值。 var array = new Uint8Array(analyser.frequencyBinCount); analyser.getByteFrequencyData(array); 此刻array中存储了从低频0Hz到高频~Hz的所有数据。频率做为X轴,能量值做为Y轴,我们可以得到类似下面的图形。 你也可以上传文件自己看一下效果 http://sandbox.runjs.cn/show/1kn8nr4l 所以,比如array[0]=100,我们就知道在x=0处画一个高为100单位长度的长条,array[1]=50,然后在x=1画一个高为50单位长度的柱条,从此类推,如果用一个for循环遍历array将其全部画出的话,便是你看到的上图。 采样 但我们要的不是那样的效果,我们只需在所有数据中进行抽样,比如设定一个步长100,进度抽取,来画出整个频谱图中的部分柱状条。 或者先根据画面的大小,设计好每根柱条的宽度,以及他们的间隔,从而计算出画面中一共需要共多少根,再来推算出这个采样步长该取多少,本例便是这样实现的。说来还是有点晕,下面看简单的代码: var canvas = document.getElementById('canvas'), meterWidth = 10, //能量条的宽度 gap = 2, //能量条间的间距 meterNum = 800 / (10 + 2); //计算当前画布上能画多少条 var step = Math.round(array.length / meterNum); //计算从analyser中的采样步长 我们的画布即Canvas宽800px,同时我们设定柱条宽10px , 柱与柱间间隔为2px,所以得到meterNum为总共可以画的柱条数。再用数组总长度除以这个数目就得到采样的步长,即在遍历array时每隔step这么长一段我们从数组中取一个值出来画,这个值为array[i*step]。这样就均匀地取出meterNum个值,从而正确地反应了原来频谱图的形状。 var canvas = document.getElementById('canvas'), cwidth = canvas.width, cheight = canvas.height - 2, meterWidth = 10, //能量条的宽度 gap = 2, //能量条间的间距 meterNum = 800 / (10 + 2), //计算当前画布上能画多少条 ctx = canvas.getContext('2d'), array = new Uint8Array(analyser.frequencyBinCount); analyser.getByteFrequencyData(array); var step = Math.round(array.length / meterNum);计算从 analyser中的采样步长 ctx.clearRect(0, 0, cwidth, cheight); //清理画布准备画画 //定义一个渐变样式用于画图 gradient = ctx.createLinearGradient(0, 0, 0, 300); gradient.addColorStop(1, '#0f0'); gradient.addColorStop(0.5, '#ff0'); gradient.addColorStop(0, '#f00'); ctx.fillStyle = gradient; //对信源数组进行抽样遍历,画出每个频谱条 for (var i = 0; i < meterNum; i++) { var value = array[i * step]; ctx.fillRect(i * 12 /*频谱条的宽度+条间间距*/ , cheight - value + capHeight, meterWidth, cheight); } 使用requestAnimationFrame让柱条动起来 但上面绘制的仅仅是某一刻的频谱,要让整个画面动起来,我们需要不断更新画面,window.requestAnimationFrame()正好提供了更新画面得到动画效果的功能,关于requestAnimationFrame的使用及更多信息可以从我的上一篇博文<requestAnimationFrame,Web中写动画的另一种选择>中了解,这里直接给出简单改造后的代码,即得到我们要的效果了:跟随音乐而灵动的频谱柱状图。 var canvas = document.getElementById('canvas'), cwidth = canvas.width, cheight = canvas.height - 2, meterWidth = 10, //能量条的宽度 gap = 2, //能量条间的间距 meterNum = 800 / (10 + 2), //计算当前画布上能画多少条 ctx = canvas.getContext('2d'); //定义一个渐变样式用于画图 gradient = ctx.createLinearGradient(0, 0, 0, 300); gradient.addColorStop(1, '#0f0'); gradient.addColorStop(0.5, '#ff0'); gradient.addColorStop(0, '#f00'); ctx.fillStyle = gradient; var drawMeter = function() { var array = new Uint8Array(analyser.frequencyBinCount); analyser.getByteFrequencyData(array); var step = Math.round(array.length / meterNum); //计算采样步长 ctx.clearRect(0, 0, cwidth, cheight); //清理画布准备画画 for (var i = 0; i < meterNum; i++) { var value = array[i * step]; ctx.fillRect(i * 12 /*频谱条的宽度+条间间距*/ , cheight - value + capHeight, meterWidth, cheight); } requestAnimationFrame(drawMeter); } requestAnimationFrame(drawMeter); 查看效果 http://sandbox.runjs.cn/show/q1ng0jgp 绘制缓慢降落的帽头 到上面一步,主要工作已经完成。最后为了美观,再实现一下柱条上方缓慢降落的帽头。 原理也很简单,就是在绘制柱条的同时在同一X轴的位置再绘制一个短的柱条,并且其开始和结束位置都要比频谱中的柱条高。难的地方便是如何实现缓慢降落。 首先要搞清楚的一点是,我们拿一根柱条来说明问题,当此刻柱条高度高于前一时刻时,我们看到的是往上冲的一根频谱,所以这时帽头是紧贴着正文柱条的,这个好画。考虑相反的情况,当此刻高度要低于前一时刻的高度时,下方柱条是立即缩下去的,同时我们需要记住上一时刻帽头的高度位置,此刻画的时候就按照前一时刻的位置将Y-1来画。如果下一时刻频谱柱条还是没有超过帽头的位置,继续让它下降,Y-1画出帽头。 通过上面的分析,所以我们在每次画频谱的时刻,需要将此刻频谱及帽头的Y值(即垂直方向的位置)记到一个循环外的变量中,在下次绘制的时刻从这个变量中读取,将此刻的值与变量中保存的上一刻的值进行比较,然后按照上面的分析作图。 最后给出实现的代码: _drawSpectrum: function(analyser) { var canvas = document.getElementById('canvas'), cwidth = canvas.width, cheight = canvas.height - 2, meterWidth = 10, //频谱条宽度 gap = 2, //频谱条间距 capHeight = 2, capStyle = '#fff', meterNum = 800 / (10 + 2), //频谱条数量 capYPositionArray = []; //将上一画面各帽头的位置保存到这个数组 ctx = canvas.getContext('2d'), gradient = ctx.createLinearGradient(0, 0, 0, 300); gradient.addColorStop(1, '#0f0'); gradient.addColorStop(0.5, '#ff0'); gradient.addColorStop(0, '#f00'); var drawMeter = function() { var array = new Uint8Array(analyser.frequencyBinCount); analyser.getByteFrequencyData(array); var step = Math.round(array.length / meterNum); //计算采样步长 ctx.clearRect(0, 0, cwidth, cheight); for (var i = 0; i < meterNum; i++) { var value = array[i * step]; //获取当前能量值 if (capYPositionArray.length < Math.round(meterNum)) { capYPositionArray.push(value); //初始化保存帽头位置的数组,将第一个画面的数据压入其中 }; ctx.fillStyle = capStyle; //开始绘制帽头 if (value < capYPositionArray[i]) { //如果当前值小于之前值 ctx.fillRect(i * 12, cheight - (--capYPositionArray[i]), meterWidth, capHeight); //则使用前一次保存的值来绘制帽头 } else { ctx.fillRect(i * 12, cheight - value, meterWidth, capHeight); //否则使用当前值直接绘制 capYPositionArray[i] = value; }; //开始绘制频谱条 ctx.fillStyle = gradient; ctx.fillRect(i * 12, cheight - value + capHeight, meterWidth, cheight); } requestAnimationFrame(drawMeter); } requestAnimationFrame(drawMeter); } Reference: A question about how to make an audio visualizer: http://stackoverflow.com/questions/3351147/html5-audio-visualizer Web audio API: http://www.html5rocks.com/en/tutorials/webaudio/intro/ File reader in JavaScript: https://developer.mozilla.org/en-US/docs/Web/API/FileReader Local audio visualizer source code: http://cbrandolino.github.io/local-audio-visualizer/docs/local_audio_visualizer Audio context from MDN: https://developer.mozilla.org/en-US/docs/Web/API/AudioContext Window.requestAnimationFrame():https://developer.mozilla.org/en/docs/Web/API/window.requestAnimationFrame 3d visualizer with three.js : http://do.adive.in/music/ A CodePen example: http://s.codepen.io/Wayou/fullpage/auCLE? Visualizer tutorial : http://www.smartjava.org/content/exploring-html5-web-audio-visualizing-sound Web audio examples from Google code : http://chromium.googlecode.com/svn/trunk/samples/audio/index.html
HTML5/CSS3时代,我们要在web里做动画选择其实已经很多了: 你可以用CSS3的animattion+keyframes; 你也可以用css3的transition; 你还可以用通过在canvas上作图来实现动画,也可以借助jQuery动画相关的API方便地实现; 当然最原始的你还可以使用window.setTimout()或者window.setInterval()通过不断更新元素的状态位置等来实现动画,前提是画面的更新频率要达到每秒60次才能让肉眼看到流畅的动画效果。 现在又多了一种实现动画的方案,那就是还在草案当中的window.requestAnimationFrame()方法。 初识requestAnimationFrame 来看MDN上对其给出的诠释: The window.requestAnimationFrame() method tells the browser that you wish to perform an animation and requests that the browser call a specified function to update an animation before the next repaint. The method takes as an argument a callback to be invoked before the repaint. window.requestAnimationFrame() 将告知浏览器你马上要开始动画效果了,后者需要在下次动画前调用相应方法来更新画面。这个方法就是传递给window.requestAnimationFrame()的回调函数。 也可这个方法原理其实也就跟setTimeout/setInterval差不多,通过递归调用同一方法来不断更新画面以达到动起来的效果,但它优于setTimeout/setInterval的地方在于它是由浏览器专门为动画提供的API,在运行时浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了CPU开销。 基本语法 可以直接调用,也可以通过window来调用,接收一个函数作为回调,返回一个ID值,通过把这个ID值传给window.cancelAnimationFrame()可以取消该次动画。 requestAnimationFrame(callback)//callback为回调函数 一个简单的例子 模拟一个进度条动画,初始div宽度为1px,在step函数中将进度加1然后再更新到div宽度上,在进度达到100之前,一直重复这一过程。 为了演示方便加了一个运行按钮(看不到例子请刷新)。 <div id="test" style="width:1px;height:17px;background:#0f0;">0%</div> <input type="button" value="Run" id="run"/> window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; var start = null; var ele = document.getElementById("test"); var progress = 0; function step(timestamp) { progress += 1; ele.style.width = progress + "%"; ele.innerHTML=progress + "%"; if (progress < 100) { requestAnimationFrame(step); } } requestAnimationFrame(step); document.getElementById("run").addEventListener("click", function() { ele.style.width = "1px"; progress = 0; requestAnimationFrame(step); }, false); 浏览器支持情况 既然还是草案状态下引入的一个功能,在使用全我们就需要关心一下各浏览器对它的支持情况了。就目前来说,主流现代浏览器都对它提供了支持,请看下图: 31+ 26+ 10+ 19+ 6+ 更为具体的浏览器兼容性可以在这里看到。 Polyfill Polyfill就是垫片,按发明这个词的人的原话来说,它就是一段这样的代码,让浏览器原生地支持我们期望使用的一些API。 就比如这里的requestAnimationFrame,在看到了上面的浏览器支持情况后,你就知道了比上面列出的浏览器版本老的就不支持该方法,但为了让代码能够有更好的浏览器兼容性在老机器上也能运行不报错,我们可以写一些代码让浏览器在不支持requestAnimationFrame的情况下使用window.setTimeout(),这是一种回退(fallback)到过去的方法。 这样一来,就可以通俗一点的理解polyfill了,它就是备胎。 下面是由Paul Irish及其他贡献者放在GitHub Gist上的代码片段,用于在浏览器不支持requestAnimationFrame情况下的回退,回退到使用setTmeout的情况。当然,如果你确定代码是工作在现代浏览器中,下面的代码是不必的。 // http://paulirish.com/2011/requestanimationframe-for-smart-animating/ // http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating // requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel // MIT license (function() { var lastTime = 0; var vendors = ['ms', 'moz', 'webkit', 'o']; for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame']; window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] || window[vendors[x] + 'CancelRequestAnimationFrame']; } if (!window.requestAnimationFrame) window.requestAnimationFrame = function(callback, element) { var currTime = new Date().getTime(); var timeToCall = Math.max(0, 16 - (currTime - lastTime)); var id = window.setTimeout(function() { callback(currTime + timeToCall); }, timeToCall); lastTime = currTime + timeToCall; return id; }; if (!window.cancelAnimationFrame) window.cancelAnimationFrame = function(id) { clearTimeout(id); }; }()); 上面代码作用有二,一是把各浏览器前缀进行统一,二是在浏览器没有requestAnimationFrame方法时将其指向setTimeout方法。 提到备胎代码呢,这里多说一句,在CSS代码中,我们也经常使用这种回退的技巧,即对同一条CSS规则,编写多条以不同浏览器前缀开头代码,或者编写一条备用样式。 下面是一个CSS中的备胎代码的例子: div { background: rgb(0, 0, 0); /* fallback */ background: rgba(0, 0, 0, 0.5); } 代码中设置div背景为黑色带50%的透明度,但IE9-的浏览器是不支持rbga格式的颜色的,所以浏览器会回退到上一条CSS规则应用rgb颜色。 Reference: 1. article about rAF from css tricks: http://css-tricks.com/using-requestanimationframe/ 2. article about rAF from Paul Irish:http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/ 3. what is polyfill http://remysharp.com/2010/10/08/what-is-a-polyfill/
Windows上帝模式(Windows Master Control Panel)由来已久,最早是从Win7优化大湿里看到的一个选项,开启后在桌面生成一个图标,点进去后里面包含了几乎全部Windows设置选项。 直到今天刷G+(view on google plus)发现另一种方式创建这个上帝模式,挻有意思的不是嘛。 在桌面新建一个文件夹 按F2重命名为GodMode.{ED7BA470-8E54-465E-825C-99712043E01C} DONE!
Overview: 构造基本的HTML页面 动态加载样式表 Viewport 字体缩放 侧边栏 导航菜单 图片自适应 其他 总结 说到响应式网页设计(Responsive web design),最近在谷歌加上碰到个奇葩贴子,通过一个原始到无法再简单的网页Motherfucking Website及满屏幕的fuck道出了网页设计的真谛,这孩子不是个激进分子就是个报复社会型的货没错,虽然整篇文章就像是泼妇骂街,但我特么是笑着读完的。。 统计了下全文共用Fuck (包括fucking) 33次,shit (包括shitty)16次,Motherfucker 8次,创下我所阅读的技术类文章里面脏话之最。 文章表达的中心思想就是最后的那句引用"最好的设计是尽量少的设计"。最重要的是我意识到平时我们都忽略了一个常识:一张未经加工的原始HTML文档就已经是响应式的了,根本不用什么CSS media属性或者指定任何样式。 通过查看HTML代码发现作者果然留下了一些信息,于是在twitter上找到他表达了我对他的膜拜之情以及想把如此精华的文章翻译成中文的意愿。作者很爽快地答应了23333~~(X___X)~~。 于是就有了同样奇葩的中文版本:妈逼的网站,原文的精髓可能由于我自身对这类表达的驾驭的不够而丢失了一些,但多少还是能够方便嫌英文阅读麻烦的同学们围观的了。 当然以上全是扯淡,一如作者所指出的,相当讽刺。 回到正题,各种屏幕尺寸满天飞的时代如何让网站自适应的究极解决方案:响应式设计(Responsive Design)。 构造基本的HTML页面 一个简单的博客页面 始终觉得再多口水都没有一个生动鲜明的例子来得实在,下面通过对一个普通HTML页面的改造来体验什么是响应式设计及如何达到。 下面构造一个基本的HTML页面,它包含网站导航菜单,正文,图片,侧边栏,表格式的布局以及页脚信息。是个非常完整而中庸的布局,几乎是常见的博客版面。 <html> <head> <title> Responsive Design Example </title> <meta http-equiv="content-type" content="text/html;charset=utf-8" /> <link rel="stylesheet" type="text/css" href="style.css"> </head> <body> <div id="main"> <nav> <ul> <li> <a href="#">Home</a> </li> <li> <a href="#">Articles</a> </li> <li> <a href="#">Gallery</a> </li> <li> <a href="#">Forum</a> </li> <li> <a href="#">About</a> </li> </ul> </nav> <aside> <ul> <li><a href="#subtitle1">item1</a> </li> <li><a href="#subtitle2">item2</a></li> <li><a href="#subtitle3">item3</a></li> <li><a href="#subtitle4">item4</a></li> <li><a href="#subtitle5">item5</a></li> </ul> </aside> <section class="post"> <article> <h1> Sample Title </h1> <p></p> <section class="grid"> <div class="item"> #1 </div> <div class="item"> #2 </div> <div class="item"> #3 </div> </section> <p></p> </article> </section> <footer> <hr> <ul> <li><small>Wayou &copy 2013|</small></li> <li><small><a href="mailto:sample@somesite.com">Contact</a></small> </li> </ul> </footer> </div> </body> </html> 文章内容填充 剩下文章部分需要填充点内容,正好MS Word有这样一个产生随机文章的彩蛋。使用方法是新建一个word文件然后打开输入" =rand(3,10) " 再回车。其中rand 函数接收两个参数,第一个表示要产生多少个自然段,第二个表示每段多少行。所以上面回车后我们会得到一篇由3个自然段组成的文章且每段有10行。 然后再另存为网页文件: 最后可以在浏览器中通过查看源码把包含内容的<p>标签复制到我们的代码中即可。 同时这里有一个专门产生填充内容的网站Fillerati。可以定义篇幅,作者信息,标题等。 当然以上两种作法多少有点装逼与做作的感觉,你完全可以随便复制点什么东西来作为内容填充的 一_一|||。 填充内容后HTML变成这样 <section class="post"> <article> <h1> Sample Title </h1> <p> Video provides a powerful way to help you prove your point. When you click Online Video, you can paste in the embed code for the video you want to add. You can also type a keyword to search online for the video that best fits your document. To make your document look professionally produced, Word provides header, footer, cover page, and text box designs that complement each other. For example, you can add a matching cover page, header, and sidebar. Click Insert and then choose the elements you want from the different galleries. Themes and styles also help keep your document coordinated. When you click Design and choose a new Theme, the pictures, charts, and SmartArt graphics change to match your new theme. When you apply styles, your headings change to match the new theme. Save time in Word with new buttons that show up where you need them. </p> <section class="grid"> <div class="item"> #1 </div> <div class="item"> #2 </div> <div class="item"> #3 </div> </section> <p> To change the way a picture fits in your document, click it and a button for layout options appears next to it. When you work on a table, click where you want to add a row or a column, and then click the plus sign. Reading is easier, too, in the new Reading view. You can collapse parts of the document and focus on the text you want. If you need to stop reading before you reach the end, Word remembers where you left off - even on another device. Video provides a powerful way to help you prove your point. When you click Online Video, you can paste in the embed code for the video you want to add. You can also type a keyword to search online for the video that best fits your document. To make your document look professionally produced, Word provides header, footer, cover page, and text box designs that complement each other. For example, you can add a matching cover page, header, and sidebar. </p> <img class="illustration" src="beauty.png" title="sample pic" alt="beauty" /> <p> Click Insert and then choose the elements you want from the different galleries. Themes and styles also help keep your document coordinated. When you click Design and choose a new Theme, the pictures, charts, and SmartArt graphics change to match your new theme. When you apply styles, your headings change to match the new theme. Save time in Word with new buttons that show up where you need them. To change the way a picture fits in your document, click it and a button for layout options appears next to it. When you work on a table, click where you want to add a row or a column, and then click the plus sign. Reading is easier, too, in the new Reading view. You can collapse parts of the document and focus on the text you want. If you need to stop reading before you reach the end, Word remembers where you left off - even on another device. </p> </article> </section> 最后出来的效果看起来是这样的: 最后为了让侧边栏更有意义一点,给文章正文加上一些子标题同时给侧边栏里的元素加上锚点连接可以在文章的子标题间进行导航。 <aside> <ul> <li> <a href="#subtitle1">item1</a> </li> <li> <a href="#subtitle2">item2</a> </li> <li> <a href="#subtitle3">item3</a> </li> <li> <a href="#subtitle4">item4</a> </li> <li> <a href="#subtitle5">item5</a> </li> </ul> </aside> <section class="post"> <article> <h1> Sample Title </h1> <p id="subtitle1"> <strong> subtitle1 </strong> </p> <p> //正文被省略 </p> <p id="subtitle2"> <strong> subtitle2 </strong> </p> <section class="grid"> <div class="item"> #1 </div> <div class="item"> #2 </div> <div class="item"> #3 </div> </section> <p> <p id="subtitle3"> <strong> subtitle3 </strong> </p> //正文被省略 </p> <p id="subtitle4"> <strong> subtitle4 </strong> </p> <img class="illustration" src="beauty.png" title="sample pic" alt="beauty" /> <p id="subtitle5"> <strong> subtitle5 </strong> </p> <p> //正文被省略 </p> </article> </section> 基本的样式 最后加上一些样式让整个页面看起来更正常些。 我们首先去掉body元素的默认外边距,去掉列表元素前面默认的加点,把菜单里的超连接的下划线也去掉。 body { margin: 0; } li { list-style: none; } /*navigation bar*/ nav { background-color: #333; } nav li { display: inline-block; padding-right: 10px; } nav li a { text-decoration: none; color: white; font-size: 1.5em; } nav li a:hover { color: #DDD; } 再修饰下字体及正文中的三个方块div以及其他,最后的样式代码差不多是这样的: html { font-family: "microsoft yahei",arial } body { margin: 0; } li { list-style: none; } /*navigation bar*/ nav { background-color: #333; } nav li { display: inline-block; padding-right: 10px; } nav li a { text-decoration: none; color: white; font-size: 1.5em; } nav li a:hover { color: #DDD; } /*sidebar*/ aside { width: 15%; float: left; } /*post*/ .post { width: 70%; margin: 0 auto; float: left; } /*grid layout*/ .grid { } .grid .item { width: 25%; height: 150px; background-color: #DDD; display: inline-block; } /*footer*/ footer { width: 100%; text-align: center; clear: both; } footer li { display: inline-block; } 其中,因为侧边栏和文章向左浮动了,为了让页脚不从最底跳到文章的后面跑到顶部去,要清除页脚footer两边的浮动。 footer{ width: 100%; text-align: center; clear:both; } 最后页面看起来着不多是这个样子的 动态加载样式表 接下来的工作是让页面成为响应式的。听起来觉得是一个全新的领域,但其实平时我们已经在实践了。比如当指定元素的尺寸时,使用百分比而不是固定像素的大小时,这样的元素就具备自适应屏幕的能力。最常见的就是指定元素宽度为100%。这样窗口缩放或屏幕不同时元素始终占据屏幕整个宽度。 一些不太实用的实践是针对不同屏幕尺寸加载不同的样式表,这其实相当于为不同尺寸写不同的样式表,感觉维护起来不那么方便。 <!-- CSS media query on a link element --> <link rel="stylesheet" media="(max-width: 800px)" href="example.css" /> 代码来自MDN 通过在引入样式表时使用media属性可以控制什么尺寸的屏幕使用哪个样式表,于是我们可以实现手机访问时下载手机版样式,电脑访问时下载正常样式。 <link rel="stylesheet" media="screen and (max-device-width: 320px)" href="mobile.css"/> 上面代码指定如果设备宽度小于320px则调用 "mobile.css"样式表。 个人觉得这样为一个站点写多个分别的样式表不怎么好,所以这里就不多说了。 Viewport 响应式设计第一件需要做的事情就是在head标签里指定viewport meta属性。 《Quick Tip: Don't Forget the Viewport Meta Tag》这篇文章很好介绍了Viewport是的缘由及作用。 简单说来在手机(iPhone Safari)上访问网页时它默认会对网页进行缩放,尽可能多地在屏幕上展示整个页面的内容。而缩放之后的效果可想而知,一个在电脑上正常展示的页面被缩放进手机屏幕(通常是240*320)里面后,很难阅读。 同时由于默认使用缩放,那么你事先设计好的在小屏幕上使用的样式将不起作用,也就是说手机上展示的是电脑版本的一个缩小版。 我们看MDN上给出的例子截图。 而在代码中指定viewport,则可以让开发者指定网页视图区域及缩放比例等。这样就能修正由浏览器自动缩放带来的影响。 通过我们指定如下代码: <meta name="viewport" content="width=device-width, initial-scale=1"> 表示使用设备宽度(即设备的屏幕宽度)并且缩放指定为1也就是不缩放。 你可能会问这样指定之后岂不是只能在手机屏幕上显示网站的部分,比如左上角。这时候正是响应式网页设计起作用的时候了。如果你专门为小屏幕的访问进行了优化比如在CSS中使用media属性(后面会讲到),那么当手机访问时会调用相应的样式规则,而不会只显示整个网站的一部分。 字体缩放 指定固定像素的字体大小是我们设计中经常使用的方式,但如果你想字体大小更具弹性的话,最好还是使用相对大小,CSS中比较常用的指定字体相对大小的单位有百分比,em以及CSS3新增的rem。 首先我们指定整个文档的字体大小为100%。表示页面字体大小为浏览器默认大小的100%。 html { font-family: "microsoft yahei",arial; font-size: 100%; } 再来看看em与rem。em单位一如他的发音它的基准单位是一个m字母的高度,同时它是指定相对于父级元素的相对大小。也就是说指定为em的元素字体大小是通过对上一层元素的字体大小计算得来的。 <div style="font-size:15px;"> <p style="font-size:2em;"> Hello! </p> </div> 上面外层div字体大小为15px,同时指定内层p元素字体大小为2em,所以p元素实际的字体大小为15px*2=30px。这点可以通过查看浏览器开发工具里面"计算后的样式"得到证实。 但需要注意的是em有个问题,正因为他会相对于低级元素来计算自己的样式,所以在层叠很多的情况下,可能出现意料之外的结果。 <div style="font-size:15px;"> <div style="font-size:2em;"> <p style="font-size:2em;"> Hello! </p> </div> </div> 比如我们期望后面的包含在最外层div中的内容字体大小统一为2em,于是分别在内层div和p上都指定了这一样式,结果p元素的字体大小其实是乘以了两次之后的结果 15px*2*2=60px。 为了解决这个问题,于是引入了一个新的单位rem。可以理解为root-em。加了个root前缀表示总是相对于根节点来计算。HTML文档的根节点当然就是<HTML>标签了。所以通过rem无论在文档任何位置指定都可以放心地得到预期的大小。 <div style="font-size:15px;"> <div style="font-size:2em;"> <p style="font-size:2rem;"> Hello! </p> </div> </div> 如果没有指定HTML根节点的字体大小,默认为16px,所以这里得到32px。 但rem 不太普适,因为浏览器对它的支持力度还不够,当然如果不考虑太多嵌套情况下em就够用了。所以我习惯在CSS中使用em来指定字体大小。 侧边栏 当缩放浏览器窗口到足够窄(这里是小于560px)时我们可以发现侧边栏与博客文章有重叠,此刻这个窗口宽度就是我们需要写样式来干预的时候了。 利用CSS中的media query我们指定当窗口小于630px时将侧边栏隐藏,而让正文占据整个屏幕宽度也就是设置为100%,并且取消正文的浮动,因为没有必要了。同时上图我们可以看到此时的菜单并没有受到影响所以暂时可以不管。 @media only screen and (max-width : 650px) { aside { display: none; } .post { width: 100%; float: none; padding: 5px; box-sizing: border-box; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; } } 另外为了不让subtitle2部分的格子被压得太小而影响其中的内容(当然现在其中并没有什么内容),所以此刻我们让这一部分同样占100%的宽度,其中每个方块占32%的宽度。 @media only screen and (max-width : 650px) { aside { display: none; } .post { width: 100%; float: none; padding: 5px; box-sizing: border-box; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; } .grid { width: 100%; } .grid .item { width: 32%; } } 导航菜单 菜单几乎是体现响应式设计最直接的一个东西了。判断一个网站是不是响应式的最好方法就是改变浏览器窗口的大小,观察网站的布局,特别是菜单,在窗口缩小到足够窄的情况下一个经典的设计是隐藏原来的菜单而只展示由三根横线组成的图标。 截图站点http://residence-mixte.com/ 由此我们可以看到一些实现上的端倪,除了常规菜单外,我们需要在HTML代码中事先摆放好这样一个横线图标元素。 所以改动我们的nav部分的HTML代码为下面这样: <nav> <ul> <a href="#" id="menuIcon">Ξ</a> <li> <a href="#">Home</a> </li> <li> <a href="#">Articles</a> </li> <li> <a href="#">Gallery</a> </li> <li> <a href="#">Forum</a> </li> <li> <a href="#">About</a> </li> </ul> </nav> 同时补充上样式: nav li a:hover,#menuIcon:hover { color: #DDD; } #menuIcon { display: none; color: white; font-weight: bold; font-size: 2em; text-decoration: none; font-family: arial; } 此刻页面倒并没有明显变化,因为这个图标开始是不显示的。 当窗口宽度小于大约490px时,我们的菜单最后一项被挤到了下面一排,所以将500px这个宽度作为分水岭写media query代码。 当屏幕宽度小于500px时,<nav> 里的菜单<ul>标签不显示,同时显示id为menuIcon的菜单图标<a>标签。 @media screen and (max-width: 500px) { nav ul { display: none; padding: 0; margin: 0 5px; } #menuIcon { display: block; text-align: right; padding: 0 5px; border-bottom: 1px #9c9c9c solid; } } 这时候我们需要一点javascript代码来实现点击三根横线显示出来刚才被我们隐藏的菜单,同时再次单击或者选中一个菜单项后重新隐藏菜单。 <script type="text/javascript"> $(function() { $("#menuIcon,nav ul li").click(function() { if ($("#menuIcon").is(":visible")) { //防止宽屏上点击 $("nav ul").toggle(300); }; }); }) </script> 但这个时候菜单不够完美,最后加上一点样式美化下。 @media screen and (max-width: 500px) { nav ul { display: none; padding: 0; margin: 0 5px; } #menuIcon { display: block; text-align: right; padding: 0 5px; border-bottom: 1px #9c9c9c solid; } nav ul li { width: 100%; } nav ul li:hover { background-color: #555; } } 目前来说工作得还算满意,但稍微测试下就会发现个问题:当菜单被点开后又关闭,再将窗口拉宽到足够宽时,正常模式的菜单不显示了。 这里给出个不太正规的解决方法,再写点代码监听窗口的resize事件,当窗口大于我们设定的500px时移除我们通过jQuery的toggle函数给菜单<ul> 标签加上的样式"style="display:none;",另外也可以通过监听那个三横的菜单图标是否可见也可以达到目的。 $(window).resize(function() { if (!$("nav ul").is(":visible")) { $('nav ul').attr('style', function(i, style){ return style.replace(/display[^;]+;?/g, ''); }); }; }); 图片自适应 普通的图片是不会自适应屏幕大小的,也就是说图片太宽的话在手持设备等屏幕较小的情况下会有水平滚动条出现。 最简单的办法让它随屏幕大小自动缩放就是指定其最大宽度为100%像这样: img { max-width:100%; } 其他 再来处理窗口足够小的时候那个表格式布局的三个方框。 可以看到窗口很窄的时候这三个div被挤压得很厉害,所以假设窗口小于420px时,我们让它们各自独立一行,占满整个窗口宽度。 @media screen and (max-width: 420px) { .grid .item { width: 100%; margin-bottom: 5px; } } 总结 本文中很多例子不是很恬当,仅用于演示教学,请轻喷。比如将菜单变成图标显示时上面例子是在500px为分水岭写的media queries,但我们知道500px其实还是很宽的,是足以容下一般长度的菜单正常显示的,只是在一般手机屏幕宽度480px 或320px,所以针对这个宽度来写media query更具实际意义。还有就是侧边栏,上面例子中使用的百分比宽度,其实侧边栏可以给个固定宽度,并且上面例子中没有考虑到侧边栏里的文字很长的情况。没有考虑到老版本IE不支持media query的情况。这些是例子中不足的地方。但作为演示还是达到了我讲解的目的。 示例代码:ResiponsiveDesign.zip 参考与扩展阅读: 1.Responsive Web Design Guidelines and Tutorials 2.一个响应式页面测试站点 3.Using the viewport meta tag to control layout on mobile browsers 4.Responsive Web Design: What It Is and How To Use It 5.viewport 相关 6.Responsive Design with CSS3 Media Queries 的系列文章 7.How To Create A Responsive Navigation 8.Adventures in Responsive Navigation
本文只是讨论和实现了动画效果,并未将动画与页面实际下载关联,有朋友们问如何应用,可以使用现成的一些插件比如这个,这个,还有这个。 之前一篇文章《CSS3 动画一瞥》简单介绍了CSS3动画相关的内容,这里继续讲一个例子。 前些时候有注意到YouTube网站放出的新特性,那就是在视频页面间切换时,页面顶部会有一道华丽丽的红色激光脉冲掠过。 那不是其他什么为了炫技的东西,它是一个进度条,平时我们见了千百遍经常以转圈形式出现的进度条。当然谷歌对于进度展示的创新不止于此啦,比如用于谷歌加的下载gif也是很有新意的一个玩意儿。 但YouTube这个进度条似乎更具创新,让人觉得相当惊艳。 所以好奇的我按捺不住想要拔开它神秘面纱的心情。 大体来看用了以下技术: Ajax:首先页面内容的加载使用的是Ajax异步请求,所以页头那个激光元素可以在内容请求与接收过程中得以展示,不然的话整个页面刷新那就无法实现了; HTML5 History API: 其次我注意到页面地址也是跟着变的。前面提到整个页面是没有刷新的(一个不太靠谱的方法可以验证这点是显示网站favicon的地方没有出现类似这样的等待图标),但页面地址却更新了,方便你把连接放送给别人时能够打开该页面。这里用到的就是HTML5的History API,通过它可以操作浏览器地址栏的地址,书签及页面状态信息等。 CSS3动画属性 :另外就是红色激光线条本身的实现上,使用了CSS3的动画或者JS写的动画,但更大的可能是两种结合。 经过一些谷歌,发现也有其他同类在讨论这个话题,并贴出了相关实现。拿来研究了下决定自己把玩一把分享给大家。 这里只是实现那个一道红光掠过的效果,不包含对Http请求各个状态的进度处理以得到页面实际的加载进度,我们将把这个动画效果写成在一个固定的时间内完成,比如3秒。 准备工作 开始之前需要多少了解一点CSS3关于动画相关的知识,可以通过我之前那篇博文,也可以到W3School进行了解。 其次,需要了解诸如 CSS3的transition等不常用的属性。 最后还需要了解jQuery的animate API的使用。 分解实现 整个动画可以分为两个部分,一个是整体向前延伸的光线,另一个就是跑在最前面不停闪烁的头部。 向前滑动的激光 首先来看如何实现一条向前延伸的光线效果的。 其实要实现这么一个效果使用CSS3的动画属性来做是非常简单的,但为什么要使用jQuery来做呢,看完下面后答案就揭晓了。 先看纯CSS3的版本。 1.新建一个html文件然后加入一个div,用来呈现这个向前的动画。 2.然后开始给id为progress的div写动画。 考虑到简略,一些CSS属性为了能够在不同浏览器里正常工作需要写很多个版本,比如CSS3的animatiion正常情况下需要为每个不同内容的浏览器写个版本的: 但我是在Chromium里做实验,遇到这种情况就只考虑-webkit-前缀的了,在完整源码里再把兼容其他浏览器的代码补全。 设置目标元素背景色为深红色(#b91f1f),高度为2px。因为这两个属性是在动画过程中不变的,所以单独写出来。同时定义动画关键帧:开始宽度为0,结束时宽度为100%。 然后对元素应用动画,设定动画时间为3秒: 现在可以保存页面看效果了。 var iframe = document.getElementById('demo1'); iframe.src = iframe.src; 查看效果 //DEMO1 我们会看到一条红色线条向右飞去。但它没有贴在页面的边缘,所以还需要将body的margin去掉。所以现在的代码应该是这样的。 效果是这样的: 但问题出现了。当动画放完后线条会一直存在,不会消失。但实际上进度完成了进度条就应该从页面消失了。所以我们改为使用jQuery来实现,这样可以在动画完成后通过JavaScript将其隐藏。 更改为上面的代码后,进度条播放完后会消失。 var iframe = document.getElementById('demo2'); iframe.src = iframe.src; 查看效果 //DEMO2 现在线条消失时太突兀了,我们需要让它渐渐消失掉,需要用到CSS的transation属性。 var iframe = document.getElementById('demo3'); iframe.src = iframe.src; 查看效果 //DEMO3 光晕与闪烁效果 我们可以看到在那束激光划过时,其头部是块闪烁且周围带光晕效果的长条,所以剩下的部分就是完成这个东西了。 首先我们看闪烁如何做。 新建一个html文档,页面也是很简单就一个div用于展示动画。并且设置其样式为带阴影效果和圆角效果,圆角是为了看起来柔和一点。 //DEMO4 效果: 然后再为其编写动画效果,这个动画效果是让它闪烁,可能通过改变其透明度来控制,然后将动画设置成无限播放模式,就出来想要的闪烁了。 //DEMO5 现在我们把这个效果加到原来那个线条上。在原来那个id为progress的div下加一个span元素用于呈现这个闪烁效果。 它必需一起处于线条的最右边,所以考虑将其位置属性设置为absolute并且将progress 那个div设为fixed。 所以最后的效果及代码大概是这样: var iframe = document.getElementById('demo6'); iframe.src = iframe.src; 查看效果 //DEMO6 例子代码:下载 Reference: http://www.youtube.com/watch?v=dN3xwItBKDA https://www.facebook.com/photo.php?fbid=217862301702396 http://jsfiddle.net/ajaSB/3/light/ Animate.css https://daneden.me/animate/
伴随HTML5而来的CSS3让前端大湿们可以用简单的CSS样式即可写出动画效果来,而在这之前,一提到动画我们可能会想到JavaScript,Flash,Java插件等。如果是用JavaScript那倒也不是很糟糕的事啦,但如果写出来的效果非要强迫客户端安装第三方插件才能显示,毕竟不是很理想。这也就是为什么谷歌会不遗余力地推广他所主导的开源项目WebRTC (Web Real-Time Communication),把实时通讯的功能都做进浏览器,像视频通话这样的高级应用直接在JavaScript里调用几个浏览器API即可实现!这在以前想都不敢想。 再加上HTML5将很多之前需要依赖外部程序或者需要程序员们写大量JS来实现的东西标准化了,一个目的就是丰富Web设计,彻底丢弃第三方插件,让浏览器干干净净。 扯远了,回到动画。 定义动画 在CSS3中定义动画是件很方便的事情。原理有点像使用Adobe公司的Flash软件来制作动画。 我还记得那时我在把玩Flash时所学习到的简单Flash动画。比如定义好一个物体的开始位置及状态,0秒的时候一个红色50X50的矩形处于画面中央,再将画面定位于时间轴上3秒处,将矩形设为100X100黄色。再右键添加补间动画。这样一个简单的动画便完成了。 下面就是这么一个动画的CSS实现。 var demoContent=' 像这样关于位移,颜色渐变,甚至旋转,3D效果等的动画,现在都可以用CSS来做了。 CSS中的@符号 首先我们来看一下CSS中的@符号。 当我首先看到这个东西的时候,完全搞不懂是什么意思。于是开始尝试去一探究竟。 请考虑这样一种情形,你想在Web页面使用设计师使用的一种字体,因为设计是那帮不懂Coding的平面设计师搞出来的,老板看了觉得还不错,剩下实现的问题就交给你了。因为这种字体不是很通用,所以用户电脑上有很大可能是没有装这一字体的,那就意味首页面在用户电脑上的呈现会不一致,页面找不到指定的字体会调用系统默认的字体。 比如下面我们在页面使用Adelle_Reg.otf字体。 通过打开查看可以得到Font name,然后基本我们会通过一句简单的CSS来搞定: 但由于我系统里并没有'Adelle Rg' 这样的字体,所以页面会是这样的 所以我们考虑把字体文件包含到CSS里去,换句话说把字体文件发送到客户端。于是实现要改,这时使用@font-face 来指定字体文件的路径,这个时候我们初次看到引入了一个@符号。 所以改过之后的代码如上图。 先通过@font-face定义了一个取名叫'customFont'的font face,会在后面使用到 然后再通过给需要的页面元素指定font-family 为刚才定义的customFont' 字体已经应用上且我们能够在Resource里面发现字体文件已经发送到了客户端浏览器。 从上面我们大致可以这样理解此种情况下的@符号,虽然不太正确(比如@import, @media),通过它定义了一个特殊场合下的变量,这里是定义字体,在动画里是定义动画关键帧,然后我们会在CSS代码的其他地方使用这个定义好的变量。 关键帧 什么是关键帧。一如上面对Flash原理的描述一样,我们知道动画其实由许多静态画面组成,第一个这样的静态画面可以表述为一帧。其中关键帧是在动画过程中体现了物理明显变化的那些帧。 比如之前的例子中,元素div由50X50红色的大小变化到状态100X100 黄色的过程中,这一头一尾的两个状态起到了对动画定义的关键作用。所以这两个状态就是整个动画的关键帧。 @keyframes 定义动画关键帧 通过之前的胡说现在我们看到@keyframes就不会觉得这个@符号有多别扭了。我们使用它来定义动画的关键帧。 CSS代码中定义关键帧重要的两点是名称和时间点。 其中状态部分指定元素的样式,因此可以是各种你想要的CSS代码,颜色尺寸透明度旋转等。'from'指定了动画过程的开始状态,'to'指定了动画结束时元素的状态。所以整个动画也就是从from指定的开始状态变化到to指定的状态的过程。 假使我们已经创建好了一个HTML文档,其结构很简单只有一个用于呈现动画的div。 所以对于上面的例子,动画的定义大概是下面这个样子的: 定义的动画取名'example' 在'from'也就是动画开始时指定元素长50px宽50px,背景色为红色 在'to'也就是动画结束时指定元素长100px宽100px,背景色为黄色 对于使用webkit内核的浏览器比如Chrome,Safari需要使用-webkit-前缀,所以需要写两套代码,以保证在Chrome或Safari里能工作 当然对于状态的定义不局限于开始和结束两个时间点,我们可以指定一个动画过程中任何时间点元素的状态。下面是定义关键帧的另一种写法。 上面定义了整个动画过程中0%,50%,100%三个时间点元素的状态。比如我们定义了一个时长为10秒的动画,那么0%就是动画开始时0秒的时候,0%后面的代码指定元素在动画开始时是怎样的,然后50%也就是动画进行到5秒的时候,元素又是什么样子。最后100% 对于动画进行到10秒也就是动画结束时元素的状态。 因此用这种写法我们可以指定的元素状态数量没有限制,可以更精确地控制整个动画。 CSS3 animation属性 当我们使用@keyframes定义好了一个动画,它并不会执行产生任何效果,直到我们通过animation属性将动画应用到相应元素上。 对于 CSS3 animation 属性其完整的语法如下: animation: name duration timing-function delay iteration-count direction; name是使用@keyframes定义好的关键帧名称 duration从字面意思可知是指定动画持续时间 timing-function 指定动画以何种方式播放,具体指的是从元素的一个状态过渡到另一个状态所使用的方式,可用的值有linear,ease, ease-in, ease-out, ease-in-out, cubic-bezier(n,n,n,n)。每种方式的讨论超出了原计划,这里只是列出 delay指定一个延时让动画不立即播放 iteration-count 指定动画重复次数,可以指定一个数字,也可以使用'infinite'表示一直播放 direction指定动画是否反向播放或者交替着播放,可用的值有normal, reverse, alternate, alternate-reverse 其中name和duration 是必需的,如果不指定duration默认为0,也就是动画持续0秒,所以就无法看到动画效果。 在前面已经定义好了关键帧了,现在我们使用animation将其应用到相应元素上。 现在打开页面就会看到最上面那个动画效果了。 使用百分比指定关键帧的版本 上面介绍过通过百分比的形象我们可以指定动画过程中任何时间点时元素的状态,将上面的版本变为百分比版本是非常容易的事情。 我们只需把关键帧的定义由from to 改为想要的时间百分比即可。 比如开始的状态不改变,增加一个动画进行到50%时颜色为黄色大小为75X75,最后为绿色大小为100X100。 效果: 往复的动画 如果我们指定了direction为alternate的话,当动画播放到结尾时,它会以相反的方向回到动画开始的状态,然后一直这样交替播放。 有了上面的基本了解,我们可以写一些简单的动画了。但真正惊赞的CSS3动画是需要花一些功夫的,这里就不继续了,或许我会在下一篇中介绍一个例子。 Another working demo (请使用Chrome浏览器观看效果): Reference: http://nettuts.s3.amazonaws.com/581_cssTransitions/demos.html http://www.w3schools.com/css/css3_animations.asp
Chromium 其实就是开发版本的Chrome, 即Chrome dev 版本。一般他的版本要比正式版的Chrome高两个及以上。比如正式版本现在是29,开发者版本已经是32了。 这表示很多新功能你可以在Chromium中提前体验,这使得像我这种假装极客的程序员爱不释手。 有的人觉得用这个版本纯粹找虐,没事闲的。但我主要是喜欢体验新鲜感那种跟正式版本不一样的风格,那素雅的图标多么让人有一种与众不同与装逼的感觉。 用Chromium唯一不足的地方是没有自动更新。而我们大家都知道它是版本帝,一天都要更新好见个小的版本。只要你不时地打开它的文件页面看看,就能感觉到时时刻刻都有开发人员在提交代码。 所以如果想要用上最新版的Chromium,还得隔三差五地去手动下载安装,之前我一直是这样干的。。。 1. Chromium Auto Updater 直到后来发现有人写了个程序来自动更新Chromium。这下方便了。 下面是下载地址: Chromium Auto Updater 1.7 如果连接失效请点页面的网盘连接: http://pan.baidu.com/s/1d0GmD 2. Chromium Updater (谷歌扩展程序) 另外一个选择就是Chromium Updater 这么一个谷歌扩展程序,点击后给列出当前版本及最新发放出的版本,然后选择适合版本进行安装,整个过程是在后台静默进行的,过程中也不需要重启Chromium。
很多时候我们需要打开命令行然后进入到相应目录进行一些操作。 常规的做法是: Win+R打开运行窗口 输入"cmd"回车打开命令行窗口 假如我们要进入的是D盘foo文件夹下的一个bar子文件夹,路径是这样的D:\foo\bar,首先输入" D:"回车进入D盘 再依次输入"cd foo"," cd bar"; 或者在资源管理器的地址栏里复制文件夹地址"D:\foo\bar", 然后输入cd 再把复制的地址一次性粘贴到cd 后面(适用于文件夹路径较长时,避免一个一个地输入) 如果需要进行频繁命令行操作,每次都要通过这样的方式来进行,势必很麻烦。 按住Shift键右击鼠标打开命令行窗口 其实Windows有个不显眼的功能是这样的,同样还是以定位到D盘foo文件夹下的一个bar子文件夹为例,在bar文件夹里,将鼠标置于空白处,按住Shift键不放,同时右击鼠标,这时在出来的右键菜单里会出现一个"打开命令行" 的菜单选项。 此刻打开后的命令行窗口的路径已经定位到了刚才的目录,即从哪里打开的,命令行的执行路径则被自动定位到了哪里。 以管理员身份在当前目录打开命令行窗口 上面的方法虽然比原始的方法方便了许多,但有些时候我们在命令行里的操作需要管理员身份,这时就要求命令行窗口是以管理员身份打开的,而上面的方法打开的是普通的命令行窗口,在此时就无法满足要求了。 同样地,我们也可以通过传统的方法打开一个以管理员身份运行的命令行窗口,方法就是在开始菜单里找到命令行窗口或者直接在C:\Windows\System32 找到cmd.exe右键选择"以管理员身份运行",然后再手动定位到需要的文件夹。 这里要介绍如何在右键菜单里添加一个菜单选项让我们可以在一个文件夹里直接右击鼠标便可以管理员身份打开一个命令行窗口,那样的话将会让工作变得非常轻松。 将以下代码复制到一个文本文件,然后保存成 cmd.reg,注意文件后缀是reg,注册表文件。 Windows Registry Editor Version 5.00 ; Created by: Shawn Brink ; http://www.sevenforums.com ; Tutorial: http://www.sevenforums.com/tutorials/47415-open-command-window-here-administrator.html [-HKEY_CLASSES_ROOT\Directory\shell\runas] [HKEY_CLASSES_ROOT\Directory\shell\runas] @="Open cmd here as Admin" "HasLUAShield"="" [HKEY_CLASSES_ROOT\Directory\shell\runas\command] @="cmd.exe /s /k pushd \"%V\"" [-HKEY_CLASSES_ROOT\Directory\Background\shell\runas] [HKEY_CLASSES_ROOT\Directory\Background\shell\runas] @="Open cmd here as Admin" "HasLUAShield"="" [HKEY_CLASSES_ROOT\Directory\Background\shell\runas\command] @="cmd.exe /s /k pushd \"%V\"" [-HKEY_CLASSES_ROOT\Drive\shell\runas] [HKEY_CLASSES_ROOT\Drive\shell\runas] @="Open cmd here as Admin" "HasLUAShield"="" [HKEY_CLASSES_ROOT\Drive\shell\runas\command] @="cmd.exe /s /k pushd \"%V\"" 然后双击运行,弹出确定对话框,点击确定,再右键一看,菜单里已经多出一个以管理员身份打开命令行窗口的选项了。 如何去除: 假如哪天你不想要这个新加的选项了,请把下面的代码复制,同样保存到一个文本文件然后存为remove.reg,双击运行之。选项就会消失,菜单恢复正常。 Windows Registry Editor Version 5.00 ; Created by: Shawn Brink ; http://www.sevenforums.com ; Tutorial: http://www.sevenforums.com/tutorials/47415-open-command-window-here-administrator.html [-HKEY_CLASSES_ROOT\Directory\shell\runas] [-HKEY_CLASSES_ROOT\Directory\Background\shell\runas] [-HKEY_CLASSES_ROOT\Drive\shell\runas] Reference: http://www.sevenforums.com/tutorials/47415-open-command-window-here-administrator.html
页面中除了传统的超链接外,还可以将邮箱地址写入<a>标签,意思不表自明,当然是用户点击后就会打开相应的邮件客户端向这个连接指向的邮件地址发邮件。 <a href="mailto:liuwayong@gmail.com" target="_blank">liuwayong@gmail.com</a> 效果: sample@test.com 了解邮件连接 一般情况下,如果你的浏览器之前有设置过 mailto: 协议的话,它会启动mailto协议里指定的程序来打开这个邮件链接。 比如我的Chrome浏览器里设置为使用Gamil,那么单击后,会自动打开Gamil页面,并且把收件人地址填好了。 另外,如果你在连接中传了subject 参数,或者还有其他参数,邮箱页面打开后,相应位置的内容会从参数当中去取,然后自动填上。 下面是完整参数的列表: 参数 描述 mailto:name@email.com 收件人邮箱 cc=name@email.com 抄送邮箱 bcc=name@email.com 匿名抄送邮箱 subject=subject text 邮件主题 body=body text 邮件正文 ? 首个参数分隔符 & 其余参数的分隔符 下面是一个带完整参数的例子: <a href="mailto:sample@test.com?Subject=Test%20Mail&cc=mail1@test.com&bcc=mail2@test.com&body=Dear%20Mary" target="_top">Send Mail</a> 这是一个测试连接 浏览器里出来的效果: 设置Chrome接管mailto协议 如果你点击上面的测试连接无法打开Gmail,说明你的Chrome没有设置好用来接管处理mailto协议。 下面进行设置: 1. 在浏览器地址栏输入chrome://settings/ 回车来到浏览器设置页面 2.搜索 'protocol handlers' 3.根据搜索结果来到协议管理设置页面,将其中的mailto 设置为你想要的处理程序,这里是Gmail. 4. 一路确定下去,然后在浏览器输入“mailto:” 回车进行测试,会自动转到Gmail页面 如果在第三步发现页面没有mailto协议及可选的操作怎么办?通过JavaScript来进行巧妙地设置。 1. 打开Gmail页面 2.把如下代码粘贴到浏览器地址栏 javascript:navigator.registerProtocolHandler("mailto","https://mail.google.com/mail/?extsrc=mailto&url=%s","Gmail") *注意代码前面要有 'javascript:'。通常在Chrome浏览器里,将上面的代码粘贴到地址栏后,前面的'javascript:'会被自动去掉,所以需要手动补上。 3. 回车确定后会出现 4.同样输入mailto:后测试页面是否跳转到Gmail页面,如果跳转,说明设置成功。 Reference: 1 .http://productforums.google.com/forum/#!topic/chrome/sPhxiTQlf4s 2. http://www.rapidtables.com/web/html/mailto.htm
我当然知道未经作者允许修改别人程序是不道德的了,但作为学习研究之用还是无可厚非,这里仅供交流。 一切都是需求驱动的 话说某天我在网上猎奇的时候无意间发现这么一款神奇的谷歌浏览器插件:Extension Source Locator。翻译成大中华语意思大概是扩展程序源码定位器! 它是干什么的呢,根据被翻译过来的不太准确的大中华语可以大概知道这玩意儿可以定位到一个你已经在谷歌浏览器上安装了的扩展程序的源码,或者说源文件 ,在你电脑磁盘的哪个地方。 这当然没什么神奇的了,你或许说我可以通过上网查查就知道谷歌浏览器扩展程序安装后与之相关的文件在磁盘什么地方了。但有了它使我们更加方便地定位到源文件所在文件夹。 更重要的是它让我意识到我可以修改一些我喜欢的扩展程序,让其更加适合自己的使用。 因为很多时候我会碰到这种情况,遇到一个扩展,装上之后非常喜欢,用段时间后觉得有些地方如果能这样,那就更好了。如果能那样,那这个插件就完美了,etc. 这个时候,第一反应还是乖乖的去该插件的评论页面给作者提建议,并且一个插件里的评论大部分确实是这样的建设性意见:如果怎么怎么着我就给你打五分。。。。 大家都知道,作者一般很忙,往往会把用户这些无趣的需求置之不理。 作为程序员的我,当然知道自己动手方能丰衣足食。 安装Extension Source Locator插件,不装其实也可以 Extension Source Locator安装页面在Google Web Store,的盆友们可以前往安装。 装好之后你再打开一个空的页面时,页面应该呈现的是这样的画面了: 如何使用: 使用过程当然是点击"Copy to clipboard" 按扭。这个页面还有任何其他按扭么一_一!! 点击后得到一个插件的文件夹地址,将其粘贴到Windows 资源管理器的地址栏中,轻击回车,你就来到了这个一谷歌浏览器扩展程序的源文件存放的地方了。 比如上面截图的的Google+ 这个扩展程序。 点开该1.2.0.418_0文件夹,里面存放了Google+ 这个扩展程序相关的源文件。 案例一:制作一个指向百度首页的谷歌扩展程序 你完全不需要知道一个谷歌扩展程序是如何工作的,也不用学习如何编写一个扩展程序,只要是一个程序员,多少应该是掌握HTML,CSS,JavaScript的。 谷歌扩展程序,包括在最近新版(Chrome 29)中新增的谷歌浏览器打包程序(Chrome packaged app), 其本质都是HTML 与JavaScript。 像 Google+啦,Gmail啦,这些是最简单的谷歌浏览器插件,点击之后只是指向相关网站而以。 国人每天都会使用百度进行搜索,如果有一个可以打开百度首页的谷歌插件就方便了,就像在谷歌浏览器里创建了一个快捷方式一样。到谷歌应用商店一搜,还真没有这样一个插件。 拿上面的Google+为例,我们需要做的仅仅是去Google+文件所在的文件夹,找到相关文件然后打开,把指向Google+的连接更改为www.baidu.com即可。 上面已经找到该扩展的文件夹了,打开一看里面非常之简陋: 一个跟语言本地化什么的相关的_locales文件夹(直接无视); 一个存放程序图标的icons文件夹(呆会我们做一个带百度logo的图标去替换里面的Google+图标); 一个谷歌扩展程序必备的清单文件manifest.json;(可以做手脚的地方也就只有这么一个文件了) 好家伙,这东西竟然连一个像样的HTML与JS文件都没有(其实也不需要有,因为他的功能灰常之简单,这个插件本身不显示任何页面,所以它没有HTML文件,更多关于谷歌浏览器插件的相关知识请移步谷歌开发者中心的扩展程序开发页面,也可以看看园子里面的文章比如Harvey的如何开发Chrome(谷歌)浏览器的插件) 。 将所有文件及文件夹复制到另一个新的文件夹,比如我们在桌面新建一个叫"Baidu"的文件夹。之后我们的操作在复制的文件上进行,目的是不破坏原来的文件,也方便我们修改好之后以这个新文件夹生成一个新的谷歌扩展程序。 唯一有用的似乎就一个JSON后缀的文件,打开一看果然看到了指向Google+的连接。 所以我们要做的就非常简单了,将连接改为"http://www.baidu.com" ,同时把程序的显示名改一下,再保存关闭; 使用强大的PS制作新的图标(我不会告诉你作为一名非著名程序员之外,我还是个半职业的平面设计师): 128X128 16X16 然后分别更名为icon16.png, icon128.png将icons文件夹中原来的图标替换。 最后一步,将我们的山寨程序安装到谷歌浏览器。 页面停留在插件页面的情况下,直接从桌面将那个Baidu文件夹拖到浏览器里,插件就这样被安装好了。 在新标签页或者Chrome app launcher里便可以看到我们新制作的百度首页插件。 点击它就可以直达百度啦\(^_______^)/。虽然没什么意义一_一。。。 案例二:让谷歌搜索中的英文关键字像中文一样红色高亮 这个功能我想了很久,但在谷歌应用商店能找到的高亮搜索关键字的插件不能完全满足我的要求。 比如这个叫word highlight的插件似乎是个不错的选择。 但它会为关键字里的每个单词加上不同的背景色,真心有点难看。 为了实现之前说的那样,跟中文搜索时有一样的体验,我开始了对它的改造。 同之前一样,复制其在磁盘上的地址,在资源浏览器中找到这个插件的源文件。 打开一看,文件还挺多的,似乎有点无从下手。 但其实找到关键点就不那么难了。因为显示相关的无非就是CSS,再者就是看JavaScript里面关于CSS颜色设置相关的代码,可以通过搜索'color'找到相关代码。 但我首先还是尝试看能不能在CSS文件里突破,不行再去JavaScript代码尝试。 从而页面代码来看,它为需要高亮的关键字加了一些CSS class: 所以打开CSS文件夹,在option_page.css 里搜索相关class,无果。 看来只能去JavaScript 代码里一探究竟了。 一看就知道words_highlight.js 应该就是实现对文字进行高亮相关的代码。所以打开它来研究。 通过在words_highlight.js文件里搜索'color',发现在代码的173行定义了一个包含颜色的数组,用的还是RGB模式的颜色。 然后在代码的270行发现了对文字进行背景及颜色设置的代码: 似乎已经很明朗了。 在谷歌搜索中随便用中文搜索一下,目的是为了获得原生的红色色值。如下图,我们得到谷歌对中文使用的颜色是#dd4b39。 但我们得到的是16进制的色值,为了代码风格的统一我们也转换成代码中需要的RGB色值。随便找一个在线色值转换工具,最后得到我们需要的RGB为rgb(221,75,57)。 将原来代码中270行对于文字背景设置的代码删除,再把color的设置由原来的black改为 想要的 rgb(221,75,57)。保存并关闭文件。 现在重启一下浏览器再随便搜索一下,效果就出来了。看起来还蛮不错的样子。
因为有时看国外教程时,手头上的PS是中文的而教程里的界面是英文的,而且中英菜单顺序在某些地方是不一样的,所以很不方便。 终于找到一个非常完美的方法可以把界面换成英文,而且不需任何语言包。 并且试了在最新的Photoshop CC版本中可用。 下面是具体方法,你想像不到的简单与方便,并且可以随时换回你原来的语言。 打开Photoshop所在文件夹,即你的安装位置,如果是绿色版本则是你解压后存放的位置 定位到Adobe Photoshop CC v14.0\Adobe Photoshop CC\Locales\zh_CN\Support Files, 比如我的PS是放在D盘的,所以最终路径是:D:\Program Files (x86)\Adobe Photoshop CC v14.0\Adobe Photoshop CC\Locales\zh_CN\Support Files 在这个文件夹里会看到一个叫 tw10428.dat 的文件 这个文件就是控制界面语言显示的,用记事本打开可以看到对应英文菜单的翻译 所以把这个文件改一下让程序找不到它,界面的语言就不会被正确翻译,这样PS会使用内置默认的英文语言来显示界面。推荐的做法不是把文件删除而是将它重命名,比如取为 tw10428.bak。这样下次你想换回原来的语言时只需把文件名改回去即可。 之前的中文版本: 更改后的英文界面: refrence:http://www.youtube.com/watch?v=3I8B8QH5uRE
有兴趣的同学可以文章最后的代码复制贴到控制台玩玩。 Go for Code 在正常模式下,一般只能向console 控制台输出简单的文字信息。但为了把信息输出得更优雅更便于阅读,除了cosole.log()方法外还可以调用 cosole.warn() 来输出警告信息,在控制台中出来的效果如下: 在输出信息前面会有一个带感叹号的黄色三角警告符号。似乎比一般的console信息要友好得多了。虽然图标是黄色的,但输出的文字仍然是黑色。 另外经常用到的是输出错误信息。可以通过调用console.erro() 来实现。 输出的效果如下: 信息前面会出现一个带叉的红色圆形图标。 这个效果要比警告信息更友好了,字体颜色成红色了。 要更牛叉莫过于对文字应用样式。而现在这一特性已经在谷歌浏览器里实现了。 在Chrome的开发者工具里,console 可以加样式,可以显示缤纷的颜色,甚至图片。简直爽翻了。 具体来说,是可以对输出到console控制台的文字进行CSS控制。 格式如下: console.log("%c需要输出的信息 ", "css 代码"); 下面是console.log() API的官方文档摘要。 谷歌开发者中心上面关于谷歌浏览器控制台console.log()的文档: Format Specifier Description %s Formats the value as a string. %d or %i Formats the value as an integer. %f Formats the value as a floating point value. %o Formats the value as an expandable DOM element (as in the Elements panel). %O Formats the value as an expandable JavaScript object. %c Formats the output string according to CSS styles you provide. 1.3D Text: console.log("%c3D Text"," text-shadow: 0 1px 0 #ccc,0 2px 0 #c9c9c9,0 3px 0 #bbb,0 4px 0 #b9b9b9,0 5px 0 #aaa,0 6px 1px rgba(0,0,0,.1),0 0 5px rgba(0,0,0,.1),0 1px 3px rgba(0,0,0,.3),0 3px 5px rgba(0,0,0,.2),0 5px 10px rgba(0,0,0,.25),0 10px 10px rgba(0,0,0,.2),0 20px 20px rgba(0,0,0,.15);font-size:5em") 2.Colorful CSS console.log("%cColorful CSS","background: rgba(252,234,187,1);background: -moz-linear-gradient(left, rgba(252,234,187,1) 0%, rgba(175,250,77,1) 12%, rgba(0,247,49,1) 28%, rgba(0,210,247,1) 39%,rgba(0,189,247,1) 51%, rgba(133,108,217,1) 64%, rgba(177,0,247,1) 78%, rgba(247,0,189,1) 87%, rgba(245,22,52,1) 100%);background: -webkit-gradient(left top, right top, color-stop(0%, rgba(252,234,187,1)), color-stop(12%, rgba(175,250,77,1)), color-stop(28%, rgba(0,247,49,1)), color-stop(39%, rgba(0,210,247,1)), color-stop(51%, rgba(0,189,247,1)), color-stop(64%, rgba(133,108,217,1)), color-stop(78%, rgba(177,0,247,1)), color-stop(87%, rgba(247,0,189,1)), color-stop(100%, rgba(245,22,52,1)));background: -webkit-linear-gradient(left, rgba(252,234,187,1) 0%, rgba(175,250,77,1) 12%, rgba(0,247,49,1) 28%, rgba(0,210,247,1) 39%, rgba(0,189,247,1) 51%, rgba(133,108,217,1) 64%, rgba(177,0,247,1) 78%, rgba(247,0,189,1) 87%, rgba(245,22,52,1) 100%);background: -o-linear-gradient(left, rgba(252,234,187,1) 0%, rgba(175,250,77,1) 12%, rgba(0,247,49,1) 28%, rgba(0,210,247,1) 39%, rgba(0,189,247,1) 51%, rgba(133,108,217,1) 64%, rgba(177,0,247,1) 78%, rgba(247,0,189,1) 87%, rgba(245,22,52,1) 100%);background: -ms-linear-gradient(left, rgba(252,234,187,1) 0%, rgba(175,250,77,1) 12%, rgba(0,247,49,1) 28%, rgba(0,210,247,1) 39%, rgba(0,189,247,1) 51%, rgba(133,108,217,1) 64%, rgba(177,0,247,1) 78%, rgba(247,0,189,1) 87%, rgba(245,22,52,1) 100%);background: linear-gradient(to right, rgba(252,234,187,1) 0%, rgba(175,250,77,1) 12%, rgba(0,247,49,1) 28%, rgba(0,210,247,1) 39%, rgba(0,189,247,1) 51%, rgba(133,108,217,1) 64%, rgba(177,0,247,1) 78%, rgba(247,0,189,1) 87%, rgba(245,22,52,1) 100%);filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#fceabb', endColorstr='#f51634', GradientType=1 );font-size:5em") 3.Rainbow Text console.log('%cRainbow Text ', 'background-image:-webkit-gradient( linear, left top, right top, color-stop(0, #f22), color-stop(0.15, #f2f), color-stop(0.3, #22f), color-stop(0.45, #2ff), color-stop(0.6, #2f2),color-stop(0.75, #2f2), color-stop(0.9, #ff2), color-stop(1, #f22) );color:transparent;-webkit-background-clip: text;font-size:5em;'); 更新:在Chrome控制台输出图片 除了上面介绍的那些炫目的文字效果外,你还可以在Chrome控制台中显示图片,自然地,显示gif这样的动态图片也是没问题的。 还是应用差不多的代码,只是将内容变成指定背景为图片。 下面是一个例子: console.log("%c", "padding:50px 300px;line-height:120px;background:url('http://wayouliu.duapp.com/img/tagsImg/youth.gif') no-repeat;"); 也可以访问我的主页然后Ctrl+Shift+J打开控制台查看更多效果。 谷歌开发者中心 Console API Reference Google+上Addy Osmani分享的post StackOverflow :Colors in JavaScript console
变量 1.JavaScript hoisting >>请看例子,我们拿Chrome的console作为JS的运行环境。 上面直接执行console.log(a), 不带一点悬念地抛出了not defined 错误。这是预料之中的。 看下面进化后的代码: 之前变量没有定义的错误没了,取而代之的是告诉我们a的值是 'undefined'。先不管a的值缘何为 'undefined' 了,至少可以知道现a这个变量是定义了,因为之前报的' a is not defined'的错误没有了。 这正是因为JavaScript 中的一个声明提前的特性起的作用。 JavaScript中可以提前使用在后面语句中声明的变量,这种特性叫被国外某网友(ben cherry)称为Hoisting (非官方术语) 。 可以理解为将变量的声明提前了,所以在变量声明前使用变量不会报错。而且这一特性不仅限于变量名,对于函数的声明也是同样的效果。 第一次对函数foo()的调用同样报 'not defined' 错误。这是合情合理同时是合法的。因为从头到尾就没有定义这么一个叫作foo() 的东西。 之后将函数调用写在最前面,但函数的定义我们写在了之后。 >> 再来看上面对a的输出值为'undefined'的问题。 这里需要深入理解Hoisting这一特性。它的提前只是将声明提前,而对变量的赋值并没有跟着提前。这点很关键。也就是为什么我们可以在第一句使用变量a但它的值却是 'undefined'。 JavaScript里面声明了但还未赋值的变量其值默认便是 'undefined'。 按照Hoisting来解释,最终生成的等价代码其实差不多应该就是这样的: 2.直接对字符串字面量调用其方法 可以直接对字符串字面量调用方法,JS解析器会自动把字符串变量转为一个字符串类型暂存,然后调用字符串上的方法,完了之后再将暂存的字符串类型销毁。 所以你会看到下面这种用法。 我们可以理解为解析器在后台声明了一个变量暂存这个字符串然后再调用的length方法。 函数 1.匿名函数无法在声明前调用 正如上面介绍的Hoisting特性,函数可以定义在调用之前也可以定义在调用之后: 但仅限于上述这种方式定义的函数。对于匿名函数上述规则不适用。 2. 参数变更影响到函数外部 当传递给函数的参数是一个数组或对象时,在函数体内对这个传入的参数的更改会影响到函数体外的原传入值。 一般说来,对参数的更改不会影响到原来变量的值,更改只在函数体内起作用: 上述代码中,将name变量赋值为'Wayou Liu' 然后传入change() 函数。在函数体内将传入的参数值改为'Liu Wayou'。然后再输出,不出意外地,输出的是变量原来的值'Wayou Liu'。因为当name传入change函数后,在函数体内,相当于有一个name的副本,这个副本的值等于name,之后在函数体内对其做的操作是在这个副本上进行的。 但情况有所不同,当传入的参数是数组、对象时,在函数体内对参数所做的更改会反映到原变量上。 可以看出,上面代码中已经把friut数组的第一个元素更改了。 下面是关于对象的例子: 可以很明显地看到函数体内对参数的改动影响到了原来的变量,这与通常情况下的传参有质的区别了。需要特别注意。 But,当在函数体内对传入的数组或对象赋值时,这个更改不会反映到函数体外的原变量身上! 请看: 按照上面函数内部的更改会反映到原变量的理论,你肯定觉得执行完change()后person变量的name属性的值已经变成'Tom'了吧。但结果让人有点难以接受。 原因在于,当在函数体内使用赋值操作时,系统就创建了一个变量名为p的变量。这个p是函数内部的变量,对它进行赋值当然只在函数体内起作用,外面的person还是原来的person。 这一步与原来代码的操作差别仅在于在函数体内是对参数赋新值呢还是对参数的属性或数组的元素进行更改。 3.使用arguments来接收个数未定的参数 有时候我们写了个函数但它接收的参数个数不确定,有时候我们在调用一个函数的时候或许也不确定要传多少个参数。参数需要变成动态的。 这时可以通过函数内arguments来接收传递到一个函数的所有参数。 具体说来,就是在函数体内默认有个arguments变量,它保存了调用这个函数时传递来的所有参数,总个数及每个参数的值。它是一个类似于数组的变量,每个参数会成为它的一个元素,可以通过游标来进行访问。 通过arguments.length可以知道参数个数。 下面这个例子来自MDN:
在进入主题之前,我们先来看一个前台页面经常用到的功能:点击页面输入框时自动选择其中文本。 很容易想到利用输入框的focus事件,当输入框获得焦点时,再调用jQuery的select()方法。 Okay,想法很简单,逻辑似乎也无错。具体我们来看一下现实到底能不能实现。 1.页面构造个表单,放上几个输入框。代码看起来是这样子的。 <form action="/" method="post"> <table> <tr> <td>Name:</td> <td> <input type="text" name="name" value=" " /> </td> </tr> <tr> <td>Age:</td> <td> <input type="number" name="age" value=" " /> </td> </tr> <tr> <td>Tel.:</td> <td> <input type="tel" name="tel" value=" " /> </td> </tr> <tr> <td>E-mail:</td> <td> <input type="email" name="email" value=" " /> </td> </tr> <tr> <td>Birth:</td> <td> <input type="datetime" name="birthday" value=" " /> </td> </tr> </table> </form> 出来的界面在Chromium里差不多是这个样子的: 2.然后开始写我们的JavaScript代码来实现单击选中框内的文本,根据之前的想法,实现起来差不多应该是下面这个样子: <script type="text/javascript"> $(function () { $("input").focus(function () { $(this).select(); }) }) </script> 3.然后再去页面小试一下,看效果出来没有。尝试之后发现,差不多算是成功了一半。什么情况叫?如果此刻你也跟着写了代码,你会发现,间单击输入框时,框中文本会闪烁。再进一步才会发现,只有当鼠标按下不放,输入框内文本才是保持选中状态,是我们预期的。当松开时选择效果消失。无尽惆怅! 而且这还只是仅仅在Chromium中的情况,在IE中更为奇葩,连一丁点选择的效果都没有绽放出来。直接把代码无视了。 对于火狐,水壶(如果你还不知道它的存在的话:火狐近亲,Mozilla 官方承认的64位高效版本火狐变种版本)我已经无力去测了。 下面直接给出经过Google之后找到的能在全浏览器中工作的代码: <script type="text/javascript"> $(function () { $("input").focus(function () { var input = $(this); setTimeout(function () { input.select(); }); }) }) </script> 而关于上面这段能够正常工作的代码,还有一点神奇之处。那就是我和大家可能都觉得 var input = $(this); setTimeout(function () { input.select(); }); 与 var input =$(this); setTimeout(function () { $(this).select(); }); 这两种写法应该是完全一样的代码吧,所以后者也应该能够达到预期的效果才对。但事实上换成第二种后,效果不见了!根本无法让文本自动选中!! 这是一般人所无法理解的高度。 Okay,回归继续看我们的输入框现在怎么样了。现在只要输入框中有文本,随便点一下就自动选中且松开鼠标后不反弹。很好,要的就是这种效果。 下面,才是本文的真正主题,如何将特性,或者说事件处理器,绑定到动态创建的页面元素上。 接着上面这个功能讲。上面的代码也许解决了一个表单页面的需求,在且仅仅是在这个页面,输入框具备这种获得焦点后自动将文本选中的特性。或者说拥有我们代码中所绑定到输入框focus事件上的处理器,当然,这个处理器就是选择文本。 如果说上面的说法有点令人头晕找不到北,下面我将用平生最为直白的语言再次阐述同一观点:假如其他页面也有输入框,是不是每个页面都去写一段相同的代码来实现这样的效果。 或者说同样是在当前页面,当用户填完相应资料后,我们再动态生成一些输入框,而这些后来生成的输入框如何也拥有获得焦点选中文本的功能。 为了演示,我们检测如果用户输入了Name,我们再在下面创建一个输入框可以输入昵称。可以预见得到,这个后来通过JavaScript代码插入到DOM中的输入框其中不会有其他输入框一样的效果的。因为我们使文本自动被选中的代码是在页面加载时执行的,而页面加载时这个后来插入的输入框还不存在呢。 下面是新加的对name输入框监听的代码: $("input[name='name']").change(function () { $("tr").first().append(' <tr>' + '<td>Nick name:</td>'+ '<td>'+ '<input type="text" name="nickname" value=" " />'+ '</td>'+ '</tr>') }) 下面到页面去测试一下,在Name中随便输入点什么吧。并且测试生成的输入框是不能将文本自动选中的。 下面给出使动态创建的元素得到之前绑定的事件处理器的方法: $("body").on('focus', "input", function () { var input = $(this); setTimeout(function () { input.select(); }); }); 这个方法未免有点取巧,但也是我觉得目前为止所知道的比较好的办法。因为jQuery1.9之前其实是有个live()函数专门来做这样类似的工作的,它可以将事件处理器绑定到未来未还未被创建的元素身上,但后来随着jQuery版本的升级,不提倡用这个live()方法了。既然不提倡了,自然有它的理由我也就不细究了,就像之前我细究了一下jQuery为什么废除了检测浏览器相关的函数一样。 如果我们将上面的方法写到网站的母版页当中,那么就不必在每个有输入框的页面都写相同的代码来实现了,同时对于后来动态创建的元素也都应用上了效果。 上面关于输入框的例子只是为了演示,当然不局限于这个例子,这样处理动态创建元素的需求还是很常见的,至少我在项目中都遇到过几次了,在不同的情形下。比如在呈现给权限不够的用户的页面中,有些按扭需要禁用,但用户是可以点击添加来增加一行,而每行都会有删除修改按扭,这时候可以将disable应用到一个表格中新增加的行中的按扭上。