阅读 896

《Flutter实战》电子书官网加速、定制分享(网站通过Gitbook生成)

本文主要通过《Flutter实战》电子书官网案例,给大家分享一下如何在不修改gitbook源码的基础上对gitbook生成的网站进行站点加速、PV统计和自定义卡片插入。本文同时也是ajax-hook(一个用于全局拦截浏览器ajax请求的库)的一个最佳实践。

去年的某一天(忘了具体时间),我们在github上开源了《Flutter实战》电子书后,用户反响强烈,期望能够提供线上电子书。为了让用户能够便捷的在线观看,我们使用gitbook生成了《Flutter实战》电子书的web站点,地址https://book.flutterchina.club/。

《Flutter实战》电子书官网上线后,我们遇到了三个问题:

  1. 在访问高峰时段,受服务器带宽限制,网站非常卡。
  2. Gitbook 生成的网站,是类SPA网站,在无代码侵入的情况下,如何正确统计PV。
  3. 如何给每个章节中插入我们自定义的卡片。

同时我们希望,这三个问题的解决方案是不需要去更改gitbook的源码,是非侵入的。下面进行详细说明:

网站加速

电子书上线后,日均PV达到5W,当时,我们租的腾讯云EMS,由于我们做社区都是共享免费的,没有任何收入来源,所以服务器费用都是自讨腰包,因此带宽当时选的是最低配1M的,这就导致在上午和下午的访问高峰时段,网站非常卡,经常一个页面要等6-10s才能打开

起初我们也想去扩容服务器带宽,但很明显,这需要银子,所以当时很多用户反馈网站卡时,我们只能建议他们去github上去阅读。

事情到了2020年3月份终于迎来了转机,CDN厂商猫云找到了我们,并且愿意给我们做免费的CDN加速。但是对gitbook生成的网站上CDN时需要对工程进行一些改造。我们的CDN加速域名是https://pcdn.flutterchina.club ,我们可以将整个网站全部上CDN,这就有一个问题,我们需要在用户访问https://book.flutterchina.club/ 时给重定向到https://pcdn.flutterchina.club ,但由于我们https://book.flutterchina.club/这个域名已经外发很久,在用户群体中已经有了认知,并且这个域名语义也比较明确,因此我们不想更改域名,所以我们需要在原网站中,让更多的内容上CDN。

我们的做法是将整个网站全部上传到CDN,然后在原站点中,将html中的图片、css、js资源引用链接全部替换为cdn链接,这样图片、css、js文件流量便会由CDN来承担。

光做到这点还不够,因为电子书每一个章节的内容都是一个html,而这些html请求还是在我们的服务器,而整个网站流量占比中,html请求流量占了80%,这是因为电子书基本都是文字,而文字最终会包含在html中。

通过研究,gitbook网站在章节切换时,并不是一次链接跳转,而是通过ajax请求拉取新章节html文档,然后再解析出正文部分替换掉当前页面的正文。那么方法便很清楚了:我们只要将ajax请求的流量也打到CDN就可以了!但是问题来了,我们如何获得ajax请求的时机并进行链接替换呢?我们先抛出问题,在本文最后解答。

PV统计

我们的网站是使用百度统计来记录页面浏览情况,但由于章节切换时是通过ajax请求拉取新章节html文档,所以只有当用户第一次进入我们网站时页面的PV才会被统计到,而后续站内跳转都不能被统计到,这和SPA网站路由切换不能被统计的原因是相同的。经过分析,gitbook生成的网站页面在切换时是通过浏览器history AP来同步浏览记录的,这和单页应用框架Vue/React Router的history模式原理是一致的,即:页面跳转时,调用history.pushState来插一条记录,此时浏览器的url也会更新。那么,要统计每一个章节的浏览记录,则只需要去监听浏览器的url是否变化,在url变化时,手动调用一次统计上报。所以,现在问题转化为如何监听url发生变化?

