Houdini:CSS 领域最令人振奋的革新

原文链接:Houdini: Maybe The Most Exciting Development In CSS You’ve Never Heard Of

已得到原文作者 Phil Walton 以及原文发布平台 smashingmag 授权翻译
想要使用某种 CSS 特性,但是因为浏览器兼容性问题而没法使用?更糟糕一点,所有浏览器都支持这种特性,但支持度不完全,在某些情况下会有 bug 出现、支持性不一致,甚至于完全不兼容。如果你曾经遇到过上述情况(我肯定你一定遇到过),那你得好好关注 Houdini

Houdini 是 W3C 新成立的一个任务小组,它的终极目标是实现 css 属性的完全兼容。Houdini 提出了一个前无古人的的设想:开放 CSS 的 API 给开发者,开发者可以通过这套接口自行扩展 CSS,并提供相应的工具允许开发者介入浏览器渲染引擎的样式和布局流程中。

但是……这意味着什么呢?这个方案靠谱吗?不管是现在还是将来,Houdini 对开发者进行扩展开发有什么帮助呢?

我将在后面的文章里尽可能地回答上面这三个问题。但在开始回答之前,要先搞清楚我们现在面临的问题是什么,以及为什么出现 Houdini 这样的解决方案。弄明白问题是什么之后,我再告诉大家 Houdini 将会如何解决这些问题,以及它目前的进展。最后,开发者们,你们还能了解具体如何做才能让 Houdini 尽早落地。

Houdini 能解决什么问题?

写文章也罢做 demo 也罢,每一次当我想要炫耀点 CSS 新花样的时候,总有人会说“这效果真是屌炸天!然而我们现在并不能好好用它,至少等个 10 年吧”。

虽然每每我都觉得这样的评论让人生气又没啥建设性,但依然承认大家的担心有理有据。纵观 CSS 历史,每一份特性草案都是经过了许多年才被广泛应用。而之所以会是“许多年”,就是因为要允许一个新特性被写入 CSS 标准需要经过一整套标准制定流程,然而就过了很多年……

我对于这个标准制定的流程肯定是毫无异议的,但你得承认,走完整个流程花儿都谢了。

flexbox 大概就是最好的例子,2009年,关于 flexbox 的草案第一次被提出,但直到今天开发者们还在抱怨着这个属性的浏览器兼容性问题。不过感谢上帝,随着现代浏览器的自动更新机制,这个问题总有解决的那一天。但是,按照现在的新属性发布流程,最新的浏览器也会和新属性提案仍然存在一个时间差。

不过同样在 web 世界里,对于如今的 JavaScript 来说好像不是什么事儿了:

在上面的流程图中,我们可以看到从想到一个新的 js 特性到运用在生产环境,大概只需要几天时间。讲真,我已经在使用 asyncawait 了,然而目前没有浏览器天生支持这两个方法吧。

估计现在你也大概感受到了 CSS 社区和 JS 社区的画风不同了:在 JS 社区里,你总能听到人们在抱怨它一天一个样 -- 跑得太快追得太累;而 CSS 社区,人们却在叹息着花精力去学习新事物是件多么吃力不讨好的事儿 -- 天知道什么时候才能用上新玩意呢。

既然如此,为什么我们不能用上 CSS Polyfill ?

脑海里闪过的第一个解决方案就是 CSS polyfill,只要 CSS polyfill 足够强大,相信 CSS 的发展速度赶超 JavaScript 不是梦。

然而,给 CSS 打补丁做起来有多难你都想不到,而且在大部分情况下,只要这么做了,性能肯定会受到影响。

JavaScript 是一种动态语言,它是如此之“动态”以至于有着让人惊叹的可扩展性,所以用 JavaScript 给 JavaScript 打补丁是可以轻松实现的,但 CSS 不是动态的呀。在某些情况下,你可以在构建过程中将一种形式的 CSS 转译成另一种形式(PostCSS 就是一个典型的例子)。而一旦你的补丁是依赖于的 DOM 结构、某一个元素的布局或者它的定位的话,那补丁程序就需要在本地运行了。

不幸的是,要在浏览器中实现这种方案挺难的。

让我们来看看一个 HTML 文档从被浏览器接收到显示在屏幕上的全过程,下面这张图里被标为蓝色的部分就是 JavaScript 可以染指的环节了:

作为开发者,看着这张图心都凉了,我们根本控制不了浏览器解析 HTML 和 CSS 的过程,只能看着它生成 DOMCSS object model (CSSOM)。没法控制级联(cascade)、没法控制浏览器布局元素的方式(layout)、也没法控制元素在屏幕上显示的过程(paint)、最后的合成(composite)也无能为力。

整个过程中,开发者能完全控制的唯一环节就是 DOM,另外 CSSOM 也可以部分控制到。即使如此,引用 Houdini 官网上的话来说,这种程度的暴露是“不确定的、兼容性不稳定的以及缺乏对关键特性的支持的”。

举个例子,浏览器中的 CSSOM 是不会告诉你它是如何处理跨源的样式表的,而且对于浏览器无法解析的 CSS 语句它的处理方式就是不解析了,也就是说——如果你要用 CSS polyfill 让浏览器去支持它尚且不支持的属性,那就不能在 CSSOM 这个环节做,只能自行解析一遍 DOM 树,找到 <style><link rel=”stylesheet”> 标签,获取其中的 CSS 样式、解析、重写,最后再加回 DOM 树中。

DOM 树都刷新了,得,渲染页面步骤重新走一遍。

(上图括号里面的文字:JavaScript Polyfills 只能去更改 DOM 和 CSSOM,大部分这样的操作,都会导致页面重新渲染。)

好吧,可能你会说这种别无选择的方法,也并不会对性能造成多大伤害(对某些网站来说,是的),但想想这个重渲染过程会多么频繁地发生,如果你的 polyfill 是需要应对页面上的所有交互呢?scroll 事件、窗口缩放、鼠标移动还有键盘输入……随时都会被触发的重渲染会把页面拖得无敌慢,用户绝对会发现的。

雪上加霜的是……大部分的 CSS polyfills 都是各有各的解析器和层级逻辑,而且“解析器”和“层级逻辑”又是两个非常复杂的东西,所以这些 polyfills 不是文件太大就是有太多 bug。

简要概括一下上面的内容:如果你想要浏览器做出它本来做不到事情(比如让它解析你给的样式,不管它能不能实现该样式),而渲染流程里你无法插手其他步骤,所以只能通过手动更新和改变 DOM 的方式。

如果我想要更改浏览器内部的渲染引擎呢?

我认为,这个问题是这篇文章的关键所在,如果你草草略过了前文,千万要在这里停下!仔细看这部分!

在看完上面那一节之后,我确定有些人干脆因噎废食地想“我不需要这个!我只想要中规中矩地写页面,并不想侵入浏览器内核然后实现一些特别前卫的效果”。

如果你是这么想的,那我强烈建议你看看自己这些年用于实现页面效果的技术。我们想要“干涉”浏览器解析样式的目的并不仅仅是为了炫技,更是为了框架作者以及开发者们能做到下面两件事情:

  • 统一跨浏览器行为,
  • 开发新特性或者给新特性打补丁,让人们可以立刻使用到新特性。

如果你曾使用过像 jQuery 那样的 JS 库,那你已经从中受惠了!事实上,良好的兼容性正是绝大多数前端库活着框架的卖点之一。Github 上受欢迎度排名前五的 JavaScript 和 DOM 仓库 — AngularJS、D3、 jQuery、 React 还有 Ember,面对使用它们的人来说,只要搞明白如何使用那些 API,就能成功达到想要的目的了,但是它们背后,在兼容各浏览器上下了多少功夫,恐怕是使用者几乎从未考虑过的。

