浅谈前端模版

Category:
发表:
大纲
  1. 1. 几种常用的Javascript模版
    1. 1.1. Mustache
  2. 2. {{name}}
    1. 2.1. Handlebars.js
    2. 2.2. Underscore#template
    3. 2.3. Embedded JS
    4. 2.4. Jade
  3. 3. Javascript模版引擎的基本原理
    1. 3.1. John Resig的实现
    2. 3.2. 更一般的例子
  4. 4. 服务端模版和客户端模版
    1. 4.1. 服务端模版的开发方式
    2. 4.2. 客户端模版的开发方式
    3. 4.3. 前后端分离与页面模版
  5. 5. 模版的技术选型
  6. 6. 参考列表

随着近几年Web方面技术的爆发式发展,前端应用变得越来越复杂,基于后端的Node.js(io.js)也开始拥有了自己的一片天地,谁也没能想到Javascript这样一门在几年前被看作是玩具的客户端脚本语言现在被赋予了新的生命力。依稀记得大学那会学习web开发时,不止一次的被人告诫Javascript就是个玩具,弄来搞一搞简单的页面交互还成,其他的就不行了。现如今Javascript及其周边的活跃程度赤裸裸的打了当年不看好Javascript语言那些人的脸。额,好像有点扯远了。

言归正传,本文将会聊聊前端模版或者说Javascript模版的相关内容。现代web开发不再像以前那般过于重视后端轻视前端,将前端代码杂乱的糅合在一起,只要能够满足展示要求就行了。现在展示层面的东西有一种说法叫做数据与界面分离。而Javascript模版作为数据与界面分离工作中最重要的一环,也越来越受到广大开发者的关注,近两年来社区中也不断涌现各种各样的Javascript模版。

本文将结合相应的示例代码,介绍几种现今流行的Javascript模版作品,并介绍一些关于服务端、客户端模版的概念。

几种常用的Javascript模版

Mustache

Mustache通常被称为JavaScript模板的基础,它有许多语言版本的实现,从其官网上可以看出基本上常用的开发语言都有Mustache模版的实现。这里我们仅介绍Mustache模版的Javascript实现。先来看一段代码,

Mustache.render("Hello, {{name}}", { name: "Jack" });

在客户端(浏览器环境)中引入Mustache文件后,就可以使用Mustache对象,此对象有一个重要的方法叫做render,这个方法有两个参数,第一个参数为模版字符串,第二个参数为传递给模版的data对象。

示例代码中的模版字符串为Hello, {{name}},其中{{name}}这种使用大括号包围起来的字符称之为占位符,意思是告诉Mustache将会使用data对象中的name属性值来替换此处的占位符。

其实,基本上所有的字符串模版都是这么个原理,只不过不同模版引擎的实现方式有所差异。

占位符{{name}}中的{{和}}我们称之为模版的界定符,一般来说,模版引擎都会提供让用户自定义界定符的功能。

一般地,我们在使用Mustache进行前端开发的时候,模版字符串不太可能就是一个简单的字符串,而是一段模版甚至是通过异步获取到的模版。比如,

<script id="template" type="x-tmpl-mustache">
    Hello {{name}}!
</script>

<script>
var template = $('#template').html();
Mustache.parse(template);
var rendered = Mustache.render(template, {name: "Luke"});
$('#target').html(rendered);
</script>

或者,

$.get('template.mst', function(template) {
    var rendered = Mustache.render(template, {name: "Luke"});
    $('#target').html(rendered);
});

前者通过给script标签一个浏览器不能解析的属性x-tmpl-mustache来保存模版,然后通过JQuery取得模版字符串后进行渲染,最后再插入到指定的dom节点中。后者通过一个get请求拿到所需的模版字符串,在回调中进行渲染,最后在插入到目标节点中。

以上两种基本上就是在客户端使用Mustache进行模版开发的经典demo。

虽然Mustache的官网上说Mustache是一款Logic-less的模版引擎,不过Mustache还是支持一些简单的逻辑语法。

常用的有:遍历、判断等。语法是这样的,

