HTML5 Storage APIs

因为太久没用这些东西,之前被人问起时只能勉强答出两个,并且连事件也不记得,只好把常用的列出来强化记忆了。

localStoragesessionStorage 的 APIs 在行为表现上是一致的,以下以 localStorage 为例,而 globalStorage 因为不是标准,所以略过。

函数和属性:

setItem(key, value)

valuekey 为名存入 localStorage

keyvalue 理论上可接受任何类型,但实际会通过 String(value) 转换为字符串,所以需慎用,比如 Object 会转换成 '[object Object]'null 会转换成 'null'

localStorage.setItem('hello', 'Chris')
localStorage.key(0) + ' ' + localStorage.getItem('hello') // 'hello Chris'

localStorage.clear()
var obj = { str: 'an object' }
localStorage.setItem(obj, { posts: [{}, {}] })
localStorage.key(0) // '[object Object]'
localStorage.getItem(obj) // '[object Object]'
var otherObj = { str: 'another object' }
localStorage.setItem(otherObj, true)
localStorage.getItem(obj) // 'true'

localStorage.clear()
localStorage.setItem(null, null)
localStorage.getItem(null) // 'null'

getItem(key)

要注意 String() 的问题。

key 不存在,getItem() 会返回 null,如果在 setItem() 时并没有进行严格验证的话,建议同时判断是否为 'null' 来确定是否存在。

访问 key/value 的其他方法

localStorage 可以像数组一样操作,而 key 也会作为普通对象的属性一样被访问。

localStorage['hello'] = 'world'
localStorage.hello // 'world'
localStorage.hello = 'Chris'
localStorage['hello'] // 'Chris'

removeItem()clear()

一个删除单个,一个删除全部,不返回任何值,key 不存在时也不会报错。

key(keyID)

返回指定 ID 的 key 名字。

keyID 只接受整数,因为会通过 Math.floor(keyID) 来转换,所以支持非整数,如 '1.1''1a'true 等等,只是结果为 NaN 时取 0;若不存在,返回 null

已知问题:

  • Chrome v25 和 Firefox v19 的行为不一样:Chrome 会将 keys 按照字符串大小进行正排序,而 Firefox 则几乎是乱序,不可预测 key 的位置。
localStorage.setItem('google', 'what the hell?')
localStorage.setItem('miscrosoft', 'not bad')
localStorage.setItem('apple', 'great')

localStorage.key(0) // Chrome: apple
localStorage.key(1) // Chrome: google

length

返回 key 的总数。

事件

目前只支持一个事件:storage

if (window.addEventListener) {
  window.addEventListener("storage", handle_storage, false);
} else {
  window.attachEvent("onstorage", handle_storage);
};

这段代码我偷懒从 Dive Into HTML5 搬过来的,专有的事件属性如下:

  • key:不解释
  • oldValue:旧值
  • newValue:新值
  • url:页面的 URL

url 有点特殊,原来是叫 uri 的,所以要兼容某些浏览器的话,最好两个都判断,较新的标准浏览器应该没有这个问题。

此事件不可终止,并且只有在 setItem()removeItem()clear() 确实影响了值才会触发:

localStorage.setItem('hello', 'world') // will fire
localStorage.clear() // will fire
localStorage.clear() // will not fire

另外,对触发页面的要求也很特别:会触发事件的页面并不是执行修改操作的页面,而是已打开的、共享同一个储存区的其他页面

最后,事件绑定只能绑定在 window 上。

以上仅在 Chrome v25 和 Firefox v19 上测试过。

→ 为什么 Google 要砍掉 Google Reader?

在 Google 宣布将会在 2013 年 7 月 1 日关闭 Google Reader 之后,Quora 就有了一个问题:「为什么 Google 要砍掉 Google Reader?」。

曾经担任过 Reader PM 的 Brian Shih 回答说:Google 会关闭 Reader,只是为了社交领域,比如 Google+,而在此之前,Google 已经三度尝试把 Reader 团队拉到社交产品的开发上:

  • 2008:OpenSocial
  • 2009:Buzz
  • 2010:Google+

因为 Facebook 一直在影响着互联网广告市场,以广告为主要收入的 Google 自然不会放弃这块蛋糕,会进入社交领域属于必然之举。

