如何选择-Web-前端模板引擎

7,452 阅读10分钟
原文链接: github.com

Web 模板就在那里

无论你是否直接使用模板引擎, Web 模板一直都在, 不在前端就在后端. 甚至可追溯到超文本标记语言 HTML 标准正式确立之前.

服务器端的模板引擎

我所知道最早的 Web 模板引擎是 PHP, 正式诞生于 1997 年, 工作在服务器端. 让我们看看 PHP 官方的 intro-whatis:

PHP(“PHP: Hypertext Preprocessor”,超文本预处理器的字母缩写)是一种被广泛应用的开放源代码的多用途脚本语言,它可嵌入到 HTML中,尤其适合 web 开发。

在 PHP 的世界里多次出现过再次包装的模板引擎, 著名的有 smarty.

PHPer 普遍赞同 PHP 本身就是最天然, 原生的 PHP 模板引擎, 因为她本来就是. 问题 smarty 有存在的必要么?

其它服务器端语言很多都有 HTML 模板引擎, 比如 JSP, mustache.

好无疑问, 这些服务器端模板引擎最终生成的结果是 HTML(XML) 字符串. 处理流程逻辑使用宿主语言本身的语法实现.

共同特征: HTML 只是个字符串, 最终结果可能还需要类似 Tidy 这样的清洁或修正验证工具.

浏览器端的模板引擎

我所知道最早的前端模板引擎是 jCT 托管于 google code,可能无法访问, 诞生于 2008 年, 宿主语言是 JavaScript, 工作在浏览器中. 很荣幸, 我就是 jCT 的作者, 相关早期博客 achun, github jCT 备份.

直到今天写这篇文章, 我才发现 pure-js 这篇文章里面也提到不少先行者, jemplate 至少在 2006 年就创建了.

今天在 OSC search JavaScript 模板引擎你会得到 100+ 个结果.

共同特征: 全都支持插值.

其它方面差异非常大, 从不同视角来看: 太多, 不能尽举

这里还有 templating-engines 受欢迎度的对比(可以添加). 甚至 best-javascript-templating-engines 投票及正反方的理由.

如何选择

我认为存在即合理, 每个引擎, 框架总有可取之处, 至少在你的应用里, 在某个时代. 所以本文不会评论某个引擎那一点不好, 那样是不客观的.

下面是通常意义上的引擎选择建议, 对于你的应用要分析几个问题: 阶进, 逐步筛选

  1. 前提, 选择的引擎能满足数据渲染需求, 且不和现有依赖冲突, 如果你已经非常熟悉某个引擎, 那你已经有答案了.
  2. 是一次性的项目需求么? 直接选择轻量的, 学习复杂最低的
  3. 是要做组件开发么?
  4. 引擎支持预编译结果, 不必每次都实时编译么?
  5. 要跨平台么? 有官方提供支持的, 首选类 React-JSX 的引擎或纯粹的 VDOM 引擎
  6. 选择学习或维护复杂度最低的, 周知, 开发者对调试的时间超过写代码的时间深恶痛绝
  7. 最后才是性能对比, 性能对比是一件非常细致的工作, 他人的对比结果不一定符合你的场景

我认为应该被弱化的问题: 对语法风格的对比, 偏好是没有可比性的, 一些语法甚至有特殊的背景原因.

现在先回答第一个问题: smarty 有存在的必要么?. 我的答案: 有. 理由很简单, 看给谁用, 看大背景. 对于前后端没有分离的应用, 或前端人员对后端语言不够熟悉, 或因岗位职责, 那么前端人员掌握一种比较通用的模板语法(语言)是现实的. 反之让 PHPer 自己去使用 smarty 那就太浪费技能了.

为什么最后才是性能对比?

性能的确很重要, 但如果性能还没有影响到你的应用体验度, 那就忽视它. 很难真实的模拟应用场景, 通常只有通过真实场景来检验, 目前的测试工具还打不到这种效果. 如果你发现有, 请推荐, 我也需要.

前述问题有些有固定答案, 余下的问题: 如何对比组件开发, 支持预编译, 复杂度?

组件开发

