這段時間寫了十幾個Angular小組件,如何將代碼中的註釋轉換成漂亮的在線文檔一直都讓我有點頭疼;更別說在企業級解決方案裏面,若是沒有良好的文檔對閱讀實在不敢想象。javascript
下面我將介紹如何使用Dgeni生成你的Typescript文檔,固然,核心仍是爲了Angular。html
Dgeni是Angular團隊開始的一個很是強大的NodeJS文檔生成工具,因此說,不光是Angular項目,也能夠運用到全部適用TypeScript、AngularJS、Ionic、Protractor等項目中。java
主要功能就是將源代碼中的註釋轉換成文檔文件,例如HTML文件。並且還提供多種插件、服務、處理器、HTML模板引擎等,來幫助咱們生成文檔格式。git
若是你以前的源代碼註釋都是在JSDoc形式編寫的話,那麼,你徹底可使用Dgeni建立文檔。github
那麼,開始吧!typescript
首先先使用angular cli建立一個項目,名也:ngx-dgeni-start。npm
ng new ngx-dgeni-start
接着還須要幾個Npm包:json
npm i dgeni dgeni-packages lodash --save-dev
dgeni 須要gulp來啓用,因此,還須要gulp相關依賴包:gulp
npm i gulp --save-dev
首先建立一個 docs/
文件夾用於存放dgeni全部相關的配置信息,api
├── 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如何生成咱們的文檔。
.config(function(log, readFilesProcessor, writeFilesProcessor) { // 設置日誌等級 log.level = 'info'; // 設置項目根目錄爲基準路徑 readFilesProcessor.basePath = sourceDir; readFilesProcessor.$enabled = false; // 指定輸出路徑 writeFilesProcessor.outputFolder = outputDir; })
.config(function(readTypeScriptModules) { // ts文件基準文件夾 readTypeScriptModules.basePath = sourceDir; // 隱藏private變量 readTypeScriptModules.hidePrivateMembers = true; // typescript 入口 readTypeScriptModules.sourceFiles = [ 'app/**/*.{component,directive,service}.ts' ]; })
.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'))
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; }
雖然 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; } } }
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。