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

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

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

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

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

同时咱们但愿,这三个问题的解决方案是不须要去更改gitbook的源码,是非侵入的。下面进行详细说明:前端

网站加速

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

起初咱们也想去扩容服务器带宽,但很明显,这须要银子,因此当时不少用户反馈网站卡时,咱们只能建议他们去github上去阅读。git

事情到了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。github

咱们的作法是将整个网站所有上传到CDN,而后在原站点中,将html中的图片、css、js资源引用连接所有替换为cdn连接,这样图片、css、js文件流量便会由CDN来承担。web

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

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

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、打赏。

相关文章
相关标签/搜索