在JavaScript代码组织中常用的最佳实践
随着jQuery等JavaScript框架使客户端Web应用程序更丰富,更实用,我开始注意到一个问题…
你如何保持这种有组织的?
- 把所有的处理程序放在一个地方,并为所有的事件写function?
- 创build函数/类来包装所有的function?
- 写得像疯了一样,只是希望它最好的工作?
- 放弃并获得新的职业?
我提到了jQuery,但它通常是任何JavaScript代码。 我发现随着线条的堆积,你将很难pipe理脚本文件或find你要找的东西。 我发现的最大的问题是有很多方法可以做同样的事情,很难知道哪一个是目前普遍接受的最佳实践。
是否有任何一般的build议,以保持您的.js文件作为您的应用程序的其余部分一样漂亮和整洁的最佳途径? 或者这只是IDE的问题? 那里有更好的select吗?
编辑
这个问题的目的是更多地关于代码组织,而不是文件的组织。 有一些非常好的合并文件或分割内容的例子。
我的问题是:什么是目前普遍接受的最佳实践方式来组织你的实际代码? 什么是你的方式,甚至推荐的方式来与页面元素进行交互,并创build不会相互冲突的可重用的代码?
有些人列出了名字空间 ,这是一个好主意。 什么是其他方式,更具体地处理页面上的元素,并保持代码组织和整齐?
如果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 } }; })();
我把不同的“命名空间”,有时单独的文件中的单独的类。 通常,我从一个文件开始,当一个类或命名空间变得足够大,以保证它,我分开它自己的文件。 使用一个工具来组合所有的文件进行生产也是一个好主意。
我尽量避免包含HTML的任何JavaScript。 所有的代码被封装到类中,每个类都在它自己的文件中。 对于开发,我有单独的<script>标签来包含每个js文件,但是它们被合并成一个更大的包来生产,以减lessHTTP请求的开销。
通常,我将为每个应用程序提供一个“主”js文件。 所以,如果我正在写一个“调查”应用程序,我会有一个名为“survey.js”的js文件。 这将包含进入jQuery代码的入口点。 我在实例化过程中创buildjQuery引用,然后将它们作为parameter passing给我的对象。 这意味着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的类。 (假设这会使屏幕变暗,并popup一个警告框。)它需要一个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>'); //... };
我已经使用这种方法构build了一些相当复杂的应用程序。
你可以把你的脚本分解成单独的文件进行开发,然后创build一个“发布”版本,把所有的脚本塞到一起,运行YUI Compressor或类似的东西。
在之前的文章的鼓励下 ,我制作了一个使用WysiHat (由changelog提到的RTE)发布的Rakefile和vendor目录的副本,并做了一些修改,包括使用JSLint进行代码检查和使用YUI Compressor进行缩小 。
这个想法是使用Sprockets (来自WysiHat)将多个JavaScript合并成一个文件,使用JSLint检查合并文件的语法,并在分发之前用YUI Compressor缩小它。
先决条件
- Java运行时
- ruby和耙gem
- 您应该知道如何将JAR放入Classpath
现在呢
- 下载Rhino并将JAR(“js.jar”)放到你的类path中
- 下载YUI压缩器并将JAR(build / yuicompressor-xyz.jar)添加到你的classpath
- 下载WysiHat并将“vendor”目录复制到JavaScript项目的根目录下
- 下载JSLint for Rhino并放入“vendor”目录
现在在JavaScript项目的根目录下创build一个名为“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
– 检查你的代码的语法(这是默认的任务,所以你可以简单地inputrake
) -
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来设置自动化unit testing。 好东西 :)
现在回答
这并不能很好地回答原来的问题。 我知道,我对此感到抱歉,但是我已经把它发布在这里,因为我希望别人来组织他们的混乱可能是有用的。
我对这个问题的解决方法是尽可能多地进行面向对象的build模,并将实现分离到不同的文件中。 那么处理程序应该尽可能短。 List
单例的例子也不错。
而命名空间……以及它们可以被更深的对象结构模仿。
if (typeof org === 'undefined') { var org = {}; } if (!org.hasOwnProperty('example')) { org.example = {}; } org.example.AnotherObject = function () { // constructor body };
我不是模仿的忠实粉丝,但是如果你有很多想要移出全球范围的对象,这可能会有所帮助。
代码组织需要采用惯例和文档标准:
1.物理文件的命名空间代码;
Exc = {};
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; ... };
4.设置约定来改善代码。 例如,将其所有内部函数或方法分组在其对象types的类属性中。
Exc.ui.domTips = function (dom, tips) { this.dom = gift; this.tips = tips; this.internal = { widthEstimates: function (tips) { ... } formatTips: function () { ... } }; ... };
5.制作名称空间,类,方法和variables的文档。 在必要的地方也讨论一些代码(一些FI和Fors,他们通常实现代码的重要逻辑)。
/** * 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; ... };
这些只是一些提示,但是对组织代码有很大的帮助。 记住你必须有纪律才能成功!
遵循良好的面向对象devise原则和devise模式,可以使您的代码易于维护和理解。 但我最近发现的最好的事情之一是信号和插槽aka发布/订阅。 查看http://markdotmeyer.blogspot.com/2008/09/jquery-publish-subscribe.html以获取简单的jQuery实现。;
这个想法很好的用于其他语言的GUI开发。 当代码中的某处出现重大事件时,您将发布全局综合事件,其他对象中的其他方法可能会订阅该事件。 这给了物体的极好的分离。
我认为Dojo(和Prototype?)有这种技术的内置版本。
另请参阅什么是信号和插槽?
在我以前的工作中,我能够成功地将Javascript模块模式应用到Ext JS应用程序。 它提供了一个简单的方法来创build很好的封装代码。
Dojo从第一天就有了模块系统。 事实上,它被认为是Dojo的基石,它把所有的东西都保存在一起:
- dojo.require – 官方文档 。
- 了解dojo.declare,dojo.require和dojo.provide 。
- 介绍Dojo 。
使用模块Dojo实现以下目标:
- Dojo代码和自定义代码的命名空间(
dojo.declare()
) – 不要污染全局空间,与其他库共存,以及用户的非Dojo感知代码。 - 按名称(
dojo.require()
)同步或asynchronous加载模块。 - 通过分析模块依赖性来自定义构build,以创build单个文件或一组相互依赖的文件(所谓的图层),以仅包含Web应用程序所需的内容。 定制版本可以包含Dojo模块和客户提供的模块。
- 透明的基于CDN的访问Dojo和用户代码。 美国在线和谷歌都以这种方式运营Dojo,但也有一些客户为他们的自定义networking应用程序。
查看JavasciptMVC 。
您可以 :
-
把你的代码分解成模型,视图和控制器层。
-
将所有代码压缩到单个生产文件中
-
自动生成代码
-
创build和运行unit testing
-
还有更多…
最重要的是,它使用jQuery,所以你也可以利用其他的jQuery插件。
我的老板还谈到了他们编写模块化代码(C语言)的时代,并抱怨现在的代码有多糟糕! 据说程序员可以在任何框架下编写程序集。 总是有一个战略来克服代码组织。 基本的问题是,谁把java脚本作为玩具,从来没有试图学习它的家伙。
在我的情况下,我写一个UI主题或应用程序屏幕的基础上的js文件,具有适当的init_screen()。 使用适当的id命名约定,我确保在根元素级别没有名称空间冲突。 在unobstrusive window.load()中,我根据最高级别的id绑定这些东西。
我严格使用java脚本closures和模式来隐藏所有的私有方法。 做完这些之后,从来没有遇到过性能/函数定义/variables定义冲突的问题。 但是,与团队合作时,执行同样的严格程序往往是困难的。
我很惊讶没有人提到MVC框架。 我一直在使用Backbone.js模块化和解耦我的代码,这是非常宝贵的。
这里有很多这样的框架,其中大部分框架也很小。 我个人的观点是,如果你要写的不仅仅是几行jQuery的华丽的UI的东西,或者想要一个丰富的Ajax应用程序,一个MVC框架将使您的生活更容易。
“写得像疯了一样,只是希望它能够发挥出最好的效果?”,我已经看到了一个这样的项目,这个项目是由2个开发人员开发和维护的,一个包含大量javascript代码的庞大应用程序。 最重要的是,你可以想到的每个可能的jquery函数都有不同的快捷键。 我build议他们把代码组织成插件,因为这就是类,模块,命名空间和整个宇宙的jquery。 但事情变得更糟,现在他们开始编写插件,replace项目中使用的3行代码的每个组合。 Personaly我觉得jQuery是魔鬼,它不应该用于大量的JavaScript项目,因为它鼓励你懒惰,而不是想办法以任何方式组织代码。 我宁愿阅读100行的JavaScript比40行链接的jQuery函数(我不是在开玩笑)的一行。 与stream行的看法相反,很容易将JavaScript代码组织到命名空间和类中。 YUI和Dojo就是这样做的。 如果你喜欢,你可以轻松地推出自己的。 我发现YUI的方法更好更高效。 但是,如果你想写任何有用的东西,你通常需要一个支持片断的好编辑器来弥补YUI的命名约定。
我为每件事情创build了单例,我不需要在屏幕上多次实例化,而是为其他所有types实例化。 它们都放在同一个文件中的同一个命名空间中。 所有的东西都被评论,并用UML,状态图来devise。 JavaScript代码是清除HTML,所以没有内联的JavaScript,我倾向于使用jQuery来最小化跨浏览器问题。
以Jquery为中心的NameSpace方式组织代码可能如下所示…并且不会与其他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>
希望这可以帮助。
OO + MVC的好主意肯定会对pipe理复杂的JavaScript应用程序有很大的帮助。
基本上,我正在组织我的应用程序和JavaScript到以下熟悉的devise(从我的桌面编程时代一直存在到Web 2.0)
图像上数字值的说明:
- 小部件代表我的应用程序的意见。 这应该是可扩展的,整齐地分离出来,MVC尝试实现而不是将我的小部件转换成意大利面代码(相当于将大块Javascript直接放在HTML中的Web应用程序中)。 每个小部件通过监听其他小部件生成的事件来通过其他小部件进行通信,从而减less小部件之间的强耦合,从而可能导致无法pipe理的代码(记住添加onclick指向脚本标记中的全局函数的那一天?
- 表示我想要在小部件中填充的数据并传递给服务器的数据的对象模型。 通过将数据封装到模型中,应用程序成为数据格式不可知论者。 例如:虽然自然在JavaScript中,这些对象模型大部分是序列化和反序列化成JSON,如果服务器使用XML进行通信,我只需要改变序列化/反序列化层,而不一定需要更改所有的部件类。
- 控制器类,pipe理业务逻辑和与服务器的通信+偶尔caching层。 该层控制与服务器的通信协议,并将必要的数据放入对象模型中
- 类被整齐地包装在相应的命名空间中。 我相信我们都知道在Javascript中全局命名空间是多么的可怕。
在过去,我会把这些文件分成自己的js,并使用惯例来在Javascript中创buildOO原则。 我很快就发现,有很多方法来编写JS OO,并不一定所有的团队成员都有相同的方法。 随着团队规模的扩大(在我的情况下超过15人),由于没有针对面向对象的Javascript的标准方法,这变得复杂了。 同时,我不想写自己的框架,重复一些我相信聪明的人的工作,而不是我已经解决的。
jQuery作为Javascript框架非常好,我喜欢它,但是随着项目变得越来越大,我显然需要为我的web应用程序增加额外的结构,特别是为了促进OO实践的标准化。 对于我自己来说,经过几次实验,我发现YUI3 Base和Widget( http://yuilibrary.com/yui/docs/widget/和http://yuilibrary.com/yui/docs/base/index.html )基础设施提供正是我所需要的。 几个原因,为什么我使用它们。
- 它提供名称空间支持。 真正的OO需求和整洁的代码组织
- 它支持类和对象的概念
- 它提供了一个标准化的手段来添加实例variables到你的类
- 它整齐地支持类扩展
- 它提供了构造函数和析构函数
- 它提供了渲染和事件绑定
- 它有基本的小部件框架
- 每个小部件现在都可以使用标准的基于事件的模型相互沟通
- 最重要的是,它为所有工程师提供了一个用于Javascript开发的OO标准
与许多观点相反,我不一定要在jQuery和YUI3之间进行select。 这两个可以和平共处。 虽然YUI3为我的复杂networking应用程序提供了必要的面向对象的模板,但jQuery仍然为我们的团队提供了易于使用的JS抽象,我们都喜欢和熟悉它。
使用YUI3,我已经成功地创build了MVC模式,将基类扩展为模型,扩展Widget作为视图的类,当然你也有需要进行必要的逻辑和服务器端调用的Controller类。
Widget可以使用基于事件的模型相互通信,并且监听事件并根据预定义的界面执行必要的任务。 简单地说,把OO + MVC结构放到JS对我来说是一个喜悦。
只是一个免责声明,我不为雅虎工作。 而只是一名devise师正试图解决原来问题所提出的同样问题。 我想如果有人find相同的面向对象框架,这也可以。 原则上,这个问题也适用于其他技术。 感谢所有提出面向对象原则+ MVC的人们,让我们的编程日子变得更易于pipe理。
在我上一个项目-Viajeros.com中,我使用了几种技术的组合。 我不知道如何组织一个networking应用程序 – 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知识,我知道必须有更好的方法来pipe理这个,但直到现在它为我们工作的伟大。
我使用Dojo的包pipe理 ( dojo.require
和dojo.provide
)和类系统( dojo.declare
,它也允许简单的多重inheritance)将我所有的类/小部件模块化为单独的文件。 不仅要保证你的代码组织起来,而且还要让你做懒/只是及时加载类/小部件。
几天前,37Signals的伙计们发布了一个RTE控制 。 他们用一种预处理器命令制作了一个捆绑javascript文件的库。
我一直在使用它来分离我的JS文件,然后最终将它们合并为一个。 这样我可以分开关注,最后,只有一个文件通过pipe道(gzipped,不会less)。
在你的模板中,检查你是否处于开发模式,并包含单独的文件,如果在生产中,包括最后一个(你必须自己“build立”)。
创build假的类,并确保任何可以扔进一个单独的函数的东西是有道理的。 还要确保评论很多,而不是写sparsehetti代码,而不是把所有的部分。 例如,一些无意的代码描绘了我的理想。 显然在现实生活中,我也写了许多基本上包含了它们function的库。
$(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. } };
使用inheritance模式来组织大型jQuery应用程序。
我认为这也许DDD(域驱动devise)。 我正在使用的应用程序虽然缺less正式的API,但通过服务器端代码(类/文件名等)的方式提供了这样的提示。 有了这个,我创build了一个顶级对象作为整个问题域的容器; 然后,我在需要的地方添加了命名空间:
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');
对于JavaScript组织一直在使用以下内容
- 所有的JavaScript文件夹
- 页面级别的JavaScript获取自己的文件与页面的同名。 ProductDetail.aspx将是ProductDetail.js
- 在库文件的JavaScript文件夹里面我有一个lib文件夹
- 把相关的库函数放在一个你想在整个应用程序中使用的lib文件夹中。
- Ajax是我在javascript文件夹之外移动的唯一的javascript,并获取它自己的文件夹。 然后我添加两个子文件夹客户端和服务器
- 客户端文件夹获取所有.js文件,而服务器文件夹获取所有服务器端文件。
I'm using this little thing. It gives you 'include' directive for both JS and HTML templates. It eleminates the mess completely.
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. }); } });
You can use jquery mx (used in javascriptMVC) which is a set of scripts that allows you to use models, views, and controllers. I've used it in a project and helped me create structured javascript, with minimal script sizes because of compression. This is a controller example:
$.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'));
You can also use only the controller side of jquerymx if you aren't interested in the view and model parts.
Your question is one that plagued me late last year. The difference – handing the code off to new developers who had never heard of private and public methods. I had to build something simple.
The end result was a small (around 1KB) framework that translates object literals into jQuery. The syntax is visually easier to scan, and if your js grows really large you can write reusable queries to find things like selectors used, loaded files, dependent functions, etc.
Posting a small framework here is impractical, so I wrote a blog post with examples (My first. That was an adventure!). You're welcome to take a look.
For any others here with a few minutes to check it out, I'd greatly appreciate feedback!
FireFox recommended since it supports toSource() for the object query example.
干杯!
亚当
I use a custom script inspired by Ben Nolan's behaviour (I can't find a current link to this anymore, sadly) to store most of my event handlers. These event handlers are triggered by the elements className or Id, for example. 例:
Behaviour.register({ 'a.delete-post': function(element) { element.observe('click', function(event) { ... }); }, 'a.anotherlink': function(element) { element.observe('click', function(event) { ... }); } });
I like to include most of my Javascript libraries on the fly, except the ones that contain global behaviour. I use Zend Framework's headScript() placeholder helper for this, but you can also use javascript to load other scripts on the fly with Ajile for example.
You don't mention what your server-side language is. Or, more pertinently, what framework you are using — if any — on the server-side.
IME, I organise things on the server-side and let it all shake out onto the web page. The framework is given the task of organising not only JS that every page has to load, but also JS fragments that work with generated markup. Such fragments you don't usually want emitted more than once – which is why they are abstracted into the framework for that code to look after that problem. 🙂
For end-pages that have to emit their own JS, I usually find that there is a logical structure in the generated markup. Such localised JS can often be assembled at the start and/or end of such a structure.
Note that none of this absolves you from writing efficient JavaScript! 🙂
Lazy Load the code you need on demand. Google does something like this with their google.loader