Babel 的理解

1,831 阅读29分钟

目录

  • 前言
    • babel 是什么
    • babel 能做什么
  • 工作流程
    • 解析 Parse
    • 转换 Transform
    • 生成 Generator
  • 使用方式
    • babel-standalone
      • 介绍
      • 版本
      • 示例
      • 问答
      • 补充
    • cli 命令
      • 介绍
      • 安装
      • 使用
      • 问答
      • 补充
    • 作为构建工具的插件
  • 配置文件
    • 一、使用 API
      • 介绍
      • 示例
      • 问答
    • 二、使用 CLI
      • 介绍
      • 示例
      • 问答
    • 三、.babelrc
      • 介绍
      • 示例
      • 问答
      • 补充
    • 四、babel.config.json
      • 介绍
      • 示例
      • 补充
  • 模块介绍
    • babel-node
  • 版本升级
  • 问答
  • 总结

前言

babel 是什么

Babel 是一个 JavaScript 编译器

这是babel 官网对 babel 一句短而精的定义, 该如何理解这句话的含义呢?首先定义它为一个编译器,其次只对 JavaScript 语言负责.

关于编译器概念可参考维基百科bk.tw.lvfukeji.com/wiki/%E7%BC…

babel 能做什么

这里我们只需要知道 babel 作为 JavaScript 编译器的工作流程:解析->转换->生成.

通俗的可以理解为 babel 负责把 JavaScript 高级语法、新增 API 转换为低规范以保障能够在低版本的环境下正常执行,接下来我们看 babel 是如何工作的.

工作流程

babel-1

解析 Parse

万事开头难,第一步的解析工作该由谁完成?

babylon

Babylon 是一款 JavaScript 解析器.

babel 本身是不负责解析工作的,而是调用了 babylon.parse 方法对源码进行词法解析生成 AST 树.

babel-2

转换 Transform

babel-traverse

babel-traverse 负责遍历 AST 树进行增删改的操作.

从第一步获取到 AST 树之后,调用 babel-traverse 库提供的 traverse 方法对树进行更新.

babel-types

一个基于 AST 树的工具库(可对节点增删改查校验).babel-traverse 对 AST 树进行操作的时候便使用了此库.

babel-3

生成 Generator

最后一步将更新之后的 AST 树进行生成代码.

babel-generator

对外提供 generator 方法接收 ast 参数返回值为改变之后的源码.

babel-4

babel-5

以上则是对 babel 编译器整个流程大概的描述信息.所以 babel 是由一系列动作配合完成的.

使用方式

babel-standalone

介绍

由于 babel 是基于 node 环境下运行,对于非 node 环境(如浏览器),babel-standalone这个开源项目提供了 babel.min.js 可通过<script>方式引入使用.

题外话:babel-standalone已加入了babel大家族(上岸成为有编制一员),以后下载的 7.x 版本 babel 包内可以看到它的身影.

版本
名称版本体积备注在线地址
babel.js6.26.01.78MB未压缩unpkg.com/babel-stand…
babel.min.js6.26.0773KB已压缩unpkg.com/babel-stand…
babel.js7.12.93.1MB未压缩unpkg.com/@babel/stan…
babel.min.js7.12.91.6MB已压缩unpkg.com/@babel/stan…
示例
  • 示例一 es6 转码
    • 使用<script>引入在线地址或者下载之后本地引用.
    • 将编写的 es6 代码放入<script type="text/babel">内,需要注意一点type 类型为 text/babel.
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>babel-standalone es6 转码</title>
    <script src="https://unpkg.com/@babel/standalone@7.12.9/babel.min.js"></script>
  </head>
  <body>
    <div id="output"></div>
    <script type="text/babel">
      const getMsg = () => {
        const name = 'Babel';
        document.getElementById(
          'output'
        ).innerHTML = `Hello ${name} version:${Babel.version}`;
      };
      getMsg();
    </script>
  </body>
</html>

examples-babel-standalone-1-1

以上这些均引入babel.min.js(可能引入的名称或版本不一样)通过调用Babel对象提供的各种 API(如transformdisableScriptTagstransformScriptTags...)实现在线实时转码.

examples-babel-standalone-2-2

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>babel-standalone 模拟在线实时转码用户输入的脚本</title>
    <script src="https://unpkg.com/@babel/standalone@7.12.9/babel.min.js"></script>
  </head>
  <body>
    输入:
    <textarea id="input" style="width: 100%" rows="15">
    class UserInfo{
      constructor(name='张三') {
        this.name = name;
      }
      getUserName(){
      return `${this.name}`;
      }
    }
    const user=new UserInfo('张三');
    console.log(user.getUserName());
    </textarea>
    实时转码:
    <pre id="output"></pre>

    <script>
      var inputEl = document.getElementById('input');
      var outputEl = document.getElementById('output');

      function transform() {
        try {
          outputEl.innerHTML = Babel.transform(inputEl.value, {
            presets: [
              'es2015',
              [
                'stage-2',
                {
                  decoratorsBeforeExport: false,
                },
              ],
            ],
          }).code;
        } catch (e) {
          outputEl.innerHTML = 'ERROR: ' + e.message;
        }
      }

      inputEl.addEventListener('keyup', transform, false);
      transform();
    </script>
  </body>
</html>

examples-babel-standalone-2-3

  • 示例三 import、export 的使用

以上示例都是通过内嵌的方式在页面直接写 es6 代码,但实际开发中有可能需要以外链的方式引入脚本,所以我们看会遇到哪些问题.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>babel-standalone import、export 的使用</title>
    <script src="https://unpkg.com/@babel/standalone@7.12.9/babel.min.js"></script>
  </head>
  <body>
    <div id="output"></div>
    <script type="text/babel" src="./index.js"></script>
  </body>
</html>
// index.js
const getMsg = () => {
  const name = 'Babel';
  document.getElementById(
    'output'
  ).innerHTML = `Hello ${name} version:${Babel.version}`;
};
getMsg();

examples-babel-standalone-3-1

把示例一从内嵌方式修改为外链引入脚本证明没问题.但我们不止编写一个 index.js 脚本,对于使用 import、export 这些 es6 语法是否也支持?

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>babel-standalone import、export 的使用</title>
    <script src="https://unpkg.com/@babel/standalone@7.12.9/babel.min.js"></script>
  </head>
  <body>
    <div id="output"></div>
    <script type="text/babel">
      export default {
        name: '小朋友',
        age: 18,
      };
    </script>
  </body>
</html>

首先我们以内嵌方式运行之后抛错Uncaught ReferenceError: exports is not defined,这里抛出exports未定义而不是 es6 的export未定义,由此说明内部是要把 es6 代码转换 CommonJS 模块规范(印证了前面介绍中提到过的由于 babel 是基于 node 环境下运行),而我们是需要在浏览器执行的,所以babel-standalone项目提供的babel.min.js包含了 babel 用到的所有插件(各种特性、规范),可以直接配置为 umd 模式即可.

6.x 版本配置data-plugins="transform-es2015-modules-umd"

7.x 版本配置data-plugins="transform-modules-umd"

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>babel-standalone import、export 的使用</title>
    <script src="https://unpkg.com/@babel/standalone@7.12.9/babel.min.js"></script>
  </head>
  <body>
    <div id="output"></div>
    <script type="text/babel" data-plugins="transform-modules-umd">
      export default {
        name: '小朋友',
        age: 18,
      };
    </script>
  </body>
</html>

