如何将Angular文档化?

这段时间写了十几个Angular小组件,如何将代码中的注释转换成漂亮的在线文档一直都让我有点头疼;更别说在企业级解决方案里面,如果没有良好的文档对阅读实在不敢想象。

下面我将介绍如何使用Dgeni生成你的Typescript文档,当然,核心还是为了Angular。

什么是Dgeni?

Dgeni是Angular团队开始的一个非常强大的NodeJS文档生成工具,所以说,不光是Angular项目,也可以运用到所有适用TypeScript、AngularJS、Ionic、Protractor等项目中。

主要功能就是将源代码中的注释转换成文档文件,例如HTML文件。而且还提供多种插件、服务、处理器、HTML模板引擎等,来帮助我们生成文档格式。

如果你之前的源代码注释都是在JSDoc形式编写的话,那么,你完全可以使用Dgeni创建文档。

那么,开始吧!

一、脚手项目

首先先使用angular cli创建一个项目,名也:ngx-dgeni-start

ng new ngx-dgeni-start

接着还需要几个Npm包:

  • Dgeni 文档生成器。
  • Dgeni Packages 源代码生成文档的dgeni软件包。
  • Lodash Javascript工具库。
npm i dgeni dgeni-packages lodash --save-dev

dgeni 需要gulp来启用,所以,还需要gulp相关依赖包:

npm i gulp --save-dev

二、文件结构

首先创建一个 docs/ 文件夹用于存放dgeni所有相关的配置信息,

├── docs/
│   ├── config/
│   │  ├── processors/
│   │  ├── templates/
│   │  ├── index.js
│   ├── dist/

config 下创建 index.js 配置文件,以及 processors 处理器和 templates 模板文件夹。

dist 下就是最后生成的结果。

三、配置文件

首先在 index.js 配置Dgeni。

const Dgeni = require('dgeni');
const DgeniPackage = Dgeni.Package;

let apiDocsPackage = new DgeniPackage('ngx-dgeni-start-docs', [
    require('dgeni-packages/jsdoc'), // jsdoc处理器
    require('dgeni-packages/nunjucks'), // HTML模板引擎
    require('dgeni-packages/typescript') // typescript包
])

先加载 Dgeni 所需要的包依赖。下一步,需要通过配置来告知dgeni如何生成我们的文档。

1、设置源文件和输出路径

.config(function(log, readFilesProcessor, writeFilesProcessor) {
    // 设置日志等级
    log.level = 'info';

    // 设置项目根目录为基准路径
    readFilesProcessor.basePath = sourceDir;
    readFilesProcessor.$enabled = false;

    // 指定输出路径
    writeFilesProcessor.outputFolder = outputDir;
})

2、设置Typescript解析器

.config(function(readTypeScriptModules) {
    // ts文件基准文件夹
    readTypeScriptModules.basePath = sourceDir;
    // 隐藏private变量
    readTypeScriptModules.hidePrivateMembers = true;
    // typescript 入口
    readTypeScriptModules.sourceFiles = [
        'app/**/*.{component,directive,service}.ts'
    ];
})

3、设置模板引擎

.config(function(templateFinder, templateEngine) {
    // 指定模板文件路径
    templateFinder.templateFolders = [path.resolve(__dirname, './templates')];
    // 设置文件类型与模板之间的匹配关系
    templateFinder.templatePatterns = [
        '${ doc.template }',
        '${ doc.id }.${ doc.docType }.template.html',
        '${ doc.id }.template.html',
        '${ doc.docType }.template.html',
        '${ doc.id }.${ doc.docType }.template.js',
        '${ doc.id }.template.js',
        '${ doc.docType }.template.js',
        '${ doc.id }.${ doc.docType }.template.json',
        '${ doc.id }.template.json',
        '${ doc.docType }.template.json',
        'common.template.html'
    ];
    // Nunjucks模板引擎,默认的标识会与Angular冲突
    templateEngine.config.tags = {
        variableStart: '{$',
        variableEnd: '$}'
    };
})

以上是Dgeni配置信息,而接下来重点是如何对文档进行解析。

