babel plugin開發思考

babel plugin開發思考

babel 定義

babel 就是把ecma較新的js語法翻譯成瀏覽器能夠識別的解釋器,具體詳見 babel官網css

babel plugin

  • 關於 plugin 的設計結構node

    plugin 是一個很常見的設計結構了,往前看 jquery的時期,jquery 暴露了一個extend 方法,把插件都掛$.extend 下react

    以後像webpack這樣的plugin,就是註冊了webpack的生命週期鉤子,而後到了生命週期觸發jquery

  • 關於 plugin 的設計理念webpack

    通常都是某個工具提供一個core,作最核心的運算,其餘部分功能開放給第三方,jquery webpack babel 都是這樣的。git

babel 實踐

基礎知識

若是你時間很充裕的話,能夠先看一下 官方的handbook,知識點很全,知識點不少,可能消化要一點時間,可是仍是建議讀一下。github

handbookweb

提煉一些比較基礎,重要的知識點瀏覽器

babel-core會把代碼轉成ast樹,ast 是一個個節點信息,例如緩存

源碼: export default class {} 對應的 ast 節點就是

{
	type: ExportDefaultDeclaration
	start: 501,
	end: 2128
	declaration: {....}
}
複製代碼

ast 節點通常是不建議直接操做的,緣由很簡單ast的node 信息比較獨立,babel 把這些獨立的 node 經過一些描述信息拼接成了一個program(總體),這些描述信息的最小單元就是path,path 還暴露了對節點添加 刪除 移動的方法,比直接修改ast 也方便不少

path 和 ast 用一個比較官方的說法是reactive的,簡單說就是ast 變了, path 也變了,path 變了,ast 也變了 是個雙向綁定

實踐

  • 一個最基礎的例子 對單文件transform 通常例子以下

    var babel = require('babel-core');
      var t = require('babel-types');
      const code = `import {uniq, extend, flatten, cloneDeep } from "lodash"`;
      const visitor = {
          Identifier(path){
              console.log(path.node.name)
          }
      }
      const result = babel.transform(code, {
          plugins: [{
              visitor: visitor
          }]
      })
    複製代碼

聲明 Identifier 類型的節點,parse 到 Identifier 時 輸出node 名字 輸出: uniq extend flatten cloneDeep

  • 稍微複雜的例子

現狀:項目中樣式存在污染問題,通常代碼組織 一個jsx 對應一個scss文件,可是項目中的部分scss文件存在這樣的寫法

input{

}

textarea{

}

.form{

}
複製代碼

對應的 jsx 代碼

render () {
	return (<div> 	
		....
	</div>)
複製代碼

目標:會parse jsx,若是最外層元素沒有設置classname,就手動加上classname = {$__dirname}_container 這樣, 而後把這個 class 包到scss文件最外面,對應的scss文件

.$__dirname}_container{
	input{
	
	}
	
	textarea{
	
	}
	
	.form{
	
	}
}
複製代碼

對應的 jsx 代碼

