前言
这并不是一篇深入babel的文章,相反这是一篇适合初学babel的demos;本demos不会介绍一大堆babel各种牛逼特性(ps:因为这我也不会,有待深入研究),相反这里提供一大堆demos来解释如何从零开启babel plugin之路,然后开发一个乞丐乞丐版BabelPluginImport,并接入webpack中应用
五分钟阅读,五分钟Demo Coding你能学会什么?
- 编写你的第一个babel plugin
- 使用babel plugin实现webpack resolve alias功能
- 实现乞丐乞丐版BabelPluginImport
- 把自己的插件接入webpack
STEP 1 | 冥想
先来试想下babel的实现,大概分几个步骤:
- js文件应该是作为字符串传递给babel
- babel对字符串进行解析,出AST
- AST应该大概是个json,这时候啥es6转es5啊都发生了,叫做转换
- 转换完的AST还得输出为String,这叫生成
STEP 2 | 小试牛刀
编写你的第一个babel plugin
babel的插件开发可以参考
先上一个最简单的demo
根据STEP 1的思路
// babel.jsvar 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 }]});复制代码
看完这个demo是不是有几个问题?
- 问题1. plugins传入[{ visitor: {} }]格式
- 问题2. 钩子函数为啥叫Identifier,而不叫Id?name?
- 问题3. 其实类似问题2,钩子函数怎么定义,如何定义,什么规范?
问题解答
- 问题1 这个babel plugin定义要求如此,我们不纠结
- 问题2 所谓钩子函数当然是跟生命周期之类的有关了,这里的钩子函数其实是babel的在解析过程中的钩子函数,比如Identifier,当解析到标识符时就会进这个钩子函数
- 问题3 钩子函数的定义可以参考babel官网 ,不过需要注意Api的首字母大写,不然会提示你没有此钩子函数
ok,对这个简单的demo没有问题之后来执行下这个demo:node babel.js,输出如下path AST:
// 因为光是一个"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,这一节小试牛刀的目的就达到了
STEP 3 | 实现resolve alias
前言
经过小试牛刀的阶段,然后自己熟悉下@babel/types的api,熟悉几个api之后就可以进行简单的开发了,这一节要讲的是ImportDeclaration
使用babel plugin实现webpack resolve alias功能
先思考下要实现resolve alias的步骤:
- 造数据_code="import homePage from '@/views/homePage';";
- 造数据const alias = {'@': './'};
- 把'@/views/homePage'变成'./views/homePage'输出
总结好我们要实现的功能,下面用demo来实现一遍
// babel.jsconst 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"; 说明我们的alias生效了STEP 4 | 乞丐乞丐版BabelPluginImport is coming
问题:
还是一样的步骤,先试想下实现一个BabelPluginImport的难点在哪?复制代码
我在 中介绍过BalbelPluginImport,其实这个插件的一个功能是把 import { Button } from 'antd' 转换为 import { Button } from 'antd/lib/button';
-> 我们这个乞丐版BabelPluginImport就简单实现下这个功能
// babel.jsvar babel = require('@babel/core');var types = require('babel-types');// Babel helper functions for inserting module loadsvar 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);复制代码
输出结果:
可以发现: import { Button } from 'antd'; -> "use strict"; var _index = require("./main/lib/index.js");原代码被转换成了下面的代码
STEP 5 | Demo Coding高光时刻
高光时刻来了,说了这么久理论知识,可以来上手自己写一个了。
5.1 create-react-app先来搭起一个项目
npx create-react-app babel-demo复制代码
5.2 简单的开发下项目,一个入口组件App.js,一个Button组件
目录结构是: src - App.js - firefly-ui文件夹 - lib文件夹 - Button.js代码很简单,如下:// App.jsimport React from 'react';import Button from 'firefly-ui';function App() { return ();}export default App;// Button.jsimport React, { Component } from 'react';class Button extends Component{ render(){ return我是button啊}}export default Button;复制代码
ok,代码写完了,一运行,崩了
这没问题,没崩就奇怪了,因为你没装firefly-ui啊,可是firefly-ui是个啥? 有这个疑问说明你跟上节奏了,我可以告诉你,firefly-ui就是你src目录的firefly-ui目录,那么下面我们就要写一个babel plugin来解决这个问题,大致思路如下:- 当解析到import { Button } from 'firefly-ui'时对这个import进行转换
- 当解析到jsx中Button时用上面转换后的import
那下面从这两个入手写babel import
5.3 npm run eject来eject出webpack配置
好的,为啥要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配置,请看下注意点,其他的没什么难点复制代码
5.4 import plugin开发
所有配置都完成了,那么还差实现../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示例的效果,可以看我的 ,有问题可以咨询我