像玩 jQuery 一樣玩 AST

2021-02-12 程式設計師黑叔
本文來自飛豬前端的 @呦嘿 同學,萌妹子手把手教你使用 AST,這篇文章寫得很不錯值得一讀。

這篇文章適合在原理性知識不通的情況下,仍然對 ast 蠢蠢欲動的開發者們,文章不具備任何專業性以及嚴謹性,它除了實用,可能一無是處。

關於 AST 的介紹,網上已經一大堆了,不僅生澀難懂,還自帶一秒勸退屬性。其實我們可以很(hao)接(bu)地(yan)氣(jin)的去了解一個看上去高端大氣的東西,比如,AST 是一個將代碼解構成一棵可以千變萬化的樹的黑魔法。所以,只要我們知道咒語怎麼念,世界的大門就打開了。有趣的是,魔法咒語長得像 jQuery~

歡迎你,魔法師

在成為一名魔法師之前,我們需要準備四樣東西:趁手的工具、又簡短又常用的使用技巧,即使看不懂也不影響使用的權威 api、 以及天馬行空的想像力。

🍭 魔法棒 之 趁手的工具🔗 AST exporer

這是一個 ast 在線調試工具,有了它,我們可以非常直觀的看到 ast 生成前後以及代碼轉換,它分五個區域。我們接下來都依賴這個工具進行代碼操作。

🔗 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實際中的 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 節點探索 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() 等。

更多可通過 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();
}

複製代碼

該例代碼大致解讀如下

遍歷代碼中所有包含 @alifd/next 的引用模塊,並做如下操作找到代碼中標籤名為 h2 的代碼塊,並修改該標籤內的文案。改變標籤中 text 屬性變為 type = "link"

最終輸出結果

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將傳遞的字符串列印到 stdoutoptions

執行 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 的引用模塊,並做如下操作
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')
                }

            }
        });
複製代碼

解讀:

改變標籤中 text 屬性變為 type = "link"
return root.toSource();
複製代碼

解讀:

天馬行空的想像力來自於 「懶」

