使用vite-ssg写一个存放笔记的小网站
代码仓库
antfu:github.com/antfu/antfu…
参考
写这个东西参考的是 antfu 的博客Anthony Fu (antfu.me) 这是他的博客地址,比较简约,其实我参考他的这个一部分是参考他的代码(因为刚开始弄这个找了很多太麻烦了,直接找仓库按照他的代码走一遍),另一部分就是布局上的一些内容,在编写的过程中我还参考了其他人的博客:虹墨空间站 www.imaegoo.com/ 这是的他的博客,我参考他的部分是他的背景,我觉得还挺好看的,就从他那里要到了这个背景源码 github.com/fan-lv/Fan/… 。总之参考的内容都在这里了,我还有一部分的内容还没有完成,但是大部分我已经写完了。
我写这个我不会把全部的流程写一遍,因为有些内容完全可以直接参考antfu的代码或者是我写的直接走一遍就行了,我只将里面我认为比较引起大家注意的部分拿出来,例如代码块显示行号,以及目录传送到任何位置的问题。
效果
首页
分类文章列表
点击列表查看文章内容
推荐使用的图标库
icones.js.org/ 这个图标库很不错,而且他和unocss 还是配套的,而且它们作者是antfu,这个图标库里面包含了很多的图标,使用起来也是很方便,还可以下载svg等等,也可以下载vue或者是react的代码,可以通过css类的方式进行使用,非常好用。
背景
这个背景原链接github.com/fan-lv/Fan/… ,源代码是通过js写的,后来我就改改,改成了ts版本的,如果要是创建不带ts的项目的话,可以直接拿这个代码,下面是ts版本的
<!-- Plum -->
<script setup lang="ts">
const el = ref<HTMLCanvasElement | null>(null)
const size = reactive(useWindowSize())
const starCount = ref<number>(0)
const starDensity = ref<number>(0.216)
const speedCoeff = ref<number>(0.05)
interface Color {
giantColor: string;
starColor: string;
cometColor: string;
}
const first = ref<boolean>(true)
const color = reactive<Color>({
giantColor: '180,184,240',
starColor: '226,225,142',
cometColor: '226,225,224'
});
interface Star {
giant: boolean;
comet: boolean;
x: number;
y: number;
r: number;
dx: number;
dy: number;
fadingOut: boolean | null;
fadingIn: boolean;
opacity: number;
opacityTresh: number;
do: number;
reset(): void;
fadeIn(): void;
fadeOut(): void;
draw(): void;
move(): void;
}
const grain = ref<Array<Star>>()
function initCanvas(canvas: HTMLCanvasElement, width = 400, height = 400, _dpi?: number) {
const ctx = canvas.getContext('2d')!
const dpr = window.devicePixelRatio || 1
// @ts-expect-error vendor
const bsr = ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1
const dpi = _dpi || dpr / bsr
canvas.style.width = `${width}px`
canvas.style.height = `${height}px`
canvas.width = dpi * width
canvas.height = dpi * height
ctx.scale(dpi, dpi)
starCount.value = width * starDensity.value
return { ctx, dpi }
}
function getProbability(percents: number) {
return ((Math.floor(Math.random() * 1000) + 1) < percents * 10);
}
function getRandInterval(min: number, max: number) {
return (Math.random() * (max - min) + min);
}
onMounted(async () => {
const canvas = el.value!
const { ctx } = initCanvas(canvas, size.width, size.height)
const { width, height } = canvas
function Star(): Star {
const star = {} as Star;
star.reset = function () {
star.giant = getProbability(3);
star.comet = star.giant || first.value ? false : getProbability(10);
star.x = getRandInterval(0, width - 10);
star.y = getRandInterval(0, height);
star.r = getRandInterval(1.1, 2.6);
star.dx =
getRandInterval(speedCoeff.value, 6 * speedCoeff.value) +
(star.comet as any as number + 1 - 1) * speedCoeff.value * getRandInterval(50, 120) +
speedCoeff.value * 2;
star.dy =
-getRandInterval(speedCoeff.value, 6 * speedCoeff.value) -
(star.comet as any as number + 1 - 1) * speedCoeff.value * getRandInterval(50, 120);
star.fadingOut = null;
star.fadingIn = true;
star.opacity = 0;
star.opacityTresh = getRandInterval(0.2, 1 - (star.comet as any as number + 1 - 1) * 0.4);
star.do = getRandInterval(0.0005, 0.002) + (star.comet as any as number + 1 - 1) * 0.001;
};
star.fadeIn = function () {
if (star.fadingIn) {
star.fadingIn = !(star.opacity > star.opacityTresh);
star.opacity += star.do;
}
};
star.fadeOut = function () {
if (star.fadingOut) {
star.fadingOut = !(star.opacity < 0);
star.opacity -= star.do / 2;
if (star.x > width || star.y < 0) {
star.fadingOut = false;
star.reset();
}
}
};
star.draw = function () {
ctx.beginPath();
if (star.giant) {
ctx.fillStyle = `rgba(${color.giantColor},${star.opacity})`;
ctx.arc(star.x, star.y, 2, 0, 2 * Math.PI, false);
} else if (star.comet) {
ctx.fillStyle = `rgba(${color.cometColor},${star.opacity})`;
ctx.arc(star.x, star.y, 1.5, 0, 2 * Math.PI, false);
for (let i = 0; i < 30; i++) {
ctx.fillStyle = `rgba(${color.cometColor},${star.opacity - (star.opacity / 20) * i})`;
ctx.rect(
star.x - (star.dx / 4) * i,
star.y - (star.dy / 4) * i - 2,
2,
2
);
ctx.fill();
}
} else {
ctx.fillStyle = 'rgba(' + color.starColor + ',' + this.opacity + ')';
ctx.rect(star.x, star.y, star.r, star.r);
}
ctx.closePath();
ctx.fill();
}
star.move = function () {
star.x += star.dx;
star.y += star.dy;
if (star.fadingOut === false) {
star.reset();
}
if (star.x > width - (width / 4) || star.y < 0) {
star.fadingOut = true;
}
};
setTimeout(function () {
first.value = false;
}, 50);
return star
}
const draw = () => {
ctx.clearRect(0, 0, width, height);
grain.value?.forEach(f => {
f.move();
f.fadeIn();
f.fadeOut();
f.draw();
})
window.requestAnimationFrame(draw);
}
const createUniverse = () => {
grain.value = Array.from({ length: starCount.value }, () => {
const m = Star();
m.reset();
return m;
});
draw();
}
createUniverse()
useEventListener(window, 'resize', () => initCanvas(canvas, size.width, size.height))
})
</script>
<template>
<div class="fixed top-0 bottom-0 left-0 right-0 pointer-events-none">
<canvas id="universe" ref="el"></canvas>
</div>
</template>
<style ></style>
其中这里面我也用到了useEventListener,他来自 @vueuse/core 库,相信大家应该也知道这个。
markdown-it-table-of-contents生成目录后放到指定位置
在编写这个代码的时候,一般的你像稀土掘金,语雀等等这些可以写文章发笔记的,文章都会显示目录,当使用这个markdown-it-table-of-contents生成目录的时候,他是直接和文章一起生成的html,虽然可以通过css去定位他的位置,为什么我要去让它可以放到任何位置,是因为我这个文章区域我使用了动画,动画影响到了我的定位等等一些其他问题,我就想把他放到我想要放的位置,然后在去做一些css上的处理
实现我使用的vue中的一个组件传送门Teleport,可以将这个插件生成的目录传送到你想要传送的位置,但是在传送的过程中,我发现他并不能传送到我想要传送的位置
一开始我是将Teleport直接在md文档中直接使用,因为它们的一些插件是可以解析md中的vue代码的,所以我就直接在md中使用了这个组件,其中 [[toc]] 是markdown-it-table-of-contents需要的标记
<Teleport>
[[toc]]
</Teleport>
但是如果直接使用了这个组件就会产生一个问题就是,我的布局是在vue代码中写的,当我跳转到这个文章的时候,我的组件里面的dom元素还没创建完,就导致我现在传送的时候不成功,并且控制台会警告说我传送的目标元素不存在,下面是我的解决办法,目前来看是成功的,一直没有出问题,也可以直接传送到我想要的位置。
<!-- DelayTeleport -->
<script setup lang="ts">
import { Teleport } from 'vue'
const slots = useSlots()
const teleportVnode = ref<VNode | null>(null)
onMounted(() => {
if (slots.default?.()) {
teleportVnode.value = h(Teleport,
{
to: '#view_side'
},
slots.default?.()
)
}
})
const delayTeleport = {
render() {
return h('div', null, [teleportVnode.value])
}
}
</script>
<template>
<delayTeleport>
<slot></slot>
</delayTeleport>
</template>
<style scoped></style>
我直接将它写成一个组件并且里面通过插槽以及h函数重新编写,然后在md中使用的时候,将传送门组件直接换成这个组件即可,其中 #view_side是我要传送的位置
代码块相关的处理
原来使用他自己的这些就行,但是我想要给代码添加行号就很费劲,尝试了很多方式,最后我选择使用vuepress实现的代码行号,我将他的实现拿了出来,然后处理成ts版本,发现很好用,而且这里面也包含了vuepress里面的代码块某一行代码需要高亮的功能,下面是完整的代码
resolveVPre.ts
/**
* Resolve the `:v-pre` / `:no-v-pre` mark from token info
*/
export const resolveVPre = (info: string): boolean | null => {
if (/:v-pre\b/.test(info)) {
return true;
}
if (/:no-v-pre\b/.test(info)) {
return false;
}
return null;
};
resolveLineNumbers.ts
/**
* Resolve the `:line-numbers` / `:no-line-numbers` mark from token info
*/
export const resolveLineNumbers = (info: string): boolean | null => {
if (/:line-numbers\b/.test(info)) {
return true;
}
if (/:no-line-numbers\b/.test(info)) {
return false;
}
return null;
};
resolveLanguage.ts
import * as languages from './languages';
interface resolveLanguageLanguage {
name: string;
ext: string;
aliases: string[];
}
/**
* A key-value map to get language info from alias
*
* - key: alias
* - value: language
*/
let languagesMap: { [alias: string]: resolveLanguageLanguage };
/**
* Lazy generate languages map
*/
const getLanguagesMap = (): { [alias: string]: resolveLanguageLanguage } => {
if (!languagesMap) {
languagesMap = Object.values(languages).reduce((result, item) => {
item.aliases.forEach((alias) => {
result[alias] = item;
});
return result;
}, {} as { [alias: string]: resolveLanguageLanguage });
}
return languagesMap;
};
/**
* Resolve language for highlight from token info
*/
export const resolveLanguage = (info: string): resolveLanguageLanguage => {
// get user-defined language alias
const alias = info.match(/^([^ :[{]+)/)?.[1] || 'text';
// if the alias does not have a match in the map
// fallback to the alias itself
return (
getLanguagesMap()[alias] ?? {
name: alias,
ext: alias,
aliases: [alias],
}
);
};
resolveHighlightLines.ts
/**
* Resolve highlight-lines ranges from token info
*/
export function resolveHighlightLines(info: string): number[][] | null {
// try to match highlight-lines mark
const match = info.match(/{([\d,-]+)}/);
// no highlight-lines mark, return `null`
if (match === null) {
return null;
}
// resolve lines ranges from the highlight-lines mark
return match[1].split(",").map((item) => {
const range = item.split("-");
if (range.length === 1) {
range.push(range[0]);
}
return range.map((str) => Number.parseInt(str, 10));
});
}
/**
* Check if a line number is in ranges
*/
export function isHighlightLine(lineNumber: number, ranges: number[][]): boolean {
return ranges.some(([start, end]) => lineNumber >= start && lineNumber <= end);
}
languages.ts
interface Language {
name: string;
ext: string;
aliases: string[];
}
const languageBash: Language = {
name: 'bash',
ext: 'sh',
aliases: ['bash', 'sh', 'shell', 'zsh'],
};
const languageCsharp: Language = {
name: 'csharp',
ext: 'cs',
aliases: ['cs', 'csharp'],
};
const languageDocker: Language = {
name: 'docker',
ext: 'docker',
aliases: ['docker', 'dockerfile'],
};
const languageFsharp: Language = {
name: 'fsharp',
ext: 'fs',
aliases: ['fs', 'fsharp'],
};
const languageJavascript: Language = {
name: 'javascript',
ext: 'js',
aliases: ['javascript', 'js'],
};
const languageKotlin: Language = {
name: 'kotlin',
ext: 'kt',
aliases: ['kotlin', 'kt'],
};
const languageMarkdown: Language = {
name: 'markdown',
ext: 'md',
aliases: ['markdown', 'md'],
};
const languagePython: Language = {
name: 'python',
ext: 'py',
aliases: ['py', 'python'],
};
const languageRuby: Language = {
name: 'ruby',
ext: 'rb',
aliases: ['rb', 'ruby'],
};
const languageRust: Language = {
name: 'rust',
ext: 'rs',
aliases: ['rs', 'rust'],
};
const languageStylus: Language = {
name: 'stylus',
ext: 'styl',
aliases: ['styl', 'stylus'],
};
const languageTypescript: Language = {
name: 'typescript',
ext: 'ts',
aliases: ['ts', 'typescript'],
};
const languageYaml: Language = {
name: 'yaml',
ext: 'yml',
aliases: ['yaml', 'yml'],
};
export {
languageBash,
languageCsharp,
languageDocker,
languageFsharp,
languageJavascript,
languageKotlin,
languageMarkdown,
languagePython,
languageRuby,
languageRust,
languageStylus,
languageTypescript,
languageYaml,
};
codePlugin.ts
import { resolveHighlightLines, isHighlightLine } from "./resolveHighlightLines";
import { resolveLanguage } from './resolveLanguage';
import { resolveLineNumbers } from './resolveLineNumbers'
import { resolveVPre } from "./resolveVPre";
import type { PluginWithOptions } from 'markdown-it';
export interface CodePluginOptions {
highlightLines?: boolean;
lineNumbers?: boolean | number;
preWrapper?: boolean;
vPre?: {
block?: boolean;
inline?: boolean;
};
}
/**
* Code plugin
*/
const codePlugin: PluginWithOptions<CodePluginOptions> = (md, { highlightLines = true, lineNumbers = true, preWrapper = true, vPre: { block: vPreBlock = true, inline: vPreInline = true } = {}, } = {}) => {
// override default fence renderer
md.renderer.rules.fence = (tokens, idx, options, env, slf) => {
const token = tokens[idx];
// get token info
const info = token.info ? md.utils.unescapeAll(token.info).trim() : '';
// resolve language from token info
const language = resolveLanguage(info);
const languageClass = `${options.langPrefix}${language.name}`;
// try to get highlighted code
const code = options.highlight?.(token.content, language.name, '') ||
md.utils.escapeHtml(token.content);
// wrap highlighted code with `<pre>` and `<code>`
let result = code.startsWith('<pre')
? code
: `<pre class="${languageClass}"><code>${code}</code></pre>`;
// resolve v-pre mark from token info
const useVPre = resolveVPre(info) ?? vPreBlock;
if (useVPre) {
result = `<pre v-pre${result.slice('<pre'.length)}`;
}
// if `preWrapper` is disabled, return directly
if (!preWrapper) {
return result;
}
// code fences always have an ending `\n`, so we should trim the last line
const lines = code.split('\n').slice(0, -1);
// resolve highlight line ranges from token info
const highlightLinesRanges = highlightLines
? resolveHighlightLines(info)
: null;
// generate highlight lines
if (highlightLinesRanges) {
const highlightLinesCode = lines
.map((_, index) => {
if (isHighlightLine(index + 1, highlightLinesRanges)) {
return '<div class="highlight-line"> </div>';
}
return '<br>';
})
.join('');
result = `${result}<div class="highlight-lines">${highlightLinesCode}</div>`;
}
// resolve line-numbers mark from token info
const useLineNumbers = resolveLineNumbers(info) ??
(typeof lineNumbers === 'number'
? lines.length >= lineNumbers
: lineNumbers);
// generate line numbers
if (useLineNumbers) {
// generate line numbers code
const lineNumbersCode = lines
.map(() => `<div class="line-number"></div>`)
.join('');
result = `${result}<div class="line-numbers" aria-hidden="true">${lineNumbersCode}</div>`;
}
result = `<div class="${languageClass} ext-${language.ext}${useLineNumbers ? ' line-numbers-mode' : ''}">${result}</div>`;
return result;
};
if (vPreInline) {
const rawInlineCodeRule = md.renderer.rules.code_inline;
if (rawInlineCodeRule) {
md.renderer.rules.code_inline = (tokens, idx, options, env, slf) => {
const token = tokens[idx];
const lang = token.info ? md.utils.escapeHtml(token.info).trim() : '';
const code = md.utils.escapeHtml(token.content);
return `<code class="${options.langPrefix}${lang}">${code}</code>`;
};
// md.renderer.rules.code_inline = (tokens, idx, options, env, slf) => {
// const result = rawInlineCodeRule(tokens, idx, options, env, slf);
// return `<code v-pre${result.slice('<code'.length)}`;
// };
}
}
};
export default codePlugin
其中还有好多css太多了,我就不复制了,直接在我的github仓库里面直接找吧,太长了。其他的内容等我写完我就全部补充上,现在就这几块我认为比较重要的部分吧。
转载自:https://juejin.cn/post/7248914499913908285