import方式随心转,感受Babel插件的威力

本文转载自微信公众号「神光的编程秘籍」,作者神说要有光zxg 。转载本文请联系神光的编程秘籍公众号。

创新互联网站建设公司一直秉承“诚信做人,踏实做事”的原则,不欺瞒客户,是我们最起码的底线! 以服务为基础,以质量求生存,以技术求发展,成交一个客户多一个朋友!专注中小微企业官网定制,成都网站设计、成都网站建设、外贸网站建设,塑造企业网络形象打造互联网企业效应。

当我们 import 一个模块的时候,可以这样默认引入:

 
 
 
 
  1. import path from 'path'; 
  2.  
  3. path.join('a', 'b'); 
  4.  
  5. function func() { 
  6.     const sep = 'aaa'; 
  7.     console.log(path.sep); 

也可以这样解构引入:

 
 
 
 
  1. import { join, sep as _sep } from 'path'; 
  2. join('a', 'b'); 
  3.  
  4. function func() { 
  5.   const sep = 'aaa'; 
  6.   console.log(_sep); 

第一种默认引入叫 default import,第二种解构引入叫 named import。

不知道大家习惯用哪一种。

如果有个需求,让你把所有的 default import 转成 named import,你会怎么做呢?

可能你会说这个不就是找到所有用到引入变量的地方,修改成直接调用方法,然后那些方法名以解构的方式写在 import 语句里么。

但如果说要改的项目有 100 多个这种文件呢?(触发 treeshking 要这样改)

这时候就可以考虑 babel 插件了,它很适合做这种有规律且数量庞大的代码的自动修改。

让我们通过这个例子感受下 babel 插件的威力吧。

因为代码比较多,大家可能没耐心看,要不我们先看效果吧:

测试效果

输入代码是这样:

 
 
 
 
  1. import path from 'path'; 
  2.  
  3. path.join('a', 'b'); 
  4.  
  5. function func() { 
  6.     const sep = 'aaa'; 
  7.     console.log(path.sep); 

我们引入该 babel 插件,读取输入代码并做转换:

 
 
 
 
  1. const { transformFileSync } = require('@babel/core'); 
  2. const importTransformPlugin = require('./plugin/importTransform'); 
  3. const path = require('path'); 
  4.  
  5. const { code } = transformFileSync(path.join(__dirname, './sourceCode.js'), { 
  6.     plugins: [[importTransformPlugin]] 
  7. }); 
  8.  
  9. console.log(code); 

打印如下:

我们完成了 default import 到 named import 的自动转换。

可能有的同学担心重名问题,我们测试一下:

可以看到,插件已经处理了重名问题。

思路分析

import 语句中间的部分叫做 specifier,我们可以通过 astexplorer.net 来可视化的查看它的 AST。

比如这样一条 import 语句:

 
 
 
 
  1. import React, {useState as test, useEffect} from 'react'; 

它对应的 AST 是这样的:

也就是说默认 import 是 ImportDefaultSpecifier,而解构 import 是 ImportSpecifier

ImportSpecifier 语句有 local 和 imported 属性,分别代表引入的名字和重命名后的名字:

那我们的目的明确了,就是把 ImportDefaultSpecifier 转成 ImportSpecifier,并且使用到的属性方法来设置 imported 属性,需要重命名的还要设置下 local 属性。

怎么知道使用到哪些属性方法呢?也就是如何分析变量的引用呢?

babel 提供了 scope 的 api,用于作用域分析,可以拿到作用域中的声明,和所有引用这个声明的地方。

比如这里就可以用 scope.getBinding 方法拿到该变量的声明:

 
 
 
 
  1. const binding = scope.getBinding('path'); 

然后用 binding.references 就可以拿到所有引用这个声明的地方,也就是 path.join 和 path.sep。

之后就可以把这两处引用改为直接的方法调用,然后修改下 import 语句为解构就可以了。

我们总结一下步骤:

  • 找到 import 语句中的 ImportDefaultSpecifier
  • 拿到 ImportDefaultSpecifier 在作用域的声明(binding)
  • 找到所有引用该声明的地方(reference)
  • 修改各处引用为直接调用函数的形式,收集函数名
  • 如果作用域中有重名的变量,则生成一个唯一的函数名
  • 根据收集的函数名来修改 ImportDefaultSpecifier 为 ImportSpecifier

原理大概过了一遍,我们来写下代码

代码实现

babel 插件是函数返回对象的形式,返回的对象中主要是通过 visitor 属性来指定对什么 AST 做什么处理。

我们搭一个 babel 插件的骨架:

 
 
 
 
  1. const { declare } = require('@babel/helper-plugin-utils'); 
  2.  
  3. const importTransformPlugin = declare((api, options, dirname) => { 
  4.     api.assertVersion(7); 
  5.  
  6.     return { 
  7.         visitor: { 
  8.             ImportDeclaration(path) { 
  9.                  
  10.             } 
  11.         } 
  12.     } 
  13. }); 
  14.  
  15. module.exports = importTransformPlugin; 

这里我们要处理的是 import 语句 ImportDeclaration。

@babel/helper-plugin-utils 包的 declare 方法的作用是给 api 扩充一个 assertVersion 方法。而 assertVersion 的作用是如果这个插件工作在了 babel6 上就会报错说这个插件只能用在 babel7,可以避免报的错看不懂。

path 是用于操作 AST 的一些 api,而且也保留了 node 之间的关联,比如 parent、sibling 等。

接下来进入正题:

我们要先取出 specifiers 的部分,然后找出 ImportDefaultSpecifier:

 
 
 
 
  1. ImportDeclaration(path) { 
  2.     // 找到 import 语句中的 default import 
  3.     const importDefaultSpecifiers = path.node.specifiers.filter(item => api.types.isImportDefaultSpecifier(item)); 
  4.     // 对每个 default import 做转换 
  5.     importDefaultSpecifiers.forEach(defaultSpecifier => { 
  6.  
  7.     }); 

然后对每一个 default import 都要根据在作用域中的声明找到所有引用的地方:

 
 
 
 
  1.  // import 变量的名字 
  2. const importId = defaultSpecifier.local.name; 
  3. // 该变量的声明 
  4. const binding = path.scope.getBinding(importId); 
  5.  
  6. binding.referencePaths.forEach(referencePath=> { 
  7.    
  8. }); 

然后对每个引用到该 import 的地方都做修改,改为直接调用函数,并且把函数名收集起来。这里要注意的是,如果作用域中有同名变量还要生成一个新的唯一 id。

 
 
 
 
  1. // 该变量的声明 
  2. const binding = path.scope.getBinding(importId); 
  3.  
  4. const referedIds = []; 
  5. const transformedIds = []; 
  6. // 收集所有引用该声明的地方的方法名 
  7. binding.referencePaths.forEach(referencePath=> { 
  8.     const currentPath = referencePath.parentPath; 
  9.     const methodName = currentPath.node.property.name; 
  10.  
  11.     // 之前方法名 
  12.     referedIds.push(currentPath.node.property); 
  13.   
  14.     if (!currentPath.scope.getBinding(methodName)) {// 如果作用域没有重名变量 
  15.         const methodNameNode = currentPath.node.property; 
  16.         currentPath.replaceWith(methodNameNode); 
  17.  
  18.         transformedIds.push(methodNameNode); // 转换后的方法名 
  19.     } else {// 如果作用域有重名变量 
  20.         const newMethodName = referencePath.scope.generateUidIdentifier(methodName); 
  21.         currentPath.replaceWith(newMethodName); 
  22.  
  23.         transformedIds.push(newMethodName); // 转换后的方法名 
  24.     } 
  25. }); 

这部分逻辑比较多,着重讲一下。

我们对每个引用了该变量的地方都要记录下引用了哪个方法,比如 path.join、path.sep 就引用了 join 和 sep 方法。

然后就要把 path.join 替换成 join,把 path.sep 替换成 sep。

如果作用域中有了 join 或者 sep 的声明,需要生成一个新的 id,并且记录下新的 id 是什么。

收集了所有的方法名,就可以修改 import 语句了:

 
 
 
 
  1. // 转换 import 语句为 named import 
  2. const newSpecifiers = referedIds.map((id, index) => api.types.ImportSpecifier(transformedIds[index], id)); 
  3. path.node.specifiers = newSpecifiers; 

没有 babel 插件基础可能看的有点晕,没关系,知道他是做啥的就行。我们接下来试下效果。

思考和代码

我们做了 default import 到 named import 的自动转换,其实反过来也一样,不也是分析 scope 的 binding 和 reference,然后去修改 AST 么?感兴趣的同学可以试下反过来转换怎么写。

插件全部代码如下:

 
 
 
 
  1. const { declare } = require('@babel/helper-plugin-utils'); 
  2.  
  3. const importTransformPlugin = declare((api, options, dirname) => { 
  4.     api.assertVersion(7); 
  5.  
  6.     return { 
  7.         visitor: { 
  8.             ImportDeclaration(path) { 
  9.                 // 找到 import 语句中的 default import 
  10.                 const importDefaultSpecifiers = path.node.specifiers.filter(item => api.types.isImportDefaultSpecifier(item)); 
  11.                 // 对每个 default import 做转换 
  12.                 importDefaultSpecifiers.forEach(defaultSpecifier => { 
  13.                     // import 变量的名字 
  14.                     const importId = defaultSpecifier.local.name; 
  15.                     // 该变量的声明 
  16.                     const binding = path.scope.getBinding(importId); 
  17.  
  18.                     const referedIds = []; 
  19.                     const transformedIds = []; 
  20.                     // 收集所有引用该声明的地方的方法名 
  21.                     binding.referencePaths.forEach(referencePath=> { 
  22.                         const currentPath = referencePath.parentPath; 
  23.                         const methodName = currentPath.node.property.name; 
  24.  
  25.                         // 之前方法名 
  26.                         referedIds.push(currentPath.node.property); 
  27.  
  28.                         if (!currentPath.scope.getBinding(methodName)) {// 如果作用域没有重名变量 
  29.                             const methodNameNode = currentPath.node.property; 
  30.                             currentPath.replaceWith(methodNameNode); 
  31.  
  32.                             transformedIds.push(methodNameNode); // 转换后的方法名 
  33.                         } else {// 如果作用域有重名变量 
  34.                             const newMethodName = referencePath.scope.generateUidIdentifier(methodName); 
  35.                             currentPath.replaceWith(newMethodName); 
  36.  
  37.                             transformedIds.push(newMethodName); // 转换后的方法名 
  38.                         } 
  39.                     }); 
  40.  
  41.                     // 转换 import 语句为 named import 
  42.                     const newSpecifiers = referedIds.map((id, index) => api.types.ImportSpecifier(transformedIds[index], id)); 
  43.                     path.node.specifiers = newSpecifiers; 
  44.                 }); 
  45.             } 
  46.         } 
  47.     } 
  48. }); 
  49. module.exports = importTransformPlugin; 

总结

我们要做 default import 转 named import,也就是 ImportDefaultSpecifier 转 ImportSpecifier,要通过 scope 的 api 分析 binding 和 reference,找到所有引用的地方,替换成直接调用函数的形式,然后再去修改 import 语句的 AST 就可以了。

babel 插件特别适合做这种有规律且转换量比较大的需求,在一些场景下是有很大的威力的。

本文标题:import方式随心转,感受Babel插件的威力
当前网址:http://www.mswzjz.cn/qtweb/news34/53634.html

攀枝花网站建设、攀枝花网站运维推广公司-贝锐智能,是专注品牌与效果的网络营销公司;服务项目有等

广告

声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 贝锐智能