专栏名称: 飞猪前端团队
公众号
目录
相关文章推荐
51好读  ›  专栏  ›  飞猪前端团队

像玩 jQuery 一样玩 AST

飞猪前端团队  · 掘金  ·  · 2021-01-31 22:50

正文

阅读 274

像玩 jQuery 一样玩 AST

本文来自飞猪前端的 @呦嘿 同学,萌妹子手把手教你使用 AST,这篇文章写得很不错值得一读。

这篇文章适合在原理性知识不通的情况下,仍然对ast蠢蠢欲动的开发者们,文章不具备任何专业性以及严谨性,它除了实用,可能一无是处。

关于AST的介绍,网上已经一大堆了,不仅生涩难懂,还自带一秒劝退属性。其实我们可以很(hao)接(bu)地(yan)气(jin)的去了解一个看上去高端大气的东西,比如,AST是一个将代码解构成一棵可以千变万化的树的黑魔法。所以,只要我们知道咒语怎么念,世界的大门就打开了。有趣的是,魔法咒语长得像jQuery~

欢迎你,魔法师

在成为一名魔法师之前,我们需要准备四样东西: 趁手的工具、 又简短又常用的 使用技巧 ,即使看不懂也不影响使用的 权威api 、 以及天马行空的 想象力。

🍭 魔法棒 之 趁手的工具

🔗 AST exporer

这是一个ast在线调试工具,有了它,我们可以非常直观的看到ast生成前后以及代码转换,它分五个区域。我们接下来都依赖这个工具进行代码操作。

20210119160723.jpg

🔗 jscodeshift

它是一个ast转换器,我们通过它来将原始代码转译成ast语法树,并借用其开放的api操作ast,最终转换成我们想要的代码。

jscodeshift的api基于recast封装,语法十分接近jquery。recast是对babel/travers & babel/types的封装,它提供简易的ast操作,而travers是babel中用于操作ast的工具,types我们可以粗浅的理解为字典,它用于描述结构树类型。

同时,jscodeshift还提供额外的功能,使得开发者们能够在项目工程阶段、亦或开发阶段皆可投入使用,同时无需感知babel转译前后的过程,只专注于如何操作或改变树,并得到结果。

尽管jscodeshift缺少中文文档,但其源码可读性非常高,这也是为什么推荐使用jscodeshift的重要原因之一。关于其api操作技巧,将在实践中为大家揭晓。

📖 魔法书 之 权威api

🔗 babel-types

ast语法字典,方便我们快速查阅结构树的类型,它是我们想要通过ast生成某行代码时的重要工具之一。

认识AST

我以为的AST

image.png

实际中的AST

假如我们有这样一份代码

var a = 1
复制代码

我们将其转化为AST,以JSON格式展示如下

{
  "type": "Program",
  "sourceType": "script",
  "body": [
    {
      "type": "VariableDeclaration",
      "kind": "var",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "a"
          },
          "init": {
            "type": "Literal",
            "value": 1
          }
        }
      ]
    }
  ]
}
复制代码

当我操作对象init中value的值 1 改为 2 时,对应的js也会跟着改变为 var a = 2 当我操作对象id中的name的值a 改为 b 时, 对应的js也会跟着改变为 var b = 2

看到这里,突然发现,操作AST无非就是 操作一组有规则的JSON ,发现新大陆有木有?? 那么只要明白规则,是不是很快就可以掌握一个世界了有!木!有!

了解AST节点

image.png

探索AST节点类型

常用节点含义对照表 image.png 看了规则后瞬间明白ast的json中那些看不懂的type是个什么玩意了(详细可对照 babel-types ),真的就是描述语法的词汇罢了! 原来掌握一个世界竟然可以这么简!单!

jscodeshift 简易操作

查找

api 类型 接收参数 描述
find fn type: ast类型
找到所有符合筛选条件的ast类型的ast节点,并返回一个array。
filter fn callback:接受一个回调,默认传递被调用的ast节点 筛选指定条件的ast节点,并返回一个array
forEach fn callback:接受一个回调,默认传递被调用的ast节点 遍历ast节点,同js的forEach函数

除此之外, 还有 some、every、closest 等用法基本一致。

删除

api 类型 接收参数 描述
remove fn type: ast类型
filter:筛选条件 找到所有符合筛选条件的ast类型的ast节点,并返回一个array。

添加 & 修改

api 类型 接收参数 描述
replaceWith fn nodes:ast节点 替换ast节点,如果为空则表示删除
insertBefore fn fn nodes:ast节点
insertAfter fn fn nodes:ast节点
toSource fn options: 配置项 ast节点转译,返回js

除此之外, 还有 some、every、closest 等用法基本一致。

其它

子节点相关操作如getAST()、nodes() 等。 指定ast节点的查找,如:findJSXElements()、hasAttributes()、hasChildren()等。

更多可通过ast explore 在操作区console查看、或直接查看 jscodeshift/collections

命令