四、处理器

Dgeni 通过一种类似 Gulp 的流管道一样,我们可以根据需要创建相应的处理器来对文档对象进行修饰,从而达到模板引擎最终所需要的数据结构。

虽说 dgeni-packages 已经提供很多种便利使用的处理器,可文档的展示总归还是因人而异,所以如何自定义处理器非常重要。

处理器的结构非常简单:

module.exports = function linkInheritedDocs() {
    return {
        // 指定运行之前处理器
        $runBefore: ['categorizer'],
        // 指定运行之后处理器
        $runAfter: ['readTypeScriptModules'],
        // 处理器函数
        $process: docs => docs.filter(doc => isPublicDoc(doc))
    };
};

最后,将处理器挂钩至 dgeni 上。

new DgeniPackage('ngx-dgeni-start-docs', []).processor(require('./processors/link-inherited-docs'))

1、过滤处理器

Dgeni 在调用Typescript解析 ts 文件后所得到的文档对象,包含着所有类型(不管私有、还是NgOninit之类的生命周期事件)。因此,适当过滤一些不必要显示的文档类型非常重要。

const INTERNAL_METHODS = [
    'ngOnInit',
    'ngOnChanges'
]

module.exports = function docsPrivateFilter() {
    return {
        $runBefore: ['componentGrouper'],
        $process: docs => docs.filter(doc => isPublicDoc(doc))
    };
};

function isPublicDoc(doc) {
    if (hasDocsPrivateTag(doc)) {
        return false;
    } else if (doc.docType === 'member') {
        return !isInternalMember(doc);
    } else if (doc.docType === 'class') {
        doc.members = doc.members.filter(memberDoc => isPublicDoc(memberDoc));
    }

    return true;
}

// 过滤内部成员
function isInternalMember(memberDoc) {
    return INTERNAL_METHODS.includes(memberDoc.name)
}

// 过滤 docs-private 标记
function hasDocsPrivateTag(doc) {
    let tags = doc.tags && doc.tags.tags;
    return tags ? tags.find(d => d.tagName == 'docs-private') : false;
}

2、分类处理器

虽然 Angular 是 Typescript 文件,但相对于 ts 而言本身对装饰器的依赖非常重,而默认 typescript 对这类的归纳其实是很难满足我们模板引擎所需要的数据结构的,比如一个 @Input() 变量,默认的情况下 ts 解析器统一用一个 tags 变量来表示,这对模板引擎来说太难于驾驭。

所以,对文档的分类是很必须的。

/**
 * 对文档对象增加一些 `isMethod`、`isDirective` 等属性
 *
 * isMethod     | 是否类方法
 * isDirective  | 是否@Directive类
 * isComponent  | 是否@Component类
 * isService    | 是否@Injectable类
 * isNgModule   | 是否NgModule类
 */
module.exports = function categorizer() {
    return {
        $runBefore: ['docs-processed'],
        $process: function(docs) {
            docs.filter(doc => ~['class'].indexOf(doc.docType)).forEach(doc => decorateClassDoc(doc));
        }
    };
    
    /** 识别Component、Directive等 */
    function decorateClassDoc(classDoc) {
        // 将所有方法与属性写入doc中(包括继承)
        classDoc.methods = resolveMethods(classDoc);
        classDoc.properties = resolveProperties(classDoc);

        // 根据装饰器重新修改方法与属性
        classDoc.methods.forEach(doc => decorateMethodDoc(doc));
        classDoc.properties.forEach(doc => decoratePropertyDoc(doc));
        
        const component = isComponent(classDoc);
        const directive = isDirective(classDoc);
        if (component || directive) {
            classDoc.exportAs = getMetadataProperty(classDoc, 'exportAs');
            classDoc.selectors = getDirectiveSelectors(classDoc);
        }
        classDoc.isComponent = component;
        classDoc.isDirective = directive;
        
        if (isService(classDoc)) {
            classDoc.isService = true;
        } else if (isNgModule(classDoc)) {
            classDoc.isNgModule = true;
        }
    }
}

3、分组处理器

