babel从入门到毁灭系列一

2,592 阅读15分钟
  • 【基础】babel历史

    Babel 在 4.0 版本之前叫做 "6to5",由澳大利亚的 Sebastian (早已离开 Babel 团队,目前就职于 Facebook,依然致力于前端工具链开发中)在他的 2014 年十一月(那是他的高中时期)开始发布的,通过 6to5 这个名字我们就能知道,最初的 Babel 就是将 ES6 的代码转换成 ES5 的代码,2015 年与 ESNext 合并并改名成 Babel。Babel 的本意是 "巴别塔",取自神话小说中。由 6to5 重命名为 Babel 前,6to5 已经走过了三个版本,所以改名后的 Babel 从版本 4.0 开始,此时已经能够转译 ES7 与 JSX,2015 年发布 5.0,引入了 stage,创建了 plugin system 来支持自定义转译,同年又发布 6.0 ,拆分了几个核心包,引入了 presets 和 plugin/presets options 的概念,18 年,发布了 7.0,除了性能方面的提升以外,增加了对 typescript 的支持,对安装包使用「@babel」的命名空间

  • 【基础】babel是什么?

    • Babel 是一个 js parser【JavaScript 编译器】
    • Babel 是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。下面列出的是 Babel 能为你做的事情:
      • 语法转换【js parser】
      • 通过 Polyfill 方式在目标环境中添加缺失的特性 (通过引入三方 polyfill 模块,例如 core-js)
      • 源码转换(codemods)
    • 一句通俗的话概括:为了低端浏览器,能转的转,转不掉的polyfill
  • 【基础】babel有哪些特点?

    • 插件化【这点和webpack很像】
      • Babel 构建在插件之上。使用现有的或者自己编写的插件可以组成一个转换管道。通过使用或创建一个 preset 即可轻松使用一组插件
      • 利用 astexplorer.net 可以立即创建一个插件,或者使用 generator-babel-plugin 生成一个插件模板。
      • 一个插件就是一个函数
      • 可调试【由于 Babel 支持 Source map,因此你可以轻松调试编译后的代码】
  • 【基础】js parser历史及babel是如何继承的?

    • 总结: babel 的 parser 是基于 acorn 扩展而来的。
      • js parser 的历史,基于火狐浏览器的 JS 引擎 SpiderMonkey 的 AST 标准,制定了 espree 的标准,最早的 estree 标准的实现是 esprima,但是随着 es2015 开始一年一个版本,esprima 的迭代速度逐渐跟不上了,这时候 acorn 流行起来,因为速度更快,而且支持插件扩展,于是 espree、babel parser(babylon) 等都基于 acorn 来实现各自的 parser。虽然 estree 系列的 js parser 挺多的,但也不是全部,terser、typescript 等都是用自己的 AST。【 estree:esprima - acorn - babel parser,非estree路线:terser、tstree(typescript)】
      • babel parser 能不断地支持新的语法,就是通过修改《词法分析、语法分析》阶段的代码来实现的。
      • 现在 babel parser 的代码里已经看不到 acorn 的依赖了,因为在 babel4 以后,babel 直接 fork 了 acorn 的代码来修改,而不是引入 acorn 包再通过插件扩展的方式。但是,原理还是一样的。
    • 关系图谱:关系图谱
    • js parser的过程:词法分析【词的分析】 + 语法分析【句的分析,组装】
  • 【基础】前置说明:

    • @babel/polyfill【core-js/stable (用于模拟 ECMAScript 的功能)】是运行时的,在运行时才会根据当前浏览器版本环境决定要不要polyfill
      • 所以npm install --save @babel/polyfill-S 而不是 -D,而且要放在入口文件的顶部,需要在你的源码之前运行的 polyfill,这样才能有效
  • 【基础】Babel配置文件

    • Project-wide configuration 项目范围的配置
      • babel.config.* files, with the following extensions: .json, .js, .cjs, .mjs.
    • File-relative configuration 文件相关配置
      • .babelrc.* files, with the following extensions: 文件,其扩展名如下:.json, .js, .cjs, .mjs.
      • .babelrc file, with no extension. 文件,没有扩展名
      • package.json files, with a 文件"babel" key. 钥匙
    • 权重顺序:babel cli【package.json】 > .babelrc > .babelrc.* > babel.config.*
  • 【基础】Babel配置选项 [重点属性系列]

    • 1、Primary options 总要配置

      • filename【Type: string】: The filename associated with the code currently being compiled, if there is one. The filename is optional, but not all of Babel's functionality is available when the filename is unknown, because a subset of options rely on the filename for their functionality === 文件名与正在编译的代码是有关系的。文件名是可选的,但是并不是所有的babel功能都能有效,当缺文件名的时候,因为有些有些子级选项依赖这个文件名
      • filenameRelative【Type: string】: Used as the default value for Babel's sourceFileName option, and used as part of generation of filenames for the AMD / UMD / SystemJS module transforms === 被用作 sourceFileName 选项的默认值。同时作为转换AMD / UMD / SystemJS module时的filenames选项一部分
      • code 【Type: boolean、Default: true】: Babel's default return value includes code and map properties with the resulting generated code. In some contexts where multiple calls to Babel are being made, it can be helpful to disable code generation and instead use ast: true to get the AST directly in order to avoid doing unnecessary work === babel默认处理之后,只返回code和sourceMap,在对babel进行多个调用的某些情况下,禁用代码生成并使用ast:true直接获取ast可能会有所帮助,以避免做不必要的工作。
          // 下面这段代码通过设置,实现在连续babel处理的时候,ast的有效传递,但是同时需要设置code为false来阻止默认的code的生成
          // 所以ast和code大部分情形下需要同时设置
          const filename = "example.js";
          const source = fs.readFileSync(filename, "utf8");
      
          // Load and compile file normally, but skip code generation.
          const { ast } = babel.transformSync(source, {
              filename,
              ast: true,
              code: false,
          });
      
          // Minify the file in a second pass and generate the output code here.
          const { code, map } = babel.transformFromAstSync(ast, source, {
              filename,
              presets: ["minify"],
              babelrc: false,
              configFile: false,
          });
      
      • ast【Type: boolean、Default: false】: Babel's default is to generate a string and a sourcemap, but in some contexts it can be useful to get the AST itself. The primary use case for this would be a chain of multiple transform passes, along the lines of === babel默认生成一个a string 和 a sourcemap,不含有ast,通过该选项可以让其返回中携带ast。
          // 下面这段代码通过设置,实现在连续babel处理的时候,ast的有效传递,但是同时需要设置code为false来阻止默认的code的生成
          // 所以ast和code大部分情形下需要同时设置
          const filename = "example.js";
          const source = fs.readFileSync(filename, "utf8");
      
          // Load and compile file normally, but skip code generation.
          const { ast } = babel.transformSync(source, {
              filename,
              ast: true,
              code: false,
          });
      
          // Minify the file in a second pass and generate the output code here.
          const { code, map } = babel.transformFromAstSync(ast, source, {
              filename,
              presets: ["minify"],
              babelrc: false,
              configFile: false,
          });
      
    • 2、Config Loading options 加载中配置

      • root【Type: string,Default: opts.cwd】: The initial path that will be processed based on the "rootMode" to determine the conceptual root folder for the current Babel project。 === babel的基根,默认值:opts.cwd,将基于“rootmode”处理初始化路径,以确定当前babel项目的概念根文件夹。这主要用于两种情况: 检查默认“configfile”值时的基目录 “babelrcroots”的默认值。
      • rootMode 【Type: "root" | "upward" | "upward-optional" 、Default: "root"】: This option, combined with the "root" value, defines how Babel chooses its project root. The different modes define different ways that Babel can process the "root" value to get the final project root。
        • "root" - Passes the "root" value through as unchanged.
        • "upward" - Walks upward from the "root" directory, looking for a directory containing a babel.config.json file, and throws an error if a babel.config.json is not found.
        • "upward-optional" - Walk upward from the "root" directory, looking for a directory containing a babel.config.json file, and falls back to "root" if a babel.config.json is not found.
        • Note:
          • 1、当单项目时尽量不要动这个选项,没有必要
          • 2、当Monorepo项目时,在每个包的基础上运行构建/测试的用户很可能希望使用“向上”,因为Monorepo通常在项目根目录中有babel.config.js。在没有“向上”的monorepo子目录中运行babel将导致babel跳过在项目根目录中加载任何babel.config.js文件,这可能导致意外错误和编译失败。
      • configFile 【Type: string | boolean 、Default: path.resolve(opts.root, "babel.config.json"), if it exists, false otherwise】: This option, combined with the "root" value, defines how Babel chooses its project root. The different modes define different ways that Babel can process the "root" value to get the final project root。
        • NOTE: This option does not affect loading of .babelrc.json files, so while it may be tempting to do configFile: "./foo/.babelrc.json", it is not recommended. If the given .babelrc.json is loaded via the standard file-relative logic, you'll end up loading the same config file twice, merging it with itself. If you are linking a specific config file, it is recommended to stick with a naming scheme that is independent of the "babelrc" name.
    • 3、Plugin and Preset options 插件和预设选项配置

      • plugins【Type: Array<PluginEntry | Plugin> (PluginEntry)、Default: []】:babel插件集合,顺序从左到右,注意env and overrides属性的merge策略
      • presets【Type: Array (PresetEntry)、Default: []】:babel预设集合,顺序从右到左,注意env and overrides属性的merge策略
    • 4、Config Merging options 合并选项配置

      • extends【Type: string、Placement: Not allowed inside of presets】:Configs may "extend" other configuration files. Config fields in the current config will be merged on top of the extended file's configuration === extends 引入另外的配置文件,当前文件的配置属性高于导入文件内的配置;不能在内部预设中设置,extends属性使得抽离公共配置成为可能
      • env【Type: { [envKey: string]: Options }、Placement: May not be nested inside of another env block.】Allows for entire nested configuration options that will only be enabled if the envKey matches the envName option、Note: env[envKey] options will be merged on top of the options specified in the root object ==== 允许只有在 envKey 与 envName 选项匹配时才启用的整个嵌套配置选项, env[envKey]选项将被合并在根部配置属性之上。(这个option比较常见,常常应用于需要针对development以及production等不同的运行环境做不同的babel配置时使用)
      • overrides【Type: Array、Placement: May not be nested inside of another overrides object, or within an env block.】
        • Allows users to provide an array of options that will be merged into the current configuration one at a time. This feature is best used alongside the "test"/"include"/"exclude" options to provide conditions for which an override should apply === 允许用户提供一系列额外的options,这些options将一次合并到当前配置中。 此功能需要与后面的test include exclude结合使用,它们可以给overridesoption提供生效条件,
            overrides: [{
                test: "./vendor/large.min.js",
                compact: true,
            }],
        
        • could be used to enable the compact option for one specific file that is known to be large and minified, and tell Babel not to bother trying to print the file nicely === compact option,只会应用于./vendor/large.min.js这个文件
      • test【Type: MatchPattern | Array (MatchPattern)】:If all patterns fail to match, the current configuration object is considered inactive and is ignored during config processing. This option is most useful when used within an overrides option object, but it's allowed anywhere. === 最佳实践是作为overrides属性的生效条件
      • include【Type: MatchPattern | Array (MatchPattern)】: 最佳实践是作为overrides属性的生效条件,和test一样
      • exclude【Type: MatchPattern | Array (MatchPattern)】: 最佳实践是作为overrides属性的生效条件,和test一样
      • ignore【Type: MatchPattern | Array (MatchPattern)】: Note: This option disables all Babel processing of a file. While that has its uses, it is also worth considering the "exclude" option as a less aggressive alternative. === 最佳实践是作为overrides属性的生效条件,但是非常激进,会让整个文件的babel进程都被忽略,建议使用exclude代替
      • only【Type: Array (MatchPattern)、Placement: Not allowed inside of presets】
        • If all of the patterns fail to match, Babel will immediately stop all processing of the current build. For example, a user may want to do something like:only: ["./src"];to explicitly enable Babel compilation of files inside the src directory while disabling everything else.
        • Note: This option disables all Babel processing of a file. While that has its uses, it is also worth considering the "test"/"include" options as a less aggressive alternative. ==== 这个选项会让整个babel进程无效,同样比较暴力,建议考虑"test"/"include"代替
    • 5、Misc options 杂项配置

      • sourceType【Type: "script" | "module" | "unambiguous"、Default: "module"】 === 告诉babel是否需要以ES6模块去编译,值有 script module(默认) unambiguous 。一般来说,我们项目都是在node环境下的,所以模块化标准用的是CommonJs。这时候如果我们想用ES6的模块化标准的话,我们就需要将其配置为 module 。而 unambiguous 就比较暴力了,他就看文件中是否出现了import/export,出现了就匹配为 script ,没出现就匹配为 module 。
        • "script" - Parse the file using the ECMAScript Script grammar. No import/export statements allowed, and files are not in strict mode. === 文件非严格模式,import/export语句不被允许,es 脚本语法,commonjs语法
        • "module" - Parse the file using the ECMAScript Module grammar. Files are automatically strict, and import/export statements are allowed. === 默认模式,文件自适应严格模式,import/export语句被允许
        • "unambiguous" - Consider the file a "module" if import/export statements are present, or else consider it a "script". === 自动识别,依据文件中有没有import/export
    • 6、Code Generator options 代码生成选项配置

      • comments【Type: boolean、Default: true】:Provides a default comment state for shouldPrintComment if no function is given. See the default value of that option for more info === 提供一个默认的注释状态给shouldPrintComment属性,从而来决定如何处理注释,要不要保留注释
      • shouldPrintComment【Type: (value: string) => boolean、Default without minified: (val) => opts.comments || /@license|@preserve/.test(val)、Default with minified: () => opts.comments】A function that can decide whether a given comment should be included in the output code from Babel === 一个函数来告诉babel要不要保留注释,该属性通常依赖comments提供的默认值
    • 7、Source Map options sourceMap配置

      • sourceMaps【Type: boolean | "inline" | "both"、Default: false】:
        • true to generate a sourcemap for the code and include it in the result object.【结果对象的尾部就包含sourceMap的引用,sourceMap以.map文件单独存在】
        • "inline" to generate a sourcemap and append it as a data URL to the end of the code, but not include it in the result object.【结果对象的尾部就包含sourceMap,但是以data:展示,会增加code的大小,不常用】
        • "both" is the same as inline, but will include the map in the result object.【.map文件和data: 都包含】
      • sourceFileName 【Type: string、Default: path.basename(opts.filenameRelative) when available, or "unknown"】:The name to use for the file inside the source map object. === sourceMap文件名称,被用做file的名称,在source map对象的内部被用到
      • sourceRoot【Type: string】The sourceRoot fields to set in the generated source map, if one is desired === 源码存放目录
      • inputSourceMap【Type: boolean | SourceMap、Default: true】:true will attempt to load an input sourcemap from the file itself, if it contains a //# sourceMappingURL=... comment. If no map is found, or the map fails to load and parse, it will be silently discarded.If an object is provided, it will be treated as the source map object itself === 如果是布尔值就去试图加载对应的sourcemap源文件,如果是对象那就回被当做sourceMap源文件处理
  • 【紧密相关】babel是怎么合并option-item配置选项的,尤其是多配置文件共存时

    • 汇总:Babel's configuration merging is relatively straightforward. Options will overwrite existing options when they are present and their value is not undefined. There are, however, a few special cases:
      • For assumptions, parserOpts and generatorOpts, objects are merged, rather than replaced.
      • For plugins and presets, they are replaced based on the identity of the plugin/preset object/function itself combined with the name of the entry.
    • 1、Option (except plugin/preset) merging 【常规属性合并规则】
      • 常规属性会后面的覆盖前面的
      {
          sourceType: "script",
          assumptions: {
              setClassFields: true,
              iterableIsArray: false
          },
          env: {
              test: {
              sourceType: "module",
              assumptions: {
                  iterableIsArray: true,
              },
              }
          }
      };
      
      • When NODE_ENV is test, the sourceType option will be replaced and the assumptions option will be merged. The effective config is:
      {
          sourceType: "module", // sourceType: "script" is overwritten
          assumptions: {
              setClassFields: true,
              iterableIsArray: true, // assumptions are merged by Object.assign
          },
      }
      
    • 2、Plugin/Preset merging【Plugin/Preset 属性合并规则】
      • 作为整体去实施替换
      plugins: [
          './other',
          ['./plug', { thing: true, field1: true }]
      ],
      overrides: [{
          plugins: [
              ['./plug', { thing: false, field2: true }],
          ]
      }]
      
      • The overrides item will be merged on top of the top-level options. Importantly, the plugins array as a whole doesn't just replace the top-level one. The merging logic will see that "./plug" is the same plugin in both cases, and { thing: false, field2: true } will replace the original options, resulting in a config as
      plugins: [
          './other',
          ['./plug', { thing: false, field2: true }],
      ],
      
      • Since merging is based on identity + name, it is considered an error to use the same plugin with the same name twice in the same plugins/presets === 因此合并是基于标识符和名称的。
        • 错误场景一:
            plugins: ["./plug", "./plug"];
            
            // is considered an error, because it's identical to
            plugins: ['./plug']
        
        • 错误场景二:
            plugins: [["./plug", { one: true }], ["./plug", { two: true }]];
            
            // is considered an error, because the second one would just always replace the first one.
            plugins: [["./plug", { two: true }]]
        
        • 正确用法:
            // If you actually do want to instantiate two separate instances of a plugin, you must assign each one a name to disambiguate them. because each instance has been given a unique name and thus a unique identity.
            plugins: [
                ["./plug", { one: true }, "first-instance-name"],
                ["./plug", { two: true }, "second-instance-name"],
            ];
        
  • 【紧密相关】关于babel在多包monorepo环境下的策略

    • 总结
      • Monorepo-structured repositories usually contain many packages, which means that they frequently run into the caveats mentioned in file-relative configuration and config file loading in general. This section is aimed at helping users understand how to approach monorepo configuration.
      • With monorepo setups, the core thing to understand is that Babel treats your working directory as its logical "root", which causes problems if you want to run Babel tools within a specific sub-package without having Babel apply to the repo as a whole. === babel默认会把当前工作目录当做‘root’
      • Separately, it is also important to decide if you want to use .babelrc.json files or just a central babel.config.json. .babelrc.json files are not required for subfolder-specific configuration like they were in Babel 6, so often they are not needed in Babel 7, in favor of babel.config.json. === z子包目录里的.babelrc.json不会默认加载执行,根目录下的babel.config.json会执行,因此这在babel6中有问题
    • 破解方法
      • 方法一:Root babel.config.json file同时配合"overrides"属性,
        • 这样当子包的脚本都在根目录时,没有问题;但是当我们cd到子包时,而且脚本也在子包项目时,就会有问题,因为子包项目无法知道到哪里寻找babel的配置
      • 方法一升级:Root babel.config.json file同时配合"overrides"属性,再配合 "rootMode" 属性
        • 但是当我们cd到子包时,而且脚本也在子包项目时,因为子包项目无法知道到哪里寻找babel的配置,但是在子包项目,配合 "rootMode" 且值为"upward"时,这样子包就知道向上查找babel配置,使之正常
      • 方法二:Root babel.config.json file 同时配合 Subpackage .babelrc.json 文件
        • 当将babel.config.json中添加 "babelrcRoots" 属性,且属性值设置为子包目录时,这样,子包在执行脚本时,就是识别到.babelrc.json,使之生效,同时.babelrc.json是继承自根目录下的babel.config.json
    • 拓展彩蛋:关于babel从6到7时如何逐步满足多包的,见github或自行百度相关历史
  • 敬请期待....

    • 插件和预设科普
    • babel的ast入坑【astExplorer的使用简介,acorn到babel-parser打怪升级】
    • babel手写插件【插桩,埋点,国际化...】
    • babel源码调试【vscode launch】