首页 / 文章 / 可用于生产的 React 基线:每个包的实际功能是什么

可用于生产的 React 基线:每个包的实际功能是什么

为 React 应用安装 Vite、Tailwind v4、Redux Toolkit、React Router、Jest 和 Prettier,并了解每个插件及配置项的存在原因。

3165 词

运行 npm create vite 可以生成一个能够渲染的 React 应用,但它并非可直接交付给真实用户的版本:没有样式系统、没有共享状态、没有路由功能、没有测试用例,也没有统一的代码格式。本指南将逐步利用 Tailwind CSS、Redux Toolkit、React Router、Jest 配合 React Testing Library 以及 Prettier 来构建这些缺失的基础组件。对于每一个工具包,都会解答两个问题:它的实际功能是什么?如果省略它会导致什么问题?完成学习后,你将拥有一个可用于开发功能的可用基础框架,更重要的是,你能够理解自己的 package.json 文件并解释其中的每一行代码。

技术栈概览:

  • Tailwind CSS:用于样式设计
  • Redux Toolkit:用于管理应用共享数据
  • React Router:用于页面导航
  • JestReact Testing Library 以及一套简单的 Babel 工具链,以确保测试能够运行
  • Prettier,让代码格式化不再因个人喜好而不同
  • 其中一些工具只需一行命令即可安装。另一些则隐藏着令人意想不到的细节;例如,“React Testing Library”实际上是由三个承担不同功能的包组成的。

    建议使用 React + TypeScript 模板创建一个全新的 Vite 项目:

    npm create vite@latest react-production-stack -- --template react-ts
    cd react-production-stack
    npm install
    

    Tailwind CSS:先集成样式功能

    所需包: tailwindcss@tailwindcss/vite

    由于样式会影响到每个组件,因此在添加其他内容之前先确认其功能正常是很重要的。

    npm install tailwindcss @tailwindcss/vite
    

    这样会将这两个包作为普通依赖而非开发依赖进行安装。严格来说,这两个包都不会在浏览器中运行:Vite 插件仅在构建时执行任务,最终只有生成的 CSS 会进入生产环境打包文件。因此许多团队会将它们归类为开发依赖,而对于打包后的单页应用来说,无论选择哪种方式都会得到相同的输出。请选定一种规范并始终如一地遵循。

    接下来,在 Vite 配置中与 React 插件一同注册该插件:

    import { defineConfig } from 'vite'
    import react from '@vitejs/plugin-react'
    import tailwindcss from '@tailwindcss/vite'
    
    export default defineConfig({
      plugins: [react(), tailwindcss()],
    })
    

    然后用一个导入语句替换 src/index.css 文件中的内容。整个文件的内容如下:

    /* Tailwind v4 is CSS-first. No config file, no content globs. */
    @import 'tailwindcss';
    

    实际上设置就这些了。Tailwind v4 采用 CSS 首先的架构:没有 tailwind.config.js 文件,也没有内容匹配列表,因为它会自动扫描源文件中的类名。

    验证其功能

    App.tsx 中的标题上暂时添加一些实用类,例如 text-3xl font-bold text-blue-600,然后运行 npm run dev,查看标题是否发生变化。如果发生了变化,说明插件与 CSS 导入已成功关联。

    为何选择实用类而非独立的样式表

    Tailwind 将样式直接放在其影响的标记上。使用独立的 CSS 文件时,很容易在编辑组件时忘记对应的样式表,从而导致大量过时且无用的规则逐渐积累。尤其是控制面板,会多次重复使用相同的组件元素(卡片、徽章、按钮),通过一套通用的实用类来组合这些元素,既能保持视觉一致性,又能减少维护代码量。相应的代价是需要改变习惯:不再自行创建诸如 .card-header-active 这样的类名,而是用预先定义的小类来组合每个元素。

    Redux Toolkit:状态存储与 React 的桥梁

    相关包: @reduxjs/toolkitreact-redux

    这两个包很容易被混淆,但它们的功能各不相同:

    • @reduxjs/toolkit 就是状态存储本身:它负责保存应用程序数据并对其进行更新。
    • react-redux 则是与 React 的连接方式:它提供了 <Provider> 以及钩子组件,用于读取和更新这些数据。

    两者都是必需的,因为单独使用任何一个都无法完成另一项功能。

    npm install @reduxjs/toolkit react-redux
    

    src/app/store.ts 中创建状态存储。该文件以一个空的 reducer 映射表开始,并导出两种从状态存储派生出的类型,这样应用程序的其他部分就无需手动定义它们了:

    import { configureStore } from '@reduxjs/toolkit'
    
    export const store = configureStore({
      reducer: {},
    })
    
    export type RootState = ReturnType<typeof store.getState>
    export type AppDispatch = typeof store.dispatch
    

    目前 reducer: {} 对象仍是空的。只有当真正需要功能时,比如处理项目或任务时,才会添加相关切片;在有任何界面使用之前就创建状态并无意义。

    接着在 src/app/hooks.ts 中定义类型化的钩子函数。近期版本的 React Redux 提供的 withTypes 工具可以一次性将 useDispatchuseSelector 与存储的类型绑定,这样组件就能自动获得完整的类型推断,无需为每次调用都手动添加注解:

    import { useDispatch, useSelector } from 'react-redux'
    import type { AppDispatch, RootState } from './store'
    
    export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
    export const useAppSelector = useSelector.withTypes<RootState>()
    

    将存储传递给组件树

    此时存储已经存在,但 React 还不知道它的存在。<Provider> 可以让其被下层的所有组件使用,因此需要将其放在 src/main.tsx 中的组件树最顶层:

    import { StrictMode } from 'react'
    import { createRoot } from 'react-dom/client'
    import { Provider } from 'react-redux'
    import { store } from './app/store'
    import App from './App'
    import './index.css'
    
    createRoot(document.getElementById('root')!).render(
      <StrictMode>
        <Provider store={store}>
          <App />
        </Provider>
      </StrictMode>,
    )
    

    现在,<Provider>内部渲染的任何内容都可以调用useAppSelectoruseAppDispatch

    验证其功能

    启动应用,确认页面仍能正常渲染,不会出现“无法找到react-redux上下文”的错误。只要组件在Provider之外使用Redux钩子,就会出现该错误。由于存储为空,目前还没有其他内容可以测试。

    何时该使用Redux,何时仅用useState就足够

    并非所有内容都适合放入Redux中,将所有状态都存入存储与将所有状态保留在本地一样是错误的。一个实用的准则是:

    • useState适用于仅对单个屏幕或组件重要的数据:比如模态框是否打开、输入框的当前值、下拉菜单中选中的选项等。
  • Redux适用于多个屏幕或组件同时需要的数据,比如在多个页面上显示的任务列表,或是同时在控制面板、任务列表和任务详情页出现的单个任务。
  • 如果某个状态需要通过多层传递或在不同屏幕间重复使用,那它就很适合存放在存储中。

    React Router:在第一个实际页面之前进行路由配置

    包名: react-router

    在还没有实际页面时就添加路由配置似乎为时过早,但实际上很快就能看到好处:每个新屏幕只需新增一个<Route>,而无需日后再对应用结构进行大规模调整。

    npm install react-router
    

    将路由表放在独立的模块中,即 src/routes/AppRoutes.tsx。目前它将 / 映射到使用 Tailwind 工具类进行样式的占位组件:

    import { Route, Routes } from 'react-router'
    
    function Placeholder() {
      return (
        <div className="flex min-h-screen items-center justify-center">
          <p className="text-slate-600">Routes coming soon</p>
        </div>
      )
    }
    
    export function AppRoutes() {
      return (
        <Routes>
          <Route path="/" element={<Placeholder />} />
        </Routes>
      )
    }
    

    src/App.tsx 会直接渲染该路由表:

    import { AppRoutes } from './routes/AppRoutes'
    
    function App() {
      return <AppRoutes />
    }
    
    export default App
    

    最后,在 src/main.tsx 中,将 Redux 提供器与 <BrowserRouter> 放在一起包裹整个应用:

    import { StrictMode } from 'react'
    import { createRoot } from 'react-dom/client'
    import { Provider } from 'react-redux'
    import { BrowserRouter } from 'react-router'
    import { store } from './app/store'
    import App from './App'
    import './index.css'
    
    createRoot(document.getElementById('root')!).render(
      <StrictMode>
        <Provider store={store}>
          <BrowserRouter>
            <App />
          </BrowserRouter>
        </Provider>
      </StrictMode>,
    )
    

    最终的调用链为 main.tsx<App /><AppRoutes /> → 与 URL 匹配的任意 <Route>。Redux 和路由器是相互独立的,因此它们的嵌套顺序并不重要;唯一的要求是两者都必须包裹 <App>

    验证其功能正常

    运行 npm run dev 并打开 / 页面。如果出现了占位文本 <BrowserRouter><Routes><Route>,说明它们的连接都是正确的。

    Jest 与 React Testing Library:四项任务,十一个包

    相关包: jest@testing-library/reactbabel-jest 以及其他几个包

    这是耗时最长的步骤。相关概念本身并不复杂,但“添加测试”实际上意味着需要安装大约十一个包来承担四项不同的功能,然后还要在路由配置完成后让某个测试通过。将这些包按任务分类后,整个流程会清晰许多。

    A组:测试运行器与模拟浏览器

    npm install -D jest jest-environment-jsdom
    
    • jest是测试运行器。它会查找*.test.tsx文件,执行这些文件并报告测试通过与失败的情况。没有它,本节中的其他功能都无法正常工作。
    • 需要jest-environment-jsdom是因为Jest在Node环境中运行,而该环境中不存在document对象。它提供了模拟的DOM环境,以便组件有地方进行渲染。

    B组:React Testing Library由三个包组成

    npm install -D @testing-library/react
    npm install -D @testing-library/jest-dom
    npm install -D @testing-library/user-event
    

    人们所说的“React Testing Library”实际上是由三个独立的库构成的,每个库都有其特定的功能:

    • @testing-library/react负责将组件渲染到模拟的页面中,并提供诸如screen.getByText(...)之类的查询方法。
  • @testing-library/jest-dom 提供了如 toBeInTheDocument() 这样易于理解的匹配器,因此无需手动将查询结果与 null 进行比较。
  • @testing-library/user-event 能模拟真实用户行为。输入内容时会触发浏览器会产生的完整聚焦、按键按下、输入及按键释放序列,而非向元素发送单个合成事件。
  • 简而言之:渲染、断言、交互。三项功能,三个插件,而且几乎总需要全部使用。

    C组:让Jest能够读取TSX文件的Babel工具链

    设立这一组的唯一原因是:Jest本身无法理解TypeScript或JSX文件。

    • babel-jest 负责连接二者。Jest会在执行文件前先将其通过Babel处理。
  • @babel/preset-typescript 会移除类型注解。它不会进行任何类型检查,只是简单地删除 : string 及类似的语法。
  • @babel/preset-react 会将 JSX 编译为普通的函数调用。
  • @babel/preset-env 会将现代语法转换为你的 Node 版本所支持的格式。
  • 与 B 组不同,这些预设需要通过一个命令一起安装。所有预设都要求有兼容的 @babel/core,如果在已经包含 Jest(它会自带 Babel 依赖)的项目中逐个安装,npm 就可能试图协调不匹配的版本。常见的现象是在下次单独安装某个包时出现 ERESOLVE unable to resolve dependency tree 错误。一次性安装整个组可以让 npm 解决版本一致性问题。

    另一个更为隐蔽的陷阱来自从PDF或网页中复制长命令。软换行的文本在粘贴后会变成真正的换行符,因此像 @babel/preset-typescript 这样的包名会被拆分成两部分,shell会将后半部分作为独立的、无意义的命令来执行。而显式的换行则能让换行符出现在你期望的位置。以下示例使用的是Windows命令提示符语法:

    npm install -D babel-jest ^
      @babel/core ^
      @babel/preset-env ^
      @babel/preset-react ^
      @babel/preset-typescript
    

    末尾的 ^ 表示命令会在下一行继续。在PowerShell中,换行符是反引号,而在bash或zsh中则是反斜杠。无论使用哪种shell,这仍然只是一个 npm install 命令。

    D组:仅供编辑器使用的类型

    npm install -D @types/jest
    

    该包不会影响测试的运行方式;因为到那时 Babel 已经移除了所有类型信息。它的存在是为了让 TypeScript 和编辑器能够识别 test(...)expect(...) 这样的全局变量,而不会将其标记为错误。

    添加测试脚本

    安装 Jest 后并不会自动生成 npm test 命令,因此需要手动在 package.json 中添加相关脚本:

    "scripts": {
      "dev": "vite",
      "build": "tsc -b && vite build",
      "lint": "eslint .",
      "test": "jest",
      "test:watch": "jest --watch"
    }
    

    npm test 会一次性运行全部测试用例。npm run test:watch 则会持续运行,仅重新执行你刚保存的文件所涉及的测试用例;工作时可在另一个终端中保持该命令处于运行状态。

    当出现问题时,还有两个命令值得记住:

    npx jest src/App.test.tsx   # run one file only
    npx jest --clearCache       # when Jest keeps showing an error
                                # you already fixed
    

    缓存命令的重要性远超表面所见。Jest会缓存处理后的文件,因此当你修改了babel.config.cjsjest.config.cjs后,它仍可能继续提供旧的输出结果,并报出你已经解决的错误。如果修复似乎不起作用,先清除缓存,再判断该修复是否确实有误。

    完整的测试配置

    以下是所有配置文件的完整内容,同时说明了每个部分的职责。

    babel.config.cjs

    这些预设与C组相同:针对当前Node版本,使用自动JSX运行时以便文件无需导入React,并移除TypeScript。内联插件能处理Jest无法处理的内容:import.meta,Vite代码会用它来实现诸如import.meta.env和热模块替换等功能,但在Jest运行的CommonJS输出环境中该语法无效。

    function stripImportMeta() {
      return {
        visitor: {
          MetaProperty(path) {
            path.replaceWithSourceString('({ url: "", hot: undefined })')
          },
        },
      }
    }
    
    module.exports = {
      presets: [
        ['@babel/preset-env', { targets: { node: 'current' } }],
        ['@babel/preset-react', { runtime: 'automatic' }],
        '@babel/preset-typescript',
      ],
      plugins: [stripImportMeta],
    }
    

    这是一个真正的 Babel 插件,以内联函数的形式实现而非作为已安装的包;Babel 可以接受这两种形式。MetaProperty 是 Babel 用于表示 import.meta 的 AST 节点类型,该插件会将所有 MetaProperty 实例替换为普通的对象,这类对象的 url 属性为空,hot 属性未定义。需要注意的是,这也会隐藏被测试代码中的任何 import.meta.env 值,因此需要读取环境变量的组件必须单独进行模拟。

    jest.config.cjs

    该文件将运行器与其他所有组件关联起来。它选择 jsdom 环境,在环境准备就绪后加载配置文件,将所有的 JavaScript 和 TypeScript 文件通过 babel-jest 处理,并将样式和图片导入映射为虚拟模块。

    module.exports = {
      testEnvironment: 'jsdom',
      setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
      moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'mjs', 'json'],
      transform: {
        '^.+\\.(ts|tsx|js|jsx|mjs)
    : 'babel-jest', }, transformIgnorePatterns: ['node_modules/(?!(react-router|cookie-es)/)'], moduleNameMapper: { '\\.(css|less|scss|sass)
    : '<rootDir>/test/styleMock.js', '\\.(png|jpg|jpeg|gif|svg|webp)
    : '<rootDir>/test/fileMock.js', }, }

    transformIgnorePatterns 这一配置值得注意。默认情况下,Jest 不会转换 node_modules 内的任何文件。通过负向前瞻断言,可以为 react-routercookie-es 开出例外,因为这两个库是以 ES 模块形式发布的,而 Jest 的 CommonJS 处理流程无法直接加载未经转换的模块。如果后续又添加了其他仅支持 ESM 的依赖,出现 SyntaxError: Cannot use import statement outside a module 错误,就可以将对应的模式添加到这里。

    jest.setup.ts

    该配置文件会注册 jest-dom 的匹配器,并将 TextEncoderTextDecoder 添加到全局作用域中。jsdom 环境并不提供这些接口,而 React Router 需要它们存在,因此这些接口是从 Node 的 node:util 模块中引入的:

    import { TextEncoder, TextDecoder } from 'node:util'
    import '@testing-library/jest-dom'
    
    Object.assign(globalThis, { TextEncoder, TextDecoder })
    

    样式与文件模拟

    Jest 不知道如何导入 CSS 文件或 PNG 图片。下面的两个虚拟模块就是 moduleNameMapper 用于重定向这些导入的地方,因此包含 import './App.css' 的组件不会导致测试运行失败:

    // test/styleMock.js
    module.exports = {}
    
    // test/fileMock.js
    module.exports = 'test-file-stub'
    

    src/App.test.tsx

    最后是测试整个配置的用例。它在 MemoryRouter 中渲染 App,该路由器将路由状态保存在内存中而非读取真实的浏览器 URL,并验证占位文本是否存在:

    import { render, screen } from '@testing-library/react'
    import { MemoryRouter } from 'react-router'
    import App from './App'
    
    test('renders the placeholder route content', () => {
      render(
        <MemoryRouter>
          <App />
        </MemoryRouter>,
      )
      expect(screen.getByText(/routes coming soon/i)).toBeInTheDocument()
    })
    

    尽管规模很小,但这个测试涉及了所有相关组件:TSX 编译、ESM 路由器包、文本编码填充函数、import.meta 的处理方式以及 jest-dom 匹配器。如果该测试通过,说明整体配置是正确的。

    Prettier:终结格式化争论

    包: prettiereslint-config-prettier

    npm install -D prettier eslint-config-prettier
    
    • prettier 会在代码保存时将其格式化为统一风格。
    • eslint-config-prettier 的功能十分单一:它用于关闭与 Prettier 格式规则冲突的 ESLint 规则,比如关于引号、分号和尾随逗号的规则。

    第二个包本身不添加任何规则,只是防止这两个工具产生冲突。在 eslint.config.js 中,它必须是配置列表中的最后一个条目,因为后面的条目会覆盖前面的条目,而它需要覆盖其他条目而非被它们覆盖。

    为何值得额外安装这个包

    如果没有自动格式化功能,代码审查时人们会更多地关注制表符与空格的使用而非逻辑结构。Prettier故意只提供少量选项,这样就几乎无需争论,而这正是它的设计目的。

    整合所有组件

    还有一个问题需要解决。tsc -b会对src目录下的所有文件进行类型检查,包括测试文件,但默认情况下它并不了解testexpect的相关信息。需要在tsconfig.app.jsontypes数组中添加相应的类型包:

    "types": ["vite/client", "jest", "@testing-library/jest-dom"]
    

    之后按照成本从低到高的顺序运行完整的验证流程:

    npm run lint    # fast, catches obvious mistakes
    npm test        # fast, catches broken behaviour
    npm run build   # slower — real compile, real Tailwind output
    npm run dev     # slowest — but the only one that proves it renders
    

    代码检查速度快,能发现明显的错误;测试可验证程序行为;构建过程会进行实际编译并生成真正的Tailwind输出;而开发服务器虽然检查速度最慢,却是唯一能证明应用能在浏览器中正常显示的工具。当这四项检查全部通过时,即便还没有编写任何功能代码,项目的基础部分就已经完成了。

    关于替代方案的说明

    Jest 部分的内容(如 Babel 预设、import.meta 插件、ESM 异常处理)之所以存在,是因为 Jest 并不使用 Vite 的构建流程。如果您希望减少组件数量,Vitest 可以复用您的 Vite 配置,并且能兼容相同的 Testing Library 包。如需了解另一种简化测试工具的方法,请参阅我们的指南 用 Node 的原生测试运行器替代 Jest。当您的团队已经熟悉 Jest 或依赖其生态系统时,上述的 Jest 设置依然是一个可靠的选择。

    关键要点

    • 将每个依赖项视为一个决策:明确它的功能以及缺少它时会出现什么问题。
    • Tailwind v4 仅需 Vite 插件和一个 CSS 导入,无需配置文件。
  • Redux Toolkit 负责存储数据,React Redux 则将其与组件相连;类型化的钩子能让代码更整洁,而本地 UI 状态仍应使用 useState 来管理。
  • 尽早引入路由器,之后添加新页面只需进行一行代码修改即可。
  • 在 Vite 项目中使用 Jest 需要一个运行器、DOM 环境、三个 Testing Library 包、Babel 工具链以及一些针对性的配置调整;如果某些调整似乎未被应用,可清除 Jest 缓存。
  • 可通过单个命令安装相互依赖的包,当命令内容跨越多行时需使用显式的换行方式。
  • 相关阅读