Vuepress开发个人博客

2023-02-14

创建项目

注意: 该文档是基于vuepress的开发流程,参考vuepress的v1文档进行。

安装yarnnpm install -g yarn

项目目录内

初始化yarn init

安装vuepress的v1版本 yarn add -D vuepress

生成package.json

{
  "name": "blog_by_vuepress",
  "version": "1.0.0",
  "description": "Blog based on vuepress",
  "main": "index.js",
  "repository": "git@github.com:SayNop/blog_by_vuepress.git",
  "author": "Leopold",
  "license": "MIT",
  "devDependencies": {
    // 此插件只支持v2版本
    "@vuepress/plugin-toc": "^2.0.0-beta.7",
    // v1版本
    "vuepress": "^1.9.8"
  },
  "scripts": {
    "blog:dev": "vuepress dev blog",
    "blog:build": "vuepress build blog"
  }
}

第一个markdown页面

新建文件夹docs

在docs中添加README.md并编写markdown内容

package.json中配置服务启动

  "scripts": {
    "docs:dev": "vuepress dev docs",
    "docs:build": "vuepress build docs"
  },

使用yarn docs:dev以调用vuepress dev docs启动服务(以docs作为根路径启动)

如果有其他根目录需配置vuepress dev new_root_dir

使用系统默认主题

参考文档中默认主题配置open in new window,将下方的yaml内容复制进README.md中即可

---
home: true
heroImage: /hero.png
heroText: Hero 标题
tagline: Hero 副标题
actionText: 快速上手 →
actionLink: /zh/guide/
features:
- title: 简洁至上
  details: 以 Markdown 为中心的项目结构,以最少的配置帮助你专注于写作。
- title: Vue驱动
  details: 享受 Vue + webpack 的开发体验,在 Markdown 中使用 Vue 组件,同时可以使用 Vue 来开发自定义主题。
- title: 高性能
  details: VuePress 为每个页面预渲染生成静态的 HTML,同时在页面被加载的时候,将作为 SPA 运行。
footer: MIT Licensed | Copyright © 2018-present Evan You
---

路由映射规则

根据vuepress的路由规则,docs中的README.md为系统根目录的页面

子路径的两种方式

  • 可在docs中新建对应文件夹并在文件夹中创建README.md即可。例如项目目录docs/about/README.md的路由为/about/

  • docs下新建about.md,其路由为/about.html

注意: 两种方式可嵌套使用。docs新增about文件夹,其中新增me.md,其路由为/about/me.html

默认导航栏

docs下新建.vuepress/config.js

  • 配置导航栏logo - 需放入静态资源目录docs/.vuepress/public。logo路径是以静态资源目录为基础,即放入docs/.vuepress/public/assets/img/logo.png
  • 配置快捷导航 - 指定文本可链接指定路由。可进行分组
  • 禁用导航栏 - 导航栏默认全局显示,可在配置文件中全局禁用
module.exports = {
  themeConfig: {
    // 导航栏引用静态资源
    logo: '/assets/img/logo.png',
    nav: [
      // 导航栏导航对应路由
      { text: 'Home', link: '/' },
      { text: 'Guide', link: '/guide/' },
      { text: 'External', link: 'https://google.com' },
      // 导航栏分组
      {
        text: 'Languages',
        ariaLabel: 'Language Menu',
        items: [
          { text: 'Chinese', link: '/language/chinese/' },
          { text: 'Japanese', link: '/language/japanese/' }
        ]
      }
    ],
    // 禁用导航栏
    navbar: false
  },
}

如果想部分页面拥有导航栏,部分没有:在指定页面开头处加入yaml配置navbarfalse

---
navbar: false
---
# This is a h1

自定义主题的说明

.vuepress/theme中新增Layout.vue。此时会检测到将使用自定义主题,所有路径下的markdown文件都会默认渲染该vue文件,如果需要在合适的位置显示md文件中的内容,使用<Content/>

.vuepress/components中新增vue文件。其中都为vue组件,可进行引用。

<!-- 定义组件,文件名为test.vue -->
<template>
    <div>
        <a href="/">root</a>
        <a href="/test1.html">test1 page</a>
        <a href="/test2/">test2 page</a>
        <a href="/test2/test3.html">test3 page</a>
    </div>
</template>


<!-- 主题中进行引用 -->
<template>
    <div id="home">
        <h1>Test theme</h1>
      	<!-- 引用组件 -->
        <test />
    </div>
