【hexo专栏】hexo的render模块的工作原理
背景
我们看到hexo的源码中有一个render模块,这个模块的作用是什么呢?我们先来看看它的文档。
用途
hexo框架中有很多类似 .ejs, .yml, .styl 这样的文件,然后在框架中或者扩展中想要把这些文件做一个转换:
1 2 3
   | hexo.render.render({path: 'path/to/file.swig'}).then(function(result){    });
  | 
 
在渲染时,无需指定特定的engine,Hexo 会自动根据扩展名猜测所要使用的渲染引擎,当然您也可以使用 engine 指定。
使用方式
然后我们在注册这个扩展的时候,我们可以用同步和异步两种模式:
异步模式:
1 2 3 4 5 6 7 8 9 10 11 12 13
   | var stylus = require('stylus');
 
  hexo.extend.renderer.register('styl', 'css', function(data, options, callback){   stylus(data.text).set('filename', data.path).render(callback); });
 
  hexo.extend.renderer.register('styl', 'css', function(data, options){   return new Promise(function(resolve, reject){     resolve('test');   }); });
  | 
 
同步模式
1 2 3 4 5 6
   | var ejs = require('ejs');
  hexo.extend.renderer.register('ejs', 'html', function(data, options){   options.filename = data.path;   return ejs.render(data.text, options); }, true);
  | 
 
原理
我们看到在hexo的源码位置: lib/extend/renderer.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 48 49 50 51
   | class Renderer {   constructor() {     this.store = {};     this.storeSync = {};   }
    list(sync) {     return sync ? this.storeSync : this.store;   }
    get(name, sync) {     const store = this[sync ? 'storeSync' : 'store'];
      return store[getExtname(name)] || store[name];   }
    isRenderable(path) {     return Boolean(this.get(path));   }
    isRenderableSync(path) {     return Boolean(this.get(path, true));   }
    getOutput(path) {     const renderer = this.get(path);     return renderer ? renderer.output : '';   }
    register(name, output, fn, sync) {     if (!name) throw new TypeError('name is required');     if (!output) throw new TypeError('output is required');     if (typeof fn !== 'function') throw new TypeError('fn must be a function');
      name = getExtname(name);     output = getExtname(output);
      if (sync) {       this.storeSync[name] = fn;       this.storeSync[name].output = output;
        this.store[name] = Promise.method(fn);     } else {       if (fn.length > 2) fn = Promise.promisify(fn);       this.store[name] = fn;     }
      this.store[name].output = output;     this.store[name].compile = fn.compile;   } }
  | 
 
相当于它内部有一个store维护了一个扩展名和对应的渲染函数的映射关系,然后我们在注册的时候,就是往这个store中添加一个映射关系。
然后当我们调用hexo.render.render的时候,我们看到hexo对象上面的render是一个Render类对象:
1 2 3 4 5 6 7
   | class Hexo extends EventEmitter{   constructor(....){     ...     this.render = new Render(this);     ....   } }
  | 
 
然后我们再看一下这个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 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
   | 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 Promise.reject(new TypeError('No input file or string!'));
      if (data.text != null) {       promise = Promise.resolve(data.text);     } else if (!data.path) {       return Promise.reject(new TypeError('No input file or string!'));     } else {       promise = 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 = 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]     });   } }
  | 
 
这个就比较好理解,大概就是根据类型,获取对应的渲染函数,然后调用渲染函数,最后返回渲染结果。
总结
这样hexo的扩展性就相对比较好了,比如前端有什么sass、less,然后比如变成css后,又想要Compress这个文件内容。另外我们也知道hexo的配置文件是yaml格式,那内部相当于是hexo.render.render({path: '${path.yml}'})比如转成json,然后做配置文件的合并什么的了。 最终实际的渲染,变成了对应的npm包的逻辑了,我们主要把文件的类型和渲染引擎的对应关系在Map里面建立好就好。
Hexo专栏
目前 Hexo 系列形成了一个小专栏,欢迎读者继续阅读: Hexo专栏地址