hexo默认的分类页面,每个分类都需要跳转才能到子页面,这样不够一目了然,考虑到当前只有百来篇博文,没必要每个分类单独跳转页面,因此打算自己实现这个功能
从Hexo添加自定义分类菜单项并定制页面布局(简洁版) 大致可以知道实现一个模板文件
然后注入js代码例如
1 2 3 4 5 6 hexo.extend .generator .register ("test" , function (locals ) { return { path : "test/index.html" , data : "foo" , layout : ["test" ], };
就可以达到目的
但是hexo的文档 写的非常的烂,关于generator的文档 描述了一些常见用法,但是没有传递给模板文件的方式
所以只能从源码入手了
hexo命令入口
我对nodejs不太熟悉,所以首先分析了一下hexo命令的调用流程,查看hexo命令在哪里
1 2 $ which hexo/c/Users/tedcy/AppData/Roaming/npm/hexo
而环境变量$PATH
中存在/c/Users/tedcy/AppData/Roaming/npm
这个路径,因此应该是npm install
的时候c从在博客根目录下node_modules/hexo/node_modules/hexo-cli/
自动安装进npm目录的
hexo-cli模块
hexo命令的内容指向了node_modules/hexo/node_modules/hexo-cli/lib/hexo.js
1 2 3 4 5 #!/usr/bin/env node 'use strict' ;require ('../lib/hexo' )();
hexo.js
这个文件的entry函数是实际入口,通过命令获取到事先注册在hexo.extend.console
上的回调,然后通过hexo.call来调用回调:
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 function loadModule (path, args ) { return Promise .try (() => { const modulePath = resolve.sync ('hexo' , { basedir : path }); const Hexo = require (modulePath); return new Hexo (path, args); }); } function entry (cwd = process.cwd(), args ) { return findPkg (cwd, args).then (path => { if (!path) return ; hexo.base_dir = path; return loadModule (path, args).catch (err => { log.error (err.message ); log.error ('Local hexo loading failed in %s' , chalk.magenta (tildify (path))); log.error ('Try running: \'rm -rf node_modules && npm install --force\'' ); throw new HexoNotFoundError (); }); }).then (mod => { if (mod) hexo = mod; log = hexo.log ; require ('./console' )(hexo); return hexo.init (); }).then (() => { let cmd = 'help' ; if (!args.h && !args.help ) { const c = args._ .shift (); if (c && hexo.extend .console .get (c)) cmd = c; } watchSignal (hexo); return hexo.call (cmd, args).then (() => hexo.exit ()).catch (err => hexo.exit (err).then (() => { handleError (null ); })); }).catch (handleError); } module .exports = entry;
那么是在哪里注册回调的呢,在项目目录下执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 $ cd node_modules && grep -nr console.register node_modules./hexo/node_modules/hexo-cli/lib/console/index.js:6: console.register('help', 'Get help on a command.', {}, require('./help')); ./hexo/node_modules/hexo-cli/lib/console/index.js:8: console.register('init', 'Create a new Hexo folder.', { ./hexo/node_modules/hexo-cli/lib/console/index.js:20: console.register('version', 'Display version information.', {}, require('./version')); ./hexo/lib/plugins/console/index.js:6: console.register('clean', 'Remove generated files and cache.', require('./clean')); ./hexo/lib/plugins/console/index.js:8: console.register('config', 'Get or set configurations.', { ./hexo/lib/plugins/console/index.js:16: console.register('deploy', 'Deploy your website.', { ./hexo/lib/plugins/console/index.js:23: console.register('generate', 'Generate static files.', { ./hexo/lib/plugins/console/index.js:33: console.register('list', 'List the information of the site', { ./hexo/lib/plugins/console/index.js:41: console.register('migrate', 'Migrate your site from other system to Hexo.', { ./hexo/lib/plugins/console/index.js:49: console.register('new', 'Create a new post.', { ./hexo/lib/plugins/console/index.js:62: console.register('publish', 'Moves a draft post from _drafts to _posts folder.', { ./hexo/lib/plugins/console/index.js:70: console.register('render', 'Render files with renderer plugins.', { ./hexo-server/index.js:14:hexo.extend.console.register('server', 'Start the server.', {
hero-server模块
可以看到hexo server
是在node_modules/hexo-server/index.js
下注册的,它需要调用node_modules/hexo-server/lib/server.js
1 2 3 4 5 6 7 8 9 10 hexo.extend .console .register ('server' , 'Start the server.' , { desc : 'Start the server and watch for file changes.' , options : [ {name : '-i, --ip' , desc : 'Override the default server IP. Bind to all IP address by default.' }, {name : '-p, --port' , desc : 'Override the default port.' }, {name : '-s, --static' , desc : 'Only serve static files.' }, {name : '-l, --log [format]' , desc : 'Enable logger. Override log format.' }, {name : '-o, --open' , desc : 'Immediately open the server url in your default web browser.' } ] }, require ('./lib/server' ));
接着查看node_modules/hexo-server/lib/server.js
:
1 2 3 4 5 6 7 8 module .exports = function (args ) { if (args.s || args.static ) { return this .load (); } return this .watch (); }
这里的this类就是之前在node_modules/hexo/node_modules/hexo-cli/lib/hexo.js
中通过loadModule动态创建好的Hexo类
核心Hexo类
源码位于node_modules/hexo/lib/hexo/index.js
续接上面的this.load()
,这部分实现会调用_generate()
,从而进入生成页面的核心流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Hexo extends EventEmitter { load (callback ) { return loadDatabase (this ).then (() => { this .log .info ('Start processing' ); return Promise .all ([ this .source .process (), this .theme .process () ]); }).then (() => { mergeCtxThemeConfig (this ); return this ._generate ({ cache : false }); }).asCallback (callback); } } module .exports = Hexo ;
_generate()
中将_runGenerators()
得到的返回值传递给了_routerReflesh()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 _generate (options = {} ) { if (this ._isGenerating ) return ; const useCache = options.cache ; this ._isGenerating = true ; this .emit ('generateBefore' ); return this .execFilter ('before_generate' , this .locals .get ('data' ), { context : this }) .then (() => this ._routerReflesh (this ._runGenerators (), useCache)).then (() => { this .emit ('generateAfter' ); return this .execFilter ('after_generate' , null , { context : this }); }).finally (() => { this ._isGenerating = false ; }); }
generate生成
那么首先查看_runGenerators()
返回了什么,这里获取到this.extend.generator.list()
的全部回调,对其执行,然后结果对其key和返回值进行map,reduce拼在一个result中
这里list到的全部generator显然就是通过hexo.extend.generator.register
注册进来的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 _runGenerators() { this .locals.invalidate (); const siteLocals = this .locals.toObject (); const generators = this .extend.generator.list (); const { log } = this ; return Promise.map (Object.keys (generators), key => { const generator = generators[key]; log.debug ('Generator: %s' , magenta (key)); return Reflect.apply (generator, this , [siteLocals]); }).reduce ((result, data) => { return data ? result.concat (data) : result; }, []); }
对于如下的生成器代码
1 2 3 4 5 6 hexo.extend .generator .register ("test" , function (locals ) { return { path : "test/index.html" , data : "foo" , layout : ["test" ], };
使用注释中的代码打印将得到
1 2 3 4 5 Generator test returned data: { path: 'test/index.html', data: 'foo', layout: ['test', [length]: 1 ] }
根据layout路由generate生成结果
那么传递给的_routerReflesh()
大致可以猜测出来,是根据generator的key和data中的值进行分发
根据data中的layout字段转发给对应模板,为了传递更多信息,会通过Locals类对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 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 _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 = { ...locals }; if (this .page .path == null ) this .page .path = path; this .path = path; this .url = full_url_for.call (ctx, path); this .config = config; this .theme = theme.config ; this .layout = 'layout' ; this .env = env; this .view_dir = join (theme_dir, 'layout' ) + sep; this .site = localsObj; } } return Locals ; } _routerReflesh (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); } } }); }
简化上述代码,传递给createLoadThemeRoute()的参数locals是对data的封装
1 2 3 4 5 6 7 const Locals = this ._generateLocals (); locals = new Locals (path, data); route.set (path, createLoadThemeRoute (generatorResult, locals, this );
createLoadThemeRoute的逻辑我没太看明白,但是可以大致猜测,主要逻辑是:
当存在缓存的时候,不进行重新生成,然后遍历全部的layout,找到存在的layout,将其转发给模板
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 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} : ${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: ${magenta(path)} ` ); }); } } log.warn (`No layout: ${magenta(path)} ` ); }; };
这里的view.render(locals)
对于nunjucks模板而言,在node_modules/hexo/lib/plugins/renderer/nunjucks.js
中存在代码是大致可以对应上的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function njkCompile (data ) { let env; if (data.path ) { env = nunjucks.configure (dirname (data.path ), nunjucksCfg); } else { env = nunjucks.configure (nunjucksCfg); } nunjucksAddFilter (env); const text = 'text' in data ? data.text : readFileSync (data.path ); return nunjucks.compile (text, env, data.path ); } function njkRenderer (data, locals ) { return njkCompile (data).render (locals); }
从而完成了generator返回值到模板传参到分析