目前市场上已经提供了很多UI组件库,当我们使用一个UI组件库的时候,文档页面是最直接的获取信息的窗口,文档页一般包含了组件的描述、组件Demo、组件参数文档,此时就会有很多方法以及工具来提供这种场景的自动生成,节省开发成本以及维护成本。
相同的,对于一个自己开发的工具函数库,类比于lodash、monment,此时依旧需要生成说明文档,来对每一个方法进行解释说明,如果开发一个通用方法,就需要自己维护一个用法说明,这必然会增大开发成本,浪费时间,此时就需要一个工具对注释进行解析并生成md文档。
分析通过上面的描述梳理一下我们的需求:
- 工具函数书写
- 标准且简洁的注释说明
- 将统一化的注释说明转换成md文档
首先我们需要对工具函数的注释统一化,可以自行定义,定义的格式将影响plugins插件的代码实现,同时还要规定函数的书写方式,例如:函数表达式、函数声明式以及变量声明后包含多个函数表达式等多种形式,形式的不同,将会在解析AST语法树的时候进行不同的遍历解析。除此之外,此插件主要借助Babel转译器以及Rollup模块打包器来实现。
梳理一下整个流程,如下:
预备知识此技术方案主要涉及运用了Rollup、Babel、AST语法树三方面的知识。
RollupRollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码。Rollup的配置文件是可选的,但是使用配置文件的作用很强大,而且很方便,可以简化命令行操作。配置文件是一个ES6模块,它对外暴露一个对象,对象中包含Rollup需要的一些选项,例如常用的input output plugins等。这个配置文件通常位于项目根目录,叫做rollup.config.js。
webpack与Rollup对比名称 | 简介 | 优点 | 缺点 | 应用场景 |
webpack | 一种前端资源模块化管理和打包工具。它可以将许多松散的模块按照依赖和规则打包成符合生产环境部署的前端资源。还可以将按需加载的模块进行代码分割,等到实际需要的时候再异步加载 | 模块化、静态资源整合、公共代码拆分、异步加载、热更新等 | 1. 配置复杂; 2. 冗余代码较多; 3. 不支持输出ESM格式的bundle | 应用程序开发 |
rollup | 一个模块打包工具, 可以将我们按照 ESM规范编写的源码构建输出如下格式:1.iife: 自执行函数, 可通过 <script> 标签加载;2.amd: 通过 RequireJS 加载;3. cjs: Node 默认的模块规范, 可通过 Webpack 加载 4. umd: 兼容 IIFE, AMD, CJS 三种模块规范 5. esm: ES2015 Module 规范, 可用 Webpack, Rollup 加载 | 1. 基于ES6,支持动态导入、tree shaking 2. 可以将所有小文件打到一个bundle里,所有代码都在同一个函作用域中,不压缩混淆的情况下代码依旧可读 3. 冗余代码少,执行快 | 1. 不支持热更新(可以通过livereload插件实现)2. 对于commonjs模块,需要用rollup-plugin-commonjs插件读成ES6代码后再处理 3. umd和iife格式无法对公共代码进行拆分,因为自执行函数会把所有的模块都放到一个函数中,并没有像webpack一样有一些引导代码,所以没有办法做到代码拆分 | 框架、组件库、生成单一umd文件的场景 |
Babel是一个工具链,主要用于将采用ES6语法编写的代码转换为向后兼容的JavaScript预发,以便能够运行在当前和旧版本的浏览器河其他环境中。
配置文件配置文件的主要作用就是告诉babel要怎么去编译,编译哪些内容,配置文件的方式有如下几种:
- 在package.json中设置babel字段
{
"name":"babel-test",
"version":"1.0.0",
"devDependencies": {
"@babel/core":"^7.4.5",
"@babel/cli":"^7.4.4",
"@babel/preset-env":"^7.4.5"
}
"babel": {
"presets": ["@babel/preset-env"]
}
}
- .babelrc文件或.babelrc.js
这两种其实是同一种配置方式,只是文件格式不同,一个是json文件,一个是js文件,这两个配置文件是针对文件夹的,即该配置文件所在的文件夹包括子文件夹都会应用此配置文件的设置,而且下层配置文件会覆盖上层配置文件,通过此种方式可以给不同的目录设置不同的规则。
{ "presets": ["@babel/preset-env"] } // .babelrc
module.exports = { presets: ['@babel/preset-env'] }; // .baeblrc.js
- babel.config.js文件
babel.config.js是针对整个项目,一个项目只有一个放在项目根目录。
注意:.babelrc文件放置在项目根目录和babel.config.js效果一致,如果两种类型的配置文件都存在,.babelrc会覆盖babel.config.js的配置。
在Babel执行编译的过程中,会从项目的目录下读取配置文件。在配置文件中,主要是对预设(presets) 和 插件(plugins) 进行配置。
pluginsplugins属性告诉babel要使用哪些插件,这些插件可以控制如何转换代码,同时plugins是可以自定义的,根据特定场景需要的特定功能进行编写以及使用。此技术方案主要是通过自定义plugins插件最终实现注释转换md的功能
AST语法树抽象语法树(Abstract Syntax Tree)简称 AST,是源代码的抽象语法结构的树状表现形式。很多工具库的核心都是通过抽象语法树这个概念来实现对代码的检查、分析等操作。
AST节点类型对照表
实现配置rollup.config.js文件import {babel} from '@rollup/plugin-babel';
import clear from 'rollup-plugin-clear';
export default {
input: './demo/demo.js',
plugins:[
clear({
targets: ['./readme']
}),
babel({ babelHelpers: 'bundled' })
]
}
由于此demo只是用来实现转换md文档功能,所以rollup.config.js文件的配置是非常简化的。
配置babel.config.js文件module.exports = {
plugins: [["./plugins.js", {
gather: "readme"
}]]
}
定义md文档格式
# ${_funName}
> 工具函数说明:${_description}
${_otherMsg}
| 参数名 | 参数类型 | 参数说明 |
| ------ | -------- | ---- |
${_paramStr}
返回:${_returns}
实现plugins插件
plugins插件的实现是整个功能最核心的部分,babel通过plugins特定的功能去解析代码,并生成最终所需要的内容。
const visitor = {
name: _LastMdName,
pre() {
this.strPart = ''
},
visitor: {
FunctionDeclaration(path) {
var curNode = path.node
if (curNode.leadingComments && curNode.leadingComments.length && curNode.leadingComments.some(o => o.type === 'CommentBlock')) {
var tempName = curNode.id.name;
var tempCommentChunks = curNode.leadingComments[0].value.split('n')
if (!tempCommentChunks[0].includes("start")) return
this.strPart += buildReadMe(tempName, tempCommentChunks) + 'n'
}
},
VariableDeclaration(path) {
var varNode = path.node;
if(varNode.declarations && varNode.declarations[0].init && varNode.declarations[0].init.properties){
let curNode = varNode.declarations[0].init.properties;
for(let i = 0; i < curNode.length; i++) {
if(curNode[i].leadingComments && curNode[i].leadingComments.length && curNode[i].leadingComments.some(o => o.type === 'CommentBlock')){
var tempName = curNode[i].key.name;
var tempCommentChunks = curNode[i].leadingComments[0].value.split('n')
if (!tempCommentChunks[0].includes("start")) return
this.strPart += buildReadMe(tempName, tempCommentChunks) + 'n'
}
}
}
}
},
post(state) {
console.log("state.opts",state.opts)
let fileName = ''
let opts = {}
if(!state.opts.generatorOpts){ // 直接执行label
fileName = state.opts.sourceMapTarget.split('.')[0]
opts = state.opts.plugins.find(p => p[0].key === _LastMdName)[1] || {}
} else {
fileName = state.opts.generatorOpts.sourceFileName.split('.')[0]
opts = state.opts.plugins.find(p => p.key === _LastMdName).options
console.log("opts",opts)
}
opts._LastFileName && (_LastFileName = opts._LastFileName)
if(opts.gather){
_allMdCache+=this.strPart
saveMdFile(opts.gather, _allMdCache)
}else {
this.strPart.length && saveMdFile(fileName, this.strPart)
}
}
}
从上面代码中可以看出,Babel插件遵循这样的一个顺序,pre -> visitor -> post,pre和post是标准的方法,visitor是一个对象,pre和post的入参只有state,所以这两个方法是无法改变AST语法结构的,pre和post的用法比较单一,一般在pre中创建一些临时对象,post中再将这些对象销毁掉。
在写babel插件的时候,最重要的就是定义一个visitor对象,这是因为babel插件遵循访问者(visitor)模式。
访问者模式简单来说,访问者模式是能把处理方法从数据结构中分离出来的一种模式,这种模式的好处就是可以根据需求增加新的处理方法,且不用修改原来的程序代码与数据结构。
例子:由于AST结构基本上是固定的,但是插件是多变的,特别的还有一些定制化的需求。在这种情况下,对于AST的操作就是一定要独立出去,这就是所谓的处理方式从数据结构中抽离。
访问者模式有两个比较重要的对象,访问者以及被访问者。可以简单认为,每个插件都是访问者,被访问的对象则是AST节点。
在有了以上重要的操作之后,其余的操作都是对文档的定义、逻辑的梳理以及实现。
输出接口在package.json中定义一个npm指令,例如:npm run build:md ,去执行rollup文件即可自动打包声称md文档,文档最终输出:
# buildTree
> 工具函数说明: 通过数组建造树形结构
| 参数名 | 参数类型 | 参数说明 |
| ------ | -------- | ---- |
|array|数组|数组|
|id_key|字符串|树形ID键名|
|parentId_key|字符串|树形父级ID键名|
返回: {array} 树形结构
# copyText
> 工具函数说明: 复制文本
| 参数名 | 参数类型 | 参数说明 |
| ------ | -------- | ---- |
|id|字符串|复制节点的id|
返回: {string} 需要复制的文本内容
总结
由于AST语法结构是固定的,但是对于不同的工具函数库,可能会产生不同的定义方式,本次实现的功能中包含了对函数声明式以及对象声明式函数的遍历,在其他场景中,还会出现不同的定义形式,解决方式即为按照AST语法树的固定定义形式去遍历查找即可。
源码github地址:github.com/mfnn/JsToMd
转自:https://juejin.cn/post/7061995134686068767
最新评论