babel 和 tsc 的使用指南

2,018 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第13天,点击查看活动详情

babel 是一个 JS 编译器,通过添加 preset 也可以对 TS 进行编译;tsc 是 typescript 的官方编译工具,可以将 TS 代码转换为目标规范的 JS。

如此看来,他们两者的功能有相似之处,在平时的开发中,我们是如何选择的呢 😉?

TLNR

  • 情况一:项目运行环境无需过多 polyfill 的支持,项目源代码到输出代码过程仅需ts(x)→js(x),无对源码的个性化处理,则使用 tsc 编译。
  • 情况二:项目运行环境需要适配目标浏览器/Node.js环境等,源代码到输出代码过程需要除ts(x)→js(x) 编译外的个性化处理,追求更快的编译速度,但无需类型检查和类型声明文件的输出,则使用 babel 编译。
  • 情况三(常见):既要引入 polyfill 适配目标浏览器版本,又要进行类型检查并输出类型声明文件,则使用 babel 进行编译,使用 tsc 进行类型检查和声明文件输出。

下面,为了更好地区分这两类编译工具,我们首先分别认识下其各自的特点吧!

认识 babel

babel 可以将 ES6 的代码转换为 ES5 的代码,使得开发者可以使用新的语言特性和API进行开发,并输出适配低版本浏览器的代码。

因此,这个转换过程中有两个要点需要实现,其一就是语法转换(syntax) ,比如箭头函数(arrow function)需要转换成普通函数实现,比如 let/const 关键字的实现。另一类就是对新的 API 进行支持,比如 Object.assign、Array.proptype.includes 等。简单归类一下,语法转换可以看做是我们使用 ES5 的语法不能实现的代码层面的修改,而新的API则是可以通过 ES5 进行实现的一些新对象、对象原型链上的新方法、静态方法等。举个例子,WeakMap、Promise 对象通过 ES5 可以实现,所以属于新的 API。

针对上面两个要点,

  • 新 API: 使用 ES5 实现新的 API ,这种实现方式也叫作 polyfill
  • 新语法规则:使用 babel 中各种插件(plugin)实现。

polyfill 和 pulgin 基本覆盖了 babel 的所有功能。

而 preset 是什么呢?preset 其实是插件的合集。为了避免开发者冗余的插件配置工作,使用 preset 可以针对需要适配的特定环境(target)进行系列插件的同时注入。

babel 工具链中有不同的包来实现 polyfill 、 plugin 和 preset。

这篇教程中详细区分了 @babel/preset-env、@babel/polyfill(@babel/core-js + @babel/regenerator)、@babel/runtime、@babel/plugin-transform-runtime 。感兴趣的同学可以自行了解每个包具体的作用,在这里我只做一个小结 😜。

  1. @babel/preset-env 提供特定环境的语法转换系列套件。
  2. @babel/polyfill 实际上可以看做 @babel/core-js + @babel/regenerator 共同组成,用 ES5 实现新的 API。
  3. @babel/runtime 提供实现新 API 的辅助函数。
  4. @babel/plugin-transform-runtime 针对语法转换部分,可以自动移除语法转换后内联的辅助函数(inline Babel helpers,这些内联的辅助函数是因语法转换生成的),而使用 @babel/runtime 里的辅助函数来替代。同时,针对 API 部分,可以自动引入相应的包做 API 转换(代替 @babel/polyfill 中同名的实现),防止全局变量污染。

使用 babel 编译

这部分主要涉及两个 preset ,包括:@babel/preset-typescript 和 @babel/preset-react。

在这里贴上一份配置文件 👇

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "modules": false
      }
    ],
    "@babel/preset-react", 
    "@babel/preset-typescript"
  ],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": {
          "version"3,
          "proposals": true
        }
      }
    ],
    ["@babel/plugin-syntax-dynamic-import"]
  ]
}

其中涉及到的 npm 包有 👇

"devDependencies": {
    "@babel/core""7.17.5",
    "@babel/plugin-syntax-dynamic-import""7.8.3",
    "@babel/plugin-transform-runtime""7.17.0",
    "@babel/preset-env""7.16.11",
    "@babel/preset-react""7.16.7",
    "@babel/preset-typescript""7.16.7",
    "@babel/runtime-corejs3""7.17.2",
  // ...
}

如果配合 webpack 使用,那么需要使用 babel-loader 来处理后缀名为.ts/.tsx的文件模块。

// webpack.config.js
module.exports = {
 //...
 module: {
  rules: [
   // ...
   {
     test/.(tsx?|js)$/// ts\tsx\js
        loader'babel-loader',
        exclude/node_modules/
      }
  ]
 }
}

认识 tsc

tsc 是 typescript 的官方编译工具,可以将 ts(x) 编译成目标的 JS。那么首先概括下他的能力。

  1. 读取 tsconfig 文件提供的配置项,输出最终的 JS 文件。
  2. 类型检查,输出类型声明文件(.d.ts)。