而声称「不作恶」的 Google,并没有声称「不赚钱」,没有直接盈利又无法和社交媒体抗衡(毕竟不是一回事)的 Reader 自然没有运营下去的价值。因此,Google 的广告策略也备受吐槽,比如 Google Glass 的恶搞视频(YouTube优酷)。

Reader 被砍掉确实让人难过,因为它不仅储存着很多不可访问的网站的文章,还有着成熟的第三方,不过天下无不散之筵席,所以并不需要太纠结于此,何况 Reeder、Feedly 甚至 Digg 都表示会做一些事情。

从某个角度来说,因 Reader 而停滞多年的 RSS 市场,可能会因此有难以想象的成长。

塞翁失马,焉知非福,不是吗?

→ Animate.css: CSS 动画懒人包

Animate.css 打包了常见的 CSS 动画代码,只要勾上然后按按钮就直接下载打包好的 CSS 文件,然后在 HTML 代码里加上对应的 class 就可以,相当便利。

→ Why I loved building Basecamp for iPhone in RubyMotion

37signals 在最近发布 Basecamp for iPhone,由于 37signals 是 Ruby 的坚定支持者,所以该 app 是由 RubyMotion 打造的,甚至开发工具也跳过了 Xcode。

在上架不久后,Nick 发了一篇文章来说明他们是为什么选择和怎样使用 RubyMotion 打造这款应用。

学习成本和 IDE 的缺陷

Nick 对使用 Objective-C 开发应用的看法时:「如果使用 Objective-C,就意味着我需要抛弃我现有的工具和工作流,然后学习新的 API、框架和更多的东西(如 IDE)」,并且提到他早年使用 Visual Basic 控件和 Visual Studio 时的不好经验,因此非常抗拒使用 Xcode 和 Interface Builder。

我相当同意 Nick 的观点,就像 CoffeeScriptBrython 和 JavaScript 的关系,对于 Ruby 或 Python 程序员来说,直接用前两者会少很多学习成本。

而 Xcode,说实话,稳定性还没 Visual Studio 好,在代码补全、文档、Interface Builder、应用发布等方面虽然会比一般编辑器要好,所以还能忍受缺陷,但如果有代替方案呢?

Auto Layout

Auto Layout 是 iOS 6 新加的 API,为马脸 iPhone 5 准备的:

# horizontal
"|-10-[switchButton]-10-|" 
"|-10-[helpButton]-10-|" 

# vertical
"|-15-[switchButton]-10-[helpButton(==switchButton)]-15-|"

第三方框架和类库

Nick 吐槽了 Objective-C 第三方框架和类库令人不满的现状:

  • (相对于 Ruby)没有令人满意的依赖管理工具,CocoaPods 做得很好,可是还不够
  • 大部分不在 CocoaPods 的框架或类库的 README 只是简单地使用「拖曳 .xcodeproj 文件到 Xcode」或「拖曳 .h.m 文件到你的项目」等等让人头痛的说明(谁叫你不用 Xcode :P)

CocoaPods 我也有在用,虽然它让使用第三方框架和类库变得简单了一些,但并不是很完美,如果能像 RubyGems 或 Node Packaged Modules 那样的话,就真的真的 save my ass 了。

调试

37signals 是使用 rakeTestFlight 来进行调试,而 RubyMotion 官方也提供了如何调试的指南

因为最近多了一些 iOS 的调试器,都很逆天的感觉,比如 SuperDB,所以在调试方面,可能脱离 Xcode 也不会造成很大的问题。

迁移到 Hexo

忙了两天,终于从 Octopress 迁移到 Hexo 了,订阅的朋友,如果收到了重复的更新请见谅。

迁移的过程中,最困难的是想要尽可能兼容原本在 Octopress 能用的东西,比如 linklog 的自定义样式。除此之外,真的深刻地感受到 Hexo 的优点,V8 引擎的快就不必说了,使用的 Stylus 和 EJS 都让人觉得舒服。Jekyll 抱着 Liquid 不放真是一大败笔。

接下来就是要为 Hexo 写一些插件,比如压缩 HTML,来达到最大的自定义。

→ The Good Man Project

The Good Man is a project shows the capacity of CSS3 animations as an animation tool.

