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

Best way to organize jQuery/JavaScript code (2013)

问题

此答案以前已被回答过,但已过时且不是最新的。我在一个文件中有2000多行代码,我们都知道这是一个糟糕的实践,特别是当我浏览代码或添加新特性时。我希望现在和将来更好地组织我的代码。

我应该提到,我正在构建一个工具(而不是一个简单的网站),在全局范围内有许多按钮、UI元素、拖放、动作侦听器/处理程序和函数,其中几个侦听器可以使用相同的函数。

示例代码

1
2
3
4
5
6
7
8
9
10
$('#button1').on('click', function(e){
    // Determined action.
    update_html();
});

... // Around 75 more of this

function update_html(){ .... }

...

更多示例代码

结论

我真的需要组织这段代码以获得最佳使用,而不是重复我自己,并且能够添加新功能和更新旧功能。我将自己解决这个问题。一些选择器可以是100行代码,其他的是1行。我看了一下require.js,发现它有点重复,实际上写的代码比需要的多。我愿意接受任何符合此标准的可能解决方案,并且链接到资源/示例始终是一个优势。

谢谢。


我将介绍一些简单的事情,可能对你有帮助,也可能对你没有帮助。有些可能是显而易见的,有些可能是极其神秘的。

第1步:划分代码

将代码分成多个模块化单元是非常好的第一步。把"一起"工作的东西聚集起来,放在他们自己的小包裹里。现在不要担心格式,将其保持在线。这个结构是后来的一点。

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

enter image description here

为了便于维护(而不必筛选1000行),将所有与头相关的事件处理程序/绑定划分到其中是有意义的。

然后,您可以使用Grunt等工具将JS重新构建回单个单元。

步骤1a:依赖关系管理

使用RequireJS或CommonJS之类的库来实现称为AMD的东西。异步模块加载允许您明确地说明代码所依赖的内容,然后允许您卸载对代码的库调用。您可以直接说"这需要jquery",AMD将加载它,并在jquery可用时执行您的代码。

这也有一个隐藏的gem:库加载将在dom准备就绪后完成,而不是之前。这将不再停止页面的加载!

第二步:模块化

看到线框了吗?我有两个广告部。他们很可能共享了事件侦听器。

在这一步中,您的任务是识别代码中的重复点,并尝试将这些重复点合成到模块中。现在,模块将包含所有内容。我们一边走一边分东西。

这个步骤的全部思想是从步骤1开始,删除所有复制的意大利面,用松散耦合的单元替换它们。所以,不要:

ad_unit1.js

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

ad_unit2.js

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

我将拥有:

ad_unit.js

1
2
3
4
5
6
 var AdUnit = function(elem) {
     this.element = elem || new jQuery();
 }
 AdUnit.prototype.bindEvents = function() {
     ... Events go here
 }

page.js

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

它允许您在事件和标记之间进行划分,同时避免重复。这是一个相当不错的步骤,稍后我们将进一步扩展。

第三步:选择一个框架!

如果您想进一步模块化和减少重复,那么在实现MVC(模型-视图-控制器)方法的周围有许多很棒的框架。我最喜欢的是脊骨/脊柱,不过,还有棱角分明的,yii,…名单还在继续。

模型代表您的数据。

一个视图代表你的标记和与之相关联的所有事件。

控制器代表您的业务逻辑——换句话说,控制器告诉页面要加载的视图和要使用的模型。

这将是一个重要的学习步骤,但这个奖是值得的:它喜欢干净的模块化代码,而不是意大利面。

你还可以做很多其他的事情,这些只是指导方针和想法。

代码特定更改

下面是对代码的一些具体改进:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
 $('.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);

            }

        }

    });
 });

最好这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
$("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);
          }
     });
});

在代码前面:

1
2
3
4
5
6
7
8
9
10
window.Layer = function() {
    this.instance = $("");
    // Markup generated here
};
window.Layer.prototype = {
   setName: function(newName) {
   },
   bindToGroup: function(parentNode) {
   }
}

突然间,您就有了一种方法,可以从代码中的任何位置创建标准层,而无需复制粘贴。你在五个不同的地方做这个。我刚给你保存了五份复印件。

再一个:

//操作的规则集包装器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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);

如果有不标准的事件或创建事件,这是注册规则的一种非常有效的方法。当与发布/子通知系统结合在一起时,以及当绑定到您在创建元素时触发的事件时,这也是非常严重的问题。别忘了模块化事件绑定!


下面是一种使用require.js将当前代码库拆分为多个文件的简单方法。我将向您演示如何将代码拆分为两个文件。之后,添加更多的文件将非常简单。

步骤1)在代码顶部,创建一个应用程序对象(或您喜欢的任何名称,如mygame):

