小松的技术博客

六和敬

若今生迷局深陷,射影含沙。便许你来世袖手天下,一幕繁华。 你可愿转身落座,掌间朱砂,共我温酒煮茶。

再见 Disqus,我要自给自足了

Disqus 无疑是目前最好的评论插件,我的博客也一直采用它。但不知何时起,一堵墙让我们与它的距离变得很远很远,很是令人无奈。在不可访问的情况下,Disqus 脚本会严重拖累博文打开的速度,因此换掉它已经是必须要做的事情了,本来国内有很好的替代品“多说”,可惜其也不再维护了,最后还是决定自己写一个,权当做做练习。现在已经完成了基本的功能并且用在了本博客上,虽然没有 Disqus 那么强大,但作为评论交流,足矣。

这是一个插件式的评论系统,虽然功能简单,我也遇到了些许阻碍,通过不断的学习和尝试才得以完成。实践出真知,需要不断学习与总结。这篇博文会总结以下几个方面:

  1. OAuth
  2. 前端基础与 rollup 打包
  3. 跨域通信

OAuth

作为一个交互式评论系统,用户体系是必须的,否则就变成简单的留言系统了。用户系统完全可以自家的,但是一个游客过来,需要完成注册、上传头像等等问题才能开始评论,显然会打消游客留言的兴趣。其次,我的后台都不是 https 的,虽然登录注册有用 RSA 加密,但还是存在怕中间人攻击的可能,所以用成熟的 OAuth 来实现用户登录是不错的选择。原本是想实现微信登录的,可惜其不支持个人开发者,所以目前采用了 Github 账号登录。简单看一下 OAuth 的协议:

上图截自RFC 6749, 其原理还是相对简单,特别是在后端使用第三方库 passport 的情况下,简单集成几句代码就可以搞定了。在前端会稍微复杂一点,因为在通信过程中,(CD)是对 Github 的服务器访问,这一步跨域了,而且也别指望别人在 Access-Control-Allow-Origin 里加上你的域名,所以无法通过 Ajax 无缝完成所有通信。这里只能退而求其次,采取另开一个 window 的方式来完成通信流程,这应该也是主流的方案,这时就涉及跨域通信的问题了,这里的通信还好,因为所有流程都是在子 window 里完成,我们只需要在请求结束时告诉父 window 请求结果,所以只需要子 window 调用postMessage, 父 window 监听 message 就好。

前端基础与 rollup 打包

如今的前端界,各种 MVVM 框架层出不穷,所以越来越多新人变得依赖框架了,但框架本是应对大规模、复杂问题的解决方案,运用到这样一个小小的评论系统无疑大才小用,而且会引入很多无用的代码。即使是DOM封装的jQuery,我也嫌它体积过大,而且无用代码太多,所以我也没有用。那用些什么呢?

  • Angular.js有一个迷你型的 jqlite,拷贝其代码,在自适当修改,作为 DOM 层的封装,我需要的基本上就是 querySelector、css、attr、prop以及事件处理等封装。
  • 网络请求,肯定是用现在最势头正旺的 ajax 库:axios,其返回的都是 Pormise,使得异步更加优雅,而且提供了拦截器,方便集中处理网络错误、无权限等问题。
  • 使用 rollup 合并、压缩:
    • 使用 rollup-plugin-jst 处理 html 模板。
    • 使用 rollup-plugin-node-resolve、rollup-plugin-commonjs 处理非 es6 的模块。
    • 使用 rollup-plugin-babel 处理 es6 到 es5 的转换。
    • 使用 rollup-plugin-uglify 压缩代码。

跨域通信

为了模仿 Disqus, 我的使用方式也和它差不多:

