HTML5 视频使用指南

视频是 HTML5 中最受欢迎的特性之一。跟以前调用插件的做法相比,只要一个 <video> 就行的便利实在是今非昔比。除此之外,HTML5 视频对移动设备的友好也是 Flash 难望项背的。到了 2013 年,浏览器和各种移动设备对 HTML5 视频的支持已经相当成熟,尤其是移动设备上,HTML5 几乎是唯一实用的网页视频发布方式。

不过,HTML5 视频有个很大的问题:兼容性。固执地坚守老旧浏览器的用户甚至不算是这个问题的根源,而是 HTML5 规范中没有规定(也不可能规定)视频所用的容器和编解码技术,所以即使在不考虑缺乏 HTML5 支持的浏览器时,各种视频技术涉及的大量规格和授权类型,使不同的浏览器(甚至同一浏览器在不同平台上的版本)对 HTML5 视频的兼容性差别极大。要想让你的所有用户都能看到你发布的视频,你必须遵循一个标准的发布方法,而这就是本文的目的。

容器和编解码器

要发布兼容度最好的视频,有必要了解常用的视频容器和编解码器,以及各平台和浏览器的支持程度。

当我们看一个“视频”的时候,其实我们是在看视频,同时在听音频。一个视频文件中,至少储存了两条数据:一条是视频轨道、一条是音频轨道。这些数据封装在一个叫 容器 的文件格式中。MP4、MKV、AVI 等都是我们熟悉的容器格式,但是它们都只是装东西的袋子,还不能决定一个视频能否被支持。

当我们播放视频时,分离器会解析容器,找到其中的视频和音频数据流,根据它们的类型把它们传给相应的 编解码器。编解码器会解码这些数据流并通过播放器和操作系统输出到屏幕和扬声器上。

由此可见,播放器必须能识别容器,并拥有视频和音频数据流相应的编解码器,才能播放视频。浏览器和操作系统对视频的支持,其实就是对容器以及视频和音频编解码器组合的支持。

Web 支持的容器和编解码器

尽管视频容器和编解码器多如牛毛,但因为授权、效率或硬件解码等原因绝大部分在 Web 上都不受支持。如今在 Web 上常见的组合不外乎三种:

H.264 + AAC + MP4

H.264 也称为 MPEG-4 Part 10 或 MPEG-4 AVC,由 动态图像专家组 开发。这种格式可以应用在从低带宽、低性能的移动设备到高带宽、高性能的台式电脑的所有设备上,因此,它被划分为多个 ProfilesLevels,它们规定了编码的细节规格,以在编码效率和编解码所需计算性能之间取得平衡。例如手机等移动设备普遍可以支持 Baseline Profile(较新的设备如 iPhone、iPad 等也可支持 Main 甚至 High Profile),电脑则可以支持 Baseline、Main 和 High Profile。

H.264 的最大优点是支持硬件解码。在 iPhone 等移动设备以及较新的电脑上,有专门的硬件模块用于 H.264 的编解码工作,这可以减少不擅此道的 CPU 的负担,大大降低能耗并提高性能,这对 CPU 性能和电池容量有限的移动设备来说至关重要。不过 H.264 是一种专利技术,从法律上讲,要发布通过 H.264 编码的视频,需要有 MPEG LA Group 的授权。

AAC 是作为代替 MP3 的格式开发的,拥有远胜过 MP3 的编码效率和多声道支持。与 H.264 相似,AAC也分为针对一般设备的 LC-AAC 和针对高性能设备的 HE-AAC。Web 上绝大部分都使用 LC-AAC

AAC 有非常广泛的支持,iPhone 等移动设备,QuickTime、Windows Media Player 等闭源播放器,和 VLC 等开源播放器都支持 AAC。另外,AAC 同样也是一项专利技术,使用需要授权。

MP4 是从苹果的 QuickTime 的规格上发展而来的容器。MP4 视频有 mp4 和 m4v 两种常见的扩展名,后者是苹果为了与纯音频的 MP4 文件(m4a)区分而创造的。

WebM

WebM 是专门为 HTML5 视频设计的,由 Google 赞助开发,是开源、有免授权费专利的技术。目前,它由 VP8 视频编码、Vorbis 音频编码和一种基于 Matroska 的容器组成。

VP8 原来由 On2 所开发,是一种与 H.264 非常类似的编码格式(性能也很接近),Google 在 2010 年收购 On2 后便将 VP8 开放源代码。

Vorbis 是由 Xiph.Org 基金会 维护的开源音频编码格式,目的是与有专利限制的 MP3AAC 竞争,是开源世界最成功的音频编码格式之一。它的编码效率可以与 AAC 相比,大大高于 MP3

WebM 最近已新增了 VP9 视频编码和 Opus 音频编码的 WebM with VP9 版本,Chrome 30 开始已经可以支持。但目前除了基于 Webkit 的几种桌面浏览器(Chrome、Safari、Opera)的较新版本外,还几乎没有其他浏览器和平台支持 WebM with VP9。

Chrome、Firefox、Opera 原生支持 WebM,IE 和 Safari 则需要安装编解码器。WebM 视频的扩展名一般是 webm。

Theora + Vorbis + Ogg

Ogg 现由 Xiph.Org 基金会维护,也是开源、有免授权费专利的技术,但它的历史比 WebM 更久。它包含 Theora 视频编码、 Vorbis 音频编码和 Ogg 容器。

Theora 源自 VP3,与 WebM 的 VP8 一样都由 On2 所开发,不过 VP3 早在 2001 年就开放源代码了。

Chrome、Firefox、Opera 以及所有的主要 Linux 发行版原生支持 Ogg。Ogg 视频的扩展名一般是 ogv。

以下是未安装任何附加组件的情况下,主要浏览器和移动设备对以上三种组合的支持程度:

IE Chrome Firefox Opera Safari iOS Android
H.264 + AAC + MP4 9+ 4+ 21+ 1 3+ 3+ 2.1+ 2
WebM 6+ 4+ 10.6+ 2.3+
Theora + Vorbis + Ogg 4+ 3.5+ 10.5+

1:在 Windows 7 以上支持,在 Mac 和 Linux 上不支持
2:播放时需要特殊处理

如何做到最大兼容

从上表可以看出,没有一种格式可以兼容所有的情况,必须使用至少两种组合。

  • 要支持移动设备、IE 和 Mac,H.264 + AAC + MP4 是必须的
  • 要支持开源平台,需要 WebM 或 Theora + Vorbis + Ogg

