用JS解释JS!详解AST及其应用

发表于 3年以前  | 总阅读数:419 次

一 AST 是什么? 1 AST:Abstract Syntax Tree - 抽象语法树 当我们查看目前主流的项目中的 devDependencies,会发现各种各样的模块工具。归纳一下有:JavaScript转译、css预处理器、elint、pretiier 等等。这些模块我们不会在生产环境用到,但它们在我们的开发过程中充当着重要的角色,而所有的上述工具,都建立在 AST 的基础上。

2 AST 工作流程

  • parse:把代码解析为AST。
  • transform:对AST中的各个节点做相关操作,如新增、删除、替换、追加。业务开发 95%的代码都在这里。
  • generator:把AST转换为代码。

3 AST 树预览

AST 辅助开发工具:https://astexplorer.net/

二 从一个简单需求上手 代码压缩的伪需求:将 square 函数参数与引用进行简化,变量由 num 转换为 n:

解法1:使用 replace 暴力转换

const sourceText = `function square(num) {
  return num * num;
}`;
sourceText.replace(/num/g, 'n');

以上操作相当的暴力,很容易引起bug,不能投入使用。如若存在字符串 "num",也将被转换:

// 转换前
function square(num) {
  return num * num;
}
console.log('param 2 result num is ' + square(2));

// 转换后
function square(n) {
  return n * n;
}
console.log('param 2 result n is ' + square(2));

解法2:使用 babel 进行 AST 操作

module.exports = () => {
  return {
    visitor: {
      // 定义 visitor, 遍历 Identifier
      Identifier(path) {
        if (path.node.name === 'num') {
          path.node.name = 'n'; // 转换变量名
        }
      }
    }
  }
};

通过定义 Identifier visitor,对 Identifier(变量) 进行遍历,如果 Identifier 名称为 "num",进行转换。以上代码解决了 num 为字符串时也进行转换的问题,但还存在潜在问题,如代码为如下情况时,将引发错误:

// 转换前
function square(num) {
  return num * num;
}
console.log('global num is ' + window.num);

// 转换后
function square(n) {
  return n * n;
}
console.log('global num is ' + window.n); // 出错了

由于 window.num 也会被上述的 visitor 迭代器匹配到而进行转换,转换后出代码为 window.n,进而引发错误。分析需求“将 square 函数参数与引用进行简化,变量由 num 转换为 n”,提炼出的3个关键词为 “square 函数、参数、引用”,对此进一步优化代码。 解法2升级:找到引用关系

module.exports = () => {
  return {
    visitor: {
      Identifier(path,) {
        // 三个前置判断
        if (path.node.name !== 'num') { // 变量需要为 num
          return;
        }
        if (path.parent.type !== 'FunctionDeclaration') { // 父级需要为函数
          return;
        }
        if (path.parent.id.name !== 'square') { // 函数名需要为 square
          return;
        }
        const referencePaths = path.scope.bindings['num'].referencePaths; // 找到对应的引用
        referencePaths.forEach(path => path.node.name = 'n'); // 修改引用值
        path.node.name = 'n'; // 修改自身的值
      },
    }
  }
};

上述的代码,可描述流程为:

转换结果:

// 转换前
function square(num) {
  return num * num;
}
console.log('global num is ' + window.num);

// 转换后
function square(n) {
  return n * n;
}
console.log('global num is ' + window.num);

在面向业务的AST操作中,要抽象出“人”的判断,做出合理的转换。

三 Babel in AST

1 API 总览

// 三剑客
const parser = require('@babel/parser').parse;
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;

// 配套包
const types = require('@babel/types');

// 模板包
const template = require('@babel/template').default;

2 @babel/parser 通过 babel/parser 将源代码转为 AST,简单形象。

const ast = parser(rawSource, {
  sourceType: 'module',
  plugins: [
    "jsx",
  ],
});

3 @babel/traverse AST 开发的核心,95% 以上的代码量都是通过 @babel/traverse 在写 visitor。

const ast = parse(`function square(num) {
  return num * num;
}`);

traverse(ast, { // 进行 ast 转换
    Identifier(path) { // 遍历变量的visitor
      // ...
    },
    // 其他的visitor遍历器
  } 
)

visitor 的第一个参数是 path,path 不直接等于 node(节点),path 的属性和重要方法组成如下:

4 @babel/generator 通过 @babel/generator 将操作过的 AST 生成对应源代码,简单形象。

const output = generate(ast, { /* options */ });

5 @babel/types @babel/types 用于创建 ast 节点,判断 ast 节点,在实际的开发中会经常用到。

