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

时间:2013-05-24 13:45:47

标签: javascript jquery design-patterns backbone.js requirejs

问题

此答案之前已经得到解答,但已经过时而且不是最新的。我在一个文件中有超过2000行代码,而且我们都知道这是不好的做法,特别是当我查看代码或添加新功能时。我想更好地组织我的代码,无论是现在还是将来。

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

示例代码

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

... // Around 75 more of this

function update_html(){ .... }

...

More example code

结论

我真的需要组织这些代码以便最好地使用,而不是重复自己,并能够添加新功能并更新旧功能。我将自己做这件事。一些选择器可以是100行代码,其他的是1.我在require.js看了一下,发现它有点重复,并且实际上编写了比需要更多的代码。我对任何符合此标准的可能解决方案持开放态度,并且链接到资源/示例总是一个加号。

感谢。

8 个答案:

答案 0 :(得分:93)

我会回顾一些可能会或可能不会帮助你的简单事情。有些可能是显而易见的,有些可能是非常神秘的。

第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() { ... });

我会:

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();

除了摆脱重复之外,您还可以在事件标记之间进行细分。这是一个相当不错的步骤,我们稍后会进一步扩展它。

第3步:选择一个框架!

如果你想进一步模块化和减少重复,那么实现MVC(模型 - 视图 - 控制器)方法的方法就有很多很棒的框架。我最喜欢的是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);

如果您有非标准事件或创建事件,这是一种非常有效的注册规则的方法。当与发布/订阅通知系统结合使用时,这也是非常严重的问题,当您绑定到每次创建元素时触发的事件时。 Fire'n'forget模块化事件绑定!

答案 1 :(得分:13)

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

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

var App = {}

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

而不是:

var selected_layer = "";

你想:

App.selected_layer = "";

而不是:

function getModified(){
...
}

你想:

App.getModified = function() {

}

请注意,此时您的代码将无法正常工作,直到您完成下一步。

步骤3)转换所有全局变量和函数引用以通过App。

更改以下内容:

selected_layer = "."+classes[1];

为:

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

getModified()

为:

App.GetModified()

步骤4)在此阶段测试您的代码 - 它应该全部有效。一开始你可能会遇到一些错误,因为你遗漏了一些错误,所以在继续之前要修复它们。

步骤5)设置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。

的代码

e.g。

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

将其放入www/js/foo.js

在该文件的最顶部,输入:

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

在底部放:

})

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

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

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

答案 2 :(得分: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();
   ...

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

答案 3 :(得分:6)

使用javascript MVC Framework以标准方式组织javascript代码。

可用的最佳JavaScript MVC框架是:

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

您还可以在框架中使用RequireJS来支持Asynchrounous js文件&amp;模块加载。
请看下面的内容,开始使用JS模块加载:
http://www.sitepoint.com/understanding-requirejs-for-effective-javascript-module-loading/

答案 4 :(得分:4)

对您的代码进行分类。这个方法对我有很大的帮助,并且适用于任何js框架:

(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:___________________

答案 5 :(得分:3)

我建议:

  1. 事件管理的发布者/订阅者模式。
  2. 面向对象
  3. 命名空间
  4. 在您的情况下,Jessica将界面划分为页面或屏幕。页面或屏幕可以是对象,也可以从某些父类扩展。使用PageManager类管理页面之间的交互。

答案 6 :(得分:2)

我建议你使用像Backbone这样的东西。 Backbone是RESTFUL支持的JavaScript库。 Ik使您的代码更清晰,更易读,与requirejs一起使用时功能强大。

http://backbonejs.org/

http://requirejs.org/

Backbone不是一个真正的图书馆。它旨在为您的javascript代码提供结构。它可以包括其他库,如jquery,jquery-ui,google-maps等。在我看来,Backbone是面向对象和模型视图控制器结构的最接近的javascript方法。

还有关于您的工作流程。如果您使用Laravel库在PHP中构建应用程序。当与RESTfull原则一起使用时,它将与Backbone完美配合。在Laravel Framework的链接下面,以及有关构建RESTfull 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/

答案 7 :(得分:0)

也许是时候开始使用yeoman http://yeoman.io/等工具开始实施整个开发工作流程了。这将有助于控制所有依赖项,构建过程以及自动测试。它的开展很多工作,但一旦实施将使未来的变化更容易。