资讯专栏INFORMATION COLUMN

搭建博客太简单,这次我们来做一个博客生成工具

chanthuang / 834人阅读

摘要:代码如下首页的模版博客网站的基本配置菜单生成,这里不讲讲中的遍历,然后生成一个数组默认按发布时间排序置顶替换五集成在编译博客的过程中,一些操作利用会简单快捷许多。

文章较长,耐心读下来我想你肯定会有所收获 : )

作为一个技术人员,见到别人那光鲜亮丽的个人博客,心里总免不了想搭建自己博客的冲动。当然,搭建博客的方式有好多种,但是大体上分这两种:

服务端数据库

例如:你可以用 WordPress 搭建自己的博客,你可以利用 PHP 和 MySQL 数据库在服务器上架设属于自己的网站。

纯静态页面

市面上有挺多的免费 静态文件HTML)托管机构,当然其中最简单,最方便的可能就是 Github Pages 了。纯静态文件构建的网站有很多的优点,比如静态网页的访问速度比较快、容易被搜索引擎检索等。

当然,仅仅用作博客的话,纯静态页面足够使用了。评论系统的话可以用第三方的插件,比如 Disqus。

Github Pages

Github Pages 是Github提供的一个静态文件托管系统,配合Github仓库,使用起来特别方便。如果你不会使用的话,请看这里。

而且,Github Pages 集成了 Jekyll,可以自动帮你把 markdown 语法编译成漂亮的 html 页面。

市面上有很多的博客生成工具,可以跟 Github pages 很好的结合,像是 Hexo。其实本质上很简单,Hexo就是帮你把 markdown 编译成了 html,
并且帮你生成了完善的目录和路由。

手把手教你写一个博客生成工具出来
通过一篇文章很难把整个工具描述的一清二楚,所以先放源代码在这里。源代码

通过我们写的工具可以作出的博客效果是这样的:http://isweety.me/

我们得知了博客生成的本质,那么动手做出一个博客生成工具也就没有那么大的难度了。我们先来梳理一下博客生成工具需要有哪些最基本的功能:

markdown 编译成 html

我们写博客,如果自己去写html的话,那怕会被累死。。 Markdown 语法帮我们解决了这个问题,如果你对markdown不了解的话,可以看这里。

生成目录结构

我们想一下,确实,一个博客的话最基本的就两个部分:目录和博客内容。我们模仿Hexo的命令,设计如下:

我们把工具命名为 Bloger
bloger init blog # 初始化一个名为blog的博客项目

bloger new hello-world # 创建一篇名为 hello-word 的博客

bloger build # 编译博客网站

bloger dev # 监听 markdown 文件,实时编译博客网站

bloger serve # 本地起服务

按照以上的设计,我们开始写工具:

一、目录设计

我们需要为我们 生成的博客项目 设计一个合理的文件目录。如下:

