這並非一篇深刻babel的文章,相反這是一篇適合初學babel的demos;本demos不會介紹一大堆babel各類牛逼特性(ps:由於這我也不會,有待深刻研究),相反這裏提供一大堆demos來解釋如何從零開啓babel plugin之路,而後開發一個乞丐乞丐版BabelPluginImport,並接入webpack中應用node
先來試想下babel的實現,大概分幾個步驟:react
babel的插件開發能夠參考 Babel插件手冊webpack
根據STEP 1的思路git
// babel.js
var babel = require('babel-core');
const _code = `a`;
const visitor = {
Identifier(path){
console.log(path);
console.log(path.node.name);
}
};
babel.transform(_code, {
plugins: [{
visitor: visitor
}]
});
複製代碼
問題解答es6
ok,對這個簡單的demo沒有問題以後來執行下這個demo:node babel.js,輸出以下path AST:github
// 由於光是一個"a",AST文件也長達284行,因此就不所有放出來了。只放出AST對象下的表示當前Identifier節點數據信息的node來看下
node: Node {
type: 'Identifier',
start: 0,
end: 1,
loc: SourceLocation {
start: [Position],
end: [Position],
identifierName: 'a'
},
name: 'a'
},
複製代碼
從這個AST node,對AST有個初步的認識,node節點會存儲當前的loc信息,還有標識符的name,這一節小試牛刀的目的就達到了web
通過小試牛刀的階段,而後本身熟悉下@babel/types的api,熟悉幾個api以後就能夠進行簡單的開發了,這一節要講的是ImportDeclarationnpm
先思考下要實現resolve alias的步驟:json
總結好咱們要實現的功能,下面用demo來實現一遍api
// babel.js
const babel = require('babel-core');
const _code = `import homePage from '@/views/homePage';`;
const alias = {
'@': './'
};
const visitor = {
ImportDeclaration(path){
for(let prop in alias){
if(alias.hasOwnProperty(prop)){
let reg = new RegExp(`${prop}/`);
path.node.source.value = path.node.source.value.replace(reg, alias[prop]);
}
}
}
};
const result = babel.transform(_code, {
plugins: [{
visitor: visitor
}]
});
console.log(result.code);
複製代碼
這個demo的主要做用是當進入到ImportDeclaration鉤子函數時把path.node.source.value裏面的@替換成了./,來node babel.js看下效果:
發現log輸出了import homePage from "./views/homePage";仍是同樣的步驟,先試想下實現一個BabelPluginImport的難點在哪?
複製代碼
我在 React性能優化之代碼分割 中介紹過BalbelPluginImport,其實這個插件的一個功能是把 import { Button } from 'antd' 轉換爲 import { Button } from 'antd/lib/button';
-> 咱們這個乞丐版BabelPluginImport就簡單實現下這個功能
// babel.js
var babel = require('@babel/core');
var types = require('babel-types');
// Babel helper functions for inserting module loads
var healperImport = require("@babel/helper-module-imports");
const _code = `import { Button } from 'antd';`;
const ImportPlugin = {
// 庫名
libraryName: 'antd',
// 庫所在文件夾
libraryDirectory: 'lib',
// 這個隊列實際上是爲了存儲待helperModuleImports addNamed的組件的隊列,不過remove和import都在ImportDeclaration完成,因此這個隊列在這個demo無心義
toImportQueue: {},
// 使用helperModuleImports addNamed導入正確路徑的組件
import: function(file){
for(let prop in this.toImportQueue){
if(this.toImportQueue.hasOwnProperty(prop)){
return healperImport.addNamed(file.path, prop, `./main/${this.libraryDirectory}/index.js`);
}
}
}
};
const visitor = {
ImportDeclaration(path, state) {
const { node, hub: { file } } = path;
if (!node) return;
const { value } = node.source;
// 判斷當前解析到的import source是不是antd,是的話進行替換
if (value === ImportPlugin.libraryName) {
node.specifiers.forEach(spec => {
if (types.isImportSpecifier(spec)) {
ImportPlugin.toImportQueue[spec.local.name] = spec.imported.name;
}
});
// path.remove是移除import { Button } from 'antd';
path.remove();
// import是往代碼中加入import _index from './main/lib/index.js';
ImportPlugin.import(file);
}
}
};
const result = babel.transform(_code, {
plugins: [
{
visitor: visitor
},
// 這裏除了自定義的visitor,還加入了第三方的transform-es2015-modules-commonjs來把import轉化爲require
"transform-es2015-modules-commonjs"
]
});
console.log(result.code);
複製代碼
輸出結果:
能夠發現:原代碼被轉換成了下面的代碼
高光時刻來了,說了這麼久理論知識,能夠來上手本身寫一個了。
npx create-react-app babel-demo
複製代碼
目錄結構是:
src
- App.js
- firefly-ui文件夾
- lib文件夾
- Button.js
代碼很簡單,以下:
// App.js
import React from 'react';
import Button from 'firefly-ui';
function App() {
return (
<div className="App">
<Button />
</div>
);
}
export default App;
// Button.js
import React, { Component } from 'react';
class Button extends Component{
render(){
return <div>我是button啊</div>
}
}
export default Button;
複製代碼
ok,代碼寫完了,一運行,崩了
這沒問題,沒崩就奇怪了,由於你沒裝firefly-ui啊,但是firefly-ui是個啥?
有這個疑問說明你跟上節奏了,我能夠告訴你,firefly-ui就是你src目錄的firefly-ui目錄,那麼下面咱們就要寫一個babel plugin來解決這個問題,大體思路以下:
那下面從這兩個入手寫babel import
好的,爲啥要eject出配置,由於你要配置babel-loader的plugins啊大佬。
ok,來配置一把
// 找到webpack.config.js -> 找到babel-loader -> 找到plugins
// 注意點:
// 在plugins裏面加入我們的import插件
// tips:import插件放在src的兄弟文件夾babel-plugins的import.js
// 因此這裏的路徑是../babel-plugins/import,由於默認是從node_modules開始
//還有個timestamp,這是由於webpackDevServer的緩存,爲了重啓清緩存加了時間戳
[
require.resolve('../babel-plugins/import'),
{
libName: 'firefly-ui',
libDir: 'lib',
timestamp: +new Date
},
]
以上是balbel-loader的plugins配置,請看下注意點,其餘的沒什麼難點
複製代碼
全部配置都完成了,那麼還差實現../babel-plugins/import.js
const healperImport = require("@babel/helper-module-imports");
let ImportPlugin = {
// 從webpack配置進Program鉤子函數讀取libName和libDir
libName: '',
libDir: '',
// helper-module-imports待引入的組件都放在這裏
toImportQueue: [],
// helper-module-imports引入過的組件都放在這裏
importedQueue: {},
// helper-module-imports替換原始import
import: function(path, file){
for(let prop in this.toImportQueue){
if(this.toImportQueue.hasOwnProperty(prop)){
// return healperImport.addNamed(file.path, prop, `./${this.libName}/${this.libDir}/${prop}.js`);
let imported = healperImport.addDefault(file.path, `./${this.libName}/${this.libDir}/${prop}.js`);
this.importedQueue[prop] = imported;
return imported;
}
}
}
};
module.exports = function ({ types }) {
return {
visitor: {
// Program鉤子函數主要接收webpack的配置
Program: {
enter(path, { opts = {} }) {
ImportPlugin.libName = opts.libName;
ImportPlugin.libDir = opts.libDir;
}
},
// ImportDeclaration鉤子函數主要處理import之類的源碼
ImportDeclaration: {
enter(path, state){
const { node, hub: { file } } = path;
if (!node) return;
const { value } = node.source;
if (value === ImportPlugin.libName) {
node.specifiers.forEach(spec => {
ImportPlugin.toImportQueue[spec.local.name] = spec.local.name;
});
path.remove();
ImportPlugin.import(path, file);
}
}
},
// Identifier主要是爲了解析jsx裏面的Button,並轉換爲helper-module-imports引入的新節點
Identifier(path){
if(ImportPlugin.importedQueue[path.node.name]){
path.replaceWith(ImportPlugin.importedQueue[path.node.name]);
}
}
}
}
}
複製代碼
這個plugin的實現,我探索了幾個小時才實現的。 若是隻是實現ImportDeclaration鉤子函數,而不實現Identifier鉤子函數的話,能夠發現import的Button已被轉換,而jsx裏面仍是Button。因此會提示Button is not defined。以下圖:
好的,按照個人demo完整實現以後,發現import和jsx裏所有被轉換了。而且程序正常運行。以下圖:
到這裏差很少就結束了,認真的同窗可能還會發現有不少問題沒有給出解答,後面有時間再繼續寫babel,由於感受這篇文章的知識點對於初學者來講已經挺多了,若是環境搭建有問題,或者本身沒法寫出plugin示例的效果,能夠看個人 babel-demo源碼,有問題能夠諮詢我