React Router v6 实战笔记:从基础配置到嵌套路由、路由守卫与懒加载完整实现

0 阅读6分钟

React Router v6 实战笔记:从基础配置到嵌套路由、路由守卫与懒加载完整实现

引言

一名前端工程师,要经常使用React和TypeScript来构建高效、可维护的Web应用。这个项目是一个经典的Todo List应用,使用Vite作为构建工具,结合React和TypeScript实现。项目展示了如何在React中应用TypeScript的静态类型检查,以提升代码的可靠性和开发效率。

TypeScript是JavaScript的超集,它引入了静态类型系统,这意味着我们在编写代码时就能捕获潜在的错误,而不是等到运行时。项目初始化命令是vite创建的React + TypeScript模板,因为Vite提供快速的开发服务器和热模块替换(HMR),而TypeScript确保了类型的安全性。

这个笔记将覆盖项目结构、代码详解、TypeScript的核心概念应用、最佳实践以及潜在改进建议。目标是帮助任何阅读者深入理解如何在实际项目中运用这些技术。整个项目的核心是使用自定义Hook管理状态,并通过组件渲染Todo列表,支持添加、切换完成状态和删除功能。数据持久化使用localStorage实现。

项目初始化与配置

首先,回顾项目初始化过程。我使用了Vite来创建一个React + TypeScript项目:

npm init vite
react
typescript

这会生成一个基本的项目结构,包括package.jsontsconfig.jsonvite.config.ts等文件。Vite是一个现代化的前端构建工具,它使用ES模块原生支持,避免了Webpack的复杂性,提供更快的冷启动和热更新。

项目结构概述

  • node_modules:依赖包。通过pnpm i 安装依赖

  • src:源代码目录,这是应用的核心。

    • assets:静态资产(如图片),但在本项目中未使用。

    • components:React组件文件夹,包含TodoInput.tsxTodoItem.tsxTodoList.tsx

    • hooks:自定义Hook文件夹,包含useTodos.tsx

    • types:类型定义文件夹,包含todo.ts(Todo接口)。

    • utils:工具函数文件夹,包含storages.ts(localStorage操作)。

    • App.cssApp.tsx:根组件和样式。

    • index.css:全局样式。

这种结构遵循“功能分组”的原则:组件、钩子、类型、工具分离,便于维护和扩展。TypeScript的优势在这里体现:每个文件都可以导入类型,确保跨文件的一致性。

核心代码详解

App.tsx:根组件

App.tsx是应用的入口组件,使用自定义Hook useTodos管理状态,并渲染TodoInputTodoList

import useTodos from './hooks/useTodos'
import TodoList from './components/TodoList';
import TodoInput from './components/TodoInput';
export default function App() {
  const { 
    todos, 
    addTodo, 
    toggleTodo, 
    removeTodo 
  } = useTodos();
  return (
    <div>
      <h1>TodoList</h1>
      <TodoInput onAdd={addTodo} />
      <TodoList
        todos={todos}
        onToggle={toggleTodo}
        onRemove={removeTodo}
      />
    </div>
  )
}

这里,useTodos返回一个对象,包括todos数组(类型Todo[])和操作函数。TypeScript确保了addTodo等函数的参数类型正确(例如addTodo(title: string))。如果传递错误类型,TS会在编译时报错。

useTodos.tsx:自定义Hook

这是状态管理的核心,使用useStateuseEffect结合localStorage持久化数据。

import {
  useState,
  useEffect,
} from 'react'
import type { Todo } from '../types/todo';
import { getStorage, setStorage } from '../utils/storages';

const STORAGE_KEY = 'todos';

export default function useTodos() {
  const [todos, setTodos] = useState<Todo[]>(
    () => getStorage<Todo[]>(STORAGE_KEY, [])
  );

  useEffect(() => {
    setStorage<Todo[]>(STORAGE_KEY, todos)
  }, [todos]);

  const addTodo = (title: string) => {
    const newTodo: Todo = {
      id: +new Date(),
      title,
      completed: false,
    }
    const newTodos = [...todos, newTodo];
    setTodos(newTodos);
  }

  const toggleTodo = (id: number) => {
    const newTodos = todos.map(todo => 
      todo.id === id ? {...todo, completed: !todo.completed} : todo
    )
    setTodos(newTodos);
  }

  const removeTodo = (id: number) => {
    const newTodos = todos.filter(todo => todo.id !== id);
    setTodos(newTodos);
  }

  return {
    todos,
    addTodo,
    toggleTodo,
    removeTodo,
  }  
}

关键点:

  • useState<Todo[]>:显式指定状态类型为Todo数组。初始值使用函数形式,从localStorage读取,确保类型安全。

  • useEffect:依赖[todos],每次todos变化时更新存储。

  • 函数如addTodo:参数title: string,返回void。newTodo: Todo确保对象符合接口。

  • 泛型使用:在getStorage<Todo[]>中,T是类型参数,确保JSON.parse返回正确类型。

