写在前面
G2 4.0 的 API 文档将全部通过代码注释生成,以保证代码和文档的同步更新,降低维护成本。
采用方案
TypeDoc + typedoc-plugin-markdown,API 文档通过 markdown 文本格式展示。
具体的工程配置详见:https://github.com/antvis/g2/pull/1569/files
规范
因为 TypeDoc 会自动根据 ts 定义做很多的自动工作,所以在在注释时,我们就可以更多得关注于解释本身,不需要过多得使用各种标记。为了保证注释风格的统一,请按照以下注释风格编写代码注释,同时需要保证注释尽可能详尽。
🙎要求:
**
- 代码中的类型定义要完整
- 需要给每个参数提供解释说明,如下所示:
/**
* 改变图表大小
* @param width 宽度值
* @param height 高度值
*/
public changeSize(width: number, height: number) {
this.width = width;
this.height = height;
// 重新渲染
this.render();
}
下面整理一下常用的注释场景以及对应 TypeDoc 的用法:
- 除一些方法内的注释外,所有注释必须以
/**
开头
/** The sweet and fleshy product of a tree or other plant. */
class Fruit {
}
- 在注释中写代码示例
/**
* Codeblocks are great for examples
*
*
Highlight JS will autodetect the language - ```
- ```typescript
- // Or you can specify the language explicitly
- const instance = new MyClass();
*/
export class MyClass {}
Tags:支持的标签见:http://typedoc.org/guides/doccomments/
类和接口注释,对于每个类,需要在类开始定义的地方,注释该类的用途,同时可以提供该类使用的代码示例,展示用法。
注意:如果是面向接口编程,那么你只需要在接口上进行注释即可,具体的实现类不需要注释,默认 typedoc 会为你生成。
/**
* A shape is the form of an object or its external boundary, outline
* @param U The unit used to measure the properties of this type of shape
*/
interface Shape<U extends Unit > {
area():number;
}
interface Unit {}
interface Centimeter extends Unit {}
/**
* The Shape implementation used in Foo component to represent Bar
*
* ```typescript
* const myCustomRectangle = new MyCustomRectangle();
*
/
class MyCustomRectangle implements Shape
5. 属性以及变量
- 对于类型中的每一个属性都要提供详细的注释
```typescript
interface GameUnit {
/** unique id of this unit in the board */
id: string
/**
* Status of this unit in the board
*
*/
status: {
/** must be between 0 and 1 */
health: number,
/** the ids of units killed by this unit */
kills: number[]
}
}
enum GameState {
/** game started but user paused it */
paused,
/** victory condition reached */
ended,
/** game is being played right now */
playing,
/** game didn't started yet - player is choosing initial race? */
notStarted,
}
class Game {
/**
* State of the game at this moment
*/
state: GameState
config: GameConfiguration
private _currentTime: Date
/** the actual current time elapsed since this game was started not counting when it was paused */
get currentTime():Date { return this._currentTime; }
set currentTime(value:Date) { this._currentTime = value; }
/** default board with and height if none is provided */
static DEFAULT_STATE = GameState.notStarted
}
declare type GameConfiguration = {
/** number of columns the board haves */
boardWith?: number
/** number of rows the board haves */
boardHeight?: number
/** board fog of war initial configuration */
fogOfWar?: string[]
}
/** the global game singleton */
export const gameContainer = new GameContainer()
- 方法和函数
/**
* @param T the type of access this file has on IO
*/
class File<T extends FileAccess> {
public constructor(fs:number) {} // constructor, no docs
/**
* Creates a new file in given path and given content
* @param path absolute path of the file to create
* @param content content of the new file or empty file if nothing given
* @param T the type of access created file will have on IO
* @return a new description of the access type the new file has
*/
public static create<T extends FileAccess>(path: string, content?: string | Stream,
permissions: string = '666'): T {
return null;
}
/**
* Internal method used by foobar
* @param interval how often file is read in the polling
* @param predicate polling will end when true
*/
private poll(interval:number, predicate: (t: T) => boolean):void {}
}
interface FileEmitter<T> extends EventEmitter {
/** registers the listener to be notified **before** a file is about to change. The change will be hold until all listeners returned promises are resolved. If any listener reject the promise the file modification action will be canceled. */
on(event: 'before-file-modification', listener: (f: File<T>) => Promise<boolean>): this
on(event: 'after-file-modification', listener: (f: File<T>, previousContent: Buffer) => void): this
}
/**
* List children of given folder
* @param FAT target files access type
* @param options.force force read operation even if files are busy
* @param options.recursive list all files recursively
* @return if given path points to a folder returns a list of direct children Files,. Returns null otherwise
*/
function listFiles<FAT extends FileAccess>(path: string,
options?: {force: boolean, recursive: boolean})
: File<FAT>[] | undefined { return [] }
- 事件使用
@event
标签
class Readable2 extends EventEmitter {
/**
* Emitted whenever the stream is relinquishing ownership of a chunk of data to a consumer.
* @event
*/
static EVENT_DATA:'data' = 'data';
/**
* Typically, this may occur if the underlying stream is unable to generate data due to an underlying internal failure
* @event
*/
static EVENT_ERROR:'error' = 'error';
/**
* Typically, this may occur if the underlying stream is unable to generate data due to an underlying internal failure
* @event
*/
addListener(event: typeof Readable2.EVENT_DATA, listener: (chunk: Buffer | string) => void): this;
addListener(event: typeof Readable2.EVENT_ERROR, listener: (error: Error) => void): this;
addListener(event: string , listener: any): this {return this; }
}
- 引用,当在注释中需要引用其他接口时,可以使用
[[]]
如下所示:
interface Car {
/**
* Once started the engine will turn off only when the
* car travel all the [[Route.size]]
*/
engine:Engine
size: number
/**
* Put in march this cart by turning on its [[engine]]
*/
start()
}
interface Route {
size:number[][]
}
interface Engine{}
@hidden
/@ignore
不想该段注释出现在 tydoc 的输出结果中,使用该标识符。最佳实践:将@hidden
放置在注释的最前面。
interface SomeThingsAreIgnored {
/**
* @hidden
* this comment should be ignored
*
*/
method1():Promise<void>;
/**
* this comment shouldn't be ignored
*/
method2():string;
}
/** @hidden */
export class ClassTotallyIgnored {
color:string;
}
最后
相对来说 TypeDoc 的注释比较简单的,只要做到两点:
- 类型定义充分
- 提供变量描述
另外可以在 code 编辑器中安装 Comments in Typescript 插件,提高效率。
在项目下运行:
npm run doc
就会在 docs/api
目录下生成 api 文档。