使用 Hexo,痛骂 Hexo,理解 Hexo,成为 Hexo。

这篇文章是用来记录我阅读 Hexo 源代码的过程和分析。

版本号

文章写的时候,Hexo 的版本为 7.2.0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
{
"name": "hexo",
"version": "7.2.0",
"description": "A fast, simple & powerful blog framework, powered by Node.js.",
"main": "dist/hexo",
"bin": {
"hexo": "./bin/hexo"
},
"scripts": {
"prepublishOnly": "npm install && npm run clean && npm run build",
"build": "tsc -b",
"clean": "tsc -b --clean",
"eslint": "eslint lib test",
"pretest": "npm run clean && npm run build",
"test": "mocha test/scripts/**/*.ts --require ts-node/register",
"test-cov": "c8 --reporter=lcovonly npm test -- --no-parallel",
"prepare": "husky install"
},
"files": [
"dist/",
"bin/"
],
"types": "./dist/hexo/index.d.ts",
"repository": "hexojs/hexo",
"homepage": "https://hexo.io/",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/hexo"
},
"keywords": [
"website",
"blog",
"cms",
"framework",
"hexo"
],
"author": "Tommy Chen <[email protected]> (https://zespia.tw)",
"maintainers": [
"Abner Chou <[email protected]> (https://abnerchou.me)"
],
"license": "MIT",
"dependencies": {
"abbrev": "^2.0.0",
"archy": "^1.0.0",
"bluebird": "^3.7.2",
"hexo-cli": "^4.3.2",
"hexo-front-matter": "^4.2.1",
"hexo-fs": "^4.1.3",
"hexo-i18n": "^2.0.0",
"hexo-log": "^4.0.1",
"hexo-util": "^3.3.0",
"js-yaml": "^4.1.0",
"js-yaml-js-types": "^1.0.0",
"micromatch": "^4.0.4",
"moize": "^6.1.6",
"moment": "^2.29.1",
"moment-timezone": "^0.5.34",
"nunjucks": "^3.2.3",
"picocolors": "^1.0.0",
"pretty-hrtime": "^1.0.3",
"resolve": "^1.22.0",
"strip-ansi": "^6.0.0",
"text-table": "^0.2.0",
"tildify": "^2.0.0",
"titlecase": "^1.1.3",
"warehouse": "^5.0.1"
},
"devDependencies": {
"0x": "^5.1.2",
"@types/abbrev": "^1.1.3",
"@types/bluebird": "^3.5.37",
"@types/chai": "^4.3.11",
"@types/js-yaml": "^4.0.9",
"@types/mocha": "^10.0.6",
"@types/node": "^18.11.8 <18.19.9",
"@types/nunjucks": "^3.2.2",
"@types/rewire": "^2.5.30",
"@types/sinon": "^17.0.3",
"@types/text-table": "^0.2.4",
"c8": "^9.0.0",
"chai": "^4.3.6",
"cheerio": "0.22.0",
"decache": "^4.6.1",
"eslint": "^8.48.0",
"eslint-config-hexo": "^5.0.0",
"hexo-renderer-marked": "^6.0.0",
"husky": "^8.0.1",
"lint-staged": "^15.2.0",
"mocha": "^10.0.0",
"rewire": "^7.0.0",
"sinon": "^17.0.1",
"ts-node": "^10.9.1",
"typescript": "^5.3.2"
},
"engines": {
"node": ">=14"
}
}

Hexo 的入口文件是 dist/hexo/index.js,我们来看看这个文件:

部分代码因为太长了,所以先注释掉,后续会拿出来说。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
const bluebird_1 = __importDefault(require("bluebird"));
const path_1 = require("path");
const tildify_1 = __importDefault(require("tildify"));
const warehouse_1 = __importDefault(require("warehouse"));
const picocolors_1 = require("picocolors");
const events_1 = require("events");
const hexo_fs_1 = require("hexo-fs");
const module_1 = __importDefault(require("module"));
const vm_1 = require("vm");
const { version } = require('../../package.json');
const hexo_log_1 = __importDefault(require("hexo-log"));
const extend_1 = require("../extend");
const render_1 = __importDefault(require("./render"));
const register_models_1 = __importDefault(require("./register_models"));
const post_1 = __importDefault(require("./post"));
const scaffold_1 = __importDefault(require("./scaffold"));
const source_1 = __importDefault(require("./source"));
const router_1 = __importDefault(require("./router"));
const theme_1 = __importDefault(require("../theme"));
const locals_1 = __importDefault(require("./locals"));
const default_config_1 = __importDefault(require("./default_config"));
const load_database_1 = __importDefault(require("./load_database"));
const multi_config_path_1 = __importDefault(require("./multi_config_path"));
const hexo_util_1 = require("hexo-util");
let resolveSync; // = require('resolve');
const libDir = (0, path_1.dirname)(__dirname);
const dbVersion = 1;
const stopWatcher = (box) => { if (box.isWatching())
box.unwatch(); };
const routeCache = new WeakMap();
const castArray = (obj) => { return Array.isArray(obj) ? obj : [obj]; };
const mergeCtxThemeConfig = (ctx) => { ... };
const createLoadThemeRoute = function (generatorResult, locals, ctx) { ... };
function debounce(func, wait) { ... }
class Hexo extends events_1.EventEmitter { ... }
Hexo.lib_dir = libDir + path_1.sep;
Hexo.prototype.lib_dir = Hexo.lib_dir;
Hexo.core_dir = (0, path_1.dirname)(libDir) + path_1.sep;
Hexo.prototype.core_dir = Hexo.core_dir;
Hexo.version = version;
Hexo.prototype.version = Hexo.version;
module.exports = Hexo;
//# sourceMappingURL=index.js.map

Hexo

重点来看Hexo类的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Hexo extends events_1.EventEmitter {
constructor(base = process.cwd(), args = {}) { ... }
_bindLocals() { ... }
init() { ... }
call(name, args, callback) { ... }
model(name, schema) { ... }
resolvePlugin(name, basedir) { ... }
loadPlugin(path, callback) { ... }
_showDrafts() { ... }
load(callback) { ... }
watch(callback) { ... }
unwatch() { ... }
_generateLocals() { ... }
_runGenerators() { ... }
_routerRefresh(runningGenerators, useCache) { ... }
_generate(options = {}) { ... }
exit(err) { ... }
execFilter(type, data, options) { ... }
execFilterSync(type, data, options) { ... }
}

构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
constructor(base = process.cwd(), args = {}) {
super();
this.base_dir = base + path_1.sep;
this.public_dir = (0, path_1.join)(base, 'public') + path_1.sep;
this.source_dir = (0, path_1.join)(base, 'source') + path_1.sep;
this.plugin_dir = (0, path_1.join)(base, 'node_modules') + path_1.sep;
this.script_dir = (0, path_1.join)(base, 'scripts') + path_1.sep;
this.scaffold_dir = (0, path_1.join)(base, 'scaffolds') + path_1.sep;
this.theme_dir = (0, path_1.join)(base, 'themes', default_config_1.default.theme) + path_1.sep;
this.theme_script_dir = (0, path_1.join)(this.theme_dir, 'scripts') + path_1.sep;
this.env = {
args,
debug: Boolean(args.debug),
safe: Boolean(args.safe),
silent: Boolean(args.silent),
env: process.env.NODE_ENV || 'development',
version,
cmd: args._ ? args._[0] : '',
init: false
};
this.extend = {
console: new extend_1.Console(),
deployer: new extend_1.Deployer(),
filter: new extend_1.Filter(),
generator: new extend_1.Generator(),
helper: new extend_1.Helper(),
highlight: new extend_1.Highlight(),
injector: new extend_1.Injector(),
migrator: new extend_1.Migrator(),
processor: new extend_1.Processor(),
renderer: new extend_1.Renderer(),
tag: new extend_1.Tag()
};
this.config = Object.assign({}, default_config_1.default);
this.log = (0, hexo_log_1.default)(this.env);
this.render = new render_1.default(this);
this.route = new router_1.default();
this.post = new post_1.default(this);
this.scaffold = new scaffold_1.default(this);
this._dbLoaded = false;
this._isGenerating = false;
const dbPath = args.output || base;
if (/^(init|new|g|publish|s|deploy|render|migrate)/.test(this.env.cmd)) {
this.log.d(`Writing database to ${(0, path_1.join)(dbPath, 'db.json')}`);
}
this.database = new warehouse_1.default({
version: dbVersion,
path: (0, path_1.join)(dbPath, 'db.json')
});
const mcp = (0, multi_config_path_1.default)(this);
this.config_path = args.config ? mcp(base, args.config, args.output)
: (0, path_1.join)(base, '_config.yml');
(0, register_models_1.default)(this);
this.source = new source_1.default(this);
this.theme = new theme_1.default(this);
this.locals = new locals_1.default();
this._bindLocals();
}

路径

