Greasemonkey历险记之荔枝FM

从13年开始我迷上了podcast,尤其是在夜晚一片寂静的时候,边听podcast边写代码或是看书学习真是一种难以言喻的幸福。最早的时候是去新浪播客收听的,可那蛋疼的速度和糟糕的设计,让我很快就转向荔枝FM了。荔枝的速度在法国挺快,中文播客也比较全。我平时收听的主要渠道是web端,荔枝的设计虽然简单,但也到清爽干净,比起它日渐复杂的移动端更得我心。但是荔枝FM主打的毕竟是移动端,web端一直都非常简陋。最让我不爽的是web端至今都没能实现订阅自选电台这个最简单,也最核心的功能。

既然官方等不到了,那就自己来弄一个吧。上一篇介绍过Greasemonkey和类似查件的作用了,他们可以在访问指定页面的时候执行预先写好的脚本,以此实现自定义页面。在这里,我需要的是一个简单的功能:在web端的左边菜单栏中增加一个按钮,用来展开自选电台列表。

有了功能需要,让我们考虑一下实现思路吧。Dev tools和Inspect element是我们手中的利器, 很快我就发现一个有趣的事实:该网站是基于angular实现的!它带有SPA的特点,当我们点击左边菜单栏按钮的时候,只有中间的内容区会刷新。左边的菜单栏和右边的播放器是基本不刷新的。这对我们要实现的功能来说真是太棒了!自选电台的脚本只在页面第一次加载的时候注入DOM,每次更换电台的时候只有中间的电台信息部分刷新,该脚本不会再次执行,开销小多了!

好了,那就开始动手吧。首先自然是怎么确定自选电台了;观察几次就会发现,每个电台都由唯一的数字代表,只要将这个数字加到网址后面就可以跳转到该电台了。比如Gadio的地址是http://www.lizhi.fm/#/29345,友的聊的地址是http://www.lizhi.fm/#/14393/。这就简单多了,只要保存每个电台的唯一地址就可以了:

var myRadioList = {  
  gadio: '[http://www.lizhi.fm/#/29345'](http://www.lizhi.fm/#/29345'),  
  友的聊: '[http://www.lizhi.fm/#/14393'](http://www.lizhi.fm/#/14393'), 
   二次元: '[http://www.lizhi.fm/#/22557'](http://www.lizhi.fm/#/22557'),  
  糖蒜广播: '[http://www.lizhi.fm/#/13461'](http://www.lizhi.fm/#/13461') 
};

好吧,这种定义方式实在太不灵活了。不过作为第一版的脚本,从简单的,可实现的方案开始并无不可,日后再迭代就是了。电台列表有了,下面就该在左边加个按钮了。用inspect element找到左边导航栏的位置:

var ul = document.querySelector('.wrap > .leftNav > .content > ul');

然后就该生成一个按钮加到ul上了:

var radioButton = document.createElement('button'); 
radioButton.class = 'my-radio'; 
radioButton.style.cssText = "margin:3px 3px 3px 15px"; 
var b = document.createElement('b'); 
b.innerHTML = '自选电台'; radioButton.appendChild(b);
ul.appendChild(radioButton);

我知道这种inline css很丑,但作为一个简单小脚本,你们就原谅我的放荡不羁吧……

好了,按钮有了。下面该生成列表,并在按钮上绑定点击事件来展开列表了。绑定事件很简单:

radioButton.onclick = toggleRadioList;

toggleRadioList就是响应点击事件的方法,由该方法来展开和收起列表。得先有列表才行啊,鉴于列表本质上就是ul元素,我们先创建一个ul节点:

var radioList = document.createElement('ul'); 
radioList.className = 'radio-list';
radioList.style.cssText = "list-style-type:none;margin:3px;padding:0px 15px;visibility:hidden";

为了方便toggle函数改变它的状态,特地加上了一个class来方便查找。好了,下面就该按定义的电台对象一次生成内部的li节点了:

var frag = document.createDocumentFragment();
Object.keys(itemList).forEach(function(key) {  
  var li = document.createElement('li');  
  var a = document.createElement('a');  
  a.href = itemList[key];  
  a.innerHTML = key;  
  a.style.cssText = "text-decoration:underline";
  li.appendChild(a);  frag.appendChild(li);
 });
radioList.appendChild(frag);
target.appendChild(radioList);

注意这里我用了一个fragment来添加li节点。其实这里并不是必须的,但从练习的角度来看,这么写是符合性能实践的。我们都知道DOM操作开销是非常高的,每次DOM变动都会使浏览器重新计生成DOM树,然后重新渲染DOM。从性能的角度来看,DOM操作的次数越少越好。于是先用一个fragment来添加li节点,最后再将该fragment注入DOM中。这样我们只用一次DOM操作就注入了所有的li节点,比每生成一个li就注入要高效的多,随着列表的增长,性能方面的差距会越来越大。但我们这里的radioList直到最后才加入DOM,所以问题要小一些,但无论如何,记得这一点对以后会有帮助的。

最后只剩下响应点击事件的toggleRadioList函数了。这个函数的作用就是找到class为radio-list的列表,如果该列表已经可见了就隐藏它,否则就显示它:

function toggleRadioList() {  
  var list = document.querySelector('.radio-list');  
  if (list.style.visibility === 'hidden')  
    list.style.visibility = 'visible';  
  else  
    list.style.visibility = 'hidden';
 }

好了,零件都齐活了,脚本应该可以用了吧?等等,我最近刚想起来一个问题:全局命名空间的冲突。大家都直到上个leanpub的脚本,所有函数是直接暴露在global下的。这会有什么问题呢?可能会出现变量冲突!比如我这有一个toggleRadioList变量,如果荔枝网站本身有一个同名变量,冲突就出现了,荔枝网站的某部分功能也就会出现问题了。为了确保冲突不出现,我们需要将我们自己定义的脚本限制在一个命名空间内,不要直接暴露出来。最适合完成这个任务的自然就是IIF了,详情我在去年的一篇文卓中已有讲解,就不再详述了。简单来说,就是将我们所有的脚本都包含在这样一个函数中:

(function() { 
  //自定义脚本内容
})()

基于JS函数作用域定义,里面包含的所有变量和函数定义都只在该函数内部可见,实现了和global的隔离。

DeepinScreenshot20150501000457.png

到这里,脚本就差不多了。还有很多可以改进的地方,比如可以增加一个输入框让用户自己输入电台地址,然后保存到localStorage中;改进一下CSS让电台列表的显示更清晰。这些东西以后再慢慢来吧,已经快要午夜了,让我们选一个podcast,从陌生人的声音中取得一丝慰藉吧。

P.S: 完整脚本地址

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 125,101评论 16 537
  • 第一部分 准入训练 第1章 进入忍者世界 js开发人员通常使用js库来实现通用和可重用的功能。这些库需要简单易用,...
    如201608阅读 611评论 6 2
  • 1.JQuery 基础 改变web开发人员创造搞交互性界面的方式。设计者无需花费时间纠缠JS复杂的高级特性。 1....
    LaBaby_阅读 271评论 0 1
  • 房间里空荡的竟毫无生气也似得 而叙旧竟只够吃俩杯茶 未凑满的情谊 晚安来填补 太多的遗憾与失落
    我们的爱人笛安阅读 11评论 0 0
  • 接孩子放学回家,绕道印象风陵,今天突然想和孩子玩一圈再回。 玩够了,天黑了,来到摩托车前准备回家,才发现车钥匙不见...
    张新乐阅读 7评论 0 0