// is开头的用于判断节点
types.isObjectProperty(node);
types.isObjectMethod(node);

// 创建 null 节点
const nullNode = types.nullLiteral();
// 创建 square 变量节点
const squareNode = types.identifier('square');

6 @babel/template @bable/types 可以创建 ast 节点,但过于繁琐,通过 @babel/template 则可以快速创建整段的 ast 节点。下面对比了获得 import React from 'react' ast 节点的两种方式:

// @babel/types
// 创建节点需要查找对应的 API,传参需要匹配方法
const types = require('@babel/types');
const ast = types.importDeclaration(
  [ types.importDefaultSpecifier(types.identifier('React')) ], 
  types.stringLiteral('react')
);

// path.replaceWith(ast) // 节点替换
// 使用 @babel/template
// 创建节点输入源代码即可,清晰易懂
const template = require('@babel/template').default;
const ast = template.ast(`import React from 'react'`);

// path.replaceWith(ast) // 节点替换

7 定义通用的 babel plugin 定义通用的 babel plugin,将有利于被 Webpack 集成,示例如下:

// 定义插件
const { declare } = require('@babel/helper-plugin-utils');

module.exports = declare((api, options) => {
  return {
    name: 'your-plugin', // 定义插件名
    visitor: { // 编写业务 visitor
      Identifier(path,) {
        // ...
      },
    }
  }
});
// 配置 babel.config.js
module.exports = {
    presets: [
        require('@babel/preset-env'), // 可配合通用的 present
    ],
    plugins: [
        require('your-plugin'),
        // require('./your-plugin') 也可以为相对目录
    ]
};

在 babel plugin 开发中,可以说就是在写 ast transform callback,不需要直接接触“@babel/parser、@babel/traverse、@babel/generator”等模块,这在 babel 内部调用了。 在需要用到 @babel/types 能力时,建议直接使用 @babel/core,从源码[1]可以看出,@babel/core 直接透出了上述 babel 模块。

const core = require('@babel/core');
const types = core.types; // const types = require('@babel/types');

四 ESLint in AST 在掌握了 AST 核心原理后,自定义 ESlint 规则也变的容易了,直接上代码:

// eslint-plugin-my-eslint-plugin
module.exports.rules = { 
  "var-length": context => ({ // 定义 var-length 规则,对变量长度进行检测
    VariableDeclarator: (node) => { 
      if (node.id.name.length <= 1){ 
        context.report(node, '变量名长度需要大于1');
      }
    }
  })
};
// .eslintrc.js
module.exports = {
  root: true,
  parserOptions: { ecmaVersion: 6 },
  plugins: [
   "my-eslint-plugin"
  ],
  rules: {
    "my-eslint-plugin/var-length": "warn" 
  }
};

体验效果 IDE 正确提示:

执行 eslint 命令的 warning:

查阅更多 ESLint API 可查看官方文档[2]。

五 获得你所需要的 JSX 解释权 第一次接触到 JSX 语法大多是在学习 React 的时候,React 将 JSX 的能力发扬光大[3]。但 JSX 不等于 React,也不是由 React 创造的。

// 使用 react 编写的源码
const name = 'John';
const element = <div>Hello, {name}</div>;
// 通过 @babel/preset-react 转换后的代码
const name = 'John';
const element = React.createElement("div", null, "Hello, ", name);

JSX 作为标签语法既不是字符串也不是 HTML,是一个 JavaScript 的语法扩展,可以很好地描述 UI 应该呈现出它应有交互的本质形式。JSX 会使人联想到模版语言,它也具有 JavaScript 的全部功能。下面我们自己写一个 babel plugin,来获得所需要对 JSX 的解释权。

1 JSX Babel Plugin 我们知道,HTML是描述 Web 页面的语言,axml 或 vxml 是描述小程序页面的语言,不同的容器两者并不兼容。但相同点是,他们都基于 JavaScript 技术栈,那么是否可以通过定义一套 JSX 规范来生成出一样的页面表现?

2 目标

export default (
  <view>
    hello <text style={{ fontWeight: 'bold' }}>world</text>
  </view>
);
<!-- 输出 Web HTML -->
<div>
  hello <span style="font-weight: bold;">world</span>
</div>
<!--输出小程序 axml -->
<view>
  hello <text style="font-weight: bold;">world</text>
</view>

目前的疑惑在于:AST 仅可用作 JavaScript 的转换,那 HTML 和 axml 等文本标记语言改怎么转换呢?不妨转换一种思路:将上述的 JSX 代码转化为 JS 的代码,在 Web 端和小程序端提供组件消费即可。这是 AST 开发的一个设计思想,AST 工具仅做代码的编译,具体的消费由下层操作,@babel/preset-react 与 react 就是这个模式。

