Closure Library 1 -- Dependence Management

潘灵均
2023-12-01

Closure Library用goog.provide()和goog.require()两个方法来建立其中的依赖关系.

calcdeps.py

Java中,多个互相依赖的类可以放在一起编译,但是Javascript不能包含前置声明因为Javascript文件的执行是线性的,执行顺序由定义在网页中的<script>标签顺序决定.为了能不使用前置声明也能定义script文件的顺序,calcdeps.py使用goog.require()和goog.provide()方法面向拓扑方法来对输入进行排序以使得所有文件的依赖关系能在文件本身加载前被加载进来.如果存在循环依赖,它会阻止输入进行拓扑排序,calcdeps.py也不能正常执行,并会报告错误.
calcdeps.py通过指定 --output_mode能产生四种类型的输出:
  • script 将所有有依赖关系的javascript文件合并成一个js文件.可以通过<script>标签来引入js文件作测试,只是这个文件略大一些.
  • list 根据依赖顺序产生一个javascript文件列表.
  • deps 产生一个goog.addDependency()方法序列,这些方法调用将被用于在运行时构造依赖图.
  • compiled 将输入文件进行编译,因此需要Closure Compiler存在.
calcdeps.py可以指定三种不同类型的输入,每种类型有自己的标记:
  • --input 标记指定的文件或者目录的依赖文件一定会出现在calcdeps.py的输出中(当然也包括它自己).
  • --path 标记指定的文件或者目录的依赖文件可能会包含input指定文件的依赖文件.当输出模式指定为deps时,所有被--path指定文件的依赖文件都将在calcdeps.py的输出文件中出现,但是如果输出模式是script,list或者compiled时,被--path指定的文件只有在它是被--input指定文件的依赖关系的子集时才会出现在输出中.例如,被--path指定的文件夹中包含所有的closure library代码,(假设就是closure-library文件夹),而被--input指定的文件只依赖一个goog.string,那么closure-library中只有goog/string/string.js以及string.js依赖的文件会出现在输出中.
  • --dep 标记只在输出为deps模式下被使用.它用来标明已经存在依赖信息的路径,例如指定Closure Library文件夹,它的deps.js已经被生成.被--dep指定的依赖信息不会出现在输出中.
实际应用中,输出模式最常用的是deps和compiled,deps模式经常被用来加载文件用来测试和开发,compiled模式被用来将产品部署到服务器上.

goog.global

实际上,goog.global是加载closure library框架中window对象的别名.这是因为在一个浏览器中,Window对象是global对象的别称,很多的global属性被定义在global对象上,例如NaN,Infinity,undefined,eval(),parseInt()等等.
global对象是可变的,所以任何想要被全局访问的值都应该被设为goog.global的属性.而那些由window对象指定但是与浏览器无关的属性应该通过window对象显示访问.
像之前解释的,在web编程中,虽然在base.js中goog.global被指定为this,但global对象就是指window,在浏览器中,this和window指向的是同一个对象,所以goog.global可以被赋值为this,也可以是window.
但是有很多非浏览器环境可以运行ECMAScript,例如Firefox扩展,这些环境中并不用window来指定全局对象,或者它根本不指定全局对象的引用,将goog.global指定为this而不是window可以使Closure与其他环境的衔接更安全,也为被编译器重命名后的全局对象提供了别名.

COMPILED

顶层Compiled常量是除了goog以外由Closure添加到全局环境中的另一个变量.这个变量的状态决定了Closure Library执行时依赖关系被如何管理的.(Compiled的值不能被直接设置,它的默认值为false,只能被编译器重新赋值).
很多开发者会犯这样的错误,他们会打出COMPILED来测试是否在调试状态.如果是要测试是否在调试状态可以使用goog.DEBUG,但不推荐用COMPILED.

goog.provide(namespace)

