Create React App TypeScript 模板实战:一条命令创建 TS 版 React 项目,吃透四个核心脚本
本文以 cra-template-typescript 模板的 README 为主体展开。这份 README 是使用 --template typescript 创建项目后自动生成到项目根目录的 README.md,它向使用者说明了项目可用脚本(npm start、npm test、npm run build、npm run eject)的行为与边界。阅读本文后,你将掌握:如何用一条命令生成 TypeScript 版 React 应用、生成的四个脚本各自做什么、以及模板背后的依赖注入与 tsconfig.json 自动生成机制在 react-scripts 源码中是如何实现的。
这份 README 在项目生命周期中的位置
在 create-react-app 仓库中,packages/cra-template-typescript/template/README.md 并不是给模板包本身写的文档,而是脚手架复制进用户新项目的说明文件。当你在命令行执行:
npx create-react-app my-app --template typescript
脚手架会把 template/ 目录 下的全部内容(src、public、README.md、gitignore 等)复制到你的项目根目录,同时把项目里原有的 README.md 重命名为 README.old.md。这一行为由 init.js 中的以下逻辑保证:
const readmeExists = fs.existsSync(path.join(appPath, 'README.md'));
if (readmeExists) {
fs.renameSync(
path.join(appPath, 'README.md'),
path.join(appPath, 'README.old.md')
);
}
所以你在任何用 TypeScript 模板创建的项目根目录看到的 “Available Scripts” 文档,其源头就是这份模板 README。下面完整继承并展开它的核心内容。
创建 TypeScript 项目的正确姿势
README 开篇声明了项目来源:
This project was bootstrapped with Create React App.
对应到仓库源码,模板名到安装包名的解析在 createReactApp.js 的 getTemplateInstallPackage 函数中完成。几个实用规则:
--template typescript会自动补全为 npm 包cra-template-typescript;- 带前缀的名称(如
cra-template-typescript本身)会原样使用; - 也支持本地路径
file:../my-custom-template和.tgz/.tar.gz归档,便于团队内私有模板。
从源码结构看,模板机制有一个版本前提:createReactApp.js 要求 react-scripts 版本不低于 3.3.0 才会把模板包加入依赖列表,否则打印兼容性警告。因此模板方案依赖较新版本的 react-scripts,当前仓库中的版本自然满足该条件。
创建完成后,脚手架还会在 init.js 中检测安装参数里是否包含 typescript,若有则触发 verifyTypeScriptSetup()(详见下文),确保 tsconfig.json 被正确生成和校准。
Available Scripts:四个脚本逐项解析
这是模板 README 的主体内容,完整覆盖如下四个脚本。说明中“在浏览器中打开 http://localhost:3000”“构建输出到 build 文件夹”等行为,分别由 react-scripts 的 start.js、test.js、eject.js 实现,仓库文档 available-scripts.md 对它们有更长的描述。
npm start
Runs the app in the development mode. Open http://localhost:3000 to view it in the browser. The page will reload if you make edits. You will also see any lint errors in the console.
要点:
- 开发服务器默认监听
localhost:3000; - 保存文件触发页面热更新(页面 reload);
- ESLint 报错会实时输出到控制台——这一点由模板写入
package.json的eslintConfig支撑:template.json 中声明的"extends": ["react-app", "react-app/jest"]覆盖了开发模式与测试模式两套规则集,实现见 eslint-config-react-app 与 jest.js。
npm test
Launches the test runner in the interactive watch mode. See the section about running tests for more information.
测试运行器以交互式 watch 模式启动,默认使用 Jest,配合 @testing-library/react 做组件渲染断言。模板自带的示例测试 App.test.tsx 展示了最小可用的 TS 测试写法:
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
其中 toBeInTheDocument() 这个自定义 matcher 来自 setupTests.ts 中导入的 @testing-library/jest-dom。测试相关的更多用法见仓库文档 running-tests.md。
npm run build
Builds the app for production to the
buildfolder. It correctly bundles React in production mode and optimizes the build for the best performance. The build is minified and the filenames include the hashes. Your app is ready to be deployed!
要点:
- 产物输出到
build/目录(这正是模板 gitignore 中/build与/coverage被忽略的原因); - 生产模式下自动启用代码压缩,文件名带内容哈希,便于长期缓存;
- 部署细节参见仓库文档 deployment.md。
npm run eject
Note: this is a one-way operation. Once you
eject, you can't go back!
README 对此的完整表述值得保留:如果对构建工具与配置不满意,可以随时 eject——该命令会把单一构建依赖 react-scripts 从项目中移除,转而把全部配置文件与传递性依赖(webpack、Babel、ESLint 等)直接复制进项目,让你获得完全控制权;除 eject 外的其余命令继续可用,但指向被复制出来的脚本,此后“on your own”。README 同时强调:你并不被强迫使用 eject,精选的特性集对中小型项目已经够用。
模板文件结构:TypeScript 项目里多了什么
对照 template 目录,TypeScript 模板相比 JavaScript 模板的差异集中在 src/ 下的文件扩展名与类型声明上:
| 文件 | 作用 |
|---|---|
| index.tsx | 入口,使用 ReactDOM.createRoot 挂载 <App />,注意 document.getElementById('root') as HTMLElement 的类型断言 |
| App.tsx | 根组件,提示 “Edit src/App.tsx and save to reload” |
| App.test.tsx | 基于 Testing Library 的默认测试 |
| setupTests.ts | 全局导入 @testing-library/jest-dom,扩展 Jest matcher |
| reportWebVitals.ts | Web Vitals 上报钩子,ReportHandler 类型显式标注,未传回调时不做任何上报 |
| gitignore | 忽略 node_modules、coverage、build 及各类 .env.*.local |
| public/ | index.html、PWA 用的 manifest.json、logo192.png/logo512.png、robots.txt、favicon.ico |
入口文件 index.tsx 的完整逻辑:
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
reportWebVitals();
可以看到模板默认采用 React 18 的 createRoot API 并包裹 StrictMode,与 index.tsx 中“pass a function to log results”的注释一致:reportWebVitals 接受一个可选的 ReportHandler,只有传入回调时才动态 import('web-vitals') 并采集 CLS/FID/FCP/LCP/TTFB 五项指标。
template.json:模板依赖是如何被装进新项目的
模板包自身的 package.json 很精简(version: 1.3.0,files 只发布 template 与 template.json),真正的“模板配置”在 template.json:
{
"package": {
"dependencies": {
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.1.0",
"@testing-library/user-event": "^13.2.1",
"@types/jest": "^27.0.1",
"@types/node": "^16.7.13",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"typescript": "^4.4.2",
"web-vitals": "^2.1.0"
},
"eslintConfig": {
"extends": ["react-app", "react-app/jest"]
}
}
}
这些依赖并不是静态写死进每个新项目的,而是由 init.js 在初始化时动态合并:
- 通过
require.resolve(${templateName}/package.json)定位已安装的模板包,并读取其中的template.json(若存在根级dependencies/scripts旧式写法,会提示已在 CRA 5 中弃用,需改用package键); - 生成
scripts:默认写入start/build/test/eject四条指向react-scripts的脚本,再与模板自定义脚本合并; - 写入
eslintConfig: { extends: 'react-app' }后,用template.json中的eslintConfig整体替换(templatePackageToReplace机制),于是 TS 模板项目最终得到["react-app", "react-app/jest"]双规则集; - 若检测到你使用 Yarn(
yarn.lock存在),脚本中的npm run/npm会被批量替换为yarn,init.js 甚至会把刚复制过来的 README 内容里的npm字样一并替换成yarn——这解释了为什么同一份模板 README 在 Yarn 用户的项目里呈现为yarn start等写法; - 模板
template/目录被整体复制到项目根目录; - 随后执行
npm install(或yarn add)安装template.json列出的全部依赖,最后再remove templateName把模板包本身从项目中卸载(见 init.js)。
值得注意的版本细节:typescript 锁定在 ^4.4.2,而 @types/react 已经是 ^19.0.0——即模板以 TypeScript 4.x 作为最低基线,但保持 React 类型定义跟进最新大版本;这也是该模板 package.json 声明 engines.node >= 14 这一前提下的组合结果。
tsconfig.json 是自动“长出来”的
TypeScript 模板的 template/ 目录里并没有 tsconfig.json,它是在安装完成后由 verifyTypeScriptSetup.js 生成和校准的。该函数的行为可以概括为三类规则:
- 建议值(可改):
target: es5、lib: ["dom", "dom.iterable", "esnext"]、allowJs、skipLibCheck、esModuleInterop、allowSyntheticDefaultImports、strict、forceConsistentCasingInFileNames、noFallthroughCasesInSwitch——首次生成时以默认值填充,之后若你手动修改过则尊重你的值; - 强制值(不可改):
module: esnext(为import()与 import/export)、moduleResolution: node(与 webpack 解析对齐)、resolveJsonModule: true、isolatedModules: true、noEmit: true、jsx: react-jsx(React 17+ 新 JSX 转换,见 verifyTypeScriptSetup.js 中的compilerOptions表)、paths必须为undefined(不支持别名导入); - include:若未设置则补为
["src"]。
另外,若 src/react-app.d.ts 不存在,会自动写入 /// <reference types="react-scripts" />,让项目获得 react-scripts/lib/react-app.d.ts 提供的静态资源导入声明(如 import logo from './logo.svg' 能通过类型检查——App.tsx 里恰好依赖了这一点)。
还有一个反向检查:对于非 TypeScript 模板的项目,如果检测到 src/ 下出现了 .ts/.tsx 文件,verifyNoTypeScript 会自动创建 tsconfig.json 并给出黄色提示,等价于引导你补装 TypeScript 支持,对应仓库文档 adding-typescript.md 描述的渐进式迁移路径。
Learn More 与进一步阅读
模板 README 收尾的 “Learn More” 指向 CRA 文档与 React 官方文档。在本仓库中,与本文主题直接相关的文档有:
- available-scripts.md:四个脚本的完整行为说明(含环境变量、CI 模式等);
- adding-typescript.md:在已有项目里补装 TypeScript 的步骤;
- running-tests.md:Jest + Testing Library 的用法详解;
- code-splitting.md 与 advanced-configuration.md:理解何时真的需要
eject之外的替代方案(如 alternatives-to-ejecting.md)。
小结
packages/cra-template-typescript/template/README.md 这份看似简短的模板 README,实际上是一个 TypeScript 版 CRA 项目的“操作手册”:npm start 提供带热更新与 lint 反馈的开发服务器,npm test 启动交互式测试,npm run build 产出带哈希的压缩构建,npm run eject 提供不可逆的完全控制出口。而支撑这些脚本开箱即用的,是 template.json 声明的依赖清单、init.js 的合并/安装/卸载流水线,以及 verifyTypeScriptSetup.js 对 tsconfig.json 的自动校准——三者共同保证了一条命令生成的 TypeScript 项目从第一天起就可编译、可测试、可构建。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00