除了 H.264 + AAC + MP4 必须使用外,WebM 和 Theora + Vorbis + Ogg 的支持范围比较相近,只不过前者的技术更新,压缩效率更高,而后者的历史更久,在较老的平台上也受到支持。可酌情挑选其中一种。以下是三种都使用的例子:

<video width="640" height="360" controls>
    <source src="code-rush.m4v" type='video/mp4; codecs="avc1.42E01E,mp4a.40.2"'>
    <source src="code-rush.webm" type='video/webm; codecs="vp8,vorbis"'>
    <source src="code-rush.ogv" type='video/ogg; codecs="theora,vorbis"'>
</video>

在这里要解释一下 <source> 里的 type 是视频文件的 MIME Type,它描述了视频的容器和编码类型。也可以不写 codecs 的内容,但写上可以让浏览器无需下载视频文件即可判断自己能否播放,从而选择正确的格式。

这里 MP4 的 type 比较复杂:

avc1.42E01E
这是视频编码规格:
Baseline – avc1.42E0xx(xx 是 Level,下同)
Main – avc1.4D40xx
High – avc1.6400xx
Level 是以 16 进制表示的:如 Level 3 是 1E(30 的 16 进制),Level 4.1 是 29(41 的 16 进制)
mp4a.40.2
这是音频编码规格,代表 LC-AAC

可以用 JavaScript 判断浏览器是否支持一种 MIME Type:

var canPlayMP4 = document.createElement("video").canPlayType('video/mp4; codecs="avc1.42E01E,mp4a.40.2"');
// "probably":浏览器认为它可以支持指定的容器和编码格式,应该可以播放
// "maybe":浏览器认为它可以支持指定的容器,但不确定能否支持编码格式,也许可以播放
// "":浏览器无法支持指定的容器

要测试你的浏览器对各种 MIME Type 的支持情况,点击此处

为 HTML5 制作视频

我们往往需要将视频素材转换成适合 HTML5 的格式。HandbrakeFFmpeg 是常用的工具,它们都是跨平台的开源软件。Handbrake 有图形界面和命令行两种使用方式,而 FFmpeg 是纯命令行的。此外,还有一些有趣的工具可供选择:Miro Video Converter 是傻瓜型的,可以转换我们提到的三种视频格式;MediaCoder 是一个国产的软件,支持的格式很多,用起来也最麻烦;Firefogg 是一个 Firefox 扩展,可以制作 WebM 和 Ogg。

建议使用 FFmpeg,请在官方下载页找到自己操作系统的预编译包使用。Windows 64bit 和 OS X 用户也可以下载我编译的版本:

FFmpeg for HTML5 Video

在 OS X 上可以用 Homebrew 来安装:

$ brew install ffmpeg --without-xvid --without-faac --without-lame --without-libvo-aacenc --with-libvpx --with-theora --with-fdk-aac --with-libvorbis --with-opus

制作 HTML5 视频时,需要考虑到用户的带宽。以 H.264 来说,常用画面规格的建议平均码率如下:

320×240/24p 640×360/24p 854×480/24p 1280×720/24p 1920×1080/24p
码率 200kbps 400kbps 800kbps 1500kbps 3000kbps
Profile 1 Baseline Main Main High High
Level 1 2 3 3 3.1 4

1:H.264 的 Profile 和 Level 与分辨率、帧率、码率的关系可以参见这张表格,也可以让转换软件自动选择。

如果对画质要求较高,或使用了更高的帧率,或使用 Theora 时,需要稍微提高码率。

另外 H.264 和 VP8 都支持 2-pass 编码,第一 pass 是扫描整个素材,记录下素材的一些特征,第二 pass 才是真正的编码。这种方法虽然比较花时间,但可以取得比 1-pass 更好的效果,时间充裕的话建议使用。

对于音频编码,AAC 和 Vorbis 的立体声码率均以 160kbps 为佳。

用 FFmpeg 制作 MP4 视频 – 文档
$ ffmpeg -pass 1 -fastfirstpass 1 -i INPUT -c:v libx264 -an -f rawvideo -y /dev/null
$ ffmpeg -pass 2 -i INPUT \
         -c:v libx264 -s 1280x720 -b:v 1500k -profile:v high -level 3.1 \
         -c:a libfdk_aac -ac 2 -b:a 160k -movflags faststart OUTPUT.m4v

重要的参数:

-pass
如果不使用 2-pass 编码,则只需要执行第二个命令,并去掉 -pass 2
-fastfirstpass
相当于 Handbrake 里的 Turbo First Pass,可以加快第一 pass 的速度
-an -f rawvideo -y /dev/null
这是在第一 pass 中不处理音频,并丢弃输出
-c:a libfdk_aac
libfdk_aac 也就是 Fraunhofer FDK AAC,是 FFmpeg 目前可以调用的最好的 AAC 编码器,但它并不是完全开源的,如果你的 FFmpeg 编译时没有包括它,可以换用 libfaac 或 FFmpeg 自带的 aac (由于是试验性的所以要加参数 -strict experimental),效果都不如 libfdk_aac
-movflags faststart
对网络视频而言十分重要,这可以让视频在下载完之前就开始播放
用 FFmpeg 制作 WebM 视频 – 文档
$ ffmpeg -pass 1 -i INPUT -c:v libvpx -an -f rawvideo -y /dev/null
$ ffmpeg -pass 2 -i INPUT \
         -c:v libvpx -s 1280x720 -b:v 1500k \
         -c:a libvorbis -ac 2 -b:a 160k OUTPUT.webm
用 FFmpeg 制作 Ogg 视频 – 文档
$ ffmpeg -i INPUT \
         -c:v libtheora -s 1280x720 -b:v 1500k \
         -c:a libvorbis -ac 2 -b:a 160k OUTPUT.ogv

FFmpeg 的详细用法请详见官方文档

添加字幕

HTML5 视频可以添加 WebVTT 字幕轨道,方法是在 <video> 内加上 <track>

<video width="640" height="360" controls>
    <source src="code-rush.m4v" type='video/mp4; codecs="avc1.42E01E,mp4a.40.2"'>
    <source src="code-rush.webm" type='video/webm; codecs="vp8,vorbis"'>
    <source src="code-rush.ogv" type='video/ogg; codecs="theora,vorbis"'>
    <track kind="subtitles" label="中文字幕" src="code-rush.chs.vtt" srclang="zh" default></track>
    <track kind="subtitles" label="英文字幕" src="code-rush.eng.vtt" srclang="en"></track>
