最佳的组织 jQuery/JavaScript 代码的方式(2013年)

107

问题

这个问题之前已经有答案了,但是它们都比较老旧,不是最新的。我的单个文件里有超过2000行代码,我们都知道这是一个不好的做法,特别是当我在查找代码或添加新功能时。我希望更好地组织我的代码,为现在和将来做好准备。

需要提到的是,我正在构建一个工具(不是简单的网站),其中有很多按钮、UI元素、拖放、操作监听器/处理程序以及在全局作用域中的函数,多个监听器可能使用同一个函数。

示例代码

$('#button1').on('click', function(e){
    // Determined action.
    update_html();
});

... // Around 75 more of this

function update_html(){ .... }

...

更多示例代码

结论

我真的需要组织这些代码,以便最好地使用它们,不重复自己并能够添加新功能和更新旧功能。我将自己独立完成这项工作。有些选择器可能会有100行代码,而其他选择器只有1行代码。我稍微看了一下require.js,发现它有点重复,并且实际上写了比需要的更多的代码。我对符合此标准的任何可能的解决方案都持开放态度,资源/示例的链接始终是一个加分项。

谢谢。


1
在编写时,你发现自己一遍又一遍地做哪些任务? - Mike Samuel
4
你访问过 http://codereview.stackexchange.com 吗? - Antony
也许可以像在jQuery中创建全局对象一样,使用命名空间来组织代码,其中全局对象是jQuery,例如你会发现jQuery.events,它将每个事件相关的方法和对象等进行了分组... - TecHunter
4
学习Angular吧!它是未来的趋势。 - Onur Yıldırım
2
你的代码不应该放在外部链接,而应该放在这里。此外,@codereview 是这些类型问题的更好场所。 - George Stocker
显示剩余5条评论
8个回答

103

我将介绍一些简单的事情,可能有用,也可能没有用。有些可能很明显,有些可能非常深奥。

步骤1:将代码分隔成模块

将代码分隔成多个模块化单元是非常好的第一步。将能够“一起”工作的内容放在它们自己的封装单元中。现在不用担心格式,保持内联即可。结构是稍后的重点。

所以,假设你有一个页面像这样:

enter image description here

将所有与页眉相关的事件处理程序/绑定器放在其中是有意义的,以便于维护(而不必浏览1000行)。

然后您可以使用诸如Grunt之类的工具将JS重新构建为单个单元。

步骤1a:依赖管理

使用RequireJS或CommonJS等库来实现称为 AMD 的东西。异步模块加载允许您明确说明您的代码依赖于什么,然后允许您将库调用卸载到代码中。您只需简单地说“这需要jQuery”,AMD将加载它,并在jQuery可用时执行您的代码

这还有一个隐藏的宝石:库加载将在DOM准备好后立即完成,而不是之前。这不再停止页面的加载!

步骤2:模块化

看看线框图?我有两个广告单元。它们很可能具有共享的事件侦听器。

您在此步骤中的任务是识别代码中重复的点,并尝试将所有这些合成为模块。目前,模块将包含所有内容。我们将在进行操作时分割内容。

这一步的整个想法是从步骤1开始删除所有复制粘贴的内容,并用松散耦合的单元替换它们。所以,不要像这样:

ad_unit1.js

 $("#au1").click(function() { ... });

ad_unit2.js

 $("#au2").click(function() { ... });

我将会有: < p > ad_unit.js

 var AdUnit = function(elem) {
     this.element = elem || new jQuery();
 }
 AdUnit.prototype.bindEvents = function() {
     ... Events go here
 }

page.js:

 var AUs = new AdUnit($("#au1,#au2"));
 AUs.bindEvents();

这让你可以将事件标记分开,避免重复。这是一个相当不错的步骤,我们稍后还会进一步扩展。
第三步:选择一个框架!
如果你想更进一步地模块化和减少重复,有很多实现MVC(Model-View-Controller)方法的框架可供选择。我最喜欢的是Backbone/Spine,但也有Angular、Yii等。列表还在继续。 模型代表你的数据。 视图代表你的标记和所有相关事件。 控制器代表你的业务逻辑,换句话说,控制器告诉页面加载哪些视图和使用哪些模型。
这将是一个重要的学习步骤,但奖励是值得的:它支持干净、模块化的代码而不是混乱的代码。
还有很多其他事情可以做,这些只是指导和想法。
代码特定的更改
以下是您代码的一些具体改进:
 $('.new_layer').click(function(){

    dialog("Create new layer","Enter your layer name","_input", {

            'OK' : function(){

                    var reply = $('.dialog_input').val();

                    if( reply != null && reply != "" ){

                            var name = "ln_"+reply.split(' ').join('_');
                            var parent = "";

                            if(selected_folder != "" ){
                            parent = selected_folder+" .content";
                            }

                            $R.find(".layer").clone()
                            .addClass(name).html(reply)
                            .appendTo("#layer_groups "+parent);

                            $R.find(".layers_group").clone()
                            .addClass(name).appendTo('#canvas '+selected_folder);

            }

        }

    });
 });