This project is built only by HTML, CSS3, web fonts and shapes, without images.

Although it's pretty amazing, but there are still some things that need to be concerned:

  • Firefox 18: animations work well, but the positions of elements are incorrect;
  • Chrome 24: most of the time, the performance is fine, no lags, very smooth, but not all the time;
  • The audio may be interrupted, and if it happends, you'll lose all sounds. Need a solution to detect download states and redownload if interrupted.

优化 CSS 和 JavaScript 请求数的另类方法

Jan Hančič 公布了一个在一次请求里同时加载 CSS 和 JavaScript 的方案,platypus.js

实现原理是利用 CSS 的 content 属性,把 JavaScript 转换成 base64 字符串放进去,再动态插入 DOM。

示例 CSS:

#platypus-0 {
  display: none;
  content: 	'YWxlcnQoJ0hpLCBteSBmcmllbmQhIEZyb20gQ2hyaXMnKQo=';
}

示例 JS:

(function ( document ) {
  var linkTags = document.querySelectorAll ( 'link[rel="stylesheet"]' );
  for ( var i = 0; i < linkTags.length; i++ ) {
    var dummyDiv = document.createElement ( 'div' );
    dummyDiv.id = 'platypus-' + i;
    document.body.appendChild ( dummyDiv );

    var styles = document.defaultView.getComputedStyle ( dummyDiv, null );
    var js = styles.getPropertyValue ( 'content' ); // w3c
    //backgroundImage = someElement.currentStyle["background-image"]; //IE
    if ( js === '' ) {
      continue;
    }

    dummyDiv.parentNode.removeChild ( dummyDiv );

    if ( js[0] === "'" || js[0] === '"' ) {
      js = js.substr ( 1, js.length - 2 );
    }

    var scriptTag = document.createElement ( 'script' );
    // window.atob - decode base64 string, unavailable on IE9-, MDN says IE10 supports it
    scriptTag.innerHTML = atob ( js );
    document.body.appendChild ( scriptTag );
  };
} ( document ) );

按照 Jan 的说法,platypus.js 兼容非兼容视图的 IE9,大致上主流的桌面、移动浏览器都可以兼容。

这个方案很有意思,虽然 CSS 文件的大小增加了,但在越来越快的带宽和越来越好的 CDN 的影响下,DNS Lookup、Connecting 和 Wating 的消耗说不定比多出的部分少不了多少,特别是小文件。

我也想过利用 localStorage 来实现类似 manifest 的效果,简单的示例代码如下:

var load = (function() {
  var insertToDOM, getSource

  insertToDOM = function( content ) {
    var script = document.createElement( 'script' )
    script.textContent = content
    document.body.appendChild( script )
  }

  getSource = function( file ) {
    var source = localStorage.getItem( file )
    if (typeof source !== 'string') {
      // if no record, get source from server
      $.ajax( {
        url: file,
        success: function( data ) {
          localStorage.setItem( file, data )
          insertToDOM( data )
        }
      } )
    } else {
      insertToDOM( source )
    }
  }

  return function(files) {
    files.forEach( function( file ) {
      getSource( file )
    })
  };
})();

load( ['//cdnjs.cloudflare.com/ajax/libs/jquery/1.8.3/jquery.min.js',
  'script.js'] )

特点:

  • 可以支持很多文件,理论上能用 Data URI 加载的都可以;
  • localStorage 的访问速度远远快于 DNS Lookup 等消耗;
  • localStorage 默认能支持最大 4MB 的储存,对于大部分站点完全足够;
  • 把脚本封装成 (fn)() 的形式并在底部插入特定的变量可判断版本、是否加载完成等,便于控制;
  • 可控性和易用性高于 manifest,不需要配置服务器,需要清空时操作 localStorage 即可,而 localStorage 的存储持久性和 manifest 基本一样。

只不过,这种做法和 AMD、CommonJS 等规范不太合拍——至少我还没想到好做法——小团队项目开发似乎挺不错,暂时只是草稿性质的想法。

Mutation 事件和 MutationObserver

Mutation 事件

