开发者社区> 问答> 正文

模板,从服务端到客户端


  英文原文 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。
  • 使用模板的话,你就必须把逻辑和表现分离开。
  • 模板可以预编译好然后缓存起来,这样服务器每次都只要发送数据就可以了。
  • 不在服务器端渲染而在客户端渲染,多少会影响性能。

展开
收起
go696 2016-09-01 16:31:57 5624 0
0 条回答
写回答
取消 提交回答
问答排行榜
最热
最新

相关电子书

更多
低代码开发师(初级)实战教程 立即下载
冬季实战营第三期:MySQL数据库进阶实战 立即下载
阿里巴巴DevOps 最佳实践手册 立即下载