如何將Angular文檔化?

這段時間寫了十幾個Angular小組件,如何將代碼中的注釋轉(zhuǎn)換成漂亮的在線文檔一直都讓我有點頭疼;更別說在企業(yè)級解決方案里面,如果沒有良好的文檔對閱讀實在不敢想象。

下面我將介紹如何使用Dgeni生成你的Typescript文檔,當然,核心還是為了Angular。

什么是Dgeni?

Dgeni是Angular團隊開始的一個非常強大的NodeJS文檔生成工具,所以說,不光是Angular項目,也可以運用到所有適用TypeScript、AngularJS、Ionic、Protractor等項目中。

主要功能就是將源代碼中的注釋轉(zhuǎn)換成文檔文件,例如HTML文件。而且還提供多種插件、服務、處理器、HTML模板引擎等,來幫助我們生成文檔格式。

如果你之前的源代碼注釋都是在JSDoc形式編寫的話,那么,你完全可以使用Dgeni創(chuàng)建文檔。

那么,開始吧!

一、腳手項目

首先先使用angular cli創(chuàng)建一個項目,名也: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相關(guān)依賴包:

npm i gulp --save-dev

二、文件結(jié)構(gòu)

首先創(chuàng)建一個 docs/ 文件夾用于存放dgeni所有相關(guān)的配置信息,

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

config 下創(chuàng)建 index.js 配置文件,以及 processors 處理器和 templates 模板文件夾。

dist 下就是最后生成的結(jié)果。

三、配置文件

首先在 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、設(shè)置源文件和輸出路徑

.config(function(log, readFilesProcessor, writeFilesProcessor) {
    // 設(shè)置日志等級
    log.level = 'info';

    // 設(shè)置項目根目錄為基準路徑
    readFilesProcessor.basePath = sourceDir;
    readFilesProcessor.$enabled = false;

    // 指定輸出路徑
    writeFilesProcessor.outputFolder = outputDir;
})

2、設(shè)置Typescript解析器

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

3、設(shè)置模板引擎

.config(function(templateFinder, templateEngine) {
    // 指定模板文件路徑
    templateFinder.templateFolders = [path.resolve(__dirname, './templates')];
    // 設(shè)置文件類型與模板之間的匹配關(guān)系
    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 的流管道一樣,我們可以根據(jù)需要創(chuàng)建相應的處理器來對文檔對象進行修飾,從而達到模板引擎最終所需要的數(shù)據(jù)結(jié)構(gòu)。

雖說 dgeni-packages 已經(jīng)提供很多種便利使用的處理器,可文檔的展示總歸還是因人而異,所以如何自定義處理器非常重要。

處理器的結(jié)構(gòu)非常簡單:

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

最后,將處理器掛鉤至 dgeni 上。

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

1、過濾處理器

Dgeni 在調(diào)用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;
}

// 過濾內(nèi)部成員
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 對這類的歸納其實是很難滿足我們模板引擎所需要的數(shù)據(jù)結(jié)構(gòu)的,比如一個 @Input() 變量,默認的情況下 ts 解析器統(tǒng)一用一個 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);

        // 根據(jù)裝飾器重新修改方法與屬性
        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 解析后在程序中的表現(xiàn)是一個數(shù)組類似,每一個文檔都被當成一個數(shù)組元素。所以需要將這些文檔進行分組。

我這里采用跟源文件相同目錄結(jié)構(gòu)分法。

/** 數(shù)據(jù)結(jié)構(gòu)*/
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` 的目錄結(jié)構(gòu)
                let fileSep = path.relative(basePath, filePath).split(path.sep);
                let groupName = fileSep.slice(0, fileSep.length - 1).join('/');

                // 不存在時創(chuàng)建它
                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 知道如何去區(qū)分?因此,我們還需要按路徑輸出處理器配置:

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

五、模板引擎

dgeni-packages 提供 Nunjucks 模板引擎來渲染文檔。之前,我們就學過如何配置模板引擎所需要的模板文件目錄及標簽格式。

接下來,只需要創(chuàng)建這些模板文件即可,數(shù)據(jù)源就是文檔對象,之前花很多功夫去了解處理器;最核心的目的就是要將文檔對象轉(zhuǎn)換成更便利于模板引擎使用。而如何編寫 Nunjucks 模板不再贅述。

在編寫分組處理器時,強制文件類型 this.docType = 'componentGroup';;而在配置按路徑輸出處理器也指明這一層關(guān)系。

因此,需要創(chuàng)建一個文件名叫 componentGroup.template.html 模板文件做為開始,為什么必須是這樣的名稱,你可以回頭看模板引擎配置那一節(jié)。

而模板文件中所需要的數(shù)據(jù)結(jié)構(gòu)名叫 doc,因此,在模板引擎中使用 {$ doc.name $} 來表示分組處理器數(shù)據(jù)結(jié)構(gòu)中的 ComponentGroup.name。

六、結(jié)論

如果有人再說 React 里面可以非常方便生成注釋文檔,而 Angular 怎么這么差,我就不同意了。

Angular依然可以非常簡單的創(chuàng)建漂亮的文檔,當然市面也有非常好的文檔生成工具,例如:compodoc

如果你對文檔化有興趣,可以參考ngx-weui,算是我一個最完整的示例了。

最后,文章中所有源代碼見 Github

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,534評論 19 139
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 47,261評論 6 342
  • Angular CLI 是什么? Angular CLI 是一個命令行接口(Command Line Interf...
    semlinker閱讀 4,325評論 0 39
  • 282169 今天我看小說的時候覺得這段話非常引人深思?,F(xiàn)在有社會上有些人不就是這樣嗎?自己不勞動站別人的便宜還認...
    鹽木閱讀 242評論 1 0
  • 不同時代的美人,從古至今,美貌總是讓人有可以走捷徑的資本。或放縱自己,了了此生,如行尸走肉,奢靡已成為生活的情調(diào)。...
    許小粟閱讀 464評論 0 1

友情鏈接更多精彩內(nèi)容