模板,从服务端到客户端

时间:2021-12-10 16:14:44

 英文原文 Client-Side Templating

  在浏览器中使用模板是一个日渐热门的趋势。将服务端的逻辑应用到客户端上,还有越来越多的类MVC模式(模型-视图-控制器:model-view-controller)的使用都使得在浏览器中“模板”的角色越来越重要。在过去,“模板”从来都是服务端的事情,但事实上在客户端开发中,模板的作用是非常强大又具有表现力的。

  为什么要使用模板?

  大体上来说,借助模板是一种能很好地将视图(views)中标记和逻辑分开的方法,还能将代码的重用性和可维护性最大化。如果使用的是语法与最终所得结果很相近的语言(比如HTML),你就能又快又好地把任务完成了。虽然模板可以用来输出任何形式的文本,但由于我们想要讨论的客户端开发是有关于HTML的,所以在这篇文章里,我们还是以HTML作为例子。

  现在的动态应用中,客户端常常需要频繁地刷新界面。这个效果可以通过服务端将HTML片段插入到客户端的文档中。这样做的话,服务器要能支持传送HTML的片段(与之相对:传送完整的页面)。还有就是,作为一个要处理这些标记片段的客户端的开发者,你应该会想能完全控制你的模板。而模板引擎(Smarty)、流量(Velocity)还有ASP这些服务器端的内容你都不用了解,也不用管那些“面条式代码”(spaghetti code):例如在HTML文档里是不是出现的臭名昭著的<?或者<%。

  那么现在来看看客户端模板吧。

  第一印象

  对初学者而言,理解“模板”的含义很重要,foldoc(免费在线计算机词典)中的解释是:模板是一种文档,不过文档中有形参,再通过模板处理系统的特定语法用实参代替形参

  让我们来看看最基本的模板长什么样子:

