聊聊 SVG 基本形状转换那些事

前言

前段时间研究 SVG 压缩优化,发现SVG预定义的 rectcircleellipselinepolylinepolygon 六种基本形状可通过path路径转换实现,这样可以在一定程度上减少代码量。不仅如此,我们常用的 SVG Path 动画(路径动画),是以操作path中两个属性值stroke-dasharraystroke-dashoffset来实现,基本形状转换为path路径,有利于实现路径动画。

SVG基本形状

SVG 提供了rectcircleellipselinepolylinepolygon六种基本形状用于图形绘制,这些形状可以直接用来绘制一些基本的形状,如矩形、椭圆等,而复杂图形的绘制则需要使用 path 路径来实现。

svg基本形状

1.rect 矩形

1
2
<rect x="10" y="10" width="30" height="30"/>
<rect x="60" y="10" rx="10" ry="10" width="30" height="30"/>

SVG中rect元素用于绘制矩形、圆角矩形,含有6个基本属性用于控制矩形的形状以及坐标,具体如下:

1
2
3
4
5
6
x       矩形左上角x位置, 默认值为 0 
y 矩形左上角y位置, 默认值为 0
width 矩形的宽度, 不能为负值否则报错, 0 值不绘制
height 矩形的高度, 不能为负值否则报错, 0 值不绘制
rx 圆角x方向半径, 不能为负值否则报错
ry 圆角y方向半径, 不能为负值否则报错

这里需要注意,rxry 的还有如下规则:

  • rxry 都没有设置, 则 rx = 0 ry = 0
  • rxry 有一个值为0, 则相当于 rx = 0 ry = 0,圆角无效
  • rxry 有一个被设置, 则全部取这个被设置的值
  • rx 的最大值为 width 的一半, ry 的最大值为 height 的一半
1
2
3
4
5
6
7
8
9
10
rx = rx || ry || 0;
ry = ry || rx || 0;

rx = rx > width / 2 ? width / 2 : rx;
ry = ry > height / 2 ? height / 2 : ry;

if(0 === rx || 0 === ry){
rx = 0,
ry = 0; //圆角不生效,等同于,rx,ry都为0
}

2.circle 圆形

1
<circle cx="100" cy="100" r="50" fill="#fff"></circle>

SVG中circle元素用于绘制圆形,含有3个基本属性用于控制圆形的坐标以及半径,具体如下:

1
2
3
r       半径
cx 圆心x位置, 默认为 0
cy 圆心y位置, 默认为 0

3.ellipse 椭圆

1
<ellipse cx="75" cy="75" rx="20" ry="5"/>

SVG中ellipse元素用于绘制椭圆,是circle元素更通用的形式,含有4个基本属性用于控制椭圆的形状以及坐标,具体如下:

1
2
3
4
rx      椭圆x半径
ry 椭圆y半径
cx 圆心x位置, 默认为 0
cy 圆心y位置, 默认为 0

4.line 直线

1
<line x1="10" x2="50" y1="110" y2="150"/>

Line绘制直线。它取两个点的位置作为属性,指定这条线的起点和终点位置。

1
2
3
4
x1 起点的x位置
y1 起点的y位置
x2 终点的x位置
y2 终点的y位置

5.polyline 折线

1
<polyline points="60 110, 65 120, 70 115, 75 130, 80 125, 85 140, 90 135, 95 150, 100 145"/>

polyline是一组连接在一起的直线。因为它可以有很多的点,折线的的所有点位置都放在一个points属性中:

1
points 点集数列,每个数字用空白、逗号、终止命令符或者换行符分隔开,每个点必须包含2个数字,一个是x坐标,一个是y坐标 如0 0, 1 1, 2 2”

6.polygon 多边形

1
<polygon points="50 160, 55 180, 70 180, 60 190, 65 205, 50 195, 35 205, 40 190, 30 180, 45 180"/>

polygon和折线很像,它们都是由连接一组点集的直线构成。不同的是,polygon的路径在最后一个点处自动回到第一个点。需要注意的是,矩形也是一种多边形,如果需要更多灵活性的话,你也可以用多边形创建一个矩形。

1
points 点集数列,每个数字用空白、逗号、终止命令符或者换行符分隔开,每个点必须包含2个数字,一个是x坐标,一个是y坐标 如0 0, 1 1, 2 2, 路径绘制完闭合图形”

SVG path 路径

SVG 的路径<path>功能非常强大,它不仅能创建其他基本形状,还能创建更多复杂的形状。<path>路径是由一些命令来控制的,每一个命令对应一个字母,并且区分大小写,大写主要表示绝对定位,小写表示相对定位。<path> 通过属性 d 来定义路径, d 是一系列命令的集合,主要有以下几个命令:

svg基本形状

通常大部分形状,都可以通过指令M(m)L(l)H(h)V(v)A(a)来实现,注意特别要区分大小写,相对与绝对坐标情况,转换时推荐使用相对路径可减少代码量,例如:

1
2
3
4
5
6
7
// 以下两个等价
d='M 10 10 20 20' // (10, 10) (20 20) 都是绝对坐标
d='M 10 10 L 20 20'

// 以下两个等价
d='m 10 10 20 20' // (10, 10) 绝对坐标, (20 20) 相对坐标
d='M 10 10 l 20 20'

SVG 基本形状路径转换原理

1.rect to path

如下图所示,一个 rect 是由 4 个弧和 4 个线段构成;如果 rect 没有设置 rx 和 ry 则 rect 只是由 4 个线段构成。rect 转换为 path 只需要将 A ~ H 之间的弧和线段依次实现即可。

svg基本形状

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
function rect2path(x, y, width, height, rx, ry) {
/*
* rx 和 ry 的规则是:
* 1. 如果其中一个设置为 0 则圆角不生效
* 2. 如果有一个没有设置则取值为另一个
*/
rx = rx || ry || 0;
ry = ry || rx || 0;

//非数值单位计算,如当宽度像100%则移除
if (isNaN(x - y + width - height + rx - ry)) return;

rx = rx > width / 2 ? width / 2 : rx;
ry = ry > height / 2 ? height / 2 : ry;

//如果其中一个设置为 0 则圆角不生效
if(0 == rx || 0 == ry){
// var path =
// 'M' + x + ' ' + y +
// 'H' + (x + width) + 不推荐用绝对路径,相对路径节省代码量
// 'V' + (y + height) +
// 'H' + x +
// 'z';
var path =
'M' + x + ' ' + y +
'h' + width +
'v' + height +
'h' + -width +
'z';
}else{
var path =
'M' + x + ' ' + (y+ry) +
'a' + rx + ' ' + ry + ' 0 0 1 ' + rx + ' ' + (-ry) +
'h' + (width - rx - rx) +
'a' + rx + ' ' + ry + ' 0 0 1 ' + rx + ' ' + ry +
'v' + (height - ry -ry) +
'a' + rx + ' ' + ry + ' 0 0 1 ' + (-rx) + ' ' + ry +
'h' + (rx + rx -width) +
'a' + rx + ' ' + ry + ' 0 0 1 ' + (-rx) + ' ' + (-ry) +
'z';
}

return path;
}

2.circle/ellipse to path

圆可视为是一种特殊的椭圆,即 rx 与 ry 相等的椭圆,所以可以放在一起讨论。 椭圆可以看成A点到C做180度顺时针画弧、C点到A做180度顺时针画弧即可。

svg基本形状

1
2
3
4
5
6
7
8
9
10
11
12
function ellipse2path(cx, cy, rx, ry) {
//非数值单位计算,如当宽度像100%则移除
if (isNaN(cx - cy + rx - ry)) return;

var path =
'M' + (cx-rx) + ' ' + cy +
'a' + rx + ' ' + ry + ' 0 1 0 ' + 2*rx + ' 0' +
'a' + rx + ' ' + ry + ' 0 1 0 ' + (-2*rx) + ' 0' +
'z';

return path;
}

3.line to path

相对来说比较简单,如下:

1
2
3
4
5
6
7
8
9
10
11
12
function line2path(x1, y1, x2, y2) {
//非数值单位计算,如当宽度像100%则移除
if (isNaN(x1 - y1 + x2 - y2)) return;

x1 = x1 || 0;
y1 = y1 || 0;
x2 = x2 || 0;
y2 = y2 || 0;

var path = 'M' + x1 + ' '+ y1 + 'L' + x2 + ' ' + y2;
return path;
}

4.polyline/polygon to path

polyline折线、polygon多边形的转换为path比较类似,差别就是polygon多边形会闭合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// polygon折线转换
points = [x1, y1, x2, y2, x3, y3 ...];
function polyline2path (points) {
var path = 'M' + points.slice(0,2).join(' ') +
'L' + points.slice(2).join(' ');
return path;
}

// polygon多边形转换
points = [x1, y1, x2, y2, x3, y3 ...];
function polygon2path (points) {
var path = 'M' + points.slice(0,2).join(' ') +
'L' + points.slice(2).join(' ') + 'z';
return path;
}

convertpath转换工具

为了方便处理SVG基本元素路径转换,本人抽空写了convertpath工具,具体如下:

安装:

1
npm i convertpath

使用:

1
2
3
4
5
6
7
8
9
10
const parse = require('convertpath');
parse.parse("./test/test.svg")
/**
* <circle cx="500" cy="500" r="20" fill="red"/>
*/
console.log(parse.toSimpleSvg())

/**
* <path d="M500,500,m-20,0,a20,20,0,1,0,40,0,a20,20,0,1,0,-40,0,Z" fill="red"/>
*/

参考资料:

