學習寫一個babel插件

前言

babel做爲現代前端項目的標配,工做中常常會用到。可是,不多人會去研究它的底層實現和設計。這篇文章是平常工做中實踐總結,將會由淺入深地和你們一塊兒學習下babel的一些基礎知識,以及編寫屬於本身的babel插件,並在項目中使用。php

AST簡介

抽象語法樹(Abstract Syntax Tree,AST),或簡稱語法樹(Syntax tree),是源代碼語法結構的一種抽象表示。它以樹狀的形式表現編程語言的語法結構,樹上的每一個節點都表示源代碼中的一種結構前端

AST生成過程

  1. 分詞 / 詞法分析: 將一個語句中的關鍵詞進行提取, 例如let a = 3; 分詞提取以後獲得let, a, =, 3node

  2. 解析 / 語法分析: 在對上面已經被拆分提取過的關鍵詞進行分析以後創建一課語法樹(AST)git

  3. 底層代碼生成: 獲得語法樹以後執行引擎(例如 chrome 的 v8引擎)會對這顆樹進行必定的優化分析, 而後生成更底層的代碼或者機器指令交由機器執行github

babel工具簡介

Babel is a compiler for writing next generation JavaScriptchrome

babel三件套

  • 解析:@babel/parse
    • 詞法解析
    • 語法解析
  • 遍歷:@babel/traverse
  • 生成:@babel/generator
import * as parser from "@babel/parser";
import traverse from "@babel/traverse";
import generate from '@babel/generator';

//源代碼
const code = `function square(n) {
  return n * n;
}`;

//解析爲ast結構
const ast = parser.parse(code, {
  // parse in strict mode and allow module declarations
  sourceType: "module",

  plugins: [
    // enable jsx and flow syntax
    "jsx",
    "flow"
  ]
});

//進行遍歷,修改節點
//第二個參數是一個訪問者對象,定義遍歷時的具體轉換規則,囊括本文95%的重點
traverse(ast, {
  enter(path) {
    if (path.isIdentifier({ name: "n" })) {
      path.node.name = "x";
    }
  }
});
//將修改後的ast結構,生成從新的代碼
const output = generate(ast, { /* options */ }, code);
複製代碼

整個流程最核心的就是traverse部分,接下來咱們回顧下traverse的核心知識npm

如何去編寫一個babel插件

Babel 是 JavaScript 編譯器,更確切地說是源碼到源碼的編譯器,一般也叫作「轉換編譯器(transpiler)」。 意思是說你爲 Babel 提供一些 JavaScript 代碼,Babel 更改這些代碼,而後返回給你新生成的代碼編程

遍歷

Babel 或是其餘編譯器中最複雜的過程 同時也是插件將要介入工做的部分。小程序

首先熟悉下常見的js結構對應的ast節點類型數組

//functionDeclaration
function square(n) {
  return n * n;
}

let a = {
	test(){},  //ObjectMethod
  	setOnly: function(){}   //ObjectProperty
}

let b = 3;  //VariableDeclaration
b = 5;   //AssignmentExpression
複製代碼

訪問者(visitor)

訪問者是一個用於 AST 遍歷的跨語言的模式。 簡單的說它們就是一個對象,定義了用於在一個樹狀結構中獲取具體節點的方法

const MyVisitor = {
  //完整寫法
  functionDeclaration: {
    enter(path) {
      console.log("Entered!");
    },
    exit(path) {
      console.log("Exited!");
    }
  },
  //經常使用寫法
  functionDeclaration(path){
  },
  
  ObjectMethod | ObjectProperty: {
    enter(path) {
      console.log("Entered!");
    },
    exit(path) {
      console.log("Exited!");
    }
  }

  ...
};
複製代碼

Path:

visitor對象每次訪問節點方法時,都會傳入一個path參數。Path 是表示兩個節點之間鏈接的對象。這個對象不只包含了當前節點的信息,也有當前節點的父節點的信息,同時也包含了添加、更新、移動和刪除節點有關的其餘不少方法

+ 屬性      
  - node   當前節點
  - parent  父節點
  - parentPath 父path
  - scope   做用域
  - context  上下文
  - ...
+ 方法
  - findParent  向父節點搜尋節點
  - getSibling 獲取兄弟節點
  - replaceWith  用AST節點替換該節點
  - replaceWithSourceString  用代碼字符串替換該節點
  - replaceWithMultiple 用多個AST節點替換該節點
  - insertBefore  在節點前插入節點
  - insertAfter 在節點後插入節點
  - remove   刪除節點

複製代碼

AST實戰講解

1. 打開在線AST工具

高亮的是對應的代碼段,左邊是一個對象的屬性,右邊對應ast中的節點信息。

注意:js中不一樣的數據類型,對應的ast節點信息也不竟相同。以圖中爲例,externalClasses對象的節點信息中類型(type)是ObjectProperty,包含key ,value等關鍵屬性(其餘類型節點可能就沒有)