{{#section}}
your logic code
{{/section}}

比如,

{{#people}}

{{name}}

{{/people}}

给模版传入数据,

{
    people: [
        {name: "Jack"},
        {name: "Fred"}
    ] 
}

那么最终得到的结果为,

<h1>Jack</h1>
<h1>Fred</h1>

Handlebars.js

Handlebars为最流行的模板引擎之一,基于Mustache构建而成,高度兼容Mustache,基本上Mustache的模版可以拿来当作Handlebars的模版来使用。与此同时,Handlebars内置了许多有用的helper,而且你可以自己通过handlebars提供的Handlebars.registerHelper来注册自定义的helper,方便模版的编写。(关于这里的helper,我们可以简单的将其看成是一个个的预处理器)

我们先来上一段代码看一下handlebars是如何工作的,

<script id="entry-template" type="text/x-handlebars-template">
    <div class="entry">
        <h1>{{title}}</h1>
        <div class="body">
            {{body}}
        </div>
    </div>
</script>

<script>
    var source = $("#entry-template").html();
    var template = Handlebars.compile(source);

    var context = {title: "My New Post", body: "This is my first post!"};
    var html = template(context);
</script>

最后得到的结果,即html变量的内容是,

<div class="entry">
    <h1>My New Post</h1>
    <div class="body">
        This is my first post!
    </div>
</div>

从上面的代码中,我们可以看出Handlebars的工作方式与Mustache是不一样的。

还记得吗,Mustache的render方式是接受两个参数一步就得到了渲染后的模版,而Handlebars要先进行Handlebars.compile,此方法其实会返回一个function,然后执行这个返回的方法并传入data数据进行模版渲染才能得到最终的模版。

Underscore#template

underscore.js是一款Javascript工具库,它提供了各种各样的工具方法,同时也提供了简单的Javascript模版。

underscore.js提供的模版方法与Mustache或者Handlebars都不太一致,它采用<%%>作为模版界定符,而且<%%>有三种变形,

  • <% %>,用于执行javascript代码
  • <%= %>,用于输出模版属性值
  • <%- %>,用于输出模版属性值,同时进行html转义

示例代码如下,

var compiled = _.template("hello: <%= name %>");
compiled({name: 'moe'}); // hello: moe

var template = _.template("<b><%- value %></b>");
template({value: '<script>'}); // <b>&lt;script&gt;</b>

var compiled = _.template("<% print('Hello ' + epithet); %>");
compiled({epithet: "stooge"}); // Hello stooge

一般地,我们可以通过使用<% %>来执行一些javascript逻辑,比如,

var template = 
    '<ul>' +
    '<% _.each(people, function(name) { %>' +
        '<li><%= name %></li>' +
    '<% }); %>' +
    '</ul>';

_.template(template, { people: ["Jack", "Fred"] } );

上面示例代码的模版渲染后,得到的结果如下,

<ul>
    <li>Jack</li>
    <li>Fred</li>
</ul>

Embedded JS

Embedded JS(EJS)是一款应用于客户端(浏览器环境)的javascript模版引擎。EJS的特别之处在于需要把模板存于单独文件中,并将文件名传递给EJS。它会加载该文件,并返回HTML。它的语法基本与underscore#template一致。示例代码如下,

// this is template.ejs file
hello, <%= name %>
// this is a js file
var html = new EJS({ url: "template.ejs" }).render({ name: "Jack" }); // hello, Jack

与此同时,EJS还内置提供了许多Tag,比如link_toimg_tag等。其实这些Tag的作用其实跟Handlebars中的helper的使用原理是一致的。

借助NodeJS平台,EJS也可以用于服务端。详情请看tj/ejs

Jade

相对于之前提到的几款javascript模版引擎,jade显得比较另类,它是专门为服务端设计、基于Nodejs环境的一款服务端模版引擎(当然也是可以直接用在浏览器端的,在浏览器端将jade文件编译成html插入dom节点即可,但是一般我们都不这么干)。而且jade的语法也较为另类,它受Haml的影响较重,采用空格和缩进来规范jade语法。

下面让我们来看一段jade模版的示例代码,

doctype html
html(lang="en")
  head
    title= pageTitle
    script(type='text/javascript').
      if (foo) {
         bar(1 + 5)
      }
  body
    h1 Jade - node template engine
    #container.col
      if youAreUsingJade
        p You are amazing
      else
        p Get on it!
      p.
        Jade is a terse and simple
        templating language with a
        strong focus on performance
        and powerful features.

经过编译后,它得到的html代码如下,

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Jade</title>
    <script type="text/javascript">
      if (foo) {
         bar(1 + 5)
      }
    </script>
  </head>
  <body>
    <h1>Jade - node template engine</h1>
    <div id="container" class="col">
      <p>You are amazing</p>
      <p>
        Jade is a terse and simple
        templating language with a
        strong focus on performance
        and powerful features.
      </p>
    </div>
  </body>
</html>

初次接触jade模版你可能会非常不适应这种模版语法,但是没关系,一旦熟悉了,你会发现使用jade来输出html比你直接写原生的html代码要快上许多,而且显得很高雅。

Jade模版相对于之前提到的几款模版有几个非常大的区别,

  • jade模版引擎关注的level更高,它更加关注html的输出而不是某个片段
  • jade模版的渲染不再是简单的模版字符串替换

除此之外,jade提供的功能非常全面,且有几个是前面提到的几种模版引擎不支持的,比如模版继承、模版包含、Mixin、脚本及样式文件的直接引入等。

关于jade更多的详细内容,请参阅官网指南,这里还有两个快速指南,Jade syntax tutorialJade logic tutorial.

Javascript模版引擎的基本原理

一般来说,Javascript模版(比如上述提到的mustach、handlebars、ejs等,不过不包括jade)就是一堆html代码中糅合了部分Javascript代码。那么Javascript模版引擎的工作就是解析模版中的Javascript代码,最终得到一段全是html的代码。说的再具体一点,模版引擎的基本原理其实就是剥离出模版中的Javascript代码进行字符串替换,使用拼接得到的字符串构造函数,然后给函数传递一个数据对象,动态的执行Javascript字符串,返回真实的html代码字符串

上面的理论没看明白?没关系,我们下面来详细的说一说。

John Resig的实现

早些时候,JQuery的作者John Resig写了一篇文章Javascript Micro-Templating。本篇文章可以说是道出了所有字符串模版引擎的基本实现原理。

文章中有一段模版解析的逻辑代码,

function tmpl(str, data){
    var fn = new Function("obj",
        "var p=[],print=function(){p.push.apply(p,arguments);};" +
        "with(obj){p.push('" +
        str
          .replace(/[\r\t\n]/g, " ")
          .split("<%").join("\t")
          .replace(/((^|%>)[^\t]*)'/g, "$1\r")
          .replace(/\t=(.*?)%>/g, "',$1,'")
          .split("\t").join("');")
          .split("%>").join("p.push('")
          .split("\r").join("\\'") +
        "');}return p.join('');");

    return data ? fn(data) : fn;
}

这段代码最核心的地方在于new Function那一块,

  1. 利用正则对模版中被类似这种<% %>包裹的字符串进行替换
  2. 通过push方法将所有的字符串片段压入临时数组变量p
  3. 最后通过p.join('')返回处理拼接后的字符串
  4. 通过new Function的方式构造一个函数fn
  5. 函数fn内部通过with语法限制变量的作用域
  6. 传入参数data(这个data其实就是模版中Javascript变量的顶层作用域)执行fn返回最终的字符串(此时返回的字符串不再包含Javascript代码)

更一般的例子

让我们把关注重心放在模版是如何变成html代码这样的whole story,而不是具体的替换细节上。

下面一段非常普通的javascript模版代码,

<h1>
<% if (username) % {>
    <b>hello, <%= username %></b>
<% } else { %>
    <b>no user!</b>
<% } %>
</h1>

这段模版使用的语法跟Underscore#TemplateEJS的模版语法是一样的,其中<%%>包裹起来的为Javascript代码,而<%=%>将会直接输出变量的值。

上面这段模版代码进行解析时,类似下面的结果,

var output = [];
output.push('<h1>');
if (username) {
    output.push('<b>hello, ' + useranem + '</b>');
} else {
    output.push('<b>no user!</b>');
}
output.push('</h1>');

解析完毕后,一般还是给出相应的渲染方法,

var render = (function() {
    var str = 
        "var output = [];" +
        "with(scope) {" +
            "output.push('<h1>')" +
            "if (username) {" +
                "output.push('<b>hello, ' + useranem + '</b>');" +
            "} else {" +
                "output.push('<b>no user!</b>');" +
            "}" +
        "}" +
        "output.push('</h1>');";

    return function(data) {
        var fn = new Function('scope', str);
        return fn(data);
    };
})();

可以看出,这个render方法最终将会返回一个Function,我们执行一下这个返回的函数,

console.log(typeof render); // function
render({
    username: 'erik'
}); // <h1><b>hello, erik</b></h1>

render({
    username: ''
}); // <h1><b>no user!</b></h1>

服务端模版和客户端模版

不同的模版引擎可以根据不同的分类标准进行分类。下面我们来谈一谈服务端模版和客户端模版。

  • 所谓的服务端模版,简单的来说就是编译(解析模版过程)和渲染(替换占位符填充数据过程)都是在服务端完成的,最终发送到客户端的其实是一个完整的html或者html片段。这种类型的模版引擎包括PHP中的Smarty以及Java中Velocity
  • 所谓的客户端模版,简单的来说就是编译和渲染都是在客户端完成的(或者编译是在服务端完成的,数据渲染是在客户端完成的)。

前面提到的几种流行模版引擎,借助Nodejs平台其实都是可以在服务端跑起来。所以他们基本上是既可以用在服务端也可以用在客户端。

服务端模版的开发方式

正如之前提到的Smarty或者Velocity模版引擎,他们是典型的服务端模版,在服务端会做完编译以及渲染工作。

拿Smarty模版引擎来举个例子,第一阶段smarty会先对模版进行解析即编译过程(如果smarty开启了cache功能,那么就可以复用模版第一次编译后的结果,后面可以使用不同的数据对已经编译好的模版进行重复数据渲染),编译完成之后会生成结果文件;第二阶段php响应客户端请求时,smarty会取出编译好的模版文件并传入数据对象进行数据填充,数据填充完毕之后将得到完成的html或者html片段,然后将此html发送给客户端。

基本上传统的服务端模版的开发都是这样的模式。

不过,随着web开发越来越复杂,分工越来越细化,这种方式逐渐暴露了一些弊端,最突出的就是服务端的模版文件由谁来写的问题?

按理说服务端的模版文件是运行在服务端的,而且在数据渲染时与服务端的逻辑耦合较多,那么应该是服务端开发人员(比如php开发人员)来写的。但另一方面,页面模版不仅仅需要展现数据,往往还需要专业的前端人员去写页面布局和样式,而这些并不是phper擅长的。所以这块给不同职责的开发人员造成了协作上的障碍。

顺便提一句,现在业界有的企业(比如某宝)由于一些历史原因,不得不继续维护着服务端模版,那么他们的工作模式是怎么样的呢?先由前端人员出一个页面demo,填充满足需求的假数据,然后把这个页面demo丢给服务端开发人员,让服务端人员去套页面。所谓的套页面其实将前端填充的假数据用模版语法来代替。很明显这种方式来回沟通的成本很高,而且不利于维护和拓展。

客户端模版的开发方式

客户端模版可分为两种,一种是在客户端完成模版的编译和渲染工作,另一种是服务端传给客户端的模版是预编译过的,客户端仅仅做一些数据渲染工作。

AngularJS框架提供了内置的模版服务,如下代码,

<!doctype html>
<html ng-app>
  <head>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular.min.js"></script>
  </head>
  <body>
    <div>
      <label>Name:</label>
      <input type="text" ng-model="yourName" placeholder="Enter a name here">
      <hr>
      <h1>Hello {{yourName}}!</h1>
    </div>
  </body>
</html>

其中的<h1>Hello {{yourName}}!</h1>看起来好像跟Mustache的模版语法是一致,它就是AngularJS的模版。不过AngularJS提供的模版是一种典型的纯客户端模版,所有的解析和渲染工作都是在客户端完成的。

一般来说,客户端模版会有如下几个特点,

  • 后端最好是服务型后端,最好只负责提供数据(比如JSON),然后客户端直接使用数据进行渲染
  • 可以将编译好的模版通过<script type="x-tmpl-xxx"></script>这样的标签插入到真实页面中,这样后端只管提供数据就行
  • 可以更加有效的隔离前后开发人员,降低沟通成本
  • 因为不是在服务端渲染,在客户端执行多少都会对性能造成一些影响

前后端分离与页面模版

现在社区有一种很热的思想,叫做前后端分离。这里说的分离更多的是倾向职责上的分离,让更合适的人去做一些事。某宝(某猫)有一些关于基于NodeJS前后端分离的实践,比如这个

现在业界对前后端仍然是处于一个探索的阶段。这里我说一下我个人对前后端分离的看法(完全不负责任的看法:))。

首先明确一下前后端分离的目的是什么?是职责上的分离,为了让合适的人去做正确的事情。而不是为了装逼为了分离而分离。

其次一旦前后端分离将会得到哪些收益?我们先来看看前后端耦合在一起有哪些常见的问题。

  • 随着前端代码、逻辑的堆积,维护扩展变得越来越困难
  • 前后端高度耦合,沟通成本高
  • 业务逻辑分散、渲染逻辑杂乱
  • ….

不说前后端分离能够全部解决上述的问题,但是起码有对症下药的方案去避免。

我比较赞同现在流行的一种前后端分离思想。基于NodeJS平台,从工作职责上将前端和后端重新进行了定义。这里的后端更多的指的是提供服务的底层(或者叫server端更加合适?),而这里的前端则包含了从NodeJS层到客户端层(浏览器层)的所有范畴。如下图,

这无疑扩大了前端的范畴,对前端从业人员也提出了更高的需求。更加具体的分层和隔层职责如下图,

看上去这种职责上的分离的确很美好。

此时,在前后端分离的大前提下,完全可以将服务端模版和客户端模版糅合在一起使用。你既可以在服务端对模版进行编译和渲染,也可以在服务端编译完成发送给客户端进行渲染。因为前后分离后的前端其实包含了以前php开发人员的职责,也就是说在前后端分离的大前提下,除了服务端,其他的层都是同一拨人在做,沟通、实现等完全不是事儿。

模版的技术选型

这里有一个在线应用,可以帮助你根据你的需求来确定你所需要用的模版引擎。

其实我个人的看法,究竟选择那种模版跟Web端的整体架构设计及应用场景这两点是最相关的。web端的整体架构设计决定你可以使用服务端模版还是客户端模版;应用场景决定你对模版的要求,比如性能、速度、逻辑支持程度等等。

参考列表