本文将基于之前的 TypeScript 表单处理项目,扩展实现删除用户、编辑用户、本地持久化(localStorage)功能,并附有详细代码注释。同时,文中也会探讨如何平滑切换到真实后端以及引入状态管理的思路。
1. 修改后的 index.html(增加编辑模态框与样式)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TS + Vite 示例 - 第三步:本地持久化 + 删除/编辑</title>
<style>
body {
font-family: sans-serif;
max-width: 700px;
margin: 2rem auto;
padding: 0 1rem;
}
button, input {
margin: 0.5rem 0;
padding: 0.5rem;
}
.error {
color: red;
}
.loading {
color: gray;
font-style: italic;
}
/* 用户列表项样式 */
.user-item {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
}
.user-info {
flex: 1;
}
.user-actions button {
margin-left: 0.5rem;
padding: 0.2rem 0.5rem;
}
/* 模态框样式 */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: white;
padding: 2rem;
border-radius: 8px;
width: 300px;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
}
.modal-content form div {
margin-bottom: 1rem;
}
.modal-content label {
display: inline-block;
width: 60px;
}
.modal-content input {
width: 200px;
}
.modal-buttons {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
}
</style>
</head>
<body>
<div id="app">
<h1>用户管理(本地存储版)</h1>
<!-- 刷新按钮 -->
<button id="refresh-btn" type="button">刷新列表</button>
<!-- 添加用户表单 -->
<h2>添加新用户</h2>
<form id="add-user-form">
<div>
<label for="name">姓名:</label>
<input type="text" id="name" name="name" required />
</div>
<div>
<label for="email">邮箱:</label>
<input type="email" id="email" name="email" required />
</div>
<button type="submit">提交</button>
</form>
<!-- 用户列表容器 -->
<h2>用户列表</h2>
<ul id="user-list" style="list-style: none; padding: 0;"></ul>
</div>
<!-- 编辑用户模态框 -->
<div id="edit-modal" class="modal" style="display: none;">
<div class="modal-content">
<h3>编辑用户</h3>
<form id="edit-user-form">
<input type="hidden" id="edit-id" />
<div>
<label>姓名:</label>
<input type="text" id="edit-name" required />
</div>
<div>
<label>邮箱:</label>
<input type="email" id="edit-email" required />
</div>
<div class="modal-buttons">
<button type="submit">保存</button>
<button type="button" id="close-modal">取消</button>
</div>
</form>
</div>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
2. 重构后的 src/main.ts(含详细注释)
/**
* TypeScript 第三步示例:
* 1. 使用 localStorage 模拟后端,实现本地持久化
* 2. 增加删除用户功能
* 3. 增加编辑用户功能(通过模态框)
* 4. 保留原有的添加用户和刷新功能
* 5. 所有操作均模拟异步延迟,便于理解真实后端的交互模式
*/
// ---------- 类型定义 ----------
interface User {
id: number;
name: string;
email: string;
}
interface NewUser {
name: string;
email: string;
}
// ---------- 用户服务层(封装所有数据操作,便于未来切换到真实 API)----------
class UserService {
private storageKey = ‘users‘; // localStorage 的键名
/**
* 初始化:如果 localStorage 中没有数据,则从 jsonplaceholder 获取初始数据并存储
* 模拟首次加载时的数据填充
*/
async init(): Promise<void> {
const stored = localStorage.getItem(this.storageKey);
if (!stored) {
const users = await this.fetchInitialUsers();
localStorage.setItem(this.storageKey, JSON.stringify(users));
}
}
/**
* 从远程 API 获取初始用户数据(仅用于初始化)
*/
private async fetchInitialUsers(): Promise<User[]> {
const response = await fetch(‘https://jsonplaceholder.typicode.com/users‘);
if (!response.ok) {
throw new Error(`获取初始数据失败: ${response.status}`);
}
const users = await response.json();
// 只保留我们需要的字段,确保类型安全
return users.map((u: any) => ({
id: u.id,
name: u.name,
email: u.email,
}));
}
/**
* 获取所有用户(模拟异步延迟)
*/
async getUsers(): Promise<User[]> {
await this.delay();
const stored = localStorage.getItem(this.storageKey);
return stored ? JSON.parse(stored) : [];
}
/**
* 添加新用户(id 自动生成,基于当前最大 id + 1)
*/
async addUser(newUser: NewUser): Promise<User> {
await this.delay();
const users = await this.getUsers();
const newId = users.length > 0 ? Math.max(...users.map(u => u.id)) + 1 : 1;
const user: User = { id: newId, ...newUser };
users.push(user);
localStorage.setItem(this.storageKey, JSON.stringify(users));
return user;
}
/**
* 更新用户信息
* @param id 用户ID
* @param updatedUser 要更新的字段(可部分更新)
*/
async updateUser(id: number, updatedUser: Partial<User>): Promise<User> {
await this.delay();
const users = await this.getUsers();
const index = users.findIndex(u => u.id === id);
if (index === -1) {
throw new Error(`用户不存在,id: ${id}`);
}
const updated = { ...users[index], ...updatedUser };
users[index] = updated;
localStorage.setItem(this.storageKey, JSON.stringify(users));
return updated;
}
/**
* 删除用户
*/
async deleteUser(id: number): Promise<void> {
await this.delay();
let users = await this.getUsers();
users = users.filter(u => u.id !== id);
localStorage.setItem(this.storageKey, JSON.stringify(users));
}
/**
* 模拟网络延迟,使操作更接近真实后端
*/
private delay(ms: number = 300): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// ---------- 创建服务实例 ----------
const userService = new UserService();
// ---------- DOM 元素获取(原有)----------
const userListElement = document.getElementById(‘user-list‘) as HTMLUListElement;
const refreshBtn = document.getElementById(‘refresh-btn‘) as HTMLButtonElement;
const addUserForm = document.getElementById(‘add-user-form‘) as HTMLFormElement;
const nameInput = document.getElementById(‘name‘) as HTMLInputElement;
const emailInput = document.getElementById(‘email‘) as HTMLInputElement;
// ---------- 编辑模态框相关元素 ----------
const editModal = document.getElementById(‘edit-modal‘) as HTMLDivElement;
const editForm = document.getElementById(‘edit-user-form‘) as HTMLFormElement;
const editIdInput = document.getElementById(‘edit-id‘) as HTMLInputElement;
const editNameInput = document.getElementById(‘edit-name‘) as HTMLInputElement;
const editEmailInput = document.getElementById(‘edit-email‘) as HTMLInputElement;
const closeModalBtn = document.getElementById(‘close-modal‘) as HTMLButtonElement;
// ---------- 辅助函数:显示加载/错误状态 ----------
function setLoading(loading: boolean): void {
if (loading) {
userListElement.innerHTML = ‘<li class="loading">加载中...</li>‘;
}
}
function showError(message: string): void {
userListElement.innerHTML = `<li class="error">错误:${message}</li>`;
}
// ---------- 渲染用户列表(为每个用户添加编辑和删除按钮)----------
function renderUserList(users: User[]): void {
if (!users.length) {
userListElement.innerHTML = ‘<li>暂无用户</li>‘;
return;
}
userListElement.innerHTML = ‘‘; // 清空容器
users.forEach(user => {
// 创建列表项容器
const li = document.createElement(‘li‘);
li.className = ‘user-item‘;
// 用户信息区域
const infoSpan = document.createElement(‘span‘);
infoSpan.className = ‘user-info‘;
infoSpan.textContent = `${user.name} (${user.email})`;
// 按钮区域
const actionsDiv = document.createElement(‘div‘);
actionsDiv.className = ‘user-actions‘;
// 编辑按钮
const editBtn = document.createElement(‘button‘);
editBtn.textContent = ‘编辑‘;
editBtn.addEventListener(‘click‘, () => openEditModal(user)); // 绑定编辑事件
// 删除按钮
const deleteBtn = document.createElement(‘button‘);
deleteBtn.textContent = ‘删除‘;
deleteBtn.addEventListener(‘click‘, async () => {
if (confirm(`确定删除用户 ${user.name} 吗?`)) {
try {
await userService.deleteUser(user.id);
await loadUsers(); // 删除后重新加载列表
} catch (error) {
alert(error instanceof Error ? error.message : ‘删除失败‘);
console.error(error);
}
}
});
actionsDiv.appendChild(editBtn);
actionsDiv.appendChild(deleteBtn);
li.appendChild(infoSpan);
li.appendChild(actionsDiv);
userListElement.appendChild(li);
});
}
// ---------- 核心:加载用户并渲染(从 localStorage)----------
async function loadUsers(): Promise<void> {
setLoading(true);
try {
const users = await userService.getUsers();
renderUserList(users);
} catch (error) {
showError(error instanceof Error ? error.message : ‘未知错误‘);
console.error(error);
} finally {
// 加载状态已由 renderUserList 覆盖
}
}
// ---------- 添加新用户(使用 service 操作 localStorage)----------
async function handleAddUser(event: Event): Promise<void> {
event.preventDefault();
const name = nameInput.value.trim();
const email = emailInput.value.trim();
if (!name || !email) {
alert(‘请填写姓名和邮箱‘);
return;
}
// 禁用提交按钮,防止重复提交
const submitBtn = addUserForm.querySelector(‘button[type="submit"]‘) as HTMLButtonElement;
const originalText = submitBtn.textContent;
submitBtn.disabled = true;
submitBtn.textContent = ‘提交中...‘;
try {
await userService.addUser({ name, email });
// 清空表单
nameInput.value = ‘‘;
emailInput.value = ‘‘;
// 重新加载列表
await loadUsers();
} catch (error) {
alert(error instanceof Error ? error.message : ‘添加失败‘);
console.error(error);
} finally {
submitBtn.disabled = false;
submitBtn.textContent = originalText || ‘提交‘;
}
}
// ---------- 编辑用户相关 ----------
// 打开模态框,填充当前用户数据
function openEditModal(user: User): void {
editIdInput.value = String(user.id);
editNameInput.value = user.name;
editEmailInput.value = user.email;
editModal.style.display = ‘flex‘; // 显示模态框
}
// 关闭模态框
function closeModal(): void {
editModal.style.display = ‘none‘;
editForm.reset(); // 可选:重置表单
}
// 处理编辑表单提交
async function handleEditUser(event: Event): Promise<void> {
event.preventDefault();
const id = parseInt(editIdInput.value, 10);
const name = editNameInput.value.trim();
const email = editEmailInput.value.trim();
if (!name || !email) {
alert(‘姓名和邮箱不能为空‘);
return;
}
// 禁用保存按钮(可选)
const submitBtn = editForm.querySelector(‘button[type="submit"]‘) as HTMLButtonElement;
const originalText = submitBtn.textContent;
submitBtn.disabled = true;
submitBtn.textContent = ‘保存中...‘;
try {
await userService.updateUser(id, { name, email });
closeModal();
await loadUsers(); // 刷新列表
} catch (error) {
alert(error instanceof Error ? error.message : ‘更新失败‘);
console.error(error);
} finally {
submitBtn.disabled = false;
submitBtn.textContent = originalText;
}
}
// ---------- 绑定事件 ----------
refreshBtn.addEventListener(‘click‘, loadUsers);
addUserForm.addEventListener(‘submit‘, handleAddUser);
editForm.addEventListener(‘submit‘, handleEditUser);
closeModalBtn.addEventListener(‘click‘, closeModal);
// 点击模态框背景也可关闭(可选)
editModal.addEventListener(‘click‘, (e) => {
if (e.target === editModal) closeModal();
});
// ---------- 初始化:先初始化数据,再加载用户列表 ----------
async function initApp(): Promise<void> {
try {
await userService.init(); // 确保 localStorage 中有数据
await loadUsers();
} catch (error) {
console.error(‘初始化失败:‘, error);
showError(‘初始化失败,请刷新重试‘);
}
}
initApp();
/**
* 扩展说明:
*
* 1. 本地持久化 (localStorage):
* - 所有用户数据存储在 localStorage 中,页面刷新后数据不丢失。
* - UserService 封装了所有 CRUD 操作,便于测试和维护。
*
* 2. 删除用户:
* - 每个用户旁添加了“删除”按钮,点击后确认删除,调用 userService.deleteUser。
* - 删除成功后重新加载列表。
*
* 3. 编辑用户:
* - 每个用户旁添加了“编辑”按钮,点击后弹出模态框。
* - 模态框中显示当前用户信息,提交后调用 userService.updateUser 更新数据。
* - 更新成功后关闭模态框并刷新列表。
*
* 4. 模拟异步延迟:
* - UserService 中的每个方法都添加了 delay(),模拟网络请求的延迟,让操作更真实。
*
* 5. 切换到真实后端:
* - 只需修改 UserService 中的方法,将 localStorage 操作替换为 fetch/axios 调用即可。
* - 例如:getUsers() 改为 fetch(‘/api/users‘),addUser 改为 POST /api/users 等。
* - 可以保留 init 方法用于获取初始数据,但不再需要 localStorage。
*
* 6. 引入状态管理(如 Pinia):
* 当应用规模变大,可将用户列表和加载状态抽离到全局 store。
* - 创建 store(例如 useUserStore),包含 users, loading, error 等状态。
* - 在组件中调用 store 的 actions(如 fetchUsers, addUser, deleteUser, updateUser)。
* - 在 Vue 中使用 Pinia 管理响应式状态,简化组件间数据传递。
*
* 7. 使用真实后端(Node.js + Express + TypeScript):
* 可另外搭建一个简单的 REST API,提供 /users 端点支持 GET, POST, PUT, DELETE。
* 前端只需修改 UserService 中的请求地址和 HTTP 方法。
*/
3. 关键改动说明
| 功能 |
实现方式 |
注释要点 |
| 本地持久化 |
使用 localStorage 存储 users 数组,所有增删改查均读写该存储。 |
UserService 类封装操作,init() 负责首次数据填充(从 jsonplaceholder 获取初始数据)。 |
| 删除用户 |
为每个用户生成删除按钮,点击后调用 userService.deleteUser(id) 并刷新列表。 |
使用 confirm 二次确认,删除后重新调用 loadUsers() 更新视图。 |
| 编辑用户 |
添加编辑按钮,点击弹出模态框;模态框中提交时调用 userService.updateUser。 |
模态框显示/隐藏通过 CSS 控制;编辑前将当前用户数据填入表单。 |
| 模拟异步 |
所有 UserService 方法内部调用 delay(),模拟网络请求延迟。 |
让代码更接近真实后端交互,方便理解异步流程。 |
| 扩展性 |
如需切换到真实后端,只需修改 UserService 中的方法实现(例如用 fetch 替换 localStorage)。 |
注释中给出了切换思路,保持前端 UI 层代码不变。 |
| 状态管理 |
当前未引入,但注释中说明了何时需要使用 Pinia 等工具。 |
当应用复杂、多个组件共享数据时,可将数据层抽离到 store。 |
4. 运行方式
- 确保已安装 Node.js,并在项目目录执行
npm install(如果之前已创建 Vite 项目)。
- 将上述
index.html 和 src/main.ts 替换原有文件。
- 运行
npm run dev,打开浏览器即可测试。
- 所有数据将保存在浏览器的 localStorage 中,刷新页面不会丢失。可通过浏览器开发者工具查看/清除存储的数据。
5. 总结
通过本次扩展,我们实现了一个功能相对完整的前端用户管理原型:
- 使用 TypeScript 定义清晰的数据模型,保证了类型安全。
- 将数据操作封装为独立的服务层(
UserService),使业务逻辑与UI分离,极大提升了代码的可维护性和可测试性。未来若要接入真实后端,只需改动此服务层。
- 在用户列表项中动态创建并绑定了编辑和删除按钮,展示了如何操作动态生成的DOM元素。
- 利用模态框(Modal)实现了友好的用户编辑交互。
- 核心在于使用
localStorage 实现了前端数据的持久化,完整模拟了后端的 CRUD 操作流程。这非常适合用于原型开发、演示或离线应用场景。
- 对所有异步操作进行了统一的错误处理和加载状态管理,提升了用户体验。
这个项目麻雀虽小,五脏俱全,涵盖了现代前端开发中的许多核心概念。希望这个实践能帮助你更好地理解如何组织TypeScript代码、进行状态管理和规划应用架构。当你需要构建更复杂的应用时,可以考虑引入如 Vue.js或 React 等前端框架,并配合专业的状态管理库。
如果你在实践过程中有更多想法或问题,欢迎到云栈社区与其他开发者一起交流探讨。