2. 打開transform開關,選擇轉換引擎,發現了新大陸

圖片
這裏咱們選擇babel和配套的babylon7,能夠根據實際須要本身選擇,這只是推薦。

注意選擇最新的babel7版本,否則下面例子中的類型會匹配不上,

3. 如今的界面結構展現以下圖,接下來就開始進行轉換邏輯的代碼編寫

假設咱們的目標是要把properties屬性中key爲‘current’的屬性改成myCurrent。let's go!

原始代碼:

/*eslint-disable*/
/*globals Page, getApp, App, wx,Component,getCurrentPages*/
Component({
  externalClasses: ['u-class'],

  relations: {
    '../tab/index': {
      type: 'child',
      linked() {
        this.changeCurrent();
      },
      linkChanged() {
        this.changeCurrent();
      },
      unlinked() {
        this.changeCurrent();
      }
    }
  },

  properties: {
    current: {
      type: String,
      value: '',
      observer: 'changeCurrent'
    }
  },

  methods: {
    changeCurrent(val = this.data.current) {
      let items = this.getRelationNodes('../tab/index');
      const len = items.length;

      if (len > 0) {
        items.forEach(item => {
          item.changeScroll(this.data.scroll);
          item.changeCurrent(item.data.key === val);
          item.changeCurrentColor(this.data.color);
        });
      }
    },
    emitEvent(key) {
      this.triggerEvent('change', { key });
    }
  }
});

複製代碼

首先在原始代碼中選中'current',查看右邊ast的節點結構,如圖:

這是一個對象屬性(ObjectProperty),關鍵節點信息爲key和value,key自己也是一個ast節點,類型爲Identifier(準確的應該是StringIdentifer,經常使用的還有NumberIdentifer等),'curent'是裏面的name屬性。因此咱們的第一步就是找到改節點,而後修改它。

查找

export default function (babel) {
  const { types: t } = babel;
  
  return {
    name: "ast-transform", // not required
    visitor: {
      Identifier(path) {
        //path.node.name = path.node.name.split('').reverse().join('');
      },
       ObjectProperty(path) {
         if (path.node.key.type === 'StringIdentifier' && 
             path.node.key.name === 'current') {
         	console.log(path,'StringIdentifier')
         }
  	   }
    }
  };
}

複製代碼

這裏須要用到@babel/typesbabeljs.io/docs/en/bab…來輔助咱們進行類型判斷,開發中會很是依賴這個字典進行查找

在控制檯會看見,path下面的節點信息不少,關鍵字段爲node和parentPath,node記錄了該節點下數據信息,例如以前提到過的key和value。parentPath表明父級節點,此例中表示ObjectExpression中properties節點信息,有時咱們須要修改父節點的數據,例如常見的節點移除操做。接下來咱們修改該節點信息。

修改

@babel/types中找到該ObjectProperty的節點信息以下,咱們須要須要構造一個新的同類型節點(ObjectProperty)來替換它。

能夠看到關鍵信息是key和value,其餘使用默認就好。value裏面的信息咱們能夠照搬,從原有的path裏面獲取,咱們更改的只是key裏面的標識符'current'。由於key自己也是一個ast節點,因此咱們還須要查看字典,看看生成Identifier節點須要什麼參數,步驟同樣。修改代碼以下:

ObjectProperty(path) {
         console.log(path,'ObjectProperty--')
         if (path.node.key.type === 'Identifier' && 
             path.node.key.name === 'current') {
            //替換節點
           path.replaceWith(t.objectProperty(t.identifier('myCurrent'), path.node.value));
         }
  	   }
複製代碼

其中咱們用到了replaceWith方法,這個方法表示用一個ast節點來替換當前節點。 還有一個經常使用的replaceWithSourceString方法,表示用一個字符串來代替該ast節點,參數爲一串代碼字符串,如:'current : {type:String};',感興趣的,能夠本身試試。

最後查看轉換後的代碼,發現'current'已經被咱們替換成了'myCurrent'。

到這裏,一個完整的例子就演示完了。這裏補充說明一下,在實際中可能會遇到嵌套結構比較深的ast結構。咱們須要嵌套類型判斷,好比:

ObjectProperty(path) {
     console.log(path,'ObjectProperty--')
      MemberExpression(memberPath) {
          console.log(path,'memberPath--')
      }
 }
複製代碼

由於遍歷中的path指定的是當前匹配的節點信息。因此能夠爲不一樣的類型遍歷指定不一樣的path參數,來獲取當前遍歷的節點信息,避免path覆蓋,例如上面的path和memberPath。

到這裏,babel的基本用法就差很少介紹完了,想要熟練掌握,還須要你在項目中反覆練習和實踐。想系統學習babel,並在實際項目中使用的同窗能夠先看看這篇babel的介紹文檔,邊寫邊查,鞏固學習

Babel的實際應用