DOM Level 3 提供了一系列的 mutation 事件,如 DOMAttrModifiedDOMNodeInserted,供开发者观察 DOM 的变动。这些事件虽然不太常用,但却非常方便好用。比如在使用 ajax 往 DOM 添加新元素时,需要检查总高度超过了容器,常见的方法是在 ajax 完成时的回调函数里执行,但这样并不利于组织代码,甚至回调函数会很臃肿;如果使用 DOMNodeInserted 就好多了:

$container.on( 'DOMNodeInserted', function() {
  if ( children_height > $container.height() ) {
    // do something
  }
})

$.ajax({
  success: function( data ) {
    $container.append( $( data.html ) )
  }
})

这样职责就清晰很多—— ajax 的回调函数仅处理返回的数据。

但 mutation 事件的性能太糟了,Chrome 也未必吃得消,而 Mozilla 在 Performance best practices in extensions 里也建议尽可能别用 mutation 事件。

因此 mutation 事件的处境很尴尬,能用的只有它,但又不能用,最终也被 DOM Level 4 引入的 MutationObserver 取代了。

MutationObserver

先看以下代码:

<ul></ul>

<script>
  var ul = document.querySelector('ul')
    , observer
  try {
    observer = new ( window.MutationObserver ||
                     window.WebKitMutationObserver )( function( mutationRecord,
                                                    observer ) {
      mutationRecord.forEach( function( mutation ) {
        console.log( mutation )
      })
    })
    observer.observe( ul, { attributes: true, childList: true } )
  } catch ( ex ) {}

  ul.className = 'list'
  ul.appendChild( document.createElement( 'li' ) )
  // 不会触发 
  ul.children[0].className = 'item'

  // 在不需要时,停止继续观察
  observer.disconnect()
</script>

目前支持的浏览器似乎只有 webkit 核心和 Firefox (14+),而在 Chrome 24 和 Safari 6.0.2 里仍属于私有实现,所以必须先判断:window.MutationObserver || window.WebKitMutationObserver

MutationObserver 创建实例时需要一个函数作为参数作为回调函数,该函数有两个参数,一个是包含所有被观察的 MutationRecord 实例的数组,一个是 MutationObserver 实例。

observer.observe( node, anObject ) 接受两个参数,需观察的对象和事件类型。事件类型用对象的形式来赋值,如 { attributes: true, childList: true },这决定了什么事件会被触发,比如上述例子执行结果为:

{
  addedNodes: null,
  attributeName: "class",
  attributeNamespace: null,
  nextSibling: null,
  oldValue: null,
  previousSibling: null,
  removedNodes: null,
  target: ul.list,
  type: "attributes"
}
{
  addedNodes: NodeList[1],
  attributeName: null,
  attributeNamespace: null,
  nextSibling: null,
  oldValue: null,
  previousSibling: #text,
  removedNodes: NodeList[0],
  target: ul,
  type: "childList"
}

MutationObserver 和一般的 event 不一样,默认仅观察指定对象,除非指定 subtree: true,所以更改 liclassName 没有触发属性变动事件:

ul.children[0].className = 'list'

此外还要注意的是,被观察的对象是 node 类型,所以类似这样的操作也会触发事件:ul.appendChild( document.createTextNode( 'Hello World' ) )

更详细的内容请参考 MDN 的文档

CSS 动画事件的妙用

MutationObserver 虽然实用度和性能都很优秀,但毕竟是 DOM Level 4 的东西——太新——在老一点的浏览器有没有别的代替方案呢?

如果只是需要 DOMNodeInserted,那可以利用 CSS 动画事件实现类似的效果:

<style>
  @-webkit-keyframes NodeInserted {  
    from {}
  }

  @keyframes NodeInserted {  
    from {  
      clip: rect(0px, auto, auto, auto);  
    }
    to {  
      clip: rect(0px, auto, auto, auto);
    }
  }

  li {
    -webkit-animation-duration: .001s;
    animation-duration: .001s;
    -webkit-animation-name: NodeInserted;
    animation-name: NodeInserted;
  }
</style>

<ul></ul>

<script>
  var nodeInserted = function( event ) {
    if ( 'NodeInserted' === event.animationName ) {
      console.log( 'New node inserted!' )
    }
  }
  document.addEventListener( 'webkitAnimationStart', nodeInserted, false )
  document.addEventListener( 'animationstart', nodeInserted, false )

  document.querySelector( 'ul' ).appendChild( document.createElement( 'li' ) )