可以看到,tsc 具备了 babel 没有的类型检查功能,也不会生成类型声明文件。并且二者在编译方式上存在一定的区别。tsc 将对整个 typescript 项目进行编译,而 babel 则是单文件编译。 二者具体的区别如下表所示 👇

babel(@babel/preset-typescript)tsc
是否类型检查
是否生成类型声明文件
polyfill支持性通过插件可以自动按需引入手动全量引入 core-js
对 ES 的支持全部标准最新标准和部分草案
对 TS 的支持不支持 const enum(会作为 enum 处理),不支持 namespace 的跨文件合并,导出非 const 的值等天然支持
编译速度

值得一提的是,tsconfig 配置文件中有一些和 babel 功能类似的配置选项,我们具体来看一下~

  • target

tsconfig 中的 target 配置的是 ES 标准的版本,比如 es5、es6、esnext 等。而 babel 中使用 preset-env 时,也有 target 的配置,其选择值更偏向于具体的运行环境,针对浏览器版本居多。

// .barbelrc

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "edge": "17",
          "firefox": "60",
          "chrome": "67",
          "safari": "11.1"
        },
        "useBuiltIns": "usage"
      }
    ]
  ]
}
  • lib

tsconfig 中的 lib 用来声明内置的对象及方法,避免类型检查时出错,这和运行环境是相关的。但是这不意味着使用了 lib 配置就引入了相应的 polyfill,实际上,polyfill 在 tsc 中是没有实现的,只能通过在项目中引入 core-js 引入 polyfill。

  • module

tsconfig 中的 module 主要用来配置模块导入导出的语法,对应的,在 babel 中使用 preset-env 时也有 modules 用来配置模块的导入导出语法。

使用 tsc 编译

在此处同样上一份配置文件 👇,如果要使用 webpack 的话,可能会用到 ts-loader。

// tsconfig.json

{
  "compilerOptions": {
    // 基本配置
    "target": "ES5", // 编译成哪个版本的 es
    "module": "ESNext", // 指定生成哪个模块系统代码
    "lib": ["dom", "dom.iterable", "esnext"], // 编译过程中需要引入的库文件的列表
    "allowJs": false, // 允许编译 js 文件
    "jsx": "react", // 在 .tsx 文件里支持 JSX
    "isolatedModules": true, // 提供额外的一些语法检查,如文件没有模块导出会报错
    "strict": true, // 启用所有严格类型检查选项

    // 模块解析选项
    "moduleResolution": "node", // 指定模块解析策略
    "esModuleInterop": true, // 支持 CommonJS 和 ES 模块之间的互操作性
    "resolveJsonModule": true, // 支持导入 json 模块
    "baseUrl": "./", // 根路径
    "paths": {
      // 路径映射,与 baseUrl 关联
      "@/*": ["./src/*"]
    },

    // 实验性选项
    "experimentalDecorators": true, // 启用实验性的ES装饰器
    "emitDecoratorMetadata": true, // 给源码里的装饰器声明加上设计类型元数据

    // 其他选项
    "forceConsistentCasingInFileNames": true, // 禁止对同一个文件的不一致的引用
    "skipLibCheck": true, // 忽略所有的声明文件( *.d.ts)的类型检查
    "allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入
    "noEmit": true // 只想使用tsc的类型检查作为函数时(当其他工具(例如Babel实际编译)时)使用它
    // "strictNullChecks": false
  },
  "exclude": ["node_modules"]
}

最佳实践

其实在这之前,babel 是不支持 TS 的,编译的工具链往往很长,可能会首先经过 tsc 处理,然后再使用 babel 处理 JS。但现在,我们有了更好的方式来结合 bable 和 tsc

由于 babel 的很高的扩展性和灵活性(而且没有类型检查会很快啊 😀),我们一般会使用它进行编译输出 JS,而 tsc 会成为必要的时候类型检查和输出类型声明文件的工具。这也是 typescript 官方文档中给出的建议!

如果是和 webpack 结合的话,也只需要 babel-loader 就够了。下面我们基于之前的 babel 配置,添加类型检查的功能吧。在这里依然需要配置 tsconfig.json 文件 👇

{
 "compilerOptions": {
  // Target latest version of ECMAScript.
  "target": "esnext",
  // Search under node_modules for non-relative imports.
  "moduleResolution": "node",
  // Process & infer types from .js files.
  "allowJs": true,
  // Don't emit; allow Babel to transform files.
  "noEmit": true,
  // Enable strictest settings like strictNullChecks & noImplicitAny.
  "strict": true,
  // Disallow features that require cross-file information for emit.
  "isolatedModules": true,
  // Import non-ES modules as default imports.
  "esModuleInterop": true
 },
 "include": [
  "src"
 ]
}

当我们运行 tsc 时,就可以进行类型检查了。当然了,如果要输出类型声明文件,则需要配置declaration 和 declarationDir了 。