goog.provide()方法提供了一个字符串,这个字符串标识一个命名空间来保证与这个命名空间有关的一系列对象都存在.即用一个字符串声明了一个命名空间.
goog.provide()需要一个namespace作参数,并用点作分隔符将namespace分割成独立的名字.从最左边的名字开始,goog.provide()方法检查goog.global中是否存在这个名字指定的对象.如果没有这样的对象,一个新对象就被创建到goog.global中.否则就用已经存在的对象.这个过程会从左到右地扫描namespace字符串,保证经过goog.provide()方法之后,所有与namespace中name匹配的对象都存在了.
// Calling this function:
goog.provide('example.of.a.long.namespace');
// Is equivalent to doing:
//
// var example = goog.global.example || (goog.global.example = {});
// if (!example.of) example.of = {};
// if (!example.of.a) example.of.a = {};
// if (!example.of.a.namespace) example.of.a.namespace = {};
// Because goog.provide() has been called, it is safe to assume that
// example.of.a.long.namespace refers to an object, so it is safe to
// add new properties to it without checking for null.
example.of.a.long.namespace.isOver = function() { alert('easy, eh?'); };

经过goog.provide()方法,就可以说example.of.a.long.namespace指向了一个对象,为这个对象添加一个属性就很安全,也不用去检查是否为空.
注意,因为namespace中每个中间命名空间已经被检测存在,这就保证了将来调用goog.provide()方法时不会重复检查已经存在的命名空间,例如,如果调用了goog.provide('example.of.another.namespace')方法,another属性就会直接加到example.of对象上,而不是重新创建一个新对象,然后再将another属性增加到它身上.
Closure Library中的每个js文件开头都会包含一到多个goog.provide(),所有的增加到namespace上的元素都会被添加到文件中,然而,这样的惯例,使得通过namespace找到对应的js文件变得困难,但是在Closure中,还是推荐遵循这种习惯.

Motivation behind goog.provide()

传统上,已经有两种创建命名空间的方法了,第一种是在一个命名空间下给所有的方法一个相同的前缀并且保证没有其他库使用相同的前缀,第二种方法是创建一个全局对象,然后让所有的方法作为它的属性,这样的命名方式可以在之后为这个命名空间加更多的子空间.
// Creates a namespace named "goog".
var goog = {};
// Creates a new namespace named "goog.array".
goog.array = {};
// Adds a function named binarySearch to the "goog.array" namespace.
goog.array.binarySearch = function(arr, target, opt_compareFn) { /*...*/ };
// Adds a function named sort to the "goog.array" namespace.
goog.array.sort = function(arr, opt_compareFn) { /*...*/ };
这种方式的一个好处是用点来分割命名空间来代表不同的包与Java编程方式很相似.更重要的是,在全局空间中只有一个goog,而不是一个方法一个名字,有效避免了多个javascript库命名冲突的问题.
但是使用命名空间有两个缺点,第一个是现在调用一个方法前需要花费更多的功夫去寻找命名空间对象.例如,调用goog.array.sort([3,5,4]),首先需要JavaScript解释器先在全局作用域中找goog对象,得到array属性,在得到sort属性的值(即sort方法),最后才真正调用方法.方法命名越长,调用该方法时查找花费的精力也越多.另一个缺点是程序员需要打的字更多,因为命名空间长.
13章中会解释Closure Compiler如何重写JavaScript来省掉多余的查找过程.但不幸的是,Compiler并没有减轻程序员需要输入更多字的负担.

goog.require(namespace)

goog.require()与goog.provide()协同工作,任何需要在文件中使用的命名空间都应该在文件开头有相对应的goog.require(),如果一个命名空间在未被goog.provide()方法声明之前调用goog.require(),那么Closure会报错.
注意goog.require()与Java中的import并不完全相同,在Closure中,定义一个以特定类型作为参数的方法并不是非得要调用goog.require()来确保这个类型被加载,例如,下面的代码就不需要goog.math.Coordinat:
goog.provide('example.radius');
/**
* @param {number} radius
* @param {goog.math.Coordinate} point
* @return {boolean} whether point is within the specified radius
*/
example.radius.isWithinRadius = function(radius, point) {
return Math.sqrt(point.x * point.x + point.y * point.y) <= radius;
};

