关于JavaScript代码组织的普遍接受的最佳实践

随着像jQuery这样的JavaScript框架使客户端web应用程序更丰富,功能更强大,我开始注意到一个问题…

你到底是怎么组织起来的?

  • 把所有的处理程序放在一个地方,并为所有事件编写函数?
  • 创建函数/类来包装您的所有功能?
  • 疯狂地写作,只希望结果是最好的?
  • 放弃,找一份新工作?

我提到了jQuery,但它实际上是一般的JavaScript代码。我发现,当一行一行开始堆积时,管理脚本文件或找到您要找的内容变得越来越困难。我发现的最大问题可能是,做同一件事有太多的方法,很难知道哪一种是目前普遍接受的最佳实践。

关于如何使. js文件像应用程序的其他部分一样整洁,有什么一般性的建议吗?或者这只是IDE的问题?还有更好的选择吗?


编辑

这个问题主要是关于代码组织,而不是文件组织。有一些合并文件或拆分内容的好例子。

我的问题是:目前普遍接受的组织实际代码的最佳实践方式是什么?您的方法是什么,甚至推荐的方法是什么,以与页面元素交互并创建互不冲突的可重用代码?

有些人列出了名称空间,这是个好主意。还有什么其他方法,更具体地说,处理页面上的元素并保持代码的组织和整洁?

51468 次浏览

您可以将脚本分解到单独的文件中进行开发,然后创建一个“发布”版本,在这个版本中,您将它们全部塞在一起,并在其中运行YUI Compressor或类似的程序。

几天前,37Signals 释放一个RTE控件的人,有了一个转折。他们制作了一个库,使用一种预处理器命令捆绑javascript文件。

我一直在用它来分离我的JS文件,然后在最后合并为一个。这样我就可以分离关注点,并且最终只有一个文件通过管道(gzip的)。

在模板中,检查是否处于开发模式,并包含单独的文件,如果处于生产模式,则包括最后一个文件(您必须自己“构建”)。

我使用一个受Ben Nolan的行为启发的自定义脚本(我再也找不到这个的当前链接了,可悲的是)来存储我的大多数事件处理程序。例如,这些事件处理程序由元素className或Id触发。 例子:< / p >
Behaviour.register({
'a.delete-post': function(element) {
element.observe('click', function(event) { ... });
},


'a.anotherlink': function(element) {
element.observe('click', function(event) { ... });
}


});

我喜欢动态地包含大多数Javascript库,除了那些包含全局行为的库。我使用Zend框架的headScript()占位符助手,但你也可以用使用javascript加载其他脚本在飞行Ajile为例。

我的老板仍然在谈论他们编写模块化代码(C语言)的时代,并抱怨现在的代码是多么糟糕!据说程序员可以在任何框架中编写程序集。总有一种策略可以克服代码组织。最基本的问题是那些把java脚本当作玩具的人,从来没有尝试去学习它。

在我的例子中,我在UI主题或应用程序屏幕的基础上编写js文件,使用适当的init_screen()。使用适当的id命名约定,我确保在根元素级别上没有名称空间冲突。在不引人注目的window.load()中,我根据顶级id将内容绑定起来。

我严格使用java脚本闭包和模式来隐藏所有私有方法。这样做之后,再也没有遇到属性/函数定义/变量定义冲突的问题。然而,当与团队一起工作时,通常很难执行同样的严格性。

如果javascript内置了名称空间,那就更好了,但我发现像Dustin Diaz描述的在这里那样组织东西对我帮助很大。

var DED = (function() {


var private_var;


function private_method()
{
// do stuff here
}


return {
method_1 : function()
{
// do stuff here
},
method_2 : function()
{
// do stuff here
}
};
})();

我把不同的“名称空间”放在不同的文件中,有时把单独的类放在不同的文件中。通常我从一个文件开始,当类或名称空间变得足够大时,我将它分离到自己的文件中。使用工具将所有文件组合起来进行生产也是一个很好的主意。

我为所有我真的不需要在屏幕上实例化几次的东西创建单例,为其他所有东西创建一个类。所有这些都放在同一个文件的同一个名称空间中。所有的东西都是用UML、状态图进行注释和设计的。javascript代码是清楚的html,所以没有内联javascript,我倾向于使用jquery来最小化跨浏览器的问题。

