├── docs ├── CNAME └── index.html ├── .npmignore ├── .gitignore ├── .travis.yml ├── renovate.json ├── package.json ├── LICENSE ├── gulpfile.js ├── dist ├── disqusjs.css └── disqus.js ├── src ├── disqusjs.css └── disqus.js └── README.md /docs/CNAME: -------------------------------------------------------------------------------- 1 | disqusjs.skk.moe -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/index.html 2 | 3 | dist/index.html 4 | 5 | docs/ 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | 4 | *.log 5 | 6 | node_modules 7 | 8 | .idea 9 | 10 | .directory 11 | 12 | _build 13 | 14 | public 15 | 16 | src/index.html 17 | 18 | dist/index.html 19 | 20 | package-lock.json 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "9" 4 | before_install: 5 | - yarn global add gulp-cli 6 | install: 7 | - yarn 8 | script: 9 | - gulp build 10 | cache: 11 | yarn: true 12 | directories: 13 | - node_modules -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "assignees": [ 3 | "SukkaW" 4 | ], 5 | "extends": [ 6 | "config:base" 7 | ], 8 | "vulnerabilityAlerts": { 9 | "labels": [ 10 | "security" 11 | ], 12 | "assignees": [ 13 | "SukkaW" 14 | ] 15 | }, 16 | "reviewers": [ 17 | "SukkaW" 18 | ], 19 | "commitMessageAction": "update", 20 | "semanticCommits": true, 21 | "semanticCommitType": "chore" 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "disqusjs", 3 | "version": "1.3.0", 4 | "description": "Alternative DISQUS - Render comments components from Disqus API", 5 | "main": "dist/disqus.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/SukkaW/DisqusJS.git" 12 | }, 13 | "keywords": [ 14 | "Disqus", 15 | "DisqusJS", 16 | "SukkaW", 17 | "API", 18 | "XHR", 19 | "JSON" 20 | ], 21 | "author": "Sukka", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/SukkaW/DisqusJS/issues" 25 | }, 26 | "homepage": "https://github.com/SukkaW/DisqusJS#readme", 27 | "dependencies": {}, 28 | "devDependencies": { 29 | "@babel/core": "^7.10.1", 30 | "@babel/preset-env": "^7.10.1", 31 | "gulp": "^4.0.2", 32 | "gulp-autoprefixer": "^7.0.1", 33 | "gulp-babel": "^8.0.0", 34 | "gulp-clean-css": "^4.3.0", 35 | "gulp-header": "^2.0.9", 36 | "gulp-uglify": "^3.0.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Sukka 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const uglify = require('gulp-uglify'); 3 | const babel = require('gulp-babel'); 4 | const autoprefixer = require('gulp-autoprefixer'); 5 | const cleanCSS = require('gulp-clean-css'); 6 | const header = require('gulp-header'); 7 | const pkg = require('./package.json'); 8 | 9 | 10 | const jsBanner = ['/*! DisqusJS v<%= pkg.version %>', 11 | 'Sukka (https://skk.moe)', 12 | 'https://disqusjs.skk.moe', 13 | '<%= pkg.license %> License */', 14 | ].join(' | '); 15 | 16 | const cssBanner = ['/*! DisqusJS - Default Theme | v<%= pkg.version %>', 17 | 'Sukka (https://skk.moe)', 18 | 'https://disqusjs.skk.moe', 19 | '<%= pkg.license %> License */' 20 | ].join(' | '); 21 | 22 | 23 | const browserslist = [ 24 | 'last 3 versions', 25 | 'since 2016', 26 | '> 1%', 27 | 'Chrome >= 49', 28 | 'Firefox >= 50', 29 | 'ie >= 11', 30 | 'Safari >= 9', 31 | ] 32 | 33 | const configs = { 34 | autoprefixer: { 35 | overrideBrowserslist: browserslist 36 | }, 37 | cleanCSS: { 38 | compatibility: 'ie11' 39 | }, 40 | }; 41 | 42 | gulp.task('minify-js', () => gulp.src('src/**/*.js') 43 | .pipe(babel({ 44 | 'presets': [ 45 | ['@babel/env', { 46 | 'targets': browserslist, 47 | 'loose': true 48 | }] 49 | ] 50 | })) 51 | .pipe(uglify({ 52 | keep_fnames: false 53 | })) 54 | .pipe(header(jsBanner, { pkg })) 55 | .pipe(gulp.dest('dist'))); 56 | 57 | gulp.task('minify-css', () => gulp.src('src/**/*.css') 58 | .pipe(autoprefixer(configs.autoprefixer)) 59 | .pipe(cleanCSS(configs.cleanCSS)) 60 | .pipe(header(cssBanner, { pkg })) 61 | .pipe(gulp.dest('dist'))); 62 | 63 | gulp.task('build', gulp.parallel('minify-js', 'minify-css')); 64 | 65 | gulp.task('default', gulp.parallel('build')); -------------------------------------------------------------------------------- /dist/disqusjs.css: -------------------------------------------------------------------------------- 1 | /*! DisqusJS - Default Theme | v1.3.0 | Sukka (https://skk.moe) | https://disqusjs.skk.moe | MIT License */#dsqjs *{margin:0;padding:0}#dsqjs a{text-decoration:none;color:#076dd0}#dsqjs .dsqjs-hide{display:none!important}#dsqjs .dsqjs-disabled{cursor:not-allowed;opacity:.5}#dsqjs #dsqjs-msg{text-align:center;margin-top:4px;margin-bottom:4px;font-size:14px}#dsqjs #dsqjs-msg .dsqjs-msg-btn{cursor:pointer}#dsqjs .dsqjs-bullet{line-height:1.4;margin:0 2px}#dsqjs .dsqjs-bullet::after{color:#c2c6cc;content:"·";font-weight:700}#dsqjs .dsqjs-clearfix:after,#dsqjs .dsqjs-clearfix:before{display:table;content:"";line-height:0;clear:both}#dsqjs .dsqjs-nav{position:relative;margin:0 0 20px;border-bottom:2px solid #e7e9ee}#dsqjs ol,#dsqjs ul{list-style:none;list-style-type:none}#dsqjs .dsqjs-no-comment{text-align:center;font-size:16px;line-height:1.5;word-wrap:break-word;overflow:hidden;color:#2a2e2e;margin-bottom:6px}#dsqjs .dsqjs-nav-tab{float:left;text-transform:capitalize;font-size:15px;padding:12px 8px;color:#656c7a;display:block;margin:0 15px 0 0;font-weight:700;line-height:1;position:relative;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}#dsqjs .dsqjs-nav-tab:last-child{margin:0}#dsqjs .dsqjs-tab-active{color:#2a2e2e}#dsqjs .dsqjs-tab-active>span:after{content:" ";display:block;height:2px;background-color:#076dd0!important;position:absolute;bottom:-5px;left:0;right:0}#dsqjs .dsqjs-post-list .dsqjs-post-item{position:relative;margin-bottom:16px}#dsqjs .dsqjs-post-list .dsqjs-post-avatar{float:left;margin-right:10px;position:relative;background:#dbdfe4;padding:0;display:block;border-radius:4px}#dsqjs .dsqjs-post-list .dsqjs-post-avatar img{width:44px;height:44px;display:block;border-radius:4px}#dsqjs .dsqjs-post-list .dsqjs-post-header{line-height:1;font-size:14px;margin-bottom:3px}#dsqjs .dsqjs-post-list .dsqjs-post-header .dsqjs-post-author{color:#656c7a;font-weight:700}#dsqjs .dsqjs-post-list .dsqjs-post-header .dsqjs-admin-badge{color:#fff;background:#687a86;padding:1px 3px;margin-left:4px;font-size:12px;line-height:1;font-weight:700;border-radius:3px;display:inline-block;position:relative;top:-1px;left:1px}#dsqjs .dsqjs-post-list .dsqjs-post-header .dsqjs-meta{display:inline-block;font-size:12px;color:#656c7a}#dsqjs .dsqjs-post-body{font-size:15px;line-height:1.5;word-wrap:break-word;overflow:hidden;color:#2a2e2e}#dsqjs .dsqjs-post-body code{padding:.2em .4em;margin:0;font-size:85%;background:#f5f5f5;color:inherit;border-radius:3px}#dsqjs .dsqjs-post-body pre{padding:.5em;overflow:auto;font-size:85%;line-height:1.45;border-radius:3px;background:#f5f5f5;margin:.5em 0}#dsqjs .dsqjs-post-body blockquote{padding:0 .8em;margin:.5em 0;color:#6a737d;border-left:.25em solid #dfe2e5}#dsqjs .dsqjs-post-body p:last-child{margin:0}#dsqjs .dsqjs-post-list.dsqjs-children>li{margin-left:30px}@media (min-width:768px){#dsqjs .dsqjs-post-list.dsqjs-children>li{margin-left:48px}#dsqjs .dsqjs-post-list .dsqjs-post-avatar{margin-right:12px}#dsqjs .dsqjs-post-list .dsqjs-post-item{margin-bottom:20px}}@media (min-width:1024px){#dsqjs .dsqjs-post-list.dsqjs-children>li{margin-left:60px}}#dsqjs .dsqjs-post-list.dsqjs-children .dsqjs-post-avatar img{width:38px;height:38px}#dsqjs .dsqjs-load-more{font-size:14px;font-weight:400;display:block;text-align:center;padding:11px 14px;margin:0 0 24px;background:#687a86;color:#fff;cursor:pointer}#dsqjs .dsqjs-load-more:hover{opacity:.8}#dsqjs footer{text-align:right;line-height:1.5;padding-top:10px;padding-right:10px;border-top:2px solid #e7e9ee;margin-top:12px;font-weight:700;font-size:16px;color:#555}#dsqjs .dsqjs-disqus-logo{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 438 80'%3e%3cpath fill='%23575B5D' d='M30.2 1.6H1v76h28.9C57 77.6 73 61.3 73 39.4v-.2c0-22-15.7-37.6-42.9-37.6zm21.3 38.1c0 12.3-8.4 19.3-21 19.3H22V20.3h8.5c12.6 0 21 7 21 19.2v.2zm35.6 38h21.2V1.5H87.1v76zm70-47.4c-10.4-2.4-13-4-13-7.4v-.2c0-2.7 2.4-5 7.6-5 6.7 0 14.3 2.7 21.2 7.6l10.6-14.9A47.9 47.9 0 0 0 152.2.3c-18.3 0-29.4 10.2-29.4 24.3v.2c0 15.7 12.4 20.3 28.6 24 10.4 2.3 12.9 4 12.9 7.2v.2c0 3.3-3 5.2-8.7 5.2-8.8 0-17.2-3.1-24.7-9l-11.7 14a53.1 53.1 0 0 0 35.6 12.5c18.5 0 30.7-9.2 30.7-24.7V54c0-14.3-10.8-20-28.3-23.7zm120.7 9.3v-.2A39.5 39.5 0 0 0 236.9.1c-23.4 0-41 17.7-41 39.5v.2a39.5 39.5 0 0 0 40.8 39.4c8.7 0 16.6-2.5 23.1-6.8l8.4 7.5L279 68.1l-7.9-6.6a38 38 0 0 0 6.8-21.9zm-21.4.5c0 2.6-.5 5-1.3 7.3l-10.4-9.3-10.6 12 10.5 9a21.7 21.7 0 0 1-7.7 1.4c-11.6 0-19.4-9.7-19.4-20.7v-.2c0-11 7.7-20.5 19.2-20.5 11.7 0 19.7 9.7 19.7 20.7v.3zm83.5 4.3c0 10.6-5.5 15.6-14 15.6s-14-5.2-14-16.1V1.6h-21.4v42.7C290.5 68 304 79 325.7 79s35.6-10.8 35.6-35.3V1.5h-21.4v42.8zm68.9-14.1c-10.6-2.4-13.2-4-13.2-7.4v-.2c0-2.7 2.5-5 7.6-5 6.8 0 14.4 2.7 21.3 7.6l10.6-14.9A47.9 47.9 0 0 0 403.8.3c-18.3 0-29.5 10.2-29.5 24.3v.2c0 15.7 12.5 20.3 28.7 24 10.3 2.3 12.8 4 12.8 7.2v.2c0 3.3-3 5.3-8.7 5.3-8.8 0-17.1-3.2-24.6-9.2l-11.7 14A53.1 53.1 0 0 0 406.4 79c18.5 0 30.7-9.2 30.7-24.7V54c0-14.3-10.8-20-28.3-23.7z'/%3e%3c/svg%3e");background-position:50% 50%;background-repeat:no-repeat;display:inline-block;height:12px;width:65.7px}#dsqjs .dsqjs-order{display:-webkit-box;display:-ms-flexbox;display:flex;float:right;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-top:10px;margin-bottom:12px}#dsqjs .dsqjs-order-radio{display:none}#dsqjs .dsqjs-order-radio:checked+.dsqjs-order-label{color:#fff;background-color:#888}#dsqjs .dsqjs-order-label{display:block;height:20px;line-height:20px;margin-right:10px;font-size:12px;border-radius:2px;padding:0 5px;background-color:#dcdcdc;cursor:pointer}#dsqjs p.dsqjs-has-more{margin-bottom:24px;margin-left:48px;font-size:13px;line-height:15px}#dsqjs p.dsqjs-has-more a.dsqjs-has-more-btn{color:#656c7a;text-decoration:underline;cursor:pointer} -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Demo Page | DisqusJS 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 114 | 115 | 116 | 117 | 118 | 123 | 124 |
125 |