但是如果上述代码被重写成如下形式,就必须调用goog.require(goog.math.Coordinate)了,因为goog.math.Coordinate命名空间被显示调用了.
goog.provide('example.radius');
goog.require('goog.math.Coordinate');
/**
* @param {number} radius
* @param {goog.math.Coordinate} point
* @return {boolean} whether point is within the specified radius
*/<pre name="code" class="javascript">example.radius.isWithinRadius = function(radius, point) {
var origin = new goog.math.Coordinate(0, 0);
var distance = goog.math.Coordinate.distance(point, origin);<span style="font-family: Arial, Helvetica, sans-serif;">return distance <= radius;</span><span style="font-family: Arial, Helvetica, sans-serif;">};</span>
 
 
</pre><br /></div><h3><span style="white-space:pre">	</span><em>goog.addDependency(relativePath,provides,requires)</em></h3><div><span style="font-style: italic; white-space: pre;">	</span>goog.addDependency()被用来创建和管理未编译JavaScript文件中的依赖图,当JavaScript被编译后,编译期goog.provide()和goog.require()会被检查以确保确保没有命名空间在被provide之前就require的,如果检查顺利通过,goog.provide()将会被对象替换掉,goog.require也会完全去除.编译后,就没有必要在client端构建依赖图了,所以当COMPILED为true的时候,goog.addDependency()和它所依赖的全局常量都会被去除.</div><div><span style="white-space:pre">	</span>然而,未编译的Closure代码依赖于goog.addDependency(),它决定了哪些额外的JavaScript文件需要被加载.看下边的例子,example.View命名空间依赖于example.Model命名空间:</div><div><pre name="code" class="javascript">// File: model.js
goog.provide('example.Model');
example.Model.getUserForEmailAddress = function(emailAddress) {
if (emailAddress == 'bolinfest@gmail.com') {
return { firstName: 'Michael', lastName: 'Bolin' };
}
};
// File: view.js
goog.provide('example.View');
goog.require('example.Model');
example.View.displayUserInfo = function(emailAddress) {
var user = example.Model.getUserForEmailAddress(emailAddress);
document.write('First Name: ' + user.firstName);
// etc.
};

这个例子中,model.js和view.js放在primitives目录下,这个目录的兄弟目录是Closure Library,在primitives目录中,调用calcdeps.py来为model.js和view.js创建deps.js.

python ../closure-library-r155/bin/calcdeps.py \
--output_mode deps \
--dep ../closure-library-r155/goog/ \
--path model.js \
--path view.js > model-view-deps.js

上面的脚本剩下下边的文件model-view-deps.js:
// This file was autogenerated by calcdeps.py
goog.addDependency("../../primitives/model.js", ['example.Model'], []);
goog.addDependency("../../primitives/view.js", ['example.View'], ['example.Model']);

传递给calcdeps.py的每个输入文件对应一个goog.addDependency().(这个方法的第一个参数是相对与base.js的路径),每个调用反映了文件中的传入给goog.require()和goog.provide()的值.这些信息可以帮助在客户端建成依赖图.这个依赖图又帮助goog.require()来加载依赖.
<!doctype html>
<html>
<head></head>
<body>
<script src="../closure-library-r155/goog/base.js"></script>
<!--
When base.js is loaded, it will call:
document.write('<script src="../closure-library-r155/goog/deps.js"></script>');
The deps.js file contains all of the calls to goog.addDependency() to build
the dependency graph for the Closure Library. The deps.js file will be
loaded after the base.js script tag but before any subsequent script tags.
-->
<!--
This loads the two calls to goog.addDependency() for example.Model and
example.View.
-->
<script src="model-view-deps.js"></script>
<script>
// When this script block is evaluated, model-view-deps.js will already have
// been loaded. Using the dependency graph built up by goog.addDependency(),
// goog.require() determines that example.View is defined in
// ../../primitives/view.js, and that its dependency, example.Model, is
// defined in ../../primitives/model.js. These paths are relative to base.js,
// so it will call the following to load those two files:
//
// document.write('<script ' +
// 'src="../closure-library-r155/goog/../../primitives/model.js"><\/script>');
// document.write('<script ' +
// 'src="../closure-library-r155/goog/../../primitives/view.js"><\/script>');
//
// Like deps.js, model.js and view.js will not be loaded until this script
// tag is fully evaluated, but they will be loaded before any subsequent
// script tags.
goog.require('example.View'); // calls document.write() twice
// The example.View namespace cannot be used here because view.js has not
// been loaded yet, but functions that refer to it may be defined:
var main = function() {
example.View.displayUserInfo('bolinfest@gmail.com');
};
// So long as view.js is loaded before this function is executed, this is not
// an issue because example.View.displayUserInfo() is not evaluated
// until main() is called. The following, however, would be evaluated
// immediately:
alert(typeof example); // alerts 'undefined'
</script>
<script>
// Both model.js and view.js will be loaded before this <script> tag,
// so example.Model and example.View can be used here.
alert(typeof example); // alerts 'object'
main(); // calls function that uses example.View
</script>
</body>
</html>

