Template was precompiled with an older version of Handlebars

使用本地高版本 Handlebars 替换 gulp-handlebars 里的底版本依赖是个好主意。

gulp-handlebars 对 Handlebars 的要求是 ^3.0.0,预编译器的版本是 6。Handlebars 4 之后升级预编译器到 7,所以如果使用最新版本 Handlebars,就会报 “Template was precompiled with an older version of Handlebars” 错误。

这个时候有几个解决方案,比如 这个页面 中提到的先删再装。不过经我实测,npm update 可能导致失效,还要重弄,太麻烦。所以最简单的,就是直接用本地的高版本覆盖依赖中的版本:

npm install handlebars --save-dev
rm -rf ./node_modules/gulp-handlebars/node_modules/handlebars
cp -r ./node_modules/handlebars ./node_modules/gulp-handlebars/node_modules

Gulp 中顺序执行任务

Gulp 顺序执行支持3中模式:callback、返回 stream、返回 Promise 对象。

书接上文,Chrome 插件中无法直接使用 Handlebars 处理模板。两种方案,一是利用沙箱,将 .eval() 放在独立的环境中执行,好处是其它跨域的操作也能这样处理,坏处是写起来麻烦。另一种则是利用 Handlebars 的“预编译”功能,将模板提前编译好,直接在代码中引用。好处是写起来更顺畅,并且从发布插件的角度来看,早晚都要这样做。

我决定选用后者,于是我需要把模板提取出来进行预编译,然后我准备写个 Gulp 任务来搞定这个。

从页面中提取模板不算太难,写个正则就好,这里提前约定,模板使用 <script type="x-handlebars-template"> 标签包裹。