DisqusJS

126 |
Disqus 替代方案 - 使用 Disqus API 渲染评论列表
127 |

DisqusJS 提供一个纯前端的、不依赖后端服务器的 Disqus 评论基础模式。

128 |
129 | 开始使用 130 | GitHub 131 |
132 |
133 |
134 |
135 |

演示

136 |

如果您当前网络支持访问 Disqus,DisqusJS 将会自动加载完整 DISQUS 模式。如果需要预览 DisqusJS 你可以 点击这里 来强制使用 DisqusJS 的评论基础模式

137 |
138 |
139 |
140 | 141 | 155 | 164 | 165 | 166 | 167 | -------------------------------------------------------------------------------- /src/disqusjs.css: -------------------------------------------------------------------------------- 1 | #dsqjs * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | #dsqjs a { 7 | text-decoration: none; 8 | color: #076dd0; 9 | } 10 | 11 | #dsqjs .dsqjs-hide { 12 | display: none !important 13 | } 14 | 15 | #dsqjs .dsqjs-disabled { 16 | cursor: not-allowed; 17 | opacity: .5; 18 | } 19 | 20 | #dsqjs #dsqjs-msg { 21 | text-align: center; 22 | margin-top: 4px; 23 | margin-bottom: 4px; 24 | font-size: 14px; 25 | } 26 | 27 | #dsqjs #dsqjs-msg .dsqjs-msg-btn { 28 | cursor: pointer; 29 | } 30 | 31 | #dsqjs .dsqjs-bullet { 32 | line-height: 1.4; 33 | margin: 0 2px; 34 | } 35 | 36 | #dsqjs .dsqjs-bullet::after { 37 | color: #c2c6cc; 38 | content: "·"; 39 | font-weight: 700 40 | } 41 | 42 | #dsqjs .dsqjs-clearfix:after, 43 | #dsqjs .dsqjs-clearfix:before { 44 | display: table; 45 | content: ""; 46 | line-height: 0; 47 | clear: both; 48 | } 49 | 50 | #dsqjs .dsqjs-nav { 51 | position: relative; 52 | margin: 0 0 20px; 53 | border-bottom: 2px solid #e7e9ee; 54 | } 55 | 56 | #dsqjs ol, 57 | #dsqjs ul { 58 | list-style: none; 59 | list-style-type: none; 60 | } 61 | 62 | #dsqjs .dsqjs-no-comment { 63 | text-align: center; 64 | font-size: 16px; 65 | line-height: 1.5; 66 | word-wrap: break-word; 67 | overflow: hidden; 68 | color: #2a2e2e; 69 | margin-bottom: 6px; 70 | } 71 | 72 | #dsqjs .dsqjs-nav-tab { 73 | float: left; 74 | text-transform: capitalize; 75 | font-size: 15px; 76 | padding: 12px 8px; 77 | color: #656c7a; 78 | display: block; 79 | margin: 0 15px 0 0; 80 | font-weight: 700; 81 | line-height: 1; 82 | position: relative; 83 | transition: all .2s ease-in-out; 84 | } 85 | 86 | #dsqjs .dsqjs-nav-tab:last-child { 87 | margin: 0; 88 | } 89 | 90 | #dsqjs .dsqjs-tab-active { 91 | color: #2a2e2e; 92 | } 93 | 94 | #dsqjs .dsqjs-tab-active>span:after { 95 | content: " "; 96 | display: block; 97 | height: 2px; 98 | background-color: #076dd0 !important; 99 | position: absolute; 100 | bottom: -5px; 101 | left: 0; 102 | right: 0; 103 | } 104 | 105 | #dsqjs .dsqjs-post-list .dsqjs-post-item { 106 | position: relative; 107 | margin-bottom: 16px; 108 | } 109 | 110 | #dsqjs .dsqjs-post-list .dsqjs-post-avatar { 111 | float: left; 112 | margin-right: 10px; 113 | position: relative; 114 | background: #dbdfe4; 115 | padding: 0; 116 | display: block; 117 | border-radius: 4px; 118 | } 119 | 120 | #dsqjs .dsqjs-post-list .dsqjs-post-avatar img { 121 | width: 44px; 122 | height: 44px; 123 | display: block; 124 | border-radius: 4px; 125 | } 126 | 127 | #dsqjs .dsqjs-post-list .dsqjs-post-header { 128 | line-height: 1; 129 | font-size: 14px; 130 | margin-bottom: 3px; 131 | } 132 | 133 | #dsqjs .dsqjs-post-list .dsqjs-post-header .dsqjs-post-author { 134 | color: #656c7a; 135 | font-weight: 700; 136 | } 137 | 138 | #dsqjs .dsqjs-post-list .dsqjs-post-header .dsqjs-admin-badge { 139 | color: #fff; 140 | background: #687a86; 141 | padding: 1px 3px; 142 | margin-left: 4px; 143 | font-size: 12px; 144 | line-height: 1; 145 | font-weight: 700; 146 | border-radius: 3px; 147 | display: inline-block; 148 | position: relative; 149 | top: -1px; 150 | left: 1px; 151 | } 152 | 153 | #dsqjs .dsqjs-post-list .dsqjs-post-header .dsqjs-meta { 154 | display: inline-block; 155 | font-size: 12px; 156 | color: #656c7a; 157 | } 158 | 159 | #dsqjs .dsqjs-post-body { 160 | font-size: 15px; 161 | line-height: 1.5; 162 | word-wrap: break-word; 163 | overflow: hidden; 164 | color: #2a2e2e 165 | } 166 | 167 | #dsqjs .dsqjs-post-body code { 168 | padding: .2em .4em; 169 | margin: 0; 170 | font-size: 85%; 171 | background: #f5f5f5; 172 | color: inherit; 173 | border-radius: 3px; 174 | } 175 | 176 | #dsqjs .dsqjs-post-body pre { 177 | padding: .5em; 178 | overflow: auto; 179 | font-size: 85%; 180 | line-height: 1.45; 181 | border-radius: 3px; 182 | background: #f5f5f5; 183 | margin: .5em 0; 184 | } 185 | 186 | #dsqjs .dsqjs-post-body blockquote { 187 | padding: 0 .8em; 188 | margin: .5em 0; 189 | color: #6a737d; 190 | border-left: .25em solid #dfe2e5; 191 | } 192 | 193 | #dsqjs .dsqjs-post-body p:last-child { 194 | margin: 0; 195 | } 196 | 197 | #dsqjs .dsqjs-post-list.dsqjs-children>li { 198 | margin-left: 30px; 199 | } 200 | 201 | @media (min-width: 768px) { 202 | #dsqjs .dsqjs-post-list.dsqjs-children>li { 203 | margin-left: 48px; 204 | } 205 | 206 | #dsqjs .dsqjs-post-list .dsqjs-post-avatar { 207 | margin-right: 12px; 208 | } 209 | 210 | #dsqjs .dsqjs-post-list .dsqjs-post-item { 211 | margin-bottom: 20px; 212 | } 213 | } 214 | 215 | @media (min-width: 1024px) { 216 | #dsqjs .dsqjs-post-list.dsqjs-children>li { 217 | margin-left: 60px; 218 | } 219 | } 220 | 221 | #dsqjs .dsqjs-post-list.dsqjs-children .dsqjs-post-avatar img { 222 | width: 38px; 223 | height: 38px; 224 | } 225 | 226 | #dsqjs .dsqjs-load-more { 227 | font-size: 14px; 228 | font-weight: 400; 229 | display: block; 230 | text-align: center; 231 | padding: 11px 14px; 232 | margin: 0 0 24px; 233 | background: #687a86; 234 | color: #fff; 235 | cursor: pointer; 236 | } 237 | 238 | #dsqjs .dsqjs-load-more:hover { 239 | opacity: .8; 240 | } 241 | 242 | #dsqjs footer { 243 | text-align: right; 244 | line-height: 1.5; 245 | padding-top: 10px; 246 | padding-right: 10px; 247 | border-top: 2px solid #e7e9ee; 248 | margin-top: 12px; 249 | font-weight: 700; 250 | font-size: 16px; 251 | color: #555; 252 | } 253 | 254 | #dsqjs .dsqjs-disqus-logo { 255 | background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 438 80'%3e%3cpath fill='%23575B5D' d='M30.2 1.6H1v76h28.9C57 77.6 73 61.3 73 39.4v-.2c0-22-15.7-37.6-42.9-37.6zm21.3 38.1c0 12.3-8.4 19.3-21 19.3H22V20.3h8.5c12.6 0 21 7 21 19.2v.2zm35.6 38h21.2V1.5H87.1v76zm70-47.4c-10.4-2.4-13-4-13-7.4v-.2c0-2.7 2.4-5 7.6-5 6.7 0 14.3 2.7 21.2 7.6l10.6-14.9A47.9 47.9 0 0 0 152.2.3c-18.3 0-29.4 10.2-29.4 24.3v.2c0 15.7 12.4 20.3 28.6 24 10.4 2.3 12.9 4 12.9 7.2v.2c0 3.3-3 5.2-8.7 5.2-8.8 0-17.2-3.1-24.7-9l-11.7 14a53.1 53.1 0 0 0 35.6 12.5c18.5 0 30.7-9.2 30.7-24.7V54c0-14.3-10.8-20-28.3-23.7zm120.7 9.3v-.2A39.5 39.5 0 0 0 236.9.1c-23.4 0-41 17.7-41 39.5v.2a39.5 39.5 0 0 0 40.8 39.4c8.7 0 16.6-2.5 23.1-6.8l8.4 7.5L279 68.1l-7.9-6.6a38 38 0 0 0 6.8-21.9zm-21.4.5c0 2.6-.5 5-1.3 7.3l-10.4-9.3-10.6 12 10.5 9a21.7 21.7 0 0 1-7.7 1.4c-11.6 0-19.4-9.7-19.4-20.7v-.2c0-11 7.7-20.5 19.2-20.5 11.7 0 19.7 9.7 19.7 20.7v.3zm83.5 4.3c0 10.6-5.5 15.6-14 15.6s-14-5.2-14-16.1V1.6h-21.4v42.7C290.5 68 304 79 325.7 79s35.6-10.8 35.6-35.3V1.5h-21.4v42.8zm68.9-14.1c-10.6-2.4-13.2-4-13.2-7.4v-.2c0-2.7 2.5-5 7.6-5 6.8 0 14.4 2.7 21.3 7.6l10.6-14.9A47.9 47.9 0 0 0 403.8.3c-18.3 0-29.5 10.2-29.5 24.3v.2c0 15.7 12.5 20.3 28.7 24 10.3 2.3 12.8 4 12.8 7.2v.2c0 3.3-3 5.3-8.7 5.3-8.8 0-17.1-3.2-24.6-9.2l-11.7 14A53.1 53.1 0 0 0 406.4 79c18.5 0 30.7-9.2 30.7-24.7V54c0-14.3-10.8-20-28.3-23.7z'/%3e%3c/svg%3e"); 256 | background-position: 50% 50%; 257 | background-repeat: no-repeat; 258 | display: inline-block; 259 | height: 12px; 260 | width: 65.7px; 261 | } 262 | 263 | #dsqjs .dsqjs-order { 264 | display: flex; 265 | float: right; 266 | align-items: center; 267 | margin-top: 10px; 268 | margin-bottom: 12px; 269 | } 270 | 271 | #dsqjs .dsqjs-order-radio { 272 | display: none; 273 | } 274 | 275 | #dsqjs .dsqjs-order-radio:checked+.dsqjs-order-label { 276 | color: #fff; 277 | background-color: #888; 278 | } 279 | 280 | #dsqjs .dsqjs-order-label { 281 | display: block; 282 | height: 20px; 283 | line-height: 20px; 284 | margin-right: 10px; 285 | font-size: 12px; 286 | border-radius: 2px; 287 | padding: 0 5px; 288 | background-color: #dcdcdc; 289 | cursor: pointer; 290 | } 291 | 292 | #dsqjs p.dsqjs-has-more { 293 | margin-bottom: 24px; 294 | margin-left: 48px; 295 | font-size: 13px; 296 | line-height: 15px; 297 | } 298 | 299 | #dsqjs p.dsqjs-has-more a.dsqjs-has-more-btn { 300 | color: #656c7a; 301 | text-decoration: underline; 302 | cursor: pointer; 303 | } 304 | -------------------------------------------------------------------------------- /dist/disqus.js: -------------------------------------------------------------------------------- 1 | /*! DisqusJS v1.3.0 | Sukka (https://skk.moe) | https://disqusjs.skk.moe | MIT License */"use strict";function DisqusJS(C){!function(e,p,t,n,o){function r(){for(var s=arguments.length,e=new Array(s),t=0;t & DisqusJS