1
2
3
4
5
6
7
8
this.base_dir = base + path_1.sep;
this.public_dir = (0, path_1.join)(base, 'public') + path_1.sep;
this.source_dir = (0, path_1.join)(base, 'source') + path_1.sep;
this.plugin_dir = (0, path_1.join)(base, 'node_modules') + path_1.sep;
this.script_dir = (0, path_1.join)(base, 'scripts') + path_1.sep;
this.scaffold_dir = (0, path_1.join)(base, 'scaffolds') + path_1.sep;
this.theme_dir = (0, path_1.join)(base, 'themes', default_config_1.default.theme) + path_1.sep;
this.theme_script_dir = (0, path_1.join)(this.theme_dir, 'scripts') + path_1.sep;
  • base_dir:项目的基础目录
  • public_dir:项目生成的静态文件存放的目录(./public),当运行hexo generate时生成的所有静态文件都会被放在这个目录下
  • source_dir:Markdown文章(./source
  • plugin_dir:插件(./node_modules),Hexo的插件机制基于Node.js的模块系统
  • script_dir:脚本(./scripts),Hexo启动时会自动执行这些JavaScript文件
  • scaffold_dir:脚手架(./scaffolds)是一种模板,可以用它快速创建新的文章
  • theme_dir:主题(./themes
    • theme_script_dir:类似于脚本(./themes/scripts

环境信息

1
2
3
4
5
6
7
8
9
10
this.env = {
args,
debug: Boolean(args.debug),
safe: Boolean(args.safe),
silent: Boolean(args.silent),
env: process.env.NODE_ENV || 'development',
version,
cmd: args._ ? args._[0] : '',
init: false
};

包含了关于 Hexo 运行环境的信息,如调试模式、安全模式、静默模式、环境变量、版本号、命令、是否初始化等。

扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
this.extend = {
console: new extend_1.Console(),
deployer: new extend_1.Deployer(),
filter: new extend_1.Filter(),
generator: new extend_1.Generator(),
helper: new extend_1.Helper(),
highlight: new extend_1.Highlight(),
injector: new extend_1.Injector(),
migrator: new extend_1.Migrator(),
processor: new extend_1.Processor(),
renderer: new extend_1.Renderer(),
tag: new extend_1.Tag()
};

这些是 Hexo 的核心组件,以后会详细介绍。

其实可以先行阅读 Hexo 官方文档,虽然初次看可能很难看懂,但是看了总是会在未来的某个时刻头脑一亮埋下伏笔。

配置

1
this.config = Object.assign({}, default_config_1.default);

默认配置的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
"use strict";
module.exports = {
title: 'Hexo',
subtitle: '',
description: '',
author: 'John Doe',
language: 'en',
timezone: '',

url: 'http://example.com',
root: '/',
permalink: ':year/:month/:day/:title/',
permalink_defaults: {},
pretty_urls: {
trailing_index: true,
trailing_html: true
},

source_dir: 'source',
public_dir: 'public',
tag_dir: 'tags',
archive_dir: 'archives',
category_dir: 'categories',
code_dir: 'downloads/code',
i18n_dir: ':lang',
skip_render: [],

new_post_name: ':title.md',
default_layout: 'post',
titlecase: false,
external_link: {
enable: true,
field: 'site',
exclude: ''
},
filename_case: 0,
render_drafts: false,
post_asset_folder: false,
relative_link: false,
future: true,
syntax_highlighter: 'highlight.js',
highlight: {
auto_detect: false,
line_number: true,
tab_replace: '',
wrap: true,
exclude_languages: [],
language_attr: false,
hljs: false,
line_threshold: 0,
first_line_number: 'always1',
strip_indent: true
},
prismjs: {
preprocess: true,
line_number: true,
tab_replace: '',
exclude_languages: [],
strip_indent: true
},

default_category: 'uncategorized',
category_map: {},
tag_map: {},

date_format: 'YYYY-MM-DD',
time_format: 'HH:mm:ss',
updated_option: 'mtime',

per_page: 10,
pagination_dir: 'page',

theme: 'landscape',
server: {
cache: false
},

deploy: {},

ignore: [],

meta_generator: true
};
//# sourceMappingURL=default_config.js.map

看着很熟悉吧,这些配置项都是我们初始化 Hexo 项目时,出现在根目录的 _config.yml 内的配置项。

可见 Hexo 官方文档

日志

1
this.log = (0, hexo_log_1.default)(this.env);

这里的 hexo_log_1 是:

1
const hexo_log_1 = __importDefault(require("hexo-log"));

hexojs/hexo-log - GitHub

index.js 中大量的导入变量都是这样命名的。

渲染

1
this.render = new render_1.default(this);

可见 Hexo 官方文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
const path_1 = require("path");
const bluebird_1 = __importDefault(require("bluebird"));
const hexo_fs_1 = require("hexo-fs");
const getExtname = (str) => {
if (typeof str !== 'string')
return '';
const ext = (0, path_1.extname)(str);
return ext.startsWith('.') ? ext.slice(1) : ext;
};
const toString = (result, options) => {
if (!Object.prototype.hasOwnProperty.call(options, 'toString') || typeof result === 'string')
return result;
if (typeof options.toString === 'function') {
return options.toString(result);
}
else if (typeof result === 'object') {
return JSON.stringify(result);
}
else if (result.toString) {
return result.toString();
}
return result;
};
class Render {
constructor(ctx) {
this.context = ctx;
this.renderer = ctx.extend.renderer;
}
isRenderable(path) {
return this.renderer.isRenderable(path);
}
isRenderableSync(path) {
return this.renderer.isRenderableSync(path);
}
getOutput(path) {
return this.renderer.getOutput(path);
}
getRenderer(ext, sync) {
return this.renderer.get(ext, sync);
}
getRendererSync(ext) {
return this.getRenderer(ext, true);
}
render(data, options, callback) {
if (!callback && typeof options === 'function') {
callback = options;
options = {};
}
const ctx = this.context;
let ext = '';
let promise;
if (!data)
return bluebird_1.default.reject(new TypeError('No input file or string!'));
if (data.text != null) {
promise = bluebird_1.default.resolve(data.text);
}
else if (!data.path) {
return bluebird_1.default.reject(new TypeError('No input file or string!'));
}
else {
promise = (0, hexo_fs_1.readFile)(data.path);
}
return promise.then(text => {
data.text = text;
ext = data.engine || getExtname(data.path);
if (!ext || !this.isRenderable(ext))
return text;
const renderer = this.getRenderer(ext);
return Reflect.apply(renderer, ctx, [data, options]);
}).then(result => {
result = toString(result, data);
if (data.onRenderEnd) {
return data.onRenderEnd(result);
}
return result;
}).then(result => {
const output = this.getOutput(ext) || ext;
return ctx.execFilter(`after_render:${output}`, result, {
context: ctx,
args: [data]
});
}).asCallback(callback);
}
renderSync(data, options = {}) {
if (!data)
throw new TypeError('No input file or string!');
const ctx = this.context;
if (data.text == null) {
if (!data.path)
throw new TypeError('No input file or string!');
data.text = (0, hexo_fs_1.readFileSync)(data.path);
}
if (data.text == null)
throw new TypeError('No input file or string!');
const ext = data.engine || getExtname(data.path);
let result;
if (ext && this.isRenderableSync(ext)) {
const renderer = this.getRendererSync(ext);
result = Reflect.apply(renderer, ctx, [data, options]);
}
else {
result = data.text;
}
const output = this.getOutput(ext) || ext;
result = toString(result, data);
if (data.onRenderEnd) {
result = data.onRenderEnd(result);
}
return ctx.execFilterSync(`after_render:${output}`, result, {
context: ctx,
args: [data]
});
}
}
module.exports = Render;
//# sourceMappingURL=render.js.map

bluebird 库是一个流行的 JavaScript Promise 库,它提供了一种更加健壮、高效和优雅的方式来处理异步操作。

在传统的基于回调函数的异步编程中,存在着回调地狱、代码耦合等问题。Promise 的出现解决了这些问题,使着异步代码更加易于理解和维护。而 bluebird 在 Promise 的基础上提供了更多的增强功能和优化。

petkaantonov/bluebird - GitHub

Render 类包含两个主要方法:renderrenderSync

  1. render 方法是一个异步方法,首先检查文件是否可根据其扩展名进行渲染。如果可以,它会获取相应的渲染器并将其应用于数据。渲染后的结果将通过一个过滤器,并在回调函数中返回。
  2. renderSync 方法是 render 的同步版本。工作方式类似,但使用同步文件读取,并使用异常代替回调和 Promise 处理错误。

其他部分包括:

  • getExtname 是一个辅助函数,用于从文件路径中提取文件扩展名。
  • toString 是一个辅助函数,根据提供的选项将结果对象转换为字符串表示。
  • Render 类的构造函数接收 Hexo 上下文 ctx,并从中初始化 renderer 属性。
  • isRenderableisRenderableSync 方法检查给定扩展名的文件是否可渲染。
  • getOutput 方法获取给定输入扩展名的输出扩展名。
  • getRenderergetRendererSync 方法获取给定扩展名的相应渲染器。

这段代码为 Hexo 提供了一种基于文件扩展名渲染文件的方式,使用每种扩展名对应的渲染器。

路由

1
this.route = new router_1.default();

这行代码实例化了 Router 类的对象,用于管理路由。在 Hexo 官方文档中,路由是 存储了网站中所用到的所有路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
const events_1 = require("events");
const bluebird_1 = __importDefault(require("bluebird"));
const stream_1 = __importDefault(require("stream"));
const { Readable } = stream_1.default;
class RouteStream extends Readable {
constructor(data) {
super({ objectMode: true });
this._data = data.data;
this._ended = false;
this.modified = data.modified;
}
// Assume we only accept Buffer, plain object, or string
_toBuffer(data) {
if (data instanceof Buffer) {
return data;
}
if (typeof data === 'object') {
data = JSON.stringify(data);
}
if (typeof data === 'string') {
return Buffer.from(data); // Assume string is UTF-8 encoded string
}
return null;
}
_read() {
const data = this._data;
if (typeof data !== 'function') {
const bufferData = this._toBuffer(data);
if (bufferData) {
this.push(bufferData);
}
this.push(null);
return;
}
// Don't read it twice!
if (this._ended)
return false;
this._ended = true;
data().then(data => {
if (data instanceof stream_1.default && data.readable) {
data.on('data', d => {
this.push(d);
});
data.on('end', () => {
this.push(null);
});
data.on('error', err => {
this.emit('error', err);
});
}
else {
const bufferData = this._toBuffer(data);
if (bufferData) {
this.push(bufferData);
}
this.push(null);
}
}).catch(err => {
this.emit('error', err);
this.push(null);
});
}
}
const _format = (path) => {
path = path || '';
if (typeof path !== 'string')
throw new TypeError('path must be a string!');
path = path
.replace(/^\/+/, '') // Remove prefixed slashes
.replace(/\\/g, '/') // Replaces all backslashes
.replace(/\?.*$/, ''); // Remove query string
// Appends `index.html` to the path with trailing slash
if (!path || path.endsWith('/')) {
path += 'index.html';
}
return path;
};
class Router extends events_1.EventEmitter {
constructor() {
super();
this.routes = {};
}
list() {
const { routes } = this;
return Object.keys(routes).filter(key => routes[key]);
}
format(path) {
return _format(path);
}
get(path) {
if (typeof path !== 'string')
throw new TypeError('path must be a string!');
const data = this.routes[this.format(path)];
if (data == null)
return;
return new RouteStream(data);
}
isModified(path) {
if (typeof path !== 'string')
throw new TypeError('path must be a string!');
const data = this.routes[this.format(path)];
return data ? data.modified : false;
}
set(path, data) {
if (typeof path !== 'string')
throw new TypeError('path must be a string!');
if (data == null)
throw new TypeError('data is required!');
let obj;
if (typeof data === 'object' && data.data != null) {
obj = data;
}
else {
obj = {
data,
modified: true
};
}
if (typeof obj.data === 'function') {
if (obj.data.length) {
obj.data = bluebird_1.default.promisify(obj.data);
}
else {
obj.data = bluebird_1.default.method(obj.data);
}
}
path = this.format(path);
this.routes[path] = {
data: obj.data,
modified: obj.modified == null ? true : obj.modified
};
this.emit('update', path);
return this;
}
remove(path) {
if (typeof path !== 'string')
throw new TypeError('path must be a string!');
path = this.format(path);
this.routes[path] = null;
this.emit('remove', path);
return this;
}
}
module.exports = Router;
//# sourceMappingURL=router.js.map

这段代码定义了一个名为 Router 的类和一个名为 RouteStream 的类。它们在 Hexo 框架中用于管理路由和从路由读取数据流。

RouteStream 类:

  • 继承自 Node.js 内置的 stream.Readable 类,因此它是一个可读流。
  • 构造函数接收一个 data 对象,该对象包含 data 属性和 modified 属性。
  • _read() 方法是可读流的实现。如果 data 是一个函数,它会尝试从该函数中获取数据。否则,它会将 data 推送到流中。
  • _toBuffer() 是一个内部方法,用于将数据转换为 Buffer

Router 类:

  • 继承自 EventEmitter,因此可以发射和监听事件。
  • routes 属性是一个对象,用于存储路径及其对应的数据。
  • list() 方法返回所有已注册路径的列表。
  • format(path) 方法用于格式化路径,移除前导斜杠、替换反斜杠,并为以斜杠结尾的路径添加 index.html
  • get(path) 方法用于获取给定路径的数据流。如果路径不存在或数据为 null,则返回 undefined
  • isModified(path) 方法用于检查给定路径的数据是否已修改。
  • set(path, data) 方法用于设置给定路径的数据。如果 data 是一个函数,它会使用 Bluebird 库将其转换为 Promise。该方法还会触发 update 事件。
  • remove(path) 方法用于移除给定路径的数据。它会将路径对应的值设置为 null,并触发 remove 事件。

Node.js 的 EventEmitter 是一个模块,提供基于事件驱动的编程方式,是 Node.js 很多核心模块和第三方模块的基础,也是 Node.js 中非常重要的一个概念和设计模式。

EventEmitter 是一个构造函数,可以创建一个新的事件发射器对象。这个对象可以发射命名事件,并且可以通过添加事件监听器来监听特定事件。当事件被发射时,所有监听该事件的函数回调都会被同步地调用。

EventEmitter 对象有以下常用方法:

  1. emitter.on(eventName, listener) 为指定事件注册一个监听器。
  2. emitter.once(eventName, listener) 为指定事件注册一个一次性的监听器,触发后就会被移除。
  3. emitter.off(eventName, listener) 移除指定事件的监听器。
  4. emitter.emit(eventName, [...args]) 发射指定事件,传递参数给监听器回调函数。

Hexo 官方文档 中也提到了 EventEmitter

博客文章

1
this.post = new post_1.default(this);

可见 Hexo 官方文档

post.js 很长,因此将部分代码分开讲解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
const assert_1 = __importDefault(require("assert"));
const moment_1 = __importDefault(require("moment"));
const bluebird_1 = __importDefault(require("bluebird"));
const path_1 = require("path");
const picocolors_1 = require("picocolors");
const js_yaml_1 = require("js-yaml");
const hexo_util_1 = require("hexo-util");
const hexo_fs_1 = require("hexo-fs");
const hexo_front_matter_1 = require("hexo-front-matter");
const preservedKeys = ['title', 'slug', 'path', 'layout', 'date', 'content'];
const rHexoPostRenderEscape = /<hexoPostRenderCodeBlock>([\s\S]+?)<\/hexoPostRenderCodeBlock>/g;
const rSwigPlaceHolder = /(?:<|&lt;)!--swig\uFFFC(\d+)--(?:>|&gt;)/g;
const rCodeBlockPlaceHolder = /(?:<|&lt;)!--code\uFFFC(\d+)--(?:>|&gt;)/g;
const STATE_PLAINTEXT = Symbol('plaintext');
const STATE_SWIG_VAR = Symbol('swig_var');
const STATE_SWIG_COMMENT = Symbol('swig_comment');
const STATE_SWIG_TAG = Symbol('swig_tag');
const STATE_SWIG_FULL_TAG = Symbol('swig_full_tag');
const isNonWhiteSpaceChar = (char) => char !== '\r'
&& char !== '\n'
&& char !== '\t'
&& char !== '\f'
&& char !== '\v'
&& char !== ' ';

PostRenderEscape 类用于处理 Swig/Nunjucks 标签和代码块的转义和还原:

Swig 和 Nunjucks 是两种流行的模板引擎。

主要语法包括:

  • {% ... %}:用于执行语句,如条件语句和循环语句。
  • {{ ... }}:用于输出变量值。

在渲染文章内容时,Hexo 需要先对 Swig/Nunjucks 标签和代码块进行转义,防止被误解析,然后再使用相应的渲染器(如 Markdown)进行渲染。渲染完成后,需要将转义的内容还原回来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
class PostRenderEscape {
constructor() {
this.stored = [];
}
static escapeContent(cache, flag, str) {
return `<!--${flag}\uFFFC${cache.push(str) - 1}-->`;
}
static restoreContent(cache) {
return (_, index) => {
(0, assert_1.default)(cache[index]);
const value = cache[index];
cache[index] = null;
return value;
};
}
restoreAllSwigTags(str) {
const restored = str.replace(rSwigPlaceHolder, PostRenderEscape.restoreContent(this.stored));
return restored;
}
restoreCodeBlocks(str) {
return str.replace(rCodeBlockPlaceHolder, PostRenderEscape.restoreContent(this.stored));
}
escapeCodeBlocks(str) {
return str.replace(rHexoPostRenderEscape, (_, content) => PostRenderEscape.escapeContent(this.stored, 'code', content));
}
/**
* @param {string} str
* @returns string
*/
escapeAllSwigTags(str) {
if (!/(\{\{.+?\}\})|(\{#.+?#\})|(\{%.+?%\})/s.test(str)) {
return str;
}
let state = STATE_PLAINTEXT;
let buffer = '';
let output = '';
let swig_tag_name_begin = false;
let swig_tag_name_end = false;
let swig_tag_name = '';
let swig_full_tag_start_buffer = '';
const { length } = str;
for (let idx = 0; idx < length; idx++) {
const char = str[idx];
const next_char = str[idx + 1];
if (state === STATE_PLAINTEXT) { // From plain text to swig
if (char === '{') {
// check if it is a complete tag {{ }}
if (next_char === '{') {
state = STATE_SWIG_VAR;
idx++;
}
else if (next_char === '#') {
state = STATE_SWIG_COMMENT;
idx++;
}
else if (next_char === '%') {
state = STATE_SWIG_TAG;
idx++;
swig_tag_name = '';
swig_full_tag_start_buffer = '';
swig_tag_name_begin = false; // Mark if it is the first non white space char in the swig tag
swig_tag_name_end = false;
}
else {
output += char;
}
}
else {
output += char;
}
}
else if (state === STATE_SWIG_TAG) {
if (char === '%' && next_char === '}') { // From swig back to plain text
idx++;
if (swig_tag_name !== '' && str.includes(`end${swig_tag_name}`)) {
state = STATE_SWIG_FULL_TAG;
}
else {
swig_tag_name = '';
state = STATE_PLAINTEXT;
output += PostRenderEscape.escapeContent(this.stored, 'swig', `{%${buffer}%}`);
}
buffer = '';
}
else {
buffer = buffer + char;
swig_full_tag_start_buffer = swig_full_tag_start_buffer + char;
if (isNonWhiteSpaceChar(char)) {
if (!swig_tag_name_begin && !swig_tag_name_end) {
swig_tag_name_begin = true;
}
if (swig_tag_name_begin) {
swig_tag_name += char;
}
}
else {
if (swig_tag_name_begin === true) {
swig_tag_name_begin = false;
swig_tag_name_end = true;
}
}
}
}
else if (state === STATE_SWIG_VAR) {
if (char === '}' && next_char === '}') {
idx++;
state = STATE_PLAINTEXT;
output += PostRenderEscape.escapeContent(this.stored, 'swig', `{{${buffer}}}`);
buffer = '';
}
else {
buffer = buffer + char;
}
}
else if (state === STATE_SWIG_COMMENT) { // From swig back to plain text
if (char === '#' && next_char === '}') {
idx++;
state = STATE_PLAINTEXT;
buffer = '';
}
}
else if (state === STATE_SWIG_FULL_TAG) {
if (char === '{' && next_char === '%') {
let swig_full_tag_end_buffer = '';
let _idx = idx + 2;
for (; _idx < length; _idx++) {
const _char = str[_idx];
const _next_char = str[_idx + 1];
if (_char === '%' && _next_char === '}') {
_idx++;
break;
}
swig_full_tag_end_buffer = swig_full_tag_end_buffer + _char;
}
if (swig_full_tag_end_buffer.includes(`end${swig_tag_name}`)) {
state = STATE_PLAINTEXT;
output += PostRenderEscape.escapeContent(this.stored, 'swig', `{%${swig_full_tag_start_buffer}%}${buffer}{%${swig_full_tag_end_buffer}%}`);
idx = _idx;
swig_full_tag_start_buffer = '';
swig_full_tag_end_buffer = '';
buffer = '';
}
else {
buffer += char;
}
}
else {
buffer += char;
}
}
}
return output;
}
}
  • escapeContent:用于将给定的字符串转义为占位符,并存储在缓存数组中。
    • 转义后的占位符格式为:<!--flag\uFFFC${index}-->
  • restoreContent:用于将占位符还原为原始字符串。
  • restoreAllSwigTags:使用正则表达式替换所有 Swig 标签占位符,调用 restoreContent 方法还原原始内容。
  • restoreCodeBlocks:使用正则表达式替换所有代码块占位符,调用 restoreContent 方法还原原始内容。
  • escapeCodeBlocks:使用正则表达式匹配所有 Markdown 代码块,调用 escapeContent 方法将它们转义为占位符。
  • escapeAllSwigTags:用于转义 Swig 模板标签。首先检查输入字符串是否包含 Swig 标签,然后使用有限状态机的方式遍历每个字符,识别出 Swig 标签的类型和内容,并调用 escapeContent 进行转义。

有限状态机(Finite State Machine;FSM)是一种数学计算模型,用于描述具有有限个状态以及基于事件进行状态转移的系统。

一个有限状态机包含以下几个基本组成部分:

  • 有限状态集合(States)。
  • 输入事件/符号集合(Input Events/Symbols)。
  • 一个初始状态(Initial State)。
  • 状态转移函数(State Transition Function)。
  • 终止状态集合(Final States)。

有限状态机的工作原理:

  1. 机器初始处于一个确定的初始状态。
  2. 机器接收一个输入事件/符号。
  3. 根据当前状态和输入事件/符号,机器按照状态转移函数进行状态转移。
  4. 机器进入新的状态,等待下一个输入事件/符号。
  5. 重复以上过程,直到进入某个终止状态或发生无法处理的输入事件/符号。

escapeAllSwigTags 使用了一个有限状态机来精确识别和处理各种 Swig 标签。它定义了五种状态,通过遍历输入字符串,根据当前状态和字符进行状态转移,从而正确处理嵌套、注释等复杂情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const prepareFrontMatter = (data, jsonMode) => {
for (const [key, item] of Object.entries(data)) {
if (moment_1.default.isMoment(item)) {
data[key] = item.utc().format('YYYY-MM-DD HH:mm:ss');
}
else if (moment_1.default.isDate(item)) {
data[key] = moment_1.default.utc(item).format('YYYY-MM-DD HH:mm:ss');
}
else if (typeof item === 'string') {
if (jsonMode || item.includes(':') || item.startsWith('#') || item.startsWith('!!')
|| item.includes('{') || item.includes('}') || item.includes('[') || item.includes(']')
|| item.includes('\'') || item.includes('"'))
data[key] = `"${item.replace(/"/g, '\\"')}"`;
}
}
return data;
};
const removeExtname = (str) => {
return str.substring(0, str.length - (0, path_1.extname)(str).length);
};
const createAssetFolder = (path, assetFolder) => {
if (!assetFolder)
return bluebird_1.default.resolve();
const target = removeExtname(path);
if ((0, path_1.basename)(target) === 'index')
return bluebird_1.default.resolve();
return (0, hexo_fs_1.exists)(target).then(exist => {
if (!exist)
return (0, hexo_fs_1.mkdirs)(target);
});
};
  • prepareFrontMatter 方法用于处理文章的 Front Matter 元数据。

    Front Matter 是指在 Markdown 或其他 markup 文件的头部添加的一组元数据。它通常被包裹在两组连续的三短横线之间,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    ---
    title: My Blog Post
    date: 2023-05-26
    tags: [blog, coding]
    ---

    # My Blog Post

    This is the content of the blog post...

    Front Matter通常采用 YAML 或 JSON 格式来表示键值对的元数据。Front Matter 中的数据可以在渲染 markdown 文件时被解析和使用,例如在博客系统中用于生成文章的元数据、URL等。不同的静态站点生成器和 markdown 渲染器支持不同的 Front Matter 语法和元数据项。

    它遍历 data 对象的每个键值对。如果值是 moment 对象或 Date 对象,则将其转换为 UTC 时区的 YYYY-MM-DD HH:mm:ss 格式的字符串。如果值是字符串,并且符合某些条件(包含特殊字符或需要转义),则将其用双引号包裹起来,并对双引号进行转义。

    这个函数主要用于在生成文章时正确处理 Front Matter 中的日期和特殊字符。

  • removeExtname 方法用于从文件路径中移除扩展名。它使用 path.extname 获取扩展名,然后从原始路径中截取并返回不包含扩展名的部分。

  • createAssetFolder 方法用于在生成文章时创建与文章相关联的资源文件夹(如果配置启用了该选项)。

    • 如果 assetFolderfalse,则直接返回一个已解决的 Promise。否则它会调用 removeExtname 来获取不包含扩展名的文件路径。
    • 如果这个路径的基础名称是 index,则直接返回一个已解决的 Promise(因为 index 通常是默认文件名,不需要创建文件夹)。否则,它会检查该路径是否存在,如果不存在,则创建该路径作为文件夹。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
class Post {
constructor(context) {
this.context = context;
}
create(data, replace, callback) {
if (!callback && typeof replace === 'function') {
callback = replace;
replace = false;
}
const ctx = this.context;
const { config } = ctx;
data.slug = (0, hexo_util_1.slugize)((data.slug || data.title).toString(), { transform: config.filename_case });
data.layout = (data.layout || config.default_layout).toLowerCase();
data.date = data.date ? (0, moment_1.default)(data.date) : (0, moment_1.default)();
return bluebird_1.default.all([
// Get the post path
ctx.execFilter('new_post_path', data, {
args: [replace],
context: ctx
}),
this._renderScaffold(data)
]).spread((path, content) => {
const result = { path, content };
return bluebird_1.default.all([
// Write content to file
(0, hexo_fs_1.writeFile)(path, content),
// Create asset folder
createAssetFolder(path, config.post_asset_folder)
]).then(() => {
ctx.emit('new', result);
return result;
});
}).asCallback(callback);
}
_getScaffold(layout) {
const ctx = this.context;
return ctx.scaffold.get(layout).then(result => {
if (result != null)
return result;
return ctx.scaffold.get('normal');
});
}
_renderScaffold(data) {
const { tag } = this.context.extend;
let splitted;
return this._getScaffold(data.layout).then(scaffold => {
splitted = (0, hexo_front_matter_1.split)(scaffold);
const jsonMode = splitted.separator.startsWith(';');
const frontMatter = prepareFrontMatter(Object.assign({}, data), jsonMode);
return tag.render(splitted.data, frontMatter);
}).then(frontMatter => {
const { separator } = splitted;
const jsonMode = separator.startsWith(';');
// Parse front-matter
const obj = jsonMode ? JSON.parse(`{${frontMatter}}`) : (0, js_yaml_1.load)(frontMatter);
Object.keys(data)
.filter(key => !preservedKeys.includes(key) && obj[key] == null)
.forEach(key => {
obj[key] = data[key];
});
let content = '';
// Prepend the separator
if (splitted.prefixSeparator)
content += `${separator}\n`;
content += (0, hexo_front_matter_1.stringify)(obj, {
mode: jsonMode ? 'json' : ''
});
// Concat content
content += splitted.content;
if (data.content) {
content += `\n${data.content}`;
}
return content;
});
}
publish(data, replace, callback) {
if (!callback && typeof replace === 'function') {
callback = replace;
replace = false;
}
if (data.layout === 'draft')
data.layout = 'post';
const ctx = this.context;
const { config } = ctx;
const draftDir = (0, path_1.join)(ctx.source_dir, '_drafts');
const slug = (0, hexo_util_1.slugize)(data.slug.toString(), { transform: config.filename_case });
data.slug = slug;
const regex = new RegExp(`^${(0, hexo_util_1.escapeRegExp)(slug)}(?:[^\\/\\\\]+)`);
let src = '';
const result = {};
data.layout = (data.layout || config.default_layout).toLowerCase();
// Find the draft
return (0, hexo_fs_1.listDir)(draftDir).then(list => {
const item = list.find(item => regex.test(item));
if (!item)
throw new Error(`Draft "${slug}" does not exist.`);
// Read the content
src = (0, path_1.join)(draftDir, item);
return (0, hexo_fs_1.readFile)(src);
}).then(content => {
// Create post
Object.assign(data, (0, hexo_front_matter_1.parse)(content));
data.content = data._content;
data._content = undefined;
return this.create(data, replace);
}).then(post => {
result.path = post.path;
result.content = post.content;
return (0, hexo_fs_1.unlink)(src);
}).then(() => {
if (!config.post_asset_folder)
return;
// Copy assets
const assetSrc = removeExtname(src);
const assetDest = removeExtname(result.path);
return (0, hexo_fs_1.exists)(assetSrc).then(exist => {
if (!exist)
return;
return (0, hexo_fs_1.copyDir)(assetSrc, assetDest).then(() => (0, hexo_fs_1.rmdir)(assetSrc));
});
}).thenReturn(result).asCallback(callback);
}
render(source, data = {}, callback) {
const ctx = this.context;
const { config } = ctx;
const { tag } = ctx.extend;
const ext = data.engine || (source ? (0, path_1.extname)(source) : '');
let promise;
if (data.content != null) {
promise = bluebird_1.default.resolve(data.content);
}
else if (source) {
// Read content from files
promise = (0, hexo_fs_1.readFile)(source);
}
else {
return bluebird_1.default.reject(new Error('No input file or string!')).asCallback(callback);
}
// Files like js and css are also processed by this function, but they do not require preprocessing like markdown
// data.source does not exist when tag plugins call the markdown renderer
const isPost = !data.source || ['html', 'htm'].includes(ctx.render.getOutput(data.source));
if (!isPost) {
return promise.then(content => {
data.content = content;
ctx.log.debug('Rendering file: %s', (0, picocolors_1.magenta)(source));
return ctx.render.render({
text: data.content,
path: source,
engine: data.engine,
toString: true
});
}).then(content => {
data.content = content;
return data;
}).asCallback(callback);
}
// disable Nunjucks when the renderer specify that.
let disableNunjucks = ext && ctx.render.renderer.get(ext) && !!ctx.render.renderer.get(ext).disableNunjucks;
// front-matter overrides renderer's option
if (typeof data.disableNunjucks === 'boolean')
disableNunjucks = data.disableNunjucks;
const cacheObj = new PostRenderEscape();
return promise.then(content => {
data.content = content;
// Run "before_post_render" filters
return ctx.execFilter('before_post_render', data, { context: ctx });
}).then(() => {
data.content = cacheObj.escapeCodeBlocks(data.content);
// Escape all Nunjucks/Swig tags
if (disableNunjucks === false) {
data.content = cacheObj.escapeAllSwigTags(data.content);
}
const options = data.markdown || {};
if (!config.syntax_highlighter)
options.highlight = null;
ctx.log.debug('Rendering post: %s', (0, picocolors_1.magenta)(source));
// Render with markdown or other renderer
return ctx.render.render({
text: data.content,
path: source,
engine: data.engine,
toString: true,
onRenderEnd(content) {
// Replace cache data with real contents
data.content = cacheObj.restoreAllSwigTags(content);
// Return content after replace the placeholders
if (disableNunjucks)
return data.content;
// Render with Nunjucks
return tag.render(data.content, data);
}
}, options);
}).then(content => {
data.content = cacheObj.restoreCodeBlocks(content);
// Run "after_post_render" filters
return ctx.execFilter('after_post_render', data, { context: ctx });
}).asCallback(callback);
}
}
module.exports = Post;
//# sourceMappingURL=post.js.map

Post 类包含了在 Hexo 框架中创建、发布和渲染文章的和新方法。

  • create(data, replace, callback):创建一篇新文章。
    1. 首先根据配置和传入的数据准备文章的 slug、布局和日期。
    2. 然后通过执行 new_post_path 过滤器获取文章路径,并调用 _renderScaffold 方法渲染文章内容。
    3. 最后将渲染后的内容写入文件,并根据配置创建相关的资源文件夹。
  • _getScaffold(layout)_renderScaffold(data)
    • _getScaffold 方法用于获取指定布局的脚手架。
    • _renderScaffold 方法用于渲染脚手架,并将传入的数据合并到模板中。
      • 先解析 Front Matter,然后使用标签插件渲染 Front Matter。最后将渲染后的 Front Matter 和原始内容合并,返回最终的文章内容。
  • publish(data, replace, callback):将草稿文章发布为正式文章。
    1. 首先从 _drafts 目录查找对应的草稿文件,读取其内容。
    2. 然后将草稿内容与传入的数据合并,调用 create 方法创建正式文章。
    3. 最后删除原始的草稿文件,并根据配置复制相关的资源文件夹。
  • render(source, data, callback):渲染文章内容。
    1. 首先读取源文件或使用传入的内容。
    2. 然后执行 before_post_render 过滤器。
    3. 接着对代码块和 Swig/Nunjucks 标签进行转义,使用 Markdown 或其他渲染器渲染文章内容。
    4. 渲染完成后,将转义的内容还原,并执行 after_post_render 过滤器。
    5. 如果禁用了 Nunjucks,则返回渲染后的内容;否则使用标签插件渲染一次。

脚手架

脚手架文件是创建新文章或页面时使用的模板文件。

可见 Hexo 官方文档

1
this.scaffold = new scaffold_1.default(this);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
"use strict";
const path_1 = require("path");
const hexo_fs_1 = require("hexo-fs");
class Scaffold {
constructor(context) {
this.context = context;
this.scaffoldDir = context.scaffold_dir;
this.defaults = {
normal: [
'---',
'layout: {{ layout }}',
'title: {{ title }}',
'date: {{ date }}',
'tags:',
'---'
].join('\n')
};
}
_listDir() {
const { scaffoldDir } = this;
return (0, hexo_fs_1.exists)(scaffoldDir).then(exist => {
if (!exist)
return [];
return (0, hexo_fs_1.listDir)(scaffoldDir, {
ignorePattern: /^_|\/_/
});
}).map(item => ({
name: item.substring(0, item.length - (0, path_1.extname)(item).length),
path: (0, path_1.join)(scaffoldDir, item)
}));
}
_getScaffold(name) {
return this._listDir().then(list => list.find(item => item.name === name));
}
get(name, callback) {
return this._getScaffold(name).then(item => {
if (item) {
return (0, hexo_fs_1.readFile)(item.path);
}
return this.defaults[name];
}).asCallback(callback);
}
set(name, content, callback) {
const { scaffoldDir } = this;
return this._getScaffold(name).then(item => {
let path = item ? item.path : (0, path_1.join)(scaffoldDir, name);
if (!(0, path_1.extname)(path))
path += '.md';
return (0, hexo_fs_1.writeFile)(path, content);
}).asCallback(callback);
}
remove(name, callback) {
return this._getScaffold(name).then(item => {
if (!item)
return;
return (0, hexo_fs_1.unlink)(item.path);
}).asCallback(callback);
}
}
module.exports = Scaffold;
//# sourceMappingURL=scaffold.js.map

Scaffold 类在 Hexo 框架中用于管理 Scaffold 模板文件,也就是脚手架文件。脚手架是用于生成新文章或页面的默认模板。

  • constructor(context):初始化了 scaffoldDir 属性,指定脚手架文件所在的目录。它还定义了一个 defaults 对象,包含了默认的脚手架内容。
  • _listDir():用于列出 scaffoldDir 目录下的所有脚手架文件。它会忽略以下划线开头的文件或目录。
  • _getScaffold(name):根据给定的名称获取对应的脚手架文件信息。
    • 先调用 _listDir() 方法获取所有脚手架文件,然后查找名称匹配的文件。
  • get(name, callback):用于获取指定名称的脚手架内容。
    • 如果找到对应的文件,它会读取并返回文件内容。如果没有找到,它会返回 defaults 对象中对应的默认脚手架内容。
  • set(name, content, callback):用于设置或创建一个新的脚手架文件。
    • 首先检查是否已经存在同名的文件,如果存在就使用原有路径,否则就在 scaffoldDir 目录下创建新文件(文件扩展名默认为 .md)。
  • remove(name, callback):用于删除一个脚手架文件。它会查找对应名称的文件,如果存在则将其删除。

数据库

1
2
3
4
5
6
7
8
this._dbLoaded = false;
this._isGenerating = false;
// If `output` is provided, use that as the
// root for saving the db. Otherwise default to `base`.
const dbPath = args.output || base;
if (/^(init|new|g|publish|s|deploy|render|migrate)/.test(this.env.cmd)) {
this.log.d(`Writing database to ${(0, path_1.join)(dbPath, 'db.json')}`);
}
  1. 初始化两个内部状态标志,用于跟踪数据库是否已经加载(_dbLoaded)和网站是否正在生成(_isGenerating)。
  2. 确定数据库文件将要被保存在的路径。如果执行 Hexo 命令时提供了 output 参数,那么数据库就会被保存在该输出路径下;不然就保存在项目根目录下。
  3. 接着检查当前执行的 Hexo 命令是否属于 initnewgpublishdeployrender 或者 migrate。如果是,就打印说数据库文件被写入到哪里去了。
1
2
3
4
this.database = new warehouse_1.default({
version: dbVersion,
path: (0, path_1.join)(dbPath, 'db.json')
});

Hexo 初始化和配置数据库,实例化过程中传入了一个配置对象:

  • version:数据库的版本号。在 index.js 的最上方能找到:
    1
    const dbVersion = 1;
  • path:数据库文件的保存路径,由先前定义的 dbPath 和文件名 db.json 拼接而成。

此处实例化用到了 warehouse 库。这是一个轻量级的数据存储库,主要用于 Node.js 应用程序中处理 JSON 数据。它提供了一种简单的方式来定义、存储和检索模型数据,类似于一个迷你数据库或 ORM(对象关系映射)。

dundalek/warehouse - GitHub

多配置文件路径管理

1
const mcp = (0, multi_config_path_1.default)(this);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
const path_1 = require("path");
const hexo_fs_1 = require("hexo-fs");
const js_yaml_1 = __importDefault(require("js-yaml"));
const hexo_util_1 = require("hexo-util");
module.exports = (ctx) => function multiConfigPath(base, configPaths, outputDir) {
const { log } = ctx;
const defaultPath = (0, path_1.join)(base, '_config.yml');
if (!configPaths) {
log.w('No config file entered.');
return (0, path_1.join)(base, '_config.yml');
}
let paths;
// determine if comma or space separated
if (configPaths.includes(',')) {
paths = configPaths.replace(' ', '').split(',');
}
else {
// only one config
let configPath = (0, path_1.isAbsolute)(configPaths) ? configPaths : (0, path_1.resolve)(base, configPaths);
if (!(0, hexo_fs_1.existsSync)(configPath)) {
log.w(`Config file ${configPaths} not found, using default.`);
configPath = defaultPath;
}
return configPath;
}
const numPaths = paths.length;
// combine files
let combinedConfig = {};
let count = 0;
for (let i = 0; i < numPaths; i++) {
const configPath = (0, path_1.isAbsolute)(paths[i]) ? paths[i] : (0, path_1.join)(base, paths[i]);
if (!(0, hexo_fs_1.existsSync)(configPath)) {
log.w(`Config file ${paths[i]} not found.`);
continue;
}
// files read synchronously to ensure proper overwrite order
const file = (0, hexo_fs_1.readFileSync)(configPath);
const ext = (0, path_1.extname)(paths[i]).toLowerCase();
if (ext === '.yml') {
combinedConfig = (0, hexo_util_1.deepMerge)(combinedConfig, js_yaml_1.default.load(file));
count++;
}
else if (ext === '.json') {
combinedConfig = (0, hexo_util_1.deepMerge)(combinedConfig, js_yaml_1.default.load(file, { json: true }));
count++;
}
else {
log.w(`Config file ${paths[i]} not supported type.`);
}
}
if (count === 0) {
log.e('No config files found. Using _config.yml.');
return defaultPath;
}
log.i('Config based on', count.toString(), 'files');
const multiconfigRoot = outputDir || base;
const outputPath = (0, path_1.join)(multiconfigRoot, '_multiconfig.yml');
log.d(`Writing _multiconfig.yml to ${outputPath}`);
(0, hexo_fs_1.writeFileSync)(outputPath, js_yaml_1.default.dump(combinedConfig));
// write file and return path
return outputPath;
};
//# sourceMappingURL=multi_config_path.js.map

multiConfigPath 函数用于处理 Hexo 配置文件。该函数会根据传入的配置文件路径来合并多个配置文件并生成一个新的配置文件 _multiconfig.yml

顺带一个读取配置文件路径的参数:

1
2
this.config_path = args.config ? mcp(base, args.config, args.output)
: (0, path_1.join)(base, '_config.yml');

根据用户的命令行输入和 Hexo 项目的配置,决定要读取的配置文件的路径。

注册模型

1
(0, register_models_1.default)(this);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
const models = __importStar(require("../models"));
module.exports = (ctx) => {
const db = ctx.database;
const keys = Object.keys(models);
for (let i = 0, len = keys.length; i < len; i++) {
const key = keys[i];
db.model(key, models[key](ctx));
}
};
//# sourceMappingURL=register_models.js.map

上三个函数是 TypeScript 编译器在编译时生成的,用于处理 ES 模块的导入和默认导出行为。它们确保在使用 CommonJS 模块时,能正确处理 ES 模块的导入。

接下来导入的 models 模块是 hexo/dist/models 目录,这里使用了 __importStar 函数导入 models 模块中的所有导出,作为 models 对象的属性。

models 模块内容很多,后续会仔细讲解。

register_models 模块导出一个函数,该函数接收一个 ctx 参数,并从 ctx 对象中获取数据库实例 db、获取 models 对象的所有键,遍历这些键,为每个模型在数据库中注册一个模型。

主要功能是导入所有的模型,并将这些模型注册到数据库中。

源文件管理

1
this.source = new source_1.default(this);
1
2
3
4
5
6
7
8
9
10
11
12
13
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
const box_1 = __importDefault(require("../box"));
class Source extends box_1.default {
constructor(ctx) {
super(ctx, ctx.source_dir);
this.processors = ctx.extend.processor.list();
}
}
module.exports = Source;
//# sourceMappingURL=source.js.map

Source 类继承自 Box 类,所以先看一眼 Box 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const path_1 = require("path");
const bluebird_1 = __importDefault(require("bluebird"));
const file_1 = __importDefault(require("./file"));
const hexo_util_1 = require("hexo-util");
const hexo_fs_1 = require("hexo-fs");
const picocolors_1 = require("picocolors");
const events_1 = require("events");
const micromatch_1 = require("micromatch");
const defaultPattern = new hexo_util_1.Pattern(() => ({}));
class Box extends events_1.EventEmitter {
constructor(ctx, base, options) {
super();
this.options = Object.assign({
persistent: true,
awaitWriteFinish: {
stabilityThreshold: 200
}
}, options);
if (!base.endsWith(path_1.sep)) {
base += path_1.sep;
}
this.context = ctx;
this.base = base;
this.processors = [];
this._processingFiles = {};
this.watcher = null;
this.Cache = ctx.model('Cache');
this.File = this._createFileClass();
let targets = this.options.ignored || [];
if (ctx.config.ignore && ctx.config.ignore.length) {
targets = targets.concat(ctx.config.ignore);
}
this.ignore = targets;
this.options.ignored = targets.map(s => toRegExp(ctx, s)).filter(x => x);
}
_createFileClass() {
const ctx = this.context;
class _File extends file_1.default {
render(options) {
return ctx.render.render({
path: this.source
}, options);
}
renderSync(options) {
return ctx.render.renderSync({
path: this.source
}, options);
}
}
_File.prototype.box = this;
return _File;
}
addProcessor(pattern, fn) {
if (!fn && typeof pattern === 'function') {
fn = pattern;
pattern = defaultPattern;
}
if (typeof fn !== 'function')
throw new TypeError('fn must be a function');
if (!(pattern instanceof hexo_util_1.Pattern))
pattern = new hexo_util_1.Pattern(pattern);
this.processors.push({
pattern,
process: fn
});
}
_readDir(base, prefix = '') {
const { context: ctx } = this;
const results = [];
return readDirWalker(ctx, base, results, this.ignore, prefix)
.return(results)
.map(path => this._checkFileStatus(path))
.map(file => this._processFile(file.type, file.path).return(file.path));
}
_checkFileStatus(path) {
const { Cache, context: ctx } = this;
const src = (0, path_1.join)(this.base, path);
return Cache.compareFile(src.substring(ctx.base_dir.length), () => getHash(src), () => (0, hexo_fs_1.stat)(src)).then(result => ({
type: result.type,
path
}));
}
process(callback) {
const { base, Cache, context: ctx } = this;
return (0, hexo_fs_1.stat)(base).then(stats => {
if (!stats.isDirectory())
return;
// Check existing files in cache
const relativeBase = base.substring(ctx.base_dir.length);
const cacheFiles = Cache.filter(item => item._id.startsWith(relativeBase)).map(item => item._id.substring(relativeBase.length));
// Handle deleted files
return this._readDir(base)
.then((files) => cacheFiles.filter((path) => !files.includes(path)))
.map((path) => this._processFile(file_1.default.TYPE_DELETE, path));
}).catch(err => {
if (err && err.code !== 'ENOENT')
throw err;
}).asCallback(callback);
}
_processFile(type, path) {
if (this._processingFiles[path]) {
return bluebird_1.default.resolve();
}
this._processingFiles[path] = true;
const { base, File, context: ctx } = this;
this.emit('processBefore', {
type,
path
});
return bluebird_1.default.reduce(this.processors, (count, processor) => {
// patten supports *nix style path only, replace backslashes on Windows
const params = processor.pattern.match(escapeBackslash(path));
if (!params)
return count;
const file = new File({
// source is used for file system path, keep backslashes on Windows
source: (0, path_1.join)(base, path),
// path is used for URL path, replace backslashes on Windows
path: escapeBackslash(path),
params,
type
});
return Reflect.apply(bluebird_1.default.method(processor.process), ctx, [file])
.thenReturn(count + 1);
}, 0).then(count => {
if (count) {
ctx.log.debug('Processed: %s', (0, picocolors_1.magenta)(path));
}
this.emit('processAfter', {
type,
path
});
}).catch(err => {
ctx.log.error({ err }, 'Process failed: %s', (0, picocolors_1.magenta)(path));
}).finally(() => {
this._processingFiles[path] = false;
}).thenReturn(path);
}
watch(callback) {
if (this.isWatching()) {
return bluebird_1.default.reject(new Error('Watcher has already started.')).asCallback(callback);
}
const { base } = this;
function getPath(path) {
return path.substring(base.length);
}
return this.process().then(() => (0, hexo_fs_1.watch)(base, this.options)).then(watcher => {
this.watcher = watcher;
watcher.on('add', path => {
this._processFile(file_1.default.TYPE_CREATE, getPath(path));
});
watcher.on('change', path => {
this._processFile(file_1.default.TYPE_UPDATE, getPath(path));
});
watcher.on('unlink', path => {
this._processFile(file_1.default.TYPE_DELETE, getPath(path));
});
watcher.on('addDir', path => {
let prefix = getPath(path);
if (prefix)
prefix += path_1.sep;
this._readDir(path, prefix);
});
}).asCallback(callback);
}
unwatch() {
if (!this.isWatching())
return;
this.watcher.close();
this.watcher = null;
}
isWatching() {
return Boolean(this.watcher);
}
}
function escapeBackslash(path) {
// Replace backslashes on Windows
return path.replace(/\\/g, '/');
}
function getHash(path) {
const src = (0, hexo_fs_1.createReadStream)(path);
const hasher = (0, hexo_util_1.createSha1Hash)();
const finishedPromise = new bluebird_1.default((resolve, reject) => {
src.once('error', reject);
src.once('end', resolve);
});
src.on('data', chunk => { hasher.update(chunk); });
return finishedPromise.then(() => hasher.digest('hex'));
}
function toRegExp(ctx, arg) {
if (!arg)
return null;
if (typeof arg !== 'string') {
ctx.log.warn('A value of "ignore:" section in "_config.yml" is not invalid (not a string)');
return null;
}
const result = (0, micromatch_1.makeRe)(arg);
if (!result) {
ctx.log.warn('A value of "ignore:" section in "_config.yml" can not be converted to RegExp:' + arg);
return null;
}
return result;
}
function isIgnoreMatch(path, ignore) {
return path && ignore && ignore.length && (0, micromatch_1.isMatch)(path, ignore);
}
function readDirWalker(ctx, base, results, ignore, prefix) {
if (isIgnoreMatch(base, ignore))
return bluebird_1.default.resolve();
return bluebird_1.default.map((0, hexo_fs_1.readdir)(base).catch(err => {
ctx.log.error({ err }, 'Failed to read directory: %s', base);
if (err && err.code === 'ENOENT')
return [];
throw err;
}), (path) => __awaiter(this, void 0, void 0, function* () {
const fullpath = (0, path_1.join)(base, path);
const stats = yield (0, hexo_fs_1.stat)(fullpath).catch(err => {
ctx.log.error({ err }, 'Failed to stat file: %s', fullpath);
if (err && err.code === 'ENOENT')
return null;
throw err;
});
const prefixPath = `${prefix}${path}`;
if (stats) {
if (stats.isDirectory()) {
return readDirWalker(ctx, fullpath, results, ignore, prefixPath + path_1.sep);
}
if (!isIgnoreMatch(fullpath, ignore)) {
results.push(prefixPath);
}
}
}));
}
exports.default = Box;
//# sourceMappingURL=index.js.map

部分辅助函数:

  • __awaiter:用于处理异步函数和生成器函数,使得可以使用 async/await 语法。
  • __importDefault:用于处理默认导入。
  • escapeBackslash:将路径中的反斜杠替换为斜杠。
  • getHash:生成文件的 SHA-1 哈希值。
  • toRegExp:将字符串转换为正则表达式。
  • isIgnoreMatch:检查路径是否与忽略模式匹配。
  • readDirWalker:递归读取目录,并收集所有符合条件的文件路径。

box/index.js 也导入了一个 file.js 文件,该文件定义了一个 File 类,用来表示文件对象,并提供了一些方法来读取和获取文件状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
"use strict";
const hexo_fs_1 = require("hexo-fs");
class File {
constructor({ source, path, params, type }) {
this.source = source;
this.path = path;
this.params = params;
this.type = type;
}
read(options) {
return (0, hexo_fs_1.readFile)(this.source, options);
}
readSync(options) {
return (0, hexo_fs_1.readFileSync)(this.source, options);
}
stat() {
return (0, hexo_fs_1.stat)(this.source);
}
statSync() {
return (0, hexo_fs_1.statSync)(this.source);
}
}
File.TYPE_CREATE = 'create';
File.TYPE_UPDATE = 'update';
File.TYPE_SKIP = 'skip';
File.TYPE_DELETE = 'delete';
module.exports = File;
//# sourceMappingURL=file.js.map
  • read(options):异步读取文件内容,返回一个 Promise。
  • readSync(options):同步读取文件内容,返回文件内容。
  • stat():异步获取文件状态,返回一个 Promise。
  • statSync():同步获取文件状态,返回文件状态。
  • 文件类型常量:明确表示文件的操作类型。

Box 类也是继承自 EventEmitter

主要的属性和方法:

  • 构造函数:初始化 Box 类的实例,设置:
    • options:默认选项和用户传入的选项合并。
    • context:上下文对象。
    • base:基础路径。
    • processors:处理器列表。
    • _processingFiles:正在处理的文件。
    • watcher:文件监视器。
    • Cache:缓存模型。
    • File:文件类。
    • ignore:忽略模式。
  • _createFileClass:创建一个文件类,用于处理文件的渲染操作。
  • addProcessor:添加一个处理器,用于处理特定模式的文件。
  • _readDir:读取目录,并递归处理所有文件。
  • _checkFileStatus:检查文件状态,比较缓存文件和实际文件。
  • process:处理目录中的所有文件。
  • _processFile:处理单个文件,根据文件类型执行处理器。
  • watch:监视目录中的文件变化,并在文件添加、修改或删除时触发相应的处理。
  • unwatch:停止监视目录。
  • isWatching:检查是否正在监视目录。

Box 类的更多详情可见 Hexo 官方文档

现在再来看 Source 类。它继承自 Box 类,拥有着 Box 类的所有属性和方法。Box 类提供了文件读取、监视、处理等能力。而 Source 类在此基础上,专注于处理特定于源文件的逻辑。

主题

1
this.theme = new theme_1.default(this);

Theme 类被定义在 hexo/dist/theme 中,未来会详细说。目前只讲 index.js 的大致逻辑。

Theme 类是 Hexo 中负责管理主题的核心模块。

可见 Hexo 官方文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
const path_1 = require("path");
const box_1 = __importDefault(require("../box"));
const view_1 = __importDefault(require("./view"));
const hexo_i18n_1 = __importDefault(require("hexo-i18n"));
const config_1 = require("./processors/config");
const i18n_1 = require("./processors/i18n");
const source_1 = require("./processors/source");
const view_2 = require("./processors/view");
class Theme extends box_1.default {
constructor(ctx, options) {
super(ctx, ctx.theme_dir, options);
this.config = {};
this.views = {};
this.processors = [
config_1.config,
i18n_1.i18n,
source_1.source,
view_2.view
];
let languages = ctx.config.language;
if (!Array.isArray(languages))
languages = [languages];
languages.push('default');
this.i18n = new hexo_i18n_1.default({
languages: [...new Set(languages.filter(Boolean))]
});
class _View extends view_1.default {
}
this.View = _View;
_View.prototype._theme = this;
_View.prototype._render = ctx.render;
_View.prototype._helper = ctx.extend.helper;
}
getView(path) {
// Replace backslashes on Windows
path = path.replace(/\\/g, '/');
const ext = (0, path_1.extname)(path);
const name = path.substring(0, path.length - ext.length);
const views = this.views[name];
if (!views)
return;
if (ext) {
return views[ext];
}
return views[Object.keys(views)[0]];
}
setView(path, data) {
const ext = (0, path_1.extname)(path);
const name = path.substring(0, path.length - ext.length);
this.views[name] = this.views[name] || {};
const views = this.views[name];
views[ext] = new this.View(path, data);
}
removeView(path) {
const ext = (0, path_1.extname)(path);
const name = path.substring(0, path.length - ext.length);
const views = this.views[name];
if (!views)
return;
views[ext] = undefined;
}
}
module.exports = Theme;
//# sourceMappingURL=index.js.map

Theme 类同样继承自 Box 类:

  • constructor(ctx, options):构造函数接受上下文对象 ctx 和选项 options,调用父类 box_1.default 的构造函数。
  • 初始化 configviews 属性为空对象。
  • processors:设置处理器数组,包括配置、国际化、源文件和视图处理器。
  • 初始化多语言支持,确保 languages 是数组,并添加 default 语言。使用 hexo-i18n 库创建 i18n 对象。
  • 定义一个新的 _View 类,继承自 view_1.default,并在其原型上添加 _theme_render_helper 属性。

撇开本地文件导入,这里的 hexo-i18n 是 Hexo 自己的 i18n 模块。

hexojs/hexo-i18n - GitHub

i18n 的全程是 Internationalization(国际化),是指在设计软件、将软件与特定语言及地区脱钩的过程。由于英文单字长度过长,所以常被简称为 i18n(18意味着在 Internationalization 这个单字中,i 和 n 之间有 18 个字母。

Theme 类也有以下方法:

  • getView(path):根据给定路径获取视图。
  • setView(path, data) 设置指定路径的 View 内容。
  • removeView(path) 移除指定路径的 View

通过实例化 Theme 类,Hexo 可以获得一个用于操作当前主题的对象,从而正确地渲染和生成静态页面。

视图(View)在 Hexo 中是用于渲染页面内容的模板文件。通常使用模板引擎语法编写,如 Swig、Pug 等,允许在模板中嵌入动态数据和逻辑。

视图主要有以下几种类型:

  1. 布局(Layout):定义了页面的基本结构,如 HTML 头部、导航栏、页脚等通用部分。所有其他视图都将被渲染到布局视图中的特定位置。
  2. 包含(Partial):可重用的模板片段,通常用于渲染页面的某一部分,如文章列表、评论区等。它们可以在其他视图中被引入和渲染。
  3. 页面(Page):用于渲染特定的页面内容,如文章、分类、标签等。它们通常会引入布局视图和其他所需的包含视图。
  4. 助手(Helper):一些辅助函数,用于在模板中执行特定的逻辑或操作,如格式化日期、生成链接等。

本地数据

1
this.locals = new locals_1.default();

Locals 类用于管理和缓存局部变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
"use strict";
const hexo_util_1 = require("hexo-util");
class Locals {
constructor() {
this.cache = new hexo_util_1.Cache();
this.getters = {};
}
get(name) {
if (typeof name !== 'string')
throw new TypeError('name must be a string!');
return this.cache.apply(name, () => {
const getter = this.getters[name];
if (!getter)
return;
return getter();
});
}
set(name, value) {
if (typeof name !== 'string')
throw new TypeError('name must be a string!');
if (value == null)
throw new TypeError('value is required!');
const getter = typeof value === 'function' ? value : () => value;
this.getters[name] = getter;
this.cache.del(name);
return this;
}
remove(name) {
if (typeof name !== 'string')
throw new TypeError('name must be a string!');
this.getters[name] = null;
this.cache.del(name);
return this;
}
invalidate() {
this.cache.flush();
return this;
}
toObject() {
const result = {};
const keys = Object.keys(this.getters);
for (let i = 0, len = keys.length; i < len; i++) {
const key = keys[i];
const item = this.get(key);
if (item != null)
result[key] = item;
}
return result;
}
}
module.exports = Locals;
//# sourceMappingURL=locals.js.map

Locals 类有两个属性:

  • cache:使用 hexo-util 模块的 Cache 类实例化缓存对象。
  • getters:初始化一个空对象,用于存储 getter 函数。

Locals 类也有以下方法:

  • get(name) 获取指定名称的本地数据。
  • set(name, value) 设置指定名称的本地数据。
  • remove(name) 移除指定名称的本地数据。
  • invalidate() 清空缓存。
  • toObject() 将本地数据转换为普通对象。

Locals 类是 Hexo 中用于管理本地数据的模块。本地数据在 Hexo 插件开发中意外重要,后续会详细讲解。

暂且,请看 Hexo 官方文档

本地数据绑定

构造函数的最后一行是用来绑定本地数据的:

1
this._bindLocals();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
_bindLocals() {
const db = this.database;
const { locals } = this;
locals.set('posts', () => {
const query = {};
if (!this.config.future) {
query.date = { $lte: Date.now() };
}
if (!this._showDrafts()) {
query.published = true;
}
return db.model('Post').find(query);
});
locals.set('pages', () => {
const query = {};
if (!this.config.future) {
query.date = { $lte: Date.now() };
}
return db.model('Page').find(query);
});
locals.set('categories', () => {
// Ignore categories with zero posts
return db.model('Category').filter(category => category.length);
});
locals.set('tags', () => {
// Ignore tags with zero posts
return db.model('Tag').filter(tag => tag.length);
});
locals.set('data', () => {
const obj = {};
db.model('Data').forEach(data => {
obj[data._id] = data.data;
});
return obj;
});
}

首先获取 Hexo 实例的数据库对象 this.database,接着使用对象解构赋值的语法从 this 对象中提取 locals 属性的值。this 指向的是当前执行上下文中的对象实例,在这里,thisHexo 实例本身。

const { locals } = this; 相当于这行代码:

1
const locals = this.locals;

通过调用 locals.set() 方法,为不同的本地数据设置了获取器函数。当其他地方访问这些本地数据时,相应的获取器函数将被执行,从数据库中查询并返回所需的数据。

具体来说,这个方法设置了以下几个本地数据及其获取器:

  • posts:获取所有已发布且不是未来日期的文章。如果 config.future 设置为 true,则也包括未来日期的文章。如果 _showDrafts() 返回 true,则也包括草稿文章。
  • pages:获取所有已发布且不是未来日期的页面。如果 config.future 设置为 true,则也包括未来日期的页面。
  • categories:获取所有包含文章的分类。
  • tags:获取所有包含文章的标签。
  • data:获取所有自定义数据。

初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
init() {
this.log.debug('Hexo version: %s', (0, picocolors_1.magenta)(this.version));
this.log.debug('Working directory: %s', (0, picocolors_1.magenta)((0, tildify_1.default)(this.base_dir)));
// Load internal plugins
require('../plugins/console')(this);
require('../plugins/filter')(this);
require('../plugins/generator')(this);
require('../plugins/helper')(this);
require('../plugins/highlight')(this);
require('../plugins/injector')(this);
require('../plugins/processor')(this);
require('../plugins/renderer')(this);
require('../plugins/tag').default(this);
// Load config
return bluebird_1.default.each([
'update_package', // Update package.json
'load_config', // Load config
'load_theme_config', // Load alternate theme config
'load_plugins' // Load external plugins & scripts
], name => require(`./${name}`)(this)).then(() => this.execFilter('after_init', null, { context: this })).then(() => {
// Ready to go!
this.emit('ready');
});
}

init 方法负责进行初始化工作:

  1. 打印 Hexo 版本和工作目录信息到控制台,使用 picocolors 库为版本和目录路径上色。
  2. 加载内部插件。
  3. 执行一系列初始化步骤后,使用了 bluebird.each 方法按顺序执行以下步骤:
    • update_package:更新 package.json 文件。
    • load_config:加载配置文件。
    • load_theme_config:加载主题配置文件。
    • load_plugins:加载外部插件和脚本。
  4. 在所有初始化步骤完成后,执行 after_init 过滤器,允许插件和主题在初始化后进行一些额外的操作。
  5. 发射 ready 事件,表示 Hexo 已经准备就绪,可以执行其他操作了。

调用控制台命令

1
2
3
4
5
6
7
8
9
10
call(name, args, callback) {
if (!callback && typeof args === 'function') {
callback = args;
args = {};
}
const c = this.extend.console.get(name);
if (c)
return Reflect.apply(c, this, [args]).asCallback(callback);
return bluebird_1.default.reject(new Error(`Console \`${name}\` has not been registered yet!`));
}

这段代码定义了 call 方法,用于调用已注册的控制台命令。

先是检查了是否传入回调函数。如果没有,但是第二个参数是一个函数的话,就将第二个参数视为回调函数,同时将 args 设置为一个空对象。这是为了兼容只传入回调函数的情况。

this.extend.console 对象中获取名为 name 的控制台命令。如果找到了对应的控制台命令对象 c,便执行以下步骤:

  1. 使用 Reflect 对象的 apply 方法调用控制台命令 c
  2. .asCallback(callback) 将上一步的调用结果转换为一个 Promise 对象,并将该 Promise 与回调函数 callback 关联。这样就可以在 Promise 完成时自动调用回调函数。

如果未能找到对应的控制台命令对象,就返回一个被拒绝的 Promise,其中包含一个表示该命令尚未注册的错误消息。

定义数据库模型

1
2
3
model(name, schema) {
return this.database.model(name, schema);
}

回到数据库Database 类,能看出 model 方法是用来创建一个模型的。

解析插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
resolvePlugin(name, basedir) {
try {
// Try to resolve the plugin with the Node.js's built-in require.resolve.
return require.resolve(name, { paths: [basedir] });
}
catch (err) {
try {
// There was an error (likely the node_modules is corrupt or from early version of npm)
// Use Hexo prior 6.0.0's behavior (resolve.sync) to resolve the plugin.
resolveSync = resolveSync || require('resolve').sync;
return resolveSync(name, { basedir });
}
catch (err) {
// There was an error (likely the plugin wasn't found), so return a possibly
// non-existing path that a later part of the resolution process will check.
return (0, path_1.join)(basedir, 'node_modules', name);
}
}
}

先是使用 require.resolve 方法用于解析模块。如果解析失败了,就继续尝试使用 resolve.sync 方法。最后如果依然失败,则返回一个可能不存在的路径以供后续处理。

require.resolve 方法是 Node.js 内置的,用于解析模块的路径。 如果 require.resolve 失败,通常是由于 node_modules 目录损坏或其他原因。

require('resolve').sync 方法是一个更兼容旧版本 npm 的解决方案,类似于早期 Hexo 版本的插件解析方式。resolve.sync 方法如果也失败,很有可能是因为插件未找到,这时候便返回一个大概并不存在的路径。

加载插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
loadPlugin(path, callback) {
return (0, hexo_fs_1.readFile)(path).then(script => {
// Based on: https://github.com/nodejs/node-v0.x-archive/blob/v0.10.33/src/node.js#L516
const module = new module_1.default(path);
module.filename = path;
module.paths = module_1.default._nodeModulePaths(path);
function req(path) {
return module.require(path);
}
req.resolve = (request) => module_1.default._resolveFilename(request, module);
req.main = require.main;
req.extensions = module_1.default._extensions;
req.cache = module_1.default._cache;
script = `(async function(exports, require, module, __filename, __dirname, hexo){${script}\n});`;
const fn = (0, vm_1.runInThisContext)(script, path);
return fn(module.exports, req, module, path, (0, path_1.dirname)(path), this);
}).asCallback(callback);
}
  1. 使用 hexo-fs 库的 readFile 异步读取插件文件的内容,返回一个 Promise 对象。

    hexo-fs 库的源代码未来再说,暂且只需要知道是 Hexo 的文件系统模块。

    hexojs/hexo-fs - GitHub

  2. 接着创建模块环境,并设置 filenamepaths 属性。这里的 module_1module 模块,定义了多种与模块加载、解析和处理相关的类型和接口。filename 属性被赋值插件的路径 pathpaths 被赋值模块的搜索路径。

  3. req 方法用于模块加载。这个方法实际上模拟了 require 函数。之后这段代码分别定义了 req.resolve 方法来解析模块的绝对路径,设置 req.main 属性来指向 Node.js 主模块、req.extensions 属性来处理模块文件的加载、req.cache 属性来缓存加载的模块。

  4. 插件的内容被包装在一个异步函数中,并传入了必要的参数、赋值给变量 script

  5. 使用 vm_1.runInThisContext 方法将包装后的插件内容编译为可执行函数。

    • vm_1vm 模块,提供了一组 API 用于在 JavaScript 中创建虚拟机和运行沙盒化的代码。
  6. 执行编译后的函数,传入需要的参数。

  7. 最终将 Promise 对象转换为回调函数形式,并进行执行。

显示草稿内容

1
2
3
4
_showDrafts() {
const { args } = this.env;
return args.draft || args.drafts || this.config.render_drafts;
}

根据用户的输入和 Hexo 的配置,确定是否应该显示草稿内容。

加载

1
2
3
4
5
6
7
8
9
10
11
12
load(callback) {
return (0, load_database_1.default)(this).then(() => {
this.log.info('Start processing');
return bluebird_1.default.all([
this.source.process(),
this.theme.process()
]);
}).then(() => {
mergeCtxThemeConfig(this);
return this._generate({ cache: false });
}).asCallback(callback);
}

load_database 模块检查数据库文件是否存在,存在则尝试加载数据库。如果加载失败,则删除数据库文件。它使用了 hexo-fs 模块来处理文件系统操作,并使用 bluebird 提供的 Promise 来管理异步操作。通过 ctx 上下文对象来传递数据库和日志对象,以及跟踪数据库加载状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
const hexo_fs_1 = require("hexo-fs");
const bluebird_1 = __importDefault(require("bluebird"));
module.exports = (ctx) => {
if (ctx._dbLoaded)
return bluebird_1.default.resolve();
const db = ctx.database;
const { path } = db.options;
const { log } = ctx;
return (0, hexo_fs_1.exists)(path).then(exist => {
if (!exist)
return;
log.debug('Loading database.');
return db.load();
}).then(() => {
ctx._dbLoaded = true;
}).catch(() => {
log.error('Database load failed. Deleting database.');
return (0, hexo_fs_1.unlink)(path);
});
};
//# sourceMappingURL=load_database.js.map

在数据库加载完成后,使用 then 方法处理返回的 Promise。在这个处理函数中,首先记录了日志信息 Start processing,然后使用 bluebird_1.default.all 方法并行处理两个任务:

  • this.source.process() 处理源文件。
  • this.theme.process() 处理主题。

处理完源文件和主题后,再次使用了 then 方法处理返回的 Promise。调用 mergeCtxThemeConfig 函数,将 Hexo 实例的上下文与主题配置合并,然后调用了 this._generate({ cache: false }) 方法,生成静态文件。

mergeCtxThemeConfig 函数在 index.js 文件的很上方:

1
2
3
4
5
const mergeCtxThemeConfig = (ctx) => {
if (ctx.config.theme_config) {
ctx.theme.config = (0, hexo_util_1.deepMerge)(ctx.theme.config, ctx.config.theme_config);
}
};

该函数用于将 ctx.config.theme_config(主题配置)合并到 ctx.theme.config(博客目录)中。这里还使用了 hexo-util 库的深度合并。hexo-util 库也是未来会讲的。

hexojs/hexo-util - GitHub

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
_generate(options = {}) {
if (this._isGenerating)
return;
const useCache = options.cache;
this._isGenerating = true;
this.emit('generateBefore');
// Run before_generate filters
return this.execFilter('before_generate', null, { context: this })
.then(() => this._routerRefresh(this._runGenerators(), useCache)).then(() => {
this.emit('generateAfter');
// Run after_generate filters
return this.execFilter('after_generate', null, { context: this });
}).finally(() => {
this._isGenerating = false;
});
}

为了防止重复生成,_generate 方法先是判断了 _isGenerating 变量,再进行接下来的操作。从 options 参数中提取 cache 选项,是为了决定是否在生成过程中使用缓存。

设置 this._isGeneratingtrue 后,便开始生成。generateBefore 事件被触发,接着执行 before_generate 过滤器。

过滤器(Filter)后续会讲解。目前先延申 Hexo 官方文档中的说明:

过滤器用于修改特定文件,Hexo 将这些文件依序传给过滤器,而过滤器可以针对文件进行修改。

其中的 execFilter 方法在 Hexo 类的最下方被定义:

1
2
3
execFilter(type, data, options) {
return this.extend.filter.exec(type, data, options);
}

Hexo 类也有一个 execFilterSync 方法:

1
2
3
execFilterSync(type, data, options) {
return this.extend.filter.execSync(type, data, options);
}

before_generate 过滤器执行完毕后,调用 _routerRefresh 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
_routerRefresh(runningGenerators, useCache) {
const { route } = this;
const routeList = route.list();
const Locals = this._generateLocals();
Locals.prototype.cache = useCache;
return runningGenerators.map((generatorResult) => {
if (typeof generatorResult !== 'object' || generatorResult.path == null)
return undefined;
const path = route.format(generatorResult.path);
const { data, layout } = generatorResult;
if (!layout) {
route.set(path, data);
return path;
}
return this.execFilter('template_locals', new Locals(path, data), { context: this })
.then(locals => { route.set(path, createLoadThemeRoute(generatorResult, locals, this)); })
.thenReturn(path);
}).then(newRouteList => {
for (let i = 0, len = routeList.length; i < len; i++) {
const item = routeList[i];
if (!newRouteList.includes(item)) {
route.remove(item);
}
}
});
}
  1. 初始化和获取路由列表:从 this 中获取 route 对象,并调用其 list 方法获取当前路由列表;生成局部变量 Locals,并将 useCache 选项赋给 Locals.prototype.cache,决定是否使用缓存。
  2. 处理生成器结果:遍历 runningGenerators 数组,对于每个生成器结果进行处理。如果 generatorResult 不是对象或 path 为空,则返回 undefined
  3. 格式化路径并处理数据:调用 route.format 方法格式化路径;解构 generatorResult,获取 datalayout。如果 layout 不存在,直接将 pathdata 设置到路由中,并返回 path
  4. 执行 template_locals 过滤器并设置路由:调用 execFilter 方法执行 template_locals 过滤器,传入新的 Locals 实例。在过滤器执行完毕后,将路径和创建的主题路由设置到路由中,并返回 path
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    const createLoadThemeRoute = function (generatorResult, locals, ctx) {
    const { log, theme } = ctx;
    const { path, cache: useCache } = locals;
    const layout = [...new Set(castArray(generatorResult.layout))];
    const layoutLength = layout.length;
    locals.cache = true;
    return () => {
    if (useCache && routeCache.has(generatorResult))
    return routeCache.get(generatorResult);
    for (let i = 0; i < layoutLength; i++) {
    const name = layout[i];
    const view = theme.getView(name);
    if (view) {
    log.debug(`Rendering HTML ${name}: ${(0, picocolors_1.magenta)(path)}`);
    return view.render(locals)
    .then(result => ctx.extend.injector.exec(result, locals))
    .then(result => ctx.execFilter('_after_html_render', result, {
    context: ctx,
    args: [locals]
    }))
    .tap(result => {
    if (useCache) {
    routeCache.set(generatorResult, result);
    }
    }).tapCatch(err => {
    log.error({ err }, `Render HTML failed: ${(0, picocolors_1.magenta)(path)}`);
    });
    }
    }
    log.warn(`No layout: ${(0, picocolors_1.magenta)(path)}`);
    };
    };
    createLoadThemeRoute 方法创建加载主题路由的闭包。
    首先从 ctx 中获取日志记录器 log 和主题对象 theme、从 locals 中获取路由的路径 path 和缓存标志 useCache。然后将 generatorResult.layout 转换为一个唯一的布局数组,记录布局的长度,同时将 locals.cache 设置为 true,以确保视图在渲染期间使用缓存。
    返回一个闭包。闭包指的是函数和函数内部引用的外部变量的组合。该闭包在调用时会渲染 HTML 视图。
    如果 useCachetrue,并且缓存中存在 generatorResult,直接返回缓存的结果。否则遍历布局数组,并尝试从主题中获取对应的视图。
    如果找到了视图,就使用该视图渲染 locals,并执行注入器和过滤器,最后返回渲染结果。否则记录警告并返回。

注入器(Injector)是 Hexo 扩展之一,在 Hexo 官方文档 中被声明为 用于将静态代码片段注入生成的 HTML 的 <head> 和/或 <body>,且注入必须在 after_render:html 过滤器之前完成。

  1. 移除旧路由:获取新生成的 newRouteList;遍历旧路由列表 routeList,如果某个路由不在 newRouteList 中,则将其移除。

_routerRefresh 方法确保了生成器结果正确应用到路由中,同时清理不再需要的旧路由,保持路由的最新状态。

路由刷新完成后,_generate 方法触发了 generateAfter 事件,并执行 after_generate 过滤器。

最后使用 finally 方法确保无论生成过程成功与否,都会将 this._isGenerating 重置为 false,允许后续生成操作进行。

通过执行前后过滤器和触发事件,_generate 方法确保生成过程的各个阶段都可以被插件或自定义代码扩展和修改。

load 方法在调用 _generate、生成静态文件后,将整个异步操作的结果传递给回调函数 callbackasCallback 方法将 Promise 转换为传统的回调形式,以兼容老式回调风格的代码。

监视文件变化

watch 函数的目的是在监视文件变化时重新生成内容,并在需要时启用缓存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
watch(callback) {
let useCache = false;
const { cache } = Object.assign({
cache: false
}, this.config.server);
const { alias } = this.extend.console;
if (alias[this.env.cmd] === 'server' && cache) {
// enable cache when run hexo server
useCache = true;
}
this._watchBox = debounce(() => this._generate({ cache: useCache }), 100);
return (0, load_database_1.default)(this).then(() => {
this.log.info('Start processing');
return bluebird_1.default.all([
this.source.watch(),
this.theme.watch()
]);
}).then(() => {
mergeCtxThemeConfig(this);
this.source.on('processAfter', this._watchBox);
this.theme.on('processAfter', () => {
this._watchBox();
mergeCtxThemeConfig(this);
});
return this._generate({ cache: useCache });
}).asCallback(callback);
}
  1. watch 函数会先从 this.config.server 中读取 cache 配置,默认值为 false。如果当前命令为 server,并且配置了 cache,就会启用缓存。
  2. 接着使用 debounce 函数防抖 _generate 方法,间隔设置为 100 毫秒:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function debounce(func, wait) {
    let timeout;
    return function () {
    clearTimeout(timeout);
    timeout = setTimeout(() => {
    func.apply(this);
    }, wait);
    };
    }
    debounce 函数的目的是限制某个函数在一段时间内的调用次数,当该函数被连续调用时,只有在停止调用后等待指定的时间才会真正执行。这样便可以优化性能。
  3. 调用 load_database_1.default 方法加载数据库,打印日志 Start processing,同时监视 sourcetheme 的变化,使用 bluebird_1.default.all 以并行方式执行。
  4. 合并主题配置并设置监控回调:这里的 mergeCtxThemeConfig 方法之前已经说过了。当 source 处理完成后,调用 _watchBox;当 theme 处理完成后,调用 _watchBox 并再次合并主题配置。
  5. 调用 _generate 方法,初次生成内容,并根据 useCache 决定是否使用缓存。
  6. 最后使用 bluebird 库的 asCallback 方法,将结果作为回调传递给 callback

停止监控

既然有了监视用的方法,那么也有取消监视的方法。

1
2
3
4
5
6
7
8
9
unwatch() {
if (this._watchBox != null) {
this.source.removeListener('processAfter', this._watchBox);
this.theme.removeListener('processAfter', this._watchBox);
this._watchBox = null;
}
stopWatcher(this.source);
stopWatcher(this.theme);
}

unwatch 方法检查并清除了 _watchBox。根据上面 watch 方法的内容,this._watchBox 是一个防抖函数,用来处理文件变动后的生成操作。如果它存在,就需要将其从 sourcetheme 对象的 processAfter 事件监听器中移除。

stopWatcher 函数被定义在 index.js 的最上方:

1
2
const stopWatcher = (box) => { if (box.isWatching())
box.unwatch(); };

它用于停止对给定对象的监控。逻辑很简单:该对象是否正在被监控,如果是,就调用 unwatch 方法停止监控。

生成本地环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
_generateLocals() {
const { config, env, theme, theme_dir } = this;
const ctx = { config: { url: this.config.url } };
const localsObj = this.locals.toObject();
class Locals {
constructor(path, locals) {
this.page = Object.assign({}, locals);
if (this.page.path == null)
this.page.path = path;
this.path = path;
this.url = hexo_util_1.full_url_for.call(ctx, path);
this.config = config;
this.theme = theme.config;
this.layout = 'layout';
this.env = env;
this.view_dir = (0, path_1.join)(theme_dir, 'layout') + path_1.sep;
this.site = localsObj;
}
}
return Locals;
}

Locals 类中,许多环境相关的属性,如 urlconfig 等属性都被设置。该方法生成了本地环境的信息对象,以便在模块渲染过程中使用。

运行生成器

同样,生成器(Generator)后续也会讲解。

Hexo 官方文档 中,生成器被解释为 会根据处理后的原始文件建立路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
_runGenerators() {
this.locals.invalidate();
const siteLocals = this.locals.toObject();
const generators = this.extend.generator.list();
const { log } = this;
// Run generators
return bluebird_1.default.map(Object.keys(generators), key => {
const generator = generators[key];
log.debug('Generator: %s', (0, picocolors_1.magenta)(key));
return Reflect.apply(generator, this, [siteLocals]);
}).reduce((result, data) => {
return data ? result.concat(data) : result;
}, []);
}
  1. invalidate 方法使 Hexo 实例的本地数据无效,以便在生成器运行之前重新加载。
  2. 提取当前 Hexo 实例的本地数据对象,将其赋值给 siteLocals 变量。
  3. 获取 Hexo 实例中已注册的所有生成器,将它们保存在 generators 变量中。
  4. 运行生成器之前,记录了一条调试信息,指示将要运行哪个生成器。
  5. 使用 bluebird 库的 map 方法对生成器进行并行处理。每个生成器都被调用 Reflect.apply(generator, this, [siteLocals]),将生成器函数应用到当前的 Hexo 实例上。
  6. 使用 bluebird 库的 reduce 方法将生成器返回的数据收集到一个数组中,并返回这个数组。

_runGenerators 方法运行 Hexo 实例中所有注册的生成器,并将它们返回的数据收集到一个数组中。

退出进程

exit 方法用于退出 Hexo 进程。

1
2
3
4
5
6
7
8
exit(err) {
if (err) {
this.log.fatal({ err }, 'Something\'s wrong. Maybe you can find the solution here: %s', (0, picocolors_1.underline)('https://hexo.io/docs/troubleshooting.html'));
}
return this.execFilter('before_exit', null, { context: this }).then(() => {
this.emit('exit', err);
});
}

这里值得说的是,exit 方法会触发 execFilter 方法、触发 before_exit 过滤器。以及最终它会触发 exit 事件。

定义属性和导出

1
2
3
4
5
6
7
Hexo.lib_dir = libDir + path_1.sep;
Hexo.prototype.lib_dir = Hexo.lib_dir;
Hexo.core_dir = (0, path_1.dirname)(libDir) + path_1.sep;
Hexo.prototype.core_dir = Hexo.core_dir;
Hexo.version = version;
Hexo.prototype.version = Hexo.version;
module.exports = Hexo;

Hexo 类本身终于讲完了,在 Hexo 类的下方,它的部分属性和原型属性被定义,并最终被导出为模块的默认输出。

  • LibDirHexo 库目录的路径。
  • core_dir:核心目录的路径。
  • version:版本号。