最新公告
  • 欢迎您光临网站无忧模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 写一个为await自动加上catch的loader逐渐了解AST以及babel

    正文概述 掘金(阿玉君_)   2020-12-13   316

    为什么要写这个loader

    我们在日常开发中经常用到async await去请求接口,解决异步。可async await语法的缺点就是若await后的Promise抛出错误不能捕获,整段代码区就会卡住。从而使下面的逻辑不能顺利执行。也许会有人说,卡住就是为了不进行后续的代码,以免造成更大的错误,可大多数情况下需要catch住错误并给出一个边界值使代码正常执行。
    我以前经常常常会这么写:

    const request = async (){
    	const { data = [] } = await getList() || {};
       	 //...other
    };
    

    这样写看似有些高端,但其实风险系数很高,假设getList()请求发生了错误并且没有捕获到,那么后边的逻辑或表达式并不会生效,后续的代码并不能顺序执行。 这种情况的最优解就是getList()能后捕获到错误,虽然现在大多数axios都会catch,但是业务开发中应该不止请求才会用到Promise。那么另一种解法是?

    const request = async (){
    	const { data = [] } = await getList().catch(err=>{ //...do you want to do }) || {};
       	 //...other
    };
    

    这个loader解决的问题

    自己写的loader就是解决日常开发中忘记写catch的情况。
    先说一下自己写的loader的功能:

    1. 可以自动为await后的promise加上catch
    2. 可以决定是否需要在catch函数中打印error以及return出一个边界值,可以选择加上自己的代码
    3. 若是await函数外层有被try catch包裹或者本身后边就已经有catch,则不会做任何处理
    //一个普通的async函数
    const fn = async () => {               
      const a = await pro()       
    }
    //会被转化成
    const fn = async () => {
      const a = await pro().catch(err=>{})
    }
    //若是需要打印error以及return出一个边界值
    const fn = async () => {
      const { a } = await pro().catch(err=>{ console.log(err); return { }  });
    }
    //or
    const fn = async () => {
      const [ a ] = await pro().catch(err=>{ console.log(err); return [ ]  });
    }
    //若是需要自己额外的代码处理,自己的代码贼会在console前面,假设自己代码为 message.error(error)
    const fn = async () => {
      const [ a ] = await pro().catch(err=>{ message.error(err);console.log(err); return [ ]  });
    }
    // 如果被try catch包裹,则不会进行任何处理,因为catch可以捕获到错误,擅自增加catch会扰乱原有的逻辑
    const fn = async () => {
    // 保持原样
    try{
      const [ a ] = await pro()
    }catch(err){}
    }
    

    具体代码+讲解

    接下来上代码

    //add-catch-loader.js
    const parser = require("@babel/parser");
    const traverse = require("babel-traverse").default;
    const t = require("babel-types");
    const template = require("@babel/template");
    const babel = require("@babel/core");
    

    先来介绍一下各个babel包的作用

    1. @babel/parser:解析js代码生成ast,因为loader读取的js文件中的源码,而我们又不能直接操作源码进行修改,只能先转为ast进行操作。
    2. babel-traverse:遍历ast,因为ast是一颗树形结构,其中每个操作符、表达式等都是一个节点,是整颗树上的一个枝干,我们通过traverse去遍历整棵树来获取其中一个节点的信息来修改它。
    3. babel-types:我用来判断一个节点的类型。
    4. @babel/template:我用来将代码段转为ast节点。
    5. @babel/core:代码生成,ast操作完后得到了一颗新的ast,那么需要把ast在转为js代码输出到文件中。

    通过上边的几个包就看出了babel处理js的三个过程:解析(parase)、转换(transform)、生成(generator)

    loader就是一个纯函数,它能获取当前文件的源代码

     //a.js
    const num = 1;
    console.log(num);
    

    那么source就是"const num = 1;console.log(num);"而我们把它转化为ast又是什么样子呢? 我把它转化为了json结果,我只截取了部分(因为太长了),大家可以去这个网站输入一段js代码看看转化成了什么样~

    AST的大概结构

    {
      "type": "File",
      "start": 0,
      "end": 32,
      "loc": {
        "start": {
          "line": 1,
          "column": 0
        },
        "end": {
          "line": 3,
          "column": 16
        }
      },
      "errors": [],
      "program": {
        "type": "Program",
        "start": 0,
        "end": 32,
        "loc": {
          "start": {
            "line": 1,
            "column": 0
          },
          "end": {
            "line": 3,
            "column": 16
    	...  
      },
      "comments": []
    }
    

    解决问题的思路

    在ast结构中,每一个有type属性的对象都是一个节点,里面包含了这个节点的全部信息,而我们既然要操做await后的promise,那么就只需要看await操作符上下的节点就可以了,先看一下await的节点长什么样子。 写一个为await自动加上catch的loader逐渐了解AST以及babel 写一个为await自动加上catch的loader逐渐了解AST以及babel 上图只是 const a = await po()这一段代码的ast,其中大部分还折叠起来了。但是我们只需要关系await后的代码ast,即po()写一个为await自动加上catch的loader逐渐了解AST以及babel AwaitExpression这个节点是await po()这段代码,CallExpression这个节点是po()这个节点。那么await po().catch(err=>{ })代码的节点又长什么样子呢? 写一个为await自动加上catch的loader逐渐了解AST以及babel 如下图,AwaitExpressionawait pro().catch(err=>{});整段代码的节点,MemberExpressionpro().catch;的节点,arguments是函数体的参数,而ArrowFunctionExpression代表的就是err={},所以我们只需要把po()替换成po().catch(err=>{})
    比较一下po()po().catch的不同(由于catch函数中的回调函数是参数,属于和po().catch一个级别,所以不把它算在内)
    po() 写一个为await自动加上catch的loader逐渐了解AST以及babel po().catch() 写一个为await自动加上catch的loader逐渐了解AST以及babel 从上图中就可以看出来CallExpression节点换成了MemberExpression,那么开始上代码。

    具体代码

    source就是读取的文件中的源码内容。 parser.parse就是将源代码转为AST,如果源代码中使用export和import,那么sourceType必须是module,plugin必须使用dynamicImport,jsx是为了解析jsx语法,classProperties是为了解析class语法。

    //add-catch-loader.js
    const parser = require("@babel/parser");
    const traverse = require("babel-traverse").default;
    const t = require("babel-types");
    const template = require("@babel/template");
    const babel = require("@babel/core");
    const { createCatchIdentifier, createArguments } = require("./utils"); //自己写的方法
    
    function addCatchLoader(source){
      let ast = parser.parse(source, {
        sourceType: "module",
        plugins: ["dynamicImport", "jsx","classProperties"],
      });
    }
    
    

    获得到AST语法树我们就可以使用traverse进行遍历了,traverse第一个参数是要遍历的ast,第二个参数是暴露出来的节点API。

    //add-catch-loader.js
    const parser = require("@babel/parser");
    const traverse = require("babel-traverse").default;
    const t = require("babel-types");
    const template = require("@babel/template");
    const babel = require("@babel/core");
    
    const createCatchIdentifier = () => {
      const catchIdentifier = t.identifier("catch");
      return catchIdentifier;
    };
    
    
    function addCatchLoader(source){
      const self = this; //缓存当前this
      let ast = parser.parse(source, {
        sourceType: "module",
        plugins: ["dynamicImport", "jsx"],
      });
      
      const awaitMap = [];
      
      traverse(ast,{
      /*
       我们既然是要替换await后的整颗节点,就要先获取AwaitExpression这个节点的信息。因为有些
       人在用async await习惯用try catch进行包裹,而用了try catch就没必要再加catch了,所以
       我们这里需要判断await的父级节点有没有try catch。若有就使用path.skip()停止接下来的循
       环,没有将当前节点的argument缓存进一个数组中,为了接下来进行比较。 
      */
          AwaitExpression(path) {
          const tryCatchPath = path.findParent((p) => {
            return t.isTryStatement(p);
          });
          if (tryCatchPath) return path.skip();
          /*
           这里leftId就是 = 左边的值,因为可能需要在catch里return,所以需要判断它的类型
          */
          const leftId = path.parent.id;
          if (leftId) {
            const type = leftId.type;
            path.node.argument.returnType = type;
          }
          awaitMap.push(path.node.argument);
        },
      /*
        CallExpression节点就是我们需要替换的节点,因为整颗ast中不止一个地方有
        CallExpression类型的节点,所以我们需要比较缓存的数组中有没有它,如有就代表是我们
        要替换的```po()```。在这里我们需要在进行一次判断,因为源代码中可能会有await后自动加
        catch的情况,我们就不必处理了。
      */
         CallExpression(path) {
          if (!awaitMap.length) return null;
          awaitMap.forEach((item, index) => {
            if (item === path.node) {
              const callee = path.node.callee;
              const returnType = path.node.returnType; //这里取出等号左边的类型
              if (t.isMemberExpression(callee)) return; //若是已经有了.catch则不需要处理
              const MemberExpression = t.memberExpression(
                item,
                createCatchIdentifier()
              );
              const createArgumentsSelf = createArguments.bind(self); //绑定当前this
              const ArrowFunctionExpression_1 = createArgumentsSelf(returnType);//创建catch的回调函数里的逻辑
              const CallExpression = t.callExpression(MemberExpression, [
                ArrowFunctionExpression_1,
              ]);
              path.replaceWith(CallExpression);
              awaitMap[index] = null;
            }
          });
        },
      })
    

    我们看一下createArgumentsSelf的逻辑

    const t = require("babel-types");
    const template = require("@babel/template");
    const loaderUtils = require("loader-utils");
    const { typeMap } = require("./constant");
    
    const createCatchIdentifier = () => {
      const catchIdentifier = t.identifier("catch");
      return catchIdentifier;
    };
    
    function createArguments(type) {
      //上边我们缓存了this并把this传入到当前函数中,就是为了取出loader的参数
      const { needReturn, consoleError, customizeCatchCode } =
        loaderUtils.getOptions(this) || {};
    
      let returnResult = needReturn && type && typeMap[type];
      let code = "";
      let returnStatement = null;
      if (returnResult) {
        code = `return ${returnResult}`;
      }
      if (code) {
        returnStatement = template.statement(code)();
      }
    
      /* 创建arguments:(err)=>{}
        先创建ArrowFunctionExpression 参数(params,body为必须);params为err
        param是参数列表,为一个数组,每一项为Identifier;body为BlockStatement;
      */
      //  创建body
      const consoleStatement =
        consoleError && template.statement(`console.log(error)`)();
      const customizeCatchCodeStatement =
        typeof customizeCatchCode === "string" &&
        template.statement(customizeCatchCode)();
      const blockStatementMap = [
        customizeCatchCodeStatement,
        consoleStatement,
        returnStatement,
      ].filter(Boolean);
      const blockStatement = t.blockStatement(blockStatementMap);
      // 创建ArrowFunctionExpression
      const ArrowFunctionExpression_1 = t.arrowFunctionExpression(
        [t.identifier("error")],
        blockStatement
      );
      return ArrowFunctionExpression_1;
    }
    
    module.exports = {
      createCatchIdentifier,
      createArguments,
    };
    
    
    

    确定了就是替换这个节点,那么我们需要创建一个MemberExpression节点,查看babel-type的问的文档 写一个为await自动加上catch的loader逐渐了解AST以及babel object和property是必须的,而在我们的ast中,object和property又分别代表什么呢? 写一个为await自动加上catch的loader逐渐了解AST以及babel po()就是object,catch就是property,这样我们的po().catch体就创建成功了。而po().catch是肯定不够的,我们需要一个完整的po().catch(err=>{}) 结构,而err=>{}作为参数是和MemberExpression节点平级的,createArgumentsSelf函数就是创建了err=>{},其中需要根据参数判断是否需要打印error,是否需要return边界值,以及是否有别的逻辑代码,原理和创建catch一样。最后创建好了使用path.replaceWith(要替换成的节点)就可以了。但是要注意将缓存节点的数组中将这个节点删掉,因为ast遍历中若是某个节点发生了改变,那么就会一直遍历,造成死循环!
    因为我目前的处理的是await后跟的是一个函数的情况,即po()是一个函数,函数执行返回的是一个promise,那么还有await后直接跟promise的情况,比如这种

    const pro = new Promise((resolve,reject)=>{ reject('我错了!') })
    
    const fn = async () => {
      const data = await pro;
    }
    

    这种情况也需要考虑进去,我代码上就不放了,pro是一个Identifier节点,思路和CallExpression完全一样。
    最后我们处理完ast节点,需要把新节点在转回代码返回回去

    //add-catch-loader.js
    const parser = require("@babel/parser");
    const traverse = require("babel-traverse").default;
    const t = require("babel-types");
    const template = require("@babel/template");
    const babel = require("@babel/core");
    
    const createCatchIdentifier = () => {
      const catchIdentifier = t.identifier("catch");
      return catchIdentifier;
    };
    
    
    function addCatchLoader(source){
      const self = this; //缓存当前this
      let ast = parser.parse(source, {
        sourceType: "module",
        plugins: ["dynamicImport", "jsx"],
      });
      
      const awaitMap = [];
      
      traverse(ast,{
      /*
       我们既然是要替换await后的整颗节点,就要先获取AwaitExpression这个节点的信息。因为有些
       人在用async await习惯用try catch进行包裹,而用了try catch就没必要再加catch了,所以
       我们这里需要判断await的父级节点有没有try catch。若有就使用path.skip()停止接下来的循
       环,没有将当前节点的argument缓存进一个数组中,为了接下来进行比较。 
      */
          AwaitExpression(path) {
          const tryCatchPath = path.findParent((p) => {
            return t.isTryStatement(p);
          });
          if (tryCatchPath) return path.skip();
          /*
           这里leftId就是 = 左边的值,因为可能需要在catch里return,所以需要判断它的类型
          */
          const leftId = path.parent.id;
          if (leftId) {
            const type = leftId.type;
            path.node.argument.returnType = type;
          }
          awaitMap.push(path.node.argument);
        },
      /*
        CallExpression节点就是我们需要替换的节点,因为整颗ast中不止一个地方有
        CallExpression类型的节点,所以我们需要比较缓存的数组中有没有它,如有就代表是我们
        要替换的```po()```。在这里我们需要在进行一次判断,因为源代码中可能会有await后自动加
        catch的情况,我们就不必处理了。
      */
         CallExpression(path) {
          if (!awaitMap.length) return null;
          awaitMap.forEach((item, index) => {
            if (item === path.node) {
              const callee = path.node.callee;
              const returnType = path.node.returnType; //这里取出等号左边的类型
              if (t.isMemberExpression(callee)) return; //若是已经有了.catch则不需要处理
              const MemberExpression = t.memberExpression(
                item,
                createCatchIdentifier()
              );
              const createArgumentsSelf = createArguments.bind(self); //绑定当前this
              const ArrowFunctionExpression_1 = createArgumentsSelf(returnType);//创建catch的回调函数里的逻辑
              const CallExpression = t.callExpression(MemberExpression, [
                ArrowFunctionExpression_1,
              ]);
              path.replaceWith(CallExpression);
              awaitMap[index] = null;
            }
          });
        },
      })
      const { code } = babel.transformFromAstSync(ast, null, {
        configFile: false, // 屏蔽 babel.config.js,否则会注入 polyfill 使得调试变得困难
      });
       return code;
    

    有些人可能在替换节点时用继续深度遍历当前节点的方法,因为要替换的节点必定是AwaitExpression的子节点嘛,我为了使整体代码结构看起来更结构化,所以这里使用了缓存节点。

    在项目中使用

    github地址,欢迎大家star or issues!

    npm i await-add-catch-loader --save-dev
    // or
    yarn add await-add-catch-loader --save-dev
    
    //webpack.config.js
    module.exports = {
      //...
        module: {
        rules: [
          {
            test: /\.(js|jsx)$/,
            exclude: /node_modules/, //刨除哪个文件里的js文件
            include: path.resolve(__dirname, "./src"),
            use: [
              {loader: "babel-loader"},
              {
                loader: 'await-add-catch-loader',
                options: {
                  needReturn: true,
                  consoleError: true,
                  customizeCatchCode: "//please input you want to do",
                },
              },
            ],
          },
        ],
      },
    }
    

    项目中的源代码: 写一个为await自动加上catch的loader逐渐了解AST以及babel loader处理后的代码 写一个为await自动加上catch的loader逐渐了解AST以及babel

    写loader中的一些困难及想法

    从功能上来说单纯为了给promise加上catch而写一个loader是完全没必要的,因为loader的核心作用是为了处理一个文件级别的模块,单纯实现一个小功能有些杀鸡用宰牛刀的感觉,我一开始的目的其实是写一个babel的插件,想在babel处理js的过程中就完成这个功能,但是babel插件有一个点就是在处理每一个ast节点时,会顺序的执行每一个插件,也就是每一个ast节点在babel插件中只进行一次处理,并不是在执行完一个插件后再去执行下一个插件,其目的是优化性能,毕竟dom树太复杂遍历一次的成本就会越高。这样带来的问题就是我的插件在处理到AwaitExpression节点前,别的插件已经把async await替换成了generator,这样我的插件就失效了。

    //webpack.config.js
    {
    test: /\.(js|jsx)$/,
    exclude: /node_modules/,
    include: path.resolve(__dirname, './src'),
    use: [
    {
    loader: 'babel-loader?cacheDirectory',
    options: {
    presets: [
    [
    '@babel/preset-env', //调用es6-es5的模块],
    '@babel/preset-react' //转化react语法的模块
    ],
    plugins: 
    [
    '@babel/plugin-transform-runtime',
    [path.resolve(__dirname, 'babel-plugin', 'await-catch-babel-plugin')]//自己写的babel插件
    ]}
    

    因为要使用'@babel/preset-env'将es6转es5,而使用这个预设必须要使用'@babel/plugin-transform-runtime'来处理async await,通过分析源码,'@babel/plugin-transform-runtime'pre阶段对async函数generator化,pre阶段就是刚进入节点的阶段,是自己写的插件在后续的遍历中没有了AwaitExpression节点。这个问题搜了好久也未曾找到解决办法,特意去了stackOverflow提问,也没人回复,但是发现一个类似的问题,也没解决办法,所以放弃了babel插件的写法。
    也曾想过使用webpack插件来完成此功能,但是也会偏离webpack插件的核心思想,所以就放弃了。 我的目的也是想更深次的学习一下webpack、babel在编译过程中做的事,掌握它们的原理,所以最后还是选择了loader的写法。


    下载网 » 写一个为await自动加上catch的loader逐渐了解AST以及babel

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元