既然可以定义export导出那该如何import导入呢?这里babel-standalone又给我们提供了data-module定义导出的模块名称,然后导入即可.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>babel-standalone import、export 的使用</title>
    <script src="https://unpkg.com/@babel/standalone@7.12.9/babel.min.js"></script>
  </head>
  <body>
    <div id="output"></div>
    <script
      type="text/babel"
      data-plugins="transform-modules-umd"
      data-module="userInfo"
    >
      export default {
        name: '小朋友',
        age: 18,
      };
    </script>
    <script type="text/babel" data-plugins="transform-modules-umd">
      import userInfo from 'userInfo';
      document.getElementById('output').innerHTML = `Hello ${userInfo.name}`;
    </script>
  </body>
</html>

examples-babel-standalone-3-2

这一些似乎都没发现问题,难道真的没有什么问题?那直接修改为外链的方式呢.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>babel-standalone import、export 的使用</title>
    <script src="https://unpkg.com/@babel/standalone@7.12.9/babel.min.js"></script>
  </head>
  <body>
    <div id="output"></div>
    <script
      type="text/babel"
      data-plugins="transform-modules-umd"
      src="./userInfo.js"
    ></script>

    <script type="text/babel" data-plugins="transform-modules-umd">
      import userInfo from 'userInfo';
      document.getElementById('output').innerHTML = `Hello ${userInfo.name}`;
    </script>
  </body>
</html>
// userInfo.js
export default {
  name: '小朋友',
  age: 18,
};

如果仔细看上面这段代码的话,有没有发现script标签内缺少了属性data-module定义模块名称,依然可以正常执行呢?

划重点,babel.min.js内部获取script标签之后对属性做了判断,如果有src属性则使用属性值作为模块名称(如src="./userInfo.js"最终以userInfo作为模块名称),如果没有src属性则获取data-module属性值作为模块名称,所以如果使用外链方式的话是不需要data-module属性的(即使配置了也无效).

上面示例演示了 export 可以使用外链的方式,那import又如何使用外链方式呢?

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>babel-standalone import、export 的使用</title>
    <script src="https://unpkg.com/@babel/standalone@7.12.9/babel.min.js"></script>
  </head>
  <body>
    <div id="output"></div>
    <script
      type="text/babel"
      data-plugins="transform-modules-umd"
      src="./userInfo.js"
    ></script>

    <script
      type="text/babel"
      data-plugins="transform-modules-umd"
      src="./index.js"
    ></script>
  </body>
</html>
// userInfo.js
export default {
  name: '小朋友',
  age: 18,
};
// index.js
import userInfo from 'userInfo';
document.getElementById('output').innerHTML = `Hello ${userInfo.name}`;

import 的使用方式同export的方式是一样的,这样就完成了以外链的方式引入脚本.

问答

感觉示例部分关于import export修改外链的方式一直提起有什么问题,结尾也没发现什么坑?

说这个问题之前,大家是否考虑过如果没有外链引入<script src="./userInfo.js">,只有<script src="./index.js">会怎样?在index.js内部已经通过import userInfo from 'userInfo';导入,是否可以不需要外链的方式引入,如果import导入多个 js 又会怎样?

稳住.所以拎到问答部分单独聊...

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>babel-standalone import、export 的使用</title>
    <script src="https://unpkg.com/@babel/standalone@7.12.9/babel.min.js"></script>
  </head>
  <body>
    <div id="output"></div>
    <script
      type="text/babel"
      data-plugins="transform-modules-umd"
      src="./index.js"
    ></script>
  </body>
</html>
// index.js
import userInfo from 'userInfo';
document.getElementById('output').innerHTML = `Hello ${userInfo.name}`;

如上面的示例的,直接外链引入index.js,抛出Uncaught TypeError: Cannot read property 'name' of undefined错误,原因是userInfoundefined所以userInfo.name也无法取到值.

那我们就从转码开始入手.

// index.js 转码之后
(function (global, factory) {
  if (typeof define === 'function' && define.amd) {
    define(['userInfo'], factory);
  } else if (typeof exports !== 'undefined') {
    factory(require('userInfo'));
  } else {
    var mod = {
      exports: {},
    };
    factory(global.userInfo);
    global.index = mod.exports;
  }
})(
  typeof globalThis !== 'undefined'
    ? globalThis
    : typeof self !== 'undefined'
    ? self
    : this,
  function (_userInfo) {
    'use strict';

    _userInfo = _interopRequireDefault(_userInfo);

    function _interopRequireDefault(obj) {
      return obj && obj.__esModule ? obj : { default: obj };
    }

    document.getElementById('output').innerHTML = 'Hello '.concat(
      _userInfo['default'].name
    );
  }
);

我们对比一下index.js转码前后:

转码前转码后
import userInfo from 'userInfo'; _userInfo = _interopRequireDefault(_userInfo);
function _interopRequireDefault(obj) {return obj && obj.__esModule ? obj : { default: obj };}

我们看到转码之后的代码通过_interopRequireDefault(_userInfo)函数对_userInfo重新赋值的操作.(_interopRequireDefault函数的作用判断对象是否需要添加default属性).

划重点:这里插一段为什么会有__esModuledefault属性,首先在本节开头提到过由于 babel 是基于 node 环境下运行,所以 babel 是要把 es6 的模块转换为 CommonJS 的形式,那么就需要了解两者的区别.

导出导入
es6export
export default
import {}
import
CommonJSmodule.exportsrequire

通过对比发现 es6 的导出、导入可以是多种形式,而 CommonJS 则是单一的对象导出、导入.所以 babel 要把 es6 转换 CommonJS 的形式就需要一些辅助改动.

关于模块对象添加__esModule属性,是为了标记此模块是否被转码,如果有此属性则直接调用模块(exports)的default属性导出对象(babel 会把 es6 的export default默认导出对象转码为 exports.default的形式,同时这种写法又符合 CommonJS 的规范module.exports = exports.default),主要是做到 es6 转码后与 CommonJS 规范的一致性.

关于default属性,上面介绍了是有__esModule属性的情况下,如果没有__esModule属性说明没有对该模块进行转换(有可能是一个第三方模块)对于这种情况直接调用模块(exports)的default属性会为undefined,所以这种情况就直接返回一个对象并且该对象添加一个default属性,把属性值指向自己(如上面这句转码之后的代码return obj && obj.__esModule ? obj : { default: obj })

快醒醒,这两个属性不是这里的重点,还记得问题是什么吗?userInfo为什么是undefined,看完转码之后的代码,我们只需要知道一点import只是导入而已,至于导入的对象是否存在,是不属于转码的职责所在(转码不会检测导入的对象否存在...),还是继续查找userInfo在哪里定义的吧...

继续看转码之后的代码发现在开头有if...else if...else对各种环境(AMD、CommonJS、UMD)做判断,由于我们是浏览器内执行(UMD 模式),所以进入else里在global.userInfo全局对象上有个userInfo,至此得出一个结论,在外链方式引入的脚本内直接使用import导入对象引用,而外部无任何声明export,此对象是undefined(其实在 AMD 的define(['userInfo'], factory)、CommonJS 的require('userInfo')也是同理).

简单一句话:无论通过哪种形式引用,必须要声明,所以外链的方式还是需要script声明.

简单一句话:无论通过哪种形式引用,必须要声明,所以外链的方式还是需要script声明.

简单一句话:无论通过哪种形式引用,必须要声明,所以外链的方式还是需要script声明.

此处使用的声明可能用词不太准确,但相信看到这里应该可以意会到.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>babel-standalone import、export 的使用</title>
    <script src="https://unpkg.com/@babel/standalone@7.12.9/babel.min.js"></script>
  </head>
  <body>
    <div id="output"></div>
    <script
      type="text/babel"
      data-plugins="transform-modules-umd"
      src="./userInfo.js"
    ></script>
    <script
      type="text/babel"
      data-plugins="transform-modules-umd"
      src="./index.js"
    ></script>
  </body>