</template>

<style>
#home{
    display: flex;
}
</style>

markdown控制使用布局

在自定义主题时,所有markdown默认使用theme中的Layout.vue进行渲染

可在开头使用yaml配置(即指定路由下的页面frontmatter)layout属性即可选择使用哪个布局文件进行渲染

---
// 使用了layout.vue进行渲染
layout: layout
---

如果frontmatter.layout中指明的布局不存在,则vuepress将继续使用默认值layout,即主布局文件Layout.vue进行渲染,此时可在主布局中v-if判断frontmatter中的值进行不同组件的动态渲染。详情如下所示。

---
// 主页的layout配置
layout: home
---

---
// 文章详情页的layout配置
layout: detail
---
<template>
  <div>
    <Header />
    <articles v-if="$page.frontmatter.layout == 'home'" />
    <detail v-else-if="$page.frontmatter.layout == 'detail'" />
    <Footer />
  </div>
</template>

元数据用例:渲染文章列表

可先打印查看元数据内容,在通过vue进行操作

<!-- 查看元数据的样式check.vue -->
<template>
    <div>
        <pre>
            <Content />
        </pre>
    </div>
</template>

查看元数据的模版内容

---
layout: check
---
{{$site}}

元数据$site会返回站点所有页面的相关信息

尝试使用元数据编写渲染站点列表的布局组件.vuepress/components中新建布局组件

<!-- 注意:template下面只可有一个div根标签,不可直接for循环div -->
<template>
    <div id="articles">
        <pre>
            <Content />
        </pre>
        <!-- <div class="articleblock" v-for="item in $site.pages"> -->
            <!-- <h1>{{ item.title }}</h1> -->
            <!-- <h4>{{ item.frontmatter.description }}</h4> -->
            <!-- <a :href="item.path">read more</a> -->
        <!-- </div> -->
        <!-- <div class="articleblock" v-for="index in $site.pages.length" :key="index"> -->
            <!-- <h1>{{ $site.pages[index-1].title }}</h1> -->
            <!-- <h4>{{ $site.pages[index-1].frontmatter.description }}</h4> -->
            <!-- <a :href="$site.pages[index-1].path">read more</a> -->
        <!-- </div> -->
        <div class="article block" v-for="index in list.length" :key="index">
            <!-- 用于渲染标题 -->
            <h1>{{list[index-1].title}}</h1>
            <!-- 用于渲染部分正文 -->
            <h4>{{list[index-1].frontmatter.description}}</h4>
            <!-- 用于跳转到详情页 -->
            <a :href="list[index-1].path">READ MORE</a>
        </div>
    </div>
</template>


<script>
 // 由于元数据会返回所有信息,需要过滤掉非文章的页面
export default {
    computed: {
        list() {
            return this.$site.pages.filter(v => {
              	// 过滤逻辑 - 可以通过路由正则匹配进行过滤,此处以结尾进行过滤
                return v.path.endsWith('html');
            }).sort((x, y) => {
                return x.Date < y.Date
            });
        }
    }
}
</script>

---
title: my first
date: '2023-01-01'
description: test
---

# my first

md文件的样式

在自定义主题时,md文件的内容将没有样式

需要寻找映射关系后自行实现

例如:

# 被映射为<h1><a href class="header-anchor">

自定义主题布局组件调用子组件

.vuepress/components/中的组件,默认注册为全局组件。无需导入可直接调用

.vuepress/theme/components/.vuepress/theme/global-components/均需要导入才可使用

导包与说明

<!-- 父组件 - 引用处 -->
<template>
	<header_wrapper />
</template>
<script>
// 将组件以指定名称导入(子组件中的name无关,父组件以指定变量名代指子组件)
import header_wrapper from '../components/header_wrapper'
export default {
    components: {
        header_wrapper,
    },
}
</script>

<!-- 子组件 - 定义处:可以不指定组件名称,也可与父组件导入名称不一致 -->
<script>
name: 'header_wrapper'
</script>

定义样式的全局变量

使用css的方式

:root
    // 全局变量
    --theme-color #60c9e6
    --bg-height 80vh
    --bg-pic url(/assets/imgs/bg07.jpeg)

    // 明亮模式变量
    --card-bg-color #fff
    --header-color #ffffffb3
    --font-color #213547
    --tag-color #f6f6f6

    