以上述方式加载依赖会动态生成很多<script>标签,这种加载很多文件的方式在生产环境下是难以接受的,但是对于开发环境来说就比较方便,因为JavaScript错误能够直接映射到源文件的代码行.不过对于拼接而成的大文件或者编译之后的javascript文件,这种方式也不好.

Function Curring(函数局部套用)

函数的局部应用是一个功能强大的技术,它能预先决定一部分函数需要的参数,剩下的参数在函数调用的时候再指定.

goog.partial(functionToCall,...)

goog.partial()的参数是一个需要调用的方法和一系列方法需要的参数,很像call()方法,call()方法对于JavaScript中的所有方法都适用.但partial()方法与call()又有很大的不同,call()方法需要明确给出调用方法所需的参数,goog.partial()返回一个新的方法,如果新方法被调用了就会将指定了的参数和其他参数传递给新方法执行.看下边这个例子:
var a = function() {
alert('Hello world!');
};
var b = goog.partial(alert, 'Hello world!');

a()和b()的功能都是输出"hello world",再看下边这个例子:
// Recall that Math.max() is a function that takes an arbitrary number of
// arguments and returns the greatest value among the arguments given.
// If no arguments are passed, then it returns -Infinity.
var atLeastTen = goog.partial(Math.max, 10);
atLeastTen(-42, 0, 7); // returns 10: equivalent to Math.max(10, -42, 0, 7);
atLeastTen(99); // returns 99: equivalent to Math.max(10, 99);
atLeastTen(); // returns 10: equivalent to Math.max(10);

atLeastTen()可以为Math.max()方法传递10和其他参数,简单而言,当用goog.partial()创建好atLeastTen(),并不是先将10传递给Math.max(),然后再将其他参数传递给Math.max(),而是当调用atLeastTen()时,将所有的参数传递给Math.max().
因此,当一个方法的前N个参数是确定的,而剩下的参数需要之后才能决定时,goog.partial()就非常有用.这种机制在事件处理中相当常见,一般在listener注册之后事件处理方法已经知道了,但是方法需要的参数(即事件对象本身)直到事件触发才知道.
使用goog.partial()也能帮助防止内存泄露,看下边这个例子:
var createDeferredAlertFunction = function() {
// Note the XMLHttpRequest constructor is not available in Internet Explorer 6.
var xhr = new XMLHttpRequest();
return function() {
alert('Hello world!');
};
};
var deferredAlertFunction = createDeferredAlertFunction();

goog.bind(functionToCall,selfObject,...)

goog.bind()和goog.partial()很相似,不同的是它的第二个参数是this绑定的对象,这种机制可以有效防止像下边这样的一个常见错误:
ProgressBar.prototype.update = function(statusBar) {
if (!this.isComplete()) {
var percentComplete = this.getPercentComplete();
statusBar.repaintStatus(percentComplete);
// Update the statusBar again in 500 milliseconds.
var updateAgain = function() { this.update(statusBar); };
setTimeout(updateAgain, 500);
}
};

