利用pnpm+vite+react+eslint+react-router+antdesign搭建一个可以用于实际生产的功能完善的react项目

395 阅读4分钟

实现的功能

环境

  • node版本: v18.13.0
  • pnpm版本: 9.0.6

前置知识(待补充链接)

  • pnpm、npm、yarm
  • vite、webpack

步骤

1. 初始化项目

pnpm create vite vite_react_project_template --template react

2. 安装依赖

pnpm install react-router-dom @ant-design/icons antd

3. 配置eslint和prettier

初始化项目的时候,已经内置了eslint,但是并没有prettier。

  • 我们打开初始化项目的/src/App.jsx文件,随便修改点错误语法,会发现已经有提示了!但是格式的错误并没有错误提示!

40B6168F-938C-4CF3-A9CE-21BC61EE198E.png

lQLPJx3lPwVV5nXNAU7NBZiwAlyVmkI8QmYGKzupRVskAA_1432_334.png

  • package.json里面也已经有了对应的依赖和执行命令

3A1124F3-016F-4B8F-8449-30248ABD0BB7.png

  • .eslintrc.cjs文件里也有了相关配置

24E878C7-6720-4A41-8750-828C69096630.png

现在来解决格式错误的高亮提示问题!

  1. 安装prettier

pnpm install prettier -D

2.安装 ESLint 插件

为了让 ESLint 和 Prettier 协同工作,你需要安装一些 ESLint 插件。以下是一些常用的插件:

  • eslint-plugin-prettier: 用于将 Prettier 的规则集成到 ESLint 中。
  • eslint-config-prettier: 用于关闭所有不必要的或可能与 Prettier 冲突的规则。

安装这些插件:

pnpm install eslint-plugin-prettier eslint-config-prettier -D

  1. 修改.eslintrc.js文件 修改后如下:

6E35D181-B2B0-4785-9898-72FB7B7BB018.png

  1. 修改package.json文件 修改后如下: E76847D9-8418-4718-ABD8-1FE7B5C047F5.png
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
  "prettier": {
    "semi": false,
    "singleQuote": true,
    "tabWidth": 2
  }
  1. 查看效果 此时再看/src/App.jsx文件,会发现错误的格式已经标注出来了

1C79A67F-23B7-4F15-9042-79314F7F86F8.png

ps:因为后面引入了typescript,所以现在配置的eslint不满足需求了,需要追加支持ts的插件

  • 安装插件 pnpm install @typescript-eslint/eslint-plugin @typescript-eslint/parser -D
  • 修改 .eslintrc.js
{ 
    "parser": "@typescript-eslint/parser",
    "plugins": ["@typescript-eslint"],
    "extends": [ 
        "plugin:@typescript-eslint/recommended" 
        // 可能还有其他的扩展配置 
    ], 
    "rules": { // 自定义规则 } }

修改完后如下:

1.png

4.配置typescipt

  • 安装 pnpm install typescript -D
  • 初始化 npx tsc --init 执行后会生成tsconfig.json文件,将文件内容改为
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "Node",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "sourceMap": true,
    "skipLibCheck": true,
    "isolatedModules": true,
    "allowJs": true,
    "lib": [
      "ESNext",
      "DOM"
    ],
    "baseUrl": ".",
    "jsx": "react",
    "outDir": ".",
    "paths": {
      "@src/*": [
        "./src/*"
      ],
    },
  },
  "exclude": ["*.js", "build", "env", "static"]
}
  • 重命名文件扩展名。将React组件从.js文件扩展名改为.tsx。例如,App.js应该改为App.tsx

问题解决: 引入ts后,会发现项目里面多了很多报错

  1. main.tsx文件里面的document.getElementById("root")飘红,并提示类型“HTMLElement | null”的参数不能赋给类型“Container”的参数。 不能将类型“null”分配给类型“Container”

62C60F08-EE3C-4F3D-B066-9556C5EFDA6E.png

原因: 这个错误是因为 ReactDOM.createRoot 方法期望一个有效的 DOM 元素作为参数,但是 document.getElementById("root") 可能返回 null,这意味着没有找到具有 ID 为 "root" 的元素。这通常发生在你的 HTML 文件中没有一个元素具有该 ID,或者你的脚本在 DOM 完全加载之前就运行了。 为了解决这个问题,你可以确保你的 HTML 文件中有一个具有 ID 为 "root" 的元素,或者你可以延迟执行你的脚本,直到 DOM 完全加载。

解决方法

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.js";
import "./index.css";

document.addEventListener('DOMContentLoaded', (event) => {
  const rootElement = document.getElementById('root');
  if (rootElement) {
    ReactDOM.createRoot(rootElement).render(
      <React.StrictMode>
        <App />
      </React.StrictMode>
    );
  }
});