</html>
// userInfo.js
export default {
  name: '小朋友',
  age: 18,
};
// index.js
import userInfo from 'userInfo';
document.getElementById('output').innerHTML = `Hello ${userInfo.name}`;
// userInfo.js 转码后
(function (global, factory) {
  if (typeof define === 'function' && define.amd) {
    define(['exports'], factory);
  } else if (typeof exports !== 'undefined') {
    factory(exports);
  } else {
    var mod = {
      exports: {},
    };
    factory(mod.exports);
    global.userInfo = mod.exports;
  }
})(
  typeof globalThis !== 'undefined'
    ? globalThis
    : typeof self !== 'undefined'
    ? self
    : this,
  function (_exports) {
    'use strict';

    Object.defineProperty(_exports, '__esModule', {
      value: true,
    });
    _exports['default'] = void 0;
    var _default = {
      name: '小朋友',
      age: 18,
    };
    _exports['default'] = _default;
  }
);
// index.js 转码之后
(function (global, factory) {
  if (typeof define === 'function' && define.amd) {
    define(['userInfo'], factory);
  } else if (typeof exports !== 'undefined') {
    factory(require('userInfo'));
  } else {
    var mod = {
      exports: {},
    };
    factory(global.userInfo);
    global.index = mod.exports;
  }
})(
  typeof globalThis !== 'undefined'
    ? globalThis
    : typeof self !== 'undefined'
    ? self
    : this,
  function (_userInfo) {
    'use strict';

    _userInfo = _interopRequireDefault(_userInfo);

    function _interopRequireDefault(obj) {
      return obj && obj.__esModule ? obj : { default: obj };
    }

    document.getElementById('output').innerHTML = 'Hello '.concat(
      _userInfo['default'].name
    );
  }
);

继上一个问题,如果外链引入了userInfo.jsindex.js里没有import userInfo from 'userInfo';又如何呢?

对于这个问题,请品一下,上面对__esModuledefault属性的介绍,就是那段这两个属性不是这里的重点,现在是这个问题的答案.

babel-standalone 如何使用多个import export.

其实和上面的示例是一样的,只需要外链多个script即可.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>babel-standalone 如何使用多个import、export</title>
    <script src="https://unpkg.com/@babel/standalone@7.12.9/babel.min.js"></script>
  </head>
  <body>
    <div id="output"></div>
    <script
      type="text/babel"
      data-plugins="transform-modules-umd"
      src="./other.js"
    ></script>
    <script
      type="text/babel"
      data-plugins="transform-modules-umd"
      src="./userInfo.js"
    ></script>
    <script
      type="text/babel"
      data-plugins="transform-modules-umd"
      src="./index.js"
    ></script>
  </body>
</html>
// other.js
export function random() {
  return Math.floor(Math.random() * 10);
}

export const randomStr = '幸运数字:';
// userInfo.js
export default {
  name: '小朋友',
  age: 18,
};
// index.js
import { randomStr, random } from 'other';
import userInfo from 'userInfo';
function init() {
  document.getElementById('output').innerHTML = `Hello ${
    userInfo.name
  } ${randomStr} ${random()}`;
}

init();
补充

对于开发者来说这种直接编写 es6 代码实时转码比较方便,同时也耗时影响性能,所以开发环境可以使用,对于生产环境还是推荐加载转码之后的脚本.

You are using the in-browser Babel transformer. Be sure to precompile your scripts for production——官网建议.

cli 命令

介绍

CLI:命令行界面(英语:Command-Line Interface,缩写:CLI)是在图形用户界面得到普及之前使用最为广泛的用户界面,它通常不支持鼠标,用户通过键盘输入指令,计算机接收到指令后,予以执行.

关于 CLI 概念可参考维基百科bk.tw.lvfukeji.com/wiki/CLI

babel 内置的 CLI, 可直接在命令行转码文件.

安装

安装 babel cli 之前先检查是否安装nodenpm(前面有提到过 babel 是基于 node 环境下运行,所以自行检索安装步骤).

babel cli分为以下两种安装方式:

  1. 全局安装:npm install --global babel-cli项目运行需要依赖全局安装,且如果多个项目依赖的版本不一致的话,无法解决...

  2. 局部安装:npm install --save-dev babel-cli直接在项目内安装,完全解决全局安装的问题.

建议局部安装,且下面的使用示例均为局部安装.

使用

在介绍使用之前,请先准备好项目环境.

  1. 创建项目.
  2. 项目中需包含package.json文件(自己新建一个)

关于 package.json的相关配置可参考docs.npmjs.com/cli/v6/conf…

接下来正式进入babel cli的使用.

项目目录结构如下:

|--babel-cli 项目名称
   |-- package.json
// package.json
{
  "name": "cli"
}
  1. 使用命令进入项目 cd babel-cli
  2. 执行局部安装命令 npm install --save-dev babel-cli

安装完成之后项目目录结构如下:

|--babel-cli 项目名称
   |-- package.json
   |-- package-lock.json 记录安装模块的版本、来源信息
   |-- node_modules node 依赖模块集合
      |-- babel-cli cli模块
      |-- babel-* 以babel开头的各个模块后面会介绍

// package.json
{
  "name": "cli",
  "devDependencies": {
    "babel-cli": "^6.26.0"
  }
}

至此所有的配置、安装已完成,接下来我们在项目中创建一个脚本文件(script.js),并且执行转码.

|--babel-cli 项目名称
   |-- package.json
   |-- package-lock.json 记录安装模块的版本、来源信息
   |-- node_modules node 依赖模块集合
      |-- babel-cli cli模块
      |-- babel-* 以babel开头的各个模块后面会介绍
   |-- script.js 脚本文件
// script.js
const hello = 'Hello';

[1, 2, 3, 4].forEach((item) => console.log(item));

执行命令进行转码...

  • 转码输出到 stdout(标准输出).

npx babel script.js

examples-babel-cli-1

  • 转码输出到指定文件.

npx babel script.js --out-file script-compiled.js

  • 转码某个目录到指定目录

npx babel src --out-dir lib

以上就是关于babel cli的简单使用,更多配置可参考官网www.babeljs.cn/docs/babel-…

问答

全局安装、局部安装、npx、 npm 有什么区别?

从安装方式来说babel cli有两种(全局安装、局部安装)上面提到过,安装完之后如何执行命令转码,同样也分为两种:

  1. 全局安装之后直接执行babel命令如(babel script.js),它是找全局node_modules/babel-cli/bin/babel.js执行.

  2. 局部安装之后需要使用npxnpm执行,它则是查找当前项目下的node_modules/.bin/babel命令执行(最终执行的是项目下的node_modules/babel-cli/bin/babel.js),

全局、局部唯一区别是查找的路径不同.

全局、局部唯一区别是查找的路径不同.

全局、局部唯一区别是查找的路径不同.

接下来对比一下npxnpm的区别.

想要使用npm运行命令转码则需要配置package.jsonscripts对象,关于 scripts的相关配置可参考docs.npmjs.com/cli/v6/comm…

// package.json
{
  "name": "cli",
  "devDependencies": {
    "babel-cli": "^6.26.0"
  },
  "scripts": {
    "babel": "babel script.js",
    "babel-compiled": "babel script.js --out-file script-compiled.js"
  }
}

如上面的使用示例可以通过配置package.jsonscripts对象,命令行进入项目目录,通过执行命令npm run babelnpm run babel-compiled实现转码.

如果不配置scripts对象就无法使用npm吗?答案是可以的,使用起来稍微麻烦一点,由于npm是查找的项目目录下node_modules/.bin/babel命令执行,所以我们可以在命令行手动调用node_modules/.bin/babel script.js实现转码.

