本文已参与「新人创作礼」活动,一起开启掘金创作之路。
——Bug修复员-鲁宽宽
前言
系列二讲述了如何从零到一搭建简单实用的npm包,也是核心的实现。
接下来的两篇分别是【文档页面】与【验证页面】的搭建。
此篇主要讲如何搭建一款可配置、灵活的使用说明文档。
页面由VuePress来生成,它是以Markdown 为中心的项目结构,通过最少的配置来实现静态网站的构建。
由于以往的文档是由jsdocs通过分析注释一键完成,而VuePress是以md文件构成,故面临的困难点就是如何将注释生成md文件。
实现流程
思路为以下几个过程:
- 安装并配置VuePress
- 解析js注释
- 写入md文件
- 执行vuepress构建
- 部署服务器
安装配置
若是没有使用过VuePress,可以查看官方网站,上手很简单的。
在项目本地安装
npm install vuepress -D
此篇只讲vuepress的使用,建议参考源码,下图为整体目录结构。
// docs/.vuepress/config.js 配置
module.exports = {
base: '/web/js-utils/', // 部署路径
title: 'JS-utils',
description: 'js、utils', // 以meta标签显示
head: [
['link', { rel: 'icon', href: '/favicon.ico' }]
],
themeConfig: {
logo: 'https://pic5.zhuanstatic.com/zhuanzh/62a63713-6579-4f7e-9d24-b7e80a21b1cf.png',
nav: [
{ text: '主页', link: '/' },
{ text: 'API', link: '/main/' }, // 对应main文件夹下的md文件
{ text: '更新记录', link: '/updateLog/' },
{ text: '博主首页', link: 'https://www.jintingyo.com', target:'_blank' },
],
sidebar: 'auto',
smoothScroll: true
}
}
// package.json 新增
"scripts": {
"docs": "node build/docs/index.js", // 解析注释并写入md文件
"docs:dev": "vuepress dev docs", // 本地预览
"docs:build": "vuepress build docs", // 打包
},
上述配置完成后就可以执行VuePress命令进行构建,支持本地预览与生产环境打包。
执行npm run docs:dev,可以进行本地预览,说明配置没有问题,真香(vuepress的强大不止这些,可以自行摸索)。
接下来要做的就是如何将注释写入main/index.md文件。
解析js注释
主要是通过正则等匹配方式来分析注释,找到规则并输出。
在此之前先声明下: 由于使用的不是jsdoc,故不需要遵守其api属性,可以设计自己的命名规范。
如注释内容:
/**
* @name regIdCardCode
* @type 正则
* @description 校验中国身份证号(包含x)
* @version 0.0.22
* @param {String} code 必填项,身份证号
* @todo
* @returns {Boolean} true 或者 false
* @parameter 41072219940212543x
* @example
* import { regIdCardCode } from "@lu-kk/js-utils"
* const code = "41072219940212543x";
* regIdCardCode(code) // true or false
*/
相关字段说明:
@name : 方法名
@type :用于归类 正则
@description : 描述
@version : 版本
@param :入参
@todo : 额外记录
@returns : 返回值与callback互斥
@callback :回调函数
@parameter :便捷用例的参数
@example :例子
这样不需要依赖第三方包,可以做到快速响应、定制化。
解析注释的代码实现:
- node读取所有文件下的注释
// 所有文件的内的内容
let allFilesContent = ''
allFiles.forEach(item => {
const fileContent = fs.readFileSync(item, 'utf-8');
allFilesContent += fileContent
})
- 分析注释组合为对象
下面只是部分代码,主要就是通过正则等将字符串转为了数组对象。
/**
* 处理注释文件为数组返回;
* @param {String} originContent - 源文件
*/
const analysisNotes = (originContent) => {
let contentList = originContent.match(/\/\*\*[\s\S]+?\*\//g);
// console.log('contentList----', contentList)
let result = [];
contentList.forEach((item) => {
// 消除换行及/** */
let content = item.replace(/[\r\n]|\/\*\*|\*\//g, '');
// console.log('content------', content)
let lineData = content.split('* @');
lineData.shift()
// console.log('lineData---', lineData)
result.push(lineData)
})
return result;
};
/**
* 处理注释文件为数组返回;
* @param {String} originContent - 注释内容
*/
exports.formatContent = (originContent) => {
let notesList = analysisNotes(originContent);
// console.log('notesList---', notesList)
let result = [];
notesList.forEach((notes) => {
let notesMap = {}
let isDocTrue = false;
notes.forEach(note => {
try {
let noteLsit = note.split(' ')
let key = noteLsit[0]
let value = noteLsit.slice(1).join(' ')
if (key == 'example') {
let exampleContent = value.replace(/\*/g, "").trim();
value = beautify(exampleContent, {
indent_size: 2
});
}
// 处理param
if (key == 'param') {
let params = {
type: value.split(' ')[0].trim().replace(/\}|\{/g, ""),
key: value.split(' ')[1].trim(),
value: value.split(' ').slice(2).join(''),
}
// console.log('params-------', params)
if (Array.isArray(notesMap[key])) {
notesMap[key].push(params)
} else {
notesMap[key] = []
notesMap[key].push(params)
}
} else {
notesMap[key] = value
}
} catch {
isDocTrue = true;
console.log(`--------------------- \n#### error ####:请检查注释是否规范!\n#### data ####:${notes}; \n#### line ####:${note}; \n---------------------`)
}
})
if (!isDocTrue) {
result.push(notesMap)
}
})
return sortHandle(result, 'name') || [];
};
// 最终效果
let result = [{
name: '',
type: ''
...
}]
写入md文件
通过上一步获取到了一个数组对象,这一环节就是将数组写入md文件。也是比较简单的,其流程为:
- 设计模板
- 循环数组生成
- 写入文件
如下面我想要的效果图:
具体代码实现:
/**
* 写入markdown文件
* @param {String} writePath - 写入文件的路径
*/
exports.writeDocs = (writePath, contentList) => {
const dataSource = formatHandle(contentList)
// console.log(writePath, dataSource)
let mdText =
`# API集合`;
// 动态渲染每一个方法
dataSource.forEach(item => {
let content =
`
***
## ${item.type}
`;
item.data.forEach(data => {
content += `
***
### ${data.name}
`
content +=
`
* **功能**:
* ${data.description}
* **版本**:
* ${data.version}
* **参数**:
`
let table = `
| 参数 | 备注 | 类型 |
|------|------|------|------|
`;
data.param.forEach(item => {
table += `|${item.key || ''}|${item.value || ''}|${item.type}|
`;
content += table;
})
if (data.returns) {
content += `
* **返回值**:
* ${data.returns}
`;
}
let example = `
* **示例**:
`;
example += '\n```javascript\n'+ data.example + '\n```\n';
content += example;
let url = `https://jintingyo.com/h5/js-utils-test-page?sdkId=${data.name}`
let qrCode = `
* **扫描二维码测试**:
<img width="200px" height="200px" src="https://api.qrserver.com/v1/create-qr-code/?data=${encodeURIComponent(url)}"></img>\n`
content += qrCode;
})
mdText += content;
});
fs.writeFile(writePath, mdText, 'utf8', function (error) {
if (error) {
console.log('### 写入失败,请排查')
throw error;
}
});
}
是不是很简单,嘿嘿。
目前所有困难点已经被我们解决,可以执行VuePress的命令进行预览了~
若预览没有问题,接下来可以选择部署在自己的服务器上,结束。
结语
其实每一个实现逻辑都不是卡点,卡点往往来自于没有方向。
就如这个npm包为例,之前我很迷茫,不知道如何设计,且总想着一次就实现地特别完美。这样的想法也导致我无从下手。
后来,我改变了方向。首先确定目标:搭建一个npm包。那么实现目标需要什么?
npm账号、代码仓库、构建工具、使用文档、测试页面、服务器、Ts支持、eslint、单元测试、容错机制等等。
太多了,以我目前的能力是一时无法完成的,所以在目标不变的前提下,可以选择关键节点实现,先将主流程跑通,后期再慢慢的添砖加瓦,这是不冲突的。
希望此系列能带给大家一些收获,感谢阅读。
获取知识、沉淀总结、分享经验、收获快乐。