React+TypeScript项目实战入门教程
本文详细介绍了如何在React项目中使用TypeScript进行开发,涵盖了从创建项目到实战应用的全过程。通过实例讲解了如何创建简单的待办事项列表,并通过React Context和Redux管理组件间的状态。此外,还介绍了如何使用Jest和React Testing Library进行测试,以及如何部署和上线项目。
React基础知识回顾 React组件基础React 是一个用于构建用户界面的开源JavaScript库。它由Facebook维护,用于构建可预测的、可重用的UI组件。React组件是构成应用程序的基本单元,可以分为类组件和函数组件两种类型。
类组件
类组件是使用ES6的类来定义的组件,它继承自React的Component类。类组件可以访问生命周期方法,也可以使用状态和上下文。
import React, { Component } from 'react';
class MyComponent extends Component {
    render() {
        return <div>Hello, World!</div>;
    }
}函数组件
函数组件是使用函数来定义的组件,它不需要访问生命周期方法或状态。函数组件通常用于简单的显示逻辑。
import React from 'react';
const MyComponent = () => {
    return <div>Hello, World!</div>;
};React组件的生命周期可以分为三个阶段:挂载(Mounting)、更新(Updating)和卸载(Unmounting)。每个阶段都有若干生命周期方法,用于在不同阶段执行特定的操作。
挂载阶段
- constructor: 初始化状态和属性。
- static getDerivedStateFromProps: 根据props更新状态。
- render: 返回要渲染的元素。
- componentDidMount: 组件挂载后的操作,通常用于异步数据获取。
class MyComponent extends Component {
    constructor(props) {
        super(props);
        this.state = { name: 'React' };
    }
    componentDidMount() {
        console.log('Component mounted');
    }
    render() {
        return <div>{this.state.name}</div>;
    }
}更新阶段
- static getDerivedStateFromProps: 根据新的props更新状态。
- shouldComponentUpdate: 决定组件是否需要更新。
- getSnapshotBeforeUpdate: 在DOM更新之前获取快照。
- componentDidUpdate: 组件更新后的操作。
class MyComponent extends Component {
    state = { message: 'Hello, World!' };
    componentDidMount() {
        console.log('Component mounted');
    }
    componentDidUpdate() {
        console.log('Component updated');
    }
    render() {
        return <div>{this.state.message}</div>;
    }
}卸载阶段
- static getDerivedStateFromProps: 组件卸载前的最后机会更新状态。
- componentWillUnmount: 组件卸载前的操作,通常用于清理订阅或定时器。
class MyComponent extends Component {
    componentDidMount() {
        console.log('Component mounted');
    }
    componentWillUnmount() {
        console.log('Component will unmount');
    }
    render() {
        return <div>Goodbye, World!</div>;
    }
}状态
状态是组件内部的可变数据,用于存储组件的状态。状态通常在constructor中初始化,并通过setState方法更新。
class MyComponent extends Component {
    constructor(props) {
        super(props);
        this.state = { count: 0 };
    }
    incrementCount = () => {
        this.setState({ count: this.state.count + 1 });
    }
    render() {
        return (
            <div>
                <p>Count: {this.state.count}</p>
                <button onClick={this.incrementCount}>Increment</button>
            </div>
        );
    }
}事件处理
React使用合成事件系统来处理事件,这些事件的行为类似于浏览器事件,但具有更好的跨浏览器一致性。
class MyComponent extends Component {
    handleButtonClick = (event) => {
        console.log('Button clicked');
        console.log(event.target); // 输出点击的按钮
    }
    render() {
        return (
            <button onClick={this.handleButtonClick}>
                Click me
            </button>
        );
    }
}TypeScript是JavaScript的一个超集,它在JavaScript的基础上增加了类型系统,使得开发者可以编写更安全、更易于维护的代码。TypeScript代码可以通过编译器编译成JavaScript代码,也可以直接运行在支持ES6的现代浏览器中。
TypeScript的主要特性包括:
- 类型注解:为变量、函数参数、返回值等添加类型注解。
- 接口:定义对象的结构。
- 泛型:定义可变类型的函数或类。
- 命名空间:组织代码和避免命名冲突。
在TypeScript中,可以通过类型注解来为变量、函数参数、返回值等添加类型。
let name: string = 'Alice';
let age: number = 25;
let isStudent: boolean = true;
function add(a: number, b: number): number {
    return a + b;
}接口
接口用于定义对象的结构,可以用来约束对象的属性和方法。
interface User {
    name: string;
    age: number;
    greet(): void;
}
class Student implements User {
    name: string;
    age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
    greet() {
        console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
    }
}函数类型
TypeScript可以为函数的参数和返回值定义类型。
function add(a: number, b: number): number {
    return a + b;
}
function logInfo<T>(item: T): void {
    console.log(item);
}
logInfo<number>(42); // 输出 42泛型
泛型是一种可以使用任意类型的机制。泛型可以定义可变类型的函数或类。
function identity<T>(arg: T): T {
    return arg;
}
let output = identity<string>('Hello, World!'); // 输出 Hello, World!创建一个新的React+TypeScript项目可以使用create-react-app工具,它提供了一个脚手架,简化了项目的创建过程。
npx create-react-app my-app --template typescript
cd my-app
npm start配置TypeScript在React项目中的使用
create-react-app已经为TypeScript配置好了项目结构和编译配置。tsconfig.json文件包含了TypeScript编译器的配置选项,而babel.config.js文件包含了Babel的配置,用于将TypeScript代码编译成ES5 JavaScript。
// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2015",
    "module": "ESNext",
    "moduleResolution": "node",
    "strict": true,
    "jsx": "react",
    "sourceMap": true,
    "baseUrl": ".",
    "paths": {
      "*": ["src/*"]
    },
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src"]
}// babel.config.js
module.exports = {
  presets: [
    '@babel/preset-typescript',
    '@babel/preset-react'
  ],
  plugins: ['@babel/plugin-transform-runtime'],
};安装和配置相关依赖
确保项目中安装了最新的TypeScript和相关依赖。
npm install typescript @types/react @types/react-dom @types/jest@types/react和@types/react-dom提供了React和ReactDOM的类型定义。
创建一个简单的待办事项列表组件,该组件允许用户添加和删除待办事项。
添加待办事项
import React, { useState } from 'react';
function TodoList() {
    const [todos, setTodos] = useState<string[]>([]);
    const addTodo = (text: string) => {
        setTodos([...todos, text]);
    };
    const removeTodo = (index: number) => {
        setTodos(todos.filter((_, i) => i !== index));
    };
    return (
        <div>
            <input
                type="text"
                placeholder="Add a todo"
                onKeyDown={(e) => {
                    if (e.key === 'Enter') {
                        addTodo(e.target.value);
                        e.target.value = '';
                    }
                }}
            />
            <ul>
                {todos.map((todo, index) => (
                    <li key={index}>
                        {todo}{' '}
                        <button onClick={() => removeTodo(index)}>Remove</button>
                    </li>
                ))}
            </ul>
        </div>
    );
}
export default TodoList;使用TypeScript增强React组件
通过添加类型注解来增强组件,确保组件的输入和输出具有正确的类型。
import React from 'react';
import { useState } from 'react';
interface TodoListProps {}
interface TodoListState {
    todos: string[];
}
const TodoList: React.FC<TodoListProps> = () => {
    const [todos, setTodos] = useState<string[]>([]);
    const addTodo = (text: string) => {
        setTodos([...todos, text]);
    };
    const removeTodo = (index: number) => {
        setTodos(todos.filter((_, i) => i !== index));
    };
    return (
        <div>
            <input
                type="text"
                placeholder="Add a todo"
                onKeyDown={(e) => {
                    if (e.key === 'Enter') {
                        addTodo(e.target.value);
                        e.target.value = '';
                    }
                }}
            />
            <ul>
                {todos.map((todo, index) => (
                    <li key={index}>
                        {todo}{' '}
                        <button onClick={() => removeTodo(index)}>Remove</button>
                    </li>
                ))}
            </ul>
        </div>
    );
};
export default TodoList;使用React Context
React Context提供了一种在组件树中传递数据的方式,而无需手动将prop从顶层组件传递到每个组件。
import React, { createContext, useState, useContext } from 'react';
interface Todo {
    text: string;
}
const TodosContext = createContext<Todo[]>([]);
function TodosProvider({ children }) {
    const [todos, setTodos] = useState<Todo[]>([]);
    const addTodo = (text: string) => {
        setTodos([...todos, { text }]);
    };
    const removeTodo = (index: number) => {
        setTodos(todos.filter((_, i) => i !== index));
    };
    return (
        <TodosContext.Provider value={{ todos, addTodo, removeTodo }}>
            {children}
        </TodosContext.Provider>
    );
}
function useTodos() {
    return useContext(TodosContext);
}
function TodoList() {
    const { todos, addTodo, removeTodo } = useTodos();
    return (
        <div>
            <input
                type="text"
                placeholder="Add a todo"
                onKeyDown={(e) => {
                    if (e.key === 'Enter') {
                        addTodo(e.target.value);
                        e.target.value = '';
                    }
                }}
            />
            <ul>
                {todos.map((todo, index) => (
                    <li key={index}>
                        {todo.text}{' '}
                        <button onClick={() => removeTodo(index)}>Remove</button>
                    </li>
                ))}
            </ul>
            <TodosProvider>
                <TodoList />
            </TodosProvider>
        </div>
    );
}
export default TodoList;使用Redux管理状态
Redux是一个用于管理应用状态的库,它可以与React无缝集成,以实现状态的集中管理。
import { createStore } from 'redux';
import { Todo, TodoAction } from './types';
const initialState: Todo[] = [];
const reducer = (state: Todo[] = initialState, action: TodoAction): Todo[] => {
    switch (action.type) {
        case 'ADD_TODO':
            return [...state, action.payload];
        case 'REMOVE_TODO':
            return state.filter((_, i) => i !== action.payload.index);
        default:
            return state;
    }
};
const store = createStore(reducer);
function TodoList() {
    const [todos, setTodos] = useState<Todo[]>(store.getState());
    useEffect(() => {
        const unsubscribe = store.subscribe(() => {
            setTodos(store.getState());
        });
        return () => unsubscribe();
    }, []);
    const addTodo = (text: string) => {
        store.dispatch({ type: 'ADD_TODO', payload: { text } });
    };
    const removeTodo = (index: number) => {
        store.dispatch({ type: 'REMOVE_TODO', payload: { index } });
    };
    return (
        <div>
            <input
                type="text"
                placeholder="Add a todo"
                onKeyDown={(e) => {
                    if (e.key === 'Enter') {
                        addTodo(e.target.value);
                        e.target.value = '';
                    }
                }}
            />
            <ul>
                {todos.map((todo, index) => (
                    <li key={index}>
                        {todo.text}{' '}
                        <button onClick={() => removeTodo(index)}>Remove</button>
                    </li>
                ))}
            </ul>
        </div>
    );
}
export default TodoList;// types.ts
export interface Todo {
    text: string;
    index: number;
}
export interface TodoAction {
    type: 'ADD_TODO' | 'REMOVE_TODO';
    payload: {
        text: string;
        index: number;
    };
}// reducer.ts
const initialState: Todo[] = [];
const todosReducer = (state: Todo[] = initialState, action: TodoAction): Todo[] => {
    switch (action.type) {
        case 'ADD_TODO':
            return [...state, { ...action.payload }];
        case 'REMOVE_TODO':
            return state.filter(todo => todo.index !== action.payload.index);
        default:
            return state;
    }
};Jest是一个JavaScript测试框架,React Testing Library是一个用于编写React组件测试的库。它们可以一起使用,以更自然的方式测试React组件。
安装依赖
npm install --save-dev jest @testing-library/react @testing-library/jest-dom编写测试
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import TodoList from './TodoList';
test('renders todo list', () => {
    render(<TodoList />);
    const inputElement = screen.getByPlaceholderText('Add a todo');
    expect(inputElement).toBeInTheDocument();
});
test('adds a new todo', () => {
    render(<TodoList />);
    const inputElement = screen.getByPlaceholderText('Add a todo');
    fireEvent.change(inputElement, { target: { value: 'New todo' } });
    fireEvent.keyDown(inputElement, { key: 'Enter', code: 'Enter', charCode: 13 });
    expect(screen.getByText('New todo')).toBeInTheDocument();
});
test('removes a todo', () => {
    render(<TodoList />);
    const inputElement = screen.getByPlaceholderText('Add a todo');
    fireEvent.change(inputElement, { target: { value: 'New todo' } });
    fireEvent.keyDown(inputElement, { key: 'Enter', code: 'Enter', charCode: 13 });
    fireEvent.click(screen.getByText('Remove'));
    expect(screen.queryByText('New todo')).not.toBeInTheDocument();
});整合TypeScript和测试库
确保项目中安装了TypeScript的类型定义。
npm install --save-dev @types/jest @types/testing-library__dom解决常见调试问题
- 使用console.log和console.error输出调试信息。
- 使用浏览器的调试工具查看组件的渲染情况。
- 使用Jest的断言和匹配器进行断言失败时的调试。
// 在组件中添加调试信息
console.log('Component rendered');
// 在测试中添加调试信息
console.log('Test started');create-react-app已经配置好了webpack和Babel,可以直接使用npm run build命令生成生产环境的代码。
npm run build将构建好的文件上传到GitHub Pages或其他服务器。
部署到GitHub Pages
- 配置package.json文件中的homepage字段。
- 将构建好的文件放到gh-pages分支。
- 使用GitHub Pages发布站点。
{
  "name": "my-app",
  "version": "0.1.0",
  "private": true,
  "homepage": "https://username.github.io/my-app",
  "dependencies": {
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "react-scripts": "3.4.1"
  },
  "scripts": {
    "predeploy": "npm run build",
    "deploy": "gh-pages -d build"
  },
  "devDependencies": {
    "gh-pages": "^4.0.0"
  }
}npm run deploy部署到其他服务器
- 使用scp或类似工具将构建好的文件上传到服务器。
- 配置服务器的web服务器(如Nginx或Apache)来提供静态文件。
scp -r build username@server:/path/to/public持续集成和持续部署(CI/CD)是指将代码自动集成到主分支,并自动部署到生产环境的过程。常见的CI/CD工具包括Jenkins、GitHub Actions、GitLab CI和Travis CI。
GitHub Actions示例
- 创建.github/workflows目录。
- 创建一个ci.yml文件,配置CI/CD流程。
name: CI/CD
on:
  push:
    branches:
      - main
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Install dependencies
      run: npm ci
    - name: Build
      run: npm run build
    - name: Deploy
      uses: peaceiris/actions-gh-pages@v3
      with:
        deploy_key: ${{ secrets.GITHUB_DEPLOY_KEY }}
        target_branch: main
        build_dir: build通过上述步骤,可以实现自动化的CI/CD流程,确保代码的质量和部署的一致性。
共同学习,写下你的评论
评论加载中...
作者其他优质文章
 
                 
             
			 
					 
					