开发工具【免费下载链接】gogocodeGoGoCode is a transformer for JavaScript/Typescript/HTML based on AST but providing a more intuitive API.项目地址https://gitcode.com/gh_mirrors/go/gogocode点击查看免费下载GoGoCode 是一款面向 JavaScript/TypeScript/HTML 的代码转换工具它基于 AST 工作却提供了远比传统工具直观的用代码找代码、用代码改代码的 API。本文以 docs/specification/basic.en.md 基础教程为主线结合仓库内 gogocode-core 的源码实现系统讲解一次代码转换的完整流程解析、查找、修改、生成并覆盖find、$_$/$$$通配符、replace、replaceBy、append/prepend、before/after、remove等全部核心 API。读完本文你将具备用十几行代码完成函数重命名、枚举字段统一、JSX 属性迁移、组件类语法改写等实战改造的能力。一次代码转换的四步流程任何一次基于 GoGoCode 的代码转换本质上都遵循同一个四步流程解析把源码字符串解析成抽象语法树AST查找在整段代码中找到想要修改的 AST 节点修改把找到的节点改成期望的样子生成把修改后的 AST 重新输出为代码字符串。后面的教程将严格沿着这四步依次展开。这套流程在源码中的对应关系非常清晰解析由 src/$.js 与 src/js-core/parse.js 完成查找与修改由 src/Ast.js 与 src/js-core/core.js 承载生成则由 src/js-core/generate.js 负责。解析代码用$把代码变成 AST安装与引入先安装 gogocodenpm install gogocode --save然后引入入口函数沿用了 jQuery 的$命名让代码写起来更简洁import $ from gogocode; // 或 CommonJS 方式 const $ require(gogocode);解析 JS / TS / HTML / Vue$的第一个参数是待解析的源码字符串默认按 JavaScript/TypeScript 解析// source 为待解析的代码字符串 const ast $(source); // 解析 HTML 需要在 parseOptions 中指定语言 const ast $(source, { parseOptions: { language: html } }); // 解析 Vue 文件 const ast $(source, { parseOptions: { language: vue } });入口函数的实现细节从 src/$.js 可以看到$内部维护了一张语言核心映射表HTML 与 Vue 分别由独立的htmlCore、vueCore处理未指定语言时默认走jsCoreconst langCoreMap { vue: vueCore, html: htmlCore, js: jsCore }$的入参很灵活见 src/$.js除了字符串还可以直接传入 AST 节点含nodeType或type属性或nodePath对象当传入无法解析的内容时会返回包含src与error字段的对象而不是直接抛异常。解析 JS 的底层基于babel/parsersrc/js-core/parse.js 中内置了typescript、jsx失败后自动重试、classProperties、decorators-legacy、optionalChaining、topLevelAwait等一长串 Babel 插件因此默认就能处理大量现代语法生成代码则基于recast见 src/js-core/generate.js可以最大程度保留原始代码的格式信息。$的扩展工具方法除了直接解析字符串入口还附带了几枚实用工具见 src/$.js$(...).loadFile(filePath, { parseOptions }); // 读取文件并解析 $.writeFile(code, filename); // 写出代码文件 $.version; // 当前版本号本文中的代码片段都可以在 GoGoCode 官方 Playground 在线实时体验切换代码类型后右侧会给出对应示例代码。用代码选择器查找目标节点把代码解析成 AST 之后进入第二步从整段代码中精确找出要修改的节点。ast.find的基本用法与其他通过 AST 节点类型匹配的转换工具不同GoGoCode 提供了一种更直观的方式——用代码找代码。假设要从下面这段代码中挑出名为log的函数function log(a) { console.log(a); } function alert(a) { alert(a); }只需这样写const ast $(source); const test1 ast.find(function log() {});GoGoCode 会基于function log() {}这个代码选择器自动匹配名为log的function节点并返回满足匹配条件的子节点。选择器本质上也是代码由 src/js-core/get-selector.js 解析成节点类型 结构的描述对象再交给 src/js-core/core.js 中的getAstsBySelector在整棵树上查找这一链路可以从 src/Ast.js 的find实现完整看到。用generate输出节点代码对找到的 AST 节点调用.generate就能拿到该节点对应的代码字符串const ast $(source); const test1 ast.find(function log() {}); const code test1.generate() // code 为以下字符串 // function log(a) { // console.log(a); // }generate还可以传入{ isPretty: true }获得格式化输出见 src/Ast.js 与 src/js-core/generate.js。$_$通配符与match假设想找出变量a的声明和初始化语句const a 123;可以直接写ast.find(const a 123)但这样只能匹配到123匹配不到456。真实代码里我们往往不确定完整内容因此 GoGoCode 支持用通配符做模糊匹配const aDef ast.find(const a $_$0);把原来的123换成$_$0就能匹配所有初始化const a的语句// 下面这些语句都能被匹配 const a 123; const a b; const a () 1; // ......$_$0位置匹配到的节点可以通过查询结果的match属性拿到const aDef ast.find(const a $_$); const match aDef.match;match是一个字典结构$_$后面的数字是它在match中的索引通过match[0]可以取到$_$0位置匹配到的 AST 集合。这个集合通常只有一个元素对应const a 123中的123用.node拿到原始 AST 节点用.value直接取到它在代码中的片段值。仓库类型定义 types/index.d.ts 对该结构有精确描述type MatchResult { [key in string | number]: Array{ node: ASTNode; value: string; }; };编写代码转换时多使用调试器观察中间结果是提升效率的好习惯。集合操作each回到最初的例子function log(a) { console.log(a); } function alert(a) { alert(a); }如果使用通配符就能按名字匹配所有函数定义.find的结果因此可能是一个集合// fns 是包含所有按名字匹配到的函数定义的集合 const fns ast.find(function $_$0() {});这个结果集fns与ast类型相同、成员方法完全一致如果集合中有多个元素直接调用方法只会作用于第一个 AST 节点。要遍历集合使用each方法。下面的例子把所有匹配到的函数名收集进数组namesconst fns ast.find(function $_$0() {}); const names []; fns.each((fnNode) { const fnName fnNode.match[0][0].value; names.push(fnName); });each的回调还会收到第二个参数index并且each之后可以继续链式调用其他方法src/Ast.js。集合还有length、eq(index)等 jQuery 风格的方法在 test/G.find.test.js 中可以看到大量针对集合行为的验证用例例如G.find(function $_$() {})一次匹配 3 个函数的测试。多通配符匹配有时需要的通配符不止一个可以在选择器中写$_$0、$_$1、$_$2、$_$3…… 比如想匹配下面函数的两个参数sum(a, b);const sumFn ast.find(sum($_$0, $_$1)); const match sumFn.match; console.log(${match[0][0].value},${match[1][0].value}); // a,b匹配任意数量的同类型节点再看一个场景console.log(a); console.log(a, b); console.log(a, b, c);这三行的参数个数各不相同分别用下面的选择器查找结果会怎样ast.find(console.log()); ast.find(console.log($_$0)); // 上面两条语句能匹配到全部三行代码 ast.find(console.log($_$0, $_$1)); // 这条语句只能匹配到前两行 ast.find(console.log($_$0, $_$1, $_$2)); // 这条语句只匹配第三行可以看到 GoGoCode 通配符的匹配原则写得越多查询限制越严格。如果想匹配任意数量的同类型节点GoGoCode 提供了$$$形式的通配符。对上面参数个数不确定的语句可以统一用ast.find(console.log($$$0))匹配。相比ast.find(console.log())$$$还能用match属性把占位符里的所有节点捞出来例如匹配console.log(a, b, c)const res ast.find(console.log($$$0)); const params res.match[$$$0]; const paramNames params.map((p) p.name); // paramNames: [a, b, c]与$_$一样$$$0匹配到的节点数组从match里取出元素依次对应 AST 节点a、b、c。$$$的能力远不止匹配不定长参数。比如要匹配字典dict的所有键值并打印const dict { a: 1, b: 2, c: f, };const res ast.find(const dict { $$$0 }); const kvs res.match[$$$0]; kvs.map((kv) ${kv.key.name}:${kv.value.value}); // a:1,b:2,c:f从底层看$$$与$_$在 src/js-core/get-selector.js 中被替换成不同的内部标记$_$替换为 expando 前缀、$$$替换为以$3结尾的变体$$$匹配到的是零到多个节点的列表而$_$匹配的通常是单个节点在replace时两者也会走不同的填充逻辑见下文源码分析。ast.has判断代码是否存在用.has可以判断源码中是否存在某段代码例如if (ast.has(import $_$0 from react)) { console.log(has React!); }这可以判断代码是否引入了 React 包其实现等价于if (ast.find(import $_$0 from react).length) { console.log(has React!); }即至少匹配到一条语句。源码 src/Ast.js 中的has正是!!this.find(selector, options)[0]。find的可选参数在 types/index.d.ts 中可以看到find支持两个实用选项ignoreSequence是否忽略匹配顺序。忽略顺序可用于{ a: $_$ }匹配{ b: 1, a: 2 }这类对象属性乱序的场景需要严格顺序的场景如function($_$, b) {}匹配function(a, b) {}。默认false。deep遍历深度默认nn匹配所有层级符合条件的节点n只匹配最外层1匹配到一个即停止。替换代码掌握了如何用选择器与通配符找到目标语句接下来进入第三步把找到的语句改成期望的样子。强大的replace函数重命名在编辑器里对代码做批量修改时我们常依赖查找/替换但字符串或正则替换无法兼容不同的缩进、换行甚至有没有分号。而 GoGoCode 的replace可以做到以接近字符串替换的形式完成 AST 级代码替换。回到第一个例子function log(a) { console.log(a); } function alert(a) { alert(a); }想把log函数重命名为record用replace一行搞定ast.replace(function log($$$0) { $$$1 }, function record($$$0) { $$$1 });replace接收两个参数第一个是代码选择器第二个是替换目标。这里用$$$0匹配参数列表、$$$1匹配函数体内的语句再把它们在第二个参数中放回原位就能保证唯一变化的是函数名。枚举列表属性重命名代码中经常出现这样的枚举列表const list [ { text: A-strategy, value: 1, tips: Atip, }, { text: B-strategy, value: 2, tips: Btip, }, { text: C-strategy, value: 3, tips: Ctip, }, ];某天为了统一代码中的各种枚举需要把text属性改名为name、value改名为id。这种改动用正则难以精确匹配且容易漏改用 GoGoCode 只需一次替换ast.replace( { text: $_$1, value: $_$2, $$$0 }, { name: $_$1, id: $_$2, $$$0 }, );其中$_$1、$_$2分别匹配text和value的属性值节点$$$0匹配其余节点作用类似 ES6 里的...剩余参数。这段代码把text、value的值提取出来填入name、id其余部分保持原样。JSX 标签属性替换再看一个更复杂的例子对下面这段代码做如下改造从alifd/next改为从antd引入h2文本Before改为AfterButton的type值映射normal - defaultmedium - middleButton上的text改为typelinkButton上的warning改为danger。import * as React from react; import * as styles from ./index.module.scss; import { Button } from alifd/next; const Btn () { return ( div h2Before/h2 div Button typenormalNormal/Button Button typeprimaryPrirmary/Button Button typesecondarySecondary/Button Button typenormal text Normal /Button Button typeprimary text Primary /Button Button typesecondary text Secondary /Button Button typenormal warning Normal /Button /div /div ); }; export default Btn;改造逻辑可以由一串链式replace完成注意replace返回的还是ast因此可以连续调用ast .replace(import { $$$0 } from alifd/next, import { $$$0 } from antd) .replace(h2转译前/h2, h2转译后/h2) .replace( Button typenormal $$$0/Button, Button typedefault $$$0/Button, ) .replace( Button sizemedium $$$0/Button, Button sizemiddle $$$0/Button, ) .replace(Button text $$$0/Button, Button typelink $$$0/Button) .replace(Button warning $$$0/Button, Button danger $$$0/Button);JSX 标签、属性同样可以作为代码参与选择与替换这正是用代码找代码理念在模板场景的延伸Vue 模板、HTML 的处理方式与之类似详见 docs/specification/vue.en.md。更复杂的函数式替换如果替换逻辑需要更多自由度replace的第二个参数还可以传一个函数该函数接收match字典作为参数返回一段新的代码来替换匹配到的内容。假设有如下常量定义const stock_code_a BABA; const stock_code_b JD; const stock_code_c TME;想批量把它们的变量名改成大写字符串ast.replace(const $_$0 $_$1, (match, node) { const name match[0][0].value; const value match[1][0].raw; return const ${name.toUpperCase()} ${value}; });这里match[0][0].value是变量名如stock_code_amatch[1][0].raw是初始化值的原始文本。回调返回null时GoGoCode 会跳过该处替换源码见 src/js-core/core.js。replaceBy找到后直接替换当前节点除了用.replace在选择器层面整体替换还可以先用.find找到对应语句再用.replaceBy直接替换它。例如想把log函数内部的console.log(a)改成alert(a)同时不影响函数外部的同名语句function log(a) { console.log(a); } console.log(a);可以链式.find到函数内部的console.log(a)再.replaceByconst console ast.find(function log($_$0) {}).find(console.log($_$0)); console.replaceBy(alert(a));replace的底层实现从 src/Ast.js 看replace会调用core.replaceSelBySel而 src/js-core/core.js 中的replaceSelBySel做了这些事先用getAstsBySelector找出所有匹配节点及各自的matchWildCardList若替换目标是函数先调用函数拿到替换串对字符串替换目标遍历match中的每个通配符$$$通配符会把匹配到的多个节点逐个generate后用分隔符默认换行部分节点类型按 node-link-map.js 用逗号连接回填$_$通配符则按原样回填单个节点的raw或value回填完的新代码再buildAstByAstStr解析为 AST最终调用replaceAstByAst完成节点级替换若替换串为空!replacer则走removePathSafe实现删除。在 test/G.replace.test.js 中可以看到丰富的验证用例例如G.replace($_$.a(), $_$.b())把m.a()全部替换为m.b()以及把await $_$()包进try/catch的复杂改写。插入代码学到这里可以尝试解决一个更完整的转换问题。下面是一段来自 React 文档的组件代码class Toggle extends React.Component { constructor(props) { super(props); this.state { isToggleOn: true }; // This binding is necessary to make this work in the callback this.handleClick this.handleClick.bind(this); } handleClick() { this.setState((prevState) ({ isToggleOn: !prevState.isToggleOn, })); } render() { return ( button onClick{this.handleClick} {this.state.isToggleOn ? ON : OFF} /button ); } }文档告诉我们React 事件的回调函数需要在constructor中显式绑定this。假如我们去掉了构造器里的绑定语句就可以写一段转换逻辑让 GoGoCode 自动识别 JSX 中的onClick回调并在constructor里帮我们补上绑定语句。用replace插入代码万能的.replace不只是简单替换用$$$捕获并回填原内容再追加要插入的语句就实现了插入代码的操作。详细步骤如下const ast $(source); // 找到 reactClass 定义语句 const reactClass ast.find(class $_$0 extends React.Component {}); // 在 jsx 中查找带 onClick 属性的标签 const onClick reactClass.find($_$0 onClick{$_$1}/$_$0); // 创建数组收集 onClick 对应 handler 的名字 const clickFnNames []; // 可能找到多个带 onClick 的标签用 each 逐个处理 onClick.each((e) { // 用 match[1][0] 找到 $_$1 匹配到的第一个 onClick 属性对应的 handler 节点 // 取其 value 作为节点名 // handlerName this.handleClick const handlerName e.match[1][0].value; clickFnNames.push(handlerName); }); // 替换原构造器用 $$$ 保留原参数与语句仅在末尾追加 bind 语句 reactClass.replace( constructor($$$0) { $$$1 }, constructor($$$0) { $$$1; ${clickFnNames.map((name) ${name} ${name}.bind(this)).join(;)} }, );clickFnNames里收集到的名字会被拼成this.handleClick this.handleClick.bind(this)之类的语句通过$$$1回填原有函数体后追加进构造器。用append/prepend向函数体或参数追加语句也可以直接用.append插入代码。.append支持两个参数第一个参数是插入位置填params表示新增一个函数参数填body表示插入到花括号包裹的代码块中。用.append实现同样的事情const ast $(source); // 找到 reactClass 定义语句 const reactClass ast.find(class $_$0 extends React.Component {}); // 在 jsx 中查找带 onClick 属性的标签 const onClick reactClass.find($_$0 onClick{$_$1}/$_$0); // 创建数组收集 onClick 对应 handler 的名字 const clickFnNames []; // 可能找到多个带 onClick 的标签用 each 逐个处理 onClick.each((e) { // 用 match[1][0] 找到 $_$1 匹配到的第一个 onClick 属性对应的 handler 节点 // 取其 value 作为节点名 // handlerName this.handleClick const handlerName e.match[1][0].value; clickFnNames.push(handlerName); }); /** 以上代码与之前相同 ***/ // 找到 constructor 方法 const constructorMethod ast.find(constructor() {}); // 在其函数体内追加 bind 语句 constructorMethod.append( body, ${clickFnNames.map((name) ${name} ${name}.bind(this)).join(;)} , );.prepend的用法与.append完全一致区别只是语句被添加到代码块的开头。从源码看src/Ast.jsappend/prepend的第一个参数表示当前节点的数组属性名如params、body、program.bodyHTML 场景下默认落到content.children字符串内容会被buildAstByAstStr解析成 AST 后再插入。用before/after在节点前后插入回到上面的 React 组件如果想在每次setState前后各打印一条state日志可以用.before和.after方法——它们会把传入的内容分别插入到当前 AST 节点的前/后const ast $(source); const reactClass ast.find(class $_$0 extends React.Component {}); reactClass.find(this.setState()).each((setState) { setState.before(console.log(before, this.state)); setState.after(console.log(after, this.state)); });before/after的底层实现是 src/Ast.js 与insertSiblingNode字符串参数同样先解析为 AST再以同层级兄弟节点的身份插入到当前节点的前一/后一位置注释节点则会被特殊处理为leading/trailing注释见 src/Ast.js。删除代码接上一节的故事我们写了一个转换程序给所有回调函数加上了.bind(this)然后翻到文档后半页发现其实还可以这样写——不绑定而是直接用 class fields 语法class Toggle extends React.Component { constructor(props) { super(props); this.state { isToggleOn: true }; // The following line is no longer needed // this.handleClick this.handleClick.bind(this) } // from class member method to public class fields syntax handleClick () { this.setState((prevState) ({ isToggleOn: !prevState.isToggleOn, })); }; render() { return ( button onClick{this.handleClick} {this.state.isToggleOn ? ON : OFF} /button ); } }首先这说明学习新工具一定要把文档读完否则会留下遗憾。其次我们可以再写一个转换工具把上一轮产出的代码改造成这个样子不留遗憾先用万能的replace把回调函数handleClick() {}改造成handleClick () {}const ast $(source); // 找到 reactClass 定义语句 const reactClass ast.find(class $_$0 extends React.Component {}); // 在 jsx 中查找带 onClick 属性的标签 const onClick reactClass.find($_$0 onClick{$_$1}/$_$0); // 创建数组收集 onClick 对应 handler 的名字 const clickFnNames []; // 可能找到多个带 onClick 的标签用 each 逐个处理 onClick.each((e) { // 用 match[1][0] 找到 $_$1 匹配到的第一个 onClick 属性对应的 handler 节点 // 取其 value 作为节点名 // handlerName this.handleClick const handlerName e.match[1][0].value; clickFnNames.push(handlerName); }); clickFnNames.forEach((name) { // 去掉前面的 this. 得到纯函数名 const fnName name.replace(this., ); // 把类方法改为 public class fields 语法 reactClass.replace( ${fnName}() {$$$0}, ${fnName} () { $$$0 }, ); });接下来看如何删除原来的.bind(this)语句。用replace删除代码删除语句最简单的方式就是把选择器匹配到的内容替换成空clickFnNames.forEach((name) { // 去掉前面的 this. 得到纯函数名 const fnName name.replace(this., ); // 把类方法改为 public class fields 语法 reactClass.replace( ${fnName}() {$$$0}, ${fnName} () { $$$0 }, ); // 删除原始的 bind 语句 reactClass.replace(this.${fnName} this.${fnName}.bind(this), ); });用remove删除代码或者先.find再调用.remove方法效果相同clickFnNames.forEach((name) { // 去掉前面的 this. 得到纯函数名 const fnName name.replace(this., ); // 把类方法改为 public class fields 语法 reactClass.replace( ${fnName}() {$$$0}, ${fnName} () { $$$0 }, ); // 删除原始的 bind 语句 reactClass.find(this.${fnName} this.${fnName}.bind(this)).remove(); });删除的底层实现remove支持两种用法src/Ast.js传入选择器时调用core.removeAst按选择器批量删除不传选择器时直接删除当前节点最终返回this.root()回到根节点。真正的删除动作在 src/js-core/core.js 的removePathSafe中如果被删节点是expression会连外层的ExpressionStatement一起删除避免输出残留多余的分号。类型定义与测试验证如果你在 TypeScript 项目中使用 gogocodepackages/gogocode-core/types/index.d.ts 提供了完整的类型声明GoGoOptionparseOptions、astFragment、isProgram、FindOptionignoreSequence、deep、MatchResult、GoGoASTfind、replace、append、remove、generate等全部方法签名都有清晰注释。仓库 packages/gogocode-core/test 目录下是按 API 分文件组织的 Jest 测试套件G.find.test.js覆盖了通配符、对象属性、JSX、HTML、Vue 模板、TypeScript 类型标注等各种查找场景G.replace.test.js覆盖了函数重命名、$$$回填、函数式替换等G.each.test.js、G.remove.test.js、G.append.test.js、G.before.test.js等则对应各自的插入/删除/遍历方法。阅读这些测试是快速理解 API 行为边界的最佳途径。进一步学习本文覆盖了 GoGoCode 做一次代码转换的完整链路。继续深入可以阅读仓库中的配套文档API 文档查看全部实例方法的完整说明Cookbook更多贴近真实场景的实战配方进阶builder 用法 与 插件机制了解如何编写可复用的转换插件Vue 转换专题面向 Vue 2 到 Vue 3 的迁移玩法。祝你的代码转换之路顺利赞分享开发工具【免费下载链接】gogocodeGoGoCode is a transformer for JavaScript/Typescript/HTML based on AST but providing a more intuitive API.项目地址https://gitcode.com/gh_mirrors/go/gogocode点击查看免费下载相关推荐GoGoCode 基础教程用代码选择器完成 JavaScript/TypeScript/HTML/Vue 的 AST 级代码转换GoGoCode 基础教程用代码选择器完成 JavaScript/TypeScript/HTML/Vue 的 AST 级代码转换 GoGoCode 是一个基于开发工具终极J engine编译指南支持Windows/Linux/macOS全平台终极J engine编译指南支持Windows/Linux/macOS全平台 J engine是一款高效的编程语言引擎本指南将带你快速掌握在Windows、GoGoCode 实战指南基于 AST 的类 jQuery 代码选择器与正则式替换语法轻松完成 JavaScript/TypeScript/HTML 代码转换GoGoCode 实战指南基于 AST 的类 jQuery 代码选择器与正则式替换语法轻松完成 JavaScript/TypeScript/HTML 代码转开发工具上一篇Sa-Token 数据结构全解析Token、Session 与插件存储 Key 的命名规范与字段格式下一篇Universal Modder 实战将零售版 12.x 人脸、发型与发色移植到魔兽世界 3.3.5a HD 人类——CharSections、.skin 分段与角色合成器规则创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考