最新公告
  • 欢迎您光临网站无忧模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 如何用 Babel 为代码自动引入依赖

    正文概述 掘金(axuebin)   2020-12-14   347

    2020年5月写的一篇文章,还没在掘金发过。原文链接

    前言

    最近在尝试玩一玩已经被大家玩腻的 Babel,今天给大家分享如何用 Babel 为代码自动引入依赖,通过一个简单的例子入门 Babel 插件开发。

    需求

    const a = require('a');
    import b from 'b';
    
    console.log(axuebin.say('hello babel'));
    
    Js

    同学们都知道,如果运行上面的代码,一定是会报错的:

    VM105:2 Uncaught ReferenceError: axuebin is not defined
    
    Shell

    我们得首先通过 import axuebin from 'axuebin' 引入 axuebin 之后才能使用。。

    为了防止这种情况发生(一般来说我们都会手动引入),或者为你省去引入这个包的麻烦(其实有些编译器也会帮我们做了),我们可以在打包阶段分析每个代码文件,把这个事情做了。

    在这里,我们就基于最简单的场景做最简单的处理,在代码文件顶部加一句引用语句:

    import axuebin from 'axuebin';
    console.log(axuebin.say('hello babel'));
    
    Js

    前置知识

    什么是 Babel

    简单地说,Babel 能够转译 ECMAScript 2015+ 的代码,使它在旧的浏览器或者环境中也能够运行。我们日常开发中,都会通过 webpack 使用 babel-loaderJavaScript 进行编译。

    Babel 是如何工作的

    首先得要先了解一个概念:抽象语法树(Abstract Syntax Tree, AST),Babel 本质上就是在操作 AST 来完成代码的转译。

    了解了 AST 是什么样的,就可以开始研究 Babel 的工作过程了。

    Babel 的功能其实很纯粹,它只是一个编译器。

    大多数编译器的工作过程可以分为三部分,如图所示:

    如何用 Babel 为代码自动引入依赖

    • Parse(解析) 将源代码转换成更加抽象的表示方法(例如抽象语法树)
    • Transform(转换) 对(抽象语法树)做一些特殊处理,让它符合编译器的期望
    • Generate(代码生成) 将第二步经过转换过的(抽象语法树)生成新的代码

    所以我们如果想要修改 Code,就可以在 Transform 阶段做一些事情,也就是操作 AST

    AST 节点

    我们可以看到 AST 中有很多相似的元素,它们都有一个 type 属性,这样的元素被称作节点。一个节点通常含有若干属性,可以用于描述 AST 的部分信息。

    比如这是一个最常见的 Identifier 节点:

    {
      type: 'Identifier',
      name: 'add'
    }
    
    Js

    所以,操作 AST 也就是操作其中的节点,可以增删改这些节点,从而转换成实际需要的 AST

    更多的节点规范可以查阅 github.com/estree/estr…

    AST 遍历

    AST 是深度优先遍历的,遍历规则不用我们自己写,我们可以通过特定的语法找到的指定的节点。

    Babel 会维护一个称作 Visitor 的对象,这个对象定义了用于 AST 中获取具体节点的方法。

    一个 Visitor 一般是这样:

    const visitor = {
      ArrowFunction(path) {
        console.log('我是箭头函数');
      },
      IfStatement(path) {
        console.log('我是一个if语句');
      },
      CallExpression(path) {}
    };
    
    Js

    visitor 上挂载以节点 type 命名的方法,当遍历 AST 的时候,如果匹配上 type,就会执行对应的方法。

    操作 AST 的例子

    通过上面简单的介绍,我们就可以开始任意造作了,肆意修改 AST 了。先来个简单的例子热热身。

    箭头函数是 ES5 不支持的语法,所以 Babel 得把它转换成普通函数,一层层遍历下去,找到了 ArrowFunctionExpression 节点,这时候就需要把它替换成 FunctionDeclaration 节点。所以,箭头函数可能是这样处理的:

    import * as t from "@babel/types";
    
    const visitor = {
      ArrowFunction(path) {
        path.replaceWith(t.FunctionDeclaration(id, params, body));
      }
    };
    
    Js

    开发 Babel 插件的前置工作

    在开始写代码之前,我们还有一些事情要做一下:

    分析 AST

    原代码目标代码都解析成 AST,观察它们的特点,找找看如何增删改 AST 节点,从而达到自己的目的。

    我们可以在 astexplorer.net 上完成这个工作,比如文章最初提到的代码:

    const a = require('a');
    import b from 'b';
    console.log(axuebin.say('hello babel'));
    
    Js

    转换成 AST 之后是这样的:

    如何用 Babel 为代码自动引入依赖

    可以看出,这个 body 数组对应的就是根节点的三条语句,分别是:

    • VariableDeclaration: const a = require('a')
    • ImportDeclaration: import b from 'b'
    • ExpressionStatement: console.log(axuebin.say('hello babel'))

    我们可以打开 VariableDeclaration 节点看看:

    如何用 Babel 为代码自动引入依赖

    它包含了一个 declarations 数组,里面有一个 VariableDeclarator 节点,这个节点有 typeidinit 等信息,其中 id 指的是表达式声明的变量名,init 指的是声明内容。

    通过这样查看/对比 AST 结构,就能分析出原代码目标代码的特点,然后可以开始动手写程序了。

    查看节点规范

    节点规范:github.com/estree/estr…

    我们要增删改节点,当然要知道节点的一些规范,比如新建一个 ImportDeclaration 需要传递哪些参数。

    写代码

    准备工作都做好了,那就开始吧。

    初始化代码

    我们的 index.js 代码为:

    // index.js
    const path = require('path');
    const fs = require('fs');
    const babel = require('@babel/core');
    
    const TARGET_PKG_NAME = 'axuebin';
    
    function transform(file) {
      const content = fs.readFileSync(file, {
        encoding: 'utf8',
      });
      const { code } = babel.transformSync(content, {
        sourceMaps: false,
        plugins: [
          babel.createConfigItem(({ types: t }) => ({
            visitor: {
            }
          }))
        ]
      });
      return code;
    }
    
    Js

    然后我们准备一个测试文件 test.js,代码为:

    // test.js
    const a = require('a');
    import b from 'b';
    require('c');
    import 'd';
    console.log(axuebin.say('hello babel'));
    
    Js

    分析 AST / 编写对应 type 代码

    我们这次需要做的事情很简单,做两件事:

    1. 寻找当前 AST 中是否含有引用 axuebin 包的节点
    2. 如果没引用,则修改 AST,插入一个 ImportDeclaration 节点

    我们来分析一下 test.jsAST,看一下这几个节点有什么特征:

    ImportDeclaration 节点

    如何用 Babel 为代码自动引入依赖

    ImportDeclaration 节点的 AST 如图所示,我们需要关心的特征是 value 是否等于 axuebin, 代码这样写:

    if (path.isImportDeclaration()) {
      return path.get('source').isStringLiteral() && path.get('source').node.value === TARGET_PKG_NAME;
    }
    
    Js

    其中,可以通过 path.get 来获取对应节点的 path,嗯,比较规范。如果想获取对应的真实节点,还需要 .node

    满足上述条件则可以认为当前代码已经引入了 axuebin 包,不用再做处理了。

    VariableDeclaration 节点

    如何用 Babel 为代码自动引入依赖

    对于 VariableDeclaration 而言,我们需要关心的特征是,它是否是一个 require 语句,并且 require 的是 axuebin,代码如下:

    /**
     * 判断是否 require 了正确的包
     * @param {*} node 节点
     */
    const isTrueRequire = node => {
      const { callee, arguments } = node;
      return callee.name === 'require' && arguments.some(item => item.value === TARGET_PKG_NAME);
    };
    
    
    if (path.isVariableDeclaration()) {
      const declaration = path.get('declarations')[0];
      return declaration.get('init').isCallExpression && isTrueRequire(declaration.get('init').node);
    }
    
    Js

    ExpressionStatement 节点

    如何用 Babel 为代码自动引入依赖

    require('c'),语句我们一般不会用到,我们也来看一下吧,它对应的是 ExpressionStatement 节点,我们需要关心的特征和 VariableDeclaration 一致,这也是我把 isTrueRequire 抽出来的原因,所以代码如下:

    if (path.isExpressionStatement()) {
      return isTrueRequire(path.get('expression').node);
    }
    
    Js

    插入引用语句

    如果上述分析都没找到代码里引用了 axuebin,我们就需要手动插入一个引用:

    import axuebin from 'axuebin';
    
    Js

    通过 AST 分析,我们发现它是一个 ImportDeclaration

    如何用 Babel 为代码自动引入依赖

    简化一下就是这样:

    {
      "type": "ImportDeclaration",
      "specifiers": [
        "type": "ImportDefaultSpecifier",
        "local": {
          "type": "Identifier",
          "name": "axuebin"
        }
      ],
      "source": {
        "type": "StringLiteral",
        "value": "axuebin"
      }
    }
    
    Js

    当然,不是直接构建这个对象放进去就好了,需要通过 babel 的语法来构建这个节点(遵循规范):

    const importDefaultSpecifier = [t.ImportDefaultSpecifier(t.Identifier(TARGET_PKG_NAME))];
    const importDeclaration = t.ImportDeclaration(importDefaultSpecifier, t.StringLiteral(TARGET_PKG_NAME));
    path.get('body')[0].insertBefore(importDeclaration);
    
    Js

    这样就插入了一个 import 语句。

    结果

    我们 node index.js 一下,test.js 就变成:

    import axuebin from "axuebin"; // 已经自动加在代码最上边
    const a = require('a');
    import b from 'b';
    require('c');
    import 'd';
    console.log(axuebin.say('hello babel'));
    
    Js

    彩蛋

    如果我们还想帮他再多做一点事,还能做什么呢?

    既然都自动引用了,那当然也要自动安装一下这个包呀!

    /**
     * 判断是否安装了某个包
     * @param {string} pkg 包名
     */
    const hasPkg = pkg => {
      const pkgPath = path.join(process.cwd(), `package.json`);
      const pkgJson = fs.existsSync(pkgPath) ? fse.readJsonSync(pkgPath) : {};
      const { dependencies = {}, devDependencies = {} } = pkgJson;
      return dependencies[pkg] || devDependencies[pkg];
    }
    
    /**
     * 通过 npm 安装包
     * @param {string} pkg 包名
     */
    const installPkg = pkg => {
      console.log(`开始安装 ${pkg}`);
      const npm = shell.which('npm');
      if (!npm) {
        console.log('请先安装 npm');
        return;
      }
      const { code } = shell.exec(`${npm.stdout} install ${pkg} -S`);
      if (code) {
        console.log(`安装 ${pkg} 失败,请手动安装`);
      }
    };
    
    // biu~
    if (!hasPkg(TARGET_PKG_NAME)) {
      installPkg(TARGET_PKG_NAME);
    }
    
    Js

    判断一个应用是否安装了某个依赖,有没有更好的办法呢?

    总结

    我也是刚开始学 Babel,希望通过这个 Babel 插件的入门例子,可以让大家了解 Babel 其实并没有那么陌生,大家都可以玩起来 ~

    更多文章可以关注公众号「前端试炼」,分享每日前端精选文章。

    完整代码见:github.com/axuebin/bab…

    • Babel 用户手册
    • Babel 插件手册
    • ast 分析
    • 节点规范

    下载网 » 如何用 Babel 为代码自动引入依赖

    常见问题FAQ

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

    发表评论

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

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

    联系作者

    请选择支付方式

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