由于Hexo中使用了bluebird
这个Promise库,会导致代码较难理解 本文会省略一些和issue无关的代码
最近看到了Hexo的issue #4976 ,其中提到了大文件的CSS在生成的过程中可能会丢失部分代码。本人感觉这个问题非常有意思,于是自己也尝试了一下大文件的CSS,没想到也成功复现了这个问题。 先说结论:问题应该出现在post.js
中的escapeAllSwigTags()
函数。由于压缩过的CSS中可能出现诸如{#main 这样的语句,而这样的语句会在这个函数中被当成swig模板进行处理,导致了代码的丢失。 解决方法:在_config.yml
的skip_render
中添加CSS的相对路径 以下为排查的过程和部分源码的分析:
hexo-clihexo中所输入的命令实际运行的是hexo/bin/hexo
文件:
1 2 #!/usr/bin/env node require ('hexo-cli' )();
hexo文件中直接导入hexo-cli
模块,查看hexo-cli
入口点:
即入口点为hexo/node_modules/hexo-cli/lib/hexo.js
,其模块导出:
查看entry()
函数: 其输入两个参数cwd
和args
。在该函数中调用了loadModule()
函数,并将其返回结果赋值给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 function entry (cwd = process.cwd(), args ) { let hexo = new Context (cwd, args); return findPkg (cwd, args).then (path => { if (!path) return ; hexo.base_dir = path; return loadModule (path, args).catch (err => { }); }).then (mod => { if (mod) hexo = mod; require ('./console' )(hexo); return hexo.init (); }).then (() => { }).catch (handleError); }
查看loadModule()
函数,在该函数中创建Hexo对象并返回:
1 2 3 4 5 6 7 8 function loadModule (path, args ) { return Promise .try (() => { const modulePath = resolve.sync ('hexo' , { basedir : path }); const Hexo = require (modulePath); return new Hexo (path, args); }); }
总结:hexo-cli中创建Hexo对象,并调用其init()函数
hexo初始化查看hexo
入口点:
即入口点为hexo/lib/hexo/index.js
,其模块导出:
先查看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 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 .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 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 .config = { ...defaultConfig }; this .log = logger (this .env ); this .render = new Render (this ); this .route = new Router (); this .post = new Post (this ); this .scaffold = new Scaffold (this ); this ._dbLoaded = false ; this ._isGenerating = false ; const dbPath = args.output || base; this .database = new Database ({ version : dbVersion, path : join (dbPath, 'db.json' ) }); const mcp = multiConfigPath (this ); this .config_path = args.config ? mcp (base, args.config , args.output ) : join (base, '_config.yml' ); registerModels (this ); this .source = new Source (this ); this .theme = new Theme (this ); this .locals = new Locals (this ); this ._bindLocals (); }
由于在hexo-cli
调用了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 init ( ) { 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初始化完成,可以开始执行用户输入的指令
generatehexo/lib/plugins/console
用于处理用户输入的指令hexo/lib/plugins/console/index.js
是该模块的入口,该模块用于向对应的extend中注册模块,以下以generate命令为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 module .exports = function (ctx ) { const { console } = ctx.extend ; console .register ('generate' , 'Generate static files.' , { options : [ {name : '-d, --deploy' , desc : 'Deploy after generated' }, {name : '-f, --force' , desc : 'Force regenerate' }, {name : '-w, --watch' , desc : 'Watch file changes' }, {name : '-b, --bail' , desc : 'Raise an error if any unhandled exception is thrown during generation' }, {name : '-c, --concurrency' , desc : 'Maximum number of files to be generated in parallel. Default is infinity' } ] }, require ('./generate' )); };
查看同目录下的generate.js
模块: 其创建了Generater对象,并调用了this.load()
函数,由于this就是Hexo对象,所以相当于调用了Hexo对象中的load()
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function generateConsole (args = {} ) { const generator = new Generater (this , args); if (generator.watch ) { return generator.execWatch (); } return this .load ().then (() => generator.firstGenerate ()).then (() => { if (generator.deploy ) { return generator.execDeploy (); } }); } module .exports = generateConsole;
查看Hexo类中的load()
函数: 该函数首先调用load_database.js
中的loadDatabase
模块,先检查是否在根目录下存在db.json
数据库文件,如果有则进行读取,否则直接返回 由于hexo将需要处理的文件分成了source
(\source目录下的文件)和theme
(\themes目录下的文件),所以分别需要对这两个部分执行process()
函数进行预处理 在异步调用结束后,需要生成的文件已经被存入了hexo对象中的database
属性中,等待被生成。此时执行mergeCtxThemeConfig()
函数进行配置的融合,并调用_generate()
函数用于执行生成前和生成后的过滤器(filter) 由于CSS文件位于\source
目录下,所以CSS文件会在this.source.process()
中被处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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); }
由于issue中所提到的CSS文件属于soruce,所以只需要研究this.source.process()
processor查看hexo/lib/box/index.js
中的process()
函数: 重点在于最后的return语句,通过_readDir()
函数读取文件到数据库中,再使用过滤器处理被删除的文件。而issue中提到的问题正是在将文件读取到数据库中发生的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 process (callback ) { const { base, Cache , context : ctx } = this ; return stat (base).then (stats => { return this ._readDir (base) .then (files => cacheFiles.filter (path => !files.includes (path))) .map (path => this ._processFile (File .TYPE_DELETE , path)); }).catch (err => { }).asCallback (callback); }
查看同文件下_readDir()
函数: 函数比较简单,即递归读取特定目录下所有文件,检查其状态并使用_processFile()
函数进行处理 读取文件本身不存在问题,问题出在对读取出来数据的处理上
1 2 3 4 5 6 7 _readDir (base, prefix = '' ) { const results = []; return readDirWalker (base, results, this .ignore , prefix) .return (results) .map (path => this ._checkFileStatus (path)) .map (file => this ._processFile (file.type , file.path ).return (file.path )); }
查看同文件下_processFile
函数:bluebird
的使用使得代码较难理解,大意就是对于每个path
,判断其是否匹配processor
中的pattern
。如果匹配,则执行processor
中的process()
函数,并将结果返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 _processFile (type, path ) { return Promise .reduce (this .processors , (count, processor ) => { const params = processor.pattern .match (path); if (!params) return count; const file = new File ({ source : join (base, path), path, params, type }); return Reflect .apply (Promise .method (processor.process ), ctx, [file]) }, 0 ).catch (err => { ctx.log .error ({ err }, 'Process failed: %s' , magenta (path)); }).finally (() => { this ._processingFiles [path] = false ; }).thenReturn (path); }
注意,这里的this
是hexo对象中的source
而非theme
,所以查看hexo/lib/hexo/source.js
:
1 2 3 4 5 6 7 class Source extends Box { constructor (ctx ) { super (ctx, ctx.source_dir ); this .processors = ctx.extend .processor .list (); } }
继续查看hexo/lib/extend/processor.js
: 可以知道最终processors中的处理器就是那些初始化时被注册的处理器(和console一样)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Processor { constructor ( ) { this .store = []; } list ( ) { return this .store ; } register (pattern, fn ) { this .store .push ({ pattern : new Pattern (pattern), process : fn }); } }
继续查看hexo/lib/plugins/processor/index.js
: 可以知道asset
、data
和post
三个处理器被成功注册,而CSS文件是归属于asset
进行处理的
1 2 3 4 5 6 7 8 9 10 11 12 module .exports = ctx => { const { processor } = ctx.extend ; function register (name ) { const obj = require (`./${name} ` )(ctx); processor.register (obj.pattern , obj.process ); } register ('asset' ); register ('data' ); register ('post' ); };
继续查看hexo/lib/plugins/processor/asset.js
: CSS文件的renderable
是true
,所以会进入processPage()
函数中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 module .exports = ctx => { return { pattern : new Pattern (path => { if (isExcludedFile (path, ctx.config )) return ; return { renderable : ctx.render .isRenderable (path) && !isMatch (path, ctx.config .skip_render ) }; }), process : function assetProcessor (file ) { if (file.params .renderable ) { return processPage (ctx, file); } return processAsset (ctx, file); } }; };
继续查看processPage()
函数: 在该函数中,主要是读取对应文件,进行一定的处理后将结果存入数据库的Page
模型中
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 function processPage (ctx, file ) { const Page = ctx.model ('Page' ); const { path } = file; const doc = Page .findOne ({source : path}); const { config } = ctx; const { timezone : timezoneCfg } = config; return Promise .all ([ file.stat (), file.read () ]).spread ((stats, content ) => { const data = yfm (content); const output = ctx.render .getOutput (path); data.source = path; data.raw = content; data.date = toDate (data.date ); return Page .insert (data); }); }
filterprocess()
函数已经完成,此时回到load()
函数中: 开始执行_generate()
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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); }
查看同目录下的_generate()
函数: 基本上就是先运行before_generate
过滤器,接着运行_runGenerators
调用生成器进行生成,最后运行after_generate
过滤器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 _generate (options = {} ) { 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 ; }); }
问题出现在before_generate
过滤器之中,查看hexo/lib/plugins/filter/before_generate/render_post.js
: 在该过滤器中,对于Post
模型和Page
模型分别调用render()
函数对post进行处理(如转义)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function renderPostFilter (data ) { const renderPosts = model => { const posts = model.toArray ().filter (post => post.content == null ); return Promise .map (posts, post => { post.content = post._content ; post.site = {data}; return this .post .render (post.full_source , post).then (() => post.save ()); }); }; return Promise .all ([ renderPosts (this .model ('Post' )), renderPosts (this .model ('Page' )) ]); }
查看hexo/lib/hexo/post.js
中的render()
函数: 在该函数中,首先运行before_post_render
过滤器,接着在对文件进行转义操作后使用渲染器对markdown等进行渲染,最后运行after_post_render
过滤器 问题就出在escapeAllSwigTags()
函数中。由于压缩过的CSS中可能出现诸如{#main这样的语句,而这样的语句会在这个函数中被当成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 32 render (source, data = {}, callback ) { const ctx = this .context ; const { config } = ctx; const { tag } = ctx.extend ; const ext = data.engine || (source ? extname (source) : '' ); let promise; 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 ); } }).then (content => { data.content = cacheObj.restoreCodeBlocks (content); return ctx.execFilter ('after_post_render' , data, { context : ctx }); }).asCallback (callback); }