3 源文件结构
所有新的源文件应该是 goog.module 文件(包含 goog.module
调用的文件)或 ECMAScript (ES) 模块(使用 import 和 export 语句)。
文件按以下顺序组成:
- 许可证或版权信息(如果有)
@fileoverviewJSDoc(如果有)goog.module语句(如果是goog.module文件)- ES
import语句(如果是 ES 模块) goog.require和goog.requireType语句- 文件的实现代码
每个存在的部分之间用恰好一个空行分隔,但文件的实现代码前面可以有 1 或 2 个空行。
3.1 许可证或版权信息(如果有)
如果文件中需要包含许可证或版权信息,则放在此处。
3.2 @fileoverview JSDoc(如果有)
格式化规则参见 ??。
3.3 goog.module 语句
所有 goog.module 文件必须在单独一行中声明恰好一个 goog.module
名称:包含 goog.module 声明的行不得换行,因此这是 80
列限制的一个例外。
goog.module
的整个参数定义了一个命名空间(namespace)。它是包名称(反映代码所在目录结构片段的标识符)加上(可选地)以
lowerCamelCase 形式连接在末尾的主类/枚举/接口名称。
示例:
goog.module('search.urlHistory.urlHistoryService');3.3.1 层级结构
模块命名空间不得被命名为另一个模块命名空间的直接子级。
不允许:
goog.module('foo.bar'); // 'foo.bar.qux' would be fine, though
goog.module('foo.bar.baz');目录层级结构反映命名空间层级结构,因此嵌套更深的子级是更高层级父目录的子目录。请注意,这意味着”父”命名空间组的所有者必然知道所有子命名空间,因为它们存在于同一目录中。
3.3.2 goog.module.declareLegacyNamespace
单个 goog.module 语句后面可以选择跟一个
goog.module.declareLegacyNamespace(); 调用。尽可能避免使用
goog.module.declareLegacyNamespace()。
示例:
goog.module('my.test.helpers');
goog.module.declareLegacyNamespace();
goog.setTestOnly();goog.module.declareLegacyNamespace
是为了简化从传统的基于对象层级的命名空间迁移而存在的,但它带来了一些命名限制。由于子模块名称必须在父命名空间之后创建,此名称不得是任何其他
goog.module 的子级或父级(例如,goog.module('parent'); 和
goog.module('parent.child');
不能同时安全存在,goog.module('parent'); 和
goog.module('parent.child.grandchild'); 也不能)。
3.3.3 goog.module 导出(Exports)
类、枚举、函数、常量和其他符号使用 exports
对象导出。导出的符号可以直接定义在 exports
对象上,也可以在本地声明后单独导出。符号只有在需要在模块外部使用时才导出。未导出的模块本地符号不需要声明
@private。对于导出的符号和模块本地符号没有规定的排序方式。
示例:
const /** !Array<number> */ exportedArray = [1, 2, 3];
const /** !Array<number> */ moduleLocalArray = [4, 5, 6];
/** @return {number} */
function moduleLocalFunction() {
return moduleLocalArray.length;
}
/** @return {number} */
function exportedFunction() {
return moduleLocalFunction() * 2;
}
exports = {exportedArray, exportedFunction};/** @const {number} */
exports.CONSTANT_ONE = 1;
/** @const {string} */
exports.CONSTANT_TWO = 'Another constant';不要将 exports 对象标注为 @const,因为编译器已经将其视为常量。
/** @const */
exports = {exportedFunction};不要使用默认导出(default exports),因为它们无法轻松转换为 ES 模块语义。
exports = FancyClass;3.4 ES 模块
ES 模块是使用 import 和 export 关键字的文件。
3.4.1 导入(Imports)
import 语句不得换行,因此这是 80 列限制的一个例外。
3.4.1.1 导入路径
ES 模块文件必须使用 import 语句来导入其他 ES 模块文件。不要对另一个 ES
模块使用 goog.require。
import './sideeffects.js';
import * as goog from '../closure/goog/goog.js';
import * as parent from '../parent.js';
import {name} from './sibling.js';3.4.1.1.1 导入路径中的文件扩展名
导入路径中的 .js 文件扩展名不是可选的,必须始终包含。
import '../directory/file';import '../directory/file.js';3.4.1.2 多次导入同一文件
不要多次导入同一文件。这会使确定文件的聚合导入变得困难。
// Imports have the same path, but since it doesn't align it can be hard to see.
import {short} from './long/path/to/a/file.js';
import {aLongNameThatBreaksAlignment} from './long/path/to/a/file.js';3.4.1.3 命名导入
3.4.1.3.1 命名模块导入
模块导入名称(import * as name)使用 lowerCamelCase
命名,从导入的文件名派生。
import * as fileOne from '../file-one.js';
import * as fileTwo from '../file_two.js';
import * as fileThree from '../filethree.js';import * as libString from './lib/string.js';
import * as math from './math/math.js';
import * as vectorMath from './vector/math.js';某些库可能通常使用违反此命名规则的命名空间导入前缀,但过于常见的开源使用使得违反规则的风格反而更具可读性。目前唯一属于此例外的库是
threejs ,使用 THREE 前缀。
3.4.1.3.2 命名默认导入
默认导入名称从导入的文件名派生,并遵循 ?? 中的规则。
import MyClass from '../my-class.js';
import myFunction from '../my_function.js';
import SOME_CONSTANT from '../someconstant.js';注意:通常这种情况不应该发生,因为本风格指南禁止使用默认导出,参见 ??。默认导入仅用于导入不符合本风格指南的模块。
3.4.1.3.3 命名具名导入
通常,通过具名导入(import {name})导入的符号应保持相同的名称。避免为导入设置别名(import {SomeThing as SomeOtherThing})。优先通过使用模块导入(import *)或重命名导出本身来解决名称冲突。
import * as bigAnimals from './biganimals.js';
import * as domesticatedAnimals from './domesticatedanimals.js';
new bigAnimals.Cat();
new domesticatedAnimals.Cat();如果需要重命名具名导入,则在生成的别名中使用被导入模块的文件名或路径中的组件。
import {Cat as BigCat} from './biganimals.js';
import {Cat as DomesticatedCat} from './domesticatedanimals.js';
new BigCat();
new DomesticatedCat();3.4.2 导出(Exports)
符号只有在需要在模块外部使用时才导出。未导出的模块本地符号不需要声明
@private。对于导出的符号和模块本地符号没有规定的排序方式。
3.4.2.1 具名导出 vs 默认导出
在所有代码中使用具名导出(named exports)。你可以在声明上应用 export
关键字,或使用 export {name}; 语法。
不要使用默认导出(default exports)。导入模块必须为这些值命名,这可能导致跨模块命名不一致。
// Do not use default exports:
export default class Foo { ... } // BAD!// Use named exports:
export class Foo { ... }// Alternate style named exports:
class Foo { ... }
export {Foo};3.4.2.2 导出的可变性
导出的变量不得在模块初始化之外被修改。
如果需要修改,有一些替代方案,包括导出对具有可变字段的对象的常量引用,或导出可变数据的访问器函数。
// Bad: both foo and mutateFoo are exported and mutated.
export let /** number */ foo = 0;
/**
* Mutates foo.
*/
export function mutateFoo() {
++foo;
}
/**
* @param {function(number): number} newMutateFoo
*/
export function setMutateFoo(newMutateFoo) {
// Exported classes and functions can be mutated!
mutateFoo = () => {
foo = newMutateFoo(foo);
};
}// Good: Rather than export the mutable variables foo and mutateFoo directly,
// instead make them module scoped and export a getter for foo and a wrapper for
// mutateFooFunc.
let /** number */ foo = 0;
let /** function(number): number */ mutateFooFunc = (foo) => foo + 1;
/** @return {number} */
export function getFoo() {
return foo;
}
export function mutateFoo() {
foo = mutateFooFunc(foo);
}
/** @param {function(number): number} mutateFoo */
export function setMutateFoo(mutateFoo) {
mutateFooFunc = mutateFoo;
}3.4.2.3 export from
export from 语句不得换行,因此这是 80 列限制的一个例外。这适用于两种
export from 形式。
export {specificName} from './other.js';
export * from './another.js';3.4.3 ES 模块中的循环依赖(Circular Dependencies)
不要在 ES 模块之间创建循环依赖,即使 ECMAScript
规范允许这样做。请注意,import 和 export 语句都可能创建循环依赖。
// a.js
import './b.js';// b.js
import './a.js';
// `export from` can cause circular dependencies too!
export {x} from './c.js';// c.js
import './b.js';
export let x;3.4.4 与 Closure 的互操作
3.4.4.1 引用 goog
要引用 Closure 的 goog 命名空间,请导入 Closure 的 goog.js。
import * as goog from '../closure/goog/goog.js';
const {compute} = goog.require('a.name');
export const CONSTANT = compute();goog.js 仅导出全局 goog 中可以在 ES 模块中使用的属性子集。
3.4.4.2 ES 模块中的 goog.require
ES 模块中的 goog.require 与在 goog.module
文件中的工作方式相同。你可以 require 任何 Closure 命名空间符号(即由
goog.provide 或 goog.module 创建的符号),goog.require
将返回该值。
import * as goog from '../closure/goog/goog.js';
import * as anEsModule from './anEsModule.js';
const GoogPromise = goog.require('goog.Promise');
const myNamespace = goog.require('my.namespace');3.4.4.3 在 ES 模块中声明 Closure 模块 ID
goog.declareModuleId 可以在 ES 模块中使用,以声明类似 goog.module
的模块 ID。这意味着此模块 ID 可以通过 goog.require、goog.module.get
等方式使用,就好像它是一个没有调用 goog.module.declareLegacyNamespace
的 goog.module 一样。它不会将模块 ID 创建为全局可用的 JavaScript
符号。
对通过 goog.declareModuleId 声明的模块 ID 使用 goog.require(或
goog.module.get)将始终返回模块对象(就像使用 import *
一样)。因此,goog.declareModuleId 的参数应始终以 lowerCamelCaseName
结尾。
注意:在 ES 模块中调用 goog.module.declareLegacyNamespace
是错误的,它只能从 goog.module
文件中调用。没有直接的方法可以将“旧式”命名空间与 ES 模块关联。
goog.declareModuleId 应仅用于将 Closure 文件就地升级为 ES
模块,其中使用了具名导出。
import * as goog from '../closure/goog.js';
goog.declareModuleId('my.esm');
export class Class {};3.5 goog.setTestOnly
在 goog.module 文件中,goog.module
语句和(如果有的话)goog.module.declareLegacyNamespace()
语句后面可以选择跟一个 goog.setTestOnly() 调用。
在 ES 模块中,import 语句后面可以选择跟一个 goog.setTestOnly()
调用。
3.6 goog.require 和 goog.requireType 语句
导入通过 goog.require 和 goog.requireType 语句完成。goog.require
语句导入的名称可以在代码和类型注解中使用,而 goog.requireType
导入的名称只能在类型注解中使用。
goog.require 和 goog.requireType
语句形成一个连续的块,中间没有空行。此块跟在 goog.module
声明之后,由单个空行分隔。goog.require 或
goog.requireType 的整个参数是在单独文件中由 goog.module
定义的命名空间。goog.require 和 goog.requireType
语句不得出现在文件的其他任何位置。
每个 goog.require 或 goog.requireType
被赋值给单个常量别名,或者被解构为多个常量别名。这些别名是在类型注解或代码中引用依赖项的唯一可接受方式。完全限定的命名空间不得在任何地方使用,除了作为
goog.require 或 goog.requireType 的参数。
例外:在 externs 文件中声明的类型、变量和函数必须在类型注解和代码中使用其完全限定名称。
当 goog.require
被赋值给单个常量别名时,它必须与导入模块命名空间的最后一个点分隔的组件匹配。
例外:在某些情况下,可以使用命名空间的其他组件来形成更长的别名。生成的别名必须保留原始标识符的大小写,以便它仍然能够正确标识其类型。更长的别名可用于消除其他相同别名的歧义,或者如果它显著提高了可读性。此外,必须使用更长的别名来防止遮蔽原生类型,如
Element、Event、Error、Map 和 Promise(更完整的列表请参见 MDN
上的
标准内置对象 和
Web API )。
重命名解构别名时,冒号后面必须有一个空格,如 ?? 中所要求的。
同一文件中不应同时包含针对同一命名空间的 goog.require 和
goog.requireType
语句。如果导入的名称同时在代码和类型注解中使用,应通过单个
goog.require 语句导入。
如果模块仅因其副作用而被导入,则调用必须是 goog.require(而不是
goog.requireType),并且可以省略赋值。需要一条注释来解释为什么需要这样做并抑制编译器警告。
各行按以下规则排序:所有左侧有名称的 require
排在最前面,按这些名称的字母顺序排列。然后是解构
require,同样按左侧名称排序。最后是任何独立的 require
调用(通常是仅为了副作用而导入的模块)。
提示:无需记住此顺序并手动执行。你可以依赖 IDE 来报告未正确排序的
require。
如果较长的别名或模块名称导致一行超过 80 列限制,则不得换行:require
行是 80 列限制的一个例外。
示例:
// Standard alias style.
const asserts = goog.require('goog.asserts');
// Namespace-based alias used to disambiguate.
const testingAsserts = goog.require('goog.testing.asserts');
// Standard destructuring into aliases.
const {MyClass} = goog.require('some.package');
const {MyType} = goog.requireType('other.package');
const {clear, clone} = goog.require('goog.array');
const {Rgb} = goog.require('goog.color');
// Namespace-based destructuring into aliases used to disambiguate.
const {MyClass: NsMyClass} = goog.require('other.ns');
const {SomeType: FooSomeType} = goog.requireType('foo.types');
const {clear: objectClear, clone: objectClone} = goog.require('goog.object');
// Namespace-based destructuring into aliases used to prevent masking native type.
const {Element: RendererElement} = goog.require('web.renderer');
// Out of sequence namespace-based aliases used to improve readability.
// Also, require lines longer than 80 columns must not be wrapped.
const {SomeDataStructure: SomeDataStructureModel} = goog.requireType('identical.package.identifiers.models');
const {SomeDataStructure: SomeDataStructureProto} = goog.require('proto.identical.package.identifiers');
// goog.require without an alias in order to trigger side effects.
/** @suppress {extraRequire} Initializes MyFramework. */
goog.require('my.framework.initialization');不推荐:
// Some legacy code uses a "default export" style to export a single class, enum,
// record type, etc. Do not use this pattern in new JS.
// When using a "default export", prefer destructuring into aliases.
const MyClass = goog.require('some.package.MyClass');
const MyType = goog.requireType('some.package.MyType');// If necessary to disambiguate, prefer PackageClass over SomeClass as it is
// closer to the format of the module name.
const SomeClass = goog.require('some.package.Class');不允许:
// Extra terms must come from the namespace.
const MyClassForBizzing = goog.require('some.package.MyClass');
// Alias must include the entire final namespace component.
const MyClass = goog.require('some.package.MyClassForBizzing');
// Alias must not mask native type (should be `const JspbMap` here).
const Map = goog.require('jspb.Map');
// Don't break goog.require lines over 80 columns.
const SomeDataStructure =
goog.require('proto.identical.package.identifiers.SomeDataStructure');
// Alias must be based on the namespace.
const randomName = goog.require('something.else');
// Missing a space after the colon.
const {Foo:FooProto} = goog.require('some.package.proto.Foo');
// goog.requireType without an alias.
goog.requireType('some.package.with.a.Type');
/**
* @param {!some.unimported.Dependency} param All external types used in JSDoc
* annotations must be goog.require'd, unless declared in externs.
*/
function someFunction(param) {
// goog.require lines must be at the top level before any other code.
const alias = goog.require('my.long.name.alias');
// ...
}3.7 文件的实现代码
实际的实现代码在所有依赖信息声明之后(至少由一个空行分隔)。
这可能包括任何模块本地声明(常量、变量、类、函数等),以及任何导出的符号。