惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Martin Fowler
Martin Fowler
Y
Y Combinator Blog
M
MIT News - Artificial intelligence
The Cloudflare Blog
WordPress大学
WordPress大学
H
Hackread – Cybersecurity News, Data Breaches, AI and More
博客园 - 司徒正美
小众软件
小众软件
Blog — PlanetScale
Blog — PlanetScale
雷峰网
雷峰网
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
J
Java Code Geeks
云风的 BLOG
云风的 BLOG
C
Check Point Blog
D
DataBreaches.Net
T
The Blog of Author Tim Ferriss
V
V2EX
F
Fortinet All Blogs
B
Blog
大猫的无限游戏
大猫的无限游戏
N
Netflix TechBlog - Medium
B
Blog RSS Feed
A
About on SuperTechFans
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC

Ariasakaの小窝

Ariasakaの小窝 Ariasakaの小窝 Ariasakaの小窝 Ariasakaの小窝 Ariasakaの小窝 Ariasakaの小窝 Ariasakaの小窝 Ariasakaの小窝 Ariasakaの小窝 Ariasakaの小窝 Ariasakaの小窝 Ariasakaの小窝 Ariasakaの小窝 Ariasakaの小窝 Ariasakaの小窝 Ariasakaの小窝 Ariasakaの小窝 Ariasakaの小窝 Ariasakaの小窝 Ariasakaの小窝 Ariasakaの小窝 Ariasakaの小窝 Ariasakaの小窝 Ariasakaの小窝 Ariasakaの小窝 Ariasakaの小窝 Ariasakaの小窝 Ariasakaの小窝 Ariasakaの小窝 Ariasakaの小窝
Ariasakaの小窝
ariasaka · 2024-09-03 · via Ariasakaの小窝

来讲讲故事吧

昔日

其实,要是写上一篇文章的时候想起来了那么多,也不会放到这里罢。

2021年底,六年级。

当年尚无什么web方面的前端知识,不过早早的就想要一个自己的网站,这个梦想其实并不是当时才有的。

四年级。

彼时小学的我,三年级在学校的兴趣班学了Scratch,跟着傻猫猫玩了不久,有时看到别人居然有自己的网站!还那么好看,便有了想法。

然后借着一次考试考好的名义整了个树莓派玩了玩Wordpress,折腾了内网穿透免费域名等等东西,可是某一天树莓派坏了,不济而终。

两年后,六年级。

偶然发现了Github Page,此物甚为有趣,居然可以部署静态站点,此时正是Hexo风行之时,自然而然搜到了许多部署Hexo博客的教程,于互联网上搜集许久,后来便选择了Butterfly主题,结下了不解之缘。

看到别人的站点,总是非常羡慕,模仿之心便是有的,四处收集,键盘一敲,杂糅一片,便是整个博客。

事实上,经历了一年的沉淀,博客的变化便不算大了,后来小修小补更多,性能也不知是怎么样了呢?

糖果屋。

这便是那著名的博客Butterfly博客圈的圣地之一,店长Akilar精心经营的地方。

进了店长的QQ群,便是万分有趣,遇到了许多许多的大佬,结识了不少的朋友。

但是,他们都说,

怎么办?!

摆烂。

卡卡卡。

动力

今年早些时候,在群里认识了Redish101数据库大佬,比我还小,从软件到硬件,从Web到底层样样精通,技术栈个个不差,实在是各位所羡慕不起的。

发现了他的reblog,是Next.js+Go编写的,开发比起原生极其方便,其实也跃跃欲试了。

前不久,突然掀起了一股小的逃离Butterfly重构潮,似乎我才是最需要重构的,来吧。

用什么呢,数据库自然强烈推荐用Next.js编写了,Next.jsNext.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 PythonC++了,而Node.js的话嘛,JS本来就是为浏览器设计的,Node依然不太舒服,C++对于字符串处理极为困难,不太可能。

那就 Python 吧, FastAPI

数据库的选择上,毕竟咱也不会SQL,即使是做了ORM映射也还是颇有难度,熟悉的JsonDB的话却只能用在静态文件上面。

MongoDB 还是最好的选择啊😋。

来写代码!

自然是先写前端,再根据需求写后端了。

项目在8月24日开始,截至今天9月3日,爆肝十一天,还未实现搜索 归档 分类和标签功能,部分分页面暂未撰写,其实进度不错。

据Redish101大佬所说,只需仔细阅读:

前往以下网站,不保证安全性哦喵~学习ReactReact

