代码管理 ################################################################################ .. _ts-modules: 模块 ******************************************************************************** .. _import-paths: 导入路径 ================================================================================ TypeScript 代码必须使用路径进行导入。这里的路径既可以是相对路径,以 ``.`` 或 ``..`` 开头,也可以是从项目根目录开始的绝对路径,如 ``root/path/to/file`` 。 在引用逻辑上属于同一项目的文件时,应使用相对路径 ``./foo`` ,不要使用绝对路径 ``path/to/foo`` 。 应尽可能地限制父层级的数量(避免出现诸如 ``../../../`` 的路径),过多的层级会导致模块和路径结构难以理解。 .. code-block:: typescript import {Symbol1} from 'google3/path/from/root'; import {Symbol2} from '../parent/file'; import {Symbol3} from './sibling'; .. _namespaces-vs-modules: 用 命名空间 还是 模块? ================================================================================ 在 TypeScript 有两种组织代码的方式:命名空间(namespace)和模块(module)。 不允许使用命名空间,在 TypeScript 中必须使用模块(即 `ES6 模块 <http://exploringjs.com/es6/ch_modules.html>`_ )。也就是说,在引用其它文件中的代码时必须以 ``import {foo} from 'bar'`` 的形式进行导入和导出。 不允许使用 ``namespace Foo { ... }`` 的形式组织代码。命名空间只能在所用的外部第三方库有要求时才能使用。如果需要在语义上对代码划分命名空间,应当通过分成不同文件的方式实现。 不允许在导入时使用 ``require`` 关键字(形如 ``import x = require('...');`` )。应当使用 ES6 的模块语法。 .. code-block:: typescript // 不要这样做!不要使用命名空间! namespace Rocket { function launch() { ... } } // 不要这样做!不要使用 <reference> ! /// <reference path="..."/> // 不要这样做!不要使用 require() ! import x = require('mydep'); .. tip:: TypeScript 的命名空间早期也被称为内部模块并使用 ``module`` 关键字,形如 ``module Foo { ... }`` 。不要使用这种用法。任何时候都应当使用 ES6 的导入语法。 .. _ts-exports: 导出 ******************************************************************************** 代码中必须使用具名的导出声明。 .. code-block:: typescript // Use named exports: export class Foo { ... } 不要使用默认导出,这样能保证所有的导入语句都遵循统一的范式: .. code-block:: typescript // 不要这样做!不要使用默认导出! export default class Foo { ... } 为什么?因为默认导出并不为被导出的符号提供一个标准的名称,这增加了维护的难度和降低可读性的风险,同时并未带来明显的益处。如下面的例子所示: .. code-block:: typescript // 默认导出会造成如下的弊端 import Foo from './bar'; // 这个语句是合法的。 import Bar from './bar'; // 这个语句也是合法的。 具名导出的一个优势是,当代码中试图导入一个并未被导出的符号时,这段代码会报错。例如,假设在 ``foo.ts`` 中有如下的导出声明: .. code-block:: typescript // 不要这样做! const foo = 'blah'; export default foo; 如果在 ``bar.ts`` 中有如下的导入语句: .. code-block:: typescript // 编译错误! import {fizz} from './foo'; 会导致编译错误: ``error TS2614: Module '"./foo"' has no exported member 'fizz'`` 。反之,如果在 ``bar.ts`` 中的导入语句为: .. code-block:: typescript // 不要这样做!这定义了一个多余的变量 fizz! import fizz from './foo'; 结果是 ``fizz === foo`` ,这往往不符合预期,且难以调试。 此外,默认导出会鼓励程序员将所有内容全部置于一个巨大的对象当中,这个对象实际上充当了命名空间的角色: .. code-block:: typescript // 不要这样做! export default class Foo { static SOME_CONSTANT = ... static someHelpfulFunction() { ... } ... } 显然,这个文件中具有文件作用域,它可以被用做命名空间。但是,这里创建了第二个作用域——类 ``Foo`` ,这个类在其它文件中具有歧义:它既可以被视为类型,又可以被视为值。 因此,应当使用文件作用域作为实质上的命名空间,同时使用具名的导出声明: .. code-block:: typescript // 应当这样做! export const SOME_CONSTANT = ... export function someHelpfulFunction() export class Foo { // 只有类 Foo 中的内容 } .. _ts-export-visibility: 导出可见性 ================================================================================ TypeScript 不支持限制导出符号的可见性。因此,不要导出不用于模块以外的符号。一般来说,应当尽量减小模块的外部 API 的规模。 .. _ts-mutable-exports: 可变导出 ================================================================================ 虽然技术上可以实现,但是可变导出会造成难以理解和调试的代码,尤其是对于在多个模块中经过了多次重新导出的符号。这条规则的一个例子是,不允许使用 ``export let`` 。 .. code-block:: typescript // 不要这样做! export let foo = 3; // 在纯 ES6 环境中,变量 foo 是一个可变值,导入了 foo 的代码会观察到它的值在一秒钟之后发生了改变。 // 在 TypeScript 中,如果 foo 被另一个文件重新导出了,导入该文件的代码则不会观察到变化。 window.setTimeout(() => { foo = 4; }, 1000 /* ms */); 如果确实需要允许外部代码对可变值进行访问,应当提供一个显式的取值器。 .. code-block:: typescript // 应当这样做! let foo = 3; window.setTimeout(() => { foo = 4; }, 1000 /* ms */); // 使用显式的取值器对可变导出进行访问。 export function getFoo() { return foo; }; 有一种常见的编程情景是,要根据某种特定的条件从两个值中选取其中一个进行导出:先检查条件,然后导出。这种情况下,应当保证模块中的代码执行完毕后,导出的结果就是确定的。 .. code-block:: typescript function pickApi() { if (useOtherApi()) return OtherApi; return RegularApi; } export const SomeApi = pickApi(); .. _ts-container-classes: 容器类 ================================================================================ 不要为了实现命名空间创建含有静态方法或属性的容器类。 .. code-block:: typescript // 不要这样做! export class Container { static FOO = 1; static bar() { return 1; } } 应当将这些方法和属性设为单独导出的常数和函数。 .. code-block:: typescript // 应当这样做! export const FOO = 1; export function bar() { return 1; } .. _ts-imports-source-organization: 导入 ******************************************************************************** 在 ES6 和 TypeScript 中,导入语句共有四种变体: ======================================== ======================================== ======================================== 导入类型 示例 用途 ======================================== ======================================== ======================================== 模块 ``import * as foo from '...';`` TypeScript 导入方式 解构 ``import {SomeThing} from '...';`` TypeScript 导入方式 默认 ``import SomeThing from '...';`` 只用于外部代码的特殊需求 副作用 ``import '...';`` 只用于加载某些库的副作用(例如自定义元素) ======================================== ======================================== ======================================== .. code-block:: typescript // 应当这样做!从这两种变体中选择较合适的一种(见下文)。 import * as ng from '@angular/core'; import {Foo} from './foo'; // 只在有需要时使用默认导入。 import Button from 'Button'; // 有时导入某些库是为了其代码执行时的副作用。 import 'jasmine'; import '@polymer/paper-button'; .. _ts-module-versus-destructuring-imports: 选择模块导入还是解构导入? ================================================================================ 根据使用场景的不同,模块导入和解构导入分别有其各自的优势。 虽然模块导入语句中出现了通配符 ``*`` ,但模块导入并不能因此被视为其它语言中的通配符导入。相反地,模块导入语句为整个模块提供了一个名称,模块中的所有符号都通过这个名称进行访问,这为代码提供了更好的可读性,同时令模块中的所有符号可以进行自动补全。模块导入减少了导入语句的数量(模块中的所有符号都可以使用),降低了命名冲突的出现几率,同时还允许为被导入的模块提供一个简洁的名称。在从一个大型 API 中导入多个不同的符号时,模块导入语句尤其有用。 解构导入语句则为每一个被导入的符号提供一个局部的名称,这样在使用被导入的符号时,代码可以更简洁。对那些十分常用的符号,例如 Jasmine 的 ``describe`` 和 ``it`` 来说,这一点尤其有用。 .. code-block:: typescript // 不要这样做!无意义地使用命名空间中的名称使得导入语句过于冗长。 import {TableViewItem, TableViewHeader, TableViewRow, TableViewModel, TableViewRenderer} from './tableview'; let item: TableViewItem = ...; .. code-block:: typescript // 应当这样做!使用模块作为命名空间。 import * as tableview from './tableview'; let item: tableview.Item = ...; .. code-block:: typescript import * as testing from './testing'; // 所有的测试都只会重复地使用相同的三个函数。 // 如果只需要导入少数几个符号,而这些符号的使用频率又非常高的话, // 也可以考虑使用解构导入语句直接导入这几个符号(见下文)。 testing.describe('foo', () => { testing.it('bar', () => { testing.expect(...); testing.expect(...); }); }); .. code-block:: typescript // 这样做更好!为这几个常用的函数提供局部变量名。 import {describe, it, expect} from './testing'; describe('foo', () => { it('bar', () => { expect(...); expect(...); }); }); ... .. _ts-renaming-imports: 重命名导入 ================================================================================ 在代码中,应当通过使用模块导入或重命名导出解决命名冲突。此外,在需要时,也可以使用重命名导入(例如 ``import {SomeThing as SomeOtherThing}`` )。 在以下几种情况下,重命名导入可能较为有用: 1. 避免与其它导入的符号产生命名冲突。 2. 被导入符号的名称是自动生成的。 3. 被导入符号的名称不能清晰地描述其自身,需要通过重命名提高代码的可读性,如将 RxJS 的 ``from`` 函数重命名为 ``observableFrom`` 。 .. _ts-import-export-type: ``import type`` 和 ``export type`` ================================================================================ 不要使用 ``import type ... from`` 或者 ``export type ... from`` 。 .. tip:: 这一规则不适用于导出类型定义,如 ``export type Foo = ...;`` 。 .. code-block:: typescript // 不要这样做! import type {Foo} from './foo'; export type {Bar} from './bar'; 应当使用常规的导入语句。 .. code-block:: typescript // 应当这样做! import {Foo} from './foo'; export {Bar} from './bar'; TypeScript 的工具链会自动区分用作类型的符号和用作值的符号。对于类型引用,工具链不会生成运行时加载的代码。这样做的原因是为了提供更好的开发体验,否则在 ``import type`` 和 ``import`` 之间反复切换会非常繁琐。同时, ``import type`` 并不提供任何保证,因为代码仍然可以通过其它的途径导入同一个依赖。 如果需要在运行时加载代码以执行其副作用,应使用 ``import '...'`` ,参见 :ref:`ts-imports-source-organization` 一节。 使用 ``export type`` 似乎可以避免将某个用作值的符号导出为 API。然而,和 ``import type`` 类似, ``export type`` 也不提供任何保证,因为外部代码仍然可以通过其它途径导入。如果需要拆分对 API 作为值的使用和作为类型的使用,并保证二者不被混用的话,应当显式地将其拆分成不同的符号,例如 ``UserService`` 和 ``AjaxUserService`` ,这样不容易造成错误,同时能更好地表达设计思路。 .. _ts-organize-by-feature: 根据特征组织代码 ******************************************************************************** 应当根据特征而非类型组织代码。例如,一个在线商城的代码应当按照 ``products`` , ``checkout`` , ``backend`` 等分类,而不是 ``views`` , ``models`` , ``controllers`` 。