ts 解析后在程序中的表现是一个数组类似,每一个文档都被当成一个数组元素。所以需要将这些文档进行分组。

我这里采用跟源文件相同目录结构分法。

/** 数据结构*/
class ComponentGroup {
    constructor(name) {
        this.name = name;
        this.id = `component-group-${name}`;
        this.aliases = [];
        this.docType = 'componentGroup';
        this.components = [];
        this.directives = [];
        this.services = [];
        this.additionalClasses = [];
        this.typeClasses = [];
        this.interfaceClasses = [];
        this.ngModule = null;
    }
}

module.exports = function componentGrouper() {
    return {
        $runBefore: ['docs-processed'],
        $process: function(docs) {
            let groups = new Map();

            docs.forEach(doc => {
                let basePath = doc.fileInfo.basePath;
                let filePath = doc.fileInfo.filePath;

                // 保持 `/src/app` 的目录结构
                let fileSep = path.relative(basePath, filePath).split(path.sep);
                let groupName = fileSep.slice(0, fileSep.length - 1).join('/');

                // 不存在时创建它
                let group;
                if (groups.has(groupName)) {
                    group = groups.get(groupName);
                } else {
                    group = new ComponentGroup(groupName);
                    groups.set(groupName, group);
                }

                if (doc.isComponent) {
                    group.components.push(doc);
                } else if (doc.isDirective) {
                    group.directives.push(doc);
                } else if (doc.isService) {
                    group.services.push(doc);
                } else if (doc.isNgModule) {
                    group.ngModule = doc;
                } else if (doc.docType === 'class') {
                    group.additionalClasses.push(doc);
                } else if (doc.docType === 'interface') {
                    group.interfaceClasses.push(doc);
                } else if (doc.docType === 'type') {
                    group.typeClasses.push(doc);
                }
            });

            return Array.from(groups.values());
        }
    };
};

但,这样还是无法让 Dgeni 知道如何去区分?因此,我们还需要按路径输出处理器配置:

.config(function(computePathsProcessor) {
    computePathsProcessor.pathTemplates = [{
        docTypes: ['componentGroup'],
        pathTemplate: '${name}',
        outputPathTemplate: '${name}.html',
    }];
})

五、模板引擎

dgeni-packages 提供 Nunjucks 模板引擎来渲染文档。之前,我们就学过如何配置模板引擎所需要的模板文件目录及标签格式。

接下来,只需要创建这些模板文件即可,数据源就是文档对象,之前花很多功夫去了解处理器;最核心的目的就是要将文档对象转换成更便利于模板引擎使用。而如何编写 Nunjucks 模板不再赘述。

在编写分组处理器时,强制文件类型 this.docType = 'componentGroup';;而在配置按路径输出处理器也指明这一层关系。

因此,需要创建一个文件名叫 componentGroup.template.html 模板文件做为开始,为什么必须是这样的名称,你可以回头看模板引擎配置那一节。

而模板文件中所需要的数据结构名叫 doc,因此,在模板引擎中使用 {$ doc.name $} 来表示分组处理器数据结构中的 ComponentGroup.name

六、结论

如果有人再说 React 里面可以非常方便生成注释文档,而 Angular 怎么这么差,我就不同意了。

Angular依然可以非常简单的创建漂亮的文档,当然市面也有非常好的文档生成工具,例如:compodoc

如果你对文档化有兴趣,可以参考ngx-weui,算是我一个最完整的示例了。

最后,文章中所有源代码见 Github

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,724评论 6 342
  • Angular CLI 是什么? Angular CLI 是一个命令行接口(Command Line Interf...
    semlinker阅读 4,170评论 0 39
  • 282169 今天我看小说的时候觉得这段话非常引人深思。现在有社会上有些人不就是这样吗?自己不劳动站别人的便宜还认...
    盐木阅读 218评论 1 0
  • 不同时代的美人,从古至今,美貌总是让人有可以走捷径的资本。或放纵自己,了了此生,如行尸走肉,奢靡已成为生活的情调。...
    许小粟阅读 406评论 0 1