</script>

这个技巧利用了 CSS 动画开始时触发的 animationstart 事件,因为如果一个元素只有在 DOM 里才会开始动画。

一些需要注意的地方:

  • Webkit 系列(Chrome 24 & Safari 6.0.2)要加上 prefix,事件绑定也是;
  • 尽量别用 jQuery.on() 绑定事件,因为 event 参数是被 jQuery 创建的,目前并不支持 event.animationName 等几个属性(1.9.0);
  • Chrome 24 有点奇葩,只有一个空的 from {} 也会触发动画事件(其他 webkit 系没测试);
  • Firefox 需要比较完整并有实际效果的 keyframes,比如示例代码里的 clip,如果想省事,可以单独用 from { opacity: 1 },但不是所有属性都可以触发;
  • keyframes 里的 CSS 属性要多注意,别不小心覆盖了需要的,比较安全的是 opacityclip
  • 建议通过判断 event.animationName 来避免非预期情况;
  • 建议用父元素做事件代理,如 document.querySelector( 'ul' ).addEventListener( 'animationstart', nodeInserted, false ),并酌情停止冒泡。

这个技巧可以用在任何支持 CSS 动画的浏览器上,泛用性比较高,可以参考我写的 Twitter Filter 用户脚本,做成 bookmarklet 一样可以用在别的浏览器。

补充:IE9 只支持 5 个 mutation 事件,不支持 CSS 动画,MutationObserver 目前还没支持的迹象,所以忘了 IE 系列吧。

Objective-C 的消息转发机制

通常,向一个对象发送它不能处理的消息会得到类似这样的错误提示:No visible @interface for 'SomeObject' declares the selector 'someSelector:'instance method '-someMethod' not found 等等,而动态语言,如 Ruby,则有另一种处理方式:

class MyClass
  def initialize
    @items = []
  end
  def method_missing(m, *args, &block)
    unless @items.respond_to? m
      super(m, *args)
    end
    @items.__send__(m, *args)
  end
end
t = MyClass.new
t.push "hello"
puts t.first # output: "hello"
t.trim       # error: undefined method `trim' (NoMethodError)

这种特性赋予了开发者很大的自由性和便利性。

虽然 Objective-C 并不是动态语言,但却具备动态类型,有类似的特性 -- 消息转发

快速转发

这种方式把消息原封不动地转发给目标对象,速度是最快的也是最简单的 -- 仅需在类里实现 - (id)forwardingTargetForSelector:(SEL)aSelector;

@interface NewArray : NSObject
@property (strong, nonatomic) NSArray *array;
- (id)forwardingTargetForSelector:(SEL)aSelector;
- (id)firstObject;
- (NSUInteger)count;
@end

@implementation NewArray
- (id)forwardingTargetForSelector:(SEL)aSelector {
  return [_array respondsToSelector:aSelector] ?
          _array : nil;
}
- (id)firstObject {
  return _array.count ? [_array objectAtIndex:0] : nil;
}
- (NSUInteger)count { return 0; }
@end

NewArray *arr = [[NewArray alloc] init];
arr.array = @[@"a", @"b"];
NSLog(@"%@ and %@, count %li", [(NSArray *)arr lastObject],
      [arr firstObject], [arr count]);
// output: "b and a, count 0"

普通转发

这种方式让你可以控制目标对象、方法和参数,但是性能没有第一种方法好。这个方法需要实现 - (void)forwardInvocation:(NSInvocation *)anInvocation;- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector;

@implementation NSArray (MessageForwarding)
- (void)forwardInvocation:(NSInvocation *)anInvocation {
  for (id obj in self) {
    // Only apply for string that lenght == 1
    if (1 == [(NSString *)obj length]) {
      [anInvocation invokeWithTarget:obj];
    }
  }
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
  NSMethodSignature *signature = [super methodSignatureForSelector:sel];
  if (!signature) {
    for (id obj in self) {
      if ((signature = [obj methodSignatureForSelector:aSelector])) {
        break;
      }
    }
  }
  return signature;
}
@end

