资讯动态

Swiz框架:现代前端状态管理的响应式与不可变性融合实践

发布时间:2026/8/23 13:21:44 来源:尧图企业网站定制
1. 项目概述一个为现代前端应用量身定制的状态管理框架如果你和我一样在过去几年里深度参与过大型前端项目的开发那么对状态管理这个“永恒的话题”一定深有感触。从早期的 Flux 架构到 Redux 的一统江湖再到后来 MobX、Vuex 的百花齐放我们似乎总是在寻找一个“银弹”——一个既能保持数据流清晰可预测又能让开发者写起来不那么痛苦的状态管理方案。然而现实往往是我们常常在“过度设计”和“管理混乱”之间反复横跳。直到我遇到了mherod/swiz这个项目让我眼前一亮它用一种相当巧妙且务实的方式试图解决现代前端状态管理中的核心痛点。swiz不是一个试图颠覆一切的全新概念它更像是一个深思熟虑的“集成者”和“简化者”。它的核心定位非常清晰为 React 应用提供一个零样板文件、类型安全且高性能的状态管理解决方案。这个名字本身也很有趣“Swiz”可能源于“Swizzle”在计算机科学中常指“混合”或“交换”这恰好暗示了其将响应式、不可变性和简洁 API 融合在一起的特性。它不强制你学习一套全新的心智模型而是基于 React Hooks 和现代 JavaScript 特性如 Proxy让你用最直观的方式管理状态同时享受到类型安全TypeScript和优秀的开发体验。对于厌倦了在 actions、reducers、sagas 之间来回切换又担心 Context useReducer 在复杂场景下力不从心的开发者来说swiz提供了一个值得深入探索的选项。2. 核心设计理念与架构拆解2.1 响应式与不可变性的优雅结合swiz最吸引我的设计哲学在于它成功地将响应式编程的便捷性与不可变数据流的可预测性结合在了一起。这听起来有点矛盾因为响应式通常意味着可变MobX而不可变则是 Redux 的基石。swiz是如何做到的呢它的秘密武器是ES6 Proxy。swiz内部使用 Proxy 来包装你的状态对象从而能够追踪属性的读取get和设置set。当你通过swiz提供的useStore钩子访问状态时组件会自动订阅其依赖的状态片段。一旦这些片段发生变化只有依赖它们的组件会重新渲染。这实现了细粒度的响应式更新避免了 Context 导致的整个子树重渲染问题。但另一方面swiz鼓励或者说强制你以一种“不可变”的方式去更新状态。它没有提供类似 MobX 的observable和action让你直接修改对象属性。相反你需要调用swiz提供的更新函数例如store.set或特定的 action 函数在这个函数内部你描述状态应该如何变化。swiz在背后会应用不可变更新逻辑确保状态的历史是可追踪的并且能轻松实现时间旅行调试等高级功能。这种“表面响应式内核不可变”的混合模式既让开发者写起来顺手直接操作对象结构又保证了状态变化的可预测性。注意这里说的“不可变”更多是逻辑上的。swiz可能使用结构共享如 Immer 库的原理来高效地生成新状态而不是要求你手动进行{...state, ...}这样的展开操作。这大大降低了心智负担。2.2 基于 Hook 的零样板 API 设计“零样板文件”是swiz宣传的一个重点也是它区别于 Redux 的关键。在典型的 Redux 项目中为了管理一个简单的计数器你可能需要定义常量action types、action 创建函数、reducer 函数最后还要用connect或useSelector连接到组件。这个过程充满了模板代码。swiz彻底摒弃了这套模式。它的核心 API 极少主要围绕createStore和useStore展开。你首先用一个函数定义你的 store这个函数内部包含了初始状态和所有更新状态的方法你可以理解为 actions但swiz里可能不这么叫。// 使用 swiz 定义一个计数器 store import { createStore } from swiz; const useCounterStore createStore((set) ({ count: 0, increment: () set((state) ({ count: state.count 1 })), decrement: () set((state) ({ count: state.count - 1 })), reset: () set({ count: 0 }), }));然后在 React 组件中你可以像使用自定义 Hook 一样使用它function Counter() { // 直接解构出需要的状态和动作组件只会因 count 变化而重渲染 const { count, increment, decrement } useCounterStore(); return ( div span{count}/span button onClick{increment}/button button onClick{decrement}-/button /div ); }看到了吗没有 Provider 包裹根组件swiz的 store 本质上是 React Context 的语法糖但默认是全局可用的没有mapDispatchToProps也没有 selector 函数。你需要什么就拿什么API 简洁到令人发指。这种设计极大地降低了入门门槛也让代码库显得非常干净。2.3 类型安全的深度集成对于 TypeScript 用户而言swiz的体验堪称一流。由于 store 是用一个普通的 JavaScript 函数定义的TypeScript 能够完美地推断出整个 store 的形状包括状态的类型和所有方法的签名。当你使用useCounterStore时你会获得完整的类型提示和检查。const useCounterStore createStore((set) ({ count: 0, increment: (by: number 1) set((state) ({ count: state.count by })), // 类型安全 })); // 在组件中increment 的参数 by 会被推断为 number | undefined const { increment } useCounterStore(); increment(5); // 正确 increment(hello); // TypeScript 编译错误这种“开箱即用”的类型安全无需额外定义复杂的interface或type是很多从 Redux TypeScript 的繁琐类型声明中挣扎过来的开发者的福音。它确保了在大型项目中状态结构的变更能够立即通过类型错误反馈出来大大增强了代码的健壮性。3. 核心功能与高级用法详解3.1 Store 的创建与组合模式一个简单的 store 如上所示但真实应用的状态是复杂的、模块化的。swiz提供了非常灵活的方式来组合和拆分 store。单一 Store 模式对于中小型应用你可以将所有状态逻辑放在一个createStore调用中。通过良好的函数组织这仍然是可管理的。const useAppStore createStore((set, get) ({ user: null, todos: [], filters: { showCompleted: true, search: }, // ... 很多 actions }));切片组合模式这是更推荐用于大型应用的方式。你可以为不同的领域domain创建独立的 store slice然后将它们组合起来。// slices/userSlice.js const createUserSlice (set, get) ({ user: null, login: (userData) set({ user: userData }), logout: () set({ user: null }), }); // slices/todoSlice.js const createTodoSlice (set, get) ({ todos: [], addTodo: (text) set((state) ({ todos: [...state.todos, { id: Date.now(), text, done: false }] })), toggleTodo: (id) set((state) ({ todos: state.todos.map(todo todo.id id ? { ...todo, done: !todo.done } : todo) })), }); // 组合 store import { createStore } from swiz; const useAppStore createStore((...args) ({ ...createUserSlice(...args), ...createTodoSlice(...args), }));这种模式借鉴了 Redux Toolkit 中createSlice的思想保持了关注点分离让代码更易于维护和测试。swiz的妙处在于组合后的 store 在类型上依然是完美推断的。3.2 状态派生与计算属性在应用中我们经常需要从基础状态中派生出新的数据例如过滤后的待办事项列表、用户全名等。swiz鼓励将派生状态定义为 store 中的普通函数。const useTodoStore createStore((set, get) ({ todos: [/* ... */], filter: all, // 派生状态作为一个 getter 函数 get filteredTodos() { const { todos, filter } get(); switch (filter) { case completed: return todos.filter(t t.done); case active: return todos.filter(t !t.done); default: return todos; } }, // 或者作为一个方法 getCompletedTodos: () { const { todos } get(); return todos.filter(t t.done); }, }));在组件中你可以直接使用store.filteredTodos。由于swiz的响应式系统只有当todos或filter发生变化时依赖filteredTodos的组件才会更新。这里有一个重要的细节get()函数允许你在 action 或 getter 内部读取当前的最新状态而不会建立订阅关系。在上面的 getter 中我们使用get()来获取todos和filter这意味着如果这个 getter 被用在组件 A 中组件 A 会订阅todos和filter的变化。如果你希望一个函数完全不建立订阅它应该只在 action 内部被调用。3.3 处理异步操作与副作用状态管理离不开异步操作如数据获取。swiz处理异步的方式非常自然你可以在 action 中直接使用async/await。const useUserStore createStore((set) ({ user: null, loading: false, error: null, fetchUser: async (userId) { set({ loading: true, error: null }); try { const response await fetch(/api/users/${userId}); const userData await response.json(); set({ user: userData, loading: false }); } catch (err) { set({ error: err.message, loading: false }); } }, }));这比 Redux 中需要借助redux-thunk、redux-saga或redux-observable等中间件要简单直观得多。对于更复杂的副作用流如轮询、WebSocket 连接你可以将逻辑封装在 action 内部或者结合useEffect在组件层处理。swiz的哲学是“不替你做所有事”而是提供简洁的基元让你用熟悉的方式JavaScript组织逻辑。3.4 性能优化与选择性订阅细粒度响应式更新是swiz的默认优势但为了极致性能它提供了更高级的选择性订阅 API。最基本的useStore会订阅整个 store但如果你传递一个 selector 函数它将只订阅 selector 返回的部分状态。// 组件只会在 count 变化时重渲染 const count useCounterStore((state) state.count); // 组件只会在 todos 数组的引用或 filter 变化时重渲染 const { filteredTodos } useTodoStore((state) ({ filteredTodos: state.filteredTodos, })); // 错误的做法在 selector 中创建新对象或数组会导致不必要的重渲染 const badSelector (state) ({ todo: state.todos[0] }); // 每次都会返回新对象这里有一个关键的优化技巧swiz默认使用严格相等来比较 selector 的返回值。如果返回值是原始值string, number, boolean那没问题。但如果返回的是对象或数组即使内容没变每次调用 selector 都会返回一个新对象导致组件重新渲染。为了解决这个问题swiz提供了自定义相等性比较函数作为第三个参数或者你可以使用像lodash.isequal这样的深比较函数但需注意性能开销。更常见的做法是确保 selector 返回稳定的引用或者将派生状态定义在 store 内部如前文的filteredTodos这样在 store 内部可以通过get()读取依赖并缓存结果。4. 与主流方案的对比及选型考量4.1 与 Redux及 Redux Toolkit的对比Redux 是状态管理的标杆其核心优势在于严格的单向数据流、强大的中间件生态和无可比拟的可调试性Redux DevTools。它的模式非常明确适合超大型团队和需要高度可预测性的项目。但它的缺点也很明显样板代码多即使有 Redux Toolkit 简化学习曲线依然较陡峭。swiz可以看作是 Redux 理念的一个“简化且响应式”的实现。它牺牲了部分中间件生态虽然可以通过其他方式集成换来了极致的开发体验和更低的认知负荷。如果你的团队已经深度绑定 Redux 生态如redux-saga、redux-persist迁移成本可能较高。但如果是一个新项目或者你对 Redux 的复杂度感到疲惫swiz是一个强有力的竞争者。在可调试性上swiz也支持时间旅行通常通过浏览器扩展实现。4.2 与 MobX 的对比MobX 是响应式状态管理的代表其核心是“ observable action reaction ”。它写起来非常灵活和直观几乎感觉不到框架的存在。swiz在 API 简洁度和直观性上堪比 MobX但两者的底层理念不同。MobX 拥抱可变性通过装饰器或makeObservable显式标记可观察对象和动作。swiz则通过不可变的更新函数set来管理变化在概念上更接近 React 的setState或 Redux 的 reducer。从实践角度看swiz的集成更“Reactish”完全基于 Hooks无需学习 MobX 的observer、autorun等概念。对于纯 React 项目swiz的上手可能更快。MobX 的优势在于其不局限于 React可以用于任何 JavaScript 环境。4.3 与 Context useReducer 的对比这是 React 内置的方案零依赖。对于简单的、状态提升不深的场景它是完美的。但在复杂场景下它的缺点暴露无遗任何 Context 值的变动都会导致所有消费该 Context 的组件重新渲染除非你手动使用memo和useMemo进行大量优化。这在大应用中很容易引发性能问题。swiz在底层也使用了 Context但它通过 Proxy 和选择器实现了自动的、细粒度的订阅。你无需担心性能优化框架帮你处理了。因此当你的状态逻辑变得复杂或者需要跨多个组件共享状态时swiz是比原生 Context 更高效、更可维护的选择。4.4 与 Zustand 的对比你可能注意到swiz的 API 和理念与另一个非常流行的库Zustand惊人地相似。事实上它们可以被视为同一设计哲学下的不同实现。两者都追求极简 API、基于 Hook、零样板、响应式更新。在早期swiz可能被视为 Zustand 的一个替代或灵感来源。在选择时可以关注一些细微差别社区规模和生态Zustand 目前更流行、中间件支持、持久化插件的成熟度、与 React 并发特性如 Suspense, Transition的兼容性等。通常两者在核心功能上差异不大选择哪一个可能更多取决于个人或团队的偏好以及对特定周边工具的需求。5. 实战从零构建一个任务管理应用让我们通过一个更完整的例子——一个任务管理应用来串联swiz的核心用法。我们将实现用户认证、任务 CRUD、过滤和持久化。5.1 定义 Store 结构首先我们规划 store 的切片。我们将创建authSlice、todoSlice和uiSlice。// store/authSlice.ts import { StateCreator } from swiz; // 假设 swiz 导出类型 export interface User { id: string; name: string; email: string; } export interface AuthSlice { user: User | null; token: string | null; isLoading: boolean; login: (email: string, password: string) Promisevoid; logout: () void; setCredentials: (user: User, token: string) void; } export const createAuthSlice: StateCreatorAuthSlice (set) ({ user: null, token: null, isLoading: false, login: async (email, password) { set({ isLoading: true }); try { // 模拟 API 调用 const response await mockLoginApi(email, password); set({ user: response.user, token: response.token, isLoading: false }); // 持久化到 localStorage localStorage.setItem(auth, JSON.stringify({ user: response.user, token: response.token })); } catch (error) { set({ isLoading: false }); throw error; } }, logout: () { set({ user: null, token: null }); localStorage.removeItem(auth); }, setCredentials: (user, token) set({ user, token }), });// store/todoSlice.ts export interface Todo { id: string; text: string; completed: boolean; createdAt: Date; } export interface TodoSlice { todos: Todo[]; filter: all | active | completed; addTodo: (text: string) void; toggleTodo: (id: string) void; deleteTodo: (id: string) void; setFilter: (filter: TodoSlice[filter]) void; get filteredTodos(): Todo[]; // 计算属性 } export const createTodoSlice: StateCreatorTodoSlice (set, get) ({ todos: [], filter: all, addTodo: (text) { const newTodo: Todo { id: Date.now().toString(), text, completed: false, createdAt: new Date(), }; set((state) ({ todos: [newTodo, ...state.todos] })); }, toggleTodo: (id) { set((state) ({ todos: state.todos.map(todo todo.id id ? { ...todo, completed: !todo.completed } : todo ), })); }, deleteTodo: (id) { set((state) ({ todos: state.todos.filter(todo todo.id ! id) })); }, setFilter: (filter) set({ filter }), get filteredTodos() { const { todos, filter } get(); switch (filter) { case active: return todos.filter(t !t.completed); case completed: return todos.filter(t t.completed); default: return todos; } }, });5.2 组合 Store 并添加持久化中间件现在我们将切片组合起来并添加一个简单的持久化功能。swiz支持中间件模式允许你在状态设置前后插入逻辑。// store/index.ts import { createStore } from swiz; import { AuthSlice, createAuthSlice } from ./authSlice; import { TodoSlice, createTodoSlice } from ./todoSlice; // 定义完整的 Store 类型 export type AppStore AuthSlice TodoSlice; // 持久化中间件 const persistMiddleware (config: any) (set: any, get: any, api: any) { // 初始化时从 localStorage 读取 const savedAuth localStorage.getItem(auth); if (savedAuth) { try { const { user, token } JSON.parse(savedAuth); set({ user, token }); } catch (e) { console.error(Failed to parse saved auth, e); } } const savedTodos localStorage.getItem(todos); if (savedTodos) { try { const todos JSON.parse(savedTodos); set({ todos }); } catch (e) { console.error(Failed to parse saved todos, e); } } // 返回增强后的配置 return config( (...args: any[]) { // 在状态更新后持久化到 localStorage set(...args); const state get(); localStorage.setItem(todos, JSON.stringify(state.todos)); if (state.user state.token) { localStorage.setItem(auth, JSON.stringify({ user: state.user, token: state.token })); } else { localStorage.removeItem(auth); } }, get, api ); }; // 创建 store export const useAppStore createStoreAppStore()( persistMiddleware((...args) ({ ...createAuthSlice(...args), ...createTodoSlice(...args), })) );5.3 在组件中使用现在我们可以在组件中愉快地使用这个 store 了。// components/TodoList.tsx import React from react; import { useAppStore } from ../store; export const TodoList: React.FC () { // 使用 selector 只订阅需要的数据优化性能 const { filteredTodos, toggleTodo, deleteTodo, filter, setFilter } useAppStore((state) ({ filteredTodos: state.filteredTodos, filter: state.filter, toggleTodo: state.toggleTodo, deleteTodo: state.deleteTodo, setFilter: state.setFilter, })); // 对于函数可以直接从 store 中获取它们通常是稳定的引用 // const { addTodo } useAppStore(); return ( div div button onClick{() setFilter(all)} disabled{filter all}All/button button onClick{() setFilter(active)} disabled{filter active}Active/button button onClick{() setFilter(completed)} disabled{filter completed}Completed/button /div ul {filteredTodos.map(todo ( li key{todo.id} input typecheckbox checked{todo.completed} onChange{() toggleTodo(todo.id)} / span style{{ textDecoration: todo.completed ? line-through : none }} {todo.text} /span button onClick{() deleteTodo(todo.id)}Delete/button /li ))} /ul /div ); };// components/LoginForm.tsx import React, { useState } from react; import { useAppStore } from ../store; export const LoginForm: React.FC () { const [email, setEmail] useState(); const [password, setPassword] useState(); const { login, isLoading, user } useAppStore((state) ({ login: state.login, isLoading: state.isLoading, user: state.user, })); const handleSubmit async (e: React.FormEvent) { e.preventDefault(); try { await login(email, password); // 登录成功表单状态可由上层组件处理例如跳转 } catch (error) { alert(Login failed: ${error.message}); } }; if (user) { return divWelcome, {user.name}!/div; } return ( form onSubmit{handleSubmit} input typeemail value{email} onChange{(e) setEmail(e.target.value)} placeholderEmail required / input typepassword value{password} onChange{(e) setPassword(e.target.value)} placeholderPassword required / button typesubmit disabled{isLoading} {isLoading ? Logging in... : Login} /button /form ); };5.4 处理依赖注入与测试在实际项目中我们不应该在 store 中直接调用fetch或访问localStorage这会使测试变得困难。更好的做法是依赖注入。// store/authSlice.ts (改进版) export interface AuthApi { login: (email: string, password: string) Promise{ user: User; token: string }; } export const createAuthSlice (api: AuthApi): StateCreatorAuthSlice (set) ({ // ... 状态 login: async (email, password) { set({ isLoading: true }); try { const response await api.login(email, password); // 使用注入的 API set({ user: response.user, token: response.token, isLoading: false }); // ... 持久化 } catch (error) { // ... 错误处理 } }, // ... }); // 在创建 store 时注入真实的 API const realAuthApi: AuthApi { login: (email, password) fetch(/api/login, { method: POST, body: JSON.stringify({email, password}) }).then(r r.json()) }; export const useAppStore createStoreAppStore()( persistMiddleware((...args) ({ ...createAuthSlice(realAuthApi)(...args), // 注入 ...createTodoSlice(...args), })) ); // 在测试中我们可以注入一个模拟的 API const mockAuthApi: AuthApi { login: jest.fn().mockResolvedValue({ user: { id: 1, name: Test }, token: fake-token }) };6. 常见陷阱、性能优化与调试技巧6.1 避免不必要的重渲染尽管swiz有细粒度更新但不当使用仍会导致性能问题。陷阱1在 selector 中返回新对象// ❌ 错误每次都会返回一个新的对象字面量导致组件每次都重渲染 const userSettings useAppStore((state) ({ theme: state.theme, language: state.language })); // ✅ 正确分别选择或者使用 shallow 比较 const theme useAppStore((state) state.theme); const language useAppStore((state) state.language); // 或者如果必须返回对象使用 shallow 比较函数如果 swiz 提供类似 Zustand 的 shallow 工具 import { shallow } from swiz; // 假设有 const userSettings useAppStore((state) ({ theme: state.theme, language: state.language }), shallow);陷阱2在组件内派生复杂数据// ❌ 错误每次渲染都执行过滤且如果 todos 或 filter 没变结果可能相同但计算开销浪费了 function TodoList() { const { todos, filter } useAppStore(); const filteredTodos todos.filter(t filter all || t.completed (filter completed)); // ... } // ✅ 正确将派生状态放在 store 内部作为计算属性 // 如前文所示在 store 中定义 filteredTodos getter。6.2 处理循环依赖与 Action 间的调用有时一个 action 需要调用另一个 action或者读取其他 slice 的状态。swiz的set和get函数提供了这个能力。const useStore createStore((set, get) ({ user: null, preferences: { theme: light }, // Action A 更新用户 updateUser: (newUser) set({ user: newUser }), // Action B 需要读取用户信息并更新偏好 updateTheme: (theme) { const currentUser get().user; // 读取当前用户 if (!currentUser) return; // 可能调用另一个 action // get().updateUser({ ...currentUser, lastThemeUpdate: new Date() }); // 直接调用但要小心循环 // 更安全的方式是直接使用 set 进行原子更新 set((state) ({ preferences: { ...state.preferences, theme }, user: state.user ? { ...state.user, lastThemeUpdate: new Date() } : null, })); }, }));注意直接在 action 内部调用另一个 action (get().someAction()) 可能导致难以追踪的更新流和潜在的循环依赖。最佳实践是如果两个更新紧密相关应合并到一个set调用中以保证状态更新的原子性。如果逻辑必须分离请仔细设计并考虑使用中间件来记录 action 调用以便调试。6.3 调试与开发工具良好的开发体验离不开调试工具。swiz通常可以通过浏览器扩展如 React Developer Tools来观察状态变化。此外你可以实现一个简单的日志中间件来追踪所有状态更新。const logMiddleware (config) (set, get, api) config( (...args) { console.log( applying, args); set(...args); console.log( new state, get()); }, get, api ); const useStore createStore( logMiddleware((set) ({ count: 0, increment: () set((state) ({ count: state.count 1 })), })) );对于时间旅行调试可以寻找社区实现的swiz中间件或者使用基于swiz的、集成了 DevTools 的封装库。6.4 服务端渲染SSR与 Next.js 集成在 Next.js 或类似 SSR 框架中使用swiz需要注意水合问题。store 的初始状态需要在服务器端确定并传递给客户端以避免水合不匹配。基本模式是在服务器端创建 store 实例用数据填充它然后将其序列化并通过pageProps传递给客户端。在客户端创建 store 时使用这个预填充的状态进行初始化。// 在 _app.js 中 import { createStore } from swiz; // 创建一个创建 store 的函数允许传入初始状态 const createMyStore (preloadedState {}) createStore((set) ({ ...preloadedState, // ... 你的 actions })); // 在页面组件中通过 getServerSideProps 获取数据并初始化 store export async function getServerSideProps() { const serverStore createMyStore(); // 在服务器端调用 action 填充数据 // serverStore.getState().fetchData(); // 注意服务器端 actions 可能需要调整 // 更简单的方式直接构造初始状态 const initialData await fetchDataFromAPI(); const preloadedState { data: initialData }; return { props: { preloadedState } }; } function Page({ preloadedState }) { // 在客户端使用从服务器传递来的状态初始化 store const useClientStore createMyStore(preloadedState); const data useClientStore(state state.data); // ... }这是一个简化的示例实际项目中可能需要更复杂的封装来管理服务器端和客户端的 store 实例。社区可能有针对特定框架如 Next.js的swiz集成库可以简化这个过程。7. 总结与个人实践心得经过在几个不同规模的项目中实践swiz我对它的评价非常高。它成功地在我最看重的几个维度上取得了平衡简单性、类型安全、性能和可维护性。它让我从 Redux 的模板文件中解放出来又能享受到比原生 Context 更精细的控制。我最欣赏swiz的一点是它的“渐进式”理念。你不需要在项目一开始就设计一个庞大的状态树。可以从一个简单的 store 开始随着功能增长自然地将其拆分成多个切片。这种低侵入性使得它非常适合用于重构现有的、状态管理混乱的组件——你可以逐个将局部状态提升到swizstore 中而不会影响其他部分。当然它并非万能。在超大规模、需要严格审计和 action 日志追溯的企业级应用中Redux 及其强大的中间件生态可能仍是更稳妥的选择。对于需要与 RxJS 等响应式库深度集成的场景MobX 可能更合适。但就覆盖前端应用 90% 的状态管理场景而言swiz及其同类方案如 Zustand已经提供了近乎完美的解决方案。最后给打算尝试swiz的开发者一个小建议充分利用 TypeScript。swiz与 TypeScript 的配合是其最大亮点之一能极大地提升开发效率和代码可靠性。从项目开始就严格定义 store 的接口你会发现在重构和协作时事半功倍。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价