这段文字可以更好地表达为:

如下所示:

$("body").on("click",".new_layer", function() {
    dialog("Create new layer", "Enter your layer name", "_input", {
         OK: function() {
             // There must be a way to get the input from here using this, if it is a standard library. If you wrote your own, make the value retrievable using something other than a class selector (horrible performance + scoping +multiple instance issues)

             // This is where the view comes into play. Instead of cloning, bind the rendering into a JS prototype, and instantiate it. It means that you only have to modify stuff in one place, you don't risk cloning events with it, and you can test your Layer stand-alone
             var newLayer = new Layer();
             newLayer
               .setName(name)
               .bindToGroup(parent);
          }
     });
});

在你的代码中早些时候:

window.Layer = function() {
    this.instance = $("<div>");
    // Markup generated here
};
window.Layer.prototype = {
   setName: function(newName) {
   },
   bindToGroup: function(parentNode) {
   }
}

突然间,您有了一种在代码中任何地方创建标准层的方法,而不需要复制粘贴。您正在五个不同的位置执行此操作。我刚刚为您节省了五次复制粘贴。

还有一个:

// 动作的规则集包装器

var PageElements = function(ruleSet) {
ruleSet = ruleSet || [];
this.rules = [];
for (var i = 0; i < ruleSet.length; i++) {
    if (ruleSet[i].target && ruleSet[i].action) {
        this.rules.push(ruleSet[i]);
    }
}
}
PageElements.prototype.run = function(elem) {
for (var i = 0; i < this.rules.length; i++) {
    this.rules[i].action.apply(elem.find(this.rules.target));
}
}

var GlobalRules = new PageElements([
{
    "target": ".draggable",
    "action": function() { this.draggable({
        cancel: "div#scrolling, .content",
        containment: "document"
        });
    }
},
{
    "target" :".resizable",
    "action": function() {
        this.resizable({
            handles: "all",
            zIndex: 0,
            containment: "document"
        });
    }
}

]);

GlobalRules.run($("body"));

// If you need to add elements later on, you can just call GlobalRules.run(yourNewElement);

如果您有非标准的事件或创建事件,那么这是一种非常有效的注册规则的方法。当与发布/订阅通知系统结合使用,并绑定到创建元素时触发的事件时,这也非常强大。轻松实现模块化事件绑定!


2
@Jessica:为什么在线工具会有所不同呢?方法仍然是相同的:将其分隔/模块化,使用框架促进组件之间的松耦合(现在它们都带有事件委托),拆分您的代码。有什么不适用于您的工具呢?您有很多按钮这一事实吗? - Sébastien Renauld
2
@Jessica:已更新。我使用类似于“视图”的概念简化和优化了图层的创建过程。那么,这对你的代码不适用吗? - Sébastien Renauld
10
@Jessica: 不优化地分割文件就像购买更多抽屉来存垃圾一样。总有一天你得清理,而在抽屉满之前清理会更容易。为什么不两全其美呢?如果你只是要分割代码,那么现在看起来你需要一个 layers.jssidebar.jsglobal_events.jsresources.jsfiles.jsdialog.js。使用 grunt 将它们重新构建为一个文件,并使用 Google Closure Compiler 进行编译和最小化。 - Sébastien Renauld
3
使用 require.js 时,你必须仔细研究 r.js 优化器,这是使 require.js 值得使用的关键。它将合并和优化所有文件: http://requirejs.org/docs/optimization.html - Willem D'Haeseleer
2
@SébastienRenauld,其他用户仍然非常欣赏您的答案和评论。如果这能让您感觉更好的话 ;) - Adriano
显示剩余11条评论

13

这是使用require.js将您当前的代码库拆分为多个文件的简单方法。我将向您展示如何将代码拆分为两个文件。在此之后添加更多文件也将变得简单明了。

步骤1) 在您的代码顶部创建一个App对象(或您喜欢的其他名称,比如MyGame):

var App = {}

步骤2) 将所有顶级变量和函数转换为属于App对象。

不要使用:

var selected_layer = "";

而是使用:

App.selected_layer = "";

function getModified(){
...
}
你想要:

App.getModified = function() {

}

请注意,在此时您的代码不会工作,直到完成下一步。

第三步)将所有全局变量和函数引用转换为通过App进行。

更改以下内容:

selected_layer = "."+classes[1];

至:

App.selected_layer = "."+classes[1];

并且:

getModified()

收件人:

App.GetModified()

第四步) 在这个阶段测试你的代码 -- 它应该是可以工作的。一开始可能会出现一些错误,因为你可能漏掉了某些东西,所以在继续之前要修复这些错误。