// jsx 源码
module.exports = function () {
  return (
    <view
      visible
      onTap={e => console.log('clicked')}
    >ABC<button>login</button></view>
  );
};

// 目标:转后为更通用的 JavaScript 代码
module.exports = function () {
  return {
    "type": "view",
    "visible": true,
    "children": [
      "ABC",
      {
        "type": "button",
        "children": [
          "login1"
        ]
      }
    ]
  };
};

明确了目标后,我们要做的事为:

  1. 将 jsx 标签转为 Object,标签名为 type 属性,如 转化为 { type: 'view' }
  2. 标签上的属性平移到 Object 的属性上,如 <view onTap={e => {}} /> 转换为 { type: 'view', onTap: e => {} }
  3. 将 jsx 内的子元素,移植到 children 属性上,children 属性为数组,如 { type: 'view', style, children: [...] }
  4. 面对子元素,重复前面3步的工作。下面是实现的示例代码:
const { declare } = require('@babel/helper-plugin-utils');
const jsx = require('@babel/plugin-syntax-jsx').default;
const core = require('@babel/core');
const t = core.types;

/*
  遍历 JSX 标签,约定 node 为 JSXElement,如
  node = <view onTap={e => console.log('clicked')} visible>ABC<button>login</button></view>
*/
const handleJSXElement = (node) => {
  const tag = node.openingElement;
  const type = tag.name.name; // 获得表情名为 View
  const propertyes = []; // 储存对象的属性
  propertyes.push( // 获得属性 type = 'ABC'
    t.objectProperty(
      t.identifier('type'),
      t.stringLiteral(type)
    )
  );
  const attributes = tag.attributes || []; // 标签上的属性
  attributes.forEach(jsxAttr => { // 遍历标签上的属性
    switch (jsxAttr.type) {
      case 'JSXAttribute': { // 处理 JSX 属性
        const key = t.identifier(jsxAttr.name.name); // 得到属性 onTap、visible
        const convertAttributeValue = (node) => {
          if (t.isJSXExpressionContainer(node)) { // 属性的值为表达式(如函数)
            return node.expression; // 返回表达式
          }
          // 空值转化为 true, 如将 <view visible /> 转化为 { type: 'view', visible: true }
          if (node === null) {
            return t.booleanLiteral(true);
          }
          return node;
        }
        const value = convertAttributeValue(jsxAttr.value);
        propertyes.push( // 获得 { type: 'view', onTap: e => console.log('clicked'), visible: true }
          t.objectProperty(key, value)
        );
        break;
      }
    }
  });
  const children = node.children.map((e) => {
    switch(e.type) {
      case 'JSXElement': {
        return handleJSXElement(e); // 如果子元素有 JSX,便利 handleJSXElement 自身
      }
      case 'JSXText': {
        return t.stringLiteral(e.value); // 将字符串转化为字符
      }
    }
    return e;
  });
  propertyes.push( // 将 JSX 内的子元素转化为对象的 children 属性
    t.objectProperty(t.identifier('children'), t.arrayExpression(children))
  );
  const objectNode = t.objectExpression(propertyes); // 转化为 Object Node
  /* 最终转化为
  {
    "type": "view",
    "visible": true,
    "children": [
      "ABC",
      {
        "type": "button",
        "children": [
          "login"
        ]
      }
    ]
  }
  */
  return objectNode;
}

module.exports = declare((api, options) => {
  return {
    inherits: jsx, // 继承 Babel 提供的 jsx 解析基础
    visitor: {
      JSXElement(path) { // 遍历 JSX 标签,如:<view />
        // 将 JSX 标签转化为 Object
        path.replaceWith(handleJSXElement(path.node));
      },
    }
  }
});

六 总结

我们介绍了什么是 AST、AST 的工作模式,也体验了利用 AST 所达成的惊艳能力。现在来想想 AST 更多的业务场景是什么?当用户:

  • 需要基于你的基础设施进行二次编程开发的时候
  • 有可视化编程操作的时候
  • 有代码规范定制的时候

AST 将是你强有力的武器。

参考资料

[1]https://github.com/babel/babel/blob/main/packages/babel-core/src/index.js#L10-L14

[2]https://cn.eslint.org/docs/developer-guide/working-with-rules

[3]https://reactjs.bootcss.com/docs/introducing-jsx.html

本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/j8_8QwFnyOr66m9aekor1g

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237231次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8065次阅读
 目录