.dark
    --card-bg-color #000
    --header-color #000000b3
    --font-color #ffffffde
    --tag-color #2f2f2f

html
    font-size 16px
    font-family Arial,'Microsoft Yahei'
    // 引用变量
    color var(--font-color)

使用stylus的方式

// palette.styl
$theme-color = #60c9e6
$bg-height = 80vh
$bg-pic = url(/assets/imgs/bg07.jpeg)

// index.styl - 不会自动导入,需手动导入
@require './palette'

#home
    display flex
    flex-direction column
    // 引用变量
    background $bg-pic no-repeat center / cover
    background-attachment fixed

组件解耦

  • 组件解耦
<template>
    <div id="article_list">
        <div class="article_list">
            <div class="article_card card_border">
                <div class="card_title_container">
                    <div class="card_title article_title">HelloWorld</div> 
                    <div class="article_time">2023-01-01 12:00:00</div>
                </div>
                <div class="card_content_container">
                    <div class="card_content">print('Hello World')</div>
                    <div class="card_tag">
                        <span class="article_category">python</span>
                        <span class="article_tag">python</span>
                        <span class="article_tag">vue</span>
                    </div> 
                </div>
            </div>
        </div>
    </div>
</template>
  • 样式解耦
<style scoped lang="stylus">
  	// 样式解耦 - 写单独的样式文件后在此处导入
    // scoped: 该组件才能使用的样式
    
    // index默认导入,可以省略
    @import '../styles/header'
</style>
  • 变量解耦 - 页眉等控制页面黑白,或侧栏是否显示等主布局组件中的全局变量与函数。通过组件操作组件根元素(template下面的第一层)来实现,或者document对象获取来进行实现。尽量避免变量在组件间相互传递。
  1. 页眉透明度 - 直接在父组件控制其style
<header_wrapper :style="{opacity: header_opacity}" />

<script>
import header_wrapper from '../components/header_wrapper'
export default {
    components: {
        detail,
        articles
    },
    data() {
        return {
            header_opacity: 0,
        }
    }, 
    methods: {
        handleScroll()
        {   
            // 桌面端进行动态渲染
            if(window.screen.availWidth > 767) {
                var scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
                if(this.$refs.demos.offsetTop) 
                    this.header_opacity = scrollTop / (this.$refs.demos.offsetTop/3)
            }
        }
    },
    mounted() {
        window.addEventListener('scroll', this.handleScroll, true)
    }
}
</script>
  1. 页眉dark模式 - 通过document控制全局
<template>
    <button class="switch" type="button"  @click="handleDark">
</template>
<script>
export default {
    data() {
        return {
            is_dark: false,
        }
    }, 
    methods: {
        handleDark(){
            this.is_dark = !this.is_dark
            if(this.is_dark){
                document.documentElement.className = 'dark'
            } else {
                document.documentElement.className = ''
            }
        }
    }
}
</script>
  1. 页眉的菜单是否展开 - 通过子组件提交事件后父组件监听或者document对象直接修改
<!-- 未拆开前,统一在父组件 -->
<!-- 控制按钮 -->
<button class="switch mobile_list_btn" type="button" @click="showSlide">
<!-- 被控制元素 -->
<div id="info" :class="show_slide?'show_info':'hidden_info'">
<script>
export default {
    data() {
        return {
            // 控制变量
            show_slide: false,
        }
    }, 
    methods: {
      	// 修改控制变量函数
        showSlide(){
            this.show_slide = !this.show_slide
        }
    }
}
</script>

<!-- 方法一:通过事件,子组件不去计算,让父组件进行计算 -->
<!-- 子组件:无需大改动,只需抛出一个事件 -->
<button class="switch mobile_list_btn" type="button" @click="slideSwitch">
<script>
export default {
    name: 'header_wrapper',
    methods: {
        slideSwitch(){
            this.$emit('slide_switch')
        }
    }
}
</script>
<!-- 父组件:无需大改动,只需添加子组件的事件监听 -->
<header_wrapper :style="{opacity: header_opacity}"  @slide_switch="showSlide" />
<div id="info" :class="show_slide?'show_info':'hidden_info'">
<script>
export default {
    data() {
        return {
            show_slide: false,
        }
    }, 
    methods: {
        showSlide(){
            this.show_slide = !this.show_slide
        }
    }
}
</script>
 
