跳到主要内容

4. 代码管理

4.1. 模块

项目上路径模块分为以下几类:

  • hzero-front 路径前缀
    • components/*
    • utils/*
    • services/*
  • 公共依赖包 hzero-front-apaas 路径前缀
    • @apaas/*
  • 子包 hzero-front-hlod 路径前缀
    • @hlod/*
  • 子包 hzero-front-hmde 路径前缀
    • @hmde/*

4.1.1. 导入路径

必须

在引用逻辑上属于同一项目的文件时,应使用相对路径 ./foo ,不要使用绝对路径 @/to/foo

应当

应尽可能地限制父层级的数量(避免出现诸如 ../../../ 的路径),过多的层级会导致模块和路径结构难以理解。

4.1.2. 导入顺序

必须

在路径导入顺序上,从上到下拆分为三层,依次书写,每层之间用换行符分隔:

  • 第一层:从 node_modules 包或者跨子包导入,比如 import { Button } from 'choerodon-ui/pro';import useDataSetEvents from 'hzero-front-apaas/lib/hooks/useDataSetEvents';
  • 第二层:从项目当前所处模块的全局别名配置中导入,比如 import { lowcodeRequest } from '@/utils/lowcodeRequest'
  • 第三层:从当前文件附近路径文件导入,比如 import styles from './index.less'

4.1.3. 用 namespace 还是 module

TypeScript 有两种组织代码的方式:命名空间(namespace)和模块(module)。

禁止

不允许使用 namespace,在 TypeScript 中必须使用模块(即 ES6 模块 )。也就是说,在引用其它文件中的代码时必须以 import {foo} from 'bar' 的形式进行导入和导出。

禁止

不允许使用 namespace Foo { ... } 的形式组织代码。namespace 只能在所用的外部第三方库有要求时才能使用。如果需要在语义上对代码划分命名空间,应当通过分成不同文件的方式实现。

禁止

不允许在导入时使用 require 关键字(形如 import x = require('...'); )。应当使用 ES6 的模块语法。

// 不要这样做!不要使用命名空间!
namespace Rocket {
function launch() {
}
}

// 不要这样做!不要使用 <reference> !
/// <reference path="..."/>

// 不要这样做!不要使用 require() !
import x = require('mydep');
提示

区别:

  • 命名空间是位于全局命名空间下的一个普通的带有名字的 JavaScript 对象,使用起来十分容易。但就像其它的全局命名空间污染一样,它很难去识别组件之间的依赖关系,尤其是在大型的应用中。
  • 像命名空间一样,模块可以包含代码和声明。不同的是模块可以声明它的依赖。
  • 在正常的 TypeScript 项目开发过程中并不建议用命名空间,但通常在通过 d.ts 文件标记 js 库类型的时候使用命名空间,主要作用是给编译器编写代码的时候参考使用。

4.2. 导出

可选

如果可能,尽量的使用具名的导出声明,减少默认导出的使用。因为这样能够保证所有的导入语句都遵循统一的范式:

// 使用具名导出
export class Foo {
}

// 使用
import {Foo} from './foo';
// 使用默认导出
export default class Foo {
}
// 默认导出会造成如下的弊端
import Foo from './foo'; // 这个语句是合法的。
import Bar from './foo'; // 这个语句也是合法的。

4.2.1. 可变导出

禁止

虽然技术上可以实现,但是可变导出会造成难以理解和调试的代码,尤其是对于在多个模块中经过了多次重新导出的符号。这条规则的一个例子是,不允许使用 export let

// 不要这样做!
export let foo = 3;
// 在纯 ES6 环境中,变量 foo 是一个可变值,导入了 foo 的代码会观察到它的值在一秒钟之后发生了改变。
// 在 TypeScript 中,如果 foo 被另一个文件重新导出了,导入该文件的代码则不会观察到变化。
window.setTimeout(() => {
foo = 4;
}, 1000 /* ms */);

如果确实需要允许外部代码对可变值进行访问,应当提供一个显式的取值器。

// 应当这样做!
let foo = 3;
window.setTimeout(() => {
foo = 4;
}, 1000 /* ms */);

// 使用显式的取值器对可变导出进行访问。
export function getFoo() {
return foo;
};

4.2.2. 容器类

不应

不要为了实现命名空间创建含有静态方法或属性的容器类。

// 不要这样做!
export class Container {
static FOO = 1;

static bar() {
return 1;
}
}
应当

应当将这些方法和属性设为单独导出的常数和函数。

// 应当这样做!
export const FOO = 1;

export function bar() {
return 1;
}

4.3. 导入

ES6TypeScript 中,导入语句共有四种变体:

导入类型示例用途
模块import * as foo from '...';TypeScript 导入方式
解构import {SomeThing} from '...';TypeScript 导入方式
默认import SomeThing from '...';只用于外部代码的特殊需求
副作用import '...';只用于加载某些库的副作用(例如自定义元素)

4.3.1. 选择模块导入还是解构导入?

可选

根据使用场景的不同,模块导入和解构导入分别有其各自的优势。

模块导入优势:

  • 模块导入语句为整个模块提供了一个名称,模块中的所有符号都通过这个名称进行访问,这为代码提供了更好的可读性,同时令模块中的所有符号可以进行自动补全。
  • 减少了导入语句的数量(模块中的所有符号都可以使用),降低了命名冲突的出现几率,同时还允许为被导入的模块提供一个简洁的名称。

解构导入优势:

  • 为每一个被导入的符号提供一个局部的名称,这样在使用被导入的符号时,代码可以更简洁。

4.3.2. 重命名导入

在代码中,应当通过使用模块导入或重命名导出解决命名冲突。此外,在需要时,也可以使用重命名导入(例如 import {SomeThing as SomeOtherThing} )。

在以下几种情况下,重命名导入可能较为有用:

  1. 避免与其它导入的符号产生命名冲突。
  2. 被导入符号的名称是自动生成的。
  3. 被导入符号的名称不能清晰地描述其自身,需要通过重命名提高代码的可读性,如将 RxJSfrom 函数重命名为 observableFrom

4.3.3. import typeexport type

禁止

不要使用 import type ... from 或者 export type ... from

警告

这一规则不适用于导出类型定义,如 export type Foo = ...;

// 不要这样做!
import type {Foo} from './foo';

export type {Bar} from './bar';
必须

使用常规的导入语句。

// 应当这样做!
import {Foo} from './foo';

export {Bar} from './bar';
此篇维护者:黄振敏