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. 导入
在 ES6
和 TypeScript
中,导入语句共有四种变体:
导入类型 | 示例 | 用途 |
---|---|---|
模块 | import * as foo from '...'; | TypeScript 导入方式 |
解构 | import {SomeThing} from '...'; | TypeScript 导入方式 |
默认 | import SomeThing from '...'; | 只用于外部代码的特殊需求 |
副作用 | import '...'; | 只用于加载某些库的副作用(例如自定义元素) |
4.3.1. 选择模块导入还是解构导入?
可选根据使用场景的不同,模块导入和解构导入分别有其各自的优势。
模块导入优势:
- 模块导入语句为整个模块提供了一个名称,模块中的所有符号都通过这个名称进行访问,这为代码提供了更好的可读性,同时令模块中的所有符号可以进行自动补全。
- 减少了导入语句的数量(模块中的所有符号都可以使用),降低了命名冲突的出现几率,同时还允许为被导入的模块提供一个简洁的名称。
解构导入优势:
- 为每一个被导入的符号提供一个局部的名称,这样在使用被导入的符号时,代码可以更简洁。
4.3.2. 重命名导入
在代码中,应当通过使用模块导入或重命名导出解决命名冲突。此外,在需要时,也可以使用重命名导入(例如 import {SomeThing as SomeOtherThing}
)。
在以下几种情况下,重命名导入可能较为有用:
- 避免与其它导入的符号产生命名冲突。
- 被导入符号的名称是自动生成的。
- 被导入符号的名称不能清晰地描述其自身,需要通过重命名提高代码的可读性,如将
RxJS
的from
函数重命名为observableFrom
。
4.3.3. import type
和 export 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';