上述方法定义在ProgressBar.prototype上,当执行该方法时,预期结果是this将会绑定到ProgressBar对象上,对于update()方法来说,确实如此,但是对于updateAgain()方法却不是这样的,因为setTimeout是在全局上下文中执行的,这就意味着this将被绑定到global对象上,在web浏览器中,this指向window对象,解决这个问题最常用的方法是引入一个新变量,通常起名为self:
ProgressBar.prototype.update = function(statusBar) {
if (!this.isComplete()) {
var percentComplete = this.getPercentComplete();
statusBar.repaintStatus(percentComplete);
// Update the statusBar again in 500 milliseconds.
var self = this;
var updateAgain = function() { self.update(statusBar); };
setTimeout(updateAgain, 500);
}
};

因为self没有this具备的特性,所以当updateAgain被执行时self不会被global对象替换掉,所以update()方法可以在原始的ProgressBar对象上执行,在Closure中,解决这个问题更优雅的方式是使用goog.bind():
ProgressBar.prototype.update = function(statusBar) {
if (!this.isComplete()) {
var percentComplete = this.getPercentComplete();
statusBar.repaintStatus(percentComplete);
// Update the statusBar again in 500 milliseconds.
var updateAgainWithGoogBind = goog.bind(this.update, this, statusBar);
setTimeout(updateAgainWithGoogBind, 500);
}
};

像goog.partial()一样,使用goog.bind()能够在限制作用域内创建方法,所以updateAgainWithGoogBind()不用去维护percentComplete引用,而updateAgain却需要维护,而且还需要防止percentComplete被当作垃圾回收掉.goog.bind()经常被用来延迟方法调用,就像上边例子所示.

Export

Closure Compiler开启它强大的重命名设置后,所有的自定义变量都会被重命名,它正是用这样的方法来进行代码编译的,为了保证变量能够在编译后通过原始名字仍然能够访问到,这个变量名字需要被导出.

goog.getObjectByName(name , opt_object)

goog.getObjectByName()传入一个字符串名字返回其相对应的对象,当访问一个由其他JavaScript库导出或者导出到全局环境中的变量时这个方法就很有帮助.
var GMap2 = goog.getObjectByName('google.maps.GMap2');
var map = new GMap2(document.body);
// window.location will not be defined if the code is executed in Rhino
var href = goog.getObjectByName('window.location.href');

乍一看,这种方式访问window.location.href要比var = window.location.href 要累赘.
事实上,使用前一种方式的好处是当这个对象不存在时会返回null,但是后一种方法只会抛出一个错误.例如,如果window被定义,但是location并没有被定义,当访问location.href时就会报错.要不使用goog.getObjectByName()去检查安全性会显得更累赘:
var href = (window && window.location && window.location.href) || null;

这是用来检测浏览器插件是否存在很通用的方法:
var workerPool = goog.getObjectByName('google.gears.workerPool');

goog.exportProperty(object,propertyName,value)

