摘要:代碼如下首頁(yè)的模版博客網(wǎng)站的基本配置菜單生成,這里不講講中的遍歷,然后生成一個(gè)數(shù)組默認(rèn)按發(fā)布時(shí)間排序置頂替換五集成在編譯博客的過(guò)程中,一些操作利用會(huì)簡(jiǎn)單快捷許多。
文章較長(zhǎng),耐心讀下來(lái)我想你肯定會(huì)有所收獲 : )
作為一個(gè)技術(shù)人員,見(jiàn)到別人那光鮮亮麗的個(gè)人博客,心里總免不了想搭建自己博客的沖動(dòng)。當(dāng)然,搭建博客的方式有好多種,但是大體上分這兩種:
服務(wù)端數(shù)據(jù)庫(kù)
例如:你可以用 WordPress 搭建自己的博客,你可以利用 PHP 和 MySQL 數(shù)據(jù)庫(kù)在服務(wù)器上架設(shè)屬于自己的網(wǎng)站。
純靜態(tài)頁(yè)面
市面上有挺多的免費(fèi) 靜態(tài)文件(HTML)托管機(jī)構(gòu),當(dāng)然其中最簡(jiǎn)單,最方便的可能就是 Github Pages 了。純靜態(tài)文件構(gòu)建的網(wǎng)站有很多的優(yōu)點(diǎn),比如靜態(tài)網(wǎng)頁(yè)的訪問(wèn)速度比較快、容易被搜索引擎檢索等。
當(dāng)然,僅僅用作博客的話,純靜態(tài)頁(yè)面足夠使用了。評(píng)論系統(tǒng)的話可以用第三方的插件,比如 Disqus。
Github PagesGithub Pages 是Github提供的一個(gè)靜態(tài)文件托管系統(tǒng),配合Github倉(cāng)庫(kù),使用起來(lái)特別方便。如果你不會(huì)使用的話,請(qǐng)看這里。
而且,Github Pages 集成了 Jekyll,可以自動(dòng)幫你把 markdown 語(yǔ)法編譯成漂亮的 html 頁(yè)面。
市面上有很多的博客生成工具,可以跟 Github pages 很好的結(jié)合,像是 Hexo。其實(shí)本質(zhì)上很簡(jiǎn)單,Hexo就是幫你把 markdown 編譯成了 html,
并且?guī)湍闵闪送晟频哪夸浐吐酚伞?/p>
手把手教你寫(xiě)一個(gè)博客生成工具出來(lái)
通過(guò)一篇文章很難把整個(gè)工具描述的一清二楚,所以先放源代碼在這里。源代碼通過(guò)我們寫(xiě)的工具可以作出的博客效果是這樣的:http://isweety.me/
我們得知了博客生成的本質(zhì),那么動(dòng)手做出一個(gè)博客生成工具也就沒(méi)有那么大的難度了。我們先來(lái)梳理一下博客生成工具需要有哪些最基本的功能:
markdown 編譯成 html
我們寫(xiě)博客,如果自己去寫(xiě)html的話,那怕會(huì)被累死。。 Markdown 語(yǔ)法幫我們解決了這個(gè)問(wèn)題,如果你對(duì)markdown不了解的話,可以看這里。
生成目錄結(jié)構(gòu)
我們想一下,確實(shí),一個(gè)博客的話最基本的就兩個(gè)部分:目錄和博客內(nèi)容。我們模仿Hexo的命令,設(shè)計(jì)如下:
我們把工具命名為 Bloger。
bloger init blog # 初始化一個(gè)名為blog的博客項(xiàng)目 bloger new hello-world # 創(chuàng)建一篇名為 hello-word 的博客 bloger build # 編譯博客網(wǎng)站 bloger dev # 監(jiān)聽(tīng) markdown 文件,實(shí)時(shí)編譯博客網(wǎng)站 bloger serve # 本地起服務(wù)
按照以上的設(shè)計(jì),我們開(kāi)始寫(xiě)工具:
一、目錄設(shè)計(jì)我們需要為我們 生成的博客項(xiàng)目 設(shè)計(jì)一個(gè)合理的文件目錄。如下:
blog
├── my.json (網(wǎng)站的基本配置)
├── index.html (首頁(yè))
├── node_modules
├── package.json
├── _posts (博客 markdown 源文件)
│ └── 2018
│ ├── test.md
│ └── hello-world.md
├── blog (_posts 中 markdown 生成的 html 文件)
│ └── 2018
│ ├── test
│ │ └──index.html (這樣設(shè)計(jì)的話,我們就可以通過(guò)訪問(wèn) https://xxx.com/blog/2018/test/ 來(lái)訪問(wèn)這篇博客了)
│ └── hello-world
│ └──index.html
└── static (博客 markdown 源文件)
├── css (網(wǎng)站的css存放的文件)
├── iconfonts (網(wǎng)站的 iconfonts 存放的文件夾)
├── images (網(wǎng)站的圖片存放的文件夾)
└── less (存放于這兒的 less 文件,會(huì)在 dev 的時(shí)候被編譯到 css 文件夾中,生成同名的 css 文件)
下面是我們寫(xiě)的工具的源碼結(jié)構(gòu):
bloger ├── bin │ └── cli.js ├── lib │ ├── less (博客的樣式文件) │ ├── pages (博客的ejs模版) │ ├── tasks (編譯網(wǎng)站的腳本) │ └── gulpfile.js └── tpl (生成的博客模版,結(jié)構(gòu)見(jiàn)上方)二、markdown編譯成html
markdown編譯成html,有許多成熟的庫(kù),這里我們選用 mdpack。這個(gè)項(xiàng)目其實(shí)是在marked上的一層封裝。
mdpack 支持模版定制,支持多markdown拼接。
一篇文章有很多的信息需要我們配置,比如 標(biāo)題、標(biāo)簽、發(fā)布日期 等等,Hexo 和 Jekyll 通常有一個(gè)規(guī)范是這樣的,在markdown文件的頂部放置文章的配置,
front-matter 格式如下:
--- title: Hello world date: 2018-09-10 tag: JavaScript,NodeJs info: 這篇文章簡(jiǎn)單介紹了寫(xiě)一個(gè)博客生成工具. ---
我們需要寫(xiě)個(gè)腳本將這些信息提取,并且轉(zhuǎn)換成一個(gè)json對(duì)象,比如上邊的信息,我們要轉(zhuǎn)換成這樣:
{
"title": "Hello world",
"date": "2018-09-10",
"tag": "JavaScript,NodeJs",
"info": "這篇文章簡(jiǎn)單介紹了寫(xiě)一個(gè)博客生成工具."
}
腳本如下:
// 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: []
};
// 把提取出來(lái)的front-matter字符串解析,生成對(duì)象
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 = "原創(chuàng)";
}
return ret;
}
try {
// 便利 _posts 文件夾,將所有的markdown內(nèi)容的front-matter轉(zhuǎn)換成對(duì)象,存放到metadata數(shù)組中
// 將生成的metadata信息寫(xiě)入一個(gè)文件中,我們命名為postMap.json,保存到所生成項(xiàng)目的根目錄,以備使用
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;
四、博客目錄生成
通過(guò)讀取postMap.json中的metadata信息,我們可以構(gòu)建一個(gè)博客目錄出來(lái)。代碼如下:
const fs = require("fs-extra");
const path = require("path");
const ejs = require("ejs");
// 首頁(yè)的ejs模版
const homeTpl = fs.readFileSync(path.resolve(__dirname, "../pages/home.ejs"), "utf8");
const root = process.cwd();
function buildHomeHtml() {
const metadata = require("./metadata");
// 博客網(wǎng)站的基本配置
const myInfo = require(path.resolve(root, "my.json"));
const htmlMenu = require("./menu")(); // 菜單生成,這里不講
// 講postMap.json中的metadata遍歷,然后生成一個(gè)blogList數(shù)組
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
};
});
// 默認(rèn)按發(fā)布時(shí)間排序
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
在編譯博客的過(guò)程中,一些操作利用 gulp 會(huì)簡(jiǎn)單快捷許多。比如 編譯less、打包iconfonts、監(jiān)聽(tīng)文件改動(dòng) 等。
但是gulp是一個(gè)命令行工具,我們?cè)趺礃幽馨裧ulp繼承到我們的工具中呢?方法很簡(jiǎn)單,如下:
const gulp = require("gulp");
require("./gulpfile.js");
// 啟動(dòng)gulpfile中的build任務(wù)
if(gulp.tasks.build) {
gulp.start("build");
}
通過(guò)以上的方法,我們可以在我們的cli工具中集成 gulp,那么好多問(wèn)題就變得特別簡(jiǎn)單,貼上完整的 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();
// 編譯博客文章頁(yè)面
function build() {
const metadata = require(path.resolve(root, "postMap.json"));
const myInfo = require(path.resolve(root, "my.json"));
const htmlMenu = require("./tasks/menu")(); // 跳過(guò)
// 刪除博客文件夾
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.");
// 我們編譯當(dāng)前項(xiàng)目下的 lib/less/*.less 和 生成的博客項(xiàng)目下的 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())
// 自動(dòng)加前綴
.pipe(autoprefixer({
browsers: ["last 2 versions"],
cascade: false
}))
// 將編譯生成的css放入生成的博客項(xiàng)目下的 static/css 文件夾中
.pipe(gulp.dest(path.resolve(root, "static/css")));
});
// 監(jiān)聽(tīng)css文件的改動(dòng),編譯css
gulp.task("cssDev", () => {
log("Starting watch less files...");
return gulp.watch([path.resolve(__dirname, "less/**/*.less"), path.resolve(root, "static/less/**/*.less")], ["css"]);
});
// 監(jiān)聽(tīng)markdown文件的改動(dòng),編譯首頁(yè)和博客文章頁(yè)
gulp.task("mdDev", () => {
log("Starting watch markdown files...");
return gulp.watch(path.resolve(root, "_posts/**/*.md"), ["home", "blog"]);
});
// 編譯首頁(yè)
gulp.task("home", buildHome);
// build博客
gulp.task("blog", build);
gulp.task("default", ["build"]);
// 監(jiān)聽(tīng)模式
gulp.task("dev", ["cssDev", "mdDev"]);
// 執(zhí)行build的時(shí)候會(huì)編譯css,編譯首頁(yè),編譯文章頁(yè)
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文件
我們已經(jīng)把gulpfile寫(xiě)完了,下面就要寫(xiě)我們的命令行工具了,并且集成gulp。代碼如下:
// cli.js
#!/usr/bin/env node
const gulp = require("gulp");
const program = require("commander"); // 命令行參數(shù)解析
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();
// 判斷是否是所生成博客項(xiàng)目的根目錄(因?yàn)槲覀儽仨氝M(jìn)入到所生成的博客項(xiàng)目中,才可以執(zhí)行我們的build和dev等命令)
const isRoot = fs.existsSync(path.resolve(root, "_posts"));
// 如果不是根目錄的話,輸出的內(nèi)容
const notRootError = chalk.red("
Error: You should in the root path of blog project!
");
// 參數(shù)解析,正如我們上面所設(shè)計(jì)的命令用法,我們實(shí)現(xiàn)了以下幾個(gè)命令
// 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 命令的話,執(zhí)行以下操作
if (program.init) {
const projectName = typeof program.init === "string" ? program.init : "blog";
const tplPath = path.resolve(__dirname, "../tpl");
const projectPath = path.resolve(root, projectName);
// 將我們的項(xiàng)目模版復(fù)制到當(dāng)前目錄下
fs.copy(tplPath, projectPath)
.then((err) => {
if (err) throw err;
console.log("
Init project success!");
console.log("
Install npm packages...
");
fs.ensureDirSync(projectPath); // 確保存在項(xiàng)目目錄
process.chdir(projectPath); // 進(jìn)入到我們生成的博客項(xiàng)目,然后執(zhí)行 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 之后執(zhí)行 npm run build,構(gòu)建博客項(xiàng)目
spawn("npm", ["run", "build"], { stdio: "inherit" }).on("close", code => {
if (code !== 0) {
process.exit(1);
}
// 構(gòu)建成功之后輸出成功信息
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 執(zhí)行的操作
if (program.build && gulp.tasks.build) {
if (isRoot) {
gulp.start("build");
} else {
console.log(notRootError);
}
}
// bloger dev執(zhí)行的操作
if (program.dev && gulp.tasks.dev) {
if (isRoot) {
gulp.start("dev");
} else {
console.log(notRootError);
}
}
// bloger new 執(zhí)行的操作
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文件夾中生成一個(gè)markdown文件,內(nèi)容是下邊的字符串模版
const template = `---
title: ${program.new}
date: ${dateTime()}
author: 作者
tag: 標(biāo)簽
intro: 簡(jiǎn)短的介紹這篇文章.
type: 原創(chuàng)
---
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執(zhí)行的操作
if (program.iconfonts && gulp.tasks.fonts) {
if (isRoot) {
gulp.start("fonts");
} else {
console.log(notRootError);
}
}
完整的項(xiàng)目源代碼:https://github.com/PengJiyuan...相關(guān)閱讀:手把手教你寫(xiě)一個(gè)命令行工具
本章完
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.hztianpu.com/yun/97673.html
摘要:在我轉(zhuǎn)前端以來(lái),一直想要實(shí)現(xiàn)一個(gè)愿望自己搭建一個(gè)可以自動(dòng)解析文檔的個(gè)人站今天終于實(shí)現(xiàn)啦,先貼上我的地址確認(rèn)需求其實(shí)一個(gè)最簡(jiǎn)單的個(gè)人站,就是許多的頁(yè)面,你只要可以用寫(xiě)出來(lái)就可以,然后掛到上。 在我轉(zhuǎn)前端以來(lái),一直想要實(shí)現(xiàn)一個(gè)愿望: 自己搭建一個(gè)可以自動(dòng)解析Markdown文檔的個(gè)人站 今天終于實(shí)現(xiàn)啦,先貼上我的blog地址 確認(rèn)需求 其實(shí)一個(gè)最簡(jiǎn)單的個(gè)人站,就是許多的HTML頁(yè)面,你只要...
摘要:重構(gòu)系統(tǒng)是一項(xiàng)非常具有挑戰(zhàn)性的事情。架構(gòu)與說(shuō)起來(lái),我一直是一個(gè)黨。如下圖是采用的架構(gòu)這與我們?cè)陧?xiàng)目上的系統(tǒng)架構(gòu)目前相似。而這是大部分所不支持的。允許內(nèi)容通過(guò)內(nèi)容服務(wù)更新使用于是,有了一個(gè)名為的框架用于管理內(nèi)容,并存儲(chǔ)為。 重構(gòu)系統(tǒng)是一項(xiàng)非常具有挑戰(zhàn)性的事情。通常來(lái)說(shuō),在我們的系統(tǒng)是第二個(gè)系統(tǒng)的時(shí)候才需要重構(gòu),即這個(gè)系統(tǒng)本身已經(jīng)很臃腫。我們花費(fèi)了太量的時(shí)間在代碼間的邏輯,開(kāi)發(fā)新的功能變得...
摘要:添加你修改的代碼找到你主題文件夾里的對(duì)應(yīng)位置以我的路徑為例子里面有個(gè)文件夾和一個(gè)文件,主要用于打包代碼輸出成樣式的文件分析下其源代碼。注意本人不提倡去修改除了下的其他個(gè)文件里的源代碼,可能后面出問(wèn)題不好還原。 showImg(https://segmentfault.com/img/remote/1460000008744124?w=1920&h=1280); 前言 之前答應(yīng)一個(gè)評(píng)論朋...
摘要:添加你修改的代碼找到你主題文件夾里的對(duì)應(yīng)位置以我的路徑為例子里面有個(gè)文件夾和一個(gè)文件,主要用于打包代碼輸出成樣式的文件分析下其源代碼。注意本人不提倡去修改除了下的其他個(gè)文件里的源代碼,可能后面出問(wèn)題不好還原。 showImg(https://segmentfault.com/img/remote/1460000008744124?w=1920&h=1280); 前言 之前答應(yīng)一個(gè)評(píng)論朋...
摘要:添加你修改的代碼找到你主題文件夾里的對(duì)應(yīng)位置以我的路徑為例子里面有個(gè)文件夾和一個(gè)文件,主要用于打包代碼輸出成樣式的文件分析下其源代碼。注意本人不提倡去修改除了下的其他個(gè)文件里的源代碼,可能后面出問(wèn)題不好還原。 showImg(https://segmentfault.com/img/remote/1460000008744124?w=1920&h=1280); 前言 之前答應(yīng)一個(gè)評(píng)論朋...
閱讀 3613·2021-10-09 09:43
閱讀 6260·2021-09-07 10:15
閱讀 2807·2019-08-30 14:03
閱讀 3144·2019-08-29 11:01
閱讀 1834·2019-08-29 10:56
閱讀 1162·2019-08-28 17:52
閱讀 3565·2019-08-26 11:42
閱讀 2627·2019-08-26 10:33