Hexo源码阅读
閱讀時間:全文 2106 字,預估用時 11 分鐘
創作日期:2018-02-26
上篇文章:vim的可配置项
BEGIN
前言
由于开发新网站, 涉及前后端博客需要改版, 增加留言、统计等配合后端及数据库的自定义功能, 但博客用惯了Hexo, 之前也写了一年博客挂在github page上, 因此决定修改hexo的next主题满足自己的需求.
实验环境
- 执行
hexo version
返回如下:
hexo: 3.4.2
hexo-cli: 1.0.4
os: Linux 4.13.0-36-generic linux x64
http_parser: 2.7.0
node: 9.1.0
v8: 6.2.414.32-node.8
uv: 1.15.0
zlib: 1.2.11
ares: 1.13.0
modules: 59
nghttp2: 1.25.0
openssl: 1.0.2m
icu: 59.1
unicode: 9.0
cldr: 31.0.1
tz: 2017b
- 辅助工具: google-chrome, vim
细则说明
- 关于NodeJS的调试方法参见NodeJS代码的debug
- 实验博客为可正常运行的, 路径为:
/project/blog/
require('modname')
方法查找模块的过程如下:
1. 参数为文件名
modname -> modname.js -> modname.json -> modname.node
2. 参数为路径地址
package.json(main) -> index.js -> index.json -> index.node
理解Hexo
用过Hexo写博客的都知道, Hexo是高度可定制的, 通过
_config.yml
配置关键信息, 可通过下载主题, 体验社区里优秀的博客界面. 但核心还是在于Hexo本身, 生成博客文章等.
找到文件进入调试模式
- 查找Hexo路径:
which hexo
返回路径/home/zxl/.nvm/versions/node/v9.1.0/bin/hexo
, 查看文件代码如下(关注第一行):
#!/usr/bin/env node
'use strict';
require('hexo-cli')();
- 启用调试模式执行文件:
/usr/bin/env node debug /home/zxl/.nvm/versions/node/v9.1.0/bin/hexo g
- 单步执行找到hexo-cli入口:
/home/zxl/.nvm/versions/node/v9.1.0/lib/node_modules/hexo/node_modules/hexo-cli/lib/hexo.js
- 具体执行命令的文件入口:
/project/blog/node_modules/hexo/lib/hexo/index.js
- 引入的模块
- chalk 🔗: 让命令行输出更个性化, 如使用
chalk.blue('hello world!')
, 则输出hello world! - tildify 🔗: 将绝对路径的家目录替换成
~
, 如tildify('/home/zxl/haha')
返回~/haha
- bluebird 🔗: 一个实现promise语法的库.
- minimist 🔗: 参数解析库, 如
hexo g
解析成{ _: [], g: true }
- hexo-fs 🔗: 文件与目录管理工具库库
- Lodash 🔗: JavaScript实用工具库
- hexo-log 🔗: 日志输出功能
- warehouse 🔗: 一个基于json格式数据结构的文本型数据库
- abbrev-js 🔗: 切割字符串产生切割串与字符的对应关系, 用户参数简称的生成
- resolve 🔗: 根据模块名返回模块的入口文件绝对路径, 即模块中package.json中main的值
- hexo-util 🔗: Hexo实用工具集
- pretty-hrtime 🔗: 时间戳格式化输出
- chalk 🔗: 让命令行输出更个性化, 如使用
hexo-cli解读
作用: 执行hexo构建的命令行工具
目录结构:
.
├── lib
│ ├── console
│ │ ├── help.js # 用于注册help参数及功能
│ │ ├── index.js # 用于注册参数及功能
│ │ ├── init.js # 注册init参数及功能
│ │ └── version.js # 注册version参数及功能
│ ├── context.js # 初始赋值hexo变量的上下文
│ ├── extend
│ │ └── console.js # 用于初始构造上下文时的命令仓库
│ ├── find_pkg.js # 检查执行命令的目录是否为博客根目录
│ ├── goodbye.js # 执行完退出时的交互
│ └── hexo.js # hexo-cli的入口
├── package.json
代码分析
文件:
/home/zxl/.nvm/versions/node/v9.1.0/lib/node_modules/hexo/node_modules/hexo-cli/lib/hexo.js
向外提供的函数: entry
作用: 执行控制
function entry(cwd, args)
: cwd值为路径, args值为传入的参数
// 目录/project/blog/下执行命令: hexo g -d
cwd = cwd || process.cwd();
args = camelCaseKeys(args || minimist(process.argv.slice(2)));
// 此时cwd = '/project/blog', args = {_: [g], d: true}
var hexo = new Context(cwd, args); // 用预定义上下文实例化了一个hexo对象, 此对象用于之后所有操作, 加载hexo模块时值会被被替换掉
// 主要代码中清理无用代码进行解读
return findPkg(cwd, args).then(function(path) { // 检查执行命令的目录是否为博客根目录, 如果是返回根目录路径, 此时path = '/project/blog'
return loadModule(path, args).catch(function() {
process.exit(2);
});
}).then(function(mod) {
if (mod) hexo = mod; // 将实例化的Hexo模块赋值给hexo变量
require('./console')(hexo); // 赋予参数help, init, version的功能
return hexo.init(); // 初始化hexo, 赋予参数clean, config, deploy, generate, list, migrate, new, publish, render的功能
}).then(function() {
return hexo.call(cmd, args).then(function() { // 调用最终的执行函数, 此时cmd = 'g', args = { _: [], d: true }
return hexo.exit(); // 任务完成后的清理工作
})
})
function loadModule(path, args) { // 用于加载模块, 传入的path = '/project/blog'
return Promise.try(function() {
var modulePath = pathFn.join(path, 'node_modules', 'hexo'); // 此时modulePath = '/project/blog/node_modules/hexo'
var Hexo = require(modulePath); // 载入模块
return new Hexo(path, args); // 实例化模块, 进入hexo解读的Hexo方法查看具体实现
})
}
hexo解读
hexo放着主要工程文件, 供命令行工具hexo-cli引入调用, 是hexo-cli里定义的功能的延时与拓展.
请联结上面的hexo-cli解读一起阅读
目录结构:
.
├── box
│ ├── file.js
│ └── index.js
├── extend # 用于注册命令
│ ├── console.js # 注册命令的方法, 其中有两个私有属性: alias = {'h': 'help', 'he', 'help', ...}, store = {'help': [Function], 'generator': [Function], ...}
│ ├── generator.js
│ ├── index.js # 入口文件
│ └── ......
├── hexo # 主功能区
│ ├── default_config.js # hexo中必须的默认配置, 同_config.yml
│ ├── index.js
│ ├── load_config.js # 加载配置文件_config.yml和默认的config进行整合, 更新theme, source, public路径
│ ├── load_database.js
│ ├── load_plugins.js # 用于加载package.json中dependencies和devDependencies字段下的依赖包, 并执行代码
│ ├── locals.js
│ ├── multi_config_path.js
│ ├── post.js
│ ├── register_models.js
│ ├── render.js # 执行渲染处理, 如render.render({configPath}) 读入_config.yml的内容
│ ├── router.js # 路由配置
│ ├── scaffold.js
│ ├── source.js
│ └── update_package.js # 根据安装的包信息更新package.json文件的版本号
├── models # 定义数据结构供hexo自建的json数据库调用
│ ├── index.js
│ ├── post.js
│ └── ......
├── plugins # 命令及命令的作用在此文件夹下定义
│ ├── console/index.js # 注册命令
│ ├── generator # 命令对应的具体实现函数
│ └── ......
代码分析
目录:
/project/blog/node_modules/hexo/lib/
默认文件:
./hexo/index.js
, 默认文件提供的默认对象: Hexo
作用: 命令功能实现
./hexo/index.js
function Hexo(base = process.cwd(), args = {})
: base值为路径, args值为输入的参数
function Hexo(base = process.cwd(), args = {}) { // 此时base = '/project/blog', args = { _: [ g ], d: true }
const mcp = multiConfigPath(this); // 执行hexo config命令时会用到
this.base_dir = base + sep; // this.base_dir = '/project/blog/'
this.source_dir = pathFn.join(base, 'source') + sep; // this.source_dir = '/project/blog/source/'
this.plugin_dir = pathFn.join(base, 'node_modules') + sep; // this.plugin_dir = '/project/blog/node_modules/'
this.script_dir = pathFn.join(base, 'scripts') + sep; // this.script_dir = '/project/blog/scripts/'
this.scaffold_dir = pathFn.join(base, 'scaffolds') + sep; // this.scaffold_dir = '/project/blog/scaffold_dir'
this.theme_dir = pathFn.join(base, 'themes', defaultConfig.theme) + sep; // this.theme_dir = '/project/blog/themes/landscape/'
this.theme_script_dir = pathFn.join(this.theme_dir, 'scripts') + sep; // this.theme_script_dir = '/project/blog/themes/landscape/scripts/'
this.env = { // 任务进程的环境变量
version: pkg.version,
......
}
this.extend = {
console: new extend.Console(),
generator: new extend.Generator(),
......
}
this.config = _.cloneDeep(defaultConfig); // 预设的默认配置
this.log = logger(this.env); // 用于日志输出
this.render = new Render(this); //
this.route = new Router(); // 路由配置
this.post = new Post(this); //
this.scaffold = new Scaffold(this); //
this.database = new Database({ // 实例化文本型数据库引擎, 载入数据
version: dbVersion,
path: pathFn.join(base, 'db.json') // '/project/blog/db.json'
});
registerModels(this); // 定义数据结构
this.source = new Source(this); //
this.theme = new Theme(this); //
this.locals = new Locals(this); //
this._bindLocals(); // 数据库相关操作
}
Hexo.prototype.init = function(){}
Hexo.prototype.init = function() {
require('../plugins/console')(this);
require('../plugins/generator')(this); // 注册命令
......
return Promise.each([ // 加载模块并执行, 具体作用可以看上面的目录结构下对应的文件分析
'update_package',
'load_config',
'load_plugins'
], name => require(`./${name}`)(self)).then(() => self.execFilter('after_init', null, {context: self})).then(() => {
self.emit('ready');
});
}
Hexo.prototype.call = function(name, args, callback) {}
: name为命令, args为参数
Hexo.prototype.call = function(name, args, callback) { // 接于hexo-cli解读, 此时: name = g, args = { _: [ ], d: true }
return new Promise((resolve, reject) => {
const c = self.extend.console.get(name); // 返回命令的全称对应下的处理函数, 如g全称为generator, 反对对应generator的处理函数
c.call(self, args).then(resolve, reject); // 执行命令, 此时要回到init函数中查看注册命令处理函数的代码
})
}
./plugins/console/generate.js
function generateConsole(args = {}) {}
: args为参数列表
function generateConsole(args = {}) {
let start = process.hrtime(); // 记录开始时间
const generatingFiles = {};
function generateFile(path) {
// 生成文件
writeFile(path, true);
}
function writeFile(path, force) {
// 写入文件
...
}
function deleteFile(path) {}
function wrapDataStream(dataStream, options) {}
function firstGenerate() {
// 生成文件入口
...
return Promise.all([
Promise.map(routeList, generateFile),
Promise.filter(publicFiles, path => !~routeList.indexOf(path)).map(deleteFile)
])
}
if (args.w || args.watch) {
// 检查参数是否带watch, 带则启动watch模式, 监听文件变化
...
}
return this.load().then(firstGenerate).then(() => {
if (args.d || args.deploy) {
return self.call('deploy', args);
}
});
}
function pipeStream() {}
function CacheStream() {}
NodeJS语法摘要
- 返回执行命令的目录:
process.cwd()
- 通过events系统模块发起事件监听及触发事件
const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('an event occurred!');
});
myEmitter.emit('event');
总结
经过几天源码阅读, 这个项目的很多部分值得我们学习, 作者也很厉害, 感谢作者为技术进步造福, 项目不是很大, 很适合阅读学习.
需要跟进理解:
- 项目架构
- 自建数据库
- 日志系统
- 文件生成
FINISH
上篇文章:vim的可配置项