








其实,要是写上一篇文章的时候想起来了那么多,也不会放到这里罢。
2021年底,六年级。
当年尚无什么web方面的前端知识,不过早早的就想要一个自己的网站,这个梦想其实并不是当时才有的。
四年级。
彼时小学的我,三年级在学校的兴趣班学了Scratch,跟着傻猫猫玩了不久,有时看到别人居然有自己的网站!还那么好看,便有了想法。
然后借着一次考试考好的名义整了个树莓派玩了玩Wordpress,折腾了内网穿透免费域名等等东西,可是某一天树莓派坏了,不济而终。
两年后,六年级。
偶然发现了Github Page,此物甚为有趣,居然可以部署静态站点,此时正是Hexo风行之时,自然而然搜到了许多部署Hexo博客的教程,于互联网上搜集许久,后来便选择了Butterfly主题,结下了不解之缘。
看到别人的站点,总是非常羡慕,模仿之心便是有的,四处收集,键盘一敲,杂糅一片,便是整个博客。
事实上,经历了一年的沉淀,博客的变化便不算大了,后来小修小补更多,性能也不知是怎么样了呢?
这便是那著名的博客,Butterfly博客圈的圣地之一,店长Akilar精心经营的地方。
进了店长的QQ群,便是万分有趣,遇到了许多许多的大佬,结识了不少的朋友。
但是,他们都说,卡。
怎么办?!
摆烂。
卡卡卡。
今年早些时候,在群里认识了Redish101数据库大佬,比我还小,从软件到硬件,从Web到底层样样精通,技术栈个个不差,实在是各位所羡慕不起的。
发现了他的reblog,是Next.js+Go编写的,开发比起原生极其方便,其实也跃跃欲试了。
前不久,突然掀起了一股小的逃离Butterfly重构潮,似乎我才是最需要重构的,来吧。
用什么呢,数据库自然强烈推荐用Next.js编写了,Next.js就 Next.js 吧,反正也没学过,对吧。
具体的话,是这样的:
ariasaka
这样子的话 我去学学nextjs吧先
Redish101
对以后的自己和协作者负责的话最好用ts
Butterfly主题自然是不错,但是它本身的问题也不少,可扩展性看似强,其实依然是魔改,既然有了魔改,也便有了不少的史山技术债。
长久以来的魔改,不仅使得博客源码混乱,也不便于扩展,接下来的问题大家也发现了,当年不精通前端知识的Aria留下的史山代码维护难度变得极高。
即使从Hexo出发,Hexo也并不是一个理想的静态博客框架,api接口令开发者望而却步,若非是Hexo的生态极好,估计选择的人也不多。
新的静态博客框架固然很多,例如Hugo VitePress之类,前面既然说了要SSR了,必然需要一个CMS,这么看来,就学一个新框架,自己编写,体验一下新的前端。
都要上学外出嘛,每次push个仓库改的确不方便,CSR不太可能,为了SEO,选择了 SSR
这么看来,前后端分离就很重要了,前端选择一个便捷的现代前端框架,自然是大佬用了都说好的 Next.js!!!
另外关于样式的话,我认为原子化模块化并不方便,TailWind那样子的CSS-in-JS的写法也并不舒适,于是选择了最容易的 原生CSS3
其它的,据数据库所推荐,还部分使用了 TypeScript。 (不过写着写着就变AnyScript了有些第三方库没写声明直接改后缀js)
后端用新的语言实在困难,掐指一算,只会Node.js Python和C++了,而Node.js的话嘛,JS本来就是为浏览器设计的,Node依然不太舒服,C++对于字符串处理极为困难,不太可能。
那就 Python 吧, FastAPI 。
数据库的选择上,毕竟咱也不会SQL,即使是做了ORM映射也还是颇有难度,熟悉的JsonDB的话却只能用在静态文件上面。
MongoDB 还是最好的选择啊😋。
自然是先写前端,再根据需求写后端了。
项目在8月24日开始,截至今天9月3日,爆肝十一天,还未实现搜索 归档 分类和标签功能,部分分页面暂未撰写,其实进度不错。
据Redish101大佬所说,只需仔细阅读:
和
前往以下网站,不保证安全性哦喵~Next.js documentationNext.js By Vercel
以及
前往以下网站,不保证安全性哦喵~TypeScript 文档Microsoft
便可以不写史山和AnyScript了。
我都没好好读TAT
这样的话,写写前端和样式,预定义测试数据,完成后便可接入后端了。
后端的话其实更简单,相当一部分查询可以丢给ai帮助完成,FastAPI的封装性很高,自己需要干涉的地方很少,只需要老老实实的写查询语句丢回来就好了。
于是:
前往以下网站,不保证安全性哦喵~AriaBlogNext-frontendGithub前往以下网站,不保证安全性哦喵~
AriaBlogNext-backendGithub
看样子事情比较顺利,不过磕磕绊绊实在不少,要不来讲一些事情吧。
Bing和Google比起百度来说无非是两个有着更多的重要有关信息的搜索引擎,任何时候都不要查阅百度和CSDN!
前往以下网站,不保证安全性哦喵~Bing SearchMicrosoft前往以下网站,不保证安全性哦喵~
Google SearchGoogle
不要忽略有意义的英文资料而只阅读中文,当您看不懂时请下载沉浸式翻译插件来读懂并获得好的体验!
前往以下网站,不保证安全性哦喵~沉浸式翻译Microsoft Edge Addons
ChatGPT将会是你编码过程中的好帮手!当你遇到了难以描述清晰或者搜索引擎没有的资料和问题时,随时准备询问它!
对于编辑器代码补全,选择CodeGeex插件,这会帮助你减少工作量。
Markdown的渲染对于任何的博客来说都是极为关键的,虽然这或许不包括WordPress等有头CMS,但是为了作者的编写体验更好,往往还是会选择使用Markdown存储数据。
我选择了 Marked+KaTex+Pangu.js+HighLight.js+HighLight.js Number+Vanilla-Lazyload+Fancybox 以及正则表达式匹配外挂标签来完整渲染 Markdown。
看起来好麻烦啊,不是在找罪受吗?
事实上,这样子做是有原因的。
毕竟像是@next/mdx这样子的便捷项目很难实现,那就只能手动生成html文本然后绑定到文章容器的dangrouslySetInnerHtml上面。
基本告别了 React 方式对内部组件操作,此时对原生html的可自定义性非常重要。
自然发现,主流的渲染器就那几个,remark的自定义渲染非常困难,几乎只能在转为rehype类型之后覆写,而且这个覆写API很不完善,还容易误操作Markdown里面本来就存在的HTML标签。
怎么能忍呢?
毕竟 marked 的自定义渲染还是很好用的,看起来没有生态,实际上自己整整就能搞定。
因为每一次的渲染耗时对网页加载比较明显,况且React的cache()的生命周期有限,采用了渲染后推送到后端缓存的方式来解决渲染耗时长的问题,事实上下次渲染直接读取文章信息的cachedHtml而不经过MDRenderer函数了。
至于缓存生命周期,后端更新的时候也许是会一起更新的。
为什么不直接在后端渲染呢?
因为Python实在没有好使的Markdown渲染库()
let currentPost:Post;
try{
currentPost=await getPostInfo(params.slug);
}catch(e){
return notFound();
}
let htmlContent:string;
if(currentPost.cachedHtml)
htmlContent=currentPost.cachedHtml; //直接使用数据库的缓存
else
htmlContent=await MDRender(currentPost.mdContent,currentPost.slug);
return (<>
<title>{currentPost.title+" | "+siteConfigs.title}</title>
<style>{`#navbar{position:fixed}`}</style>
<PostHeader postInfo={currentPost}/>
<div id="main-container" className="post">
<PostContent htmlContent={htmlContent} postInfo={currentPost}/>
...
</div>
</>);我采用自定义渲染函数来渲染一些组件,使其可以被第三方的js很好的操作
import { cache } from "react";
import { marked } from "marked";
import { siteConfigs } from "config";
import { MarkedCustomTags } from "utils/mdplugins";
const MDRenderer=cache(async (mdContent,slug="")=>{
const renderer=new marked.Renderer();
//...预处理
renderer.link=MarkedCustomTags.a; //放到另一个文件去了,不然太冗长
renderer.image=MarkedCustomTags.img;
renderer.codespan=MarkedCustomTags.codespan;
renderer.code=MarkedCustomTags.code;
renderer.del=MarkedCustomTags.del;
renderer.heading=MarkedCustomTags.heading;
marked.use({renderer: renderer});
let renderedHtml=await marked.parse(mdContent); //推送内容到后端缓存
if(slug){
fetch(`${siteConfigs.backEndUrl}/update/post/pushRenderedHtmlCache`,{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
slug: slug,
html: renderedHtml,
secret: process.env.SECRET
})
});
}
return renderedHtml;
});
export default MDRenderer;export class MarkedCustomTags{
...
static img({href,title,text}){
return `<a href="${href}" title="点击查看大图" data-fancybox="gallery"><img class="normal-img lazy-img" data-src="${href}" alt="${text}" src="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=" onerror="this.src='data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='"/></a>`;
} //适配fancybox
static code({text,lang}){
let language=lang?lang.toUpperCase():"";
if(!language) language="TEXT";
if(language == "") language="TEXT";
let highlightedCode;
try{ //未知语言异常处理
highlightedCode=hljs.highlight(language,text).value;
}
catch(e){
language="TEXT";
highlightedCode=hljs.highlight(language,text).value;
}
return `<details class="hljs-folder" open><summary><span class="hljs-lang">${language}</span><button class="hljs-copy" title="复制代码"><svg xmlns="http://www.w3.org/2000/svg" width="0.88em" height="1em" viewBox="0 0 448 512"><path fill="#4a2121" d="M208 0h124.1C344.8 0 357 5.1 366 14.1L433.9 82c9 9 14.1 21.2 14.1 33.9V336c0 26.5-21.5 48-48 48H208c-26.5 0-48-21.5-48-48V48c0-26.5 21.5-48 48-48M48 128h80v64H64v256h192v-32h64v48c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V176c0-26.5 21.5-48 48-48"/></svg></button></summary><pre class="hljs language-${language}" data-language="${language}"><code>${highlightedCode}</code></pre></details>`;
} //处理高亮,并添加折叠框和复制键,因为本来图标库只针对React组件,实在不行的话,就只能用svg了
...
static heading({text,depth}){
let id=stringRandom(8,{numbers: false});
return `<a class="heading-link" onclick=\"document.documentElement.scroll({top:this.offsetTop-70,behavior:\'smooth\'})\"><h${depth} id="title-${id}">${text}</h${depth}></a>`;
} //添加锚点,怕标题重名,生成随机字串
...
}MathJax文档实在少,放弃了()
因为 $\KaTeX$ 需要一个有DOM结构的文档才能处理渲染,且我们无法用JSDOM的方式干涉 $\KaTeX$ 的内部document,故放弃服务器渲染的想法,转由客户端渲染。
当然,你会发现,包裹 $\KaTeX$ 公式的 $ $ $$ $$ 符号内部公式如果有Markdown语法的话会被渲染器直接转义,所以用一个正则表达式匹配非代码块内的 $ $ 和 $$ $$ 符号,然后对内部的 Markdown 语法转义,最后在客户端组件副效应期间调用渲染。
export class MDFilters{ //语法过滤器
static latex(text){
//部分GPT撰写
const mathBlockRegexMultiLine=/\$\$[\s\S]*?\$\$/g; //块级公式
const mathBlockRegexSingleLine=/.\$(.*?)\$/g; //行内公式
const codeBlockRegex=/[\s\S]*?
let codeBlockMatches=[]; //把代码块丢出来免得污染
let tmptext=text.replace(codeBlockRegex,(match)=>{
codeBlockMatches.push(match);
return `__CODEBLOCK_${codeBlockMatches.length - 1}__`;
});
tmptext=tmptext.replace(mathBlockRegexMultiLine,(match)=>{
return match.replace(/([*_~`>#+\-=|{}.!])/g,'\\$1');//先替换块级
});
tmptext=tmptext.replace(mathBlockRegexSingleLine,(match)=>{
return match.replace(/([*_~`>#+\-=|{}.!])/g,'\\$1');//再替换行内
});
return tmptext.replace(/__CODEBLOCK_(\d+)__/g,(match,index)=>{
return codeBlockMatches[index]; //代码块丢回去
});
}
...
};这个类该这么用呢?晚点就知道了QwQ。
新建一个客户端组件插入在正文后面即可。
"use client"
import renderMathInText from 'katex/dist/contrib/auto-render';
import { useEffect } from "react";
import 'katex/dist/katex.min.css';
export default function KaTex(){
useEffect(()=>{(async ()=>{
renderMathInText(document.body,{
delimiters: [
{left: '$$', right: '$$', display: true},
{left: '$', right: '$', display: false},
],
throwOnError : false
});
})()},[]);
return <></>
}懒得换了()
使用正则表达式匹配{% xxx xxx,xxx %}xxx{% endxxx %}形式即可
export class TagPlugin{
static linkTag(markdown){
const linkRegex=/\{%\s*link\s+([^,]+)\s*,\s*([^,]+)\s*,\s*([^\s%][^%]*)\s*%\}/g;
return markdown.replace(linkRegex,(match,title,subtitle,link)=>{
return `<a class="etag-link" href="${link}" title="${title}">
<span class="etag-link-tip">
前往以下网站,不保证安全性哦喵~
</span>
<img class="etag-link-img" src="https://api.iowen.cn/favicon/${link[0]=="/"||link[0]=="."?siteConfigs.siteUrl.split("://")[1]:link.split("://")[1]?.split("/")[0]}.png" onerror="this.src='https://img.0v0.my/2024/07/07/668a8ffdacde3.png'">
</img>
<span class="etag-link-title">
${title}
</span>
<span class="etag-link-subtitle">
${subtitle}
</span>
</a>
`;//加空行,不然下面有标题渲染不了
//***一定一定不能缩进,不然缩进当代码块处决你QAQ!
});
}
static foldingTag(markdown){
const foldingRegex=/{%\s*folding\s+(\w+)(?:\s+open)?\s*,\s*([^%]+)\s*%}([\s\S]*?){%\s*endfolding\s*%}/g;
return markdown.replace(foldingRegex, (match,color,summary,content)=>{
return `<details class="etag-folding"${match.includes('open')?' open':''} style="border:2px solid rgba(${colorName[color]},.3);">
<summary class="etag-folding-summary" style="background-color:rgba(${colorName[color]},.15);">${summary}</summary>
<div class="etag-folding-content">
${content}
</div>
</details>
`;
});
}
static checkboxTag(markdown){
const checkboxRegex=/{%\s*checkbox\s*(checked,)?(\s*([^%]*))?\s*%}/g;
return markdown.replace(checkboxRegex,(match,checked,labelText)=>{
const isChecked=checked?'checked':'';
const text=labelText ? labelText.trim() : '';
return `<label class="etag-checkbox">
<input type="checkbox" ${isChecked} disabled />
<span>${text}</span>
</label>`;
});
}
static noteTag(markdown){
const noteRegex=/{%\s*note\s+(\w+)(?:\s+\w+)?\s*%}([\s\S]*?){%\s*endnote\s*%}/g;
return markdown.replace(noteRegex,(match,icon,content)=>{
icon=icon.toLowerCase().trim();
content=content.trim();
return `<div class="etag-note ${icon}">${content}</div>
`;
});
}
.....
};看起来函数很多是吧,总不能一层套一层的函数执行,所以来写一个链式调用的类,像Remark一样。
我把这些预处理的东西叫做过滤器,只需要把它们依次加入最后统一预处理即可。
export class MarkdownRewriter{
constructor(markdown){
this.filter=[];
this.markdown=markdown;
}
addFilter(filter){
this.filter.push(filter);
return this;
}
runRewrite(){
return this.filter.reduce((markdown,filter)=>filter(markdown),this.markdown);
}
}
------------
mdContent=(new MarkdownRewriter(mdContent))
.addFilter(MDFilters.latex)
.addFilter(TagPlugin.linkTag)
.addFilter(TagPlugin.foldingTag)
.addFilter(TagPlugin.hideToggleTag)
.addFilter(TagPlugin.colorTextTag)
.addFilter(TagPlugin.inlineImgTag)
.addFilter(TagPlugin.checkboxTag)
.addFilter(TagPlugin.noteTag)
.addFilter(TagPlugin.tipTag)
.addFilter(TagPlugin.tabsTag)
.runRewrite();"use client"
import { useEffect } from "react"
import { Fancybox } from "@fancyapps/ui";
import "@fancyapps/ui/dist/fancybox/fancybox.css";
export default function FancyBox() {
useEffect(()=>{
Fancybox.bind("[data-fancybox]",{
toolbar : true,
smallBtn : true,});
},[]);
return <></>
}"use client"
import { useEffect } from "react";
export default function HLJSNum(){
useEffect(()=>{(async()=>{
const hljslm=require("utils/highlightjs-line-number")
Array.from(document.querySelectorAll('.hljs code')).map(block=>{
if(block.getAttribute("numbered")) return;
block.setAttribute("numbered",true);
hljslm.lineNumbersBlock(block);
}
)
})()},[]);
return <></>;
}"use client";
import pangu from "pangu"
import { useEffect } from "react"
export default function Pangu({container}:{container:string}){
useEffect(()=>{
pangu.spacingElementById(container)
},[]);
return <></>;
}引入使用即可,朴实无华。
这里提供用栈一个根据现有页面的 DOM 生成 TOC 的方案:
const generateTOC=cache((html)=>{
const {document}=new JSDOM(html).window; //*服务端渲染需要
const headers=Array.from(document.querySelectorAll('h1,h2,h3,h4,h5,h6'));
const tocTree=[];
const createListItem=(header)=>{
const level=parseInt(header.tagName.substring(1));
const id=header.id||header.textContent.replace(/\s+/g,'-');
header.id=id;
return {
text: header.textContent,
href: id,
level,
children: []
};
};
let ulStack=[{level:0,children:tocTree}];
headers.forEach(header=>{
const item=createListItem(header);
const level=item.level;
while(ulStack[ulStack.length-1].level>=level)
ulStack.pop();
ulStack[ulStack.length-1].children.push(item);
ulStack.push(item);
});
//熟悉的OI()
//其实我想写DFS的,但是怕炸,算了
//生成DOM可以直接递归生成
return tocTree;
});我很想念友链朋友圈,实在是不错的一个项目,可以随便浏览友链的文章,不过似乎根据我当前的友链结构并不能很好的抓取。
看了看文档,似乎有一个通用选择器,也许改改类名就可以了
前往以下网站,不保证安全性哦喵~开发文档FriendCircle
/* 规则一(common1):*/
/* avatar : '.cf-friends img::attr(src)' */
/* link : '.cf-friends a::attr(href)' */
/* name : '.cf-friends a::text' */
/* 规则二(common2):*/
/* avatar : 'img.cf-friends-avatar' */
/* link : 'a.cf-friends-link' */
/* name : '.cf-friends-name' */<a className="flink-item cf-friends-link" href={link.url} target="_blank" rel="noopener noreferrer"
style={{backgroundColor:link.color,color:"white"}}>
<div className="flink-avatar" dangerouslySetInnerHTML={{
__html:`<img class="flink-avatar-img lazy-img cf-friends-avatar" src="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=" data-src=${link.avatar} alt=${link.name}
onerror="this.src='${siteConfigs.falldownAvatar}'"/>`
}}>
</div>
<span className="flink-name cf-friends-name">{link.name}</span>
<span className="flink-desc">{link.description}</span>
</a>果然运行的很完美。
前端也挺朴实无华的
"use client";
import "styles/Pages.css";
import "styles/PostContent.css";
import "styles/FriendCircle.css";
import { useEffect } from "react";
import { HomeRightSide } from "@/components/RightSide";
import { siteConfigs } from "@/config";
export default function Page() {
useEffect(()=>{
window.UserConfig={ //全局变量
// 填写你的api地址
private_api_url: siteConfigs.fcircleUrl,
// 初始加载几篇文章
page_init_number: 20,
// 点击加载更多时,一次最多加载几篇文章,默认10
page_turning_number: 10,
// 头像加载失败时,默认头像地址
error_img: siteConfigs.falldownAvatar,
// 进入页面时第一次的排序规则
sort_rule: 'created',
// 本地文章缓存数据过期时间(天)
expire_days: 1,
}
import("fcircle-theme-yyyz/dist/fcircle.min"); //引入直接就开始执行了
},[]);
return <div className="page" id="main-container">
<style>{`#navbar{position:fixed}`}</style>
<div id="article-container" className="page flink">
<div id="post-maincontent" className="page flink">
<h1>朋友圈</h1>
<div id="hexo-circle-of-friends-root"/>
</div>
</div>
<HomeRightSide/>
</div>;
}咕咕咕。。。
最后来个效果图QwQ!
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。