goog.exportProperty()给object设置一个属性(property值.通常,object已经一个属性(有名字和值),但是通过goog.exportProperty()可以确保即使在编译后也能够使用名字访问到属性.以下边未编译源码为例:
goog.provide('Lottery');
Lottery.doDrawing = function() {
Lottery.winningNumber = Math.round(Math.random() * 1000);
};
// In uncompiled mode, this is redundant.
goog.exportProperty(Lottery, 'doDrawing', Lottery.doDrawing);
现在以编译后代码为例:
var a = {}; // Lottery namespace
a.a = function() { /* ... */ }; // doDrawing has been renamed to 'a'
a.doDrawing = a.a; // doDrawing exported on Lottery

编译后的代码中,Lottery对象有两个属性(a和doDrawing),这两个属性都指向同一个方法.将doDrawing导出并没有把这个属性的重命名版本给覆盖掉(即没有将a.a覆盖掉),这是经过深思熟虑后的方案,来使得Lottery编译后可以使用简短的名字访问到方法a.a,这样就能缩短代码量.
注意将易变属性导出却不会带来我们想要的效果.例如winingNumber是Lottery对象的一个可变属性,将其导出:
Lottery.winningNumber = 747;
goog.exportProperty(Lottery, 'winningNumber', Lottery.winningNumber);
Lottery.getWinningNumber = function() { return Lottery.winningNumber; };
goog.exportProperty(Lottery, 'getWinningNumber', Lottery.getWinningNumber);

与源码一起编译后变为:
var a = {};
a.b = function() { a.a = Math.round(Math.random() * 1000); };
a.doDrawing = a.b;
a.a = 747;
a.winningNumber = a.a;
a.c = function() { return a.a; };
a.getWinningNumber = a.c;

现在考虑一下下边的代码:
var hijackLottery = function(myNumber) {
Lottery.doDrawing();
Lottery.winningNumber = myNumber;
return Lottery.getWinningNumber();
};

当用未编译的Lottery时,hijackLottery()将返回myNumber,当使用未编译的hijackLottery(),调用的是编译后的Lottery库时,返回结果就变为一个随机数.这是因为Lottery.winingNumber=myNumber,设置了winingNumber属性,但是Lottery.个体WiningNumber()返回的是一个名字为a的属性值.解决方案就是将一个setter方法导出而非导出一个可变属性.
Lottery.setWinningNumber = function(myNumber) {
Lottery.winningNumber = myNumber;
};
goog.exportProperty(Lottery, 'setWinningNumber', Lottery.setWinningNumber);

现在hijackLottery()不管是编译后还是编译前,返回结果都是myNumer了:
var hijackLottery = function(myNumber) {
Lottery.doDrawing();
Lottery.setWinningNumber(myNumber);
return Lottery.getWinningNumber();
};

并不是只有方法才应该导出,导出只读的基本数据也是可以的:
Lottery.MAX_TICKET_NUMBER = 999;
goog.exportProperty(Lottery, 'MAX_TICKET_NUMBER', Lottery.MAX_TICKET_NUMBER);

在事件中,你也需要导出可变方法:
Lottery.doDrawingFunction_ = function() {};
Lottery.setDoDrawingFunction = function(f) {
Lottery.doDrawingFunction_ = f;
};
goog.exportProperty(Lottery, 'setDoDrawingFunction',
Lottery.setDoDrawingFunction);
Lottery.doDrawing = function() {
Lottery.doDrawingFunction_.apply(null, arguments);
};
goog.exportProperty(Lottery, 'doDrawing', Lottery.doDrawing);


将新的doDrawing方法作为参数传递给Lottery.setDoDrawingFunction()就可以改变doDrawing方法,而不用重置Lottery对象的doDrawing属性.下次调用doDrawing()时,就会调用新的doDrawing方法.这在编译和未编译模式下都适用.

goog.exportSymbol(publicPath, object, opt_objectToExportTo)

goog.exportSymbol()与goog.exportProperty()方法类似,不同之处是goog.exportSymbol的第一个参数是一个导出全路径而不是一个属性名.只要全路径中任何一个中间对象不存在,goog.exportSymbol()就会创建出它们.这里还是以lottery为例:
goog.exportSymbol('Lottery.doDrawing', Lottery.doDrawing);
goog.exportSymbol('Lottery.getWinningNumber', Lottery.getWinningNumber);
goog.exportSymbol('Lottery.setWinningNumber', Lottery.setWinningNumber);
goog.exportSymbol('Lottery.MAX_TICKET_NUMBER', Lottery.MAX_TICKET_NUMBER);

使用goog.exportSymbol()有一个好处,就是它会创建一个新的Lottery对象,并为它添加四个属性.不像goog.exportProperty(),Lottery对象不会有重命名版本,因为goog.exportSymbol()会为Lottery创建一个新对象,而不是将属性添加到已经存在的对象上.

Type Assertions

 类似资料:

相关阅读

相关文章

相关问答