zoukankan      html  css  js  c++  java
  • 实现一个简单的静态博客生成器

    作为一名程序员,写博客是积累知识、提升水平必不可少的一个方法。我们写博客主要有三种方法,一种是使用掘金、博客园、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('
    ');
    
      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('
    ');
    
      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} 以预览网页`
        );
      });
    };
    

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

    参考文章

    作者:Tuzilow
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
  • 相关阅读:
    ConcurrentHashMap的size方法是线程安全的吗?
    redis cluster介绍与gossip协议
    leetcode刷题篇 21题合并两个排序的链表 java C++版本
    MySQL索引凭什么能让查询效率提高这么多?
    从零开始学习html(十五)css样式设置小技巧——下
    从零开始学习html(十五)css样式设置小技巧——上
    从零开始学习html(十四)单位和值
    从零开始学习html(十三) CSS代码缩写,占用更少的带宽
    从零开始学习html(十二)CSS布局模型——下
    从零开始学习html(十二)CSS布局模型——上
  • 原文地址:https://www.cnblogs.com/xueyubao/p/14387918.html
Copyright © 2011-2022 走看看