5. 测试路由功能

  • 调整项目结构

E9A180B4-A10D-4B27-9DEC-333E47BA59A9.png

  • 新增src/components/Menu/index.tsx文件
import React from 'react';
import { Menu } from 'antd';
import { Link } from 'react-router-dom';

function AppMenu() {
  return (
    <Menu mode="horizontal">
      <Menu.Item key="home">
        <Link to="/">Home</Link>
      </Menu.Item>
      <Menu.Item key="about">
        <Link to="/about">About</Link>
      </Menu.Item>
    </Menu>
  );
}

export default AppMenu;
  • 新增src/pages/About.tsx文件
import React from 'react';

const About: React.FC = () => {
  return <h1>About Page</h1>;
};

export default About;
  • 新增src/pages/Home.tsx文件
import React from 'react';

const Home: React.FC = () => {
  return <h1>Home Page</h1>;
};

export default Home;
  • 新增src/routes/index.tsx文件
import React, { Suspense } from 'react'
import { RouteObject, useRoutes } from "react-router-dom";
import Home from "../pages/Home";
import About from "../pages/About";

const routeObjects: RouteObject[] = [
  {
    path: '/',
    element: (
      <Suspense>
        <Home />
      </Suspense>
    )
  },
  {
    path: '/about',
    element: (
      <Suspense>
        <About />
      </Suspense>
    ),
  }
]

function Pages() {
    const routes = useRoutes(routeObjects)
    return routes
  }
  
  export default Pages

  • 修改App.tsx文件
import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import Menu from './components/Menu'
import Pages from './routes';
const App: React.FC = () => {
  return (
    <Router>
      <Menu />
      <Pages />
    </Router>
  );
};

export default App;
  • 执行pnpm dev启动项目,会发现路由配置成功了

6. 配置单元测试

  • 安装vitest和相关依赖
pnpm add -D vitest jsdom @testing-library/react @testing-library/jest-dom @vitejs/plugin-react @vitest/coverage-v8
  • vite.config.ts文件中添加vitest插件
export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./src/setupTests.ts'],
  },
})
  • 新增 src/setupTests.ts文件
import '@testing-library/jest-dom'
  • package.json文件中添加测试脚本:
"scripts": {
  "test": "vitest",
  "coverage": "vitest run --coverage"
}
  • 新增一个单元测试文件src/components/Button/test/Button.spec.tsx
import { describe, it, expect } from 'vitest';
import { render, fireEvent } from '@testing-library/react';
import { Button } from 'antd';
import { vi } from 'vitest';
import React from 'react';

// 测试用例:测试按钮点击事件
describe('Button', () => {
  it('should call onClick handler when clicked', () => {
    const handleClick = vi.fn(); // 创建一个模拟函数

    const { getByText } = render(<Button onClick={handleClick}>Click Me</Button>);
    const button = getByText('Click Me');

    fireEvent.click(button); // 模拟点击事件

    expect(handleClick).toHaveBeenCalledTimes(1); // 验证点击事件是否被调用
  });
});

问题记录

    1. Button.spec.tsx一定是tsx类型文件,如果是ts类型会报错

43099DE7-AC3A-4828-8C06-21AC77DF963A.png

排查很久。。。改为.tsx后问题解决

    1. 需要显式引用React

220BCAC2-E202-49DD-849A-318310B67EEF.png

添加import React from 'react';后报错消失

FA9457A3-6D3C-4C93-B9B7-879C9C24975C.png

  • 执行pnpm test发现单元测试已生效

64FCF5FC-4A5D-48B6-9323-402B4F1517AC.png

  • 执行pnpm coverage查看覆盖率报告

97F960E1-C4E4-408B-BF92-ED2F6545D9EF.png

7.配置husky

  • 安装husky:
 pnpm i husky -D
 pnpm i lint-staged -D //让你只对本次提交的文件进行校验,而不是整个项目的所有文件
  • 安装相关插件:
pnpm i @commitlint/config-conventional -D
pnpm i @commitlint/cli -D
  • 配置一个 commitlint 内容插件来确定一种 msg 风格
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.ts
  • 配置 .husky钩子
// commit-msg
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx --no -- commitlint --edit "$1"

// pre-commit
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
pnpm run lint
# //需要 pnpm run prepare 生成 .husky 配置文件。不然commit 前的自动校验不会生效。

// pre-push
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
pnpm run test

2.png

3.png

4.png

这里用了默认的持续监听,所以需要手动按q来停止监听,但是提交前校验的时候我们不需要这个持续监听,所以修改下 pre-push文件

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
pnpm run test --watch=false

5.png

结语

至此,一个简单的拥有基本功能的项目算是搭建完成了,后续有新的功能会继续添加的!