',u=function(s,e){return'
'},g=function(s,e,t){var n=s.avatarEl,r=s.createdAt;return'
'+n+'
'+e+'
'+t+"
"},v='如需完整体验请针对 disq.us | disquscdn.com | disqus.com 启用代理并 尝试完整 Disqus 模式 | 强制完整 Disqus 模式',b=function(s){return n(s,{method:"GET"}).then(function(s){return o.all([s.ok,s.status,s.json(),s.headers])}).then(function(s){var e=s[0],t=s[1],n=s[2],r=s[3];if(e)return{ok:e,status:t,data:n,headers:r};throw new Error}).catch(function(s){throw s})},y=function(s,e){try{t.setItem(s,e)}catch(s){}},a=function(s){function e(s){return s<10?"0"+s:s}return s=Date.parse(new Date(s)),(s=new Date(s+288e5)).getFullYear()+"-"+e(s.getMonth()+1)+"-"+e(s.getDate())+" "+e(s.getHours())+":"+e(s.getMinutes())};function s(){var s;e.DISQUS?e.DISQUS.reset({reload:!0,config:function(){this.page.identifier=M.config.identifier,this.page.url=M.config.url,this.page.title=M.config.title}}):(s=p.createElement("script"),q(d).innerHTML='
评论完整模式加载中... 如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理,或切换至 评论基础模式
'+l+"
",q("dsqjs-force-dsqjs").addEventListener(h,D),s.src="https://"+M.config.shortname+".disqus.com/embed.js",s.setAttribute("data-timestamp",+new Date),(p.head||p.body).appendChild(s))}function E(){q(d).innerHTML='
正在检查 Disqus 能否访问...
'+l+"
";function s(r){return new o(function(s,e){var t=new Image,n=setTimeout(function(){t.onerror=t.onload=null,e()},3e3);t.onerror=function(){clearTimeout(n),e()},t.onload=function(){clearTimeout(n),s()},t.src="https://"+r+"/favicon.ico?"+ +new Date+"="+ +new Date})}return o.all([s("disqus.com"),s(M.config.shortname+".disqus.com")]).then(w,D)}function L(){q("dsqjs-reload-disqus").addEventListener(h,E),q("dsqjs-force-disqus").addEventListener(h,w)}function i(){f("评论基础模式加载中... "+v),L();var s=M.config.api+"3.0/threads/list.json?forum="+encodeURIComponent(M.config.shortname)+"&thread="+encodeURIComponent("ident:"+M.config.identifier)+"&api_key="+encodeURIComponent(k());b(s).then(function(s){var e=s.data;if(0===e.code&&1===e.response.length){var t=e.response[0],n=t.id,r=t.title,o=t.isClosed,a=t.posts;M.page={id:n,title:r,isClosed:o,length:a,comment:[]},q(d).innerHTML='
评论基础模式加载中... '+v+"
"+u(a,M.config.siteName)+'

    评论列表加载中...

