从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: 完整脚本地址
网友评论