65.9K
CodeProject 正在变化。 阅读更多。
Home

充分利用VS2012中的JavaScript智能感知

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (13投票s)

2012年9月17日

CPOL

13分钟阅读

viewsIcon

50200

在使用 AMD/require.js 时,充分利用 VS2012 中的 JavaScript Intellisense

引言  

在我看来,Intellisense for JavaScript 始终感觉像是 Visual Studio 的一个未完成的附加功能,它承诺了很多,但对于大型项目却无法兑现。仅仅为了 Intellisense 就手动添加并维护另一套依赖项引用似乎从未值得。然而,随着 Visual Studio 2012 的发布,Intellisense 得到了更新,这是否意味着现在值得重新审视?

在这篇文章中,我使用了Colin Eberhardt 的 HTML5 Property Finder 应用,并使用 AMD(异步模块定义)对其进行了增强,允许每个 JavaScript 文件定义自己的依赖项,这与 C# 类似。这个单一的依赖项数据源随后用于在编写代码时自动提供 Intellisense,在浏览器调试时按正确的顺序加载每个 JavaScript 文件(告别长长的 script 标签列表!),以及在发布时构建一个单一的优化 JavaScript 文件。

Intellisense for JavaScript    

在编写 C# 时,Intellisense 通过提供语句的自动补全以及带有上下文敏感文档的类型/成员/变量查找,使生活更加轻松。作为一名开发人员,它很快就成为你工具包中不可或缺的一部分,以至于当你开始一个 JavaScript 项目并看到它的性能有多差时,绝望很快就会袭来!此时,值得退一步思考为什么会这样,为什么 JavaScript Intellisense 无法与 C# 对应物相提并论?

事实很简单,JavaScript 缺乏 Intellisense 所依赖的许多语言结构。其动态特性意味着类型或类型信息概念毫无意义,任何对象都可以在运行时添加新的方法/属性,或者反过来删除方法/属性。其他语言中理所当然的一些概念,如命名空间和导入,Simply 缺失。

这并不是对语言本身的批评,它的使用随着时间的推移已经发展成为它最初并非为其设计的用途。因此,开发人员填补了这些空白,例如,现在存在许多技术可以允许导入,每种技术都有其优缺点。然而,从 Intellisense 的角度来看,这使得情况更加糟糕,与其有一个标准的实现 X 的技术,不如现在需要处理 N 种潜在冲突的技术。

为了克服这些问题,VS2010 引入了伪执行 JavaScript 代码。这涉及在特殊的 Intellisense JavaScript 上下文中运行代码,以确定特定对象在代码特定点上的状态。如果一个文件需要另一个文件,告知 Intellisense 依赖项的唯一方法是添加一个专门构造的 Intellisense 注释,指向另一个文件。

VS2012 将这一点向前推进了一步,增加了一个用于从 JavaScript 与 Intellisense 交互的 API,以及用于运行专门针对 Intellisense 上下文的附加代码的钩子。这是个好消息,我们现在可以编写代码来帮助 Intellisense 理解特定构造在我们的代码中是如何使用的。本文的其余部分将介绍如何将流行的 AMD 框架 require.js 集成到 Colin 的 Property Finder 应用中,并帮助 Intellisense 与之协同工作。

什么是 AMD?  

AMD 的基本思想是每个 JavaScript 文件都成为一个模块,其命名方式类似于 C# 中的命名空间,根据其在文件系统中的路径。所有模块的代码都放在一个回调函数中,模块以几种标准方式之一声明其依赖项。这是 Property Finder 项目中的一个简单示例:
define("model/JSONFileDataSource", function (require) {
  var $ = require("lib/jquery");
  return function () {
    /// <summary>
    /// A test version of JSONDataSource, which returns 'canned' responses.
    /// </summary>
    this.findProperties = function (location, pageNumber, callback) {
      function fetchData() {
        $.ajax(...);
      }
      ...
    };
  };
}); 

