本文转载自微信公众号「神光的随心受编程秘籍」,作者神说要有光zxg 。转感转载本文请联系神光的插件编程秘籍公众号。
当我们 import 一个模块的随心受时候,可以这样默认引入:
import path from path; path.join(a,转感 b); function func() { const sep = aaa; console.log(path.sep); }也可以这样解构引入:
import { join, sep as _sep } from path; join(a, b); function func() { const sep = aaa; console.log(_sep); }第一种默认引入叫 default import,第二种解构引入叫 named import。插件
不知道大家习惯用哪一种。随心受
如果有个需求,转感让你把所有的插件 default import 转成 named import,你会怎么做呢?随心受
可能你会说这个不就是找到所有用到引入变量的地方,修改成直接调用方法,转感然后那些方法名以解构的插件方式写在 import 语句里么。
但如果说要改的随心受项目有 100 多个这种文件呢?(触发 treeshking 要这样改)
这时候就可以考虑 babel 插件了,它很适合做这种有规律且数量庞大的转感代码的自动修改。
让我们通过这个例子感受下 babel 插件的插件威力吧。
因为代码比较多,大家可能没耐心看,要不我们先看效果吧:
输入代码是这样:
import path from path; path.join(a, b); function func() { const sep = aaa; console.log(path.sep); }我们引入该 babel 插件,读取输入代码并做转换:
const { transformFileSync } = require(@babel/core); const importTransformPlugin = require(./plugin/importTransform); const path = require(path); const { code } = transformFileSync(path.join(__dirname, ./sourceCode.js), { plugins: [[importTransformPlugin]] }); console.log(code);打印如下:
我们完成了 default import 到 named import 的自动转换。
可能有的同学担心重名问题,云服务器提供商我们测试一下:
可以看到,插件已经处理了重名问题。
import 语句中间的部分叫做 specifier,我们可以通过 astexplorer.net 来可视化的查看它的 AST。
比如这样一条 import 语句:
import React, { useState as test, useEffect} from react;它对应的 AST 是这样的:
也就是说默认 import 是 ImportDefaultSpecifier,而解构 import 是 ImportSpecifier
ImportSpecifier 语句有 local 和 imported 属性,分别代表引入的名字和重命名后的名字:
那我们的目的明确了,就是把 ImportDefaultSpecifier 转成 ImportSpecifier,并且使用到的属性方法来设置 imported 属性,需要重命名的还要设置下 local 属性。
怎么知道使用到哪些属性方法呢?也就是如何分析变量的引用呢?
babel 提供了 scope 的 api,用于作用域分析,可以拿到作用域中的声明,云南idc服务商和所有引用这个声明的地方。
比如这里就可以用 scope.getBinding 方法拿到该变量的声明:
const binding = scope.getBinding(path);然后用 binding.references 就可以拿到所有引用这个声明的地方,也就是 path.join 和 path.sep。
之后就可以把这两处引用改为直接的方法调用,然后修改下 import 语句为解构就可以了。
我们总结一下步骤:
找到 import 语句中的 ImportDefaultSpecifier 拿到 ImportDefaultSpecifier 在作用域的声明(binding) 找到所有引用该声明的地方(reference) 修改各处引用为直接调用函数的形式,收集函数名 如果作用域中有重名的变量,则生成一个唯一的函数名 根据收集的函数名来修改 ImportDefaultSpecifier 为 ImportSpecifier原理大概过了一遍,我们来写下代码
babel 插件是函数返回对象的形式,返回的对象中主要是通过 visitor 属性来指定对什么 AST 做什么处理。
我们搭一个 babel 插件的骨架:
const { declare } = require(@babel/helper-plugin-utils); const importTransformPlugin = declare((api, options, dirname) => { api.assertVersion(7); return { visitor: { ImportDeclaration(path) { } } } }); module.exports = importTransformPlugin;这里我们要处理的是 import 语句 ImportDeclaration。
@babel/helper-plugin-utils 包的 declare 方法的作用是亿华云计算给 api 扩充一个 assertVersion 方法。而 assertVersion 的作用是如果这个插件工作在了 babel6 上就会报错说这个插件只能用在 babel7,可以避免报的错看不懂。
path 是用于操作 AST 的一些 api,而且也保留了 node 之间的关联,比如 parent、sibling 等。
接下来进入正题:
我们要先取出 specifiers 的部分,然后找出 ImportDefaultSpecifier:
ImportDeclaration(path) { // 找到 import 语句中的 default import const importDefaultSpecifiers = path.node.specifiers.filter(item => api.types.isImportDefaultSpecifier(item)); // 对每个 default import 做转换 importDefaultSpecifiers.forEach(defaultSpecifier => { }); }然后对每一个 default import 都要根据在作用域中的声明找到所有引用的地方:
// import 变量的名字 const importId = defaultSpecifier.local.name; // 该变量的声明 const binding = path.scope.getBinding(importId); binding.referencePaths.forEach(referencePath=> { });然后对每个引用到该 import 的地方都做修改,改为直接调用函数,并且把函数名收集起来。这里要注意的是,如果作用域中有同名变量还要生成一个新的唯一 id。
// 该变量的声明 const binding = path.scope.getBinding(importId); const referedIds = []; const transformedIds = []; // 收集所有引用该声明的地方的方法名 binding.referencePaths.forEach(referencePath=> { const currentPath = referencePath.parentPath; const methodName = currentPath.node.property.name; // 之前方法名 referedIds.push(currentPath.node.property); if (!currentPath.scope.getBinding(methodName)) { // 如果作用域没有重名变量 const methodNameNode = currentPath.node.property; currentPath.replaceWith(methodNameNode); transformedIds.push(methodNameNode); // 转换后的方法名 } else { // 如果作用域有重名变量 const newMethodName = referencePath.scope.generateUidIdentifier(methodName); currentPath.replaceWith(newMethodName); transformedIds.push(newMethodName); // 转换后的方法名 } });这部分逻辑比较多,着重讲一下。
我们对每个引用了该变量的地方都要记录下引用了哪个方法,比如 path.join、path.sep 就引用了 join 和 sep 方法。
然后就要把 path.join 替换成 join,把 path.sep 替换成 sep。
如果作用域中有了 join 或者 sep 的声明,需要生成一个新的 id,并且记录下新的 id 是什么。
收集了所有的方法名,就可以修改 import 语句了:
// 转换 import 语句为 named import const newSpecifiers = referedIds.map((id, index) => api.types.ImportSpecifier(transformedIds[index], id)); path.node.specifiers = newSpecifiers;没有 babel 插件基础可能看的有点晕,没关系,知道他是做啥的就行。我们接下来试下效果。
我们做了 default import 到 named import 的自动转换,其实反过来也一样,不也是分析 scope 的 binding 和 reference,然后去修改 AST 么?感兴趣的同学可以试下反过来转换怎么写。
插件全部代码如下:
const { declare } = require(@babel/helper-plugin-utils); const importTransformPlugin = declare((api, options, dirname) => { api.assertVersion(7); return { visitor: { ImportDeclaration(path) { // 找到 import 语句中的 default import const importDefaultSpecifiers = path.node.specifiers.filter(item => api.types.isImportDefaultSpecifier(item)); // 对每个 default import 做转换 importDefaultSpecifiers.forEach(defaultSpecifier => { // import 变量的名字 const importId = defaultSpecifier.local.name; // 该变量的声明 const binding = path.scope.getBinding(importId); const referedIds = []; const transformedIds = []; // 收集所有引用该声明的地方的方法名 binding.referencePaths.forEach(referencePath=> { const currentPath = referencePath.parentPath; const methodName = currentPath.node.property.name; // 之前方法名 referedIds.push(currentPath.node.property); if (!currentPath.scope.getBinding(methodName)) { // 如果作用域没有重名变量 const methodNameNode = currentPath.node.property; currentPath.replaceWith(methodNameNode); transformedIds.push(methodNameNode); // 转换后的方法名 } else { // 如果作用域有重名变量 const newMethodName = referencePath.scope.generateUidIdentifier(methodName); currentPath.replaceWith(newMethodName); transformedIds.push(newMethodName); // 转换后的方法名 } }); // 转换 import 语句为 named import const newSpecifiers = referedIds.map((id, index) => api.types.ImportSpecifier(transformedIds[index], id)); path.node.specifiers = newSpecifiers; }); } } } }); module.exports = importTransformPlugin;我们要做 default import 转 named import,也就是 ImportDefaultSpecifier 转 ImportSpecifier,要通过 scope 的 api 分析 binding 和 reference,找到所有引用的地方,替换成直接调用函数的形式,然后再去修改 import 语句的 AST 就可以了。
babel 插件特别适合做这种有规律且转换量比较大的需求,在一些场景下是有很大的威力的。