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.json、tsconfig.json、vite.config.ts等文件。Vite是一个现代化的前端构建工具,它使用ES模块原生支持,避免了Webpack的复杂性,提供更快的冷启动和热更新。
项目结构概述
-
node_modules:依赖包。通过pnpm i 安装依赖
-
src:源代码目录,这是应用的核心。
-
assets:静态资产(如图片),但在本项目中未使用。
-
components:React组件文件夹,包含
TodoInput.tsx、TodoItem.tsx、TodoList.tsx。 -
hooks:自定义Hook文件夹,包含
useTodos.tsx。 -
types:类型定义文件夹,包含
todo.ts(Todo接口)。 -
utils:工具函数文件夹,包含
storages.ts(localStorage操作)。 -
App.css、
App.tsx:根组件和样式。 -
index.css:全局样式。
-
这种结构遵循“功能分组”的原则:组件、钩子、类型、工具分离,便于维护和扩展。TypeScript的优势在这里体现:每个文件都可以导入类型,确保跨文件的一致性。
核心代码详解
App.tsx:根组件
App.tsx是应用的入口组件,使用自定义Hook useTodos管理状态,并渲染TodoInput和TodoList。
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
这是状态管理的核心,使用useState和useEffect结合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定义了预期属性。确保todos是Todo[],函数是(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带来的生产力提升。