第五步) 设置requirejs。我假设你有一个网页,由web服务器提供服务,其代码位于:

www/page.html

以及jQuery的使用

www/js/jquery.js
如果这些路径不完全像这样,下面的内容将无法工作,您必须修改路径。

下载requirejs并将require.js放在您的www/js目录中。

在您的page.html中,删除所有脚本标签并插入一个类似于以下的脚本标签:

<script data-main="js/main" src="js/require.js"></script>

创建 www/js/main.js 文件,其内容如下:

require.config({
 "shim": {
   'jquery': { exports: '$' }
 }
})

require(['jquery', 'app']);

然后将您在步骤1-3中修复的所有代码(其唯一的全局变量应为App)放入:

www/js/app.js

在文件最顶端放置以下内容:

require(['jquery'], function($) {

在底部放置:

})

然后在浏览器中加载page.html,您的应用程序就应该可以工作了!

步骤6)创建另一个文件

这是您的工作付出得到回报的地方,您可以一遍又一遍地做这个。

www/js/app.js中提取一些引用$和App的代码。

例如:

$('a').click(function() { App.foo() }

将它放在www/js/foo.js中。

在该文件的顶部,添加:

require(['jquery', 'app'], function($, App) {

在底部放置:

})

然后将 www/js/main.js 的最后一行改为:

require(['jquery', 'app', 'foo']);

就是这样!每次你想把代码放在自己的文件中时都要这样做!


这里有多个问题 - 显而易见的是,你在最后将所有文件分割开来,并且没有使用 r.js 预处理器,每个脚本每次页面加载都会强制向每个用户浪费 400 字节的数据。此外,你实际上并没有解决 OP 的问题 - 只是提供了一个通用的 require.js 如何使用的指南。 - Sébastien Renauld
7
嗯?我的回答是针对这个问题的。r.js 显然是下一步,但问题在于组织,而不是优化。 - Lyn Headley
我喜欢这个答案,我从未使用过require.js,所以我必须看看是否可以使用它并从中获得任何好处。我大量使用模块模式,但也许这将允许我将一些东西抽象出来,然后再引用它们。 - Tony
1
@SébastienRenauld:这个答案不仅仅是关于require.js的。它主要讲述了为你正在构建的代码创建一个命名空间。我认为你应该欣赏其中的优点,并在发现任何问题时进行编辑。 :) - Mithun Satheesh

10

假设您不想将您的代码移植到像Backbone这样的框架,也不想使用像Require这样的加载器库,我会假定您的问题和评论是因为您希望以最简单的方式更好地组织现有代码。

我理解在2000多行代码中查找要处理的部分很烦人。解决方法是将代码拆分成不同的文件,每个功能一个文件。例如,sidebar.jscanvas.js等。然后您可以使用Grunt将它们合并到一起进行生产,与Usemin一起使用,您可以得到类似以下的内容:

在您的html中:

<!-- build:js scripts/app.js -->
<script src="scripts/sidebar.js"></script>
<script src="scripts/canvas.js"></script>
<!-- endbuild -->

在你的 Gruntfile 文件中:

useminPrepare: {
  html: 'app/index.html',
  options: {
    dest: 'dist'
  }
},
usemin: {
  html: ['dist/{,*/}*.html'],
  css: ['dist/styles/{,*/}*.css'],
  options: {
    dirs: ['dist']
  }
}

如果您想要使用Yeoman,它会为您提供所有这些的样板代码。

对于每个文件本身,您需要确保遵循最佳实践,并且所有的代码和变量都在该文件中,并且不依赖其他文件。这并不意味着您不能从其他文件调用函数,重点是要封装变量和函数。类似于命名空间。我假设您不想将所有代码都移植为面向对象的,但如果您不介意进行一些重构,我建议添加类似于称为模块模式的东西的等效物。看起来像这样:

sidebar.js

var Sidebar = (function(){
// functions and vars here are private
var init = function(){
  $("#sidebar #sortable").sortable({
            forceHelperSize: true,
            forcePlaceholderSize: true,
            revert: true,
            revert: 150,
            placeholder: "highlight panel",
            axis: "y",
            tolerance: "pointer",
            cancel: ".content"
       }).disableSelection();
  } 
  return {
   // here your can put your "public" functions
   init : init
  }
})();

接着你可以像这样加载这段代码:

