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

IT技术 javascript jquery design-patterns architecture formatting
2021-01-30 04:01:56

随着像 jQuery 这样的 JavaScript 框架使客户端 Web 应用程序更丰富、更实用,我开始注意到一个问题......

你到底是如何组织起来的?

  • 将所有处理程序放在一个位置并为所有事件编写函数?
  • 创建函数/类来包装您的所有功能?
  • 写得像疯了一样,只是希望它能达到最好的效果?
  • 放弃并获得新的职业?

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

是否有任何关于保持.js文件与应用程序的其余部分一样漂亮和整洁的最佳方式的一般建议或者这只是IDE的问题?有没有更好的选择?


编辑

这个问题更多地是关于代码组织而不是文件组织。有一些非常好的合并文件或拆分内容的例子。

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

有些人列出了命名空间,这是一个好主意。还有哪些其他方法,更具体地说是处理页面上的元素并保持代码井井有条?

6个回答

这将是一个要好很多,如果JavaScript的有内置的命名空间,但我发现,组织之类的东西达斯汀·迪亚兹介绍这里帮助我很多。

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

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

@MattBriggs 也称为 the module pattern,它基于IIFE pattern.
2021-03-20 04:01:56
@robsch 他的例子不带任何参数,但大多数会。请参阅我的示例,了解通常是如何完成的(TypeScript,但 99% 相同): repl.it/@fatso83/Module-Pattern-in-TypeScript
2021-03-31 04:01:56
你不需要以某种方式导出类吗?如何从这样的module外部创建对象?还是应该createNewSomething()在返回对象中有一个方法,以便对象创建仅在module内发生?嗯...我希望类(构造函数)从外面是可见的。
2021-04-08 04:01:56
我通常将其称为“克罗克福德方式”。来自我的 +1
2021-04-09 04:01:56
你甚至可以走得更远。请参阅此链接:wait-till-i.com/2007/08/22/...
2021-04-09 04:01:56

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

通常,我将为每个应用程序创建一个“主”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>');
  //...
};

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

我想在 C# 的世界里,它也可以是一篇很好的文章来推广使用var,现在我想起来了。大多数反对 using 的论点var是您无法确定返回内容的“类型”,但我想该论点应该反对不知道返回内容的“类”。如果使用 Apps Hungarian,那么您就不必担心……很有趣。
2021-03-16 04:01:56
@DanAbramov 谢谢你的链接。我真的必须阅读乔尔的所有博客,他解释得非常好。绝对配得上他的名气/声誉。从现在开始,我将称之为Systems Hungarian代码气味和Apps Hungarian实践:)
2021-03-23 04:01:56
@Marnen:我明白你的意思,但它作为程序员的指南并不是毫无用处。$ 前缀提醒我稍后阅读我的代码时它是什么,从而有助于更快地理解。
2021-03-26 04:01:56
我发现使用$作为变量名前缀是一种更常见的做法,但我可能是错的。所以,$s = $('...')而不是jS = $('...'),我猜只是一个偏好问题。不过很有趣,因为匈牙利表示法被认为是一种代码气味。我的一些 JavaScript 代码约定/首选项与我的 C#/Java 编码约定有多么不同,这很奇怪。
2021-03-28 04:01:56
@jamie 在这种情况下,这不是代码异味,这恰恰是匈牙利语很好的少数情况之一您可能想阅读此内容
2021-04-09 04:01:56

您可以将脚本分解为单独的文件进行开发,然后创建一个“发布”版本,将它们全部塞满并在其上运行YUI Compressor或类似的东西。

有时会出现不需要的 javascript 脚本。将它们发送给客户是浪费。我认为最好只发送需要的东西。当然,对于整天都在使用的 Web 应用程序(例如 Intranet 应用程序),最好在第一次加载页面时立即发送整个批次。
2021-03-21 04:01:56
@DOK 编译应包括切除未使用的东西。
2021-03-26 04:01:56
还有一个延迟加载的概念来尝试减少带宽需求,您可以在其中加载初始页面,然后执行所需脚本文件的异步加载(如该问题的其他答案中所述)。尽管如此,这可能需要更多的请求,实际上可能不太有用。@DOK,如果JS被缓存,一个中等的请求可能比几个小的请求好。
2021-04-02 04:01:56

受早期帖子的启发,我制作了一份WysiHat(变更日志中提到的 RTE)分发Rakefile供应商目录的副本,并进行了一些修改,包括使用JSLint 进行代码检查和使用YUI Compressor进行缩小

这个想法是使用链轮(来自 WysiHat)将多个 JavaScript 合并到一个文件中,使用 JSLint 检查合并文件的语法,并在分发前使用 YUI Compressor 将其缩小。

先决条件

  • Java 运行时
  • Ruby和耙宝石
  • 您应该知道如何将 JAR 放入Classpath

现在做

  1. 下载Rhino并将 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,您可以包含(或require)其他 JavaScript 文件的 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
};

我不是模仿的忠实粉丝,但是如果您有许多想要移出全局范围的对象,这可能会有所帮助。

代码组织要求采用约定和文档标准:
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. 设置约定以改进代码。例如,将其所有内部函数或方法分组在其对象类型的类属性中。

Exc.ui.domTips = function (dom, tips) {
    this.dom = gift;
    this.tips = tips;
    this.internal = {
        widthEstimates: function (tips) {
            ...
        }
        formatTips: function () {
            ...
        }
    };
    ...
};


5. 制作命名空间、类、方法和变量的文档。必要时还会讨论一些代码(一些 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;
    ...
};


这些只是一些提示,但这对组织代码有很大帮助。记住你必须有纪律才能成功!