我们知道,url变化后浏览器不跳转的情况只有两种,一种是hash(哈希,url中#号后面的内容)发生了变化,对应的会触发onhashchange事件;另一种就是通过调用history.pushState导致,而我们的情况正式这一种,那在调用history.pushState时浏览器有没有像hash变化时会触发一个事件呢?很遗憾,答案是否定的。那么我们该怎么办?

起初世界上没有光,于是上帝说要有光,于是便有了光。

没有事件,我们造一个不就得了。

怎么造?思路我们可以参考杀毒软件,杀毒软件的主动防御原理是通过拦截应用的一些操作行为来辨识是否存在异常,具体实现是会在底层拦截操作系统API,拦截的方式是在操作系统API入口里先写入跳转到自定义方法的指令,这样一来,当应用程序调用某个系统API后,便会首先跳转到杀毒软件定义的方法中,然后杀毒软件结合应用的整个行为链条来判断是否存在异常,如果没有异常,则跳回原API继续执行,如果存在异常则阻断。这种拦截方式,有一个专用的术语叫API挂钩(Hook)。我们可以采用这种思想来拦截history.pushState,代码如下:

function hookAPI(api, ob, fn) {
    return function () {
        var result = api.apply(ob, [].slice.call(arguments));
        //延迟1s上报
        setTimeout(fn, 1000);
        return result;
    }
}

if (history.pushState) {
    history.pushState = hookAPI(history.pushState, history, function(){
     //上报PV
     _hmt.push(['_trackPageview', location.pathname]);
    });
}
复制代码

现在我们解决了PV上报的问题,但是,现在请打开思路,在我们的网站中,难道我们就只能通过这一种方式来获得页面切换的时机吗?可以告诉你,答案是否定的!那还有什么方法呢?我们先按下不表,等再聊完最后一个问题,我们再给出答案。

给文章内容中插卡片

《Flutter实战》的纸质书3月中旬终于出版了,为了告知读者,我们需要在电子书网站上在每一篇文章顶部插入一个购买卡片,效果如下:

插入卡片

直接可以想到的作法有3种:

  1. 找到gitbook的布局代码,看看是否可以自定义;经过简单几次尝试,发现我们要插入卡片的右侧内容区域在站内页面跳转时都是动态生成的,并不能再gitbook的布局代码中修改,尝试失败。
  2. 给每一篇文章内容中通过脚本插一段卡片的的h5代码。可行!但不优雅。
  3. 在页面切换后,通过jQuery动态将卡片dom插入指定区域。代码相对简单(用过jQuery的人自然懂),但是现在问题又转化为如何获取页面切换的时机,这个我们在上面PV统计部分已经讨论过,不在赘述。

最终解决方案-银弹

现在请大家仔细想想上面提到的三个问题,有没有一个银弹,可以同时解决这三个问题。答案是肯定的!现在我们先回顾一下上面三个问题的关键:

  1. 从cdn请求html,问题关键是如何获得ajax请求的时机并进行连接替换。

  2. PV统计和文章内容中插卡片,问题关键是如何获取页面切换的时机。

而我们已经知道,页面切换时是要通过ajax请求获取新页面的html文档的,所以,我们只需要能截获到ajax请求的时机,便可以完成上面的三件事!

那么如何拦截ajax请求呢?通过分析gitbook生成的网站的代码,发现依赖了jQuery,如果gitbook的ajax请求都是通过jQuery发起,那么我们只需配置一下jQuery ajax的钩子即可,但经过试验,发现只有请求search_plus_index.json这么一个是通过jQuery发起的,其它的ajax请求是gitbook自己通过XMLHttpRequest来发起的。那么,我们想在不侵入gitbook代码的基础上来拦截ajax请求,怎么做?其实答案很简单,如果读者看过我之前的博客,那么应该知道我曾开源过一个专门用于拦截浏览器XMLHttpRequest的库ajax-hook,他非常精巧,具体使用可以参考github上介绍,我们这里直接使用即可:

// 通过ajax-hook库提供的hook方法直接全局拦截XMLHttpRequest对象
ah.hook({
    open: function (arg, xhr) {
        // 如果不是本地调试,ajax流量直接打到cdn, 注意CDN服务器启用了CORS跨域
        if (location.hostname !== 'localhost') {
            if (arg[1][0] === '.') arg[1] = arg[1].slice(1);
            arg[1] = "https://pcdn.flutterchina.club" + arg[1]
        }
    },
    setRequestHeader: function (arg, xhr) {
        // 发现gitbook在发起ajax请求是会设置一些自定义头,为了使CORS生效,干掉这些自定义头
        // 使跨域请求变为一个简单请求,避免预检失败。
        if (arg[0] !== 'Accept') return true;
    },
    onload:function(){
        setTimeout(function(){
          //添加实体书卡片
           if ( $("#book-search-results .ad").length === 0) {
                $(".ad").clone().show().prependTo("#book-search-results")
            }
          //统计PV, 网站打开时会首先请求一次search_plus_index.json文件,如果判断是该文件请求,则排除
           var extension=xhr.responseURL.split(".").pop()
           if(extension!=='json'){
               _hmt.push(['_trackPageview', location.pathname]);
           }
        });
    }
})
复制代码

注意:我们CDN开启了CORS,允许前端跨域,关于CORS ,如不清楚请自行百度。

现在,我们的三个问题就全部解决了,加速后,目前网站访问高峰时段也能接近于秒开,同时PV已能准确收集,卡片也正常插入,读者可以自行去《Flutter实战》电子书官网体验效果。

带点货

《Flutter实战》电子书官网加速和定制正是ajax-hook开源库的一个最佳实践,ajax-hook 最初是笔者为了研究目的写的一段代码,当时由于代码量只有50行,笔者认为很精巧,于是便传到了github,没想到发到github上之后受到了很多人star,更没想到star能破千,还有很多一线互联网公司都在用。考虑到最初的代码只是一个拦截的核心功能(虽然后来有修改)但API还是比较偏底层,使用起来很不方便,并且容易出错,为了彻底解决这个问题,于是这个周末,笔者又重构了一次,发布了全新的2.0版本,在2.0版本中,增加了更易用、更强大的的proxy(...) API,读者有兴趣可以去github ajax-hook 主页了解,如果觉得有用,也欢迎star、打赏。