文章较长,耐心读下来我想你确定会有所收获 : )
做为一个技术人员,见到别人那光鲜亮丽的我的博客,内心总免不了想搭建本身博客的冲动。固然,搭建博客的方式有好多种,可是大致上分这两种:javascript
例如:你能够用 WordPress 搭建本身的博客,你能够利用 PHP 和 MySQL 数据库在服务器上架设属于本身的网站。css
市面上有挺多的免费 静态文件(HTML)托管机构,固然其中最简单,最方便的可能就是 Github Pages 了。纯静态文件构建的网站有不少的优势,好比静态网页的访问速度比较快、容易被搜索引擎检索等。html
固然,仅仅用做博客的话,纯静态页面足够使用了。评论系统的话能够用第三方的插件,好比 Disqus。java
Github Pages 是Github提供的一个静态文件托管系统,配合Github仓库,使用起来特别方便。若是你不会使用的话,请看这里。node
并且,Github Pages 集成了 Jekyll,能够自动帮你把 markdown
语法编译成漂亮的 html
页面。git
市面上有不少的博客生成工具,能够跟 Github pages 很好的结合,像是 Hexo。其实本质上很简单,Hexo就是帮你把 markdown
编译成了 html
,
而且帮你生成了完善的目录和路由。github
经过一篇文章很难把整个工具描述的一清二楚,因此先放源代码在这里。 源代码经过咱们写的工具能够做出的博客效果是这样的:http://isweety.me/web
咱们得知了博客生成的本质,那么动手作出一个博客生成工具也就没有那么大的难度了。咱们先来梳理一下博客生成工具须要有哪些最基本的功能:数据库
markdown
编译成 html
咱们写博客,若是本身去写html的话,那怕会被累死。。 Markdown 语法帮咱们解决了这个问题,若是你对markdown不了解的话,能够看这里。npm
咱们想一下,确实,一个博客的话最基本的就两个部分:目录和博客内容。咱们模仿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,有许多成熟的库,这里咱们选用 mdpack。这个项目实际上是在marked上的一层封装。
mdpack 支持模版定制,支持多markdown拼接。
一篇文章有不少的信息须要咱们配置,好比 标题、标签、发布日期 等等,Hexo 和 Jekyll 一般有一个规范是这样的,在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('\n'); 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('\nUpadate home html success!\n'); } ); } module.exports = buildHomeHtml;
在编译博客的过程当中,一些操做利用 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'))); });
咱们已经把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('\nError: You should in the root path of blog project!\n'); // 参数解析,正如咱们上面所设计的命令用法,咱们实现了如下几个命令 // 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('\nInit project success!'); console.log('\nInstall npm packages...\n'); 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('\nProject created!\n')); console.log(`${chalk.cyan('You can')} ${chalk.grey(`cd ${projectName} && npm start`)} ${chalk.cyan('to serve blog website.')}\n`); }); }); }); } // 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 = `---\ntitle: ${program.new}\ndate: ${dateTime()}\nauthor: 做者\ntag: 标签\nintro: 简短的介绍这篇文章.\ntype: 原创\n---\n\nBlog 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(`\nFile ${program.new}.md already exist!\n`)); process.exit(2); } fs.outputFile(path.resolve(postRoot, thisYear, `${program.new}.md`), template, 'utf8', (err) => { if (err) throw err; console.log(chalk.green(`\nCreate new blog ${chalk.cyan(`${program.new}.md`)} done!\n`)); }); } 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...相关阅读:手把手教你写一个命令行工具
本章完