现在,想想 CSS 在跨浏览器上的问题。甚至像 Bootstrap 或者 Foundation 这样宣称兼容性良好的 CSS 框架也都没有把浏览器 bug 标准化,而只是尽量去避免它们。不要以为 CSS 的兼容性问题只是个老毛病,就拿 flexbox 来说,我们也还面对着各种各样的跨浏览器兼容问题。(译者注:flexbox 的兼容问题,现在在主流的移动端页面开发上,已经有所缓解,译者曾整理过一个 gist 用于移动端 html 5 页面的 flexbox 效果,欢迎使用纠错)

试想一下,你可以随心所欲地使用想用的 CSS 属性,在每个浏览器上,你的页面长得和你设想的一样(,这职业生涯得过的多欢脱啊。你看到的那些酷炫的属性都能在保证性能的前提下使用,比如网格布局( CSS grids)、对齐(CSS snap points )还有 sticky 定位…… 而要实现这一切,你只需要把代码从 Github 上下载下来而已。

好吧,上面描绘的是 Houdini 的蓝图,Houdini 小组想要将这个梦想实现。

也许你从未想过写个 CSS polyfill 或者开发一些实验性的特性,但你可能会希望其他人能做这些事情,毕竟一旦有了这些工具,受益的可是全部开发者啊。

Houdini 的目前进展

在前面,我曾提到过开发者是很难干涉浏览器的渲染过程的,除了 DOM 和 CSSOM 这两个环节外。

Houdini 小分队为了解决这个问题引入了一些新的标准,首次给了开发者介入另外几个渲染环节的权限。下面这张图片展示的是每个环节对应的新标准,开发者可以用这些标准来控制对应的环节。(注意:灰色区块是还在实现中的标准,目前暂时无法使用。)


在接下来的几节内容中,我将大概介绍一下每一种新规范以及它们能做到什么,其他规范这里就不再赘述了,感兴趣的朋友可以看这里

CSS Parsing API

CSS Parser API 还没有被写入规范,所以下面我要说的内容随时都会有变化,但是它的基本思想不会变:允许开发者自由扩展 CSS 词法分析器,引入新的结构(constructs),比如新的媒体规则、新的伪类、嵌套、@extends@apply 等等。

只要新的词法分析器知道如何解析这些新结构,CSSOM 就不会直接忽略它们,而是把这些结构放到正确的地方。

CSS 属性与值 API

我曾提过 CSS 已经有自定义属性了,这太让人兴奋了,CSS 将解锁多少新技能啊。CSS Properties and Values API 的出现进一步推动了自定义属性,它还允许自定义属性添加不同的类型,大大增强了自定义属性的能力。

在自定义属性中加入不同的类型是很棒啦,不过这个 API 最大的的卖点是,开发者可以在自定义属性上做!动!画!而仅凭现在的技术,我们是做不到的……

来看看这个例子:

body {
  --primary-theme-color: tomato;
  transition: --primary-theme-color 1s ease-in-out;
}
body.night-theme {
  --primary-theme-color: darkred;
}

night-theme 类被加到 <body> 元素上之后,页面所有有 –primary-theme-color 的元素属性值都会慢慢从 tomato 变成 darked 。如果今天你想要在自己的页面上实现这个效果,那就需要费劲儿的一个个给元素添加过渡动画。

译者注:为什么我满脑子想的都是性能,页面全部重绘似乎是不可避免得了,毕竟从 body 元素的颜色都换了嘛。不过话又说回来,主题都换了,重绘 (repaint) 也是理所应当的

这个 API 的另一个卖点是注册一个 “apply hook”,也就是开发者可以在 cascade 步骤结束的时候,通过一个钩子更改一个元素的自定义属性值,这个特性对于 polyfills 开发可是很有意义的。

CSS Typed OM

你可以把 CSS Typed OM 视为 CSSOM 2.0,它的目的在于解决目前模型的一些问题,并实现 CSS Parsing API 和 CSS 属性与值 API 相关的特性。

提升性能是 Typed OM 的另一重任。将现在 CSSOM 的字符串值转成有意义的 JS 表达式可以有效的提升性能。

CSS Layout API

开发者可以通过 CSS Layout API 实现自己的布局模块(layout module),这里的“布局模块”指的是display 的属性值。也就是说,这个 API 实现以后,开发者首次拥有了像 CSS 原生代码(比如 display:flexdisplay:table)那样的布局能力。

让我们来看一个用例,在 Masonry layout library 上大家可以看到开发者们是有多想实现各种各样的复杂布局,其中一些布局光靠 CSS 是不行的。虽然这些布局会让人耳目一新印象深刻,但是它们的页面性能往往都很差,在一些低端设备上性能问题犹为明显。

CSS Layout API 暴露了一个 registerLayout 方法给开发者,接收一个布局名(layout name)作为后面在 CSS 中使用的属性值,还有一个包含有这个布局逻辑的 JavaScript 类。假如你想要用这个方法定义一个 masonry 的类,可以这么写:

registerLayout('masonry', class {
  static get inputProperties() {
    return ['width', 'height']
  }
  static get childrenInputProperties() {
    return ['x', 'y', 'position']
  }
  layout(children, constraintSpace, styleMap, breakToken) {
    // Layout logic goes here.
  }
}

如果上面这个例子你看不明白也用不着担心。关键在下面的代码,当你下载好 masonry.js 后,将它加入你的站点,然后这么来写 CSS 你就能得到一个 masonry 布局的样式了:

body {
  display: layout('masonry');
}

CSS Paint API

CSS Paint API 和上面说到的 Layout API 非常相似。它提供了一个 registerPaint 方法,操作方式和 registerLayout 方法也很相似。当想要构建一个 CSS 图像的时候,开发者随时可以调用paint() 函数,也可以使用刚刚注册好的名字。

下面这段代码展示时如何画一个有颜色的圆型:

registerPaint('circle', class {
  static get inputProperties() { return ['--circle-color']; }
  paint(ctx, geom, properties) {
    // 改变填充颜色
    const color = properties.get('--circle-color');
    ctx.fillStyle = color;
    // 确定圆心和半径
    const x = geom.width / 2;
    const y = geom.height / 2;
    const radius = Math.min(x, y);
    // 画圆
    ctx.beginPath();
    ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
    ctx.fill();
  }
});

在 CSS 中可以这样使用它:

.bubble {
  --circle-color: blue;
  background-image: paint('circle');
}

你将在页面上看到一个以蓝色圆形为背景的元素,它的类是 .bubble,这个圆形背景将始终占满 .bubble 元素。

Worklets

你已经看过了一些和规范相关的代码(比如 registerLayoutregisterPaint),估计现在你想知道的是,这些代码得放在哪里呢?答案就是 worklet 脚本(工作流脚本文件)。

Worklets 的概念和 web worker 类似,它们允许你引入脚本文件并执行特定的 JS 代码,这样的 JS 代码要满足两个条件:第一,可以在渲染流程中调用;第二,和主线程独立。

Worklet 脚本严格控制了开发者所能执行的操作类型,这就保证了性能。

复合滚动和动画

虽然关于 composited scrolling and animation 还没有官方的规范出来,但它可以算是 Houdini 项目中相当广为人知且颇被期待的特性之一。在设想中,这个 API 将会使得开发者能在合成器(compositor)的 worklet (而不是在主线程)中执行程序,还能更改一个 DOM 元素的属性,不过是不会引起渲染引擎重新计算布局或者样式的属性,比如 transformopacity 或者滚动条位置(scroll offset)。

开发者可以通过这个 API 创建高性能的滚动和输入动画,比如滚动头效果、视差效果。你可以在 Github 上看到更多这个 API 试图实现的效果

虽说正式规范还没有确定,但 Chrome 已经在实验性工具中加上了它。事实上 Chrome 的工程师们正在使用这些 API 最终会暴露的语言基元(primitives)来实现 CSS snap pointssticky 定位。这说明了什么?Houdini API 的性能已经足够说服 Chrome 在它之上实现新特性了。单单这一点应该就能说服一直在担心性能问题的的你了吧。

Surma 在 Youtube 上发布了一个模拟 Twitter 应用头部滚动效果的 demo,源码可以在这里查看。

那么现在我们能做什么?

在开头我就说了,我认为所有的 web 开发者都应该关注 Houdini,这个项目将会大大改善我们的未来。即使你可能不会直接接触到 Houdini 规范,但你肯定也会间接享受到它为你带来的便利,毕竟很多特性将基于它被构建出来。

虽然所描绘的美妙未来暂时还不会到来,但它也不会有你想象中那么遥远。今年早些时候,所有主流浏览器厂商都派代表参加了 Houdini 在悉尼的线下会议,在那次会议上,厂商们对于 Houdini 的方向和进程基本都达到了一致。

相信从我说的这些话里,你应该能相信 Houdini 的到来只是时间问题,它一定会成为正式的规范。

浏览器也是软件之一,它当然不可能一次性把所有的特性全加上,肯定是有给特性划分重要程度的。而这个划分方式常常是取决于用户对某个特性的需求度。

所以如果你真的很在意浏览器上样式和布局的可扩展性,如果你真的很想活在 CSS 新特性一出就能直接用进项目的世界里,快去和浏览器的开发者团队联系,告诉他们你真的很需要 Houdini。

另一种参与方式是,把现在不容易或根本不可能实现的、但你希望有一天可以用 CSS 实现的效果列出来, W3C 有提供一个用例文档,你可以向那个 repo 提 pr。如果有的浏览器没有提供那样的文档,那干脆你来帮他们新建一个!

Houdini 项目小组(也可以说是 W3C 的所有成员)真的非常希望听到全世界 web 开发者的声音。事实上很多开发浏览器的人他们本身并不是职业的 web 开发者,像 c++ 程序员是真的不容易明白 web 开发者的痛点啊。

他们需要我们!

 

原文参考资料

特别鸣谢 Houdini 小组成员 Ian Kilpatrick 和 Shane Stephens 帮我 review 这篇文章。
感谢 @寸志 老师对译文的审阅。

来源: Houdini:CSS 领域最令人振奋的革新 – 前端外刊评论 – 知乎专栏

Categories: css3

Javascript 的 Debounce 和 Throttle 的原理及实现 · Issue #7 · lishengzxc/bblog

分析_.debounce.throttle

DOM 上有些事件是会频繁触发的,比如mouseoverscrollresize…。以前有个需求,是做一个图表,是用canvas画的,最初,如果图表画完,用户拖拽浏览器窗口,改变浏览器大小的话,图表并不会自适应的变化,所以就需要监听resize事件,每当窗口大小变化后,再重新绘制。但是resize是频繁触发的,这就导致了页面的明显的卡顿,因为每次resize后的回调要执行大量的计算。

当时比较急,遇到这个问题以后,直接就查了.debounce.throttle,就直接用了lodash。现在回过头了,看下源码,看看它的实现。

Debounce

英文释义:

n. 防反跳

按键防反跳(Debounce)为什么要去抖动呢?机械按键在按下时,并非按下就接触的很好,尤其是有簧片的机械开关,会在接触的瞬间反复的开合多次,直到开关状态完全改变。

我们希望开关只捕获到那次最后的精准的状态切换。在 Javascript 中,那些 DOM 频繁触发的事件,我们想在某个时间点上去执行我们的回调,而不是每次事件每次触发,我们就执行该回调。有点啰嗦,再简单一点的说,我们希望多次触发的相同事件的触发合并为一次触发(其实还是触发了好多次,只是我们只关注那一次)。

所以,在 Javascript 中,我们就希望频繁事件的回调函数在某段连续时间内,在事件触发后只执行一次

resize事件为例,在 2s 内的该事件会被触发多次(具体几次未知,不同浏览器并不一样)。我们需要对resize的回调函数做 Debounce 100ms 化,这样resize的回调会在 2.1s 后触发,之前 2s 以内的resize我就无视了。

我们先自己实现一个

/**
 *
 * @param fn {Function}   实际要执行的函数
 * @param delay {Number}  延迟时间,单位是毫秒(ms)
 *
 * @return {Function}     返回一个“防反跳”了的函数
 */

function debounce(fn, delay) {

  // 定时器,用来 setTimeout
  var timer

  // 返回一个函数,这个函数会在一个时间区间结束后的 delay 毫秒时执行 fn 函数
  return function () {

    // 保存函数调用时的上下文和参数,传递给 fn
    var context = this
    var args = arguments

    // 每次这个返回的函数被调用,就清除定时器,以保证不执行 fn
    clearTimeout(timer)

    // 当返回的函数被最后一次调用后(也就是用户停止了某个连续的操作),
    // 再过 delay 毫秒就执行 fn
    timer = setTimeout(function () {
      fn.apply(context, args)
    }, delay)
  }
}

将 Debounce 化后的闭包函数作为频繁触发的事件的回调,其实还是频繁执行的,只不过,返回的闭包函数内部通过clearTimeout(),让真正需要执行的回调函数不执行了,只有在连续时间内,不在触发频繁事件后的delay秒后,执行真正的回调。

Demo

http://lishengzxc.github.io/bblog/Debounce.html

document.addEventListener('mousemove', debounce(() => console.log(new Date().getTime()), 1000), false);

再鼠标不移动后的 1000ms 后,执行了真正的回调,打印了当前时间戳。

比较合理的应用场景还有,在一个表单的输入框中(包括多行文本输入框),想当用户停止输入后,再ajax

input.addEventListener('keyup', debounce(() => ajax(...), 1000), false);

_.debounce()分析

先看看underscore的。

  _.debounce = function(func, wait, immediate) {
    var timeout, result;

    var later = function(context, args) {
      timeout = null;
      if (args) result = func.apply(context, args);
    };

    var debounced = restArgs(function(args) {
      if (timeout) clearTimeout(timeout);
      if (immediate) {
        var callNow = !timeout;
        timeout = setTimeout(later, wait);
        if (callNow) result = func.apply(this, args);
      } else {
        timeout = _.delay(later, wait, this, args);
      }

      return result;
    });

    debounced.cancel = function() {
      clearTimeout(timeout);
      timeout = null;
    };

    return debounced;
  };

它的debounce还接受第三个参数immediate,这个参数是用来配置回调函数是在一个时间区间的最开始执行(immediatetrue),还是最后执行(immediatefalse),如果immediatetrue,意味着是一个同步的回调,可以传递返回值。

关键的地方是,单独拿出了一个later函数通过控制timer来觉得连续的时间除一开始后,是不是要执行回调。

loadshdebounce,接受更多的配置:

  • [options.leading=false] (boolean): Specify invoking on the leading edge of the timeout.
  • [options.maxWait] (number): The maximum time func is allowed to be delayed before it’s invoked.
  • [options.trailing=true] (boolean): Specify invoking on the trailing edge of the timeout.

lodashdebounce源代码

leading=true等效于underscoreimmediate=truetrailing则正好相反。maxWait设置了超时时间,在规定的超时间后,一定调用回调。(通过内部设置了两个setTimeout,一个用来完成基础功能,让回调只执行一次,还有一个用来控制超时)

Throttle

英文释义:

n. 节流阀

throttle就是设置固定的函数执行速率,从而降低频繁事件回调的执行次数。前面提到的canvas绘制好的图表后,用户改变窗口大小后,重新绘制图表,就很适合使用throttle。最近在写一个 Tank 游戏,用户可以非常快的点击开火,但是我们需要通过 Throttle,来降低一些开火的频率。

我们先自己实现一个

/**
*
* @param fn {Function}   实际要执行的函数
* @param delay {Number}  执行间隔,单位是毫秒(ms)
*
* @return {Function}     返回一个“节流”函数
*/

function throttle(fn, threshhold) {

  // 记录上次执行的时间
  var last

  // 定时器
  var timer

  // 默认间隔为 250ms
  threshhold || (threshhold = 250)

  // 返回的函数,每过 threshhold 毫秒就执行一次 fn 函数
  return function () {

    // 保存函数调用时的上下文和参数,传递给 fn
    var context = this
    var args = arguments

    var now = +new Date()

    // 如果距离上次执行 fn 函数的时间小于 threshhold,那么就放弃
    // 执行 fn,并重新计时
    if (last && now < last + threshhold) {
      clearTimeout(timer)

      // 保证在当前时间区间结束后,再执行一次 fn
      timer = setTimeout(function () {
        last = now
        fn.apply(context, args)
      }, threshhold)

    // 在时间区间的最开始和到达指定间隔的时候执行一次 fn
    } else {
      last = now
      fn.apply(context, args)
    }
  }
}

代码中,比较关键的部分是最后部分的if .. else ..,每次回调执行以后,需要保存执行的函数的时间戳,为了计算以后的事件触发回调时与之前执行回调函数的时间戳的间隔,从而根据间隔判断要不要执行回调。

_.throttle()分析

直接看underscore的。因为lodash没有对其再进行包装。

 _.throttle = function(func, wait, options) {
    var timeout, context, args, result;
    var previous = 0;
    if (!options) options = {};

    var later = function() {
      previous = options.leading === false ? 0 : _.now();
      timeout = null;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    };

    var throttled = function() {
      var now = _.now();
      if (!previous && options.leading === false) previous = now;
      var remaining = wait - (now - previous);
      context = this;
      args = arguments;
      if (remaining <= 0 || remaining > wait) {
        if (timeout) {
          clearTimeout(timeout);
          timeout = null;
        }
        previous = now;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
      } else if (!timeout && options.trailing !== false) {
        timeout = setTimeout(later, remaining);
      }
      return result;
    };

    throttled.cancel = function() {
      clearTimeout(timeout);
      previous = 0;
      timeout = context = args = null;
    };

    return throttled;
  };

其中previous相当于自己实现代码中的last。它还接受leadingtrailing来控制真正回调触发的时机,这和lodash_.debounce 差不太多。

最后

这里有一个可视化分析页面:http://demo.nimius.net/debounce_throttle/,大家可以点开看看。

来源: Javascript 的 Debounce 和 Throttle 的原理及实现 · Issue #7 · lishengzxc/bblog

Categories: JavaScript

说说 Javascript 的 Date · Issue #5 · lishengzxc/bblog

基础的 Date() 就不说了~ :)

如何获得某个月的天数?

不知道大家遇到过这个问题吗?我想如果你们写过日期组件一定有这个问题,我当时的解决方案是这样的:

以下的三个方法,month 参数我都根据 JS 本身对于 Date 的月份定义,采用0为1月

最老实的办法

const EVERY_MONTH_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];

