Feb 13

为Nana添加音乐播放器功能

Lrdcq , 2017/02/13 19:15 , 日誌 , 閱讀(4551) , Via 本站原創
作为伪春菜的小工具,音乐播放器功能当然必不可少,而现在,用h5随手写一个音乐播放器当然是轻车熟路老司机上路信手拈来的事儿。稍等,来看看常见的网易云音乐的web端播放器,我们需要的最重要的一个特性就是播放器常驻页面,也就是说切换页面不会打断音乐播放。为此,事实上的页面结构是全站webapp化+ajax+动态渲染,再js修改title和url(pushState)——嗯,这些也是现代前端界非常非常常用的技能。不过稍等一下,我们的博客别说全站webapp化了,网页里面连一个ajax请求都找不到。事实上这个博客是非常非常单纯的独立php页面+混编模式写成的,如url中所看到的那样每一个页面就是一个.php文件混编出来的,前后端完全没有分离。那么,给我的选择就是要么完全重构网站做成webapp,要么用奇技淫巧结局这个问题。

初步思路

当然,我选择用技巧来解决这个问题。目标是在最小的代码改动的情况下完成这个feature。

首先考虑到需要有一个音乐播放器即audio对象常驻在页面中,所以单页面结构肯定跑不了了。如果无法用ajax加载页面,最简单的嵌入当前这个架构的页面的方法当然是嵌入iframe了。同时,只要不跨域,内外层页面是可以相互通讯的,那么外层页面可以读取到iframe页面的状态变化,并且通过监听onLoad修改外层页面的title和url(pushState)。因此,基本结构是:
music_player.html :iframe iframe.onload -> 修改title/url
iframe : 当前实际访问的页面
(function(){
  var frame = document.getElementById('page-frame');
  frame.onload = function () {
      history.replaceState({}, "", frame.contentWindow.location.href);
        document.title = frame.contentWindow.document.title;
  };
})();

其中有几个需要注意的细节是:

1. iframe通讯需要在同一个域下进行,所以需要完整运行的测试环境(而不是单纯的前端页面运行)。
2. iframe加载的页面是会在父级页面生成正常的history历史的,也可以正常的前进和后退。所以可以看到这里修改url用的是history.replaceState,用push的话就会产生一条多余的历史了。同时由于自带前进与后退,所以也不用自己手动接受onpopstate事件自己去iframe返回页面了,简直是一举多得的好事儿。

在此基础上,就可以先行实现音乐播放器了,这个不必多说。

页面混编

在前端层面把播放器功能完成了,但是整个框架才搭建到一半。还记得我们为什么要往回设置页面url么,当然是为了页面可重现。那么,显然,现在的情况下,把iframe的地址直接使用出来,刷新或者跳转后,得到的是原本iframe中的页面,无法得到音乐播放器的外层页面。考虑到地址可复制性和服务端可读性,最后决定在所有界面后边添加参数,带get参数中music=1的即跳转到播放器页面。

后端本着最小改动的原则,在全局入口index.php加入中断,一旦music=1,混编输出播放器页,其中这一页iframe的混编src即当前访问地址减去music=1。输出播放器页最好exit(0)即可。对应的,打开播放器的连接和frame.onload进行replaceState设置时都得在前端把music=1这个参数手动加上。

因此加上后端混编后,修改的代码包括:
index.php
...
require_once ("plugin/nana_musicplayer.php");
//other code
...

nana_musicplayer.php
<?php
if ($_GET["music"] == 1) {
  $url = ($_SERVER["REQUEST_URI"]) ? $_SERVER["REQUEST_URI"] : $_ENV["SCRIPT_NAME"];
  $url = str_replace('music=1', '', $nav);
?>
<!DOCTYPE html>
...
<iframe src="<?php echo $url; ?>"></iframe>
...
</html>
<?php
  exit(0);
}
?>

当然,这里只是举例子,实际html放置在了模版字符串中。就算是二次开发也要保持(旧技术)的一致。
最后的实际运行流程是,打开播放器是当前url+music=1,播放器的iframe刷新的每一个url都手动加上music=1。把iframe的url直接用来刷新播放器页,就权且关闭播放器了。

其他优化

用iframe的onload事件刷url和title是有一些问题的,很明显的感觉到刷得太迟了,特别是页面中包涵大图的时候,会等很久很久,用户会觉得很奇怪。因此,我们实际期待的是,如果有一个onStartLoad事件给我们,当然最好不过了。而html的世界是残酷的,就算有这么一个事件,html的title也不一定已经知道了,这是不现实的。那么换一个思路,如果我们iframe里面的页面,一旦知道的title,就通知一下父级界面不就好了。因此,我们在通用混编模版的title下面加了一段:
<title>{pagetitle}{blogname} - {blogdesc}</title>
<script type="text/javascript">window.top.postMessage('iframe_startLoad','*');</script>

然后,实际播放页面接受事件的代码是这样的:
(function(){
  var frame = document.getElementById('page-frame');
  window.addEventListener("message", function (e) {
    if (e.data == 'iframe_startLoad') {
      var url = frame.contentWindow.location.href;
      if (url.match(/\?/)) {
        if (url.slice(-1) == '&' || url.slice(-1) == '?') {
          url += "music=1";
        } else {
          url += "&music=1";
        }
      } else {
        url += "?music=1";
      }
      history.replaceState({}, "", url);
        document.title = frame.contentWindow.document.title;
    }
  }, false);
})();

这样的一通改动过后,整个iframe加载,history跳转的体验就非常native和流畅了。

播放器

讲了这么就,最后剩下h5播放器就十分简单了。我们只需要一个audio对象常驻在外层播放器页面就可以了。
  var player = document.createElement("audio");
  player.loop = true;
  if (window.localStorage[NANA_MUSIC_PLAYER_VOL]) {
    player.volume = window.localStorage[NANA_MUSIC_PLAYER_VOL];
  }
  var reload_music = function () {
    if (window.localStorage[NANA_MUSIC_PLAYER_URL]) {
      player.src = window.localStorage[NANA_MUSIC_PLAYER_URL];
      player.play();
    }
  };
  window.nanaMusicPlayer = player;
  reload_music();

在外层界面我们仅仅需要的逻辑是:

1.初始化audio对象,并且暴露到windows对象上。这样iframe就可以取到并且操作了。
2.另外播放器初始化数据,主要是音量和当前播放音乐地址。由于iframe内页是时常在刷新的,所以不能期待进入播放器后等待iframe加载完毕再来操作播放器开始播放。所以内页nana端操作播放器的同时,会把这些数据记录在localStorage中,这样外页播放器刷新之后就直接有播放数据可以继续了。今后可能还会加入当前播放进度之内的数据。

至于播放器暂停播放,进度条,音量调节等东西,当然是在内页的nana模块的。nana模块和外页播放器的交互操作只包括获取实际播放器对象和设置当前播放url和音量:
var player = window.top.nanaMusicPlayer;
window.localStorage[NANA_MUSIC_PLAYER_URL] = music[0];
window.localStorage[NANA_MUSIC_PLAYER_VOL] = d;

其他的就直接操作持有的player即可了。简直是轻松加愉快。

至此第一版Nana音乐播放器就完成了。当然在播放器层面功能是非常残缺的,但是在前端页面架构功能上,基本和网易云音乐的逻辑一致了,在这种混编的前后端开发模型下,还是可喜可贺的。点击右下角Nana体验咯。
logo