美文网首页斯诺技术分享
AST 与前端工程化实战

AST 与前端工程化实战

作者: 小哪吒 | 来源:发表于2019-08-29 11:15 被阅读0次

    AST : 全称为 Abstract Syntax Tree,意为抽象语法树,它是源代码语法结构的一种抽象表示。

    AST 是一个非常基础但是同时非常重要的知识点,我们熟知的 TypeScript、babel、webpack、vue-cli 得都是依赖 AST 进行开发的。本文将通过 AST 与前端工程化的实战向大家展示 AST 的强大以及重要性。

    直播分享视频地址:AST 与前端工程化实战

    一、初识 AST

    1、demo-1

    第一次看见 AST 这个概念的时候还是在《你不知道的 JavaScript》一书中看到的。我们先看个例子

    const a = 1
    复制代码
    

    传统编译语言中,源代码执行会先经历三个阶段

    • 词法分析阶段:将字符组成的字符串分解成一个个代码块(词法单元),例子中代码会被解析成 const、a、=、1 四个词法单元。

    • 语法分析阶段:将词法单元流转换成一个由元素逐级嵌套组成的语法结构树,即所谓的抽象语法树。例子中被解析出来的 const、a、=、1 这四个词法单元组成的词法单元流则会被转换成如下结构树

    [图片上传中...(image-143ec0-1567048431087-7)]

    <figcaption></figcaption>

    • 代码生成阶段:将 AST 转换成一系列可执行的机器指令代码,对应例子的话就是机器通过执行指令会在内存中创建一个变量 a,并将值 1 赋值给它。

    2、demo-2

    我们再来拆解一个 recast 官方的例子,相对来说也会复杂一些

    function add (a, b) {
      return a + b
    }
    复制代码
    
    • 首先,进入到词法分析阶段,我们会拿到 function、add、(、a、,、b、)、{、return、a、+、b、} 13 个代码块
    • 然后进入语法分析阶段,具体如图所示

    [图片上传中...(image-560773-1567048431086-6)]

    <figcaption></figcaption>

    上图中的 FunctionDeclarationIdentifierBlockStatement 等这些代码块的类型的说明请点击链接自行查看:AST 对象文档

    二、recast

    由于文章中用到的 AST 相关的依赖包是 recast ,加上它本身是木有文档的,只有一个非常简短的 README.md 文件,所以这里单独开一篇对其常见的一些 API 做个介绍。开始之前,先给大家推荐一个在线查看 AST 结构的平台,非常好用

    相信对 babel 稍有了解的同学都知道,babel 有一系列包对 AST 进行了封装,专门来处理编译这块的事宜。而 recast 也是基于 @babel/core@babel/parser@babel/types等包进行封装开发的。

    引入

    引入 recast 有两种方法,一种是 import 的形式,一种则是 CommonJs 的形式,分别如下

    • import 形式
    import { parse, print } from 'recast'
    console.log(print(parse(source)).code)
    
    import * as recast from 'recast'
    console.log(recast.print(recast.parse(source)).code)
    复制代码
    
    • CommonJs 形式
    const { parse, print } = require('recast')
    console.log(print(parse(source)).code)
    
    const recast = require('recast')
    console.log(recast.print(recast.parse(source)).code)
    复制代码
    

    引入了 recast 之后,我们一起来看看 recast 都能做些什么吧

    1、recast.parse

    我们回到我们例子,我们直接对它进行 parse ,看看 parse 后的 AST 结构是如何的

    // parse.js
    const recast = require('recast')
    
    const code = `function add (a, b) {
      return a + b
    }`
    
    const ast = recast.parse(code)
    // 获取代码块 ast 的第一个 body,即我们的 add 函数
    const add = ast.program.body[0]
    console.log(add)
    复制代码
    

    执行 node parse.js 即可在我们的终端查看到 add 函数的结构了

    FunctionDeclaration {
      type: 'FunctionDeclaration',
      id: Identifier...,
      params: [Identifier...],
      body: BlockStatement...
    }
    复制代码
    

    当然你想看更多内容直接去 AST Explorer 平台 将模式调成 recast 模式即可看到 ast 的全览了,和我们上面分析的内容基本是一致的。

    [图片上传中...(image-68f72c-1567048431086-5)]

    <figcaption></figcaption>

    2、recast.print

    目前为止,我们只是对其进行了拆解,如果将 ast 组装成我们能执行的代码呢?OK,这就需要用到 recast.print 了,我们对上面拆解好的代码原封不动的组装起来

    // print.js
    const recast = require('recast')
    
    const code = `function add (a, b) {
      return a + b
    }`
    
    const ast = recast.parse(code)
    
    console.log(recast.print(ast).code)
    复制代码
    

    然后执行 node print.js ,可以看到,我们打印出了

    function add (a, b) {
      return a + b
    }
    复制代码
    

    官方给的解释就是,这就只是一个逆向处理而已,即

    recast.print(recast.parse(source)).code === source
    复制代码
    

    3、recast.prettyPrint

    除了我们上面提及的 recast.print 外,recast 还提供一个代码美化的 API 叫 recast.prettyPrint

    // prettyPrint.js
    const recast = require('recast')
    
    const code = `function add (a, b) {
      return a +                b
    }`
    
    const ast = recast.parse(code)
    
    console.log(recast.prettyPrint(ast, { tabWidth: 2 }).code)
    复制代码
    

    执行 node prettyPrint.js ,会发现 code 里面的 N 多空格都能被格式化掉,输出如下

    function add(a, b) {
      return a + b;
    }
    复制代码
    

    详细的配置请自行查看:prettyPrint

    4、recast.types.builders

    i. API

    关于 builder 的 API ,别担心,我肯定是不会讲的,因为太多了。

    [图片上传中...(image-e34c1f-1567048431086-4)]

    <figcaption></figcaption>

    想要具体了解每一个 API 能做什么的,可以直接在 Parser API - Builders 中进行查看,或者直接查看 recast builders 定义

    ii. 实战阶段

    OK,终于进入到 recast 操作相关的核心了。我们要想改造我们的代码,那么 recast.types.builders 则是我们最重要的工具了。这里我们继续通过改造 recast 官方案例来了解 recast.types.builders 构建工具。

    搞个最简单的例子,现在我们要做一件事,那就是将 function add (a, b) {...} 改成 const add = function (a, b) {...}

    我们从第一章节了解到,如果我们需要将其做成 const 声明式的话,需要先一个 VariableDeclaration 以及一个 VariableDeclarator,然后我们声明一个 function 则有需要创建一个 FunctionDeclaration,剩下的则是填充表达式的参数和内容体了。具体操作如下

    // builder1.js
    const recast = require('recast')
    const {
      variableDeclaration,
      variableDeclarator,
      functionExpression
    } = recast.types.builders
    
    const code = `function add (a, b) {
      return a + b
    }`
    
    const ast = recast.parse(code)
    const add = ast.program.body[0]
    
    ast.program.body[0] = variableDeclaration('const', [
      variableDeclarator(add.id, functionExpression(
        null, // 这里弄成匿名函数即可
        add.params,
        add.body
      ))
    ])
    
    const output = recast.print(ast).code
    
    console.log(output)
    复制代码
    

    执行 node builder1.js ,输出如下

    const add = function(a, b) {
      return a + b
    };
    复制代码
    

    看到这,是不是觉得很有趣。真正好玩的才刚开始呢,接下来,基于此例子,我们做个小的延伸。将其直接改成 const add = (a, b) => {...} 的格式。

    这里出现了一个新的概念,那就是箭头函数,当然,recast.type.builders 提供了 arrowFunctionExpression 来允许我们创建一个箭头函数。所以我们第一步先来创建一个箭头函数

    const arrow = arrowFunctionExpression([], blockStatement([])
    复制代码
    

    打印下 console.log(recast.print(arrow)),输出如下

    () => {}
    复制代码
    

    OK,我们已经获取到一个空的箭头函数了。接下来我们需要基于上面改造的基础进一步进行改造,其实只要将 functionExpression 替换成 arrowFunctionExpression 即可。

    ast.program.body[0] = variableDeclaration('const', [
      variableDeclarator(add.id, b.arrowFunctionExpression(
        add.params,
        add.body
      ))
    ])
    复制代码
    

    打印结果如下

    const add = (a, b) => {
      return a + b
    };
    复制代码
    

    OK,到这里,我们已经知道 recast.types.builders 能为我们提供一系列 API,让我们可以疯狂输出。

    5、recast.run

    读取文件命令行。首先,我新建一个 read.js ,内容如下

    // read.js
    recast.run((ast, printSource) => {
      printSource(ast)
    })
    复制代码
    

    然后我再新建一个 demo.js,内容如下

    // demo.js
    function add (a, b) {
      return a + b
    }
    复制代码
    

    然后执行 node read demo.js,输出如下

    function add (a, b) {
      return a + b
    }
    复制代码
    

    我们能看出来,我们直接在 read.js 中读出了 demo.js 里面的代码内容。那么具体是如何实现的呢?

    其实,原理非常简单,无非就是直接通过 fs.readFile 进行文件读取,然后将获取到的 code 进行 parse 操作,至于我们看到的 printSource 则提供一个默认的打印函数 process.stdout.write(output),具体代码如下

    import fs from "fs";
    
    export function run(transformer: Transformer, options?: RunOptions) {
      return runFile(process.argv[2], transformer, options);
    }
    
    function runFile(path: any, transformer: Transformer, options?: RunOptions) {
      fs.readFile(path, "utf-8", function(err, code) {
        if (err) {
          console.error(err);
          return;
        }
    
        runString(code, transformer, options);
      });
    }
    
    function defaultWriteback(output: string) {
      process.stdout.write(output);
    }
    
    function runString(code: string, transformer: Transformer, options?: RunOptions) {
      const writeback = options && options.writeback || defaultWriteback;
      transformer(parse(code, options), function(node: any) {
        writeback(print(node, options).code);
      });
    }
    复制代码
    

    6、recast.visit

    这是一个 AST 节点遍历的 API,如果你想要遍历 AST 中的一些类型,那么你就得靠 recast.visit 了,这里可以遍历的类型与 recast.types.builders 中的能构造出来的类型一致,builders 做的事是类型构建,recast.visit 做事的事则是遍历 AST 中的类型。不过使用的时候需要注意以下几点

    • 每个 visit,必须加上 return false,或者 this.traverse(path),否则报错。
    if (this.needToCallTraverse !== false) {
      throw new Error(
        "Must either call this.traverse or return false in " + methodName
      );
    }
    复制代码
    
    • 在需要遍历的类型前面加上 visit 即可遍历,如需要遍历 AST 中的箭头函数,那么直接这么写即可
    recast.run((ast, printSource) => {
      recast.visit(ast, {
        visitArrowFunctionExpression (path) {
          printSource(path.node)
          return false
        }
      })
    })
    复制代码
    

    7、recast.types.namedTypes

    一个用来判断 AST 对象是否为指定类型的 API。

    namedTypes 下有两个 API,一个是 namedTypes.Node.assert:当类型不配置的时候,直接报错退出。另外一个则是 namedTypes.Node.check:判断类型是否一致,并输出 true 或 false。

    其中 Node 为任意 AST 对象,比如我相对箭头函数做一个函数类型判定,代码如下

    // namedTypes1.js
    const recast = require('recast')
    const t = recast.types.namedTypes
    
    const arrowNoop = () => {}
    
    const ast = recast.parse(arrowNoop)
    
    recast.visit(ast, {
      visitArrowFunctionExpression ({ node }) {
        console.log(t.ArrowFunctionExpression.check(node))
        return false
      }
    })
    复制代码
    

    执行 node namedTypes1.js,能看出打印台输出结果为 true。

    同理,assert 用法也差不多。

    const recast = require('recast')
    const t = recast.types.namedTypes
    
    const arrowNoop = () => {}
    
    const ast = recast.parse(arrowNoop)
    
    recast.visit(ast, {
      visitArrowFunctionExpression ({ node }) {
        t.ArrowFunctionExpression.assert(node)
        return false
      }
    })
    复制代码
    

    你想判断更多的 AST 对象类型的,直接做替换 Node 为其它 AST 对象类型即可。

    三、前端工程化

    现在,咱来聊聊前端工程化。

    前段工程化可以分成四个块来说,分别为

    • 模块化:将一个文件拆分成多个相互依赖的文件,最后进行统一的打包和加载,这样能够很好的保证高效的多人协作。其中包含

      1. JS 模块化:CommonJS、AMD、CMD 以及 ES6 Module。
      2. CSS 模块化:Sass、Less、Stylus、BEM、CSS Modules 等。其中预处理器和 BEM 都会有的一个问题就是样式覆盖。而 CSS Modules 则是通过 JS 来管理依赖,最大化的结合了 JS 模块化和 CSS 生态,比如 Vue 中的 style scoped。
      3. 资源模块化:任何资源都能以模块的形式进行加载,目前大部分项目中的文件、CSS、图片等都能直接通过 JS 做统一的依赖关系处理。
    • 组件化:不同于模块化,模块化是对文件、对代码和资源拆分,而组件化则是对 UI 层面的拆分。

      1. 通常,我们会需要对页面进行拆分,将其拆分成一个一个的零件,然后分别去实现这一个个零件,最后再进行组装。
      2. 在我们的实际业务开发中,对于组件的拆分我们需要做不同程度的考量,其中主要包括细粒度和通用性这两块的考虑。
      3. 对于业务组件,你更多需要考量的是针对你负责业务线的一个适用度,即你设计的业务组件是否成为你当前业务的 “通用” 组件,比如我之前分析过的权限校验组件,它就是一个典型的业务组件。感兴趣的小伙伴可以点击 传送门 自行阅读。
    • 规范化:正所谓无规矩不成方圆,一些好的规范则能很好的帮助我们对项目进行良好的开发管理。规范化指的是我们在工程开发初期以及开发期间制定的系列规范,其中又包含了

      1. 项目目录结构
      2. 编码规范:对于编码这块的约束,一般我们都会采用一些强制措施,比如 ESLint、StyleLint 等。
      3. 联调规范:这块可参考我以前知乎的回答,前后端分离,后台返回的数据前端没法写,怎么办?
      4. 文件命名规范
      5. 样式管理规范:目前流行的样式管理有 BEM、Sass、Less、Stylus、CSS Modules 等方式。
      6. git flow 工作流:其中包含分支命名规范、代码合并规范等。
      7. 定期 code review
      8. … 等等

      以上这些,我之前也写过一篇文章做过一些点的详细说明,TypeScript + 大型项目实战

    • 自动化:从最早先的 grunt、gulp 等,再到目前的 webpack、parcel。这些自动化工具在自动化合并、构建、打包都能为我们节省很多工作。而这些前端自动化其中的一部分,前端自动化还包含了持续集成、自动化测试等方方面面。

    而,处于其中任何一个块都属于前端工程化。

    四、实战:AST & webpack loader

    而本文提及的实战,则是通过 AST 改造书写一个属于我们自己的 webpack loader,为我们项目中的 promise 自动注入 catch 操作,避免让我们手动书写那些通用的 catch 操作。

    1、AST 改造

    讲了这么多,终于进入到我们的实战环节了。那么我们实战要做一个啥玩意呢?

    场景:日常的中台项目中,经常会有一些表单提交的需求,那么提交的时候就需要做一些限制,防止有人手抖多点了几次导致请求重复发出去。此类场景有很多解决方案,但是个人认为最佳的交互就是点击之后为提交按钮加上 loading 状态,然后将其 disabled 掉,请求成功之后再解除掉 loading 和 disabled 的状态。具体提交的操作如下

    this.axiosFetch(this.formData).then(res => {
      this.loading = false
      this.handleClose()
    }).catch(() => {
      this.loading = false
    })
    复制代码
    

    这样看着好像还算 OK,但是如果类似这样的操作一多,或多或少会让你项目整体的代码看起来有些重复冗余,那么如何解决这种情况呢?

    很简单,咱直接使用 AST 编写一个 webpack loader,让其自动完成一些代码的注入,若我们项目中存在下面的代码的时候,会自动加上 catch 部分的处理,并将 then 语句第一段处理主动作为 catch 的处理逻辑

    this.axiosFetch(this.formData).then(res => {
      this.loading = false
      this.handleClose()
    })
    复制代码
    

    我们先看看,没有 catch 的这段代码它的 AST 结构是怎样的,如图

    [图片上传中...(image-afa359-1567048431085-3)]

    <figcaption></figcaption>

    其 MemberExpression 为

    this.axiosFetch(this.formData).then
    复制代码
    

    arguments 为

    res => {
      this.loading = false
      this.handleClose()
    }
    复制代码
    

    OK,我们再来看看有 catch 处理的代码它的 AST 结构又是如何的,如图

    [图片上传中...(image-8b6938-1567048431085-2)]

    <figcaption></figcaption>

    其 MemberExpression 为

    this.axiosFetch(this.formData).then(res => {
      this.loading = false
      this.handleClose()
    }).catch
    复制代码
    

    其中有两个 ArrowFunctionExpression,分别为

    // ArrowFunctionExpression 1
    res => {
      this.loading = false
      this.handleClose()
    }
    // ArrowFunctionExpression 2
    () => {
      this.loading = false
    }
    复制代码
    

    所以,我们需要做的事情大致分为以下几步

    1. 对 ArrowFunctionExpression 类型进行遍历,获得其 BlockStatement 中的第一个 ExpressionStatement,并保存为 firstExp
    2. 使用 builders 新建一个空的箭头函数,并将保存好的 firstExp 赋值到该空箭头函数的 BlockStatement 中
    3. 对 CallExpression 类型进行遍历,将 AST 的 MemberExpression 修改成为有 catch 片段的格式
    4. 将改造完成的 AST 返回

    现在,按照我们的思路,我们一步一步来做 AST 改造

    首先,我们需要获取到已有箭头函数中的第一个 ExpressionStatement,获取的时候我们需要保证当前 ArrowFunctionExpression 类型的 parent 节点是一个 CallExpression 类型,并且保证其 property 为 promise 的then 函数,具体操作如下

    // promise-catch.js
    const recast = require('recast')
    const {
      identifier: id,
      memberExpression,
      callExpression,
      blockStatement,
      arrowFunctionExpression
    } = recast.types.builders
    const t = recast.types.namedTypes
    
    const code = `this.axiosFetch(this.formData).then(res => {
      this.loading = false
      this.handleClose()
    })`
    const ast = recast.parse(code)
    let firstExp
    
    recast.visit(ast, {
      visitArrowFunctionExpression ({ node, parentPath }) {
        const parentNode = parentPath.node
        if (
          t.CallExpression.check(parentNode) &&
          t.Identifier.check(parentNode.callee.property) &&
          parentNode.callee.property.name === 'then'
        ) {
          firstExp = node.body.body[0]
        }
        return false
      }
    })
    复制代码
    

    紧接着,我们需要创建一个空的箭头函数,并将 firstExp 赋值给它

    const arrowFunc = arrowFunctionExpression([], blockStatement([firstExp]))
    复制代码
    

    随后,我们则需要对 CallExpression 类型的 AST 对象进行遍历,并做最后的 MemberExpression 改造工作

    recast.visit(ast, {
      visitCallExpression (path) {
        const { node } = path
    
        const arrowFunc = arrowFunctionExpression([], blockStatement([firstExp]))
        const originFunc = callExpression(node.callee, node.arguments)
        const catchFunc = callExpression(id('catch'), [arrowFunc])
        const newFunc = memberExpression(originFunc, catchFunc)
    
        return false
      }
    })
    复制代码
    

    最后我们在 CallExpression 遍历的时候将其替换

    path.replace(newFunc)
    复制代码
    

    初版的全部代码如下

    // promise-catch.js
    const recast = require('recast')
    const {
      identifier: id,
      memberExpression,
      callExpression,
      blockStatement,
      arrowFunctionExpression
    } = recast.types.builders
    const t = recast.types.namedTypes
    
    const code = `this.axiosFetch(this.formData).then(res => {
      this.loading = false
      this.handleClose()
    })`
    const ast = recast.parse(code)
    let firstExp
    
    recast.visit(ast, {
      visitArrowFunctionExpression ({ node, parentPath }) {
        const parentNode = parentPath.node
        if (
          t.CallExpression.check(parentNode) &&
          t.Identifier.check(parentNode.callee.property) &&
          parentNode.callee.property.name === 'then'
        ) {
          firstExp = node.body.body[0]
        }
        return false
      }
    })
    
    recast.visit(ast, {
      visitCallExpression (path) {
        const { node } = path
    
        const arrowFunc = arrowFunctionExpression([], blockStatement([firstExp]))
        const originFunc = callExpression(node.callee, node.arguments)
        const catchFunc = callExpression(id('catch'), [arrowFunc])
        const newFunc = memberExpression(originFunc, catchFunc)
    
        path.replace(newFunc)
    
        return false
      }
    })
    
    const output = recast.print(ast).code
    console.log(output)
    复制代码
    

    执行 node promise-catch.js ,打印台输出结果

    this.axiosFetch(this.formData).then(res => {
      this.loading = false
      this.handleClose()
    }).catch(() => {
      this.loading = false
    })
    复制代码
    

    所以能看出来,我们已经是完成了我们想要完成的样子了

    [图片上传中...(image-be2925-1567048431085-1)]

    <figcaption></figcaption>

    1. 但是我们还得对一些情况做处理,第一件就是需要在 CallExpression 遍历的时候保证其 arguments 为箭头函数。

    2. 紧接着,我们需要判定我们获取到的 firstExp 是否存在,因为我们的 then 处理中可以是一个空的箭头函数。

    3. 然后防止 promise 拥有一些自定义的 catch 操作,则需要保证其 property 为 then。

    4. 最后为了防止多个 CallExpression 都需要做自动注入的情况,然后其操作又不同,则需要在其内部进行 ArrowFunctionExpression 遍历操作

    经过这些常见情况的兼容后,具体代码如下

    recast.visit(ast, {
      visitCallExpression (path) {
        const { node } = path
        const arguments = node.arguments
    
        let firstExp
    
        arguments.forEach(item => {
          if (t.ArrowFunctionExpression.check(item)) {
            firstExp = item.body.body[0]
    
            if (
              t.ExpressionStatement.check(firstExp) &&
              t.Identifier.check(node.callee.property) &&
              node.callee.property.name === 'then'
            ) {
              const arrowFunc = arrowFunctionExpression([], blockStatement([firstExp]))
              const originFunc = callExpression(node.callee, node.arguments)
              const catchFunc = callExpression(id('catch'), [arrowFunc])
              const newFunc = memberExpression(originFunc, catchFunc)
    
              path.replace(newFunc)
            }
          }
        })
    
        return false
      }
    })
    复制代码
    

    然后由于之后需要做成一个 webpack-loader,用在我们的实际项目中。所以我们需要对 parse 的解析器做个替换,其默认的解析器为 recast/parsers/esprima,而一般我们项目中都会用到 babel-loader ,所以我们这也需要将其解析器改为 recast/parsers/babel

    const ast = recast.parse(code, {
      parser: require('recast/parsers/babel')
    })
    复制代码
    

    2、webpack loader

    到这里,我们对于代码的 AST 改造已经是完成了,但是如何将其运用到我们的实际项目中呢?

    OK,这个时候我们就需要自己写一个 webpack loader 了。

    其实,关于如何开发一个 webpack loader,webpack 官方文档 已经讲的很清楚了,下面我为小伙伴们做个小总结。

    i. 本地进行 loader 开发

    首先,你需要本地新建你开发 loader 的文件,比如,我们这将其丢到 src/index.js 下,webpack.config.js 配置则如下

    const path = require('path')
    
    module.exports = {
      // ...
      module: {
        rules: [
          {
            test: /\.js$/,
            use: [
              // ... 其他你需要的 loader
              { loader: path.resolve(__dirname, 'src/index.js') }
            ]
          }
        ]
      }
    }
    复制代码
    

    src/index.js 内容如下

    const recast = require('recast')
    const {
      identifier: id,
      memberExpression,
      callExpression,
      blockStatement,
      arrowFunctionExpression
    } = recast.types.builders
    const t = recast.types.namedTypes
    
    module.exports = function (source) {
      const ast = recast.parse(source, {
        parser: require('recast/parsers/babel')
      })
    
      recast.visit(ast, {
        visitCallExpression (path) {
          const { node } = path
          const arguments = node.arguments
    
          let firstExp
    
          arguments.forEach(item => {
            if (t.ArrowFunctionExpression.check(item)) {
              firstExp = item.body.body[0]
    
              if (
                t.ExpressionStatement.check(firstExp) &&
                t.Identifier.check(node.callee.property) &&
                node.callee.property.name === 'then'
              ) {
                const arrowFunc = arrowFunctionExpression([], blockStatement([firstExp]))
                const originFunc = callExpression(node.callee, node.arguments)
                const catchFunc = callExpression(id('catch'), [arrowFunc])
                const newFunc = memberExpression(originFunc, catchFunc)
    
                path.replace(newFunc)
              }
            }
          })
    
          return false
        }
      })
    
      return recast.print(ast).code
    }
    复制代码
    

    然后,搞定收工。

    ii. npm 发包

    这里我在以前的文章中提及过,这里不谈了。如果还没搞过 npm 发包的小伙伴,可以点击下面链接自行查看

    揭秘组件库一二事(发布 npm 包片段)

    OK,到这一步,我的 promise-catch-loader 也是已经开发完毕。接下来,只要在项目中使用即可

    npm i promise-catch-loader -D
    复制代码
    

    由于我的项目是基于 vue-cli3.x 构建的,所以我需要在我的 vue.config.js 中这样配置

    // js 版本
    module.exports = {
      // ...
      chainWebpack: config => {
        config.module
          .rule('js')
          .test(/\.js$/)
          .use('babel-loader').loader('babel-loader').end()
          .use('promise-catch-loader').loader('promise-catch-loader').end()
      }
    }
    // ts 版本
    module.exports = {
      // ...
      chainWebpack: config => {
        config.module
          .rule('ts')
          .test(/\.ts$/)
          .use('cache-loader').loader('cache-loader').end()
          .use('babel-loader').loader('babel-loader').end()
          .use('ts-loader').loader('ts-loader').end()
          .use('promise-catch-loader').loader('promise-catch-loader').end()
      }
    }
    复制代码
    

    然后我项目里面拥有以下 promise 操作

    <script lang="ts">
    import { Component, Vue } from 'vue-property-decorator'
    import { Action } from 'vuex-class'
    
    @Component
    export default class HelloWorld extends Vue {
      loading: boolean = false
      city: string = '上海'
    
      @Action('getTodayWeather') getTodayWeather: Function
    
      getCityWeather (city: string) {
        this.loading = true
        this.getTodayWeather({ city: city }).then((res: Ajax.AjaxResponse) => {
          this.loading = false
          const { low, high, type } = res.data.forecast[0]
          this.$message.success(`${city}今日:${type} ${low} - ${high}`)
        })
      }
    }
    </script>
    复制代码
    

    然后在浏览器中查看 source 能看到如下结果

    [图片上传中...(image-1fb21f-1567048431084-0)]

    <figcaption></figcaption>

    关于代码,我已经托管到 GitHub 上了,promise-catch-loader

    总结

    到这步,我们的实战环节也已经是结束了。当然,文章只是个初导篇,更多的类型还得小伙伴自己去探究。

    AST 它的用处还非常的多,比如我们熟知的 Vue,它的 SFC(.vue) 文件的解析也是基于 AST 去进行自动解析的,即 vue-loader,它保证我们能正常的使用 Vue 进行业务开发。再比如我们常用的 webpack 构建工具,也是基于 AST 为我们提供了合并、打包、构建优化等非常实用的功能的。

    总之,掌握好 AST,你真的可以做很多事情。

    最后,希望文章的内容能够帮助小伙伴了解到:什么是 AST?如何借助 AST 让我们的工作更加效率?AST 又能为前端工程化做些什么?

    作者:qiangdada
    链接:https://juejin.im/post/5d50d1d9f265da03aa25607b
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    相关文章

      网友评论

        本文标题:AST 与前端工程化实战

        本文链接:https://www.haomeiwen.com/subject/kjabectx.html