当前位置: 首页 > 科技观察

导入方式随心所欲,感受Babel插件的强大

时间:2023-03-20 11:30:45 科技观察

本文转载自微信公众号《神光的编程秘籍》,作者神说有光zxg。转载本文请联系神光编程秘籍公众号。当我们导入一个模块时,可以默认这样导入:importpathfrom'path';path.join('a','b');functionfunc(){constsep='aaa';console.log(path.sep);}也可以这样解构和引入:import{join,sepas_sep}from'path';join('a','b');functionfunc(){constsep='aaa';console.log(_sep);}第一种默认导入称为defaultimport,第二种解构导入称为namedimport。不知道你习惯的是哪一种。如果需要将所有默认导入转换为命名导入,你会怎么做?可能你会说,这是把所有用到变量的地方都找出来,修改成直接调用方法,然后那些方法名以开头的解构方式写在import语句里?但是如果工程中有100多个这样的文件要改怎么办?(触发treeshking这样改)这时候可以考虑babel插件,很适合这种规则的、大规模代码的自动修改。让我们通过这个例子来感受一下babel插件的强大吧。因为代码很多,大家可能没有耐心看,我们先看看效果吧:测试效果输入代码如下:importpathfrom'path';path.join('a','b');functionfunc(){constsep='aaa';console.log(path.sep);}我们引入babel插件,读取输入代码并做转换:const{transformFileSync}=require('@babel/core');constimportTransformPlugin=require('./plugin/importTransform');constpath=require('path');const{code}=transformFileSync(path.join(__dirname,'./sourceCode.js'),{plugins:[[importTransformPlugin]]});控制台。日志(代码);打印如下:我们已经完成了从默认导入到命名导入的自动转换。有些同学可能会担心重名的问题。我们来测试一下:可以看到插件已经对重名问题进行??了处理。思路分析import语句的中间部分称为说明符,我们可以通过astexplorer.net直观地查看其AST。比如这样一条import语句:importReact,{useStateastest,useEffect}from'react';其对应的AST如下:也就是说,默认导入是ImportDefaultSpecifier,解构后的导入是ImportSpecifier。ImportSpecifier语句有local和imported属性,分别代表importedName和renamedname:那么我们的目的就很明确了,就是将ImportDefaultSpecifier转化为ImportSpecifier,使用attribute方法设置import属性,如果是就设置local属性需要重命名。你怎么知道要使用哪些属性方法?即如何分析变量引用?Babel提供了scopeAPI用于作用域解析,可以获取作用域中的声明以及所有引用该声明的地方。例如,您可以使用scope.getBinding方法来获取变量的声明:constbinding=scope.getBinding('path');然后使用binding.references获取所有引用此声明的地方,即path.join和path.sep。之后可以把这两个引用改成直接方法调用,然后修改import语句解构即可。总结一下步骤:在import语句中找到ImportDefaultSpecifier,在scope中获取ImportDefaultSpecifier声明(绑定),找到所有对该声明的引用(reference),修改各处的引用直接调用函数,收集函数名if在作用域中如果有同名变量,则生成一个唯一的函数名。根据收集到的函数名修改ImportDefaultSpecifier为ImportSpecifier。过段时间,我们来写下实现babel插件的代码。函数返回对象,返回的对象主要是通过visitor属性来指定用什么AST来做什么。让我们构建一个babel插件的框架:const{declare}=require('@babel/helper-plugin-utils');constimportTransformPlugin=declare((api,options,dirname)=>{api.assertVersion(7);return{visitor:{ImportDeclaration(path){}}}});module.exports=importTransformPlugin;这里我们要处理import语句ImportDeclaration。@babel/helper-plugin-utils包的declare方法用于将assertVersion方法扩展到api。assertVersion的作用是如果插件在babel6上运行,会报错说这个插件只能在babel7上使用,可以避免报错。Path是一些用来操作AST的API,也保留了节点之间的关联关系,比如parent,sibling等。接下来进入正题:我们需要先把specifiers部分拿出来,然后找出ImportDefaultSpecifier:ImportDeclaration(path){//在import语句中找到defaultimportconstimportDefaultSpecifiers=path.node.specifiers.filter(item=>api.types.isImportDefaultSpecifier(item));//转换每个默认importimportDefaultSpecifiers.forEach(defaultSpecifier=>{});}然后对于每一个defaultimport,根据作用域中的声明查找所有引用://导入变量的名称constimportId=defaultSpecifier.local.name;//变量的声明constbinding=path.scope.getBinding(importId);binding.referencePaths.forEach(referencePath=>{});然后对每个引用import做修改,改为直接调用函数,收集函数名。这里需要注意的是,如果scope中存在同名变量,则会生成一个新的唯一id。//变量的声明constbinding=path.scope.getBinding(importId);constreferredIds=[];consttransformedIds=[];//收集所有引用声明的方法名binding.referencePaths.forEach(referencePath=>{constcurrentPath=referencePath.parentPath;constmethodName=currentPath.node.property.name;//前面的方法名referredIds.push(currentPath.node.property);if(!currentPath.scope.getBinding(methodName)){//如果scopeisnotThevariablewithsamenameNode=currentPath.node.property;currentPath.replaceWith(methodNameNode);transformedIds.push(methodNameNode);//转换后的方法名}else{//如果scope有一个变量与同名constnewMethodName=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语句://ConverttheimportstatementtonamedimportconstnewSpecifiers=referredIds.map((id,index)=>api.types.ImportSpecifier(transformedIds[index],id));path.node.specifiers=newSpecifiers;没有babel插件基础,可能看起来有点晕,不过没关系,知道他是干什么的就行。接下来我们试试看。为了思想和代码,我们做了一个从默认导入到命名导入的自动转换。其实反过来也是一样的。不也是分析作用域的绑定和引用,然后修改AST吗?有兴趣的同学可以试试反向转换怎么写。插件完整代码如下:const{declare}=require('@babel/helper-plugin-utils');constimportTransformPlugin=declare((api,options,dirname)=>{api.assertVersion(7);return{visitor:{ImportDeclaration(path){//在import语句中找到defaultimportconstimportDefaultSpecifiers=path.node.specifiers.filter(item=>api.types.isImportDefaultSpecifier(item));//转换每一个defaultimportimportDefaultSpecifiers.forEach(defaultSpecifier=>{//导入变量的名称constimportId=defaultSpecifier.local.name;//变量的声明constbinding=path.scope.getBinding(importId);constreferredIds=[];consttransformedIds=[];//收集所有对声明的引用本地方法名binding.referencePaths.forEach(referencePath=>{constcurrentPath=referencePath.parentPath;constmethodName=currentPath.node.property.name;//上一个方法名referenceIds.push(currentPath.node.property);if(!currentPath.scope.getBinding(methodName)){//如果scope没有相同的namevariableconstmethodNameNode=currentPath.node.property;currentPath.replaceWith(methodNameNode);transformedIds.push(methodNameNode);//转换后的方法名}else{//如果scope有同名变量constnewMethodName=referencePath.scope.generateUidIdentifier(methodName);currentPath.replaceWith(newMethodName);transformedIds.push(newMethodName);//转换后的方法名}});//将导入语句转换为命名导入constnewSpecifiers=referredIds.map((id,index)=>api.types.ImportSpecifier(transformedIds[index],id));path.node.specifiers=newSpecifiers;});}}}});module.exports=importTransformPlugin;总而言之,我们有要做defaultimport命名导入,即ImportDefaultSpecifier到ImportSpecifier,需要通过scopeAPI解析绑定和引用,找到所有的引用,替换为直接调用函数的形式,然后修改import的AST陈述。babel插件特别适合这种有规律且比较大的转换需求,在某些场景下非常强大。