function getDays(year, month) {
  if (month === 1 && isLeap(year)) return 29;
  return EVERY_MONTH_DAYS[month];
}

手动做了每个月天数的映射,如果是2月份并闰年,那天数+1
随便安利一个自己写的 osx 上的日历插件 https://github.com/lishengzxc/ng2-calendar

那没有更好的方法呢?手动 map 和闰年判断的逻辑没有就好了。

稍微 diao 一点的办法

function getDays(year, month) {
  if (month === 1) return new Date(year, month, 29).getMonth() === 1 ? 29 : 28;
  return new Date(year, month, 31).getMonth() === month ? 31 : 30;
}

我们发现,new Date()的第三个参数是可以大于我们所知的每个月的最后一天的的,比如:

new Date(2016, 0, 200) //Mon Jul 18 2016 00:00:00 GMT+0800 (CST)

这样,我们就利用这个 JS 的特性,用29和31这两个关键点,去判断除了那个月的最后一天+1还是那个月吗?(其实28和30是关键点)。

再稍微 diao 一点的方法

function getDays(year, month) {
  return new Date(year, month + 1, 0).getDate();
}

new Date()的第三个参数传小于1的值会怎么样了,比如传0,我们就获得了上个月的最后一天,当然传负数也没问题:

new Date(2016, 0, -200) //Sun Jun 14 2015 00:00:00 GMT+0800 (CST)

