最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 像玩 jQuery 一样玩 AST

    正文概述 掘金(飞猪前端团队)   2021-02-01   549

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

    欢迎你,魔法师

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

    ? 魔法棒 之 趁手的工具

    ? AST exporer

    像玩 jQuery 一样玩 AST

    ? jscodeshift

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

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

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

    ? 魔法书 之 权威api

    ? babel-types

    认识AST

    我以为的AST

    像玩 jQuery 一样玩 AST

    实际中的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节点

    像玩 jQuery 一样玩 AST

    探索AST节点类型

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

    jscodeshift 简易操作

    查找

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

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

    删除

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

    添加 & 修改

    api类型接收参数描述
    replaceWithfnnodes:ast节点替换ast节点,如果为空则表示删除insertBeforefnfnnodes:ast节点insertAfterfnfnnodes:ast节点toSourcefnoptions: 配置项ast节点转译,返回js

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

    其它

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

    命令

    // -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: { name: { name: 'h2' } }})
      .forEach((path) => {
      // jsxText 对应 text
      	path.node.children = [j.jsxText('转译后')]
    })
    

    解读:

    • 筛选标签为h2的html,更改该标签的内容的text为“转译后”
        root
        		// 筛选Button的 element开放句式
            .find(j.JSXOpeningElement, { name: { name: 'Button' } })
    				// JSXAttribute 对应 element 的 attribute 句式, 如 type="normal" ...
            .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") {
                  	// 判断该ast节点的兄弟节点是否存在 type,
                    // 如果有,则修改type的值为link,如果没有则改变当前节点为type=“link”
                    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 {
                      	// stringLiteral 对应 string类型字段值
                        path.node.value = j.stringLiteral('link')
                    }
    
                }
            });
    

    解读:

    • 遍历代码中所有Button标签,并做如下操作
      1. 改变标签中type和size属性的值
      2. 改变标签中text属性变为 type = "link"
      3. 改变标签中warning属性为danger
    return root.toSource();
    

    解读:

    • 返回由ast转换后的js。

    天马行空的想象力来自于“懒”

    假如我们想插入一大段代码,按照ast的写法就得使用大量的type生成大量的节点对象,如此繁琐,大可不必,万事总有暴力解决法 ?。

    const formRef = j('const formRef = React.createRef();').nodes()[0].program.body[0]
    path.insertAfter(formRef)
    

    假如我们想句式转换,比如element的text句式转attr标签。

    const getStringEle = (source) => {
        if (Array.isArray(source)) {
            let arr = []
            source.forEach((item, i, items) => {
                if (!item.replace(/\s+|\n/g, '').length && i!==0 && i!== (items.length - 1 )){
                    arr.push('<></>')
                }
                arr.push(item)
            })
            return arr.join('')
        } else {
            return source
        }
    }
    
    ...
    .find(j.JSXAttribute)
    .forEach(path => {
      const attrVal = ((path.node.value || {}).expression || {}).value ? path.node.value.expression : path.node.value
    	const childrenEleStr = getStringEle(j(path).toSource())
      
      j(path).replaceWith(j.jsxIdentifier(
        `attr={[${childrenEleStr.replace(/<><\/>/g, ',')}]}`
      ))
      
    })
    
    

    掌握更多的链式写法,就能玩出更多的花样~ 这点和jQuery如出一辙。

    让文件结合工程run起来

    以上我们都基于ast exporer,并不能实用于项目场景,或者满足工程需要。 真实的工程化场景,并不满足于一份文件,如果想让ast工程化,真正的落实在项目中,利用ast重构业务代码,解放重复的劳动力,以下是一个很好的解决思路。

    以下基于node,我推荐两个工具

    npx & execa

    利用npx实现一个复杂命令,来创建一个简易cli。通过execa批量执行jscodeshift。

    关键代码如下

    package.json
      "bin": {
        "ast-cli": "bin/index.js"
      },
    
    index.js
    #! /usr/bin/env node
    require('./cli').main()
    
    main()
    ...
    
    const path = require('path')
    const execa = require('execa');
    const jscodeshiftBin = require.resolve('.bin/jscodeshift');
    
    module.exports.main = async () => {
    	...
      const astFilesPath = ...
      astFilesPath.forEach(async (transferPath, i) => {
        const outdrr = await execa.sync(jscodeshiftBin, ['-t', transferPath, src])
        if (outdrr.failed) {
          console.log(`编译出错: ${outdrr}`)
        }
      })
      ...
    }
    
    ...
    
    

    起源地下载网 » 像玩 jQuery 一样玩 AST

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元