小程序的主要差別對比:

  1. 自定義組件不支持relations的關係申明

  2. 不支持getRelationNodes 的API調用

  3. transition動畫數據結構不一樣

  4. onLaunch, onShow, onLoad中不支持使用selectComponentselectAllComponents

  5. 微信的wxs語法

  6. 登陸流程,百度系使用passport,非百度系使用Oauth

代碼展現

relations爲例,進行演示,完整項目請查看互轉工程項目

微信的使用demo:

relations: {
    './custom-li': {
      type: 'child', // 關聯的目標節點應爲子節點
      linked: function(target) {
        // 每次有custom-li被插入時執行,target是該節點實例對象,觸發在該節點attached生命週期以後
      },
      linkChanged: function(target) {
        // 每次有custom-li被移動後執行,target是該節點實例對象,觸發在該節點moved生命週期以後
      },
      unlinked: function(target) {
        // 每次有custom-li被移除時執行,target是該節點實例對象,觸發在該節點detached生命週期以後
      }
    }
  },
複製代碼

互轉源碼:

let linkedBody = '';
if (path.node.type === 'ObjectProperty' && path.node.key.name === 'relations') {
        //獲取到relations屬性中type的value
        //獲取到relations屬性中linked函數
        let componentName = '';
        let relationsValue = '';
        path.traverse({
            ObjectMethod(path) {
                if (path.node.key.name === 'linked') {
                    linkedBody = path.node.body;
                }
            },
            ObjectProperty(path) {
                if (path.node.key.type === 'StringLiteral' && path.node.key.value) {
                    relationsValue = path.node.key.value || '';
                    let index = relationsValue.lastIndexOf('./');
                    let lastIndex = relationsValue.lastIndexOf('/');
                    componentName = relationsValue.substring(index + 2, lastIndex);
                }
                // '../grid/index''grid'
                if (path.node.key.name === 'type') {
                    if (context.isDesgin) {
                        //添加組件庫前綴
                        componentName = 'u-' + componentName;
                    }
                    let action = path.node.value.value === 'parent' ? 'relationComponentsParent' : 'relationComponentsChild';
                    contextStore.dispatch({
                        action,
                        payload: componentName
                    });
                    relationsMap[relationsValue] = path.node.value.value;
                }
            }
        });
        if (!linkedBody) {
            path.remove();
            return;
        } else {
            path.replaceWith(t.objectMethod('method', t.identifier('attached'), [], linkedBody, false));
        }
    }
複製代碼

組件庫的按需加載:

使用組件庫的時候,不想打包全部組件,只打包項目中引入的組件

按需實現源碼:

visitor: {
    ImportDeclaration(path, {opts})
    {
        const specifiers = path.node.specifiers;
        const source = path.node.source;

        // 判斷傳入的配置參數是不是數組形式
        if (Array.isArray(opts)) {
            opts.forEach(opt => {
                assert(opt.libraryName, 'libraryName should be provided');
            });
            if (!opts.find(opt => opt.libraryName === source.value)) return;
        } else {
            assert(opts.libraryName, 'libraryName should be provided');
            if (opts.libraryName !== source.value) return;
        }

        const opt = Array.isArray(opts) ? opts.find(opt => opt.libraryName === source.value) : opts;
        opt.camel2UnderlineComponentName = typeof opt.camel2UnderlineComponentName === 'undefined'
            ? false
            : opt.camel2UnderlineComponentName;
        opt.camel2DashComponentName = typeof opt.camel2DashComponentName === 'undefined'
            ? false
            : opt.camel2DashComponentName;

        if (!t.isImportDefaultSpecifier(specifiers[0]) && !t.isImportNamespaceSpecifier(specifiers[0])) {
            // 遍歷specifiers生成轉換後的ImportDeclaration節點數組
            const declarations = specifiers.map((specifier) => {
                // 轉換組件名稱
                const transformedSourceName = opt.camel2UnderlineComponentName
                    ? camel2Underline(specifier.imported.name)
                    : opt.camel2DashComponentName
                        ? camel2Dash(specifier.imported.name)
                        : specifier.imported.name;
                // 利用自定義的customSourceFunc生成絕對路徑,而後建立新的ImportDeclaration節點
                return t.ImportDeclaration([t.ImportDefaultSpecifier(specifier.local)],
                    t.StringLiteral(opt.customSourceFunc(transformedSourceName)));
            });
            // 將當前節點替換成新建的ImportDeclaration節點組
            path.replaceWithMultiple(declarations);
        }
    }
}
複製代碼

而後安裝babel-cli工具,將代碼打包,發佈到npm,就能夠在項目中使用了。若是再優化完善下,是否是就能夠把現有項目中ant-design的按需加載功能移除了。。。

在項目中設置.babelrc文件,增長自定義插件配置

效果:

//以前
import { button, table } from 'union-design';

//如今
import button from 'union-design/src/components/button/index.js';
import table from 'union-design/src/components/table/index.js';
複製代碼
相關文章
相關標籤/搜索