</video>

以下是字幕文件的内容示例:

WEBVTT

1
00:00:02.150 --> 00:00:12.150
从1998年3月到1999年4月
一个独立纪录片摄制组跟随着网景公司的一个软件工程师团队
记录了他们公司和互联网历史上的分水岭

2
00:00:16.970 --> 00:00:20.240
我和很多到这里来寻找

3
00:00:20.240 --> 00:00:22.050
硅谷体验的人都聊过

事实上,它的格式和 SRT 字幕几乎一样,只不过秒和毫秒之间是点,而不是 SRT 的逗号,另外第一行要加上一个 WEBVTT 标识。

WebVTT 是非常新的技术,目前还处于草案阶段。以下是当前主要浏览器和移动设备的支持情况:

IE Chrome Firefox Opera Safari iOS Android
WebVTT 10+ 18+ 24+ 1 15+ 6+ 7+ 4.4+

1:默认禁用,在 about:config 中打开 media.webvtt.enabled 设置项来启用

要详细了解 WebVTT,可以看看这篇文章

不支持 HTML5

当然是 Flash fallback 了:

<video width="640" height="360" controls>
  <source src="code-rush.mp4" type='video/mp4; codecs="avc1.42E01E,mp4a.40.2"'>
  <source src="code-rush.webm" type='video/webm; codecs="vp8,vorbis"'>
  <source src="code-rush.ogv" type='video/ogg; codecs="theora,vorbis"'>
  <track kind="subtitles" label="中文字幕" src="code-rush.chs.vtt" srclang="zh" default></track>
  <track kind="subtitles" label="英文字幕" src="code-rush.eng.vtt" srclang="en"></track>
  <object type="application/x-shockwave-flash" data="http://releases.flowplayer.org/swf/flowplayer-3.2.1.swf" width="640" height="360">
    <param name="movie" value="http://releases.flowplayer.org/swf/flowplayer-3.2.1.swf" />
    <param name="allowFullScreen" value="true" />
    <param name="wmode" value="transparent" />
    <param name="flashVars" value="config={'playlist':['code-rush_poster.jpg',{'url':'code-rush.mp4','autoPlay':false}]}" />
    <img alt="Code Rush" src="code-rush_poster.jpg" width="640" height="360" title="No video playback capabilities, please download the video below" />
  </object>
</video>

<video> 标签里,按往常一样加上 Flash 播放器即可。注意:Flash 支持播放 H.264 视频,所以不需要再制作一个 FLV 出来。

Nginx区分PC或手机访问不同网站

近几年来,随着手机和pad的普及,越来越多的用户选择使用移动客户端访问网站,而为了获取更好的用户体验,就需要针对不同的设备显示出最合适的匹配,这样就是近年来流行的“响应式web设计”。

响应式web设计是一种纯前端技术js、css等实现的针对不同设备访问同一网址看到不同的布局,是页面内容更适合当前设备阅读。但这个不是本文的重点,重点还是放在nginx如何实现上来。

本文要讲的的是如何使用nginx区分pc和手机访问不同的网站,是物理上完全隔离的两套网站(一套移动端、一套pc端),这样带来的好处pc端和移动端的内容可以不一样,移动版网站不需要包含特别多的内容,只要包含必要的文字和较小的图片,这样会更节省流量。有好处当然也就会增加困难,难题就是你需要维护两套环境,并且需要自动识别出来用户的物理设备并跳转到相应的网站,当判断错误时用户可以自己手动切换回正确的网站。

下面以264查询网为实例来说明如何实现上面的需求。
明确的的需求:
1.制作两个站点PC端网站www.264.cn,和移动端网站m.264.cn
2.使用pc或移动设备访问任何一个域名都会跳到相应的站点。
3.用户可以选择访问移动版还是PC版网站,移动版网站始终有切换到PC版的链接,PC版当网站通过手机访问时会提供移动版网站的链接。
4.当用户选着访问其中一种类型的网站后,保存设置结果生效时间为24小时,当然长短可以自己设置。

简单的服务器端实现方法
有两套网站代码,一套PC版放在/usr/local/website/web,一套移动版放在/usr/local/website/mobile。只需要修改nginx的配置文件件,nginx通过UA来判断是否来自移动端访问,实现不同的客户端访问不同内容。
这种方法的缺点是移动端和PC端用同一个域名,存在黑帽的嫌疑,而且UA并不是总是判断的准确,如果判断错误的情况下,用户不能手动修改访问的网站类型。
关键的Nginx配置如下:

location / {
	#默认PC端访问内容
    root /usr/local/website/web;

	#如果是手机移动端访问内容
    if ( $http_user_agent ~ "(MIDP)|(WAP)|(UP.Browser)|(Smartphone)|(Obigo)|(Mobile)|(AU.Browser)|(wxd.Mms)|(WxdB.Browser)|(CLDC)|(UP.Link)|(KM.Browser)|(UCWEB)|(SEMC\-Browser)|(Mini)|(Symbian)|(Palm)|(Nokia)|(Panasonic)|(MOT\-)|(SonyEricsson)|(NEC\-)|(Alcatel)|(Ericsson)|(BENQ)|(BenQ)|(Amoisonic)|(Amoi\-)|(Capitel)|(PHILIPS)|(SAMSUNG)|(Lenovo)|(Mitsu)|(Motorola)|(SHARP)|(WAPPER)|(LG\-)|(LG/)|(EG900)|(CECT)|(Compal)|(kejian)|(Bird)|(BIRD)|(G900/V1.0)|(Arima)|(CTL)|(TDG)|(Daxian)|(DAXIAN)|(DBTEL)|(Eastcom)|(EASTCOM)|(PANTECH)|(Dopod)|(Haier)|(HAIER)|(KONKA)|(KEJIAN)|(LENOVO)|(Soutec)|(SOUTEC)|(SAGEM)|(SEC\-)|(SED\-)|(EMOL\-)|(INNO55)|(ZTE)|(iPhone)|(Android)|(Windows CE)|(Wget)|(Java)|(curl)|(Opera)" )
	{
		root /usr/local/website/mobile;
	}

	index index.html index.htm;
}

纯客户端js实现方式
下面这段代码放到首页<head>和</head>之间即可

