TSX 到 JavaScript 的完整旅程
对于很多开发人员来说,可能仅仅学习了框架的各种方法的使用,但是并没有关心代码的流转。
比如说React中的TSX/JSX文件是怎么转变成浏览器认识的JS文件的,VUE里面的vue文件是如何转变的。
在这里,介绍下TSX文件的流转。至于vue文件,这个后续再谈论。
只记一句话:TSX 不是浏览器能直接执行的语法,必须经过编译和构建。
先从一个直觉问题开始
你写的是:
const Button = ({ label }: { label: string }) => <button>{label}</button>;
浏览器最终执行的是:
const Button = ({ label }) => React.createElement('button', null, label);
或者在新 JSX Transform 下:
import { jsx as _jsx } from 'react/jsx-runtime';
const Button = ({ label }) => _jsx('button', { children: label });
核心结论:TSX -> JS 不是一步魔法,而是由 TypeScript、Babel、Bundler 共同完成的一条流水线。
项目内的主要文件
src/App.tsx TSX + React 常见写法
src/ES6Features.tsx class/箭头函数/可选链/空值合并 等 ES6+ 语法
主要命令在 package.json:
"step1:tsx-to-jsx": "tsc --jsx preserve",
"step2:jsx-to-js": "babel dist-step1 --out-dir dist-step2 --extensions '.jsx'",
"step3:direct-compile": "tsc --project tsconfig.step3.json",
"step3:new-jsx": "tsc --project tsconfig.new-jsx.json",
"step4:es6-to-es5": "tsc src/ES6Features.tsx ... && babel dist-es6 --out-dir dist-es5 --config-file ./.babelrc.es5"
第一层:TSX 先去类型,再决定是否保留 JSX
Step 1: npm run step1:tsx-to-jsx
作用:
- 移除 TypeScript 类型
- 保留 JSX(输出
.jsx) - 生成声明文件
.d.ts
查看产物:
cat dist-step1/App.jsx
能明确看到“类型没了,但 JSX 还在”。
第二层:把 JSX 真正变成函数调用
Step 2: npm run step2:jsx-to-js
作用:Babel 读取上一步的 JSX,转换为可执行 JS。
查看产物:
cat dist-step2/App.js
你会看到典型的 _jsx / _jsxs(来自 react/jsx-runtime)。
第三层:TypeScript 也可以一步到位做 JSX 转换
传统模式(React.createElement)
命令:
npm run step3:direct-compile
配置在 tsconfig.step3.json:
"jsx": "react"
查看:
cat dist-step3/App.js
新模式(React 17+ JSX Transform)
命令:
npm run step3:new-jsx
配置在 tsconfig.new-jsx.json:
"jsx": "react-jsx"
查看:
cat dist-new-jsx/App.js
- 在 React 17 后出现了
jsx这个方法,但是React.createElement也得到了保留 - 两种输出都可以,取决于你的编译配置
react-jsx通常更现代,也不用在每个文件手动import React
第四层:为什么还要处理 class、箭头函数、可选链
上面步骤主要解决的是 TypeScript 和 JSX。问题是,ES6+ 语法在老环境不一定可运行。
例如这些语法:
class- 箭头函数
() => {} - 可选链
a?.b?.c - 空值合并
x ?? y async/await
这时候要用 @babel/preset-env 做“按目标环境降级”。
Step 4: npm run step4:es6-to-es5
它做两件事:
- 先用
tsc把ES6Features.tsx变成dist-es6/ES6Features.js(仍保留较新语法) - 再用 Babel +
@babel/preset-env变成dist-es5/ES6Features.js
命令:
cat dist-es6/ES6Features.js
cat dist-es5/ES6Features.js
dist-es6/ES6Features.js 中间产物说明
这个中间产物是 TypeScript 编译的直接输出,保留了以下 ES6+ 语法特性:
- ✅
class语法(ES6) - ✅ 箭头函数
() => {}(ES6) - ✅
async/await语法(ES2017) - ✅ 可选链
?.(ES2020) - ✅ 空值合并
??(ES2020) - ✅
const/let声明(ES6) - ✅ 模板字符串(ES6)
- ✅ 解构赋值(ES6)
- ✅ 展开运算符(ES6)
- ✅ Promise(ES6)
这一步只负责去掉 TypeScript 类型,不做进一步的语法降级。假如你直接在现代浏览器(Node 14+)运行它,能正常执行。
dist-es5/ES6Features.js 最终产物说明
这个文件经过 Babel 转换,所有 ES6+ 语法都被改写成 ES5:
你会直观看到:
class被改写成函数和辅助方法- 箭头函数被改写为普通函数
- 可选链被展开为兼容写法
async/await被改写为 Promise 链或生成器
这样做的代价是代码体积增大(需要更多 helper 函数),但优势是兼容老浏览器(IE11、旧版 Android 等)。
如何选择
- 现代项目:用
dist-es6即可(size 小、加载快) - 需要兼容老环境:用
dist-es5(多了 polyfill 和 helper,但兼容性好)
打包阶段:Webpack
命令:
npm run build:webpack
输出:
ls -la dist-webpack/
Webpack 负责把模块组织成浏览器可加载的 bundle,并串联 loader。语法转换能力来自 ts-loader / babel-loader 和它们的配置。
流转过程
TSX 源码
-> TypeScript: 去类型,转换成 JSX
-> Babel: 按目标环境降级 ES6+ 语法(可选)
-> Webpack: 打包模块、输出 bundle