加载更多评论
'+l+"
",L(),q("dsqjs-order-"+M.sortType).setAttribute("checked","true"),i()}else{if(0!==e.code||1===e.response.length)throw new Error;f('当前 Thread 尚未创建。是否切换至 完整 Disqus 模式?'),q("dsqjs-force-disqus").addEventListener(h,w)}}).catch(T);function e(s){function n(s){return{comment:s,author:s.author.name,isPrimary:!!M.config.admin&&s.author.username===M.config.admin,children:t(+s.id),hasMore:s.hasMore}}var e=[],r=[],t=function(e){if(0===r.length)return null;var t=[];return r.forEach(function(s){s.parent===e&&t.unshift(n(s))}),t.length?t:null};return s.forEach(function(s){(s.parent?r:e).push(s)}),e.map(n)}var i=function t(s){void 0===s&&(s="");function n(){Array.prototype.slice.call(o).forEach(function(s){return s.removeEventListener("change",d)}),r.removeEventListener(h,i),Array.prototype.slice.call(a).forEach(function(s){return s.removeEventListener(h,E)})}var r=q("dsqjs-load-more"),o=p.getElementsByClassName("dsqjs-order-radio"),a=p.getElementsByClassName("dsqjs-has-more-btn"),i=function(){n(),t(M.page.next)},d=function(s){var e=s.target;M.sortType=e.getAttribute("value"),y(j,M.sortType),n(),M.page.comment=[],M.page.next="",q("dsqjs-post-container").innerHTML='

正在切换排序方式...