进行组件开发已经不再是选择模板引擎的问题了, 这是生态环境选择的问题. 现实是你的应用需要更快的完成, 时间点是第一位的, 选择流行框架吧, 甚至眼前他们提供足够多的组件让你使用或参考. 如果你的应用有独立的生态环境, 需要技术选型以便长期维护, 那继续看下文.

预编译

预编译应该具备:

  1. 编译结果在目标环境不再需要编译过程.
  2. 编译结果可调试性, 这意味着结果应该包含原生 ECMAScript 代码, 而不是纯粹的数据描述.

周知 React-JSX 是支持预编译的, 官方的说法是 React Without JSX, 即总是 build 过的.

一些基于字符串处理的引擎也支持预编译.

如果你需要预编译, 建议抛弃编译结果依然是基于字符串拼接的引擎, 还不如不预编译, 那是 HTML5 未被广泛支持之前的选择.

另外, 极其专业的 polyfill.io 可以帮你解决 99% 的浏览器兼容问题.

至少也要有类似 React-JSX 这样的编译结果才具有可调试性. 备注: Vue.js 支持多种模板引擎, 可达到同样效果

原 ReactJS 代码: 其中用到了 Web Components 的概念

class HelloMessage extends React.Component {
  render() {
    return <div>Hello <x-search>{this.props.name}</x-search>!</div>;
  }
}

编译后:

class HelloMessage extends React.Component {
  render() {
    return React.createElement(
      "div",
      null,
      "Hello ",
      React.createElement(
        "x-search",
        null,
        this.props.name
      ),
      "!"
    );
  }
}

不少 VDOM 引擎也可以编译类似效果, 比如 htmltemplate-vdom.

    <script>
        var id = 3;
        var env = {
            people: [
                {
                    id: 'id1',
                    name: 'John',
                    inner: [{ title: 'a1' }, { title: 'b1' }],
                    city: 'New York',
                    active: true
                },
                {
                    id: 'id2',
                    name: 'Mary',
                    inner: [{ title: 'a2' }, { title: 'b2' }],
                    city: 'Moscow'
                }
            ],
            githubLink: 'https://github.com/agentcooper/htmltemplate-vdom',
            itemClick: function(id) {
                env.people.forEach(function(person) {
                    person.active = String(person.id) === String(id);
                });
                loop.update(env);
            }
            // Omitted ....
        };
    </script>

复杂度

很难用唯一的标准去评判两个引擎那个复杂度低. 这是由使用者的思维模式不同造成的. 例如前一章节列出的引擎在使用上以及预编译结果上的区别, 不同使用者感触是不同的. 这正是这些不同引擎存在的合理性, 价值性. 世界因不同而精彩.

有的使用者认为这个应用场景有字符串模板就满足了需求, 轻量够用.
有的使用者认为字符串拼接技术的模板引擎不够强壮, 不够时代感.
有的使用者认为 OOP 够理性, 够逻辑, 够抽象.
有的使用者认为原生 HTML 才叫前端.
有的使用者认为 VDOM 适用性更广.

这些评判都有各自的理由, 着眼点不同, 标准也就不同了.

字符串类模板通常都很轻量, 不在本节讨论范围之内.

对于非字符串模板复杂度评判的共性标准是什么? 我认为, 可以考量数据绑定的复杂度.

本文所指的数据绑定不只是插值, 还包括上下文以及事件, 甚至是整个运行期的宿主环境.

事实上至少需要达到 VDOM 级别的引擎才具有这种能力, 因为通过 VDOM 可以映射到真实的 DOM 节点.

大概有几种模式(组合):

  1. 入口参数是个 Object, 模板中的变量 x 是该对象的 .x 属性. 例: virtual-stache-example
  2. 特定语法或属性, 比如: Vue.js 的 <a v-on:click="doSomething">...</a>, 属性 computed, methods
  3. 抽象的语义化属性, 比如: Vue.js 的 active 这个词适用于多种场景, 容易理解且不产生歧义
  4. 不负责绑定, 需要使用者非常熟悉原生方法, 用原生方法进行绑定. 比如: PowJS

