Skip to Content
JavaScript3 源文件结构

3 源文件结构

所有新的源文件应该是 goog.module 文件(包含 goog.module 调用的文件)或 ECMAScript (ES) 模块(使用 importexport 语句)。

文件按以下顺序组成:

  1. 许可证或版权信息(如果有)
  2. @fileoverview JSDoc(如果有)
  3. goog.module 语句(如果是 goog.module 文件)
  4. ES import 语句(如果是 ES 模块)
  5. goog.requiregoog.requireType 语句
  6. 文件的实现代码

每个存在的部分之间用恰好一个空行分隔,但文件的实现代码前面可以有 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 模块是使用 importexport 关键字的文件。

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 规范允许这样做。请注意,importexport 语句都可能创建循环依赖。

// 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.providegoog.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.requiregoog.module.get 等方式使用,就好像它是一个没有调用 goog.module.declareLegacyNamespacegoog.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.requiregoog.requireType 语句

导入通过 goog.requiregoog.requireType 语句完成。goog.require 语句导入的名称可以在代码和类型注解中使用,而 goog.requireType 导入的名称只能在类型注解中使用。

goog.requiregoog.requireType 语句形成一个连续的块,中间没有空行。此块跟在 goog.module 声明之后,由单个空行分隔goog.requiregoog.requireType 的整个参数是在单独文件中由 goog.module 定义的命名空间。goog.requiregoog.requireType 语句不得出现在文件的其他任何位置。

每个 goog.requiregoog.requireType 被赋值给单个常量别名,或者被解构为多个常量别名。这些别名是在类型注解或代码中引用依赖项的唯一可接受方式。完全限定的命名空间不得在任何地方使用,除了作为 goog.requiregoog.requireType 的参数。

例外:在 externs 文件中声明的类型、变量和函数必须在类型注解和代码中使用其完全限定名称。

goog.require 被赋值给单个常量别名时,它必须与导入模块命名空间的最后一个点分隔的组件匹配。

例外:在某些情况下,可以使用命名空间的其他组件来形成更长的别名。生成的别名必须保留原始标识符的大小写,以便它仍然能够正确标识其类型。更长的别名可用于消除其他相同别名的歧义,或者如果它显著提高了可读性。此外,必须使用更长的别名来防止遮蔽原生类型,如 ElementEventErrorMapPromise(更完整的列表请参见 MDN 上的 标准内置对象 Web API )。

重命名解构别名时,冒号后面必须有一个空格,如 ?? 中所要求的。

同一文件中不应同时包含针对同一命名空间的 goog.requiregoog.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 文件的实现代码

实际的实现代码在所有依赖信息声明之后(至少由一个空行分隔)。

这可能包括任何模块本地声明(常量、变量、类、函数等),以及任何导出的符号。

Last updated on