var App = {}

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

而不是:

var selected_layer ="";

你想要:

App.selected_layer ="";

而不是:

1
2
3
function getModified(){
...
}

你想要:

1
2
3
App.getModified = function() {

}

请注意,在完成下一步之前,您的代码将不会工作。

步骤3)将所有全局变量和函数引用转换为go-through-app。

更改如下内容:

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

到:

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

还有:

1
getModified()

到:

1
App.GetModified()

步骤4)在这个阶段测试您的代码——它应该都能工作。一开始你可能会有一些错误,因为你错过了一些东西,所以在继续之前先解决这些错误。

步骤5)设置需求。我假设您有一个网页,由Web服务器提供服务,其代码位于:

1
www/page.html

和jQuery

1
www/js/jquery.js

如果这些路径与此不完全相同,下面的路径将不起作用,您必须修改这些路径。

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

在您的page.html中,删除所有脚本标记并插入脚本标记,如:

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

创建内容为:

1
2
3
4
5
6
7
require.config({
"shim": {
   'jquery': { exports: '$' }
 }
})

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

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

1
www/js/app.js

在文件的最上面,放:

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

在最底层:

1
})

然后在浏览器中加载page.html。你的应用程序应该可以工作!

步骤6)创建另一个文件

这就是你的工作回报的地方,你可以一次又一次地这样做。

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

例如

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

放在www/js/foo.js

在文件的最上面,放:

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

在最底层:

1
})

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

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

就是这样!每次您想将代码放入自己的文件时都要这样做!


对于您的问题和评论,我假设您不愿意将代码移植到框架(如主干)或使用加载程序库(如所需)。你只需要一个更好的方法来组织你已经拥有的代码,以最简单的方式。

我知道滚动2000多行代码来查找您要处理的部分是很烦人的。解决方案是将代码拆分到不同的文件中,每个文件对应一个功能。例如,sidebar.jscanvas.js等,然后你可以用咕噜声把它们连在一起生产,再加上usemin,你可以得到如下的东西:

在HTML中:

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

在您的gruntfile中:

1
2
3
4
5
6
7
8
9
10
11
12
13
useminPrepare: {
  html: 'app/index.html',
  options: {
    dest: 'dist'
  }
},
usemin: {
  html: ['dist/{,*/}*.html'],
  css: ['dist/styles/{,*/}*.css'],
  options: {
    dirs: ['dist']
  }
}

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

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

旁巴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
  }
})();

然后您可以像这样加载这段代码:

1
2
3
$(document).ready(function(){
   Sidebar.init();
   ...

这将允许您拥有一个更易于维护的代码,而不必重写过多的代码。


使用JavaScriptMVC框架以标准方式组织JavaScript代码。

可用的最佳JavaScript MVC框架有:

  • 骨干
  • 角度的
  • 坎斯
  • 余烬
  • 反应物

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

您还可以在框架中使用RequireJS来支持异步JS文件和模块加载。
查看以下内容开始JS模块加载:
http://www.sitepoint.com/understanding-requirejs-for-effective-javascript-module-loading/


对代码进行分类。这个方法对我有很大帮助,并且可以使用任何JS框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(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编辑),您可以折叠所有条目,只看到标题:

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


我建议:

  • 事件管理的发布服务器/订阅服务器模式。
  • 对象方向
  • 命名空间
  • 在您的案例中,Jessica将界面划分为页面或屏幕。页面或屏幕可以是对象,并且可以从一些父类进行扩展。使用PageManager类管理页面之间的交互。


    我建议你用一些像脊骨的东西。主干是一个RESTful支持的JavaScript库。ik使您的代码更清晰、更可读,并且在与requirejs一起使用时功能强大。

    http://backbonejs.org网站/

    http://requirejs.org网站/

    主干不是真正的库。它旨在为您的JavaScript代码提供结构。它可以包括其他库,如jquery、jquery ui、google maps等。我认为主干是最接近面向对象和模型视图控制器结构的javascript方法。

    也与您的工作流程有关。如果用PHP构建应用程序,请使用Laravel库。当与restfull原则一起使用时,它将完美地与主干一起工作。下面是Laravel框架的链接和有关构建restfull API的教程:

    网址:http://laravel.com/

    以下是Nettus的教程。Nettus有很多高质量的教程:

    http://net.tutspus.com/tutorials/javascript-ajax/understanding-主干网-js-and-the-server/


    也许是时候开始使用诸如yeoman http://yeoman.io/之类的工具实现整个开发工作流了。这将有助于控制您的所有依赖项、构建过程以及自动化测试(如果需要)。它有很多工作要开始,但一旦实现,将使未来的更改更容易。