<h1>{{title}}</h1>
<ul>
{{#names}}
<li>{{name}}</li>
{{
/names}}
</ul>

  如果你写过HTML,那么你一定很熟悉上面的代码。上文的HTML中有一些占位符。这些占位符将会被真实的数据取代。例如这个对象:

 var data = {
"title": "Story",
"names": [
{
"name": "Tarzan"},
{
"name": "Jane"}
]
}

  把数据和模板结合起来,就会得到下面的HTML代码:

 <h1>Story</h1>
<ul>
<li>Tarzan</li>
<li>Jane</ul>
</ul>

  将模板和数据分离开来对于维护HTML来说是一件好事。比如说,如果想要更改标签或者添加类(class)就只需要更改模板就可以了。另外,对于需要迭代出现的元素(比如<li>),程序员只需要写一次就好了。

  模板引擎

  模板的语法是根据你需要的模板引擎来决定的(例如:占位符{{title}})。引擎是负责分析模板,用提供的数据替换占位符(变量、函数、循环等等)。

  有些模板引擎看起来没有什么逻辑性。这指的不是在模板中只能插入简单的占位符,而是说智能标签(intelligent tags)方面的特性很少(比如数组迭代器,条件渲染等等)。有些引擎就有很多特性和很好的可扩展性。关于这一点就不在这展开讲了,你需要问问自己,在模板中你是否需要、需要多少逻辑。

  每个模板引擎都有自己的API,不过通常你都能找到像render()和compile()这样的方法。渲染的过程就是将真正的数据放入模板然后呈现出来。也就是说,渲染就是用真正的数据替代了占位符。如果在此期间木板上有什么逻辑,就会被执行。编译模板指的是解析模板,然后将它转换成一个JavaScript函数。模板中的逻辑都会被解释为纯JS(plain JavaScript),给定的数据会被传入这些JS函数中,这么做可以最大程度地优化HTML。

  Mustache实例

  上文中的例子可以借助模板引擎实现,例如使用了Mustache模板语法的mustache.js。关于这种语法更多信息,我会在后面告诉你的。现在先来看看下面的JS代码能得到什么效果:

var template = '<h1>{{title}}</h1><ul>{{#names}}<li>{{name}}</li>{{/names}}</ul>';
var data = {"title": "Story", "names": [{"name": "Tarzan"}, {"name": "Jane"}]};
var result = Mustache.render(template, data);

  现在我们需要在页面上显示模板,你需要写这么一行代码:

document.body.innerHTML = result;

  第一个客户端模板就完成了!在代码文件中加入下面这句,你就可以试一试上面的例子了,或者看下在线演示

<script src="https://raw.github.com/janl/mustache.js/master/mustache.js"></script>

  组织模板

  如果你和我一样,不喜欢HTML文档里出现很长的内容,既造成了阅读的困难还增加了维护的负担。理想情况下,我们可以把模板分开维护,既能享受模板的语法高亮的便利,又能保证HTML的可读性。

  但事情总不会十全十美的。如果一个项目中要使用非常多的模板,出于避免过多Ajax请求而影响性能的原因,我们不希望这么多文件被分开加载下来。

  场景1:脚本标签

  常见的解决方案就是把所有的模板直接放在<scrpit>标签中,<script>标签的可选类型要稍作更改,比如改成type=”type/template”(浏览器在渲染或解析时会将这个属性忽略)。

<script id="myTemplate" type="text/x-handlebars-template">
<h1>{{title}}</h1>
<ul>
{{#names}}
<li>{{name}}</li>
{{
/names}}
</ul>
</script>

  这样的做,你就可以把所有的模板都放在HTML文档中,避免了额外的Ajax请求。

  script标签中的内容会后面被JavaScript当做模板来使用。请看下面的代码,这次我们用的是Handlebars模板引擎再结合一些jQuery,模板就用刚刚的里的。也可以直接看在线演示

var template = $('#myTemplate').html();
var compiledTemplate = Handlebars.compile(template);
var result = compiledTemplate(data);

  最终效果和上文的Mustache例子是一样的。Handlebars也可以使用Mustache格式的模板,所以在这里我们就用一样的模板了。不过要注意,它们之间还是有一个很重要的区别:Handlebars是先得到一个中间结果,再通过这个中间值得到HTML的。它先是将模板编译成一个JS函数(称之为compiledTemplate),然后数据再被传入这个函数中执行,再返回最终结果

  场景2:预编译模板

  虽然说将渲染模板包装在一个方法里看起来要方便多了,但是将编译和渲染分开也有显而易见的优点。最重要的是,分开以后,可以把编译放在服务器端完成。我们可以在服务器上执行JS代码(比如使用Node),有些模板引擎支持这样的预编译。

  我们可以用一个JS文档(叫它comiled.js吧)将多个预编译好的文件放在一起。这个文件的内容看起来可能是这样的:

 var myTemplates = {
templateA: function() { ….},
templateB: function() { ….};
templateC: function() { ….};
};

  然后在应用中,我们只需要将数据传入这些预编译好的模板中:

var result = myTemplates.templateB(data);

  这个方法远比上文中讨论过的将所有的模板放在<script type=”text/javascript”>中要好,客户端会忽略编译过程。取决于你的应用套件(application stack),这个解决方式并不一定很难实现,我们会在下文看到它具体的实现。

  Node.js示例

  任何模板预编译脚本至少要满足下面的要求:

  1. 读取模板文件,
  2. 编译模板,
  3. 最后的结果可以被合并入一个或多个文件、

  下文中的Node.js脚本就实现了上面说的那3点(使用Hogan.js模板引擎):

 var fs = require('fs'),
hogan
= require('hogan.js');
var templateDir = './templates/',
template,
templateKey,
result
= 'var myTemplates = {};';
fs.readdirSync(templateDir).forEach(function(templateFile) {
template
= fs.readFileSync(templateDir + templateFile, 'utf8');
templateKey
= templateFile.substr(0, templateFile.lastIndexOf('.'));
result
+= 'myTemplates["'+templateKey+'"] = ';
result
+= 'new Hogan.Template(' + hogan.compile(template, {asString: true}) + ');'
});
fs.writeFile(
'compiled.js', result, 'utf8');

  这段代码先是读取了在templates目录下所有的文件,再编译了这些模板,最后将它们写入compiled.js。

  注意!现在得到的结果是完全没有优化过的代码,也没有做任何错误处理。不过它还是完成我们想要它做的事,也不需要很长的代码来预编译模板。

  场景3:AMD和RequireJS

  随着异步牵引模块(通常我们都称之为AMD)越来越多地被使用,为了更好地组织你的APP,建议将模块解耦。RequireJS是现在主流的模块加载器之一,在模块定义中,你可以特定某些依赖,在实际的模块里你就可以使用它们了(工厂模式)。

  在使用模块时,RequireJS有一个text插件用于规定基于文本的依赖。默认是将AMD的依赖当做JavaScript来处理,不过模板并不是JS而是文本(比如HTML格式的模板),所以我们需要用上这个插件:

 define(['handlebars', 'text!templates/myTemplate.html'], function(Handlebars, template) {
var myModule = {
render: function() {
var data = {"title": "Story", "names": [{"name": "Tarzan"}, {"name": "Jane"}]};
var compiledTemplate = Handlebars.compile(template);
return compiledTemplate(data);
}
};
return myModule;
});

  这样,就能在单独的文件中管理各个模板了,虽然这么做是挺好的,但无疑增加了很多额外的Ajax请求,而且仍然需要在客户端编译模板。但是,可以用RequireJS中的r.js来优化这些额外的请求。这个决定了依赖,将模板或者依赖植入模块定义中,大大减小了请求数。

  你会发现我们还没有说到预处理,事实上有两个方法可以完成预处理。可以写一个r.js的插件或者别的程序来预处理模板。这么做的话就会改动了模块定义:我们需要在优化之前先使用一个模板*字符串*,然后再使用一个模板*方法*。不过这些问题也不是很难处理,你可以去检测它的变量类型或者将逻辑抽象出来(写在插件中或者直接写在应用中)。

  监听模板

  在场景2和场景3中,如果将模板当做未编译的资源我们还能将应用构建地更好。就像你在写CoffeeScript、Less或者SCSS,在开发时,可以监听模板文件的变化,一旦发现文件出现变化,就立刻自动重新编译,就像从CoffeeScript编译到JavaScript一样。这样我们在代码中处理的模板都是已经预编译过了的,还方便了在开发过程汇中将预编译模板做相关的内联优化。

define(['templates/myTemplate.js'], function(compiledTemplate) {
var myModule = {
render: function() {
var data = {"title": "Story", "names": [{"name": "Tarzan"}, {"name": "Jane"}]};
return compiledTemplate(data);
};
};
return myModule;
}

  性能问题

  用客户端模板完成UI更新时的渲染是常见的方法。还是那句话,想要达到性能最优,那就要在第一次请求页面时尽可能少的请求额外的资源。这样浏览器在渲染HTML页面时不会因为要去加载JS资源或者别的数据而中断渲染。这听起来挺难的,特别是在又要动态加载内容又要尽可能减少加载时间的页面上。理想情况下,模板是既可以在客户端也可以在服务端使用的,这样可以提供最优的性能还能保持它的可维护性。

  有两个问题还需要考虑一下:

  1. 我的应用中哪里是有最多动态加载的呢?又是哪部分需要最短的加载时间的呢?
  2. 处理种种问题的程序是要放在客户端还是服务端呢?

  实际问题实际分析。确实使用预处理过的模板,客户端可以比较轻易地快速渲染出效果。但是如果你需要重用模板,你会偏爱逻辑较少的模板一些。

  结论

  我们已经看到了客户端模板的种种好处,比如:

  • 服务器和API最好只负责提供数据(比如JSON);客户端模板就能直接把数据套上了。
  • 客户端方向的开发者可以自如地使用HTML和JS。
  • 使用模板的话,你就必须把逻辑和表现分离开。
  • 模板可以预编译好然后缓存起来,这样服务器每次都只要发送数据就可以了
  • 不在服务器端渲染而在客户端渲染,多少会影响性能

  上述的文字已经介绍了很多关于(客户端)模板的知识,希望现在你对这些内容有了更深的认识。


作者: Lars Kappert  来源: 伯乐在线  发布时间: 2014-06-24 09:45