有什么办法既可以不需要命令行每次都输入node_modules/...这一堆路径,也不需要配置scripts对象.答案就是npx,它直接查找项目下的node_modules/.bin对应的命令执行,无需我们手动输入或配置scripts对象.(不信你看上面的使用部分,就是直接使用的npx).

即安装了全局,又安装了局部,npxnpm该如何查找node_modules

上面的一个问题第 2 点提到查找当前项目下的node_modules这是不严谨的,应该是由近到远(就近原则),如果项目没有node_modules模块集合,才查找全局.

为什么上面的使用示例输出的代码没有转码?

// 转码前
const hello = 'Hello';

[1, 2, 3, 4].forEach((item) => console.log(item));
// 转码后
const hello = 'Hello';

[1, 2, 3, 4].forEach((item) => console.log(item));

这里只对babel cli做一个初步了解,关于(语法、新特性)转码、配置文件后面会介绍,另外如果开发工具库使用babel cli还是比较适合的.

补充
  • 关于npm如何查找目录下node_modules包的问题?
|--babel-cli 项目名称
   |-- package.json
   |-- package-lock.json 记录安装模块的版本、来源信息
   |-- node_modules node 依赖模块集合
      |-- babel-cli cli模块
      |-- babel-* 以babel开头的各个模块后面会介绍
   |-- script.js 脚本文件
// package.json
{
  "name": "cli",
  "devDependencies": {
    "babel-cli": "^6.26.0"
  },
  "scripts": {
    "babel": "babel script.js",
    "babel-compiled": "babel script.js --out-file script-compiled.js"
  }
}

上面列出了示例的目录结构以及部分相关的文件,因为只是示例所以目录结构相对简单一些(所有的文件都在项目的一级目录下),我们以该示例为例看能否找到npm是如何检索node_modulespackage.json所在位置的答案.

  1. 首先执行命令cd babel-cli进入项目目录.
  2. 其次执行npm run babel.

我们拆分一下npm run babel的过程,看能了解多少.

  • 首先是启动系统安装的npm(不知道安装位置请自行检索,我使用的是which npm 命令查找位置).

    • 启动/usr/local/bin/npm、内部调用npm-cli.jsnpm.js./config/core.js'这些流程都不是重点.
  • 其次以当前所在的工作目录开始进行检索,什么是当前工作目录(Node 调用process.cwd获取的目录),直白一点就是在命令终端哪个目录下执行了npm run ....,那它就是当前工作目录.

    • 由于我之前在终端通过命令cd进入了项目,所以当前目录为babel-cli
  • 最后由于示例目录node_modulespackage.json都在一级目录下所以找到之后解析package.jsonscripts对象,调用node_modules.bin下的babel,剩下的事情交给babel处理...

这难道就结束了?一个示例几句话就没了?那如果当前目录没有node_modulespackage.json该如何处理呢?,如果有多级目录又是怎样?这些情况都没叨叨...

我不信.

那就接着看,首先对项目结构修改如下:

|--babel-cli 项目名称
   |-- package.json
   |-- package-lock.json 记录安装模块的版本、来源信息
   |-- node_modules node 依赖模块集合
      |-- babel-cli cli模块
      |-- babel-* 以babel开头的各个模块后面会介绍
   |-- utils 工具类脚本文件集合
      |-- index.js
   |-- script.js 脚本文件

// utils/index.js
export const get = () => {
  return 'get';
};

目录新增了utils/index.js.

// package.json
{
  "name": "config-babel-babelrc",
  "devDependencies": {
    "babel-cli": "^6.26.0",
    "babel-plugin-transform-es2015-arrow-functions": "^6.22.0"
  },
  "scripts": {
    "babel": "babel script.js",
    "babel-compiled": "babel script.js  --out-file script-compiled.js",
    "utils": "babel ./utils/index.js"
  }
}

package.jsonscripts对象新增了utils.这里需要注意的是 由于package.jsonindex.js不在同一个目录,所以utils的值需要配置为相对路径才可以找到该文件"babel ./utils/index.js".

script.js文件与package.json同级所以不需要配置相对路径("babel script.js").

首先我们进入cd babel-cli/utils目录,执行npm run utils看是什么结果.

// 输出结果
export const get = () => {
  return 'get';
};

请忽略代码未转码,后面配置文件章节会介绍如何配置转码.

为什么当前目录utils没有node_modulespackage.json,非但没有抛错而且还可以正常输出,上面提到过以当前所在的目录开始进行检索,并未提及当前目录检索不到的情况如何处理,现在遇到了这种情况,我们直接看源码片段是怎么做的.

// npm/lib/config/core.js
Conf.prototype.loadPrefix = require('./load-prefix.js');
// npm/lib/config/load-prefix.js

// try to guess at a good node_modules location.
// If we are *explicitly* given a prefix on the cli, then
// always use that.  otherwise, infer local prefix from cwd.
if (Object.prototype.hasOwnProperty.call(cli, 'prefix')) {
  p = path.resolve(cli.prefix);

  process.nextTick(cb);
} else {
  findPrefix(process.cwd()).then((found) => {
    p = found;
    cb();
  }, cb);
}
// npm/node_modules/find-npm-prefix/find-prefix.js

function findPrefix(dir) {
  return new Promise((resolve, reject) => {
    dir = path.resolve(dir);

    // this is a weird special case where an infinite recurse of
    // node_modules folders resolves to the level that contains the
    // very first node_modules folder
    let walkedUp = false;
    while (path.basename(dir) === 'node_modules') {
      dir = path.dirname(dir);
      walkedUp = true;
    }
    if (walkedUp) {
      resolve(dir);
    } else {
      resolve(findPrefix_(dir));
    }
  });
}
function findPrefix_(dir, original) {
  if (!original) original = dir;

  const parent = path.dirname(dir);
  // this is a platform independent way of checking if we're in the root
  // directory
  if (parent === dir) return Promise.resolve(original);

  return new Promise((resolve, reject) => {
    fs.readdir(dir, (err, files) => {
      if (err) {
        // an error right away is a bad sign.
        // unless the prefix was simply a non
        // existent directory.
        if (err && dir === original && err.code !== 'ENOENT') {
          reject(err);
        } else {
          resolve(original);
        }
      } else if (
        files.indexOf('node_modules') !== -1 ||
        files.indexOf('package.json') !== -1
      ) {
        resolve(dir);
      } else {
        resolve(findPrefix_(parent, original));
      }
    });
  });
}

上面的三个源码片段就是如何检索位置的.重点是find-prefix.js文件内的findPrefix方法接收参数dir这个值是process.cwd()(当前目录),所以看到这里知道为什么是从当前目录开始检索了吧,注意这里是 检索到node_modulespackage.json文件 则停止,否则依此向上级目录检索.

所以无论在哪个目录下执行npm ...都可以,它始终是要依次向上检索目录找到packpage.json文件(如果顶级目录不存在则抛错...),另外如果当前目录只有node_modulespackage.json其中之一也是会抛错的,因为源码是检索到其中之一则停止检索,言外之意默认这两个是同级(都在同一个目录下),关于npm更多...还需要自己探索.

  • npx又是如何查找目录下node_modules包的问题?

其实和npm是相似的都是从当前目录开始检索,并依次向上,所以我们直接看源码.

// npm/bin/npx-cli.js
const npx = require('libnpx');
const path = require('path');

const NPM_PATH = path.join(__dirname, 'npm-cli.js');

npx(npx.parseArgs(process.argv, NPM_PATH));
// npm/node_modules/libnpx/index.js
function localBinPath(cwd) {
  return require('./get-prefix.js')(cwd).then((prefix) => {
    return prefix && path.join(prefix, 'node_modules', '.bin');
  });
}
// npm/node_modules/libnpx/get-prefix.js
function getPrefix(root) {
  const original = (root = path.resolve(root));
  while (path.basename(root) === 'node_modules') {
    root = path.dirname(root);
  }
  if (original !== root) {
    return Promise.resolve(root);
  } else {
    return Promise.resolve(getPrefixFromTree(root));
  }
}

