最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • webpack loader源码转译,对React组件包裹错误边界

    正文概述 掘金(Doerme)   2021-07-15   740

    前言

    React项目常常会遇到整个页面突然白屏。遇到这种问题大概率是界面渲染过程中,组件render方法抛出异常导致React runtime崩溃。本文介绍编写webpack loader在项目构建中模块编译时自动对React组件做错误边界包裹处理。

    阅读本文,你将会了解使用@babel/parser@babel/traverse@babel/template@babel/generator四个库,把JavaScript模块代码,从源码按需转化成目标代码的实践过程。

    为什么使用webpack loader

    背景

    webpack模块编译阶段,入口bundle用到的模块都将逐一编译,每个模块的编译过程中会根据webpackmodule配置,执行每个loaderloader默认暴露的方法,能获取到模块的源码,经过处理并最终返回目标代码完成对一个模块的一次转译过程。

    结论

    可以使用loader,通过接收源码,分析源码,修改源码,最终返回源码的步骤实现对React组件模块的源码,增加包裹错误边界的逻辑。

    步骤

    1.分析源码

    分析jsx/tsx模块的源码,有很多种方式。本方案主要使用@babel/parser对源码进行AST抽象语法树对象的转换。

    源码转换成AST对象后,接下来要做的就是对AST对象的分析。本文主要介绍React的ESM规范模块做相关的处理,ESM规范下React的组件暴露主要有以下四种方式:

    export default ComponentA // 情况1 export default
    
    export default {ComponentA, ComponentB, ComponentC} // 情况2 export default {}
    
    // 情况3 export const
    export const ComponentA = (props) => {
     // 组件代码实现
    } 
    
    export {ComponentA, ComponentB, ComponentC} // 情况4 export {}
    

    假定前置开发好错误边界组件,为高阶函数HOC:ErrorBoundaryWrap,目前需要做的是对以上四种方式的React模块export时,进行HOC的包裹,上文代码对应的转换如下:

    export default ErrorBoundaryWrap(ComponentA) // 情况1
    
    // 情况2
    export default {
      ComponentA: ErrorBoundaryWrap(ComponentA), 
      ComponentB: ErrorBoundaryWrap(ComponentB), 
      ComponentC: ErrorBoundaryWrap(ComponentC)
    }
    
    // 情况3
    export const ComponentA = ErrorBoundaryWrap((props) => {
     // 组件代码实现
    })
    
    // 情况4
    const ComponentAerrorBoundary = ErrorBoundaryWrap(ComponentA)
    const ComponentBerrorBoundary = ErrorBoundaryWrap(ComponentB)
    const ComponentCerrorBoundary = ErrorBoundaryWrap(ComponentC)
    export {
      ComponentAerrorBoundary as ComponentA, 
      ComponentBerrorBoundary as ComponentB, 
      ComponentCerrorBoundary as ComponentC
    }
    

    2.修改源码

    根据上述的四种情况,拟定一份源码,作为我们用例的编写:origin.jsx

    import React from 'react'
    
    const Hello1 = () => {
      return (
        <>
          <h1>Hello1 is here</h1>
        </>
      )
    }
    const Hello2 = () => {
      return (
        <>
          <h1>Hello2 is here</h1>
        </>
      )
    }
    export const Hello3 = () => {
      return (
        <>
          <h1>Hello3 is here</h1>
        </>
      )
    }
    const Hello5 = () => {
      return (
        <>
          <h1>Hello5 is here</h1>
        </>
      )
    }
    const Hello6 = () => {
      return (
        <>
          <h1>Hello6 is here</h1>
        </>
      )
    }
    export {Hello1, Hello2}
    
    export default  {
      Hello5, Hello6
    }
    
    // export default Hello1
    

    或许这里有同学会有疑问,为什么不用tsx作为源码?

    其实在使用@babel/parser把源码转换AST对象过程中,可以通过配置引入typescript插件,对tsx进行转译为js语法,所以在实际处理AST对象的过程中无需考虑typescript语法相关的节点。同理jsx插件会把源码中的jsx语法转换成React Element对象。

    const sourceAst = parser.parse(source, {
        sourceType: 'unambiguous',
        plugins: ['jsx', 'typescript']
     })
    

    AST对象分析

    不了解AST相关类型对象同学,可以到https://astexplorer.net/或相同功能的网站,粘贴源码直观查看源码转换成AST对象后的结构。

    webpack loader源码转译,对React组件包裹错误边界

    接下来细说,上述四种情况中情况2具体处理流程:

    export default  {
      Hello5, Hello6
    }
    

    转换为

    export default {
      Hello5: ErrorBoundaryWrap(Hello5), 
      Hello6: ErrorBoundaryWrap(Hello6), 
    }
    

    webpack loader源码转译,对React组件包裹错误边界

    通过工具或者对源码parse后sourceAst对象进行打印,可以看出export default是一个类型为ExportDefaultDeclaration的节点。

    确定类型后,该怎么在sourceAst中寻找节点呢?接下来引出第二个库@babel/traverse

    traverse(sourceAst, {
        Program(path){
            // type 为'Program'节点处理
        }
        ImportDeclaration(path){
            // type 为'ImportDeclaration'节点处理
        }
        ArrowFunctionExpression(path){
            // type 为'ArrowFunctionExpression'节点处理
        }
        ExportSpecifier(path){
            // type 为'ExportSpecifier'节点处理
        }
        ExportDefaultDeclaration(path){
            // type 为'ExportDefaultDeclaration'节点处理
        }
    })
    

    @babel/traverse默认返回traverse方法,traverse主要接收两个参数,AST对象,和针对各种类型节点的处理回调函数。

    traverse方法会遍历传入的AST对象的每个节点,根据当前节点的类型执行对应的回调函数,回调函数会接收到path对象入参,path对象包含当前节点信息及该节点父节点子节点兄弟节点相关对象的引用

    由于// export default Hello1被注释,当前AST中仅有一个类型为ExportDefaultDeclaration节点,通过执行以下代码,能看到ExportDefaultDeclaration只会执行一次。

    traverse(sourceAst, {
        // 其他节点类型处理
        ExportDefaultDeclaration(path){
            console.log(`#ExportDefaultDeclaration`, path)
            // type 为'ExportDefaultDeclaration'节点处理
        }
    })
    // 输出一次 #ExportDefaultDeclaratio
    

    完整代码请见 github.com/efoxTeam/re… 执行yarn && yarn test运行代码。

    接下来分析上述代码打印出来的path对象:

    // path对象的方法属性不完全展示
    NodePath {
      parentPath: <ref *1> NodePath {  // 父节点path对象引用
      },
      node: Node {  // 当前节点
        type: 'ExportDefaultDeclaration', // 节点类型
        declaration: Node { // 子节点类型 ObjectExpression 相对于代码 {Hello5, Hello6} 部分
          type: 'ObjectExpression',
          properties: [Array] // 子节点属性, 下文会再展开
        }
      },
      type: 'ExportDefaultDeclaration',  //当前节点类型
      parent: Node { // 父节点
      },
    }
    

    源AST转换为目标AST

    得到要操作的节点对象,接下来再确认一下需要做的事情:

    export default  {
      Hello5, Hello6
    }
    

    上述代码片段需要转换为下面的目标代码:

    export default {
      Hello5: ErrorBoundaryWrap(Hello5), 
      Hello6: ErrorBoundaryWrap(Hello6), 
    }
    

    接下来打印ExportDefaultDeclaration类型的子节点properties属性值path.node.declaration.properties,对应代码{Hello5, Hello6}部分

    console.log(path.node.declaration.properties)
    // 保留关键部分的输出
    ExportDefaultDeclaration [
      {
        type: 'ObjectProperty',
        computed: false,
        key: {
          type: 'Identifier',
          name: 'Hello5', // 能获取到Hello5 Key值
          loc: undefined,
          leadingComments: undefined,
          innerComments: undefined,
          trailingComments: undefined,
          extra: {}
        },
      },
      {
        type: 'ObjectProperty',
        computed: false,
        key: {
          type: 'Identifier',
          name: 'Hello6', // 能获取到Hello6 Key值
          loc: undefined,
          leadingComments: undefined,
          innerComments: undefined,
          trailingComments: undefined,
          extra: {}
        },
      }
    ]
    

    拿到Hello5,Hello6两个Key值之后,可以构造出我们需要的代码片段,接下来介绍@babel/template库,使用templateAPI可以把源码转换为AST节点。生成新的AST节点后,可以对原AST进行节点插入或替换。代码如下:

    let replaceNodeString = 'export default {'
    let adot = ''
    path.node.declaration.properties.forEach(item => {
      if (item?.value?.name) {
        replaceNodeString += ` ${adot} ${item.value.name}: ErrorBoundary(${item.value.name})`
        adot = ','
      }
    })
    replaceNodeString += '}'
    const newNode = template.statement(replaceNodeString)()
    newNode.isdeal = true
    path.replaceWithMultiple([newNode])
    

    上述代码通过遍历path.node.declaration.properties生成如下代码片段:

    export default {
      Hello5: ErrorBoundaryWrap(Hello5), 
      Hello6: ErrorBoundaryWrap(Hello6), 
    }
    

    再把代码片段通过template.statement(replaceNodeString)()转换成目标节点对象,再使用path.replaceWithMultiple方法替换掉原本属于:

    export default  {
      Hello5, Hello6
    }
    

    节点

    以上就完成了对情况4的React暴露组件代码的错误边界包裹。另外三种情况的处理和避免重复处理相同的AST节点、误处理相同类型的AST节点、以及引入ErrorBoundaryWrap高阶组件的实现请见 github.com/efoxTeam/re…

    3.生成目标代码

    最后,当源AST经过处理,达到目标AST后,通过使用@babel/generator把目标AST转化为目标代码,作为loader露出方法的返回值返回。

    const { code } = generate(sourceAst)
    return code
    

    到此,一个特定功能的模块转译loader完成。

    结语

    本方案主要通过webpack loader的实现,对jsx/tsx后缀的JavaScript模块,进行暴露引用的4种情况,进行错误边界的包裹。用工程化手段自动化对React组件做容错处理,避免组件渲染异常导致react runtime render的崩溃。

    推荐相关读物

    • babel 官网 babeljs.io/
    • 掘金小册子《babel 插件通关秘籍》 juejin.cn/book/694611…

    起源地下载网 » webpack loader源码转译,对React组件包裹错误边界

    常见问题FAQ

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

    发表评论

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

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

    联系作者

    请选择支付方式

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