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

前言

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

需求

const a = require('a');
import b from 'b';

console.log(axuebin.say('hello babel'));

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

VM105:2 Uncaught ReferenceError: axuebin is not defined

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

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

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

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

前置知识

什么是 Babel

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

Babel 是如何工作的

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

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

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

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

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

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

AST 节点

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

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

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

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

更多的节点规范可以查阅 https://github.com/estree/estree[1]

AST 遍历

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

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

一个 Visitor 一般是这样:

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

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));  }};

开发 Babel 插件的前置工作

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

分析 AST

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

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

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

转换成 AST 之后是这样的:

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

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

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

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

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

查看节点规范

节点规范:https://github.com/estree/estree[3]

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

写代码

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

初始化代码

我们的 index.js 代码为:

// index.jsconst 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;}

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

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

分析 AST / 编写对应 type 代码

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

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

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

ImportDeclaration 节点

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

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

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

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

VariableDeclaration 节点

对于 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);
}

ExpressionStatement 节点

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

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

插入引用语句

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

import axuebin from 'axuebin';

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

简化一下就是这样:

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

当然,不是直接构建这个对象放进去就好了,需要通过 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);

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

Babel Types 模块是一个用于 AST 节点的 Lodash 式工具库,它包含了构造、验证以及变换 AST 节点的方法。

结果

我们 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'));

彩蛋

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

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

/**
 * 判断是否安装了某个包
 * @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);
}

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

总结

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

完整代码见:https://github.com/axuebin/babel-inject-dep-demo[4]

  • Babel 用户手册[5]
  • Babel 插件手册[6]
  • ast 分析[7]
  • 节点规范[8]

参考资料

[1]

https://github.com/estree/estree: https://github.com/estree/estree

[2]

https://astexplorer.net: https://astexplorer.net

[3]

https://github.com/estree/estree: https://github.com/estree/estree

[4]

https://github.com/axuebin/babel-inject-dep-demo: https://github.com/axuebin/babel-inject-dep-demo

[5]

Babel 用户手册: https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/user-handbook.md

[6]

Babel 插件手册: https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md

[7]

ast 分析: https://astexplorer.net/

[8]

节点规范: https://github.com/estree/estree

最后

如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:

  1. 点个「在看」,让更多的人也能看到这篇内容(喜欢不点在看,都是耍流氓 -_-)

  2. 欢迎加我微信「qianyu443033099」拉你进技术群,长期交流学习...

  3. 关注公众号「前端下午茶」,持续为你推送精选好文,也可以加我为好友,随时聊骚。

点个在看支持我吧,转发就更好了
(0)

相关推荐

  • webpack的入门实践,看这篇就够了

    我会将所有的读者概括为初学者,即使你可能有基础,学习本节之前我希望你具有一定的JavaScript和node基础 文中的 ... ...代表省略掉部分代码,和上面的代码相同 文中的文件夹如果没有说创建 ...

  • 用于代码克隆检测的深度学习代码片段

    摘要: https://m.toutiao.com/is/JWhQ522/ 代码克隆检测是软件维护和进化的一个重要问题. 许多方法考虑结构或标识符,但现有的检测技术都没有对两种信息来源进行建模. 这些 ...

  • python ast 语法分析

    ast(Abstract Syntax Trees)是python中非常有用的一个模块,我们可以通过分析python的抽象语法树来对python的代码进行分析和修改. ast作用在python代码的语 ...

  • 试试激动人心的 WebView2

    WebView2 实在诱人,最新的 Edge(Chromium) 性能强悍,而且所有使用 WebView2  的应用可以共用一个运行时,Windows 11 已经自带 WebView2 ,就连 Off ...

  • Vue SSR 即时编译技术

    当我们在服务端渲染 Vue 应用时,无论服务器执行多少次渲染,大部分 VNode 渲染出的字符串是不变的,它们有一些来自于模板的静态 html,另一些则来自模板动态渲染的节点(虽然在客户端动态节点有可 ...

  • 解读在React中使用JSX语法

    JSX是JavaScript的一种语法扩展,被应用到React体系结构中,它的格式与模板语言类似,但实际上完全在JavaScript内部实现.组成React应用程序的元素最小单元,JSX用于声明Rea ...

  • 手写一个webpack,看看AST怎么用

    Vue中文社区 以下文章来源于进击的大前端 ,作者蒋鹏飞 Dennis 进击的大前端前端工程师,2020年开始写博客.一年时间成为掘金"优秀作者",思否2020年度"To ...

  • 「译」一个超级小的编译器

    今天我们一起动手写一个编译器,但不是我们平常所说的编译器,而是一个超级超级小的编译器,小到如果你把本文件的所有注释都删了,真正的代码也就200多行. 我们将把lisp风格的函数调用编译成C风格的函数调 ...

  • Javascript装饰器原理

    一个以@开头的描述性词语.英语的decorator动词是decorate,装饰的意思.其中词根dek(dec发音)原始印欧语系中意思是"接受".即,原来的某个事物接受一些新东西(而 ...

  • 如何用Vue+TypeScript项目配置实战?本文教你

    ❝ 最近想学习一下TypeScript语法,但是只是看官方文档又有些乏味,还是通过项目在实践中学习比较有趣,所以在这里记录一下我的学习历程,与Vue项目结合开发.(官方文档 请戳 >>) ...

  • Gulp插件,及使用方法。

    Gulp插件 gulp-htmlmin : html文件压缩 gulp-csso : 压缩css gulp-babel : javaScript语法转换 gulp-less : less语法转换 gu ...