Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

使用 babel 转译 typescript 代码 #119

Open
lmk123 opened this issue Nov 25, 2022 · 0 comments
Open

使用 babel 转译 typescript 代码 #119

lmk123 opened this issue Nov 25, 2022 · 0 comments

Comments

@lmk123
Copy link
Owner

lmk123 commented Nov 25, 2022

目前,我的项目没有用到 babel,因为我认为我的目标用户应该都有足够高的浏览器版本,所以我是直接使用 tsc 来编译 ts 代码的,且 tsconfig 的 target 设为了 ESNext,这意味着完全不会有 polyfill 打包进最终的生成的代码里。

但随着我使用的新特性越来越多,对浏览器版本的要求也越来越高,所以我还是决定为项目引入 babel。

TypesScript VS Babel

首先要了解直接用 TypesScript 编译(ts-loadertsc)和用 Babel 编译(babel-loader@babel/preset-typescript)的区别。

直接用 TypeScript 编译的话,TypeScript 做了两件事情:编译代码和检测类型,而用 Babel 编译的话,Babel 只负责编译代码,所以还需要额外使用 tsc --noemit(如果你还想生成声明文件的话,用 tsc --emitDeclarationOnly)命令来检测类型。

另外,用 Babel 编译代码的话,还需要在 tsconfig 中添加 "isolatedModules": true

有关这两者的差异可以查看 TypeScript 的官方文档:Using Babel with TypeScript

现在常见的做法是使用 Babel 编译代码、用 TypeScript 检测类型。@babel/preset-env 可以根据浏览器范围确定输出的代码,这比 TypeScript 自己的 target 选项要灵活的多。

项目背景

划词翻译使用了 monorepos 组织代码,在这个 monorepos 中,项目类型分为两种:lib(即模块)和 app(即实际需要运行起来的项目)。

lib 类型的项目使用了 rollup 来打包,且只提供 cjs / es 两种输出类型,也就是说,如果 app 要使用 lib,则必须使用 webpack 这类模块打包工具,不能直接通过 <script> 标签引入。

app 类型的项目使用了 webpack 来打包。

这次改造会针对这两种类型的项目进行。

对于 lib 类型的项目

lib 类型现在使用了 @rollup/plugin-typescript 来编译。

要想改为使用 babel 来编译代码的话,需要先添加一个 babel.config.js:

module.exports = {
  presets: [
    '@babel/preset-env',
    '@babel/preset-typescript',
  ],
}

然后将下面的这部分 rollup 配置:

import ts from '@rollup/plugin-typescript'

export default {
  plugins: [ts()],
}

改为:

import { nodeResolve } from '@rollup/plugin-node-resolve'
import babel from '@rollup/plugin-babel'
// 由于我的 lib 项目没有用到 commonjs 模块,所以不需要 commonjs 插件
// import commonjs from '@rollup/plugin-commonjs'

// 我的 lib 项目只用到了 .ts 文件
const extensions = ['.ts' /*, '.js', '.jsx', '.tsx'*/]

export default {
  plugins: [nodeResolve({ extensions }), babel({ extensions })],
}

然后就能正常编译了。

@babel/runtime 的问题

由于我们没有使用 browserslist 文件,也没有给 @babel/preset-env 指定 targets,所以 babel 默认将我们的代码转为了 es5 兼容代码,检查 babel 生成的文件的话,会发现 babel 注入了很多 runtime 代码(runtime 代码的介绍,类似于 TypeScript 里的 tslib)。

作为一个 lib 项目,我不希望 runtime 代码注入到最终生成的代码当中,现在我有两个选择:

一,将 runtime 代码作为模块的一个依赖

这样做的话,所有 lib 都可以从 @babel/runtime 模块导入 runtime 代码,能有效减少 app 最终的项目体积。

我参考了 @rollup/plugin-babel 的说明,做了一些改动。

首先是 rollup 配置:

import { nodeResolve } from '@rollup/plugin-node-resolve'
import babel from '@rollup/plugin-babel'
const extensions = ['.ts']

export default {
  plugins: [nodeResolve({ extensions }), babel({ extensions, babelHelper: 'runtime' })],
  external: [/@babel\/runtime/]
}

然后 npm i -D @babel/plugin-transform-runtime 并将它加入 babel config 中:

module.exports = {
  presets: [
    '@babel/preset-env',
    '@babel/preset-typescript',
    '@babel/plugin-transform-runtime'
  ],
}

最后 npm i @babel/runtime 将它作为项目的依赖。

这样就完成配置了,运行 rollup,它报错了

[!] (plugin babel) Error: Cannot find package '@babel/preset-plugin-transform-runtime' imported from workspaces/packages/mylib/babel-virtual-resolve-base.js

这个错报的很奇怪,因为 babel config 里写的明明是 @babel/plugin-transform-runtime,但 rollup babel 插件却在读取 @babel/preset-plugin-transform-runtime