前往以下网站,不保证安全性哦喵~Next.js documentationNext.js By Vercel

以及

前往以下网站,不保证安全性哦喵~TypeScript 文档Microsoft

便可以不写史山和AnyScript了。

我都没好好读TAT

这样的话,写写前端和样式,预定义测试数据,完成后便可接入后端了。

后端的话其实更简单,相当一部分查询可以丢给ai帮助完成,FastAPI的封装性很高,自己需要干涉的地方很少,只需要老老实实的写查询语句丢回来就好了。

于是:

前往以下网站,不保证安全性哦喵~AriaBlogNext-frontendGithub前往以下网站,不保证安全性哦喵~AriaBlogNext-backendGithub

看样子事情比较顺利,不过磕磕绊绊实在不少,要不来讲一些事情吧。

善用GPT和搜索引擎

BingGoogle比起百度来说无非是两个有着更多的重要有关信息的搜索引擎,任何时候都不要查阅百度CSDN

前往以下网站,不保证安全性哦喵~Bing SearchMicrosoft前往以下网站,不保证安全性哦喵~Google SearchGoogle

不要忽略有意义的英文资料而只阅读中文,当您看不懂时请下载沉浸式翻译插件来读懂并获得好的体验!

前往以下网站,不保证安全性哦喵~沉浸式翻译Microsoft Edge Addons

ChatGPT将会是你编码过程中的好帮手!当你遇到了难以描述清晰或者搜索引擎没有的资料和问题时,随时准备询问它!

前往以下网站,不保证安全性哦喵~ChatGPTOpenAI

对于编辑器代码补全,选择CodeGeex插件,这会帮助你减少工作量。

Markdown

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 的自定义渲染还是很好用的,看起来没有生态,实际上自己整整就能搞定。

因为每一次的渲染耗时对网页加载比较明显,况且Reactcache()的生命周期有限,采用了渲染后推送到后端缓存的方式来解决渲染耗时长的问题,事实上下次渲染直接读取文章信息的cachedHtml而不经过MDRenderer函数了。

至于缓存生命周期,后端更新的时候也许是会一起更新的。

为什么不直接在后端渲染呢?

因为Python实在没有好使的Markdown渲染库()

JAVASCRIPT
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很好的操作

JAVASCRIPT
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;
JAVASCRIPT
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>`;
    } //添加锚点,怕标题重名,生成随机字串
    ...
}

对 KaTeX 数学公式的支持

MathJax文档实在少,放弃了()

因为 $\KaTeX$ 需要一个有DOM结构的文档才能处理渲染,且我们无法用JSDOM的方式干涉 $\KaTeX$ 的内部document,故放弃服务器渲染的想法,转由客户端渲染。

当然,你会发现,包裹 $\KaTeX$ 公式的 $ $ $$ $$ 符号内部公式如果有Markdown语法的话会被渲染器直接转义,所以用一个正则表达式匹配非代码块内的 $ $$$ $$ 符号,然后对内部的 Markdown 语法转义,最后在客户端组件副效应期间调用渲染。

JAVASCRIPT
export class MDFilters{ //语法过滤器
    static latex(text){
        //部分GPT撰写
        const mathBlockRegexMultiLine=/\$\$[\s\S]*?\$\$/g; //块级公式
        const mathBlockRegexSingleLine=/.\$(.*?)\$/g; //行内公式
        const codeBlockRegex=/

[\s\S]*?

TEXT
        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。

新建一个客户端组件插入在正文后面即可。

JAVASCRIPT
"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 <></>
}

Hexo一样的外挂标签

懒得换了()

使用正则表达式匹配{% xxx xxx,xxx %}xxx{% endxxx %}形式即可

JAVASCRIPT
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一样。

我把这些预处理的东西叫做过滤器,只需要把它们依次加入最后统一预处理即可。

JAVASCRIPT
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();

FancyBox HighLightJsNumber Pangu 等插件的使用

JAVASCRIPT
"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 <></>
}
JAVASCRIPT
"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 <></>;
}
TYPESCRIPT
"use client";
import pangu from "pangu"
import { useEffect } from "react"

export default function Pangu({container}:{container:string}){
    useEffect(()=>{
        pangu.spacingElementById(container)
    },[]);
    return <></>;
}

引入使用即可,朴实无华。

TOC 生成

这里提供用栈一个根据现有页面的 DOM 生成 TOC 的方案:

TYPESCRIPT
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

CSS
/* 规则一(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'  */
HTML
<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>

果然运行的很完美。

前端也挺朴实无华的

JAVASCRIPT
"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!