发布于 

【hexo专栏】讲解hexo插件体系的实现原理

背景

hexo里面有一个插件机制,它能让我们扩展hexo的能力,那么它的实现原理是怎么样的呢?本文会写一下hexo里面是如何实现的,这样在后续我们自己的框架开发中,也能借鉴一下。

hexo插件的官方文档地址:

实现原理

猜想

如果我们不考虑hexo是如何实现的,那我们会怎么实现?

首先想到的是读取当前项目的package.json,然后看看里面有什么hexo-开头的依赖的名字。然后例如这个包的package.json里面有某个字段可以标识这个是什么类型的,比如是generate或者theme,然后根据这个字段去执行对应的逻辑。

然后如果是特定的类型的,比如是theme,我们可以做一个递归操作,此处需要考虑用户可能安装了多个主题,所以只能执行配置的主题里面的依赖的hexo-逻辑。

hexo的实现

我们翻看一下hexo的代码: 路径: hexo/lib/hexo/load_plugns.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
function loadModuleList(ctx, basedir) {
const packagePath = join(basedir, 'package.json');

// Make sure package.json exists
return exists(packagePath).then(exist => {
if (!exist) return [];

// Read package.json and find dependencies
return readFile(packagePath).then(content => {
const json = JSON.parse(content);
const deps = Object.keys(json.dependencies || {});
const devDeps = Object.keys(json.devDependencies || {});

return basedir === ctx.base_dir ? deps.concat(devDeps) : deps;
});
}).filter(name => {
// Ignore plugins whose name is not started with "hexo-"
if (!/^hexo-|^@[^/]+\/hexo-/.test(name)) return false;

// Ignore plugin whose name is started with "hexo-theme"
if (/^hexo-theme-|^@[^/]+\/hexo-theme-/.test(name)) return false;

// Ignore typescript definition file that is started with "@types/"
if (name.startsWith('@types/')) return false;

// Make sure the plugin exists
const path = ctx.resolvePlugin(name, basedir);
return exists(path);
}).then(modules => {
return Object.fromEntries(modules.map(name => [name, ctx.resolvePlugin(name, basedir)]));
});
}

然后我们看到其排除了不是以hexo-开头的包,然后又排除了以hexo-theme-开头的包,然后又排除了以@types/开头的包。

然后其调用了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function loadModules(ctx) {
return Promise.map([ctx.base_dir, ctx.theme_dir], basedir => loadModuleList(ctx, basedir))
.then(([hexoModuleList, themeModuleList]) => {
return Object.entries(Object.assign(themeModuleList, hexoModuleList));
})
.map(([name, path]) => {
// Load plugins
return ctx.loadPlugin(path).then(() => {
ctx.log.debug('Plugin loaded: %s', magenta(name));
}).catch(err => {
ctx.log.error({err}, 'Plugin load failed: %s', magenta(name));
});
});
}

然后我们看一下这个loadPlugin的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
loadPlugin(path, callback) {
return readFile(path).then(script => {
// Based on: https://github.com/joyent/node/blob/v0.10.33/src/node.js#L516
const module = new Module(path);
module.filename = path;
module.paths = Module._nodeModulePaths(path);

function req(path) {
return module.require(path);
}

req.resolve = request => Module._resolveFilename(request, module);

req.main = require.main;
req.extensions = Module._extensions;
req.cache = Module._cache;

script = `(function(exports, require, module, __filename, __dirname, hexo){${script}\n});`;

const fn = runInThisContext(script, path);

return fn(module.exports, req, module, path, dirname(path), this);
}).asCallback(callback);
}

上面这段代码需要重点理解一下,我们看到它是通过runInThisContext来执行的,然后我们看一下这个函数的文档:

所以我们看到这些插件包有一个hexo的对象。相当于这边整体被一个方法包裹起来了:

1
(function(exports, require, module, __filename, __dirname, hexo){${script}\n});

比如我们以 hexo-generate-index 为例:

1
hexo.extend.generator.register('index', require('./lib/generator'));

然后lib/generator的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module.exports = function(locals) {
const config = this.config;
const posts = locals.posts.sort(config.index_generator.order_by);

posts.data.sort((a, b) => (b.sticky || 0) - (a.sticky || 0));

const paginationDir = config.pagination_dir || 'page';
const path = config.index_generator.path || '';

return pagination(path, posts, {
perPage: config.index_generator.per_page,
layout: ['index', 'archive'],
format: paginationDir + '/%d/',
data: {
__index: true
}
});
};

然后我们可以看到里面有一个this,这个this就是hexo对象。

另外关于插件的配置,可以通过类似如下的方式来配置:

1
2
3
4
hexo.config.index_generator = Object.assign({
per_page: typeof hexo.config.per_page === 'undefined' ? 10 : hexo.config.per_page,
order_by: '-date'
}, hexo.config.index_generator);

然后我们看一下这个执行在 hexo/lb/hexo/index.js的init方法中:

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
init() {
this.log.debug('Hexo version: %s', magenta(this.version));
this.log.debug('Working directory: %s', magenta(tildify(this.base_dir)));

// Load internal plugins
require('../plugins/console')(this);
require('../plugins/filter')(this);
require('../plugins/generator')(this);
require('../plugins/helper')(this);
require('../plugins/injector')(this);
require('../plugins/processor')(this);
require('../plugins/renderer')(this);
require('../plugins/tag')(this);

// Load config
return Promise.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');
});
}

至此,让hexo.extend挂载了一些我们的想要扩展的内容。

总结

跟我们第二节的时候开头想的实现方式相差不多,另外没有读取插件package.json里面的一些字段,另外对于主题只需要读取一个也是类似的。

有一点没想到的比如,这个包可能是类似hexo-generate-xxx,也可能是npm私域带有scope的包,这里拉下了npm私域带有scope的包,开头没想到。正则:^@[^/]+\/hexo-theme-这样的。

然后插件入口通过runInThisContext去实现,而不是直接require(‘’),因为直接require,也不能直接在pacakge.json的main文件中直接写hexo这样的方法,而是需要那边去require(‘hexo’)包来实现(如果这样实现也不是不行)。

Hexo专栏

目前 Hexo 系列形成了一个小专栏,欢迎读者继续阅读: Hexo专栏地址


如果你有什么意见和建议,可以点击: 反馈地址 进行反馈。