NSArray *arr = @[[NSMutableString stringWithString:@"a"],
                 [NSMutableString stringWithString:@"bc"]];
[(NSMutableString *)arr appendString:@"!"];
NSLog(@"%@", arr);
// output: "a! bc"

-methodSignatureForSelector: 的作用是告诉运行时参数有多少个、是什么类型,这是用来兼容 C 的。

转发的注意事项

可能你已经在上述代码里发现了一些问题,这是它们的共同问题:

  1. 发送非自己处理的消息时需要转换类型,如 [(NSArray *)arr lastObject];
  2. 如果第一个接收消息的对象实现了同样的方法,则不会触发转发,如第一例中的 [arr count]

第一个问题,目的是让运行时知道要找什么类型,以第一例为例,如果改成 [(NSString *)arr lastObject] 则会触发 Xcode 报错,CodeRunner 则只会警告。一般来说,用目标类型即可,比如 NSArray 就用 NSArray,不过我比较喜欢动态语言的方式,所以一般用 id

第二个问题,需要用别的方式来解决,NSProxy

NSProxy

使用 NSProxy 需要实现一个子类。

#import <Foundation/Foundation.h>
@interface ArrayProxy : NSProxy
@property (strong, nonatomic) NSArray *items;
+ (id)proxyWithTarget:(NSArray *)anArray;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
@end

@implementation ArrayProxy
+ (id)proxyWithTarget:(NSArray *)anArray {
  ArrayProxy *proxy = [ArrayProxy alloc];
  proxy.items = anArray;
  return proxy;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
  for (id obj in _items) {
    [anInvocation invokeWithTarget:obj];
  }
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
  // DO NOT do this:
  // NSMethodSignature *signature = [super methodSignatureForSelector:sel];
  // Will raise `NSInvalidArgumentException`
  NSMethodSignature *signature;
  for (id obj in _items) {
    if ((signature = [obj methodSignatureForSelector:aSelector])) {
      break;
    }
  }
  return signature;
}
@end

@implementation NSArray (MessageForwarding)
- (id)do { return [ArrayProxy proxyWithTarget:self]; }
@end

@implementation NSString (MessageForwarding)
- (NSUInteger)count {
  NSLog(@"%li", self.length);
  return self.length;
}
@end

NSArray *arr = @[@"a", @"bc"];
[[arr do] count];
// output: 1, 2

因为是经过 NSProxy 来处理,所以就不受对象是否能响应对应消息的限制。另,不要在 -methodSignatureForSelector: 里往 super 发送消息,会抛出 NSInvalidArgumentException

需要注意的是,-do 只返回最后一个结果:

NSLog(@"%li", [[arr do] length]); // 2

结尾

上面的示例代码仅供参考如何实现消息转发,除此以外的部分有更好的实现方式,比如 firstObject 就应该用 category 来实现,而有些需求,则可以使用 delegate,比如 UI 元素。

我对上面三种消息转发的主要用法是扩展无法通过 category 实现的功能,比如为 NSMutableArray 添加可操作自身的方法,Objective-C 不允许操作自身,所以像 while(obj = arr.unshift()) 之类的技巧就无法使用。

使用 Pow 和 Privoxy 绕开 OS X 的沙盒限制和 SOCKS5 兼容问题

Apple 在 OS X 上推行沙盒之后,发现原来能用 PAC 代理配置不能用于所有程序了,原因是因为启用沙盒的程序无法访问 PAC 文件。除此之外,也发现了 SOCKS5 的兼容问题(如 Safari、Tweetbot),也就意味着在日益严峻的天朝网络环境里某些提高生产力的工具,如 shadowsocks ,就半残废了,只能走全局的 SOCKS 代理,而想要访问大陆站点的时候,比如优酷,这明显不科学,只能想办法解决。

沙盒的权限问题

虽然沙盒政策不让程序访问未授权的文件,不过对于「远程」文件倒没限制,所以我们可以把 PAC 文件部署在本地的服务器上。

安装 Pow 或 Anvil参考,使用 Apache/nginx 也可以,总之可以通过类似 http://localhost/proxy.pac 的方式访问本地的 PAC 文件即可。