<div id="comment_thread"></div>
<script type="text/javascript">
    window.soal_config = {
        page:'{{url}}',
        el: document.getElementById('comment_thread')
    };
    (function() {
        var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
        dsq.src = 'http://cgsdream.org/static/js/comment_embed.js';
        (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
    })();
</script>

使用者只需要在界面上嵌套上述代码, 并且给定页面的标识{{url}},就可以享受评论功能了。当然目前它还不足以作为一个可以共用的评论插件,但对于我自己来说是足够的了。

上面的代码主要加载了一个 embed.js,然后由 embed.js 创建 一个 iframe, 并且给iframe 指定加载路径,再补充上跨域相关的功能。这里之所以要跨域,主要是完成两个功能:

  1. iframe 高度自适应
  2. 用于 OAuth 的 window 要屏幕居中

要实现跨域,主要借助只是 postMessage 了。其它的还是 DOM 层面的操作。不过这些早已经有成熟的方案了,我也就直接拿来用了,我直接用了 iframe-size。这样 iframe 高度自适应就轻松解决。那剩下的就是 OAuth Window 屏幕居中的问题了。

OAuth Window 屏幕居中涉及了三个 window 对象,首先是使用评论插件的界面的 window 对象,姑且成为Host Window, 然后是 iframe 中的 window 对象 iframe Window, 最后还有个用于 OAuth 的 OAuth Window。网页上打开一个新的popup window 的默认显示在左上角,而我们期望 OAuth Window 是出现在浏览器中间的,因此我们要通过 Host Window 去计算位置,然后将位置信息传递给 iframe Window, 最终由 iframe Window 打开 OAuth Window。下面看看代码流程:

首先是iframe Window 想要打开 OAuth Window:

$el.querySelector('.js_login_github').on('click', function () {
    // 如果存在 window.parent,则发送获取位置信息的消息
    if(window.parent){
        if ('parentIFrame' in window) {
            window.parentIFrame.sendMessage('getSizeForOpenWindow')
        }
    }else{
        // 不是在 iframe 中, 那么就直接以自己的 window 来计算
        var dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : screen.left,
            dualScreenTop = window.screenTop  !== undefined ? window.screenTop : screen.top;
        var measureWidth = window.innerWidth || window.document.documentElement.clientWidth,
            measureHeight = window.innerHeight || window.document.documentElement.clientHeight;
        openAuthWindow(measureWidth, measureHeight, dualScreenLeft, dualScreenTop)
    }
})

Host Window 在 messageCallback 中处理消息:

iframeResizer({
    log                     : false, 
    inPageLinks             : true,
    messageCallback: function(messageData){
    // 当收到 getSizeForOpenWindow 的消息时,就通过 window 信息计算位置信息,然后返回给 iframe Window
        if(messageData.message === 'getSizeForOpenWindow'){
            if(iframe.iFrameResizer){
                var dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : screen.left,
                    dualScreenTop = window.screenTop  !== undefined ? window.screenTop : screen.top;
                var measureWidth = window.innerWidth || window.document.documentElement.clientWidth,
                    measureHeight = window.innerHeight || window.document.documentElement.clientHeight;
                iframe.iFrameResizer.sendMessage({
                    type:'windowSizeInfo',
                    windowWidth: measureWidth,
                    windowHeight: measureHeight,
                    dualScreenLeft: dualScreenLeft,
                    dualScreenTop: dualScreenTop
                })
            }
        }
    }
}, iframe)

iframe Window 收到位置相关信息后,执行打开逻辑:

window.iFrameResizer = {
    messageCallback: function(message){
        if(message && message.type === 'windowSizeInfo'){
            openAuthWindow(message.windowWidth, message.windowHeight, message.dualScreenLeft, message.dualScreenTop)
        }
    }
}

这样整个流程就结束了。

整个功能实现难度也不算太大,不过之前没有了解过 OAuth,也没有实践过跨域操作,还是花费了不少时间去了解与掌握这些东西,即使是 DOM 层的东西,理论学得多,实践得少,也是会处处碰壁的,还得多多实践才是。

←支付宝← →微信 →