前言
前言
相信现在大部分前端同学都听说过并用过 通天塔
,其官网自我介绍如下: Babel 是一个 JavaScript 编译器
.在开发中,我们最常使用它来做 ES6+
语法方向 ES5
还会配合打包工具做压缩、混淆等工作,其工作过程可以简单概括为三个步骤:
- 解析:将代码转换为抽象语法树(
ast
) - 兑换:
通天塔
或相关插件ast
执行操作转换 - 生成:使用新的抽象语法树重新生成代码
从以上三个步骤可以看出,它使用抽象语法树来贯穿整个过程。基于抽象语法树,我们很容易做一些静态分析。本文引用了我在实际工作中遇到的三个例子,并使用 通天塔
解决这三个实际问题。在我们开始之前,有必要介绍一下 通天塔
相关概念和 API
.
相关概念
我们先介绍几个会用到的包:
@babel/cli
:通天塔
用于从命令行编译文件的命令行客户端。@babel/解析器
: 将代码解析为ast
@babel/遍历
: 遍历ast
@babel/模板
: 转换中ast
在此过程中,需要更换节点。这个库可以用静态声明代替,下面会提到。@babel/发电机
:将要ast
生成为代码@babel/类型
: 用于生成节点或检查节点类型的工具包
介绍完会用到的包,再介绍一些 通天塔
插件概念。插件返回一个函数,在这个函数中, 通天塔
帮助我们穿越 ast
,我们使用访问者模式为每个节点类型定义处理函数。如下:
导出默认函数(){
返回 {
访客:{
标识符:{
输入:(路径){
},
退出:(路径){
}
},
},
};
}
复制代码
标识符
是一种节点,它可以是一个对象 进入
属性和 出口
属性。 ast
是一个树形结构, 进入
是从根节点到子节点的方向遍历时触发的回调函数。 出口
是子节点回溯到根节点时触发的回调函数。插件开发完成后,可以在项目根目录下下载 .babelrc
该插件在文件中被引用。
{
“插件”:[“./src/plugins/autoBindThis”]
}
复制代码
以上是对本文将用到的一些包和一些概念的简单介绍。更详细的文档内容可以移到以下两个链接:
babel中文网站 : 通天塔
各种相关概念和教程
babel-plugin-handbook : 插件开发的相关教程和思路
那么废话不多说,让我们进入实战吧。
自动绑定这个
做过 反应
开发的同学应该知道,我们有时会这样写代码:
类比较{
构造函数(){
这个。函数 = 这个。功能绑定(这个)
}
函数(){
这个。设置状态({
//xxx
})
}
使成为() {
return < 子函数 = {this.func}/ >
}
}
复制代码
因为 功能
该函数在子组件中调用,如果 功能
在函数中有 这个
相关操作,然后 这个
最后一点将是不准确的。之前常用的写法是在构造函数中使用。 绑定
显式绑定一次。然后了解之后 通天塔
之后,您可以使用 通天塔
来解决这个问题。
思路也很清晰,找到所有的函数定义,去掉一些肯定不需要绑定的函数——比如 构造函数
,组件的生命周期钩子等;然后在 构造函数
每个函数的函数 这个
绑定。但需要注意的一点是,如果原始代码不存在 构造函数
函数,那么这个插件必须先 构造函数
功能来补充。开发中 通天塔
相关的事情, 探索者 这个网站是必不可少的。
在这里我们可以看到代码被解析成抽象语法树的数据结构,我们也可以很方便的看到对应的节点及其属性。那么我们先实现第一个需求:判断原始代码是否存在 构造函数
函数,如果不存在则需要添加。
在这里你可以看到 类体
这个节点,它的 身体
属性对应这个类的所有方法,所以我们只需要遍历数组来判断它是否存在。 构造函数
函数,存在则忽略,不存在则添加。如何弥补?如果是刚入门的话,可能上手有点困难,建议在上面的网站上写一篇。 构造函数
函数来查看它的数据结构是什么样的。
从上图可以看出,它是一个 类方法
输入,然后有 种类
, 钥匙
以此类推,然后配合我们小编的提示:
只知道它的参数应该怎么传,这两个东西互相配合,然后构造出我们需要的东西 节点
.具体代码实现如下:
模块。出口=函数({类型:t}){
返回 {
访客:{
类体:{
输入(路径){
const allMethods = 路径。节点。身体
//判断构造函数是否存在
const 构造函数Exist = allMethods。查找(方法 => 方法。种类 === '构造函数')
if (!constructorExist) {
// 构造一个构造函数并将其填充到数组中
小路。节点。身体。取消移位(
吨。类方法(
'构造函数',
吨。标识符('构造函数'),
[],
吨。块语句([])
)
)
}
}
}
}
};
};
复制代码
然后删除一些不需要的绑定 这个
函数,遍历所有剩余函数,转到 构造函数
给函数添加绑定代码,也就是添加类似 this.fun = this.fun.bind(this)
这样的声明。
每个句子都属于 表达式语句
类型,存储在 身体
在属性中。所以我们需要构造 节点
塞满 身体
在属性中。
以上是绑定语句的抽象语法树的节点信息。看起来这只是一行代码,但是构造起来还是挺麻烦的:
常量 subAst = t。表达式语句(
吨。赋值表达式(
'=',
吨。成员表达式(
吨。 thisExpression(), t。标识符(路径。节点。键。名称)
),
吨。调用表达式(
吨。成员表达式(
吨。 memberExpression(t.thisExpression(), t.identifier(path.node.key.name)),
吨。标识符('绑定')
),
[吨。这个表达式()]
)
)
)
复制代码
这时候就可以使用上面提到的 通天塔模板
这个包,只需要下面几行简单的代码就可以实现上述功能:
常量模板 = 要求('@babel/模板');
常量模板字符串 = 模板。默认(`this.METHOD = this.METHOD.bind(this)`)
常量 subAst = 模板字符串({
方法:t。标识符(路径。节点。键。名称)
})
复制代码
最后,我们来看看这个插件实现的完整代码:
const disabled = [ 'constructor', 'render' /*其他生命周期函数...*/]
常量模板 = 要求('@babel/模板');
模块。出口=函数({类型:t}){
返回 {
访客:{
类体:{
输入(路径){
const allMethods = 路径。节点。身体
const constructorExist = [...allMethods]。查找(方法 => 方法。种类 === '构造函数')
if (!constructorExist) {
小路。节点。身体。取消移位(
吨。类方法('构造函数',
吨。标识符('构造函数'),
[],
吨。块语句([])
)
)
}
},
},
类方法:{
输入(路径){
if (!disabled.includes(path.node.kind) && !disabled.includes(path.node.key.name)) {
const 构造函数 = 路径。容器。 find(method => method.kind === 'constructor');
如果(构造函数){
常量模板字符串 = 模板。默认(`this.METHOD = this.METHOD.bind(this)`)
常量 subAst = 模板字符串({
方法:t。标识符(路径。节点。键。名称)
})
构造函数。身体。身体。推(
亚斯特
)
}
}
}
},
}
};
};
复制代码
然后你可以通过 babel-cli
命令行工具调用 通天塔
编译相应的文件 - 像这样 npx babel test.js --out-file ./out/test.js
,别忘了 .babelrc
该插件在文件中配置。结果如下:
在实践这个的过程中,我个人觉得一开始如何构建节点是比较无能的。希望通过上面的展示,能给大家一些思路。
生成文档
接下来我们来看一个生成文档的案例,这也是实际场景中遇到的需求。不管是开发前端组件,还是写后端 API
,如果开发完成后没有对应的文档,别人很难一下子看出来怎么用。但是很多时候,写完相应的MD文档,人们甚至都懒得去维护它。所以这里我想做一个工具,根据一些注解信息来生成文档,方便大家在写注解的过程中写文档。如果其他人甚至不想写这些笔记怎么办? 埃斯林特+哈士奇
可以帮助你,我会写一篇关于 埃斯林特
, 样式表
Plugin 的文章,但是是时候在那里展开了。实际上 沙哑
这并不是要阻止所有人,总有人不假装置身于自己的环境中。所以要彻底解决这个问题,个人的想法应该是禁止直接提交 掌握
分支(发布分支),仅通过 特征
分支合并到 掌握
, 强制卡 先生
.有点扯远了,下面正式看一下这个工具的具体实现。
假设我现在有这样一个服务器文件:
常量表达 = 要求('表达')
常量应用程序 = 快递()
常量端口 = 3000
/** * @path /login * @param username 用户名字符串 必填 * @param password 密码字符串 必填 */
应用程序。 get('/login', (req, res) => {
水库发送('你好世界!')
})
/** * @path /user * @param userId userid string 必填 * @param fields 要查询的字段数组|undefined */
应用程序。 get('/user', (req, res) => {
水库发送('用户信息')
})
应用程序。听(端口,()=> {
安慰。 log( `监听端口 ${port} 的示例应用程序`)
})
复制代码
我希望把 登录
, 用户
提取这两个接口前面的注释,生成接口文档。我应该怎么办?老规矩,第一 探索者
看看他们长什么样。
注意节点有 领先的评论
和 尾随评论
这两个属性分别代表节点前后的评论。我们规定相关的注释写在节点前面,所以需要处理。 领先的评论
足够的。总体思路如下:
- 读取所有要处理的文件,生成
ast
- 正确的
表达式语句
要处理的节点类型,这取决于情况。 - 处理节点
领先的评论
,按照约定的前缀和字段顺序拼接字符串,输出到医学博士
在文件中
我只是贴出整体代码,行数不多。感觉代码里全是“业务”处理,所以只贴一些注释,不再展开。
const { parse } = require('@babel/parser')
const traverse = require('@babel/traverse')
常量路径 = 要求('路径')
常量目录 = 路径。解决(__dirname,'./server')
常量 fs = 要求('fs')
// 读取所有文件
常量文件 = fs。读取目录同步(目录)
常量文档 = {}
文件。 forEach(文件名 => {
常量文件路径 = ` ${dir} / ${filename} `
if (!doc[文件名]) doc[文件名] = ``
// 读取文件内容
常量内容 = fs。 readFileSync(文件路径,{编码:'utf8'})
// 生成 ast
常量 ast = 解析(内容)
遍历。默认(AST,{
表达式语句:{
输入(路径){
if (path.node.leadingComments && path.node.leadingComments.length > 0) {
让评论=路径。节点。领先的评论[0]。价值
让 tableHead = false
评论=评论。拆分('\n')。地图(项目=>项目。replaceAll('*','')。修剪())。过滤器(v => !!v)
评论。 forEach(线=> {
常量结果 = 行。分裂( ' ')。过滤器(v => !!v)
常量评论名 = 结果[0]
// 添加接口地址
if (commentName == '@path') {
常量 apiPath = 结果[1]
doc[文件名] += ` ## ${apiPath} `
}
if (commentName == '@param') {
// 添加标题
如果(!tableHead){
doc[filename] += `|参数名称|参数说明|类型|必填| |---|---|---|---| `
表头 = 真
}
让 [_, fieldName, desc = null, type = null, required = 'NoRequired'] = 结果
文档[文件名] += `| ${字段名} | ${描述} | ${type.replaceAll( '|' , '\\|' )} | ${必需 === '必需' ?真:假} | `
}
})
}
}
}
})
目的。键(文档)。 forEach(文件名 => {
// 输出文档
fs。写入文件同步(
小路。解决(__dirname, `./doc/ ${filename.replace( '.js' , '.md' )} `),
doc[文件名],
{ 编码:'utf8' }
)
})
})
复制代码
最终生成的界面文档如下所示:
其实代码的实现并不难,只是这个生成文档的代码实现很难抽象出一个公共方法。现在有一些现成的库,比如 jsdoc
.但是如果这些库不能满足你的专业化需求,你可以考虑使用上面的方法来实现自己的一套工具。上面是一个接口文档的例子,当然你也可以把它当成组件使用文档等等。
全球化
国际化也是我们在业务中经常遇到的要求。实现方式可以是使用一些库,也可以是自己实现一个。 翻译
函数,然后在代码中写各种文字的时候需要用到这个 翻译
功能来设置它。其实也可以用 通天塔
的静态分析功能,自动执行此操作,编码时照常编写文本,之后 通天塔
转换后,生成的代码会自动为你戴上 翻译
功能。
我们这里主要处理两种节点:
- 纯文本:对应
字符串字面量
类型 - 模板字符串:对应
模板文字
类型
纯文本
纯文本的处理比较简单,假设我们的翻译函数是 吨
,或者老办法,在上面的网站上看看 文本
和 t('文本')
有什么区别,那么你可以快速编写如下代码:
遍历。默认(AST,{
字符串字面量: {
输入(路径){
常量 { 值 } = 路径。节点
如果 (!(
小路。父母。类型 === 'CallExpression' && ['t', 'require']。包括(路径。父。被调用者。名称)
)
) {
常量 astNode = t。调用表达式(
吨。标识符('t'),
[吨。字符串字面量(值)]
)
小路。替换(astNode)
}
}
}
}
复制代码
这里需要注意的是,有些字符串不需要再次套用 吨
功能,例如,它已被自己应用 吨
函数,或对关联路径的引用。然后我们可以使用 用。。。来代替
这种方法来替换节点。
模板字符串
我们再来看看模板字符串的处理,如果是转换前的情况:
const text3 = ` ${num} :parameter1: ${num1} ${num1} ,参数二:${num2} `
复制代码
然后我想要这样的转换:
const text3 = t(`#num#:参数1:#num1##num1#,参数2:#num2#`,{
无论
数字1:数字1,
数字2:数字2
});
复制代码
所以我们先观察一下模板字符串 节点
:
有两个属性需要注意:
仿佛
: 普通文本表达式
:表达
这两个属性之间有一个规则:表达式的两边必须是普通文本,如果不是,那么这个 准
里面 价值
该属性是空字符串。使用这个规则,我们就可以开始拼接和替换了。
模板文字:{
输入(路径){
const quasis = [...路径。节点。准]
常量表达式 = [...路径。节点。表达]
让 str = ''
常量变量 = 新的 Set()
而(准长度){
const准=准。转移()
str += 准。价值。生的
如果(表达式。长度){
常量表达式 = 表达式。转移()
str += `# ${expression.name} #`
// 收集变量
变量。添加(表达式。名称)
}
}
常量属性 = []
如果(变量。大小> 0){
for(让变量的变量){
常量属性 = t。对象属性(
吨。标识符(变量),
吨。标识符(变量)
)
特性。推(属性)
}
}
常量参数 = [
吨。模板文字(
[
吨。模板元素({
原始:str,
熟:str
})
],
[]
),
]
如果(属性。长度){
参数。推(
吨。对象表达式(
特性
)
)
}
// 构建 t 函数抽象节点
常量 astNode = t。调用表达式(
吨。标识符('t'),
参数
)
//替换节点
小路。替换(astNode)
}
}
复制代码
因为上面花了一些篇幅讲如何构建节点,这里就不详细解释了。替换前后的结果是这样的:
翻译功能
最后一步是实现全局翻译功能。实际上,为什么要用那个替换模板字符串中的变量呢?让我们来看看中英文的一些表达方式。在中文里,我们说 我手头有5个项目
,对应的英文实际上可能是 我有五个项目
.然后我们在代码中写 我手头有 ${num} 件物品
,对应的英文术语是 我有 ${num} 个项目
,上述模板字符串的替换方式可以看成是一种中间状态,用一些占位符标记,方便中英文转换时插入变量。
模块。出口=函数(字符串,参数= {}){
常量和 = {
'文本':'文本',
'#num#:参数1:#num1##num1#,参数2:#num2#':'#num#:param1:#num1##num1#,param2:#num2#',
“字符串”:“字符串”
}
让结果 = en[字符串]
if (!result) 返回字符串
const reg = /(#([^#][0-9a-zA-Z]+)#)/
if ( Object.keys(params).length === 0) {
返回结果
} 别的 {
让 regResult
while (regResult = reg.exec(result)) {
常量原点 = 结果[1]
常量变量 = 结果[2]
结果=结果。替换(原点,参数 [变量])
}
返回结果
}
}
复制代码
所以上述转换后的代码输出如下,整体符合预期:
最后
本文引用实际应用中遇到的三个例子,并使用 通天塔
解决之道,属于引玉。如果平时在实际场景中使用 通天塔
如果你来做其他事情,请在评论区分享如果你觉得这篇文章有用或有趣,请关注并喜欢它
版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议。转载请附上原文出处链接和本声明。
这篇文章的链接: https://homecpp.art/3018/8821/0729
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通