function getPrefixFromTree(current) {
  if (isRootPath(current, process.platform)) {
    return false;
  } else {
    return Promise.all([
      fileExists(path.join(current, 'package.json')),
      fileExists(path.join(current, 'node_modules')),
    ]).then((args) => {
      const hasPkg = args[0];
      const hasModules = args[1];

      if (hasPkg || hasModules) {
        return current;
      } else {
        return getPrefixFromTree(path.dirname(current));
      }
    });
  }
}

关于npx需要注意一点如果当前目录只有package.json,它会尝试下载对应模块到当前目录.

作为构建工具的插件

介绍

现在一些构建工具集成了 babel 或提供了配置方式,这些插件只是配置方式不同,如同babel cli一样 无论全局、局部(npx、npm)哪种方式最终的转码流程都是一样,所以这里不对插件的配置使用做介绍.

webpack=>babel-loaderwww.webpackjs.com/loaders/bab…

gulp=>gulp-babelwww.npmjs.com/package/gul…

rollup=>rollup-plugin-babelgithub.com/rollup/roll…

配置文件

关于 babel 的配置有以下四种方式,官网也有相关介绍(www.babeljs.cn/docs/config…).

一、使用 API

介绍

这种方式是通过babel-core模块提供的各种 API 对代码进行转码,由于使用场景较少所以先介绍此方式的使用.

示例

项目目录结构如下:

|--config-babel-api 项目名称
   |-- package.json
// package.json
{
  "name": "config-babel-api"
}
  1. 使用命令进入项目 cd config-babel-api
  2. 执行局部安装命令 npm install babel-core --save-dev

安装完成之后项目目录结构如下:

|--config-babel-api 项目名称
   |-- package.json
   |-- package-lock.json 记录安装模块的版本、来源信息
   |-- node_modules node 依赖模块集合
      |-- babel-core babel的核心模块(包)
      |-- babel-* 以babel开头的各个模块后面会介绍
// package.json
{
  "name": "config-babel-api",
  "devDependencies": {
    "babel-core": "^6.26.3"
  }
}

接下来我们在项目中创建一个脚本文件(script.js).

// script.js
[1, 2, 3, 4].forEach((item) => console.log(item));

代码相对比较简单,一个forEach循环输出每一项的值,这里的回调使用了箭头函数的写法,为了防止某些环境不支持箭头函数所以对其进行转码.

由于babel-core模块提供了转码的 API 所以我们直接调用即可,关于更多的 API 可参考官网(www.babeljs.cn/docs/babel-…)

// transform
babel.transform(code: string, options?: Object, callback: Function)

transform接收三个参数(字符串(需要转码的代码)、配置项、回调),所以还需要对(script.js)做如下修改.

// script.js
const babel = require('babel-core');
const codeStr = '[1, 2, 3, 4].forEach((item) => console.log(item));';
const result = babel.transform(codeStr, {
  plugins: ['babel-plugin-transform-es2015-arrow-functions'],
});

console.log('输出结果:', result.code);
  • require('babel-core')引入模块.
  • 由于transform接收的是字符串,所以需要把转码的代码变为字符串(如果代码较多也是同样操作(整体作为一个字符串)),
  • 调用transform.
    • 第一个参数接收字符串变量codeStr.
    • 配置plugins,对箭头函数进行转码需要下载npm install --save-dev babel-plugin-transform-es2015-arrow-functions模块(包).
// package.json
{
  "name": "config-babel-api",
  "devDependencies": {
    "babel-core": "^6.26.3",
    "babel-plugin-transform-es2015-arrow-functions": "^6.22.0"
  }
}

最后在终端或命令窗口进入当前脚本所在目录cd config-babel-api,使用 node 命令执行node script.js查看输出结果(箭头函数已转换为普通函数).

// 输出结果:
// [1, 2, 3, 4].forEach(function (item) {
//   return console.log(item);
// });

以上就是使用 babel API 的方式实现转码.

问答

能否使用 babel cli 脚手架的方式转码?

如果看过前面讲过的章节使用方式.babel-standalone中的示例二 模拟在线实时转码用户输入的脚本 就是使用的 API 这种方式,如果还看过使用方式.cli命令章节应该记得关于问答部分的最后一个问题为什么上面的使用示例输出的代码没有转码?当时没有给出明确答案,所以在接下来的二、使用 CLI章节介绍如何使用.

一、使用 CLI

介绍

之前的章节或多或少都提到过babel cli,也曾留下一个问题——babel cli如何配置转码,下面直接进入正题.

示例

项目目录结构如下:

|--config-babel-cli 项目名称
   |-- package.json
// package.json
{
  "name": "config-babel-cli"
}
  1. 使用命令进入项目 cd config-babel-cli
  2. 执行局部安装命令 npm install --save-dev babel-cli babel-plugin-transform-es2015-arrow-functions

安装完成之后项目目录结构如下:

|--config-babel-cli 项目名称
   |-- package.json
   |-- package-lock.json 记录安装模块的版本、来源信息
   |-- node_modules node 依赖模块集合
      |-- babel-cli cli模块
      |-- babel-plugin-transform-es2015-arrow-functions 箭头转换模块
      |-- babel-* 以babel开头的各个模块后面会介绍
// package.json
{
  "name": "config-babel-cli",
  "devDependencies": {
    "babel-cli": "^6.26.0",
    "babel-plugin-transform-es2015-arrow-functions": "^6.22.0"
  }
}

接下来我们在项目中创建一个脚本文件(script.js),并且执行转码.

// script.js
[1, 2, 3, 4].forEach((item) => console.log(item));

还记得如何使用babel cli吗?

首先终端或命令窗口进入当前脚本所在目录cd config-babel-cli,其次命令行执行npx babel script.js即可.

注意: 这里需要对命令行进行配置 npx babel --plugins babel-plugin-transform-es2015-arrow-functions script.js

注意: 这里需要对命令行进行配置 npx babel --plugins babel-plugin-transform-es2015-arrow-functions script.js

注意: 这里需要对命令行进行配置 npx babel --plugins babel-plugin-transform-es2015-arrow-functions script.js

只有这样才解决之前章节提到转码之后代码没有转的问题.

npx对应的还有一个npm是否还记得?(如果不想在命令行输入那么多...那么就使用npm吧),所以需要对package.json做修改(在使用方式.cli命令 章节提到过如何修改).

{
  "name": "config-babel-cli",
  "devDependencies": {
    "babel-cli": "^6.26.0",
    "babel-plugin-transform-es2015-arrow-functions": "^6.22.0"
  },
  "scripts": {
    "babel": "babel --plugins babel-plugin-transform-es2015-arrow-functions script.js",
    "babel-compiled": "babel script.js --plugins babel-plugin-transform-es2015-arrow-functions --out-file script-compiled.js"
  }
}

配置scripts对象之后,npm run babelnpm run babel-compiled即可.

// script.js 转码后
[1, 2, 3, 4].forEach(function (item) {
  return console.log(item);
});
问答

是否还有其它更方便的配置方式?

无论是 使用 API还是使用 CLI 命令行的方式都有一个问题就是配置稍嫌繁琐,示例只是演示了转换箭头函数,es6 那么多新语法、特性难道需要在命令行配置 N 多...所以下一章节介绍的配置方式可能会解决这个问题.

三、.babelrc

介绍

首先了解一下后缀 rc 的由来:在 UNIX 世界,rc 经常被用作程序之启动脚本的文件名。它是“run commands”(运行命令)的缩写。这一缩写据说源于 1965 年 MIT CTSS 的 runcom 命令。 ——参考维基百科(bk.tw.lvfukeji.com/baike-Rc%E6…)

