上篇文章分析了hexo g
过程中的文件预渲染过程。本篇文章将会分析一下文件渲染的过程。
generate 入口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 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
过程,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 _generate (options = {} ) { 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 ; }); }
这里execFilter
是一个很巧妙的设计,是一个拦截器的概念,通过传入对应的拦截器名称,然后就会执行对应的逻辑。里面和其他类似,同样是通过register
来注册事件。找到对应的路径。
before_generate 1 2 3 4 5 6 module .exports = (ctx ) => { const { filter } = ctx.extend filter.register ("before_generate" , require ("./render_post" )) }
然后看具体的实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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" )), ]) }
先过滤了post.content
已经存在的文章,然后调用this.post.render
来做最后的渲染,即把 json 结构数据->最终的 html 结构。
post.render 来看后面的 render 过程。
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 render (source, data = {}, callback ) { return promise.then (content => { data.content = content; return ctx.execFilter ('before_post_render' , data, { context : ctx }); }).then (() => { ctx.log .debug ('Rendering post: %s' , 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); }
上面的代码分为三步骤:before_post_render
Filter 逻辑、render 逻辑、after_post_render
Filter 逻辑。后面分别介绍。
before_post_render before_post_render
做的一个很关键的步骤就是代码块渲染,一般代码块会以```形式存在,而这里做的逻辑就是把代码块转换成 html 标签结构。
1 2 3 4 5 ### Create a new post \`\` \` bash $ hexo new "My New Post" \`\` \`
例如上面的结构会被转换成下面的结构。
1 2 3 ### Create a new post <hexoPostRenderCodeBlock > <figure class ="highlight bash" > <table > <tr > <td class ="gutter" > <pre > <span class ="line" > 1</span > <br > </pre > </td > <td class ="code" > <pre > <span class ="line" > $ hexo new <span class ="string" > " My New Post" </span > </span > <br > </pre > </td > </tr > </table > </figure > </hexoPostRenderCodeBlock >
render 过程 render 过程同样是使用了最开始的 extends 扩展中注册的方法。我们来看一下具体的ctx.render
实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 render (data, options, callback ) { 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 => { }).then (result => { const output = this .getOutput (ext) || ext; return ctx.execFilter (`after_render:${output} ` , result, { context : ctx, args : [data] }); }).asCallback (callback); }
我们依然是只看一下核心实现。现实根据后缀拿到对应的渲染方法,这里的渲染方法和其他的结构类似,都是注册上去的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 module .exports = (ctx ) => { const { renderer } = ctx.extend const yaml = require ("./yaml" ) renderer.register ("yml" , "json" , yaml, true ) renderer.register ("yaml" , "json" , yaml, true ) const nunjucks = require ("./nunjucks" ) renderer.register ("njk" , "html" , nunjucks, true ) renderer.register ("j2" , "html" , nunjucks, true ) }
上述代码就是对应的注册逻辑,即把 yml 后缀渲染成 json 格式的其中一个方法。那么如果我们想扩展一些渲染方法,那么可以在插件中去做这件事情。比如hexo-renderer-ejs
、hexo-renderer-inferno
、hexo-renderer-marked
都是这样做的。你可以去对应的插件代码上查看。md 后缀的文件就是通过hexo-renderer-marked
这个插件做的渲染过程,最终会生成 html 标签结构的文件。
after_post_render 里面做了一些收尾操作,比如excerpt.js
做的事情就是匹配到 md 文章中的<!--more-->
这个标示,然后渲染成<span id="more"></span>
这个结构。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const rExcerpt = /<!-- ?more ?-->/i function excerptFilter (data ) { const { content } = data if (typeof data.excerpt !== "undefined" ) { data.more = content } else if (rExcerpt.test (content)) { data.content = content.replace (rExcerpt, (match, index ) => { data.excerpt = content.substring (0 , index).trim () data.more = content.substring (index + match.length ).trim () return '<span id="more"></span>' }) } else { data.excerpt = "" data.more = content } }
执行到这一步,文章的 render 过程基本已经完成。后续的部分将是 generators 的关键过程。
_runGenerators 这一步骤会调用之前注册的所有 generator,包含插件注册的,比如hexo-generator-archive
和hexo-generator-category
等。
1 2 3 4 07 :59 :58.367 DEBUG Generator : page07 :59 :58.367 DEBUG Generator : post07 :59 :58.367 DEBUG Generator : archive
赋值 layout 还是以 post 文章为例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function postGenerator (locals ) { return posts.map ((post, i ) => { const layouts = ["post" , "page" , "index" ] if (layout !== "post" ) layouts.unshift (layout) post.__post = true return { path, layout : layouts, data : post, } }) }
对于 post 文章来讲,梳理了一下数据结构。会转换成类似下面的结构。data 里面的内容还是之前处理出来的数据,多出来的比较关键的数据就是layout
的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 { path : '2021/09/30/hello-world/' , layout : [ 'post' , 'page' , 'index' ], data : { title : 'Hello World' , _content : 'Welcome to [Hexo](https://hexo.io/)' , source : '_posts/hello-world.md' , raw : '---\n' + 'title: Hello World\n' + '---\n' + 'Welcome to [Hexo](https://hexo.io/)' , slug : 'hello-world' , published : true , date : Moment <2021 -09 -30T15 :24 :18 +08 :00 >, updated : Moment <2021 -09 -30T15 :24 :18 +08 :00 >, comments : true , layout : 'post' , photos : [], link : '' , _id : 'ckuzae01t0000jpp2464lgr8v' , content : '<p>Welcome to <a href="https://hexo.io/">Hexo</a>' , site : { data : {} }, excerpt : '' , more : '<p>Welcome to <a href="https://hexo.io/">Hexo</a>' , path : [Getter ], permalink : [Getter ], full_source : [Getter ], asset_dir : [Getter ], tags : [Getter ], categories : [Getter ], __post : true } }
然后接下来继续看下面的逻辑。把数据最终转换成对应的文件结构。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 _routerReflesh (runningGenerators, useCache ) { return runningGenerators.map (generatorResult => { const path = route.format (generatorResult.path ); const { data, layout } = generatorResult; return this .execFilter ('template_locals' , new Locals (path, data), {context : this }) .then (locals => { route.set (path, createLoadThemeRoute (generatorResult, locals, this )); }) .thenReturn (path); }).then (newRouteList => { }); }
根据 layout 逐个生成 里面比较关键的逻辑就是这个createLoadThemeRoute
,里面调用了view.render
。
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 const createLoadThemeRoute = function (generatorResult, locals, ctx ) { const { log, theme } = ctx const { path, cache : useCache } = locals const layout = [...new Set (castArray (generatorResult.layout ))] const layoutLength = layout.length locals.cache = true return () => { if (useCache && routeCache.has (generatorResult)) return routeCache.get (generatorResult) for (let i = 0 ; i < layoutLength; i++) { const name = layout[i] const view = theme.getView (name) if (view) { log.debug (`Rendering HTML ${name} : ${magenta(path)} ` ) return view .render (locals) .then ((result ) => ctx.extend .injector .exec (result, locals)) .then ((result ) => ctx.execFilter ("_after_html_render" , result, { context : ctx, args : [locals], }) ) .tap ((result ) => { if (useCache) { routeCache.set (generatorResult, result) } }) .tapCatch ((err ) => { log.error ({ err }, `Render HTML failed: ${magenta(path)} ` ) }) } } } }
比如layout: ['post', 'idnex', 'archive']
这种情况,会分别使用对应的view.render
,而view.render
是在 theme 预处理的阶段做的,里面会遍历 theme 文件,以 icarus 为例,里面的layout/post.jsx
路径其实就是代表了这种post
的渲染方式,我们将 view 打印出来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 view : _View { path : 'post.jsx' , source : '/Users/huidizhu/Personal/blog/blog-test/node_modules/hexo-theme-icarus/layout/post.jsx' , data : { _content : "const { Component } = require('inferno');\n" + "const Article = require('./common/article');\n" + '\n' + 'module.exports = class extends Component {\n' + ' render() {\n' + ' const { config, page, helper } = this.props;\n' + '\n' + ' return <Article config={config} page={page} helper={helper} index={false} />;\n' + ' }\n' + '};\n' }, _compiledSync : [Function (anonymous)], _compiled : [Function (anonymous)] }
这里的 _compiled
就是对应 post 生成的关键方法。继续去找 _compiled
的定义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const renderer = render.getRenderer (ext)if (renderer && typeof renderer.compile === "function" ) { const compiled = renderer.compile (data) this ._compiledSync = (locals ) => { const result = compiled (locals) return ctx.execFilterSync (...buildFilterArguments (result)) } this ._compiled = (locals ) => Promise .resolve (compiled (locals)).then ((result ) => ctx.execFilter (...buildFilterArguments (result)) ) }
里面其实就是拿到了对应的 jsx
渲染方法,即在hexo-renderer-inferno
插件中定义的 jsx 渲染方法。