render () {
	return (<div className =`{$__dirname}_container`> 
		....
	</div>)
複製代碼
分析問題

項目中的文件上千個,確定不能經過手動babel一個一個文件執行,確定要結合webpack,由於webpack自動會幫咱們收集依賴,有優點。

基本寫法,bable.config.js 寫下以下代碼

module.exports = {
  "presets": [
    "@babel/preset-env",
    "@babel/preset-react"
  ],
  "ignore": [
    "node_modules/**",
    "dist"
  ],
  "env": {
    "test": {
      "plugins": ["@babel/plugin-transform-runtime"]
    }
  },
  "plugins": [
    ["founder-transform", {test: 'fz'}],      //測試插件
    ["syntax-dynamic-import"],
    ["@babel/plugin-proposal-export-default-from"],
    ["@babel/plugin-proposal-export-namespace-from"],
    ["@babel/plugin-proposal-decorators", { "legacy": true }], 
    ["@babel/plugin-proposal-class-properties", { "loose" : true }], 
    ["import", {
      "libraryName": "mtui-d",
      "libraryDirectory": "lib",
      "style":true
      }
    ]
  ]
}
複製代碼

這裏有個注意點,咱們自定義的插件必定要寫在plugin最前面,關於plugin的執行順序,引用官網的話

  • 插件在 Presets 前運行。
  • 插件順序從前日後排列。
  • Preset 順序是顛倒的(從後往前)。

大體就是。plugin: founder-transform ...... import -> presets: @babel/preset-react @babel/preset-env

preset 也是一系列插件啦,因此你能夠想象時一排插件對ast進行改寫

#####開始實踐

  1. 提取jsx文件,捕獲 import 'xxxx.scss' 記錄scss 文件路徑
  2. 提取到 export default class 裏 render 最外層元素, 若是沒有classname, 就加上classname,而後去對應的scss 文件封上一層 classname
  3. 若是外層有classname,可是對應的scss文件沒有出現classname 就把path記錄下來,手動確認是否有污染

是否是很簡單?

主流程就兩步

  1. 在 render 的jsx 最外層綁個classname
  2. 而後去 把classname 封到最外層

第二步就不說了,兩行代碼解決

let content = fs.readFileSync(path);

fs.writeFileSync(path, 'utf-8')

第一步也很簡單,嘩嘩寫下以下代碼,截取關鍵代碼吧

  1. 提取jsx文件,捕獲 import 'xxxx.scss' 記錄scss 文件路

    ImportDeclaration(path, _ref = {opts:{}}){
         co(function *(){
             const specifiers = path.node.specifiers;
             const {value} = path.node.source;
             if(specifiers.length === 0 && value.slice(-5) === ".scss" && _ref.filename.indexOf('/container/') > -1){   //只對container目錄下文件改造
                 const cssPath = paths.join(_ref.filename, '..' , value);
                 let exist = yield statPromise(cssPath);
                 let source = exist && fs.readFileSync(cssPath).toString()
    
                 if(!containerJSCSS[_ref.filename]){
                     containerJSCSS[_ref.filename] = {}
                 }
    
                 if(!containerJSCSS[_ref.filename].cssPath){
                     containerJSCSS[_ref.filename].cssPath = []   
                 }
    
                 containerJSCSS[_ref.filename].filename = _ref.filename;
                 containerJSCSS[_ref.filename].cssPath.push(cssPath);
                 containerJSCSS[_ref.filename].cssSource = source;
             }
         })
    複製代碼

    },

  2. 在 render 的jsx 最外層綁個classname

    ExportDefaultDeclaration(path, _ref = {opts: {}}){
            const declaration = path.node.declaration;
            let code = _ref.file.code;
            let ast = _ref.file.ast;       //class 
            if(_ref.filename.indexOf('/container/') > -1 && declaration.type === "ClassDeclaration"){
                if(declaration && declaration.body){      // render return()
                    let {body: declarationBody} = declaration.body;
                    let render = declarationBody && declarationBody.find(_ => {
                        return _.key.name === "render" && _.type === "ClassMethod" 
                    })
    
                if(render && render.body){
                    let {body: renderBody} = render.body;
                    let returnStatement = renderBody.find(_ => _.type === "ReturnStatement") || {};
                    let {argument} = returnStatement;
    
                    if(argument && argument.type === "JSXElement"){ // render return (<> </>)
                        let {openingElement: {attributes, name}} = argument;
                        if(name.type === "JSXIdentifier"){
                            if(!containerJSCSS[_ref.filename]){
                                containerJSCSS[_ref.filename] = {}
                            }
                            containerJSCSS[_ref.filename]['wrapElement'] = name.name
                        }
                        let attributesClass = attributes.find(_ => {
                            return _.name && _.name.name !== "className"
                        })
    
                        containerJSCSS[_ref.filename]['wrapElementClass'] = !!attributesClass
                        containerJSCSS[_ref.filename]['filename'] = _ref.filename;
    
                       
                        attributes.push(t.jSXAttribute(t.JSXIdentifier('className'), t.stringLiteral(_ref.filename)));		//綁classname
                        
                        const output = generate(ast, {}, code);  
                        output.code += `/** ${_ref.filename} **/\n`;
                        fs.writeFile('./data.jsx', output.code, {
                            flag: 'a'
                        }, (err) => {
                            if (err) throw err;
                        })
                    }
                }
            }
        }
    }
    複製代碼

代碼很簡單,就是一步一步獲取export class 下的 render return( ) A元素罷了

可是當代碼跑起來的時候,出現了兩個問題

  • const output = generate(ast, {}, code); 發現code 不是源文件,裏面出現了es5的代碼
  • 咱們在babel plugin 聲明的 containerJSCSS 怎麼傳出去呢?我須要最終的結果,最後還須要一些批量的操做

第一個問題很簡單,拆分一下

  • babel plugin 調用的時機?

    webpack 自己的設計是管道的,一個文件處理完,下一個文件進來,拿到第一個jsx 過一遍babel-loader, 而後一次執行 plugin 到 preset的插件

  • babel-loader 作了啥?

    關鍵代碼 result = yield transform(source, options);

    不看babel-loader其餘優化,最基礎的操做就是transform了,source是源文件,option是參數,babel-loader傳的參數

  • 既然咱們寫的插件是第一個執行的,爲啥code 不是源碼呢,還出現了es5的代碼 ?

    猜測:babel 對每個語句類型 例如 ImportDeclaration 就是import 語句 看成一個鉤子,當捕獲到了import 的時候,會按照插件的順序依次下發,出現了es5的代碼,頗有多是其餘的插件有比ImportDeclaration更前的鉤子先修改了ast樹 驗證:

    Program: {
          enter(path, _ref = {opts:{}}) {
              if( _ref.filename.indexOf('/container/') > -1 ){
                  let _ast = _ref.file.ast;
                  const output = generate(_ast, {}, _ref.file.code);  
                  output.code += `/** ${_ref.filename} **/\n`;
                  fs.writeFile('./data.jsx', output.code, {
                      flag: 'a'
                  }, (err) => {
                          if (err) throw err;
                  })
              }
          }
      }
    複製代碼

    咱們在最插件多增長了一個鉤子,這個鉤子是最早執行的,先於ImportDeclaration 的,打印出來就是源碼,哈哈哈哈

解決思路:

在 Program enter裏緩存住 ast 樹,在基於這個ast樹 生成代碼就行了,可是成本是巨大的,由於ast 是引用類型,你須要作一次深拷貝,否則仍是會被修改

第二個問題

  • babel plugin 裏的變量 在webpack 任務結束後怎麼傳遞出去?

由於babel 自己是基於管道的,他並不知道進入這個管道的文件是否是最後一個文件,咱們直接把這個變量掛在global 下,而後在 webpack compilation done的鉤子作後續的批量操做,代碼示例

const fs = require('fs');

class ConsoleLogOnBuildWebpackPlugin {
  apply(compiler) {
    compiler.hooks.done.tap('done', compilation => {
        fs.writeFile('./data.txt', JSON.stringify(global.containerJSCSS), 'utf-8', (err) => {
            if (err) throw err;
        })
    });
  }
}

module.exports.default = ConsoleLogOnBuildWebpackPlugin
複製代碼

總結

若是不是特殊需求 別在babel裏作狀態翻轉的操做,例如以前我作的這樣

其餘plugin 的 鉤子已經把ast 改寫了,你的鉤子執行的邏輯又依賴的以前的ast,這樣很很差

相關文章
相關標籤/搜索