为什么要写这个loader

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

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

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

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

这个loader解决的问题

自己写的loader就是解决日常开发中忘记写catch的情况。
先说一下自己写的loader的功能:
1. 可以自动为await后的promise加上catch
2. 可以决定是否需要在catch函数中打印error以及return出一个边界值,可以选择加上自己的代码
3. 若是await函数外层有被try catch包裹或者本身后边就已经有catch,则不会做任何处理

  1. //一个普通的async函数
  2. const fn = async () => {
  3. const a = await pro()
  4. }
  5. //会被转化成
  6. const fn = async () => {
  7. const a = await pro().catch(err=>{})
  8. }
  9. //若是需要打印error以及return出一个边界值
  10. const fn = async () => {
  11. const { a } = await pro().catch(err=>{ console.log(err); return { } });
  12. }
  13. //or
  14. const fn = async () => {
  15. const [ a ] = await pro().catch(err=>{ console.log(err); return [ ] });
  16. }
  17. //若是需要自己额外的代码处理,自己的代码贼会在console前面,假设自己代码为 message.error(error)
  18. const fn = async () => {
  19. const [ a ] = await pro().catch(err=>{ message.error(err);console.log(err); return [ ] });
  20. }
  21. // 如果被try catch包裹,则不会进行任何处理,因为catch可以捕获到错误,擅自增加catch会扰乱原有的逻辑
  22. const fn = async () => {
  23. // 保持原样
  24. try{
  25. const [ a ] = await pro()
  26. }catch(err){}
  27. }

具体代码+讲解