该示例定义了一个名为 model/JSONFileDataSource 的模块,它依赖于另一个名为 lib/jquery 的模块。依赖项被分配给一个本地作用域变量,模块通过从回调函数返回一个构造函数来导出自身。这允许另一个模块引用它:
define("example", function (require) {
  var DataSource = require("model/JSONFileDataSource");
  var dataSource = new DataSource();
  dataSource.findProperties(...);
  return ...;
});

缺失的部分是加载器,它提供了上面使用的 define 全局方法。它有两个职责:在调用回调函数之前加载引用的依赖项(如上所示),以及从入口点主模块引导加载,例如:
<script src="Scripts/lib/require.js" type="text/javascript" data-main="example"></script>

Property Finder 应用中的 AMD

在将 Property Finder 项目迁移到使用 AMD 的过程中,我尝试尽可能遵循 require.js 文档中推荐的最佳实践。然而,为了达到我想要的 Intellisense 集成水平,我被迫做出了一项妥协。

文档建议使用匿名模块,通过依赖加载器根据其文件路径推导名称来避免冗余。不幸的是,由于 Intellisense 加载引用的方式,运行在 Intellisense 上下文中的加载器根本无法访问文件路径。因此,为了保持 Intellisense 的准确性,所有模块都必须显式命名。令人遗憾的是,施加了这样的限制,但这与 C# 中显式命名类型的方式并无二致。

你可能已经读了很多,现在想尝试一下。你需要 Visual Studio 2012,支持 Web 项目(我使用的是 VS Express 2012 for Web),以及这个 git 仓库的克隆最新代码的 zip 包。我保留了 Colin 的原始目录结构,但只更新了 iPhonePropertySearch 项目——除了 Colin,还有人真的使用 Windows Phone 吗? Smile | <img src= " />   


