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

服务端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)` 替代直接赋值