<script type="text/javascript">// <![CDATA[
 if(/AppleWebKit.*Mobile/i.test(navigator.userAgent) || (/MIDP|SymbianOS|NOKIA|SAMSUNG|LG|NEC|TCL|Alcatel|BIRD|DBTEL|Dopod|PHILIPS|HAIER|LENOVO|MOT-|Nokia|SonyEricsson|SIE-|Amoi|ZTE/.test(navigator.userAgent))){
 	if(window.location.href.indexOf("?mobile")<0){
 		try{
 			if(/Android|webOS|iPhone|iPod|BlackBerry/i.test(navigator.userAgent)){
                                 //触屏手机版地址
 				window.location.href="http://m.264.cn";
 			}else if(/iPad/i.test(navigator.userAgent)){
                                 //pad版地址
 			}else{
                                 //普通手机版地址
 				window.location.href="http://wap.264.cn"
 			}
 		}catch(e){}
 	}
 }
 // ]]></script>

推荐的nginx区别手机和PC访问方法
利用前端js和后端nginx配合,js通过设置cookie来设定当前访问哪页面。

增加设置cookie的js代码,这段代码需要在移动网站和PC网站的所有页面都要放置。

function createCookie(name, value, days, domain, path) {
  var expires = '';
  if (days) {
    var d = new Date();
    d.setTime(d.getTime() + (days*24*60*60*1000));
    expires = '; expires=' + d.toGMTString();
  }
  domain = domain ? '; domain=' + domain : '';
  path = '; path=' + (path ? path : '/');
  document.cookie = name + '=' + value + expires + path + domain;
}

function readCookie(name) {
  var n = name + '=';
  var cookies = document.cookie.split(';');
  for (var i = 0; i < cookies.length; i++) {
     var c = cookies[i].replace(/^\s+/, '');
     if (c.indexOf(n) == 0) {
       return c.substring(n.length);
     }
   }
   return null;
 }
 
 function eraseCookie(name, domain, path) {
   setCookie(name, '', -1, domain, path);
 }
 
 

nginx增加如下配置,根据UA和cookie判断当前是移动端还是PC端访问

if ($http_user_agent ~* '(Android|webOS|iPhone|iPod|BlackBerry)') {
  set $mobile_request '1';
}
if ($http_cookie ~ 'mobile_request=full') {
  set $mobile_request '';
}
if ($mobile_request = '1') {
  rewrite ^.+ http://m.264.cn$uri;
}

移动版页面添加PC版链接
默认用户进来时会先判断UA,如果是手机端访问就会进入手机版,但也会存在误判进入手机版或者需要更多信息进入PC版,那么就需要在移动版的页面放入代码,让用户可以从移动版切换到web版并且下次访问会保留设置。

<a onclick="setCookie('iphone_mode', 'full', 1, '264.cn')" href="http://www.264.cn">
  电脑版
</a>

如果用户访问不正确时,点击电脑版链接就可以进入PC版网站,并且24小时内再次访问会记忆上次访问的网站类型设置。

PC版网站增加访问手机版的链接
在PC版的网站适当的地方加入下面的链接让用户可以切换到手机版的网站。

<a onclick="deleteCookie('mobile_mode', '264.cn');" href="http://m.264.cn">
  手机版
</a>

完整的nginx端配置,当然是去掉了与本文功能无关的配置,并不是一个完可用的配置,只是给大家一个整体的框架。

PC版网站配置

upstream app_server {
  server 0.0.0.0:9001;
}

server {
  listen 80;
  server_name www.264.cn;

  root /path/to/main_site;
  # ...

  location / {
    proxy_set_header X-Real-IP $remote_addr;
    # ...

    if ($http_user_agent ~* '(Android|webOS|iPhone|iPod|BlackBerry)') {
      set $mobile_request '1';
    }
    if ($http_cookie ~ 'mobile_request=full') {
      set $mobile_request '';
    }
    if ($mobile_request = '1') {
      rewrite ^.+ http://m.264.cn$uri;
    }

    # serve cached pages ...

    if (!-f $request_filename) {
      proxy_pass http://app_server;
      break;
    }
  }
}

手机移动版配置

upstream m_app_server {
server 0.0.0.0:9001;
}

server {
  listen 80;
  server_name m.264.cn;

  root /path/to/mobile_site;
  # ...

  location / {
    proxy_set_header X-Real-IP $remote_addr;
    # ...

    if ($http_user_agent ~* '(Android|webOS|iPhone|iPod|BlackBerry)') {
      set $mobile_request '1';
    }
    if ($http_cookie ~ 'mobile_request=full') {
      set $mobile_request '';
    }
    if ($mobile_request != '1') {
      rewrite ^.+ http://www.264.cn$uri;
    }

    # serve cached pages ...

    if (!-f $request_filename) {
      proxy_pass http://m_app_server;
      break;
    }
  }
}

 

Nginx区分PC或手机访问不同网站

Nginx CORS实现JS跨域

1. 什么是跨域

简单地理解就是因为JavaScript同源策略的限制,a.com 域名下的js无法操作b.com或是c.a.com域名下的对象。

同源是指相同的协议、域名、端口。特别注意两点:

  • 如果是协议和端口造成的跨域问题“前台”是无能为力的,
  • 在跨域问题上,域仅仅是通过“协议+域名+端口”来识别,两个不同的域名即便指向同一个ip地址,也是跨域的。

2. 跨域解决方案

跨域解决方案有多种,大多是利用JS Hack:

3. CORS

CORS: 跨域资源共享(Cross-Origin Resource Sharing)http://www.w3.org/TR/cors/