Date.prototype.各种String

具体的文档解释懒得再复制一下给大家看,参考链接:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date

这里主要和大家普及以下知识:

GMT(格林尼治平时)

格林尼治平时(又称格林尼治平均时间或格林尼治标准时间,旧译格林威治标准时间;英语:Greenwich Mean Time,GMT)是指位于英国伦敦郊区的皇家格林尼治天文台的标准时间,因为本初子午线被定义在通过那里的经线。

自1924年2月5日开始,格林尼治天文台每隔一小时会向全世界发放调时信息。

理论上来说,格林尼治标准时间的正午是指当太阳横穿格林尼治子午线时(也就是在格林尼治上空最高点时)的时间。由于地球在它的椭圆轨道里的运动速度不均匀,这个时刻可能与实际的太阳时有误差,最大误差达16分钟。

由于地球每天的自转是有些不规则的,而且正在缓慢减速,因此格林尼治时间已经不再被作为标准时间使用。现在的标准时间,是由原子钟报时的协调世界时(UTC)。

所以我们也从 MDN 上的文档看到对于toGMTString()的解释是:

Returns a string representing the Date based on the GMT (UT) time zone. Use toUTCString() instead.

UTC(世界标准时间)

协调世界时,又称世界标准时间或世界协调时间,简称UTC(从英文「Coordinated Universal Time」/法文「Temps Universel Cordonné」而来),是最主要的世界时间标准,其以原子时秒长为基础,在时刻上尽量接近于格林尼治平时

CST(北京时间)

北京时间,China Standard Time,中国标准时间。在时区划分上,属东八区,比协调世界时早8小时,记为UTC+8。

不过这个CST这个缩写比较纠结的是它可以同时代表四个不同的时间:

  • Central Standard Time (USA) UT-6:00
  • Central Standard Time (Australia) UT+9:30
  • China Standard Time UT+8:00
  • Cuba Standard Time UT-4:00

插一个中国地区 JS 客户端时间和服务端时间不一致的问题

总结就是,前后端去传时间的时候,尽量都用 UTC 时间。

ISO 日期和时间的表示方法

if ( !Date.prototype.toISOString ) {
  ( function() {

    function pad(number) {
      if ( number < 10 ) {
        return '0' + number;
      }
      return number;
    }

    Date.prototype.toISOString = function() {
      return this.getUTCFullYear() +
        '-' + pad( this.getUTCMonth() + 1 ) +
        '-' + pad( this.getUTCDate() ) +
        'T' + pad( this.getUTCHours() ) +
        ':' + pad( this.getUTCMinutes() ) +
        ':' + pad( this.getUTCSeconds() ) +
        '.' + (this.getUTCMilliseconds() / 1000).toFixed(3).slice(2, 5) +
        'Z';
    };

  }() );
}

通过 Polyfill 我们就能知道 ISO 是怎么表示时间的,最主要的特征是最后一位是“Z”,然后表示的总是 UTC 时间。

额外的补充

.valueOf() 和 .getTime()

.valueOf()的功能和.getTime()一样。

该方法通常在 JavaScript 内部被调用,而不是在代码中显式调用。什么意思?没有 valueOf,那么Date的实例是不能进行运算的。

var obj = Object.create(null);
obj + 1; // Uncaught TypeError: Cannot convert object to primitive value(…)

.toJSON

直接看这个 API 的名字的时候,我以为会返回一个 JSON 格式的字符串,但其实是这么一个东西

new Date().toJSON() // "2016-05-05T06:03:28.130Z"

其实是这么回事

JSON.stringify(new Date()) // ""2016-05-05T06:06:02.615Z""

那结果能够被 parse 吗?

