Challenge-Me: JavaScript 包含引擎 - 第 I 部分






3.81/5 (24投票s)
实现 JavaScript 缺失的在 JavaScript 文件中包含其他 JavaScript 文件的功能。
前言
首先,本文开启我的 Challenge-Me 系列文章,旨在涵盖那些难以解决或几乎不可能解决的问题。
其次,我将原来的大文章分为三篇较小的文章,原因有两个:便于阅读,以及该问题的每种解决方案都可以成为一篇独立的文章。
第一部分
引言
JavaScript 是一种很棒的语言。它的灵活性是难以想象的。但它也有其固有的缺点,这些缺点源于其最初的范围:为静态 HTML 页面注入活力。其中一个缺点是缺少在 JavaScript 文件中包含其他 JavaScript 文件的功能。虽然几年前这还不是一个大问题,但如今随着 JavaScript 应用程序的规模和复杂性不断增加,这已成为一个严峻的问题。
挑战
主要挑战
创建一个引擎,实现 JavaScript 缺失的在 JavaScript 文件中包含其他 JavaScript 文件的功能。
次要挑战
- 适用于在线和离线应用程序
- 显示是否存在循环依赖,以及涉及哪些文件
- 不使用
eval
有什么好处?便于开发大型 JavaScript 应用程序,这些应用程序可以组织成相关的文件,形成逻辑依赖树。
次要挑战是为了确立高质量标准(如果事情值得做,就应该做好)。
- 为什么不仅仅是在线(因为大约 90% 的 JavaScript 代码是在线使用的)?因为可以在不经过 HTTP 服务器的情况下快速简便地测试大量 JavaScript 代码。
- 循环依赖怎么办?当应用程序加载缓慢或因循环依赖导致内存不足时,看到这种情况非常令人沮丧,因此检测到循环依赖并停止加载过程是必须的。此外,指出哪些文件构成了循环依赖会更好,可以节省开发者大量寻找“大海捞针”的时间,尤其是在应用程序的文件树复杂且循环依赖涉及十几个文件的情况下。
- 关于
eval()
函数:它在执行 JavaScript 代码方面非常方便(而且这对于我们要做的事情可能非常有用),但有一个缺点:它对资源的扱方式不佳,因此在这种情况下,它完全不适合执行大段代码。
解决方案 1
逻辑
由于实际解决方案是对先前解决方案的持续改进,我将展示我到最新解决方案的整个过程。
大约四年前,我的第一个解决方案是在不断增长的 JavaScript 应用程序的每个文件中放入一个注释 “//Requires files:”,当我想要使用一个文件时,我查看它需要哪些文件,将它们放在一个列表中,然后对于列表中的每个文件,我都做同样的事情,直到找到独立的文件,在这个过程中创建文件在文档头部放置的顺序。
这个系统最多可以处理 10 到 20 个文件;超过这个数量就很难使用了,即使在开始这样做之前,我就知道这是一个临时解决方案,直到我有时间找到一个更合适的解决方案。
我之所以记得这个粗糙的解决方案,是因为它实际上描述了所需的算法,而需要做的是自动化这个过程(而且这并不像看起来那么容易)。
所以,让我们看看如何实现:每个文件将有一个包含部分,由一个 $include
函数组成,该函数将动态地将脚本对象添加到 HTML 文档的头部,这些脚本对象将加载函数参数指定的那些文件。
还有一个问题:当一个脚本开始执行时,它会一直执行下去(它不会因为当前脚本对象之前出现了新的脚本对象而停止,也不会像我们期望的那样先加载和执行那个脚本)。这意味着,如果你在 FileA.js 中有语句 var A = 5;
,在 FileB.js 中有 $include(“FileA.js”); alert(A);
,结果将是 undefined
而不是预期的 5
!
因此,我们可以推断,这个解决方案仅在所有文件仅包含函数声明(总是如此)并且对象声明不与其他文件中的对象相关时才有效。
如果你像 C# 代码一样组织代码,并且没有类外的声明,那么就没有问题。
必须考虑的另一个方面是,script
标签的 src
属性包含脚本相对于加载它的 HTML 文档的路径;由于页面逻辑上放置在不同的文件夹中,要包含同一个文件,我们将需要不同的 src
属性。
但是,Web 项目中的 JavaScript 文件通常都放在一个文件夹中(可能包含子文件夹),因此最好创建我们的包含引擎,使其相对于该文件夹而不是 HTML 文档来包含文件。
实现
var IncludingEngine = {};
IncludingEngine.FindPath = function()
{
var scripts = document.getElementsByTagName("script");
var foundNo = 0;
for (var i=0; i<scripts.length; i++)
{
if (scripts[i].src.match(this.FileName))
{
this.Path = scripts[i].src.replace(this.FileName, "");
foundNo ++;
}
}
if (foundNo == 0)
throw new Error("The name of this file isn't " +
this.FileName + "!\r\nPlease change it back!");
if (foundNo > 1)
throw new Error("There are " + foundNo + " files with the name " +
this.FileName + "!\r\nThere can be only one!");
}
IncludingEngine.Init = function ()
{
this.FileName = "IncludingEngine.js";
this.Path = "";
//the root path of the engine; all files included
//by this engine will be relative to this path
this.FilesLoaded = new Array();
//will store the files already loaded in order
//not to load (execute) a file many times
this.FindPath();
}
function $include()
{
for (var i = 0; i < arguments.length; i ++)
{
//check if the file was already loaded
//(this way we avoid circular dependencies also)
if (!IncludingEngine.FilesLoaded[arguments[i]])
{
var script;
script = document.createElement("script");
script.src = IncludingEngine.Path + arguments[i];
script.type = "text/javascript";
document.getElementsByTagName("head")[0].appendChild(script);
IncludingEngine.FilesLoaded[arguments[i]] = 1;
}
}
}
首先,我们找到 IncludingEngine.js 所在路径,因为后续文件将相对于此路径指定。我们还将有一个哈希表来跟踪已包含的文件,这样它们就不会被执行一次或多次。$include
函数(我使用了 $
,因为它让它看起来很特别)将向文档的头部添加脚本对象,这些对象将加载参数指定的文件。
注释
- 您也可以使用
document.write
将脚本添加到文档的头部(实际上,这在我最初的解决方案中使用过)。 - 您可能已经看到过这种方法,因为它很容易推断出来,但远未达到挑战的要求。
- 在第二部分,我将介绍一种截然不同的方法。
- 此实现的最新版本可以在 IncludingEngine.jsFramework.com 找到
以下是“JavaScript 包含引擎”系列文章的所有部分:第一部分、第二部分、第三部分。此外,您可能还想访问 IncludingEngine 网站以获取更多信息。