您没有提到服务器端语言是什么。或者,更确切地说,您在服务器端使用什么框架(如果有的话)。

IME,我在服务器端组织东西,让它全部震动到网页上。该框架的任务不仅是组织每个页面必须加载的JS,还包括与生成的标记一起工作的JS片段。这样的片段通常不希望发出超过一次——这就是为什么它们被抽象到框架中,以便代码处理该问题。: -)

对于必须发出自己的JS的最终页面,我通常发现生成的标记中有一个逻辑结构。这种本地化的JS通常可以在这种结构的开始和/或结束处组装。

请注意,这些都不能免除您编写高效JavaScript的责任!: -)

受之前帖子的启发,我复制了Rakefile供应商目录,并使用< >强WysiHat < / >强 (changelog提到的RTE)分发,并进行了一些修改,包括使用< >强JSLint < / >强进行代码检查,使用< >强YUI Compressor < / >强进行缩小。

这个想法是使用< >强链轮< / >强(来自WysiHat)将多个JavaScripts合并到一个文件中,使用JSLint检查合并文件的语法,并在发布之前使用YUI Compressor缩小它。

先决条件

  • Java运行时
  • 红宝石和耙子宝石
  • 您应该知道如何将JAR放入类路径

现在做的

  1. 下载犀牛并将JAR(“js.jar”)放到类路径中
  2. 下载YUI Compressor并将JAR (build/yuicompressor-xyz.jar)放到类路径中
  3. 下载WysiHat并将“vendor”目录复制到你的JavaScript项目的根目录
  4. 下载用于Rhino的JSLint并将其放在“vendor”目录中

现在,在JavaScript项目的根目录中创建一个名为“Rakefile”的文件,并向其添加以下内容:

require 'rake'


ROOT            = File.expand_path(File.dirname(__FILE__))
OUTPUT_MERGED   = "final.js"
OUTPUT_MINIFIED = "final.min.js"


task :default => :check


desc "Merges the JavaScript sources."
task :merge do
require File.join(ROOT, "vendor", "sprockets")


environment  = Sprockets::Environment.new(".")
preprocessor = Sprockets::Preprocessor.new(environment)


%w(main.js).each do |filename|
pathname = environment.find(filename)
preprocessor.require(pathname.source_file)
end


output = preprocessor.output_file
File.open(File.join(ROOT, OUTPUT_MERGED), 'w') { |f| f.write(output) }
end


desc "Check the JavaScript source with JSLint."
task :check => [:merge] do
jslint_path = File.join(ROOT, "vendor", "jslint.js")


sh 'java', 'org.mozilla.javascript.tools.shell.Main',
jslint_path, OUTPUT_MERGED
end


desc "Minifies the JavaScript source."
task :minify => [:merge] do
sh 'java', 'com.yahoo.platform.yui.compressor.Bootstrap', '-v',
OUTPUT_MERGED, '-o', OUTPUT_MINIFIED
end

如果你正确地完成了所有操作,你应该能够在控制台中使用以下命令:

  • rake merge——将不同的JavaScript文件合并为一个
  • rake check—检查代码的语法(这是默认的任务,所以您可以简单地键入rake)
  • rake minify——准备JS代码的缩小版本

关于源合并

使用Sprockets, JavaScript预处理器,您可以包括(或require)其他JavaScript文件。使用以下语法来包含初始文件(名为“main.js”,但你可以在Rakefile中更改)中的其他脚本:

(function() {
//= require "subdir/jsfile.js"
//= require "anotherfile.js"


// some code that depends on included files
// note that all included files can be in the same private scope
})();

然后……

看看WysiHat提供的Rakefile来设置自动化单元测试。不错的东西:)

现在揭晓答案

这并没有很好地回答最初的问题。我知道,对此我很抱歉,但我把它贴在这里,因为我希望它能对其他人整理他们的混乱有所帮助。

我解决这个问题的方法是尽可能多地进行面向对象建模,并将实现分离到不同的文件中。那么处理程序应该尽可能短。List单例也是一个很好的例子。

和名称空间……它们可以被更深层次的物体结构所模仿。

if (typeof org === 'undefined') {
var org = {};
}


if (!org.hasOwnProperty('example')) {
org.example = {};
}


org.example.AnotherObject = function () {
// constructor body
};

