[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"list-\u002Farticles\u002Fweb\u002F3":3},{"items":4,"total":137},[5,25,33,43,52,63,74,85,97,109,119,127],{"id":6,"type":7,"slug":8,"title":9,"date":10,"category":11,"tags":12,"body_markdown":18,"permalink":19,"excerpt_src":20,"media_type":20,"media_title":20,"media_author":20,"media_url":20,"rating":20,"layout":20,"pv":21,"admin_only":21,"created_at":22,"updated_at":22,"deleted_at":20,"status":23,"cover":24},179,"article","usage_of_weakmap","ES2015 WeakMap的学习和使用","2017-02-27 13:15:00","web",[13,14,15,16,17],"JavaScript","ES2015","ES6","WeakMap","数据结构","\nES2015（ES6）中新增了几种数据类型，包括Map WeakMap Set WeakSet等。其中Map可以与我们熟悉的对象Object进行对照，他们的功能都是提供一个键值对集合，主要的区别在于Object的key只能是字符串，而Map的key可以是任意类型。关于Map的大致用法可以参考[MDN](https:\u002F\u002Fdeveloper.mozilla.org\u002Fzh-CN\u002Fdocs\u002FWeb\u002FJavaScript\u002FReference\u002FGlobal_Objects\u002FMap)，我在前一篇文章[《也谈JavaScript数组去重》](https:\u002F\u002Fwww.toobug.net\u002Farticle\u002Farray_unique_in_javascript.html)中也有提及。\n\n今天要讨论的主角是WeakMap。\n\n按照[MDN](https:\u002F\u002Fdeveloper.mozilla.org\u002Fzh-CN\u002Fdocs\u002FWeb\u002FJavaScript\u002FReference\u002FGlobal_Objects\u002FWeakMap)上的说明\n\n> WeakMap 对象是键\u002F值对的集合，且其中的键是弱引用的。其键只能是对象，而值则可以是任意的。\n\n从这段描述来看，我们可以大致推断出，WeakMap与Map的主要区别在于两点：\n\n1. WeakMap对key的引用是弱引用\n2. WeakMap的key只能是对象\n\n这两点意味着什么呢？反正我第一眼看到的时候不是拉格良日懵就是三元二次懵的状态。于是围绕WeakMap去查阅了一些资料，渐渐地有了一些更深入的认识，记录成本文。\n\n\u003C!-- more -->\n\n## WeakMap的特性\n\n具体而言，WeakMap大致有如下一些明显的特性：\n\n### 1. key只能使用对象\n\n例如：\n\n```javascript\nvar m = new WeakMap();\nvar k = {};\n\n\u002F\u002F 设置键值对\nm.set(k, 1);\n\n\u002F\u002F 取值\nm.get(k);   \u002F\u002F1\n\n\u002F\u002F 非对象的key会报错\nm.set('k', 1);  \u002F\u002F报错\n```\n\n在上例中，我们使用了对象`k`作为WeakMap的key，设置了value为`1`。到下面取值的时候，只能使用同一个对象`k`去取。也就是说WeakMap是按照key的引用来对value进行存取的。\n\n> 关于“key只能使用对象”和“value的查找是通过比较key的引用”这两个命题，其实是互为因果的，本质上是一个先有鸡还是先有蛋的问题：\n>\n> 正因为WeakMap只能使用对象作为key，所以取值的时候对key进行查找也只能按对象引用进行查找。\n>\n> 正因为WeakMap在查找的时候只能按对象引用进行查找，所以只能使用对象作为key，否则存进去的值根本无法查找取出。\n\n### 2. key中的对象保持弱引用\n\n弱引用正是WeakMap中“Weak”的含义。熟悉JavaScript的朋友都知道引用是怎么回事，简单地说，当一个对象被引用的时候，往往意味着它正在被使用，或者在将来有可能会被使用。此时对象不会被垃圾回收机制回收掉。\n\n```javascript\nvar obj = {};\n......\nobj = null;\n```\n\n这段代码中，一开始`obj`引用了使用字面量创建的空对象，因此这个空对象不会被回收。很久以后`obj`被指向了`null`，不再引用空对象，此时这个空对象就不再能从任何代码中被访问到，将被回收掉。\n\n> 为简单起见，这里不讨论循环引用的垃圾回收问题。\n\n而弱引用则可以理解为“引用了对象，但是不影响它的垃圾回收”。假设，请注意，是假设，仅仅是假设，假设上例中第一句`obj = {}`是一个弱引用的关系，那么因为这个空对象没有其它的引用，它将很快被垃圾回收，在下方无法访问到。\n\n具体到WeakMap而言，大概是这样：\n\n```javascript\nvar obj = {};\nvar wm = new WeakMap();\nwm.set(obj, 1);\nwm.get(obj);\t\u002F\u002F 1\n......\nobj = null;\nwm.get(obj);\t\u002F\u002F 这句没有意义\n```\n\n在这个例子中，WeakMap实例`wm`（弱）引用了`obj`对象（空对象），接着下方代码释放了对空对象的引用（`obj = null`），此时和上例一样，空对象将被垃圾回收。也即`wm`中持有的空对象（弱）引用并不影响对对象本身的垃圾回收。这就是WeakMap中“弱引用”的含义。\n\n值得注意的是最后一行代码（`wm.get(obj)`），因为`obj`的引用已经被修改，所以这里无法访问到原来`obj`关联的值`1`，同时，会不会因为空对象已经被垃圾回收，所以`wm`中其实也已经没有了这个值。那么，到底是因为我们找不到路所以取不到值，还是因为它的值本来就不存在了呢？答案应该是Both吧，不过这个问题不翻规范的话，有点像哲学问题了，挺好玩。\n\n### 3. 无法遍历\n\nWeakMap和Map\u002FObject等有一个很大的不同，即它是不可遍历的。你无法使用`for...in`或者`for...of`等语句知道WeakMap中的内容。\n\n至于具体的原因……其实我不知道。MDN上是这样介绍的：\n\n> 正由于这样的弱引用，WeakMap 的 key 是非枚举的 (没有方法能给出所有的 key)。如果key 是可枚举的话，其列表将会受垃圾回收机制的影响，从而得到不确定的结果.\n\n从这段描述中，可以大致验证下我们上面的问题，当对象引用消失后，到底是因为我们缺少了引用所以无法从WeakMap中取到值还是WeakMap中的值本身也会消失？稍微了解一些JS垃圾回收知识的朋友都清楚，JS的垃圾回收会在一些条件（剩余内存、CPU负荷情况）下触发，也就是说可能会有一定延时。而上文说如果可以遍历，结果会受垃圾回收机制影响，大致可以理解为：如果可以遍历，那么在垃圾回收之前，将遍历到已经没有引用的对象和对应的值，在垃圾回收之后，则对象和值一起消失。因此可以大概猜出结论：WeakMap中的值会在垃圾回收时才消失。\n\n## WeakMap的使用场景\n\n客观地说，WeakMap的使用场景并不是很多，而且在条件不是非常苛刻的前提下，一般都可以有替代解决方案。不过很多情况下使用WeakMap来解决问题会更简单可靠一些。\n\n### 一个例子\n\n看完前面又臭又长的理论，不知道你会不会已经急得抓耳挠腮了：这么个key只能是对象，又不能遍历的东东，到底有什么用啊？反正我当时是急得想跺脚了，几乎所有的文章都只说这是个什么东西，并不告诉你它有什么用。经过多方求证，最后终于了解到它的核心思想：\n\n**在不改变对象本身的情况下扩展对象。**\n\n怎么理解呢？假如有100只鸡，现在要对每只鸡称重并记录。那么，鸡的体重记录到哪里就成了一个问题。我们有两种选择：\n\n1. 记录到一个本本上\n2. 想办法用笔写到鸡身上\n\n```javascript\n\u002F\u002F 1只鸡\nvar chicken = new Chicken();\n\n\u002F\u002F 100只鸡\nvar chickenList = [chicken, xxx, ...];\n\n\u002F\u002F 方法1:记录到本本上\nvar notebook = [];\nchickenList.forEach(function(chickenItem, index){\n\tnotebook[index] = getWeight(chickenItem);\n});\n\n\u002F\u002F 方法2:记录到鸡身上\nchickenList.forEach(function(chickenItem, index){\n\tchickenItem.weight = getWeight(chickenItem);\n});\n```\n\n首先我们看一下第2种方法，记录到鸡身上。这种方法的好处在于我们不需要额外的笔记本（变量）。但同时它也有很明显的缺点：\n\n1. 破坏了鸡的卖相，有时候这是很严重的事情，比如你想把一只5斤的鸡当成6斤卖出去，结果鸡身上直接写“我只有5斤”（修改了原有对象，可能导致意外的行为）\n2. 可能碰到一些战斗鸡，一个字都写不上去（对象冻结了或者有不可覆盖的属性）\n3. 可能写到一些本来就写了字的地方，导致根本看不清（与对象原有属性冲突）\n4. 如果鸡的生产线上有光学设备，有可能破坏生产线的生产，因为这只鸡变成了一只非标品，只能使用人工生产，降低效率（破坏JS引擎的hidden class优化机制）\n\n我们再来看一下第1种方法。这种方法的好处在于完全不用改动原来的对象，但它有比较明显的问题：\n\n1. 需要一个专门的本本来记录结果（额外变量）\n2. 本本无法和鸡精准地一一对应，只能靠一些索引或者标记（例如给每只鸡起一个名字）去（不可靠）地记录对应关系（无法精准地对比到是哪一个对象）\n3. 本本上的结果可以随时被别人修改\n\n针对上述第2个问题，一般我们在日常开发中，都会给对象加上一些标识，例如`id`属性，去与`notebook`记录的数据做一一对应。这也是我们在处理这一类需求的时候，一般不会觉得有困扰的原因。但这只适用于你能控制`chicken`对象结构的情况。如果你并不知道你要处理是一个什么样的对象，那么这种方法就会面临上方提到的第2个问题。\n\n这样的需求用Map是否可以解决呢？答案是肯定的：\n\n```javascript\n\u002F\u002F 1只鸡\nvar chicken = new Chicken();\n\n\u002F\u002F 100只鸡\nvar chickenList = [chicken, xxx, ...];\n\n\u002F\u002F 记录到另一个本本上\nvar notebook = new Map();\nchickenList.forEach(function(chickenItem, index){\n\tnotebook.set(chickenItem, getWeight(chickenItem));\n});\n```\n\n这里将本本`notebook`换成了一个Map，而Map可以保留对`chicken`的引用，从而解决上面说的使用数组或者对象来记录时无法精准对应的问题。\n\n这里用Map解决了上述第2个问题，但仍然存在两个问题：\n\n1. 需要额外变量`notebook`来存储所有的重量数据\n2. `notebook`中的数据可能随时被别人修改\n\n此时，我们终于可以看看WeakMap在这个问题上是如何表现的了：\n\n```javascript\n\u002F\u002F 1只鸡\nvar chicken = new Chicken();\n\n\u002F\u002F 100只鸡\nvar chickenList = [chicken, xxx, ...];\n\n\u002F\u002F 记录到WeakMap\nvar notebook = new WeakMap();\nchickenList.forEach(function(chickenItem, index){\n\tnotebook.set(chickenItem, getWeight(chickenItem));\n});\n```\n\n咦？怎么感觉跟Map的例子一样一样的？没错。Map和WeakMap的作用和用法非常相似。但是因为WeakMap弱引用和不可遍历，这里有一些不同的事情：\n\n1. 当你拿到`chicken`引用的时候，获取重量并记录到WeakMap `notbook`中，但是一旦释放`chicken`，则`notebook`中对应的数据也无法再访问，这可以很好地节省内存\n2. 因为`notebook`不可遍历，也就意味着你只有在需要使用`chicken`（有引用）时，才可以访问`notebook`，可以有效防止被外部修改\n\n打个比方，你的本本上，关于这只鸡的记录，只有当鸡来了的时候才存在，而且只有鸡在场的时候才能查到，当鸡走了，在本本上关于鸡的记录是不存在的，也就不存在被别人偷看、修改一说。也就是说，这个本本上关于鸡的记录好像根本就是这只鸡自己带来的一样。这就是上方所说的**在不改变对象本身的情况下扩展对象**的含义。\n\n同样的原理和写法，可以应用到其它的场景中，比如标记对象的状态（用于任务调度、错误处理等），比如为DOM元素添加额外的关联数据等等。\n\n### 应用1：事件系统\n\n在Node中，如果我们要使用事件系统的话，一般会将让自己的Class（构造函数）继承`EventEmitter`。而如果要为任意对象添加事件机制的话，就不那么容易了。有了`WeakMap`，就可以比较容易地处理这件事情了：\n\n```javascript\nvar listeners = new WeakMap();\n\n\u002F\u002F 监听事件\nfunction on(object, event, fn){\n\tvar thisListeners = listeners.get(object);\n\tif(!thisListeners) thisListeners = {};\n\tif(!thisListeners[event]) thisListeners[event] = [];\n\tthisListeners[event].push(fn);\n\tlisteners.set(object, thisListeners);\n}\n\n\u002F\u002F 触发事件\nfunction emit(object, event){\n\tvar thisListeners = listeners.get(object);\n\tif(!thisListeners) thisListeners = {};\n\tif(!thisListeners[event]) thisListeners[event] = [];\n\tthisListeners[event].forEach(function(fn){\n\t\tfn.call(object, event);\n\t});\n}\n\n\u002F\u002F 使用\nvar obj = {};\n\non(obj, 'hello', function(){\n\tconsole.log('hello');\n});\n\nemit(obj, 'hello');\n```\n\n### 应用2：私有变量\n\n```javascript\nfunction Constructor() {\n    var data = new WeakMap();\n\n \t\u002F\u002F 重写构建函数\n    Constructor = function() {\n    \t\u002F\u002F 挂一个私有变量存储\n        data.set(this, {});\n    }\n\n \t\u002F\u002F 方法\n    Constructor.prototype.doSth = function () {\n    \tvar privateVar = data.get(this);\n    \t......\n    };\n\n    return new Constructor();\n};\n```\n\n我们使用`data`来扩展`this`对象，用来存储私有变量，这个私有变量在外部无法被访问，而且随`this`对象的销毁和消失，简直完美。\n\n你可能会说，私有变量不是都已经有现成的方案了吗？\n\n```javascript\nfunction Constructor() {\n    var data = {};\n\n \t\u002F\u002F 方法\n    this.doSth = function () {\n    \tvar privateVar = data;\n    \t......\n    };\n};\n```\n\n没错，这正是经典的私有变量解决方案。但是这个方案有一个问题，即所有访问私有变量的方法（如`doSth()`）都只能挂在实例上，即每个实例中除了私有变量外，还有自己的特权方法。而使用WeakMap实现私有变量的方案中，方法可以挂在原型上。这两者在性能上会有一些差异。\n\n> ES2015虽然有Class，但是仍然不支持私有变量的定义，要使用私有变量的话，实现方法和ES5没有太大差异。\n\n## WeakMap的模拟\n\n如果你有关注过ES2015的特性在ES5中的模拟降级情况的话，应该会发现很多人说过，WeakMap是无法模拟的。事实的确是这样，但是如果我们稍微放宽一些条件，还是有办法模拟出一个可用的WeakMap的。\n\n首先，我们看一个[web components polyfill](https:\u002F\u002Fgithub.com\u002Fwebcomponents\u002Fwebcomponentsjs)中[对WeakMap的模拟](https:\u002F\u002Fgithub.com\u002Fwebcomponents\u002Fwebcomponentsjs\u002Fblob\u002Fmaster\u002Fsrc\u002FWeakMap\u002FWeakMap.js)：\n\n```javascript\nif (typeof WeakMap === 'undefined') {\n  (function() {\n    var defineProperty = Object.defineProperty;\n    var counter = Date.now() % 1e9;\n\n    var WeakMap = function() {\n      \u002F\u002F 记录一个唯一的name属性\n      this.name = '__st' + (Math.random() * 1e9 >>> 0) + (counter++ + '__');\n    };\n\n    WeakMap.prototype = {\n      \u002F\u002F 在WeakMap的key对象中添加与WeakMap实例name相同的属性\n      \u002F\u002F 利用这个属性来保存value\n      \u002F\u002F 下面的API原理类似\n      set: function(key, value) {\n        var entry = key[this.name];\n        if (entry && entry[0] === key)\n          entry[1] = value;\n        else\n          defineProperty(key, this.name, {value: [key, value], writable: true});\n        return this;\n      },\n      get: function(key) {\n        var entry;\n        return (entry = key[this.name]) && entry[0] === key ?\n            entry[1] : undefined;\n      },\n      delete: function(key) {\n        var entry = key[this.name];\n        if (!entry || entry[0] !== key) return false;\n        entry[0] = entry[1] = undefined;\n        return true;\n      },\n      has: function(key) {\n        var entry = key[this.name];\n        if (!entry) return false;\n        return entry[0] === key;\n      }\n    };\n\n    window.WeakMap = WeakMap;\n  })();\n}\n```\n\n上面的代码中我们加了一点注释，这个模拟的核心在于，`weakMap.set(key, value)`时，将`value`直接写入`key`这个对象中，作为对象的一个属性。因此它的生命周期与对象本身完全一致，也不会影响对象的垃圾回收，基本达到WeakMap的核心诉求。\n\n而它也有一些不足之处，最大的问题就是会修改对象本身。我们前文说过，WeakMap的核心思想是“在不改变对象本身的情况下扩展对象”，而我们的模拟却刚好违背了这一思想，将值扩展到了对象本身。如上文所说，总有一些对象是不可修改的，在这种情况下就会出现问题。这也是刚刚提到的必须在放宽一些条件的情况下，才可以模拟WeakMap的意思。\n\n基于同样的原理，还有一个模拟WeakMap的库\u003Chttps:\u002F\u002Fgithub.com\u002FBenvie\u002FWeakMap>。这个库的源码可见这里\u003Chttps:\u002F\u002Fgithub.com\u002FBenvie\u002FWeakMap\u002Fblob\u002Fmaster\u002Fweakmap.js>，它将数据以及对数据的操作封装到了`Data()`中，然后将它挂到了对象的一个属性`globalId`上。详细的关系大概如下：\n\n```\nobject:{\n\t[globalId]: data{\n\t\t[puid1]: value1\n\t\t[puid2]: value2\n\t}\n}\n```\n\n值得一提的是，`globalId`是全局唯一的，也就是说在一次脚本运行的过程中（不关掉页面，或者Node进程不退出），所有对象上的`globalId`都是相同的。\n\n相比web components的模拟而言，这个库的模拟要严谨细心得多，比如当对象不可写时会抛出错误，比如在产生随机ID时会进行重复判断，确保逻辑正确，比如它只在对象上写入`[globalId]`一个属性，尽量减少对对象的修改。当然这些逻辑也导致了代码可读性直线下降，解读这段代码花了我整整一个晚上的时间，有兴趣的同学可以自己看一下是否能够读懂。\n\n## 结\n\n本文简单总结了下自己在学习WeakMap过程中记录的相关知识点，包括WeakMap的特性、使用场景以及模拟实现。\n\n总体说来，WeakMap是一个有独特特性和使用场景的数据类型，它的出现使我们能更从容地应对一些开发过程中的问题。但话又说回来，在WeakMap出现之前，同样的问题我们也一样能解决，只是可能稍微麻烦一些，或者性能稍微差一些。\n\n另，本文所使用比喻仅为说明问题而设，各位看官明白要表达的意思即可，勿在比喻中钻牛角尖。\n\n参考资料：\n\n- \u003Chttps:\u002F\u002Fsanwen8.cn\u002Fp\u002F1e9XKBe.html>\n- \u003Chttps:\u002F\u002Fwww.sitepen.com\u002Fblog\u002F2015\u002F03\u002F19\u002Flegitimate-memory-efficient-privacy-with-es6-weakmaps\u002F>\n- \u003Chttp:\u002F\u002Fwww.2ality.com\u002F2016\u002F01\u002Fprivate-data-classes.html>\n- \u003Chttps:\u002F\u002Fdeveloper.mozilla.org\u002Fzh-CN\u002Fdocs\u002FWeb\u002FJavaScript\u002FReference\u002FGlobal_Objects\u002FWeakMap>\n- \u003Chttp:\u002F\u002Fstackoverflow.com\u002Fquestions\u002F29413222\u002Fwhat-are-the-actual-uses-of-es6-weakmap>\n- \u003Chttp:\u002F\u002Ffitzgeraldnick.com\u002F2014\u002F01\u002F13\u002Fhiding-implementation-details-with-e6-weakmaps.html>\n- \u003Chttps:\u002F\u002Filikekillnerds.com\u002F2015\u002F02\u002Fwhat-are-weakmaps-in-es6\u002F>\n- \u003Chttps:\u002F\u002Fsegmentfault.com\u002Fa\u002F1190000002549235>\n- \u003Chttps:\u002F\u002Fdeveloper.mozilla.org\u002Fzh-CN\u002Fdocs\u002FWeb\u002FJavaScript\u002FReference\u002FGlobal_Objects\u002FWeakMap>\n","\u002Farticle\u002Fusage_of_weakmap.html",null,0,"2026-08-28 04:37:17","published","",{"id":26,"type":7,"slug":27,"title":28,"date":29,"category":11,"tags":30,"body_markdown":32,"permalink":20,"excerpt_src":20,"media_type":20,"media_title":20,"media_author":20,"media_url":20,"rating":20,"layout":20,"pv":21,"admin_only":21,"created_at":22,"updated_at":22,"deleted_at":20,"status":23,"cover":24},178,"to-a-dark-future","To A Dark Futuer","2017-02-24 09:10",[11,31],"未来","\n开年来真的有点忙，好不容易写起来的公众号又好久没拔草了。\n\n最近忙的其中一件事是写一个slides，也就是传说中的PPT。（明明用的是keynote，你才PPT，你全家都PPT！）写的是web前端的一些普及性的介绍。\n\n在写的过程中，发现越写越悲观，写到最后竟然隐隐觉得web要完。当然，这是一件政治极其不正确的事情，所以也不敢到处乱发，只能当成呓语在公众号写一写，给为数不多的朋友诉一诉这其中的一些想法。\n\n## 标准的危机\n\n曾经有几年，大家是非常热衷于web标准的。但是如果问一问现在来面试的朋友，估计应该没有多少人还在意web标准这件事情。大家会关注 Angular \u002F React \u002F Vue ，会关注 AR \u002F VR ，但是真的没有人关注web标准。\n\n那我们关注一下可好？\n\n唉，算了。反正大概的感觉就是真正需要成为标准的东西一直拖拖拉拉，比如像 web components 至今没有定稿，导致至今没有一个像样的组件方案。最后靠框架来填补这块空白。而像 web bluetooth \u002F web usb \u002F web vr 这类的东西倒是层出不穷。\n\n除了标准本身外，厂商的跟进也是个大问题。一方面厂商对标准越来越不上心，爱理不理，另一方面，又为了各自的利益，先做非标准实现，再回头去影响标准。\n\n总而言之，标准现在就是一锅粥。\n\n\u003C!-- more -->\n\n## 性能的危机\n\n如果说浏览器是最复杂的桌面软件，不知道会不会有人打我。反正至少在常见软件中应该是这样的。就不说什么 video 相当于一个视频播放器， img 相当于一个图片查看器之类的媒体相关的了。就算只是一个简单的 span ，你大概也无法想象它到底有多复杂，光是DOM这一层就够头疼了，要暴露它的内容、位置、样式等各种各样的属性API，就意味着远远不是放一个文本这么简单的事情。DOM的复杂性为 web 性能的提升放了一个非常大的瓶颈。更何况现在的DOM简直是越来越复杂了。虽然浏览器在尽全力优化它的性能，但是复杂度摆在那，真的经不起作啊。\n\n一个性能有巨大瓶颈的平台，注定是应用受限的，因此一旦需要和其它技术同台竞技，web 往往是性能输家。\n\n## 场景的危机\n\n如果说性能的危机还不算啥，那场景的危机真的是灭顶之灾。\n\nweb 随着PC互联网而生，URL 成为它成功的重要因素。可链接、可传播，这是一个巨大的优势。也正是因为 URL 的重要性，才导致了域名生意的火爆，想想 www.jd.com 就可以卖300万，图啥？不就是图用户在输入网址的时候简单一点么？\n\n然而移动互联网改变了一切。你回想一下，从你早上起床刷朋友圈开始，到早上骑个单车，微信支付个早餐，公交车上刷刷微博和新闻，到上班时跟同事讨论些问题，中午点个外卖，下午看看美女图养养眼，到晚上约会个妹子吃吃饭，最后打个车回家，看看电视剧，刷刷朋友圈睡觉。有用到网址么？也许再过两年，该有朋友问什么是网址了吧？\n\n网址的消失让 web 失去了不可替代的优势。当然你可能会说，web 还有快速发版，不用安装等优势啊。诚然，然而当今世界，连iOS上的热更新都成熟到快焦了，快速发版还有什么优势可言？连小程序都来抢饭碗了，还有什么不安装的优势可言？\n\n想下来，如果有人跟我说“web已经没有立足之地了”，可能一开始我会拒绝，但是仔细想想，差得也并不多啊。\n\n也会有人说，web不死，你以为你每天用的app的界面里就没有web么？对，是，有，而且有好多。然而，在这些场景下的web，有什么是不可能被替代的呢？React-Native \u002F Weex 都已经快要宣战了，native代替这一点点web可能也只是一件需要时间的事情了吧。\n\n## 结\n\n嗯，刀刃上的钢并不见更好，反而让刀越来越重。场景却越来越小。这就是越写越悲观的原因。\n\n所以在小密圈有此一问：你觉得5年后的web是什么样子？\n\n希望是我想错了或者想多了。\n",{"id":34,"type":7,"slug":35,"title":36,"date":37,"category":11,"tags":38,"body_markdown":41,"permalink":42,"excerpt_src":20,"media_type":20,"media_title":20,"media_author":20,"media_url":20,"rating":20,"layout":20,"pv":21,"admin_only":21,"created_at":22,"updated_at":22,"deleted_at":20,"status":23,"cover":24},168,"array_unique_in_javascript","也谈JavaScript数组去重","2017-01-05 09:00:00",[13,39,40],"数组","去重","\nJavaScript的数组去重是一个老生常谈的话题了。随便搜一搜就能找到非常多不同版本的解法。\n\n昨天在微博上看到一篇文章，也写数组去重，主要推崇的方法是将利用数组元素当作对象key来去重。我在微博转发了“用对象key去重不是个好办法…”然后作者问什么才是推荐的方法。\n\n细想一下，这样一个看似简单的需求，如果要做到完备，涉及的知识和需要注意的地方着实不少，于是诞生此文。\n\n## 定义重复（相等）\n\n要去重，首先得定义，什么叫作“重复”，即具体到代码而言，两个数据在什么情况下可以算是相等的。这并不是一个很容易的问题。\n\n对于原始值而言，我们很容易想到`1`和`1`是相等的，`'1'`和`'1'`也是相等的。那么，`1`和`'1'`是相等的么？\n\n如果这个问题还好说，只要回答“是”或者“不是”即可。那么下面这些情况就没那么容易了。\n\n\u003C!-- more -->\n\n### NaN\n\n初看`NaN`时，很容易把它当成和`null`、`undefined`一样的独立数据类型。但其实，它是数字类型。\n\n```javascript\n\u002F\u002F number\nconsole.log(typeof NaN);\n```\n\n根据规范，比较运算中只要有一个值为NaN，则比较结果为`false`，所以会有下面这些看起来略蛋疼的结论：\n\n```javascript\n\u002F\u002F 全都是false\n0 \u003C NaN;\n0 > NaN;\n0 == NaN;\n0 === NaN;\n```\n\n以最后一个表达式`0 === NaN`为例，在规范中有明确规定（\u003Chttp:\u002F\u002Fwww.ecma-international.org\u002Fecma-262\u002F6.0\u002F#sec-strict-equality-comparison>）：\n\n> 4. If Type(x) is Number, then\n>   a. If x is NaN, return false.\n>   b. If y is NaN, return false.\n>   c. If x is the same Number value as y, return true.\n>   d. If x is +0 and y is −0, return true.\n>   e. If x is −0 and y is +0, return true.\n>   f. Return false.\n\n这意味着任何涉及到`NaN`的情况都不能简单地使用比较运算来判定是否相等。比较科学的方法只能是使用`isNaN()`：\n\n```javascript\nvar a = NaN;\nvar b = NaN;\n\n\u002F\u002F true\nconsole.log(isNaN(a) && isNaN(b));\n```\n\n### 原始值和包装对象\n\n看完`NaN`是不是头都大了。好了，我们来轻松一下，看一看原始值和包装对象这一对冤家。\n\n如果你研究过`'a'.trim()`这样的代码的话，不知道是否产生过这样的疑问：`'a'`明明是一个原始值（字符串），它为什么可以直接调用`.trim()`方法呢？当然，很可能你已经知道答案：因为JS在执行这样的代码的时候会对原始值做一次包装，让`'a'`变成一个字符串对象，然后执行这个对象的方法，执行完之后再把这个包装对象脱掉。可以用下面的代码来理解：\n\n```javascript\n\u002F\u002F 'a'.trim();\nvar tmp = new String('a');\ntmp.trim();\n```\n\n这段代码只是辅助我们理解的。但包装对象这个概念在JS中却是真实存在的。\n\n```javascript\nvar a = new String('a');\nvar b = 'b';\n```\n\n`a`即是一个包装对象，它和`b`一样，代表一个字符串。它们都可以使用字符串的各种方法（比如`trim()`），也可以参与字符串运算（`+`号连接等）。\n\n但他们有一个关键的区别：类型不同！\n\n```javascript\ntypeof a; \u002F\u002F object\ntypeof b; \u002F\u002F string\n```\n\n在做字符串比较的时候，类型的不同会导致结果有一些出乎意料：\n\n```javascript\nvar a1 = 'a';\nvar a2 = new String('a');\nvar a3 = new String('a');\n\na1 == a2; \u002F\u002F true\na1 == a3; \u002F\u002F true\na2 == a3; \u002F\u002F false\na1 === a2; \u002F\u002F false\na1 === a3; \u002F\u002F false\na2 === a3; \u002F\u002F false\n```\n\n同样是表示字符串`a`的变量，在使用严格比较时竟然不是相等的，在直觉上这是一件比较难接受的事情，在各种开发场景下，也非常容易忽略这些细节。\n\n### 对象和对象\n\n在涉及比较的时候，还会碰到对象。具体而言，大致可以分为三种情况：纯对象、实例对象、其它类型的对象。\n\n**纯对象**\n\n> 纯对象（plain object）具体指什么并不是非常明确，为减少不必要的争议，下文中使用纯对象指代由字面量生成的、成员中不含函数和日期、正则表达式等类型的对象。\n\n如果直接拿两个对象进行比较，不管是`==`还是`===`，毫无疑问都是不相等的。但是在实际使用时，这样的规则是否一定满足我们的需求？举个例子，我们的应用中有两个配置项：\n\n```javascript\n\u002F\u002F 原来有两个属性\n\u002F\u002F var prop1 = 1;\n\u002F\u002F var prop2 = 2;\n\n\u002F\u002F 重构代码时两个属性被放到同一个对象中\n\nvar config = {\n    prop1: 1,\n    prop2: 2\n};\n```\n\n假设在某些场景下，我们需要比较两次运行的配置项是否相同。在重构前，我们分别比较两次运行的`prop1`和`prop2`即可。而在重构后，我们可能需要比较`config`对象所代表的配置项是否一致。在这样的场景下，直接用`==`或者`===`来比较对象，得到的并不是我们期望的结果。\n\n在这样的场景下，我们可能需要自定义一些方法来处理对象的比较。常见的可能是通过`JSON.stringify()`对对象进行序列化之后再比较字符串，当然这个过程并非完全可靠，只是一个思路。\n\n> 如果你觉得这个场景是无中生有的话，可以再回想一下断言库，同样是基于对象成员，判断结果是否和预期相符。\n\n**实例对象**\n\n实例对象主要指通过构造函数（类）生成的对象。这样的对象和纯对象一样，直接比较都是不等的，但也会碰到需要判断是否是同一对象的情况。一般而言，因为这种对象有比较复杂的内部结构（甚至有一部分数据在原型上），无法直接从外部比较是否相等。比较靠谱的判断方法是由构造函数（类）来提供静态方法或者实例方法来判断是否相等。\n\n```javascript\nvar a = Klass();\nvar b = Klass();\n\nKlass.isEqual(a, b);\n```\n\n**其它对象**\n\n其它对象主要指数组、日期、正则表达式等这类在`Object`基础上派生出来的对象。这类对象各有各的特殊性，一般需要根据场景来构造判断方法，决定两个对象是否相等。\n\n比如，日期对象，可能需要通过`Date.prototype.getTime()`方法获取时间戳来判断是否表示同一时刻。正则表达式可能需要通过`toString()`方法获取到原始字面量来判断是否是相同的正则表达式。\n\n### ==和===\n\n在一些文章中，看到某一些数组去重的方法，在判断元素是否相等时，使用的是`==`比较运算符。众所周知，这个运算符在比较前会先查看元素类型，当类型不一致时会做隐式类型转换。这其实是一种非常不严谨的做法。因为无法区分在做隐匿类型转换后值一样的元素，例如`0`、`''`、`false`、`null`、`undefined`等。\n\n同时，还有可能出现一些只能黑人问号的结果，例如：\n\n```javascript\n[] == ![]; \u002F\u002Ftrue\n```\n\n### Array.prototype.indexOf()\n\n在一些版本的去重中，用到了`Array.prototype.indexOf()`方法：\n\n```javascript\nfunction unique(arr) {\n    return arr.filter(function(item, index){\n        \u002F\u002F indexOf返回第一个索引值，\n        \u002F\u002F 如果当前索引不是第一个索引，说明是重复值\n        return arr.indexOf(item) === index;\n    });\n}\n```\n\n```javascript\nfunction unique(arr) {\n    var ret = [];\n    arr.forEach(function(item){\n        if(ret.indexOf(item) === -1){\n            ret.push(item);\n        }\n    });\n    return ret;\n}\n```\n\n既然`==`和`===`在元素相等的比较中是有巨大差别的，那么`indexOf`的情况又如何呢？大部分的文章都没有提及这点，于是只好求助规范。通过规范（\u003Chttp:\u002F\u002Fwww.ecma-international.org\u002Fecma-262\u002F6.0\u002F#sec-array.prototype.indexof>），我们知道了`indexOf()`使用的是严格比较，也就是`===`。\n\n> 再次强调：按照前文所述，`===`不能处理`NaN`的相等性判断。\n\n### Array.prototype.includes()\n\n`Array.prototype.includes()`是ES2016中新增的方法，用于判断数组中是否包含某个元素，所以上面使用`indexOf()`方法的第二个版本可以改写成如下版本：\n\n```javascript\nfunction unique(arr) {\n    var ret = [];\n    arr.forEach(function(item){\n        if(!ret.includes(item)){\n            ret.push(item);\n        }\n    });\n    return ret;\n}\n```\n\n那么，你猜猜，`includes()`又是用什么方法来比较的呢？如果想当然的话，会觉得肯定跟`indexOf()`一样喽。但是，程序员的世界里最怕想当然。翻一翻规范，发现它其实是使用的另一种比较方法，叫作“SameValueZero”比较（\u003Chttps:\u002F\u002Ftc39.github.io\u002Fecma262\u002F2016\u002F#sec-samevaluezero>）。\n\n> 1. If Type(x) is different from Type(y), return false.\n> 2. If Type(x) is Number, then\n>   a. If x is NaN and y is NaN, return true.\n>   b. If x is +0 and y is -0, return true.\n>   c. If x is -0 and y is +0, return true.\n>   d. If x is the same Number value as y, return true.\n>   e. Return false.\n> 3. Return SameValueNonNumber(x, y).\n\n注意`2.a`，如果`x`和`y`都是`NaN`，则返回`true`！也就是`includes()`是可以正确判断是否包含了`NaN`的。我们写一段代码验证一下：\n\n```javascript\nvar arr = [1, 2, NaN];\narr.indexOf(NaN); \u002F\u002F -1\narr.includes(NaN); \u002F\u002F true\n```\n\n可以看到`indexOf()`和`includes()`对待`NaN`的行为是完全不一样的。\n\n## 一些方案\n\n从上面的一大段文字中，我们可以看到，要判断两个元素是否相等（重复）并不是一件简单的事情。在了解了这个背景后，我们来看一些前面没有涉及到的去重方案。\n\n### 遍历\n\n双重遍历是最容易想到的去重方案：\n\n```javascript\nfunction unique(arr) {\n    var ret = [];\n    var len = arr.length;\n    var isRepeat;\n    for(var i=0; i\u003Clen; i++) {\n        isRepeat = false;\n        for(var j=i+1; j\u003Clen; j++) {\n            if(arr[i] === arr[j]){\n                isRepeat = true;\n                break;\n            }\n        }\n        if(!isRepeat){\n            ret.push(arr[i]);\n        }\n    }\n    return ret;\n}\n```\n\n双重遍历还有一个优化版本，但是原理和复杂度几乎完全一样：\n\n```javascript\nfunction unique(arr) {\n    var ret = [];\n    var len = arr.length;\n    for(var i=0; i\u003Clen; i++){\n        for(var j=i+1; j\u003Clen; j++){\n            if(arr[i] === arr[j]){\n                j = ++i;\n            }\n        }\n        ret.push(arr[i]);\n    }\n    return ret;\n}\n```\n\n这种方案没什么大问题，用于去重的比较部分也是自己编写实现（`arr[i] === arr[j]`），所以相等性可以自己针对上文说到的各种情况加以特殊处理。唯一比较受诟病的是使用了双重循环，时间复杂度比较高，性能一般。\n\n### 使用对象key来去重\n\n```javascript\nfunction unique(arr) {\n    var ret = [];\n    var len = arr.length;\n    var tmp = {};\n    for(var i=0; i\u003Clen; i++){\n        if(!tmp[arr[i]]){\n            tmp[arr[i]] = 1;\n            ret.push(arr[i]);\n        }\n    }\n    return ret;\n}\n```\n\n这种方法是利用了对象（`tmp`）的key不可以重复的特性来进行去重。但由于对象key只能为字符串，因此这种去重方法有许多局限性：\n\n1. 无法区分隐式类型转换成字符串后一样的值，比如`1`和`'1'`\n2. 无法处理复杂数据类型，比如对象（因为对象作为key会变成`[object Object]`）\n3. 特殊数据，比如`'__proto__'`会挂掉，因为`tmp`对象的`__proto__`属性无法被重写\n\n对于第一点，有人提出可以为对象的key增加一个类型，或者将类型放到对象的value中来解决：\n\n```javascript\nfunction unique(arr) {\n    var ret = [];\n    var len = arr.length;\n    var tmp = {};\n    var tmpKey;\n    for(var i=0; i\u003Clen; i++){\n        tmpKey = typeof arr[i] + arr[i];\n        if(!tmp[tmpKey]){\n            tmp[tmpKey] = 1;\n            ret.push(arr[i]);\n        }\n    }\n    return ret;\n}\n```\n\n该方案也同时解决第三个问题。\n\n而第二个问题，如果像上文所说，在允许对对象进行自定义的比较规则，也可以将对象序列化之后作为key来使用。这里为简单起见，使用`JSON.stringify()`进行序列化。\n\n```javascript\nfunction unique(arr) {\n    var ret = [];\n    var len = arr.length;\n    var tmp = {};\n    var tmpKey;\n    for(var i=0; i\u003Clen; i++){\n        tmpKey = typeof arr[i] + JSON.stringify(arr[i]);\n        if(!tmp[tmpKey]){\n            tmp[tmpKey] = 1;\n            ret.push(arr[i]);\n        }\n    }\n    return ret;\n}\n```\n\n### Map Key\n\n可以看到，使用对象key来处理数组去重的问题，其实是一件比较麻烦的事情，处理不好很容易导致结果不正确。而这些问题的根本原因就是因为key在使用时有限制。\n\n那么，能不能有一种key使用没有限制的对象呢？答案是——真的有！那就是ES2015中的`Map`。\n\n> `Map`是一种新的数据类型，可以把它想象成key类型没有限制的对象。此外，它的存取使用单独的`get()`、`set()`接口。\n\n```javascript\nvar tmp = new Map();\ntmp.set(1, 1);\ntmp.get(1); \u002F\u002F 1\n\ntmp.set('2', 2);\ntmp.get('2'); \u002F\u002F 2\n\ntmp.set(true, 3);\ntmp.get(true); \u002F\u002F 3\n\ntmp.set(undefined, 4);\ntmp.get(undefined); \u002F\u002F 4\n\ntmp.set(NaN, 5);\ntmp.get(NaN); \u002F\u002F 5\n\nvar arr = [], obj = {};\n\ntmp.set(arr, 6);\ntmp.get(arr); \u002F\u002F 6\n\ntmp.set(obj, 7);\ntmp.get(obj); \u002F\u002F 7\n```\n\n由于Map使用单独的接口来存取数据，所以不用担心key会和内置属性重名（如上文提到的`__proto__`）。使用`Map`改写一下我们的去重方法：\n\n```javascript\nfunction unique(arr) {\n    var ret = [];\n    var len = arr.length;\n    var tmp = new Map();\n    for(var i=0; i\u003Clen; i++){\n        if(!tmp.get(arr[i])){\n            tmp.set(arr[i], 1);\n            ret.push(arr[i]);\n        }\n    }\n    return ret;\n}\n```\n\n### Set\n\n既然都用到了ES2015，数组这件事情不能再简单一点么？当然可以。\n\n除了`Map`以外，ES2015还引入了一种叫作`Set`的数据类型。顾名思义，`Set`就是集合的意思，它不允许重复元素出现，这一点和数学中对集合的定义还是比较像的。\n\n```javascript\nvar s = new Set();\ns.add(1);\ns.add('1');\ns.add(null);\ns.add(undefined);\ns.add(NaN);\ns.add(true);\ns.add([]);\ns.add({});\n```\n\n如果你重复添加同一个元素的话，`Set`中只会存在一个。包括`NaN`也是这样。于是我们想到，这么好的特性，要是能和数组互相转换，不就可以去重了吗？\n\n```javascript\nfunction unique(arr){\n    var set = new Set(arr);\n    return Array.from(set);\n}\n```\n\n我们讨论了这么久的事情，居然两行代码搞定了，简直不可思议。\n\n然而，不要只顾着高兴了。有一句话是这么说的“不要因为走得太远而忘了为什么出发”。我们为什么要为数组去重呢？因为我们想得到不重复的元素列表。而既然已经有`Set`了，我们为什么还要舍近求远，使用数组呢？是不是在需要去重的情况下，直接使用`Set`就解决问题了？这个问题值得思考。\n\n## 小结\n\n最后，用一个测试用例总结一下文中出现的各种去重方法：\n\n```javascript\nvar arr = [1,1,'1','1',0,0,'0','0',undefined,undefined,null,null,NaN,NaN,{},{},[],[],\u002Fa\u002F,\u002Fa\u002F]\nconsole.log(unique(arr));\n```\n\n> 测试中没有定义对象的比较方法，因此默认情况下，对象不去重是正确的结果，去重是不正确的结果。\n\n|方法      |结果                                              |说明                                     |\n|----------|--------------------------------------------------|-----------------------------------------|\n|indexOf#1 |NaN被去掉                                         |                                         |\n|indexOf#2 |NaN重复                                           |                                         |\n|includes  |正确                                              |                                         |\n|双重循环#1|NaN重复                                           |                                         |\n|双重循环#2|NaN重复                                           |                                         |\n|对象#1    |字符串和数字无法区分，对象、数组、正则表达式被去重|                                         |\n|对象#2    |对象、数组、正则表达式被去重                      |                                         |\n|对象#3    |对象、数组被去重，正则表达式被消失                |JSON.stringify(\u002Fa\u002F)结果为{}，和空对象一样|\n|Map       |正确                                              |　                                       |\n|Set       |正确                                              |　                                       |\n\n最后的最后：任何脱离场景谈技术都是妄谈，本文也一样。去重这道题，没有正确答案，请根据场景选择合适的去重方法。\n","\u002Farticle\u002Farray_unique_in_javascript.html",{"id":44,"type":7,"slug":45,"title":46,"date":47,"category":11,"tags":48,"body_markdown":51,"permalink":20,"excerpt_src":20,"media_type":20,"media_title":20,"media_author":20,"media_url":20,"rating":20,"layout":20,"pv":21,"admin_only":21,"created_at":22,"updated_at":22,"deleted_at":20,"status":23,"cover":24},166,"oauth-in-mini-program","小程序接入OAuth","2016-12-29 09:17",[49,50],"小程序","OAuth","\n> 注：该方案实现后并未提审，所以只讨论技术可行性，不讨论是否合规是否过审的问题。\n\n这个问题按不同情况和不同解决方案大致有五条路可走：\n\n## 1 密码模式的OAuth\n\n简单来说，就是用户提供用户名和密码。可能有人会说：靠，这叫毛线的OAuth？对，我也是这么想的，但是以API规范著称的Github对非web应用就是给出的这样的方案：\n\n[GitHub Developer Guide](https:\u002F\u002Flink.zhihu.com\u002F?target=https%3A\u002F\u002Fdeveloper.github.com\u002Fv3\u002Foauth\u002F%23non-web-application-flow)\n\n> \"Use Basic Authentication to create an OAuth2 token using the interface below. With this technique, a username and password need not be stored permanently, and the user can revoke access at any time. (Make sure to understand how to work with two-factor authentication if you or your users have two-factor authentication enabled.)\"\n\n翻译一下：\n\n> （非web流程）“使用HTTP Basic Auth（译注：即用户名和密码）来创建OAuth2 token，流程见XXX。使用这种方案时，不应该永久保存用户名和密码，用户要可以随时撤销授权。（如果你或者你的用户开户了两步验证，请确保你了解如何正确处理两步验证的情况。）”\n\n但，题主所说的微博、豆瓣是否支持这种模式，存疑。另外这种方式对用户来说非常不友好，毕竟要将密码交到第三方手中，如果是我来用，我是不会输的。\n\n## 2 直接使用密码登录\n\n靠？这又TMD叫什么OAuth？没错，这不是OAuth。但是对大部分使用OAuth的网站来说，都会让用户补充一下自己的用户名和密码。简单来说就是用户除了可以选择OAuth之外，还可以使用自己的用户名和密码登录。\n\n如果网站是一个很有节操的网站，并没有自己的用户名和密码，完全依赖第三方怎么办？\n\n简单啊，让用户补充一个不就好了？！\n\n## 3 授权码\n\n回想第二种方式为什么可行呢？当然你可以说这都不涉及OAuth了，当然可行了。\n\n我们也可以换个思路，上面第二种方法可行，是因为我们将OAuth的授权与网站本身进行了绑定，换句话说，当你输入用户名，我就能找到你是谁，你对应的OAuth信息是什么。\n\n那么，同样的思路，我们可以使用一个“授权码”（名字随意取，你高兴的话叫它“红包口令”都行），这个授权码和用户信息是绑定的，然后引导用户在小程序上输入授权码即可完成。\n\n从用户体验的角度来说，这个授权码不有太长，因为需要用户手工输入。\n\n从安全性来说，有一定隐患，所以一定要加上时效限制。\n\n## 4 授权码加强版-扫码\n\n在最近更新的一个版本中，小程序终于加上了扫码的能力。所以可以将上一种方案中的授权码做成一个二维码，让用户在小程序中扫描即可。\n\n## 5 unionId机制\n\n微信小程序支持微信开放平台已有的unionId机制。简单说把小程序当成一个平台，然后去[微信开放平台](https:\u002F\u002Flink.zhihu.com\u002F?target=https%3A\u002F\u002Fopen.weixin.qq.com\u002F)申请一个第三方网站登录。这样用户就有两套登录，一套在网站中，一套在小程序中。这两套机制中对同一个用户来说，unionId是相同的，因此可以直接将小程序中用户身份与网站身份对应起来。\n\n不过微信开放平台申请需要做一些认证，比较麻烦。\n\n## 小结\n\n严格来说，只有第一种是满足要求的，即完全依赖第三方登录。\n\n第二种依赖网站自有账号体系。后面三种都需要依赖微信的账号体系，然后将这个账号信息与已有用户进行绑定。\n\n另外这五种方案都需要后台配合，因为安全的OAuth流程中后台是不可以缺位的。\n",{"id":53,"type":7,"slug":54,"title":55,"date":56,"category":11,"tags":57,"body_markdown":60,"permalink":61,"excerpt_src":20,"media_type":20,"media_title":20,"media_author":20,"media_url":20,"rating":20,"layout":20,"pv":21,"admin_only":21,"created_at":22,"updated_at":22,"deleted_at":20,"status":23,"cover":62},165,"capture_video_on_web","如何使用web录制视频","2016-09-25 19:58",[58,59],"Video","WebRTC","\n最近在某个需求的评审会上，产品同学脑洞大开，提出了**使用web录制视频**的想法。并兴致勃勃地说“看，XXX网站可以调用摄像头，还能聊天呢！”本着负（Zhuang）责（Bi）的原则，我们也对该方案做了认真的预研。大致结论：\n\n1. 非实时录制时（文件上传框），兼容性相对较好，且API和性能稳定\n2. 实时录制视频在Chrome for Android中可行，其它机型和浏览器均不可使用。考虑到相关标准仍处于不稳定状态，不建议在产品中使用\n3. 微信有非公开接口可以调用实时视频录制（微证券使用）\n\n详细方案如下：\n\n\u003C!-- more -->\n\n## 方案一：使用文件上传框\n\n文件上传框`\u003Cinput type=\"file\">`是前端同学非常熟悉的一个HTML控件，它的主要作用是用来上传文件。而在移动端，这个文件上传框被赋予了更多的使命，除了可以选择文件上传之外，还可以调用摄像头来拍摄照片或者视频并上传。\n\n具体的使用方式：\n\n```html\n\u003Cinput type=\"file\" accept=\"video\u002F*\"\u002F>\n```\n\n或者\n\n```html\n\u003Cinput type=\"file\" accept=\"video\u002Fmp4,video\u002Fx-m4v,video\u002F*\"\u002F>\n```\n\n这两种写法的区别在于对不同机型来说兼容性可能略有区别，但是具体的情况未做一一测试总结。经过初步测试，该方案可以在以下环境中运行：\n\n- Android 4.4+\n- iOS 6.0+\n- 微信webview\n- Chrome for Android\n\n> 注：该兼容性列表是我们简单测试一部分机器后的结论，不做任何保证。事实上我们也碰到一部分Android机器是例外。下文兼容性列表同理。\n\n该方案API简单易用，且功能由浏览器或webview原生实现，性能比较稳定。\n\n\n该方案缺点：非实时录制视频，无法确定用户是录制的还是选取的已有的视频文件。\n\n相关demo \u003Chttp:\u002F\u002Fcodepen.io\u002FTooBug\u002Fembed\u002FRRZQxr\u002F>\n\n## 方案二：视频录制\n\n要使用web录制视频，需要两个相关API，一个用于调用摄像头，一个用于录制。调用摄像头后会产生一个视频流，然后调用录制API将这个视频流压缩和保存。\n\n其中调用摄像头的API叫作`getUserMedia()`，以前属于`navigator`对象（Chrome 21-49），后来规范修改，现在属于`MediaDevices`（Chrome 49+）。该API还负责提示用户授权。\n\n视频流叫作`MediaStream`。拿到`MediaStream`后，可配合`ObjectURL`，产生一个虚拟URL，供浏览器`video`标签调用，实现视频回放（回显）。\n\n用于录制视频的API叫作`MediaRecorder`。该API在Chrome 49+可用。\n\n该方案兼容性：\n\n- Chrome 49+\n- Firefox 29+\n- Chrome for Android\n\n相关Demo地址：\u003Chttps:\u002F\u002Fsimpl.info\u002Fmediarecorder\u002F>\n\n## 方案三：WebRTC视频流远程录制\n\nWebRTC是指实现web实时通信的一系列规范，一般可以通俗地理解为“P2P视频聊天”。实现这个功能依赖于上方说的摄像头调用API `getUserMedia()`取到`MediaStream`，同时还依赖一个P2P网络连接和传输的API来实现视频流数据的传输，这个API叫作`RTCPeerConnection`。\n\n在实际运作时，需要服务额外处理两个浏览器在P2P通信之前的Session建立相关的逻辑：\n\n![WebRTC实际原理图](\u002Fassets\u002Fcapture_video_on_web\u002F1.png)\n\n同时，还需要服务端支持来完成浏览器在NAT等复杂网络环境中的通信“打洞”需求：\n\n![WebRTC实际原理图](\u002Fassets\u002Fcapture_video_on_web\u002F2.png)\n\n使用该方案录制视频的原理是通过服务端模拟一个浏览器（Peer），实现相关视频流接收解码协议以及`RTCPeerConnection`协议。\n\nSession管理的服务端和打洞的服务端实现和维护比较麻烦，但有例可循，而模拟Peer的部分则实现过于复杂，因此，虽然该方案理论上可行，且浏览器兼容性稍好，但仍然认为该方案在实际操作中不可行。\n\n这个方案的兼容性\n\n- Firefox 17+\n- Chrome 21+\n- Edge 12+\n- Chrome for Android\n\n## 方案四：截图上传\n\n该方案原理：在使用`getUserMedia()`获取视频流之后，将该视频流定时投映到一张2d画布中（`canvas`），然后将画布中的画面提取成图片数据（`base64`）。\n\n该方案原理比较简单，兼容性\n\n- Firefox 17+\n- Chrome 21+\n- Edge 12+\n- Chrome for Android\n\n但同时也有明显缺陷：\n\n- 无法获取声音数据，只能获取到图片数据\n- 需要上传后由后台转换成视频\n- 帧数多时图片可能较大，造成性能问题（比如崩溃）\n- 帧数多时图片可能较大，造成网络传输慢\n\n## 相关文档\n\n- [MDN上的navigator.getUserMedia文档](https:\u002F\u002Fdeveloper.mozilla.org\u002Fzh-CN\u002Fdocs\u002FWeb\u002FAPI\u002FNavigator\u002FgetUserMedia)\n- [MDN上的MediaDevices.getUserMedia文档](https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FMediaDevices\u002FgetUserMedia)\n- [MDN上的MediaStream文档](https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FMediaStream)\n- [MDN上的MediaRecorder文档](https:\u002F\u002Fdeveloper.mozilla.org\u002Fzh-CN\u002Fdocs\u002FWeb\u002FAPI\u002FMediaRecorder)\n- [W3C Media Capture and Streams规范（发布候选状态）](https:\u002F\u002Fwww.w3.org\u002FTR\u002Fmediacapture-streams\u002F)\n- [W3C MediaRecording规范（Working Draft草稿状态）](https:\u002F\u002Fw3c.github.io\u002Fmediacapture-record\u002FMediaRecorder.html)\n- [W3C webrtc规范（Working Draft草稿状态）](http:\u002F\u002Fw3c.github.io\u002Fwebrtc-pc\u002F)\n- [webrtc官方网站](https:\u002F\u002Fwebrtc.github.io)\n- [教程：webrtc入门](https:\u002F\u002Fcodelabs.developers.google.com\u002Fcodelabs\u002Fwebrtc-web\u002F)\n- [文章：真实世界中的webrtc](http:\u002F\u002Fwww.html5rocks.com\u002Fen\u002Ftutorials\u002Fwebrtc\u002Finfrastructure\u002F)\n","\u002Farticle\u002Fcapture_video_on_web.html","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fcapture_video_on_web\u002F1.png",{"id":64,"type":7,"slug":65,"title":66,"date":67,"category":11,"tags":68,"body_markdown":72,"permalink":73,"excerpt_src":20,"media_type":20,"media_title":20,"media_author":20,"media_url":20,"rating":20,"layout":20,"pv":21,"admin_only":21,"created_at":22,"updated_at":22,"deleted_at":20,"status":23,"cover":24},164,"alert_in_node","Node.js中低成本实现错误告警","2016-01-27 20:18",[69,70,71],"Node","错误","告警","\n相比其他语言（特指PHP）而言，Node.js应用更需要关注出错信息，因为一旦处理不慎，就会导致应用crash。\n\n一种偷懒的方法是使用[PM2](https:\u002F\u002Fgithub.com\u002FUnitech\u002Fpm2)之类的进程管理软件来启动Node.js进程，从而达到出错crash后自动重新启动应用的目的。\n\n当然更好的办法则是手工捕获错误，然后进行适当的处理，防止应用产生未被接住的错误导致crash。\n\n在捕获到Node.js产生的错误后，下一步自然是记录到错误日志中，以便日后可以进行分析，并针对性地排查修改。本文要说的，即是对错误日志的处理方式之一——告警。\n\n告警是运维工作中非常重要的一个环节，它能让开发者（维护者）及时获知应用出错状态和详情，及早介入处理，将线上故障的影响降低到最低。而要实现告警功能，则需要从两方面入手，一方面是对错误信息进行集中处理（分类、分级、合并、限流等），另一方面需要将这些错误信息及时推送出去。\n\n\u003C!-- more -->\n\n## 推送\n\n推送渠道可以有很多种，常见的包括邮件、短信、微信等，Geek一点的还可以考虑用slack、GTalk(死了吧)、Telegram机器人推送什么的。\n\n为了降低开发成本，这里选用了[Server Chan](http:\u002F\u002Fsc.ftqq.com\u002F1.version)作为推送服务，它的使用极其简单，只要登录之后就会获得一个key，然后访问带key的URL `http:\u002F\u002Fsc.ftqq.com\u002F{KEY}.send` 即可完成消息推送。而推送的渠道则有两种，一种是手机客户端，另一种是微信。想要哪种就使用哪种，在网站绑定即可，推送时是不分渠道的。\n\n## 日志\n\n接下来是应用的错误日志收集，在打听了很多方案之后先用了[bunyan](https:\u002F\u002Fgithub.com\u002Ftrentm\u002Fnode-bunyan)这个模块作为日志记录工具。bunyan的优势在于：\n\n- 结构化日志数据，方便后续整理分析\n- 完善的错误分级 `fatal` \u002F `error` \u002F `warn` \u002F `info` \u002F `debug` \u002F `trace` 一应俱全\n- 多种错误处理方式：文件、控制台、流\n- 可扩展：可以通过扩展流的方式自定义错误处理逻辑\n- 日志文件自动滚动\n\n这里我们主要用到bunyan的扩展性，自定义一个流来获取错误，然后在自定义的逻辑中调用推送逻辑完成告警。\n\n大概的代码：\n\n```javascript\nvar ServerChan = require('bunyan-serverchan');\n\nvar logger = bunyan.createLogger({\n\tname: 'myapp',\n\tstreams: [{\n\t\tlevel: 'error',\n\t\tstream: new ServerChan({key:'MY_KEY'})\n\t}]\n});\n```\n\n首先我们定义了一个`logger`用来记录日志，记录到的`error`级别以上的日志会送给`ServerChan`的实例（一个“stream”）。关于`ServerChan`，稍后解释。\n\n接下来，在出错的地方调用`logger`记录错误：\n\n```javascript\nxxx.on('error',function(err){\n\tlogger.error(err, 'Something went wrong:%s',err.message);\n});\n```\n\n此时错误的记录部分就算完成了，bunyan会负责将错误信息传递给`ServerChan`的实例。\n\n## ServerChan模块\n\n在上面的代码中，我们通过`require('bunyan-serverchan')`引入了`ServerChan`，这个模块负责接受错误信息，并调用Server Chan的URL完成推送。\n\n那这个模块到底是什么呢？\n\n没错，是我写的，欢迎到\u003Chttps:\u002F\u002Fgithub.com\u002FTooBug\u002Fbunyan-serverchan>围观。\n\n事实上这个模块的逻辑极其简单，调用构造函数之后会生成一个对象，只要保证这个对向的`write`方法是存在的，即可以用于bunyan的自定义stream。也就是说，虽然bunyan的概念中是一个自定义stream，但我们并不需要真的实现一个stream，只需要一个有`write`方向的对象即可。\n\n`write`方向负责接受错误信息，并完成自定义逻辑（推送）。\n\n## 结\n\nOver，就这么简单。\n","\u002Farticle\u002Falert_in_node.html",{"id":75,"type":7,"slug":76,"title":77,"date":78,"category":11,"tags":79,"body_markdown":83,"permalink":84,"excerpt_src":20,"media_type":20,"media_title":20,"media_author":20,"media_url":20,"rating":20,"layout":20,"pv":21,"admin_only":21,"created_at":22,"updated_at":22,"deleted_at":20,"status":23,"cover":24},163,"using_pm2_deploy","使用PM2 Deploy部署基于Git版本管理的网站应用","2014-11-19 13:29",[80,81,82],"Node.js","PM2","部署","\n按照官方介绍，PM2是一款用于生产环境Node.js应用进程管理的工具。按照民间介绍，它主要有这样几个功能：保证Node.js应用永远在线（挂掉自动重启）、自动负载均衡、零中断重启应用等。\n\n鉴于它是如此优秀，这里还是简要介绍一下前两个功能。\n\n## 安装\n\n首先，它是一个Node.js写的工具，使用npm即可安装使用：\n\n\tnpm install -g pm2\n\n## 运行Node.js程序\n\n如果不使用pm2，运行Node.js程序是这样：\n\n\tnode xxx.js\n\n使用pm2，是这样：\n\n\tpm2 start xxx.js\n\n### 监视模式\n\n如果你正在开发Node.js应用，需要在代码变更后自动重启应用，只需要在pm2的参数中加上`--watch`即可：\n\n\tpm2 start xxx.js --watch\n\n\u003C!-- more -->\n\n### cluster模式\n\n默认情况下pm2是以fork模式启动应用的，如果以cluster模式启动的话，则可以使用pm2自带的负载均衡、零间断重启等功能。\n\n\tpm2 start xxx.js -i 4\n\n上面的命令会以cluster模式启动4个应用进程，并自动为它们提供负载均衡，并且可以使用gracefulReload达到更新应用时不中断服务的效果。\n\n> 关于cluster模式，可参见朴灵《深入浅出Node.js》一书。\n>\n> pm2低版本默认是以cluster模式启动的。\n\n## 部署应用\n\n这才是本文的重点。PM2的部署功能可以实现网站应用的半自动部署功能。注意本文标题没有加“Node.js”，意味着这个功能并不只适用于Node.js网站应用，事实上它部署功能是用shell写的，跟网站使用什么语言没什么关系。\n\nPM2的部署功能与版本管理工具（Git，不确定是否支持SVN，下文以Git为例）结合比较紧，因此需要保证网站项目使用版本管理工具管理代码，并且服务器可以访问到版本管理服务器。\n\n部署功能是在新版本（0.12？）中才添加进来的。如果你使用的是旧版本的，需要先升级：\n\n```sh\nnpm install -g pm2@latest\npm2 updatePM2\n```\n\n接下来需要建立一个部署的配置文件，这个文件在本机（操作发布的机器）和服务器上都需要有，因此最好放入Git版本管理中，并且推送到远程代码库（Git服务器）。\n\n切换到项目目录下，然后执行\n\n```sh\npm2 ecosystem\n```\n\n即可得到一个示例json文件（例如我得到的是`ecosystem.json5`），将它做对应的修改，大致如下：\n\n```json\n{\n\t\"apps\" : [{\n\t\t\"name\" : \"xxx\", \u002F\u002F项目的名字\n\t\t\"script\" : \"xxx.js\",  \u002F\u002F项目主入口（Node.js）\n\t\t\"env\": {\n\t\t\t\"COMMON_VARIABLE\": \"true\"\n\t\t},\n\t\t\"env_production\" : {\n\t\t\t\"NODE_ENV\": \"production\"\n\t\t}\n\t}],\n\t\"deploy\" : {\n\t\t\"production\" : {\n\t\t\t\"user\" : \"toobug\",\n\t\t\t\"host\" : \"server.toobug.net\",\n\t\t\t\"ref\"  : \"origin\u002Fmaster\", \u002F\u002F需要部署的分支\n\t\t\t\"repo\" : \"git@github.com:TooBug\u002Fxxx.git\",\n\t\t\t\"path\" : \"\u002Fvar\u002Fwww\u002Fxxx\", \u002F\u002Fweb目录\n\t\t\t\"post-deploy\" : \"npm install && pm2 startOrRestart ecosystem.json --env production\"\n\t\t}\n\t}\n}\n```\n\n需要注意：\n\n1. `apps.name`和`apps.script`应该与PM2识别应用有关，后续执行`pm2 restart`的时候可以对应到进程（未证实）\n2. `deploy`中可以含有多个环境，需要能够通过SSH（公钥认证）登录服务器\n3. web目录并不是真正的放版本库文件的目录，PM2会再建立一个`source`子目录，这个才是真正放代码的目录\n4. `post-deploy`是指代码部署完之后执行的命令，这里以Node.js为例子，执行依赖安装，然后重启PM2中的进程\n\n然后就可以使用\n\n```sh\npm2 deploy ecosystem.json production\n```\n\n自动发布网站项目了，非常方便。\n\n```sh\n$>pm2 deploy dev\n--> Deploying to production environment\n--> on host server.toobug.net\n  ○ deploying\n  ○ hook pre-deploy\n  ○ fetching updates\nFetching origin\n  ○ resetting HEAD to origin\u002Fmaster\nHEAD is now at eda2cdd xxx\n  ○ executing post-deploy npm install && pm2 startOrRestart ecosystem.json --env production\nmanpath: can't set the locale; make sure $LC_* and $LANG are correct\nNow using node v0.11.13\n[PM2] restartProcessId process id 0\n┌──────────┬────┬──────┬──────┬────────┬───────────┬────────┬─────────────┬──────────┐\n│ App name │ id │ mode │ PID  │ status │ restarted │ uptime │      memory │ watching │\n├──────────┼────┼──────┼──────┼────────┼───────────┼────────┼─────────────┼──────────┤\n│ xxx      │ 0  │ fork │ 7384 │ online │        33 │ 0s     │ 12.438 MB   │ disabled │\n└──────────┴────┴──────┴──────┴────────┴───────────┴────────┴─────────────┴──────────┘\n Use pm2 info \u003Cid|name> to get more details about an app\n  ○ hook test\n  ○ successfully deployed origin\u002Fmaster\n--> Success\n```\n\n在使用过程中还有几个值得注意的点：\n\n- 在部署过程中，PM2会执行一次`git reset --hard`，意味着如果你修改了配置文件之类的，会被还原，因此最好使用环境变量或者新建文件（不在管理库中）的方式来指定服务器专用的配置项（比如数据库连接信息等）\n- 执行服务器命令时需要关注环境变量，比如使用nvm来管理node版本的话，有可能导致PM2连接后找不到node（以及npm\u002Fpm2）所在路径，解决办法是在脚本最前面加上指定环境变量的脚本，例如`source ~\u002F.bashrc`\n\n## End\n\n就是这样，水文一篇，你要是当成PM2的广告读也行，主要是这个功能真的是很方便。尤其是有多个环境的话，几条命令就能搞定，再也不用登录服务器手工做一堆事情了。\n","\u002Farticle\u002Fusing_pm2_deploy.html",{"id":86,"type":7,"slug":87,"title":88,"date":89,"category":11,"tags":90,"body_markdown":94,"permalink":95,"excerpt_src":20,"media_type":20,"media_title":20,"media_author":20,"media_url":20,"rating":20,"layout":20,"pv":21,"admin_only":21,"created_at":22,"updated_at":22,"deleted_at":20,"status":23,"cover":96},162,"css_image_sprites_on_retina_screen","Retina屏下的CSS雪碧图","2014-03-19 13:39",[91,92,93],"CSS","雪碧图","背景图","\nCSS雪碧图早已经成为前端知识体系中一个必备知识了，时至今日，可能很多人都觉得这一块已经没有什么东西可以再讲了的。但事实上雪碧图一直都可以引出新的话题，比如从最早的连接数和体积的平衡到格式之争到图像摆放位置的策略，再到合并图像的颗粒度，再到内存占用、CPU占用等性能问题……\n\n没错，今天还要在这一古老的话题上展开，引入一个新的问题，那就是雪碧图在retina屏下存在的问题及应对方案。（值得注意的是，retina屏一般指分辨率为普通屏幕两倍的屏，这样按照普通尺寸开发出来的网站相当于被放大了2倍，会导致图像模糊之类的现象产生，理想的解决方案是为retina专门适配一套皮肤，但本文关注的问题是未适配retina屏幕的网站所出现的问题。）\n\n> 雪碧图本身不是浏览器或者web标准中的技术，因此它的不少细节取决于浏览器的实现，本文中的讨论的内容正是如此，为避免争议，本文所有结论的得出场景限定为Mac OSX 10.9.1、Chrome浏览器V33。是否适用iPhone、iPad等场景未做相应测试。\n\n## 无处不在的白边\n\n如前文所述，在retina屏上浏览未做专门适配的网站，会出现图像模糊等问题，无法达到最佳效果，但一般情况下，仍然处于可以接受的范围。不过，在某些网站上，却出现了比图像模糊更糟糕的情况：\n\n![WebQQ在retina屏下出现白边](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F1.png)\n\n\u003C!-- more -->\n\n![财付通首页菜单在retina屏下出现白边](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F2.png)\n\n![支付宝的按钮在retina屏下出现白边](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F3.png)\n\n从上面三张图看到，不少的互联网产品在retina屏下都出现了白边。那么这些白边出现的原因是什么呢？通过查看这些出问题的页面，发现存在一个共同点，那就是这些白边所在的地方都使用了背景图，而且都是使用雪碧图合并的。于是问题就浮现出来了，正是由于雪碧图在retina屏上的放大导致了白边的产生。更为技术化的表达则是，**图片放大过程中进行了插值运算，导致原来整齐的图片边界混入了插值后模糊的像素**，从而导致原来整齐的边界处出现“白边”。\n\n打开WebQQ的雪碧图（\u003Chttp:\u002F\u002F0.web.qstatic.com\u002Fwebqqpic\u002Fpubapps\u002F0\u002F50\u002Fimages\u002Feqq_sprite.gif?t=20111011001>），就可以验证这一结论。\n\n![WebQQ雪碧图](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F4.gif)\n\n如果您在阅读本文时刚好使用的retina屏幕，可能已经能看到图标边上的白边了，为了统一说明，特放上图像编辑软件中局部放大的图片。\n\n![WebQQ雪碧图局部放大](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F5.png)\n\n图中可以看到，边缘是非常整齐的，但我们在retina屏上截到的图却是这样：\n\n![WebQQ雪碧图局部放大](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F6.png)\n\n可以看到，由于retina屏下，图片被强制放大，导致了原本整齐的边界不再整齐，从而使得页面上出现“白边”。\n\n## 真的是因为雪碧图吗\n\n至此，我们已经推断出白边是因为图片被放大而导致，那么这跟雪碧图有关系吗？如果不用雪碧图会出现这样的现象么？为了验证这个结论，本文曾一度中断，最终还是拿到了比较令人信服的结果。\n\n首先，我们准备一张如下的图片：\n\n![实验用图1](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F7.png)\n\n这张图放了四个色块，其中左上和右下的色块有留白。接下来我们在浏览器中打开它，结果如下：\n\n![实验用图1](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F8.png)\n\n可以看到，图片中有颜色交界的地方都有插值运算而导致模糊，但边缘却是清晰的！也就是说，如果没有拼图的话，浏览器是可以处理好图片的边缘的。为了保险起见，接下来又做了一个实验，准备了一张20*20的纯红色图片，并与CSS写的红色背景进行混合，看看是否有“白边”出现。\n\n![实验用图2](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F9.gif)\n\n可以看到，在CSS背景色不断变化的过程中，图片与背景可以完全融合，没有任何奇怪的现象出现。\n\n至此，我们终于判定，导致“白边”的原因就是因为雪碧图中不同图像之间在拼合后产生了插值而导致边缘部分模糊。\n\n## 解决之道\n\n知道了原因就好解决了，既然白边的出现是因为插值，并且这个插值行为不可控，那就只好将插值的部分移出视野之外了。讲人话就是：切图的时候多留点“出血”。\n\n继续拿WebQQ为例，如上面所说，原文中聊天气泡图标的背景宽度是20px，两边各加1px，总共22px，效果如下：\n\n![WebQQ图标改进1](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F10.png)\n\n![WebQQ图标改进1效果](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F11.png)\n\n可以看到白边已经减少了不少，但仍然存在。继续在两边各加1px，总共24px，效果如下：\n\n![WebQQ图标改进2](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F12.png)\n\n![WebQQ图标改进2效果](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F13.png)\n\n至此，问题完美解决，结论是：\n\n**切图时请为图标在各个方向上多留2px空间**，即可保证retina屏下不出现意料之外的毛边（白边）。\n\n## The End?\n\n这就完了？当然没有，还有另外一类案例解决不了，就是开头提到的支付宝的按钮。如果你不记得了，没关系，我再放一次图：\n\n![支付宝的按钮在retina屏下出现白边](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F3.png)\n\n看一下它的雪碧图(\u003Chttps:\u002F\u002Fi.alipayobjects.com\u002Fe\u002F201204\u002F2vCVR5Bh4d.png>)和结构：\n\n![支付宝按钮雪碧图](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F14.png)\n\n![支付宝按钮结构](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F15.png)\n\n可以看出，这个按钮其实是由左边两边拼合而成（分别由内外两层元素组成），白边来自右边的结构。如果把左边的背景屏蔽掉，会看得更清楚：\n\n![支付宝按钮结构](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F16.png)\n\n这种情况下，背景图小于容器本身，因此无法将插值部分排除到视野外，也即上面说的多留2px也无法解决。（事实上左边已经留有N像素了……）那就只好再利用上面在验证是否是雪碧图才有问题时说的另外一个结论了：边缘部分是不会被插值的。\n\n于是，将这个雪碧图需要插值的部分改到边缘去，如下图（只改了上面的几个）：\n\n![支付宝雪碧图修改版](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F17.png)\n\n效果如下：\n\n![支付宝雪碧图修改版效果](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F18.png)\n\n至此问题解决。\n\n> 注：之所以会采用这样一种结构的按钮，是因为它可以根据文字长度进行自适应，`background-position`的`x`值取`right`即可保证雪碧图是始终靠按钮右边对齐的。而修改版中改变雪碧图结构后，则需要手工指定`background-position`的`x`值。\n\n> 一种更好的解决方案则是直接使用CSS3来写按钮，在IE下进行降级。\n\n## 结\n\n这应该是博客中图片最多的一篇了，关注的也是一个非常非常非常小的点，起因只是因为支付宝的按钮在我发现这个问题一年后仍然没有改过，于是忍不住研究了一下这问题到底有多难解决。\n\n最后，根据上述实验和推断过程小结一下在应用雪碧图的过程中值得注意的点：\n\n1. 雪碧图中请给背景留出足够的空间（出血），否则可能导致retina屏下产生毛边（白边）\n2. 雪碧图如果图标排得太过密集，可能导致retina屏下出现“窜色”（与毛边一样，是由于插值导致）\n3. 如果插值区域无法避免，请将它放在图片边缘位置\n\n最后的最后，一句题外话，以上所有现象均可在部分浏览器放大页面时出现（我忘记是什么浏览器了，曾有项目因此被产品经理报bug，最终将所有图标周围留了2px空白解决）。\n","\u002Farticle\u002Fcss_image_sprites_on_retina_screen.html","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F1.png",{"id":98,"type":7,"slug":99,"title":100,"date":101,"category":11,"tags":102,"body_markdown":107,"permalink":108,"excerpt_src":20,"media_type":20,"media_title":20,"media_author":20,"media_url":20,"rating":20,"layout":20,"pv":21,"admin_only":21,"created_at":22,"updated_at":22,"deleted_at":20,"status":23,"cover":24},161,"learning_es6_generator","学习ES6生成器（Generator）","2013-12-29 13:35:00",[14,103,104,105,106],"Generator","生成器","回调","异步","\n这几天，TJ大神的koa框架突然在国内火起来了，随之而来的，则是其使用的ES6生成器（Generator）引起了广大码农的强烈兴趣，各种文章也如雨后春笋般拔地而起，比如[这篇](https:\u002F\u002Fwww.imququ.com\u002Fpost\u002Fgenerator-function-in-es6.html)、[这篇](http:\u002F\u002Fbg.biedalian.com\u002F2013\u002F12\u002F21\u002Fharmony-generator.html)、还有[这篇](https:\u002F\u002Fdeveloper.mozilla.org\u002Fzh-CN\u002Fdocs\u002FJavaScript\u002FGuide\u002FIterators_and_Generators)。这个神奇的生成器被视为解决JS“回调恶魔金字塔”的利器。在动手实践之后，发现介绍ES6生成器的文章仍然有些疏漏，因此有了这篇文章，权当是对各位大大们的补充好了。\n\n## 背景\n\n在JS的使用场景中，异步操作的处理是一个不可回避的问题，如果不做任何抽象、组织，只是“跟着感觉走”，那么面对“按顺序发起3个ajax请求”的需求，很容易就能写出如下代码（假设已引入jQuery）：\n\n```javascript\n\u002F\u002F 第1个ajax请求\n$.ajax({\n  url:'http:\u002F\u002Fecho.113.im',\n  dateType:'json',\n  type:'get',\n  data:{\n    data:JSON.stringify({status:1,data:'hello world'}),\n    type:'json',\n    timeout:1000\n  },\n  success:function(data){\n    if(data.status === 1){\n      \u002F\u002F 第2个ajax请求\n      $.ajax({\n        ......此处省略500字\n        success:function(data){\n          if(data.status === 1){\n            \u002F\u002F 第3个ajax请求\n            $.ajax({\n              ......此处省略500字\n              success:function(data){\n                if(data.status === 1){\n\n                }\n              }\n            });\n          }\n        }\n      });\n    }\n  }\n});\n```\n\n当顺序执行的异步操作越来越多的时候，回调层级也就越多，这也就是传说中的“回调恶魔金字塔”。\n\n\u003C!-- more -->\n\n## 生成器的卢山真面目\n\n所谓“生成器”，其实是一个函数，但是这个函数的行为会比较特殊：\n\n1. 它并不直接执行逻辑，而是用来生成另一个对象（这也正是“生成器”的含义）\n2. 它所生成的对象中的函数可以把逻辑拆开来，一片一片调用执行，而不是像普通的函数，只能从头到尾一次执行完毕\n\n生成器的语法和普通函数类似，特殊之处在于：\n\n1. 字面量（函数声明\u002F函数表达式）的关键字`function`后面多了一个`*`，而且这个`*`前后允许有空白字符\n2. 函数体中多了`yield`运算符\n\n举个粟子：\n\n```javascript\nfunction * GenA(){\n  console.log('from GenA, first.');\n  yield 1;\n  console.log('from GenA, second.');\n  var value3 = yield 2;\n  console.log('from GenA, third.',value3);\n  return 3;\n}\n\nvar a = GenA();\n```\n\n接下来依次执行：\n\n```javascript\na.next();\n\u002F\u002F from GenA, first.\n\u002F\u002F Object {value:1,done:false}\n\na.next();\n\u002F\u002F from GenA, second.\n\u002F\u002F Object {value:2,done:false}\n\na.next(333);\n\u002F\u002F from GenA, third.\n\u002F\u002F 333\n\u002F\u002F Object {value:3,done:true}\n\na.next();\n\u002F\u002F Object {value:undefined,done:true}\n```\n\n这个例子反映了生成器的基本用法，有以下几点值得注意：\n\n1. 在调用`GenA()`时，函数体中的逻辑并不会执行（控制台没有输出），直接调用`a.next()`时才会执行\n2. `a`是一个对象，它由生成器`GenA()`调用而来，注意`GenA()`并没有返回`a`对象，这非常像构造函数的执行形式，但是不允许添加`new`\n3. 调用`a.next()`时，函数体中的逻辑才开始真正执行，每次调用时会到`yield`语句结束，并将`yield`的运算数作为结果返回\n4. `a.next()`返回的结果是一个对象，对`yield`的运算数做了包装，并带上了`done`属性\n5. 当`done`属性为`false`时，表示该函数逻辑还未执行完，可以调用`a.next()`继续执行\n6. 最后一次返回的结果为`return`语句返回的结果，且`done`值为`true`。如果不写`return`，则值为`undefined`\n7. `value3 = yield 2`这句是指，这一段逻辑返回2，在下一次调用`a.next()`时，将参数赋给value3。换句话说，这句只执行了后面半段就暂停了，等到再次调用`a.next()`时才会将参数赋给value3并继续执行下面的逻辑\n8. 返回值中`done`为`true`时，仍然可以继续调用，返回的值为`undefined`\n\n## 同步场景下生成器的使用\n\n来看看同步场景下，如何使用生成器：\n\n```javascript\nfunction * Square(){\n  for(var i=1;;i++){\n    yield i*i;\n  }\n}\n\nvar square = Square();\n\nsquare.next(); \u002F\u002F 1\nsquare.next(); \u002F\u002F 4\nsquare.next(); \u002F\u002F 9\n......\n```\n\n同步场景下大概就是这么用的，很无趣是吧？我也这么觉得，其实和直接函数调用差别不大。不过值得注意的是，我们在循环中并没有设中止条件，因为调用一个`square.next()`方法，它才会执行一次，不调用则不执行，所以不用担心死循环的问题。\n\n## 异步场景下的生成器使用\n\n如何用生成器解决异步场景下的“回调恶魔金字塔”呢？满心期待对吧，很遗憾，它并不能那么简单地解决……\n\n从前面的例子中，其实已经可以体会出来了，生成器的用法中并不包含对异步的处理，所以其实没有办法帮助我们对异步回调进行封闭。那么为什么大家将它视为解决回调嵌套的神器呢？在翻阅了不少资料后找到[这篇文章](http:\u002F\u002Fblog.stevensanderson.com\u002F2013\u002F12\u002F21\u002Fexperiments-with-koa-and-javascript-generators\u002F)，文章作者一开始也认为生成器并不能解决回调嵌套的问题，但下面自己做了解释，如果生成器的返回的是一系列的Promise对象的话，情况就会不一样了，举个粟子：\n\n```javascript\nfunction myAjax(){\n  return fetch('http:\u002F\u002Fecho.113.im?data=1');\n}\n```\n\n我们使用`window.fetch`方法来处理ajax请求，这个方法会返回一个Promise对象。然后，我们使用一个生成器来包装这个操作：\n\n```javascript\nfunction * MyLogic(){\n  var serverData = yield myAjax();\n  console.log('MyLogic after myAjax');\n  console.log('serverStatus:%s',serverData.status);\n}\n```\n\n使用的时候这样用：\n\n```javascript\nvar myLogic = MyLogic();\nvar promise = myLogic.next().value;\npromise.then(function(serverData){\n  myLogic.next(serverData);\n});\n```\n\n可以看到，我们这里的`myAjax1()`以及`MyLogic()`函数中，并没有使用回调，就完成了异步操作。\n\n这里有几个值得注意的点：\n\n1. `myAjax()`函数返回的是一个Promise对象\n2. `myLogic`中的第一个语句，返回给外界的是`myAjax()`返回的Promise对象，等外界再次调用`next()`方法时将数据传进来，赋值给`serverDate`\n3. `promise`的状态是由第三段代码，在外部进行处理，完成的时候调用`myLogic.next()`方法并将`serverData`再传回`MyLogic()`中\n\n你一定会问，下面这个`promise.done`不就是回调操作么？Bingo！这正是精华所在！我们来看一下这段代码做了什么：\n\n首先，`myLogic.next()`返回了一个Promise对象（`promise`），然后，`promise.then`中的回调函数所做的事情就是调用`myLogic.next()`方法就行了，除了调用`next()`方法，其它的什么事情都没有。此时，我们就会想到一个程序员特别喜欢的词，叫“封装”！既然这个回调函数只是调用`myLogic.next()`方法，那为什么不把它封装起来？\n\n## 异步封装\n\n首先，我们保持`myAjax()`和`MyLogic`定义不变，而将`myLogic.next()`放到一个函数来调用，这个函数专门负责调用`myLogic.next()`，得到返回的Promise对象，然后在Promise被resolve的时候再次调用`myLogic.next()`：\n\n```javascript\nvar myLogic = MyLogic();\n\nfunction genRunner(){\n\n  \u002F\u002F 调用next()获取promise\n  var yieldValue = myLogic.next();\n  var promise = yieldValue.value;\n\n  if(promise){\n    promise.then(function(data){\n      \u002F\u002F promise被resolve的时候再次调用genRunner\n      \u002F\u002F 以继续执行MyLogic中后面的逻辑\n      genRunner();\n    });\n  }\n}\n```\n\n这样我们就把不停地调用`myLogic.next()`和不停地`promise.then()`的过程进行了封装。运行`genRunner()`跑一下：\n\n```\nMyLogic after myAjax1\nUncaught (in promise) TypeError: Cannot read property 'status' of undefined(…)\n```\n\n可见`MyLogic`在`yield`后的语句的确被执行了，但是`serverData`却没有值，这是因为我们在调用`myLogic.next()`的时候没有把值传回去。稍微修改下代码：\n\n```javascript\n\u002F\u002F diff1: genRunner接受参数val\nfunction genRunner(val){\n\n  \u002F\u002F diff2: .next调用时把参数传过去，yield左边可以被赋值\n  var yieldValue = myLogic.next(val);\n  var promise = yieldValue.value;\n\n  if(promise){\n    promise.then(function(data){\n      \u002F\u002F diff3: 调用genRunner时传递参数\n      genRunner(data);\n    });\n  }\n}\n```\n\n这次一切都对了：\n\n```\nMyLogic after myAjax1\nserverStatus:200\n```\n\n至此我们已经把封装最核心的部分抽离出来了，我们的业务代码`MyLogic()`已经是“异步操作，同步写法”，而我们亲眼见证了这一切是怎么办到的。那么接下来？为什么不再封装得更通用一些呢？\n\n```javascript\nvar genRunner = function(GenFunc){\n\n  return new Promise(function(resolve, reject){\n\n    var gen = GenFunc();\n\n    var innerRun = function(val){\n\n      var val = gen.next(val);\n\n      \u002F\u002F 如果已经跑完了，则resolve\n      if(val.done){\n        resolve(val.value);\n        return;\n      }\n      \u002F\u002F 如果有返回值，则调用`.then`\n      \u002F\u002F 否则直接调用下一次innerRun()\n      \u002F\u002F 为简单起见，假设有值的时候永远是promise\n      if(val.value){\n        val.value.then(function(data){\n          innerRun(data);\n        });\n      }else{\n        innerRun(val.value);\n      }\n\n    }\n    innerRun();\n\n  });\n\n};\n```\n\n这里我们将刚刚看过的封装改成了`innerRun()`，并加上了自动调用。外面再封装了一层`genRunner()`，返回一个Promise。在`genFunc`全程调用完之后，Promise被resolve。\n\n用起来大约是这样：\n\n```javascript\ngenRunner(function*(){\n\n  var serverData = yield myAjax();\n  console.log('MyLogic after myAjax');\n  console.log('serverStatus:%s',serverData.status);\n\n}).then(function(message){\n\n  console.log(message);\n\n});\n```\n\n生活真美好！\n\n最后，以别人文章中的一段koa框架使用代码收尾吧：\n\n```javascript\nvar koa = require('koa'),\n  app = koa();\n\napp.use(function *() {\n\n  \u002F\u002F 这是这个例子中最重要的部分，我们进行了一系列异步操作，却没有回调\n  var city = yield geolocation.getCityAsync(this.req.ip);\n  var forecast = yield weather.getForecastAsync(city);\n\n  this.body = 'Today, ' + city + ' will be ' + forecast.temperature + ' degrees.';\n\n});\n\napp.listen(8080);\n```\n\n眼熟吗？koa就是像我们刚刚做的这样，封装了对生成器返回值的处理和调用`next()`方法的细节（这里的`app.use()`就像前面的`genRunner()`函数），使得我们的逻辑代码看起来是如此简单，这正是koa的伟大之处，也是ES6生成器这一特性能迅速引起如此多轰动的真正原因。\n","\u002Farticle\u002Flearning_es6_generator.html",{"id":110,"type":7,"slug":111,"title":112,"date":113,"category":11,"tags":114,"body_markdown":117,"permalink":118,"excerpt_src":20,"media_type":20,"media_title":20,"media_author":20,"media_url":20,"rating":20,"layout":20,"pv":21,"admin_only":21,"created_at":22,"updated_at":22,"deleted_at":20,"status":23,"cover":24},160,"how_to_design_front_end_template_engine","如何设计一个前端模板引擎","2013-08-25 09:55:24",[115,116],"模板引擎","Template","\n前端模板引擎现在已经被广泛应用于前端开发了，几乎每个项目都会使用。微博上甚至出现了“不写个模板引擎就没办法在前端界混了”的言论。当然，这是玩笑话，却也能在一定程度上反映前端模板的普及程度。如果你还没有了解过前端模板引擎，赶紧去补补课吧。\n\n本文其实算不上是一篇讲模板引擎设计的文章，写这篇文章的动力来自于自己使用过一些模板引擎（jQuery Tmpl、jade、ejs、artTemplate以及ThinkPHP自带后端模板引擎）之后的心得，所以可能不会涉及到模板引擎设计的方方面面，更多地是讲模板引擎之间的一些有差异的细节以及我的思考和取舍。\n\n## 来历\n\n```javascript\nvar tempHtml = '\u003Ctable>' +\n    '     \u003Ctr>' +\n    '       \u003Cth>Hello\u003C\u002Fth>' +\n    '       \u003Cth>World\u003C\u002Fth>' +\n    '       \u003Cth>!\u003C\u002Fth>' +\n    '     \u003C\u002Ftr>' +\n    '     \u003Ctr>' +\n    '       \u003Ctd>' + myData.col1 + '\u003C\u002Ftd>' +\n    '       \u003Ctd>' + myData.col2 +'\u003C\u002Ftd>' +\n    '       \u003Ctd>' + (myData.col3 === 'yes'?'!':'?') + '\u003C\u002Ftd>' +\n    '     \u003C\u002Ftr>' +\n    '   \u003C\u002Ftable>';\n\ndocument.querySelector('#myDiv').innerHTML = tempHtml;\n```\n\n相信上面的代码对哪怕做过一点点涉及界面开发的都应该很熟悉吧。当我们想把一段结构和一段数据组合起来，再放到页面上时，就会常常面临这样一段复杂的代码。\n\n这段代码相信不用我说你也会觉得它实在有点复杂：要处理结构中字符串本身的拼接，还要注意结构与数据的拼接，处理数据拼接时还要注意运算优先级（尤其在使用`?:`三元运算符时），还要为了可读性考虑纠结的缩进……\n\n当然，最麻烦的还不是这里，当我们想要对一个数据循环遍历并输出时，居然还要自己去写循环，再一圈一圈地把这些结构拼起来，最后再拼上首尾的结构。而当数据为空时，又要自己去写一个“暂无数据”之类的占位符……\n\n\u003C!-- more -->\n\n为了解决这些问题，前端模板引擎应运而生。\n\n使用了模板引擎之后，上面的例子可能是类似这样：\n\n```javascript\nvar tmpl = '\u003Ctable>' +\n    ......\n    '   \u003Ctd>{%col1}\u003C\u002Ftd>' +\n    '   \u003Ctd>{%col2}\u003C\u002Ftd>' +\n    '   \u003Ctd>{{if col3===\"yes\"}}!{{else}}?{{end if}}' +\n    ......\n\ndocument.querySelector('#myDiv').innerHTML = template(tmplHtml,myData);\n```\n\n这个例子看起来跟上面也没啥区别呀？字符串拼接一点没见少。没错，但这里有了一个变化，即字符串拼接不再是必须行为（为了可读性而换行拼接另算），因此将这段模板存放到别的地方成为可能，比如直接放入HTML中，使用时用js提取过来即可。这样，代码的复杂度就大大降低了。\n\n## 职责\n\n上面的例子我们看到了模板引擎带来的好处，可以极大简化代码的编写，自然也可以减少出错的可能性。那具体说来，模板引擎到底应该做些什么事情？套用一下重构中的结构、表现、行为分离的概念，模板引擎在做的事情也无非是几个分离，但对应的对象是结构、数据、逻辑和表现：\n\n- 结构：最终要展现出来的结构框架，也即“模板”本身，比如上例中的表格。\n- 数据：不用多解释，要展现出来的数据。\n- 逻辑：即业务逻辑代码，不包含可能存在于模板引擎中的逻辑处理。\n- 表现：数据的具体表现，比如日期可以有各种不同的格式。\n\n### 数据和结构分离\n\n这是一个模板引擎最最基本的功能，即将结构框架和要展现的数据完全独立开来，不让它们直接进行拼接。比如上例中，在使用了模板引擎之后，`tmpl`和`myData`就已经不存在直接拼接的行为，而是最后使用模板引擎将它们两者组合起来。如果这一点做不到，实在不能称为“模板引擎”。\n\n### 数据和逻辑分离\n\n所谓数据和逻辑分离，是指数据处理应该和业务逻辑分离开来。业务逻辑应该只专业于数据读写、用户交互等与业务具体相关的事情，至于数据展现上的一些抉择，比如这里是显示“Yes”还是显示“是”，应该交给处理数据的部分来进行。这样的分工可以有效保持数据处理部分的完整性，使业务逻辑和数据处理部分的耦合减少。对这两部分来说，都是既便于复用，也便于后期维护。\n\n#### 分支逻辑\n\n在上例中，我们可以看到在处理的处理中有一个分支逻辑，用于判断`col3`的值是否为`yes`，然后生成不同的内容。很明显的是，这个逻辑是因为数据而存在，因而应该被放到数据处理部分，因而上例将它写入了模板中。\n\n反过来，如果我们使用了一个无逻辑模板引擎，即逻辑引擎无力处理这种简单的逻辑，我们就只能将这个判断写到业务逻辑中，先判断`col3 === yes`，然后根据这个结构在`myData`中写入一个新的值`col3Display`。这样的话很不利于维护，比如如果`col3`改成了`col3`，首先想到的是需要在模板中更改名，然后需要在模板处理的部分改名，除了这些之处，还需要修改处理`col3Display`的地方，而这个地方是存在于业务逻辑中的。这种耦合也让业务逻辑和模板分别复用变成几乎不可能的事情。\n\n一般而言，模板中需要支持的逻辑主要为分支逻辑和遍历逻辑（下面会提及）即可，其它复杂的逻辑使用得并不普遍。当前市面上的模板引擎几乎100%包含这些简单逻辑的处理，如上面举的例子，这是非常合理的。\n\n#### 数据遍历\n\n基于上面数据和逻辑分离的思想，除了分支逻辑之外，模板引擎还应该支持一个更为基础的功能，即数据自动遍历。也就是说，当我们传入一个数据数组的时候，模板引擎应该能够对数组中的元素自动遍历，对每个数据生成组合后的html片段再组合起来返回给用户，比如：\n\n```javascript\nvar tmplStr = '\u003Cli>My Name is {%name%}, I\\'m {%age%} years old.\u003C\u002Fli>';\n\nvar arr = [{\n    name:'TooBug',\n    age:18\n},{\n    name:'ThreeBug',\n    age:18.1\n}];\n\nvar html1 = template(tmplStr,arr);\n\u002F\u002F 结果：\n\u002F\u002F \u003Cli>My Name is TooBug, I\\'m 18 years old.\u003C\u002Fli>\n\u002F\u002F \u003Cli>My Name is ThreeBug, I\\'m 18.1 years old.\u003C\u002Fli>\n```\n\n如果模板没有自动遍历的功能，那么开发者又只好把用于数据处理的逻辑写入业务代码了：\n\n```javascript\nvar tmplHtml = '';\narr.forEach(function(dataItem){\n\n  tmplHtml += template(tmplStr,dataItem);\n\n});\n```\n\n早期的artTemplate就不支持自动遍历，要么采用上面的方法在业务代码中做，要么在业务代码中把数组再包装成一个对象，在模板中写遍历的代码。这是一件很纠结的事情。（to 糖饼：特此吐槽。）\n\n### 表现与逻辑分离\n\n数据的最佳存储方式与数据的最佳表现方式很多时候并不一致。比如时间类型的数据，最佳的存储方式无疑是时间戳（如`1377399298`），而最佳的表现方式则是我们最为熟悉的年月日的表示方式（如`2013年8月25日`）。此时就需要在数据展现前进行一些格式化。\n\n数据的格式化不像分支逻辑或者遍历逻辑那么简单，它是一个五花八门的工作。比如就时间而言，就有无数种格式，有时候需要`2013年8月25日`，有时候需要`2013-08-25`，有时候需要`11:03`，有时候需要`2 mins ago`……更别说其它更多的数据类型了。因此，做数据格式化往往有一些专门的逻辑，简单一点的可能是一个小函数，复杂一点的则可能是类似moment.js之类的库。\n\n于是，如何处理格式化库、业务逻辑、模板引擎的关系就成为一个很重要的问题。\n\n按照我崇尚的逻辑分离的思想，格式化代码不应该出现在业务代码中，最理想的方式是作为一个单独的文件外挂进来，然后由模板引擎直接调用。拿moment.js为例，最理想的方式就是可以直接在模板中写类似这样的代码：\n\n```html\n......\n\u003Ctd>moment(pubDate).format('YYYY-MM-DD')\u003C\u002Ftd>\n......\n```\n\n事实上，目前大部分模板也是允许这样操作的。但也有部分模板引擎选择了封闭了外部变量的访问，以artTemplate为典型。封闭对外部变量的访问最大的考量就在于阻止在模板中意外修改外部变量。因此，artTemplate在封闭对外部变量访问的同时，提供了另一种方案，即辅助方法机制。用户可以为模板引擎指定一些辅助方法，模板引擎可以访问这些辅助方法。如：\n\n```javascript\ntemplate.helper('myFormat', helper.myFormat);\n```\n\n加了这句代码之后就可以在artTemplate的模板中使用moment库来做日期时间的格式化。\n\n就上面这个例子来说，与直接使用外挂js中的格式化方法唯一的区别只是是否有`helper`这个全局命名空间的区别（artTemplate理想的方式是辅助方法不占用全局命名空间）。但我觉得，“辅助方法”这个概念又增加了不少的学习门槛，想必作者也为此接到了不少咨询。作为模板用户应该知道它所编写的代码会产生怎样的后果，作为模板引擎不应该以易用性和可维护性为代价来保障这个有点牵强的安全性。\n\n在处理格式化方法的问题时，还有一种方案，以jQuery tmpl为代表，在调用`tmpl`方法时可以传入一个对象，对象中的成员函数都可以直接在模板中使用。这种方案与artTemplate的辅助方法机制有些类似，但jQuery tmpl中的辅助方法只对本次渲染的模板有效。\n\n```javascript\n$('myTest').tmpl({\n  myFormat:function(val){\n    return val;\n  }\n},myData);\n```\n\n这种方案其实有点难评价是好是坏。它的就地编写就地使用的方式用起来还是很方便的，而且方法直接写在渲染语句中，也不会给维护带来特别大的问题。但丢一大堆方法在渲染参数中还是多少会有些不爽。算是一种折衷的方案吧。\n\n## 易用性\n\n易用性是衡量一个模板引擎是否优秀的重要指标，因为它的目的就是简化前端开发工作，如果引入模板引擎反而使得代码更加复杂难懂，则有些得不偿失，毕竟模板引擎也是有学习和性能成本的。\n\n### 简化还是繁化\n\n如果一个模板写得人头昏眼花，也许应该回过头来想一下这个模板引擎设计是不是有问题。看一段代码：\n\n```html\n\u003C%if(myData.testArr){%>\n  \u003C%for(var i=0;i\u003CmyData.testArr.length;i++){%>\n    \u003Cinput type=\"checked\"\u003C%if(myData.testArr[i].checked){%> checked\u003C%}%>\u002F>\n  \u003C%}%>\n\u003C%}%>\n```\n\n上面这段代码简化自某个项目的真实代码，一眼看去会不会觉得非常繁杂？其实代码很简单，无非是判断一个数组是否存在，然后对数组元素遍历输出checkbox。但看起来就是觉得头疼，各种符号穿插其中，各种符号鱼龙混杂，甚至连编辑器高亮都可能完全失效。\n\n我觉得这个模板引擎的语法设计是有如下问题的：\n\n- 为降低学习成本使用原生JS语法，这个想法是好的，但这个做法却在客观上增加了代码的复杂性，比如需要用户自己管理临时变量，需要自己管理代码的开始与结束（大括号）。\n- 对JS原生语法的支持有限，比如对于数组的遍历，并不支持使用原生的`forEach`方法，进一步加大代码复杂性。\n- 没有较好地处理“逻辑插值”的问题。（所谓“逻辑插值”是指标记中的某个部分需要按分支逻辑来处理的情况。）导致了在标签属性部分的代码十分复杂。而更为严重的是，尖括号`\u003C>`会严重干扰到编辑器的语法解析过程，导致语法高亮出现错误。\n\n来看jade的处理方法：\n\n```jade\n- if(myData.testArr)\n  - each dataItem in myData.testArr\n    input(type=\"checked\",checked=dataItem.checked)\n```\n\n再来看一下上面提到的三个问题：\n\n- 学习成本：上面的jade代码相信你可以秒懂，既然如此，学习成本便不是问题。\n- 原生语法：jade通过`-`来区分逻辑与标记，在逻辑代码中，随意使用任何原生js语法。\n- 逻辑插值：jade使用`checked=true\u002Ffalse`的方式处理布尔属性，避免了大部分逻辑插值的情况，虽然不能完全解决问题，但已经足够好读了。\n\n此外，jade还有同时适用于对象和数组的each语法，遍历起来十分方便。\n\n看完上面的例子，应该不用多说了，模板如果不能简化开发工作，反而使代码变得复杂和难以维护，那么我个人认为宁可不要。\n\n### 模板标记的选用\n\n在模板标记的选用上，各个引擎可谓是八仙过海各显神通。在这个问题上，也确实没有很多可以拿来比较的东西，但还是有些小点值得一说。\n\n#### 模板标记与页面重构\n\n按照[彪叔](http:\u002F\u002Fwww.twinsenliang.net)的观点，模板标记应该选用“看起来像文本”的标记，比如尖括号就应该避免，因为这样可以让重构同学在做页面的时候大概预览到模板标记所在处的效果，而不是被隐藏掉。\n\n不过，这其实是个不折不扣的伪命题。举个例子，`\u003C%=var%>`这种语法来自ASP，而这种语法在浏览器中并不会被隐藏掉，而是原样显示，因此并不会影响预览效果。至于编码时，则更没区别了，只是敲不同的键而已。既然如此，那么所谓“被隐藏掉”是在哪里呢？答案其实是——DreamWeaver的可视化界面……面对已经被边缘掉的DW，这个问题的的确确可以被彻底忽视了。\n\n当然，如果你坚持使用DW的话，我仍然要说这是个伪命题，因为时至今日，已经很少有人再把模板标记放到正常的文档流中了，因此不管选择什么样的标记，这些不在文档流中的模板都始终不会被看到。\n\n#### 避免与后端语言模板冲突\n\n这倒是一个很值得考虑的问题。在使用jQuery tmpl与ThinkPHP时，由于两者的模板机制均有使用`$`符号，因此经常导致前端模板标记被后端解析，从而导致页面异常。最终只能通过后端强制指定不解析模板来解决问题。\n\n考虑到这个问题的话，在选用前端模板标记时其实也没办法做到尽善尽美，因为后端模板的标记也不少，很难避免所有可能冲突的情况。但我们可以适当做些考虑，避开一些常用的后端模板。比如我会更倾向于使用`{%raw%}{%var%}{%endraw%}`的方式（虽然我也没法证明它是一个更好的选择）。\n\n#### 美观\n\n呵呵。美观是个很主观的话题，所以作者觉得哪个好看就用哪个吧……\n\n不过，除了输出变量之外，其它的模板标记其实更多的是一种语法设计，在这方面，确实要考虑美观的事情，上面说过，如果一个模板写出来异常复杂就不好了。\n\n#### 模板标记转义\n\n其实这个问题与模板标记本身关系不大，主要是指如何让模板强制不解析某个模板标记，原样输出它。一般比较理想的方式是对标记进行转义，但转义也有两个层次的含义：\n\n- 在HTML级别的转义，比如我要输出`\u003C%=var%>`，则直接在模板中写转义后的`&lt;%=var%&gt;`。\n- 在模板级别的转义，比如我要在jade中输出`#{var}`，则在模板中写入`\\#{var}`即可。\n\n对于这两种转义，我更看重的是第二种，因为一个模板应当要有输出任何字符的能力，包括它用到的模板标记本身。（注意在HTML级别转义的话，输出的结果文本是不一样的。）\n\n### 模板位置及提取\n\n在文章开头的例子中，我们把模板写到了JS中，随后也做了说明，模板文本是可以写到HTML中的。接下来就来看看将模板放到HTML中的几种主要方法。（还有更多的方法，玉伯有一篇文章中有详述，由于原文被墙，可以到CSDN转载的页面查看：[《[浅析]淘宝详情页的BigRender优化的最佳方式》](http:\u002F\u002Fwww.csdn.net\u002Farticle\u002F2011-09-27\u002F304989)）\n\n#### 直接放入文档流中的DOM\n\n将模板直接放到DOM中是一种比较原始的方案，这种方法会在写HTML时直接将模板写进去，然后使用JS动态从父容器中取出进行渲染，最后将生成的HTML字符串再写回父容器中。这种方案的弊端很多：\n\n- 模板会被渲染出来，这是开发者不希望看到的。而如果使用css来隐藏的话又在项目中添加了没有意义的代码。\n- 模板存在被修改的可能。一旦DOM节点被渲染后就无法保证它不被修改，有可能等我们用JS去取模板时，它已经被改得面目全非甚至都不在文档中了。（外部JS库、UI库、开发框架如jQuery Mobile、浏览器插件都可能修改页面中的DOM。）\n\n后来，在前辈们的探索下，找到了一种比较完美的方式：放到`textarea`中，这种方案在很大程度上避开了以上问题，只需要处理`textarea`本身即可。取用的时候只要取`textarea`的值即可。\n\n#### 放入script标签\n\n将模板标记放到`textarea`中，虽然使得模板本身避免了被渲染和修改，但`textarea`本身还是需要隐藏。后来，前辈们又发现一个更NB的方案：将模板放到`type`属性不为`script`(以及一堆同义词)的`script`标签中，这个script是一个标准的DOM元素，但又不会受到其它的影响，浏览器也会直接忽略它，并不渲染。\n\n现在一个常见的模板放到`script`标签中可能是这样：\n\n```html\n\u003Cscript type=\"text\u002Ftmpl\" id=\"myTmpl\">\n  模板放这里\n  ......\n\u003C\u002Fscript>\n```\n\n这是一种比较完美的方式，也是现在比较主流的方式。\n\n#### template元素\n\n鉴于模板引擎的广泛使用，web components组件规范中直接定义了一个`template`元素，专门用来存放模板。\n\n```html\n\u003Ctemplate id=\"myTmpl\">\n  模板放这里\n  ......\n\u003C\u002Ftemplate>\n```\n\n目前Chrome和Firefox均已支持这个元素（移动端暂没有浏览器支持）。\n\n#### 模板提取方式\n\n好吧，上面三小节基本是在吹水，其实模板本身放哪里跟模板引擎的关系并不太大，模板引擎只要接受模板字符串进行处理就好了。但说回来，如果模板引擎能辅助用户进行模板的自动提取，则无疑是在易用性上的一个很好的亮点。\n\n目前jQuery tmpl和artTemplate都做了这些方面的努力。在jQuery tmpl中，直接在（经jQuery包裹后的）包含模板的元素上调用`.tmpl`方法即可，jQuery tmpl会自动去页面中提取出模板字符串并进行处理：\n\n```javascript\n$('#myTmpl').tmpl(myData);\n```\n\n而artTemplate允许通过指定包含模板的元素ID的方式自动提取模板：\n\n```javascript\ntemplate.render('myTmpl',myData);\n```\n\n## 性能\n\n任何JS库、框架都逃不开性能这个命题，模板引擎自然也不例外。但模板引擎的性能一直是一个争议不断的话题。\n\n### 性能是否是伪命题\n\n我们来看一个性能测试的页面\u003Chttp:\u002F\u002Faui.github.io\u002FartTemplate\u002Ftest\u002Ftest-speed.html>，其实结果很有意思，看完才知道，原来不同的模板引擎之间的性能差异真的如此巨大！\n\n不过更有意思的是，测试完成后，作者写了一句话：“测试已完成，请不要迷恋速度。”这其实是句很有意思的话，之前也有跟作者聊过性能方面的问题，作者也认为“性能是个伪命题”的观点。原因是这个测试的数据量是100*10000=100W。也即在百万级别的数据渲染时，才有几秒钟的性能差异。\n\n回到我们的日常web开发，单次渲染数据量上千就已经差不多是极限了，此时模板引擎之间的性能差异微乎其微，几乎无法被用户感知。\n\n另外还有一个特别值得注意的地方，模板引擎做的工作只是“模板+数据=包含数据的HTML字符串”，而这些HTML字符串真正显示在页面上还要经过一道DOM操作，而这个DOM操作在数据量大时比模板引擎本身的计算所要消耗的时间要大得多！如果100W条数据显示在DOM上，我觉得即使是Chrome也很难逃脱卡死的命运。\n\n这样看来，模板引擎的性能问题还真可能是个伪命题。那是否可以忽略这一问题呢？我认为也不可取，模板引擎还是要关注性能的，因为你不知道用户会怎么使用你的引擎。比如我就曾经为了偷懒，在一个页面上渲染了6000条数据，此时模板引擎在IE下的表现也可以差到用户可感知的程度。\n\n### 有关预编译\n\n在[《高性能JavaScript模板引擎原理解析》](http:\u002F\u002Fcdc.tencent.com\u002F?p=5723)一文中，artTemplate作者解释了其高性能来源于针对模板的预编译。即在首次渲染时会根据模板解析的结果生成一个用于拼接数据的函数，后续调用时直接使用这个函数而不需要再次解析模板。这是个非常好的思路，目前也有越来越多的模板引擎采用了预编译的方式，所以我猜测一下，上面的测试如果换用最新版、并引入更多的模板引擎再做一次的话，差异应该不会那么明显。\n\n关于预编译的时机，目前的模板引擎基本都放在浏览器端首次调用时进行预编译。artTemplate是我见到的第一个尝试把模板预编译过程放在构建阶段的模板引擎，它可以在发布前对产品中用到的模板进行预处理，最后发出去的直接是拼接字符串的函数。详细详情可以在[这里](https:\u002F\u002Fgithub.com\u002Fcdc-im\u002Fatc)查看。\n\n这其实是个很不错的思路，尤其是在前端编译的概念有燎原之势的这个时机，只要集成方便，还是很有前途的。不过略遗憾的是，目前artTemplate的预编译工具还没有提供Grunt.js的插件，需要单独编译。\n\n顺序发散一下，如果我们在引入jQuery的项目发布前，扫描一下调用的API，然后把jQuery也在发布前预编译一遍会怎样？其实想象空间还挺大的。\n\n## 功能\n\n前面已经说过，一个模板引擎最主要的功能就是实现数据和逻辑、结构的分离，简化开发工作。因此一些剩下的功能就被放到了最后，这些并不是模板引擎的主要考量因素，但如果做好，也会成为很不错的亮点。\n\n### 异常处理\n\n模板引擎都有一套特定的语法，当我们写的模板无法按照这套语法进行解析或者是在处理数据时发生错误时便会产生错误。有一部分模板引擎在产生错误时不会做过多的处理，此时错误被直接抛出，但产生错误的代码行却往往是模板引擎本身，一定程度上会给调试带来困难。\n\nartTemplate在错误处理上有一些尝试，出错时会带上出错的模板本身，这样开发者可以定位到出错的模板代码。而jade则更进一步，直接能定位到出错的位置所在的字符。\n\n当然，这些辅助信息并不一定100%准确，因为很可能错误产生于真正报错之前，这是几乎所有的语言调试过程中面临的一个问题。不过，有了这些辅助信息还是能提升开发者的调试效率的。\n\n### 包含、mixin复用\n\n在后端模板的世界中，模板之间的相互包含是一件非常普遍的事情，因为后端模板往往是以页为单位来进行输出渲染，而相对来讲，前端模板的应用场景则会显示更碎片化。因此个人觉得前端模板中的模板相互包含、引用并不是一个非常普遍的需求。不过，也有不少的模板为了与在后端（Node.js）的使用方式保持一致，也为前端提供了模板包含功能。\n\n除了使用包含来完成模板复用外，jade还提供了一种叫作`mixin`的方式来复用模板：\n\n```jade\nmixin article(title)\n  .article\n    .article-wrapper\n      h1= title\n      if block\n        block\n      else\n        p No content provided\n\n+article('Hello world')\n\n+article('Hello world')\n  p This is my\n  p Amazing article\n```\n\n会编译成：\n\n```html\n\u003Cdiv class=\"article\">\n  \u003Cdiv class=\"article-wrapper\">\n    \u003Ch1>Hello world\u003C\u002Fh1>\n    \u003Cp>No content provided\u003C\u002Fp>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\n\u003Cdiv class=\"article\">\n  \u003Cdiv class=\"article-wrapper\">\n    \u003Ch1>Hello world\u003C\u002Fh1>\n    \u003Cp>This is my\u003C\u002Fp>\n    \u003Cp>Amazing article\u003C\u002Fp>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\n```\n\n这种方式其实已经非常接近[Shadow DOM](https:\u002F\u002Fwww.toobug.net\u002Farticle\u002Fwhat_is_shadow_dom.html)中对DOM的封装思路。（当然，如果你玩过LESS、SASS之类的CSS预处理语言，也会觉得这个mixin的概念似曾相识。）\n\n个人以为，相对模板包含而言，这种mixin的复用方式其实是对前端模板而言更为友好的方式。\n\n## 结\n\n洋洋洒洒地码了这么多，其实仍然没有去讲怎么设计一个模板引擎，只是提出了自己使用模板引擎过程中看到的一些差异和一些值得思考的点。真正设计一个工业级的模板引擎还是颇费工夫的。\n\n至于模板引擎的实现，则完全是编码的硬功夫了，除了编译原理外也没啥好说的了，就不打算说了。（装下B，真实原因是我也没写过……Wahaha……）\n\n> 2013-12-04：感谢Barret Lee在评论中给出一篇好文[《javascript模板引擎原理，几行代码的事儿》](http:\u002F\u002Fbarretlee.com\u002Fprinciple-of-javascript-template\u002F)，这是一篇真正在讲如何用js实现一个模板引擎的文章，推荐阅读。\n","\u002Farticle\u002Fhow_to_design_front_end_template_engine.html",{"id":120,"type":7,"slug":121,"title":122,"date":123,"category":11,"tags":124,"body_markdown":126,"permalink":20,"excerpt_src":20,"media_type":20,"media_title":20,"media_author":20,"media_url":20,"rating":20,"layout":20,"pv":21,"admin_only":21,"created_at":22,"updated_at":22,"deleted_at":20,"status":23,"cover":24},159,"why-browsers-not-build-more-selectors","为什么浏览器不内建更多选择器","2012-09-06 09:27",[125,13],"浏览器","\n来自知乎问题[JavaScript 为什么不内建选择器？](https:\u002F\u002Fwww.zhihu.com\u002Fquestion\u002F20433809\u002Fanswer\u002F15189900)\n\n首先，与这个问题有关的浏览器中的技术概念其实大致有这些：\n\n1. BOM，即浏览器对象模型，简单说就是浏览器提供的环境，包括各种对象及其方法，比如window\n2. DOM，即文档对象模型，它是由HTML经解析后生成的一个模型，我们对页面可视部分进行操作实际上就是在操作DOM\n3. JavaScript，浏览器提供了BOM，也解析了DOM，那么，开发者如何去操作它们呢？这时候就轮到JavaScript出场了，所以JavaScript（在这个例子中）是用来操作DOM和BOM的\n\n那么所谓的选择器，`getElement(s)ByXXXX`实际上是由DOM提供的，而且对DOM的level实现的程度不一样，这些方法也不一样，比如DOM level 0中主要的方法（确切说是属性）就是`document.images`，`document.links`之类。而DOM level 3中则提供了`querySelector(All)`之类更为方便的API。\n\n\u003C!-- more -->\n\n第二个想说的点是关于为什么没有更精简的API和写法：\n\n1. 在DOM中，所有的方法必须隶属于DOM对象，所以在非别名实现的情况下，以`$`函数来实现是不可能的，即使实现，也会是类似`document.$`之类的方法\n2. 假设使用别名方法实现（即`window.$ = document.$`），那么会有一个很严重的后果——染污全局空间，即jQuery（或者prototype或者其它的任何类库）再也不能使用`$`函数\n3. 即使不纠结是否别名实现的问题，假设可以用`$classname`这样的函数来选择，那么也是有问题的。第一，方法名字不够直观，作为一个浏览器（确切地说，是一份标准），引入一个名称含义不确定的方法是不可以接受的，虽然`getElementById`很长，但是任何人都能一眼看明白它是做什么的，对不对？第二，使用`$`开头似乎不符合JavaScript中的要求，`$`开头的应该是留给机器编译后的代码来避免冲突。（这一点是听说的，我没有自己去看规范。）\n\n第三个想说的是关于jQuery（及风格类似）的选择器。事实上jQuery的选择器的确很方便，但它也引起了很多的争议，包括占用$符号和承载太多重载的jQuery方法（即$），可能跟jQuery的理念有关，“write less, do more”，而一份web标准因为它强大的影响力和不可随意更新（bug就只能bug了，在下一份标准出来前不可能有修复的机会）的特性，显然还要在API设计的方方面面考虑更多的东西。\n",{"id":128,"type":7,"slug":129,"title":130,"date":131,"category":11,"tags":132,"body_markdown":134,"permalink":135,"excerpt_src":20,"media_type":20,"media_title":20,"media_author":20,"media_url":20,"rating":20,"layout":20,"pv":21,"admin_only":21,"created_at":22,"updated_at":22,"deleted_at":20,"status":23,"cover":136},158,"what_is_shadow_dom","[译]什么是Shadow Dom？","2012-06-07 18:24",[133],"ShadowDOM","\n如果你做过网站，那么很可能你已经用过一些JavaScript类库。既然如此，你可能会对这些不知名的类库作者心存感激。\n\n这些作者——web开发领域的勇士们——都面对着同样的一个问题——封装。他们会花大量的精力在面向对象的经典问题之一上面，即如何封装自己的代码，以便与类库使用者的代码分离。\n\n除了SVG，现在的Web平台只提供了一种原生的方法去隔离代码块，这并不优雅。没错，我说的就是iframe。对大部分需要封装的场景来说，frames太重而且限制太多。\n\n如果我需要把每个自定义的按钮都放到iframe里，你是什么感觉，会不会疯掉？\n\n所以，我们需要一些更好的东西。事实上，大部分的浏览器已经变相地提供了一种强大技术去隐藏一些实现细节。这个技术就是所谓的“shadow DOM”。\n\n\u003C!-- more -->\n\n### 我的名字是DOM，Shadow DOM\n\nShadow DOM是指浏览器的一种能力，它允许在文档（`document`）渲染时插入一棵DOM元素子树，但是这棵子树不在主DOM树中。看一个简单的slider：\n\n```html\n\u003Cinput id=\"foo\" type=\"range\"\u002F>\n```\n\n把这段代码放到webkit内核的浏览器中，它会这样显示：\n\n![slider](\u002Fassets\u002Fwhat_is_shadow_dom\u002F1.png)\n\n很简单吧，这里有一个滑槽，还有一个滑块可以沿滑槽滑动。\n\n嗯。一切看起来都那么美好，喝杯咖啡先……等下等下，这里居然有一个可以在`input`元素中滑动的元素！为什么我不能通过JavaScript看到它？\n\n```javascript\nvar slider = document.getElementsById(\"foo\");\nconsole.log(slider.firstChild); \u002F\u002F 返回 null\n```\n\n这是一种魔法么？\n\n我的观点来看，不是。这只是shadow DOM在起作用。你看，浏览器的开发者们已经意识到了手工编写这些DOM元素的表现和行为很困难而且很SB。所以，从一定程度上讲，他们骗了我们，给了我们一个输入框，但拥有比输入框更多的功能。\n\n他们为你——web开发者设定了一个边界，界定了哪些是你可以访问的，哪些实现细节是访问不到的。然而，浏览器本身却可以随意跨越这个边界。设置这样一个边界之后，它们就可以在你看不见的地方使用熟悉的web技术、同样的HTML元素去创建更多的功能，而不是像你一样要在页面上用div和span来堆。\n\n有一些很简单，就像上面说的slider。而有一些却相当复杂。我们来看一下`video`元素，它有一些按钮、进度条、hover态的音量控制，像这样：\n\n![slider](\u002Fassets\u002Fwhat_is_shadow_dom\u002F2.png)\n\n所有的这一切都只是HTML和CSS——但是是隐藏在shadow DOM子树中的。\n\n借用XXX的一首诗，“它是怎样工作的？”为了直观一些，我们假装可以用JavaScript操作它。看这个简单的页面：\n\n```html\n\u003Chtml>\n\u003Chead>\n\u003Cstyle> p { color: Green; } \u003C\u002Fstyle>\n\u003C\u002Fhead>\n\u003Cbody>\n\u003Cp>My Future is so bright\u003C\u002Fp>\n\u003Cdiv id=\"foo\">\u003C\u002Fdiv>\n\u003Cscript>\n    var foo = document.getElementById('foo');\n    \u002F\u002F 注意：这里只是模拟，不是真实的API\n    foo.shadow = document.createElement('p');\n    foo.shadow.textContent = 'I gotta wear shades';\n\u003C\u002Fscript>\n\u003C\u002Fbody>\n\u003C\u002Fhtml>\n```\n\n我们获得了一个这样的DOM树：\n\n```html\n\u003Cp>My Future is so bright\u003C\u002Fp>\n\u003Cdiv id=\"foo\">\u003C\u002Fdiv>\n```\n\n但是它像是被这样渲染出来的：\n\n```html\n\u003Cp>My Future is so bright\u003C\u002Fp>\n\u003Cdiv id=\"foo\"> \u003C!-- shadow subtree begins -->\n    \u003Cp>I gotta wear shades\u003C\u002Fp>\n\u003C\u002Fdiv> \u003C!-- shadow subtree ends -->\n```\n\n看起来是这样：\n\n![slider](\u002Fassets\u002Fwhat_is_shadow_dom\u002F3.png)\n\n注意一下，为什么渲染的句子的第二部分不是绿色的？这是因为文档（document）中选择器p不能获取到shadown DOM。很酷对不对？！如果一个框架开发者被赋予这样的能力会怎么样？想象一下你只需要写你的widget，而不用担心被不知哪里蹦出来的选择器愚弄……简直令人陶醉。\n\n### 事件的情况\n\n为了保持自然，shadow DOM子树中的事件可以在文档（document）中被监听。比如，你点击一下`audio`元素中的静音按钮，你可以在一个包裹它的div中监听到这个事件。\n\n```html\n\u003Cdiv onclick=\"alert('who dat?')\">\n    \u003Caudio controls src=\"test.wav\">\u003C\u002Faudio>\n\u003C\u002Fdiv>\n```\n\n但是，如果你要确认事件的来源，会发现它是audio元素，而不是它内部的按钮。\n\n```html\n\u003Cdiv onclick=\"alert('fired by:' + event.target)\">\n    \u003Caudio controls src=\"test.wav\">\u003C\u002Faudio>\n\u003C\u002Fdiv>\n```\n\n为什么这样？因为当事件穿过shadown DOM边界的时候，会被重新设定`target`，以避免暴露shadow DOM子树内部结构。用这种方式，你可以监听到从shadow DOM中产生的事件，而实现者也可以继续隐藏细节。\n\n### 通过CSS访问（Reaching into）Shadow\n\n另一个需要提到的技巧是怎样通过CSS来访问shadow DOM子树。假设我想自定义我的slider。我想让它有一些样式，而不是系统原生的那样，像这样：\n\n```css\ninput[type=range].custom {\n    -webkit-appearance: none;\n    background-color: Red;\n    width: 200px;\n}\n```\n\n结果如下：\n\n![slider](\u002Fassets\u002Fwhat_is_shadow_dom\u002F4.png)\n\n很好，但是我怎样定义滑块的样式呢？我们已经知道，常规的CSS选择器并不能获取到shadow DOM子树。但事实上，这里有一些很方便的伪元素，可以取到shadow DOM子树中的元素。例如，slider中的滑块在webkit中可以这样访问：\n\n```css\ninput[type=range].custom::-webkit-slider-thumb {\n    -webkit-appearance: none;\n    background-color: Green;\n    opacity: 0.5;\n    width: 10px;\n    height: 40px;\n}\n```\n\n样子如下：\n\n![slider](\u002Fassets\u002Fwhat_is_shadow_dom\u002F5.png)\n\n很完美对不对？想想看，你可以为shadow DOM子树中的元素赋予样式，而不需要真的访问到这些元素。而这些shadow DOM的作者有了决定哪些部分可以被赋予样式的权利。如果你是作者，在做一些UI widget toolkit的时候，难道不想有这样的能力吗？\n\n### 带有洞（hole）的Shadow DOM，无穷的想象力\n\n讲完了这些令人惊叹的能力，我们想象一样，如果给一个有shadown DOM子树的元素插入子元素会怎样？我们来实验一下：\n\n```javascript\n\u002F\u002F Create an element with a shadow DOM subtree.\nvar input = document.body.appendChild(document.createElement('input'));\n\u002F\u002F Add a child to it.\nvar test = input.appendChild(document.createElement('p'));\n\u002F\u002F .. with some text.\ntest.textContent = 'Team Edward';\n```\n\n结果如下：\n\n![slider](\u002Fassets\u002Fwhat_is_shadow_dom\u002F6.png)\n\n哇！欢迎来到twilight DOM的世界！它是文档（document）的一部分，可以被遍历到，但是不会渲染！它是不是很有用呢？不一定，但是如果你需要的话它确实就在那等你。\n\n但是，如果我们真的有能力把元素的子元素放入shadow DOM子树中会怎么样？想象一下shadow DOM是一个模板，通过它的某个洞（hole）可以看到内部的子元素：\n\n```javascript\n\u002F\u002F 注意：这里只是模拟，不是真实的API\nvar element = document.getElementById('element');\n\u002F\u002F 创建shadow DOM子树\nelement.shadow = document.createElement('div');\nelement.shadow.innerHTML = '\u003Ch1>Think of the Children\u003C\u002Fh1>' +\n    '\u003Cdiv class=\"children\">{{children-go-here}}\u003C\u002Fdiv>';\n\u002F\u002F Now add some children.\nvar test = element.appendChild(document.createElement('p'));\ntest.textContent = 'I see the light!';\n```\n\n如果你去遍历DOM，你会看到这个：\n\n```html\n\u003Cdiv id=\"element\">\n    \u003Cp>I see the light\u003C\u002Fp>\n\u003C\u002Fdiv>\n```\n\n但是像是这样渲染出来的：\n\n```html\n\u003Cdiv id=\"element\">\n    \u003Cdiv> \u003C!-- shadow tree begins -->\n        \u003Ch1>Think of the Children\u003C\u002Fh1>\n        \u003Cdiv class=\"children\"> \u003C!-- shadow tree hole begins -->\n            \u003Cp>I see the light\u003C\u002Fp>\n        \u003C\u002Fdiv> \u003C!-- shadow tree hole ends -->\n    \u003C\u002Fdiv> \u003C!-- shadow tree ends -->\n\u003C\u002Fdiv>\n```\n\n当你添加子元素的时候，从DOM树中看像一个正常的子元素，但是渲染的时候，他们从“洞（hole）”中进到了shadow DOM子树。\n\n写到这里，你应该会承认，这真的很酷，也会问：\n\n浏览器中什么时候才会有呢？\n\n### 家庭作业\n\n你认为听完了这么多说教的内容会没有家庭作业？作为一个JavaScript类库或者框架的开发者，尝试者去想象一下你可以利用shadow DOM制作的跟之前不一样的伟大的东西。然后想一下shadow DOM可以应用到的一些特定的使用场景（加上真实的或者模拟的代码）。\n\n最后，共享你想到的使用场景到public-webapps邮件列表。关于在web平台中加入这种能力的讨论正在进行，我们需要你的帮助。\n\n如果你不是一个框架作者，你仍然可以参与进来，你可以给shadown DOM加油，也可以将这份快乐传播到你最喜欢的社交网络上，因为快乐就是我们工作的全部。\n\n附：SVG和shadow DOM\n\n差点忘了，至于你信不信，我反正信了，SVG确实已经用到了shadow DOM，从一开始就是这样。但是比较麻烦的是，SVG的shadow DOM非常……非常……水（shady），不不，不是这个词，是另一个词，以sh开头，以y结尾。（译注：对英文语境不是太熟悉，评论中有人提到是shy。）对对，就是它！我可以继续说，但是请相信我对SVG shadow DOM的评价。或者你可以查看文档。\n\n原文地址：\u003Chttp:\u002F\u002Fglazkov.com\u002F2011\u002F01\u002F14\u002Fwhat-the-heck-is-shadow-dom\u002F>\n\n译者注：应该说这是一篇关于前端理论的文章，并不能给前端开发人员带来什么便利，至少短期内没有，但是了解这种技术对我们从事web相关的软件开发会有好处，而且指不定哪天这种开发模式就出现了呢！\n\n> 2013年5月24日更新：修正了一下之前翻译不通的地方。另外Chrome 23+已经实现了Shadow Dom，Chrome 25+已经默认打开了Shadow Dom的使用选项。因此，现在可以有一些更具体的案例可以看了，比如\u003Chttps:\u002F\u002Fgithub.com\u002FTooooBug\u002FshadowDatePicker>。\n","\u002Farticle\u002Fwhat_is_shadow_dom.html","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fwhat_is_shadow_dom\u002F1.png",46]