JSON.parse(JSON.stringify(new Date())) // "2016-05-05T06:19:24.766Z"
JSON.parse('"' + new Date().toJSON() + '"') // "2016-05-05T06:19:24.766Z"

但是结果只是字符串而已。需要再讲这个字符串交给 new Date() 才行。

.toLocaleFormat()

不属于任何标准。在JavaScript 1.6中被实现。似乎也只有 Firefox 自持这个 API,其实正确姿势是用.toLocaleDateString()

.toLocale各种String()

.toLcale各种String(locales [, options]])

妈的这个 API 有点烦,看 MDN 的文档你就知道。这个 API 是用来本地化时间的。

这里稍微说下我对这些参数的理解:

locales

var date = new Date(Date.UTC(2012, 11, 20, 3, 0, 0));

// formats below assume the local time zone of the locale;
// America/Los_Angeles for the US

// US English uses month-day-year order
alert(date.toLocaleString("en-US"));
// → "12/19/2012, 7:00:00 PM"

// British English uses day-month-year order
alert(date.toLocaleString("en-GB"));
// → "20/12/2012 03:00:00"

// Korean uses year-month-day order
alert(date.toLocaleString("ko-KR"));
// → "2012. 12. 20. 오후 12:00:00"

// Arabic in most Arabic speaking countries uses real Arabic digits
alert(date.toLocaleString("ar-EG"));
// → "٢٠‏/١٢‏/٢٠١٢ ٥:٠٠:٠٠ ص"

// for Japanese, applications may want to use the Japanese calendar,
// where 2012 was the year 24 of the Heisei era
alert(date.toLocaleString("ja-JP-u-ca-japanese"));
// → "24/12/20 12:00:00"

// when requesting a language that may not be supported, such as
// Balinese, include a fallback language, in this case Indonesian
alert(date.toLocaleString(["ban", "id"]));
// → "20/12/2012 11.00.00"

locales所指的地区的时区和语言输出。

options

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleString

  • localeMatcher 选择本地匹配的什么算法,似乎没什么大用
  • timeZone 再设置下 UTC 时区
  • hour12 是否12小时制
  • formatMatcher 各日期时间单元的格式化
    • weekday Possible values are "narrow", "short", "long".
    • era Possible values are "narrow", "short", "long".
    • year Possible values are "numeric", "2-digit".
    • month Possible values are "numeric", "2-digit", "narrow", "short", "long".
    • day Possible values are "numeric", "2-digit".
    • hour Possible values are "numeric", "2-digit".
    • minute Possible values are "numeric", "2-digit".
    • second Possible values are "numeric", "2-digit".
    • timeZoneName Possible values are "short", "long".

栗子:

var date = new Date(Date.UTC(2012, 11, 20, 3, 0, 0));

date.toLocaleString("en-US", {hour12: false}); // "12/19/2012, 19:00:00"

var options = {timeZoneName:'long',weekday: "long", year: "2-digit", month: "narrow", day: "numeric"};
date.toLocaleString("en-US", options); // "Thursday, D 20, 12, China Standard Time"

插一个JavaScript 显示 Y-m-d H:i:s 的日期时间格式

老实的方法

let date = new Date();
let result = [
  [
    date.getFullYear(),
    date.getMonth() + 1,
    date.getDate()
  ].join('-'),
  [
    date.getHours(),
    date.getMinutes(),
    date.getSeconds()
  ].join(':')
].join(' ').replace(/\b\d\b/g, '0$&');

diao 一点的方法

