【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');
return exists(packagePath).then(exist => { if (!exist) return [];
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 => { if (!/^hexo-|^@[^/]+\/hexo-/.test(name)) return false;
if (/^hexo-theme-|^@[^/]+\/hexo-theme-/.test(name)) return false;
if (name.startsWith('@types/')) return false;
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]) => { 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 => { 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)));
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);
return Promise.each([ 'update_package', 'load_config', 'load_theme_config', 'load_plugins' ], name => require(`./${name}`)(this)).then(() => this.execFilter('after_init', null, { context: this })).then(() => { 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专栏地址