Basic Shapes – SVG 1.1 (Second Edition)
基本形状 - SVG | MDN
SVG (一) 图形, 路径, 变换总结; 以及椭圆弧线, 贝塞尔曲线的详细解释
路径 - SVG | MDN
XMLDOM

探究 dataURI 中使用 SVG 正确姿势

为了减少首页的请求数量,按照以往的思路,会直接将 SVG 转换为 base64 后插入了 CSS 文件中。代码可能是这样的:

1
2
3
.svg {
background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iMjUwOCIgaGVpZ2h0PSIyNTIuNyIgdmlld0JveD0iMCAwIDI1MDggMjUyLjciPjxwb2x5Z29uIHBvaW50cz0iNCwyNTIuNyAyNTA0LDAgMjUwNCwyNTIuNyIgc3R5bGU9ImZpbGw6I2U2ZWJlYSIgLz48L3N2Zz4=');
}

初步开发完成后,为了进一步优化代码,在查询资料时读到了这篇文章:Optimizing SVGs in data URIs。参考文章内容进行优化之后:

1
2
3
.svg {
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' version='1.1'%3E%3Cpolygon points='4,252.7 2504,0 2504,252.7' style='fill:%23e6ebea'/%3E%3C/svg%3E");
}

对比这两段代码,最明显的效果是代码量减少了一半多,而且代码非常清晰,几乎就是 SVG 代码原文,日后如果有一些细微的需求变更,比如更改填充色,可以直接在代码里修改无需进行编码转换。

在 dataURI 中使用 SVG 的最佳方法 Optimizing SVGs in data URIs

不久前,CSS-Tricks 发表了 “Probably Don’t Base64 SVG”,得出结论:如果你在 data URI 中直接使用 SVG,数据量会比转化成 base64 编码格式时小。

这个观点是正确的,但是这里还有一些复杂的地方以及可优化的空间。

更好的浏览器兼容性

例如下面这段代码:

1
2
3
.bg {
background: url('data:image/svg+xml;utf8,<svg ...> ... </svg>');
}

在那些流行于 web 开发者中的浏览器中是有效的,但是在 IE 中则无法正常工作。因为从技术角度来说这是一种畸形的 data URI,而 IE 很严格(原文: This is because technically it’s a malformed data URI, and IE is being strict.)。

RFC 2397 定义了 data URI:

URL 的形式:

1
data:[<mediatype>][;base64],<data>

<mediatype> 描述数据的 MIME 类型,;base64 的出现意味着数据被编码成 base64 格式。如果没有声明;base64,对于 URL 安全字符使用 ASCII 编码,而安全范围以外的字符则使用十六进制数编码为%xx格式。如果省略<mediatype>,默认为text/plain;charset=US-ASCII

换句话说,根据标准,只有如下两种编码 data URI 的方法是有效的:

  • 1.data:mime/type;base64,[actual data]:base64 编码,更适合于二进制数据(PNG,fonts,SVG 等等)
  • 2.data:mime/type;charset=[charset],[actual data]:URL 编码的普通文本,更适合与文本标记语言(SVG,HTML等)

所以,把一个 SVG 文件编码为 data URL 的正确方式为 data:image/svg+xml;charset=utf8,[actual data]。我猜大部分浏览器对是否存在charset=字符串比较宽容,但是在 IE 浏览器里是必须的。为了代码的最大兼容性(例如一些小众浏览器,邮件客户端,等等),它应该被包含在内。

但这并不是全部。记得这段话么?

如果没有声明;base64,对于 URL 安全字符使用ASCII 编码,而安全范围以外的字符则使用十六进制数编码为%xx格式。如果省略<mediatype>,默认为text/plain;charset=US-ASCII

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="512" height="512" viewBox="0 0 512 512"><g id="icomoon-ignore">
</g>
<path d="M224 387.814v124.186l-192-192 192-192v126.912c223.375 5.24 213.794-151.896 156.931-254.912 140.355 151.707 110.55 394.785-156.931 387.814z"></path>
</svg>

根据 Chris 的建议,我们使用 SVGO 来优化我们的 SVG 文件(如果你更习惯图形界面,GUI 版本:SCGOMG)。结果是这样的:

1
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><path d="M224 387.814V512L32 320l192-192v126.912C447.375 260.152 437.794 103.016 380.93 0 521.287 151.707 491.48 394.785 224 387.814z"/></svg>

文件小了很多!而且如果你打算用 CSS 来设定图像的尺寸,你还可以去掉widthheight属性让代码更简洁。

1
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M224 387.814V512L32 320l192-192v126.912C447.375 260.152 437.794 103.016 380.93 0 521.287 151.707 491.48 394.785 224 387.814z"/></svg>

现在,我们把精简后的 SVG 丢进 URL 编码器,会得到这样的东西:

1
%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20512%20512%22%3E%3Cpath%20d%3D%22M224%20387.814V512L32%20320l192-192v126.912C447.375%20260.152%20437.794%20103.016%20380.93%200%20521.287%20151.707%20491.48%20394.785%20224%20387.814z%22%2F%3E%3C%2Fsvg%3E

目前这是唯一能在 IE 中工作的版本。非常明显,这甚至比 base64 编码过后的 SVG 都要长:

1
PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48cGF0aCBkPSJNMjI0IDM4Ny44MTRWNTEyTDMyIDMyMGwxOTItMTkydjEyNi45MTJDNDQ3LjM3NSAyNjAuMTUyIDQzNy43OTQgMTAzLjAxNiAzODAuOTMgMCA1MjEuMjg3IDE1MS43MDcgNDkxLjQ4IDM5NC43ODUgMjI0IDM4Ny44MTR6Ii8

“引号是关键”

