我们上篇文章分析了hexo-cli
的执行原理,那么本篇文章,将分析一下hexo
这个依赖中做的事情。将尝试分析一下hexo
的内部处理机制。
整体流程 在详细分析之前,我们可以先来看一下整体的执行流程,我们以hexo g
命令为例,我们可以在命令行中加上--debug
参数,例hexo g --debug
,里面可以打印出一些开发时候埋入的关键log信息。如下所示:
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 $ npx hexo g --debug 07:59:56.062 DEBUG Writing database to /Users/huidizhu/Personal/blog/blog/db.json 07:59:56.071 DEBUG Hexo version: 5.4.0 07:59:56.071 DEBUG Working directory: ~/Personal/blog/blog/ 07:59:56.156 DEBUG Config loaded: ~/Personal/blog/blog/_config.yml 07:59:56.156 INFO Validating config 07:59:56.165 DEBUG Second Theme Config loaded: ~/Personal/blog/blog/_config.icarus.yml Inferno is in development mode. 07:59:56.367 DEBUG Plugin loaded: hexo-generator-archive 07:59:56.367 DEBUG Plugin loaded: hexo-generator-category ... INFO ======================================= ██╗ ██████╗ █████╗ ██████╗ ██╗ ██╗███████╗ ██║██╔════╝██╔══██╗██╔══██╗██║ ██║██╔════╝ ██║██║ ███████║██████╔╝██║ ██║███████╗ ██║██║ ██╔══██║██╔══██╗██║ ██║╚════██║ ██║╚██████╗██║ ██║██║ ██║╚██████╔╝███████║ ╚═╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ============================================= INFO === Checking package dependencies === INFO === Checking theme configurations === INFO === Registering Hexo extensions === 07:59:56.828 DEBUG Script loaded: node_modules/hexo-theme-icarus/scripts/index.js 07:59:56.832 DEBUG Loading database. 07:59:56.954 INFO Start processing 07:59:56.999 DEBUG Processed: _posts/1. hexo博客构建.md 07:59:56.999 DEBUG Processed: _posts/11. 如何实现一个轮播图.md 07:59:56.999 DEBUG Processed: _posts/12. background属性和img标签.md 07:59:56.999 DEBUG Processed: _posts/10. Express+MySQL实现登陆界面.md ... 07:59:57.014 DEBUG Processed: source /img/avatar.png 07:59:57.014 DEBUG Processed: source /img/favicon.svg 07:59:57.014 DEBUG Processed: source /img/logo.svg ... 07:59:58.303 DEBUG Rendering post: /Users/huidizhu/Personal/blog/blog/source/_posts/32. hexo源码分析(一).md 07:59:58.303 DEBUG Rendering post: /Users/huidizhu/Personal/blog/blog/source/_posts/33. hexo源码分析(二).md 07:59:58.367 DEBUG Generator: page 07:59:58.367 DEBUG Generator: post 07:59:58.367 DEBUG Generator: archive 07:59:58.367 DEBUG Generator: category 07:59:58.367 DEBUG Generator: index 07:59:58.401 INFO Files loaded in 1.57 s 07:59:58.407 INFO Deleted: 2020/05/07/29. 浏览器重绘和requestAnimationFrame/index.html 07:59:58.407 INFO Deleted: 2019/08/03/28. vue-cli源码学习2.x/index.html ... 07:59:58.417 INFO Deleted: 2017/10/24/5. 如何实现一个导航栏/index.html 07:59:58.424 DEBUG Rendering HTML post: hexo源码分析(二)/index.html ... 07:59:58.545 DEBUG Rendering HTML archive: archives/index.html 07:59:58.549 DEBUG Rendering HTML archive: archives/page/2/index.html ... 07:59:58.574 DEBUG Rendering HTML category: categories/前端/page/2/index.html 07:59:58.579 DEBUG Rendering HTML category: categories/hexo/index.html ... 07:59:59.323 INFO 96 files generated in 922 ms 07:59:59.344 DEBUG Database saved
可以看到整体的大致流程就是上面的这些,然后了解了这个内容之后就更方便我们找对应的逻辑实现了。
Hexo类 我们承接上文hexo源码分析(一) 中的逻辑介绍,里面对Hexo进行了实例化,并执行了hexo.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 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 class Hexo extends EventEmitter { constructor (base = process.cwd(), args = {} ) { super (); this .base_dir = base + sep; this .public_dir = join (base, 'public' ) + sep; this .source_dir = join (base, 'source' ) + sep; this .plugin_dir = join (base, 'node_modules' ) + sep; this .script_dir = join (base, 'scripts' ) + sep; this .scaffold_dir = join (base, 'scaffolds' ) + sep; this .theme_dir = join (base, 'themes' , defaultConfig.theme ) + sep; this .theme_script_dir = join (this .theme_dir , 'scripts' ) + sep; this .extend = { console : new Console (), deployer : new Deployer (), filter : new Filter (), generator : new Generator (), helper : new Helper (), injector : new Injector (), migrator : new Migrator (), processor : new Processor (), renderer : new Renderer (), tag : new Tag () }; this .database = new Database ({ version : dbVersion, path : join (dbPath, 'db.json' ) }); registerModels (this ); this ._bindLocals (); } 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内部的启动逻辑。里面的逻辑还是很多的,后面会找一些比较关键的核心流程来进行分析一下。
_config.yml 和 _config.[theme-name].yml 1 2 3 4 5 return Promise .each ([ 'load_config' , 'load_theme_config' , ]
_config.yml
和_config.[theme-name].yml
起到配置的作用,是在init过程中引入的,最终会被转换成json结构的配置,然后作为hexo中的一个变量贯穿整个流程。
_config.yml 根目录配置 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 module .exports = async ctx => { if (!ctx.env .init ) return ; const baseDir = ctx.base_dir ; let configPath = ctx.config_path ; const path = await exists (configPath) ? configPath : await findConfigPath (configPath); if (!path) return ; configPath = path; let config = await ctx.render .render ({ path }); if (!config || typeof config !== 'object' ) return ; ctx.log .debug ('Config loaded: %s' , magenta (tildify (configPath))); ctx.config = deepMerge (ctx.config , config); if (!config.theme ) return ; const theme = config.theme .toString (); config.theme = theme; const themeDirFromThemes = join (baseDir, 'themes' , theme) + sep; const themeDirFromNodeModules = join (ctx.plugin_dir , 'hexo-theme-' + theme) + sep; let ignored = []; if (await exists (themeDirFromThemes)) { ctx.theme_dir = themeDirFromThemes; ignored = ['**/themes/*/node_modules/**' , '**/themes/*/.git/**' ]; } else if (await exists (themeDirFromNodeModules)) { ctx.theme_dir = themeDirFromNodeModules; ignored = ['**/node_modules/hexo-theme-*/node_modules/**' , '**/node_modules/hexo-theme-*/.git/**' ]; } ctx.theme_script_dir = join (ctx.theme_dir , 'scripts' ) + sep; ctx.theme = new Theme (ctx, { ignored }); };
上面的代码一些关键的地方做了注释,做的比较核心的两个事情是:
yml后缀的文件转换成 json结构
然后根绝_config.yml中 theme的配置找到对应的 theme路径,为后面load_theme_config.js
的执行做下铺垫。
关于 yml转换成 json结构的设计我会在后面的文章分析 render的时候会分析中间如何实现的。
_config.[theme-name].yml 这个和上面的类似,你可以去hexo/lib/hexo/load_theme_config.js
中查看对应的实现。作为一个变量贯穿整个hexo的运行逻辑。
npm依赖插件加载 1 2 3 4 5 return Promise .each ([ 'load_plugins' ],
这一部分是插件加载的逻辑,我以 icarus主题为例,来看一下都加载了哪些插件,同样是使用--debug
参数来查看加载的插件。
1 2 3 4 5 6 7 8 9 07:59:56.367 DEBUG Plugin loaded: hexo-generator-archive 07:59:56.367 DEBUG Plugin loaded: hexo-generator-category 07:59:56.367 DEBUG Plugin loaded: hexo-generator-index 07:59:56.367 DEBUG Plugin loaded: hexo-generator-tag 07:59:56.367 DEBUG Plugin loaded: hexo-renderer-inferno 07:59:56.455 DEBUG Plugin loaded: hexo-server 07:59:56.471 DEBUG Plugin loaded: hexo-renderer-ejs 07:59:56.471 DEBUG Plugin loaded: hexo-renderer-marked 07:59:56.582 DEBUG Plugin loaded: hexo-renderer-stylus
我们来看这些插件,比如hexo-generator-archive
、hexo-generator-category
、hexo-generator-index
、hexo-generator-tag
比如这些都是对应的页面实现,下面的hexo-renderer-inferno
、hexo-renderer-ejs
这些是对应的模板渲染插件,里面会注册对应的 render方法。
1 2 3 4 5 6 module .exports = ctx => { if (!ctx.env .init || ctx.env .safe ) return ; return loadModules (ctx).then (() => loadScripts (ctx)); };
然后这里先是加载了所有的插件,然后执行对应定义的 script文件。
插件加载 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 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)); }); }); } 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)])); }); }
可以看到里面的加载插件逻辑是根据ctx.base_dir
和ctx.theme_dir
的两个路径,然后去对应的两个路径下面的package.json
文件找依赖项,正则匹配到对应的以hexo-
开头的命名方式。然后调用ctx.loadPlugin(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 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); }
我们回到loadPlugin
的实现,里面又一个很有意思的实现,使用了NodeJS的vm模块,const fn = runInThisContext(script, path)
这部分实际上是创建了一个沙箱,因为引入的插件是外部开发的,因此需要做一层隔离,因此这里使用runInThisContext
来做这件事情,防止获取到hexo内部的变量。
关于沙箱暂时了解不深,就不再展开分析,后续有时间会研究一下。
script执行 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function loadScripts (ctx ) { const baseDirLength = ctx.base_dir .length ; return Promise .filter ([ ctx.theme_script_dir , ctx.script_dir ], scriptDir => { return scriptDir ? exists (scriptDir) : false ; }).map (scriptDir => listDir (scriptDir).map (name => { const path = join (scriptDir, name); return ctx.loadPlugin (path).then (() => { ctx.log .debug ('Script loaded: %s' , displayPath (path, baseDirLength)); }).catch (err => { ctx.log .error ({err}, 'Script load failed: %s' , displayPath (path, baseDirLength)); }); })); }
和插件加载的逻辑很相似,路径是theme_script_dir
和script_dir
,然后我们就可以在里面定义一些我们主题想定义的逻辑,比如npm依赖版本检查等等逻辑。
开篇提出的问题 其实代码读到这里我们已经可以看到里面的插件加载机制了,其实已经可以解决我们在hexo源码分析(一)
中开头提出的问题了。插件的加载逻辑是会分别读取根目录下的和主题文件夹下面的npm依赖,然后加载进来。从代码上看似乎并无问题,那么问题出在哪里了呢?
由于我拉的代码是github上最新的代码,是否是版本的问题呢,我们来看一看博客中node_modules
中的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 function loadModules (ctx ) { return loadModuleList (ctx).map (name => { const path = ctx.resolvePlugin (name); 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)); }); }); } function loadModuleList (ctx ) { const packagePath = join (ctx.base_dir , 'package.json' ); return exists (packagePath).then (exist => { if (!exist) return []; }); }) }
问题很明显了,两处的代码不一样,目前5.4.0
这个版本你的hexo就是上述实现,其中对插件的加载是只读取了根目录下面的package.json
并没有读取主题npm依赖下的文件。我们再看一下hexo
最近的提交记录,发现在十几天前这个bug被修复了feat: load hexo plugin in the theme’s package.json ,但是还没有新发布版本。
至此开篇引出的问题已经解决,可惜错失了一个fix的机会。
小结 虽然问题至此已经发现,但是我们对于hexo的分析还没有分析完毕,我们将继续梳理hexo的主干流程,并尝试分析hexo的设计思想。