我不太喜欢模仿,但是如果您有很多想要移出全局作用域的对象,这可能会很有帮助。

在我以前的工作中,我成功地将Javascript模块模式应用到Ext JS应用程序中。它提供了一种创建良好封装代码的简单方法。

我尽量避免在HTML中包含任何javascript。所有代码都封装在类中,每个类都在自己的文件中。对于开发,我有单独的<脚本>标签,包括每个js文件,但他们被合并到一个更大的包生产,以减少HTTP请求的开销。

通常,我会为每个应用程序创建一个“main”js文件。所以,如果我正在编写一个“调查”应用程序,我将有一个名为“survey.js”的js文件。这将包含进入jQuery代码的入口点。我在实例化期间创建jQuery引用,然后将它们作为参数传递到我的对象中。这意味着javascript类是“纯粹的”,不包含任何对CSS id或类名的引用。

// file: survey.js
$(document).ready(function() {
var jS = $('#surveycontainer');
var jB = $('#dimscreencontainer');
var d = new DimScreen({container: jB});
var s = new Survey({container: jS, DimScreen: d});
s.show();
});

我还发现命名约定对于可读性很重要。例如:我在所有jQuery实例前加'j'。

在上面的例子中,有一个类叫做DimScreen。(假设这会使屏幕变暗并弹出一个警告框。)它需要一个div元素,它可以放大以覆盖屏幕,然后添加一个警告框,因此我传入一个jQuery对象。jQuery有一个插件概念,但它似乎有局限性(例如,实例不是持久的,不能访问),没有真正的好处。因此,DimScreen类将是一个标准的javascript类,只是恰好使用jQuery。

// file: dimscreen.js
function DimScreen(opts) {
this.jB = opts.container;
// ...
}; // need the semi-colon for minimizing!




DimScreen.prototype.draw = function(msg) {
var me = this;
me.jB.addClass('fullscreen').append('<div>'+msg+'</div>');
//...
};

我已经使用这种方法构建了一些相当复杂的应用程序。

遵循好的OO设计原则和设计模式对使你的代码易于维护和理解大有帮助。 但我最近发现的最好的东西之一是信号和槽,也就是发布/订阅。 看看http://markdotmeyer.blogspot.com/2008/09/jquery-publish-subscribe.html

这个想法在其他语言的GUI开发中也得到了很好的应用。当代码中某个重要的事件发生时,你会发布一个全局合成事件,其他对象中的其他方法可能会订阅该事件。 这提供了很好的对象分离。< / p >

我认为Dojo(和Prototype?)有这种技术的内置版本。

参见什么是信号和槽?

Dojo从一开始就使用模块系统。事实上,它被认为是Dojo的基石,是将所有Dojo结合在一起的粘合剂:

  • # EYZ0。
  • # EYZ0。
  • # EYZ0。

使用模块Dojo可以实现以下目标:

  • Dojo代码和自定义代码的名称空间(dojo.declare()) —不污染全局空间,与其他库以及用户的非dojo感知代码共存。
  • 按名称(dojo.require())同步或异步加载模块。
  • 通过分析模块依赖关系来创建单个文件或一组相互依赖的文件(所谓的层),以只包括您的web应用程序所需的内容。自定义构建也可以包括Dojo模块和客户提供的模块。
  • 对Dojo和用户代码的基于cdn的透明访问。AOL和谷歌都以这种方式提供Dojo,但一些客户也为他们的自定义web应用程序这样做。

按需加载所需的代码。谷歌用google.loader做了类似的事情

在我的上一个项目- viajeros.com -我使用了几种技术的组合。我不知道如何组织一个网页应用程序——Viajeros是一个面向旅行者的社交网站,它有明确定义的部分,所以很容易将每个部分的代码分开。

我根据站点部分使用名称空间模拟和延迟加载模块。在每个页面加载我声明一个“vjr”对象,并总是加载一组常用函数到它(vjr.base.js)。然后每个HTML页面用简单的语句决定哪些模块需要:

vjr.Required = ["vjr.gallery", "vjr.comments", "vjr.favorites"];

Vjr.base.js从服务器获取并执行每一个gzip文件。

vjr.include(vjr.Required);
vjr.include = function(moduleList) {
if (!moduleList) return false;
for (var i = 0; i < moduleList.length; i++) {
if (moduleList[i]) {
$.ajax({
type: "GET", url: vjr.module2fileName(moduleList[i]), dataType: "script"
});
}
}
};