接下来上代码

  1. //add-catch-loader.js
  2. const parser = require("@babel/parser");
  3. const traverse = require("babel-traverse").default;
  4. const t = require("babel-types");
  5. const template = require("@babel/template");
  6. 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就是一个纯函数,它能获取当前文件的源代码

  1. //a.js
  2. const num = 1;
  3. console.log(num);
  4. ```
  5. 那么source就是"const num = 1;console.log(num);"而我们把它转化为ast又是什么样子呢?
  6. 我把它转化为了json结果,我只截取了部分(因为太长了),大家可以去[这个网站](https://astexplorer.net/)输入一段js代码看看转化成了什么样~
  7. ## AST的大概结构
  8. ```json
  9. {
  10. "type": "File",
  11. "start": 0,
  12. "end": 32,
  13. "loc": {
  14. "start": {
  15. "line": 1,
  16. "column": 0
  17. },
  18. "end": {
  19. "line": 3,
  20. "column": 16
  21. }
  22. },
  23. "errors": [],
  24. "program": {
  25. "type": "Program",
  26. "start": 0,
  27. "end": 32,
  28. "loc": {
  29. "start": {
  30. "line": 1,
  31. "column": 0
  32. },
  33. "end": {
  34. "line": 3,
  35. "column": 16
  36. ...
  37. },
  38. "comments": []
  39. }

解决问题的思路

在ast结构中,每一个有type属性的对象都是一个节点,里面包含了这个节点的全部信息,而我们既然要操做await后的promise,那么就只需要看await操作符上下的节点就可以了,先看一下await的节点长什么样子。

上图只是 const a = await po()这一段代码的ast,其中大部分还折叠起来了。但是我们只需要关系await后的代码ast,即po()。

AwaitExpression这个节点是await po()这段代码,CallExpression这个节点是po()这个节点。那么await po().catch(err=>{ })代码的节点又长什么样子呢?

如下图,AwaitExpression是await pro().catch(err=>{});整段代码的节点,MemberExpression是pro().catch;的节点,arguments是函数体的参数,而ArrowFunctionExpression代表的就是err={},所以我们只需要把po()替换成po().catch(err=>{})。
比较一下po()和po().catch的不同(由于catch函数中的回调函数是参数,属于和po().catch一个级别,所以不把它算在内)
po()

po().catch()

从上图中就可以看出来CallExpression节点换成了MemberExpression,那么开始上代码。

具体代码

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

  1. //add-catch-loader.js
  2. const parser = require("@babel/parser");
  3. const traverse = require("babel-traverse").default;
  4. const t = require("babel-types");
  5. const template = require("@babel/template");
  6. const babel = require("@babel/core");
  7. const { createCatchIdentifier, createArguments } = require("./utils"); //自己写的方法
  8.  
  9. function addCatchLoader(source){
  10. let ast = parser.parse(source, {
  11. sourceType: "module",
  12. plugins: ["dynamicImport", "jsx","classProperties"],
  13. });
  14. }

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

  1. //add-catch-loader.js
  2. const parser = require("@babel/parser");
  3. const traverse = require("babel-traverse").default;
  4. const t = require("babel-types");
  5. const template = require("@babel/template");
  6. const babel = require("@babel/core");
  7.  
  8. const createCatchIdentifier = () => {
  9. const catchIdentifier = t.identifier("catch");
  10. return catchIdentifier;
  11. };
  12.  
  13. function addCatchLoader(source){
  14. const self = this; //缓存当前this
  15. let ast = parser.parse(source, {
  16. sourceType: "module",
  17. plugins: ["dynamicImport", "jsx"],
  18. });
  19.  
  20. const awaitMap = [];
  21.  
  22. traverse(ast,{
  23. /*
  24. 我们既然是要替换await后的整颗节点,就要先获取AwaitExpression这个节点的信息。因为有些
  25. 人在用async await习惯用try catch进行包裹,而用了try catch就没必要再加catch了,所以
  26. 我们这里需要判断await的父级节点有没有try catch。若有就使用path.skip()停止接下来的循
  27. 环,没有将当前节点的argument缓存进一个数组中,为了接下来进行比较。
  28. */
  29. AwaitExpression(path) {
  30. const tryCatchPath = path.findParent((p) => {
  31. return t.isTryStatement(p);
  32. });
  33. if (tryCatchPath) return path.skip();
  34. /*
  35. 这里leftId就是 = 左边的值,因为可能需要在catch里return,所以需要判断它的类型
  36. */
  37. const leftId = path.parent.id;
  38. if (leftId) {
  39. const type = leftId.type;
  40. path.node.argument.returnType = type;
  41. }
  42. awaitMap.push(path.node.argument);
  43. },
  44. /*
  45. CallExpression节点就是我们需要替换的节点,因为整颗ast中不止一个地方有
  46. CallExpression类型的节点,所以我们需要比较缓存的数组中有没有它,如有就代表是我们
  47. 要替换的```po()```。在这里我们需要在进行一次判断,因为源代码中可能会有await后自动加
  48. catch的情况,我们就不必处理了。
  49. */
  50. CallExpression(path) {
  51. if (!awaitMap.length) return null;
  52. awaitMap.forEach((item, index) => {
  53. if (item === path.node) {
  54. const callee = path.node.callee;
  55. const returnType = path.node.returnType; //这里取出等号左边的类型
  56. if (t.isMemberExpression(callee)) return; //若是已经有了.catch则不需要处理
  57. const MemberExpression = t.memberExpression(
  58. item,
  59. createCatchIdentifier()
  60. );
  61. const createArgumentsSelf = createArguments.bind(self); //绑定当前this
  62. const ArrowFunctionExpression_1 = createArgumentsSelf(returnType);//创建catch的回调函数里的逻辑
  63. const CallExpression = t.callExpression(MemberExpression, [
  64. ArrowFunctionExpression_1,
  65. ]);
  66. path.replaceWith(CallExpression);
  67. awaitMap[index] = null;
  68. }
  69. });
  70. },
  71. })

我们看一下createArgumentsSelf的逻辑

  1. const t = require("babel-types");
  2. const template = require("@babel/template");
  3. const loaderUtils = require("loader-utils");
  4. const { typeMap } = require("./constant");
  5.  
  6. const createCatchIdentifier = () => {
  7. const catchIdentifier = t.identifier("catch");
  8. return catchIdentifier;
  9. };
  10.  
  11. function createArguments(type) {
  12. //上边我们缓存了this并把this传入到当前函数中,就是为了取出loader的参数
  13. const { needReturn, consoleError, customizeCatchCode } =
  14. loaderUtils.getOptions(this) || {};
  15.  
  16. let returnResult = needReturn && type && typeMap[type];
  17. let code = "";
  18. let returnStatement = null;
  19. if (returnResult) {
  20. code = `return ${returnResult}`;
  21. }
  22. if (code) {
  23. returnStatement = template.statement(code)();
  24. }
  25.  
  26. /* 创建arguments:(err)=>{}
  27. 先创建ArrowFunctionExpression 参数(params,body为必须);params为err
  28. param是参数列表,为一个数组,每一项为Identifier;body为BlockStatement;
  29. */
  30. // 创建body
  31. const consoleStatement =
  32. consoleError && template.statement(`console.log(error)`)();
  33. const customizeCatchCodeStatement =
  34. typeof customizeCatchCode === "string" &&
  35. template.statement(customizeCatchCode)();
  36. const blockStatementMap = [
  37. customizeCatchCodeStatement,
  38. consoleStatement,
  39. returnStatement,
  40. ].filter(Boolean);
  41. const blockStatement = t.blockStatement(blockStatementMap);
  42. // 创建ArrowFunctionExpression
  43. const ArrowFunctionExpression_1 = t.arrowFunctionExpression(
  44. [t.identifier("error")],
  45. blockStatement
  46. );
  47. return ArrowFunctionExpression_1;
  48. }
  49.  
  50. module.exports = {
  51. createCatchIdentifier,
  52. createArguments,
  53. };

确定了就是替换这个节点,那么我们需要创建一个MemberExpression节点,查看babel-type的问的文档

object和property是必须的,而在我们的ast中,object和property又分别代表什么呢?

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的情况,比如这种

  1. const pro = new Promise((resolve,reject)=>{ reject('我错了!') })
  2.  
  3. const fn = async () => {
  4. const data = await pro;
  5. }

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

  1. //add-catch-loader.js
  2. const parser = require("@babel/parser");
  3. const traverse = require("babel-traverse").default;
  4. const t = require("babel-types");
  5. const template = require("@babel/template");
  6. const babel = require("@babel/core");
  7.  
  8. const createCatchIdentifier = () => {
  9. const catchIdentifier = t.identifier("catch");
  10. return catchIdentifier;
  11. };
  12.  
  13. function addCatchLoader(source){
  14. const self = this; //缓存当前this
  15. let ast = parser.parse(source, {
  16. sourceType: "module",
  17. plugins: ["dynamicImport", "jsx"],
  18. });
  19.  
  20. const awaitMap = [];
  21.  
  22. traverse(ast,{
  23. /*
  24. 我们既然是要替换await后的整颗节点,就要先获取AwaitExpression这个节点的信息。因为有些
  25. 人在用async await习惯用try catch进行包裹,而用了try catch就没必要再加catch了,所以
  26. 我们这里需要判断await的父级节点有没有try catch。若有就使用path.skip()停止接下来的循
  27. 环,没有将当前节点的argument缓存进一个数组中,为了接下来进行比较。
  28. */
  29. AwaitExpression(path) {
  30. const tryCatchPath = path.findParent((p) => {
  31. return t.isTryStatement(p);
  32. });
  33. if (tryCatchPath) return path.skip();
  34. /*
  35. 这里leftId就是 = 左边的值,因为可能需要在catch里return,所以需要判断它的类型
  36. */
  37. const leftId = path.parent.id;
  38. if (leftId) {
  39. const type = leftId.type;
  40. path.node.argument.returnType = type;
  41. }
  42. awaitMap.push(path.node.argument);
  43. },
  44. /*
  45. CallExpression节点就是我们需要替换的节点,因为整颗ast中不止一个地方有
  46. CallExpression类型的节点,所以我们需要比较缓存的数组中有没有它,如有就代表是我们
  47. 要替换的```po()```。在这里我们需要在进行一次判断,因为源代码中可能会有await后自动加
  48. catch的情况,我们就不必处理了。
  49. */
  50. CallExpression(path) {
  51. if (!awaitMap.length) return null;
  52. awaitMap.forEach((item, index) => {
  53. if (item === path.node) {
  54. const callee = path.node.callee;
  55. const returnType = path.node.returnType; //这里取出等号左边的类型
  56. if (t.isMemberExpression(callee)) return; //若是已经有了.catch则不需要处理
  57. const MemberExpression = t.memberExpression(
  58. item,
  59. createCatchIdentifier()
  60. );
  61. const createArgumentsSelf = createArguments.bind(self); //绑定当前this
  62. const ArrowFunctionExpression_1 = createArgumentsSelf(returnType);//创建catch的回调函数里的逻辑
  63. const CallExpression = t.callExpression(MemberExpression, [
  64. ArrowFunctionExpression_1,
  65. ]);
  66. path.replaceWith(CallExpression);
  67. awaitMap[index] = null;
  68. }
  69. });
  70. },
  71. })
  72. const { code } = babel.transformFromAstSync(ast, null, {
  73. configFile: false, // 屏蔽 babel.config.js,否则会注入 polyfill 使得调试变得困难
  74. });
  75. return code;

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

在项目中使用

[github地址](https://github.com/mayu888/await-add-catch-loader),```欢迎大家star or issues!```

  1. npm i await-add-catch-loader --save-dev
  2. // or
  3. yarn add await-add-catch-loader --save-dev
  4.  
  5. //webpack.config.js
  6. module.exports = {
  7. //...
  8. module: {
  9. rules: [
  10. {
  11. test: /\.(js|jsx)$/,
  12. exclude: /node_modules/, //刨除哪个文件里的js文件
  13. include: path.resolve(__dirname, "./src"),
  14. use: [
  15. {loader: "babel-loader"},
  16. {
  17. loader: 'await-add-catch-loader',
  18. options: {
  19. needReturn: true,
  20. consoleError: true,
  21. customizeCatchCode: "//please input you want to do",
  22. },
  23. },
  24. ],
  25. },
  26. ],
  27. },
  28. }

项目中的源代码:

loader处理后的代码

写loader中的一些困难及想法

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

  1. //webpack.config.js
  2. {
  3. test: /\.(js|jsx)$/,
  4. exclude: /node_modules/,
  5. include: path.resolve(__dirname, './src'),
  6. use: [
  7. {
  8. loader: 'babel-loader?cacheDirectory',
  9. options: {
  10. presets: [
  11. [
  12. '@babel/preset-env', //调用es6-es5的模块],
  13. '@babel/preset-react' //转化react语法的模块
  14. ],
  15. plugins:
  16. [
  17. '@babel/plugin-transform-runtime',
  18. [path.resolve(__dirname, 'babel-plugin', 'await-catch-babel-plugin')]//自己写的babel插件
  19. ]}

因为要使用'@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的更多相关文章

  1. 如何手动写一个Python脚本自动爬取Bilibili小视频

    如何手动写一个Python脚本自动爬取Bilibili小视频 国庆结束之余,某个不务正业的码农不好好干活,在B站瞎逛着,毕竟国庆嘛,还让不让人休息了诶-- 我身边的很多小伙伴们在朋友圈里面晒着出去游玩 ...

  2. .NET 除了用 Task 之外,如何自己写一个可以 await 的对象?

    .NET 中的 async / await 写异步代码用起来真的很爽,就像写同步一样.我们可以在各种各样的异步代码中看到 Task 返回值,这样大家便可以使用 await 等待这个方法.不过,有时需要 ...

  3. 写一个umi插件 自动生成代码 解放cv的双手

    引言 最近在写一个中台项目,使用的react的umi框架. 各种增删改查.基本是列表页 新建页 详情页这种页面 为了避免不必要的简单重复(主要是想偷懒) 于是想去实现自己的一个代码生成器 探索 首先, ...

  4. Cordova webapp实战开发:(5)如何写一个Andorid下自动更新的插件?

    在 <Cordova webapp实战开发:(4)Android环境搭建>中我们搭建好了开发环境,也给大家布置了调用插件的预习作业,做得如何了呢?今天我们来学一下如何自己从头建立一个And ...

  5. 写一个shell,自动执行jmeter测试脚本

    贡献一个自己写的shell脚本,很简单,但又可以高效率的自动执行jmeter压测脚本. #!/bin/bash #author:zhangyl #version:V1 #该脚本放置于压测脚本的上一层目 ...

  6. 写一个TT模板自动生成spring.net下面的配置文件。

    这个是目标. 然后想着就怎么开始 1.

  7. 通过用jQuery写一个页面,我学到了什么

    概述 前几天面试,hr发来一个测试文件,让我做做看.我一看,其实就是根据PSD需求写一个页面,再加上一些互动效果即可. 刚好我之前学了切图,jquery等知识还没练手过,于是高兴的答应了. 最后花了3 ...

  8. 定义一组抽象的 Awaiter 的实现接口,你下次写自己的 await 可等待对象时将更加方便

    我在几篇文章中都说到了在 .NET 中自己实现 Awaiter 情况.async / await 写异步代码用起来真的很爽,就像写同步一样.然而实现 Awaiter 没有现成的接口,它需要你按照编译器 ...

  9. Cordova webapp实战开发:(6)如何写一个iOS下获取APP版本号的插件?

    上一篇我们学习了如何写一个Andorid下自动更新的插件,我想还有一部分看本系列blog的开发人员希望学习在iOS下如何做插件的吧,那么今天你就可以来看看这篇文字了. 本次练习你能学到的 学习如何获取 ...

随机推荐

  1. 04、MyBatis DynamicSQL(Mybatis动态SQL)

    1.动态SQL简介 动态 SQL是MyBatis强大特性之一. 动态 SQL 元素和使用 JSTL 或其他类似基于 XML 的文本处理器相似. MyBatis 采用功能强大的基于 OGNL 的表达式来 ...

  2. Java并发编程 - Runnbale、Future、Callable 你不知道的那点事(二)

    Java并发编程 - Runnbale.Future.Callable 你不知道的那点事(一)大致说明了一下 Runnable.Future.Callable 接口之间的关系,也说明了一些内部常用的方 ...

  3. 即时编译器 (JIT) 详解

    最近听我的导师他们讨论Java的即时编译器(JIT),当时并不知道这是啥东西,所以就借着周末的时间,学习了一下! 一.概述 在部分的商用虚拟机(Sun HotSpot)中,Java程序最初是通过解释器 ...

  4. MFC的窗口句柄

    1.窗口.控件的指针和句柄的相互转化 1)指针转化为句柄在MFC应用程序中首先要获得窗口的指针,然后将其转化为句柄 CWnd* pWnd; HANDLE hWnd = pWnd->GetSafe ...

  5. 网站滑到指定的位置给div添加动画效果

    <!DOCTYPE html> <html> <head> <style> .anim-show { width:100px; height:100px ...

  6. .Net 开源项目 FreeRedis 实现思路之 - Redis 6.0 客户端缓存技术

    写在开头 FreeRedis 是一款继 CSRedisCore 之后重写的 .NET redis 客户端开源组件,以 MIT 协议开源托管于 github,目前支持 .NET 5..NETCore 2 ...

  7. U盘数据丢失怎么办,还能恢复吗

    有时候在用U盘的时候会出现数据丢失或者U盘无法打开的问题,检查过之后,发现U盘格式变成了RAW,这是怎么回事?遇到这种情况该怎么解决呢? 首先来看看造成u盘格式变为RAW的主要原因: 1.非正常退出u ...

  8. Android应用测试指南

    一.Android 的 SDK Windows 版本安装 按顺序安装以下内容 1.    安装JDK(Java Development Kit, 即Java开发工具包) 2. 安装Eclipse 集成 ...

  9. 工作中用到的redis操作

    del exists 1.字符串 set,get 2.列表 lRange lRem lPush rPush 3.有序列表 zadd zrem zscore 4.hash hset hget hdel

  10. # 夏普R shv39 0基础精简优化指南

    手机介绍 夏普AQUOS R是目前市面上用户数量和好评数量都非常多的一款产品.它性价比极高,适合各个年龄段的用户选择来满足办公或者家用或者娱乐等不同方面的需求.目前闲鱼价格在400左右,搭载骁龙835 ...