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  =>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  =>extend .injector .exec (result, locals))           .then (result  =>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返回值到模板传参到分析