持续创作,加速成长!这是我参与「掘金日新计划 · 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 。感兴趣的同学可以自行了解每个包具体的作用,在这里我只做一个小结 😜。
- @babel/preset-env 提供特定环境的语法转换系列套件。
- @babel/polyfill 实际上可以看做 @babel/core-js + @babel/regenerator 共同组成,用 ES5 实现新的 API。
- @babel/runtime 提供实现新 API 的辅助函数。
- @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。那么首先概括下他的能力。
- 读取 tsconfig 文件提供的配置项,输出最终的 JS 文件。
- 类型检查,输出类型声明文件(.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了 。