Linux 系统etc目录有.bashrc .zshrc (可能其它版本名称为bashrc zshrc).

Vue——.vuerc.

npm——.npmrc.

如以上操作系统、框架都有自己的.[name]rc文件,所以后缀 rc 文件不是某个框架特有的,可以理解为一种约定大于配置的规范,所以 babel 也不例外可以配置rc结尾的文件,在调用 babel 启动的时候便会读取.babelrc文件.

// node_modules/babel-core/lib/transformation/file/options/build-config-chain.js
var BABELRC_FILENAME = '.babelrc';
function findConfigs(loc) {
  var configLoc = _path2.default.join(loc, BABELRC_FILENAME);
}

源码大概 80 多行的findConfigs方法内通过拼接路径读取此配置文件(path/.babelrc)

知道后缀rc之后,那.babelrc前缀.呢?它作为一个命名标准,用来隐藏文件不可见.所以使用命令ls显示文件清单(列表)时,不会显示.前缀的文件.(显示全部需要使用ls -a命令).

注意:Window 创建 .babelrc的文件时,提示必须键入文件名,重命名时需要.babelrc.前缀后缀都是.即可,这样就创建了.babelrc的文件.

以上就是关于.babelrc的简单介绍(友情提示,赶紧检查一下自己的硬盘有没有隐藏一些遗忘的文件...文件夹...).

示例
  • 示例一

项目目录结构如下:

|--config-babel-babelrc 项目名称
   |-- .babelrc 配置文件
   |-- package.json
   |-- package-lock.json 记录安装模块的版本、来源信息
   |-- node_modules node 依赖模块集合
      |-- babel-cli cli模块
      |-- babel-plugin-transform-es2015-arrow-functions 箭头转换模块
      |-- babel-* 以babel开头的各个模块后面会介绍
// .babelrc
{
  "plugins": ["babel-plugin-transform-es2015-arrow-functions"]
}
// package.json
{
  "name": "config-babel-babelrc",
  "devDependencies": {
    "babel-cli": "^6.26.0",
    "babel-plugin-transform-es2015-arrow-functions": "^6.22.0"
  },
  "scripts": {
    "babel": "babel script.js",
    "babel-compiled": "babel script.js  --out-file script-compiled.js"
  }
}
// script.js
[1, 2, 3, 4].forEach((item) => console.log(item));

最后执行npm run babel得到的结果如下:

// script.js转码后
[1, 2, 3, 4].forEach(function (item) {
  return console.log(item);
});
  • 示例二

与创建配置文件.babelrc相对应的还有一种配置是通过package.json内新增babel对象实现转码(其实就是把.babelrc内的配置项移到packpage.json内).

// package.json
{
  "name": "config-babel-babelrc",
  "devDependencies": {
    "babel-cli": "^6.26.0",
    "babel-plugin-transform-es2015-arrow-functions": "^6.22.0"
  },
  "scripts": {
    "babel": "babel script.js",
    "babel-compiled": "babel script.js  --out-file script-compiled.js"
  },
  "babel": {
    "plugins": ["babel-plugin-transform-es2015-arrow-functions"]
  }
}

注意:如果即有.babelrc文件同时在package.json内配置了babel对象,则优先使用的是.babelrc文件.

注意:如果即有.babelrc文件同时在package.json内配置了babel对象,则优先使用的是.babelrc文件.

注意:如果即有.babelrc文件同时在package.json内配置了babel对象,则优先使用的是.babelrc文件.

问答

babel是如何查找.babelrc文件的?

首先babel使用的是相对文件配置查找,即从正在执行的脚本所在目录依次向上查找.babelrc文件,是不是感觉和npm查找package.json相似.下面是查找的源码,感兴趣的可以看一下while循环.

如果多个子目录都存在.babelrc文件,采用的也是就近原则.

// node_modules/babel-core/lib/transformation/file/options/build-config-chain.js

var BABELIGNORE_FILENAME = '.babelignore';
var BABELRC_FILENAME = '.babelrc';
var PACKAGE_FILENAME = 'package.json';

ConfigChainBuilder.prototype.findConfigs = function findConfigs(loc) {
  if (!loc) return;

  if (!(0, _pathIsAbsolute2.default)(loc)) {
    loc = _path2.default.join(process.cwd(), loc);
  }

  var foundConfig = false;
  var foundIgnore = false;

  while (loc !== (loc = _path2.default.dirname(loc))) {
    if (!foundConfig) {
      var configLoc = _path2.default.join(loc, BABELRC_FILENAME);

      if (exists(configLoc)) {
        this.addConfig(configLoc);
        foundConfig = true;
      }

      var pkgLoc = _path2.default.join(loc, PACKAGE_FILENAME);
      if (!foundConfig && exists(pkgLoc)) {
        foundConfig = this.addConfig(pkgLoc, 'babel', JSON);
      }
    }

    if (!foundIgnore) {
      var ignoreLoc = _path2.default.join(loc, BABELIGNORE_FILENAME);
      if (exists(ignoreLoc)) {
        this.addIgnoreConfig(ignoreLoc);
        foundIgnore = true;
      }
    }

    if (foundIgnore && foundConfig) return;
  }
};
补充

注意区分一下npmnpxbabel对输出当前工作目录process.cwd的一个区别.

process.cwd()方法,按照官方解释返回 Node.js 进程的当前工作目录.难道返回的工作目录还有区别?带着疑问我们通过以下示例看能否找到答案.

我们还是通过示例看一下,项目结构如下:

|--config-babel-babelrc 项目名称
   |-- .babelrc 配置文件
   |-- package.json
   |-- package-lock.json 记录安装模块的版本、来源信息
   |-- node_modules node 依赖模块集合
      |-- babel-cli cli模块
      |-- babel-plugin-transform-es2015-arrow-functions 箭头转换模块
      |-- babel-* 以babel开头的各个模块后面会介绍
   |-- utils 工具类脚本文件集合
      |-- index.js
   |-- script.js 脚本文件
// package.json
{
  "name": "config-babel-babelrc",
  "devDependencies": {
    "babel-cli": "^6.26.0",
    "babel-plugin-transform-es2015-arrow-functions": "^6.22.0"
  },
  "scripts": {
    "babel": "babel script.js",
    "babel-compiled": "babel script.js  --out-file script-compiled.js",
    "utils": "babel ./utils/index.js"
  }
}

以下测试需要提前准备三点:

  1. 在 npm 包(自行检索安装位置),找到/usr/local/lib/node_modules/npm/bin/npm-cli.js编辑脚本插入输出语句.

  2. 在 npm 包(自行检索安装位置),找到/usr/local/lib/node_modules/npm/bin/npx-cli.js编辑脚本插入输出语句.

  3. 在项目的node_modules包内找到node_modules/babel-cli/bin/babel.js编辑脚本插入输出语句.

// npm-cli.js
console.log('npm输出当前工作目录:', process.cwd());
// npx-cli.js
console.log('npx输出当前工作目录:', process.cwd());
// babel.js
console.log('babel输出当前工作目录:', process.cwd());
  • 测试一: 根目录下 npm 与 babel 输出当前工作目录
  1. 命令终端进入目录cd config-babel-babelrc
  2. 执行命令npm run babel
// 执行npm run babel 输出当前工作目录
npm输出当前工作目录: `/babel/examples/config-babel-babelrc`;
babel输出当前工作目录: `/babel/examples/config-babel-babelrc`;
  1. 执行命令npm run utils
// 执行npm run utils 输出当前工作目录
npm输出当前工作目录: `/babel/examples/config-babel-babelrc`;
babel输出当前工作目录: `/babel/examples/config-babel-babelrc`;