// -t 转换文件的文件路径 可以是本地或者url 
// myTransforms ast执行文件
// fileA fileB 待操作的文件
// --params=options 用于执行文件接收的参数
jscodeshift -t myTransforms fileA fileB --params=options
复制代码

更多命令查看 🔗 jscodeshift

实践

接下来,我将在实践中传递技巧。

简单的例子

我们先来看一个例子,假设有如下代码

import * as React from 'react';
import styles from './index.module.scss';
import { Button } from "@alifd/next";


const Button = () => {
  return (
    <div>
      <h2>转译前</h2>
      <div>
        <Button type="normal">Normal</Button>
        <Button type="primary">Prirmary</Button>
        <Button type="secondary">Secondary</Button>
        

        <Button type="normal" text>Normal</Button>
        <Button type="primary" text>Primary</Button>
        <Button type="secondary" text>Secondary</Button>
        

        <Button type="normal" warning>Normal</Button>
      </div>
    </div>
  );
};

export default Button;

复制代码

执行文件(通过jscodeshift进行操作)

module.exports = (file, api) => {
    const j = api.jscodeshift;
    const root = j(file.source);
    root
        .find(j.ImportDeclaration, { source: { value: "@alifd/next" } })
        .forEach((path) => {
            path.node.source.value = "antd";
        })
    root
    	.find(j.JSXElement, {openingElement: { name: { name: 'h2' } }})
  		.forEach((path) => {
        	path.node.children = [j.jsxText('转译后')]
        })
    root
        .find(j.JSXOpeningElement, { name: { name: 'Button' } })
        .find(j.JSXAttribute)
        .forEach((path) => {
            const attr = path.node.name
            const attrVal = ((path.node.value || {}).expression || {}).value ? path.node.value.expression : path.node.value

            if (attr.name === "type") {
                if (attrVal.value === 'normal') {
                    attrVal.value = 'default'
                }
            }

            if (attr.name === "size") {
                if (attrVal.value === 'medium') {
                    attrVal.value = 'middle'
                }
            }

            if (attr.name === "warning") {
                attr.name = 'danger'
            }

            if (attr.name === "text") {
                const attrType = path.parentPath.value.filter(item => item.name.name === 'type')
                attr.name = 'type'
                if (attrType.length) {
                    attrType[0].value.value = 'link'
                    j(path).replaceWith('')
                } else {
                    path.node.value = j.stringLiteral('link')
                }

            }
        });

    return root.toSource();
}

复制代码

该例代码大致解读如下

  1. 将js转换为ast
  2. 遍历代码中所有包含@alifd/next的引用模块,并做如下操作
    1. 改变该模块名为antd。
  3. 找到代码中标签名为h2的代码块,并修改该标签内的文案。
  4. 遍历代码中所有Button标签,并做如下操作
    1. 改变标签中type和size属性的值
    2. 改变标签中text属性变为 type = "link"
    3. 改变标签中warning属性为danger
  5. 返回由ast转换后的js。

最终输出结果

import * as React from 'react';
import styles from './index.module.scss';
import { Button } from "antd";


const Button = () => {
  return (
    <div>
      <h2>转译后</h2>
      <div>
        <Button type="default">Normal</Button>
        <Button type="primary">Prirmary</Button>
        <Button type="secondary">Secondary</Button>
        

        <Button type="link" >Normal</Button>
        <Button type="link" >Primary</Button>
        <Button type="link" >Secondary</Button>
        

        <Button type="default" danger>Normal</Button>
      </div>
    </div>
  );
};

export default Button;
复制代码

逐句解读

获取必要的数据

// 获取操作ast用的api,获取待编译的文件主体内容,并转换为AST结构。
const j = api.jscodeshift;
const root = j(file.source);
复制代码

执行jscodeshift命令后,执行文件接收 3 个参数

file
属性 描述
path 文件路径
source 待操作的文件主体,我们主要用到这个。
api
属性 描述
jscodeshift 对jscodeshift库的引用,我们主要用到这个。
stats --dry 运行期间收集统计信息的功能
report 将传递的字符串打印到stdout
options

执行jscodeshift命令时,接收额外传入的参数,目前用不到,不做额外赘述。

代码转换

// root: 被转换后的ast跟节点  
root
	// ImportDeclaration 对应 import 句式
  .find(j.ImportDeclaration, { source: { value: "@alifd/next" } })
  .forEach((path) => {
  // path.node 为import句式对应的ast节点
  	path.node.source.value = "antd";
	})
复制代码

解读:

  • 遍历代码中所有包含@alifd/next的引用模块,并做如下操作
    1. 改变该模块名为antd。
root
	// JSXElement 对应 element 完整句式,如 <h2 ...> ... </h2>
	// openingElement 对应 element 的 开放标签句式, 如 <h2 ...>
  .find(j.JSXElement, {openingElement






请到「今天看啥」查看全文


推荐文章
爆笑gif图  ·  哈哈哈,让你嘚瑟,太特么贱了
8 年前
经典人生感悟  ·  生 气 真 傻 (写的真好)
7 年前
美好滁州  ·  幽默一刻!
7 年前