$(document).ready(function(){
   Sidebar.init();
   ...

这将使您的代码更易于维护,而无需过多重写。


1
你可能需要认真重新考虑倒数第二个片段,它并不比内联编写代码更好:你的模块需要 #sidebar #sortable。你可以通过将代码内联并保存两个 IETF 来节省内存。 - Sébastien Renauld
重点在于你可以使用任何需要的代码。我这里只是使用了原始代码中的一个例子。 - Jesús Carrera
我同意Jesus的观点,这只是一个例子,OP可以轻松地添加一个“对象”选项,使他们能够指定元素的选择器,而不是硬编码。但这只是一个快速的例子。我想说我喜欢模块化设计,这是我主要使用的模式,但即使如此,我仍在尝试更好地组织我的代码。我通常使用C#,所以函数命名和创建感觉很普遍。我尝试保持“模式”,例如下划线表示局部和私有,变量仅为“对象”,然后我在返回中引用该函数,它是公共的。 - Tony
然而,我仍然发现这种方法存在挑战,并希望有更好的方法来解决这个问题。但是,它比在全局空间中声明我的变量和函数以与其他js发生冲突要好得多...哈哈 - Tony

6

使用JavaScript MVC框架来以标准的方式组织JavaScript代码。

最佳的JavaScript MVC框架有:

选择一个JavaScript MVC框架需要考虑很多因素。阅读以下比较文章,它将帮助您基于项目重要的因素选择最佳框架: http://sporto.github.io/blog/2013/04/12/comparison-angular-backbone-can-ember/

你也可以使用RequireJS与框架配合支持异步js文件和模块加载。
查看以下内容开始JS模块加载:
http://www.sitepoint.com/understanding-requirejs-for-effective-javascript-module-loading/


4

对你的代码进行分类。这种方法对我非常有帮助,适用于任何JavaScript框架:

(function(){//HEADER: menu
    //your code for your header
})();
(function(){//HEADER: location bar
    //your code for your location
})();
(function(){//FOOTER
    //your code for your footer
})();
(function(){//PANEL: interactive links. e.g:
    var crr = null;
    $('::section.panel a').addEvent('click', function(E){
        if ( crr) {
            crr.hide();
        }
        crr = this.show();
    });
})();

在您最喜欢的编辑器中(最好使用Komodo Edit),您可以折叠所有条目并只查看标题:

(function(){//HEADER: menu_____________________________________
(function(){//HEADER: location bar_____________________________
(function(){//FOOTER___________________________________________
(function(){//PANEL: interactive links. e.g:___________________

2
+1 对于一个不依赖于库的标准JS解决方案。 - hobberwickey
2
所有这样做的只是把代码组织成离散的部分。据我所知,这是A:良好的实践,和B:您真的不需要社区支持的库。并非所有项目都适合于Backbone、Angular等框架,通过将代码封装在函数中模块化是一个好的通用解决方案。 - hobberwickey
您可以随时依赖于任何喜爱的库来使用此方法。但是上述解决方案适用于纯JavaScript、自定义库或任何著名的JS框架。 - user2377782
@SébastienRenauld:什么是IETF?如果你指的是闭包,我听说过IEFE和IIFE这些术语。而且你所说的“它们不是免费的”是什么意思? - Bergi
@bergi:立即执行的临时函数。它们会消耗内存和处理能力,比你想象的要多得多。有很多基准测试可以参考。 - Sébastien Renauld
显示剩余5条评论

3
我建议:
  1. 使用发布/订阅模式管理事件。
  2. 面向对象编程
  3. 命名空间
在您的情况下,Jessica,请将界面分成页面或屏幕。页面或屏幕可以作为对象,并从某些父类扩展。使用PageManager类来管理页面之间的交互。

你能举例或提供资源来详细说明吗? - Kivylius
1
“面向对象”是什么意思?JS中几乎所有的东西都是对象。而且JS中没有类。 - Bergi

2
我建议你使用类似 Backbone 的东西。Backbone 是一个支持 RESTFUL 的 JavaScript 库。它能够让你的代码更加简洁易读,在与 requirejs 一起使用时,具有强大的效果。

http://backbonejs.org/

http://requirejs.org/

Backbone不是一个真正的库。它的目的是为你的JavaScript代码提供结构。它能够包含其他库,如jquery、jquery-ui、google-maps等。在我看来,Backbone是最接近面向对象和模型视图控制结构的JavaScript方法。
此外,关于您的工作流程...如果您在PHP中构建应用程序,请使用Laravel库。当与RESTful原则一起使用时,它将与Backbone无缝配合。以下是Laravel框架的链接以及有关构建RESTful API的教程:

http://maxoffsky.com/code-blog/building-restful-api-in-laravel-start-here/

http://laravel.com/

以下是来自Nettuts的教程。Nettuts有很多高质量的教程:

http://net.tutsplus.com/tutorials/javascript-ajax/understanding-backbone-js-and-the-server/


0
也许现在是时候开始使用诸如 yeoman http://yeoman.io/ 等工具来实施整个开发工作流程了。这将有助于控制所有的依赖关系、构建过程,如果需要,还可以进行自动化测试。虽然一开始需要付出很多努力,但一旦实施完成,将会使未来的修改变得更加容易。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接