- A+
作为一名程序员,写博客是积累知识、提升水平必不可少的一个方法。我们写博客主要有三种方法,一种是使用掘金、博客园、CSDN等博客网站,第二种是自己搭建网站,存放自己的博客,第三种就是使用静态博客生成器,将生成的网页部署到服务器或者github pages、gitee pages等服务上。
这三种方法中,第一种自由度太低,并且定制样式很麻烦;第二种每写一篇博客都要新建个页面,非常麻烦。因此我选择了第三种方法,在使用了hexo、vuepress,gridea等多种静态博客生成器后,我决定自己写一个来提升自己的能力。
明确需求
首先我们要明确需求,确定我们想要的效果
- 初始化博客文件夹,载入模板
crn init
- 根据模板创建markdown文件,
crn new "Hello CoinRailgun"
- 根据markdown文件生成html文件,
crn build
- 本地运行网站,
crn server
开始编写
安装依赖
根据上面我们分析出来的需求,确定出我们所需要的依赖,并且安装好他们
art-template
编写模板所用的模板引擎commander
用来编写clidayjs
处理时间front-matter
处理markdown顶部的yml声明fs-extra
fs的扩充模块glob
匹配指定文件名highlight.js
高亮代码块koa
和koa-static
启动本地服务markdown-it
、markdown-it-anchor
、markdown-it-toc-done-right
解析markdownuslug
解析锚点的汉字
"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 ?? '.'
,当dir
为null
或undefined
时,使用问号右边的值。
在初始化的时候,需要明确好用户使用的目录应该是什么样的
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
template/theme/default/layout/layout.art
template/theme/default/layout/page.art
- 其他请查看CoinRailgun默认主题模板
然后,一些网站的基础数据,比如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>
以文章列表项为例,这个模板需要title
、date
、category
、url
、abstracts
和tags
,其中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} 以预览网页` ); }); };
这样我们就了解了制作一个静态博客生成器的思路和过程。