<!-- 方法二:通过document,子组件计算后通过通过document控制元素 -->
<!-- 子组件:需要增加document查找被控制的元素 -->
<button class="switch mobile_list_btn" type="button" @click="show_Slide">
<script>
export default {
    name: 'header_wrapper',
    data() {
        return {
            show_slide: false,
        }
    }, 
    methods: {
        show_Slide(){
            this.show_slide = !this.show_slide
            if(this.show_slide){
              	// 获取页面中的指定元素通过修改class修改其样式
                document.getElementById('info').className = 'show_info'
            } else {
                document.getElementById('info').className = 'hidden_info'
            }
        }
    },
    mounted() {
        window.addEventListener('scroll', this.handleScroll, true)
        if(window.screen.availWidth > 767) document.body.addEventListener('touchstart',function(){})
    }
}
</script>
<!-- 父组件:无需修改子组件的调用,同时将本身的控制函数与变量放入子组件。 -->
<header_wrapper :style="{opacity: header_opacity}" />
<!-- 移除控制代码。被控制处默认先不显示菜单,等待document修改 -->
<div id="info" class="hidden_info">

文章总数,分类总数等统计信息

通过vuepress中的blog插件进行控制,githubopen in new window有源码与参考案例

标签总数与分类总数

安装blog插件yarn add -D @vuepress/plugin-blog

配置blog插件

module.exports = {
    plugins: [
        ['@vuepress/blog', {
          	// 对需要统计的文章frontmatter字段进行指明
          	// 并指明总览页面的路由与总览页面的frontmatter信息
          	// 总布局根据layout渲染总览页面
            frontmatters: [
                {
                    id: "tag",
                    keys: ['tag', 'tags'],
                    path: '/tags/',
                    frontmatter: {
                        title: 'Tag',
                        layout: 'tags'
                    },
                    pagination: {
                        lengthPerPage: 10,
                        prevText: '',
                        nextText: ''
                    }
                },
                {
                    id: "category",
                    keys: ['category', 'categories'],
                    path: '/categories/',
                    frontmatter: {
                        title: 'Category',
                        layout: 'tags'
                    },
                    pagination: {
                        lengthPerPage: 10,
                        prevText: '',
                        nextText: ''
                    }
                }
            ]
        }],
    ],
}

配置完成后使用$tag$category 可获取blog插件统计的标签和分类的元数据信息。$tag.length可获取总数,用于在首页展示。

文章总数

通过vue的computed在页面打开时计算文章总数并返回一个变量

<template>
文章总数: {{ count }}
</template>
<script>
export default {
    computed: {
      	// 返回变量count
        count() {
          	// 筛选出文章样式的page,才能正确返回文章数量
            let list = this.$site.pages.filter(item => {
                return item.frontmatter.layout === 'detail';
            })
            return list.length;
        }
    }
}
</script>

文章大纲快捷导航与高亮当前片段

快捷导航

框架会默认将会提取 h2h3 标题open in new window,并存储在 this.$page.headers 中。遍历headers即可得到对应需要导航的大纲。由于markdown-it在渲染时将标题自动生成id锚点,只需在遍历时生成a标签href到对应id即可实现跳转

<template>
    <section class="article_sidebar">
            <ul>
                <li :class="'level' + item.level" v-for="item in $page.headers" :key="item.slug">
                  	<!-- 跳转到对应id需要‘#’后跟id值 -->
                    <a class="sidebar-link" :href="'#'+item.slug">{{ item.title }}</a>
                </li>
            </ul>
        </section>
</template>

注意: 由于headers中会按顺序返回全部标题而不是递归形式,只可通过样式控制显示的不同。在遍历生成节点时动态绑定不同的类,再通过css类选择器去进行渲染

高亮当前片段

通过当前距离页面顶部的距离与每个标题与页面顶部的距离进行比较,得到当前所处的文章片段

  • 在渲染时计算每个标题与页面顶部的距离
mounted() {
    // 计算并绑定每个导航的距离
    if (this.$frontmatter.layout == 'detail') {
        do {
            // 防止渲染未完成
            var titles = document.getElementsByClassName('header-anchor')
        } while (!titles.length)
        for (let title of titles) {
            this.height_list.push(title.parentElement.offsetTop)
        }
        // console.log(this.height_list)
    }
}
  • 在滚动时将当前距离与每个标题距离进行比较
data() {
    return {
        height_list: []
    }
},
methods: {
    scroll_acitve(){
        // 文章导航栏
        if(this.$frontmatter.layout == 'detail') {
            var scrollTop = window.pageYOffset ?? document.documentElement.scrollTop ?? document.body.scrollTop;
            if (scrollTop > this.height_list[0]) {
                let i
                for (i=0; i < this.height_list.length-1; i++) {
                    if (scrollTop > this.height_list[i] && scrollTop < this.height_list[i+1]) {
                        break
                    }
                }
                document.getElementsByClassName('article_sidebar')[0].childNodes[0].childNodes.forEach((item, index) => {
                    if (index == i) {
                        item.classList.add('active')
                    } else {
                        item.classList.remove('active')
                    }
                })
            }
        }
    }
},
mounted() {
    // 滚动触发头部与文章页导航
    window.addEventListener('scroll', this.scroll_acitve, true)
}

注意: 在明确标题后给对应的标题添加高亮类,但要同时移除其余标题的高亮类!

移动端适配

  • media对象 在桌面端与移动端样式不用时,通过media改写对应的样式

  • 切换标签的class 菜单等在移动端需要折叠、隐藏等,通过编写不同的class样式控制其是否显示

文章分页与文章分类

通过vuepress中的theme-blog插件进行控制,githubopen in new window有源码与参考案例

  • config.js配置文件中指明文档文件夹位置。可以只设置部分。
module.exports = {
    plugins: [
        ['@vuepress/blog', {
            directories: [
                {
                    // 当前分类的唯一 ID
                    id: 'detail',
                    // 目标文件夹
                    dirname: 'detail',
                    // 文章列表的路径
                    path: '/',
                    // 列表页面使用布局文件 - 布局文件不存在将仍然赋值为blog插件的默认值
                    layout: 'home',
                    // 单篇文章使用的布局文件
                    itemLayout: 'detail',
                    // 单个文章的链接
                    // itemPermalink: '/detail/:slug'
                    itemPermalink: '/:regular' // vuepress默认的生成方式
                },
            ],
        }]
    ]
}

注意:
使用blog插件会修改文档的路由,如有需要参考blog文档中对文档路由的说明open in new window进行配置
使用blog插件会修改首页frontmatter的layout配置信息(即blog目录中的README.md中的配置不生效)。在文档open in new window中有说明默认使用IndexPost布局文件,若其不存在将赋值为Layout布局文件。

  • 配置分页器
['@vuepress/blog', {
    globalPagination: {
        // 分页排序
        sorter: (prev, next) => {
            const dayjs = require('dayjs')
            const prevTime = dayjs(prev.frontmatter.date)
            const nextTime = dayjs(next.frontmatter.date)
            return prevTime - nextTime > 0 ? -1 : 1
        },
        prevText:'PREV',  // 上一页按钮的显示文本
        nextText:'NEXT',  // 下一页按钮的显示文本
        lengthPerPage:'5',
        // layout:'home', // Layout for pagination page - 分页的布局文件,不能修改frontmatter的值
    }
}]
  • 配置文章分类。指明生成页面的路径。以标签举例,将生成/tags/可用于标签总览,/tags/tags123可用于指定标签内的文章列表
['@vuepress/blog', {
    frontmatters: [
        {
            id: "tag",
            keys: ['tag', 'tags'],  // 文档标签的提取词
            path: '/tags/',  // 标签的生成路径
            frontmatter: {  // 生成页面的frontmatter
                title: 'Tag',  // 生成页面的标题
                layout: 'tags'
            },
            pagination: {  // 生成页面的分页器
                lengthPerPage: 3,
                prevText: 'PREV',
                nextText: 'NEXT'
            }
        },
        {
            id: "category",
            keys: ['category', 'categories'],  // 文档分类的提取词
            path: '/categories/',  // 分类的生成路径
            frontmatter: {  // 生成页面的frontmatter
                title: 'Category',  // 生成页面的标题
                layout: 'categories'
            },
            pagination: {  // 生成页面的分页器
                lengthPerPage: 3,
                prevText: 'PREV',
                nextText: 'NEXT'
            }
        }
    ],
}]

某种语言的代码块高亮不生效

代码高亮是通过prismjs实现
检查打包是是否出现Language does not exist: *的情况,说明prismjs不支持该语言的高亮,可前往官网查看支持语言列表open in new window
例如:配置文件一般使用systemd来表示Systemd configuration file