以 Anvil 为例,安装并启动后,将 PAC 文件所在目录拖曳到状态栏的图标即可,得到如 http://pac.dev/ 的地址。将加上文件名后的完整地址,如 http://pac.dev/proxy.pac,复制到 Network Proxies 设置里 Automatic Proxy Configuration 的文本框里:

Automatic Proxy Conguration with Anvil

SOCKS5 的问题

虽然绕过了沙盒的限制,但是 Safari 和 Tweetbot 是不支持 SOCKS5 的,而 Chrome/Firefox 却只支持 SOCKS5,这里就有两种方法。

第一种,PAC 里这样写:

    'SOCKS5 127.0.0.1:8080;SOCKS 127.0.0.1:8080'

第二种,把 SOCSK5 转换成都支持的 HTTP 代理,这就需要使用 Privoxy。Privoxy 非常强大,但是比 PAC 复杂,如非需要,建议还是用第一种方法处理。

注意:本文是通过 homebrew 来安装 Privoxy,配置方式和通过官方安装指南安装的有所区别。

打开命令行依次执行以下命令:

brew install privoxy
cd /usr/local/etc/privoxy/
echo 'listen-address 0.0.0.0:8118\nforward-socks5 / localhost:8080 .' >> config

0.0.0.0 可以让别的设备用,如果不需要,可以用 127.0.0.18118 是默认端口;localhost:8080 是我的 SOCKS5 地址,根据需要自行修改。

需要注意的是,Privoxy 读取配置文件只会在当前目录读取,所以运行时建议用这个命令:privoxy /usr/local/etc/privoxy/config,没看到任何信息就代表成功了。

把 PAC 文件里的 SOCKS5 地址改成 HTTP 地址:

function FindProxyForURL(url, host) {
  proxy = 'PROXY 127.0.0.1:8118'
  if (shExpMatch(url, "*.twitter.com/*")) {
    return proxy;
  }
  return 'DIRECT'
}

好了,配合之前解决的沙盒问题,PAC 又可以正常工作了。

如果需要自启动,在命令行执行如下命令:

cd ~
touch org.privoxy.plist | echo '<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict><key>Label</key><string>org.privoxy.launchd.privoxy</string><key>ProgramArguments</key><array><string>/usr/local/sbin/privoxy</string><string>/usr/local/etc/privoxy/config</string></array><key>RunAtLoad</key><true/></dict></plist>' >> org.privoxy.plist
sudo chown root:wheel org.privoxy.plist
sudo cp org.privoxy.plist /Library/LaunchDaemons/org.privoxy.plist
launchctl load /Library/LaunchDaemons/org.privoxy.plist
launchctl start org.privoxy.launchd.privoxy

Privoxy 的进阶内容

其实 Privoxy 并不只是把 SOCKS5 转换成 HTTP 代理,她本身还有很强大的功能,比如 actionfilter,在这仅仅简单介绍一下,具体用途看官方文档

Action 可以起到 PAC 文件的作用,也可以当成 AdBlock、user script 和 Stylebot 来使用。

默认的文件分别是 /usr/local/etc/privoxy/user.action/usr/local/etc/privoxy/user.filter

大概的语法如下:

direct      = +forward-override{forward .}
ssh         = +forward-override{forward-socks5 127.0.0.1:8080 .}
default     = direct
{default}
/
{direct} 
.youku.com
.twitter.com
{ssh}
.twitter.com
# 后面的规则优先级比前面的高
{+block}
.weibo.com # 屏蔽微博
{-block}
.weibo.com/chrisyipw # 但是不屏蔽自己
{+block-as-image}
.weibo.com # 屏蔽微博下所有合法图片,即使扩展名不是图片扩展名也会被屏蔽

可以通过不同的 alias 配置不同的代理程序,如果是用 SSH 而不是 shadowsocks,可以考虑链接多个服务器进行分流,毕竟 SSH 并发性能不高。

还有更高级的,比如结合 filter 修改页面内容,比如重定向 URL 什么的:

{+filter{filter_name}}
.weibo.com
{+fast-redirects{check-decoded-url}}
news.google.com/news/url.*&url=http.*&

具体看 user.action 里的注释吧,而 filter 是需要结合 user.filter 来用的,如果想要使用多个 action 文件,在 config 加入 actionsfile name.action 就行。