每个“模块”都有这样的结构:

vjr.comments = {}


vjr.comments.submitComment = function() { // do stuff }
vjr.comments.validateComment = function() { // do stuff }


// Handlers
vjr.comments.setUpUI = function() {
// Assign handlers to screen elements
}


vjr.comments.init = function () {
// initialize stuff
vjr.comments.setUpUI();
}


$(document).ready(vjr.comments.init);

鉴于我有限的Javascript知识,我知道一定有更好的方法来管理这个,但到目前为止,它为我们工作得很好。

“疯狂地写作,只是希望它能得到最好的结果?”,我曾经见过一个这样的项目,它是由两个开发人员开发和维护的,一个包含大量javascript代码的巨大应用程序。最重要的是,每个可能的jquery函数都有不同的快捷方式。我建议他们组织代码作为插件,因为这是jquery的类,模块,命名空间…还有整个宇宙。但事情变得更糟了,现在他们开始编写插件来取代项目中使用的每一个3行代码的组合。 我个人认为jQuery是魔鬼,它不应该用在有大量javascript的项目中,因为它会让你变得懒惰,不考虑以任何方式组织代码。我宁愿读100行javascript,也不愿读一行包含40个链式jQuery函数的代码(我不是在开玩笑)。 与流行的观点相反,用等价的名称空间和类组织javascript代码非常容易。这就是YUI和Dojo所做的。如果你喜欢,你可以很容易地自己卷。我发现YUI的方法更好更有效。但是如果你想写一些有用的东西,你通常需要一个支持代码片段的优秀编辑器来弥补YUI命名约定

我认为这可能与DDD(领域驱动设计)有关。我正在开发的应用程序,虽然缺乏正式的API,但确实通过服务器端代码(类/文件名等)给出了提示。有了这些,我创建了一个顶级对象作为整个问题域的容器;然后,我在需要的地方添加名称空间:

var App;
(function()
{
App = new Domain( 'test' );


function Domain( id )
{
this.id = id;
this.echo = function echo( s )
{
alert( s );
}
return this;
}
})();


// separate file
(function(Domain)
{
Domain.Console = new Console();


function Console()
{
this.Log = function Log( s )
{
console.log( s );
}
return this;
}
})(App);


// implementation
App.Console.Log('foo');

以Jquery为中心的命名空间方式组织您的代码可能看起来如下…并且不会与其他Javascript API(如Prototype, Ext)冲突。

<script src="jquery/1.3.2/jquery.js" type="text/javascript"></script>
<script type="text/javascript">


var AcmeJQ = jQuery.noConflict(true);
var Acme = {fn: function(){}};


(function($){


Acme.sayHi = function()
{
console.log('Hello');
};


Acme.sayBye = function()
{
console.log('Good Bye');
};
})(AcmeJQ);


// Usage
//          Acme.sayHi();
// or
// <a href="#" onclick="Acme.sayHi();">Say Hello</a>




</script>

希望这能有所帮助。

创建假类,并确保任何可以被扔进单独函数的有意义的东西都被这样做了。还要确保大量注释,而不是编写面条式的代码,而是将代码分块编写。例如,一些无意义的代码描述了我的理想。显然,在现实生活中,我也编写了许多包含它们功能的库。

$(function(){
//Preload header images
$('a.rollover').preload();


//Create new datagrid
var dGrid = datagrid.init({width: 5, url: 'datalist.txt', style: 'aero'});
});


var datagrid = {
init: function(w, url, style){
//Rendering code goes here for style / width
//code etc


//Fetch data in
$.get(url, {}, function(data){
data = data.split('\n');
for(var i=0; i < data.length; i++){
//fetching data
}
})
},
refresh: function(deep){
//more functions etc.
}
};

我使用Dojo的包管理 (dojo.requiredojo.provide)和类系统(dojo.declare也允许简单的多重继承)将我所有的类/小部件模块化到单独的文件中。这不仅能让你的代码保持有序,而且还能让你对类/小部件进行惰性/及时加载。

看看JavasciptMVC

你可以:

  • 将代码分成模型层、视图层和控制器层。

  • 将所有代码压缩到一个生产文件中

  • 自动生成代码

  • 创建并运行单元测试

  • 还有更多……

最重要的是,它使用jQuery,所以你也可以利用其他jQuery插件。

对于JavaScript组织一直使用以下

  1. 所有javascript的文件夹
  2. 页面级javascript获得与页面同名的自己的文件。ProductDetail。aspx应该是ProductDetail.js
  3. 在库文件的javascript文件夹中,我有一个lib文件夹
  4. 将相关的库函数放在希望在整个应用程序中使用的lib文件夹中。
  5. Ajax是唯一的javascript,我移动到javascript文件夹之外,并获得它自己的文件夹。然后我添加两个子文件夹客户端和服务器
  6. Client文件夹获取所有的.js文件,而server文件夹获取所有的服务器端文件。

我用这个小东西。它为JS和HTML模板提供了“include”指令。它完全消除了混乱。

https://github.com/gaperton/include.js/

$.include({
html: "my_template.html" // include template from file...
})
.define( function( _ ){ // define module...
_.exports = function widget( $this, a_data, a_events ){ // exporting function...
_.html.renderTo( $this, a_data ); // which expands template inside of $this.


$this.find( "#ok").click( a_events.on_click ); // throw event up to the caller...
$this.find( "#refresh").click( function(){
widget( $this, a_data, a_events ); // ...and update ourself. Yep, in that easy way.
});
}
});

你的问题是去年年底困扰我的一个问题。区别在于——将代码交给从未听说过私有方法和公共方法的新开发人员。我必须建造一些简单的东西。

最终的结果是一个小框架(大约1KB),它将对象文字转换为jQuery。语法在视觉上更容易扫描,如果你的js变得非常大,你可以编写可重用的查询来查找使用的选择器、加载的文件、依赖的函数等。

在这里发布一个小框架是不切实际的,所以我写了一个带有示例的博客文章(我的第一个。那真是一次冒险!)欢迎您来看看。

对于这里有几分钟时间来查看的其他人,我非常感谢反馈!

推荐使用FireFox,因为它支持对象查询示例的toSource()。

干杯!

亚当

我很惊讶没有人提到MVC框架。我一直在使用Backbone.js来模块化和解耦我的代码,这是非常宝贵的。

有相当多这样的框架,其中大多数也非常小。我个人的观点是,如果您想为华丽的UI编写几行jQuery以外的东西,或者想要一个丰富的Ajax应用程序,那么MVC框架将使您的工作更加轻松。

您可以使用jquery mx(在javascriptMVC中使用),这是一组允许您使用模型、视图和控制器的脚本。我曾在一个项目中使用它,并帮助我创建结构化javascript,由于压缩,脚本大小最小。以控制器为例:

$.Controller.extend('Todos',{
".todo mouseover" : function( el, ev ) {
el.css("backgroundColor","red")
},
".todo mouseout" : function( el, ev ) {
el.css("backgroundColor","")
},
".create click" : function() {
this.find("ol").append("<li class='todo'>New Todo</li>");
}
})


new Todos($('#todos'));

如果您对视图和模型部分不感兴趣,也可以使用jquery ymx的仅控制器侧。

好的OO + MVC原则对于管理一个复杂的javascript应用程序肯定大有帮助。

基本上,我将我的应用程序和javascript组织成以下熟悉的设计(从我的桌面编程到Web 2.0一直存在)

JS OO和MVC

图像上数值的说明:

  1. 表示应用程序视图的小部件。这应该是可扩展的,并整齐地分离出来,从而实现MVC试图实现的良好分离,而不是将我的小部件变成意大利面条代码(相当于在web应用程序中直接将大块Javascript放入HTML中)。每个小部件通过侦听其他小部件生成的事件来与其他小部件通信,从而减少小部件之间的强耦合,从而导致难以管理的代码(还记得在脚本标记中添加指向全局函数的onclick的日子吗?呃!…)
  2. 对象模型表示我希望在小部件中填充并来回传递给服务器的数据。通过将数据封装到其模型中,应用程序将成为数据格式不可知者。例如:虽然在Javascript中,这些对象模型主要是序列化和反序列化为JSON,但如果服务器以某种方式使用XML进行通信,我只需要更改序列化/反序列化层,而不一定需要更改所有小部件类。
  3. 管理业务逻辑和与服务器通信的控制器类+偶尔的缓存层。该层控制与服务器的通信协议,并将必要的数据放入对象模型中
  4. 类被整齐地包装在相应的名称空间中。我相信我们都知道在Javascript中全局命名空间有多糟糕。

在过去,我会将文件分离到自己的js中,并使用常见的实践在Javascript中创建OO原则。问题是,我很快发现有多种方法来编写JS面向对象,并不是所有的团队成员都有相同的方法。随着团队规模的扩大(在我的例子中超过15人),这就变得复杂起来,因为面向对象Javascript没有标准的方法。与此同时,我不想编写自己的框架并重复一些我确信比我更聪明的人已经解决过的工作。

jQuery是非常好的Javascript框架,我喜欢它,但随着项目越来越大,我显然需要额外的结构为我的web应用程序,特别是促进标准化OO实践。对我自己来说,经过几次实验,我发现YUI3 Base和Widget (http://yuilibrary.com/yui/docs/widget/http://yuilibrary.com/yui/docs/base/index.html)基础设施完全满足了我的需要。我使用它们的几个原因。

  1. 它提供了Namespace支持。真正需要面向对象和整洁的代码组织
  2. 它支持类和对象的概念
  3. 它提供了一种向类中添加实例变量的标准化方法
  4. 它巧妙地支持类扩展
  5. 它提供了构造函数和析构函数
  6. 它提供了呈现和事件绑定
  7. 它有基本的小部件框架
  8. 现在每个小部件都可以使用标准的基于事件的模型相互通信
  9. 最重要的是,它为Javascript开发提供了面向对象的标准

与许多观点相反,我不一定要在jQuery和YUI3之间做出选择。这两者可以和平共处。虽然YUI3为我复杂的web应用程序提供了必要的OO模板,但jQuery仍然为我的团队提供了我们都喜欢和熟悉的易于使用的JS抽象。

使用YUI3,我成功地创建了MVC模式,通过分离将Base扩展为模型的类,将Widget扩展为视图的类,当然还有进行必要逻辑和服务器端调用的控制器类。

Widget可以使用基于事件的模型相互通信,并根据预定义的接口侦听事件并执行必要的任务。简单地说,把OO + MVC结构的JS是一个乐趣对我来说。

声明一下,我不是为雅虎工作的!仅仅是一个建筑师在试图解决最初的问题所带来的问题。我认为如果有人找到了等效的OO框架,这也可以。基本上,这个问题也适用于其他技术。感谢所有提出OO原则+ MVC的人,使我们的编程更易于管理。

代码组织要求采用约定和文档标准 1. 物理文件的命名空间代码;

. xml
Exc = {};
< p >
2。
. javascript 3.

Exc = {};
Exc.ui = {};
Exc.ui.maskedInput = function (mask) {
this.mask = mask;
...
};
Exc.ui.domTips = function (dom, tips) {
this.dom = gift;
this.tips = tips;
...
};
< p >
4. 设置约定以改进代码。例如,将其所有内部函数或方法分组在对象类型的class属性中
Exc.ui.domTips = function (dom, tips) {
this.dom = gift;
this.tips = tips;
this.internal = {
widthEstimates: function (tips) {
...
}
formatTips: function () {
...
}
};
...
};


5。制作名称空间、类、方法和变量的文档。必要时也讨论一些代码(一些fi和for,它们通常实现代码的重要逻辑)。

/**
* Namespace <i> Example </i> created to group other namespaces of the "Example".
*/
Exc = {};
/**
* Namespace <i> ui </i> created with the aim of grouping namespaces user interface.
*/
Exc.ui = {};


/**
* Class <i> maskdInput </i> used to add an input HTML formatting capabilities and validation of data and information.
* @ Param {String} mask - mask validation of input data.
*/
Exc.ui.maskedInput = function (mask) {
this.mask = mask;
...
};


/**
* Class <i> domTips </i> used to add an HTML element the ability to present tips and information about its function or rule input etc..
* @ Param {String} id - id of the HTML element.
* @ Param {String} tips - tips on the element that will appear when the mouse is over the element whose identifier is id <i> </i>.
*/
Exc.ui.domTips = function (id, tips) {
this.domID = id;
this.tips = tips;
...
};
< p >
这些只是一些技巧,但这对组织代码有很大帮助。记住,你必须自律才能成功!< / p >