gulp.task('template', function () {
  let promise = new Promise((resolve, reject) => {
    fs.readFile('popup.html', (err, content) => {
      if (err) reject(err);
      resolve(content);
    }
  });
  promise.then((content) => {
    content.replace(/<script([^>]*)>([\S\s]+?)<\/script>/g, (match, attr, template) {
      // 具体处理模板,略掉了
    });
  })
});

模板编译之后,还需要用 Webpack 打包才能使用,于是要增加 Webpack 的任务。这个比较简单,参考它的 文档 即可。不过很明显,必须等全部模板预编译完成才能打包。之前我已经试过用 run sequence 顺序执行任务,不过这里我用到 Promise,情况似乎有变。

稍微 Google 一下,得知要能顺序执行任务有三种选择:

使用 callback

gulp.task('task', function (callback) {
  setTimeout(function () {
    // 任务执行完成后,调用 callback
    callback();
  }, 5000);
});

返回 stream

这个好像是标准做法,也是我之前做的。

gulp.task('task', function (callback) {
   returen gulp.src('*.js')
     .pipe(uglify())
     .pipe(gulp.desc('dist/');
});

返回 Promise 对象

gulp.task('task', function () {
  return new Promise(resolve, reject) => {
    resolve();
  });
});

既然支持 Promise 那就好办了,在 .then() 前面加一个 return 就好。


完整代码

参考文章

Handling Sync Tasks with Gulp JS

在Chrome 扩展中使用 Handlebars

Chrome 扩展中无法直接使用 Handlebars,最好使用预编译功能在开发环境中将模板处理好然后直接使用。

Chrome 扩展可以访问到各种用户敏感数据,比如 cookie 之类,所以 Chrome 团队对它的限制非常严格(见 Using eval in Chrome Extensions. Safely.),比如常规环境下完全不能使用 evalnew Function()。这给我们使用 Handlebars 之类的模板工具带来不小的麻烦。

一个解决方案就是使用上文中介绍的 sandbox。将可能使用到 eval 的代码放到一个独立沙盒中,不让它访问到那些敏感信息;然后通过 postMessage API 与之进行数据通讯,完成模板生成工作。

我觉得这个方案不太理想。首先我不喜欢 <iframe>;其次每次渲染模板还要 post 来 post 去,渲染结果也是异步传递(这点暂时存疑)。

另一套方案,则是利用 Handlebars 的预编译功能,将模板在开发环境中编译成函数,在扩展中直接使用。这样做的好处也很明显,因为发布插件时也会先处理代码,这个时候将模板处理完,工作效率更高。

等写完再补充具体代码吧。

Handlebars 4

Handlebars升级到4.0后,{{#if}}不再作为一层影响查找,以前的`../../`可以改成`../`了。

图文……好像有关。

Handlebars自从升到2.0之后,就放开了,版本号蹭蹭涨,如今已经升到4.0版。

这个版本最显著的变化在于层级结构,以往的版本中,所有的Block Helper都可以形成一个层级。对于{{#each}}{{#with}}这种还好说,层级关系比较明确,很好理解;但是{{#if}},也有可能形成一个层级,这就很奇怪了。尤其像我前文所述,它只是可能形成一个层级,就更诡异了。

所以这个修改其实是件好事儿。

接下来,代码时间。

Before

    {{#each list}}
      {{#if is_true}}
        <p>这里需要向上两层才能找到值。{{../../value}}注意,这里是两层。</p>
      {{/if}}
    {{/each}}
    var data = {
        list: [
          // items..
        ],
        value: 'value'
      }
      , template = Handlebars.compile(hbs);
    template(data);

After

    {{#each list}}
      {{#if is_true}}
        <p>这里需要向上两层才能找到值。{{../value}}那,这里只有一层了</p>
      {{/if}}
    {{/each}}

JavaScript是一样的。

总结

为了写这篇文章又去看了一遍Handlebars的文档,发现新增了不少特性,尤其开始支持子表达式,看来有必要再好好看一遍。

记一个踩过三次的坑……

Handlebars.js在遇到形如{{#value}}{{/value}}的模板,且value为object时,会以value为环境查询里面的内容,和mustache.php的表现不一致,可能导致差异。

图文无关。太长时间没写文被降权了么,怎么最近流量跌这么厉害……

老后台的架构仍然是PHP渲染页面,这里我选择mustache.php作为模板引擎。在某些场景下(比如搜索),我希望在前端使用复用同样的模板。正如前面一篇文章所说,我选择的是Handlebars.js,它号称支持mustache语法。

然则在使用当中,我发现二者的实现仍然是有区别的。先上模板。

    {{#value}}{{value}}{{/value}}

这段模板很简单,检查value是否存在,如果是的话则将其输出。这时:

  • mustache.php先以value作为逻辑判断依据;接下来,在value里找不到value这个key,便回到上一层,输出value的值
  • 而在handlebars.js眼中,{{value}}的类型也是很重要的,如果是String或者Number,那就直接输出;如果是Object,它就会采用{{#with ..}}{{/with}}操作,从value里面取值,而value里没有value,所以输出空。

当模板为

    {{#has_value}}{{value}}{{/has_value}}

此时,因为has_value是布尔值,所以Handlebars.js放弃把这个对象当做执行环境,而是继续维持当前的层级,所以就能正确输出value的值。


这个问题虽然不很复杂,但是调试起来并不方便,而且文档中也没提过,所以三次踩坑每次都耽误不少时间。记录如此,避免再次遭遇。

助力Handlebars

单页应用中,模板引擎是必备工具。这方面的选择比较丰富,旧轮子不断获得改进,新的轮子也不断被发明出来。我比较喜欢Handlebars,本文介绍一些加强Handlebars的做法。

单页应用中,模板引擎是必备工具。这方面的选择比较丰富,旧轮子不断获得改进,新的轮子也不断被发明出来。我比较喜欢Handlebars,原因如下:

  1. 模板语言简单。继承自mustache,用{{content}}标记出要替换的内容即可。
  2. 包含逻辑判断和循环。在mustache基础上加强了这个部分,使得代码更易读。
  3. 不支持复杂的逻辑,尤其是嵌套JS。我认为这是模板引擎的关键,任何复杂的逻辑放在表现层都会使得系统不稳定。
  4. 支持预编译。可以加快实际运行的速度。
  5. 提供扩展功能,可以方便地增加自定义功能。这也是本文重点。

简单,是Handlebars的优势;但是原始版有点过分过头,它既缺少Angular模板中的过滤器,难以格式化输出数据;也没有足够的逻辑判断。使得我们必须在使用模板前进行大量预运算,逻辑据处理成模板能辨识的标记,把数据格式化成用户能识别的内容,这与代码复用的原则相违背。所幸Handlebars提供好用的扩展接口,给我们增添功能,适配业务逻辑的机会。

继续阅读“助力Handlebars”