var date = new Date();
var result = date.toLocaleString('zh-CN', { hour12: false })
  .replace(/\//g, '-').replace(/\b\d\b/g, '0$&');

一些有用的时间库

来源: 说说 Javascript 的 Date · Issue #5 · lishengzxc/bblog

Categories: JavaScript

【前端性能】高性能滚动 scroll 及页面渲染优化 – ChokCoco – 博客园

最近在研究页面渲染及web动画的性能问题,以及拜读《CSS SECRET》(CSS揭秘)这本大作。

本文主要想谈谈页面优化之滚动优化。

主要内容包括了为何需要优化滚动事件,滚动与页面渲染的关系,节流与防抖,pointer-events:none 优化滚动。因为本文涉及了很多很多基础,可以对照上面的知识点,选择性跳到相应地方阅读。

 

   滚动优化的由来

滚动优化其实也不仅仅指滚动(scroll 事件),还包括了例如 resize 这类会频繁触发的事件。简单的看看:

1
2
3
4
var i = 0;
window.addEventListener('scroll',function(){
    console.log(i++);
},false);

输出如下:

在绑定 scroll 、resize 这类事件时,当它发生时,它被触发的频次非常高,间隔很近。如果事件中涉及到大量的位置计算、DOM 操作、元素重绘等工作且这些工作无法在下一个 scroll 事件触发前完成,就会造成浏览器掉帧。加之用户鼠标滚动往往是连续的,就会持续触发 scroll 事件导致掉帧扩大、浏览器 CPU 使用率增加、用户体验受到影响。

在滚动事件中绑定回调应用场景也非常多,在图片的懒加载、下滑自动加载数据、侧边浮动导航栏等中有着广泛的应用。

当用户浏览网页时,拥有平滑滚动经常是被忽视但却是用户体验中至关重要的部分。当滚动表现正常时,用户就会感觉应用十分流畅,令人愉悦,反之,笨重不自然卡顿的滚动,则会给用户带来极大不舒爽的感觉。

 

   滚动与页面渲染的关系

为什么滚动事件需要去优化?因为它影响了性能。那它影响了什么性能呢?额……这个就要从页面性能问题由什么决定说起。

我觉得搞技术一定要追本溯源,不要看到别人一篇文章说滚动事件会导致卡顿并说了一堆解决方案优化技巧就如获至宝奉为圭臬,我们需要的不是拿来主义而是批判主义,多去源头看看。

从问题出发,一步一步寻找到最后,就很容易找到问题的症结所在,只有这样得出的解决方法才容易记住。

说教了一堆废话,不喜欢的直接忽略哈,回到正题,要找到优化的入口就要知道问题出在哪里,对于页面优化而言,那么我们就要知道页面的渲染原理:

浏览器渲染原理我在我上一篇文章里也要详细的讲到,不过更多的是从动画渲染的角度去讲的:【Web动画】CSS3 3D 行星运转 && 浏览器渲染原理 。

想了想,还是再简单的描述下,我发现每次 review 这些知识点都有新的收获,这次换一张图,以 chrome 为例子,一个 Web 页面的展示,简单来说可以认为经历了以下下几个步骤:

  • JavaScript:一般来说,我们会使用 JavaScript 来实现一些视觉变化的效果。比如做一个动画或者往页面里添加一些 DOM 元素等。
  • Style:计算样式,这个过程是根据 CSS 选择器,对每个 DOM 元素匹配对应的 CSS 样式。这一步结束之后,就确定了每个 DOM 元素上该应用什么 CSS 样式规则。
  • Layout:布局,上一步确定了每个 DOM 元素的样式规则,这一步就是具体计算每个 DOM 元素最终在屏幕上显示的大小和位置。web 页面中元素的布局是相对的,因此一个元素的布局发生变化,会联动地引发其他元素的布局发生变化。比如,<body> 元素的宽度的变化会影响其子元素的宽度,其子元素宽度的变化也会继续对其孙子元素产生影响。因此对于浏览器来说,布局过程是经常发生的。
  • Paint:绘制,本质上就是填充像素的过程。包括绘制文字、颜色、图像、边框和阴影等,也就是一个 DOM 元素所有的可视效果。一般来说,这个绘制过程是在多个层上完成的。
  • Composite:渲染层合并,由上一步可知,对页面中 DOM 元素的绘制是在多个层上进行的。在每个层上完成绘制过程之后,浏览器会将所有层按照合理的顺序合并成一个图层,然后显示在屏幕上。对于有位置重叠的元素的页面,这个过程尤其重要,因为一旦图层的合并顺序出错,将会导致元素显示异常。

这里又涉及了层(GraphicsLayer)的概念,GraphicsLayer 层是作为纹理(texture)上传给 GPU 的,现在经常能看到说 GPU 硬件加速,就和所谓的层的概念密切相关。但是和本文的滚动优化相关性不大,有兴趣深入了解的可以自行 google 更多。

简单来说,网页生成的时候,至少会渲染(Layout+Paint)一次。用户访问的过程中,还会不断重新的重排(reflow)和重绘(repaint)。

其中,用户 scroll 和 resize 行为(即是滑动页面和改变窗口大小)会导致页面不断的重新渲染。

当你滚动页面时,浏览器可能会需要绘制这些层(有时也被称为合成层)里的一些像素。通过元素分组,当某个层的内容改变时,我们只需要更新该层的结构,并仅仅重绘和栅格化渲染层结构里变化的那一部分,而无需完全重绘。显然,如果当你滚动时,像视差网站(戳我看看)这样有东西在移动时,有可能在多层导致大面积的内容调整,这会导致大量的绘制工作。

 

   防抖(Debouncing)和节流(Throttling)

scroll 事件本身会触发页面的重新渲染,同时 scroll 事件的 handler 又会被高频度的触发, 因此事件的 handler 内部不应该有复杂操作,例如 DOM 操作就不应该放在事件处理中。

针对此类高频度触发事件问题(例如页面 scroll ,屏幕 resize,监听用户输入等),下面介绍两种常用的解决方法,防抖和节流。

防抖(Debouncing)

防抖技术即是可以把多个顺序地调用合并成一次,也就是在一定时间内,规定事件被触发的次数。

通俗一点来说,看看下面这个简化的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 简单的防抖动函数
function debounce(func, wait, immediate) {
    // 定时器变量
    var timeout;
    return function() {
        // 每次触发 scroll handler 时先清除定时器
        clearTimeout(timeout);
        // 指定 xx ms 后触发真正想进行的操作 handler
        timeout = setTimeout(func, wait);
    };
};
// 实际想绑定在 scroll 事件上的 handler
function realFunc(){
    console.log("Success");
}
// 采用了防抖动
window.addEventListener('scroll',debounce(realFunc,500));
// 没采用防抖动
window.addEventListener('scroll',realFunc);

上面简单的防抖的例子可以拿到浏览器下试一下,大概功能就是如果 500ms 内没有连续触发两次 scroll 事件,那么才会触发我们真正想在 scroll 事件中触发的函数。

上面的示例可以更好的封装一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 防抖动函数
function debounce(func, wait, immediate) {
    var timeout;
    return function() {
        var context = this, args = arguments;
        var later = function() {
            timeout = null;
            if (!immediate) func.apply(context, args);
        };
        var callNow = immediate && !timeout;
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
        if (callNow) func.apply(context, args);
    };
};
var myEfficientFn = debounce(function() {
    // 滚动中的真正的操作
}, 250);
// 绑定监听
window.addEventListener('resize', myEfficientFn);

节流(Throttling)

防抖函数确实不错,但是也存在问题,譬如图片的懒加载,我希望在下滑过程中图片不断的被加载出来,而不是只有当我停止下滑时候,图片才被加载出来。又或者下滑时候的数据的 ajax 请求加载也是同理。

这个时候,我们希望即使页面在不断被滚动,但是滚动 handler 也可以以一定的频率被触发(譬如 250ms 触发一次),这类场景,就要用到另一种技巧,称为节流函数(throttling)。

节流函数,只允许一个函数在 X 毫秒内执行一次。

与防抖相比,节流函数最主要的不同在于它保证在 X 毫秒内至少执行一次我们希望触发的事件 handler。

与防抖相比,节流函数多了一个 mustRun 属性,代表 mustRun 毫秒内,必然会触发一次 handler ,同样是利用定时器,看看简单的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 简单的节流函数
function throttle(func, wait, mustRun) {
    var timeout,
        startTime = new Date();
    return function() {
        var context = this,
            args = arguments,
            curTime = new Date();
        clearTimeout(timeout);
        // 如果达到了规定的触发时间间隔,触发 handler
        if(curTime - startTime >= mustRun){
            func.apply(context,args);
            startTime = curTime;
        // 没达到触发间隔,重新设定定时器
        }else{
            timeout = setTimeout(func, wait);
        }
    };
};
// 实际想绑定在 scroll 事件上的 handler
function realFunc(){
    console.log("Success");
}
// 采用了节流函数
window.addEventListener('scroll',throttle(realFunc,500,1000));

上面简单的节流函数的例子可以拿到浏览器下试一下,大概功能就是如果在一段时间内 scroll 触发的间隔一直短于 500ms ,那么能保证事件我们希望调用的 handler 至少在 1000ms 内会触发一次。

 

   使用 rAF(requestAnimationFrame)触发滚动事件

上面介绍的抖动与节流实现的方式都是借助了定时器 setTimeout ,但是如果页面只需要兼容高版本浏览器或应用在移动端,又或者页面需要追求高精度的效果,那么可以使用浏览器的原生方法 rAF(requestAnimationFrame)。

requestAnimationFrame

window.requestAnimationFrame() 这个方法是用来在页面重绘之前,通知浏览器调用一个指定的函数。这个方法接受一个函数为参,该函数会在重绘前调用。

rAF 常用于 web 动画的制作,用于准确控制页面的帧刷新渲染,让动画效果更加流畅,当然它的作用不仅仅局限于动画制作,我们可以利用它的特性将它视为一个定时器。(当然它不是定时器)

通常来说,rAF 被调用的频率是每秒 60 次,也就是 1000/60 ,触发频率大概是 16.7ms 。(当执行复杂操作时,当它发现无法维持 60fps 的频率时,它会把频率降低到 30fps 来保持帧数的稳定。)

简单而言,使用 requestAnimationFrame 来触发滚动事件,相当于上面的:

1
throttle(func, xx, 1000/60) //xx 代表 xx ms内不会重复触发事件 handler

简单的示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var ticking = false; // rAF 触发锁
function onScroll(){
  if(!ticking) {
    requestAnimationFrame(realFunc);
    ticking = true;
  }
}
function realFunc(){
    // do something...
    console.log("Success");
    ticking = false;
}
// 滚动事件监听
window.addEventListener('scroll', onScroll, false);

上面简单的使用 rAF 的例子可以拿到浏览器下试一下,大概功能就是在滚动的过程中,保持以 16.7ms 的频率触发事件 handler。

使用 requestAnimationFrame 优缺点并存,首先我们不得不考虑它的兼容问题,其次因为它只能实现以 16.7ms 的频率来触发,代表它的可调节性十分差。但是相比 throttle(func, xx, 16.7) ,用于更复杂的场景时,rAF 可能效果更佳,性能更好。

总结一下

  • 防抖动:防抖技术即是可以把多个顺序地调用合并成一次,也就是在一定时间内,规定事件被触发的次数。
  • 节流函数:只允许一个函数在 X 毫秒内执行一次,只有当上一次函数执行后过了你规定的时间间隔,才能进行下一次该函数的调用。
  • rAF:16.7ms 触发一次 handler,降低了可控性,但是提升了性能和精确度。

 

   简化 scroll 内的操作

上面介绍的方法都是如何去优化 scroll 事件的触发,避免 scroll 事件过度消耗资源的。

但是从本质上而言,我们应该尽量去精简 scroll 事件的 handler ,将一些变量的初始化、不依赖于滚动位置变化的计算等都应当在 scroll 事件外提前就绪。

建议如下:

避免在scroll 事件中修改样式属性 / 将样式操作从 scroll 事件中剥离

 

输入事件处理函数,比如 scroll / touch 事件的处理,都会在 requestAnimationFrame 之前被调用执行。

因此,如果你在 scroll 事件的处理函数中做了修改样式属性的操作,那么这些操作会被浏览器暂存起来。然后在调用 requestAnimationFrame 的时候,如果你在一开始做了读取样式属性的操作,那么这将会导致触发浏览器的强制同步布局。

 

   滑动过程中尝试使用 pointer-events: none 禁止鼠标事件

大部分人可能都不认识这个属性,嗯,那么它是干什么用的呢?

pointer-events 是一个 CSS 属性,可以有多个不同的值,属性的一部分值仅仅与 SVG 有关联,这里我们只关注 pointer-events: none 的情况,大概的意思就是禁止鼠标行为,应用了该属性后,譬如鼠标点击,hover 等功能都将失效,即是元素不会成为鼠标事件的 target。

可以就近 F12 打开开发者工具面板,给 <body> 标签添加上 pointer-events: none 样式,然后在页面上感受下效果,发现所有鼠标事件都被禁止了。

那么它有什么用呢?

pointer-events: none 可用来提高滚动时的帧频。的确,当滚动时,鼠标悬停在某些元素上,则触发其上的 hover 效果,然而这些影响通常不被用户注意,并多半导致滚动出现问题。对 body 元素应用 pointer-events: none ,禁用了包括 hover 在内的鼠标事件,从而提高滚动性能。

1
2
3
.disable-hover {
    pointer-events: none;
}

大概的做法就是在页面滚动的时候, 给 <body> 添加上 .disable-hover 样式,那么在滚动停止之前, 所有鼠标事件都将被禁止。当滚动结束之后,再移除该属性。

可以查看这个 demo 页面。

上面说 pointer-events: none 可用来提高滚动时的帧频 的这段话摘自 pointer-events-MDN ,还专门有文章讲解过这个技术:

使用pointer-events:none实现60fps滚动

这就完了吗?没有,张鑫旭有一篇专门的文章,用来探讨 pointer-events: none 是否真的能够加速滚动性能,并提出了自己的质疑:

pointer-events:none提高页面滚动时候的绘制性能?

结论见仁见智,使用 pointer-events: none 的场合要依据业务本身来定夺,拒绝拿来主义,多去源头看看,动手实践一番再做定夺。

 

其他参考文献(都是好文章,值得一读):

 

到此本文结束,如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

如果本文对你有帮助,请点下推荐,写文章不容易。

来源: 【前端性能】高性能滚动 scroll 及页面渲染优化 – ChokCoco – 博客园

Categories: html5, JavaScript

「理解HTTP」之常见的状态码 – 魅族科技开发团队 – SegmentFault

状态码的职责是当客户端向服务器端发送请求时,描述返回请求结果。借助状态码,用户可以知道服务器端是正常处理了请求,还是出现了什么错误。

RFC2616定义的状态码,由3位数字和原因短信组成。
数字中的第一位指定了响应类别,后两位无分类。响应类别有以下5种:

Type Reason-phrase Note
1XX Informational 信息性状态码,表示接受的请求正在处理
2XX Success 成功状态码,表示请求正常处理完毕
3XX Redirection 重定向状态码,表示需要客户端需要进行附加操作
4XX Client Error 客户端错误状态码,表示服务器无法处理请求
5XX Server Error 服务器错误状态码,表示服务器处理请求出错

RFC2616记录的HTTP状态码有37种,再加上「WebDAV」(RFC49185842)和「Additional HTTP Status Codes」(RFC6585),数量就达到60多种。
然并卵,这么多种HTTP状态码,其实常用的大概只有14种,本文就讲讲这14种状态码。

2XX Success

This class of status code indicates that the client’s request was successfully received, understood, and accepted.

2xx 响应结果表示从客户端发来的请求在服务器端被正常处理了。

200 OK

请求被成功处理,服务器会根据不同的请求方法返回结果:
GET:请求的对应资源会作为响应返回。
HEAD:请求的对应资源的响应头(entity-header)会作为响应返回,不包括响应体(message-body)。
POST:返回处理对应请求的结果。

204 No Content

该状态码表示服务器接收到的请求已经处理完毕,但是服务器不需要返回响应体.
比如,客户端是浏览器的话,发出的请求返回204响应,那么浏览器显示的页面不会发生更新。

206 Partial Content

该状态码表示客户端进行了范围请求,而服务器成功执行了这部分的GET请求。
客户端发起的请求,必须在请求头中包含Range字段。服务端响应报文中,必须包含由Content-Range指定范围的实体内容(entity-bodies )

3XX Redirection

This class of status code indicates that further action needs to be taken by the user agent in order to fulfill the request.

3XX 响应结果表明浏览器需要执行某些特殊的处理以完成请求。

301 Moved Permanently

永久性重定向。该状态码表示请求的资源已经被分配了新的URI,并且以后使用资源现在所指的URI。并且根据请求的方法有不同的处理方式:
HEAD:必须在响应头部Location字段中指明新的永久性的URI。
GET:除了有Location字段以外,还需要在响应体中附上永久性URI的超链接文本。
POST:客户端在发送POST请求,受到301响应之后,不应该自动跳转URI,应当让用户确认跳转。

比如,如果一个URI已经在浏览器中被收藏为书签,这时应该按照Location首部字段提示的URI重新保存。
例如建立一个收藏的书签:

http://wan.bigertech.com

当访问这个书签的时候,请求会被重定向到

http://wan.meizu.com

并且对应的书签会被改变,指向http://wan.meizu.com
不信?Try yourself.

302 Found

临时性重定向。该状态码表示请求的资源已被分配了新的URI,希望用户本次能使用新的URI访问。
和301 Moved Permanently 状态码相似,但302状态码代表的资源不是被永久移动,只是临时性质的。
如果,用户把一个URI收藏为书签,302响应是不会像301那样去更新书签。

303 See Other

该状态码表示由于请求对应的资源存在另一个URI,应使用GET方法定向获取请求的资源。303与302不同之处在于,302是不会改变请求的方法,如果请求方法是POST的话,重定向的请求也应该是POST。而对于303,使用POST请求的话,重定向的请求应该是GET请求。
但是有一点是需要注意的,许多HTTP/1.1版以前的浏览器不能正确理解303状态码,很多现存的浏览器讲302响应视为303响应,并且使用GET方式访问Location中规定的的URI,而无视原先请求的方法。
在RFC2616中有相关的这样一段原文:

Note: Many pre-HTTP/1.1 user agents do not understand the 303 status. When interoperability with such clients is a concern, the 302 status code may be used instead, since most user agents react to a 302 response as described here for 303.

304 Not Modified

该状态码表示客户端发送附带条件请求时,服务器端允许请求访问资源,但未满足条件的情况。304状态码返回时,不包含任何响应的主题部分。附带条件的请求指的是采用GET方法的请求头中包含:If-MatchIf-Modified-SinceIf-None-MatchIf-RangeIf-Unmodified-Since中任一首部。

307 Temporary Redirect

临时重定向。该状态码与302和303的有着类似的含义,不同之处在于,307状态码并不会指定客户端要用什么样的请求方法请求重定向地址。(302指定使用原有请求方法,303指定使用GET方法)

4XX Client Error

The 4xx class of status code is intended for cases in which the client seems to have erred.

4XX 的响应结果表明客户端是发生错误的原因所在

400 Bad Request

表示该请求报文中存在语法错误,导致服务器无法理解该请求。客户端需要修改请求的内容后再次发送请求。

401 Unauthorized

该状态码表示发送的请求需要有通过HTTP认证(Basic认证,Digest认证)的认证信息。返回含有401的响应,必须在头部包含WWW-Authenticate以指明服务器需要哪种方式的认证。
当客户端再次请求该资源的时候,需要在请求头中的Authorization包含认证信息。
更多关于认证授权的信息关注RFC2617

403 Forbidden

该状态码表明对请求资源的访问被服务器拒绝了。服务器没有必要给出拒绝的详细理由,但如果想做说明的话,可以在实体的主体部分原因进行描述,这样就能让用户看到了。
未获得文件系统的访问权限,访问权限出现某些问题,从未授权的发送源IP地址试图访问等情况都可能发生403响应。

404 Not Found

该状态码表明服务器上无法找到指定的资源。通常被用于服务器不想透露拒绝请求的原因,或者没有其他的响应可提供。

5XX Server Error

Response status codes beginning with the digit “5” indicate cases in which the server is aware that it has erred or is incapable of performing the request.

5XX 的响应结果表明服务器本身发生错误,或者没有足够的能力来处理请求。

500 Internal Server Error

该状态码表明服务器端在执行请求时发生了错误。也有可能是Web应用存在的BUG或某些临时的故障。

503 Service Unavailable

该状态码表明服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。如果事先得知解除以上需要的时间,最好写入Retry-After首部字段再返回给客户端。

来源: 「理解HTTP」之常见的状态码 – 魅族科技开发团队 – SegmentFault

Categories: html5

HTTP header详解_PHP程序员,雷雪松的博客

HTTP(HyperText Transfer Protocol)即超文本传输协议,所有的WWW文件都必须遵守这个标准。设计HTTP最初的目的是为了提供一种发布和接收HTML页面的方法。HTTP协议采用了请求/响应模型,浏览器或其他客户端发出请求,服务器给与响应。HTTP header 大体分为Request和Response两部分。

Requests部分

Header 解释 示例
Accept 指定客户端能够接收的内容类型 Accept: text/plain, text/html
Accept-Charset 浏览器可以接受的字符编码集。 Accept-Charset: iso-8859-5
DontTrackMeHere 指定浏览器可以支持的web服务器返回内容压缩编码类型。 DontTrackMeHere: compress, gzip
Accept-Language 浏览器可接受的语言 Accept-Language: en,zh
Accept-Ranges 可以请求网页实体的一个或者多个子范围字段 Accept-Ranges: bytes
Authorization HTTP授权的授权证书 Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
Cache-Control 指定请求和响应遵循的缓存机制 Cache-Control: no-cache
Connection 表示是否需要持久连接。(HTTP 1.1默认进行持久连接) Connection: close
Cookie HTTP请求发送时,会把保存在该请求域名下的所有cookie值一起发送给web服务器。 Cookie: $Version=1; Skin=new;
Content-Length 请求的内容长度 Content-Length: 348
Content-Type 请求的与实体对应的MIME信息 Content-Type: application/x-www-form-urlencoded
Date 请求发送的日期和时间 Date: Tue, 15 Nov 2010 08:12:31 GMT
Expect 请求的特定的服务器行为 Expect: 100-continue
From 发出请求的用户的Email From: raykaeso@leixuesong.cn
Host 指定请求的服务器的域名和端口号 Host: www.leixuesong.cn
If-Match 只有请求内容与实体相匹配才有效 If-Match: “737060cd8c284d8af7ad3082f209582d”
If-Modified-Since 如果请求的部分在指定时间之后被修改则请求成功,未被修改则返回304代码 If-Modified-Since: Sat, 29 Oct 2010 19:43:31 GMT
If-None-Match 如果内容未改变返回304代码,参数为服务器先前发送的Etag,与服务器回应的Etag比较判断是否改变 If-None-Match: “737060cd8c284d8af7ad3082f209582d”
If-Range 如果实体未改变,服务器发送客户端丢失的部分,否则发送整个实体。参数也为Etag If-Range: “737060cd8c284d8af7ad3082f209582d”
If-Unmodified-Since 只在实体在指定时间之后未被修改才请求成功 If-Unmodified-Since: Sat, 29 Oct 2010 19:43:31 GMT
Max-Forwards 限制信息通过代理和网关传送的时间 Max-Forwards: 10
Pragma 用来包含实现特定的指令 Pragma: no-cache
Proxy-Authorization 连接到代理的授权证书 Proxy-Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
Range 只请求实体的一部分,指定范围 Range: bytes=500-999
Referer 先前网页的地址,当前请求网页紧随其后,即来路 Referer: www.leixuesong.cn
TE 客户端愿意接受的传输编码,并通知服务器接受接受尾加头信息 TE: trailers,deflate;q=0.5
Upgrade 向服务器指定某种传输协议以便服务器进行转换(如果支持) Upgrade: HTTP/2.0, SHTTP/1.3, IRC/6.9, RTA/x11
User-Agent User-Agent的内容包含发出请求的用户信息 User-Agent: Mozilla/5.0 (Linux; X11)
Via 通知中间网关或代理服务器地址,通信协议 Via: 1.0 fred, 1.1 nowhere.com (Apache/1.1)
Warning 关于消息实体的警告信息 Warn: 199 Miscellaneous warning

Responses 部分

Header 解释 示例
Accept-Ranges 表明服务器是否支持指定范围请求及哪种类型的分段请求 Accept-Ranges: bytes
Age 从原始服务器到代理缓存形成的估算时间(以秒计,非负) Age: 12
Allow 对某网络资源的有效的请求行为,不允许则返回405 Allow: GET, HEAD
Cache-Control 告诉所有的缓存机制是否可以缓存及哪种类型 Cache-Control: no-cache
Content-Encoding web服务器支持的返回内容压缩编码类型。 Content-Encoding: gzip
Content-Language 响应体的语言 Content-Language: en,zh
Content-Length 响应体的长度 Content-Length: 348
Content-Location 请求资源可替代的备用的另一地址 Content-Location: /index.htm
Content-MD5 返回资源的MD5校验值 Content-MD5: Q2hlY2sgSW50ZWdyaXR5IQ==
Content-Range 在整个返回体中本部分的字节位置 Content-Range: bytes 21010-47021/47022
Content-Type 返回内容的MIME类型 Content-Type: text/html; charset=utf-8
Date 原始服务器消息发出的时间 Date: Tue, 15 Nov 2010 08:12:31 GMT
ETag 请求变量的实体标签的当前值 ETag: “737060cd8c284d8af7ad3082f209582d”
Expires 响应过期的日期和时间 Expires: Thu, 01 Dec 2010 16:00:00 GMT
Last-Modified 请求资源的最后修改时间 Last-Modified: Tue, 15 Nov 2010 12:45:26 GMT
Location 用来重定向接收方到非请求URL的位置来完成请求或标识新的资源 Location: http://www.leixuesong.cn/724
Pragma 包括实现特定的指令,它可应用到响应链上的任何接收方 Pragma: no-cache
Proxy-Authenticate 它指出认证方案和可应用到代理的该URL上的参数 Proxy-Authenticate: Basic
refresh 应用于重定向或一个新的资源被创造,在5秒之后重定向(由网景提出,被大部分浏览器支持)
Retry-After 如果实体暂时不可取,通知客户端在指定时间之后再次尝试 Retry-After: 120
Server web服务器软件名称 Server: Apache/1.3.27 (Unix) (Red-Hat/Linux)
Set-Cookie 设置Http Cookie Set-Cookie: UserID=raykaeso; Max-Age=3600; Version=1
Trailer 指出头域在分块传输编码的尾部存在 Trailer: Max-Forwards
Transfer-Encoding 文件传输编码 Transfer-Encoding:chunked
Vary 告诉下游代理是使用缓存响应还是从原始服务器请求 Vary: *
Via 告知代理客户端响应是通过哪里发送的 Via: 1.0 fred, 1.1 nowhere.com (Apache/1.1)
Warning 警告实体可能存在的问题 Warning: 199 Miscellaneous warning
WWW-Authenticate 表明客户端请求实体应该使用的授权方案 WWW-Authenticate: Basic
X-Cache 表示你的 http request 是由 proxy server 返回的 X-Cache:HIT TCP_MEM_HIT dirn:-2:-2

来源: HTTP header详解_PHP程序员,雷雪松的博客

Categories: html5