结论:在根目录下无论执行命令npm run babelnpm run utils最后输出的当前工作目录都是一致.

  • 测试二: utils 目录下 npm 与 babel 输出当前工作目录
  1. 命令终端进入目录cd config-babel-babelrc/utils注意这里的路径是进入 utils 目录
  2. 执行命令npm run babel
// 执行npm run babel 输出当前工作目录
npm输出当前工作目录: `/babel/examples/config-babel-babelrc/utils`;
babel输出当前工作目录: `/babel/examples/config-babel-babelrc`;
  1. 执行命令npm run utils
// 执行npm run utils 输出当前工作目录
npm输出当前工作目录: `/babel/examples/config-babel-babelrc/utils`;
babel输出当前工作目录: `/babel/examples/config-babel-babelrc`;

结论:在 utils 目录下无论执行命令npm run babelnpm run utils最后输出的当前工作目录都不一致.

  • 测试三: 根目录下 npx 与 babel 输出当前工作目录
  1. 命令终端进入目录cd config-babel-babelrc
  2. 执行命令npx babel script.js
// 执行npx babel script.js 输出当前工作目录
npx输出当前工作目录: `/babel/examples/config-babel-babelrc`;
babel输出当前工作目录: `/babel/examples/config-babel-babelrc`;
  1. 执行命令npx babel utils/index.js
// 执行npx babel utils/index.js 输出当前工作目录
npx输出当前工作目录: `/babel/examples/config-babel-babelrc`;
babel输出当前工作目录: `/babel/examples/config-babel-babelrc`;

结论:在根目录下无论执行命令npx babel script.jsnpx babel utils/index.js最后输出的当前工作目录都是一致.

  • 测试四: utils 目录下 npx 与 babel 输出当前工作目录
  1. 命令终端进入目录cd config-babel-babelrc/utils注意这里的路径是进入 utils 目录
  2. 执行命令npx babel ../script.js
// 执行npx babel ../script.js 输出当前工作目录
npx输出当前工作目录: `/babel/examples/config-babel-babelrc/utils`;
babel输出当前工作目录: `/babel/examples/config-babel-babelrc/utils`;
  1. 执行命令npx babel index.js
// 执行npx babel index.js 输出当前工作目录
npx输出当前工作目录: `/babel/examples/config-babel-babelrc/utils`;
babel输出当前工作目录: `/babel/examples/config-babel-babelrc/utils`;

结论:在 utils 目录下无论执行命令npx babel ../script.jsnpx babel index.js最后输出的当前工作目录都是一致.

通过以上四个测试发现只有测试二的结论与其它测试不一样,也就是说当进入utils目录执行npm run ...命令时,npm输出的当前工作目录与babel输出当前工作目录不不一致,这个是什么原因引起的?下面我们开始分析.

首先我们看一下 npm 大致做了哪些工作.

执行npm run ...命令,从npm/bin/npm-cli.js->npm/lib/npm.js

// npm/lib/npm.js
Object.keys(abbrevs)
  .concat(plumbing)
  .forEach(function addCommand(c) {
    Object.defineProperty(npm.commands, c, {
      get: function () {
        if (!loaded) {
          throw new Error(
            'Call npm.load(config, cb) before using this command.\n' +
              'See the README.md or bin/npm-cli.js for example usage.'
          );
        }
        var a = npm.deref(c);
        if (c === 'la' || c === 'll') {
          npm.config.set('long', true);
        }

        npm.command = c;
        if (commandCache[a]) return commandCache[a];

        var cmd = require(path.join(__dirname, a + '.js'));

        commandCache[a] = function () {
          var args = Array.prototype.slice.call(arguments, 0);
          if (typeof args[args.length - 1] !== 'function') {
            args.push(defaultCb);
          }
          if (args.length === 1) args.unshift([]);

          // Options are prefixed by a hyphen-minus (-, \u2d).
          // Other dash-type chars look similar but are invalid.
          Array(args[0]).forEach(function (arg) {
            if (/^[\u2010-\u2015\u2212\uFE58\uFE63\uFF0D]/.test(arg)) {
              log.error(
                'arg',
                'Argument starts with non-ascii dash, this is probably invalid:',
                arg
              );
            }
          });

          if (!registryRefer) {
            registryRefer = [a]
              .concat(args[0])
              .map(function (arg) {
                // exclude anything that might be a URL, path, or private module
                // Those things will always have a slash in them somewhere
                if (arg && arg.match && arg.match(/\/|\\/)) {
                  return '[REDACTED]';
                } else {
                  return arg;
                }
              })
              .filter(function (arg) {
                return arg && arg.match;
              })
              .join(' ');
            npm.referer = registryRefer;
          }

          cmd.apply(npm, args);
        };

        Object.keys(cmd).forEach(function (k) {
          commandCache[a][k] = cmd[k];
        });

        return commandCache[a];
      },
      enumerable: fullList.indexOf(c) !== -1,
      configurable: true,
    });

    // make css-case commands callable via camelCase as well
    if (c.match(/-([a-z])/)) {
      addCommand(
        c.replace(/-([a-z])/g, function (a, b) {
          return b.toUpperCase();
        })
      );
    }
  });

此处是对cmdList(npm/lib/config/cmd-list.js)命令列表添加对应的执行脚本(如run命令对应的是npm/lib/run-script.js),然后执行cmd.apply(npm, args)时,则调用runScript

// npm/lib/run-script.js
function runScript(args, cb) {
  if (!args.length) return list(cb);

  var pkgdir = npm.localPrefix;
  var cmd = args.shift();

  readJson(path.resolve(pkgdir, 'package.json'), function (er, d) {
    if (er) return cb(er);
    run(d, pkgdir, cmd, args, cb);
  });
}
function run(pkg, wd, cmd, args, cb) {
  if (!pkg.scripts) pkg.scripts = {};

  var cmds;
  if (cmd === 'restart' && !pkg.scripts.restart) {
    cmds = [
      'prestop',
      'stop',
      'poststop',
      'restart',
      'prestart',
      'start',
      'poststart',
    ];
  } else {
    if (pkg.scripts[cmd] == null) {
      if (cmd === 'test') {
        pkg.scripts.test = "echo 'Error: no test specified'";
      } else if (cmd === 'env') {
        if (isWindowsShell) {
          log.verbose('run-script using default platform env: SET (Windows)');
          pkg.scripts[cmd] = 'SET';
        } else {
          log.verbose('run-script using default platform env: env (Unix)');
          pkg.scripts[cmd] = 'env';
        }
      } else if (npm.config.get('if-present')) {
        return cb(null);
      } else {
        let suggestions = didYouMean(cmd, Object.keys(pkg.scripts));
        suggestions = suggestions ? '\n' + suggestions : '';
        return cb(new Error('missing script: ' + cmd + suggestions));
      }
    }
    cmds = [cmd];
  }

  if (!cmd.match(/^(pre|post)/)) {
    cmds = ['pre' + cmd].concat(cmds).concat('post' + cmd);
  }

  log.verbose('run-script', cmds);
  chain(
    cmds.map(function (c) {
      // pass cli arguments after -- to script.
      if (pkg.scripts[c] && c === cmd) {
        pkg.scripts[c] = pkg.scripts[c] + joinArgs(args);
      }

      // when running scripts explicitly, assume that they're trusted.
      return [lifecycle, pkg, c, wd, { unsafePerm: true }];
    }),
    cb
  );
}

注意!注意!注意!关键就在runScript函数内定义了pkgdir变量并且通过pkgdir作为路径readJson加载package.json文件.(通过变量名pkgdir也可以看出是package.json的目录名),下一步是调用run(d, pkgdir, cmd, args, cb);函数时把此变量作为参数传递了出去.所以在后面的代码拿到的工作目录都是pkgdir变量.