blog
  ├── my.json (网站的基本配置)
  ├── index.html (首页)
  ├── node_modules
  ├── package.json
  ├── _posts (博客 markdown 源文件)
  │   └── 2018
  │       ├── test.md
  │       └── hello-world.md
  ├── blog (_posts 中 markdown 生成的 html 文件)
  │   └── 2018
  │       ├── test
  │       │   └──index.html (这样设计的话,我们就可以通过访问 https://xxx.com/blog/2018/test/ 来访问这篇博客了)
  │       └── hello-world
  │           └──index.html
  └── static (博客 markdown 源文件)
      ├── css (网站的css存放的文件)
      ├── iconfonts (网站的 iconfonts 存放的文件夹)
      ├── images (网站的图片存放的文件夹)
      └── less (存放于这儿的 less 文件,会在 dev 的时候被编译到 css 文件夹中,生成同名的 css 文件)

下面是我们写的工具的源码结构:

bloger
  ├── bin
  │   └── cli.js
  ├── lib
  │   ├── less (博客的样式文件)
  │   ├── pages (博客的ejs模版)
  │   ├── tasks (编译网站的脚本)
  │   └── gulpfile.js
  └── tpl (生成的博客模版,结构见上方)
二、markdown编译成html

markdown编译成html,有许多成熟的库,这里我们选用 mdpack。这个项目其实是在marked上的一层封装。
mdpack 支持模版定制,支持多markdown拼接。

三、文章信息配置

一篇文章有很多的信息需要我们配置,比如 标题标签发布日期 等等,HexoJekyll 通常有一个规范是这样的,在markdown文件的顶部放置文章的配置,
front-matter 格式如下:

 ---
 title: Hello world
 date: 2018-09-10
 tag: JavaScript,NodeJs
 info: 这篇文章简单介绍了写一个博客生成工具.
 ---

我们需要写个脚本将这些信息提取,并且转换成一个json对象,比如上边的信息,我们要转换成这样:

{
  "title": "Hello world",
  "date": "2018-09-10",
  "tag": "JavaScript,NodeJs",
  "info": "这篇文章简单介绍了写一个博客生成工具."
}

脚本如下:

// task/metadata.js
const frontMatter = require("@egoist/front-matter"); // 截取头部front-matter信息
const fs = require("fs");
const path = require("path");
const root = process.cwd();

const metadata = {
  post: []
};

// 把提取出来的front-matter字符串解析,生成对象
function getMetadata(content) {
  const head = frontMatter(content).head.split("
");
  const ret = {};
  head.forEach((h) => {
    const [key, value] = h.split(": ");
    ret[key.trim()] = value.trim();
  });

  if (!ret.type) {
    ret.type = "原创";
  }

  return ret;
}

try {
  // 便利 _posts 文件夹,将所有的markdown内容的front-matter转换成对象,存放到metadata数组中
  // 将生成的metadata信息写入一个文件中,我们命名为postMap.json,保存到所生成项目的根目录,以备使用
  fs.readdirSync(path.resolve(root, "_posts"))
  .filter(m => fs.statSync(path.resolve(root, "_posts", m)).isDirectory())
  .forEach((year) => {
    fs.readdirSync(path.resolve(root, "_posts", year))
      .forEach((post) => {
        const content = fs.readFileSync(path.resolve(root, "_posts", year, post), "utf8");
        metadata.post.push({
          year,
          filename: post.split(".md")[0],
          metadata: getMetadata(content)
        });
      });
  });

  fs.writeFileSync(path.resolve(root, "postMap.json"), JSON.stringify(metadata), "utf8");
} catch (err) {}

module.exports = metadata;
四、博客目录生成

通过读取postMap.json中的metadata信息,我们可以构建一个博客目录出来。代码如下:

const fs = require("fs-extra");
const path = require("path");
const ejs = require("ejs");
// 首页的ejs模版
const homeTpl = fs.readFileSync(path.resolve(__dirname, "../pages/home.ejs"), "utf8");
const root = process.cwd();

function buildHomeHtml() {
  const metadata = require("./metadata");
  // 博客网站的基本配置
  const myInfo = require(path.resolve(root, "my.json"));
  const htmlMenu = require("./menu")(); // 菜单生成,这里不讲

  // 讲postMap.json中的metadata遍历,然后生成一个blogList数组
  const blogList = metadata.post.map((postInfo) => {
    const data = postInfo.metadata;

    return {
      title: data.title,
      date: data.date,
      url: `/blog/${postInfo.year}/${postInfo.filename}`,
      intro: data.intro,
      tags: data.tag.split(","),
      author: data.author,
      type: data.type,
      top: data.top === "true" ? true : false
    };
  });

  // 默认按发布时间排序
  blogList.sort((a, b) => new Date(a.date) - new Date(b.date));

  // 置顶
  blogList.sort((a, b) => !a.top);

  // ejs替换
  fs.outputFile(
    path.resolve(root, "index.html"),
    ejs.render(homeTpl, {
      name: myInfo.name,
      intro: myInfo.intro,
      homepage: myInfo.homepage,
      links: myInfo.links,
      blogList,
      htmlMenu
    }),
    (err) => {
      console.log("
Upadate home html success!
");
    }
  );
}

module.exports = buildHomeHtml;
五、集成gulp

在编译博客的过程中,一些操作利用 gulp 会简单快捷许多。比如 编译less打包iconfonts监听文件改动 等。
但是gulp是一个命令行工具,我们怎么样能把gulp继承到我们的工具中呢?方法很简单,如下:

const gulp = require("gulp");
require("./gulpfile.js");

// 启动gulpfile中的build任务
if(gulp.tasks.build) {
  gulp.start("build");
}

通过以上的方法,我们可以在我们的cli工具中集成 gulp,那么好多问题就变得特别简单,贴上完整的 gulpfile:

const fs = require("fs");
const path = require("path");
const url = require("url");
const del = require("del");
const gulp = require("gulp");
const log = require("fancy-log");
const less = require("gulp-less");
const minifyCSS = require("gulp-csso");
const autoprefixer = require("gulp-autoprefixer");
const plumber = require("gulp-plumber");
const iconfont = require("gulp-iconfont");
const iconfontCss = require("gulp-iconfont-css");
const mdpack = require("mdpack");
const buildHome = require("./tasks/home");
const root = process.cwd();

// 编译博客文章页面
function build() {
  const metadata = require(path.resolve(root, "postMap.json"));
  const myInfo = require(path.resolve(root, "my.json"));
  const htmlMenu = require("./tasks/menu")(); // 跳过
  // 删除博客文件夹
  del.sync(path.resolve(root, "blog"));
  
  // 遍历_posts文件夹,编译所有的markdown文件
  // 生成的格式为 blog/${year}/${filename}/index.html
  fs.readdirSync(path.resolve(root, "_posts"))
  .filter(m => fs.statSync(path.resolve(root, "_posts", m)).isDirectory())
  .forEach((year) => {
    fs.readdirSync(path.resolve(root, "_posts", year))
      .forEach((post) => {
        const filename = post.split(".md")[0];
        const _meta = metadata.post.find(_m => _m.filename === filename).metadata;
        const currentUrl = url.resolve(myInfo.homepage, `blog/${year}/${filename}`);
        const mdConfig = {
          entry: path.resolve(root, "_posts", year, post),
          output: {
            path: path.resolve(root, "blog", year, filename),
            name: "index"
          },
          format: ["html"],
          plugins: [
            // 去除markdown文件头部的front-matter
            new mdpack.plugins.mdpackPluginRemoveHead()
          ],
          template: path.join(__dirname, "pages/blog.ejs"),
          resources: {
            markdownCss: "/static/css/markdown.css",
            highlightCss: "/static/css/highlight.css",
            title: _meta.title,
            author: _meta.author,
            type: _meta.type,
            intro: _meta.intro,
            tag: _meta.tag,
            keywords: _meta.keywords,
            homepage: myInfo.homepage,
            name: myInfo.name,
            disqusUrl: myInfo.disqus ? myInfo.disqus.src : false,
            currentUrl,
            htmlMenu
          }
        };
        mdpack(mdConfig);
      });
  });
}

// 编译css
gulp.task("css", () => {
  log("Compile less.");
  // 我们编译当前项目下的 lib/less/*.less 和 生成的博客项目下的 static/less/**/*.less
  return gulp.src([path.resolve(__dirname, "less/*.less"), path.resolve(root, "static/less/**/*.less")])
    .pipe(plumber())
    .pipe(less({
      paths: [root]
    }))
    // css压缩
    .pipe(minifyCSS())
    // 自动加前缀
    .pipe(autoprefixer({
      browsers: ["last 2 versions"],
      cascade: false
    }))
    // 将编译生成的css放入生成的博客项目下的 static/css 文件夹中
    .pipe(gulp.dest(path.resolve(root, "static/css")));
});

// 监听css文件的改动,编译css
gulp.task("cssDev", () => {
  log("Starting watch less files...");
  return gulp.watch([path.resolve(__dirname, "less/**/*.less"), path.resolve(root, "static/less/**/*.less")], ["css"]);
});

// 监听markdown文件的改动,编译首页和博客文章页
gulp.task("mdDev", () => {
  log("Starting watch markdown files...");
  return gulp.watch(path.resolve(root, "_posts/**/*.md"), ["home", "blog"]);
});

// 编译首页
gulp.task("home", buildHome);

// build博客
gulp.task("blog", build);

gulp.task("default", ["build"]);

// 监听模式
gulp.task("dev", ["cssDev", "mdDev"]);

// 执行build的时候会编译css,编译首页,编译文章页
gulp.task("build", ["css", "home", "blog"]);

// 生成iconfonts
gulp.task("fonts", () => {
  console.log("Task: [Generate icon fonts and stylesheets and preview html]");
  return gulp.src([path.resolve(root, "static/iconfonts/svgs/**/*.svg")])
    .pipe(iconfontCss({
      fontName: "icons",
      path: "css",
      targetPath: "icons.css",
      cacheBuster: Math.random()
    }))
    .pipe(iconfont({
      fontName: "icons",
      prependUnicode: true,
      fontHeight: 1000,
      normalize: true
    }))
    .pipe(gulp.dest(path.resolve(root, "static/iconfonts/icons")));
});
六、cli文件

我们已经把gulpfile写完了,下面就要写我们的命令行工具了,并且集成gulp。代码如下:

// cli.js
#!/usr/bin/env node

const gulp = require("gulp");
const program = require("commander"); // 命令行参数解析
const fs = require("fs-extra");
const path = require("path");
const spawn = require("cross-spawn");
const chalk = require("chalk");
const dateTime = require("date-time");
require("../lib/gulpfile");
const { version } = require("../package.json");
const root = process.cwd();

// 判断是否是所生成博客项目的根目录(因为我们必须进入到所生成的博客项目中,才可以执行我们的build和dev等命令)
const isRoot = fs.existsSync(path.resolve(root, "_posts"));
// 如果不是根目录的话,输出的内容
const notRootError = chalk.red("
Error: You should in the root path of blog project!
");

// 参数解析,正如我们上面所设计的命令用法,我们实现了以下几个命令
// bloger init [blogName]
// bloger new [blog]
// bloger build
// bloger dev
// bloger iconfonts
program
  .version(version)
  .option("init [blogName]", "init blog project")
  .option("new [blog]", "Create a new blog")
  .option("build", "Build blog")
  .option("dev", "Writing blog, watch mode.")
  .option("iconfonts", "Generate iconfonts.")
  .parse(process.argv);

// 如果使用 bloger init 命令的话,执行以下操作
if (program.init) {
  const projectName = typeof program.init === "string" ? program.init : "blog";
  const tplPath = path.resolve(__dirname, "../tpl");
  const projectPath = path.resolve(root, projectName);
  // 将我们的项目模版复制到当前目录下
  fs.copy(tplPath, projectPath)
    .then((err) => {
      if (err) throw err;
      console.log("
Init project success!");
      console.log("
Install npm packages...
");
      fs.ensureDirSync(projectPath); // 确保存在项目目录
      process.chdir(projectPath); // 进入到我们生成的博客项目,然后执行 npm install 操作
      const commond = "npm";
      const args = [
        "install"
      ];
      
      // npm install
      spawn(commond, args, { stdio: "inherit" }).on("close", code => {
        if (code !== 0) {
          process.exit(1);
        }
        // npm install 之后执行 npm run build,构建博客项目
        spawn("npm", ["run", "build"], { stdio: "inherit" }).on("close", code => {
          if (code !== 0) {
            process.exit(1);
          }
          // 构建成功之后输出成功信息
          console.log(chalk.cyan("
Project created!
"));
          console.log(`${chalk.cyan("You can")} ${chalk.grey(`cd ${projectName} && npm start`)} ${chalk.cyan("to serve blog website.")}
`);
        });
      });
    });
}

// bloger build 执行的操作
if (program.build && gulp.tasks.build) {
  if (isRoot) {
    gulp.start("build");
  } else {
    console.log(notRootError);
  }
}

// bloger dev执行的操作
if (program.dev && gulp.tasks.dev) {
  if (isRoot) {
    gulp.start("dev");
  } else {
    console.log(notRootError);
  }
}

// bloger new 执行的操作
if (program.new && typeof program.new === "string") {
  if (isRoot) {
    const postRoot = path.resolve(root, "_posts");
    const date = new Date();
    const thisYear = date.getFullYear().toString();
    // 在_posts文件夹中生成一个markdown文件,内容是下边的字符串模版
    const template = `---
title: ${program.new}
date: ${dateTime()}
author: 作者
tag: 标签
intro: 简短的介绍这篇文章.
type: 原创
---

Blog Content`;
    fs.ensureDirSync(path.resolve(postRoot, thisYear));
    const allList = fs.readdirSync(path.resolve(postRoot, thisYear)).map(name => name.split(".md")[0]);
    // name exist
    if (~allList.indexOf(program.new)) {
      console.log(chalk.red(`
File ${program.new}.md already exist!
`));
      process.exit(2);
    }
    fs.outputFile(path.resolve(postRoot, thisYear, `${program.new}.md`), template, "utf8", (err) => {
      if (err) throw err;
      console.log(chalk.green(`
Create new blog ${chalk.cyan(`${program.new}.md`)} done!
`));
    });
  } else {
    console.log(notRootError);
  }
}

// bloger iconfonts执行的操作
if (program.iconfonts && gulp.tasks.fonts) {
  if (isRoot) {
    gulp.start("fonts");
  } else {
    console.log(notRootError);
  }
}
完整的项目源代码:https://github.com/PengJiyuan...

相关阅读:手把手教你写一个命令行工具

本章完

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/97673.html

相关文章

  • 手把手教你用vue搭建个人站

    摘要:在我转前端以来,一直想要实现一个愿望自己搭建一个可以自动解析文档的个人站今天终于实现啦,先贴上我的地址确认需求其实一个最简单的个人站,就是许多的页面,你只要可以用写出来就可以,然后挂到上。 在我转前端以来,一直想要实现一个愿望: 自己搭建一个可以自动解析Markdown文档的个人站 今天终于实现啦,先贴上我的blog地址 确认需求 其实一个最简单的个人站,就是许多的HTML页面,你只要...

    xietao3 评论0 收藏0
  • Repractise架构篇一: CMS的重构与演进

    摘要:重构系统是一项非常具有挑战性的事情。架构与说起来,我一直是一个党。如下图是采用的架构这与我们在项目上的系统架构目前相似。而这是大部分所不支持的。允许内容通过内容服务更新使用于是,有了一个名为的框架用于管理内容,并存储为。 重构系统是一项非常具有挑战性的事情。通常来说,在我们的系统是第二个系统的时候才需要重构,即这个系统本身已经很臃肿。我们花费了太量的时间在代码间的逻辑,开发新的功能变得...

    William_Sang 评论0 收藏0
  • 2017年最新基于hexo搭建个人免费博客——自定义页面样式一

    摘要:添加你修改的代码找到你主题文件夹里的对应位置以我的路径为例子里面有个文件夹和一个文件,主要用于打包代码输出成样式的文件分析下其源代码。注意本人不提倡去修改除了下的其他个文件里的源代码,可能后面出问题不好还原。 showImg(https://segmentfault.com/img/remote/1460000008744124?w=1920&h=1280); 前言 之前答应一个评论朋...

    curried 评论0 收藏0
  • 2017年最新基于hexo搭建个人免费博客——自定义页面样式一

    摘要:添加你修改的代码找到你主题文件夹里的对应位置以我的路径为例子里面有个文件夹和一个文件,主要用于打包代码输出成样式的文件分析下其源代码。注意本人不提倡去修改除了下的其他个文件里的源代码,可能后面出问题不好还原。 showImg(https://segmentfault.com/img/remote/1460000008744124?w=1920&h=1280); 前言 之前答应一个评论朋...

    KevinYan 评论0 收藏0
  • 2017年最新基于hexo搭建个人免费博客——自定义页面样式一

    摘要:添加你修改的代码找到你主题文件夹里的对应位置以我的路径为例子里面有个文件夹和一个文件,主要用于打包代码输出成样式的文件分析下其源代码。注意本人不提倡去修改除了下的其他个文件里的源代码,可能后面出问题不好还原。 showImg(https://segmentfault.com/img/remote/1460000008744124?w=1920&h=1280); 前言 之前答应一个评论朋...

    leoperfect 评论0 收藏0

发表评论

0条评论

chanthuang

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<