这并非一篇深刻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看下效果:
仍是同样的步骤,先试想下实现一个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源码,有问题能够咨询我