这些模式只是理论方面的, 通常是模板引擎设计者要解决的问题. 对于使用者来说不如直接问:

  1. 可以在 HTML 模板中直接写最简单的 console.log(context) 来调试么?
  2. 可以在多层 DOM 树绑定或传递不同的上下文参数么?
  3. 可以在多层 DOM 树内层向上访问已经生成的 Node 么?

模板引擎团队会给你正确的解决办法, 但通常和问题字面描述的目标有所差异. 我觉得这就是你评判选择的关键, 你对官方给出的正确方法的认可度.

嵌入到 DOM 中

嵌入到 HTML 中

这是本文开篇 PHP 自述里面的话, 历史原因 PHP 依然是服务器端的超文本预处理器, HTML 在 PHP 中依然是字符串. 但是:

PHP 视角中的 HTML 就是字符串, PHP 真的无缝嵌入到 HTML 这个 "宿主" 中了

在 WEB 业内标准完善, 环境大大提高的今天, 前端模板引擎能不能突破仅仅嵌入到 HTML 字符串或嵌入到 VDOM, 能不能真正的

嵌入到 DOM 中

PowJS 做到了这一点, 当然我也是 PowJS 的设计者. PowJS 是这么实现的:

  • 实现模板必须要实现的指令
  • 预编译输出原生 ECMAScript 代码
  • 模板语法结构与 ECMAScript 函数写法一致

最终, 写 PowJS 模板就像在写 ECMAScript 函数.

GoHub index 中的写法

<template>
  <details func="repo" param="data" if="is.object(data.content)&&!sel(`#panel details[sha='${data.sha}']`)"
    open
    let="ctx=data.content"
    sha="{{data.sha}}"
    origin="{{ctx.Repo}}"
    repo="{{data.owner}}/{{data.repo}}"
    subdir="{{ctx.Subdir||''}}"
    filename="{{ctx.Filename}}"
    render=":ctx"
    do="this.renew(sel(`#panel details[repo='${data.owner}/${data.repo}']`))"
    break
  >
    <summary>{{ctx.Description}}</summary>
    <div if="':';" each="ctx.Package,val-pkg">
      <p title="{{pkg.Progress}}: {{pkg.Synopsis}}">{{pkg.Import}}</p>
    </div>
  </details>
  <dl func="list" param="data"
    if="!sel(`#panel details[sha='${data.sha}']`)&&':'||'';"
    each="data.content,data.sha,val-rep"
    do="this.appendTo(sel('#panel'))">
    <details sha="{{sha}}" repo="{{rep.repo}}">
      <summary>{{rep.synopsis}}</summary>
    </details>
  </dl>
</template>

模板中出现的指令并不陌生, 是多数模版都会实现的指令. 这里面还有:

  • 全局对象 is,sel
  • 模板(函数)命名 repo, list
  • 模板(函数)入口形参 data
  • 自定义局部变量 ctx
  • 下层模板(函数)形参推导 data.sha->sha
  • 遍历值到下层模板形参推导 (ctx.Package,val-pkg)->pkg; (data.content,val-rep)->rep
  • DOM 节点操作 this.renew, this.appendTo. 这直接渲染到页面 DOM 树
  • 流程控制 break
  • 伪节点 if="':';", 渲染时根本不生成 div 节点, 它是个伪节点, 相当于块代码符号 "{}"

关键: 整个模板结构, 指令语义和 ECMAScript 函数完全一致.

  • 没有数据绑定, 你写的是 ECMAScript 函数, 传参数好了, 要什么绑定
  • 没有事件绑定, 每个节点都是真实存在的, 直接写 addEventListener 好了
  • 要调试, 随便找个 doiflet 插入 _=console.log(x), 好了, 逗号表达式几乎可以无缝插入所有原生语句
  • 导出视图是 ECMAScript 源码, 下图截取自演示 My Folders

image

那么 PowJS 是最终的选择么?

PowJS 的理念是原生性, 原生的 DOM, 原生的 ECMAScript.

原生也同样是 PowJS 的问题所在, 不是所有的使用者都喜欢原生, 我相信有的使用者更喜欢更抽象风格, 他们眼中的原生总是带了点 "原始".

我的观点依然是:

你的需求是选择模板的关键, 世界因不同而精彩

友情推广

希望 Golang 开发者使用 Gohub, Golang-China 需要您的贡献.