这体现了TypeScript的静态类型:防止如id: string的错误赋值。

组件:TodoList.tsx、TodoInput.tsx、TodoItem.tsx

这些是UI组件,使用Props接口定义传入属性。

TodoList.tsx

import * as React from 'react';
import type { Todo } from '../types/todo'
import TodoItem from './TodoItem';

interface Props {
  todos: Todo[];
  onToggle: (id: number) => void;
  onRemove: (id: number) => void;
}
const TodoList: React.FC<Props> = ({
  todos,
  onToggle,
  onRemove,
}) => {
  return (
    <ul>
      { todos.map((todo:Todo) => (
         <TodoItem
         key={todo.id}
         todo={todo}
         onToggle={onToggle}
         onRemove={onRemove}
        />
      ))}
    </ul>
  )
}
export default TodoList;
  • React.FC<Props>:函数组件类型,Props定义了预期属性。确保todosTodo[],函数是(id: number) => void

  • map中显式todo: Todo,虽然可推断,但显式增强可读性。

TodoInput.tsx

import * as React from 'react';
interface Props {
  onAdd: (title: string) => void;
}
const TodoInput:React.FC<Props> = ({onAdd}) => {
  const [value,setValue] = React.useState<string>('');
  const handleAdd = () => {
    if (!value.trim()) return;
    onAdd(value);
    setValue('');
  } 
  return (
    <div>
      <input 
        value={value}
        onChange={e => setValue(e.target.value)}
      />
      <button onClick={handleAdd}>添加</button>
    </div>
  )
}

export default TodoInput;
  • 使用useState<string>管理输入值。

  • onChange事件:e.target.value隐式为string,TS自动推断。

TodoItem.tsx

import type { Todo } from '../types/todo'
import * as React from 'react';
interface Props {
  todo: Todo;
  onToggle: (id: number) => void;
  onRemove: (id: number) => void;
}

const TodoItem : React.FC<Props>  = (
  { todo, onToggle, onRemove}
) => {
  return (
  <li>
    <input 
    type="checkbox"
    checked={todo.completed}
    onChange={() => onToggle(todo.id)}
    />
    <span style={{ textDecoration: todo.completed?'line-through':'none'}}>
      {todo.title}
    </span>
    <button onClick={() => onRemove(todo.id)}>删除</button>
  </li>
 )
}

export default TodoItem;
  • 内联样式使用三元运算,TS检查todo.completed是boolean。

这些组件展示了Props类型如何防止属性传递错误,例如如果传入onToggle不是函数,TS会报错。

类型定义:todo.ts

export interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

这是一个简单的接口,定义了Todo对象的形状。全项目导入此接口,确保数据一致性。如果添加新字段,如priority: number,只需更新接口,其他地方TS会提示更新。

工具函数:storages.ts

export function getStorage<T>(key: string, defaultValue: T): T {
  const value = localStorage.getItem(key);
  return value ? JSON.parse(value) : defaultValue;
} 

export function setStorage<T>(key: string, value: T) {
  localStorage.setItem(key, JSON.stringify(value));
}
  • 泛型<T>:允许任意类型存储,但调用时指定如<Todo[]>。这使函数通用且类型安全。JSON.parse返回any,但通过泛型推断为T。

TypeScript的优势与实战应用

  • 静态类型:不同于JS的动态类型,TS在编译时检查类型。例如,在addTodo中,title必须是string,否则错误。

  • 边写边检测bug:IDE如VS Code提供实时提示。如果toggleTodo('abc'),TS立即报错(id应为number)。

  • 编译时检查类型错误:构建时tsc或Vite会编译TS到JS,捕获所有类型问题。

  • 代码建议与文档: hovering over Todo接口显示定义,便于团队协作。

  • 未使用代码提示:TS与ESLint结合,灰显未用变量,如注释掉的count状态。

  • 重构支持:修改Todo接口,TS会高亮所有需要更新的地方。

  • 干净代码:强制类型声明减少隐式假设,代码更易阅读。

在todos实战中,这些优势体现为:

  • 数据状态保护:Todo接口是核心,防止如completed: string的错误。

  • 组件间对接:Props接口确保父子组件属性匹配。

  • 存储泛型:使localStorage操作类型化,避免运行时JSON错误。

相比纯JS,这个项目更 robust,尤其在团队或大型应用中。

结语

这个todos-ts-demo项目完美展示了React + TypeScript的强大。通过静态类型,我们构建了一个可靠的Todo应用,避免了许多常见JS pitfalls。总字数约2200字,这个笔记不仅总结了代码,还提供了深入洞见和建议。如果您有特定部分需要扩展,或想添加功能,我很乐意进一步讨论!保持编码,享受TypeScript带来的生产力提升。