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