',r.classList.add("dsqjs-hide"),t()},e=""===s?"":"&cursor="+s;r.classList.add("dsqjs-disabled");function c(s){var e=s.createdAt;return Date.parse(new Date(e))}function l(s,e){return s.parent&&e.parent?c(s)-c(e):0}var u=M.config.api+"3.0/threads/listPostsThreaded?forum="+encodeURIComponent(M.config.shortname)+"&thread="+encodeURIComponent(M.page.id)+encodeURIComponent(e)+"&api_key="+encodeURIComponent(k())+"&order="+encodeURIComponent(M.sortType);b(u).then(function(s){var e,t=s.data;if(0===t.code&&0",L(),q("dsqjs-force-disqus").addEventListener(h,D)}}).catch(function(){""===s?T():(r.classList.remove("dsqjs-disabled"),r.innerHTML="加载更多评论失败,点击重试",r.addEventListener(h,i))})},m=function(s){function o(s){return s.comment.author.profileUrl?(s.comment.avatarEl='",s.comment.authorEl='"):(s.comment.avatarEl='',s.comment.authorEl='"),M.config.adminLabel&&s.isPrimary&&(s.comment.authorEl+=''+M.config.adminLabel+""),s}function a(s){var e="",t="",t=s.isDeleted?"此评论已被删除":(e=s.authorEl+'',function(s){var e=p.createElement("div");e.innerHTML=s;var t=e.getElementsByTagName("a");return Array.prototype.slice.call(t).forEach(function(s){var e=decodeURIComponent(s.href.replace(/https:\/\/disq\.us\/url\?url=/g,"")).replace(/(.*):.+cuid=.*/,"$1");s.href=e,s.innerHTML=e,s.rel="external noopener nofollow noreferrer",s.target="_blank"}),e.innerHTML}(n(s.message)));return g(s,e,t)}var n=function(s){return s.replace(/a\.disquscdn\.com/g,"c.disquscdn.com")},t="";e(s).map(function(s){s.children&&(s.nesting=1);var e="";(s=o(s)).hasMore&&(e='

切换至 完整 Disqus 模式 显示更多回复

'),t+='
  • '+a(s.comment)+function t(s){var n=s.nesting,e=s.children||[];if(e){var r="",r=n':'
      ';return e.map(function(s){(s=o(s)).nesting=n+1;var e=s.hasMore?'

      切换至 完整 Disqus 模式 显示更多回复

      ':"";r+='
    • '+a(s.comment)+t(s)+e+"
    • "}),0!==(r+="
    ").length?r:void 0}}(s)+e+"
  • "}),f("你可能无法访问 Disqus,已启用评论基础模式。"+v),q("dsqjs-post-container").innerHTML=t,L()}}function T(s){console.log(s),f('评论基础模式加载失败,是否 重载尝试完整 Disqus 模式 ?'),q("dsqjs-reload-dsqjs").addEventListener(h,i),q("dsqjs-reload-disqus").addEventListener(h,E)}function D(){y("dsqjs_mode","dsqjs"),i()}function w(){y("dsqjs_mode","disqus"),s()}var M={},m=p.location.origin+p.location.pathname+p.location.search;M.config=r({api:"https://disqus.skk.moe/disqus/",identifier:m,url:m,title:p.title,siteName:"",nesting:parseInt(C.nesting)||4,nocomment:"这里冷冷清清的,一条评论都没有"},C),M.page={};var I=M.config.apikey,k=function(){return Array.isArray(I)?I[Math.floor(Math.random()*I.length)]:I};e.disqus_config=function(){this.page.url=M.config.url,this.page.identifier=M.config.identifier,this.page.title=M.config.title},q(d).innerHTML='
    '+l+"
    ",n&&t&&o?(M.mode=t.getItem("dsqjs_mode"),M.sortType=t.getItem(j)||t.getItem("disqus.sort"),M.sortType||(y(j,"desc"),M.sortType="desc"),("disqus"===M.mode?s:"dsqjs"===M.mode?i:E)()):(f("你的浏览器版本过低,不兼容评论基础模式。"+v),L())}(window,document,localStorage,fetch,Promise)}try{module.exports=DisqusJS}catch(s){} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DisqusJS 2 | 3 | > 纯前端、超轻量级的「评论基础模式」实现:使用 Disqus API 渲染评论列表 4 | 5 | [![npm version](https://img.shields.io/npm/v/disqusjs.svg?style=flat-square)](https://www.npmjs.com/package/disqusjs) 6 | [![Author](https://img.shields.io/badge/Author-Sukka-b68469.svg?style=flat-square)](https://skk.moe) 7 | [![npm license](https://img.shields.io/npm/l/disqusjs.svg?style=flat-square)](https://github.com/SukkaW/DisqusJS/blob/master/LICENSE) 8 | [![Size](https://badge-size.herokuapp.com/SukkaW/DisqusJS/master/dist/disqus.js?compression=gzip&style=flat-square)](https://github.com/SukkaW/DisqusJS/tree/master/dist) 9 | [![Travis](https://img.shields.io/travis/SukkaW/DisqusJS.svg?style=flat-square)](https://travis-ci.org/SukkaW/DisqusJS) 10 | [![Codacy Badge](https://img.shields.io/codacy/grade/20babb75dd6047c2828f231e7254bb5b.svg?style=flat-square)](https://app.codacy.com/app/SukkaW/DisqusJS) 11 | [![Dependency Status](https://img.shields.io/david/SukkaW/DisqusJS.svg?style=flat-square)](https://david-dm.org/SukkaW/DisqusJS) 12 | [![jsDelivr Hits](https://data.jsdelivr.com/v1/package/npm/disqusjs/badge)](https://www.jsdelivr.com/package/npm/disqusjs) 13 | 14 | ## 简介 15 | 16 | 使用 Disqus API 获取到的数据渲染评论列表,搭配 Disqus API 的反代可以实现在网络审查地区加载 Disqus 评论列表;支持自动检测访客的 Disqus 可用性自动选择加载原生 Disqus(评论完整模式)和 DisqusJS 提供的评论基础模式。 17 | 18 | ## 功能 19 | 20 | - 展示评论列表、支持按照「最新」、「最早」、「最佳」排序 21 | - 判断访客能否访问 Disqus、自动选择「评论基础模式」或「Disqus 完整模式」 22 | 23 | ## Demo 24 | 25 | - https://disqusjs.skk.moe 26 | - https://blog.skk.moe 27 | 28 | ## 使用 29 | 30 | ### 安装 31 | 32 | #### 直接引入 33 | 34 | 首先下载 [经过编译和压缩的 DisqusJS 相关文件](https://github.com/SukkaW/DisqusJS/tree/master/dist),在你需要安装 DisqusJS 的页面的 `` 之前引入 DisqusJS 的 css,在需要在需要显示评论的位置引入 DisqusJS 的 js: 35 | 36 | ```html 37 | 38 | ``` 39 | 40 | ```html 41 | 42 | ``` 43 | 44 | 你也可以使用 CDN 加载上述文件: 45 | 46 | ```html 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | ``` 55 | 56 | DisqusJS 从 v1.3.0 版本开始使用 Fetch API 代替 XMLHttpRequest,因此不再兼容低于 IE 11 的老旧浏览器。这些浏览器将会收到如下提示: 57 | 58 | ``` 59 | 你的浏览器版本过低,不兼容评论基础模式。如需完整体验请针对 disq.us | disquscdn.com | disqus.com 启用代理并尝试完整 Disqus 模式 60 | ``` 61 | 62 | 如果需要为 IE8 及以上浏览器提供 DisqusJS 评论基础模式的兼容性支持,请在 DisqusJS 加载之前添加如下的 Polyfill: 63 | 64 | ```html 65 | 66 | 67 | 68 | 69 | ``` 70 | 71 | #### 从 NPM 安装 72 | 73 | 你可以轻松将 DisqusJS 引入你现有的项目: 74 | 75 | ```bash 76 | npm i --save disqusjs 77 | ``` 78 | 79 | ```js 80 | import 'disqusjs/dist/disqusjs.css' 81 | import DisqusJS from 'disqusjs' 82 | ``` 83 | 84 | ### 使用 85 | 86 | 创建一个 DisqusJS 的容器: 87 | 88 | ```html 89 |
    90 | ``` 91 | 92 | > 这个容器和 Disqus 原版评论的容器相同。 93 | 94 | 使用下述代码初始化一个 DisqusJS 实例,注意初始化需在 DisqusJS 加载完成后执行: 95 | 96 | ```html 97 | 110 | ``` 111 | 112 | 完成上述步骤后,DisqusJS 就已经在您的站点安装好了,但是你现在还不能使用它。要使用 DisqusJS,你还需要进行一些配置。 113 | 114 | ### 配置 Disqus Application 115 | 116 | 在 [Disqus API Application](https://disqus.com/api/applications/) 处注册一个 Application。 117 | 118 | ![](https://i.loli.net/2018/10/08/5bbb71f99369d.png) 119 | 120 | 点击新创建的 Application,获取你的 API Key(公钥)。 121 | 122 | ![](https://i.loli.net/2018/10/08/5bbb723acbe07.png) 123 | 124 | 在 Application 的 Settings 页面设置你的域名,Disqus 会检查 API 请求的 Referrer。 125 | 126 | ![](https://i.loli.net/2018/10/08/5bbb735b9d2a4.png) 127 | 128 | ### 配置 DisqusJS 参数 129 | 130 | **shortname** `{string}` 131 | 132 | - 你的 Disqus Forum 的 shortname,你可以在 [Disqus Admin - Settings - General - Shortname](https://disqus.com/admin/settings/general/) 获取你的 shortname 133 | - **必须**,无默认值 134 | 135 | **siteName** `{string}` 136 | 137 | - 你站点的名称,将会显示在「评论基础模式」的 header 中;该配置应该和 [Disqus Admin - Settings - General - Website Name](https://disqus.com/admin/settings/general/) 一致 138 | - 非必须,无默认值 139 | 140 | **identifier** `{string}` 141 | 142 | - 当前页面的 identifier,用来区分不同页面 143 | - **建议**,默认值为 `document.location.origin + document.location.pathname + document.location.search` 144 | 145 | **url** `{string}` 146 | 147 | - 当前页面的 URL,Disqus 的爬虫会爬取该 URL 获取页面相关信息 148 | - **建议**,默认值为 `document.location.origin + document.location.pathname + document.location.search` 149 | 150 | **title** `{string}` 151 | 152 | - 当前页面的标题,如果没有设置默认为当前页面的标题。当页面标题中有其他信息(比如站点名称)而不想在 Disqus 中展示时,可以设置此项。 153 | - 非必须,默认值为 `document.title` 154 | 155 | **api** `{string}` 156 | 157 | - DisqusJS 请求的 API Endpoint,通常情况下你应该配置一个 Disqus API 的反代并填入反代的地址。你也可以直接使用 DISQUS 官方 API 的 Endpoint `https://disqus.com/api/`,或是使用我搭建的 Disqus API 反代 Endpoint `https://disqus.skk.moe/disqus/`。如有必要可以阅读关于搭建反代的 [相关内容](https://github.com/SukkaW/DisqusJS#%E8%B0%83%E8%AF%95%E8%BF%9B%E9%98%B6%E4%BD%BF%E7%94%A8--%E5%BC%80%E5%8F%91%E7%9B%B8%E5%85%B3) 158 | - **建议**,默认值为 `https://disqus.skk.moe/disqus/` 159 | 160 | **apikey** `{string || Array}` 161 | 162 | - DisqusJS 向 API 发起请求时使用的 API Key,你应该在配置 Disqus Application 时获取了 API Key 163 | - DisqusJS 支持填入一个 包含多个 API Key 的 Array,在每次请求时会随机使用其中一个;如果你只填入一个 API Key,可以填入 string 或 Array。 164 | - **必填**,无默认值 165 | 166 | **nesting** `{Number}` 167 | 168 | - 最大评论嵌套数;超过嵌套层数的评论,会不论从属关系显示在同一层级下 169 | - 非必须,默认值为 `4` 170 | 171 | **nocomment** `{string}` 172 | 173 | - 没有评论时的提示语(对应 Disqus Admin - Settings - Community - Comment Count Link - Zero comments) 174 | - 非必须,默认值为 `这里冷冷清清的,一条评论都没有` 175 | 176 | --- 177 | 178 | 以下配置和 Disqus Moderator Badge 相关,缺少一个都不会显示 Badge 179 | 180 | **admin** `{string}` 181 | 182 | - 你的站点的 Disqus Moderator 的用户名(也就是你的用户名)。你可以在 [Disqus - Settings - Account - Username](https://disqus.com/home/settings/account/) 获取你的 Username 183 | - 非必须,无默认值 184 | 185 | **adminLabel** `{string}` 186 | 187 | - 你想显示在 Disqus Moderator Badge 中的文字。该配置应和 [Disqus Admin - Settings - Community - Moderator Badge Text](https://disqus.com/admin/settings/community/) 相同 188 | - 非必须,无默认值 189 | 190 | ### PJAX 站点注意事项 191 | 192 | 如果你在使用 DisqusJS v0.2.5 版本,需要在 PJAX 的页面跳转事件下销毁 Disqus 实例(Disqus 不支持 PJAX)、并通过 `window.disqusjs.load();` 重新加载 DisqusJS。DisqusJS v0.2.5 版本支持自动判断当前页面是否存在 `#disqus_thread` 容器,如果容器不存在就不加载。 193 | 194 | DisqusJS v1.0.0 及之后的版本使用了新的方法加载 DisqusJS,并去除了对 `#disqus_thread` 容器的判断,在没有容器的页面初始化 DisqusJS 实例会报错。在切换页面时需要销毁已有的 Disqus 实例和 DisqusJS 实例,然后重新初始化一个新的 DisqusJS 实例。 195 | 196 | DisqusJS v1.2.6 开始支持检测是否存在 Disqus 实例,并在加载 Disqus 时直接调用 `DISQUS.reset()` 方法重载 Disqus 评论,无需用户手动销毁现有的 Disqus 实例。 197 | 198 | 代码可以参考 [DIYgod 的这条 commit](https://github.com/DIYgod/diygod.me/commit/31012c21a457df5ab172c2e24bc197d5a0de8e69#diff-566630479f69d2ba36b6b996f6ba5a8f),DIYgod 在这次 commit 中将 DisqusJS 从 v0.2.5 升级到了 v1.0.8。 199 | 200 | ## 如何搭建 Disqus API 反代 201 | 202 | 使用 Caddy 或者 Nginx 都可以搭建一个反代服务器,需要反代的 Endpoint 是 `https://disqus.com/api/`。这里介绍的是针对不使用服务器和后端程序,使用 Serverless 平台搭建 Disqus API 反代的方法。 203 | 204 | > 当然,你也可以直接使用我搭建的反代 `https://disqus.skk.moe/disqus/`。 205 | 206 | ### Vercel (ZEIT Now) 207 | 208 | [ZEIT Now](https://zeit.co) 是一个 Serverless 平台。免费 Plan 提供每月 100 GiB 流量和无限的请求次数。 209 | [sukkaw/disqusjs-proxy-example](https://github.com/SukkaW/disqusjs-proxy-example) 提供了一个使用 Now Router 进行反代的样例配置文件。 210 | 211 | ### Cloudflare Workers 212 | 213 | [Cloudflare Workers](https://www.cloudflare.com/products/cloudflare-workers/) 提供了一个在 Cloudflare 上运行 JavaScript 的平台。免费 Plan 可提供每天 `100000` 次免费请求次数额度。 214 | [idawnlight/disqusjs-proxy-cloudflare-workers](https://github.com/idawnlight/disqusjs-proxy-cloudflare-workers) 提供了一份使用 Cloudflare Workers 进行反代的样例代码。 215 | 216 | ### Heroku 217 | 218 | [Heroku](https://www.heroku.com/) 是一个支持多种编程语言的 SaaS 平台。不绑定信用卡每月有 550 小时的免费运行时间、绑定信用卡后每月有 1000 小时的免费运行时间。 219 | [ysc3839/disqusjs-proxy](https://github.com/ysc3839/disqusjs-proxy) 提供了一个直接部署至 Heroku 的 Disqus API 反代项目。你可以点击 [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/ysc3839/disqusjs-proxy) 直接部署。 220 | 221 | ### Firebase 222 | 223 | [Firebase Cloud Functions](https://firebase.google.com/products/functions/) 提供了执行 Node.js 代码的 Serverless 平台。需绑定银行卡 (Visa 或 MasterCard) 才能启用互联网出站访问功能。 224 | [ysc3839/disqusjs-proxy 的 `firebase` 分支](https://github.com/ysc3839/disqusjs-proxy/tree/firebase) 提供了一个可以部署在 Firebase 上的反代样例。 225 | 226 | ## 注意 227 | 228 | - Disqus API 不支持通过 AJAX 方式调用创建评论或者初始化页面,所以自动初始化页面和创建匿名评论在不搭配专门的后端程序的话不能实现。 229 | - Disqus API 会检查请求是否包含 `Origin`、`X-Request-With` 等响应头、拦截 AJAX 请求。就算 Disqus API 不做检查,把你的私钥和公钥一起明文写在前端也是 **十分愚蠢** 的 230 | - 所以如果 DisqusJS 检测到当前页面没有初始化、会提示是否切换到 Disqus 完整模式进行初始化。 231 | - DisqusJS 仅在当前域名首次访问时检测 Disqus 可用性并选择模式,并把结果持久化在 localStorage 中,之后访问都会沿用之前的模式。 232 | - 一些广告拦截规则(如 [Fanboy’s Enhanced Tracking List](https://github.com/ryanbr/fanboy-adblock)) 会导致检测失败,可在广告拦截器的自定义规则中添加 `@@||*disqus*.com/favicon.ico*` 解决。当然你不必担心访客,因为 DisqusJS 会提供 `尝试 Disqus 完整模式 | 强制 Disqus 完整模式` 的按钮供访客切换。 233 | - 一个 Disqus Application 的 Rate Limit 是每小时 1000 次;DisqusJS 一次正常加载会产生 2 次请求。DisqusJS 支持将多个 API Key 填入一个 Array 中,并在请求时随机选择(负载均衡)。你可以创建多个 Disqus API Application 并分别获取 API Key。 234 | - 我搭建了一个 Disqus API 反代的服务 `https://disqus.skk.moe/disqus/` 供没有能力搭建反代的用户使用,不保证 SLA、缓存 TTL 3 小时。 235 | 236 | ## 谁在使用 DisqusJS? 237 | 238 | - Sukka: [Sukka's Blog](https://blog.skk.moe) 239 | - DIYgod: [Hi, DIYgod](https://diygod.me) ([source](https://github.com/DIYgod/diygod.me)) 240 | - imlonghao: [Imlonghao](https://imlonghao.com/) 241 | - Yrom Wang: [Yrom's](https://yrom.net/) (Still using DisqusJS v0.2.5) 242 | - h404bi: [Chawye Hsu's Blog](https://www.h404bi.com/blog/) ([source](https://github.com/h404bi/www.h404bi.com)) 243 | - ysc3839: [YSC's blog](https://blog.ysc3839.com/) 244 | 245 | 如果你的站点或者个人博客在使用 DisqusJS,来 [把你的网站分享给其他人吧](https://github.com/SukkaW/DisqusJS/issues/12)! 246 | 247 | ## 调试、进阶使用 & 开发相关 248 | 249 | - ~~`a.disquscdn.com` 和~~ `c.disquscdn.com` 解析到 Cloudflare 而不是 Fastly,可用性大大增强;`disqus.com` 和 `shortname.disqus.com` 仍然被墙;`disq.us` 解析到 Fastly 连通性较差,DisqusJS 通过解析获得了原链接。 250 | - `a.disquscdn.com` 重新解析到 Fastly,可用性不如 `c.disquscdn.com`,DisqusJS 内部已增加替换 `a.disquscdn.com` 为 `c.disquscdn.com` 以改善速度。 251 | - DisqusJS 检测访客的 Disqus 可用性是通过检测 `disqus.com/favicon.ico` 和 `${disqusjs.config.shortname}.disqus.com/favicon.ico` 是否能正常加载,如果有一个加载出错或超时(2s)就判定 Disqus 不可用。 252 | - DisqusJS 在 localStorage 中持久化了 Disqus 连通性检查结果,key 为 `dsqjs_mode`,value 为 `disqus` 或者 `dsqjs`。需要调整 DisqusJS 的模式时可以直接操作 localStorage。 253 | - Disqus 自己的 config 保存在全局变量 `window.disqus_config` 中,你可能好奇为什么没有引入。实际上由于 `disqus_config` 和 DisqusJS 中有很多重复的配置,所以 DisqusJS 直接将相关配置项赋给了 `disqus_config`,所以用户只需要配置 DisqusJS 即可。 254 | - DisqusJS 并没有使用 Webpack 将 `disqusjs.css` 和 `disqus.js` 打包在一起,大家可以开发自己的 DisqusJS 主题。所有 DisqusJS 创建的 HTML 元素都包裹在 `
    ` 之中、几乎所有的元素都有自己的类名并都以 `dsqjs-` 为前缀,防止污染。 255 | - DisqusJS 从 v1.2.0 版本开始实现了评论排序。Disqus 将评论排序方式持久化在 localStorage 中、key 为 `disqus.sort`,DisqusJS 沿用了这一位置。 256 | 257 | ## Todo List 258 | 259 | [DisqusJS GitHub Project](https://github.com/SukkaW/DisqusJS/projects/1) 260 | 261 | ## Author 作者 262 | 263 | **DisqusJS** © [Sukka](https://github.com/SukkaW), Released under the [MIT](https://github.com/SukkaW/DisqusJS/blob/master/LICENSE) License.
    264 | Authored and maintained by Sukka with help from contributors ([list](https://github.com/SukkaW/DisqusJS/graphs/contributors)). 265 | 266 | > [Personal Website](https://skk.moe) · [Blog](https://blog.skk.moe) · GitHub [@SukkaW](https://github.com/SukkaW) · Telegram Channel [@SukkaChannel](https://t.me/SukkaChannel) · Twitter [@isukkaw](https://twitter.com/isukkaw) · Keybase [@sukka](https://keybase.io/sukka) 267 | -------------------------------------------------------------------------------- /src/disqus.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The variable used in DisqusJS 3 | * 4 | * DisqusJS Mode 5 | * @param {String} disqusjs.mode = dsqjs | disqus - Set which mode to use, should store and get in localStorage 6 | * @param {String} disqusjs.sortType = popular | asc(oldest first) | desc(latest first) - Set which sort type to use, should store and get in localStorage 7 | * 8 | * DisqusJS Config 9 | * @param {String} disqusjs.config.shortname - The disqus shortname 10 | * @param {String} disqusjs.config.siteName - The Forum Name 11 | * @param {String} disqusjs.config.identifier - The identifier of the page 12 | * @param {String} disqusjs.config.title - The title of the page 13 | * @param {String} disqusjs.config.url - The url of the page 14 | * @param {String} disqusjs.config.api - Where to get data 15 | * @param {String} disqusjs.config.apikey - The apikey used to request Disqus API 16 | * @param {Number} disqusjs.config.nesting - The max nesting level of Disqus comment 17 | * @param {String} disqusjs.config.nocomment - The msg when there is no comment 18 | * @param {String} disqusjs.config.admin - The disqus forum admin username 19 | * @param {String} disqusjs.config.adminLabel - The disqus moderator badge text 20 | * 21 | * DisqusJS Info 22 | * @param {String} disqusjs.page.id = The thread id, used at next API call 23 | * @param {String} disqusjs.page.next = The cursor of next page of list 24 | * @param {Boolean} disqusjs.page.isClosed - Whether the comment is closed 25 | * @param {Number} disqusjs.page.length - How many comment in the thread 26 | */ 27 | 28 | function DisqusJS(config) { 29 | ((window, document, localStorage, fetch, Promise) => { 30 | // 封装一下基于 Object.assign 的方法 31 | function _extends(...args) { 32 | _extends = Object.assign || function (target) { 33 | for (let i = 0, len = arguments.length; i < len; i++) { 34 | const source = arguments[i]; 35 | for (const key in source) { 36 | if (Object.prototype.hasOwnProperty.call(source, key)) { 37 | target[key] = source[key]; 38 | } 39 | } 40 | } 41 | 42 | return target; 43 | } 44 | return _extends.apply(this, args); 45 | }; 46 | 47 | const $$ = (elementID) => document.getElementById(elementID); 48 | /** 49 | * msg - 提示信息 50 | * 51 | * @param {String} url 52 | */ 53 | const msg = (str) => { 54 | const msgEl = $$('dsqjs-msg'); 55 | if (msgEl) msgEl.innerHTML = str; 56 | } 57 | 58 | // 定义一些常量 59 | const CLICK = 'click'; 60 | const DISQUS_CONTAINER_EL_ID = 'disqus_thread'; 61 | const DSQJS_STORAGE_KEY_SORT_TYPE = 'dsqjs_sort' 62 | 63 | /** 64 | * htmlTpl - DisqusJS 的 HTML 模板片段 65 | * 66 | * msg: DisqusJS 提示信息模板 67 | * footer: 尾部信息模板 68 | */ 69 | 70 | /* 71 | target="_blank" rel="external nofollow noopener noreferrer" 72 | */ 73 | const HTML_TPL_A_ATTR = `target="_blank" rel="external nofollow noopener noreferrer"`; 74 | /* 75 |
    76 | */ 77 | const HTML_TPL_EL_MSG = `
    `; 78 | /* 79 |
    80 | 82 |
    83 | */ 84 | const HTML_TPL_EL_FOOTER = ``; 85 | 86 | /* 87 |
    88 | 102 |
    103 | */ 104 | const HTML_TPL_EL_HEADER = (num, title) => `
    `; 105 | 106 | const HTML_TPL_EL_COMMENT = ({ avatarEl, createdAt }, authorEl, message) => `
    ${avatarEl}
    ${authorEl}
    ${message}
    `; 107 | 108 | const HTML_TPL_EL_ASK_FOR_FULL = '如需完整体验请针对 disq.us | disquscdn.com | disqus.com 启用代理并 尝试完整 Disqus 模式 | 强制完整 Disqus 模式'; 109 | 110 | /** 111 | * _get(url) - 对 Fetch 的一个封装 112 | * 113 | * @param {String} url 114 | * @return {Promise} - 一个 Promise 对象,返回请求结果 115 | */ 116 | 117 | const _get = (url) => fetch(url, { method: 'GET' }) 118 | .then(resp => Promise.all([resp.ok, resp.status, resp.json(), resp.headers])).then(([ok, status, data, headers]) => { 119 | if (ok) { 120 | return { 121 | ok, 122 | status, 123 | data, 124 | headers 125 | }; 126 | } else { 127 | throw new Error; 128 | } 129 | }).catch(error => { 130 | throw error; 131 | }); 132 | 133 | // localstorage 操作类 134 | // 用于持久化某些数据(如 newComment 的评论者的相关信息) 135 | 136 | /** 137 | * setLS(kwy, value) - 设置 localStorage 138 | * 139 | * @param {String} key 140 | * @param {String} value 141 | */ 142 | const setLS = (key, value) => { 143 | try { 144 | localStorage.setItem(key, value); 145 | } catch (e) { 146 | } 147 | } 148 | 149 | /** 150 | * formatDate(date) - 解析 date 为 yyyy-MM-dd hh:mm:ss 151 | * 152 | * @param {String} date - 传入评论创建日期(XML 格式) 153 | * @return {string} - 格式化后的日期 154 | */ 155 | const formatDate = (date) => { 156 | // 不足两位补 0 157 | const x = (input) => (input < 10) ? `0${input}` : input; 158 | // 将传入的 date 转化为时间戳 159 | date = Date.parse(new Date(date)); 160 | 161 | // Disqus API 返回的是 UTC 时间,所以在时间戳上加 28800000 毫秒补上 8 小时时差 162 | date = new Date(date + 8 * 60 * 60 * 1000); 163 | const y = date.getFullYear(); 164 | const m = x(date.getMonth() + 1); 165 | const d = x(date.getDate()); 166 | const h = x(date.getHours()); 167 | const minute = x(date.getMinutes()); 168 | return `${y}-${m}-${d} ${h}:${minute}`; 169 | } 170 | 171 | /* 172 | * loadDisqus() - 加载 Disqus 173 | */ 174 | function loadDisqus() { 175 | if (window.DISQUS) { 176 | window.DISQUS.reset({ 177 | reload: true, 178 | config() { 179 | this.page.identifier = disqusjs.config.identifier; 180 | this.page.url = disqusjs.config.url; 181 | this.page.title = disqusjs.config.title; 182 | } 183 | }); 184 | } else { 185 | const s = document.createElement('script'); 186 | 187 | // 显示提示信息 188 | // Disqus 加载成功以后会把 #disqus_thread 内的内容全部覆盖 189 | $$(DISQUS_CONTAINER_EL_ID).innerHTML = `
    评论完整模式加载中... 如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理,或切换至 评论基础模式
    ${HTML_TPL_EL_FOOTER}
    ` 190 | $$('dsqjs-force-dsqjs').addEventListener(CLICK, useDsqjs); 191 | 192 | s.src = `https://${disqusjs.config.shortname}.disqus.com/embed.js`; 193 | s.setAttribute('data-timestamp', +new Date()); 194 | (document.head || document.body).appendChild(s); 195 | } 196 | } 197 | 198 | function checkDisqus() { 199 | $$(DISQUS_CONTAINER_EL_ID).innerHTML = `
    正在检查 Disqus 能否访问...
    ${HTML_TPL_EL_FOOTER}
    `; 200 | 201 | const runcheck = (domain) => { 202 | return new Promise((resolve, reject) => { 203 | const img = new Image; 204 | // 处理加载超时 205 | const timeout = setTimeout(() => { 206 | img.onerror = img.onload = null; 207 | reject(); 208 | }, 3000); 209 | 210 | img.onerror = () => { 211 | clearTimeout(timeout); 212 | reject(); 213 | } 214 | 215 | img.onload = () => { 216 | clearTimeout(timeout); 217 | resolve(); 218 | } 219 | 220 | img.src = `https://${domain}/favicon.ico?${+(new Date)}=${+(new Date)}`; 221 | }) 222 | } 223 | 224 | // 测试 Disqus 的域名 225 | // *.disquscdn.com 没有被墙所以不做检查 226 | return Promise.all([ 227 | runcheck('disqus.com'), 228 | runcheck(`${disqusjs.config.shortname}.disqus.com`) 229 | ]).then(useDisqus, useDsqjs); 230 | } 231 | 232 | function assignClickEventForAskForFulButton() { 233 | $$('dsqjs-reload-disqus').addEventListener(CLICK, checkDisqus); 234 | $$('dsqjs-force-disqus').addEventListener(CLICK, useDisqus); 235 | } 236 | 237 | function loadDsqjs() { 238 | // DisqusJS 加载中信息 239 | msg(`评论基础模式加载中... ${HTML_TPL_EL_ASK_FOR_FULL}`); 240 | assignClickEventForAskForFulButton(); 241 | 242 | /* 243 | * 获取 Thread 信息 244 | * Disqus API 只支持通过 Thread ID 获取评论列表,所以必须先通过 identifier 获取当前页面 Thread ID 245 | * 246 | * API Docs: https://disqus.com/api/docs/threads/list/ 247 | * API URI: /3.0/threads/list.json?forum=[disqus_shortname]&thread=ident:[identifier]&api_key=[apikey] 248 | */ 249 | const url = `${disqusjs.config.api}3.0/threads/list.json?forum=${encodeURIComponent(disqusjs.config.shortname)}&thread=${encodeURIComponent(`ident:${disqusjs.config.identifier}`)}&api_key=${encodeURIComponent(apikey())}`; 250 | 251 | _get(url).then(({ data }) => { 252 | if (data.code === 0 && data.response.length === 1) { 253 | const { id, title, isClosed, posts: length } = data.response[0]; 254 | disqusjs.page = { id, title, isClosed, length, comment: [] }; 255 | 256 | // 在 #disqus_thread 中填充 DisqusJS Container 257 | $$(DISQUS_CONTAINER_EL_ID).innerHTML = `
    评论基础模式加载中... ${HTML_TPL_EL_ASK_FOR_FULL}
    ${HTML_TPL_EL_HEADER(length, disqusjs.config.siteName)}

      评论列表加载中...

    加载更多评论
    ${HTML_TPL_EL_FOOTER}
    `; 258 | 259 | assignClickEventForAskForFulButton(); 260 | 261 | $$(`dsqjs-order-${disqusjs.sortType}`).setAttribute('checked', 'true'); 262 | 263 | // 获取评论列表 264 | getComment() 265 | } else if (data.code === 0 && data.response.length !== 1) { 266 | // 当前页面可能还未初始化(需要创建 thread) 267 | // Disqus API 的 threads/create 需要在服务端发起请求,不支持 AJAX Call 268 | msg('当前 Thread 尚未创建。是否切换至 完整 Disqus 模式?') 269 | $$('dsqjs-force-disqus').addEventListener(CLICK, useDisqus); 270 | } else { 271 | throw new Error; 272 | } 273 | }).catch(loadError) 274 | 275 | /** 276 | * getComment(cursor) - 获取评论列表 277 | * 278 | * @param {String} cursor - 传入 cursor 用于加载下一页的评论 279 | */ 280 | const getComment = (cursor = '') => { 281 | const $loadMoreBtn = $$('dsqjs-load-more'); 282 | const $orderRadio = document.getElementsByClassName('dsqjs-order-radio'); 283 | const $loadHideCommentInDisqus = document.getElementsByClassName('dsqjs-has-more-btn'); 284 | 285 | const unregisterListenerForSwitchTypeRadioAndGetMoreCommentBtn = () => { 286 | // 为按钮们取消事件,避免重复绑定 287 | // 重新 getComment() 时会重新绑定 288 | Array.prototype.slice.call($orderRadio).forEach(i => i.removeEventListener('change', switchSortType)); 289 | $loadMoreBtn.removeEventListener(CLICK, getMoreComment); 290 | Array.prototype.slice.call($loadHideCommentInDisqus).forEach(i => i.removeEventListener(CLICK, checkDisqus)); 291 | } 292 | 293 | const getMoreComment = () => { 294 | unregisterListenerForSwitchTypeRadioAndGetMoreCommentBtn(); 295 | // 加载下一页评论 296 | getComment(disqusjs.page.next); 297 | } 298 | 299 | const getCommentError = () => { 300 | if (cursor === '') { 301 | loadError(); 302 | } else { 303 | $loadMoreBtn.classList.remove('dsqjs-disabled'); 304 | // 在按钮上显示提示信息 305 | $loadMoreBtn.innerHTML = '加载更多评论失败,点击重试'; 306 | // 重新在按钮上绑定 加载更多按钮 307 | $loadMoreBtn.addEventListener(CLICK, getMoreComment); 308 | } 309 | }; 310 | 311 | // 切换排序方式 312 | const switchSortType = ({ target }) => { 313 | // 通过 event.target 获取被选中的按钮 314 | disqusjs.sortType = target.getAttribute('value'); 315 | // 将结果在 localStorage 中持久化 316 | setLS(DSQJS_STORAGE_KEY_SORT_TYPE, disqusjs.sortType); 317 | unregisterListenerForSwitchTypeRadioAndGetMoreCommentBtn(); 318 | 319 | // 清空评论列表和其它参数 320 | disqusjs.page.comment = []; 321 | disqusjs.page.next = ''; // 不然切换排序方式以后以后加载更多评论就会重复加载 322 | 323 | // 显示加载中提示信息 324 | // 反正只有评论基础模式已经加载成功了才会看到排序选项,所以无所谓再提示一次 Disqus 不可访问了 325 | $$('dsqjs-post-container').innerHTML = '

    正在切换排序方式...

    '; 326 | // 把 加载更多评论 隐藏起来 327 | $loadMoreBtn.classList.add('dsqjs-hide'); 328 | getComment(); 329 | } 330 | 331 | // 处理传入的 cursor 332 | const cursorParam = (cursor === '') ? '' : `&cursor=${cursor}`; 333 | 334 | // 在发起请求前禁用 加载更多评论 按钮防止重复调用 335 | $loadMoreBtn.classList.add('dsqjs-disabled'); 336 | /* 337 | * 获取评论列表 338 | * 339 | * API Docs: https://disqus.com/api/docs/posts/list/ 340 | * API URI: /3.0/posts/list.json?forum=[shortname]&thread=[thread id]&api_key=[apikey] 341 | * 342 | * https://github.com/SukkaW/DisqusJS/issues/6 343 | * 可以使用 include=deleted 来获得已被删除评论列表 344 | * 345 | * https://blog.fooleap.org/disqus-api-comments-order-by-desc.html 346 | * 处理评论嵌套问题,使用了一个隐藏 API /threads/listPostsThreaded 347 | * 用法和 /threads/listPosts 相似,和 /threads/post 的区别也只有 include 字段不同 348 | * 这个能够返回已删除评论,所以也不需要 include=deleted 了 349 | * sort 字段提供三个取值: 350 | * - desc (降序) 351 | * - asc (升序) 352 | * - popular(最热) 353 | * 这个 API 的问题在于被嵌套的评论总是降序,看起来很不习惯 354 | * 355 | * 每次加载翻页评论的时候合并数据并进行重排序 356 | * 用户切换排序方式的时候直接取出进行重新渲染 357 | */ 358 | const sortCommentParseDate = ({ createdAt }) => Date.parse(new Date(createdAt)); 359 | const sortCommentparentAsc = (a, b) => { 360 | if (a.parent && b.parent) { 361 | return sortCommentParseDate(a) - sortCommentParseDate(b); 362 | } else { 363 | return 0; 364 | } 365 | }; 366 | 367 | const url = `${disqusjs.config.api}3.0/threads/listPostsThreaded?forum=${encodeURIComponent(disqusjs.config.shortname)}&thread=${encodeURIComponent(disqusjs.page.id)}${encodeURIComponent(cursorParam)}&api_key=${encodeURIComponent(apikey())}&order=${encodeURIComponent(disqusjs.sortType)}`; 368 | 369 | _get(url).then(({ data }) => { 370 | if (data.code === 0 && data.response.length > 0) { 371 | // 解禁 加载更多评论 372 | $loadMoreBtn.classList.remove('dsqjs-disabled'); 373 | 374 | // 将获得的评论数据和当前页面已有的评论数据合并 375 | disqusjs.page.comment.push(...data.response) 376 | 377 | // 将所有的子评论进行降序排列 378 | disqusjs.page.comment.sort(sortCommentparentAsc); 379 | 380 | // 用当前页面的所有评论数据进行渲染 381 | renderComment(disqusjs.page.comment); 382 | 383 | // 为排序按钮们委托事件 384 | Array.prototype.slice.call($orderRadio).forEach(i => i.addEventListener('change', switchSortType)); 385 | Array.prototype.slice.call($loadHideCommentInDisqus).forEach(i => i.addEventListener(CLICK, checkDisqus)); 386 | 387 | if (data.cursor.hasNext) { 388 | // 将 cursor.next 存入 disqusjs 变量中供不能传参的不匿名函数使用 389 | disqusjs.page.next = data.cursor.next; 390 | // 确保 加载更多评论按钮 文字正常 391 | $loadMoreBtn.innerHTML = '加载更多评论' 392 | // 显示 加载更多评论 按钮 393 | $loadMoreBtn.classList.remove('dsqjs-hide'); 394 | $loadMoreBtn.addEventListener(CLICK, getMoreComment); 395 | } else { 396 | // 没有更多评论了,确保按钮隐藏 397 | $loadMoreBtn.classList.add('dsqjs-hide'); 398 | } 399 | } else if (data.code === 0 && data.response.length === 0) { 400 | // 当前没有评论,显示提示信息 401 | msg(`你可能无法访问 Disqus,已启用评论基础模式。${HTML_TPL_EL_ASK_FOR_FULL}`) 402 | $$('dsqjs-post-container').innerHTML = `

    ${disqusjs.config.nocomment}

    ` 403 | 404 | assignClickEventForAskForFulButton(); 405 | 406 | $$('dsqjs-force-disqus').addEventListener(CLICK, useDsqjs); 407 | } else { 408 | throw new Error; 409 | } 410 | }).catch(getCommentError); 411 | } 412 | 413 | /** 414 | * parseCommentData(data) - 解析评论列表 415 | * 416 | * @param {Array} data - 评论列表 JSON 417 | * @return {Array} - 解析后的评论列表数据 418 | */ 419 | const parseCommentData = (data) => { 420 | let topLevelComments = []; 421 | let childComments = []; 422 | 423 | const commentJSON = (comment) => ({ 424 | comment, 425 | author: comment.author.name, 426 | 427 | // 如果不设置 admin 会返回 undefined,所以需要嘴一个判断 428 | isPrimary: (disqusjs.config.admin ? (comment.author.username === disqusjs.config.admin) : false), 429 | 430 | children: getChildren(+comment.id), 431 | 432 | /* 433 | * Disqus 改变了 Private API 的行为 434 | * https://github.com/fooleap/disqus-php-api/issues/44 435 | * 默认隐藏更多的评论,通过 hasMore 字段判断 436 | */ 437 | // 将 hasMore 字段提升到上层字段中 438 | hasMore: comment.hasMore 439 | }); 440 | 441 | const getChildren = (id) => { 442 | // 如果没有子评论,就不需要解析子评论了 443 | if (childComments.length === 0) return null; 444 | 445 | const list = []; 446 | childComments.forEach(comment => { 447 | if (comment.parent === id) { 448 | list.unshift(commentJSON(comment)); 449 | } 450 | }) 451 | 452 | return list.length ? list : null 453 | } 454 | 455 | data.forEach(comment => { 456 | // 如果没有 comment.parent 说明是第一级评论 457 | const c = comment.parent ? childComments : topLevelComments; 458 | c.push(comment); 459 | }); 460 | return topLevelComments.map(commentJSON); 461 | } 462 | 463 | /* 464 | * renderCommentData(data) - 渲染评论列表 465 | * 466 | * @param {Array} data - 从 getComment() 获取到的 JSON 467 | */ 468 | const renderComment = (data) => { 469 | /* 470 | * processData(data) - 处理评论列表 471 | * 472 | * @param {Array} data - 解析后的评论列表 JSON 473 | */ 474 | const processData = (data) => { 475 | // 处理 Disqus Profile URL 476 | if (data.comment.author.profileUrl) { 477 | /* 478 | Avatar Element 479 | 480 | 481 | 482 | Author Element 483 | 486 | */ 487 | data.comment.avatarEl = `` 488 | data.comment.authorEl = `` 489 | } else { 490 | data.comment.avatarEl = `` 491 | data.comment.authorEl = `` 492 | } 493 | 494 | // 处理 Admin Label 495 | // 需要同时设置 isPrimary 和 adminLabel;admin 已经在 processData() 中做过判断了 496 | if (disqusjs.config.adminLabel && data.isPrimary) { 497 | data.comment.authorEl += `${disqusjs.config.adminLabel}`; 498 | } 499 | 500 | return data; 501 | } 502 | 503 | /** 504 | * removeDisqUs(input) - 将 comment 中的短链接 disq.us 去除 505 | * @param {String} input - 评论信息 506 | * @return {string} msg - 经过处理的评论信息 507 | */ 508 | const removeDisqUs = (input) => { 509 | const el = document.createElement('div'); 510 | el.innerHTML = input; 511 | const aTag = el.getElementsByTagName('a'); 512 | // Use Array.prototype.slice.call(aTag) instead of [...aTag] because when using gulp, [..aTag] may be replaced by [].concat(aTag), which is not the same meaning. 513 | Array.prototype.slice.call(aTag).forEach(i => { 514 | const link = decodeURIComponent(i.href.replace(/https:\/\/disq\.us\/url\?url=/g, '')).replace(/(.*):.+cuid=.*/, '$1'); 515 | 516 | i.href = link; 517 | i.innerHTML = link; 518 | i.rel = 'external noopener nofollow noreferrer'; 519 | i.target = '_blank'; 520 | }); 521 | 522 | return el.innerHTML; 523 | } 524 | 525 | /** 526 | * replaceDisquscdn(str) - 将 a.disquscdn.com 替换为 c.disquscdn.com 527 | * @param {String} str - 字符串 528 | * @return {string} - 替换后的字符串 529 | */ 530 | const replaceDisquscdn = (str) => str.replace(/a\.disquscdn\.com/g, 'c.disquscdn.com'); 531 | 532 | const renderPostItem = (s) => { 533 | let authorEl = ''; 534 | let message = ''; 535 | if (s.isDeleted) { 536 | message = `此评论已被删除`; 537 | } else { 538 | authorEl = `${s.authorEl}`; 539 | message = removeDisqUs(replaceDisquscdn(s.message)); 540 | } 541 | 542 | return HTML_TPL_EL_COMMENT(s, authorEl, message) 543 | } 544 | 545 | const childrenComments = (data) => { 546 | const nesting = data.nesting; 547 | const children = (data.children || []); 548 | 549 | if (!children) { 550 | return; 551 | } 552 | 553 | let html = ''; 554 | // 如果当前评论嵌套数大于 4 则不再右移 555 | if (nesting < disqusjs.config.nesting) { 556 | html = '