第一个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