我做了下面三种尝试,均未解决此问题:

  • 加入 commonjs 插件
  • 给 babel 插件添加 skipPreflightCheck: true 配置(来源
  • 给 node resolve 插件添加 rootDir 配置,将它设为 workspaces 的根目录:rootDir: path.join(process.cwd(), '../..')

但均无效。

所以目前来看,这个方法是行不通了,等下次我再试试看要怎么解决这个问题。

二,不要注入任何 runtime 代码,由 app 负责注入

lib 生成的代码不添加任何 runtime:原样保留 async / awaitString.prototype.matchAll 等现代浏览器才支持的写法,然后当有 app 使用这个 lib 时,由 app 负责注入。

即使不是因为上面的报错,我也更倾向于这种做法,毕竟不同 app 对浏览器的支持要求不尽相同:面向 C 端用户的网站可能要尽可能兼容老浏览器,但企业内部网站、基于 Electron 的应用就不需要这么严格,使用固定的 browserslist 配置无法满足所有项目的要求。

如果使用这种方式,lib 只需要将 TypeScript 的 target 设为 ESNext,然后直接用 TypeScript 编译即可,但 app 需要做一些额外配置。

以在 Webpack 里用到的 babel-loader 为例,为了加快 babel-loader 速度,我们一般会 exclude: /node_modules/,即告诉 babel 不要处理 node_modules 里的代码,但如果我们需要 babel 来处理 node_modules 里的一些代码的时候,就需要这么写了:

(以下配置来自 (babel-loader 项目主页)[https://www.npmjs.com/package/babel-loader]的“Some files in my node_modules are not transpiled for ie 11” 一节)

{
  test: /\.m?js$/,
    exclude: {
      and: [/node_modules/], // Exclude libraries in node_modules ...
      not: [
        // Except for a few of them that needs to be transpiled because they use modern syntax
        /unfetch/,
        /d3-array|d3-scale/,
        /@hapi[\\/]joi-date/,
      ]
    },
    use: {
      loader: 'babel-loader',
      options: {
        presets: [
          ['@babel/preset-env', { targets: "ie 11" }]
        ]
      }
    }
}

对于 app 类型的项目

需要做如下改动:

  • 添加一个 .browserslistrc 文件来确定要支持的浏览器范围。
  • 给 tsconfig 添加 "isolatedModules": true
  • 添加 babel config 文件
  • 将 webpack 配置里的 ts-loader 替换为 babel-loader

babel.config.js 文件内容:

module.exports = {
  presets: [
    [
      '@babel/preset-env',
        // 测试代码时只需要满足当前 Node.js 就行了
      process.env.NODE_ENV === 'test'
        ? { targets: { node: 'current' } }
        : { bugfixes: true },
    ],
    [
      '@babel/preset-react',
      {
          // https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html
        runtime: 'automatic',
      },
    ],
    '@babel/preset-typescript',
  ],
}

webpack.config.js 中 babel-loader 相关配置:

{
  test: /\.(tsx?|jsx?|mjs|cjs|js)$/,
  exclude: {
    and: [/node_modules/],
    not: [
      // 所有 @hcfyapp 域下的 node_modules 都要经过 babel 处理
      /@hcfyapp[\\/]/,
    ]
  },
  use: {
    loader: 'babel-loader',
    options: {
      cacheDirectory: true
    },
  },
}

然后运行 webpack,发现报了一个错:

Module build failed (from ../../node_modules/babel-loader/lib/index.js):
TypeError: Duplicate declaration "MyApp"

export default function MyApp() {

看了一下出错的文件,发现文件开头有这么一行代码:

import { MyApp } from './module'

但是这里 import 的 MyApp 是一个 TypeScript Interface,我猜测是启用了 isolatedModules 之后导致 TypeScript 没法判断这个 MyApp 是不是类型。不过这个问题也好解决,改个名字就可以了。

对改造结果进行确认

改完之后,webpack 就可以正常运行了,但是还有一些事情需要确认。

确认 React JSX Transform

React 17 引入了新的 JSX Transform,详情见官网介绍 Introducing the New JSX Transform

我要确保的就是:babel 在开发环境下引用的是 react/jsx-dev-runtime,在生产环境下引用的是 react/jsx-runtime

我确认的方法是使用一个 webpack 插件 Webpack Bundle Analyzer,完成打包后,这个插件会弹出来一个网页,包含 webpack 处理的所有模块的信息。

在启动了 webpack 的生产环境打包后,我搜了一下 react,就能看到我的代码里使用的是 react-jsx-runtime.production.min.js,这是符合预期的。

在启动 webpack 的开发环境后,搜到的是 react-jsx-runtime.development.js,理想状态是开发环境应该使用 react-jsx-dev-runtime.development.js

但神奇的是我找不到让 babel 引入 jsx-dev-runtime 的方法,谷歌搜到的都是对官方介绍的解读;@babel/preset-react 虽然有 development 选项,但是设为 true 之后引用的还是 jsx-runtime;@babel/plugin-transform-react-jsx的文档示例里用的也是 jsx-runtime。

在 TypeScript 里可以用 jsx 选项选择使用哪一个,但 babel 似乎无法做到,先放一放吧。

确认 polyfills 代码引用方式

polyfills 指的是由 core-js 提供的现代浏览器的特性如 PromiseString.prototype.includes 等。

给 @babel/preset-env 添加 debug: true,会打印出我们使用的所有插件和 polyfill,但看不到 runtime 代码的情况,先略过。

注意:只有当 webpack 以开发模式运行时才会打印出来这些信息。

由于没有给 @babel/preset-env 配置 useBuiltIns 选项,所以目前项目没有加入任何 polyfill。

我根据文档使用了 useBuiltIns: "entry", corejs: "3.26" 并安装了 core-js v3.26.1,然后就能在控制台看到每个文件使用到的 core-js 代码。

Webpack Bundle Analyzer 插件里也能搜到 core-js 的使用情况。

确认 runtime 代码引用方式

runtime 代码是指 babel 在转换语法时用到的辅助函数,例如 _extends

在 Webpack Bundle Analyzer 弹出的分析报告中搜索 @babel/runtime,能看到 babel 是统一从 @babel/runtime 里引用辅助函数的,例如 ./../node_modules/@babel/runtime/helpers/esm/extends.js,这是符合我的预期的。

换句话说,只要不是给每个文件都单独注入了类似 _extends 这样的辅助函数就行。

总结

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant