碎碎念 仅有版本0.2.8及以前没有跨域功能,最新版本已经更新,请按照官方文档配置防止跨域即可,无需在nginx端进行设置了。
Memos是一款很受欢迎的备忘录应用,可以在服务器中利用Docker便捷的部署,可以在线发布说说,备忘录,并且可以利用API展示到前端,功能上很强大,但是由于API的多次更新,很难做到及时兼容,再加上每次升级后数据库都不可逆,很容易造成不可挽回的后果,给作者提建议作者似乎也有自己的想法,并不遵从大众的意见,于是只能继续使用旧版本,并且一直在找替代品。
在这期间,我找到了一些也很优秀的产品,比如My-flomo-server ,Nano ,blinko 等等,但是都因为各种原因,如内存占用,部署环境,数据存储等原因,无法再我的环境上使用,不得不说Memos这个产品本身确实很优秀,由于是Golang开发的,后端很轻,内存占用仅有30MB左右,而市面上很多同类型产品都是基于SpringBoot开发,在内存方面,Java的占用众所周知。
但是我也并不着急,毕竟Memos-0.21.0版本目前还是很适合我的,于是我也在慢慢找,在这个期间,我注意到了一个项目,Moments,这个项目的早期版本是基于世界上最好的语言: PHP,从2.1.0开始,作者听从社区的意见,修改成了Golang,内存大幅减小,功能性也更强,迭代到现在,基本功能已经实现,比如插入视频,豆瓣读书和电影,分享链接,分享音乐,分享图片,在Memos中我需要自定义这些,比如使用{bilibili 视频ID},但是在Moments,这些视频和音乐都有很完善的接口,返回数据也很直观,十分适合直接调用,于是我毫不犹豫的换了过来,最终实现了我很满意的效果。
这篇文章就给大家介绍一下我的修改方案,方便大家抄作业,顺便给也想要换掉Memos的朋友提供一条新路。
前期要求 硬件要求 软件要求 docker环境反向代理工具(本文以Nginx为例) 介绍与展示 这里先给大家展示一下最终的效果,注意该教程可能仅适合部分主题,如果出现主题不适配的情况请自行适配,这里以本站主题Hexo-theme-butterfly为基础进行修改:
说说页面:
由于前端才是最主要的展示区域,在这里我尽可能做了最多的适配,适配了豆瓣阅读书籍卡片分享,豆瓣电影分享,图片分享,链接分享,音乐分享,Bilibili分享,Youtube分享,黑夜模式等多种适配,这里仅展示部分功能,其中bilibili视频因为目前官方版本有问题,无法添加,yoputube由于拉低网络加载速度,不予展示。其他的具体效果可以上网站我的说说页面自行查看。
亮色模式 暗色模式 书籍分享 电影分享 音乐分享 图片分享 链接分享
轻量朋友圈
这里也没什么过多可以展示的,因为网站功能太多,无法一一展示,建议大家直接进入网站自行查看。
功能说明
Moments作为一个轻量朋友圈,其功能都是分享上的部分,如下所示:
分享:链接,图片,音乐,视频,书籍,电影 信息:自定义位置,自定义标签,是否公开 页面:Markdown渲染,编辑说说,删除说说,暗夜模式,自定义图标,信息,CSS及JS代码 功能:S3存储,文件查询,多用户注册,点赞,评论,API 简单介绍完毕,下面我就来教大家如何进行部署!
部署教程 Moments部署 Compose部署 官方给予了很完善的教程,这里我仅仅简单介绍一下docker-compose部署的方式,如果你想以源码等其他方式进行部署,请查看文章开头部分的github地址进行查阅。
首先,在服务器任意位置创建文件:docker-compose.yaml,填入以下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 version: '3' services: moments: image: kingwrcy/moments:latest container_name: moments restart: always environment: port: 3000 JWT_KEY: "自己随便生成点字符串" ENABLE_SWAGGER: "true" ports: - "3003:3000" volumes: - ./data:/app/data
注意文件,我将当前文件夹下的./data文件夹挂载了进去,数据都会在里面,迁移时仅需整体打包到新服务器即可。然后执行以下两条命令,后续需要升级也仅需要执行这两个命令:
1 2 docker-compose pull docker-compose up -d
如果网络环境不佳,可尝试替换docker源,这里找了一篇参考文章:Docker镜像加速说明 ,这里我推荐使用1panel的国内镜像:https://docker.1panel.live
如果一切正常,那么服务应该已经跑起来了,可以在终端输入:curl 127.0.0.1:3003,如果有输出,则代表正常。
通过反向代理将其添加到某个域名中,这里就不再多说了,各大面板都有极其完备的反代文档。
Nginx修改 在开头的Waring提到,Moments在跨域上没有进行任何设置,也就是默认的不允许跨域,这可能会导致无法在网站上使用API展示,如果你是1panel部署的服务,可以在网站管理中找到网站目录,点击进入:
返回到上一级目录,也就是域名名称的文件夹下,找到Proxy文件夹,编辑里面的root.conf文件为如下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 location / { add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS' ; add_header Access-Control-Allow-Headers 'Origin, X-Requested-With, Content-Type, Accept, Authorization' ; if ($request_method = 'OPTIONS' ) { add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS' ; add_header Access-Control-Allow-Headers 'Origin, X-Requested-With, Content-Type, Accept, Authorization' ; add_header Access-Control-Max-Age 1728000 ; add_header Content-Type 'text/plain charset=UTF-8' ; add_header Content-Length 0 ; return 204 ; } proxy_pass http://127.0.0.1:3003; proxy_set_header Host $host ; proxy_set_header X-Real-IP $remote_addr ; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for ; proxy_set_header REMOTE-HOST $remote_addr ; proxy_set_header Upgrade $http_upgrade ; proxy_set_header Connection $http_connection ; proxy_set_header X-Forwarded-Proto $scheme ; proxy_http_version 1 .1 ; add_header X-Cache $upstream_cache_status ; add_header Cache-Control no -cache; proxy_ssl_server_name off ; proxy_ssl_name $proxy_host ; add_header Strict-Transport-Security "max-age=31536000" ; }
下面是原代理设置,可以仅仅复制上面部分内容,下面保持不变,注意端口不要出问题。
如果是宝塔面板,可以在网站设置的配置文件找到Nginx配置文件,依照以上配置进行添加请求头部分,也可以通过最下面其他设置中的跨域访问CORS配置部分快捷的进行设置:
如果你是兰亭雷池进行的外部Nginx,找到目录/data/safeline/resources/nginx/custom_params,在里面找到你的对应的配置,每个网站有一个唯一ID,为递增排序,如果不知道对应哪一个,可以查看上一目录中的sites-enable目录下的文件,其中有一定的信息可以助你分辨是否为对应配置。
当然这里的编辑工具可以使任何可以编辑的页面,可视化是最方便的,这里是由于腾讯云无法通过root直接登录,打开的可视化界面权限不够,所以我选择在页面中使用管理员权限打开vim进行编辑。
比如我在这里是backend_29文件,在该目录下打开命令行并执行sudo vi ./backend_29,打开vim,点击i进行编辑,粘贴下面的内容:
1 2 3 add_header Access-Control-Allow-Origin * always;add_header Access-Control-Allow-Methods * always;add_header Access-Control-Allow-Headers * always;
编辑完成后,点击esc退出编辑,输入:wq进行保存,然后执行以下命令,检查配置文件是否运行正常:
1 2 3 4 docker exec safeline-tengine nginx -t
如果输出正常,执行命令重启Nginx:
1 docker exec safeline-tengine nginx -s reload
此时跨域配置应该就结束了,下面教大家如何将其数据在前端进行展示。
Nginx配置部分我也不是很懂,只是我通过这样的方式实践后是可行的,如果有更加优秀的配置欢迎留言!
前端实现 说说页面 由于该项目利用了MetingJS和APlayer,所以请提前引入这两个包,Hexo-theme-butterfly中虽然有内置的两个包,仅需修改配置文件即可开启,但是版本比较老,这里我建议自行引入最新版本,在配置中引入以下文件,注意css和js应该是分开引入的:
1 2 3 <link rel ="stylesheet" href ="https://fastly.jsdelivr.net/npm/aplayer@1.10.1/dist/APlayer.min.css" media ="all" onload ="this.media=" all" " > <script src ="https://fastly.jsdelivr.net/npm/aplayer@1.10.1/dist/APlayer.min.js" > </script > <script src ="https://fastly.jsdelivr.net/npm/meting@2.0.1/dist/Meting.min.js" > </script >
新建页面shuoshuo,在文件内写入一下内容:
1 2 3 4 5 6 7 --- title: 日常哔哔,键盘侠的日常吐槽 aside: false --- <div id ="talk" > </div > <div class ="limit" > - 只展示最近30条说说 -</div > <script src ="/js/shuoshuo.js" no-pjax > </script >
其中的JS文件地址清自行修改,自行创建,并写入以下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 function renderTalks ( ) { const talkContainer = document .querySelector ('#talk' ); if (!talkContainer) return ; talkContainer.innerHTML = '' ; const generateIconSVG = ( ) => { return `<svg viewBox="0 0 512 512"xmlns="http://www.w3.org/2000/svg"class="is-badge icon"><path d="m512 268c0 17.9-4.3 34.5-12.9 49.7s-20.1 27.1-34.6 35.4c.4 2.7.6 6.9.6 12.6 0 27.1-9.1 50.1-27.1 69.1-18.1 19.1-39.9 28.6-65.4 28.6-11.4 0-22.3-2.1-32.6-6.3-8 16.4-19.5 29.6-34.6 39.7-15 10.2-31.5 15.2-49.4 15.2-18.3 0-34.9-4.9-49.7-14.9-14.9-9.9-26.3-23.2-34.3-40-10.3 4.2-21.1 6.3-32.6 6.3-25.5 0-47.4-9.5-65.7-28.6-18.3-19-27.4-42.1-27.4-69.1 0-3 .4-7.2 1.1-12.6-14.5-8.4-26-20.2-34.6-35.4-8.5-15.2-12.8-31.8-12.8-49.7 0-19 4.8-36.5 14.3-52.3s22.3-27.5 38.3-35.1c-4.2-11.4-6.3-22.9-6.3-34.3 0-27 9.1-50.1 27.4-69.1s40.2-28.6 65.7-28.6c11.4 0 22.3 2.1 32.6 6.3 8-16.4 19.5-29.6 34.6-39.7 15-10.1 31.5-15.2 49.4-15.2s34.4 5.1 49.4 15.1c15 10.1 26.6 23.3 34.6 39.7 10.3-4.2 21.1-6.3 32.6-6.3 25.5 0 47.3 9.5 65.4 28.6s27.1 42.1 27.1 69.1c0 12.6-1.9 24-5.7 34.3 16 7.6 28.8 19.3 38.3 35.1 9.5 15.9 14.3 33.4 14.3 52.4zm-266.9 77.1 105.7-158.3c2.7-4.2 3.5-8.8 2.6-13.7-1-4.9-3.5-8.8-7.7-11.4-4.2-2.7-8.8-3.6-13.7-2.9-5 .8-9 3.2-12 7.4l-93.1 140-42.9-42.8c-3.8-3.8-8.2-5.6-13.1-5.4-5 .2-9.3 2-13.1 5.4-3.4 3.4-5.1 7.7-5.1 12.9 0 5.1 1.7 9.4 5.1 12.9l58.9 58.9 2.9 2.3c3.4 2.3 6.9 3.4 10.3 3.4 6.7-.1 11.8-2.9 15.2-8.7z"fill="#1da1f2"></path></svg>` ; } const waterfall = (a ) => { function b (a, b ) { var c = window .getComputedStyle (b); return parseFloat (c["margin" + a]) || 0 } function c (a ) { return a + "px" } function d (a ) { return parseFloat (a.style .top ) } function e (a ) { return parseFloat (a.style .left ) } function f (a ) { return a.clientWidth } function g (a ) { return a.clientHeight } function h (a ) { return d (a) + g (a) + b ("Bottom" , a) } function i (a ) { return e (a) + f (a) + b ("Right" , a) } function j (a ) { a = a.sort (function (a, b ) { return h (a) === h (b) ? e (b) - e (a) : h (b) - h (a) }) } function k (b ) { f (a) != t && (b.target .removeEventListener (b.type , arguments .callee ), waterfall (a)) } "string" == typeof a && (a = document .querySelector (a)); var l = [].map .call (a.children , function (a ) { return a.style .position = "absolute" , a }); a.style .position = "relative" ; var m = []; l.length && (l[0 ].style .top = "0px" , l[0 ].style .left = c (b ("Left" , l[0 ])), m.push (l[0 ])); for (var n = 1 ; n < l.length ; n++) { var o = l[n - 1 ], p = l[n], q = i (o) + f (p) <= f (a); if (!q) break ; p.style .top = o.style .top , p.style .left = c (i (o) + b ("Left" , p)), m.push (p) } for (; n < l.length ; n++) { j (m); var p = l[n], r = m.pop (); p.style .top = c (h (r) + b ("Top" , p)), p.style .left = c (e (r)), m.push (p) } j (m); var s = m[0 ]; a.style .height = c (h (s) + b ("Bottom" , s)); var t = f (a); window .addEventListener ? window .addEventListener ("resize" , k) : document .body .onresize = k }; const fetchAndRenderTalks = ( ) => { const url = 'https://mm.liushen.fun/api/memo/list' ; const cacheKey = 'talksCache' ; const cacheTimeKey = 'talksCacheTime' ; const cacheDuration = 30 * 60 * 1000 ; const cachedData = localStorage .getItem (cacheKey); const cachedTime = localStorage .getItem (cacheTimeKey); const currentTime = new Date ().getTime (); if (cachedData && cachedTime && (currentTime - cachedTime < cacheDuration)) { const data = JSON .parse (cachedData); renderTalks (data); } else { if (talkContainer) { talkContainer.innerHTML = '' ; fetch (url, { method : 'POST' , headers : { 'Content-Type' : 'application/json' , }, body : JSON .stringify ({ size : 30 }) }) .then (res => res.json ()) .then (data => { if (data.code === 0 && data.data && Array .isArray (data.data .list )) { localStorage .setItem (cacheKey, JSON .stringify (data.data .list )); localStorage .setItem (cacheTimeKey, currentTime.toString ()); renderTalks (data.data .list ); } }) .catch (error => { console .error ('Error fetching data:' , error); }); } } function renderTalks (list ) { if (Array .isArray (list)) { let items = list.map (item => formatTalk (item, url)); items.forEach (item => talkContainer.appendChild (generateTalkElement (item))); waterfall ('#talk' ); } else { console .error ('Data is not an array:' , list); } } }; const formatTalk = (item, url ) => { let date = formatTime (new Date (item.createdAt ).toString ()); let content = item.content ; let imgs = item.imgs ? item.imgs .split (',' ) : []; let text = content; content = text.replace (/\[(.*?)\]\((.*?)\)/g , `<a href="$2">@$1</a>` ) .replace (/- \[ \]/g , '⚪' ) .replace (/- \[x\]/g , '⚫' ); content = content.replace (/\n/g , '<br>' ); content = `<div class="talk_content_text">${content} </div>` ; if (imgs.length > 0 ) { const imgDiv = document .createElement ('div' ); imgDiv.className = 'zone_imgbox' ; imgs.forEach (e => { const imgLink = document .createElement ('a' ); imgLink.href = e; imgLink.setAttribute ('data-fancybox' , 'gallery' ); imgLink.className = 'fancybox' ; imgLink.setAttribute ('data-thumb' , e); const imgTag = document .createElement ('img' ); imgTag.src = e; imgLink.appendChild (imgTag); imgDiv.appendChild (imgLink); }); content += imgDiv.outerHTML ; } if (item.externalUrl ) { const externalUrl = item.externalUrl ; const externalTitle = item.externalTitle ; const externalFavicon = item.externalFavicon ; const externalContainer = ` <div class="shuoshuo-external-link"> <a class="external-link" href="${externalUrl} " target="_blank" rel="external nofollow noopener noreferrer"> <div class="external-link-left" style="background-image: url(${externalFavicon} )"></div> <div class="external-link-right"> <div class="external-link-title">${externalTitle} </div> <div>点击跳转<i class="fa-solid fa-angle-right"></i></div> </div> </a> </div>` ; content += externalContainer; } const ext = JSON .parse (item.ext || '{}' ); if (ext.music && ext.music .id ) { const music = ext.music ; const musicUrl = music.api .replace (':server' , music.server ) .replace (':type' , music.type ) .replace (':id' , music.id ); content += ` <meting-js server="${music.server} " type="${music.type} " id="${music.id} " api="${music.api} "></meting-js> ` ; } if (ext.doubanMovie && ext.doubanMovie .id ) { const doubanMovie = ext.doubanMovie ; const doubanMovieUrl = doubanMovie.url ; const doubanTitle = doubanMovie.title ; const doubanImage = doubanMovie.image ; const doubanDirector = doubanMovie.director || '未知导演' ; const doubanRating = doubanMovie.rating || '暂无评分' ; const doubanRuntime = doubanMovie.runtime || '未知时长' ; content += ` <a class="douban-card" href="${doubanMovieUrl} " target="_blank"> <div class="douban-card-bgimg" style="background-image: url('${doubanImage} ');"></div> <div class="douban-card-left"> <div class="douban-card-img" style="background-image: url('${doubanImage} ');"></div> </div> <div class="douban-card-right"> <div class="douban-card-item"><span>电影名: </span><strong>${doubanTitle} </strong></div> <div class="douban-card-item"><span>导演: </span><span>${doubanDirector} </span></div> <div class="douban-card-item"><span>评分: </span><span>${doubanRating} </span></div> <div class="douban-card-item"><span>时长: </span><span>${doubanRuntime} </span></div> </div> </a> ` ; } if (ext.doubanBook && ext.doubanBook .id ) { const doubanBook = ext.doubanBook ; const bookUrl = doubanBook.url ; const bookTitle = doubanBook.title ; const bookImage = doubanBook.image ; const bookAuthor = doubanBook.author ; const bookRating = doubanBook.rating ; const bookPubDate = doubanBook.pubDate ; const bookTemplate = ` <a class="douban-card" href="${bookUrl} " target="_blank"> <div class="douban-card-bgimg" style="background-image: url('${bookImage} ');"></div> <div class="douban-card-left"> <div class="douban-card-img" style="background-image: url('${bookImage} ');"></div> </div> <div class="douban-card-right"> <div class="douban-card-item"> <span>书名: </span><strong>${bookTitle} </strong> </div> <div class="douban-card-item"> <span>作者: </span><span>${bookAuthor} </span> </div> <div class="douban-card-item"> <span>出版年份: </span><span>${bookPubDate} </span> </div> <div class="douban-card-item"> <span>评分: </span><span>${bookRating} </span> </div> </div> </a> ` ; content += bookTemplate; } if (ext.video && ext.video .type ) { const videoType = ext.video .type ; const videoUrl = ext.video .value ; if (videoType === 'bilibili' ) { const biliTemplate = ` <div style="position: relative; padding: 30% 45%; margin-top: 10px;"> <iframe style="position: absolute; width: 100%; height: 100%; left: 0; top: 0; border-radius: 12px;" src="${videoUrl} &autoplay=0" scrolling="no" frameborder="no" allowfullscreen> </iframe> </div> ` ; content += biliTemplate; } else if (videoType === 'youtube' ) { const youtubeTemplate = ` <div style="position: relative; padding: 30% 45%; margin-top: 10px;"> <iframe width="100%" style="position: absolute; width: 100%; height: 100%; left: 0; top: 0; border-radius: 12px;" src="${videoUrl} " title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen> </iframe> </div> ` ; content += youtubeTemplate; } } return { content : content, user : item.user .nickname || '匿名' , avatar : item.user .avatarUrl || 'https://p.liiiu.cn/i/2024/03/29/66061417537af.png' , date : date, location : item.location || '陕西西安' , tags : item.tags ? item.tags .split (',' ).filter (tag => tag.trim () !== '' ) : ['无标签' ], text : content.replace (/\[(.*?)\]\((.*?)\)/g , '[链接]' + `${imgs.length ? '[图片]' : '' } ` ) }; }; const generateTalkElement = (item ) => { const talkItem = document .createElement ('div' ); talkItem.className = 'talk_item' ; const talkMeta = document .createElement ('div' ); talkMeta.className = 'talk_meta' ; const avatar = document .createElement ('img' ); avatar.className = 'no-lightbox avatar' ; avatar.src = item.avatar ; const info = document .createElement ('div' ); info.className = 'info' ; const talkNick = document .createElement ('span' ); talkNick.className = 'talk_nick' ; talkNick.innerHTML = `${item.user} ${generateIconSVG()} ` ; const talkDate = document .createElement ('span' ); talkDate.className = 'talk_date' ; talkDate.textContent = item.date ; const talkContent = document .createElement ('div' ); talkContent.className = 'talk_content' ; talkContent.innerHTML = item.content ; const talkBottom = document .createElement ('div' ); talkBottom.className = 'talk_bottom' ; const TagContainer = document .createElement ('div' ); const talkTag = document .createElement ('span' ); talkTag.className = 'talk_tag' ; talkTag.textContent = `🏷️${item.tags} ` ; const locationTag = document .createElement ('span' ); locationTag.className = 'location_tag' ; locationTag.textContent = `🌍${item.location} ` ; TagContainer .appendChild (talkTag); TagContainer .appendChild (locationTag); const commentLink = document .createElement ('a' ); commentLink.href = 'javascript:;' ; commentLink.onclick = () => goComment (item.text ); const commentIcon = document .createElement ('span' ); commentIcon.className = 'icon' ; const commentIconInner = document .createElement ('i' ); commentIconInner.className = 'fa-solid fa-message fa-fw' ; commentIcon.appendChild (commentIconInner); commentLink.appendChild (commentIcon); talkMeta.appendChild (avatar); info.appendChild (talkNick); info.appendChild (talkDate); talkMeta.appendChild (info); talkItem.appendChild (talkMeta); talkItem.appendChild (talkContent); talkBottom.appendChild (TagContainer ); talkBottom.appendChild (commentLink); talkItem.appendChild (talkBottom); return talkItem; }; const goComment = (e ) => { const match = e.match (/<div class="talk_content_text">([\s\S]*?)<\/div>/ ); const textContent = match ? match[1 ] : "" ; const n = document .querySelector (".atk-textarea" ); n.value = `> ${textContent} \n\n` ; n.focus (); btf.snackbarShow ("已为您引用该说说,不删除空格效果更佳" ); }; const formatTime = (time ) => { const d = new Date (time); const ls = [ d.getFullYear (), d.getMonth () + 1 , d.getDate (), d.getHours (), d.getMinutes (), d.getSeconds (), ]; const r = ls.map ((a ) => (a.toString ().length === 1 ? '0' + a : a)); return `${r[0 ]} -${r[1 ]} -${r[2 ]} ${r[3 ]} :${r[4 ]} ` ; }; fetchAndRenderTalks (); } renderTalks ();
自行修改js文件中的Moments地址为你的地址,在文件中,有一个gocomment函数,实现的是获取卡片中的文本内容,如果如果出现不匹配的情况,请自行修改一下类名,这里我匹配的是artalk的输入框。
然后引入样式文件,这个文件可以在配置文件中引用,也可以在页面文件中类似于shuoshuo.js一样引用,样式内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 :root { --liushen-card-bg : #fff ; --liushen-card-border : 1px solid #e3e8f7 ; --card-box-shadow : 0 3px 8px 6px rgba (7 ,17 ,27 ,0.09 ); --card-hover-box-shadow : 0 3px 8px 6px rgba (7 ,17 ,27 ,0.2 ); --liushen-card-secondbg : #f1f3f8 ; --liushen-button-hover-bg : #2679cc ; --liushen-text : #4c4948 ; --liushen-button-bg : #f1f3f8 ; --liushen-fancybox-bg : rgba (255 ,255 ,255 ,0.5 ); } :root , [data-theme=dark] { --liushen-card-bg : #181818 ; --liushen-card-secondbg : #30343f ; --liushen-card-border : 1px solid #42444a ; --card-box-shadow : 0 3px 8px 6px rgba (7 ,17 ,27 ,0.09 ); --card-hover-box-shadow : 0 3px 8px 6px rgba (7 ,17 ,27 ,0.2 ); --liushen-button-bg : #30343f ; --liushen-button-hover-bg : #2679cc ; --liushen-text : rgba (255 ,255 ,255 ,0.702 ); --liushen-fancybox-bg : rgba (0 ,0 ,0 ,0.5 ); } #talk .talk_item { width : calc (33.333% - 6px ); background : var (--liushen-card-bg); border : var (--liushen-card-border); box-shadow : var (--card-box-shadow); transition : box-shadow .3s ease-in-out; border-radius : 12px ; display : flex; flex-direction : column; padding : 20px ; margin-bottom : 9px ; margin-right : 9px ; } #talk .talk_item :hover { box-shadow : var (--card-hover-box-shadow); } @media (max-width : 900px ) { #talk .talk_item { width : calc (50% - 5px ); } } @media (max-width : 450px ) { #talk .talk_item { width : calc (100% ); } } #talk { position : relative; width : 100% ; box-sizing : border-box; } #talk .talk_meta .avatar { margin : 0 !important ; width : 60px ; height : 60px ; border-radius : 12px ; } #talk .talk_bottom ,#talk .talk_meta { display : flex; align-items : center; } #talk .talk_meta { display : flex; align-items : center; width : 100% ; padding-bottom : 10px ; border-bottom : 1px dashed grey; } #talk .talk_bottom { margin-top : 15px ; padding-top : 10px ; border-top : 1px dashed grey; justify-content : space-between; } #talk .talk_meta .info { display : flex; flex-direction : column; margin-left : 10px ; } #talk .talk_meta .info .talk_nick { color : #6dbdc3 ; font-size : 1.2rem ; } #talk .talk_meta .info svg .is-badge .icon { width : 15px ; padding-top : 3px ; } #talk .talk_meta .info span .talk_date { opacity : .6 ; } #talk .talk_item .talk_content { margin-top : 10px ; } #talk .talk_item .talk_content .zone_imgbox { display : flex; flex-wrap : wrap; --w : calc (25% - 8px ); gap : 10px ; margin-top : 10px ; } #talk .talk_item .talk_content .zone_imgbox a { display : block; border-radius : 12px ; width : var (--w); aspect-ratio : 1 /1 ; position : relative; } #talk .talk_item .talk_content .zone_imgbox a :first-child { width : 100% ; aspect-ratio : 1.8 ; } #talk .talk_item .talk_content .zone_imgbox img { border-radius : 10px ; width : 100% ; height : 100% ; margin : 0 !important ; object-fit : cover; } #talk .talk_item .talk_bottom { opacity : .9 ; } #talk .talk_item .talk_bottom .icon { float : right; transition : all .3s ; } #talk .talk_item .talk_bottom .icon :hover { color : #49b1f5 ; } #talk .talk_item .talk_bottom span .talk_tag ,#talk .talk_item .talk_bottom span .location_tag { font-size : 14px ; background-color : var (--liushen-card-secondbg); border-radius : 12px ; padding : 3px 15px 3px 10px ; transition : box-shadow 0.3s ease; box-shadow : 0 2px 4px rgba (0 , 0 , 0 , 0.1 ); } #talk .talk_item .talk_bottom span .location_tag { margin-left : 5px ; } #talk .talk_item .talk_bottom span .talk_tag :hover ,#talk .talk_item .talk_bottom span .location_tag :hover { box-shadow : 0 4px 8px rgba (0 , 0 , 0 , 0.3 ); } #talk .talk_item .talk_content >a { margin : 0 3px ; color : #ff7d73 !important ; } #talk .talk_item .talk_content >a :hover { text-decoration : none !important ; color : #ff5143 !important } @media screen and (max-width : 900px ) { #talk .talk_item .talk_content .zone_imgbox { --w : calc (33% - 5px ); } #talk .talk_item #post-comment { margin : 0 3px } } @media screen and (max-width : 768px ) { .zone_imgbox { gap : 6px ; } .zone_imgbox { --w : calc (50% - 3px ); } span .talk_date { font-size : 14px ; } } #talk .talk_item .talk_content .douban-card { margin-top : 10px !important ; text-decoration : none; align-items : center; border-radius : 12px ; color : #faebd7 ; display : flex; justify-content : center; margin : 10px ; max-width : 400px ; overflow : hidden; padding : 15px ; position : relative; } .douban-card .douban-card-bgimg { background-position : 50% ; background-repeat : no-repeat; background-size : 100% ; filter : blur (15px ) brightness (.6 ); height : 115% ; position : absolute; width : 115% ; } .douban-card .douban-card-left { align-items : center; display : flex; flex-direction : column; position : relative; } .douban-card .douban-card-left .douban-card-img { transition : all .5s ease; height : 130px ; position : relative; width : 80px ; background-position : 50% ; background-repeat : no-repeat; background-size : 100% ; } .douban-card .douban-card-left :hover .douban-card-img { filter : blur (5px ) brightness (.6 ); transform : perspective (800px ) rotateX (180deg ); } .douban-card .douban-card-right { color : #faebd7 ; display : flex; flex-direction : column; font-size : 14px ; line-height : 1.5 ; margin-left : 12px ; position : relative; } .douban-card .douban-card-right .douban-card-item { margin-top : 4px ; max-width : 95% ; overflow : hidden; text-overflow : ellipsis; white-space : nowrap; } #talk .talk_item .talk_content .shuoshuo-external-link { width : 100% ; height : 80px ; margin-top : 10px ; border-radius : 12px ; background-color : var (--liushen-card-secondbg); color : var (--liushen-card-text); border : var (--liushen-card-border); transition : background-color .3s ease-in-out; } .shuoshuo-external-link :hover { background-color : var (--liushen-button-hover-bg); } .shuoshuo-external-link .external-link { display : flex; color : var (--liushen-text) !important ; width : 100% ; height : 100% ; } .shuoshuo-external-link .external-link :hover { color : white !important ; } .shuoshuo-external-link .external-link :hover { text-decoration : none !important ; } .shuoshuo-external-link .external-link-left { width : 60px ; height : 60px ; margin : 10px ; border-radius : 12px ; background-size : cover; background-position : center; } .shuoshuo-external-link .external-link-right { display : flex; flex-direction : column; justify-content : center; width : calc (100% - 80px ); padding : 10px ; } .shuoshuo-external-link .external-link-right .external-link-title { font-size : 1.0rem ; font-weight : 800 ; white-space : nowrap; overflow : hidden; text-overflow : ellipsis; } .shuoshuo-external-link .external-link-right i { margin-left : 5px ; } .limit { width : 100% ; text-align : center; margin-top : 30px ; }
如果一切正常,应该就可以显示了,如果仍然有问题可以评论区讨论,我们在这里继续进行下一步:
首页轮播 本次修改我还修改了首页轮播部分的内容,由于请求的地址一致,这里我直接用了同一个缓存,可以使网站的加载速度更近一步,缓存时间为半个小时,缓存位置为localstorage,缓存数据共30条,由于首页的轮播只需要最新的内容,这里我们取到缓存后节选前五条即可。
在主题配置文件内部引入外部js,名称位置随意,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 let talkTimer = null ;const cacheKey = 'talksCache' ;const cacheTimeKey = 'talksCacheTime' ;const cacheDuration = 30 * 60 * 1000 ; function indexTalk ( ) { if (talkTimer) { clearInterval (talkTimer); talkTimer = null ; } if (!document .getElementById ('bber-talk' )) return ; function toText (ls ) { let text = []; ls.forEach (item => { text.push (item.content .replace (/#(.*?)\s/g , '' ).replace (/\{(.*?)\}/g , '' ).replace (/\!\[(.*?)\]\((.*?)\)/g , '<i class="fa-solid fa-image"></i>' ).replace (/\[(.*?)\]\((.*?)\)/g , '<i class="fa-solid fa-link"></i>' )); }); return text; } function talk (ls ) { let html = '' ; ls.forEach ((item, i ) => { html += `<li class="item item-${i + 1 } ">${item} </li>` }); let box = document .querySelector ("#bber-talk .talk-list" ); box.innerHTML = html; talkTimer = setInterval (() => { box.appendChild (box.children [0 ]); }, 3000 ); } const cachedData = localStorage .getItem (cacheKey); const cachedTime = localStorage .getItem (cacheTimeKey); const currentTime = new Date ().getTime (); if (cachedData && cachedTime && (currentTime - cachedTime < cacheDuration)) { const data = toText (JSON .parse (cachedData)); talk (data.slice (0 , 6 )); } else { fetch ('https://mm.liushen.fun/api/memo/list' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' }, body : JSON .stringify ({ size : 30 }) }) .then (res => res.json ()) .then (data => { if (data.code === 0 && data.data && Array .isArray (data.data .list )) { localStorage .setItem (cacheKey, JSON .stringify (data.data .list )); localStorage .setItem (cacheTimeKey, currentTime.toString ()); const formattedData = toText (data.data .list ); talk (formattedData.slice (0 , 6 )); } }) .catch (error => console .error ('Error fetching data:' , error)); } } function whenDOMReady ( ) { indexTalk (); } whenDOMReady ();document .addEventListener ("pjax:complete" , whenDOMReady);
注意自行修改其中第42行的API地址为你自己的。
然后在主题文件中添加以下的样式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 #main_top { display : flex; justify-content : center; z-index : 1 ; max-width : 1200px ; margin : 20px auto; width : 100% ; padding : 0 15px ; margin-top : 40px ; margin-bottom : 0px ; } .hide-aside #main_top { width : 80% ; } .hide-aside #main_top #bber-talk { max-width : 936px ; } @media screen and (min-width : 2000px ) { .hide-aside #main_top #bber-talk { max-width : 80% ; } #main_top { max-width : 70% ; } } @media screen and (max-width : 1210px ) { .hide-aside #main_top { padding : 0 12px ; } } @media screen and (max-width : 900px ) { .hide-aside #main_top { width : 100% ; padding : 0 15px ; } } @media screen and (max-width : 768px ) { .hide-aside #main_top { padding : 0 5px ; } div #main_top { margin-top : 20px ; padding : 0 5px ; } } #bber-talk { box-sizing : border-box; cursor : pointer; width : 100% ; min-height : 50px ; padding : .5rem 1rem ; display : flex; align-items : center; overflow : hidden; font-weight : 700 ; } #bber-talk ,#bber-talk a { color : var (--font-color); } #bber-talk svg .icon { width : 1em ; height : 1em ; vertical-align : -.15em ; fill : currentColor; overflow : hidden; font-size : 20px ; } #bber-talk .item i { margin-left : 5px ; } #bber-talk > i { font-size : 1.1rem ; } #bber-talk .talk-list { flex : 1 ; max-height : 32px ; font-size : 16px ; padding : 0 ; margin : 0 ; overflow : hidden; } #bber-talk .talk-list :hover { color : var (--default-bg-color); transition : all .2s ease-in-out; } #bber-talk .talk-list li { list-style : none; width : 100% ; white-space : nowrap; text-overflow : ellipsis; overflow : hidden; margin-left : 10px ; } @media screen and (min-width : 770px ) { #bber-talk .talk-list { text-align : center; margin-right : 20px ; } }
此时我们还缺少一个插入点,我们需要确保我们的轮播条在最顶部的位置轮播,创建文件themes\butterfly\layout\includes\others\memos_home.pug,写入以下内容:
1 2 3 4 5 6 7 if (is_home()) #main_top #bber-talk.cardHover.bb_talk_swipper(onclick=`pjax.loadUrl("/shuoshuo/")`) svg.icon(t='1660960757124', viewBox='0 0 1024 1024', version='1.1', xmlns='http://www.w3.org/2000/svg', p-id='3946', width='200', height='200') path(d='M526.432 924.064c-20.96 0-44.16-12.576-68.96-37.344L274.752 704H192c-52.928 0-96-43.072-96-96V416c0-52.928 43.072-96 96-96h82.752l182.624-182.624c24.576-24.576 47.744-37.024 68.864-37.024C549.184 100.352 576 116 576 160v704c0 44.352-26.72 60.064-49.568 60.064zM192 384c-17.632 0-32 14.368-32 32v192c0 17.664 14.368 32 32 32h96c8.48 0 16.64 3.36 22.624 9.376l192.064 192.096c3.392 3.36 6.496 6.208 9.312 8.576V174.016a145.824 145.824 0 0 0-9.376 8.608l-192 192C304.64 380.64 296.48 384 288 384h-96zM687.584 730.368a31.898 31.898 0 0 1-18.656-6.016c-14.336-10.304-17.632-30.304-7.328-44.672l12.672-17.344C707.392 617.44 736 578.624 736 512c0-69.024-25.344-102.528-57.44-144.928-5.664-7.456-11.328-15.008-16.928-22.784-10.304-14.336-7.04-34.336 7.328-44.672 14.368-10.368 34.336-7.04 44.672 7.328 5.248 7.328 10.656 14.464 15.968 21.504C764.224 374.208 800 421.504 800 512c0 87.648-39.392 141.12-74.144 188.32l-12.224 16.736c-6.272 8.704-16.064 13.312-26.048 13.312z', p-id='3947') path(d='M796.448 839.008a31.906 31.906 0 0 1-21.088-7.936c-13.28-11.648-14.624-31.872-2.976-45.152C836.608 712.672 896 628.864 896 512s-59.392-200.704-123.616-273.888c-11.648-13.312-10.304-33.504 2.976-45.184 13.216-11.648 33.44-10.336 45.152 2.944C889.472 274.56 960 373.6 960 512s-70.528 237.472-139.488 316.096c-6.368 7.232-15.2 10.912-24.064 10.912z', p-id='3948') ul.talk-list 说说加载中。。。
创建了元素,我们需要通过一个注入点引入这个文件,将其插入到主页中,打开文件themes\butterfly\layout\includes\layout.pug,按照下面的位置插入我们的文件引用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 - var htmlClassHideAside = theme.aside.enable && theme.aside.hide ? 'hide-aside' : '' - page.aside = is_archive() ? theme.aside.display.archive: is_category() ? theme.aside.display.category : is_tag() ? theme.aside.display.tag : page.aside - var hideAside = !theme.aside.enable || page.aside === false ? 'hide-aside' : '' - var pageType = is_post() ? 'post' : 'page' - pageType = page.type ? pageType + ' type-' + page.type : pageType doctype html html(lang=config.language data-theme=theme.display_mode class=htmlClassHideAside) head include ./head.pug body if theme.preloader.enable !=partial('includes/loading/index', {}, {cache: true}) if theme.background #web_bg !=partial('includes/sidebar', {}, {cache: true}) #body-wrap(class=pageType) include ./header/index.pug + include ./others/memos_home.pug main#content-inner.layout(class=hideAside) if body div!= body else block content if theme.aside.enable && page.aside !== false include widget/index.pug - const footerBg = theme.footer_img - const footer_bg = footerBg ? footerBg footer#footer(style=footer_bg) !=partial('includes/footer', {}, {cache: true}) include ./rightside.pug !=partial('includes/rightmenu', {}, {cache: true}) include ./additional-js.pug
添加第22行,注意不要抄前面的加号,缩进与上面的include对齐即可。这样我们的首页轮播也实现了,如果有样式不对的地方请自行微调。
额外教程 Meting 由于Moments的音乐部分需要使用MetingJS,如果用默认的服务可能会很慢,非常影响速度,所以我建议自建,这里我找到的项目是:
这个项目支持多种部署方式,除了源码部署,还可以通过Docker部署,Deno平台部署以及Vercel一键部署,速度上大家自行判断。
由于作者使用的为轻量化框架Deno,对于X-Forwarded请求头或transparent proxy并不支持,所以实际有用的只有X-Forwarded-Host请求头,我们需要将/meting的流量都转发到/上,我们需要自己修改Nginx配置文件,这里官方是有介绍的,只需要在Nginx配置文件中添加一个转发即可,详情请见上方链接,这里我只说明雷池的修改方式:
首先在终端中cd到目录/data/safeline/resources/nginx/custom_params,通过vim打开对应的配置文件,比如这里我对应的配置文件ID为49:
1 2 root@VM-4-11-ubuntu:/home/ubuntu# cd /data/safeline/resources/nginx/custom_params root@VM-4-11-ubuntu:/data/safeline/resources/nginx/custom_params# vi ./backend_49
在打开的vim窗口中,点击字母i,输入以下的内容:
1 2 3 4 location ^~ /meting/ { proxy_pass http://[你的源地址IP]:3040/; proxy_set_header X-Forwarded-Host $scheme ://$host :$server_port /meting; }
输入完成后,点击esc,输入:wq强制保存并退出。
同样通过以下命令检查并重启Nginx:
1 2 3 4 5 root@VM-4-11-ubuntu:/data/safeline/resources/nginx/custom_params# docker exec safeline-tengine nginx -t nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful root@VM-4-11-ubuntu:/data/safeline/resources/nginx/custom_params# docker exec safeline-tengine nginx -s reload root@VM-4-11-ubuntu:/data/safeline/resources/nginx/custom_params#
重启应该就实现效果了,但是访问根目录可能并没有变https,因为我们这里的配置是将/meting的数据发送到根域名并采用https,所以在音乐页面,我们可以填写的api地址如下:
1 https://meting.example.com/meting/api?server=:server&type=:type&id=:id&r=:r
保存后应该就可以看到后台没有http访问的报错和警告了。
当然这里的编辑工具可以使任何可以编辑的页面,可视化是最方便的,这里是由于腾讯云无法通过root直接登录,打开的可视化界面权限不够,所以我选择在页面中使用管理员权限打开vim进行编辑。
S3配置 这里我感觉稍微有点卡顿,如果错误配置可能会影响使用(可能是我太笨了QAQ),所以稍微记录一下,也希望能给别人提供一些帮助。
在S3上我选择缤纷云,每个月有10GB流量免费额度,和50GB存储免费额度,放在轻量朋友圈上是包用不完的,首先创建一个存储桶,如果你不打算绑定外部地址,那就选择公开桶,但是经过测试公开桶需要余额大于零。如果你打算绑定备案域名,那就不需要公开桶,然后配置好权限访问后,如果师公开桶,你会得到一个桶的域名,比如缤纷云的话,一般格式为https://你的桶名称.s3.bitiful.net,这个就是你图片直链的域名。
如果你打算自行绑定域名到桶,那就是自己的域名了,然后去左边对象存储的AccessKey中创建一个子用户,你会获得一个子用户AccessKey和SecretKey.
回到Moments后台的S3配置中,按照下图进行配置:
注意最后一个参数,不是图片后缀名称,而是说S3服务商所支持的图片参数,比如我这里选了文件格式为avif,如果不支持他会自动返回其他格式。
这样就配置好了,后面你在友圈产生的任何图片,比如分享书籍,电影产生的背景图,分享图片产生的直链都会自动传到存储中,这个图片没有后缀名,不要被误导了。
总结 Moments可能功能不是很多,但是非常适合我个人,我也希望作者赶紧修好B站的上传,这玩意可是刚需呜呜呜。
界面上足够简单,占用比Memos还低,仅仅十几MB的存储,几乎所有服务器都能无负担的部署成功,使用SQLite存储也很好迁移,作者非常好,让大家提意见,他一点点实现,我认为这才是开源社区应该有的模样,而不是这个版本还没完善,就破坏性更新下一版本,同时会自动修改数据库格式,如果错误更新不允许回退,我个人认为这个领域霸主 似乎有点德不配位,当然我可能并没有资格去评判,Time will tell, the audience will show.
我个人是偏向于不断的更新,跟上作者的更新步伐,体验最新的功能,很乐意给予反馈和贡献,我也希望可以给开源项目作者提供一定的帮助,但是如果每次更新都需要花大量的时间阅读文档重新适配API等各种东西,在精力上可能稍微有点接受不了。当然可能是我个人要求太多,能力较低,无法正确适配API,Memos抛开API不谈,用起来还是很舒服的,功能强大,同时功能也很完善,是付费产品flomo的完美自托管替代品,希望作者越做越好!
参考链接 每日一图 图片来自哲风壁纸