假如我們想插入一大段代碼,按照 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
    有趣的是,魔法咒語長得像 jQuery~歡迎你,魔法師在成為一名魔法師之前,我們需要準備四樣東西:趁手的工具、又簡短又常用的使用技巧,即使看不懂也不影響使用的權威 api、 以及天馬行空的想像力。🔗 jscodeshift它是一個 ast 轉換器,我們通過它來將原始代碼轉譯成 ast 語法樹,並借用其開放的 api 操作 ast,最終轉換成我們想要的代碼。jscodeshift 的 api 基於 recast 封裝,語法十分接近 jquery。
  • 如何像玩遊戲一樣學英語
    她說「懂你英語」是「英語流利說」APP開發的一個英語課程,在手機上就能學,像遊戲一樣,有升級闖關。「學習動力一下就不一樣了,雖然最近年底開會、出差忙到飛起,但每天不練個半小時英語,就不舒服,開啟起早貪黑學習模式。」她說。我因為希望以後能滿世界工作和玩兒,內心一直想學好英語,但因為懶,經常學一個月休三個月。
  • AST抽象語法樹:最基礎的 Javascript 重點知識
    通過抽象語法樹解析,我們可以像童年時拆解玩具一樣,透視Javascript這臺機器的運轉,並且重新按著你的意願來組裝。現在,我們拆解一個簡單的add函數function add(a, b) {    return a + b}首先,我們拿到的這個語法塊,是一個FunctionDeclaration(函數定義)對象。
  • 教程:---《JQuery如何工作》
    最近發現jquery很成熟了,我看了jquery官方的document,我想,如果沒有人翻譯過,我打算邊看文檔,順道邊翻譯。如果有人發現已經有人翻譯過了,勞煩通知一下,我就不做重複勞動了。這裡先提供入門章節內容。
  • 怕什麼玩樂無窮,玩一樣有一樣的歡喜!
    怕什麼玩樂無窮,玩一樣有一樣的歡喜! 看到篇講趙元任的文章,說這人特會玩。玩數學,玩哲學,玩音樂,玩30多種方言十幾門外語,憑著學來的地道方言,騙倒無數「淚汪汪」的「老鄉」。當年學霸罵人神功還不像現在那麼厲害,所以鬥嘴一般是小V贏的多,不過很快老師就幫他報仇了,被老師叫起來提問的總是我,不會是學霸。也是,只有他才有資格不聽課。現在想想,也感謝老師的包容,只是被叫起來回答問題而已,從來沒被訓斥或罰站。印象中,學霸一直在玩,打80分,打橋牌,下圍棋,踢足球,看武俠小說……而且在我們那個小城,他玩什麼都是孤獨求敗。
  • 像玩Angry Birds一樣練口語
    至少,它做到了隨時隨地練口語,即時練習即時得分,更重要的,它的闖關模式讓人們感覺,英語學習也可以像玩Angry Birds那樣富有挑戰的鬥志,以及有趣。「學習英語口語是一件很苦逼的事情。我們希望讓這個過程更好玩一點」。英語流利說聯合創始人王翌這樣說。
  • 三和大神直播進廠:像撿錢一樣,就是沒有妹子玩
    三和大神直播進廠:像撿錢一樣,手機隨便玩隨著現在學生工返校,現在在深圳,很多工廠都出現了用工荒,就是待遇一再提高,卻總是招不滿工人。三和大神這位三和大神今天發了一個這樣的帖子:美滋滋上班就像撿錢一樣 手機隨便玩 就是沒有妹子。管理這麼松,估計也就是倉庫搬運、保安之類的不需要技術含量,不需要學歷保證的工作吧。
  • 動手編譯自定義版本的最新 jQuery 類庫?
    你是不是覺得jQuery類庫不夠靈活,不像其它的框架或者類庫,例如,dojo那樣一樣可以動態的加載模塊,或者你在你的項目中沒有使用jQuery提供的所有功能,比如,不需要AJAX相關功能,只需要DOM相關的操作功能。
  • jquery與js的區別是什麼?js與jquery的用法區別介紹
    在學習js的時候我們肯定能夠接觸到jquery,那麼,js與jquery之間有什麼區別呢?本篇文章將給大家來分享關於jquery與js之間的區別比較,有需要的小夥伴可以參考一下,希望能夠幫助到你們。我們來簡單看一下jquery與js的概念。js是一種腳本語言,常用於網頁客戶端編程,使網頁在客戶端瀏覽器中,實現更多地動態功能,表現出更加豐富的視覺效果。jquery是一個快速、簡潔的JavaScript框架,極大的簡化了javascript編程。
  • 讓工作像玩遊戲一樣快樂?用友雲做到了
    「為什麼有些青少年在網吧裡玩遊戲玩三天三夜、廢寢忘食也不回家?」在2018用友雲新品發布會上,用友網絡CTO程操紅向與會嘉賓拋出了一個非常好的問題。如何讓工作像玩遊戲一樣快樂?企業服務提供商用友已經潛心探索並實踐出來了。4月19日,用友雲發布數位化企業工作入口、實時會計 智能財務、用友雲營銷服務新品——友零售三款雲服務新品。
  • 全球第一的Magformers,像樂高一樣玩!
    除了送孩子到外面上相關課程,在家還要玩STEM玩具!在玩樂和操作過程中,來提升思維邏輯、空間構建能力。 Magformers(麥格弗)磁力片玩具是一款會讓人驚豔的STEM玩具,3歲以上的孩子就能玩!他們家有的產品,可以搭出 1 個摩天輪:
  • 這種手工像玩泥巴一樣簡單有趣,DIY的小花飾品,每一個都很驚豔
    昨天有個可愛的姑娘問我,有沒有一種手工,像玩泥巴一樣有趣,做出來的成品還得很精緻,並且能夠保存很久,不容易壞!哈哈哈,當然有啊,聚合物粘土或軟陶,樹脂粘土不就是嗎!姑娘,記得點讚哈!平時會用到的首飾或飾品就能自制,那些普通的陶瓷物品或玻璃物品上粘上幾朵就能當藝術品一樣送人哦。像玩泥巴一樣的手工材料,一起玩起來吧!更多的手工教程可以到我主頁查看哦~
  • 像玩電子遊戲一樣的說英語?是的!
    於是後面給自己不斷找資料,繼續提升聽說讀寫的能力,直到YouTube推薦了我一則TED演講(大數據就是這麼的暖心,哈哈)---《像玩遊戲一樣的說英文》,我才發現自己的「病灶」在哪裡。
  • jquery中$的作用與使用方法總結
    使用jquery.js框架使用起來方便多了!突然相起來把以住的學習資料整理一下放在這樣的一個個人知識庫中存儲下來。總體感覺Jquery.js框架最有用,這裡的有用指的就大不了就是使用起來更好理解,更方便,代碼更少,jquery體系中最重要的工具就那個代表國際財富的$!
  • 六成網友不玩手機睡不著覺 稱像憋尿一樣難受
    超六成網友不玩手機睡不著覺「關上電腦去睡覺,躺在床上玩手機。」微博上非常流行這句話。記者調查顯示,超過六成的網友,不玩手機睡不著。但是,有科學研究表明,睡前玩手機超1小時,睡眠質量會受到影響。心理專家建議,戒掉睡前玩手機的習慣有點像戒菸,關鍵是從心理上戒掉。故事>>睡前不玩手機就像憋了尿一樣難受小江是典型的手機控,等公交、吃飯前、開會時,都會盯著手機屏幕「劃拉劃拉」,手機快成了她身體的一部分。
  • 怎麼像玩遊戲一樣愛上學習?原來學習真的讓人快樂!
    但玩遊戲就不一樣了,一玩就上頭,一直玩一直上頭,它滿足了即刻反饋、即刻滿足兩大條件,讓我們的大腦不由自主的上癮。只拿最簡單的消消樂類遊戲來說,不少人都會玩到上千關,一直玩下去。為啥這麼原理簡單的遊戲,卻有這麼多人痴迷?
  • JQuery的動畫操作
    人工智慧時代的動畫壹 · JQuery的介紹Write less,Do more1.Jquery是目前使用最為廣泛的javascript函數庫,jquery是一個函數庫,一個js文件,頁面用script標籤引入即可,<script type="text/javascript" src="jquery-3.4.1.js">2.JQuery加載將獲取元素的語句寫到頁面頭部,會因為元素還沒有加載而出錯,就要用ready來進行解決
  • jQuery.each()方法
    <head><meta charset="utf-8"><title>jQuery.each()方法</title><script src="https://cdn.staticfile.org/jquery
  • jQuery學習筆記
    /// w3school教程https://www.w3school.com.cn/jquery/index.asphttps://jquery.com/download/// 第一個為壓縮版本(將複雜的變量名簡化
  • 像玩積木一樣蓋高樓大廈
    廣州日報訊 (全媒體記者蔣幸端)以後蓋房子就像玩積木,使用機器將預製好的部件在工地上進行裝配