摘要:最近的技術項目里大量用到了需要修改源文件代碼的需求,也就理所當然的用到了及其插件開發。在這里我要推薦一款實現了這些標簽的插件,建議在你的項目中加入這個插件并用起來,不用再艱難的書寫三元運算符,會大大提升你的開發效率。具體可以參見插件手冊。
最近的技術項目里大量用到了需要修改源文件代碼的需求,也就理所當然的用到了Babel及其插件開發。這一系列專題我們介紹下Babel相關的知識及使用。
對于剛開始接觸代碼編譯轉換的同學,單純的介紹Babel相關的概念只是會當時都能看懂,但是到了自己去實現一個需求的時候就又會變得不知所措,所以我們再介紹中穿插一些例子。
大概分為以下幾塊:
0、Babel基礎介紹
1、使用npm上好用的Babel插件提升開發效率
2、使用Babel做代碼轉換使用到的模塊及執行流程
3、示例:類中插入方法、類方法中插入代碼
4、Babel插件開發介紹
5、示例:通過Babel實現打包構建優化 -- 組件模塊按需打包
用到的名詞:
AST:Abstract Syntax Tree, 抽象語法樹
DI: Dependency Injection, 依賴注入
我們在實際的開發過程中,經常有需要修改js源代碼的需求,比如一下幾種情形:
ES6/7轉化為瀏覽器可支持的ES5甚至ES3代碼;
JSX代碼轉化為js代碼(原來是Facebook團隊支持在瀏覽器中執行轉換,現在轉到在babel插件中維護);
部分js新的特性動態注入(用的比較多的就是babel-plugin-transform-runtime);
一些便利性特性支持,比如:React If/Else/For/Switch等標簽支持;
于是,我們就需要一款支持動態修改js源代碼的模塊,babel則是用的最多的一個。
Babel的解析引擎Babel使用的引擎是babylon,babylon并非由babel團隊自己開發的,而是fork的acorn項目,不過acorn引擎只提供基本的解析ast的能力,遍歷還需要配套的acorn-travesal, 替換節點需要使用acorn-,而這些開發,在Babel的插件體系開發下,變得一體化了。
如何使用使用方式有很多種:
webpack中作為js(x)文件的loader使用;
多帶帶在Node代碼中引入使用;
命令行中使用:
package.json中配置:
"scripts": {
"build": "rimraf lib && babel src --out-dir lib"
}
命令中執行:npm run build。
通常,如果我們在項目根目錄下配置一個.babelrc文件,其配置規則會被babel引入并使用。
1、使用npm上好用的Babel插件提升開發效率在使用webpack做打包工具的時候,我們隊js(x)文件使用的loader通常就是babel-loader,babel只是提供了最基礎的代碼編譯能力,主要用到的一些代碼轉換則是通過插件的方式實現的。在loader中配置插件有兩種方式:presets及plugins,這里要注意presets配置的也是插件,只是優先級比較高,而且他的執行順序是從左到右的,而plugins的優先級順序則是從右到左的。我們經常用到的插件會包括:ES6/7轉ES5代碼的babel-plugin-es2015,React jsx代碼轉換的babel-plugin-react,對新的js標準特性有不同支持程度的babel-plugin-stage-0等(不同階段js標準特性的制定是不一樣的,babel插件支持程度也就不一樣,0表示完全支持),將瀏覽器里export語法轉換為common規范exports/module.exports的babel-plugin-add-module-exports,根據運行時動態插入polyfill的babel-plugin-transform-runtime(絕不建議使用babel-polyfill,一股腦將所有polyfill插入,打的包會很大),對Generator進行編譯的babel-plugin-transform-regenerator等。想了解更多的配置可以參見這篇文章:如何寫好.babelrc?Babel的presets和plugins配置解析(https://excaliburhan.com/post...)
如果你是基于完全組件化(標簽式)的開發模式的話,如果能提供常用的控制流標簽如:If/ElseIf/Else/For/Switch/Case等給我們的話,那么我們的開發效率則會大大提升。在這里我要推薦一款實現了這些標簽的babel插件:jsx-control-statement,建議在你的項目中加入這個插件并用起來,不用再艱難的書寫三元運算符,會大大提升你的開發效率。
2、使用Babel做代碼轉換使用到的模塊及執行流程Babel將源碼轉換AST之后,通過遍歷AST樹(其實就是一個js對象),對樹做一些修改,然后再將AST轉成code,即成源碼。
將js源碼轉換為AST用到的模塊叫:babylon,對樹進行遍歷并做修改用到的模塊叫:babel-traverse,將修改后的AST再生成js代碼用到的模塊則是:babel-generator。而babel-core模塊則是將三者結合使得對外提供的API做了一個簡化,使用babel-core只需要執行以下的簡單代碼即可:
import { transform } from "babel-core"; var result = babel.transform("code();", options); result.code; result.map; result.ast;
我們在Node中使用的時候一般都是使用的三步轉換的方式,方便做更多的配置及操作。所以整個的難點主要就在對AST的操作上,為了能對AST做一些操作后進而能對js代碼做到修改,babel對js代碼語法提供了各種類型,比如:箭頭函數類型ArrowFunctionExpression,for循環里的continue語句類型:ContinueStatement等等,我們主要就是根據這些不同的語法類型來對AST做操作(生成/替換/增加/刪除節點),具體有哪些類型全部在:babel-types(https://www.npmjs.com/package...。
其實整個大的操作流程還是比較簡單的,我們直接上例子好了。
3、示例 Babel使用案例0:往類中插入方法比如我們有這樣的需求:我們有一個jsx代碼模板,該模板中有一個類似與下面的組件類:
class MyComponent extends React.Component { constructor(props, context) { super(props, context); } // 其他代碼 }
我們會需要根據當前的DSL生成對應的render方法并插入進MyComponent組件類中,該如何實現呢?
上面已經講到,我們對代碼的操作其實是通過對代碼生成的AST操作生成一個新的AST來完成的,而對AST的操作則是通過babel-traverse這個庫來實現的。
該庫通過簡單的hooks函數的方式,給我們提供了在遍歷AST時可以操作當前被遍歷到的節點的相關操作,要獲取并修改(增刪改查)當前節點,我們需要知道AST都有哪些節點類型,而所有的節點類型都存放于babel-types這個庫中。我們先看完整的實現代碼,然后再分析:
// 先引入相關的模塊 const babylon = require("babylon"); const Traverse = require("babel-traverse").default; const generator = require("babel-generator").default; const Types = require("babel-types"); const babel = require("babel-core"); // === helpers === // 將js代碼編譯成AST function parse2AST(code) { return babylon.parse(code, { sourceType: "module", plugins: [ "asyncFunctions", "classConstructorCall", "jsx", "flow", "trailingFunctionCommas", "doExpressions", "objectRestSpread", "decorators", "classProperties", "exportExtensions", "exponentiationOperator", "asyncGenerators", "functionBind", "functionSent" ] }); } // 直接將一小段js通過babel.template生成對應的AST function getTemplateAst(tpl, opts = {}) { let ast = babel.template(tpl, opts)({}); if (Array.isArray(ast)) { return ast; } else { return [ast]; } } /** * 檢測傳入參數是否已在插入代碼中定義 */ checkParams = function(argv, newAst) { let params = []; const vals = getAstVals(newAst); if (argv && argv.length !== 0) { for (let i = 0; i < argv.length; i++) { if (vals.indexOf(argv[i]) === -1) { params.push(Types.identifier(argv[i])); } else { throw TypeError("參數名" + argv[i] + "已在插入代碼中定義,請更名"); } } } return params; } const code = ` class MyComponent extends React.Component { constructor(props, context) { super(props, context); } // 其他代碼 } `; const insert = [ { // name為方法名 name: "render", // body為方法體 body: ` return (我是render方法的返回內容); `, // 方法參數 argv: null, // 如果原來的Class有同名方法則強制覆蓋 isCover: true } ]; const ast = parse2AST(code); Traverse(ast, { // ClassBody表示當前類本身節點 ClassBody(path) { if (!Array.isArray(insert)) { throw TypeError("插入字段類型必須為數組"); } for (let key in insert) { const methodObj = insert[key], name = methodObj.name, argv = methodObj.argv, body = methodObj.body, isCover = methodObj.isCover; if (typeof name !== "string") { throw TypeError("方法名必須為字符串"); } const newAst = getTemplateAst(body, { sourceType: "script" }); const params = checkParams(argv, newAst); // 通過Types.ClassMethodAPI,生成方法AST const property = Types.ClassMethod("method", Types.identifier(name), params, Types.BlockStatement(newAst)); // 插入進AST path.node.body.push(property); } } }); console.log(generator(ast).code);
其中,最核心的地方就是下面的這一行代碼:
const property = Types.ClassMethod("method", Types.identifier(name), params, Types.BlockStatement(newAst));
確定好我們要進行怎么樣的操作(比如要往一個類中插入一個方法),休閑要確定是怎樣的鉤子名(這里是ClassBody),然后通過要插入的代碼生成對應的AST,生成AST可以通過Babel.Types的相關方法一點點生成,但是這里有個比較方便的API:babel.template,然后通過path的相關操作將新生成的AST插入即可。
穿插:AST樹的創建方法一些AST樹的創建方法,有:
1、使用babel-types定義的創建方法創建
比如創建一個var a = 1;
types.VariableDeclaration( "var", [ types.VariableDeclarator( types.Identifier("a"), types.NumericLiteral(1) ) ] )
如果使用這樣創建一個ast節點,肯定要累死了,可以:
使用replaceWithSourceString方法創建替換
使用template方法來創建AST結點
template方法其實也是babel體系中的一部分,它允許使用一些模板來創建ast節點
比如上面的var a = 1可以使用:
var gen = babel.template(`var NAME = VALUE;`); var ast = gen({ NAME: t.Identifier("a"), VALUE: t.NumberLiteral(1) });
也可以簡單寫:
var gen = babel.template(`var a = 1;`); var ast = gen({});Babel使用案例1:往類的方法中插入代碼
這個案例會更復雜一點,大家可以先試著去實現下,明天再講解具體實現。
往方法中要插入代碼,我們先找下類中方法的babel-types值是什么,查閱文檔:https://www.npmjs.com/package...可以發現是叫:ClassMethod。于是就可以像下面這樣實現:
const injectCode = [{ name: "constructor", code: insertCodeNext, }]; const ast = parse2AST(originCode); Traverse(ast, { ClassMethod(path) { if (!Array.isArray(injectCode)) { throw TypeError("插入字段類型必須為數組"); } // 獲取當前方法的名字 const methodName = path.get("body").container.key.name; for (let key in injectCode) { const inject = injectCode[key], name = inject.name, code = inject.code, pos = inject.pos; if (methodName === name) { const newAst = getTemplateAst(code, { sourceType: "script" }); if (pos === "prev") { Array.prototype.unshift.apply(path.node.body.body, newAst); } else { Array.prototype.push.apply(path.node.body.body, newAst); } } } } }); console.log(generator(ast).code);
其實跟往Class中插入method一樣的道理。
4、Babel插件開發介紹Babel的插件就是一個帶有babel參數的函數,該函數返回類似于babel-traverse的配置對象,即下面的格式:
module.exports = function(babel) { var t = babel.types; return { visitor: { ImportDeclaration(path, ref) { var opts = ref.opts; // 配置的參數 } } }; };
在babel插件的時候,配置的參數就會存放在ref參數里,見上面的代碼所所示。具體可以參見babel插件手冊:https://github.com/thejamesky...。
下面我們看一個具體的示例。
5、示例:通過Babel實現打包構建優化 -- 組件模塊按需打包 需求比如,我們有一個UI組件庫,在入口文件中會把所有的組件放在這里,并export出對外服務,大概類似于如下的代碼:
export Button from "./lib/button/index.js"; export Input from "./lib/input/index.js"; // ......
那么我們在使用的時候就可以如下引用:
import {Button} from "ant"
這樣就有一個問題,就是比如我們只是用了一個Button組件,這樣引用就會導致會把所有的組件打包進來,導致整個js文件會非常大。我們能不能把代碼動態實時的編譯成如下的代碼來解決這個問題?
import Button from "ant/lib/button";
我們可以寫個babel插件來實現這樣的需求。
// 入口文件 var extend = require("extend"); var astExec = require("./ast-transform"); // 一些個變量預設 var NEXT_MODULE_NAME = "ant"; var NEXT_LIB_NAME = "lib"; var MEXT_LIB_NAME = "lib"; module.exports = function(babel) { var t = babel.types; return { visitor: { ImportDeclaration: function ImportDeclaration(path, _ref) { var opts = _ref.opts; var next = opts.next || {}; var nextJsName = next.nextJsName || NEXT_MODULE_NAME; var nextCssName = next.nextCssName || NEXT_MODULE_NAME; var nextDir = next.dir || NEXT_LIB_NAME; var nextHasStyle = next.hasStyle; var node = path.node; var baseOptions = { node: node, path: path, t: t, jsBase: "", cssBase: "", hasStyle: false }; if (!node) { return; } var jsBase; var cssBase; if (node.source.value === nextJsName) { jsBase = nextJsName + "/" + nextDir + "/"; cssBase = nextCssName + "/" + nextDir + "/"; astExec(extend(baseOptions, { jsBase: jsBase, cssBase: cssBase, hasStyle: nextHasStyle })); } } } }; };
這里將部分的功能多帶帶放到了一個ast-transform文件中,代碼如下:
function transformName(name) { if (!name) return ""; return name.replace(/[A-Z]/g, function(ch, index) { if (index === 0) return ch.toLowerCase(); return "-" + ch.toLowerCase(); }); } module.exports = function astExec(options) { var node = options.node; // 當前節點 var path = options.path; // path輔助處理變量 var t = options.t; // babel-types var jsBase = options.jsBase; var cssBase = options.cssBase; var hasStyle = options.hasStyle; node.specifiers.forEach(specifier => { if (t.isImportSpecifier(specifier)) { var comName = specifier.imported.name; var lcomName = transformName(comName); var libName = jsBase + lcomName; var libCssName = cssBase + lcomName + "/index.scss"; // AST節點操作 path.insertAfter(t.importDeclaration([t.ImportDefaultSpecifier(t.identifier(comName))], t.stringLiteral(libName))); if (hasStyle) { path.insertAfter(t.importDeclaration([], t.stringLiteral(libCssName))); } } }); // 把原來的代碼刪除掉 path.remove(); };
這樣我們在用的時候就可以像下面這樣使用:
在.babelrc文件中像下面這樣配置即可:
{ "presets": [...], // babel-preset-react等 "plugins" :[ [ "armor-fusion", { next: { jsName: "ant", //js庫名,默認值:ant cssName: "ant", //css庫名,當如果其他的主題包時,可以換成別的主題包名,默認值:ant dir: "lib", //目錄名,一般不需要設置,默認值:lib hasStyle: true //會編譯出scss引用,不加則默認不會編譯 } } ] ] }
大家可以把上面比較實用的插件功能整理下放到自己的github上,也許能給你的面試加分也說不定哦。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/83916.html
摘要:的工作過程的處理主要過程解析轉換生成。代碼轉換處理,處理工具插件等就是在這個階段進行代碼轉換,返回新的。若感興趣了解更多內容,插件中文開發文檔提供了很多詳細資料。 Babel簡介 Babel是Javascript編譯器,是種代碼到代碼的編譯器,通常也叫做『轉換編譯器』。 Babel的工作過程 Babel的處理主要過程:解析(parse)、轉換(transform)、生成(generat...
摘要:描述這個插件可以讓我們的代碼更加的簡潔和美觀。安裝使用提供了兩個重要的接口使用了這個插件,的和就可以忘記來,它們就用不著了。現在有美女個。 可先查看我的redux簡單入門 react-redux簡介 react-redux是使用redux開發react時使用的一個插件,另外插一句,redux不是react的產品,vue和angular中也可以使用redux;下面簡單講解,如何使用rea...
摘要:最后還可以跟我們的進行結合管理代碼什么是說明白點就是模塊打包機,可以很好的管理我們的模塊,可以對瀏覽器進行更好的兼容。安裝首選我們要安裝,中已經給我們下載了我們通過進行安裝管理。 webpack入門及結合react進行開發 重要提示(2017年7月26號更新) 本文介紹的是最新版的3.4.1,并且其中又跟React結合的例子!showImg(https://segmentfault.c...
摘要:是一個對象,它表示兩個節點之間的連接。接著返回一個對象,其屬性是這個插件的主要節點訪問者。所以上面的執行方式是運行引入了自定義插件的打包文件現在為明顯減小,自定義插件成功插件文件目錄覺得好玩就關注一下歡迎大家收藏寫評論 目錄 Babel簡介 Babel運行原理 AST解析 AST轉換 寫一個Babel插件 Babel簡介 Babel 是一個 JavaScript 編譯器,它能將es...
摘要:雖然夠好用,奈何沒有瀏覽器對其可以完全支持,本文書寫時間,開發版號稱已經支持的特性。開始安裝本系列假定讀者都有使用經驗,如果還沒有,趕緊去這里了解并安裝吧。到此,我們的已經準備就緒。 通過前面章節的講解,大家對ES2015的一些新語法有了初步的理解,之前我們的測試代碼都可以直接放入 Chrome Console 中直接運行,為了更好的學習后面的面向對象和模塊開發,我先用一章介紹一下 B...
閱讀 894·2021-09-03 10:42
閱讀 1511·2019-08-30 15:56
閱讀 1444·2019-08-29 17:27
閱讀 870·2019-08-29 15:25
閱讀 3156·2019-08-26 18:27
閱讀 2480·2019-08-26 13:41
閱讀 1888·2019-08-26 10:39
閱讀 1570·2019-08-23 18:36