你可能注意到了,Chris 使用单引号(')来界定 data URIs。这是因为他的 SVG 文件未编码时使用双引号(")来包裹属性值,为了避免冲突而使用了单引号来代替。这一点点微小的改变其实是真正精简 data URI 的关键。

"' 都是有效的属性分隔符(即:attribute="value"attribute='value' 都有效),但是只有'可以直接在 URL 中使用而无须编码转换。现在我们替换双引号,编码<>,得到:

1
%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath d='M224 387.814V512L32 320l192-192v126.912C447.375 260.152 437.794 103.016 380.93 0 521.287 151.707 491.48 394.785 224 387.814z'/%3E%3C/svg%3E

所以,当你把 SVG 作为 data URI使用时:

用单引号替换包裹属性值的双引号
编码 <>#,和剩余的 " (例如在文本内容中的双引号),以及其他一直的不安全 URL 字符(例如 %
使用双引号来分隔 data URI(<img src="">url("")

网友 jakob-e 在 SASS 中实现了这个算法,使整个流程变得非常简单:

以上就是如何得到能够在 IE (以及标准)中使用最精简的 data URI。总结一下:

base64 编码

1
data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48cGF0aCBkPSJNMjI0IDM4Ny44MTRWNTEyTDMyIDMyMGwxOTItMTkydjEyNi45MTJDNDQ3LjM3NSAyNjAuMTUyIDQzNy43OTQgMTAzLjAxNiAzODAuOTMgMCA1MjEuMjg3IDE1MS43MDcgNDkxLjQ4IDM5NC43ODUgMjI0IDM4Ny44MTR6Ii8+PC9zdmc+

完全 URL 编码

1
data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20512%20512%22%3E%3Cpath%20d%3D%22M224%20387.814V512L32%20320l192-192v126.912C447.375%20260.152%20437.794%20103.016%20380.93%200%20521.287%20151.707%20491.48%20394.785%20224%20387.814z%22%2F%3E%3C%2Fsvg%3E

最大程度优化的 URL 编码

1
data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath d='M224%20387.814V512L32 320l192-192v126.912C447.375 260.152 437.794 103.016 380.93 0 521.287 151.707 491.48 394.785 224 387.814z'/%3E%3C/svg%3E

我们测试发现,结果它在 IE9+ 以及 安卓3.x 以上的浏览器中都能够完美显示。

参考资料:

Optimizing SVGs in data URIs

Probably Don’t Base64 SVG

localstorage基本知识

HTML5 LocalStorage 发展历程

说到本地存储,这玩意真是历尽千辛万苦才走到HTML5这一步,之前的历史大概如下图所示:

HTML5 LocalStorage发展历史

最早的Cookies自然是大家都知道,问题主要就是太小,大概也就4KB的样子,而且IE6只支持每个域名20个cookies,太少了。优势就是大家都支持,而且支持得还蛮好。很早以前那些禁用cookies的用户也都慢慢的不存在了,就好像以前禁用javascript的用户不存在了一样。

到了HTML5把这些都统一了,官方建议是每个网站5MB,非常大了,就存些字符串,足够了。比较诡异的是居然所有支持的浏览器目前都采用的5MB,尽管有一些浏览器可以让用户设置,但对于网页制作者来说,目前的形势就5MB来考虑是比较妥当的。

HTML 本地存储API

localstorage在浏览器的API有两个:localStoragesessionStorage,存在于window对象中:localStorage对应window.localStoragesessionStorage对应window.sessionStorage
localStoragesessionStorage的区别主要是在于其生存期。

本地存储基本使用方法

localStorage用法:

1
2
3
4
5
localStorage.setItem("b","isaac");//设置b为"isaac"
var b = localStorage.getItem("b");//获取b的值,为"isaac"
var a = localStorage.key(0); // 获取第0个数据项的键名,此处即为“b”
localStorage.removeItem("b");//清除c的值
localStorage.clear();//清除当前域名下的所有localstorage数据

sessionStorage用法(在当前页面才能生效,反之清楚存储):

1
2
3
4
5
sessionStorage.setItem("b","isaac");//设置b为"isaac"
var b = sessionStorage.getItem("b");//获取b的值,为"isaac"
var a = sessionStorage.key(0); // 获取第0个数据项的键名,此处即为“b”
sessionStorage.removeItem("b");//清除c的值
sessionStorage.clear();//清除当前域名下的所有localstorage数据

本地存储作用域

HTML5 LocalStorage发展历史

  • 这里的作用域指的是:如何隔离开不同页面之间的localStorage(总不能在百度的页面上能读到腾讯的localStorage吧,哈哈哈)。
  • localStorage只要在相同的协议、相同的主机名、相同的端口下,就能读取/修改到同一份localStorage数据。
  • sessionStoragelocalStorage更严苛一点,除了协议、主机名、端口外,还要求在同一窗口(也就是浏览器的标签页)下。

本地存储生存期

localStorage理论上来说是永久有效的,即不主动清空的话就不会消失,即使保存的数据超出了浏览器所规定的大小,也不会把旧数据清空而只会报错。但需要注意的是,在移动设备上的浏览器或各Native App用到的WebView里,localStorage都是不可靠的,可能会因为各种原因(比如说退出App、网络切换、内存不足等原因)被清空。
sessionStorage的生存期顾名思义,类似于session,只要关闭浏览器(也包括浏览器的标签页),就会被清空。由于sessionStorage的生存期太短,因此应用场景很有限,但从另一方面来看,不容易出现异常情况,比较可靠。

本地存储的空间(SIZE)

各主流浏览器(包含PC、移动端)竟然惊人的一致,pc端为5M,移动的为2.5M。值得说明的是,安卓上手 Q 、手机QQ浏览器、微信中则是 2.5M 的数量级,因此在移动端,本地存储的 SIZE 更加珍贵。IOS 待测试。 综上,SIZE 在 2 - 5M 的区间。 测试页面:Web Storage Support Test

超过最大值的行为

  • 各浏览器也惊人的一致,都是抛出一个错误QUOTA_EXCEEDED_ERR
  • firefox以及opera中,用户可以自己设置本地存储的大小。

HTML5 LocalStorage发展历史

整站本地存储的规划

客户端的存储空间非常宝贵,然而站点也因为业务的不同,很难有一个统一的实施细则,但是有几个大原则不会变。

  • 只保存重要页面的重要数据
    典型的,首页首屏
    对业务庞大的站点,这点尤其重要
  • 极大提高用户体验的数据
    比如表单的状态,可以提交之前保存,当用户刷新页面时可以还原
    静态资源,比如 js 和 css
  • 一个请求一个 key 值(一个 cgi 一个 key 值)
    避免请求链接加参数的 key (http://request-ajax.cgi[params]),这样必然让 key 值趋于冗余从而撑爆空间
    以上几大原则仅作参考,一切从实际业务出发。

参考资料:

HTML5 LocalStorage 本地存储

localstorage 必知必会

使用localStorage必须了解的点

解析HTML/XML生成文档对象模型

前段时间研究svg压缩处理优化,针对文本html、xml文件解析成DOM文档对象,一直困扰着我。那么,今天讲讲借助htmlparser2sax-jsxmldom 解析HTML/XML。

htmlparser2

htmlparser2可以用来处理HTML / XML / RSS的解析器,可以接收流文件,并提供回调接口。cheerio底层就是用此原理。

安装使用

htmlparser2线上解析演示

1
npm install htmlparser2

使用todo

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
28
29
30
31
32
33
34
//解析
var htmlparser = require("htmlparser2");
var parser = new htmlparser.Parser({
onopentag: function(name, attribs){
if(name === "script" && attribs.type === "text/javascript"){
console.log("JS! Hooray!");
}
},
ontext: function(text){
console.log("-->", text);
},
onclosetag: function(tagname){
if(tagname === "script"){
console.log("That's it?!");
}
}
}, {decodeEntities: true});
parser.write("Xyz <script type='text/javascript'>var foo = '<<bar>>';</ script>");
parser.end();

//生成简单的文档对象
var htmlparser = require("htmlparser2");
var rawHtml = "Xyz <script language= javascript>var foo = '<<bar>>';< / script><!--<!-- Waah! -- -->";
var handler = new htmlparser.DomHandler(function (error, dom) {
if (error){
throw new error()
}else{
console.log(dom);
}

});
var parser = new htmlparser.Parser(handler);
parser.write(rawHtml);
parser.done();

htmlparser2监听事件Events

监听处理键值对象函数,仅对有效的键值进行处理,否则中断。

  • onopentag( name, attributes)
  • onopentagname( name)
  • onattribute( name, value)
  • ontext( text)
  • onclosetag( name)
  • onprocessinginstruction( name, data)
  • oncomment( data)
  • oncommentend()
  • oncdatastart()
  • oncdataend()
  • onerror( error)
  • onreset()
  • onend()

htmlparser2解析方法

  • write (别名: parseChunk)

解析数据块,触发相应的回调函数

  • end (别名: done)

解析buffer数据和清除堆栈结束,触发 onend 回调函数。

  • reset

重置buffer以及stack,触发 onreset 函数

  • parseComplete

重置解析器解析数据,触发调用 end。

htmlparser2参数 Option

  • Option: xmlMode

表示是否是特殊标签<script><style>应该得到特殊处理, 如果是空”empty” 的标签(如< br >)含有子元素。如果没有特殊处理,则特殊标签将做文本处理。
解析其它XML内容(不包含HTML文件), 设置为true。默认值:false。

  • Option: decodeEntities

设置为true,文档中内容实体部分将解码。默认值为false。

  • Option: lowerCaseTags

设置为true,所有标签将小写展示。xmlMode不设置情况下,默认值为true。

  • Option: lowerCaseAttributeNames

设置为true,所以属性name将设置为小写。由于影响解析速度,默认值为false。

  • Option: recognizeCDATA

设置为true,CDATA区域将被认为文本,即使xmlMode选项不启用。注意,xmlMode被设置为true,那么CDATA节总是会被认为是文本。

  • Option: recognizeSelfClosing

设置为true,其关闭标签将触发 onclosetag 事件即使xmlMode没有设置为true。注意: xmlMode设置为true,那么自闭标签总是被认可。

htmlparser2主要是提供了对HTML / XML / RSS的解析,效率比较高,而相应生成简单的文档对象,需要借助domhandler模块,只是增加了DOM level 1不便于操作。

sax-js

sax-style风格处理XML和HTML的解析器,sax js

安装使用

1
npm install sax-js

使用todo

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
var sax = require("./lib/sax"),
strict = true, // set to false for html-mode
parser = sax.parser(strict);

parser.onerror = function (e) {
// an error happened.
};
parser.ontext = function (t) {
// got some text. t is the string of text.
};
parser.onopentag = function (node) {
// opened a tag. node has "name" and "attributes"
};
parser.onattribute = function (attr) {
// an attribute. attr has "name" and "value"
};
parser.onend = function () {
// parser stream is done, and ready to have more stuff written to it.
};

parser.write('<xml>Hello, <who name="world">world</who>!</xml>').close();

// stream usage
// takes the same options as the parser
var saxStream = require("sax").createStream(strict, options)
saxStream.on("error", function (e) {
// unhandled errors will throw, since this is a proper node
// event emitter.
console.error("error!", e)
// clear the error
this._parser.error = null
this._parser.resume()
})
saxStream.on("opentag", function (node) {
// same object as above
})
// pipe is supported, and it's readable/writable
// same chunks coming in also go out.
fs.createReadStream("file.xml")
.pipe(saxStream)
.pipe(fs.createWriteStream("file-copy.xml"))

由于sax-js解析的许多方法基本与htmlparser2一致,只是解析效率上比htmlparser2稍差。

XMLDOM

XMLDOM是一款非常强大的解析工具,可实现浏览使用的 javascript文档对象模型 (W3C DOM) 。完美兼容DOM Level 2 以及部分 DOM Level 3。支持支持DOMParser和XMLSerializer接口浏览。解析为文档对象

参考资料:
创建和导出SVG的技巧
SVG 导出与优化

安装:

npm install xmldom

使用todo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var DOMParser = require('xmldom').DOMParser;
var doc = new DOMParser().parseFromString(
'<xml xmlns="a" xmlns:c="./lite">\n'+
'\t<child>test</child>\n'+
'\t<child></child>\n'+
'\t<child/>\n'+
'</xml>'
,'text/xml');
//doc相当于document,doc.documentElement相当于document.documentElement
doc.documentElement.setAttribute('x','y');
doc.documentElement.setAttributeNS('./lite','c:x','y2');
var nsAttr = doc.documentElement.getAttributeNS('./lite','x')
console.info(nsAttr)
console.info(doc)

API特征

DOMParser

1
parseFromString(xmlsource,mimeType)

options extension by xmldom(not BOM standard!!)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//added the options argument
new DOMParser(options)

//errorHandler is supported
new DOMParser({
/**
* locator is always need for error position info
*/
locator:{},
/**
* you can override the errorHandler for xml parser
* @link http://www.saxproject.org/apidoc/org/xml/sax/ErrorHandler.html
*/
errorHandler:{warning:function(w){console.warn(w)},error:callback,fatalError:callback}
//only callback model
//errorHandler:function(level,msg){console.log(level,msg)}
})

XMLSerializer 可以将DOM subtree 和 DOM document转换为文本

1
serializeToString(node)

DOM level2 方法(method) 和 属性(attribute):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
attribute:
nodeValue|prefix
readonly attribute:
nodeName|nodeType|parentNode|childNodes|firstChild|lastChild|previousSibling|nextSibling|attributes|ownerDocument|namespaceURI|localName
method:
insertBefore(newChild, refChild)
replaceChild(newChild, oldChild)
removeChild(oldChild)
appendChild(newChild)
hasChildNodes()
cloneNode(deep)
normalize()
isSupported(feature, version)
hasAttributes()
1
2
3
4
method:
hasFeature(feature, version)
createDocumentType(qualifiedName, publicId, systemId)
createDocument(namespaceURI, qualifiedName, doctype)

参考资料:

domhandler

DOMParser

XMLSerializer 可以将DOM subtree 和 DOM document转换为文本

SVG 导出与优化(转载)

使用 Illustrator 导出 SVG

Illustrator 是目前相对较好的 SVG 设计工具,在 Illustrator 中另存为 SVG 是较常用的导出 SVG 方法,具体如何操作网上有很多资料。如果你熟悉 Illustrator 或者决定使用 Illustrator 绘制图标,你可以从查看官方帮助文档存储图稿-以 SVG 格式存储,或者 Exporting SVG for the web with Adobe Illustrator CC。也可以看看 Sara Soueidan 在她的文章 Tips for Creating and Exporting Better SVGs for the Web 中介绍的一些 Illustrator 关于创建和导出 SVG 的技巧。

另外在 Illustrator 上直接复制选中内容,粘贴至文本编辑器,也可以得到选中内容的 SVG 代码。我在做网页用的 SVG 时就经常这么做,画个矩形作为图标的边界,把图标连同矩形一起复制,然后在粘贴到文本编辑器里,再删除矩形的代码。但这对设计师来说较复杂了,也不适合批量操作,此外这种方法无法控制路径数据的精度。

还有一种方法是使用 Illustrator 画板导出 SVG,画板大小做为图标切图区域,在另存为 SVG 时选择导出多个画板,建议画布命名为资源文件名。Illustrator 中增加新画板对新手来说经常会忽视不齐像素,我用一个脚本把选中的图层转为画板并且自动对齐像素,最后用再另外一个脚本导出画板。如果图标很多,因为画板最多只能支持 100 个,这种方法还是有一些局限。你可以下载 Selection_to_Artboard.jsx (用于将选择图层转为画板) 和 Artboards_To_SVG.jsx (用于将画板导出为 SVG) 这两个脚本,它们仅适用于较小项目。

当项目较大时推荐使用 Illustrator SVG Exporter,这是一个只有选择保存路径对话框的 Illustrator 脚本,用于导出文档中带 “.svg” 后缀的路径、复合路径、组合、图层、画板等等,具体操作可见 Illustrator SVG Exporter 的项目主页。Illustrator SVG Exporter 导出的 SVG 是沿着内容边界裁切的,所以需要在组合或图层内包含一个矩形作为切图边界,这个矩形可以是无填充的路径,最后再统一去掉这个作为切图区域矩形的代码。

svg优化

Material design icons 的 SVG 也带有多余的矩形的,应该是类似的导出方法。当图标数量巨大时,需要使用编程的方法来删除 SVG 文件内作为切图区域矩形的多余代码,除非团队里有很多人手,否则人工操作几乎不可能。Google 并没公布他们用的处理脚本。对代码恐惧,连运行代码都有难度的设计师而言,确实是很大挑战。选用画板导出方式则不需要处理这种问题,下文会介绍批量删除多余代码的方法。

Illustrator SVG Exporter 默认导出的 SVG 路径数据是精确到 4 位小数的,对于界面使用的小图标并不需要这么精确,可以通过修改源码,将精确度改为 1 或 2 位小数。代码大概在 41 行位置,找到svgOptions.coordinatePrecision = 4,将数值改为1或2。

1
svgOptions.coordinatePrecision = 1;

David Deraedt 的 Layer Exporter 是个拥有类似功能的 Illustrator 扩展,除了导出 SVG 外,还支持 PNG、JPG 格式,另外还提供一些简单的设置,具体使用方法可以查阅项目主页文档。

注意事项

  • 尽量把路径描边需要扩展为填充,除非确定需要使用描边的特性。
  • 对于同一个图标,或者图标内同类元素尽量组合成复合路径或合并路径。
  • 合理的编组或者图层,一个项目内的设计内容要么是按编组划分的,要么按图层划分,不要混用两种方式。
  • 注意画板的坐标、路径节点的坐标尽量是整数。
  • 不要用 Illustrator 打开 SVG 文件修改再保存,这样经常会导致保存后的 SVG 代码中 viewport、path 标签的数值偏移。

Photoshop 中使用 Generate 导出 SVG

在 Photoshop CC 2015 可以使用画板导出 SVG,将画板大小作为切图区域,这样可以不需要额外图层作为边界。

svg优化

注意事项

  • 尽量把路径描边需要扩展为填充,除非确定需要使用描边的特性。
  • 对于同一个图标,或者图标内同类元素尽量放在同一个矢量图层内。
  • 因为 Photoshop 没有矢量预览功能,所以尽量注意路径结合处的细节。
  • Photoshop 导出的 SVG 代码不可设置,而且 SVG 代码中元素属性被分离成 CSS 样式。

Sketch 导出 SVG

如果使用 Sketch 导出 SVG 建议通过切片方式,并且切片不要包含在画板内(这张情况会导致 SVG 路径的数据是安装画板坐标生成的)。如果使用可导出图层就需要增加矩形图层作为切图边界,该图层不可以设置为无填充或者不可见,建议填充一个固定的色值。Sketch 的优点是导出不需要借助复制的脚本或插件。

svg优化

Sketch 导出的 SVG 代码冗余比较多且无法设置,而且经常增加一些奇怪的行为,如果画布中只有一个图层,Sketch 会将路径的数据加在路径父级的标签上。Github 上有一些清理 Sketch SVG 代码的工具,都没有界面的,对设计师来说又是挑战。

Github 上有一些清理 Sketch SVG 代码的工具,都是命令行工具或某个编程语言的模块/包,而不是 Sketch 插件。以下列出几个排名较高的清理工具。

SVG 代码优化

设计软件导出的 SVG 都包含各种多余的代码,这会导致文件体积较大,一般最终使用的 SVG 都会对 SVG 进行优化处理。

常用的 SVG 代码优化工具 SVG Optimizer (简称 SVGO) 是一个 Node.js 命令行工具。也就是说这是没有界面的,要在终端上敲代码来优化 SVG,这是非常高效处理方法,但对不熟悉命令行工具的设计师来说可能会有难度。熟悉命令行工具之后,会使用发现大部分界面工具的效率并不高,建议设计师最好花点时间熟悉下命令行工具操作方式。

svgomg 是 SVGO 的 Node.js 网页应用,有很多设置项,但每次只能优化一个 SVG 文件,如果网页速度太慢,可以下载源码后在本地搭建。

svgo-gui 是 SVGO 的跨平台界面工具,但目前已不维护,官方推荐使用命令行或网页版本。

SVGO 相关的工具还有 Node.js 模块版本的imagemin-svgo,gulp 插件版本的gulp-svgmin,项目主页上都有示例代码。

SVGCleaner 另一个跨平台的带 GUI 界面的 SVG 优化软件,对命令行不熟悉的设计师可以选择这个软件。

参考资料:
创建和导出SVG的技巧
SVG 导出与优化

移动端H5音频解决方案

音频传输原理

目前音频在互联网上的传输基于流媒体技术, 音频是流媒体的一种, 称其为流媒体是一种形象的比喻, 将数据的传输比作水流, “流”的重要作用体现在可以明显的节省时间, 实现边下载边播放, 而不需要下载完成后才进行播放。

  • 缓存

流媒体的传输需要缓存, 因为Internet是一个分组交换网, 在其上面传输文件数据都要拆分成一个个数据包, 最后在接收端进行组装, 由于网络是动态变化的, 各个数据包选择的路由可能不尽相同, 故到达客户端的时间延迟也就不等, 先发的数据后到也是常有的事. 因此, 使用缓存来保证到达的数据包进行正确的排序, 从而使媒体数据能连续输出.

之前提到的边下边播, 也是在缓存中接收到足够的数据时才能进行播放的, 这一点在HTML5的文档中也有所体现.

[ 题外话: HTML5 media 文档中, 提到了两个事件 ‘canplay’ 和 ‘canplaythrough’. ‘canplay’: ‘表示浏览器已经加载了足够的数据去播放媒体文件, 但预计以后的播放可能会产生停顿去加载新的数据’ ‘canplaythrough’: ‘表示浏览器已经加载了足够的数据去播放媒体文件并且预计在播放中不会产生停顿去加载新的数据’ ]

  • UDP
    流媒体传输的实现需要合适的传输协议. 由于TCP需要较多的开销, 因此不太适合传输实时数据. 在流媒体传输的实现方案中一般采用HTTP/TCP来传输控制信息, 使用RTP/UDP来传输实时声音数据。

目前可使用的技术

  • Web Audio API
    Web Auido API 是 JavaScript 中用于在网页应用中处理音频的一个高级应该用接口, 主要用于实现Web端的音频处理, 并可以同已存在的其他API相配合, 包括XMLHttpRequest, Canvas 2D 和 WebGL 3D API.

其支持的功能包括

支持各种类型的音频滤波器以实现各种音频效果, 包括回声, 消除噪音等
支持利用合成声音 (Sound synthesis) 创建电子音乐
支持3D位置音频模拟效果, 比如某种声音随着游戏场景而移动 (3D游戏中应该用处比较大…)
支持外部输入的声音与 WebRTC 进行集成
以及各种高端音频处理方法…

但是…

由于移动端对其支持不是很完善 [ iOS已支持, 安卓原生浏览器目前还不支持 ], 目前还是使用支持广泛的 HTML5 Audio 好一些

Web Audio

  • HTML5 Audio 标签

Audio 标签是 HTML5 新定义的一个元素, 提供了在 Web 端播放音频的很多功能. 虽然没有 Web Audio API 那么强大, 但足以满足日常音频播放需求, 更重要的是使浏览器原生支持音频播放, 而不依赖 Flash 或 Sliverlight 之类的外部插件。

HTML5 Audio

参考资料:
howler.js
howler.js github库
howler.js中文介绍
SoundJS
移动端音频解决方案

HTTP 代理原理及实现(二)转载

在上篇《HTTP 代理原理及实现(一)》里,我介绍了 HTTP 代理的两种形式,并用 Node.js 实现了一个可用的普通 / 隧道代理。普通代理可以用来承载 HTTP 流量;隧道代理可以用来承载任何 TCP 流量,包括 HTTP 和 HTTPS。今天这篇文章介绍剩余部分:如何将浏览器与代理之间的流量传输升级为 HTTPS。

上篇文章中实现的代理,是一个标准的 HTTP 服务,针对浏览器的普通请求和 CONNECT 请求,进行不同的处理。Node.js 为创建 HTTP 或 HTTPS Server 提供了高度一致的接口,要将 HTTP 服务升级为 HTTPS 特别方便,只有一点点准备工作要做。

我们知道 TLS 有三大功能:内容加密、身份认证和数据完整性。其中内容加密依赖于密钥协商机制;数据完整性依赖于 MAC(Message authentication code)校验机制;而身份认证则依赖于证书认证机制。一般操作系统或浏览器会维护一个受信任根证书列表,包含在列表之中的证书,或者由列表中的证书签发的证书都会被客户端信任。

提供 HTTPS 服务的证书可以自己生成,然后手动加入到系统根证书列表中。但是对外提供服务的 HTTPS 网站,不可能要求每个用户都手动导入你的证书,所以更常见的做法是向 CA(Certificate Authority,证书颁发机构)申请。根据证书的不同级别,CA 会进行不同级别的验证,验证通过后 CA 会用他们的证书签发网站证书,这个过程通常是收费的(有免费的证书,最近免费的 Let’s Encrypt 也很火,这里不多介绍)。由于 CA 使用的证书都是由广泛内置在各系统中的根证书签发,所以从 CA 获得的网站证书会被绝大部分客户端信任。

通过 CA 申请证书很简单,本文为了方便演示,采用自己签发证书的偷懒办法。现在广泛使用的证书是 x509.v3 格式,使用以下命令可以创建:

1
2
openssl genrsa -out private.pem 2048
openssl req -new -x509 -key private.pem -out public.crt -days 99999

第二行命令运行后,需要填写一些证书信息。需要注意的是Common Name 一定要填写后续提供HTTPS服务的域名或 IP。例如你打算在本地测试,Common Name可以填写127.0.0.1。证书创建好之后,再将public.crt 添加到系统受信任根证书列表中。为了确保添加成功,可以用浏览器验证一下:

1
![HTTP 代理](//st.imququ.com/i/webp/static/uploads/2015/11/fake_certificate.png.webp)

接着,可以改造之前的 Node.js 代码了,需要改动的地方不多:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
var http = require('http');
var https = require('https');
var fs = require('fs');
var net = require('net');
var url = require('url');

function request(cReq, cRes) {
var u = url.parse(cReq.url);

var options = {
hostname : u.hostname,
port : u.port || 80,
path : u.path,
method : cReq.method,
headers : cReq.headers
};

var pReq = http.request(options, function(pRes) {
cRes.writeHead(pRes.statusCode, pRes.headers);
pRes.pipe(cRes);
}).on('error', function(e) {
cRes.end();
});

cReq.pipe(pReq);
}

function connect(cReq, cSock) {
var u = url.parse('http://' + cReq.url);

var pSock = net.connect(u.port, u.hostname, function() {
cSock.write('HTTP/1.1 200 Connection Established\r\n\r\n');
pSock.pipe(cSock);
}).on('error', function(e) {
cSock.end();
});

cSock.pipe(pSock);
}

var options = {
key: fs.readFileSync('./private.pem'),
cert: fs.readFileSync('./public.crt')
};

https.createServer(options)
.on('request', request)
.on('connect', connect)
.listen(8888, '0.0.0.0');

可以看到,除了将http.createServer换成 https.createServer,增加证书相关配置之外,这段代码没有任何改变。这也是引入 TLS 层的妙处,应用层不需要任何改动,就能获得诸多安全特性。

运行服务后,只需要将浏览器的代理设置为HTTPS 127.0.0.1:8888即可,功能照旧。这样改造,只是将浏览器到代理之间的流量升级为了 HTTPS,代理自身逻辑、与服务端的通讯方式,都没有任何变化。

最后,还是写段 Node.js 代码验证下这个 HTTPS 代理服务:

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
28
29
30
var https = require('https');

var options = {
hostname : '127.0.0.1',
port : 8888,
path : 'imququ.com:80',
method : 'CONNECT'
};

//禁用证书验证,不然自签名的证书无法建立 TLS 连接
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";

var req = https.request(options);

req.on('connect', function(res, socket) {
socket.write('GET / HTTP/1.1\r\n' +
'Host: imququ.com\r\n' +
'Connection: Close\r\n' +
'\r\n');

socket.on('data', function(chunk) {
console.log(chunk.toString());
});

socket.on('end', function() {
console.log('socket end.');
});
});

req.end();

这段代码和上篇文章最后那段的区别只是http.request换成了https.request,运行结果完全一样,这里就不贴了。本文所有代码可以从这个仓库获得:proxy-demo

参考资料:
node-http-proxy
node http-proxy和nginx代理性能对比?

HTTP 代理原理及实现(一)转载

Web 代理是一种存在于网络中间的实体,提供各式各样的功能。现代网络系统中,Web 代理无处不在。我之前有关 HTTP 的博文中,多次提到了代理对 HTTP 请求及响应的影响。今天这篇文章,我打算谈谈 HTTP 代理本身的一些原理,以及如何用 Node.js 快速实现代理。

HTTP 代理存在两种形式,分别简单介绍如下:
第一种是 [RFC 7230 - HTTP/1.1: Message Syntax and Routing](https://tools.ietf.org/html/rfc7230)(即修订后的 RFC 2616,HTTP/1.1 协议的第一部分)描述的普通代理。这种代理扮演的是「中间人」角色,对于连接到它的客户端来说,它是服务端;对于要连接的服务端来说,它是客户端。它就负责在两端之间来回传送 HTTP 报文。

第二种是 Tunneling TCP based protocols through Web proxy servers(通过 Web 代理服务器用隧道方式传输基于 TCP 的协议)描述的隧道代理。它通过 HTTP 协议正文部分(Body)完成通讯,以 HTTP 的方式实现任意基于 TCP 的应用层协议代理。这种代理使用 HTTP 的 CONNECT 方法建立连接,但 CONNECT 最开始并不是 RFC 2616 - HTTP/1.1 的一部分,直到 2014 年发布的 HTTP/1.1 修订版中,才增加了对 CONNECT 及隧道代理的描述,详见 RFC 7231 - HTTP/1.1: Semantics and Content。实际上这种代理早就被广泛实现。

普通代理

第一种 Web 代理原理特别简单:

HTTP 客户端向代理发送请求报文,代理服务器需要正确地处理请求和连接(例如正确处理 Connection: keep-alive),同时向服务器发送请求,并将收到的响应转发给客户端。

下面这张图片来自于《HTTP 权威指南》,直观地展示了上述行为:

HTTP 代理

假如我通过代理访问 A 网站,对于 A 来说,它会把代理当做客户端,完全察觉不到真正客户端的存在,这实现了隐藏客户端 IP 的目的。当然代理也可以修改 HTTP 请求头部,通过X-Forwarded-IP这样的自定义头部告诉服务端真正的客户端 IP。但服务器无法验证这个自定义头部真的是由代理添加,还是客户端修改了请求头,所以从 HTTP 头部字段获取 IP 时,需要格外小心。这部分内容可以参考我之前的《HTTP 请求头中的 X-Forwarded-For》这篇文章。

给浏览器显式的指定代理,需要手动修改浏览器或操作系统相关设置,或者指定 PAC(Proxy Auto-Configuration,自动配置代理)文件自动设置,还有些浏览器支持 WPAD(Web Proxy Autodiscovery Protocol,Web 代理自动发现协议)。显式指定浏览器代理这种方式一般称之为正向代理,浏览器启用正向代理后,会对 HTTP 请求报文做一些修改,来规避老旧代理服务器的一些问题,这部分内容可以参考我之前的《Http 请求头中的 Proxy-Connection》这篇文章。

还有一种情况是访问 A 网站时,实际上访问的是代理,代理收到请求报文后,再向真正提供服务的服务器发起请求,并将响应转发给浏览器。这种情况一般被称之为反向代理,它可以用来隐藏服务器 IP 及端口。一般使用反向代理后,需要通过修改 DNS 让域名解析到代理服务器 IP,这时浏览器无法察觉到真正服务器的存在,当然也就不需要修改配置了。反向代理是 Web 系统最为常见的一种部署方式,例如本博客就是使用 Nginx 的proxy_pass 功能将浏览器请求转发到背后的 Node.js 服务。

了解完第一种代理的基本原理后,我们用 Node.js 实现一下它。只包含核心逻辑的代码如下:

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
var http = require('http');
var net = require('net');
var url = require('url');

function request(cReq, cRes) {
var u = url.parse(cReq.url);

var options = {
hostname : u.hostname,
port : u.port || 80,
path : u.path,
method : cReq.method,
headers : cReq.headers
};

var pReq = http.request(options, function(pRes) {
cRes.writeHead(pRes.statusCode, pRes.headers);
pRes.pipe(cRes);
}).on('error', function(e) {
cRes.end();
});

cReq.pipe(pReq);
}

http.createServer().on('request', request).listen(8888, '0.0.0.0');

以上代码运行后,会在本地8888端口开启 HTTP 代理服务,这个服务从请求报文中解析出请求 URL 和其他必要参数,新建到服务端的请求,并把代理收到的请求转发给新建的请求,最后再把服务端响应返回给浏览器。修改浏览器的 HTTP 代理为127.0.0.1:8888后再访问 HTTP 网站,代理可以正常工作。

但是,使用我们这个代理服务后,HTTPS 网站完全无法访问,这是为什么呢?答案很简单,这个代理提供的是 HTTP 服务,根本没办法承载 HTTPS 服务。那么是否把这个代理改为 HTTPS 就可以了呢?显然也不可以,因为这种代理的本质是中间人,而 HTTPS 网站的证书认证机制是中间人劫持的克星。普通的 HTTPS 服务中,服务端不验证客户端的证书,中间人可以作为客户端与服务端成功完成 TLS 握手;但是中间人没有证书私钥,无论如何也无法伪造成服务端跟客户端建立 TLS 连接。当然如果你拥有证书私钥,代理证书对应的 HTTPS 网站当然就没问题了。

HTTP 抓包神器 Fiddler 的工作原理也是在本地开启 HTTP 代理服务,通过让浏览器流量走这个代理,从而实现显示和修改 HTTP 包的功能。如果要让 Fiddler 解密 HTTPS 包的内容,需要先将它自带的根证书导入到系统受信任的根证书列表中。一旦完成这一步,浏览器就会信任 Fiddler 后续的「伪造证书」,从而在浏览器和 Fiddler、Fiddler 和服务端之间都能成功建立 TLS 连接。而对于 Fiddler 这个节点来说,两端的 TLS 流量都是可以解密的。

如果我们不导入根证书,Fiddler 的 HTTP 代理还能代理 HTTPS 流量么?实践证明,不导入根证书,Fiddler 只是无法解密 HTTPS 流量,HTTPS 网站还是可以正常访问。这是如何做到的,这些 HTTPS 流量是否安全呢?这些问题将在下一节揭晓。

隧道代理

第二种 Web 代理的原理也很简单:

HTTP 客户端通过 CONNECT 方法请求隧道代理创建一条到达任意目的服务器和端口的 TCP 连接,并对客户端和服务器之间的后继数据进行盲转发。

下面这张图片同样来自于《HTTP 权威指南》,直观地展示了上述行为:

HTTP 代理

假如我通过代理访问 A 网站,浏览器首先通过 CONNECT 请求,让代理创建一条到 A 网站的 TCP 连接;一旦 TCP 连接建好,代理无脑转发后续流量即可。所以这种代理,理论上适用于任意基于 TCP 的应用层协议,HTTPS 网站使用的 TLS 协议当然也可以。这也是这种代理为什么被称为隧道的原因。对于 HTTPS 来说,客户端透过代理直接跟服务端进行 TLS 握手协商密钥,所以依然是安全的,下图中的抓包信息显示了这种场景:

HTTP 代理

可以看到,浏览器与代理进行 TCP 握手之后,发起了 CONNECT 请求,报文起始行如下:

CONNECT imququ.com:443 HTTP/1.1

对于 CONNECT 请求来说,只是用来让代理创建 TCP 连接,所以只需要提供服务器域名及端口即可,并不需要具体的资源路径。代理收到这样的请求后,需要与服务端建立 TCP 连接,并响应给浏览器这样一个 HTTP 报文:

HTTP/1.1 200 Connection Established

浏览器收到了这个响应报文,就可以认为到服务端的 TCP 连接已经打通,后续直接往这个 TCP 连接写协议数据即可。通过 Wireshark 的 Follow TCP Steam 功能,可以清楚地看到浏览器和代理之间的数据传递:

HTTP 代理

可以看到,浏览器建立到服务端 TCP 连接产生的 HTTP 往返,完全是明文,这也是为什么 CONNECT 请求只需要提供域名和端口:如果发送了完整 URL、Cookie 等信息,会被中间人一览无余,降低了 HTTPS 的安全性。HTTP 代理承载的 HTTPS 流量,应用数据要等到 TLS 握手成功之后通过 Application Data 协议传输,中间节点无法得知用于流量加密的 master-secret,无法解密数据。而 CONNECT 暴露的域名和端口,对于普通的 HTTPS 请求来说,中间人一样可以拿到(IP 和端口很容易拿到,请求的域名可以通过 DNS Query 或者 TLS Client Hello 中的 Server Name Indication 拿到),所以这种方式并没有增加不安全性。

了解完原理后,再用 Node.js 实现一个支持 CONNECT 的代理也很简单。核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var http = require('http');
var net = require('net');
var url = require('url');

function connect(cReq, cSock) {
var u = url.parse('http://' + cReq.url);

var pSock = net.connect(u.port, u.hostname, function() {
cSock.write('HTTP/1.1 200 Connection Established\r\n\r\n');
pSock.pipe(cSock);
}).on('error', function(e) {
cSock.end();
});

cSock.pipe(pSock);
}

http.createServer().on('connect', connect).listen(8888, '0.0.0.0');

以上代码运行后,会在本地8888端口开启 HTTP 代理服务,这个服务从 CONNECT 请求报文中解析出域名和端口,创建到服务端的 TCP 连接,并和 CONNECT 请求中的 TCP 连接串起来,最后再响应一个 Connection Established 响应。修改浏览器的 HTTP 代理为127.0.0.1:8888后再访问 HTTPS 网站,代理可以正常工作。

最后,将两种代理的实现代码合二为一,就可以得到全功能的 Proxy 程序了,全部代码在 50 行以内(当然异常什么的基本没考虑,这是我博客代码的一贯风格):

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
var http = require('http');
var net = require('net');
var url = require('url');

function request(cReq, cRes) {
var u = url.parse(cReq.url);

var options = {
hostname : u.hostname,
port : u.port || 80,
path : u.path,
method : cReq.method,
headers : cReq.headers
};

var pReq = http.request(options, function(pRes) {
cRes.writeHead(pRes.statusCode, pRes.headers);
pRes.pipe(cRes);
}).on('error', function(e) {
cRes.end();
});

cReq.pipe(pReq);
}

function connect(cReq, cSock) {
var u = url.parse('http://' + cReq.url);

var pSock = net.connect(u.port, u.hostname, function() {
cSock.write('HTTP/1.1 200 Connection Established\r\n\r\n');
pSock.pipe(cSock);
}).on('error', function(e) {
cSock.end();
});

cSock.pipe(pSock);
}

http.createServer()
.on('request', request)
.on('connect', connect)
.listen(8888, '0.0.0.0');

需要注意的是,大部分浏览器显式配置了代理之后,只会让 HTTPS 网站走隧道代理,这是因为建立隧道需要耗费一次往返,能不用就尽量不用。但这并不代表 HTTP 请求不能走隧道代理,我们用 Node.js 写段程序验证下(先运行前面的代理服务):

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
var http = require('http');

var options = {
hostname : '127.0.0.1',
port : 8888,
path : 'imququ.com:80',
method : 'CONNECT'
};

var req = http.request(options);

req.on('connect', function(res, socket) {
socket.write('GET / HTTP/1.1\r\n' +
'Host: imququ.com\r\n' +
'Connection: Close\r\n' +
'\r\n');

socket.on('data', function(chunk) {
console.log(chunk.toString());
});

socket.on('end', function() {
console.log('socket end.');
});
});

req.end();

这段代码运行完,结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
HTTP/1.1 301 Moved Permanently
Server: nginx
Date: Thu, 19 Nov 2015 15:57:47 GMT
Content-Type: text/html
Content-Length: 178
Connection: close
Location: https://imququ.com/

<html>
<head><title>301 Moved Permanently</title></head>
<body bgcolor="white">
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx</center>
</body>
</html>

socket end.

可以看到,通过 CONNECT 让代理打开到目标服务器的 TCP 连接,用来承载 HTTP 流量也是完全没问题的。

最后,HTTP 的认证机制可以跟代理配合使用,使得必须输入正确的用户名和密码才能使用代理,这部分内容比较简单,这里略过。在本文第二部分,我打算谈谈如何把今天实现的代理改造为 HTTPS 代理,也就是如何让浏览器与代理之间的流量走 HTTPS 安全机制。。

参考资料:
HTTP 代理原理及实现(一)
HTTP 代理原理及实现(二)
node-http-proxy
node http-proxy和nginx代理性能对比?

Git查看历史记录

在项目工作中经常有遇到反复改动的情况,由此需要查看提交历史记录,来分析具体问题,使用git log 命令查看,可以很方便查到每次commit记录。

一、git log查看历史记录

当我们切换到Git库项目文件,使用git log 命令查看,查看每次commit历史记录:

1
git log

会出现类似如下结果,记录每次commit的md5值、时间说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
commit 3f54135d7ae20f26bbd41b105dcb6d14b2222a3a
Author: pfan <768065158@qq.com>
Date: Fri Oct 28 15:18:55 2016 +0800

modify content.vue

commit c522e1693db50a81588c59dc1b537315e86818b4
Author: pfan <768065158@qq.com>
Date: Fri Oct 28 15:13:55 2016 +0800

modify flip

commit 5bb0894e02048ca6c1a9fc6e9be4dac8f9af38e6
Author: pfan <768065158@qq.com>
Date: Fri Oct 28 14:13:23 2016 +0800

二、查找具体文件更改记录

使用git log 命令查看,很宽泛把所有的改动commit都罗列出来了,但往往我们的场景是需要显示指定文件被更改情况。那么,我们可以使用在git log后面加参数,更多参数使用请参详Git 基础 - 查看提交历史

1
git log --pretty=oneline 文件名

结果,则会显示这个针对这个文件修改的md5值:

1
2
3
4
5
6
7
659ec40bfec5a0e29e268316bcff310aef3a4a4c fixed searchpage
b388e7ed465561a00911139ef57b9077c1b7fb59 fix upload bug
5b4fd6c62bd682d35783f7e99c15a8a15a84b158 form field
163d6fd87059579c02a32bfc784c60fcef33d50c fixed svgfile 匹配 、上传作者、删除数组更新数据
1b33156df3255415c60616e01794a69a839bdaef add search pager delete demo.html
482b686cec440161bacd64af2a829aed54221ddc svg icon pushlish 1.0.2
55c55cbd5898242c5add2b762949c8a3336c59fe add svgicon_platform

三、git show查看具体改动

当我们拿到改动的commit md5哈希值时,可使用git show即可显示具体的某次的改动的修改,如:

1
git show 3f54135d7ae20f26bbd41b105dcb6d14b2222a3a

结果就会展现改动记录以及具体代码:

1
2
3
4
5
6
7
8
9
10
commit 3f54135d7ae20f26bbd41b105dcb6d14b2222a3a
Author: pfan <768065158@qq.com>
Date: Fri Oct 28 15:18:55 2016 +0800

modify content.vue

diff --git a/dist/index.html b/dist/index.html
index 1f54ffa..0416d15 100644
--- a/dist/index.html
+++ b/dist/index.html

四、扩展删除缓存,重新按gitignore走

请保证,所有分支已经提交成功。

1
2
3
git rm -r --cached .   //删除缓存文件,但不删除文件
git add .
git commit -m 'update .gitignore'

参考资料:
git查看某个文件的修改历史
Git 基础 - 查看提交历史

CSS3D-陀螺仪、重力感应

大家可能还记得前段时间淘宝造物节的宣传动画效果,让自己对CSS 360全景更充满好奇之心。尽从效果上,就吸引了大家不少的眼球,当然大家更期待的是如何能实现这样的效果。那么我们来开始一起探讨这方面的事情。

在具备独立完成这样的效果之前,咱们需要对几个知识点要有所了解:

  • 设备的摄像头Camera(Camera API)
  • 陀螺仪、重力感应
  • CSS 3D (CSS3 Transform 3D)

而这些都涉及很多的知识面,今天我们先来介绍其中有关于陀螺仪相在的知识点。

HTML5 Orientation

HTML5 Orientation是HTML5中一个非常酷的特性,它主要用来检测智能设备(手机、平板等)的运动方向。在现在的移动端开发中已给用户带来更良好的体验,也在很多项目中发挥了重要的重用。

目前在以下场景中常能看到其相关的身影:

控制游戏:Web游戏应用监控设备方向,并将其解释为控制屏幕上的精灵在某方向上的倾斜。
手势识别:Web应用监控设备的加速,并将其应用于信号处理,以便识别特定首饰。距离说明,使用摇晃手势清除web表单。
地图:Web地图应用使用设备方向,将地图与实际情况对齐。

在使用设备运动方向(Device Orientation)API之前,先得确保浏览器支持这些API。要得到相关的数据,可以直接从Can I Use.com得到相关数据:

当然,用户是不知道自己的浏览器是否支持,所以在我们的代码中要做一些事情,就是在使用运动方向API之前先做一些检测,如果支持就使用该API,如果不支持,就提供相关的提示信息:

1
2
3
4
5
if (window.DeviceOrientationEvent) {
// 支持DeviceOrientation API写在这里
} else {
console.log("对不起,您的浏览器还不支持Device Orientation!!!");
}

先不要急着这里面的代码怎么写,先来了解一些相关的知识点。帮助更好的理解设备方向(Device Orientation)和更好的使用好设备方向。

地球坐标系统

先来看一张地球坐标系统的图:

地球坐标系统

地心地固坐标系(Earth-Centered, Earth-Fixed,简称ECEF)简称地心坐标系,是一种以地心为原点的地固坐标系(也称地球坐标系),是一种笛卡儿坐标系。原点 O (0,0,0)为地球质心,z 轴与地轴平行指向北极点,x轴指向本初子午线与赤道的交点,y 轴垂直于xOz平面(即东经90度与赤道的交点)构成右手坐标系。

地球坐标系统是由x、y、z三个轴组成,基于重力和标准磁场方向。简单点讲,地球坐标系统是一个位于用户位置的东、北、上系,其拥有3个轴,地面相切与1984世界测地系统的Spheriod的用户所在位置。

东(x)在地面上,垂直于北轴,向东为正 (东西方向)
北(y)在地面上,向正北为正(指向北极)(南北方向)
上(z)垂直于地面,向上为正(上下方向)

对于一个移动设备,例如电话或平板,设备坐标系的定义于屏幕的标准方向相关。这意味着类似于键盘的滑动元素没有展开、类似于显示器的选择元素折叠至 其默认位置。如果在设备旋转或展开滑动键盘时屏幕方向发生变化,这不会影响关于设备的坐标系的方向。用户希望获得这些屏幕方向的变化可以使用现有的orientationchange事件。对于膝上电脑,设备的坐标系定义于集成键盘。

x在屏幕或键盘平面上,屏幕或键盘的右侧为正。
y在屏幕或键盘屏幕上,屏幕或键盘的上方为正。
z垂直于屏幕或键盘屏幕,离开屏幕或键盘为正。

如下图所示:

地球坐标系统

从地球坐标系到设备坐标系的转变必须按照下列系统转换。旋转必须使用右手规则,即正向沿一个轴旋转为从该轴的方向看顺时针旋转。从两个系重合开始,旋转应用下列规则:

以设备坐标系z轴为轴,旋转alpha度。alpha的作用域为(0, 360)。
以设备坐标系x轴为轴,旋转beta度。beta的作用域为(-180, 180)。
已设备坐标系y轴为轴,旋转gamma度。gamma的作用域为(-90, 90)。

如下图所示:
地球坐标系统

Alpha, Beta 和 Gamma 角

Alpha(α), Beta(β) 和 Gamma(γ)角也称之为旋转数据。旋转数据作为欧拉角(Euler Angle)返回,是设备坐标系和地球坐标系之间的差异值。

在解释Alpha、Beta和Gamma这三个角之前,我们需要定义存在的空间。如下图所示,展示了移动设备上使用的三维坐标系统:

地球坐标系统

在手机或者平板上,设备定位方向是基于屏幕方向的。对于手机和平板来说,他们都是基于纵向模式的设备,对于台式机或笔记本电脑来说,他们的定位方向和键盘有关。

Alpha(α)角

Alpha(α)角代表的是z轴。因此,任何沿着z轴旋转都会使用Alpha(α)角变化。Alpha(α)的变化范围是(0~360)度之间。当α = 0时,设备是直接每日向地球的北极。下图显示了α旋转。

地球坐标系统

设备逆时针旋转,Alpha(α)值增加。

Beta(β)角

Beta(β)角代表的是x轴。设备绕着x轴旋转将导致Beta(β)角变化。Beta(β)的变化范围是(-180 ~ 180)度之间。当设备平行于地球表面时β = 0,比如说,你把手机平放在桌面上。下图显示了β旋转:

地球坐标系统

Gamma(γ)角

Gamma(γ)角代表的是y轴。设备绕着y轴旋转将导致Gamma(γ)角变化。Gamma(γ)角的变化范围是(-90 ~ 90)度。当设备平行于地球表面时γ = 0。下图显示了γ旋转:

地球坐标系统

上面简单介绍了Alpha(α), Beta(β) 和 Gamma(γ)角。从网上整了几张有关于Beta(β) 和 Gamma(γ)角旋转的数据示意图:

Beta(β)上下翻动

地球坐标系统

从上面的图片中,不难观察出相应的结果,在向上翻动手势过程中:

Beta(β)值有较明显变化,由初始值变化至约90度
Gamma(γ) 和 Alpha(α) 绝对值之差趋近于0
Beta(β)开始变化早于Gamm(γ)和Alpha(α)

Gamma(γ)左右翻动

地球坐标系统

从图中,很明显的可以看出,Gamma(γ)做着优雅的 “正弦曲线”变化,而Alpha(α)和Beta(β)基本保持着不变, 所以我们可以得出以下结论:

Alpha(α)和Beta(β)值基本保持不变
向左翻转时,Gamma(γ)在负数方向做0到约-90到0的变化
向右翻转时,Gamma(γ)在正数方向做0到90到0的变化

综合上述,我们可以用图来更好阐述设备坐标和地球坐标之间的关系:

地球坐标系统

设备的初始位置,地球(XYZ)与设备(xyz)坐标系重合。

地球坐标系统

设备以z轴为轴,旋转Alpha(α)度,原坐标x、y轴显示为x0、y0。

地球坐标系统

设备以x轴为轴,旋转Beta(β)度,原坐标y、z轴显示为y0、z0。

地球坐标系统

设备以y轴为轴,旋转Beta(β)度,原坐标x、z轴显示为x0、z0。

因此,Alpha(α)、Beta(β)和Gamma(γ)组成一组Z-X’-Y’’式的固有Tait-Bryan角度。注意这里对角度的选择遵循数学惯例,但这意味着Alpha(α)与罗盘指向相反。这还意味着这些角度不匹配车辆动力学中的roll-pitch-yaw惯例。

对于不能提供三个角度绝对值的实现,作为替代,可以提供关于任意方向的相对值。在这种情况下,必须设absolute属性为false,否则必须设absolute属性为true。

对于不能提供所有三个角度的实现,其必须设未知的角度的值为null。如果提供了某一角度,必须恰当的设置absolute属性。如果实现不能提供任何方向信息,则触发事件时所有属性都必须被设为null。

什么是重力感应

说到重力感应有一个东西不得不提,那就是就是陀螺仪,陀螺仪就是内部有一个陀螺,陀螺仪一旦开始旋转,由于轮子的角动量,陀螺仪有抗拒方向改变的特性,它的轴由于陀螺效应始终与初始方向平行,这样就可以通过与初始方向的偏差计算出实际方向。

deviceorientation事件

设备方向事件会返回设备旋转角度数据,如果手机或者笔记本电脑有指南针的话,返回数据中还会包括设备当前的朝向。在HTML5 OrientationAPI提供了三个相应的DOM事件:

deviceorientation,其提供设备的物理方向信息,表示为一系列本地坐标系的旋角
devicemotion,其提供设备的加速信息,表示为定义在设备上的坐标系中的卡尔迪坐标。其还提供了设备在坐标系中的自转速率。若可行的话,事件应该提供设备重心处的加速信息。
compassneedscalibration,其用于通知Web站点使用罗盘信息校准上述事件。

何时使用设备方向事件

设备方向事件有几种使用场景,例如:

更新移动的用户的地图
UI调整,像是增加Paralax(视差滚动:指让多层背景以不同的速度移动,形成立体的运动效果,带来非常出色的视觉体验)效果
结合地理定位,用于导航

检测和监听方向事件

在监听DeviceOrientationEvent事件前,我们首先要检查浏览器是否支持。然后再在window中增加deviceorientation事件监听。

1
2
3
4
if (window.DeviceOrientationEvent) {
window.addEventListener('deviceorientation', deviceOrientationHandler, false);
document.getElementById("doeSupported").innerText = "";
}

处理设备方向事件

当设备移动或者方向改变时,设备方向事件就会被触发。它会返回当前位置相对于地球坐标的差值。

事件通常会返回Alipha、Beta和Gamma三个值。在移动端Safari浏览器中,还会返回webkitCompassHeading属性值,这个属性值与指南针(compass)的导向有关。

检测和监听移动事件

监听DeviceMotionEvents事件前,首先要检查浏览器是否支持,然后再在window上监听devicemotion事件。

1
2
3
4
if (window.DeviceMotionEvent) {
window.addEventListener("devicemotion", deviceMotionHandler);
setTimeout(stopJump, 3*1000);
}

处理设备移动事件

当需要每隔一定时间返回设备的旋转速率(以度每秒为单位)或者移动速率数据时,设备移动事件就会被触发。有些设备没有可以排除重力影响的硬件装置。

该事件返回4个属性值,包括accelerationIncludingGravity、acceleration。

让我们来看一个在平坦桌面上,屏幕朝上的手机的例子。

状态 旋转 加速(m/s2)度 重力加速度(m/s2)
不移动 [0, 0, 0] [0, 0, 0] [0, 0, 9.8]
朝着天空移动 [0, 0, 0] [0, 0, 5] [0, 0, 14.81]
向右侧移动 [0, 0, 0] [3, 0, 0] [3, 0, 9.81]
向上且向右移动 [0, 0, 0] [5, 0, 5] [5, 0, 14.81]

相反,如果手机被握住,保持手机屏幕和地面垂直,且屏幕面对观察者:

状态 旋转 加速(m/s2)度 重力加速度(m/s2)
不移动 [0, 0, 0] [0, 0, 0] [0, 9.81, 0]
朝着天空移动 [0, 0, 0] [0, 5, 0] [0, 14.81, 0]
向右侧移动 [0, 0, 0] [3, 0, 0] [3, 9.81, 0]
向上且向右移动 [0, 0, 0] [5, 5, 0] [5, 14.81, 0]

有关于deviceorientation事件常用方法:

注册一个deviceorientation事件的接收器:

1
2
3
window.addEventListener("deviceorientation", function(event) {
// 处理event.alpha、event.beta及event.gamma
}, true);

将设备放置在水平表面,屏幕顶端指向西方,则其方向信息如下:

1
2
3
4
5
{
alpha: 90,
beta: 0,
gamma: 0
}

为了获得罗盘指向,可以简单的使用360度减去alpha。若设被平行于水平表面,其罗盘指向为(360 - alpha)。若用户手持设备,屏幕处于一个垂直平面且屏幕顶端指向上方。beta的值为90,alpha和gamma无关。

用户手持设备,面向alpha角度,屏幕处于一个垂直屏幕,屏幕顶端指向右方,则其方向信息如下:

1
2
3
4
5
{
alpha: 270 - alpha,
beta: 0,
gamma: 90
}

只用自定义界面通知用户校准罗盘:

1
2
3
4
5
6
7
8
9
window.addEventListener("compassneedscalibration", function(event) {
alert('您的罗盘需要校准,请将设备沿数字8方向移动。');
event.preventDefault();
}, true);
注册一个devicemotion时间的接收器:
window.addEventListener("devicemotion", function(event) {
// 处理event.acceleration、event.accelerationIncludingGravity、
// event.rotationRate和event.interval
}, true);

将设备放置在水平表面,屏幕向上,acceleration为零,则其accelerationIncludingGravity信息如下:

1
2
3
4
5
{
x: 0,
y: 0,
z: 9.81
}

设备做自由落体,屏幕水平向上,accelerationIncludingGravity为零,则其acceleration信息如下:

1
2
3
4
5
{
x: 0,
y: 0,
z: -9.81
}

将设备安置于车辆至上,屏幕处于一个垂直平面,顶端向上,面向车辆后部。车辆行驶速度为v,向右侧进行半径为r的转弯。设备记录acceleration和accelerationIncludingGravity在位置x处的情况,同时设备还会记录rotationRate.gamma的负值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
acceleration: {
x: v^2/r,
y: 0,
z: 0
},
accelerationIncludingGravity: {
x: v^2/r,
y: 0,
z: 9.81
},
rotationRate: {
alpha: 0,
beta: 0,
gamma: -v/r*180/pi
}
}

旋转的立方体

既然有HTML5 Orientation这样的API,那么我们之前制作的CSS 3D盒子就可以在称动设备上做一些重力感应的效果,比如旋转你的手机,让立方体动起来。接下来,来看一个这方面的示例。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<div id="wrapper">
<div id="platform">
<div id="dice">
<div class="side front">
<div class="dot center"></div>
</div>
<div class="side front inner"></div>
<div class="side top">
<div class="dot dtop dleft"></div>
<div class="dot dbottom dright"></div>
</div>
<div class="side top inner"></div>
<div class="side right">
<div class="dot dtop dleft"></div>
<div class="dot center"></div>
<div class="dot dbottom dright"></div>
</div>
<div class="side right inner"></div>
<div class="side left">
<div class="dot dtop dleft"></div>
<div class="dot dtop dright"></div>
<div class="dot dbottom dleft"></div>
<div class="dot dbottom dright"></div>
</div>
<div class="side left inner"></div>
<div class="side bottom">
<div class="dot center"></div>
<div class="dot dtop dleft"></div>
<div class="dot dtop dright"></div>
<div class="dot dbottom dleft"></div>
<div class="dot dbottom dright"></div>
</div>
<div class="side bottom inner"></div>
<div class="side back">
<div class="dot dtop dleft"></div>
<div class="dot dtop dright"></div>
<div class="dot dbottom dleft"></div>
<div class="dot dbottom dright"></div>
<div class="dot center dleft"></div>
<div class="dot center dright"></div>
</div>
<div class="side back inner"></div>
</div>
</div>
</div>

(function() {
var space = document.getElementById('dice');
if (window.DeviceOrientationEvent) {

window.addEventListener('deviceorientation', function(event) {
var alpha = event.alpha,
beta = event.beta,
gamma = event.gamma;

space.style.webkitTransform = 'rotateX(' + beta + 'deg) rotateY(' + gamma + 'deg) rotateZ(' + alpha + 'deg)';
space.style.transform = 'rotateX(' + beta + 'deg) rotateY(' + gamma + 'deg) rotateZ(' + alpha + 'deg)';
space.style.mozTransform = 'rotateX(' + beta + 'deg) rotateY(' + gamma + 'deg) rotateZ(' + alpha + 'deg)';

}, false);
} else {
document.querySelector('body').innerHTML = '你的瀏覽器不支援喔';
}
})();

参考资料:
W3C DeviceOrientation事件规范
检测设备方向
HTML5 控制裝置陀螺儀 ( 三軸 )
html5重力感应事件之DeviceMotionEvent
移动终端学习2:触屏原生js事件及重力感应
移动开发事件
移动端资源集合
html5实现微信摇一摇功能
HTML5摇一摇(上)—如何判断设备摇动
陀螺仪的基础知识