至此已经知道为什么会出现测试二的情况(结论:在 utils 目录下无论执行命令npm run babelnpm run utils最后输出的当前工作目录都不一致.),原因就在于npm则是找到package.json所在路径作为参数wd传递.所以后面调用 babel 时,输出的console.log('babel路径:',process.cwd());便是它.

以下为部后续执行代码

// npm/lib/run-script.js
chain(
  cmds.map(function (c) {
    // pass cli arguments after -- to script.
    if (pkg.scripts[c] && c === cmd) {
      pkg.scripts[c] = pkg.scripts[c] + joinArgs(args);
    }

    // when running scripts explicitly, assume that they're trusted.
    return [lifecycle, pkg, c, wd, { unsafePerm: true }];
  }),
  cb
);
// npm/node_modules/slide/lib/chain.js
module.exports = chain;
var bindActor = require('./bind-actor.js');
chain.first = {};
chain.last = {};
function chain(things, cb) {
  var res = [];
  (function LOOP(i, len) {
    if (i >= len) return cb(null, res);
    if (Array.isArray(things[i]))
      things[i] = bindActor.apply(
        null,
        things[i].map(function (i) {
          return i === chain.first
            ? res[0]
            : i === chain.last
            ? res[res.length - 1]
            : i;
        })
      );
    if (!things[i]) return LOOP(i + 1, len);
    things[i](function (er, data) {
      if (er) return cb(er, res);
      if (data !== undefined) res = res.concat(data);
      LOOP(i + 1, len);
    });
  })(0, things.length);
}
// npm/node_modules/npm-lifecycle/index.js
function lifecycle(pkg, stage, wd, opts) {
  lifecycle_(pkg, stage, wd, opts, env, (er) => {
    if (er) return reject(er);
    return resolve();
  });
}

function lifecycle_(pkg, stage, wd, opts, env, cb) {
  chain(
    [
      packageLifecycle && [runPackageLifecycle, pkg, stage, env, wd, opts],
      [runHookLifecycle, pkg, stage, env, wd, opts],
    ],
    done
  );
}
function runPackageLifecycle(pkg, stage, env, wd, opts, cb) {
  // run package lifecycle scripts in the package root, or the nearest parent.
  var cmd = env.npm_lifecycle_script;

  var note = '\n> ' + pkg._id + ' ' + stage + ' ' + wd + '\n> ' + cmd + '\n';
  runCmd(note, cmd, pkg, env, stage, wd, opts, cb);
}
function runCmd(note, cmd, pkg, env, stage, wd, opts, cb) {
  if (unsafe) {
    runCmd_(cmd, pkg, env, wd, opts, stage, unsafe, 0, 0, cb);
  } else {
    uidNumber(user, group, function (er, uid, gid) {
      if (er) {
        er.code = 'EUIDLOOKUP';
        opts.log.resume();
        process.nextTick(dequeue);
        return cb(er);
      }
      runCmd_(cmd, pkg, env, wd, opts, stage, unsafe, uid, gid, cb);
    });
  }
}
function runCmd_(cmd, pkg, env, wd, opts, stage, unsafe, uid, gid, cb_) {
  function cb(er) {
    cb_.apply(null, arguments);
    opts.log.resume();
    process.nextTick(dequeue);
  }

  const [sh, args, conf] = getSpawnArgs({
    cmd,
    wd,
    opts,
    uid,
    gid,
    unsafe,
    env,
  });

  opts.log.verbose('lifecycle', logid(pkg, stage), 'PATH:', env[PATH]);
  opts.log.verbose('lifecycle', logid(pkg, stage), 'CWD:', wd);
  opts.log.silly('lifecycle', logid(pkg, stage), 'Args:', args);

  var proc = spawn(sh, args, conf, opts.log);
}
// npm/node_modules/npm-lifecycle/lib/spawn.js
const _spawn = require('child_process').spawn;
function spawn(cmd, args, options, log) {
  const cmdWillOutput = willCmdOutput(options && options.stdio);

  if (cmdWillOutput) startRunning(log);
  const raw = _spawn(cmd, args, options);
  const cooked = new EventEmitter();
}
//https://github.com/nodejs/node/blob/v14.16.1/lib/child_process.js
function spawn(file, args, options) {
  const child = new ChildProcess();

  options = normalizeSpawnArguments(file, args, options);
  debug('spawn', options);
  child.spawn(options);

  return child;
}

function normalizeSpawnArguments(file, args, options) {
  return {
    // Make a shallow copy so we don't clobber the user's options object.
    ...options,
    args,
    detached: !!options.detached,
    envPairs,
    file,
    windowsHide: !!options.windowsHide,
    windowsVerbatimArguments: !!windowsVerbatimArguments,
  };
}

之所以展示这么多后续代码 无非就是表明:

  1. wd 参数一直在传递.
  2. 最后在spawn.js脚本内_spawn(cmd, args, options)新建子进程时参数options(对象类型)内的键cwd对应的值便是参数wd.
  3. 这个_spawn函数新建子进程就是后面的babel.(babel接收的process.cwd则来源于上面的npm).

四、babel.config.js

介绍

这种配置方式是在项目的根(root)目录下新建babel.config.js配置文件便可以应用到整个项目,之所以放在最后,因为它是babel7新增的配置方式.

可参考官网关于babel.config.js的更多介绍www.babeljs.cn/docs/config….

示例

项目结构如下:

|--config-babel-babelconfig 项目名称
   |-- babel.config.js 配置文件
   |-- package.json
   |-- package-lock.json 记录安装模块的版本、来源信息
   |-- node_modules node 依赖模块集合
      |-- babel-cli cli模块
      |-- babel-core 核心模块
      |-- babel-plugin-transform-es2015-arrow-functions 箭头转换模块
      |-- babel-* 以babel开头的各个模块后面会介绍
   |-- script.js 脚本文件
// package.json
{
  "name": "config-babel-babelconfig",
  "devDependencies": {
    "@babel/cli": "^7.12.10",
    "@babel/core": "^7.12.10",
    "@babel/plugin-transform-arrow-functions": "^7.12.1"
  },
  "scripts": {
    "babel": "babel script.js",
    "babel-compiled": "babel script.js --out-file script-compiled.js"
  }
}
// babel.config.js
module.exports = function (api) {
  api.cache(true);

  const plugins = ['@babel/plugin-transform-arrow-functions'];

  return {
    plugins,
  };
};
// script.js
[1, 2, 3, 4].forEach((item) => console.log(item));

执行npm run babel输出结果:

// script.js 转码后
[1, 2, 3, 4].forEach(function (item) {
  return console.log(item);
});

示例相对简单只是演示了babel7新增的基于根目录的配置方式.

补充

这个补充内容可能会啰嗦很多很多...所以一定要给自己一巴掌保持清醒(两巴掌见效更快).

项目根目录配置方式.

这是babel7中新增的功能,以根目录默认为当前目录,在根目录检索一个babel.config.js的文件或后缀为.json,.cjs,.mjs的文件,因为babel7提供了多种后缀格式的配置文件.

// node_modules/@babel/core/lib/config/files/configuration.js
const ROOT_CONFIG_FILENAMES = [
  'babel.config.js',
  'babel.config.cjs',
  'babel.config.mjs',
  'babel.config.json',
];

以上是源码定义的要检索的文件名.根据变量名也可以看出来(根目录配置文件名称).

注意只在根目录下检索,也可以通过配置 configFile 指定加载配置文件

babel7新增的基于根的配置方式与之前的相对目录配置有什么区别?

其实在上面提供的官网链接已经回答了这个问题,这里只做了解.

在之前介绍的三、.babelrc是相对的配置(可以把配置文件写在各个目录下)