<?xml version="1.0" encoding="utf-8" standalone="yes"?><?xml-stylesheet type="text/css" href="https://imnerd.org/css/rss.css"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>JavaScript on 怡红院落</title><link>https://imnerd.org/tags/javascript.html</link><description>Recent content in JavaScript on 怡红院落</description><language>zh-CN</language><copyright>© 2021</copyright><lastBuildDate>Mon, 07 Feb 2022 00:00:00 +0800</lastBuildDate><atom:link href="https://imnerd.org/tags/javascript/index.xml" rel="self" type="application/rss+xml"/><item><title>断点调试之压缩造成的血案</title><link>https://imnerd.org/debug-a-compress-problem-using-by-breakpoint.html</link><pubDate>2022-12-07</pubDate><guid>https://imnerd.org/debug-a-compress-problem-using-by-breakpoint.html</guid><description>前段时间组里的小伙伴让我帮忙排查一个线上问题，我觉得排查流程比较有意思，想着记录一下看看是否能对其它同学有所帮助，遂有此文。
事情的起因是前几天线上突然收到一个报警，错误内容是 TypeError: C.fn is not a function。相关同学尝试排查无果后又回滚了最近上线的变更也没有排查到问题。虽然最终确认了复现路径，但是在本地却无法复现。
🔍 初步排查 在线上复现该错误后，点击错误堆栈的文件跳转，快速定位到线上出错的代码。由于线上都是压缩过的代码，这里我们可以点击左下角的 {} 进行代码美化。
经过美化后我们可以看出来，应该就是 189624 行出了问题。我们直接尝试在这一行上打断点，之后会发现代码会在这块疯狂打转。这是因为它处于一个 for 循环中。仔细观察不难看出代码其实上是 this.head 这个链的递归执行，每次执行完当前 C 都会被赋值成链的下一个值，并执行该值对应的 fn() 方法。也就是问题是这个链上的某个值没有 fn() 方法，最终导致了这个报错。
大概确认问题后，我们需要看一下最终这个 C 的值是什么。由于处在循环当中，一次一次的点击下一步实在是麻烦。由于我们有明确的目标，所以我们可以尝试添加条件断点，让只有符合我们条件的断点才停下来，否则都忽略正常执行。
在 189624 行右键点击 Add conditional breakpoint&amp;hellip; 选项，并输入 typeof C.fn !== 'function' 作为条件表达式。这样我们就实现了一个仅在 C.fn 不是一个方法的时候才会触发的条件断点。
条件断点触发后，我们可以在控制台中基于断点时的上下文输出变量进行调试。可以从下左图我们可以清晰的看到，此时的 C.fn 的确是不存在的。
由于刚才我们已知 this.head 应该是一条链，依次执行链上的方法。所以理论上来说链上的每个元素都是一样的。于是乎我就尝试输出了 this.head 链上所有的元素想看一下这个链到底是什么样子的。模拟代码里的循环我也在控制台尝试写了下，发现输出的结果如下左图展示。在链的最后一个元素就是我们有问题的元素。
而之前我们已知的是在本地开发环境是无法复现这个问题的，所以我照猫画虎在本地同样的位置也输出了一下 this.head 链，结果见上右图。发现和线上输出的，除了最后这个有问题的元素，其它的输出基本是一样的。
看来问题的原因就在于线上的代码执行在链上增加了这么一个玩意导致的，而本地由于没有这个多余的元素所以没有触发问题。
🐞 确认问题 找到原因后我就想着从代码层面捋一下是哪里给增加了这么个玩意。由于之前的代码中可以明显的看到 i.prototype.finish 的字样，初步猜测这应该是一个类的定义。于是乎就想看看这个类是在哪里实例化执行的。
通过刚报错时的压缩后的代码，我们可以看到报错的模块是”protobuf.js“这个模块。于是乎我在项目和依赖中查找是哪个模块依赖了它，最终查到了是我们内部使用的一个 IM 消息模块有用到。
之后在具体的依赖模块中搜索 .finish() 相关字样，查到了最终的调用在如下地方。serialize() 方法会调用 Request.encode() 方法，它返回一个 $Writer 基类的实例，而 $Writer 就是 protobuf.js 模块中的 Writer 基类。Request.encode() 方法实例化完 Writer 基类后会执行一系列的成员函数，执行完毕后会返回 Writer 实例，并调用它的 finish() 方法。
了解执行流程之后，我就顺着 Request.encode(req).finish() 这一句开始向上对 Request.encode() 方法进行断点（下左图）。如下图先尝试在末尾断点输出 o.head（o 是压缩后指向 Writer 实例的变量），发现此时已经存在异常链元素了（下右图）。 中间的代码稍微打了下断点发现也依旧如此。最终在头部断点处发现了端倪。尝试在开头增加断电之后，发现在 120274 行执行完毕之后 o.head 链上就已经存在了异常数据了。
那我们尝试翻看下代码看一下 o.create() 方法具体干了什么。从下图左我们可以看到 Writer.create() 本质其实就是 Writer 基类的实例化工厂方法。而下图中可以看到 Writer 的构造方法对一些成员属性赋了初值。其中关键的 this.head 的初值是一个 Op 基类的实例。下图右可以看到 Op 基类的构造方法中也是赋了一些初值。同时我们可以看到 function noop() {} 实际上就是一个空方法。也就是说 this.head 默认指向了一个空方法实例化的 Op 对象。
乍一看整个流程其实非常简单，本质上构造函数内都是一些简单的赋值操作，不会有什么问题。于是乎还是按照链路依次向上排查问题。因为上一趴我们排查到执行完 Writer.create() 工厂方法后就有问题了，所以这里我们需要对 Writer 的构造函数进行断点排查。
尝试如下图在构造方法末尾断点后，输出 this.head 链，发现此时已经有异常数据了。而这个时候不过只是做了初值的操作而已，这怎么就能出问题了呢？由于断点情况下我能在当前上下文中进行调试，所以此时我尝试自己执行一下 Op 基类的实例化操作（见下图）。这时候发现确实它的 next 属性不对，是我们要找的问题元素！
此时此刻，我感觉我们已经越来越接近真相了！
如下图左我们在 f 变量上 hover 一会儿，会出现它的定义处链接，点击后会直接跳转到它的定义处下图右（其实就离的不太远）。
大家可能也都注意到了，我们刚才看的代码中 this.next 明明是定义成 undefined 怎么这里给定义成 g 了？而这个 g 又对上了 189456 行 g = s.base64，所以我们才看到 this.head.next 的值这么奇怪。而我们尝试看一下引用的 protobuf.js 代码，发现代码里 this.next 虽然是等于 g 但是它并没有关联到 u.base64 上。
由于我之前有解决过一些压缩再压缩后代码异常的 Case，所以至此我基本上可以断定，由于 protobuf.js 在我们的依赖中是引入的压缩后的代码，而压缩后的代码再走压缩导致了变量指向出现错乱从而导致的问题。这也侧面印证了为什么只有线上可以，本地无法复现的原因。因为本地是没有走压缩的。
🛠 如何解决 找到问题后有两种解决方法。一是正向的去查找压缩工具造成这个问题的原因；二是反向的去规避该问题，我们不引入压缩后的代码而是正常引入未压缩的代码，最终统一由项目进行压缩处理。
这两种方法都能解决问题。而第一种需要的时间会比较久，所以我们先采用了第二种方法临时解决一下。由于该依赖包不是我们维护的，我们只能使用 patch-package 给模块打补丁的方式进行修复。它的功能是在安装完依赖后会根据我们的 diff 文件对依赖进行修改。
这里我们的修改比较简单，找到我们依赖模块引入 protobuf.min.js 的地方，将其修改成 protobuf.js 即可。
🗒 后记 undefined 在压缩后就变成了 g 这个初步猜想应该是本地想要定义一个没有定义的变量，这样就是 undefined 了。我尝试克隆了下 protobuf.js 仓库进行了尝试，发现应该是 UglifyJS 中配置了 marguel.eval 导致有这个特性。
以上就是压缩造成的血案完整的排查经过，整个的过程总结一下有以下几个经验可以供大家参考：
除了单步断点，我们还有条件断点、日志断点等多种断点方式帮助我们排查问题，合理使用会加速我们排查问题的速度。 断点后当前 JS 环境会停留在当时的上下文中，我们可以在控制台执行、输出我们想要的当时环境的数据帮助排查。 控制台中我们也可以 hover 查看定义位置，进行定义间快速跳转。 压缩后的代码不可怕，我们可以通过源码对比，无法压缩的关键字进行定位查找。 只要是可以复现的问题，那都不是问题！ 最后祝大家开工大吉，新的一年没有 Bug！</description></item><item><title>清除 useEffect 副作用</title><link>https://imnerd.org/clean-up-useeffect-side-effect.html</link><pubDate>2022-10-23</pubDate><guid>https://imnerd.org/clean-up-useeffect-side-effect.html</guid><description>在 React 组件中，我们会在 useEffect() 中执行方法，并返回一个函数用于清除它带来的副作用影响。以下是我们业务中的一个场景，该自定义 Hooks 用于每隔 2s 调用接口更新数据。
import { useState, useEffect } from &amp;#39;react&amp;#39;; export function useFetchDataInterval(fetchData) { const [list, setList] = useState([]); useEffect(() =&amp;gt; { const id = setInterval(async () =&amp;gt; { const data = await fetchData(); setList(list =&amp;gt; list.concat(data)); }, 2000); return () =&amp;gt; clearInterval(id); }, [fetchData]); return list; } 🐚 问题 该方法的问题在于没有考虑到 fetchData() 方法的执行时间，如果它的执行时间超过 2s 的话，那就会造成轮询任务的堆积。而且后续也有需求把这个定时时间动态化，由服务端下发间隔时间，降低服务端压力。
所以这里我们可以考虑使用 setTimeout 来替换 setInterval。由于每次都是上一次请求完成之后再设置延迟时间，确保了他们不会堆积。以下是修改后的代码。
import { useState, useEffect } from &amp;#39;react&amp;#39;; export function useFetchDataInterval(fetchData) { const [list, setList] = useState([]); useEffect(() =&amp;gt; { let id; async function getList() { const data = await fetchData(); setList(list =&amp;gt; list.concat(data)); id = setTimeout(getList, 2000); } getList(); return () =&amp;gt; clearTimeout(id); }, [fetchData]); return list; } 不过改成 setTimeout 之后会引来新的问题。由于下一次的 setTimeout 执行需要等待 fetchData() 完成之后才会执行。如果在 fetchData() 还没有结束的时候我们就卸载组件的话，此时 clearTimeout() 只能无意义的清除当前执行时的回调，fetchData() 后调用 getList() 创建的新的延迟回调还是会继续执行。
在线示例：CodeSandbox
可以看到在点击按钮隐藏组件之后，接口请求次数还是在继续增加着。那么要如何解决这个问题？以下提供了几种解决方案。
🌟如何解决 { async function getList() { id = setTimeout(async () = { const data = await fetchData(); setList(list = list.concat(data)); getList(); }, 2000); } getList(); return () = clearTimeout(id); }); return list; } ``` -- 🐋 Promise Effect 该问题的原因是 Promise 执行过程中，无法取消后续还没有定义的 setTimeout() 导致的。所以最开始想到的就是我们不应该直接对 timeoutID 进行记录，而是应该向上记录整个逻辑的 Promise 对象。当 Promise 执行完成之后我们再清除 timeout，保证我们每次都能确切的清除掉任务。
在线示例：CodeSandbox
import { useState, useEffect } from &amp;#39;react&amp;#39;; export function useFetchDataInterval(fetchData) { const [list, setList] = useState([]); useEffect(() =&amp;gt; { let getListPromise; async function getList() { const data = await fetchData(); setList((list) =&amp;gt; list.concat(data)); return setTimeout(() =&amp;gt; { getListPromise = getList(); }, 2000); } getListPromise = getList(); return () =&amp;gt; { getListPromise.then((id) =&amp;gt; clearTimeout(id)); }; }, [fetchData]); return list; } 🐳 AbortController 上面的方案能比较好的解决问题，但是在组件卸载的时候 Promise 任务还在执行，会造成资源的浪费。其实我们换个思路想一下，Promise 异步请求对于组件来说应该也是副作用，也是需要”清除“的。只要清除了 Promise 任务，后续的流程自然不会执行，就不会有这个问题了。
清除 Promise 目前可以利用 AbortController 来实现，我们通过在卸载回调中执行 controller.abort() 方法，最终让代码走到 Reject 逻辑中，阻止了后续的代码执行。
在线示例：CodeSandbox
import { useState, useEffect } from &amp;#39;react&amp;#39;; function fetchDataWithAbort({ fetchData, signal }) { if (signal.aborted) { return Promise.reject(&amp;#34;aborted&amp;#34;); } return new Promise((resolve, reject) =&amp;gt; { fetchData().then(resolve, reject); signal.addEventListener(&amp;#34;aborted&amp;#34;, () =&amp;gt; { reject(&amp;#34;aborted&amp;#34;); }); }); } function useFetchDataInterval(fetchData) { const [list, setList] = useState([]); useEffect(() =&amp;gt; { let id; const controller = new AbortController(); async function getList() { try { const data = await fetchDataWithAbort({ fetchData, signal: controller.signal }); setList(list =&amp;gt; list.concat(data)); id = setTimeout(getList, 2000); } catch(e) { console.error(e); } } getList(); return () =&amp;gt; { clearTimeout(id); controller.abort(); }; }, [fetchData]); return list; } 🐬 状态标记 上面一种方案，我们的本质是让异步请求抛错，中断了后续代码的执行。那是不是我设置一个标记变量，标记是非卸载状态才执行后续的逻辑也可以呢？所以该方案应运而生。
定义了一个 unmounted 变量，如果在卸载回调中标记其为 true。在异步任务后判断如果 unmounted === true 的话就不走后续的逻辑来实现类似的效果。
在线示例：CodeSandbox
import { useState, useEffect } from &amp;#39;react&amp;#39;; export function useFetchDataInterval(fetchData) { const [list, setList] = useState([]); useEffect(() =&amp;gt; { let id; let unmounted; async function getList() { const data = await fetchData(); if(unmounted) { return; } setList(list =&amp;gt; list.concat(data)); id = setTimeout(getList, 2000); } getList(); return () =&amp;gt; { unmounted = true; clearTimeout(id); } }, [fetchData]); return list; } 🎃 后记 问题的本质是一个长时间的异步任务在过程中的时候组件卸载后如何清除后续的副作用。
这个其实不仅仅局限在本文的 Case 中，我们大家平常经常写的在 useEffect 中请求接口，返回后更新 State 的逻辑也会存在类似的问题。
只是由于在一个已卸载组件中 setState 并没有什么效果，在用户层面无感知。而且 React 会帮助我们识别该场景，如果已卸载组件再做 setState 操作的话，会有 Warning 提示。
再加上一般异步请求都比较快，所以大家也不会注意到这个问题。
所以大家还有什么其他的解决方法解决这个问题吗？欢迎评论留言~
注： 题图来自《How To Call Web APIs with the useEffect Hook in React》</description></item><item><title>记一次换行引发的血案</title><link>https://imnerd.org/jsonp-lf-bug.html</link><pubDate>2018-10-16</pubDate><guid>https://imnerd.org/jsonp-lf-bug.html</guid><description>话说最近真是流年不利，感觉各种BUG犹如天灾一样全部冒出来了，这不昨天又解了一个非常无语的问题，大概是关于换行和正则的臭虫，下面给大家吐槽一下。
数据“野”了 昨天同事反馈某个页面的数据没有正常显示，最开始我还以为是接口没有返回数据，结果看了下请求发现接口有正常的数据呀。没办法就一路反查回去，最后查到居然代码里接口请求抛错了？！因为定义了 Promise 的 catch 流程，所以也没有把错误抛出来。因为之前这个页面都是正常的，很久都没有改动所以我第一反应是这个数据异常了，查了半天的数据格式问题。可是问题就在于明明看到数据是正常的呀，服务端没有报错，接口数据也是可以正常解析的。最后我突然想起来，我们的接口是 JSONP 的会不会是 JSONP 功能挂了？查了一下果然是这样。
我们都知道 JSONP 需要定义一个 callback 回调名称，最后数据加载的时候执行这个回调返回数据才算成功。可以看到图里面虽然加了 callback=? 但是并没有对 ? 进行替换，服务端那边应该也是增加了校验这种情况下并没有给我们添加回调方法名。实际上服务端这里的处理都是没问题的，因为即使 ? 被添加到数据里了也会有问题，因为 ? 是逻辑运算符并不能被定义成变量。而没有加回调方法的后果就是虽然接口返回正常，但是最终数据没有人接收就“野”了。
我的回调呢？ 我们在内部是使用了 zepto 这个基础库的。由于添加回调方法名并做替换肯定是 zepto 内部的行为，所以当我查到这的时候我都懵了。难道一年难遇一次国际知名库的 BUG 就这样被我水逆的碰到了？怀着一脸惊讶的表情我打开了 zepto 的文件继续查了下去。最终我发现还真是 zepto 里面 callback=? 没有替换成功。定义好 callback 方法之后 zepto 里面是这么去替换接口地址的参数的：
// https://github.com/madrobby/zepto/blob/master/src/ajax.js#L121-L126 window[callbackName] = function(){ responseData = arguments } script.src = options.url.replace(/\?(.+)=\?/, &amp;#39;?$1=&amp;#39; + callbackName) document.head.appendChild(script) 通过正则匹配到两个 ? 后并替换最后一个 ? 为回调方法名。最开始我一直没绕过来，我在想这特么是什么骚操作？能匹配到 callback=? 么？?callback=? 是可以匹配到，万一这个东西在后面 &amp;amp;callback=? 不就挂了么？最后才反应过来它这么做是直接匹配了整个地址后的 query，忽略了参数名称直接匹配 ?。对于这种操作，我只能说：
那么在当前的情况下这种操作会有什么问题呢，我怀着不服输的精神又查了下去。结果我发现在这种情况下这个正则居然跪了！最后我在这打印出来看，发现传进去的 URL 里面多了一个回车符，导致这段正则失效了。因为我们知道正则里面的 . 元字符是匹配除了回车符以外的所有字符。（这无语的BUG&amp;hellip;果然水逆就算是啥也不干问题也会自动找上门啊&amp;hellip;
当我查到这的时候我就思考了两个问题：
这个回车从哪里来的？参数里怎么会有回车？因为这个参数是直接从当前页的 URL 获取的，所以是我们在拼接的时候操作的有问题，还是服务端下发的当前地址就有问题？ 为什么到了最后 URL 地址这还有回车，正常情况下到这步的时候应该库都会对其进行转码编译了，例如回车符会被编成 %0A 这样其实 zepto 内部再处理就没有问题了呀？所以是哪里给漏编码了呢？ 哪里来的回车符 顺着上面两个问题，由于第一个问题的成本比较低，先看了下回车符哪里来的问题。我优先看了下当前地址，发现在当前地址的时候已经有问题了。而其他路径进入的这个页面都是正常的，只有这个特殊情况下有问题，遂反馈给服务端反查一下。最后服务端(PHP)那查到原来是因为他们读取文件时按行分割没有注意到方法里会带着换行的问题。
大概就是服务端那会有一个 token 文件，里面按行记录着一堆 ID，服务端会使用 file() 读取这个文件，然后将每个 ID 都 map 成一个地址下发下去。使用 file() 的好处是它在读取文件的时候能自动输出一个按行分割后的数组，这样就不需要额外操作。不过服务端同学没有注意，PHP 文档里也非常清楚的写明了：
Note:
Each line in the resulting array will include the line ending, unless FILE_IGNORE_NEW_LINES is used, so you still need to use rtrim() if you do not want the line ending present.
via: http://php.net/manual/zh/function.file.php
也就是说这种方法默认分割后的数组每个数据是包含最后的那个换行符的！想要去掉换行符需要添加 FILE_IGNORE_NEW_LINES 的标记参数。我自己也试了下发现果真如此！可以看到数组的前三个里面字符串的长度都是 4。
最后服务端从来源上解决了这个问题。
为什么没被转义 虽然问题解决了，但是我的第二个疑问其实还没有被解决。本来应该被转义的字符为什么没有被转义？是道德的沦丧还是人性的泯灭zepto 出问题了还是我们的代码里有什么潜在的风险？
我先去检查了一下 zepto 自己本身，发现它们的所有数据拼接都没有问题，使用了 $.param() 方法，而该方法内是使用了 escape() 对键值都做了编码的。zepto 出问题是不可能的了，那只能是我自己代码里的问题了。回到业务代码里查了一圈，最后发现，在某个阴暗的角落，居然窝藏了这么一段代码：
data.topicListApi = location.protocol + `//imnerd.org/detail?u=${uid}&amp;amp;sign=${sign}&amp;amp;n=10&amp;amp;tid=${tid}${onlineTypeParam}${tagParam}${rawUrlParam}${topUrlParam}`; 这一堆参数不经过任何编码就直接进行字符串拼接的操作&amp;hellip;
后记 好啦，问题的来龙去脉前因后果总算是查清楚了。虽然这里面有各种坑，虽然服务端已经帮忙处理了，但是我明白最主要的问题还是最后的那个前段拼接的问题。所以我以血泪的历史告诉大家 URL 拼接一定要编码别搞什么骚操作啊！同时后面我 git blame 查了下写这个代码的同学，虽然离职了&amp;hellip;但是我还是想&amp;hellip;</description></item><item><title>Untrusted 通关攻略!</title><link>https://imnerd.org/untrusted-clearance.html</link><pubDate>2014-11-13</pubDate><guid>https://imnerd.org/untrusted-clearance.html</guid><description>今天早上刷微博的时候看到 @fakefish 分享了一个游戏微博，游戏的名字叫做《Untrusted》，通过修改JS代码来通关的游戏，作者把游戏代码托管在了Github上，游戏地址在 http://alexnisnevich.github.io/untrusted/ 。
Level 1 这关简单，移动玩家对象@先拾取⌘然后移动到出口就好了。
Level 2 这关看着挺吓人的，路都被#号给各种拦着了，但是其实读一下代码发现也就那么回事。13行的new ROT.Map.DividedMaze(map.getWidth(), map.getHeight())负责根据地图大小生成迷宫，30行到33行在出口的四个方向生成了#号阻拦我们。看着其实挺恐怖的，但是其实我们只要开辟一个新思路不移动@对象到出口而是把出口移动到对象边上就好了。
当然没办法移动现有的这个出口了，我就尝试着再新建了一个出口在@的旁边。map.placeObject(7,6,'exit');，一次性成功！
Level 3 这一关#栅栏把@和出口给隔开来了，首先想到的是把生成#栅栏的代码删除掉。但是很不幸的是过关验证函数validateLeve()上清楚的写着一定要有一定数量的栅栏才行。所以我们转变思路，用栅栏把@和出口都包括进去就好了。为了方便我就直接生成在了边缘了。
for (y = 0; y &amp;lt;= map.getHeight(); y++) { map.placeObject(0, y, &amp;#39;block&amp;#39;); map.placeObject(map.getWidth(), y, &amp;#39;block&amp;#39;); } for (x = 0; x &amp;lt;= map.getWidth(); x++) { map.placeObject(x, 0, &amp;#39;block&amp;#39;); map.placeObject(x, map.getHeight()-1, &amp;#39;block&amp;#39;); } Level 4 这一关和上一关的感觉是一样的，应该可以抄袭上一关的代码。不过你仔细读代码的话会发现比上一关少了过关验证函数。所以我这里就取巧用了第二关的方法，用 map.placeObject(map.getWidth() - 5, map.getHeight() - 5, 'exit');在@对象旁边新建了一个出口。
Level 5 初看血红的图像可能还觉得兴奋，觉得直接移动过去就好了。不过仔细一看代码你就会发现不是那么回事。代码随机在地图上生成了75个看不见的mine对象，不能触碰到它，否则就Game Over。
要越过无形的东西只要让它现行知道它的方位避开它就好了，所以我们可以用map.setSquareColor(x, y, '#000')给他们都附上一个颜色。
Level 6 这一关就比较高级了，有一个攻击者来守护出口，并且会跟随你的步伐靠近你并杀掉你。我的第一想法比较简单，就是建议一个横向屏障让其无法靠近我，不过为了能让自己到达出口位置，我设置了一个空的出口。
for(var x = 0; x &amp;lt; map.getWidth()-3; x++) { map.placeObject(x, 11, &amp;#39;block&amp;#39;) } Level 7 这一关有一个电话符号，吃了它就可以在移动过程中执行回调函数。在过关路上有不同颜色的障碍物，如果@对象颜色和障碍物的颜色不一样就不允许通过。结合以上两个消息，得到的解决办法是通过一次障碍物前用电话回调函数进行一次“变装”就好了，为了方便我直接指定了在障碍物的前一个位置进行“换装“。
if(player.atLocation(24, 12) || player.atLocation(33,12)) player.setColor(&amp;#39;#f00&amp;#39;); if(player.atLocation(27,12) || player.atLocation(36,12)) player.setColor(&amp;#39;#ff0&amp;#39;); if(player.atLocation(30,12)) player.setColor(&amp;#39;#0f0&amp;#39;); Level 8 到这里就成功进入第二章了！这一关出现了一片一片的绿森林，不幸的是路被他们给挡住了。可供我们修改的代码也非常有限，仅仅只能修改101行的几个字符。可以看到functionList是一个函数组成的数组，看代码的意思应该是让我们给电话回调函数指定一个functionList里面设置好的函数。
理解一下后我们就能想到利用电话回调函数执行重新生成森林的代码，这样每次@对象旁边的道路就会“开辟”出来。
Level 9 哈哈这关做的很漂亮，乘船过河的说。这里我们发现可以自定义对象，raft对象的transport参数提醒了我们可以通过设置这个参数让@对象穿过自己。所以我另辟蹊径，自己创建了一个可以通过的通道。
map.defineObject(&amp;#39;boat&amp;#39;, { &amp;#39;type&amp;#39;: &amp;#39;dynamic&amp;#39;, &amp;#39;symbol&amp;#39;: &amp;#39;▓&amp;#39;, &amp;#39;color&amp;#39;: &amp;#39;#420&amp;#39;, &amp;#39;transport&amp;#39;: true, // (prevents player from drowning in water) &amp;#39;behavior&amp;#39;: function () {} }); for (var y = 5; y &amp;lt; 15; y++) { map.placeObject(1, y, &amp;#39;boat&amp;#39;); } Level 10 这一关有各种攻击者挡住你的去路。因为可以自定义各个攻击者的行为函数，所以我这里的想法是让攻击者自动让出一个通道出来。
map.defineObject(&amp;#39;attackDrone&amp;#39;, { &amp;#39;type&amp;#39;: &amp;#39;dynamic&amp;#39;, &amp;#39;symbol&amp;#39;: &amp;#39;d&amp;#39;, &amp;#39;color&amp;#39;: &amp;#39;red&amp;#39;, &amp;#39;onCollision&amp;#39;: function (player) { player.killedBy(&amp;#39;an attack drone&amp;#39;); }, &amp;#39;behavior&amp;#39;: function (me) { if(me.getY() == 12) me.move(&amp;#39;left&amp;#39;) if(me.getY() == 11) me.move(&amp;#39;down&amp;#39;) } }); map.defineObject(&amp;#39;reinforcementDrone&amp;#39;, { &amp;#39;type&amp;#39;: &amp;#39;dynamic&amp;#39;, &amp;#39;symbol&amp;#39;: &amp;#39;d&amp;#39;, &amp;#39;color&amp;#39;: &amp;#39;yellow&amp;#39;, &amp;#39;onCollision&amp;#39;: function (player) { player.killedBy(&amp;#39;a reinforcement drone&amp;#39;); }, &amp;#39;behavior&amp;#39;: function (me) { me.move(&amp;#39;down&amp;#39;); } }); map.defineObject(&amp;#39;defenseDrone&amp;#39;, { &amp;#39;type&amp;#39;: &amp;#39;dynamic&amp;#39;, &amp;#39;symbol&amp;#39;: &amp;#39;d&amp;#39;, &amp;#39;color&amp;#39;: &amp;#39;green&amp;#39;, &amp;#39;onCollision&amp;#39;: function (player) { player.killedBy(&amp;#39;a defense drone&amp;#39;); }, &amp;#39;behavior&amp;#39;: function (me) { if(me.getX() == map.getWidth() - 10) return false; if(me.getY() == 12) me.move(&amp;#39;right&amp;#39;) if(me.getY() == 11) me.move(&amp;#39;down&amp;#39;) } }); Level 11 同样是控制对象的运动行为，需要良好的控制R机器人拿到K钥匙并交给@后才能顺利通关。这里比较简单，只要让R在能右的时候往右走不能右走的时候往下走就好了，然后@在门口静候机器人送钥匙过来就好啦。
me.canMove(&amp;#39;right&amp;#39;)?me.move(&amp;#39;right&amp;#39;):me.move(&amp;#39;down&amp;#39;) Level 12 哈哈，和上关以一样，不过高级了一点增加了两个阻碍物。解法是一样的，看个人的控制情况了，我的想法比较简单，就是左边的话是能下就下不能下就右，右边就是能上就上，不能上就右。
if(me.getX() &amp;lt; map.getWidth() / 2 || me.getX() == map.getWidth() - 2) me.canMove(&amp;#39;down&amp;#39;) ? me.move(&amp;#39;down&amp;#39;) : me.move(&amp;#39;right&amp;#39;) else me.canMove(&amp;#39;up&amp;#39;) ? me.move(&amp;#39;up&amp;#39;) : me.move(&amp;#39;right&amp;#39;) Level 13 这一关在上一关的基础上又更上一层楼，出了个迷宫般。开始我绞尽脑汁想怎么让机器人自动出来的算法，不过想来我是实在没有那个本事了。后来突发奇想，既然之前的关卡中有攻击者对象根据@对象的操作做出反应，那么我也可以通过@对象来控制机器人咯？所以我比较简单的设置了一个上下左右四个位置，只要@在这个位置上就做出对应的操作。不过作者的Github项目里面收录了各种机器人自动和人工控制的算法，比我这个好用多了，大家可以去欣赏一下：官方第13关的解决办法收录。
这关自己的代码太dirty了就不放上来了 Level 14 这一关很简单，就是用同颜色的钥匙开同颜色的锁，然后最终拿到A并过关的意思。可以操作的地方不多，一看就是让我们设置当开绿锁的时候我们应该把哪把钥匙献上。要么是redKey要么是blueKey，greenKey自己肯定是不可能了。通过实验你会发现答案是blueKey。
Level 15 这一关我也想了很久，同样是过河，但是比之前那关少了一条船，最重要的是可以修改代码的地方不多。我想了一下觉得既然在行为中player被killed了那我就可以再新建一个player了，然后我填写了map.placePlayer(map.getWidth()-2,map.getHeight())这个代码后成功了，但是并不是我想的一样。大概是因为新建了Player然后直接跳到了Player的判定而跳过了Kill的操作了。
Level 16 这关是随机新建了25个无形的墙壁（你还不能删除这些墙壁，因为validateLeve()函数有验证），每个墙壁有一个颜色如果@对象的颜色和墙壁颜色不同的话就会被撞，相同就会通过。
由于作者非常好像的给出了一部分代码让我们通过canvas将无形的墙壁变的有形，大大的降低了问题的难度。所以我们只要让无形的颜色变成有形的颜色，在通过墙壁之前“换装”成相应的颜色就好了。换装同样是用电话回调函数，不过因为不知道怎么获取最近的墙壁的颜色，我选择的是在墙壁的颜色（总共3个）随机，最多只要点两下就会粗来正确的颜色了。
function createLaser(centerX, centerY, angleInDegrees, length, color) { var angleInRadians = angleInDegrees * Math.PI / 180; var x1 = centerX - Math.cos(angleInRadians) * length / 2; var y1 = centerY + Math.sin(angleInRadians) * length / 2; var x2 = centerX + Math.cos(angleInRadians) * length / 2; var y2 = centerY - Math.sin(angleInRadians) * length / 2; // map.createLine() creates a line with an effect when // the player moves over it, but doesn&amp;#39;t display it map.createLine([x1, y1], [x2, y2], function (player) { if (player.getColor() != color) { player.killedBy(&amp;#39;a &amp;#39; + color + &amp;#39; laser&amp;#39;); } }); // using canvas to draw the line var ctx = map.getCanvasContext(); ctx.beginPath(); ctx.strokeStyle = color; ctx.lineWidth = 5; ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); } player.setPhoneCallback(function(){ var colors = [&amp;#39;red&amp;#39;, &amp;#39;yellow&amp;#39;, &amp;#39;teal&amp;#39;]; colors.filter(function(color){ return player.getColor() != color; }); player.setColor(colors[parseInt(Math.random()*(colors.length-1))]); }) Level 17 这关大概的意思是每个紫色的出口都是传送门，但是你不知道你是传送到下一个传送门还是荆棘中。所以我们的目标是让传送到传送门的传送门给我们现实出来，这里我利用上一关刚学会的canvas做标记操作。
if(t1.getType() == &amp;#39;teleporter&amp;#39; &amp;amp;&amp;amp; t2.getType() == &amp;#39;teleporter&amp;#39;) { var t1p = map.getCanvasCoords(t1); canvas.strokeStyle = &amp;#39;green&amp;#39;; canvas.moveTo(t1p.x, t1p.y); canvas.lineTo(t1p.x+5,t1p.y+5); canvas.stroke(); } Level 18 这一关是让我们编写一个jump()回调函数让@跳过&amp;quot;悬崖&amp;quot;抵达出口。不过我们如果真的被jump这个词给迷惑住了的话可能真的会想一会儿。我看了一下电话回调函数中执行jump()函数的条件是当@对象下面不是空，那就是立即可以执行了（初始状态下面是#号不是空）。所以我直接在悬崖上架了一座桥让@“跳”过去。
function jump() { for(x = Math.floor(w/2)-5; x&amp;lt;Math.floor(w/2)+5; x++) { map.placeObject(x, Math.floor(h/2), &amp;#39;block&amp;#39;); } } Level 19 这关没怎么理解真谛，大概的意思是让红色和绿色的@碰到一块吧，反正我随便左右上下左右上下的按了一通就过去了。不过有大牛看出了前因后果，我摘抄一下：
19 巨坑爹的一关 我居然认真的读了该主页 并且认为一个玩TCS 的副教授人搞人机交互 以及 UI 毫无不妥。毕竟高德纳不也写了LATEX嘛。。 然后看到它把Lorem 放上去还觉得很有艺术性。。 然后觉得这哥们的姓很有特色 居然叫Eval 天生搞计算机的命啊。。。 然后我就去搜了一下 没找到 paper 才意识到被蒙了。 。。。。。。 忒缺德了。 欺负老实人啊。。。。
总之。。 这是个抓虫子 游戏 网页本质上是一个 递归组合 也就是 盒子里面套盒子 按上会走到 外面的盒子 按下 会进入里面的盒子 按左右会 走到 并列的盒子 操纵绿色符号追击红色符号 策略就是 首先走到根盒子 然后看红色在哪个盒子 就进入哪个盒子 尽可能地 在它外面的盒子 然后 慢慢接近它 很容易就抓到了。。
Level 20 这一关是天降毒雨，我们必须顶着毒雨和上面的BOSS作斗争，消灭所有的BOSS之后拿到A之后才能通关。这关我想了很久，然后在我翻API的时候突然发现有map.overrideKey这个函数，可以复写一个方向键的回调函数，解决了我想了半天没办法触发的问题。然后我们只要做向上发射的子弹去消灭BOSS就好了。这里因为我们要往右上下移动，所以我选择复写了左方向键。
map.defineObject(&amp;#39;arrow&amp;#39;, { &amp;#39;type&amp;#39;: &amp;#39;dynamic&amp;#39;, &amp;#39;symbol&amp;#39;: &amp;#39;↑&amp;#39;, &amp;#39;color&amp;#39;: &amp;#39;green&amp;#39;, &amp;#39;interval&amp;#39;: 100, &amp;#39;projectile&amp;#39;: true, &amp;#39;behavior&amp;#39;: function (me) { me.move(&amp;#39;up&amp;#39;); } }); function shoot() { for (x = 0; x &amp;lt; map.getWidth(); x++) { map.placeObject(x,12,&amp;#39;arrow&amp;#39;); } } map.overrideKey(&amp;#39;left&amp;#39;, shoot); } Level 21 这一关是耗费我最久的一关了，什么阻碍都没有，然后你也不可以操作代码，但是就是没法过关。看代码的原因应该是map.finalLevel这个值变成True了表示最后一关，所以就没办法再下一关了。
最后搜索了一下发现原来Menu界面下可以插件scripts文件夹，就是游戏的源码了，而且可以修改的说。进Object.js文件修改exit对象的行为判断函数，把if(!map.finalLevel){}去掉就好了。
Level 22 这一关是作者的谢幕，至此全部通关。</description></item><item><title>批量下载虾米已下载歌曲（新）</title><link>https://imnerd.org/multi-download-xiami-songs-new.html</link><pubDate>2013-01-04</pubDate><guid>https://imnerd.org/multi-download-xiami-songs-new.html</guid><description>前言 呵呵，没错，公子我又再次光荣的把硬盘搞挂了，然后音乐又没了！鉴于之前已经有过一次经验（批量下载虾米已下载歌曲），所以本以为这次也会轻而易举了的。没想到居然在我写出那篇日志后没多久，官方居然各种更新（更新页面，下载机制，Flash播放器），然后我那篇日志的方法就没法用了。不过官方这个也都是小改，所以我也只要小改下代码就成啦！哈哈！
有什么变化 首先要说的是虾米官方的下载思路还是没变的，基本上还是之前日志里头说的那样：
在网站点击下载之后，网站向你的账户中未下载列表传递下载歌曲信息，之后未下载列表则返回一个以emoun://特有协议开头的文件，用以打开虾歌（这个和迅雷thunder://以及电骡等的ed2k://是一个道理）。虾歌打开后查询未下载的表单，返回表单中的歌曲，然后下载。同时将未下载列表中的歌曲提交到已下载列表中，并在未下载列表中删除。 - 批量下载虾米已下载歌曲
官方改变的就是下载列表和网站点击下载这两步：
下载列表 老版的是直接显示列表显示每首歌的，新版则是按照下载时间来显示，一次下载为一次订单，订单内再显示此次下载的歌曲。这样正则匹配获取歌曲的ID就需要发生变化了。
点击下载提交订单 以前只要登录后向服务器POST歌曲的ID就能提交订单了。新版则需要提交sign和_xiamitoken这两个新参数，三个参数一块POST才行。sign参数暂时还不知道如何算出来的，_xiamitoken则是你的cookies。看源码意外的让我发现了参数居然都直接写在页面里头了，为了简便，我就直接抓取订单页面正则获取这两个参数了。同时还加了判断Referer这种老手段，这个也不算难。
VIP机制 以前是没有VIP的，自从被阿里收购之后就增加了VIP功能。好在现在可以用体验点（以前的红包)购买VIP，15个体验点可以买一个月的VIP，一个月的VIP能免费下100首歌曲。我的歌曲数在400+，花了80个体验店办了半年的，可以免费下600首歌曲。等于用80个体验店下600首歌的意思，变相的节省了下体验点，哈哈！
解题步骤 一、获取登录COOKIES 这一步和老版还是一样的，直接复制过来吧。
获取登录后的COOKIES文件，方便之后的抓取工作。修改代码中的第2行和第3行，填入自己的账号和密码。如果成功的话，会在该文件的同级目录下得到一个cookies.txt文件。
&amp;lt;?php $name = &amp;#39;&amp;#39;; //输入你的账号 $password = &amp;#39;&amp;#39;; //输入你的密码 $curl_post = &amp;#39;email=&amp;#39;.$name.&amp;#39;&amp;amp;amp;password=&amp;#39;.$password.&amp;#39;&amp;amp;amp;done=/&amp;amp;amp;submit=登 录&amp;#39;; $cookie_file = dirname(__FILE__).&amp;#39;/cookie.txt&amp;#39;; $curl = curl_init(); curl_setopt($curl, CURLOPT_URL, &amp;amp;quot;http://www.xiami.com/member/login&amp;amp;quot;); curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); curl_setopt($curl, CURLOPT_POST, 1); curl_setopt($curl, CURLOPT_POSTFIELDS, $curl_post); curl_setopt($curl, CURLOPT_COOKIEJAR, $cookie_file); curl_exec($curl); curl_close($curl); ?&amp;gt;二、抓取下载列表并提交到未下载列表 这一步因为列表的改变有些许变化。而且新版订单还有个新订单和旧订单之分，真是蛋疼，还得做两次解析。
&amp;lt;?php set_time_limit(0); //设置成不限制页面运行时间 function get($url) { $cookie_file = dirname(__FILE__).&amp;#39;/cookie.txt&amp;#39;; $curl = curl_init($url); curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); curl_setopt($curl, CURLOPT_COOKIEFILE, $cookie_file); $data = curl_exec($curl); curl_close($curl); return $data; } $song = array(); $pageinfo = get(&amp;#39;http://www.xiami.com/account/myorders-old&amp;#39;); $preg = &amp;#39;/\…\&amp;lt;\/a\&amp;gt;\&amp;lt;a href\=\&amp;#34;\/account\/myorders-old\/page\/(.*?)\&amp;#34; class\=\&amp;#34;p\_num\&amp;#34;&amp;gt;/s&amp;#39;; preg_match_all($preg, $pageinfo, $match); $page = $match[1][0]; for($i=1;$i&amp;lt;$page;$i++) { $data = get(&amp;#34;http://www.xiami.com/account/myorders-old/page/&amp;#34;.$i); $preg = &amp;#39;/\&amp;lt;td class\=\&amp;#34;iname\&amp;#34;\&amp;gt;\&amp;lt;a href\=\&amp;#34;\/song\/(.*?)\&amp;#34;/s&amp;#39;; preg_match_all($preg, $data, $match); foreach($match[1] as $item) $song[] = $item; } $pageinfo = get(&amp;#39;http://www.xiami.com/account/myorders&amp;#39;); $preg = &amp;#39;/\…\&amp;lt;\/a\&amp;gt;\&amp;lt;a href\=\&amp;#34;\/account\/myorders\/page\/(.*?)\&amp;#34; class\=\&amp;#34;p\_num\&amp;#34;&amp;gt;/s&amp;#39;; preg_match_all($preg, $pageinfo, $match); $page = $match[1][0]; for($i=1;$i&amp;lt;$page;$i++) { $data = get(&amp;#34;http://www.xiami.com/account/myorders-old/page/&amp;#34;.$i); $preg = &amp;#39;/\&amp;lt;td class\=\&amp;#34;iname\&amp;#34;\&amp;gt;\&amp;lt;a href\=\&amp;#34;\/song\/(.*?)\&amp;#34;/s&amp;#39;; preg_match_all($preg, $data, $match); foreach($match[1] as $item) $song[] = $item; } $song = array_unique($song, SORT_NUMERIC); echo &amp;#39;var song = new Array(&amp;#39;.implode(&amp;#39;,&amp;#39;, $song).&amp;#39;)&amp;#39;; ?&amp;gt;批量提交下载歌曲 我之前也讲了，新版新增sign和_xiamitoken两个参数，同时还增加了Referer验证。整体代码还是采用上版中的PHP+AJAX异步提交，只是在PHP部分有稍许修改。
将上一步得到的代码复制替换掉相同的那部分，保存为download.html
&amp;lt;script type=&amp;#34;text/javascript&amp;#34;&amp;gt; //将第二步获得的代码复制到下面 var song = new Array(1769629171,3338870,1769902385,1770432131,2868586,1770149760,367633,1769832130,35526,1769962750,1770551145,1770457081,3486103,1769400110,127903,3367334,2078855,71830,376007,376015,2074918,2730,1770450144,3455520,79157,79151,81366,65256,390070,2138604,1770345727,376050,376006,149193,388168,1769665592,3497062,1769236069,3632104,1768923954,1769491757,3608275,2092882,1770354410,3480460,3620143,2095376,2561774,3664677,1770462404,1769056924,3527076,2126340,2089410,1769177482,2083322,1769831952,1769316186,1770524409,1769517803,1190507,1964547,1769107542,3486203,1769686818,1769028326,1858814,2084011,1769235816,1770614545,2083187,2605276,378268,2515019,1122167,1769176497,3302053,2083102,3638520,189072,371017,1770168732,1083760,2122948,2070331,2128868,3319126,2561592,1381654,3441719,1768939471,2091290,2098665,2067235,1770060224,3562953,54342,1769831786,1770145312,76323,2067242,173117,136054,1770068043,1769833104,1769833105,1769833106,1769833107,1769833108,2082305,1768989931,1769356638,1769082273,1769776879,1769740304,1205851,1768984809,1769102373,1769740095,1768989928,1769381931,1769227674,3599015,1768962602,3463177,1769381938,1769381935,1769381934,1769381932,3410377,2067242,3381901,3381903,373969,2072395,373971,373990,374039,3381910,3381911,3381912,382134,378711,193010,162269,143775,127673,120041,89443,52732,382512,383210,1769274527,1769004670,1768958960,3599311,3365855,2314604,2080987,2073790,385907,115384,374057,377936,380029,382560,382852,383962,33806,378041,380287,385729,385760,388406,389153,381833,83126,386773,389072,389080,389077,43542,52726,375183,382821,384595,385074,386954,373818,376387,378307,379302,382777,384646,384670,385137,385976,386073,386347,2342433,3550893,1769699970,2095102,2095107,2017034,2286524,2342421,2286523,3338183,1768988423,2286527,2380720,3464931,1176114,1769334977,2155384,1769839800,3225258,2091936,1769334987,3187867,2095104,3413844,1769072635,1007695,3636957,3467770,1768940269,1769292436,1769850021,1769102619,3409064,1769870173,1769850022,1769850019,1769463117,3446206,2028877,2136483,1769072646,1769291745,1769072643,1769113798,196142,3187959,2079575,2286522,2079581,2385273,1769673320,3513664,1769071426,3502285,1769801604,1769801605,3484274,1769303054,1769303056,1769303057,1769303058,1769303059,1769303060,1769303061,1769303062,1769303055,1768914983,378646,3586293,1769830111,1769830112,1769830113,1769830114,1769830115,1769830116,1769830117,1769830118,1769830119,1769830120,1769570445,); //复制结束 //下面的代码不需要修改 var XHR; //定义一个全局对象 function xm_download(id){ if(window.ActiveXObject){//IE的低版本系类 XHR=new ActiveXObject(&amp;#39;Microsoft.XMLHTTP&amp;#39;); }else if(window.XMLHttpRequest){//非IE系列的浏览器，但包括IE7 IE8 XHR=new XMLHttpRequest(); } XHR.open(&amp;#34;GET&amp;#34;,&amp;#34;download.php?id=&amp;#34;+id,true); XHR.send(null); } for (var i=0;i&amp;lt;song.length;i++) { xm_download(song[i]); } &amp;lt;/script&amp;gt; 将下面的代码保存为download.php，放在与download.html同级目录中
&amp;lt;?php $cookie_file = dirname(__FILE__).&amp;#39;/cookie.txt&amp;#39;; //登录COOKIES文件地址 $id = $_GET[&amp;#39;id&amp;#39;]; $url = &amp;#34;http://www.xiami.com/download/pay?id=$id&amp;#34;; $curl = curl_init($url); curl_setopt($curl, CURLOPT_COOKIEFILE, $cookie_file); curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); $data = curl_exec($curl); curl_close($curl); $preg = &amp;#39;/\&amp;lt;input type\=\&amp;#34;hidden\&amp;#34; name\=\&amp;#34;sign\&amp;#34; id\=\&amp;#34;sign\&amp;#34; value\=\&amp;#34;(.*?)&amp;#34; \/\&amp;gt;/s&amp;#39;; preg_match_all($preg, $data, $match); $sign = $match[1][0]; $pieces = explode(&amp;#39;&amp;lt;input type=&amp;#34;hidden&amp;#34; value=&amp;#34;&amp;#39;, $data); $piece = end($pieces); $cookies = explode(&amp;#39;&amp;#34;&amp;#39;, $piece); $cookie = $cookies[0]; $data = &amp;#34;song_ids[]=$id&amp;amp;amp;use_vip=1&amp;amp;amp;down=1&amp;amp;amp;inpour_amount=0&amp;amp;amp;sign=$sign&amp;amp;amp;_xiamitoken=$cookie&amp;#34;; $curl = curl_init($url); curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); curl_setopt($curl, CURLOPT_POST, 1); curl_setopt($curl, CURLOPT_COOKIEFILE, $cookie_file); curl_setopt($curl, CURLOPT_REFERER, $url); curl_setopt($curl, CURLOPT_POSTFIELDS, $data); $data = curl_exec($curl); curl_close($curl); ?&amp;gt;四、打开虾歌查询订单 最后一步是打开虾歌，选择“文件”→“检查未完成下载”，静候片刻你就可以欢呼了！之后静静等待虾歌批量下载成功就好了。</description></item><item><title>红心电台</title><link>https://imnerd.org/red-heart-fm.html</link><pubDate>2013-07-07</pubDate><guid>https://imnerd.org/red-heart-fm.html</guid><description> --
前两天写了篇标签实现同步显示歌词"Audio标签的文章，想要更深入学习一下，遂决定写一个网页版音乐播放器试试（其实有更深部的动机的，不过我不告诉你们，哼哼！）。在整个网页的编写过程中，我学习到了很多的东西，感觉对jQuery又有了更新的认识。同时新接触了jQuery UI这个非常可靠的工具。不过说到底也还是新手，代码写的很脏，也没想到重构什么的，功能实现出来就好了，至于美观什么的，哈哈，你是在开玩笑么！
放上本篇文章主角：红心电台 - https://imnerd.org/lab/player 简单介绍：红心电台是一个利用HTML5制作的能够存储播放列表的在线音乐播放器。是的，没错，虽然是叫做红心电台，但是基本上也就只是一个能存播放列表的播放器而已。另外，本网页是用HTML5写的，然后由于播放的音乐基本是MP3格式的，所以只支持Webkit系的浏览器（Chrome为首）和IE9+以上的浏览器，对Firefox和Opera的用户感到抱歉了，OGG的音乐库毕竟还是挺小众的。具体浏览器支持可以看红心电台的HELP。
使用说明：
1、左上角有搜索框，用户可自行添加歌曲到播放列表中。
2、默认播放列表的歌曲在用户添加过歌曲后会自行清除（需要刷新页面），用户不必手动清除。
3、默认支持快捷键，左右方向键为上一首下一首，空格键为暂停，Delete键为删除当前播放曲目。
如果对红心电台有什么意见或者是建议，又或者是发现了什么问题，可以随时与我联系（联系方式见关于页面），当然你也可以在本文留言，我都是可以看的到的。</description></item><item><title>如何批量下载虾米已下载歌曲</title><link>https://imnerd.org/multi-download-xiami-songs.html</link><pubDate>2012-09-07</pubDate><guid>https://imnerd.org/multi-download-xiami-songs.html</guid><description> --
本文方法已失效，新版见：如何批量下载虾米已下载歌曲（新）
前言 首先要说明的是，这不是一篇讲述如何免费批量下载虾米高质量音乐的文章，而是讲述一个杯具男如何因为懒惰而DIY出的成果。所以如果抱有前述心理的同学到此可以关闭网页了。另外本文中代码运用到了PHP+CURL+JavaScript，推荐大家在本地环境中进行操作。同时最重要的是，本文不是讨论如何绕过虾米的收费机制下载歌曲，所以你必须保证你有足够的虾米币或者红包用来下载歌曲才行，否则一切免谈。
事出有因 事情是这样的。前两天安装了Linux Mint，在Linux中打开了音乐，没过一会儿就卡掉了，当时我也没在意。结果一回到Windows就提示我文件夹损坏了，里面的文件也自然都悲剧了。用恢复软件恢复了效果也不太理想。400首歌总共2G多的文件也不是说割舍就能割舍的，所以就有了下面这些。
解题思路 虾米下载的机制是这样的：在网站点击下载之后，网站向你的账户中未下载列表传递下载歌曲信息，之后未下载列表则返回一个以emoun://特有协议开头的文件，用以打开虾歌（这个和迅雷thunder://以及电骡等的ed2k://是一个道理）。虾歌打开后查询未下载的表单，返回表单中的歌曲，然后下载。同时将未下载列表中的歌曲提交到已下载列表中，并在未下载列表中删除。这样，一次下载就完成了。
如图所示，点击下载歌曲，网站向http://www.xiami.com/download/song页面POST提交了相关Data，同时网页返回一个emoun://协议开头的文件（图中左侧红色方框标记的）。
所以我的想法是这样的：首先登录网页抓取已下载列表中已经下载过歌曲的信息，然后批量的提交到未下载列表。然后打开虾歌去查询未下载列表中的歌曲就好了。如果和我所想一样的话，虾歌应该会一次性批量得到所有歌曲，那么我们只要等待下载完成就好了。
解题步骤 一、获取登录COOKIES 获取登录后的COOKIES文件，方便之后的抓取工作。修改代码中的第2行和第3行，填入自己的账号和密码。 如果成功的话，会在该文件的同级目录下得到一个cookies.txt文件。
$name = &amp;#39;&amp;#39;; //输入你的账号 $password = &amp;#39;&amp;#39;; //输入你的密码 $curl_post = &amp;#39;email=&amp;#39;.$name.&amp;#39;&amp;amp;password=&amp;#39;.$password.&amp;#39;&amp;amp;done=/&amp;amp;submit=登 录&amp;#39;; $cookie_file = dirname(__FILE__).&amp;#39;/cookie.txt&amp;#39;; $curl = curl_init(); curl_setopt($curl, CURLOPT_URL, &amp;#34;http://www.xiami.com/member/login&amp;#34;); curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); curl_setopt($curl, CURLOPT_POST, 1); curl_setopt($curl, CURLOPT_POSTFIELDS, $curl_post); curl_setopt($curl, CURLOPT_COOKIEJAR, $cookie_file); curl_exec($curl); curl_close($curl); 二、抓取下载列表并提交到未下载列表 有了上一步之后，这一步就变的非常的简单。这里主要考虑的是效率的问题，由于我的歌曲打开400首，虾米一页显示20首。所以我大概有20页要抓取，这对于PHP来说是一个不小的工作量，在本地运行更甚。如果你的页面少可以使用下面的代码，如果你要抓的页面多，可以尝试将其改写成multi_curl同步抓取，或者改写成AJAX形式，亦或是分批抓取都行。使用代码时候记得修改第三行的抓取页数。
set_time_limit(0); //设置成不限制页面运行时间 $page = &amp;#39;&amp;#39;; //填写你的已经下载页面的页数 $cookie_file = dirname(__FILE__).&amp;#39;/cookie.txt&amp;#39;; $song = &amp;#39;&amp;#39;; //如果页数过多可以分批获取，以免等待时间过长 for($i=1;$i&amp;lt;=$page;$i++) { $curl = curl_init(); curl_setopt($curl, CURLOPT_URL, &amp;#34;http://www.xiami.com/account/mysongs/s/2/page/&amp;#34;.$i); curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); curl_setopt($curl, CURLOPT_COOKIEFILE, $cookie_file); $data = curl_exec($curl); curl_close($curl); $preg = &amp;#39;/\&amp;lt;td.class\=\&amp;#34;iname\&amp;#34;\&amp;gt;\&amp;lt;a.href\=\&amp;#34;\/song\/(.*?)\&amp;#34;\&amp;gt;(.*?)\&amp;lt;\/a\&amp;gt;/s&amp;#39;; preg_match_all($preg, $data, $match); foreach($match[1] as $item) { $song .= $item . &amp;#39;,&amp;#39;; } } $song = str_replace(&amp;#39; &amp;#39;, &amp;#39;&amp;#39;, $song); $song = substr($song, 0, -1); echo &amp;#34;var song = new Array($song);&amp;#34;; /*end*/ 三、批量提交下载歌曲 上一步的代码中，我将下载歌曲的ID输出成了JavaScript数组的形式。因为这一步中我们将以歌曲数为基数向虾米网站POST提交信息，400对于PHP来说仍然是一个庞大的数字，对于本地更甚。你可以修改成multi_curl的形式，不过我第一反应是AJAX异步传输，这就是为什么我将歌曲的ID输出成了JavaScript数组的原因。
原理很简单，JS用来模拟循环，PHP则负责POST提交数据。使用代码时记得将xiami.song.download.html中的第3行替换成你在上一步中获得的代码。
//将第二步获得的代码复制到下面，记得删除最后一位数的逗号 var song = new Array(1769629171,3338870,1769902385,1770432131,2868586,1770149760,367633,1769832130,35526,1769962750,1770551145,1770457081,3486103,1769400110,127903,3367334,2078855,71830,376007,376015,2074918,2730,1770450144,3455520,79157,79151,81366,65256,390070,2138604,1770345727,376050,376006,149193,388168,1769665592,3497062,1769236069,3632104,1768923954,1769491757,3608275,2092882,1770354410,3480460,3620143,2095376,2561774,3664677,1770462404,1769056924,3527076,2126340,2089410,1769177482,2083322,1769831952,1769316186,1770524409,1769517803,1190507,1964547,1769107542,3486203,1769686818,1769028326,1858814,2084011,1769235816,1770614545,2083187,2605276,378268,2515019,1122167,1769176497,3302053,2083102,3638520,189072,371017,1770168732,1083760,2122948,2070331,2128868,3319126,2561592,1381654,3441719,1768939471,2091290,2098665,2067235,1770060224,3562953,54342,1769831786,1770145312,76323,2067242,173117,136054,1770068043,1769833104,1769833105,1769833106,1769833107,1769833108,2082305,1768989931,1769356638,1769082273,1769776879,1769740304,1205851,1768984809,1769102373,1769740095,1768989928,1769381931,1769227674,3599015,1768962602,3463177,1769381938,1769381935,1769381934,1769381932,3410377,2067242,3381901,3381903,373969,2072395,373971,373990,374039,3381910,3381911,3381912,382134,378711,193010,162269,143775,127673,120041,89443,52732,382512,383210,1769274527,1769004670,1768958960,3599311,3365855,2314604,2080987,2073790,385907,115384,374057,377936,380029,382560,382852,383962,33806,378041,380287,385729,385760,388406,389153,381833,83126,386773,389072,389080,389077,43542,52726,375183,382821,384595,385074,386954,373818,376387,378307,379302,382777,384646,384670,385137,385976,386073,386347,2342433,3550893,1769699970,2095102,2095107,2017034,2286524,2342421,2286523,3338183,1768988423,2286527,2380720,3464931,1176114,1769334977,2155384,1769839800,3225258,2091936,1769334987,3187867,2095104,3413844,1769072635,1007695,3636957,3467770,1768940269,1769292436,1769850021,1769102619,3409064,1769870173,1769850022,1769850019,1769463117,3446206,2028877,2136483,1769072646,1769291745,1769072643,1769113798,196142,3187959,2079575,2286522,2079581,2385273,1769673320,3513664,1769071426,3502285,1769801604,1769801605,3484274,1769303054,1769303056,1769303057,1769303058,1769303059,1769303060,1769303061,1769303062,1769303055,1768914983,378646,3586293,1769830111,1769830112,1769830113,1769830114,1769830115,1769830116,1769830117,1769830118,1769830119,1769830120,1769570445,); //复制结束 //下面的代码不需要修改 var XHR; //定义一个全局对象 function xm_download(id){ if(window.ActiveXObject){//IE的低版本系类 XHR=new ActiveXObject(&amp;#39;Microsoft.XMLHTTP&amp;#39;); }else if(window.XMLHttpRequest){//非IE系列的浏览器，但包括IE7 IE8 XHR=new XMLHttpRequest(); } XHR.open(&amp;#34;GET&amp;#34;,&amp;#34;download.php?id=&amp;#34;+id,true); XHR.send(null); } for (var i=0;i&amp;lt;song.length;i++) { xm_download(song[i]); } $data = &amp;#39;pid=&amp;amp;ptype=&amp;amp;song_count=1&amp;amp;id=&amp;#39;.$_GET[&amp;#39;id&amp;#39;]; $cookie_file = dirname(__FILE__).&amp;#39;/cookie.txt&amp;#39;; //登录COOKIES文件地址 $curl = curl_init(); curl_setopt($curl, CURLOPT_URL, &amp;#34;http://www.xiami.com/download/song&amp;#34;); curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); curl_setopt($curl, CURLOPT_POST, 1); curl_setopt($curl, CURLOPT_COOKIEFILE, $cookie_file); curl_setopt($curl, CURLOPT_POSTFIELDS, $data); $data = curl_exec($curl); curl_close($curl); 四、打开虾歌查询订单 最后一步是打开虾歌，选择“文件”→“检查未完成下载”，静候片刻你就可以欢呼了！之后静静等待虾歌批量下载成功就好了。</description></item><item><title>Firefox 4.0</title><link>https://imnerd.org/firefox4-0.html</link><pubDate>2011-10-27</pubDate><guid>https://imnerd.org/firefox4-0.html</guid><description>[1g1g]Anyone of us-Gareth Gates#playID:192280[/] --
哈哈，Firefox终于更新到4.0了呢，最重要的是Firebug已经支持4.0啦！(其实1.7版就支持的，只是我等到FB的官方推送后才知道。)在虚拟机里头安装了一下，发现除了easycomment那个插件不兼容(估计以后也不兼容了吧，感觉都没人维护了)，其它的都较好。
不得不说，FF是越来越朝Chrome靠拢了，连操作习惯都趋向于Chrome，最明显的要数插件按钮的位置了。之前FF都是放在状态栏上的，我习惯这样，也觉得这样挺好的，觉得Chrome也该放到状态栏上。结果反倒是FF先变了，也放到导航栏上去了！= =！这让我情何以堪啊...好在现在自己还可以选择位置，没有强制。另外Firefox对textarea元素的处理也趋向于Chrome化了，Chrome中用户是可以自定义textarea元素的长宽的，当然有个最小值。而Firefox比Chrome更高一层楼，连最小值都没有了，用户可以任意调整textarea的长宽了。这让做Web Design的情何以堪！然后我发现FF对于javascript的alert方法的支持也发生了改变，以前是弹出一个小窗口，现在是直接页面显示，这个跟Opera有些类似。好吧，Firefox我真的没说你在抄袭！另外FF的启动速度也有了较大的提高。不过我觉得还是好慢啊，记得微博上看到过有人说没有超过Chrome的话谁也不会去关心你相对于上个版本更新了多少。这个讲的真的是精辟呢！
分享一下我现在Firefox的界面，然后向大家推荐一下我觉得比较好的插件。
Movable Firefox Button
这个插件能够更改Firefox4.0左上角菜单按钮的位置和样式，得到如图的效果，做到可视区域最大化。
下载地址：官方下载
Awesome screenshot: Capture and Annotate
一个网页截图插件，从Chrome发展过来的。在Chrome的时候就非常喜欢这个插件，偶然搜索一下，发现FF也有相应的插件，而且在FF下的表现也不俗哦！
下载地址：官方下载
Tab Utilities
在新标签页打开书签、历史、地址、搜索，以及更多增强标签式浏览的使用功能。功能很强大，不过我一般就用它来实现双击标签关闭页面以及双击标签栏新建标签。下载地址：官方下载
标签管理器
如果只是要实现双击关闭标签的话我推荐使用这个插件，功能简单设置不复杂，是入门级的插件。
下载地址：普通下载
Speed Dial
模仿Opera和Chrome在新建页面上显示你常去的网站。用了Chrome之后就非常的喜欢这个设计，所以用Speed Dial模拟了一下，也是非常的不错呢！
下载地址：官方下载
火狐主页插件0.8
跟Speed Dial有着相同的效果但是比SD更加好看而且加载速度也不错，推荐大家安装这个
下载地址：普通下载
另外，诸如Firebug, Autoproxy, Greasemonkey, Flashgot之类的简直可以说FF标配的插件我就不介绍啦！最后大家再去安装一个主题就非常的完美咯！
小惊喜：打开Firefox按下键盘的Alt键，你们会发现什么呢？呵呵！更多乐趣期待大家发现</description></item></channel></rss>