你应该能够打开 Scripts 文件夹中的任何 JavaScript 文件(不包括 _references.jslib/*),并获得文件中顶部引用的任何依赖项的完整 Intellisense 自动补全。根据文件中具体位置以及你尝试自动补全的依赖项,可能需要按几次 ctrl+space 才能使依赖项完全加载,稍后我将详细解释原因。

 

Intellisense 使用 JavaScript 代码的伪执行来提供自动补全建议,因此为了取得任何进展,我们需要将其指向我们的代码。最简单的方法是依赖于所有 Web 项目默认添加的隐式 Intellisense 引用 ~/Scripts/_references.js。此文件将在任何其他代码之前由 Intellisense 解析,允许你添加对其他文件的引用,并对特定于 Intellisense 执行环境的 JavaScript 代码进行一般性更改(即,该文件不会包含在客户端代码中)。

对于 Property Finder 应用,我们首先引用 jQuery 以避免加载顺序问题,然后是 require.js。Require.js 还需要额外的配置来告诉它解析模块引用的基位置,在本例中是项目根目录下的 Scripts 文件夹。如果你需要自定义任何路径,Intellisense 文档更详细地介绍了路径格式和解析规则。

// jQuery is loaded separately into the page to avoid load
// order problems so we must reference it separately here
// also.
// https://requirejs.node.org.cn/docs/jquery.html#advanced
/// <reference path="lib/jquery-1.8.1.js" />
// Require.js is included directly when loaded in the browser
// (only while developing) so we need to reference it here so
// that intellisense will recognise it.
/// <reference path="lib/require.js" />
// Tell require.js where this projects scripts are located.
requirejs.config({
  baseUrl: '~/Scripts/'
}); 

当 Intellisense 遇到 jQuery 和 require.js 引用时,它还会查找同名的 intellisense 文件,并加载这些文件以及原始文件。在这种情况下,有一个 jquery-1.8.1.intellisense.js 文件为 jQuery 提供了额外的文档,还有一个 require.intellisense.js 文件为 Intellisense 提供了它正确解析 require.js 依赖项语法所需的额外帮助。

如果你想在你的项目中使用 require.js 和 jQuery,以下文件是使 Intellisense 正确工作的最低要求:
  • Scripts/_references.js  
  • Scripts/lib/require.js 
  • Scripts/lib/require.intellisense.js 
  • Scripts/lib/jquery.js 
  • Scripts/lib/jquery-1.8.1.js 
  • Scripts/lib/jquery-1.8.1.intellisense.js 

以及你的 HTML 页面中的以下引导代码(其中 Scripts/app.js 是你的应用程序的根):

  <script src="Scripts/lib/jquery-1.8.1.js" type="text/javascript"></script>
  <script src="Scripts/lib/require.js" type="text/javascript" data-main="Scripts/app"></script>

发布优化 

AMD 经常受到批评的一点是,客户端不应负责解析已发布代码中的所有脚本引用。这样做会导致用户体验大大减慢,因为他们的浏览器在后台费力地进行多次往返来加载单个 JavaScript 文件,如果再考虑到移动连接,情况就变得非常糟糕了!

理想情况下,我们应该运行加载器代码一次来生成完整的依赖项包,丢弃加载器代码(它不再需要),然后对生成的包运行标准的 JavaScript 优化器。正如通常发生的那样,其他人已经考虑过这一点,并以 require.js 优化器和 almond.js 的形式为我们完成了这项工作。

require.js 优化器是一个基于 Node.js 的命令行工具,它解析模块依赖项并优化结果。通常 require.js 仍然需要在 HTML 页面中单独包含。但是,通过在优化器输出中包含almond.js,它充当 require.js 的轻量级捆绑替代品。

要从项目目录运行优化器,请运行以下命令:

r.js -o build.json 

这告诉 require.js 优化器使用 build.json 中的配置来生成优化后的输出文件。Property Finder 项目中的 build.json 基于almond.js 文档中的示例:
{
  baseUrl: 'Scripts',
  name: '../almond',
  include: ['lib/jquery-1.8.1','app'],
  insertRequire: ['app'],
  out: 'Scripts/app.min.js',
  wrap: true
}

要使你的应用程序使用新的优化版本,请从 HTML 页面中删除对 jQuery 和 require.js 的两个脚本引用,并在它们的位置添加:
<script src="Scripts/app.min.js" type="text/javascript"></script>

有人可能会争辩说,这里的问题不大,因为一旦应用程序打包,所有代码都将与设备本地。然而,在资源受限的移动设备上,我们应该努力使浏览器的生活尽可能轻松。

幕后发生了什么?

当你开始输入时,你会在 JavaScript Language Service 输出窗口中看到日志,说明已 require 的模块已加载。这些是文件的直接依赖项,但依赖项的依赖项呢?这些依赖项被称为传递依赖项,每次按下 ctrl+space,都会加载另一层传递依赖项,直到加载完整个依赖项树中引用的所有内容。

你可能会认为,这种渐进式依赖加载必然会导致某种形式的竞态条件,即尝试自动补全尚未加载的引用。起初我也对此感到担忧,但事实证明有两个非常方便的缓解因素。

依赖项缓存

第一个与 intellisense 如何处理引用的文件有关。一旦一个文件被检测为当前文件的依赖项,这一事实就会被固定下来,直到当前文件关闭。然后,在处理当前文件之前,会自动处理这一组依赖项。这对我们来说是个好消息,因为它意味着层层传递依赖加载只需要发生一次。

不幸的是,这也有一个反面影响,事实证明自动加载是无法支持匿名模块的原因。自动加载是 Intellisense 的一项功能,因此它发生在 require.js 请求模块之前,因此,就它而言,它正在加载它未请求的匿名模块,因此无法准确命名。最终结果是它会恐慌,并且日志中会显示以下内容:

ERROR:mismatch::Error: Mismatched anonymous define() module

对象图遍历 

第二个原因更为平凡,如果你需要 n 层深度传递依赖项,你很可能已经通过至少 n 个对象来访问该依赖项。这是一个令人困惑的陈述,但我想不到更好的描述方式,所以也许举个例子会有帮助:

假设 Car (n=0) 依赖于 Wheel (n=1),而 Wheel 本身依赖于 Tyre (n=2)。当你从 Car 的实例引用 Tyre 时,它很可能通过类似 this.frontLeftWheel.tyre.PROPERTY 的引用找到。在这种情况下,在需要 Tyre 中定义的属性之前,你已经遍历了 3 个对象,所以 Intellisense 可以提供它们(即 3>2)。

这显然不是万无一失的,所以当你第一次编辑一个新打开的文件时,你可能会时不时地遇到 Intellisense 错误。这些错误表现为一个包含所有已知属性的自动补全列表,旁边有黄色图标。如果发生这种情况,则按预设次数按 ctrl+space 应该可以解决问题,通过允许 Intellisense 遍历这些额外的传递依赖项层。

结论 - 值得吗? 

让我们先快速回顾一下这些变化:
  1. 删除了引用所有 JavaScript 文件(按依赖顺序)的 intellisense.js 文件,以及从每个 JavaScript 文件中反向引用 intellisense.js 文件的操作。 不再需要为 Intellisense 维护源文件列表,因为这是从文件中列出的 AMD 定义的依赖项派生出来的。添加了一个 Scripts/_references.js 文件,但由于这是一个内置的隐式引用,因此无需从每个 JavaScript 文件中反向引用它。
     
  2. 删除了建立 JavaScript 代码命名空间对象的 Namespaces.js 文件。 AMD 不使用任何全局变量,因此无需以相同的方式对代码进行命名。此外,由于相对文件路径构成了模块标识符,因此命名冲突会隐式地得到防止。
     
  3. 删除了 index.html 中的 script 标签列表,该列表按依赖顺序引用了所有 JavaScript 文件。 另一个不再需要的源文件长列表,它被一个用于 require.js 的脚本标签和对应用程序主模块的引用所取代。
    事实上,原始列表包含一个依赖顺序错误,PropertySearchViewModel 引用了 LocationViewModel 但却在其之前包含。由于引用使用方式的原因,这并没有导致运行时错误,但确实说明了弄错依赖列表有多么容易。  
     
  4. 添加了一个优化的发布版本 JavaScript,该版本再次从 AMD 定义的依赖项构建。

我认为以上内容很明显,AMD 可以为大型 JavaScript 项目带来许多好处,当与“即用型”Intellisense 结合使用时,这是一个非常有吸引力的选择。我希望你觉得受到启发去尝试一下,祝你集成到你的项目中好运!

 

Chris 


附录 - 两个常见的 JavaScript 错误

在将项目迁移到使用 AMD 的过程中,我遇到了一些似乎对 JavaScript 语义存在误解的情况。我不想批评 Colin 的代码,我在这里涵盖这些只是为了让可能在这些概念上挣扎过的任何人受益。

new window["ViewModel"][state.factoryName](); 

上面的代码通过调用基于变量的构造函数来创建一个新的视图模型。这种模式没有任何问题,代码运行也很好,这里的问题是 JSLint/JSHint 报告的不必要的方括号表示法使用:

Line 1: new window["ViewModel"][state.factoryName]();
['ViewModel'] is better written in dot notation. 

如果属性名是常量字符串,则不需要方括号表示法,对此规则的例外情况是当常量字符串以数字开头或包含非法的标识符字符时。然而,在这种情况下,它是一个完全由字母组成的字符串,并且可以更清晰地用点表示法重写为:
new ViewModel[state.factoryName]();

我发现的第二个问题在功能上是健全的,但突显了对另一项语言功能可能存在的误解:
propertySearchViewModel.addToFavourites.call(propertySearchViewModel, currentViewModel());

Function 原型上的 call 方法允许使用指定的上下文而不是与调用关联的隐式上下文来调用方法。上述代码中的隐式上下文是 propertySearchViewModel(这是调用函数引用的对象),因此无需强制上下文为 propertySearchViewModel。相反,代码可以更清晰地重写为:
propertySearchViewModel.addToFavourites(currentViewModel());

对于以下任何情况,此规则均适用:

a.b.apply(a, [c...]) 等同于 a.b(c...)
a.b.apply(a) 等同于 a.b()
a.b.call(a, c...) 等同于 a.b(c...)
a.b.call(a) 等同于 a.b()

注意:在这种情况下,JSLint/JSHint 不会发出任何警告,但我认为 应该发出警告
© . All rights reserved.