{
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" }]
]
}
}
}
目前,我的项目没有用到 babel,因为我认为我的目标用户应该都有足够高的浏览器版本,所以我是直接使用 tsc 来编译 ts 代码的,且 tsconfig 的 target 设为了
ESNext
,这意味着完全不会有 polyfill 打包进最终的生成的代码里。但随着我使用的新特性越来越多,对浏览器版本的要求也越来越高,所以我还是决定为项目引入 babel。
TypesScript VS Babel
首先要了解直接用 TypesScript 编译(
ts-loader
、tsc
)和用 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:
然后将下面的这部分 rollup 配置:
改为:
然后就能正常编译了。
@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 配置:
然后
npm i -D @babel/plugin-transform-runtime
并将它加入 babel config 中:最后
npm i @babel/runtime
将它作为项目的依赖。这样就完成配置了,运行 rollup,它报错了:
这个错报的很奇怪,因为 babel config 里写的明明是
@babel/plugin-transform-runtime
,但 rollup babel 插件却在读取@babel/preset-plugin-transform-runtime
。我做了下面三种尝试,均未解决此问题:
skipPreflightCheck: true
配置(来源)rootDir: path.join(process.cwd(), '../..')
但均无效。
所以目前来看,这个方法是行不通了,等下次我再试试看要怎么解决这个问题。
二,不要注入任何 runtime 代码,由 app 负责注入
lib 生成的代码不添加任何 runtime:原样保留
async / await
、String.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” 一节)
对于 app 类型的项目
需要做如下改动:
.browserslistrc
文件来确定要支持的浏览器范围。"isolatedModules": true
babel.config.js 文件内容:
webpack.config.js 中 babel-loader 相关配置:
然后运行 webpack,发现报了一个错:
看了一下出错的文件,发现文件开头有这么一行代码:
但是这里 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 提供的现代浏览器的特性如
Promise
、String.prototype.includes
等。给 @babel/preset-env 添加
debug: true
,会打印出我们使用的所有插件和 polyfill,但看不到 runtime 代码的情况,先略过。由于没有给 @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
这样的辅助函数就行。总结
无