实现一个简单的静态博客生成器

  • A+
所属分类:Web前端
摘要

作为一名程序员,写博客是积累知识、提升水平必不可少的一个方法。我们写博客主要有三种方法,一种是使用掘金、博客园、CSDN等博客网站,第二种是自己搭建网站,存放自己的博客,第三种就是使用静态博客生成器,将生成的网页部署到服务器或者github pages、gitee pages等服务上。

作为一名程序员,写博客是积累知识、提升水平必不可少的一个方法。我们写博客主要有三种方法,一种是使用掘金、博客园、CSDN等博客网站,第二种是自己搭建网站,存放自己的博客,第三种就是使用静态博客生成器,将生成的网页部署到服务器或者github pages、gitee pages等服务上。

这三种方法中,第一种自由度太低,并且定制样式很麻烦;第二种每写一篇博客都要新建个页面,非常麻烦。因此我选择了第三种方法,在使用了hexo、vuepress,gridea等多种静态博客生成器后,我决定自己写一个来提升自己的能力。

项目地址:https://github.com/Tuzilow/CoinRailgun

明确需求

首先我们要明确需求,确定我们想要的效果

  1. 初始化博客文件夹,载入模板crn init
  2. 根据模板创建markdown文件,crn new "Hello CoinRailgun"
  3. 根据markdown文件生成html文件,crn build
  4. 本地运行网站,crn server

开始编写

安装依赖

根据上面我们分析出来的需求,确定出我们所需要的依赖,并且安装好他们

  • art-template编写模板所用的模板引擎
  • commander用来编写cli
  • dayjs处理时间
  • front-matter处理markdown顶部的yml声明
  • fs-extrafs的扩充模块
  • glob匹配指定文件名
  • highlight.js高亮代码块
  • koakoa-static启动本地服务
  • markdown-itmarkdown-it-anchormarkdown-it-toc-done-right解析markdown
  • uslug解析锚点的汉字
"dependencies": {   "art-template": "^4.13.2",   "commander": "^7.0.0",   "dayjs": "^1.10.4",   "front-matter": "^4.0.2",   "fs-extra": "^9.1.0",   "glob": "^7.1.6",   "highlight.js": "^10.5.0",   "koa": "^2.13.1",   "koa-static": "^5.0.0",   "markdown-it": "^12.0.4",   "markdown-it-anchor": "^7.0.1",   "markdown-it-toc-done-right": "^4.2.0",   "uslug": "^1.0.4" } 

搭建项目结构

. ├─ bin │    └─ crn.js  # 执行文件 ├─ lib	# crn.js调用的各个函数 │    ├─ build.js │    ├─ clean.js │    ├─ new.js │    ├─ preview.js │    └─ init.js ├─ package.json └─ template # 模板        ├─ site.config.json # 配置文件        └─ theme # 主题               └─ default # 默认主题                      ├─ assets                      └─ layout 

crn.js

同样,根据需求将各个命令、命令的参数和说明先写出来

关于commander具体如何使用,可以查看commander文档

#! /usr/bin/env node  const program = require('commander'); const version = require('../package.json').version;  program   .version(version)   .command('init [dir]')   .description('初始化博客')   .action(require('../lib/init'));  program   .command('new <name>')   .description('创建新的文章')   .action(require('../lib/new.js'));  program   .command('server [dir]')   .description('本地预览网站')   .option('-d, --dir <dir>', 'build时输出的目录')   .action(require('../lib/preview.js'));  program   .command('build [dir]')   .description('将文章渲染为html')   .option('-o, --output <dir>', '输出目录')   .action(require('../lib/build'));  program   .command('clean')   .description('清空build出来的静态文件')   .option('-d, --dir <dir>', 'build时输出的目录')   .action(require('../lib/clean.js'));  program.parse(process.argv); 

init

初始化的时候可以传入一个目录,表示准备初始化的目录,这里我用了ES2020的新语法dir = dir ?? '.',当dirnullundefined时,使用问号右边的值。

在初始化的时候,需要明确好用户使用的目录应该是什么样的

Blog ├─ build ├─ site.config.json ├─ source │    └─ _posts │           └─ blog.md └─ theme        └─ default               ├─ assets               └─ layout 

将预先准备好的模板根据设计的目录拷贝到目标目录下,而不是直接调用项目中的,因为拷贝到目标目录下后,使用者就可以更方便的自定义模板,可以更方便的写自己的样式。

关于fs-extra模块的各种API可以查看fs-extra文档

关于dayjs可以查看dayjs文档

const path = require('path'); const fs = require('fs-extra'); const dayjs = require('dayjs');  module.exports = (dir) => {   dir = dir ?? '.';    const templateDir = path.resolve(__dirname, '..', 'template');   fs.copySync(templateDir, path.resolve(dir));   fs.ensureDirSync(path.resolve(dir, 'source'));    newPost(dir); };  function newPost(dir) {   const firstPost = [     '---',     'title: Hello World',     'date: ' + dayjs().format('YYYY/MM/DD HH:mm:ss'),     'tags: ' + '[blog,CoinRailgunn]',     'category: ' + 'welcome',     '---',     '',     'Welcome to my blog, this is my first post',     '<!-- more -->'   ].join('n');    const file = path.resolve(dir, 'source', '_posts', 'hello.md');   fs.outputFileSync(file, firstPost);    console.log("博客初始化完成,键入'crn new <postName>'即可创建新的文章"); } 

new

创建新文章的函数和初始化函数有部分的逻辑是相同的,这里我没有将他们封装起来,如果感兴趣的话你们可以试试。创建文章需要传入一个name,为创建的文章名,然后将其保存至source/_post

const fs = require('fs-extra'); const path = require('path'); const dayjs = require('dayjs');  module.exports = (name) => {   const post = [     '---',     `title: ${name}`,     'date: ' + dayjs().format('YYYY/MM/DD HH:mm:ss'),     'tags: ' + '[blog]',     'category: ' + 'code',     '---',     '',   ].join('n');    const file = path.resolve('source', '_posts', `${name}.md`);   fs.outputFileSync(file, post);    console.log(`source/_posts/${name}.md 创建成功!`); }; 

build

生成静态页是整个项目最关键的部分,因为代码很多这里讲一下我的思路,详细代码可以查看项目仓库

首先我们要设计好各个页面的url,以下为我的设计:

  • 首页:/index.html/page/1/index.html
  • 不同页码:/page/页码/index.html
  • 文章页:/categories/分类名/文章名/index.html
  • 关于我页面:/about/index.html
  • 归档页:/archives/index.html
  • 分类页:/categories/index.html
  • 标签页:/tags/index.html
  • 404页:/404/index.html(这个我忘了做了

目前的浏览器会自动隐藏index.html,因此使用目录名/index.html的方式可以美化页面的地址栏

第一步,根据设计好的url编写好各个页面模板,这里我使用的是art-template

然后,一些网站的基础数据,比如author、keywords、description等,是不会发生改变的,因此需要将他们写在统一的配置文件里site.config.json,下面是我的部分配置文件

{   "basic": {     "icon": "",     "avatar": "",     "title": "",     "author": "",     "description": "",     "keywords": []   },   "theme": {     "name": "default",     "highlight": "github-gist",     "pageSize": 7,     "exclude": [       "life"     ],     "friends": [],     "about": {       "label": "about me.",       "url": "/about"     },     "nav": [       {         "name": "archives",         "label": "归档",         "url": "/archives"       },       {         "name": "categories",         "label": "分类",         "url": "/categories"       },       {         "name": "tags",         "label": "标签",         "url": "/tags"       }     ],     "links": [],     "footer": {       "beian": "",       "copyright": {         "year": "2019-2021"       }     }   },   "dev_server": {     "port": 3000   } } 

在根据markdown和模板生成html时,我们要确定模板上需要的数据,并且将配置文件和markdown的内容转换为模板上的数据

<!-- layout/post_item.art --> <div class="post-item__title">   <a href="{{url}}">     {{title}}   </a> </div> <div class="post-item__desc">   <p class="post-item__desc-date">     <i class="fa fa-calendar" aria-hidden="true"></i>     {{date}}   </p>   <p class="post-item__desc-category">     <i class="fa fa-folder-o" aria-hidden="true"></i>     <a href="/categories">       {{category || ''}}     </a>   </p> </div> <div class="post-item__abstract">   <p class="post-item__abstract-content">{{@ abstracts}}</p>   <p class="more" style="display:none;">     <a href="{{url}}">查看更多</a>   </p> </div> <div class="post-item__tags">   {{each tags}}   <a href="/tags">     <i class="fa fa-tag" aria-hidden="true"></i>     {{$value}}   </a>   {{/each}} </div> 

以文章列表项为例,这个模板需要titledatecategoryurlabstractstags,其中url是根据设计好的/categories/分类名/文章名/index.html生成出来的,其他的参数都是从markdown文件中解析出来的,并且这些参数都写在文件头部的yml配置中,而abstracts一般是使用<!--more-->分割出来。

明确了以上内容后,我们就需要获取这些参数然后传递给模板渲染出来

const template = fs.readFileSync(postTemplate, 'utf-8'); const content = fs.readFileSync(fullPath, 'utf-8'); const fm = require('front-matter');  function renderAbstracts() {   // .... }  const postItem = art.render(template, {   ...fm(content).attributes,   abstracts: renderAbstracts(), }); 

这样我们就得到了渲染后的文章列表项,然后再传入post_list.art 渲染出来文章列表后传入page.art中,与其他的数据相组合拿到完整的一个页面。渲染出页面后使用fs.outputFileSync将页面保存到一开始设计好的目录中build/page/1/index.html

大致思路就是这样,更多具体实现可以查看项目仓库

server

生成所有页面后,就可以开启本地预览了,这里我使用的是koa,使用express或者其他的框架都是大差不差的。直接将build目录设置为静态资源即可访问。

const Koa = require('koa'); const staticServe = require('koa-static'); const path = require('path');  module.exports = (dir, options) => {   dir = dir ?? '.';   const app = new Koa();    const siteConfig = require(path.resolve(dir, 'site.config.json'));    const outputDir = path.resolve(dir, options.dir ?? 'build');   app.use(staticServe(outputDir));    app.listen(siteConfig.dev_server.port, () => {     console.log(       `在浏览器中打开 http://localhost:${siteConfig.dev_server.port} 以预览网页`     );   }); }; 

这样我们就了解了制作一个静态博客生成器的思路和过程。

参考文章