使用 Hexo,痛骂 Hexo,理解 Hexo,成为 Hexo。
这篇文章是用来记录我阅读 Hexo 源代码的过程和分析。
版本号
文章写的时候,Hexo 的版本为 7.2.0 :
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 87 88 89 90 91 92 93 94 95 96 97 98 { "name" : "hexo" , "version" : "7.2.0" , "description" : "A fast, simple & powerful blog framework, powered by Node.js." , "main" : "dist/hexo" , "bin" : { "hexo" : "./bin/hexo" } , "scripts" : { "prepublishOnly" : "npm install && npm run clean && npm run build" , "build" : "tsc -b" , "clean" : "tsc -b --clean" , "eslint" : "eslint lib test" , "pretest" : "npm run clean && npm run build" , "test" : "mocha test/scripts/**/*.ts --require ts-node/register" , "test-cov" : "c8 --reporter=lcovonly npm test -- --no-parallel" , "prepare" : "husky install" } , "files" : [ "dist/" , "bin/" ] , "types" : "./dist/hexo/index.d.ts" , "repository" : "hexojs/hexo" , "homepage" : "https://hexo.io/" , "funding" : { "type" : "opencollective" , "url" : "https://opencollective.com/hexo" } , "keywords" : [ "website" , "blog" , "cms" , "framework" , "hexo" ] , "author" : "Tommy Chen <[email protected] > (https://zespia.tw)" , "maintainers" : [ "Abner Chou <[email protected] > (https://abnerchou.me)" ] , "license" : "MIT" , "dependencies" : { "abbrev" : "^2.0.0" , "archy" : "^1.0.0" , "bluebird" : "^3.7.2" , "hexo-cli" : "^4.3.2" , "hexo-front-matter" : "^4.2.1" , "hexo-fs" : "^4.1.3" , "hexo-i18n" : "^2.0.0" , "hexo-log" : "^4.0.1" , "hexo-util" : "^3.3.0" , "js-yaml" : "^4.1.0" , "js-yaml-js-types" : "^1.0.0" , "micromatch" : "^4.0.4" , "moize" : "^6.1.6" , "moment" : "^2.29.1" , "moment-timezone" : "^0.5.34" , "nunjucks" : "^3.2.3" , "picocolors" : "^1.0.0" , "pretty-hrtime" : "^1.0.3" , "resolve" : "^1.22.0" , "strip-ansi" : "^6.0.0" , "text-table" : "^0.2.0" , "tildify" : "^2.0.0" , "titlecase" : "^1.1.3" , "warehouse" : "^5.0.1" } , "devDependencies" : { "0x" : "^5.1.2" , "@types/abbrev" : "^1.1.3" , "@types/bluebird" : "^3.5.37" , "@types/chai" : "^4.3.11" , "@types/js-yaml" : "^4.0.9" , "@types/mocha" : "^10.0.6" , "@types/node" : "^18.11.8 <18.19.9" , "@types/nunjucks" : "^3.2.2" , "@types/rewire" : "^2.5.30" , "@types/sinon" : "^17.0.3" , "@types/text-table" : "^0.2.4" , "c8" : "^9.0.0" , "chai" : "^4.3.6" , "cheerio" : "0.22.0" , "decache" : "^4.6.1" , "eslint" : "^8.48.0" , "eslint-config-hexo" : "^5.0.0" , "hexo-renderer-marked" : "^6.0.0" , "husky" : "^8.0.1" , "lint-staged" : "^15.2.0" , "mocha" : "^10.0.0" , "rewire" : "^7.0.0" , "sinon" : "^17.0.1" , "ts-node" : "^10.9.1" , "typescript" : "^5.3.2" } , "engines" : { "node" : ">=14" } }
Hexo 的入口文件是 dist/hexo/index.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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 "use strict" ;var __importDefault = (this && this .__importDefault ) || function (mod ) { return (mod && mod.__esModule ) ? mod : { "default" : mod }; }; const bluebird_1 = __importDefault (require ("bluebird" ));const path_1 = require ("path" );const tildify_1 = __importDefault (require ("tildify" ));const warehouse_1 = __importDefault (require ("warehouse" ));const picocolors_1 = require ("picocolors" );const events_1 = require ("events" );const hexo_fs_1 = require ("hexo-fs" );const module_1 = __importDefault (require ("module" ));const vm_1 = require ("vm" );const { version } = require ('../../package.json' );const hexo_log_1 = __importDefault (require ("hexo-log" ));const extend_1 = require ("../extend" );const render_1 = __importDefault (require ("./render" ));const register_models_1 = __importDefault (require ("./register_models" ));const post_1 = __importDefault (require ("./post" ));const scaffold_1 = __importDefault (require ("./scaffold" ));const source_1 = __importDefault (require ("./source" ));const router_1 = __importDefault (require ("./router" ));const theme_1 = __importDefault (require ("../theme" ));const locals_1 = __importDefault (require ("./locals" ));const default_config_1 = __importDefault (require ("./default_config" ));const load_database_1 = __importDefault (require ("./load_database" ));const multi_config_path_1 = __importDefault (require ("./multi_config_path" ));const hexo_util_1 = require ("hexo-util" );let resolveSync; const libDir = (0 , path_1.dirname )(__dirname);const dbVersion = 1 ;const stopWatcher = (box ) => { if (box.isWatching ()) box.unwatch (); }; const routeCache = new WeakMap ();const castArray = (obj ) => { return Array .isArray (obj) ? obj : [obj]; };const mergeCtxThemeConfig = (ctx ) => { ... };const createLoadThemeRoute = function (generatorResult, locals, ctx ) { ... };function debounce (func, wait ) { ... }class Hexo extends events_1.EventEmitter { ... }Hexo .lib_dir = libDir + path_1.sep ;Hexo .prototype .lib_dir = Hexo .lib_dir ;Hexo .core_dir = (0 , path_1.dirname )(libDir) + path_1.sep ;Hexo .prototype .core_dir = Hexo .core_dir ;Hexo .version = version;Hexo .prototype .version = Hexo .version ;module .exports = Hexo ;
Hexo
类
重点来看Hexo
类的定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Hexo extends events_1.EventEmitter { constructor (base = process.cwd(), args = {} ) { ... } _bindLocals ( ) { ... } init ( ) { ... } call (name, args, callback ) { ... } model (name, schema ) { ... } resolvePlugin (name, basedir ) { ... } loadPlugin (path, callback ) { ... } _showDrafts ( ) { ... } load (callback ) { ... } watch (callback ) { ... } unwatch ( ) { ... } _generateLocals ( ) { ... } _runGenerators ( ) { ... } _routerRefresh (runningGenerators, useCache ) { ... } _generate (options = {} ) { ... } exit (err ) { ... } execFilter (type, data, options ) { ... } execFilterSync (type, data, options ) { ... } }
构造函数
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 constructor (base = process.cwd(), args = {} ) { super (); this .base_dir = base + path_1.sep ; this .public_dir = (0 , path_1.join )(base, 'public' ) + path_1.sep ; this .source_dir = (0 , path_1.join )(base, 'source' ) + path_1.sep ; this .plugin_dir = (0 , path_1.join )(base, 'node_modules' ) + path_1.sep ; this .script_dir = (0 , path_1.join )(base, 'scripts' ) + path_1.sep ; this .scaffold_dir = (0 , path_1.join )(base, 'scaffolds' ) + path_1.sep ; this .theme_dir = (0 , path_1.join )(base, 'themes' , default_config_1.default .theme ) + path_1.sep ; this .theme_script_dir = (0 , path_1.join )(this .theme_dir , 'scripts' ) + path_1.sep ; this .env = { args, debug : Boolean (args.debug ), safe : Boolean (args.safe ), silent : Boolean (args.silent ), env : process.env .NODE_ENV || 'development' , version, cmd : args._ ? args._ [0 ] : '' , init : false }; this .extend = { console : new extend_1.Console (), deployer : new extend_1.Deployer (), filter : new extend_1.Filter (), generator : new extend_1.Generator (), helper : new extend_1.Helper (), highlight : new extend_1.Highlight (), injector : new extend_1.Injector (), migrator : new extend_1.Migrator (), processor : new extend_1.Processor (), renderer : new extend_1.Renderer (), tag : new extend_1.Tag () }; this .config = Object .assign ({}, default_config_1.default ); this .log = (0 , hexo_log_1.default )(this .env ); this .render = new render_1.default (this ); this .route = new router_1.default (); this .post = new post_1.default (this ); this .scaffold = new scaffold_1.default (this ); this ._dbLoaded = false ; this ._isGenerating = false ; const dbPath = args.output || base; if (/^(init|new|g|publish|s|deploy|render|migrate)/ .test (this .env .cmd )) { this .log .d (`Writing database to ${(0 , path_1.join)(dbPath, 'db.json' )} ` ); } this .database = new warehouse_1.default ({ version : dbVersion, path : (0 , path_1.join )(dbPath, 'db.json' ) }); const mcp = (0 , multi_config_path_1.default )(this ); this .config_path = args.config ? mcp (base, args.config , args.output ) : (0 , path_1.join )(base, '_config.yml' ); (0 , register_models_1.default )(this ); this .source = new source_1.default (this ); this .theme = new theme_1.default (this ); this .locals = new locals_1.default (); this ._bindLocals (); }
路径
1 2 3 4 5 6 7 8 this .base_dir = base + path_1.sep ;this .public_dir = (0 , path_1.join )(base, 'public' ) + path_1.sep ;this .source_dir = (0 , path_1.join )(base, 'source' ) + path_1.sep ;this .plugin_dir = (0 , path_1.join )(base, 'node_modules' ) + path_1.sep ;this .script_dir = (0 , path_1.join )(base, 'scripts' ) + path_1.sep ;this .scaffold_dir = (0 , path_1.join )(base, 'scaffolds' ) + path_1.sep ;this .theme_dir = (0 , path_1.join )(base, 'themes' , default_config_1.default .theme ) + path_1.sep ;this .theme_script_dir = (0 , path_1.join )(this .theme_dir , 'scripts' ) + path_1.sep ;
base_dir
:项目的基础目录
public_dir
:项目生成的静态文件存放的目录(./public
),当运行hexo generate
时生成的所有静态文件都会被放在这个目录下
source_dir
:Markdown文章(./source
)
plugin_dir
:插件(./node_modules
),Hexo的插件机制基于Node.js的模块系统
script_dir
:脚本(./scripts
),Hexo启动时会自动执行这些JavaScript文件
scaffold_dir
:脚手架(./scaffolds
)是一种模板,可以用它快速创建新的文章
theme_dir
:主题(./themes
)
theme_script_dir
:类似于脚本(./themes/scripts
)
环境信息
1 2 3 4 5 6 7 8 9 10 this .env = { args, debug : Boolean (args.debug ), safe : Boolean (args.safe ), silent : Boolean (args.silent ), env : process.env .NODE_ENV || 'development' , version, cmd : args._ ? args._ [0 ] : '' , init : false };
包含了关于 Hexo 运行环境的信息,如调试模式、安全模式、静默模式、环境变量、版本号、命令、是否初始化等。
扩展
1 2 3 4 5 6 7 8 9 10 11 12 13 this .extend = { console : new extend_1.Console (), deployer : new extend_1.Deployer (), filter : new extend_1.Filter (), generator : new extend_1.Generator (), helper : new extend_1.Helper (), highlight : new extend_1.Highlight (), injector : new extend_1.Injector (), migrator : new extend_1.Migrator (), processor : new extend_1.Processor (), renderer : new extend_1.Renderer (), tag : new extend_1.Tag () };
这些是 Hexo 的核心组件,以后会详细介绍。
其实可以先行阅读 Hexo 官方文档 ,虽然初次看可能很难看懂,但是看了总是会在未来的某个时刻头脑一亮埋下伏笔。
配置
1 this .config = Object .assign ({}, default_config_1.default );
默认配置的内容:
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 "use strict" ;module .exports = { title : 'Hexo' , subtitle : '' , description : '' , author : 'John Doe' , language : 'en' , timezone : '' , url : 'http://example.com' , root : '/' , permalink : ':year/:month/:day/:title/' , permalink_defaults : {}, pretty_urls : { trailing_index : true , trailing_html : true }, source_dir : 'source' , public_dir : 'public' , tag_dir : 'tags' , archive_dir : 'archives' , category_dir : 'categories' , code_dir : 'downloads/code' , i18n_dir : ':lang' , skip_render : [], new_post_name : ':title.md' , default_layout : 'post' , titlecase : false , external_link : { enable : true , field : 'site' , exclude : '' }, filename_case : 0 , render_drafts : false , post_asset_folder : false , relative_link : false , future : true , syntax_highlighter : 'highlight.js' , highlight : { auto_detect : false , line_number : true , tab_replace : '' , wrap : true , exclude_languages : [], language_attr : false , hljs : false , line_threshold : 0 , first_line_number : 'always1' , strip_indent : true }, prismjs : { preprocess : true , line_number : true , tab_replace : '' , exclude_languages : [], strip_indent : true }, default_category : 'uncategorized' , category_map : {}, tag_map : {}, date_format : 'YYYY-MM-DD' , time_format : 'HH:mm:ss' , updated_option : 'mtime' , per_page : 10 , pagination_dir : 'page' , theme : 'landscape' , server : { cache : false }, deploy : {}, ignore : [], meta_generator : true };
看着很熟悉吧,这些配置项都是我们初始化 Hexo 项目时,出现在根目录的 _config.yml
内的配置项。
可见 Hexo 官方文档 。
日志
1 this .log = (0 , hexo_log_1.default )(this .env );
这里的 hexo_log_1
是:
1 const hexo_log_1 = __importDefault (require ("hexo-log" ));
index.js
中大量的导入变量都是这样命名的。
渲染
1 this .render = new render_1.default (this );
可见 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 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 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 "use strict" ;var __importDefault = (this && this .__importDefault ) || function (mod ) { return (mod && mod.__esModule ) ? mod : { "default" : mod }; }; const path_1 = require ("path" );const bluebird_1 = __importDefault (require ("bluebird" ));const hexo_fs_1 = require ("hexo-fs" );const getExtname = (str ) => { if (typeof str !== 'string' ) return '' ; const ext = (0 , path_1.extname )(str); return ext.startsWith ('.' ) ? ext.slice (1 ) : ext; }; const toString = (result, options ) => { if (!Object .prototype .hasOwnProperty .call (options, 'toString' ) || typeof result === 'string' ) return result; if (typeof options.toString === 'function' ) { return options.toString (result); } else if (typeof result === 'object' ) { return JSON .stringify (result); } else if (result.toString ) { return result.toString (); } return result; }; class Render { constructor (ctx ) { this .context = ctx; this .renderer = ctx.extend .renderer ; } isRenderable (path ) { return this .renderer .isRenderable (path); } isRenderableSync (path ) { return this .renderer .isRenderableSync (path); } getOutput (path ) { return this .renderer .getOutput (path); } getRenderer (ext, sync ) { return this .renderer .get (ext, sync); } getRendererSync (ext ) { return this .getRenderer (ext, true ); } render (data, options, callback ) { if (!callback && typeof options === 'function' ) { callback = options; options = {}; } const ctx = this .context ; let ext = '' ; let promise; if (!data) return bluebird_1.default .reject (new TypeError ('No input file or string!' )); if (data.text != null ) { promise = bluebird_1.default .resolve (data.text ); } else if (!data.path ) { return bluebird_1.default .reject (new TypeError ('No input file or string!' )); } else { promise = (0 , hexo_fs_1.readFile )(data.path ); } return promise.then (text => { data.text = text; ext = data.engine || getExtname (data.path ); if (!ext || !this .isRenderable (ext)) return text; const renderer = this .getRenderer (ext); return Reflect .apply (renderer, ctx, [data, options]); }).then (result => { result = toString (result, data); if (data.onRenderEnd ) { return data.onRenderEnd (result); } return result; }).then (result => { const output = this .getOutput (ext) || ext; return ctx.execFilter (`after_render:${output} ` , result, { context : ctx, args : [data] }); }).asCallback (callback); } renderSync (data, options = {} ) { if (!data) throw new TypeError ('No input file or string!' ); const ctx = this .context ; if (data.text == null ) { if (!data.path ) throw new TypeError ('No input file or string!' ); data.text = (0 , hexo_fs_1.readFileSync )(data.path ); } if (data.text == null ) throw new TypeError ('No input file or string!' ); const ext = data.engine || getExtname (data.path ); let result; if (ext && this .isRenderableSync (ext)) { const renderer = this .getRendererSync (ext); result = Reflect .apply (renderer, ctx, [data, options]); } else { result = data.text ; } const output = this .getOutput (ext) || ext; result = toString (result, data); if (data.onRenderEnd ) { result = data.onRenderEnd (result); } return ctx.execFilterSync (`after_render:${output} ` , result, { context : ctx, args : [data] }); } } module .exports = Render ;
bluebird
库是一个流行的 JavaScript Promise 库,它提供了一种更加健壮、高效和优雅的方式来处理异步操作。
在传统的基于回调函数的异步编程中,存在着回调地狱、代码耦合等问题。Promise 的出现解决了这些问题,使着异步代码更加易于理解和维护。而 bluebird
在 Promise 的基础上提供了更多的增强功能和优化。
Render
类包含两个主要方法:render
和 renderSync
。
render
方法是一个异步方法,首先检查文件是否可根据其扩展名进行渲染。如果可以,它会获取相应的渲染器并将其应用于数据。渲染后的结果将通过一个过滤器,并在回调函数中返回。
renderSync
方法是 render
的同步版本。工作方式类似,但使用同步文件读取,并使用异常代替回调和 Promise 处理错误。
其他部分包括:
getExtname
是一个辅助函数,用于从文件路径中提取文件扩展名。
toString
是一个辅助函数,根据提供的选项将结果对象转换为字符串表示。
Render
类的构造函数接收 Hexo
上下文 ctx
,并从中初始化 renderer
属性。
isRenderable
和 isRenderableSync
方法检查给定扩展名的文件是否可渲染。
getOutput
方法获取给定输入扩展名的输出扩展名。
getRenderer
和 getRendererSync
方法获取给定扩展名的相应渲染器。
这段代码为 Hexo 提供了一种基于文件扩展名渲染文件的方式,使用每种扩展名对应的渲染器。
路由
1 this .route = new router_1.default ();
这行代码实例化了 Router
类的对象,用于管理路由。在 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 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 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 "use strict" ;var __importDefault = (this && this .__importDefault ) || function (mod ) { return (mod && mod.__esModule ) ? mod : { "default" : mod }; }; const events_1 = require ("events" );const bluebird_1 = __importDefault (require ("bluebird" ));const stream_1 = __importDefault (require ("stream" ));const { Readable } = stream_1.default ;class RouteStream extends Readable { constructor (data ) { super ({ objectMode : true }); this ._data = data.data ; this ._ended = false ; this .modified = data.modified ; } _toBuffer (data ) { if (data instanceof Buffer ) { return data; } if (typeof data === 'object' ) { data = JSON .stringify (data); } if (typeof data === 'string' ) { return Buffer .from (data); } return null ; } _read ( ) { const data = this ._data ; if (typeof data !== 'function' ) { const bufferData = this ._toBuffer (data); if (bufferData) { this .push (bufferData); } this .push (null ); return ; } if (this ._ended ) return false ; this ._ended = true ; data ().then (data => { if (data instanceof stream_1.default && data.readable ) { data.on ('data' , d => { this .push (d); }); data.on ('end' , () => { this .push (null ); }); data.on ('error' , err => { this .emit ('error' , err); }); } else { const bufferData = this ._toBuffer (data); if (bufferData) { this .push (bufferData); } this .push (null ); } }).catch (err => { this .emit ('error' , err); this .push (null ); }); } } const _format = (path ) => { path = path || '' ; if (typeof path !== 'string' ) throw new TypeError ('path must be a string!' ); path = path .replace (/^\/+/ , '' ) .replace (/\\/g , '/' ) .replace (/\?.*$/ , '' ); if (!path || path.endsWith ('/' )) { path += 'index.html' ; } return path; }; class Router extends events_1.EventEmitter { constructor ( ) { super (); this .routes = {}; } list ( ) { const { routes } = this ; return Object .keys (routes).filter (key => routes[key]); } format (path ) { return _format (path); } get (path ) { if (typeof path !== 'string' ) throw new TypeError ('path must be a string!' ); const data = this .routes [this .format (path)]; if (data == null ) return ; return new RouteStream (data); } isModified (path ) { if (typeof path !== 'string' ) throw new TypeError ('path must be a string!' ); const data = this .routes [this .format (path)]; return data ? data.modified : false ; } set (path, data ) { if (typeof path !== 'string' ) throw new TypeError ('path must be a string!' ); if (data == null ) throw new TypeError ('data is required!' ); let obj; if (typeof data === 'object' && data.data != null ) { obj = data; } else { obj = { data, modified : true }; } if (typeof obj.data === 'function' ) { if (obj.data .length ) { obj.data = bluebird_1.default .promisify (obj.data ); } else { obj.data = bluebird_1.default .method (obj.data ); } } path = this .format (path); this .routes [path] = { data : obj.data , modified : obj.modified == null ? true : obj.modified }; this .emit ('update' , path); return this ; } remove (path ) { if (typeof path !== 'string' ) throw new TypeError ('path must be a string!' ); path = this .format (path); this .routes [path] = null ; this .emit ('remove' , path); return this ; } } module .exports = Router ;
这段代码定义了一个名为 Router
的类和一个名为 RouteStream
的类。它们在 Hexo 框架中用于管理路由和从路由读取数据流。
RouteStream
类:
继承自 Node.js 内置的 stream.Readable
类,因此它是一个可读流。
构造函数接收一个 data
对象,该对象包含 data
属性和 modified
属性。
_read()
方法是可读流的实现。如果 data
是一个函数,它会尝试从该函数中获取数据。否则,它会将 data
推送到流中。
_toBuffer()
是一个内部方法,用于将数据转换为 Buffer
。
Router
类:
继承自 EventEmitter
,因此可以发射和监听事件。
routes
属性是一个对象,用于存储路径及其对应的数据。
list()
方法返回所有已注册路径的列表。
format(path)
方法用于格式化路径,移除前导斜杠、替换反斜杠,并为以斜杠结尾的路径添加 index.html
。
get(path)
方法用于获取给定路径的数据流。如果路径不存在或数据为 null
,则返回 undefined
。
isModified(path)
方法用于检查给定路径的数据是否已修改。
set(path, data)
方法用于设置给定路径的数据。如果 data
是一个函数,它会使用 Bluebird
库将其转换为 Promise。该方法还会触发 update
事件。
remove(path)
方法用于移除给定路径的数据。它会将路径对应的值设置为 null
,并触发 remove
事件。
Node.js 的 EventEmitter
是一个模块,提供基于事件驱动的编程方式,是 Node.js 很多核心模块和第三方模块的基础,也是 Node.js 中非常重要的一个概念和设计模式。
EventEmitter
是一个构造函数,可以创建一个新的事件发射器对象。这个对象可以发射命名事件,并且可以通过添加事件监听器来监听特定事件。当事件被发射时,所有监听该事件的函数回调都会被同步地调用。
EventEmitter
对象有以下常用方法:
emitter.on(eventName, listener)
为指定事件注册一个监听器。
emitter.once(eventName, listener)
为指定事件注册一个一次性的监听器,触发后就会被移除。
emitter.off(eventName, listener)
移除指定事件的监听器。
emitter.emit(eventName, [...args])
发射指定事件,传递参数给监听器回调函数。
Hexo 官方文档 中也提到了 EventEmitter
。
博客文章
1 this .post = new post_1.default (this );
可见 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 26 27 28 "use strict" ;var __importDefault = (this && this .__importDefault ) || function (mod ) { return (mod && mod.__esModule ) ? mod : { "default" : mod }; }; const assert_1 = __importDefault (require ("assert" ));const moment_1 = __importDefault (require ("moment" ));const bluebird_1 = __importDefault (require ("bluebird" ));const path_1 = require ("path" );const picocolors_1 = require ("picocolors" );const js_yaml_1 = require ("js-yaml" );const hexo_util_1 = require ("hexo-util" );const hexo_fs_1 = require ("hexo-fs" );const hexo_front_matter_1 = require ("hexo-front-matter" );const preservedKeys = ['title' , 'slug' , 'path' , 'layout' , 'date' , 'content' ];const rHexoPostRenderEscape = /<hexoPostRenderCodeBlock>([\s\S]+?)<\/hexoPostRenderCodeBlock>/g ;const rSwigPlaceHolder = /(?:<|<)!--swig\uFFFC(\d+)--(?:>|>)/g ;const rCodeBlockPlaceHolder = /(?:<|<)!--code\uFFFC(\d+)--(?:>|>)/g ;const STATE_PLAINTEXT = Symbol ('plaintext' );const STATE_SWIG_VAR = Symbol ('swig_var' );const STATE_SWIG_COMMENT = Symbol ('swig_comment' );const STATE_SWIG_TAG = Symbol ('swig_tag' );const STATE_SWIG_FULL_TAG = Symbol ('swig_full_tag' );const isNonWhiteSpaceChar = (char ) => char !== '\r' && char !== '\n' && char !== '\t' && char !== '\f' && char !== '\v' && char !== ' ' ;
PostRenderEscape
类用于处理 Swig/Nunjucks 标签和代码块的转义和还原:
Swig 和 Nunjucks 是两种流行的模板引擎。
主要语法包括:
{% ... %}
:用于执行语句,如条件语句和循环语句。
{{ ... }}
:用于输出变量值。
在渲染文章内容时,Hexo 需要先对 Swig/Nunjucks 标签和代码块进行转义,防止被误解析,然后再使用相应的渲染器(如 Markdown)进行渲染。渲染完成后,需要将转义的内容还原回来。
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 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 class PostRenderEscape { constructor ( ) { this .stored = []; } static escapeContent (cache, flag, str ) { return `<!--${flag} \uFFFC${cache.push(str) - 1 } -->` ; } static restoreContent (cache ) { return (_, index ) => { (0 , assert_1.default )(cache[index]); const value = cache[index]; cache[index] = null ; return value; }; } restoreAllSwigTags (str ) { const restored = str.replace (rSwigPlaceHolder, PostRenderEscape .restoreContent (this .stored )); return restored; } restoreCodeBlocks (str ) { return str.replace (rCodeBlockPlaceHolder, PostRenderEscape .restoreContent (this .stored )); } escapeCodeBlocks (str ) { return str.replace (rHexoPostRenderEscape, (_, content ) => PostRenderEscape .escapeContent (this .stored , 'code' , content)); } escapeAllSwigTags (str ) { if (!/(\{\{.+?\}\})|(\{#.+?#\})|(\{%.+?%\})/ s.test (str)) { return str; } let state = STATE_PLAINTEXT ; let buffer = '' ; let output = '' ; let swig_tag_name_begin = false ; let swig_tag_name_end = false ; let swig_tag_name = '' ; let swig_full_tag_start_buffer = '' ; const { length } = str; for (let idx = 0 ; idx < length; idx++) { const char = str[idx]; const next_char = str[idx + 1 ]; if (state === STATE_PLAINTEXT ) { if (char === '{' ) { if (next_char === '{' ) { state = STATE_SWIG_VAR ; idx++; } else if (next_char === '#' ) { state = STATE_SWIG_COMMENT ; idx++; } else if (next_char === '%' ) { state = STATE_SWIG_TAG ; idx++; swig_tag_name = '' ; swig_full_tag_start_buffer = '' ; swig_tag_name_begin = false ; swig_tag_name_end = false ; } else { output += char; } } else { output += char; } } else if (state === STATE_SWIG_TAG ) { if (char === '%' && next_char === '}' ) { idx++; if (swig_tag_name !== '' && str.includes (`end${swig_tag_name} ` )) { state = STATE_SWIG_FULL_TAG ; } else { swig_tag_name = '' ; state = STATE_PLAINTEXT ; output += PostRenderEscape .escapeContent (this .stored , 'swig' , `{%${buffer} %}` ); } buffer = '' ; } else { buffer = buffer + char; swig_full_tag_start_buffer = swig_full_tag_start_buffer + char; if (isNonWhiteSpaceChar (char)) { if (!swig_tag_name_begin && !swig_tag_name_end) { swig_tag_name_begin = true ; } if (swig_tag_name_begin) { swig_tag_name += char; } } else { if (swig_tag_name_begin === true ) { swig_tag_name_begin = false ; swig_tag_name_end = true ; } } } } else if (state === STATE_SWIG_VAR ) { if (char === '}' && next_char === '}' ) { idx++; state = STATE_PLAINTEXT ; output += PostRenderEscape .escapeContent (this .stored , 'swig' , `{{${buffer} }}` ); buffer = '' ; } else { buffer = buffer + char; } } else if (state === STATE_SWIG_COMMENT ) { if (char === '#' && next_char === '}' ) { idx++; state = STATE_PLAINTEXT ; buffer = '' ; } } else if (state === STATE_SWIG_FULL_TAG ) { if (char === '{' && next_char === '%' ) { let swig_full_tag_end_buffer = '' ; let _idx = idx + 2 ; for (; _idx < length; _idx++) { const _char = str[_idx]; const _next_char = str[_idx + 1 ]; if (_char === '%' && _next_char === '}' ) { _idx++; break ; } swig_full_tag_end_buffer = swig_full_tag_end_buffer + _char; } if (swig_full_tag_end_buffer.includes (`end${swig_tag_name} ` )) { state = STATE_PLAINTEXT ; output += PostRenderEscape .escapeContent (this .stored , 'swig' , `{%${swig_full_tag_start_buffer} %}${buffer} {%${swig_full_tag_end_buffer} %}` ); idx = _idx; swig_full_tag_start_buffer = '' ; swig_full_tag_end_buffer = '' ; buffer = '' ; } else { buffer += char; } } else { buffer += char; } } } return output; } }
escapeContent
:用于将给定的字符串转义为占位符,并存储在缓存数组中。
转义后的占位符格式为:<!--flag\uFFFC${index}-->
。
restoreContent
:用于将占位符还原为原始字符串。
restoreAllSwigTags
:使用正则表达式替换所有 Swig 标签占位符,调用 restoreContent
方法还原原始内容。
restoreCodeBlocks
:使用正则表达式替换所有代码块占位符,调用 restoreContent
方法还原原始内容。
escapeCodeBlocks
:使用正则表达式匹配所有 Markdown 代码块,调用 escapeContent
方法将它们转义为占位符。
escapeAllSwigTags
:用于转义 Swig 模板标签。首先检查输入字符串是否包含 Swig 标签,然后使用有限状态机的方式遍历每个字符,识别出 Swig 标签的类型和内容,并调用 escapeContent
进行转义。
有限状态机(Finite State Machine;FSM)是一种数学计算模型,用于描述具有有限个状态以及基于事件进行状态转移的系统。
一个有限状态机包含以下几个基本组成部分:
有限状态集合(States)。
输入事件/符号集合(Input Events/Symbols)。
一个初始状态(Initial State)。
状态转移函数(State Transition Function)。
终止状态集合(Final States)。
有限状态机的工作原理:
机器初始处于一个确定的初始状态。
机器接收一个输入事件/符号。
根据当前状态和输入事件/符号,机器按照状态转移函数进行状态转移。
机器进入新的状态,等待下一个输入事件/符号。
重复以上过程,直到进入某个终止状态或发生无法处理的输入事件/符号。
escapeAllSwigTags
使用了一个有限状态机来精确识别和处理各种 Swig 标签。它定义了五种状态,通过遍历输入字符串,根据当前状态和字符进行状态转移,从而正确处理嵌套、注释等复杂情况。
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 const prepareFrontMatter = (data, jsonMode ) => { for (const [key, item] of Object .entries (data)) { if (moment_1.default .isMoment (item)) { data[key] = item.utc ().format ('YYYY-MM-DD HH:mm:ss' ); } else if (moment_1.default .isDate (item)) { data[key] = moment_1.default .utc (item).format ('YYYY-MM-DD HH:mm:ss' ); } else if (typeof item === 'string' ) { if (jsonMode || item.includes (':' ) || item.startsWith ('#' ) || item.startsWith ('!!' ) || item.includes ('{' ) || item.includes ('}' ) || item.includes ('[' ) || item.includes (']' ) || item.includes ('\'' ) || item.includes ('"' )) data[key] = `"${item.replace(/"/g, '\\" ')}"`; } } return data; }; const removeExtname = (str) => { return str.substring(0, str.length - (0, path_1.extname)(str).length); }; const createAssetFolder = (path, assetFolder) => { if (!assetFolder) return bluebird_1.default.resolve(); const target = removeExtname(path); if ((0, path_1.basename)(target) === ' index') return bluebird_1.default.resolve(); return (0, hexo_fs_1.exists)(target).then(exist => { if (!exist) return (0, hexo_fs_1.mkdirs)(target); }); };
prepareFrontMatter
方法用于处理文章的 Front Matter 元数据。
Front Matter 是指在 Markdown 或其他 markup 文件的头部添加的一组元数据。它通常被包裹在两组连续的三短横线之间,例如:
1 2 3 4 5 6 7 8 9 --- title: My Blog Post date: 2023-05-26 tags: [blog, coding] --- # My Blog Post This is the content of the blog post...
Front Matter通常采用 YAML 或 JSON 格式来表示键值对的元数据。Front Matter 中的数据可以在渲染 markdown 文件时被解析和使用,例如在博客系统中用于生成文章的元数据、URL等。不同的静态站点生成器和 markdown 渲染器支持不同的 Front Matter 语法和元数据项。
它遍历 data
对象的每个键值对。如果值是 moment
对象或 Date
对象,则将其转换为 UTC 时区的 YYYY-MM-DD HH:mm:ss
格式的字符串。如果值是字符串,并且符合某些条件(包含特殊字符或需要转义),则将其用双引号包裹起来,并对双引号进行转义。
这个函数主要用于在生成文章时正确处理 Front Matter 中的日期和特殊字符。
removeExtname
方法用于从文件路径中移除扩展名。它使用 path.extname
获取扩展名,然后从原始路径中截取并返回不包含扩展名的部分。
createAssetFolder
方法用于在生成文章时创建与文章相关联的资源文件夹(如果配置启用了该选项)。
如果 assetFolder
为 false
,则直接返回一个已解决的 Promise。否则它会调用 removeExtname
来获取不包含扩展名的文件路径。
如果这个路径的基础名称是 index
,则直接返回一个已解决的 Promise(因为 index
通常是默认文件名,不需要创建文件夹)。否则,它会检查该路径是否存在,如果不存在,则创建该路径作为文件夹。
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 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 class Post { constructor (context ) { this .context = context; } create (data, replace, callback ) { if (!callback && typeof replace === 'function' ) { callback = replace; replace = false ; } const ctx = this .context ; const { config } = ctx; data.slug = (0 , hexo_util_1.slugize )((data.slug || data.title ).toString (), { transform : config.filename_case }); data.layout = (data.layout || config.default_layout ).toLowerCase (); data.date = data.date ? (0 , moment_1.default )(data.date ) : (0 , moment_1.default )(); return bluebird_1.default .all ([ ctx.execFilter ('new_post_path' , data, { args : [replace], context : ctx }), this ._renderScaffold (data) ]).spread ((path, content ) => { const result = { path, content }; return bluebird_1.default .all ([ (0 , hexo_fs_1.writeFile )(path, content), createAssetFolder (path, config.post_asset_folder ) ]).then (() => { ctx.emit ('new' , result); return result; }); }).asCallback (callback); } _getScaffold (layout ) { const ctx = this .context ; return ctx.scaffold .get (layout).then (result => { if (result != null ) return result; return ctx.scaffold .get ('normal' ); }); } _renderScaffold (data ) { const { tag } = this .context .extend ; let splitted; return this ._getScaffold (data.layout ).then (scaffold => { splitted = (0 , hexo_front_matter_1.split )(scaffold); const jsonMode = splitted.separator .startsWith (';' ); const frontMatter = prepareFrontMatter (Object .assign ({}, data), jsonMode); return tag.render (splitted.data , frontMatter); }).then (frontMatter => { const { separator } = splitted; const jsonMode = separator.startsWith (';' ); const obj = jsonMode ? JSON .parse (`{${frontMatter} }` ) : (0 , js_yaml_1.load )(frontMatter); Object .keys (data) .filter (key => !preservedKeys.includes (key) && obj[key] == null ) .forEach (key => { obj[key] = data[key]; }); let content = '' ; if (splitted.prefixSeparator ) content += `${separator} \n` ; content += (0 , hexo_front_matter_1.stringify )(obj, { mode : jsonMode ? 'json' : '' }); content += splitted.content ; if (data.content ) { content += `\n${data.content} ` ; } return content; }); } publish (data, replace, callback ) { if (!callback && typeof replace === 'function' ) { callback = replace; replace = false ; } if (data.layout === 'draft' ) data.layout = 'post' ; const ctx = this .context ; const { config } = ctx; const draftDir = (0 , path_1.join )(ctx.source_dir , '_drafts' ); const slug = (0 , hexo_util_1.slugize )(data.slug .toString (), { transform : config.filename_case }); data.slug = slug; const regex = new RegExp (`^${(0 , hexo_util_1.escapeRegExp)(slug)} (?:[^\\/\\\\]+)` ); let src = '' ; const result = {}; data.layout = (data.layout || config.default_layout ).toLowerCase (); return (0 , hexo_fs_1.listDir )(draftDir).then (list => { const item = list.find (item => regex.test (item)); if (!item) throw new Error (`Draft "${slug} " does not exist.` ); src = (0 , path_1.join )(draftDir, item); return (0 , hexo_fs_1.readFile )(src); }).then (content => { Object .assign (data, (0 , hexo_front_matter_1.parse )(content)); data.content = data._content ; data._content = undefined ; return this .create (data, replace); }).then (post => { result.path = post.path ; result.content = post.content ; return (0 , hexo_fs_1.unlink )(src); }).then (() => { if (!config.post_asset_folder ) return ; const assetSrc = removeExtname (src); const assetDest = removeExtname (result.path ); return (0 , hexo_fs_1.exists )(assetSrc).then (exist => { if (!exist) return ; return (0 , hexo_fs_1.copyDir )(assetSrc, assetDest).then (() => (0 , hexo_fs_1.rmdir )(assetSrc)); }); }).thenReturn (result).asCallback (callback); } render (source, data = {}, callback ) { const ctx = this .context ; const { config } = ctx; const { tag } = ctx.extend ; const ext = data.engine || (source ? (0 , path_1.extname )(source) : '' ); let promise; if (data.content != null ) { promise = bluebird_1.default .resolve (data.content ); } else if (source) { promise = (0 , hexo_fs_1.readFile )(source); } else { return bluebird_1.default .reject (new Error ('No input file or string!' )).asCallback (callback); } const isPost = !data.source || ['html' , 'htm' ].includes (ctx.render .getOutput (data.source )); if (!isPost) { return promise.then (content => { data.content = content; ctx.log .debug ('Rendering file: %s' , (0 , picocolors_1.magenta )(source)); return ctx.render .render ({ text : data.content , path : source, engine : data.engine , toString : true }); }).then (content => { data.content = content; return data; }).asCallback (callback); } let disableNunjucks = ext && ctx.render .renderer .get (ext) && !!ctx.render .renderer .get (ext).disableNunjucks ; if (typeof data.disableNunjucks === 'boolean' ) disableNunjucks = data.disableNunjucks ; const cacheObj = new PostRenderEscape (); return promise.then (content => { data.content = content; return ctx.execFilter ('before_post_render' , data, { context : ctx }); }).then (() => { data.content = cacheObj.escapeCodeBlocks (data.content ); if (disableNunjucks === false ) { data.content = cacheObj.escapeAllSwigTags (data.content ); } const options = data.markdown || {}; if (!config.syntax_highlighter ) options.highlight = null ; ctx.log .debug ('Rendering post: %s' , (0 , picocolors_1.magenta )(source)); return ctx.render .render ({ text : data.content , path : source, engine : data.engine , toString : true , onRenderEnd (content ) { data.content = cacheObj.restoreAllSwigTags (content); if (disableNunjucks) return data.content ; return tag.render (data.content , data); } }, options); }).then (content => { data.content = cacheObj.restoreCodeBlocks (content); return ctx.execFilter ('after_post_render' , data, { context : ctx }); }).asCallback (callback); } } module .exports = Post ;
Post
类包含了在 Hexo 框架中创建、发布和渲染文章的和新方法。
create(data, replace, callback)
:创建一篇新文章。
首先根据配置和传入的数据准备文章的 slug、布局和日期。
然后通过执行 new_post_path
过滤器获取文章路径,并调用 _renderScaffold
方法渲染文章内容。
最后将渲染后的内容写入文件,并根据配置创建相关的资源文件夹。
_getScaffold(layout)
和 _renderScaffold(data)
:
_getScaffold
方法用于获取指定布局的脚手架。
_renderScaffold
方法用于渲染脚手架,并将传入的数据合并到模板中。
先解析 Front Matter,然后使用标签插件渲染 Front Matter。最后将渲染后的 Front Matter 和原始内容合并,返回最终的文章内容。
publish(data, replace, callback)
:将草稿文章发布为正式文章。
首先从 _drafts
目录查找对应的草稿文件,读取其内容。
然后将草稿内容与传入的数据合并,调用 create
方法创建正式文章。
最后删除原始的草稿文件,并根据配置复制相关的资源文件夹。
render(source, data, callback)
:渲染文章内容。
首先读取源文件或使用传入的内容。
然后执行 before_post_render
过滤器。
接着对代码块和 Swig/Nunjucks 标签进行转义,使用 Markdown 或其他渲染器渲染文章内容。
渲染完成后,将转义的内容还原,并执行 after_post_render
过滤器。
如果禁用了 Nunjucks,则返回渲染后的内容;否则使用标签插件渲染一次。
脚手架
脚手架文件是创建新文章或页面时使用的模板文件。
可见 Hexo 官方文档 。
1 this .scaffold = new scaffold_1.default (this );
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 "use strict" ;const path_1 = require ("path" );const hexo_fs_1 = require ("hexo-fs" );class Scaffold { constructor (context ) { this .context = context; this .scaffoldDir = context.scaffold_dir ; this .defaults = { normal : [ '---' , 'layout: {{ layout }}' , 'title: {{ title }}' , 'date: {{ date }}' , 'tags:' , '---' ].join ('\n' ) }; } _listDir ( ) { const { scaffoldDir } = this ; return (0 , hexo_fs_1.exists )(scaffoldDir).then (exist => { if (!exist) return []; return (0 , hexo_fs_1.listDir )(scaffoldDir, { ignorePattern : /^_|\/_/ }); }).map (item => ({ name : item.substring (0 , item.length - (0 , path_1.extname )(item).length ), path : (0 , path_1.join )(scaffoldDir, item) })); } _getScaffold (name ) { return this ._listDir ().then (list => list.find (item => item.name === name)); } get (name, callback ) { return this ._getScaffold (name).then (item => { if (item) { return (0 , hexo_fs_1.readFile )(item.path ); } return this .defaults [name]; }).asCallback (callback); } set (name, content, callback ) { const { scaffoldDir } = this ; return this ._getScaffold (name).then (item => { let path = item ? item.path : (0 , path_1.join )(scaffoldDir, name); if (!(0 , path_1.extname )(path)) path += '.md' ; return (0 , hexo_fs_1.writeFile )(path, content); }).asCallback (callback); } remove (name, callback ) { return this ._getScaffold (name).then (item => { if (!item) return ; return (0 , hexo_fs_1.unlink )(item.path ); }).asCallback (callback); } } module .exports = Scaffold ;
Scaffold
类在 Hexo 框架中用于管理 Scaffold 模板文件,也就是脚手架文件。脚手架是用于生成新文章或页面的默认模板。
constructor(context)
:初始化了 scaffoldDir
属性,指定脚手架文件所在的目录。它还定义了一个 defaults
对象,包含了默认的脚手架内容。
_listDir()
:用于列出 scaffoldDir
目录下的所有脚手架文件。它会忽略以下划线开头的文件或目录。
_getScaffold(name)
:根据给定的名称获取对应的脚手架文件信息。
先调用 _listDir()
方法获取所有脚手架文件,然后查找名称匹配的文件。
get(name, callback)
:用于获取指定名称的脚手架内容。
如果找到对应的文件,它会读取并返回文件内容。如果没有找到,它会返回 defaults
对象中对应的默认脚手架内容。
set(name, content, callback)
:用于设置或创建一个新的脚手架文件。
首先检查是否已经存在同名的文件,如果存在就使用原有路径,否则就在 scaffoldDir
目录下创建新文件(文件扩展名默认为 .md
)。
remove(name, callback)
:用于删除一个脚手架文件。它会查找对应名称的文件,如果存在则将其删除。
数据库
1 2 3 4 5 6 7 8 this ._dbLoaded = false ;this ._isGenerating = false ;const dbPath = args.output || base;if (/^(init|new|g|publish|s|deploy|render|migrate)/ .test (this .env .cmd )) { this .log .d (`Writing database to ${(0 , path_1.join)(dbPath, 'db.json' )} ` ); }
初始化两个内部状态标志,用于跟踪数据库是否已经加载(_dbLoaded
)和网站是否正在生成(_isGenerating
)。
确定数据库文件将要被保存在的路径。如果执行 Hexo 命令时提供了 output
参数,那么数据库就会被保存在该输出路径下;不然就保存在项目根目录下。
接着检查当前执行的 Hexo 命令是否属于 init
、new
、g
、publish
、deploy
、render
或者 migrate
。如果是,就打印说数据库文件被写入到哪里去了。
1 2 3 4 this .database = new warehouse_1.default ({ version : dbVersion, path : (0 , path_1.join )(dbPath, 'db.json' ) });
Hexo 初始化和配置数据库,实例化过程中传入了一个配置对象:
version
:数据库的版本号。在 index.js
的最上方能找到:
path
:数据库文件的保存路径,由先前定义的 dbPath
和文件名 db.json
拼接而成。
此处实例化用到了 warehouse
库。这是一个轻量级的数据存储库,主要用于 Node.js 应用程序中处理 JSON 数据。它提供了一种简单的方式来定义、存储和检索模型数据,类似于一个迷你数据库或 ORM(对象关系映射)。
多配置文件路径管理
1 const mcp = (0 , multi_config_path_1.default )(this );
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 "use strict" ;var __importDefault = (this && this .__importDefault ) || function (mod ) { return (mod && mod.__esModule ) ? mod : { "default" : mod }; }; const path_1 = require ("path" );const hexo_fs_1 = require ("hexo-fs" );const js_yaml_1 = __importDefault (require ("js-yaml" ));const hexo_util_1 = require ("hexo-util" );module .exports = (ctx ) => function multiConfigPath (base, configPaths, outputDir ) { const { log } = ctx; const defaultPath = (0 , path_1.join )(base, '_config.yml' ); if (!configPaths) { log.w ('No config file entered.' ); return (0 , path_1.join )(base, '_config.yml' ); } let paths; if (configPaths.includes (',' )) { paths = configPaths.replace (' ' , '' ).split (',' ); } else { let configPath = (0 , path_1.isAbsolute )(configPaths) ? configPaths : (0 , path_1.resolve )(base, configPaths); if (!(0 , hexo_fs_1.existsSync )(configPath)) { log.w (`Config file ${configPaths} not found, using default.` ); configPath = defaultPath; } return configPath; } const numPaths = paths.length ; let combinedConfig = {}; let count = 0 ; for (let i = 0 ; i < numPaths; i++) { const configPath = (0 , path_1.isAbsolute )(paths[i]) ? paths[i] : (0 , path_1.join )(base, paths[i]); if (!(0 , hexo_fs_1.existsSync )(configPath)) { log.w (`Config file ${paths[i]} not found.` ); continue ; } const file = (0 , hexo_fs_1.readFileSync )(configPath); const ext = (0 , path_1.extname )(paths[i]).toLowerCase (); if (ext === '.yml' ) { combinedConfig = (0 , hexo_util_1.deepMerge )(combinedConfig, js_yaml_1.default .load (file)); count++; } else if (ext === '.json' ) { combinedConfig = (0 , hexo_util_1.deepMerge )(combinedConfig, js_yaml_1.default .load (file, { json : true })); count++; } else { log.w (`Config file ${paths[i]} not supported type.` ); } } if (count === 0 ) { log.e ('No config files found. Using _config.yml.' ); return defaultPath; } log.i ('Config based on' , count.toString (), 'files' ); const multiconfigRoot = outputDir || base; const outputPath = (0 , path_1.join )(multiconfigRoot, '_multiconfig.yml' ); log.d (`Writing _multiconfig.yml to ${outputPath} ` ); (0 , hexo_fs_1.writeFileSync )(outputPath, js_yaml_1.default .dump (combinedConfig)); return outputPath; };
multiConfigPath
函数用于处理 Hexo 配置文件。该函数会根据传入的配置文件路径来合并多个配置文件并生成一个新的配置文件 _multiconfig.yml
。
顺带一个读取配置文件路径的参数:
1 2 this .config_path = args.config ? mcp (base, args.config , args.output ) : (0 , path_1.join )(base, '_config.yml' );
根据用户的命令行输入和 Hexo 项目的配置,决定要读取的配置文件的路径。
注册模型
1 (0 , register_models_1.default )(this );
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 "use strict" ;var __createBinding = (this && this .__createBinding ) || (Object .create ? (function (o, m, k, k2 ) { if (k2 === undefined ) k2 = k; var desc = Object .getOwnPropertyDescriptor (m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable )) { desc = { enumerable : true , get : function ( ) { return m[k]; } }; } Object .defineProperty (o, k2, desc); }) : (function (o, m, k, k2 ) { if (k2 === undefined ) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this .__setModuleDefault ) || (Object .create ? (function (o, v ) { Object .defineProperty (o, "default" , { enumerable : true , value : v }); }) : function (o, v ) { o["default" ] = v; }); var __importStar = (this && this .__importStar ) || function (mod ) { if (mod && mod.__esModule ) return mod; var result = {}; if (mod != null ) for (var k in mod) if (k !== "default" && Object .prototype .hasOwnProperty .call (mod, k)) __createBinding (result, mod, k); __setModuleDefault (result, mod); return result; }; const models = __importStar (require ("../models" ));module .exports = (ctx ) => { const db = ctx.database ; const keys = Object .keys (models); for (let i = 0 , len = keys.length ; i < len; i++) { const key = keys[i]; db.model (key, models[key](ctx)); } };
上三个函数是 TypeScript 编译器在编译时生成的,用于处理 ES 模块的导入和默认导出行为。它们确保在使用 CommonJS 模块时,能正确处理 ES 模块的导入。
接下来导入的 models
模块是 hexo/dist/models
目录,这里使用了 __importStar
函数导入 models
模块中的所有导出,作为 models
对象的属性。
models
模块内容很多,后续会仔细讲解。
register_models
模块导出一个函数,该函数接收一个 ctx
参数,并从 ctx
对象中获取数据库实例 db
、获取 models
对象的所有键,遍历这些键,为每个模型在数据库中注册一个模型。
主要功能是导入所有的模型,并将这些模型注册到数据库中。
源文件管理
1 this .source = new source_1.default (this );
1 2 3 4 5 6 7 8 9 10 11 12 13 "use strict" ;var __importDefault = (this && this .__importDefault ) || function (mod ) { return (mod && mod.__esModule ) ? mod : { "default" : mod }; }; const box_1 = __importDefault (require ("../box" ));class Source extends box_1.default { constructor (ctx ) { super (ctx, ctx.source_dir ); this .processors = ctx.extend .processor .list (); } } module .exports = Source ;
Source
类继承自 Box
类,所以先看一眼 Box
类:
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 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 "use strict" ;var __awaiter = (this && this .__awaiter ) || function (thisArg, _arguments, P, generator ) { function adopt (value ) { return value instanceof P ? value : new P (function (resolve ) { resolve (value); }); } return new (P || (P = Promise ))(function (resolve, reject ) { function fulfilled (value ) { try { step (generator.next (value)); } catch (e) { reject (e); } } function rejected (value ) { try { step (generator["throw" ](value)); } catch (e) { reject (e); } } function step (result ) { result.done ? resolve (result.value ) : adopt (result.value ).then (fulfilled, rejected); } step ((generator = generator.apply (thisArg, _arguments || [])).next ()); }); }; var __importDefault = (this && this .__importDefault ) || function (mod ) { return (mod && mod.__esModule ) ? mod : { "default" : mod }; }; Object .defineProperty (exports , "__esModule" , { value : true });const path_1 = require ("path" );const bluebird_1 = __importDefault (require ("bluebird" ));const file_1 = __importDefault (require ("./file" ));const hexo_util_1 = require ("hexo-util" );const hexo_fs_1 = require ("hexo-fs" );const picocolors_1 = require ("picocolors" );const events_1 = require ("events" );const micromatch_1 = require ("micromatch" );const defaultPattern = new hexo_util_1.Pattern (() => ({}));class Box extends events_1.EventEmitter { constructor (ctx, base, options ) { super (); this .options = Object .assign ({ persistent : true , awaitWriteFinish : { stabilityThreshold : 200 } }, options); if (!base.endsWith (path_1.sep )) { base += path_1.sep ; } this .context = ctx; this .base = base; this .processors = []; this ._processingFiles = {}; this .watcher = null ; this .Cache = ctx.model ('Cache' ); this .File = this ._createFileClass (); let targets = this .options .ignored || []; if (ctx.config .ignore && ctx.config .ignore .length ) { targets = targets.concat (ctx.config .ignore ); } this .ignore = targets; this .options .ignored = targets.map (s => toRegExp (ctx, s)).filter (x => x); } _createFileClass ( ) { const ctx = this .context ; class _File extends file_1.default { render (options ) { return ctx.render .render ({ path : this .source }, options); } renderSync (options ) { return ctx.render .renderSync ({ path : this .source }, options); } } _File.prototype .box = this ; return _File; } addProcessor (pattern, fn ) { if (!fn && typeof pattern === 'function' ) { fn = pattern; pattern = defaultPattern; } if (typeof fn !== 'function' ) throw new TypeError ('fn must be a function' ); if (!(pattern instanceof hexo_util_1.Pattern )) pattern = new hexo_util_1.Pattern (pattern); this .processors .push ({ pattern, process : fn }); } _readDir (base, prefix = '' ) { const { context : ctx } = this ; const results = []; return readDirWalker (ctx, base, results, this .ignore , prefix) .return (results) .map (path => this ._checkFileStatus (path)) .map (file => this ._processFile (file.type , file.path ).return (file.path )); } _checkFileStatus (path ) { const { Cache , context : ctx } = this ; const src = (0 , path_1.join )(this .base , path); return Cache .compareFile (src.substring (ctx.base_dir .length ), () => getHash (src), () => (0 , hexo_fs_1.stat )(src)).then (result => ({ type : result.type , path })); } process (callback ) { const { base, Cache , context : ctx } = this ; return (0 , hexo_fs_1.stat )(base).then (stats => { if (!stats.isDirectory ()) return ; const relativeBase = base.substring (ctx.base_dir .length ); const cacheFiles = Cache .filter (item => item._id .startsWith (relativeBase)).map (item => item._id .substring (relativeBase.length )); return this ._readDir (base) .then ((files ) => cacheFiles.filter ((path ) => !files.includes (path))) .map ((path ) => this ._processFile (file_1.default .TYPE_DELETE , path)); }).catch (err => { if (err && err.code !== 'ENOENT' ) throw err; }).asCallback (callback); } _processFile (type, path ) { if (this ._processingFiles [path]) { return bluebird_1.default .resolve (); } this ._processingFiles [path] = true ; const { base, File , context : ctx } = this ; this .emit ('processBefore' , { type, path }); return bluebird_1.default .reduce (this .processors , (count, processor ) => { const params = processor.pattern .match (escapeBackslash (path)); if (!params) return count; const file = new File ({ source : (0 , path_1.join )(base, path), path : escapeBackslash (path), params, type }); return Reflect .apply (bluebird_1.default .method (processor.process ), ctx, [file]) .thenReturn (count + 1 ); }, 0 ).then (count => { if (count) { ctx.log .debug ('Processed: %s' , (0 , picocolors_1.magenta )(path)); } this .emit ('processAfter' , { type, path }); }).catch (err => { ctx.log .error ({ err }, 'Process failed: %s' , (0 , picocolors_1.magenta )(path)); }).finally (() => { this ._processingFiles [path] = false ; }).thenReturn (path); } watch (callback ) { if (this .isWatching ()) { return bluebird_1.default .reject (new Error ('Watcher has already started.' )).asCallback (callback); } const { base } = this ; function getPath (path ) { return path.substring (base.length ); } return this .process ().then (() => (0 , hexo_fs_1.watch )(base, this .options )).then (watcher => { this .watcher = watcher; watcher.on ('add' , path => { this ._processFile (file_1.default .TYPE_CREATE , getPath (path)); }); watcher.on ('change' , path => { this ._processFile (file_1.default .TYPE_UPDATE , getPath (path)); }); watcher.on ('unlink' , path => { this ._processFile (file_1.default .TYPE_DELETE , getPath (path)); }); watcher.on ('addDir' , path => { let prefix = getPath (path); if (prefix) prefix += path_1.sep ; this ._readDir (path, prefix); }); }).asCallback (callback); } unwatch ( ) { if (!this .isWatching ()) return ; this .watcher .close (); this .watcher = null ; } isWatching ( ) { return Boolean (this .watcher ); } } function escapeBackslash (path ) { return path.replace (/\\/g , '/' ); } function getHash (path ) { const src = (0 , hexo_fs_1.createReadStream )(path); const hasher = (0 , hexo_util_1.createSha1Hash )(); const finishedPromise = new bluebird_1.default ((resolve, reject ) => { src.once ('error' , reject); src.once ('end' , resolve); }); src.on ('data' , chunk => { hasher.update (chunk); }); return finishedPromise.then (() => hasher.digest ('hex' )); } function toRegExp (ctx, arg ) { if (!arg) return null ; if (typeof arg !== 'string' ) { ctx.log .warn ('A value of "ignore:" section in "_config.yml" is not invalid (not a string)' ); return null ; } const result = (0 , micromatch_1.makeRe )(arg); if (!result) { ctx.log .warn ('A value of "ignore:" section in "_config.yml" can not be converted to RegExp:' + arg); return null ; } return result; } function isIgnoreMatch (path, ignore ) { return path && ignore && ignore.length && (0 , micromatch_1.isMatch )(path, ignore); } function readDirWalker (ctx, base, results, ignore, prefix ) { if (isIgnoreMatch (base, ignore)) return bluebird_1.default .resolve (); return bluebird_1.default .map ((0 , hexo_fs_1.readdir )(base).catch (err => { ctx.log .error ({ err }, 'Failed to read directory: %s' , base); if (err && err.code === 'ENOENT' ) return []; throw err; }), (path ) => __awaiter (this , void 0 , void 0 , function * () { const fullpath = (0 , path_1.join )(base, path); const stats = yield (0 , hexo_fs_1.stat )(fullpath).catch (err => { ctx.log .error ({ err }, 'Failed to stat file: %s' , fullpath); if (err && err.code === 'ENOENT' ) return null ; throw err; }); const prefixPath = `${prefix} ${path} ` ; if (stats) { if (stats.isDirectory ()) { return readDirWalker (ctx, fullpath, results, ignore, prefixPath + path_1.sep ); } if (!isIgnoreMatch (fullpath, ignore)) { results.push (prefixPath); } } })); } exports .default = Box ;
部分辅助函数:
__awaiter
:用于处理异步函数和生成器函数,使得可以使用 async/await
语法。
__importDefault
:用于处理默认导入。
escapeBackslash
:将路径中的反斜杠替换为斜杠。
getHash
:生成文件的 SHA-1 哈希值。
toRegExp
:将字符串转换为正则表达式。
isIgnoreMatch
:检查路径是否与忽略模式匹配。
readDirWalker
:递归读取目录,并收集所有符合条件的文件路径。
box/index.js
也导入了一个 file.js
文件,该文件定义了一个 File
类,用来表示文件对象,并提供了一些方法来读取和获取文件状态:
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 "use strict" ;const hexo_fs_1 = require ("hexo-fs" );class File { constructor ({ source, path, params, type } ) { this .source = source; this .path = path; this .params = params; this .type = type; } read (options ) { return (0 , hexo_fs_1.readFile )(this .source , options); } readSync (options ) { return (0 , hexo_fs_1.readFileSync )(this .source , options); } stat ( ) { return (0 , hexo_fs_1.stat )(this .source ); } statSync ( ) { return (0 , hexo_fs_1.statSync )(this .source ); } } File .TYPE_CREATE = 'create' ;File .TYPE_UPDATE = 'update' ;File .TYPE_SKIP = 'skip' ;File .TYPE_DELETE = 'delete' ;module .exports = File ;
read(options)
:异步读取文件内容,返回一个 Promise。
readSync(options)
:同步读取文件内容,返回文件内容。
stat()
:异步获取文件状态,返回一个 Promise。
statSync()
:同步获取文件状态,返回文件状态。
文件类型常量:明确表示文件的操作类型。
Box
类也是继承自 EventEmitter
。
主要的属性和方法:
构造函数:初始化 Box 类的实例,设置:
options
:默认选项和用户传入的选项合并。
context
:上下文对象。
base
:基础路径。
processors
:处理器列表。
_processingFiles
:正在处理的文件。
watcher
:文件监视器。
Cache
:缓存模型。
File
:文件类。
ignore
:忽略模式。
_createFileClass
:创建一个文件类,用于处理文件的渲染操作。
addProcessor
:添加一个处理器,用于处理特定模式的文件。
_readDir
:读取目录,并递归处理所有文件。
_checkFileStatus
:检查文件状态,比较缓存文件和实际文件。
process
:处理目录中的所有文件。
_processFile
:处理单个文件,根据文件类型执行处理器。
watch
:监视目录中的文件变化,并在文件添加、修改或删除时触发相应的处理。
unwatch
:停止监视目录。
isWatching
:检查是否正在监视目录。
Box
类的更多详情可见 Hexo 官方文档 。
现在再来看 Source
类。它继承自 Box
类,拥有着 Box
类的所有属性和方法。Box
类提供了文件读取、监视、处理等能力。而 Source
类在此基础上,专注于处理特定于源文件的逻辑。
主题
1 this .theme = new theme_1.default (this );
Theme
类被定义在 hexo/dist/theme
中,未来会详细说。目前只讲 index.js
的大致逻辑。
Theme
类是 Hexo 中负责管理主题的核心模块。
可见 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 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 "use strict" ;var __importDefault = (this && this .__importDefault ) || function (mod ) { return (mod && mod.__esModule ) ? mod : { "default" : mod }; }; const path_1 = require ("path" );const box_1 = __importDefault (require ("../box" ));const view_1 = __importDefault (require ("./view" ));const hexo_i18n_1 = __importDefault (require ("hexo-i18n" ));const config_1 = require ("./processors/config" );const i18n_1 = require ("./processors/i18n" );const source_1 = require ("./processors/source" );const view_2 = require ("./processors/view" );class Theme extends box_1.default { constructor (ctx, options ) { super (ctx, ctx.theme_dir , options); this .config = {}; this .views = {}; this .processors = [ config_1.config , i18n_1.i18n , source_1.source , view_2.view ]; let languages = ctx.config .language ; if (!Array .isArray (languages)) languages = [languages]; languages.push ('default' ); this .i18n = new hexo_i18n_1.default ({ languages : [...new Set (languages.filter (Boolean ))] }); class _View extends view_1.default { } this .View = _View; _View.prototype ._theme = this ; _View.prototype ._render = ctx.render ; _View.prototype ._helper = ctx.extend .helper ; } getView (path ) { path = path.replace (/\\/g , '/' ); const ext = (0 , path_1.extname )(path); const name = path.substring (0 , path.length - ext.length ); const views = this .views [name]; if (!views) return ; if (ext) { return views[ext]; } return views[Object .keys (views)[0 ]]; } setView (path, data ) { const ext = (0 , path_1.extname )(path); const name = path.substring (0 , path.length - ext.length ); this .views [name] = this .views [name] || {}; const views = this .views [name]; views[ext] = new this .View (path, data); } removeView (path ) { const ext = (0 , path_1.extname )(path); const name = path.substring (0 , path.length - ext.length ); const views = this .views [name]; if (!views) return ; views[ext] = undefined ; } } module .exports = Theme ;
Theme
类同样继承自 Box
类:
constructor(ctx, options)
:构造函数接受上下文对象 ctx
和选项 options
,调用父类 box_1.default
的构造函数。
初始化 config
和 views
属性为空对象。
processors
:设置处理器数组,包括配置、国际化、源文件和视图处理器。
初始化多语言支持,确保 languages
是数组,并添加 default
语言。使用 hexo-i18n
库创建 i18n
对象。
定义一个新的 _View
类,继承自 view_1.default
,并在其原型上添加 _theme
、_render
和 _helper
属性。
撇开本地文件导入,这里的 hexo-i18n
是 Hexo 自己的 i18n 模块。
i18n 的全程是 Internationalization (国际化),是指在设计软件、将软件与特定语言及地区脱钩的过程。由于英文单字长度过长,所以常被简称为 i18n(18意味着在 Internationalization 这个单字中,i 和 n 之间有 18 个字母。
Theme
类也有以下方法:
getView(path)
:根据给定路径获取视图。
setView(path, data)
设置指定路径的 View
内容。
removeView(path)
移除指定路径的 View
。
通过实例化 Theme
类,Hexo 可以获得一个用于操作当前主题的对象,从而正确地渲染和生成静态页面。
视图(View
)在 Hexo 中是用于渲染页面内容的模板文件。通常使用模板引擎语法编写,如 Swig、Pug 等,允许在模板中嵌入动态数据和逻辑。
视图主要有以下几种类型:
布局(Layout):定义了页面的基本结构,如 HTML 头部、导航栏、页脚等通用部分。所有其他视图都将被渲染到布局视图中的特定位置。
包含(Partial):可重用的模板片段,通常用于渲染页面的某一部分,如文章列表、评论区等。它们可以在其他视图中被引入和渲染。
页面(Page):用于渲染特定的页面内容,如文章、分类、标签等。它们通常会引入布局视图和其他所需的包含视图。
助手(Helper):一些辅助函数,用于在模板中执行特定的逻辑或操作,如格式化日期、生成链接等。
本地数据
1 this .locals = new locals_1.default ();
Locals
类用于管理和缓存局部变量:
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 "use strict" ;const hexo_util_1 = require ("hexo-util" );class Locals { constructor ( ) { this .cache = new hexo_util_1.Cache (); this .getters = {}; } get (name ) { if (typeof name !== 'string' ) throw new TypeError ('name must be a string!' ); return this .cache .apply (name, () => { const getter = this .getters [name]; if (!getter) return ; return getter (); }); } set (name, value ) { if (typeof name !== 'string' ) throw new TypeError ('name must be a string!' ); if (value == null ) throw new TypeError ('value is required!' ); const getter = typeof value === 'function' ? value : () => value; this .getters [name] = getter; this .cache .del (name); return this ; } remove (name ) { if (typeof name !== 'string' ) throw new TypeError ('name must be a string!' ); this .getters [name] = null ; this .cache .del (name); return this ; } invalidate ( ) { this .cache .flush (); return this ; } toObject ( ) { const result = {}; const keys = Object .keys (this .getters ); for (let i = 0 , len = keys.length ; i < len; i++) { const key = keys[i]; const item = this .get (key); if (item != null ) result[key] = item; } return result; } } module .exports = Locals ;
Locals
类有两个属性:
cache
:使用 hexo-util
模块的 Cache
类实例化缓存对象。
getters
:初始化一个空对象,用于存储 getter
函数。
Locals
类也有以下方法:
get(name)
获取指定名称的本地数据。
set(name, value)
设置指定名称的本地数据。
remove(name)
移除指定名称的本地数据。
invalidate()
清空缓存。
toObject()
将本地数据转换为普通对象。
Locals
类是 Hexo 中用于管理本地数据的模块。本地数据在 Hexo 插件开发中意外重要,后续会详细讲解。
暂且,请看 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 26 27 28 29 30 31 32 33 34 35 36 _bindLocals ( ) { const db = this .database ; const { locals } = this ; locals.set ('posts' , () => { const query = {}; if (!this .config .future ) { query.date = { $lte : Date .now () }; } if (!this ._showDrafts ()) { query.published = true ; } return db.model ('Post' ).find (query); }); locals.set ('pages' , () => { const query = {}; if (!this .config .future ) { query.date = { $lte : Date .now () }; } return db.model ('Page' ).find (query); }); locals.set ('categories' , () => { return db.model ('Category' ).filter (category => category.length ); }); locals.set ('tags' , () => { return db.model ('Tag' ).filter (tag => tag.length ); }); locals.set ('data' , () => { const obj = {}; db.model ('Data' ).forEach (data => { obj[data._id ] = data.data ; }); return obj; }); }
首先获取 Hexo
实例的数据库对象 this.database
,接着使用对象解构赋值的语法从 this
对象中提取 locals
属性的值。this
指向的是当前执行上下文中的对象实例,在这里,this
是 Hexo
实例本身。
const { locals } = this;
相当于这行代码:
1 const locals = this .locals ;
通过调用 locals.set()
方法,为不同的本地数据设置了获取器函数。当其他地方访问这些本地数据时,相应的获取器函数将被执行,从数据库中查询并返回所需的数据。
具体来说,这个方法设置了以下几个本地数据及其获取器:
posts
:获取所有已发布且不是未来日期的文章。如果 config.future
设置为 true
,则也包括未来日期的文章。如果 _showDrafts()
返回 true
,则也包括草稿文章。
pages
:获取所有已发布且不是未来日期的页面。如果 config.future
设置为 true
,则也包括未来日期的页面。
categories
:获取所有包含文章的分类。
tags
:获取所有包含文章的标签。
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 init ( ) { this .log .debug ('Hexo version: %s' , (0 , picocolors_1.magenta )(this .version )); this .log .debug ('Working directory: %s' , (0 , picocolors_1.magenta )((0 , tildify_1.default )(this .base_dir ))); require ('../plugins/console' )(this ); require ('../plugins/filter' )(this ); require ('../plugins/generator' )(this ); require ('../plugins/helper' )(this ); require ('../plugins/highlight' )(this ); require ('../plugins/injector' )(this ); require ('../plugins/processor' )(this ); require ('../plugins/renderer' )(this ); require ('../plugins/tag' ).default (this ); return bluebird_1.default .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' ); }); }
init
方法负责进行初始化工作:
打印 Hexo 版本和工作目录信息到控制台,使用 picocolors
库为版本和目录路径上色。
加载内部插件。
执行一系列初始化步骤后,使用了 bluebird.each
方法按顺序执行以下步骤:
update_package
:更新 package.json
文件。
load_config
:加载配置文件。
load_theme_config
:加载主题配置文件。
load_plugins
:加载外部插件和脚本。
在所有初始化步骤完成后,执行 after_init
过滤器,允许插件和主题在初始化后进行一些额外的操作。
发射 ready
事件,表示 Hexo 已经准备就绪,可以执行其他操作了。
调用控制台命令
1 2 3 4 5 6 7 8 9 10 call (name, args, callback ) { if (!callback && typeof args === 'function' ) { callback = args; args = {}; } const c = this .extend .console .get (name); if (c) return Reflect .apply (c, this , [args]).asCallback (callback); return bluebird_1.default .reject (new Error (`Console \`${name} \` has not been registered yet!` )); }
这段代码定义了 call
方法,用于调用已注册的控制台命令。
先是检查了是否传入回调函数。如果没有,但是第二个参数是一个函数的话,就将第二个参数视为回调函数,同时将 args
设置为一个空对象。这是为了兼容只传入回调函数的情况。
从 this.extend.console
对象中获取名为 name
的控制台命令。如果找到了对应的控制台命令对象 c
,便执行以下步骤:
使用 Reflect
对象的 apply
方法调用控制台命令 c
。
.asCallback(callback)
将上一步的调用结果转换为一个 Promise 对象,并将该 Promise 与回调函数 callback
关联。这样就可以在 Promise 完成时自动调用回调函数。
如果未能找到对应的控制台命令对象,就返回一个被拒绝的 Promise,其中包含一个表示该命令尚未注册的错误消息。
定义数据库模型
1 2 3 model (name, schema ) { return this .database .model (name, schema); }
回到数据库 的 Database
类,能看出 model
方法是用来创建一个模型的。
解析插件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 resolvePlugin (name, basedir ) { try { return require .resolve (name, { paths : [basedir] }); } catch (err) { try { resolveSync = resolveSync || require ('resolve' ).sync ; return resolveSync (name, { basedir }); } catch (err) { return (0 , path_1.join )(basedir, 'node_modules' , name); } } }
先是使用 require.resolve
方法用于解析模块。如果解析失败了,就继续尝试使用 resolve.sync
方法。最后如果依然失败,则返回一个可能不存在的路径以供后续处理。
require.resolve
方法是 Node.js 内置的,用于解析模块的路径。 如果 require.resolve
失败,通常是由于 node_modules
目录损坏或其他原因。
require('resolve').sync
方法是一个更兼容旧版本 npm 的解决方案,类似于早期 Hexo 版本的插件解析方式。resolve.sync
方法如果也失败,很有可能是因为插件未找到,这时候便返回一个大概并不存在的路径。
加载插件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 loadPlugin (path, callback ) { return (0 , hexo_fs_1.readFile )(path).then (script => { const module = new module_1.default (path); module .filename = path; module .paths = module_1.default ._nodeModulePaths (path); function req (path ) { return module .require (path); } req.resolve = (request ) => module_1.default ._resolveFilename (request, module ); req.main = require .main ; req.extensions = module_1.default ._extensions ; req.cache = module_1.default ._cache ; script = `(async function(exports, require, module, __filename, __dirname, hexo){${script} \n});` ; const fn = (0 , vm_1.runInThisContext )(script, path); return fn (module .exports , req, module , path, (0 , path_1.dirname )(path), this ); }).asCallback (callback); }
使用 hexo-fs
库的 readFile
异步读取插件文件的内容,返回一个 Promise 对象。
hexo-fs
库的源代码未来再说,暂且只需要知道是 Hexo 的文件系统模块。
接着创建模块环境,并设置 filename
和 paths
属性。这里的 module_1
是 module
模块,定义了多种与模块加载、解析和处理相关的类型和接口。filename
属性被赋值插件的路径 path
;paths
被赋值模块的搜索路径。
req
方法用于模块加载。这个方法实际上模拟了 require
函数。之后这段代码分别定义了 req.resolve
方法来解析模块的绝对路径,设置 req.main
属性来指向 Node.js 主模块、req.extensions
属性来处理模块文件的加载、req.cache
属性来缓存加载的模块。
插件的内容被包装在一个异步函数中,并传入了必要的参数、赋值给变量 script
。
使用 vm_1.runInThisContext
方法将包装后的插件内容编译为可执行函数。
vm_1
是 vm
模块,提供了一组 API 用于在 JavaScript 中创建虚拟机和运行沙盒化的代码。
执行编译后的函数,传入需要的参数。
最终将 Promise 对象转换为回调函数形式,并进行执行。
显示草稿内容
1 2 3 4 _showDrafts ( ) { const { args } = this .env ; return args.draft || args.drafts || this .config .render_drafts ; }
根据用户的输入和 Hexo 的配置,确定是否应该显示草稿内容。
加载
1 2 3 4 5 6 7 8 9 10 11 12 load (callback ) { return (0 , load_database_1.default )(this ).then (() => { this .log .info ('Start processing' ); return bluebird_1.default .all ([ this .source .process (), this .theme .process () ]); }).then (() => { mergeCtxThemeConfig (this ); return this ._generate ({ cache : false }); }).asCallback (callback); }
load_database
模块检查数据库文件是否存在,存在则尝试加载数据库。如果加载失败,则删除数据库文件。它使用了 hexo-fs
模块来处理文件系统操作,并使用 bluebird
提供的 Promise 来管理异步操作。通过 ctx
上下文对象来传递数据库和日志对象,以及跟踪数据库加载状态。
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 "use strict" ;var __importDefault = (this && this .__importDefault ) || function (mod ) { return (mod && mod.__esModule ) ? mod : { "default" : mod }; }; const hexo_fs_1 = require ("hexo-fs" );const bluebird_1 = __importDefault (require ("bluebird" ));module .exports = (ctx ) => { if (ctx._dbLoaded ) return bluebird_1.default .resolve (); const db = ctx.database ; const { path } = db.options ; const { log } = ctx; return (0 , hexo_fs_1.exists )(path).then (exist => { if (!exist) return ; log.debug ('Loading database.' ); return db.load (); }).then (() => { ctx._dbLoaded = true ; }).catch (() => { log.error ('Database load failed. Deleting database.' ); return (0 , hexo_fs_1.unlink )(path); }); };
在数据库加载完成后,使用 then
方法处理返回的 Promise。在这个处理函数中,首先记录了日志信息 Start processing
,然后使用 bluebird_1.default.all
方法并行处理两个任务:
this.source.process()
处理源文件。
this.theme.process()
处理主题。
处理完源文件和主题后,再次使用了 then
方法处理返回的 Promise。调用 mergeCtxThemeConfig
函数,将 Hexo
实例的上下文与主题配置合并,然后调用了 this._generate({ cache: false })
方法,生成静态文件。
mergeCtxThemeConfig
函数在 index.js
文件的很上方:
1 2 3 4 5 const mergeCtxThemeConfig = (ctx ) => { if (ctx.config .theme_config ) { ctx.theme .config = (0 , hexo_util_1.deepMerge )(ctx.theme .config , ctx.config .theme_config ); } };
该函数用于将 ctx.config.theme_config
(主题配置)合并到 ctx.theme.config
(博客目录)中。这里还使用了 hexo-util
库的深度合并。hexo-util
库也是未来会讲的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 _generate (options = {} ) { if (this ._isGenerating ) return ; const useCache = options.cache ; this ._isGenerating = true ; this .emit ('generateBefore' ); return this .execFilter ('before_generate' , null , { context : this }) .then (() => this ._routerRefresh (this ._runGenerators (), useCache)).then (() => { this .emit ('generateAfter' ); return this .execFilter ('after_generate' , null , { context : this }); }).finally (() => { this ._isGenerating = false ; }); }
为了防止重复生成,_generate
方法先是判断了 _isGenerating
变量,再进行接下来的操作。从 options
参数中提取 cache
选项,是为了决定是否在生成过程中使用缓存。
设置 this._isGenerating
为 true
后,便开始生成。generateBefore
事件被触发,接着执行 before_generate
过滤器。
过滤器(Filter)后续会讲解。目前先延申 Hexo 官方文档 中的说明:
过滤器用于修改特定文件,Hexo 将这些文件依序传给过滤器,而过滤器可以针对文件进行修改。
其中的 execFilter
方法在 Hexo
类的最下方被定义:
1 2 3 execFilter (type, data, options ) { return this .extend .filter .exec (type, data, options); }
Hexo
类也有一个 execFilterSync
方法:
1 2 3 execFilterSync (type, data, options ) { return this .extend .filter .execSync (type, data, options); }
before_generate
过滤器执行完毕后,调用 _routerRefresh
方法:
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 _routerRefresh (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); } } }); }
初始化和获取路由列表:从 this
中获取 route
对象,并调用其 list
方法获取当前路由列表;生成局部变量 Locals
,并将 useCache
选项赋给 Locals.prototype.cache
,决定是否使用缓存。
处理生成器结果:遍历 runningGenerators
数组,对于每个生成器结果进行处理。如果 generatorResult
不是对象或 path
为空,则返回 undefined
。
格式化路径并处理数据:调用 route.format
方法格式化路径;解构 generatorResult
,获取 data
和 layout
。如果 layout
不存在,直接将 path
和 data
设置到路由中,并返回 path
。
执行 template_locals
过滤器并设置路由:调用 execFilter
方法执行 template_locals
过滤器,传入新的 Locals
实例。在过滤器执行完毕后,将路径和创建的主题路由设置到路由中,并返回 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 29 30 31 32 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} : ${(0 , picocolors_1.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: ${(0 , picocolors_1.magenta)(path)} ` ); }); } } log.warn (`No layout: ${(0 , picocolors_1.magenta)(path)} ` ); }; };
createLoadThemeRoute
方法创建加载主题路由的闭包。
首先从 ctx
中获取日志记录器 log
和主题对象 theme
、从 locals
中获取路由的路径 path
和缓存标志 useCache
。然后将 generatorResult.layout
转换为一个唯一的布局数组,记录布局的长度,同时将 locals.cache
设置为 true
,以确保视图在渲染期间使用缓存。
返回一个闭包。闭包指的是函数和函数内部引用的外部变量的组合。该闭包在调用时会渲染 HTML 视图。
如果 useCache
为 true
,并且缓存中存在 generatorResult
,直接返回缓存的结果。否则遍历布局数组,并尝试从主题中获取对应的视图。
如果找到了视图,就使用该视图渲染 locals
,并执行注入器和过滤器,最后返回渲染结果。否则记录警告并返回。
注入器(Injector)是 Hexo 扩展之一,在 Hexo 官方文档 中被声明为 用于将静态代码片段注入生成的 HTML 的 <head>
和/或 <body>
中 ,且注入必须在 after_render:html
过滤器之前完成。
移除旧路由:获取新生成的 newRouteList
;遍历旧路由列表 routeList
,如果某个路由不在 newRouteList
中,则将其移除。
_routerRefresh
方法确保了生成器结果正确应用到路由中,同时清理不再需要的旧路由,保持路由的最新状态。
路由刷新完成后,_generate
方法触发了 generateAfter
事件,并执行 after_generate
过滤器。
最后使用 finally
方法确保无论生成过程成功与否,都会将 this._isGenerating
重置为 false
,允许后续生成操作进行。
通过执行前后过滤器和触发事件,_generate
方法确保生成过程的各个阶段都可以被插件或自定义代码扩展和修改。
load
方法在调用 _generate
、生成静态文件后,将整个异步操作的结果传递给回调函数 callback
。asCallback
方法将 Promise
转换为传统的回调形式,以兼容老式回调风格的代码。
监视文件变化
watch
函数的目的是在监视文件变化时重新生成内容,并在需要时启用缓存。
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 watch (callback ) { let useCache = false ; const { cache } = Object .assign ({ cache : false }, this .config .server ); const { alias } = this .extend .console ; if (alias[this .env .cmd ] === 'server' && cache) { useCache = true ; } this ._watchBox = debounce (() => this ._generate ({ cache : useCache }), 100 ); return (0 , load_database_1.default )(this ).then (() => { this .log .info ('Start processing' ); return bluebird_1.default .all ([ this .source .watch (), this .theme .watch () ]); }).then (() => { mergeCtxThemeConfig (this ); this .source .on ('processAfter' , this ._watchBox ); this .theme .on ('processAfter' , () => { this ._watchBox (); mergeCtxThemeConfig (this ); }); return this ._generate ({ cache : useCache }); }).asCallback (callback); }
watch
函数会先从 this.config.server
中读取 cache
配置,默认值为 false
。如果当前命令为 server
,并且配置了 cache
,就会启用缓存。
接着使用 debounce
函数防抖 _generate
方法,间隔设置为 100 毫秒:1 2 3 4 5 6 7 8 9 function debounce (func, wait ) { let timeout; return function ( ) { clearTimeout (timeout); timeout = setTimeout (() => { func.apply (this ); }, wait); }; }
debounce
函数的目的是限制某个函数在一段时间内的调用次数,当该函数被连续调用时,只有在停止调用后等待指定的时间才会真正执行。这样便可以优化性能。
调用 load_database_1.default
方法加载数据库,打印日志 Start processing
,同时监视 source
和 theme
的变化,使用 bluebird_1.default.all
以并行方式执行。
合并主题配置并设置监控回调:这里的 mergeCtxThemeConfig
方法之前已经说过了。当 source
处理完成后,调用 _watchBox
;当 theme
处理完成后,调用 _watchBox
并再次合并主题配置。
调用 _generate
方法,初次生成内容,并根据 useCache
决定是否使用缓存。
最后使用 bluebird
库的 asCallback
方法,将结果作为回调传递给 callback
。
停止监控
既然有了监视用的方法,那么也有取消监视的方法。
1 2 3 4 5 6 7 8 9 unwatch ( ) { if (this ._watchBox != null ) { this .source .removeListener ('processAfter' , this ._watchBox ); this .theme .removeListener ('processAfter' , this ._watchBox ); this ._watchBox = null ; } stopWatcher (this .source ); stopWatcher (this .theme ); }
unwatch
方法检查并清除了 _watchBox
。根据上面 watch
方法的内容,this._watchBox
是一个防抖函数,用来处理文件变动后的生成操作。如果它存在,就需要将其从 source
和 theme
对象的 processAfter
事件监听器中移除。
stopWatcher
函数被定义在 index.js
的最上方:
1 2 const stopWatcher = (box ) => { if (box.isWatching ()) box.unwatch (); };
它用于停止对给定对象的监控。逻辑很简单:该对象是否正在被监控,如果是,就调用 unwatch
方法停止监控。
生成本地环境
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 _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 = Object .assign ({}, locals); if (this .page .path == null ) this .page .path = path; this .path = path; this .url = hexo_util_1.full_url_for .call (ctx, path); this .config = config; this .theme = theme.config ; this .layout = 'layout' ; this .env = env; this .view_dir = (0 , path_1.join )(theme_dir, 'layout' ) + path_1.sep ; this .site = localsObj; } } return Locals ; }
Locals
类中,许多环境相关的属性,如 url
、config
等属性都被设置。该方法生成了本地环境的信息对象,以便在模块渲染过程中使用。
运行生成器
同样,生成器(Generator)后续也会讲解。
在 Hexo 官方文档 中,生成器被解释为 会根据处理后的原始文件建立路由 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 _runGenerators ( ) { this .locals .invalidate (); const siteLocals = this .locals .toObject (); const generators = this .extend .generator .list (); const { log } = this ; return bluebird_1.default .map (Object .keys (generators), key => { const generator = generators[key]; log.debug ('Generator: %s' , (0 , picocolors_1.magenta )(key)); return Reflect .apply (generator, this , [siteLocals]); }).reduce ((result, data ) => { return data ? result.concat (data) : result; }, []); }
invalidate
方法使 Hexo
实例的本地数据无效,以便在生成器运行之前重新加载。
提取当前 Hexo
实例的本地数据对象,将其赋值给 siteLocals
变量。
获取 Hexo
实例中已注册的所有生成器,将它们保存在 generators
变量中。
运行生成器之前,记录了一条调试信息,指示将要运行哪个生成器。
使用 bluebird
库的 map
方法对生成器进行并行处理。每个生成器都被调用 Reflect.apply(generator, this, [siteLocals])
,将生成器函数应用到当前的 Hexo
实例上。
使用 bluebird
库的 reduce
方法将生成器返回的数据收集到一个数组中,并返回这个数组。
_runGenerators
方法运行 Hexo
实例中所有注册的生成器,并将它们返回的数据收集到一个数组中。
退出进程
exit
方法用于退出 Hexo 进程。
1 2 3 4 5 6 7 8 exit (err ) { if (err) { this .log .fatal ({ err }, 'Something\'s wrong. Maybe you can find the solution here: %s' , (0 , picocolors_1.underline )('https://hexo.io/docs/troubleshooting.html' )); } return this .execFilter ('before_exit' , null , { context : this }).then (() => { this .emit ('exit' , err); }); }
这里值得说的是,exit
方法会触发 execFilter
方法、触发 before_exit
过滤器。以及最终它会触发 exit
事件。
定义属性和导出
1 2 3 4 5 6 7 Hexo .lib_dir = libDir + path_1.sep ;Hexo .prototype .lib_dir = Hexo .lib_dir ;Hexo .core_dir = (0 , path_1.dirname )(libDir) + path_1.sep ;Hexo .prototype .core_dir = Hexo .core_dir ;Hexo .version = version;Hexo .prototype .version = Hexo .version ;module .exports = Hexo ;
Hexo
类本身终于讲完了,在 Hexo
类的下方,它的部分属性和原型属性被定义,并最终被导出为模块的默认输出。
LibDir
:Hexo
库目录的路径。
core_dir
:核心目录的路径。
version
:版本号。