如何将Angular文档化?

作者: cipchk | 来源:发表于2017-05-21 14:49 被阅读173次

    这段时间写了十几个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

    相关文章

      网友评论

        本文标题:如何将Angular文档化?

        本文链接:https://www.haomeiwen.com/subject/eaamxxtx.html