第一个Preact应用 #
一、创建项目 #
1.1 使用 Vite 创建 #
bash
npm create vite@latest my-app -- --template preact
cd my-app
npm install
npm run dev
1.2 项目结构 #
text
my-app/
├── src/
│ ├── app.jsx 主应用组件
│ ├── app.css 应用样式
│ ├── main.jsx 入口文件
│ └── index.css 全局样式
├── index.html HTML 模板
├── package.json 项目配置
└── vite.config.js Vite 配置
二、理解入口文件 #
2.1 main.jsx #
jsx
import { render } from 'preact';
import { App } from './app';
import './index.css';
render(<App />, document.getElementById('app'));
解析:
| 部分 | 说明 |
|---|---|
import { render } |
导入 Preact 渲染函数 |
import { App } |
导入根组件 |
render(<App />, ...) |
将组件渲染到 DOM |
2.2 index.html #
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Preact App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
三、创建计数器应用 #
3.1 基础版本 #
jsx
import { useState } from 'preact/hooks';
export function App() {
const [count, setCount] = useState(0);
return (
<div class="app">
<h1>计数器</h1>
<p>当前计数: {count}</p>
<button onClick={() => setCount(count + 1)}>
增加
</button>
<button onClick={() => setCount(count - 1)}>
减少
</button>
</div>
);
}
3.2 添加样式 #
css
.app {
max-width: 400px;
margin: 50px auto;
padding: 20px;
text-align: center;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
h1 {
color: #333;
margin-bottom: 20px;
}
p {
font-size: 24px;
margin: 20px 0;
}
button {
padding: 10px 20px;
margin: 0 5px;
font-size: 16px;
border: none;
border-radius: 4px;
cursor: pointer;
background-color: #007bff;
color: white;
transition: background-color 0.2s;
}
button:hover {
background-color: #0056b3;
}
四、构建待办事项应用 #
4.1 完整代码 #
jsx
import { useState } from 'preact/hooks';
import './app.css';
export function App() {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState('');
const addTodo = () => {
if (input.trim()) {
setTodos([...todos, {
id: Date.now(),
text: input,
completed: false
}]);
setInput('');
}
};
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
));
};
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
const handleKeyDown = (e) => {
if (e.key === 'Enter') {
addTodo();
}
};
return (
<div class="todo-app">
<h1>待办事项</h1>
<div class="input-group">
<input
type="text"
value={input}
onInput={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="添加新任务..."
/>
<button onClick={addTodo}>添加</button>
</div>
<ul class="todo-list">
{todos.map(todo => (
<li key={todo.id} class={todo.completed ? 'completed' : ''}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span>{todo.text}</span>
<button
class="delete-btn"
onClick={() => deleteTodo(todo.id)}
>
删除
</button>
</li>
))}
</ul>
{todos.length > 0 && (
<div class="stats">
<p>
总计: {todos.length} |
已完成: {todos.filter(t => t.completed).length}
</p>
</div>
)}
</div>
);
}
4.2 样式文件 #
css
.todo-app {
max-width: 500px;
margin: 50px auto;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
h1 {
text-align: center;
color: #333;
}
.input-group {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.input-group input {
flex: 1;
padding: 10px;
font-size: 16px;
border: 1px solid #ddd;
border-radius: 4px;
}
.input-group button {
padding: 10px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.todo-list {
list-style: none;
padding: 0;
}
.todo-list li {
display: flex;
align-items: center;
padding: 10px;
border-bottom: 1px solid #eee;
}
.todo-list li.completed span {
text-decoration: line-through;
color: #999;
}
.todo-list span {
flex: 1;
margin-left: 10px;
}
.delete-btn {
padding: 5px 10px;
background-color: #dc3545;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.stats {
text-align: center;
margin-top: 20px;
color: #666;
}
五、组件拆分 #
5.1 拆分后的结构 #
text
src/
├── components/
│ ├── TodoInput.jsx
│ ├── TodoItem.jsx
│ └── TodoStats.jsx
├── app.jsx
└── main.jsx
5.2 TodoInput 组件 #
jsx
export function TodoInput({ onAdd }) {
const [input, setInput] = useState('');
const handleSubmit = () => {
if (input.trim()) {
onAdd(input);
setInput('');
}
};
return (
<div class="input-group">
<input
type="text"
value={input}
onInput={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSubmit()}
placeholder="添加新任务..."
/>
<button onClick={handleSubmit}>添加</button>
</div>
);
}
5.3 TodoItem 组件 #
jsx
export function TodoItem({ todo, onToggle, onDelete }) {
return (
<li class={todo.completed ? 'completed' : ''}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span>{todo.text}</span>
<button
class="delete-btn"
onClick={() => onDelete(todo.id)}
>
删除
</button>
</li>
);
}
5.4 TodoStats 组件 #
jsx
export function TodoStats({ todos }) {
if (todos.length === 0) return null;
const completed = todos.filter(t => t.completed).length;
return (
<div class="stats">
<p>
总计: {todos.length} | 已完成: {completed}
</p>
</div>
);
}
5.5 组合使用 #
jsx
import { useState } from 'preact/hooks';
import { TodoInput } from './components/TodoInput';
import { TodoItem } from './components/TodoItem';
import { TodoStats } from './components/TodoStats';
export function App() {
const [todos, setTodos] = useState([]);
const addTodo = (text) => {
setTodos([...todos, {
id: Date.now(),
text,
completed: false
}]);
};
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
));
};
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<div class="todo-app">
<h1>待办事项</h1>
<TodoInput onAdd={addTodo} />
<ul class="todo-list">
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
))}
</ul>
<TodoStats todos={todos} />
</div>
);
}
六、添加本地存储 #
6.1 持久化 Hook #
jsx
import { useState, useEffect } from 'preact/hooks';
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
6.2 使用持久化 #
jsx
export function App() {
const [todos, setTodos] = useLocalStorage('todos', []);
// ... 其他代码
}
七、构建和部署 #
7.1 构建生产版本 #
bash
npm run build
7.2 预览构建结果 #
bash
npm run preview
7.3 部署到静态服务器 #
bash
# 构建后的文件在 dist/ 目录
# 可部署到任何静态服务器
八、总结 #
通过这个示例,我们学习了:
| 知识点 | 说明 |
|---|---|
| useState | 管理组件状态 |
| 事件处理 | onClick、onInput 等 |
| 条件渲染 | 根据状态显示不同内容 |
| 列表渲染 | map 渲染列表 |
| 组件拆分 | 将功能拆分为独立组件 |
| Props | 父子组件通信 |
下一步学习:
最后更新:2026-03-28