标签

SolidJS

Solid 是一种用于构建用户界面的声明式的、高效的 JavaScript 库。它的设计目标是提供类似于 React 的组件化开发体验,同时在性能和响应性方面进行优化。Solid 采用了响应性原语来实现高效更新,不使用虚拟 DOM,而是在编译时确定组件的更新逻辑,从而在运行时提供更快的渲染性能。

SolidJS
服务端5月27日 16:05
SolidJS Router 如何使用?有哪些高级特性?SolidJS Router 是 SolidJS 官方的客户端路由库,基于细粒度响应式系统构建,支持嵌套路由、数据预加载、懒加载和 SSR。它与 React Router、Vue Router 有何不同?核心差异在于路由状态天然响应式,数据获取与路由切换真正并行。下面从基础用法到高级特性逐层展开。 ## 基本使用 安装 @solidjs/router 后,在应用入口用 Router 包裹路由定义: ```jsx import { render } from "solid-js/web"; import { Router, Route } from "@solidjs/router"; import Home from "./pages/Home"; import About from "./pages/About"; render( () => ( <Router> <Route path="/" component={Home} /> <Route path="/about" component={About} /> </Router> ), document.getElementById("app") ); ``` 需要注意,新版本中不再需要 Routes 组件,Route 直接作为 Router 的子组件使用。 ## 根级布局 通过 Router 的 root 属性指定根布局组件,适合放置导航栏、侧边栏和全局 Context Provider。根布局在路由切换时不会重新渲染,这是性能优化的重要一环: ```jsx function Layout(props) { return ( <div> <nav> <A href="/">首页</A> <A href="/about">关于</A> </nav> <main>{props.children}</main> </div> ); } render(() => <Router root={Layout}> <Route path="/" component={Home} /> <Route path="/about" component={About} /> </Router>, document.getElementById("app")); ``` root 布局不会随路由切换而重新挂载,这使得全局状态(如登录态、主题切换)可以稳定地维持在布局层。 ## 动态路由参数 使用冒号语法定义动态参数,通过 useParams 钩子响应式地访问参数值: ```jsx import { useParams } from "@solidjs/router"; function UserProfile() { const params = useParams(); return <div>用户 ID: {params.id}</div>; } // 路由定义 <Route path="/users/:id" component={UserProfile} /> ``` useParams 返回的 params 是响应式对象,参数变化时组件自动更新。这一点与 React Router 的 useParams 不同——SolidJS 的版本天然具备细粒度响应性,不需要借助额外的状态管理。 ## 可选参数与通配符 在参数名后加问号声明可选参数,匹配有无该参数两种情况: ```jsx <Route path="/stories/:id?" component={Stories} /> ``` 通配符用 * 匹配任意后代路径,必须是路径的最后一段,可以命名: ```jsx <Route path="/docs/*rest" component={Docs} /> ``` 在组件内通过 params.rest 获取匹配的剩余路径,适合文档站、知识库等深层嵌套场景。 ## 路由导航 SolidJS Router 提供三种导航方式,覆盖声明式和编程式两大场景: **声明式导航**:A 组件会自动处理点击事件,并根据当前路径添加 active 状态类名,这是与普通 a 标签的核心区别: ```jsx import { A } from "@solidjs/router"; <A href="/about" activeClass="current">关于</A> ``` **编程式导航**:useNavigate 返回导航函数,适合在异步操作完成后跳转: ```jsx import { useNavigate } from "@solidjs/router"; function LoginButton() { const navigate = useNavigate(); const handleLogin = async () => { await login(); navigate("/dashboard", { replace: true }); }; return <button onClick={handleLogin}>登录</button>; } ``` navigate 的第二个参数支持 replace(替换历史记录,阻止用户回退)和 state(传递路由状态,目标页面通过 useLocation 获取)。 ## 查询参数管理 useSearchParams 提供对 URL 查询参数的读写能力,常用于分页、筛选等场景: ```jsx import { useSearchParams } from "@solidjs/router"; function ProductList() { const [searchParams, setSearchParams] = useSearchParams(); const page = () => parseInt(searchParams.page || "1"); return ( <div> <span>当前页: {page()}</span> <button onClick={() => setSearchParams({ page: page() + 1 })}> 下一页 </button> </div> ); } ``` setSearchParams 会合并更新查询参数,不会覆盖其他已有的参数。查询参数的变化也会触发响应式更新。 ## 嵌套路由 嵌套路由让父组件包裹子路由,通过 props.children 渲染子路由内容。这是构建复杂页面布局的关键模式: ```jsx <Router> <Route path="/users" component={UserLayout}> <Route path="/" component={UserList} /> <Route path="/:id" component={UserDetail} /> <Route path="/:id/edit" component={UserEdit} /> </Route> </Router> function UserLayout(props) { return ( <div class="user-section"> <Sidebar /> {props.children} </div> ); } ``` 只有叶节点(最内层的 Route)会被渲染为独立路由,父路由承担布局职责。嵌套层级没有上限,但建议控制在 3 层以内以保持可维护性。 ## MatchFilter 路径参数验证 MatchFilter 可以对动态路径参数施加验证规则,不满足条件的路径不会匹配该路由。支持三种验证方式: ```jsx import type { MatchFilters } from "@solidjs/router"; const filters: MatchFilters = { parent: ["mom", "dad"], // 枚举值——只匹配指定选项 id: /^\d+$/, // 正则表达式——只允许数字 slug: (v) => v.length > 3 && v.endsWith(".html"), // 自定义验证函数 }; <Route path="/users/:parent/:id/:slug" component={UserPage} matchFilters={filters} /> ``` MatchFilter 的执行顺序是枚举 → 正则 → 自定义函数。未通过验证的路径会跳过该路由,继续匹配后续定义,这种机制避免了在组件内部做参数校验的额外开销。 ## 路由守卫与权限控制 SolidJS Router 没有内置守卫 API,但通过组件组合可以灵活实现。核心思路是在父路由组件中检查认证状态,未认证则重定向: ```jsx import { Outlet, useNavigate } from "@solidjs/router"; import { createEffect } from "solid-js"; function AuthGuard() { const navigate = useNavigate(); createEffect(() => { const token = sessionStorage.getItem("token"); if (!token) { navigate("/signin", { replace: true }); } }); return <Outlet />; } // 路由配置 <Route path="/dashboard" component={AuthGuard}> <Route path="/" component={Dashboard} /> <Route path="/settings" component={Settings} /> </Route> ``` 这里用 Outlet 代替 props.children 渲染子路由,效果相同但语义更明确。认证检查放在 createEffect 中,当 token 状态响应式变化时会自动重新判断。 这种模式的优势在于:守卫逻辑与路由配置解耦,可以针对不同的路由层级应用不同的守卫策略(如管理员路由、付费用户路由等)。 ## 懒加载 使用 Solid 的 lazy 函数实现路由级代码分割,只在路由被访问时加载对应组件,显著减少首屏加载体积: ```jsx import { lazy, Suspense } from "solid-js"; const Dashboard = lazy(() => import("./pages/Dashboard")); const Settings = lazy(() => import("./pages/Settings")); <Suspense fallback={<Loading />}> <Route path="/dashboard" component={Dashboard} /> <Route path="/settings" component={Settings} /> </Suspense> ``` Suspense 包裹路由区域,在组件加载期间展示 fallback UI。与 React 的 Suspense 不同,Solid 的版本不依赖 Concurrent Mode,实现更轻量。 ## 数据预加载(Preload) Preload 是 SolidJS Router 区别于其他路由库的核心特性。它在路由加载或链接被悬停时提前获取数据,与组件懒加载并行执行,从根本上避免了数据瀑布流问题: ```jsx import { query } from "@solidjs/router"; // query 创建带缓存的数据获取函数 export const getUser = query(async (id) => { const res = await fetch(`/api/users/${id}`); return res.json(); }, "getUser"); // preload 函数在路由匹配时或链接悬停时被调用 export function preloadUser({ params, location, intent }) { return getUser(params.id); } <Route path="/users/:id" component={UserDetail} preload={preloadUser} /> ``` preload 函数接收三个参数: - **params**:当前路由参数,与 useParams 返回值一致 - **location**:位置对象,包含 pathname、search、hash、query、state、key - **intent**:触发原因,有四个值——initial(首次加载)、native(浏览器前进后退)、navigate(编程式导航)、preload(链接悬停预加载) 可以根据 intent 区分处理策略,比如只在 initial 和 navigate 时执行重量级数据获取,在 preload(悬停)时仅预取轻量数据。 query API 提供四层缓存机制: 1. 服务端请求去重(请求生命周期内) 2. 浏览器预加载缓存(5 秒) 3. 响应式重新获取(基于 key 变化自动触发) 4. 浏览器前进后退缓存(5 分钟) 这种多层缓存设计意味着:用户悬停链接时数据已经开始加载,点击时数据已就绪,回退时直接命中缓存,整个体验接近瞬时切换。 ## 配置对象方式 除了 JSX 声明式配置,还支持以对象数组定义路由,适合需要动态生成路由的大型项目: ```jsx const routes = [ { path: "/users", component: lazy(() => import("./pages/users")), children: [ { path: "/:id", component: lazy(() => import("./pages/users/[id]")), preload: preloadUser, }, ], }, ]; render(() => <Router>{routes}</Router>, document.getElementById("app")); ``` 配置对象方式与 JSX 方式功能完全等价,选择哪种取决于项目的代码组织偏好。 ## HashRouter 与 MemoryRouter SolidJS Router 提供两种替代路由模式,应对不同的部署和测试需求: - **HashRouter**:使用 URL hash 部分(#后面的内容)进行路由,不需要服务端配置重写规则,适合 GitHub Pages 等静态托管环境 - **MemoryRouter**:将路由历史保存在内存中,不操作浏览器 URL,适合单元测试和 Storybook 集成 ```jsx import { HashRouter, MemoryRouter } from "@solidjs/router"; // 静态部署——无需服务端配置 <HashRouter> <Route path="/" component={Home} /> </HashRouter> // 单元测试——不依赖浏览器 URL <MemoryRouter> <Route path="/" component={Home} /> </MemoryRouter> ``` ## 404 与错误处理 使用 * 通配符定义兜底路由,处理所有未匹配的路径: ```jsx <Route path="*404" component={NotFound} /> ``` 通配符路由可以在任意嵌套层级使用,确保子路由未匹配时也能正确兜底。命名通配符(如 *404 中的 "404")可以在组件内通过 useParams 获取。 ## useIsRouting 与路由过渡 useIsRouting 钩子返回布尔信号,指示路由是否正在过渡中,常用于顶部加载条: ```jsx import { useIsRouting } from "@solidjs/router"; function App() { const isRouting = useIsRouting(); return ( <div> {isRouting() && <LoadingBar />} {/* 路由内容 */} </div> ); } ``` 结合 Solid 的 Transition API,可以实现路由切换时的平滑过渡效果,新内容在加载完成后一次性替换,避免中间状态的闪烁。 ## 核心要点总结 | 特性 | 用途 | 核心 API | |------|------|----------| | 基本路由 | 路径与组件映射 | Router, Route | | 根级布局 | 全局布局不重渲染 | root 属性 | | 动态参数 | 路径参数提取 | useParams | | 可选参数与通配符 | 灵活路径匹配 | ? 和 * 语法 | | 路由导航 | 声明式与编程式跳转 | A, useNavigate | | 查询参数 | URL search 读写 | useSearchParams | | 嵌套路由 | 布局与子路由组合 | props.children, Outlet | | 参数验证 | 约束路径参数格式 | MatchFilter | | 路由守卫 | 认证与权限控制 | useNavigate, Outlet | | 数据预加载 | 并行获取避免瀑布流 | preload, query | | 懒加载 | 代码分割按需加载 | lazy, Suspense | | 路由过渡 | 加载状态反馈 | useIsRouting | | 替代模式 | 静态部署与测试 | HashRouter, MemoryRouter | SolidJS Router 的设计哲学是将路由与 Solid 的细粒度响应式系统深度融合。preload + query 的组合让数据获取与路由切换真正并行,四层缓存机制覆盖了从悬停预取到浏览器回退的全部场景——这是区别于 React Router 和 Vue Router 的核心优势。
服务端5月27日 16:01
SolidJS 和 React 有什么区别?前端框架该怎么选?SolidJS 和 React 都基于 JSX 和组件化思想,但底层运行机制完全不同。理解它们的核心差异,才能在项目选型时做出正确判断。 ## 渲染机制:虚拟 DOM vs 细粒度响应式 React 的核心是虚拟 DOM。每次状态变化,组件函数重新执行,React 通过 Diff 算法比较新旧虚拟 DOM 树,计算出最小 DOM 操作量再更新真实 DOM。这意味着即使只有一个状态变量改变,组件函数体也会完整执行一遍。 SolidJS 完全不使用虚拟 DOM。它在编译阶段分析模板,将状态与具体 DOM 节点建立绑定关系。状态变化时,只更新绑定的那几个 DOM 节点,组件函数只执行一次。 ```jsx // React:count 变化时,整个组件重新执行 function Counter() { const [count, setCount] = useState(0); console.log('组件重新执行'); // 每次 count 变化都会打印 return <p>{count}</p>; } // SolidJS:count 变化时,只更新 {count()} 对应的文本节点 function Counter() { const [count, setCount] = createSignal(0); console.log('组件只执行一次'); // 只打印一次 return <p>{count()}</p>; } ``` 这个差异直接决定了两个框架在频繁更新场景下的性能表现。 ## 状态管理对比 React 的 useState 返回的是值本身,读取状态就是读取一个普通变量。SolidJS 的 createSignal 返回的是一个 getter 函数,必须在响应式上下文(JSX 模板、createEffect、createMemo)中调用才能建立依赖追踪。 ```jsx // React:状态是值 const [count, setCount] = useState(0); console.log(count); // 直接读取值 // SolidJS:状态是函数 const [count, setCount] = createSignal(0); console.log(count()); // 必须调用函数读取值 ``` 对于复杂嵌套对象,SolidJS 提供了 createStore,支持细粒度嵌套更新——修改对象深层属性时,只触发依赖该属性的视图更新,而不是整个对象关联的视图都刷新。 ```jsx // SolidJS 的 createStore 支持嵌套细粒度更新 const [user, setUser] = createStore({ name: '张三', address: { city: '北京' } }); setUser('address', 'city', '上海'); // 只更新 city,name 关联的视图不受影响 ``` React 中要实现类似效果,需要配合 useReducer 和不可变数据更新模式,心智负担更重。 ## 副作用和派生状态 React 的 useEffect 需要手动声明依赖数组,遗漏或多余的依赖是常见 Bug 来源。SolidJS 的 createEffect 自动追踪依赖,在函数体内读取的任何信号都会被自动收集。 ```jsx // React:手动声明依赖 useEffect(() => { document.title = `${name} - ${count}`; }, [name, count]); // 遗漏依赖 = Bug // SolidJS:自动追踪 createEffect(() => { document.title = `${name()} - ${count()}`; // 自动追踪 name 和 count }); ``` 派生状态同理。React 的 useMemo 也需要手动依赖,SolidJS 的 createMemo 自动追踪: ```jsx // React const fullName = useMemo(() => `${firstName} ${lastName}`, [firstName, lastName]); // SolidJS const fullName = createMemo(() => `${firstName()} ${lastName()}`); ``` 自动追踪减少了出错概率,但也需要注意:在响应式上下文之外读取信号不会建立依赖,这是 SolidJS 新手常踩的坑。 ## 条件渲染和列表渲染 React 用 JavaScript 原生语法处理条件和列表,SolidJS 提供了专用的控制流组件,性能更优。 ```jsx // React 条件渲染 {isLoggedIn && <Dashboard />} // SolidJS 条件渲染(Show 组件,条件切换时不销毁 DOM) <Show when={isLoggedIn()}> <Dashboard /> </Show> ``` Show 组件在条件为 false 时不会渲染子组件,条件变为 true 时才创建。与 React 的 && 短路不同,Show 在条件反复切换时不会反复创建销毁,适合频繁切换的场景。 列表渲染方面,SolidJS 的 For 组件对每个列表项建立独立响应式绑定,更新单项时不需要重新映射整个列表: ```jsx // React:key 变化时重新渲染 {items.map(item => <Item key={item.id} data={item} />)} // SolidJS:每项独立响应 <For each={items()}> {(item) => <Item data={item} />} </For> ``` ## 服务端渲染和元框架 React 有 Next.js,SolidJS 有 SolidStart。两者的定位相似:提供路由、SSR、API 路由等全栈能力。 Next.js 的生态更成熟,App Router 支持 React Server Components(RSC),可以将组件标记为服务端组件,减少客户端 JavaScript 体积。SolidStart 目前没有 RSC 的等价方案,但依靠 SolidJS 本身极小的运行时体积(约 7KB,React 约 40KB),客户端 JS 体积已经很小。 SSR 场景下,SolidJS 支持 Streaming SSR 和同构渲染,与 React 18 的 Suspense Streaming 思路类似,但实现更轻量。 ## 实际性能差距 根据 JS Framework Benchmark 数据: | 指标 | React | SolidJS | |------|-------|---------| | 创建 1000 行表格 | 较慢 | 快 30-40% | | 更新单行 | 较慢 | 快 3-5 倍 | | 交换行 | 较慢 | 快 2-3 倍 | | 内存占用 | 较高 | 低 40-50% | | 核心包体积 | ~40KB | ~7KB | 这些数字在小型应用中感知不明显,但在大型数据表格、实时仪表盘、高频交互界面中,差距会变得显著。 ## 什么时候选 React - 团队已有 React 经验,学习成本几乎为零 - 项目依赖大量 React 生态库(antd、react-query、react-hook-form 等) - 需要 React Server Components 的能力 - 企业级应用,优先考虑人才招聘和长期维护 - 需要 Next.js 的成熟方案(ISR、边缘渲染等) ## 什么时候选 SolidJS - 应用对性能要求极高,尤其是大量动态列表或高频更新场景 - 包体积敏感(嵌入式 Web 应用、移动端 H5) - 团队愿意投入学习成本,追求更优雅的响应式模型 - 新项目从零开始,不依赖 React 特有生态 - 对内存占用有严格要求(低配设备、长运行时间应用) ## 迁移注意事项 从 React 迁移到 SolidJS,语法层面相似度高,但思维模式需要转换: 1. **组件只执行一次**——不要在组件体内写副作用逻辑,必须放在 createEffect 或事件处理中 2. **信号是函数不是值**——忘记调用 `count()` 而写成 `count` 是最常见的错误 3. **解构会丢失响应性**——`const { name } = props` 会丢失响应式追踪,必须用 `props.name` 或拆分信号 4. **没有 Hook 规则限制**——SolidJS 的响应式原语可以在任何地方调用,不限于组件顶层 5. **ref 不是 useEffect 的替代品**——onMount 和 onCleanup 更适合处理 DOM 操作 从 SolidJS 迁回 React 的情况较少见,但如果项目后续需要接入 React 独有生态(如 RSC),需要重写响应式逻辑。
服务端5月27日 16:01
SolidJS 有哪些性能优化技巧?如何避免常见的性能陷阱?SolidJS 的细粒度响应式系统本身已经具备出色的性能基础,但如果不理解其运行机制,仍然容易踩坑。以下是实际开发中最值得关注的优化技巧和常见陷阱。## 一、用 createMemo 缓存昂贵计算,但别滥用createMemo 会缓存计算结果,仅当依赖变化时才重新求值。对于涉及遍历、排序、过滤等开销较大的派生状态,使用 createMemo 能有效减少不必要的重复计算。```javascriptconst [items, setItems] = createSignal([]);// 昂贵计算用 memo 缓存const total = createMemo(() => items().reduce((sum, item) => sum + item.price, 0));```但对于简单表达式,普通函数就足够了,createMemo 本身也有开销:```javascript// 简单派生不需要 memoconst doubled = () => count() * 2;// 只有计算成本高时才值得 memoconst filtered = createMemo(() => items().filter(expensivePredicate));```**陷阱**:不要在 createMemo 中执行副作用。Memo 是纯函数,副作用应该放在 createEffect 中。## 二、用 batch 合并多次更新,减少渲染次数SolidJS 默认在同步代码中会逐次触发响应式更新。如果一个操作中需要修改多个信号,用 batch 包裹可以将它们合并为一次更新:```javascriptimport { batch } from 'solid-js';function handleFormSubmit(data) { batch(() => { setName(data.name); setAge(data.age); setEmail(data.email); });}```**陷阱**:在事件处理函数中,SolidJS 的批量机制已经自动生效,不需要手动 batch。但在 setTimeout、Promise 回调等异步场景中,batch 是必要的。## 三、For 与 Index 的选择:动态列表 vs 静态列表SolidJS 提供了两种列表渲染方式,选择错误会带来性能问题:- **For**:以对象引用作为 key,当列表项被插入或删除时,只会创建/销毁对应项,其余项保持不变。适合动态增删的列表。- **Index**:以数组索引作为 key,当列表顺序变化时,只会移动已有 DOM 节点而不会销毁重建。适合只读或顺序固定的列表。```javascript// 动态列表 - 用 For<For each={todos()}> {(todo) => <TodoItem text={todo.text} done={todo.done} />}</For>// 静态列表 - 用 Index 更高效<Index each={columns()}> {(col, i) => <Column name={col().name} index={i} />}</Index>```**陷阱**:用 Index 渲染频繁增删的列表,会导致 DOM 节点频繁创建和销毁,性能反而更差。## 四、用 lazy 和 Suspense 实现代码分割大型应用中,将非首屏组件用 lazy 延迟加载,能显著减小初始包体积:```javascriptimport { lazy, Suspense } from 'solid-js';const Dashboard = lazy(() => import('./Dashboard'));const Settings = lazy(() => import('./Settings'));function App() { return ( <Suspense fallback={<p>加载中...</p>}> <Dashboard /> <Settings /> </Suspense> );}```**技巧**:将 Suspense 边界放在尽可能靠近懒加载组件的位置,这样其他不受影响的内容可以正常显示。## 五、避免不必要的响应式包装SolidJS 的响应式系统会追踪信号读取。如果一个值不需要被追踪,就不要创建信号:```javascript// 错误:用 createSignal 包装不需要独立追踪的派生值const [doubled, setDoubled] = createSignal(count() * 2);// 正确:简单派生用函数即可const doubled = () => count() * 2;// 需要缓存时才用 createMemoconst expensive = createMemo(() => computeHeavy(items()));```**陷阱**:SolidJS 组件函数体只执行一次。如果在组件函数体中读取信号但不处于响应式上下文(如 JSX、createEffect、createMemo)内,该读取只发生在初始化阶段,后续信号变化不会触发更新。## 六、用 untrack 隔离不需要追踪的依赖在 createEffect 中读取信号会自动建立依赖关系。但有时你只需要读取当前值,不想因此触发 effect 重新执行:```javascriptimport { untrack } from 'solid-js';createEffect(() => { const currentUserId = userId(); // 依赖:userId 变化时重新执行 const theme = untrack(() => theme()); // 不追踪:theme 变化不会触发此 effect fetchUserData(currentUserId, theme);});```**典型场景**:日志记录、分析上报等需要读取但不需响应变化的场景。## 七、用 Store 管理复杂嵌套状态当状态是深层嵌套的对象时,用 createStore 替代多个 createSignal。Store 提供细粒度的嵌套响应式追踪,只有真正变化的属性才会触发更新:```javascriptimport { createStore } from 'solid-js/store';const [user, setUser] = createStore({ profile: { name: 'Alice', age: 28 }, settings: { theme: 'dark', lang: 'zh' }});// 只更新 profile.name,settings 不会触发任何更新setUser('profile', 'name', 'Bob');```**陷阱**:直接给 Store 属性赋整个新对象会丢失细粒度更新。使用 reconcile 可以智能地对比新旧数据,只更新变化的部分:```javascriptimport { reconcile } from 'solid-js/store';setUser(reconcile(newUserData));```## 八、不要解构 props这是 SolidJS 中最常见的反模式之一。解构会破坏响应式追踪,因为解构发生在组件函数体中(只执行一次),此时读取的是初始值:```javascript// 错误:解构丢失响应式function UserCard({ name, avatar }) { return <div>{name}</div>; // name 永远是初始值}// 正确:通过 props 函数调用保持响应式function UserCard(props) { return <div>{props.name}</div>;}```如果需要在派生逻辑中使用 props,用 mergeProps 或 splitProps:```javascriptimport { mergeProps, splitProps } from 'solid-js';function MyComponent(props) { const [local, rest] = splitProps(props, ['className', 'style']); return <div class={local.className} {...rest} />;}```## 九、用控制流组件替代条件表达式SolidJS 提供了 Show、Switch/Match 等控制流组件,它们比 JavaScript 条件表达式(三元运算符、&&)更高效,因为控制流组件能精确管理 DOM 节点的创建和销毁:```javascriptimport { Show, Switch, Match } from 'solid-js';// 用 Show 替代 && 运算符<Show when={isLoggedIn()} fallback={<LoginForm />}> <Dashboard /></Show>// 用 Switch/Match 替代多重三元表达式<Switch fallback={<p>未知状态</p>}> <Match when={status() === 'loading'}><Spinner /></Match> <Match when={status() === 'error'}><ErrorMessage /></Match> <Match when={status() === 'success'}><Content /></Match></Switch>```**技巧**:Show 的 when 还支持键函数(key function),当条件从 true 变为 false 再变回 true 时,可以用 key 控制是否复用 DOM:```javascript<Show when={selectedUser()} keyed> {(user) => <Profile name={user.name} />}</Show>```## 十、用 onCleanup 管理副作用生命周期SolidJS 组件没有 unmount 生命周期钩子,但 onCleanup 可以在任何响应式作用域中注册清理逻辑:```javascriptimport { onCleanup } from 'solid-js';function Timer() { const [count, setCount] = createSignal(0); const timer = setInterval(() => setCount(c => c + 1), 1000); onCleanup(() => clearInterval(timer)); return <p>{count()}</p>;}```**典型场景**:清除定时器、取消订阅、关闭 WebSocket 连接、释放 WebGPU 资源等。## 十一、避免在 createEffect 中写入信号在 effect 中写入信号是最容易导致无限循环的场景:```javascript// 危险:可能导致无限循环createEffect(() => { setCount(count() + 1); // count 变化 -> effect 重新执行 -> count 再变 -> ...});// 正确做法:用 createMemo 派生新值const nextCount = createMemo(() => count() + 1);// 如果确实需要根据依赖执行副作用,确保写入不同的信号createEffect(() => { const id = selectedId(); setFormData(loadForm(id)); // selectedId 和 formData 是不同信号,不会循环});```## 总结SolidJS 性能优化的核心思路是:理解细粒度响应式的工作机制,让系统只更新真正需要更新的部分。关键原则包括:合理使用 memo 和 batch、选择正确的列表渲染方式、避免破坏响应式追踪(不解构 props)、用控制流组件替代条件表达式、用 Store 管理复杂状态、以及不在 effect 中写入信号。掌握这些技巧后,SolidJS 的性能优势才能被充分发挥。
服务端5月27日 16:01
SolidJS 中的控制流组件有哪些?如何使用 Show、For、Switch 等?SolidJS 的控制流组件是框架响应式系统的核心部分,与 React 中直接使用 JavaScript 表达式不同,SolidJS 通过专用组件让响应式追踪更加精确,从而实现细粒度的 DOM 更新。下面逐一介绍每个控制流组件的用法和设计原理。 ## Show — 条件渲染 `Show` 是最常用的控制流组件,用于根据条件决定是否渲染内容。 ```jsx import { Show, createSignal } from "solid-js"; function App() { const [isLoggedIn, setIsLoggedIn] = createSignal(false); return ( <Show when={isLoggedIn()} fallback={<LoginButton />}> <UserDashboard /> </Show> ); } ``` **关键细节:** - `when` 的值会被响应式追踪,变化时只更新受影响的 DOM 节点 - `fallback` 在条件为 falsy 时显示,省略则不渲染任何内容 - 当条件从 `false` 变为 `true` 时,子元素不会被重新创建,而是重新插入 DOM(记忆化机制) - `when` 为 truthy 时,子元素函数可以接收 `when` 的值: ```jsx <Show when={currentUser()}> {(user) => <p>欢迎,{user().name}</p>} </Show> ``` **为什么不用 `&&` 和三元表达式?** 在 SolidJS 中,`{condition() && <Component />}` 虽然语法上可行,但每次条件变化时子元素会被销毁再重建。而 `Show` 会保留子元素的 DOM 引用,切回时直接复用,性能更优。 ## Switch / Match — 多条件分支 `Switch` 配合 `Match` 处理多个互斥条件,逻辑类似 JavaScript 的 `switch/case`。 ```jsx import { Switch, Match, createSignal } from "solid-js"; function StatusPanel() { const [status, setStatus] = createSignal("loading"); return ( <Switch fallback={<p>未知状态</p>}> <Match when={status() === "loading"}> <Spinner /> </Match> <Match when={status() === "success"}> <SuccessView /> </Match> <Match when={status() === "error"}> <ErrorNotice /> </Match> </Switch> ); } ``` **注意事项:** - `Match` 按书写顺序评估,第一个 `when` 为 truthy 的分支胜出 - `fallback` 是所有分支都不匹配时的默认内容 - 嵌套 `Show` 可以替代 `Switch`,但当条件超过两个时 `Switch` 更清晰 ## For — 键控列表渲染 `For` 是 SolidJS 中推荐的列表渲染方式,通过引用相等性追踪每个列表项。 ```jsx import { For, createSignal } from "solid-js"; function TodoList() { const [todos, setTodos] = createSignal([ { id: 1, text: "学习 SolidJS" }, { id: 2, text: "构建项目" }, ]); return ( <ul> <For each={todos()}> {(todo) => <li>{todo.text}</li>} </For> </ul> ); } ``` **For 与 Array.map 的区别:** | 特性 | `<For>` | `array.map()` | |------|---------|---------------| | 更新策略 | 只更新变化的项 | 重新映射整个数组 | | 键追踪 | 按引用相等自动追踪 | 无追踪 | | DOM 复用 | 复用已有 DOM 节点 | 条件变化时可能重建 | | 性能 | 大列表场景优势明显 | 小列表无感,大列表卡顿 | 当数组发生变化时,`For` 只会对新增、删除、移动的项操作 DOM,不会触及其他项的更新。这也是为什么 SolidJS 文档强烈推荐用 `For` 替代 `map`。 ## Index — 索引键控列表 `Index` 与 `For` 类似,但以索引位置作为键而非引用。 ```jsx import { Index } from "solid-js"; function ScoreBoard() { const [scores, setScores] = createSignal([95, 87, 72]); return ( <Index each={scores()}> {(score, i) => <div>第 {i + 1} 名:{score()} 分</div>} </Index> ); } ``` **For 还是 Index?** - 用 `For`:列表项是对象,顺序可能变化(如拖拽排序),需要按身份追踪 - 用 `Index`:列表项是原始值(string/number),位置稳定,值会更新 `Index` 的子元素函数中,每项是 getter 函数(`item()`),因为值可能在该位置上变化。而 `For` 的子元素函数直接接收项对象本身,因为身份不变。 ## Dynamic — 动态组件选择 `Dynamic` 根据运行时条件选择渲染哪个组件,适合标签页、多态渲染等场景。 ```jsx import { Dynamic, createSignal } from "solid-js"; import Home from "./Home"; import About from "./About"; import Contact from "./Contact"; const tabs = { home: Home, about: About, contact: Contact }; function App() { const [activeTab, setActiveTab] = createSignal("home"); return ( <> <nav> {Object.keys(tabs).map((key) => ( <button onClick={() => setActiveTab(key)}>{key}</button> ))} </nav> <Dynamic component={tabs[activeTab()]} /> </> ); } ``` `component` 属性接收组件函数或组件字符串(如 `"div"`),还可以通过其他属性传递 props。 ## Portal — 跨 DOM 层级渲染 `Portal` 将子元素渲染到 DOM 中的其他位置,常用于模态框、通知、下拉菜单等需要脱离当前组件 DOM 层级的场景。 ```jsx import { Portal } from "solid-js/web"; function Modal() { return ( <Portal mount={document.getElementById("modal-root")}> <div class="modal-overlay"> <div class="modal-content">模态框内容</div> </div> </Portal> ); } ``` `Portal` 解决的核心问题是 CSS 层叠上下文——模态框不会被父元素的 `overflow: hidden` 或 `z-index` 裁切。 ## Suspense — 异步加载协调 `Suspense` 配合资源(`createResource`)使用,在异步数据加载完成前显示 fallback。 ```jsx import { Suspense, createResource } from "solid-js"; function UserProfile() { const [user] = createResource(fetchUser); return ( <Suspense fallback={<Skeleton />}> <h2>{user().name}</h2> <p>{user().bio}</p> </Suspense> ); } ``` **嵌套 Suspense:** ```jsx <Suspense fallback={<PageSkeleton />}> <Suspense fallback={<AvatarSkeleton />}> <UserAvatar /> </Suspense> <Suspense fallback={<FeedSkeleton />}> <PostFeed /> </Suspense> </Suspense> ``` 外层 `Suspense` 等待所有子资源加载完成,内层各自独立 fallback,互不阻塞。这种模式让页面不同区域可以独立加载,而不是整个页面白屏等待。 ## ErrorBoundary — 错误边界 `ErrorBoundary` 捕获子组件树中的运行时错误,防止整个应用崩溃。 ```jsx import { ErrorBoundary } from "solid-js"; function App() { return ( <ErrorBoundary fallback={(err, reset) => ( <div> <p>出错了:{err.message}</p> <button onClick={reset}>重试</button> </div> )}> <UnstableComponent /> </ErrorBoundary> ); } ``` `fallback` 接收错误对象和 `reset` 函数,调用 `reset` 可以重新渲染子组件树。 ## 组件选择速查 | 场景 | 推荐组件 | |------|---------| | 简单条件显示/隐藏 | `Show` | | 多个互斥条件 | `Switch` + `Match` | | 渲染对象数组 | `For` | | 渲染原始值数组 | `Index` | | 运行时切换组件 | `Dynamic` | | 模态框/弹窗/通知 | `Portal` | | 异步数据加载 | `Suspense` | | 运行时错误兜底 | `ErrorBoundary` | **核心原则:** SolidJS 的控制流组件不是语法糖,它们与响应式系统深度集成,是精确追踪依赖和最小化 DOM 更新的关键。在实际开发中,优先使用这些组件而非 JavaScript 表达式,才能充分发挥 SolidJS 的细粒度响应式性能优势。
服务端5月27日 16:00
SolidJS 为什么不用虚拟 DOM?细粒度响应式如何实现更高性能?虚拟 DOM 曾是前端框架的核心创新,但 SolidJS 选择了一条不同的路——完全抛弃虚拟 DOM,转而依靠编译时优化和细粒度响应式系统来实现高性能。这种设计在 JS Framework Benchmark 中持续领先,更新速度可达 React 的 5-10 倍。 ## 虚拟 DOM 的性能瓶颈在哪? 虚拟 DOM 的工作流程是:状态变化 → 生成新虚拟树 → Diff 对比新旧树 → 最小化 DOM 更新。这个流程存在三个固有开销: 1. **全量渲染**:React 中 `setState` 触发后,整个组件函数重新执行,所有 JSX 表达式重新求值,即使只有一处状态改变 2. **Diff 计算**:需要遍历虚拟树进行逐层对比,组件树越大,Diff 成本越高 3. **内存占用**:同时持有新旧两棵虚拟树,对大型应用内存压力显著 ```javascript // React:状态更新触发组件重渲染 function TodoList({ items }) { const [filter, setFilter] = useState("all"); // filter 变化时,整个 TodoList 重新执行 // 即使 items 没变,所有子组件也会重渲染 return ( <div> <FilterBar filter={filter} onChange={setFilter} /> {items.filter(matchFilter(filter)).map(item => <TodoItem key={item.id} />)} </div> ); } ``` ## SolidJS 的核心架构:编译时 + 细粒度响应式 SolidJS 从两个层面同时优化:编译时将 JSX 转换为直接 DOM 操作,运行时通过 Signal 精准追踪依赖。 ### 编译时:JSX 到 DOM 指令 SolidJS 的编译器不会生成 `createElement` 调用,而是将 JSX 直接编译为真实的 DOM 创建和更新指令: ```javascript // 编译前:你写的 JSX function Counter() { const [count, setCount] = createSignal(0); return <div class="counter"><span>{count()}</span></div>; } // 编译后:实际运行的代码 function Counter() { const [count, setCount] = createSignal(0); const _el$ = document.createElement("div"); _el$.className = "counter"; const _el$2 = document.createElement("span"); // 关键:这里建立的是 signal 与 DOM 节点的直接绑定 insert(_el$2, count); _el$.appendChild(_el$2); return _el$; } ``` 编译产物中没有任何虚拟节点对象,也没有 Diff 算法。`insert` 函数在首次渲染时执行 DOM 插入,后续 `count` 变化时直接更新 `_el$2` 的文本内容。 ### 运行时:Signal 的依赖追踪机制 `createSignal` 创建的 Signal 内部维护一个订阅者集合。当 Signal 值被读取时,当前执行的响应式上下文(Effect、Memo 或表达式)自动注册为订阅者;当值被写入时,仅通知这些订阅者执行更新: ```javascript const [count, setCount] = createSignal(0); // 读取 count() 时,这个 Effect 自动注册为 count 的订阅者 createEffect(() => { document.getElementById("display").textContent = count(); }); // 只有上面的 Effect 会被触发,其他不依赖 count 的代码完全不受影响 setCount(1); ``` 这种机制的核心优势是**更新粒度到单个 DOM 节点级别**——一次 `setCount` 调用只会修改一个 `textContent`,不涉及组件重渲染、虚拟树对比或任何中间层。 ## 与 React 的关键差异对比 | 维度 | React | SolidJS | |------|-------|---------| | 状态更新 | 触发整个组件重渲染 | 仅更新绑定的 DOM 节点 | | 更新粒度 | 组件级 | 节点级 | | 编译策略 | JSX → React.createElement | JSX → 原生 DOM 操作 | | 运行时开销 | 虚拟树 Diff + 协调 | 无 Diff,直接 DOM 操作 | | 内存占用 | 持有虚拟树 | 仅 Signal + 订阅者集合 | | 组件模型 | 函数重执行 | 函数只执行一次 | "函数只执行一次"是 SolidJS 与 React 最本质的区别。React 的组件是渲染函数,每次状态更新都重新调用;SolidJS 的组件是设置函数,只在挂载时执行一次,后续更新完全由 Signal 驱动。 ## 实际性能表现 在 JS Framework Benchmark 的标准化测试中,SolidJS 的表现: - **创建行**:比 React 快约 3-4 倍 - **更新行**:比 React 快约 5-10 倍 - **内存占用**:约为 React 的 30%-50% - **包体积**:SolidJS 运行时约 7KB(gzip),React + ReactDOM 约 40KB+ 需要注意的是,这些数据来自极端场景的基准测试。在实际业务中,DOM 操作通常不是主要瓶颈,网络请求和数据处理往往占据更多时间。SolidJS 的优势在高频更新场景(实时数据、动画、大型表格)中最为明显。 ## 什么时候选择 SolidJS? **适合的场景**: - 实时数据仪表盘、金融行情等高频更新界面 - 大型列表或表格的渲染与交互 - 对首屏加载和运行时性能有严苛要求的应用 - 需要极小包体积的嵌入式或移动端场景 **需要权衡的方面**: - 生态系统远小于 React,第三方组件库较少 - 团队学习成本:响应式思维与 React 的单向数据流思维差异较大 - 社区和招聘资源相对有限 SolidJS 证明了虚拟 DOM 并非前端框架的必选项。通过编译时优化消除运行时开销,通过细粒度响应式实现精准更新,它在架构层面提供了另一种解决前端性能问题的思路。
服务端5月27日 15:53
SolidJS 响应式系统的工作原理是什么?SolidJS 的响应式系统是其区别于 React、Vue 等框架的核心设计,它放弃了虚拟 DOM,转而采用细粒度响应式 + 直接 DOM 更新的方式。理解这套机制,是掌握 SolidJS 的关键。 ## 响应式原语:Signal、Effect、Memo、Resource ### createSignal — 响应式状态的基本单元 createSignal 创建一个响应式状态,返回一个 getter 函数和 setter 函数: ```javascript const [count, setCount] = createSignal(0); // 读取:调用 getter console.log(count()); // 0 // 写入:调用 setter setCount(1); setCount(prev => prev + 1); // 支持函数式更新 ``` 关键点:getter 是一个函数而非变量引用。这是依赖追踪的前提——只有执行 count() 时,SolidJS 才能知道当前上下文依赖了这个 signal。setter 触发后,不会重跑整个组件,只会精确更新依赖 count() 的那些 DOM 节点或计算。 createSignal 还接受第二个参数用于配置,例如自定义相等性判断: ```javascript const [value, setValue] = createSignal(0, { equals: (newVal, oldVal) => newVal === oldVal }); ``` 当 equals 返回 true 时,setter 不会触发订阅者更新,这是避免无效渲染的一道防线。 ### createEffect — 自动追踪依赖的副作用 createEffect 在执行时自动收集内部读取的所有 signal,当这些 signal 变化时重新执行: ```javascript const [name, setName] = createSignal("Alice"); createEffect(() => { console.log(`Hello, ${name()}`); // 首次执行输出 "Hello, Alice" // setName("Bob") 后自动再次执行,输出 "Hello, Bob" }); ``` 与 React 的 useEffect 不同,这里不需要手动声明依赖数组。SolidJS 在运行时通过执行 getter 函数自动收集依赖,既减少了手写依赖的负担,也避免了依赖遗漏导致的 bug。 注意事项: - createEffect 在 DOM 更新之后、浏览器绘制之前执行,适合同步外部系统(日志、DOM 测量等) - 不要在 effect 中设置 signal 来派生状态,应使用 createMemo - effect 内条件性地读取 signal 会导致依赖不固定,每次执行收集到的依赖可能不同 ### createMemo — 缓存派生计算 createMemo 创建一个只读的派生 signal,只在依赖变化时重新计算: ```javascript const [count, setCount] = createSignal(0); const doubled = createMemo(() => count() * 2); console.log(doubled()); // 0 setCount(3); console.log(doubled()); // 6 — 自动重新计算 ``` createMemo 的价值在于避免重复计算。当多个 effect 或 DOM 节点依赖同一个派生值时,memo 保证计算只执行一次,结果被缓存和共享。 ### createResource — 异步数据加载 createResource 专门处理异步数据获取,返回一个响应式的信号对象: ```javascript const [user] = createResource(userId, async (id) => { const res = await fetch(`/api/users/${id}`); return res.json(); }); // 模板中可直接使用 <div>{user.loading ? "加载中..." : user()?.name}</div> ``` createResource 内置了 loading、error 状态管理,比手动用 createSignal + createEffect 管理异步更可靠。 ## 依赖追踪的底层机制 SolidJS 的依赖追踪基于发布-订阅模式,核心流程分三步: **第一步:收集。** 当 createEffect 或 createMemo 执行时,SolidJS 将当前计算上下文压入一个全局栈。此时任何 signal 的 getter 被调用,都会将当前上下文注册为该 signal 的订阅者。 **第二步:触发。** 当 signal 的 setter 被调用且值确实发生了变化(通过 equals 判断),signal 通知所有订阅者。 **第三步:调度。** 被通知的计算不会立即同步执行,而是被放入一个调度队列。SolidJS 会批量处理同一事件循环中的多个更新,然后按依赖顺序依次执行,避免重复计算。 这种设计保证了: - 更新是批量且有序的,不会出现中间态 - 一个 signal 变化不会导致无关的 effect 执行 - 依赖关系在运行时动态建立,条件分支中读取的 signal 会随执行路径变化 ## createStore — 深层响应式对象 createSignal 适合原始值和浅层状态,但面对嵌套对象时逐一创建 signal 不现实。createStore 通过 Proxy 实现深层响应式: ```javascript const [state, setState] = createStore({ user: { name: "Alice", address: { city: "Beijing" } } }); // 读取深层属性自动追踪 createEffect(() => { console.log(state.user.address.city); }); // 精确更新,只触发依赖 city 的 effect setState("user", "address", "city", "Shanghai"); ``` createStore 的关键特性是"按需追踪"——只有实际被读取的嵌套属性才会建立响应式关系。修改 state.user.address.city 不会触发只依赖 state.user.name 的 effect,这是细粒度响应式在对象层面的体现。 ## 批量更新与事务 默认情况下,同一事件处理函数中的多个 setter 会被自动批处理: ```javascript const [x, setX] = createSignal(0); const [y, setY] = createSignal(0); createEffect(() => { console.log(`x=${x()}, y=${y()}`); }); // 批量更新,effect 只执行一次 setX(1); setY(2); // 输出 "x=1, y=2",而不是先输出 "x=1, y=0" 再输出 "x=1, y=2" ``` 在异步回调中,可以使用 batch 函数手动批处理: ```javascript import { batch } from "solid-js"; setTimeout(() => { batch(() => { setX(1); setY(2); }); }, 1000); ``` ## onCleanup — 清理副作用 createEffect 中产生的副作用(定时器、事件监听、订阅)需要在 effect 重新执行或组件销毁时清理。onCleanup 注册清理回调: ```javascript createEffect(() => { const timer = setInterval(() => console.log(count()), 1000); onCleanup(() => clearInterval(timer)); }); ``` onCleanup 在 effect 重新执行前和所属组件销毁时自动调用,是防止内存泄漏的关键机制。 ## 与 React 的核心差异 理解 SolidJS 响应式系统的最好方式是与 React 对比: | 维度 | React | SolidJS | |------|-------|---------| | 更新粒度 | 组件级重渲染 | 表达式级精确更新 | | 依赖声明 | 手动 useEffect 依赖数组 | 运行时自动追踪 | | DOM 策略 | 虚拟 DOM diff + 批量更新 | 直接 DOM 操作 | | 组件行为 | 函数每次渲染重新执行 | 函数只执行一次 | | 状态原语 | useState 返回值引用 | createSignal 返回 getter 函数 | | 异步处理 | 手动管理 loading/error | createResource 内置状态 | React 的组件函数在每次 state 变化时重新执行,内部的变量、函数都会重新创建。SolidJS 的组件函数只在挂载时执行一次,后续更新全部由响应式系统驱动,不需要 re-render 这个概念。 ## 总结 SolidJS 响应式系统的核心思路是:通过 getter 函数调用实现运行时依赖收集,通过发布-订阅机制实现精确触发,通过调度队列实现批量有序更新。这套机制让 SolidJS 在保持声明式编程体验的同时,获得了接近原生 DOM 操作的性能。 掌握 createSignal、createEffect、createMemo、createStore 这四个原语及其背后的依赖追踪机制,是理解 SolidJS 的基础,也是前端面试中的高频考点。
服务端5月27日 15:52
SolidJS 如何实现服务端渲染(SSR)?有哪些渲染模式?SolidJS 是一个以细粒度响应式著称的前端框架,其服务端渲染(SSR)方案与 React、Vue 有本质区别——它不需要虚拟 DOM diff,也不依赖整棵组件树的重渲染。SolidJS 的 SSR 利用编译时优化,将 JSX 直接编译为高效的 DOM 操作和字符串拼接,因此在服务端渲染性能上具备天然优势。 ## SolidJS SSR 的核心原理 SolidJS 的服务端渲染基于一个关键设计:**编译时确定性**。在编译阶段,SolidJS 已经知道哪些响应式依赖会影响哪些 DOM 节点,所以服务端可以直接将组件渲染为 HTML 字符串,客户端水合时只需将响应式系统"接上"已有的 DOM,而不需要重新执行渲染逻辑。 这与 React 的 SSR 形成鲜明对比:React 在服务端调用 `renderToString` 生成 HTML,客户端仍需要执行完整的组件代码来重建虚拟 DOM 并进行 diff 对比。SolidJS 的水合过程要轻量得多——它只是在现有 DOM 节点上绑定事件监听器和响应式追踪,不产生额外的 JavaScript 执行开销。 ## 三种渲染模式详解 ### 1. 静态生成(SSG) SSG 适用于内容固定的页面,在构建时生成 HTML 文件,部署后由 CDN 直接返回,无需服务器运行时参与。 ```javascript import { renderToStringAsync } from "solid-js/web"; export async function getStaticPaths() { return ["/about", "/contact"]; } export default async function Page({ params }) { const html = await renderToStringAsync(() => <Component params={params} />); return html; } ``` `renderToStringAsync` 是 SSG 的核心 API。它与同步版本的 `renderToString` 不同之处在于:支持 `createResource` 等异步数据获取操作,会等待所有 Suspense 边界内的异步操作完成后才输出最终 HTML。这意味着 SSG 生成的页面是数据完整的,不存在客户端闪烁问题。 **适用场景**:博客、文档站、营销页面等更新频率低的内容型站点。 ### 2. 服务端渲染(SSR) SSR 在每次请求时动态生成 HTML,保证用户看到的是最新的数据状态。 ```javascript // 服务端入口 import { renderToStringAsync } from "solid-js/web"; import App from "./App"; export default async function handler(req) { const html = await renderToStringAsync(() => <App url={req.url} />); return new Response(html, { headers: { "Content-Type": "text/html" }, }); } // 客户端入口 - 水合 import { hydrate } from "solid-js/web"; import App from "./App"; hydrate(() => <App />, document.getElementById("app")); ``` 关键点在于 `hydrate` 函数。它不会重新渲染 DOM,而是扫描已有的 HTML 结构,只做三件事:绑定事件处理器、建立响应式依赖追踪、注册副作用。这个过程的开销远小于 React 的 `hydrateRoot`,因为 React 需要遍历整棵虚拟 DOM 树进行一致性校验,而 SolidJS 直接跳过了这一步。 在 SolidStart 框架中,SSR 的配置更加简化: ```javascript import { StartServer, createHandler } from "@solidjs/start/server"; export default createHandler(() => ( <StartServer document={({ assets, children }) => ( <html> <head>{assets}</head> <body>{children}</body> </html> )} /> )); ``` `createHandler` 封装了路由匹配、数据预取和 HTML 生成的完整流程,`assets` 插槽自动注入编译后的 CSS 和 JavaScript 资源引用。 **适用场景**:电商商品页、用户仪表盘、搜索结果页等需要实时数据的动态页面。 ### 3. 流式渲染(Streaming SSR) 流式渲染是 SolidJS SSR 中性能最优的模式,它将页面分块逐步发送到客户端,用户不必等待整个页面渲染完成就能看到内容。 ```javascript import { renderToStream } from "solid-js/web"; export default async function handler(req, res) { const stream = renderToStream(() => <App url={req.url} />); stream.pipe(res); } ``` 流式渲染的工作机制是:组件树中 Suspense 边界以上的部分立即输出 HTML,Suspense 内部的异步内容在数据就绪后以 script 标签的形式注入到流中,客户端收到后自动替换占位内容。这样页面的首屏渲染时间(FCP)可以大幅缩短。 `deferStream` 选项控制流式传输的行为: ```javascript const [data] = createResource(fetchData, { deferStream: true // 数据未就绪时暂停流,而不是先发送占位符 }); ``` 当 `deferStream` 为 `true` 时,渲染会等待数据返回后再继续推送 HTML,适用于 SEO 敏感的场景(搜索引擎爬虫可能不会执行流式注入的 script)。设为 `false` 则先发送 fallback UI,数据到达后再替换,适合用户体验优先的场景。 **适用场景**:内容丰富的列表页、包含多个数据源的聚合页面、对首屏性能要求极高的应用。 ## 三种模式对比 | 特性 | SSG | SSR | 流式渲染 | |------|-----|-----|----------| | 渲染时机 | 构建时 | 请求时 | 请求时(分块) | | 数据实时性 | 静态 | 实时 | 实时 | | 首屏速度 | 最快(CDN) | 中等 | 快(渐进输出) | | 服务器压力 | 无 | 每次请求渲染 | 每次请求渲染 | | SEO 友好度 | 高 | 高 | 中(需配置 deferStream) | | 适用场景 | 静态内容 | 动态内容 | 混合内容 | ## 数据获取与同构设计 SolidJS 使用 `createResource` 统一处理服务端和客户端的数据获取: ```javascript function ProductList() { const [products] = createResource(fetchProducts); return ( <Switch> <Match when={products.loading}> <p>加载中...</p> </Match> <Match when={products.error}> <p>加载失败</p> </Match> <Match when={products()}> <ul> {products().map(item => <li key={item.id}>{item.name}</li>)} </ul> </Match> </Switch> ); } ``` 在服务端,`createResource` 会自动执行数据获取函数并等待结果;在客户端水合后,如果数据已被序列化到页面中,则直接使用缓存值,避免重复请求。这种序列化通过 `@solidjs/start` 内置的传输层自动完成,开发者无需手动处理。 判断运行环境也很常见: ```javascript import { isServer } from "solid-js/web"; function Component() { const data = isServer ? readFromDatabase() // 服务端直接读取 : fetchFromAPI(); // 客户端走网络请求 return <div>{data}</div>; } ``` 注意 `isServer` 是编译时常量,SolidJS 编译器会在打包时将对应分支的代码从另一端的 bundle 中移除,不存在运行时开销。 ## Hydration 的性能优势 SolidJS 水合过程的性能优势来源于其架构设计: - **无虚拟 DOM**:不需要在客户端重建组件树来对比差异 - **细粒度更新**:响应式系统只追踪实际使用到的数据依赖,不涉及组件级别的比较 - **编译时优化**:模板中的静态部分和动态部分在编译时已经分离,水合时只处理动态绑定 实测数据表明,在同等复杂度的页面上,SolidJS 的水合速度比 React 快 3-5 倍,内存占用减少约 60%。这使得 SolidJS 特别适合交互密集、组件层级深的应用场景。 ## 实践建议 **选择渲染模式时**:优先考虑 SSG,内容变化不频繁就用静态生成;需要实时数据时用 SSR;页面数据源多、首屏要求高时用流式渲染。 **性能优化方向**:合理使用 Suspense 边界划分流式渲染的分块;利用 `deferStream` 在 SEO 和用户体验之间找到平衡;避免在组件顶层创建不必要的响应式状态,减少水合时需要绑定的追踪关系。 **避坑要点**:不要在服务端使用浏览器 API(window、document 等),用 `isServer` 做条件守卫;确保 `createResource` 的数据获取函数在服务端和客户端都能正确执行;流式渲染下 SEO 爬虫可能看不到异步内容,关键数据应考虑 SSR 或 SSG 模式。
服务端5月27日 15:52
SolidJS 中如何管理复杂状态?有哪些状态管理方案?SolidJS 的响应式系统与其他框架有本质区别:它不依赖虚拟 DOM,而是通过细粒度响应式追踪实现精确更新。理解这一点,是选择正确状态管理方案的前提。本文从实际场景出发,逐一拆解 SolidJS 内置的状态管理原语,帮你建立清晰的选择思路。 ## 一、基础原语:createSignal 与 createStore ### createSignal:简单值的首选 `createSignal` 是 SolidJS 最基础的响应式原语,返回一个 getter 和一个 setter。适用于管理原始类型或简单对象状态。 ```javascript import { createSignal } from 'solid-js'; function Counter() { const [count, setCount] = createSignal(0); // getter 调用触发依赖追踪 // 注意:必须调用 count() 而非 count return ( <button onClick={() => setCount(prev => prev + 1)}> 点击了 {count()} 次 </button> ); } ``` 关键点:getter 是函数调用,这是 SolidJS 响应式追踪的入口。只有执行 `count()` 时,SolidJS 才知道当前代码依赖了这个信号。 setter 支持直接赋值和函数式更新两种方式: ```javascript // 直接赋值 setCount(5); // 基于前值更新(推荐) setCount(prev => prev + 1); ``` **适用场景**:计数器、开关状态、表单输入值等简单状态。 ### createStore:嵌套对象的精确更新 当状态是深层嵌套对象时,`createSignal` 会引发问题——替换整个对象会导致所有依赖该对象的组件重新渲染,即使只改了一个字段。 `createStore` 解决了这个问题:它对对象的每个属性都建立独立的响应式追踪,修改 `state.user.name` 只会触发依赖 `name` 的代码更新。 ```javascript import { createStore } from 'solid-js/store'; const [state, setState] = createStore({ user: { name: '张三', age: 25 }, settings: { theme: 'dark', lang: 'zh' } }); // 精确更新:只触发依赖 user.name 的响应 setState('user', 'name', '李四'); // 函数式更新 setState('user', 'age', prev => prev + 1); // 批量更新同一层级 setState('user', { name: '王五', age: 30 }); ``` `createStore` 的 setter 使用路径语法,逐层指定要更新的属性位置,实现了对象级别的细粒度响应式。 **适用场景**:用户信息、配置对象、表单状态、列表数据等结构化状态。 ## 二、派生状态:createMemo 与 createComputed ### createMemo:缓存计算结果 `createMemo` 创建一个只读的派生信号,只在依赖变化时重新计算。适合将计算逻辑从模板中抽离,避免每次渲染都重复执行。 ```javascript import { createMemo } from 'solid-js'; function ShoppingCart() { const [items, setItems] = createStore({ list: [] }); // 只在 items.list 变化时重新计算 const totalPrice = createMemo(() => items.list.reduce((sum, item) => sum + item.price * item.quantity, 0) ); const itemCount = createMemo(() => items.list.reduce((sum, item) => sum + item.quantity, 0) ); return ( <div> <p>共 {itemCount()} 件商品</p> <p>合计 ¥{totalPrice()}</p> </div> ); } ``` `createMemo` 的值会被缓存,多个消费者读取同一个 memo 不会触发重复计算。 ### createComputed:立即执行的副作用 `createComputed` 类似 `createMemo`,但不缓存返回值,用于需要在依赖变化时立即执行副作用的场景(如同步 DOM 操作、日志记录)。它在响应式系统中属于同步执行的观察者。 ```javascript import { createComputed } from 'solid-js'; // 当路由变化时自动更新页面标题 createComputed(() => { document.title = `${currentRoute().name} - 我的应用`; }); ``` 日常开发中 `createMemo` 更常用,`createComputed` 主要用于需要同步响应的场景。 ## 三、异步状态:createResource `createResource` 是 SolidJS 处理异步数据的核心方案,内置 loading/error 状态管理,与 Suspense 深度集成。 ```javascript import { createResource } from 'solid-js'; async function fetchUser(id) { const res = await fetch(`/api/users/${id}`); return res.json(); } function UserProfile(props) { const [user, { mutate, refetch }] = createResource( () => props.userId, // source signal:当 userId 变化时自动重新请求 fetchUser ); return ( <div> {/* 内置 loading 和 error 状态 */} <Show when={user.loading}> <p>加载中...</p> </Show> <Show when={user.error}> <p>加载失败:{user.error.message}</p> </Show> <Show when={user()}> <h2>{user().name}</h2> <p>{user().email}</p> </Show> <button onClick={() => refetch()}>刷新</button> </div> ); } ``` `createResource` 返回的第二个对象包含两个实用方法: - **mutate**:手动设置数据,跳过请求(乐观更新的关键) - **refetch**:重新触发请求 乐观更新示例: ```javascript const [todos, { mutate }] = createResource(fetchTodos); async function addTodo(text) { const newTodo = { id: Date.now(), text, done: false }; // 先在本地更新 mutate(prev => [...prev, newTodo]); // 再发请求,失败则回滚 try { await api.addTodo(newTodo); refetch(); // 用服务端数据同步 } catch { mutate(prev => prev.filter(t => t.id !== newTodo.id)); } } ``` **适用场景**:API 数据获取、需要 loading/error 状态的异步操作。 ## 四、跨组件状态共享 ### Context API:作用域内的全局状态 SolidJS 的 Context API 与 React 类似,但利用了细粒度响应式,Provider 值的变化只会更新实际使用该值的组件。 ```javascript import { createContext, useContext } from 'solid-js'; const ThemeContext = createContext(); function ThemeProvider(props) { const [theme, setTheme] = createSignal('light'); const toggle = () => setTheme(prev => prev === 'light' ? 'dark' : 'light'); return ( <ThemeContext.Provider value={{ theme, toggle }}> {props.children} </ThemeContext.Provider> ); } // 子组件中使用 function ThemedButton() { const { theme, toggle } = useContext(ThemeContext); return ( <button style={{ background: theme() === 'dark' ? '#333' : '#fff' }} onClick={toggle} > 切换主题 </button> ); } ``` ### 模块级 Signal:最简单的全局状态 对于不需要作用域隔离的简单全局状态,直接在模块顶层创建信号即可: ```javascript // store.js import { createSignal } from 'solid-js'; const [currentUser, setCurrentUser] = createSignal(null); export { currentUser, setCurrentUser }; // 任何组件直接 import 使用 import { currentUser, setCurrentUser } from './store'; ``` 这种方式比 Context 更简洁,但缺乏作用域隔离,适合小型应用或真正全局的状态(如登录用户信息)。 ## 五、高级工具:produce、reconcile 与 unwrap `createStore` 的配套工具函数在复杂场景下必不可少。 ### produce:类 Immer 的可变写法 `produce` 允许在 store 更新中使用可变写法,底层仍然是不可变更新: ```javascript import { produce } from 'solid-js/store'; // 不用 produce:路径语法 setState('items', items => items.map(item => item.id === id ? { ...item, done: true } : item )); // 使用 produce:可变写法,更直观 setState(produce(state => { const item = state.items.find(i => i.id === id); if (item) item.done = true; })); ``` ### reconcile:高效对比更新 当服务端返回完整数据需要覆盖本地 store 时,`reconcile` 会做精细对比,只更新变化的属性,避免不必要的响应触发: ```javascript import { reconcile } from 'solid-js/store'; // 服务端返回新数据 const serverData = await fetchFullState(); // reconcile 对比后只更新变化的部分 setState(reconcile(serverData)); // 对比:直接赋值会替换整个对象,触发所有依赖响应 // setState(serverData); // 不推荐 ``` ### unwrap:读取原始数据 `createStore` 返回的 state 是 Proxy 对象,在某些场景(序列化、传给非 Solid 代码)需要拿到原始对象: ```javascript import { unwrap } from 'solid-js/store'; const rawState = unwrap(state); console.log(JSON.stringify(rawState)); // 可以正常序列化 ``` ## 六、方案选择指南 | 场景 | 推荐方案 | 理由 | |------|----------|------| | 简单原始值(开关、计数) | createSignal | 最轻量,getter/setter 足够 | | 嵌套对象、列表 | createStore | 细粒度追踪,避免整体替换 | | 派生计算值 | createMemo | 缓存计算,依赖追踪 | | 异步数据获取 | createResource | 内置 loading/error,Suspense 集成 | | 同步副作用 | createComputed | 立即执行,不缓存 | | 组件树共享状态 | Context API | 作用域隔离,避免 prop drilling | | 真正全局的状态 | 模块级 Signal | 最简洁,无需 Provider | | Store 可变更新 | produce | 直观写法,底层不可变 | | 服务端数据覆盖 | reconcile | 精细对比,最小化更新 | 选择的核心原则:**从简单方案开始,按需升级**。大部分场景下 createSignal + createStore + createResource 已经足够,不需要引入额外状态管理库。 ## 七、常见误区 **误区一:给 createStore 套 React 思维**。SolidJS 的 Store 不需要 reducer/action,直接用 `setState` 路径语法或 `produce` 即可更新。不需要模仿 Redux 的模式。 **误区二:过度使用 Context**。Context 适合主题、国际化等需要作用域隔离的状态。如果是全局唯一的登录信息,模块级 Signal 更简洁。 **误区三:忽略 getter 调用**。`createSignal` 返回的是 getter 函数,必须在响应式上下文(组件函数体、createMemo/createEffect 内)中调用才能建立依赖追踪。在 setTimeout 或事件回调中读取值不会建立追踪关系——这本身没问题,但需要理解其中的区别。 **误区四:createResource 不需要手动管理 loading**。`createResource` 与 `<Suspense>` 配合时可以自动处理 loading 状态,但你仍然可以在组件内通过 `resource.loading` 做更细粒度的控制。
服务端5月27日 15:52
SolidJS 组件生命周期有哪些钩子?与 React 有什么区别?## SolidJS 有哪些生命周期钩子? SolidJS 的生命周期设计与 React、Vue 等框架截然不同。它的组件函数只会执行一次,后续的状态变更通过细粒度的响应式系统直接更新 DOM,而不需要重新执行组件函数。这种设计使得 SolidJS 只需要少量的生命周期钩子就能覆盖绝大部分场景。 SolidJS 提供三个核心生命周期函数:`onMount`、`onCleanup`、`onError`,以及响应式原语 `createEffect` 来处理副作用。 ## onMount:组件挂载后执行一次 `onMount` 在组件首次渲染完成之后执行,且只执行一次。它本质上是 `createEffect` 的一个不追踪依赖的变体,内部实现相当于 `createEffect(() => untrack(fn))`。 适用场景包括:数据请求、DOM 操作、订阅初始化等只需要在挂载时执行一次的逻辑。 ```javascript import { onMount, createSignal } from "solid-js"; function UserProfile(props) { const [user, setUser] = createSignal(null); onMount(async () => { const res = await fetch(`/api/users/${props.id}`); setUser(await res.json()); }); return <div>{user()?.name}</div>; } ``` 注意:`onMount` 的回调函数不支持返回清理函数。如果需要清理,请在 `onMount` 内部调用 `onCleanup`。 ## onCleanup:响应式作用域销毁时执行 `onCleanup` 注册一个清理函数,当所在的响应式作用域被销毁或重新计算时触发。它可以在组件体、`createEffect`、`onMount` 等任何响应式上下文中使用。 ```javascript import { onCleanup, createSignal } from "solid-js"; function Timer() { const [seconds, setSeconds] = createSignal(0); const interval = setInterval(() => setSeconds(s => s + 1), 1000); onCleanup(() => clearInterval(interval)); return <div>Elapsed: {seconds()}s</div>; } ``` 关键细节:`onCleanup` 在组件卸载时触发;在 `createEffect` 内使用时,每次 effect 重新执行前也会触发上一次注册的清理函数。在 SSR 环境中,`onMount` 和 `createEffect` 不会执行,但直接在组件体中调用的 `onCleanup` 仍会执行,这可能导致意外行为。 ## onError:子作用域错误捕获 `onError` 注册一个错误处理函数,当子作用域抛出异常时触发。只有最近的父级 `onError` 会执行,类似 JavaScript 的异常冒泡机制。如果在处理器中重新抛出错误,它会继续向上传播。 ```javascript import { onError, createSignal } from "solid-js"; function SafeComponent() { const [data, setData] = createSignal(null); onError((err) => { console.error("Child scope error:", err); }); return <ChildThatMightThrow />; } ``` ## createEffect:自动追踪依赖的响应式副作用 `createEffect` 是 SolidJS 响应式系统的核心原语。它会自动追踪回调函数中读取的所有 Signal,当任意依赖变化时重新执行。第一次执行总是在组件挂载之后。 ```javascript import { createEffect, createSignal } from "solid-js"; function SearchBox() { const [query, setQuery] = createSignal(""); createEffect(() => { console.log("Searching:", query()); // 自动追踪 query 这个 Signal 的依赖 }); return <input onInput={(e) => setQuery(e.target.value)} />; } ``` 与 React 的 `useEffect` 不同,`createEffect` 不需要手动声明依赖数组,也不支持返回清理函数。需要清理时,在 `createEffect` 内部调用 `onCleanup`。 ## SolidJS 与 React 生命周期对比 两者在设计哲学上存在根本性差异: ### 组件执行机制 React 的组件函数在每次状态更新时都会重新执行,hooks 依靠调用顺序来维持状态。SolidJS 的组件函数只执行一次,状态更新通过 Signal 直接触发 DOM 更新。 ### 依赖追踪方式 React 的 `useEffect` 需要手动维护依赖数组,遗漏依赖是常见的 bug 来源。SolidJS 的 `createEffect` 自动追踪依赖,读取了哪些 Signal 就订阅哪些,无需开发者手动管理。 ### 副作用清理 React 在 `useEffect` 回调中返回清理函数。SolidJS 使用独立的 `onCleanup` 函数,可以在任何响应式上下文中调用,更加灵活。 ### Hooks 调用限制 React 的 hooks 不能在条件语句、循环或嵌套函数中调用(Rules of Hooks)。SolidJS 没有这个限制,因为组件只执行一次,不存在调用顺序依赖的问题。 | 对比项 | React | SolidJS | |--------|-------|---------| | 组件函数执行 | 每次渲染重新执行 | 只执行一次 | | 副作用钩子 | `useEffect` | `createEffect` | | 依赖管理 | 手动声明依赖数组 | 自动追踪 | | 挂载钩子 | `useEffect(fn, [])` | `onMount(fn)` | | 清理机制 | `useEffect` 返回函数 | `onCleanup(fn)` | | 错误处理 | Error Boundary 组件 | `onError(fn)` | | 条件调用 hooks | 不允许 | 允许 | | 更新粒度 | 组件级重渲染 | 细粒度 DOM 更新 | ## 实际开发中的注意事项 **避免在组件函数体中直接读取 Signal**。在组件函数体(非 `createEffect` 等响应式上下文)中读取 Signal 只会拿到初始值,不会建立响应式绑定。响应式逻辑应放在 `createEffect`、`createMemo` 或 JSX 表达式中。 **`onMount` 中的异步操作**。`onMount` 支持异步回调,但如果异步操作完成后组件已卸载,更新 Signal 不会报错但也不会反映到 DOM。建议在 `onMount` 中配合 `onCleanup` 设置取消标记。 **SSR 中的行为差异**。`onMount` 和 `createEffect` 在服务端渲染时不会执行,但组件函数体中的 `onCleanup` 会执行。需要确保清理逻辑不会依赖仅客户端存在的资源。
服务端5月27日 15:52
SolidJS 如何进行单元测试和集成测试?有哪些测试工具推荐?SolidJS 的测试生态以 Vitest 为核心,搭配官方测试库 @solidjs/testing-library,可以覆盖从信号级单元测试到组件集成测试的完整链路。下面从环境搭建、单元测试、响应式测试、集成测试到最佳实践逐一展开。 ## 环境搭建 安装测试依赖: ```bash npm install -D vitest @solidjs/testing-library @testing-library/jest-dom jsdom @testing-library/user-event ``` 在 vite.config.ts 中配置 Vitest: ```typescript import { defineConfig } from "vite"; import solidPlugin from "vite-plugin-solid"; export default defineConfig({ plugins: [solidPlugin()], test: { globals: true, environment: "jsdom", transformMode: { web: [/\.jsx?$/] }, setupFiles: "./test/setup.ts", }, }); ``` 创建 test/setup.ts 文件,注册 jest-dom 匹配器: ```typescript import "@testing-library/jest-dom"; ``` 在 package.json 中添加脚本: ```json { "scripts": { "test": "vitest", "test:coverage": "vitest run --coverage" } } ``` ## 单元测试:组件渲染与交互 组件测试的核心思路是渲染组件、查找元素、模拟交互、断言结果,与用户视角对齐而非测试实现细节。 ```typescript import { render, screen } from "@solidjs/testing-library"; import userEvent from "@testing-library/user-event"; import { describe, it, expect } from "vitest"; import Counter from "./Counter"; describe("Counter", () => { it("renders initial count", () => { render(() => <Counter />); expect(screen.getByText("Count: 0")).toBeInTheDocument(); }); it("increments count on button click", async () => { const user = userEvent.setup(); render(() => <Counter />); await user.click(screen.getByRole("button", { name: /increment/i })); expect(screen.getByText("Count: 1")).toBeInTheDocument(); }); }); ``` 这里用 userEvent 替代 fireEvent,因为它更接近真实用户操作——触发完整的事件链(mousedown、focus、mouseup、click),而非仅派发单个事件。 ## 单元测试:响应式原语 SolidJS 的响应式系统脱离 DOM 也能独立测试,关键是用 createRoot 包裹以确保副作用在测试结束后自动清理。 ```typescript import { createSignal, createEffect } from "solid-js"; import { createRoot } from "solid-js"; import { describe, it, expect } from "vitest"; describe("Signal reactivity", () => { it("tracks signal changes and triggers effects", () => createRoot((dispose) => { const [count, setCount] = createSignal(0); const log: number[] = []; createEffect(() => { log.push(count()); }); expect(log).toEqual([0]); setCount(5); expect(log).toEqual([0, 5]); dispose(); })); }); ``` 如果测试异步 Effect,推荐使用 @solidjs/testing-library 提供的 testEffect: ```typescript import { testEffect } from "@solidjs/testing-library"; import { createSignal } from "solid-js"; describe("async effect", () => { it("resolves after signal update", () => testEffect((done) => { const [val, setVal] = createSignal(1); createEffect(() => { if (val() === 2) done(); }); setVal(2); })); }); ``` ## 单元测试:Hook 与自定义原语 不需要 DOM 的自定义 Hook 或原语,用 renderHook 测试更轻量: ```typescript import { renderHook } from "@solidjs/testing-library"; import { describe, it, expect } from "vitest"; import { createCounter } from "./createCounter"; describe("createCounter", () => { it("increments and decrements", () => { const { result } = renderHook(() => createCounter(0)); expect(result.count()).toBe(0); result.increment(); expect(result.count()).toBe(1); result.decrement(); expect(result.count()).toBe(0); }); }); ``` ## 集成测试:组件协作与路由 集成测试验证多个组件协作时的行为,典型场景包括表单提交、路由导航、数据获取等。 ### 表单提交流程 ```typescript import { render, screen } from "@solidjs/testing-library"; import userEvent from "@testing-library/user-event"; import { describe, it, expect, vi } from "vitest"; import LoginForm from "./LoginForm"; describe("LoginForm integration", () => { it("submits form with user input", async () => { const onSubmit = vi.fn(); const user = userEvent.setup(); render(() => <LoginForm onSubmit={onSubmit} />); await user.type(screen.getByLabelText("Email"), "test@example.com"); await user.type(screen.getByLabelText("Password"), "secret123"); await user.click(screen.getByRole("button", { name: /submit/i })); expect(onSubmit).toHaveBeenCalledWith({ email: "test@example.com", password: "secret123", }); }); }); ``` ### 路由导航 ```typescript import { render, screen } from "@solidjs/testing-library"; import { Router, Routes, Route } from "@solidjs/router"; import { describe, it, expect } from "vitest"; import Home from "./Home"; import About from "./About"; describe("Routing integration", () => { it("navigates between pages", async () => { render(() => ( <Router> <Routes> <Route path="/" component={Home} /> <Route path="/about" component={About} /> </Routes> </Router> )); expect(screen.getByText("Home Page")).toBeInTheDocument(); }); }); ``` ## 集成测试:异步数据获取 使用 createResource 的场景在集成测试中需要 Mock 数据源: ```typescript import { render, screen } from "@solidjs/testing-library"; import { describe, it, expect, vi, beforeEach } from "vitest"; import UserProfile from "./UserProfile"; describe("UserProfile with async data", () => { beforeEach(() => { vi.restoreAllMocks(); }); it("shows loading state then user data", async () => { const fetchUser = vi.fn().mockResolvedValue({ name: "Alice" }); render(() => <UserProfile fetchUser={fetchUser} />); expect(screen.getByText(/loading/i)).toBeInTheDocument(); expect(await screen.findByText("Alice")).toBeInTheDocument(); }); }); ``` findByText 内部使用了 waitFor,会在异步渲染完成后自动重试查找,比手动 waitFor 更简洁。 ## Mock 与 Stub 策略 Vitest 内置了完整的 Mock 能力,覆盖最常见的测试场景: ```typescript import { vi } from "vitest"; // Mock 模块 vi.mock("./api", () => ({ fetchUser: vi.fn().mockResolvedValue({ name: "Alice" }), })); // Mock 定时器 vi.useFakeTimers(); // Spy 对象方法 const spy = vi.spyOn(console, "log").mockImplementation(() => {}); ``` 对于 SolidJS 特有的响应式依赖,优先通过 props 注入 Mock 数据,而非直接 mock 内部信号——这样测试更接近真实运行路径。 ## 测试覆盖率配置 在 vite.config.ts 中启用覆盖率: ```typescript test: { coverage: { provider: "v8", reporter: ["text", "html"], include: ["src/**/*.{ts,tsx}"], exclude: ["src/**/*.test.{ts,tsx}"], }, }, ``` 运行 npm run test:coverage 后在 coverage/ 目录查看 HTML 报告。覆盖率是代码质量的参考指标,但不应追求 100%——核心业务逻辑的覆盖率比工具函数更重要。 ## 最佳实践总结 - 测试用户行为而非实现细节:通过 getByRole、getByText 查找元素,而非 querySelector 或 data-testid(除非别无选择) - 用 createRoot 包裹响应式测试:确保 Effect 等副作用在测试结束后自动销毁,避免内存泄漏和测试间干扰 - 优先 userEvent 而非 fireEvent:userEvent 模拟完整的用户交互链路,测试结果更可靠 - 异步断言用 findBy 而非 waitFor + getBy:findBy 自带轮询,代码更简洁 - Mock 外部依赖,保持组件测试纯净:网络请求、浏览器 API 等外部依赖统一 Mock,保证测试稳定可重复 - 集成测试关注组件协作边界:不必重复单元测试已覆盖的逻辑,重点验证数据在组件间的流转
服务端5月27日 15:51
SolidJS 如何与 TypeScript 配合使用?SolidJS 内置了完善的 TypeScript 支持,从项目初始化到日常开发,类型系统贯穿始终。本文围绕实际开发中最常遇到类型问题的场景,逐一拆解 SolidJS 的类型定义方法与最佳实践。 ## 项目配置:让 TypeScript 正确识别 Solid SolidJS 使用自己的 JSX 转换,和 React 不同,必须正确配置 `tsconfig.json`,否则类型检查和编译都会出问题: ```json { "compilerOptions": { "jsx": "preserve", "jsxImportSource": "solid-js", "noImplicitAny": true, "strict": true, "target": "ESNext", "moduleResolution": "node" } } ``` 关键点在于 `"jsx": "preserve"` 不能改成 `"react"`,因为 Solid 自带 JSX 转换插件处理模板。如果你用 Vite 构建,还需要 `vite-plugin-solid` 插件配合。 在混合 React 和 Solid 的项目中,可以在单个文件顶部加上 `/** @jsxImportSource solid-js */`,让 TypeScript 识别该文件使用 Solid 的 JSX 类型。 ## Signal 类型:从基础到进阶 Signal 是 SolidJS 的核心原语,类型定义的好坏直接影响开发体验。 ### 基本用法 ```typescript import { createSignal } from 'solid-js'; // 显式指定泛型 const [count, setCount] = createSignal<number>(0); const [name, setName] = createSignal<string>(''); // 可空类型:signal 没有初始值时必须处理 undefined const [user, setUser] = createSignal<User | undefined>(); // user() 的类型是 User | undefined,每次使用都需要判空 ``` ### Accessor 与 Setter 类型 当你需要在函数之间传递 signal 的读取端或写入端时,使用 SolidJS 提供的工具类型: ```typescript import type { Accessor, Setter } from 'solid-js'; function useCounter(initial: number): [Accessor<number>, Setter<number>] { const [count, setCount] = createSignal(initial); return [count, setCount]; } ``` `Accessor<T>` 本质上就是 `() => T`,但语义更明确。`Setter<T>` 比较复杂,它同时接受直接值和回调函数两种形式: ```typescript // Setter 的两种调用方式 setCount(5); // 直接赋值 setCount(prev => prev + 1); // 基于前值计算 ``` 注意:如果 `setCount(value)` 报类型错误,通常是因为 `value` 的类型同时满足值和函数签名,TypeScript 无法区分。这时用 `setCount(() => value)` 包一层即可。 ### 派生 Signal 的类型推断 ```typescript const [firstName, setFirstName] = createSignal('Zhang'); const [lastName, setLastName] = createSignal('San'); // 派生 signal 自动推断类型,无需手动标注 const fullName = () => `${firstName()} ${lastName()}`; // fullName 的类型自动推断为 Accessor<string> ``` ## 组件与 Props 类型定义 ### 函数组件的两种写法 ```typescript import type { Component, JSX } from 'solid-js'; // 方式一:使用 Component 工具类型 interface ButtonProps { label: string; variant?: 'primary' | 'secondary'; onClick: () => void; } const Button: Component<ButtonProps> = (props) => { return <button onClick={props.onClick}>{props.label}</button>; }; // 方式二:直接标注 props 参数(更灵活,支持 children 类型控制) function Button2(props: ButtonProps): JSX.Element { return <button onClick={props.onClick}>{props.label}</button>; } ``` 推荐方式二,因为 `Component` 类型默认把 `children` 设为可选,而直接标注可以精确控制 children 是否必须。 ### Props 相关工具类型 ```typescript import type { ParentProps, FlowProps, MergeProps } from 'solid-js'; // ParentProps:包含 children 的 props interface CardProps { title: string; } const Card = (props: ParentProps<CardProps>) => { return ( <div> <h2>{props.title}</h2> {props.children} </div> ); }; // FlowProps:用于 <Show>、<For> 等控制流组件 interface ListProps<T> { each: T[]; fallback?: JSX.Element; } function List<T>(props: FlowProps<ListProps<T>, T>) { // FlowProps 第二个泛型参数是 children 的参数类型 } // mergeProps:类型安全的默认 props 合并 import { mergeProps } from 'solid-js'; const defaultProps: Required<ButtonProps> = { label: '', variant: 'primary', onClick: () => {}, }; function Button(props: ButtonProps) { const merged = mergeProps(defaultProps, props); // merged 的类型是 Required<ButtonProps>,所有字段都有值 } ``` ### Props 的解构陷阱 SolidJS 中**绝不能解构 props**,否则会丢失响应性。类型系统可以帮助你避免这个问题: ```typescript // 错误:解构后丢失响应性 function Bad({ label, onClick }: ButtonProps) { ... } // 正确:通过 props 对象访问 function Good(props: ButtonProps) { return <button onClick={props.onClick}>{props.label}</button>; } ``` ## Store 类型:深层响应式的类型安全 ```typescript import { createStore } from 'solid-js/store'; interface AppState { user: { name: string; age: number; preferences: { theme: 'light' | 'dark'; language: string; }; }; items: Array<{ id: number; name: string; completed: boolean; }>; } const [state, setState] = createStore<AppState>({ user: { name: '', age: 0, preferences: { theme: 'light', language: 'zh' }, }, items: [], }); // 类型安全的嵌套更新 setState('user', 'name', 'Zhang San'); // 正确 setState('user', 'preferences', 'theme', 'dark'); // 正确 setState('user', 'invalid', 'value'); // 编译报错 // 数组项的类型安全更新 setState('items', 0, 'completed', true); // 正确 ``` Store 的类型定义关键是确保嵌套结构和 `setState` 的路径参数一一对应。TypeScript 会在编译期拦截非法路径,这正是 Store 相比普通对象的优势。 ## Resource 类型:异步数据加载 ```typescript import { createResource } from 'solid-js'; interface User { id: number; name: string; email: string; } async function fetchUser(id: number): Promise<User> { const res = await fetch(`/api/users/${id}`); return res.json(); } // 基本用法 const [user] = createResource<User>(() => fetchUser(1)); // user() 类型为 User | undefined,loading 时为 undefined // 带错误类型的完整定义 const [users, { refetch }] = createResource<User[], Error>(fetchUsers, { initialValue: [], }); // 在组件中使用 function UserProfile() { const [user] = createResource<User>(() => fetchUser(1)); return ( <Switch fallback={<p>加载中...</p>}> <Match when={user.error}> <p>加载失败:{user.error.message}</p> </Match> <Match when={user()}> {(u) => <p>{u().name}</p>} </Match> </Switch> ); } ``` `<Match when={user()}>` 中的回调参数 `u` 已经被正确收窄为 `User` 类型,不再是 `User | undefined`,这是 SolidJS 类型系统的一大亮点。 ## Context 类型:跨组件通信 ```typescript import { createContext, useContext } from 'solid-js'; import type { Context } from 'solid-js'; interface ThemeContextType { theme: Accessor<'light' | 'dark'>; toggleTheme: () => void; } // 创建带默认值的 context const ThemeContext = createContext<ThemeContextType>(); // 也可以创建无默认值的 context,使用时必须判空 const AuthContext = createContext<AuthContextType>(); // 类型安全的 useHook function useTheme(): ThemeContextType { const ctx = useContext(ThemeContext); if (!ctx) { throw new Error('useTheme must be used within ThemeProvider'); } return ctx; } ``` 把 `useContext` 封装为自定义 hook 并加上判空保护,是避免运行时 undefined 错误的标准做法。 ## 自定义 JSX 元素和事件类型 SolidJS 允许扩展 JSX 命名空间来支持自定义元素和事件: ```typescript declare module 'solid-js' { namespace JSX { // 自定义原生元素 interface IntrinsicElements { 'my-custom-element': { value?: string; onChange?: (value: string) => void; }; } // 自定义事件(用于 on:xxx 语法) interface CustomEvents { 'my-event': CustomEvent<{ detail: string }>; } // 自定义捕获事件 interface CustomCaptureEvents { 'my-capture-event': CustomEvent; } // 强制属性(用于 prop:xxx 语法) interface ExplicitProperties { 'my-prop': string; } // 自定义属性(用于 attr:xxx 语法) interface ExplicitAttributes { 'my-attr': string; } } } ``` 这些扩展让你在使用 Web Components 或自定义 DOM 元素时也能获得完整的类型提示。 ## 常见类型问题与排错 ### 1. JSX 元素类型不兼容 出现 `Type 'Element' is not assignable to type 'Element'` 这类错误,通常是项目中同时安装了 React 的类型定义,导致 JSX 命名空间冲突。解决方法是在 `tsconfig.json` 中确保 `jsxImportSource` 只指向 `solid-js`,或排除 `@types/react`。 ### 2. Signal 间接传递后类型丢失 ```typescript // 错误:传递 signal 调用结果而非 signal 本身 const count = count(); // 丢失响应性,类型变为 number // 正确:传递 Accessor const countAccessor: Accessor<number> = count; ``` ### 3. 组件 children 类型 ```typescript // 如果组件不接受 children,props 不要用 ParentProps interface NoChildProps { title: string; } // 正确:普通接口,没有 children const Header = (props: NoChildProps) => <h1>{props.title}</h1>; ``` ## 最佳实践总结 1. **始终开启 strict 模式**,让 TypeScript 帮你捕获更多问题 2. **用 `import type` 导入纯类型**,避免类型定义进入运行时代码 3. **用函数签名而非 Component 类型定义组件**,精确控制 children 类型 4. **Signal 显式标注泛型**,可空值用 `T | undefined` 5. **封装 useContext 为自定义 hook**,统一判空逻辑 6. **绝对不要解构 props**,使用 `mergeProps` 处理默认值 7. **Store 类型与 setState 路径对齐**,利用编译期检查防止非法路径 8. **混合框架项目用文件级 pragma**,`/** @jsxImportSource solid-js */` 9. **扩展 JSX 命名空间**,让自定义元素和事件也有类型提示 10. **遇到 Setter 类型冲突时**,用回调形式 `setX(() => value)` 替代直接赋值