使用Disqus JS让Disqus评论在国内可用
Disqus在国内近几年一直访问不了,那么许多平常访问的游客甚至不知道博客有评论功能,这无疑是让人遗憾的事情。
要想让Disqus的功能可用,可以使用Disqus API + 反向代理的方法,前端模拟Disqus的评论界面,再使用一个服务器反向代理Disqus API的请求,那么就可以让评论可以在国内网显示出来。
笔者对Disqus JS项目早有所耳闻,但是一直没有精力去使用,今天正好将其调试好,然而碰到了一些我意想不到的问题,导致花了很长时间。
神奇的Disqus JS
Disqus JS是一个前端脚本和反向代理服务器结合的轻量化的显示Disqus评论的方案。其比起其他的Disqus反向代理方案的优点是可以使用serverless的纯反代功能的服务器(例如免费的Cloudflare Workers),这样的话就不用担心自己的云服务器可能会换的问题。当然,这也限制了其功能,让它在基础模式下不能够发表评论。
其实使用起来很简单,遵循Github 项目上的介绍,很快就能够完成。主要就是在网页中引入javascript脚本和CSS样式表,并初始化一个js类即可,从CDN引入资源如下。
1 | <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/disqusjs@1.3/dist/disqusjs.css"> |
初始化的类的过程如下
1 | <script> |
但是我却碰到了许许多多的坑。
Disqus JS与InstantClick的兼容
在之前的博文中介绍过InstantClick这个基于PJAX机制的轻量Javascript框架,能够将网页变为单页式应用,减小跳转延迟。但是InstantClick的修改HTML DOM的行为会导致一些加载上的问题。
例如,我将引入js和css资源的过程与执行初始化的过程都放在<body>
内部,理论上按顺序执行初始化应该在加载好js脚本之后,但是InstantClick在跳转到文章页面就先执行了初始化,而此时js脚本还未加载好,就会出现DisqusJS is undefined
的错误。
解决这个错误倒也不难,只要知道了原因,就可以使用onload
事件去执行初始化。因此我现在引入资源的过程如下,让css与js都异步加载。
1 | <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/disqusjs@1.3/dist/disqusjs.css" media="none" onload="if(media!='all')media='all'"> |
在古早版本中的Disqus JS中还有其他与PJAX机制兼容的问题,但是现在的版本已经对其进行了修复。
Disqus的identifiers机制
我在解决上了上面一个PJAX兼容性问题后,接着又碰到了下一个问题。我的评论区提示 “ 当前Thread尚未创建。是否切换至完整Disqus模式 ”。我便去Github项目的文档里找有没有解释,当时看到这条issue #38: Thread初始化失败 中作者回复:
Since DisqusJS is only a pure front-end project which doesn't require any server, so create thread at Disqus requires user manually switches to Disqus Mode.
意即,每个网页的Disqus评论是需要在Disqus的服务器中创建一个Thread来储存评论的,如果之前没有创建过,那么作为一个轻后端的项目在基础评论模式下Disqus JS是不会调用API去创建Thread的。
这话是没有问题的,但是问题在于我的Thread尚未创建的原因不是因为“这个网页”没有创建Thread,而是网页的identifier没有对应上,导致无法获取该网页的Thread(即使它存在)。
什么是identifiers呢?Disqus在JavaScript configuration variables和What is a Disqus identifier?中对Disqus如何区别不同的网页,即建立评论Thread与网页的对应关系的方法进行了介绍。总结而言,一个Thread真正unique的id,是它的thread id,而identifiers和urls都是用来对应到thread id的。如果我们保证identifier或url的唯一性,我们就可以通过identifier或url找到对应的唯一的评论Thread。如果identifiers不唯一(多个网页对应了一个identifier),那么就无法保证通过一个identifier对应一个网页。
在使用Disqus原生的js框架时,Disqus推荐在window.disqus_config
变量中写明page.url
、page.identifier
与page.title
,这样可以帮助Disqus给网页创建唯一的identifier。如果没有配置disqus_config,那么Disqus会使用网页的Url作为page.url
,并以此来索引Thread
id。
我的Disqus使用的问题在于,原来主题中在使用Disqus时是使用下面的两行javascript脚本来注册window.disqus_config,而使用window.location的问题在于,在localhost上和在有域名的服务器上浏览网页时会产生不同的location,这就导致一个网页无法对应唯一的identifier和url。
1 | this.page.url = window.location.toString() |
我发现这个问题的方法,是发现使用Disqus API
threads/list.json
请求对应的identifier的thread列表时,会返回一堆thread而不是一个唯一的thread(原因就是identifier不唯一对应网页时,Disqus
API会返回该shortname下的所有Thread),而Disqus
JS认为这种情况下属于没有创建Thread。
知道了这个问题后,我去Disqus网站上把之前注册的网站删除(意味着删除了所有的Thread),重新建立我网站的评论系统。
你以为这样就能正常工作了吗? Too young.
Disqus API的中文编码
我在此时使用下面的代码来建立identifier,使用url_for是因为想给作为identifier的path前加个斜杠/。然而我万万没想到的是,这个行为让我花了很多的时间去寻找问题所在。
1 | identifier: '<% url_for(page.path) %>' |
url_for
的功能远不止给page.path
加个斜杠,他还会对url进行编码,相当于再调用javascript的encodeURI
函数,但是我当时并不知道这个问题,还以为是Disqus
JS或浏览器在发GET请求时自动把url给编码了。
我来分析一下对encodeURI(page.path)会产生什么问题。
在使用完整评论模式(第一次使用会自动创建Thread)时,创建的thread的identifier是encodeURI(page.path),然后Disqus的数据库里的identifier值就是encodeURI(page.path),即Disqus不会在创建Thread时对identifier解码。
但是在使用Disqus
API查询Thread列表时,他会对GET的参数进行URL解码,即相当于调用decodeURI,于是在与数据库里的identifier匹配时,它拿decodeURI(encodeURI(page.path))
(也就是page.path)和数据库进行匹配,而数据库里只存了encodeURI(page.path),那么自然匹配是失败的,然后threads/list.json
API就会返回所有的Thread。
经过我的测试,如果你的GET方法的参数为thread=$IDENT
,Disqus
API服务器会首先调用ident:$IDE = decodeURIComponent($IDENT)
,然后会在数据库里查询$IDE
,但是像我说的那种情况,在数据库里的键值为encodeURI($IDE)
,自然是查不到的。
令我疑惑的是,Disqus在自己的网站上I'm receiving the message "We were unable to load Disqus."说自己不支持非ascii码作为url,但是我即使直接通过curl请求未编码的中文,也不存在问题。
所以我现在不会主动对path进行编码,而是靠浏览器帮我编码传输。
当然,这里有一点很关键,我们不会主动对page.path
进行编码,而是靠浏览器帮我们进行了GET参数的自动编码。而不同操作系统的不同浏览器对GET参数自动编码时采取的方案是不同的,虽然说现在主流浏览器都是采取%符号+utf-8编码,但是也有IE浏览器是使用系统编码,详情可以参考关于URL编码,因此依赖浏览器还是不稳定的,理论上Disqus
JS应该自己对GET的参数进行encodeURIComponent后再发送请求。
关于encode这点,我已经给Disqus JS提了Pull Request并合并进master了。
现在,理论上你即使用IE浏览器也可以看到评论框的基本模式(没有安全补丁的xp系统由于SSL证书问题可能打不开网页,IE8之前的浏览器也不太行)。
gulp-babel 对展开语法(Spread syntax)的错误转换
当我fork了Disqus JS的源码修改GET行为的参数编码时,我在本地测试上遇到了问题。该项目使用gulp生成部署时代码,使用gulp-babel精简javascript代码。我使用gulp-babel对整个javascript脚本进行精简后得到部署环境中的js文件,然后在本地服务器运行并用浏览器测试时,我发现插件不能正常工作了。奇怪的是,当不用gulp-babel精简时的原脚本是可以正常运行的。
那么基本可以确定是gulp-babel让脚本转换时出了问题,经过排查,我发现问题出在将HTMLCollection
转换为Array
的过程中。原代码是[...aTag]
,其中aTag
为一个HTMLCollection
,这里用了展开语法(Spread
syntax),可以将可迭代的对象展开为数组,是ES6标准后来提出的语法。观察生成后的js文件,gulp-babel将对应的代码转换为了[].concat(aTag)
,如果要达到类似的效果,concat
的参数只能是数组,而不会把可迭代对象展开,[].concat(aTag)
的写法会把aTag作为一个Object追加为数组的元素。因此这两者不是等价的。
那么,我们应该使用什么来达到这一行为而且不会被gulp改变语义呢?我们目前的需求是将HTMLCollection
转换为Array
,而HTMLCollection
为什么可以转换为数组?首先,它是一个array-like的对象,更详细地说,它有length
属性,且可以用数字作索引,那么这样的对象就有转换为Array
的条件。另外,HTMLCollection
在ES6标准中也成为了一个可迭代对象,而可迭代对象也是可以通过一些方法转换为Array
的。
下表是我总结的三种可以将其转换为数组的方法,其中Array.prototype.slice.call
支持的参数是array-like的对象,且支持的浏览器最为广泛,我不知道有什么浏览器是不支持它的。Array.from
支持array-like的对象和可迭代对象,功能最丰富,但是也是ES6标准开始才有的方法。Spread
syntax也是ES6标准开始的语法,且由于会被gulp-babel错误转换直接否决。
由于我们这里的需求只是将HTMLCollection
转换为Array
,因此选择支持最广泛也没什么缺点(可能代码长了点)的Array.prototype.slice.call
即可。
Parameter | Array.prototype.slice.call | Array.from | Spread Syntax |
---|---|---|---|
Array-like objects with a length property and indexed elements | Yes | Yes | No |
Iterable objects | No | Yes | Yes |
当然,gulp-babel有插件支持对Spread syntax的转换, @babel/plugin-transform-spread可以完成该功能。
One More Thing
前面我们提到了IE浏览器在进行GET查询时对参数的编码行为和其他浏览器不同,那么我们不妨再谈一谈对IE浏览器兼容做的工作。
众所周知,IE11是不兼容ES6标准的,因此如果js代码中有ES6标准的用法都是不能直接运行于IE浏览器的,故我们如果要考虑IE浏览器,用到的ES6标准中的语法和新接口都需要使用polyfill的方法来进行兼容。
Disqus JS用到了ES6标准中的fetch API和Promise,这两者都是无法在IE中运行的。在Disqus JS的readme也提到了需要用polyfill的实现来代替。那么,如何判断当前浏览器是IE然后动态地加载polyfill代码呢?
我一开始使用对Request Header进行判断的检测方法,但是随着我对Feature detection的进一步了解,我知道了如果是想实现跨浏览器的支持,那么Feature detection比针对浏览器版本的检测更加有效和全面。例如,我当初只检测了IE,但如果用户用的是另一个也不支持ES6标准的古老浏览器呢?如果用户切换了Header呢?
针对浏览器的版本判断,总是需要考虑更多的内容,需要想得更加全面,也需要精确地了解哪个版本的浏览器不支持什么标准的什么特性,而这是极其消耗精力的。如果使用Feature detection,我们能够花更少的精力就做到跨浏览器一致性。
例如,我只需要使用下面的代码就能简单地判断浏览器是否支持fetch特性。同样的,不同的特性都可以进行判断而决定是否对某一功能使用polyfill。这样能够做更精细而全面的跨浏览器一致性行为。
1 | if (window.fetch) { |
时光飞逝,Windows XP于2001年发售,截止至今已经19年了,Windows 7是2009年发售的。不算Vista,至少2009年后基本没有多少新增个人Windows XP的用户了。理论上,一台PC用了11年还能不换零件坚持用简直是奇迹。(其实我家里还有两台Windows XP的PC)
根据statcounter的统计数据,全球范围内截止至2020年7月,Windows在桌面操作系统的市场份额是77.68%,其中XP又占Windows阵营中的0.82% (在中国是2.87%),也就是说Windows XP占所有桌面操作系统的0.64%,不知道其中有没有包括众多的工业用机。
不过我想,从人群划分来看,应该几乎没有使用Windows XP的用户会点击到这个页面吧。
- 文章链接: https://renzibei.com/2020/07/26/use-disqusjs/
- 版权声明: 本网站所有文章(包含文字、图片等内容)除特别声明外,均系作者原创,采用 CC BY-NC-ND 4.0 许可协议。引用与转载时请遵守协议、注明出处。