当前几乎所有的浏览器(Internet Explorer 8+, Firefox 3.5+, Safari 4+和 Chrome 3+)都可通过名为跨域资源共享(Cross-Origin Resource Sharing)的协议支持ajax跨域调用。(see: http://caniuse.com/#search=cors)

Chrome, Firefox, Opera and Safari 都使用的是 XMLHttpRequest2 对象, IE使用XDomainRequest。XMLHttpRequest2的Request属性:open()、setRequestHeader()、timeout、withCredentials、upload、send()、send()、abort()。

XMLHttpRequest2的Response属性:status、statusText、getResponseHeader()、getAllResponseHeaders()、entity、overrideMimeType()、responseType、response、responseText、responseXML。

启用 CORS 请求

假设您的应用已经在 example.com 上了,而您想要从 www.example2.com 提取数据。一般情况下,如果您尝试进行这种类型的 AJAX 调用,请求将会失败,而浏览器将会出现“源不匹配”的错误。利用 CORS,www.example2.com 服务端只需添加一个HTTP Response头,就可以允许来自 example.com 的请求:

Access-Control-Allow-Origin: http://example.com
Access-Control-Allow-Credentials: true(可选)

可将 Access-Control-Allow-Origin 添加到某网站下或整个域中的单个资源。要允许任何域向您提交请求,请设置如下:

Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true(可选)

其实,该网站 (html5rocks.com) 已在其所有网页上均启用了 CORS。启用开发人员工具后,您就会在我们的响应中看到 Access-Control-Allow-Origin 了。

提交跨域请求

如果服务器端已启用了 CORS,那么提交跨域请求就和普通的 XMLHttpRequest 请求没什么区别。例如,现在 example.com 可以向 www.example2.com 提交请求了:

var xhr = new XMLHttpRequest();
// xhr.withCredentials = true; //如果需要Cookie等
xhr.open('GET', 'http://www.example2.com/hello.json');
xhr.onload = function(e) {
  var data = JSON.parse(this.response);
  ...
}
xhr.send();

4. 服务端Nginx配置

要实现CORS跨域,服务端需要这个一个流程:http://www.html5rocks.com/static/images/cors_server_flowchart.png

对于简单请求,如GET,只需要在HTTP Response后添加Access-Control-Allow-Origin。

对于非简单请求,比如POST、PUT、DELETE等,浏览器会分两次应答。第一次preflight(method: OPTIONS),主要验证来源是否合法,并返回允许的Header等。第二次才是真正的HTTP应答。所以服务器必须处理OPTIONS应答。

http://enable-cors.org/server_nginx.html这里是一个nginx启用COSR的参考配置。

流程如下:

  1. 首先查看http头部有无origin字段;
  2. 如果没有,或者不允许,直接当成普通请求处理,结束;
  3. 如果有并且是允许的,那么再看是否是preflight(method=OPTIONS);
  4. 如果是preflight,就返回Allow-Headers、Allow-Methods等,内容为空;
  5. 如果不是preflight,就返回Allow-Origin、Allow-Credentials等,并返回正常内容。

用伪代码表示:

location /pub/(.+) {
    if ($http_origin ~ <允许的域(正则匹配)>) {
        add_header 'Access-Control-Allow-Origin' "$http_origin";
        add_header 'Access-Control-Allow-Credentials' "true";
        if ($request_method = "OPTIONS") {
            add_header 'Access-Control-Max-Age' 86400;
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE';
            add_header 'Access-Control-Allow-Headers' 'reqid, nid, host, x-real-ip, x-forwarded-ip, event-type, event-id, accept, content-type';
            add_header 'Content-Length' 0;
            add_header 'Content-Type' 'text/plain, charset=utf-8';
            return 204;
        }
    }
    # 正常nginx配置
    ......
}

但是由于Nginx 的 if 是邪恶的,所以配置就相当地让人不爽(是我的配置不够简洁吗?)。下面nginx-spdy-push里/pub接口启用CORS的配置:

# push publish
# broadcast channel name must start with '_'
# (i.e., normal channel must not start with '_')

# GET    /pub/channel_id -> get statistics about a channel
# POST   /pub/channel_id -> publish a message to the channel
# DELETE /pub_admin?id=channel_id -> delete the channel

#rewrite_log on;

# server_name test.gw.com.cn
# listen      2443 ssl spdy

location ~ ^/pub/([-_.A-Za-z0-9]+)$ {

    set $cors "local";

    # configure CORS based on https://gist.github.com/alexjs/4165271
    # (See: http://www.w3.org/TR/2013/CR-cors-20130129/#access-control-allow-origin-response-header )

    if ( $http_origin ~* "https://.+\.gw\.com\.cn(?=:[0-9]+)?" ) {
        set $cors "allow";
    }
    if ($request_method = "OPTIONS") {
        set $cors "${cors}options";
    }

    # if CORS request is not a simple method
    if ($cors = "allowoptions") {
        # Tells the browser this origin may make cross-origin requests
        add_header 'Access-Control-Allow-Origin' "$http_origin";
        # in a preflight response, tells browser the subsequent actual request can include user credentials (e.g., cookies)
        add_header 'Access-Control-Allow-Credentials' "true";

        # === Return special preflight info ===

        # Tell browser to cache this pre-flight info for 1 day
        add_header 'Access-Control-Max-Age' 86400;

        # Tell browser we respond to GET,POST,OPTIONS in normal CORS requests.
        # Not officially needed but still included to help non-conforming browsers.
        # OPTIONS should not be needed here, since the field is used
        # to indicate methods allowed for 'actual request' not the preflight request.
        # GET,POST also should not be needed, since the 'simple methods' GET,POST,HEAD are included by default.
        # We should only need this header for non-simple requests  methods (e.g., DELETE), or custom request methods (e.g., XMODIFY)
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE';

        # Tell browser we accept these headers in the actual request
        add_header 'Access-Control-Allow-Headers' 'reqid, nid, host, x-real-ip, x-forwarded-ip, event-type, event-id, accept, content-type';

        # === response for OPTIONS method ===

        # no body in this response
        add_header 'Content-Length' 0;
        # (should not be necessary, but included for non-conforming browsers)
        add_header 'Content-Type' 'text/plain, charset=utf-8';
        # indicate successful return with no content
        return 204;
    }

    if ($cors = "allow") {
        rewrite /pub/(.*) /pub_cors/$1 last;
    }

    if ($cors = "local") {
        rewrite /pub/(.*) /pub_int/$1 last;
    }
}

location ~ /pub_cors/(.*) {
    internal;
    # Tells the browser this origin may make cross-origin requests
    add_header 'Access-Control-Allow-Origin' "$http_origin";
    # in a preflight response, tells browser the subsequent actual request can include user credentials (e.g., cookies)
    add_header 'Access-Control-Allow-Credentials' "true";

    push_stream_publisher                   admin; # enable delete channel
    set $push_stream_channel_id             $1;

    push_stream_store_messages              on;  # enable /sub/ch.b3
    push_stream_channel_info_on_publish     on;
}

location ~ /pub_int/(.*) {
  #  internal;
    push_stream_publisher                   admin; # enable delete channel
    set $push_stream_channel_id             $1;

    push_stream_store_messages              on;  # enable /sub/ch.b3
    push_stream_channel_info_on_publish     on;
}

5. 客户端javascript代码

下面是https://spdy.gw.com.cn/sse.html里的代码

var xhr = new XMLHttpRequest();
//xhr.withCredentials = true;
xhr.open("POST", "https://test.gw.com.cn:2443/pub/ch1", true);
// xhr.setRequestHeader("accept", "application/json");

xhr.onload = function()  {
  $('back').innerHTML = xhr.responseText;
  $('ch1').value = + $('ch1').value + 1;
}
xhr.onerror = function() {
  alert('Woops, there was an error making the request.');
};

xhr.send($('ch1').value);

页面的域是https://spdy.gw.com.cn, XMLHttpRequest的域是https://test.gw.com.cn:2443,不同的域,并且Post方式。

用Chrome测试,可以发现有一次OPTIONS应答。如过没有OPTIONS应答,可能是之前已经应答过,被浏览器缓存了'Access-Control-Max-Age' 86400;,清除缓存,再试验。

6. 相关链接

在nginx上使用php代理运行cgi程序

我们用到的很多开源程序比如mailman, nagios等等,都有WEB端管理界面。在那个Apache一家独大的年代,这个问题可以很好解决,因为apache本身可以运行cgi程序。但随着 nginx服务器的大规模应用,而恰好nginx又没有cgi模块,所以我们不得不采用一些变通的手段来解决它。

在网上广为流传的解决方法是一个老外写的perl脚本,但这个脚本本身有很多问题,而且需要在后台启动一个守护进程,本人对用perl写的网络服务守护进程的稳定性很怀疑,在看了它的代码后,发现用PHP即可很好的解决这个问题。

CGI其实本质上就是一个普通的二进制程序,你可以在后台直接运行它。而服务器要做的事就是将WEB传递的变量作为参数传递给这个程序并执行,而将执行返回的结果显示到页面上。

明白了这个道理,我们就可以开始着手解决这个问题了。其过程无非就是将PHP作为一个proxy,使其运行指定的程序,并把程序输出结果echo出来。

我们把这个PHP脚本命名为cgi.php,把它随便放到一个你认为合适的位置,然后用rewrite将后缀为cgi的请求都转发到cgi.php上。以下为参考的配置格式

#rewrite cgi请求到cgi.php上,并把cgi文件名作为php的pathinfo
rewrite ^/nagios/cgi-bin/(.*) /cgi.php/$1 break;
 
location /nagios/
{
    gzip off;
    alias /usr/local/nagios/share/;
    index index.html index.htm index.php;
}
 
location ~ .*\.php(\/.*)*$ {
    fastcgi_pass    127.0.0.1:9000;
    fastcgi_index   index.php;
    include fcgi.conf;
    fastcgi_param SCRIPT_FILENAME /usr/local/nagios/share$fastcgi_script_name;
 
    #pathinfo必须设置
    fastcgi_param  PATH_INFO $fastcgi_script_name;
 
    #以下两个为cgi.php需要用到的变量名,分别为cgi程序目录,和cgi默认index程序
    fastcgi_param  CGI_BASE  /usr/local/nagios/sbin;
    fastcgi_param  CGI_INDEX status.cgi;
}

注意上面配置文件的注释部分,在你自己设置的时候必须填上合适的值。下面就是最重要的cgi.php文件了

<?php
/*
   use php to execute mailman cgi app
   hack by 70 (magike.net@gmail.com)
   https://joyqi.com
 */
 
// get cgi base from fastcgi param
$cgi_base = '';
if (isset($_SERVER['CGI_BASE'])) {
    $cgi_base = rtrim($_SERVER['CGI_BASE'], '/') . '/';
} else {
    die('PLEASE CONFIGURE YOUR CGI_BASE PARAM');
}
 
// get pathinfo
$pathinfo = '';
if (isset($_SERVER['PATH_INFO'])) {
    $pathinfo = $_SERVER['PATH_INFO'];
} else if (isset($_SERVER['CGI_INDEX'])) {
    $pathinfo = $_SERVER['CGI_INDEX'];
} else {
    die('PLEASE CONFIGURE YOUR PATH_INFO PARAM');
}
 
// get real cgi path
$cgi_path = $cgi_base;
$cgi_file = trim($pathinfo, '/');
$cgi_file_levels = explode('/', $cgi_file);
$cgi_file_exists = false;
 
while (count($cgi_file_levels) > 0) {
    $cgi_path = $cgi_path . '/' . array_shift($cgi_file_levels);
 
    if (is_file($cgi_path)) {
        $cgi_file_exists = true;
        break;
    }
}
 
if (!$cgi_file_exists) {
    die('NOT EXISTS PAGE!' . $cgi_file);
}
 
$cgi_pathinfo = '';
if (!empty($cgi_file_levels)) {
    $cgi_pathinfo = '/' . implode('/', $cgi_file_levels);
}
 
if (is_readable($cgi_path)) {
 
    $descriptorspec = array(
            0 => array("pipe", "r"),  // stdin is a pipe that the child will read from
            1 => array("pipe", "w"),  // stdout is a pipe that the child will write to
            2 => array("file", "/tmp/error-output.txt", "a") // stderr is a file to write to
            );
 
    $cwd = $cgi_base;
    $env = $_ENV;
 
    $env['SCRIPT_FILENAME'] = $cgi_path;
    $env['SCRIPT_NAME'] = $cgi_file;
    $env['DOCUMENT_ROOT'] = CGI_BASE;
    $env['PATH_INFO'] = $cgi_pathinfo;
 
    // http auth support (nagios etc.)
    if (isset($_SERVER['PHP_AUTH_USER'])) {
        $env['REMOTE_USER'] = $_SERVER['PHP_AUTH_USER'];
    }
 
    $process = proc_open($cgi_path, $descriptorspec, $pipes, $cwd, $env);
    if (is_resource($process)) {
        $stdin = file_get_contents("php://input");
 
        if (!empty($stdin)) {
            fwrite($pipes[0], $stdin);
            fclose($pipes[0]);
        }
 
        //stream_set_blocking($pipes[1], 0);
        stream_set_timeout($pipes[1], 3);
        $result = stream_get_contents($pipes[1]);
        fclose($pipes[1]);
        $return_value = proc_close($process);
 
        list($header, $body) = preg_split("/\r?\n\r?\n/", $result, 2);
 
        $headers = explode("\n", $header);
        foreach ($headers as $line) {
            header(trim($line));
        }
 
        echo $body;
    } else {
        die('ERROR APPLICATION!');
    }
 
} else {
    die('ERROR PAGE!' . $cgi_path);
}

Nginx配置免费SSL证书StartSSL,解决Firefox不信任问题

先在StartSSL上申请免费一年的SSL证书,具体过程网上很多教程。然后把申请到的key和crt文件上传到服务器,比如/usr/local/nginx/certs/.

 Nginx配置SSL证书

直接贴上我的nginx的部分配置:

server {
        listen 443;
	server_name   liuzhichao.com www.liuzhichao.com ;
        ssl on;
        ssl_certificate /usr/local/nginx/ssl/ssl.crt;
        ssl_certificate_key /usr/local/nginx/ssl/ssl.key;if($http_transfer_encoding ~* chunked){return444;}

	gzip on;if(-d $request_filename){
		rewrite ^/(.*)([^/])$ $scheme://$host/$1$2/ permanent;}

	 root   /home/wwwroot/;

	 ssi off;
	 ssi_silent_errors off;
	 ssi_types text/shtml;

	 location /{
		 index  index.html index.htm index.shtml index.php;
		 autoindex	off;}

	location /nginx_status {
		stub_status on;
		access_log off;}

	 location ~(favicon.ico){ 
		 log_not_found off;
		 access_log   off;}

	 location ~* \.(gif|jpg|jpeg|png|bmp|swf)$ {
		 expires 1y;}

	 location ~* \.(js|css)$ {
		 expires 7d;}#------------
	 location ~*^(.+)\.(php[3-9]?|phtm[l]?)(\/.*)*$ {set $real_script_name $1.$2;set $path_info $3;if(!-f $document_root$real_script_name){return404;}

		  fastcgi_pass 127.0.0.1:8999; fastcgi_param HTTPS on;
		  include enable_php.conf;}}

现在重启Nginx,Chrome应该能正常显示Https.如果只想使用Https连接,可以再添加一个server,然后跳转到https

server {
        listen 80;
	server_name   liuzhichao.com www.liuzhichao.com ;
        rewrite     ^   https://$server_name$request_uri? permanent;}

 解决Firefox不信任StartSSL证书问题

wget http://cert.startssl.com/certs/ca.pem
wget http://cert.startssl.com/certs/sub.class1.server.ca.pem
cat ca.pem sub.class1.server.ca.pem >> ca-certs.crt
cat ca-certs.crt >> ssl.crt

再次重启Nginx,本想这下Firefox也应该能正常识别证书了,但是重启Nginx遇到了SSL: error:0906D066:PEM routines:PEM_read_bio:bad end line error错误。

[emerg]: SSL_CTX_use_certificate_chain_file("/usr/local/nginx/certs/ssl.crt")
 failed (SSL: error:0906D066:PEM routines:PEM_read_bio:bad end line error:140DC009:SSL routines:SSL_CTX_use_certificate_chain_file:PEM lib)
configuration file /usr/local/nginx/conf/nginx.conf test failed

这个的意思就是server.crt读取到意外错误行.这是因为我们在合并StartSSL提供的crt证书时,直接cat到了ssl.crt里。使用vi或者nano命令打开并编辑ssl.crt,找到:

-----END CERTIFICATE----------BEGIN CERTIFICATE-----

修改为:

-----END CERTIFICATE----------BEGIN CERTIFICATE-----

保存这个crt文件,再次重启Nginx服务,输入申请证书时私钥的密码,启动成功后,现在使用Firefox访问网站也能信任证书了。

Discover Meteor Book 中文

Meteor 是一个构建在 Node.js 之上的平台,用来开发实时网页程序。Meteor 位于程序数据库和用户界面之间,保持二者之间的数据同步更新。

因为 Meteor 是基于 Node.js 开发的,所以在客户端和服务器端都使用 JavaScript 作为开发语言。而且,Meteor 程序的代码还能在前后两端共用。

Meteor 这个平台很强大,网页程序开发过程中的很多复杂、容易出错的功能都能抽象出来,实现起来很简单。

那么,你为什么要花时间学习 Meteor,而不去学其他框架呢?拨开 Meteor 的各种功能,我们认为原因只有一个:因为 Meteor 易于学习。

http://zh.discovermeteor.com/pdf

Git工作流指南:Gitflow工作流

常用的分支约定:

用于新建发布分支的分支: develop
用于合并的分支: master
分支命名: release-* 或 release/*

维护分支

git-workflow-release-cycle-4maintenance

维护分支或说是热修复(hotfix)分支用于生成快速给产品发布版本(production releases)打补丁,这是唯一可以直接从master分支fork出来的分支。修复完成,修改应该马上合并回master分支和develop分支(当前的发布分支),master分支应该用新的版本号打好Tag

Bug修复使用专门分支,让团队可以处理掉问题而不用打断其它工作或是等待下一个发布循环。你可以把维护分支想成是一个直接在master分支上处理的临时发布。

 

https://github.com/oldratlee/translations/blob/master/git-workflows-and-tutorials/workflow-feature-branch.md

 

 

 

自定义元素:在 HTML 中定义新元素

Eric Bidelman
翻译: 米粽 (Leo Deng) 已发布: 八月 28th, 2013  已更新: 十二月 18th, 2013

This article discusses APIs that are not yet fully standardized and still in flux. Be cautious when using experimental APIs in your own projects.

引言

现在的 web 严重缺乏表达能力。你只要瞧一眼“现代”的 web 应用,比如 GMail,就会明白我的意思:

现代 web 应用:使用

堆砌而成。
堆砌

一点都不现代。然而可悲的是,这就是我们构建 web 应用的方式。在现有基础上我们不应该有更高的追求吗?

时髦的标记,行动起来!

HTML 为我们提供了一个完美的文档组织工具,然而 HTML 规范定义的元素却很有限。

假如 GMail 的标记不那么糟糕,而是像下面这样漂亮,那会怎样?

真是令人耳目一新!这个应用太合理了,既有意义,又容易理解。最妙的是,它是可维护的,只要查看声明结构就可以清楚地知道它的作用。

自定义元素,救救我们!就指望你了!
赶紧开始吧

自定义元素 允许开发者定义新的 HTML 元素类型。该规范只是 Web 组件模块提供的众多新 API 中的一个,但它也很可能是最重要的一个。没有自定义元素带来的以下特性,Web 组件都不会存在:

定义新的 HTML/DOM 元素
基于其他元素创建扩展元素
给一个标签绑定一组自定义功能
扩展已有 DOM 元素的 API
注册新元素

使用 document.registerElement() 可以创建一个自定义元素:

var XFoo = document.registerElement(‘x-foo’);
document.body.appendChild(new XFoo());
document.registerElement()

的第一个参数是元素的标签名。这个标签名必须包括一个连字符(-)。因此,诸如 、 和 都是合法的标签名,而 和 <foo_bar> 则不是。这个限定使解析器能很容易地区分自定义元素和 HTML 规范定义的元素,同时确保了 HTML 增加新标签时的向前兼容。

第二个参数是一个(可选的)对象,用于描述该元素的 prototype。在这里可以为元素添加自定义功能(例如:公开属性和方法)。稍后详述。

自定义元素默认继承自 HTMLElement,因此上一个示例等同于:

var XFoo = document.registerElement(‘x-foo’, {
prototype: Object.create(HTMLElement.prototype)
});

调用 document.registerElement(‘x-foo’) 向浏览器注册了这个新元素,并返回一个可以用来创建 元素实例的构造器。如果你不想使用构造器,也可以使用其他实例化元素的技术。

如果你不希望在 window 全局对象中创建元素构造器,还可以把它放进命名空间(var myapp = {}; myapp.XFoo = document.registerElement(‘x-foo’);)。

然后直接上代码了~~

<!DOCTYPE html>
<!–
Copyright 2013 The Polymer Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
–>
<html>
<head>
<title></title>
<meta charset=”UTF-8″>
<link rel=”import” href=”test-component.html”>
</head>
<body>

<x-foo-shadowdom></x-foo-shadowdom>
<script>

var XFooProto = Object.create(HTMLElement.prototype);

XFooProto.createdCallback = function() {
// 1. 为元素附加一个 shadow root。
var shadow = this.createShadowRoot();

// 2. 填入标记。
shadow.innerHTML = “<b>I’m in the element’s Shadow DOM!</b>”;
};

var XFoo = document.registerElement(‘x-foo-shadowdom’, {prototype: XFooProto});

</script>
</body>
</html>

http://www.html5rocks.com/zh/tutorials/webcomponents/customelements/#intro

Mac 电脑 装机必备软件 推荐

一、输入法

Mac 自带的中文输入法确实很不好用,因此建议安装第三方输入法,至于搜狗、QQ、百度三个输入法的选择,主要看个人习惯了,功能上差不多,笔者使用的是搜狗。

1.搜狗输入法:http://ifunapple.com/2014/06/sougou-pinyin-29/
2.QQ输入法:http://ifunapple.com/2013/09/qqime/
3.百度输入法:http://ifunapple.com/2013/06/baidu-ime/

二、浏览器

Mac 上的流行浏览器和 Windows 上差不多,除了 IE 被 Safari 替代,个人感觉 Chrome 性能最高,Safari 也不错,而 Firefox 作为开发者一定要安装。
1.Chrome:http://ifunapple.com/2014/08/chrome-36/
2.Safari:系统自带
3.Firefox:http://ifunapple.com/2014/08/firefox-31/

三、下载工具

Mac 上的下载工具并不多,大部分时候使用浏览器直接下载就可以,迅雷和Folx是比较不错的两款下载工具。
1.迅雷:http://ifunapple.com/2014/06/thunder-2-6/
2.Folx:http://ifunapple.com/2014/05/folx-go/

四、看图工具

Mac 系统默认使用「预览」打开图片,但「预览」最大的不足是不支持图片上一张、下一张的切换,很不方便,因此建议安装第三方看图软件
1.ArcSoft Photo+:http://ifunapple.com/2013/12/arcsoft-photo-3/

五、音乐播放器

音乐播放直接使用iTunes就可以,而在线音乐播放软件目前「百度音乐」和「QQ 音乐」做的比较不错,其中百度音乐支持音乐下载。
1.百度音乐:http://ifunapple.com/2014/01/baidu-music-103/
2.QQ 音乐:http://ifunapple.com/2014/08/qq-music-1-4/
3.iTunes:系统自带

六、视频播放器

Quicktime 支持的视频格式太少了,MPlayerX和KPlayer都可以作为首选视频播放器,支持几乎所有常见的视频格式。
1.MPlayerX:http://ifunapple.com/2014/03/mplayerx-1022/
2.KPlayer:http://ifunapple.com/2014/05/kplayer-143/

七、压缩解压工具

BetterZip和Entropy都是很不错的压缩解压缩工具,支持 ZIP、RAR、TAR 等几乎所有常见的压缩格式。
1.BetterZip:http://ifunapple.com/2014/03/betterzip-233/
2.Entropy:http://ifunapple.com/2013/12/entropy/

八、聊天社交工具

1.QQ:http://ifunapple.com/2014/05/qq-3-1-2/
2.微信:http://ifunapple.com/2014/02/weichat/

九、系统增强

这六款软件是站长认为必须安装的系统增强工具,能够大大提升使用 Mac 的效率
1.Parallels Desktop 9:Mac 上最好用的虚拟机,虽然说 Mac 上的软件已经够多,但肯定有一些软件需要你必须在 Windows 上运行,比如网银插件等,因此安装个 Windows 系统以备不时之需,但站长不建议安装双系统,主要是来回切换太麻烦了,建议在 Mac 上安装Parallels Desktop虚拟机,然后在虚拟机中安装 Windows。

http://ifunapple.com/2014/07/parallels-desktop-9-0-24237/
2.Paragon NTFS:解决你的移动硬盘或 U 盘在 Mac 上只能读不能写的问题。
http://ifunapple.com/2014/04/ntfs-11-2/

3.CleanMyMac:系统清理和应用卸载工具。
http://ifunapple.com/2014/07/cleanmymac-2-2-7-b1/

4.iStat Menus:系统监控工具,如 CPU、网速、温度等等。
http://ifunapple.com/2014/08/istat-menus-5/

5.TotalFinder:Finder 增强工具,如剪切、多 Tab、显示隐藏文件、复制路径等。
http://ifunapple.com/2014/07/totalfinder-1-6-2/

6.BetterSnapTool:窗口大小和位置控制工具,比如拖拽窗口完成最大化。
http://ifunapple.com/2014/05/bettersnaptool/

十、文档办公

Mac 上的办公软件就是微软的Office和苹果的iWork了,如果习惯了 Office,就安装 Office 吧。
1.Microsoft Office:http://ifunapple.com/2014/06/office/
2.iWork:http://ifunapple.com/2013/10/iwork-2013/

十一、其他

1.邮件客户端:推荐使用系统自带的
2.PDF阅读工具:推荐使用系统预览应用
3.文本编辑:简单的使用系统的「文本编辑」就够用了

http://ifunapple.com/2014/08/subject-zjbb/