這段時間寫了十幾個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。