Играйте в AST как в jQuery

внешний интерфейс внешний фреймворк

Эта статья от @yohei из внешнего интерфейса Fliggy. Милая девушка учит вас, как использовать AST. Эта статья хорошо написана, и ее стоит прочитать.

Эта статья подойдет разработчикам, которые еще рвутся в аст, когда у них нет базовых знаний.В статье нет никакого профессионализма и строгости.Возможно бесполезна кроме практичности.

В Интернете уже много вводных про АСТ, мало того, что он отрывистый и сложный для понимания, так еще и обладает свойством односекундного убеждения. На самом деле мы можем узнать о чем-то, что выглядит высококлассно и атмосферно, например, AST — это черная магия, которая разлагает код в постоянно меняющееся дерево. Итак, как только мы знаем, как читать мантру, дверь в мир открывается. Интересно, что волшебное заклинание похоже на jQuery~

добро пожаловать, волшебник

Прежде чем стать магом, нам нужно подготовить четыре вещи:удобные инструменты,короткий и общийнавыки и умения, даже если вы этого не понимаете, это не влияет на использованиеавторитетный API, и непредсказуемыйвоображение.

🍭 Удобный инструмент для волшебной палочки

🔗AST exporer

Это онлайн-инструмент отладки ast. С его помощью мы можем интуитивно увидеть генерацию ast и преобразование кода. Он разделен на пять областей. Мы все полагаемся на этот инструмент для операций с кодом.

20210119160723.jpg

🔗jscodeshift

Это преобразователь ast, с помощью которого мы переводим исходный код в синтаксическое дерево ast, используем его открытый API для работы с ast и, наконец, преобразуем его в нужный нам код.

API jscodeshift основан на инкапсуляции преобразования, а синтаксис очень близок к jquery. Recast — это инкапсуляция babel/travers и babel/types.Он обеспечивает простые операции ast, а travers — это инструмент для работы ast в babel.Типы можно грубо понимать как словарь, который используется для описания типа дерева структуры.

В то же время jscodeshift также предоставляет дополнительные функции, так что разработчики могут использовать его на этапе разработки проекта или на этапе разработки, и им не нужно воспринимать процесс до и после перевода Babel, а только сосредоточиться на том, как работать или изменять дерево. , и получить результат.

Хотя у jscodeshift нет документации на китайском языке, его исходный код очень удобочитаем, что является одной из важных причин, по которой jscodeshift рекомендуется. Что касается его навыков работы с API, то он раскроется на практике.

API 📖 авторитетная книга магии

🔗babel-types

Грамматический словарь ast удобен для быстрого поиска типа дерева структуры, это один из важных инструментов, когда мы хотим сгенерировать строку кода через ast.

Познакомьтесь с АСТ

АСТ я думал

image.png

АСТ в действии

Если у нас есть такой код

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 с 1 на 2, соответствующий js также будет изменен на var a = 2 Когда я изменяю значение a имени в идентификаторе объекта на b, соответствующий js также будет изменен на var b = 2

Увидев это, я вдруг обнаружил, что эксплуатация AST не что иное, какУправление обычным набором JSON, обнаружили, что в Новом Свете есть древесина? ? Так что, пока вы понимаете правила, вы скоро сможете освоить мир! Древесина! имеют!

Понимание узлов AST

image.png

Изучите типы узлов AST

Таблица сравнения значений общего узлаimage.pngПрочитав правила, я сразу понял, что за непонятные типы в json у аста (подробности сравните пожалуйстаbabel-types), на самом деле просто словарь для описания грамматики! Оказывается, освоить мир может быть так просто! Один!

jscodeshift простая операция

найти

api Типы получить параметры описывать
find fn тип: аст тип
Находит все узлы ast типа ast, соответствующие критериям фильтра, и возвращает массив.
filter fn обратный вызов: принимает обратный вызов, по умолчанию передавая вызываемый узел ast Отфильтруйте узлы ast с указанными условиями и верните массив
forEach fn обратный вызов: принимает обратный вызов, по умолчанию передавая вызываемый узел ast Пройдите через узел ast, так же, как функция forEach js

Кроме тогонекоторые, каждый, ближайшийИспользование в основном такое же.

удалять

api Типы получить параметры описывать
remove fn тип: аст тип
фильтр: условие фильтра Находит все узлы ast типа ast, соответствующие критериям фильтра, и возвращает массив.

Добавить и изменить

api Типы получить параметры описывать
replaceWith fn узлы: восточный узел Замените узел ast или удалите, если он пуст.
insertBefore fn fn узлы: восточный узел
insertAfter fn fn узлы: восточный узел
toSource fn опции: элементы конфигурации перевод узла ast, возврат js

Кроме тогонекоторые, каждый, ближайшийИспользование в основном такое же.

разное

Операции, связанные с подузлами, такие как getAST(), nodes() и т. д. Укажите поиск узлов ast, таких как: findJSXElements(), hasAttributes(), hasChildren() и т. д.

Больше можно просматривать в консоли рабочей зоны через AST Explore или непосредственно просмотрено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 в аст
  2. Перейдите все упомянутые модули, содержащие @alifd/next в коде, и выполните следующие действия.
    1. Измените имя модуля на antd.
  3. Найдите в коде блок кода с тегом h2 и измените копию в теге.
  4. Перейдите все метки кнопок в коде и выполните следующие действия.
    1. Изменить значение атрибутов типа и размера в теге
    2. Измените текстовый атрибут в теге на type="link"
    3. Измените атрибут предупреждения в теге на опасность
  5. Возвращает js, преобразованный ast.

конечный результат

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 В основном мы используем это для библиотеки JCodeshift.
stats  --dryВозможность сбора статистики во время выполнения
report вывести переданную строку на стандартный вывод
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('转译后')]
})

Интерпретация:

  • Отфильтруйте html с тегом h2 и измените текст содержимого тега на «переведенный».
    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')
                }

            }
        });

Интерпретация:

  • Перейдите все метки кнопок в коде и выполните следующие действия.
    1. Изменить значение атрибутов типа и размера в теге
    2. Измените текстовый атрибут в теге на type="link"
    3. Измените атрибут предупреждения в теге на опасность
return root.toSource();

Интерпретация:

  • Возвращает js, преобразованный ast.

Образное воображение происходит от «ленивого»

Если мы хотим вставить большой кусок кода, согласно методу написания ast, мы должны использовать большое количество типов для генерации большого количества объектов node.Это так громоздко и ненужно.Всегда есть насильственное решение ко всему 🌝.

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

Предположим, мы хотим преобразовать предложения, такие как текстовое предложение элемента, в тег 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.

Позвольте файлу объединиться с проектом для запуска

Все вышеперечисленное основано на ast exporer, который нельзя использовать в сценариях проекта или удовлетворить инженерные потребности. Реальный инженерный сценарий не удовлетворяется документом. Если вы хотите спроектировать AST и фактически внедрить его в проект, используйте AST для реконструкции бизнес-кода и высвобождения повторяющейся работы. Следующее является хорошим решением.

Следующее основано на узле, я рекомендую два инструмента

npx & execa

Используйте npx для реализации сложной команды для создания простого кли. Выполняйте jscodeshift партиями через execa.

Код ключа выглядит следующим образом

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}`)
    }
  })
  ...
}

...