[{"data":1,"prerenderedAt":269},["ShallowReactive",2],{"blog-index":3,"$f236xlv7iodrlp":129},[4,18,30,44,53,63,73,82,91,101,109,118],{"id":5,"path":6,"title":7,"description":8,"categorySlug":9,"categoryLabel":10,"publishedAt":11,"readingMinutes":12,"featured":13,"tags":14,"searchText":17},"blog\u002Fblog\u002Ftypescript-narrowing.md","\u002Fblog\u002Ftypescript-narrowing","TypeScript 类型收窄入门","使用 typeof 和条件判断缩小联合类型的范围，让后续代码更安全。","typescript","TypeScript","2026-09-25",5,true,[10,15,16],"类型收窄","学习笔记","联合类型允许一个值有几种可能类型。使用前先判断当前是哪一种，就可以在分支里安全地调用对应操作，这个过程叫类型收窄。 使用 typeof 判断原始类型 function formatValue(value: string | number) { if (typeof value === 'number') { return value.toFixed(2) } return value.trim() } 在 if 分支里，TypeScript 知道 value 是数字；另一个分支里则知道它是字符串。 使用属性判断对象类型 当几个对象类型有不同属性时，可以通过 'key' in value 判断当前分支。对 API 数据仍要保留运行时校验，因为类型声明不会验证服务端实际返回的内容。 小结 先通过条件判断缩小类型，再访问对应属性，是处理表单输入、请求结果和可选值时很常见的写法。",{"id":19,"path":20,"title":21,"description":22,"categorySlug":23,"categoryLabel":24,"publishedAt":25,"readingMinutes":26,"featured":13,"tags":27,"searchText":29},"blog\u002Fblog\u002Freact-controlled-form.md","\u002Fblog\u002Freact-controlled-form","React 受控表单：从一个输入框开始","用一个简单的姓名输入框，理解 value、onChange 与 React state 之间的关系。","react","React","2026-09-18",6,[24,28,16],"表单","表单是很常见的页面交互。React 里的“受控组件”听起来有些抽象，先从一个输入框开始：让输入框显示的值始终来自组件状态。 让状态成为数据来源 在受控输入框里，value 由 state 提供，每次输入时再通过 onChange 更新 state。这样页面显示的内容和 React 保存的值保持一致。 import { useState } from 'react' export function NameField() { const [name, setName] = useState('') return ( \u003Clabel> 昵称 \u003Cinput value={name} onChange={(event) => setName(event.target.value)} \u002F> \u003C\u002Flabel> ) } 处理表单提交 表单提交时可以读取当前 state。preventDefault() 用来阻止浏览器默认刷新行为，之后再把数据交给页面自己的处理函数。 function handleSubmit(event: React.FormEvent\u003CHTMLFormElement>) { event.preventDefault() console.log(name) } 真实项目里，提交前还需要做必填校验，并在提交期间给用户明确反馈。 容易遇到的小问题 给输入框设置了 value，却没有 onChange，输入框会变成只读。 初始值可能是 undefined，之后又变成字符串。文本输入框最好从一开始就使用 ''。 表单里有多个字段时，可以分别管理状态；字段更多时再考虑统一的表单方案。 小结 受控输入框把数据流讲得很清楚：state 决定当前显示值，用户输入触发事件，事件再更新 state。理解这个循环后，表单校验和提交状态也更容易组织。",{"id":31,"path":32,"title":33,"description":34,"categorySlug":35,"categoryLabel":36,"publishedAt":37,"readingMinutes":38,"featured":13,"tags":39,"searchText":43},"blog\u002Fblog\u002Fcss-grid-cards.md","\u002Fblog\u002Fcss-grid-cards","用 CSS Grid 排列自适应卡片","使用 repeat、minmax 和 auto-fit，让卡片列数随容器宽度自然变化。","styles","页面与样式","2026-09-10",4,[40,41,42],"CSS","Grid","页面布局","文章列表和项目卡片经常需要多列布局。CSS Grid 可以根据容器宽度自动调整列数，减少手写多个断点的工作。 使用 minmax 设置列宽范围 .card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr)); gap: 1rem; } 每列至少需要约 15rem 的宽度，空间充足时平分剩余区域；容器变窄时，列数会自动减少。 检查内容变化 布局测试时不要只看相同长度的标题。把较长标题、空描述和较窄视口也放进页面，确认卡片高度与阅读顺序仍然清楚。 什么时候用 Flexbox 单行导航、按钮组等一维排列常用 Flexbox；需要同时控制行和列时，Grid 通常更直接。两者可以按布局关系组合使用。",{"id":45,"path":46,"title":47,"description":48,"categorySlug":9,"categoryLabel":10,"publishedAt":49,"readingMinutes":12,"featured":13,"tags":50,"searchText":52},"blog\u002Fblog\u002Ftypescript-component-props.md","\u002Fblog\u002Ftypescript-component-props","给 React 组件的 Props 添加 TypeScript 类型","用必填属性和可选属性描述组件接口，让使用方式在编辑器里更清晰。","2026-09-02",[10,24,51],"类型","给组件 Props 添加类型后，编辑器可以提示必填字段，也能在传错数据时尽早发现问题。 声明组件需要的数据 type NoticeProps = { title: string description: string tone?: 'info' | 'success' | 'warning' } function Notice({ title, description, tone = 'info' }: NoticeProps) { return \u003Csection data-tone={tone}>\u003Ch2>{title}\u003C\u002Fh2>\u003Cp>{description}\u003C\u002Fp>\u003C\u002Fsection> } title 和 description 是必填项；tone 后面的问号表示可选，并在解构时提供默认值。 选择 type 还是 interface 描述对象形状时，两者都能完成常见任务。团队已有约定时跟随约定即可，重点是让组件接口容易读、容易更新。 小结 先把真正需要的 Props 写出来，避免一开始为所有可能性增加复杂类型。随着组件复用范围扩大，再补充变体或泛型也不迟。",{"id":54,"path":55,"title":56,"description":57,"categorySlug":23,"categoryLabel":24,"publishedAt":58,"readingMinutes":12,"featured":59,"tags":60,"searchText":62},"blog\u002Fblog\u002Freact-props-and-state.md","\u002Fblog\u002Freact-props-and-state","React 组件里的 Props 和 State","用一个简单的信息卡片区分外部传入的数据和组件内部会变化的数据。","2026-08-28",false,[24,61,16],"组件","React 组件常用 Props 接收外部数据，用 State 保存组件内部会变化的数据。先把两者分开理解，页面状态会更容易追踪。 Props 由使用者传入 type ProfileCardProps = { name: string description: string } function ProfileCard({ name, description }: ProfileCardProps) { return \u003Csection>\u003Ch2>{name}\u003C\u002Fh2>\u003Cp>{description}\u003C\u002Fp>\u003C\u002Fsection> } 父组件可以通过属性传入不同内容。子组件读取 Props 来展示信息，不应该直接修改传进来的值。 State 保存交互状态 像展开\u002F收起、输入框内容这样的变化，可以放在 state 里。更新 state 后，React 会重新渲染相关界面。 const [expanded, setExpanded] = useState(false) 怎么选择 如果值由父组件决定，用 Props；如果值由当前组件的交互维护，用 State。状态需要被多个组件共享时，可以再考虑把它提升到共同的父组件。",{"id":64,"path":65,"title":66,"description":67,"categorySlug":35,"categoryLabel":36,"publishedAt":68,"readingMinutes":12,"featured":59,"tags":69,"searchText":72},"blog\u002Fblog\u002Fmobile-layout-checklist.md","\u002Fblog\u002Fmobile-layout-checklist","做移动端页面适配时的检查笔记","从内容宽度、触控区域、文字换行和键盘焦点几个方面检查窄屏体验。","2026-08-20",[40,70,71],"响应式","可访问性","页面在桌面上看起来正常，不代表在手机上也好用。适配时可以从内容本身开始，而不只是缩小字号。 先设置合理的内容宽度 主体使用弹性宽度和最大宽度，卡片留出适当内边距。避免固定像素宽度导致横向滚动，也要检查长英文、代码和链接是否会撑破容器。 检查触控和焦点 按钮之间要留出足够间距，键盘焦点应清楚可见。导航折叠后仍应能用键盘访问，关闭菜单后焦点位置也要合理。 验证文字和顺序 长标题要能自然换行，标签过多时允许换行。用窄屏模拟器检查页面顺序、横向滚动条和固定元素是否遮挡内容。",{"id":74,"path":75,"title":76,"description":77,"categorySlug":23,"categoryLabel":24,"publishedAt":78,"readingMinutes":38,"featured":59,"tags":79,"searchText":81},"blog\u002Fblog\u002Freact-list-keys.md","\u002Fblog\u002Freact-list-keys","React 列表渲染里的 key 有什么用","通过稳定的 key 帮助 React 识别列表项目，避免用数组下标造成的状态错位。","2026-08-12",[24,80,16],"列表","用 map 渲染数组时，React 要知道每一项在更新前后是否还是同一个项目。key 就是列表项的稳定标识。 使用数据本身的标识 {tasks.map((task) => ( \u003Cli key={task.id}>{task.title}\u003C\u002Fli> ))} 如果数据里有稳定且唯一的 id，优先使用它。插入、删除或重新排序后，React 就能更准确地对应列表项。 为什么通常不选数组下标 数组下标表达的是当前位置。当列表顺序变化时，同一个下标可能对应另一条数据，输入框等带状态的内容就可能跟着错位。 静态、永不改变顺序的列表用下标通常不会出问题；可变列表应尽量使用稳定标识。key 也只供 React 使用，不会作为普通 Props 传到组件里。",{"id":83,"path":84,"title":85,"description":86,"categorySlug":9,"categoryLabel":10,"publishedAt":87,"readingMinutes":38,"featured":59,"tags":88,"searchText":90},"blog\u002Fblog\u002Ftypescript-union-status.md","\u002Fblog\u002Ftypescript-union-status","用联合类型描述请求状态","把加载、成功和失败等状态限制在明确的集合里，减少拼写错误和遗漏分支。","2026-08-04",[10,89,16],"联合类型","如果用任意字符串表示状态，'sucess' 这样的拼写错误可能一直留到运行时。联合类型可以把可用值限制在一个明确集合中。 声明有限的状态值 type RequestState = 'idle' | 'loading' | 'success' | 'error' function getMessage(state: RequestState) { if (state === 'loading') return '正在加载' if (state === 'error') return '加载失败' if (state === 'success') return '加载完成' return '等待开始' } 编辑器会提示允许的值。判断完所有状态后，类型检查也能帮助发现遗漏分支。 需要携带数据时 可以把状态和对应数据组合成判别联合类型，例如成功状态带数据、错误状态带错误信息。这样每种状态能携带恰当的信息，而不是把所有字段都写成可选。",{"id":92,"path":93,"title":94,"description":95,"categorySlug":23,"categoryLabel":24,"publishedAt":96,"readingMinutes":38,"featured":59,"tags":97,"searchText":100},"blog\u002Fblog\u002Freact-use-state-practice.md","\u002Fblog\u002Freact-use-state-practice","用 useState 管理一个简单的计数交互","从加一和重置两个操作，熟悉 React state 的声明与更新方式。","2026-07-30",[24,98,99],"useState","练习记录","计数器是观察 React 更新流程的小练习。组件第一次渲染时创建 state，用户点击按钮后更新 state，React 再根据新值刷新界面。 声明和更新状态 const [count, setCount] = useState(0) function increase() { setCount((current) => current + 1) } 当新值依赖旧值时，使用函数形式的更新更清楚，也能避免连续更新时读取到旧值。 让操作有清楚的反馈 按钮文案应描述操作，计数值可以用文本展示。重置按钮则把状态恢复到初始值：setCount(0)。 这个练习也适合检查按钮的键盘操作、焦点样式和窄屏下的间距，不只是确认数字会变化。",{"id":102,"path":103,"title":104,"description":105,"categorySlug":23,"categoryLabel":24,"publishedAt":106,"readingMinutes":12,"featured":59,"tags":107,"searchText":108},"blog\u002Fblog\u002Freact-small-components.md","\u002Fblog\u002Freact-small-components","把重复的界面拆成小组件","从重复的按钮和卡片开始，认识组件拆分的几个简单信号。","2026-07-16",[24,61,99],"页面出现重复结构时，可以先比较这些结构是否真的一致，再决定要不要提取组件。拆分组件的目标是让一段界面更容易理解和复用。 先找出稳定的部分 例如多个卡片都有标题、说明和链接，可以把这三部分作为 Props 传入。颜色、间距等样式如果完全相同，也可以放在组件内部统一维护。 不必过早抽象 只出现一次、内容还在快速变化的区域，暂时留在页面里往往更简单。等重复模式清楚以后，再提取组件会更容易定义接口。 一个实用的检查方式是：读组件调用处时，能否大致看懂页面结构；读组件本身时，能否说清它只负责什么。",{"id":110,"path":111,"title":112,"description":113,"categorySlug":23,"categoryLabel":24,"publishedAt":114,"readingMinutes":26,"featured":59,"tags":115,"searchText":117},"blog\u002Fblog\u002Freact-use-effect-notes.md","\u002Fblog\u002Freact-use-effect-notes","React useEffect 的依赖和清理","整理 useEffect 何时运行，以及订阅和定时器为什么需要清理。","2026-06-30",[24,116,16],"useEffect","useEffect 用来让组件和外部系统同步，例如订阅事件或启动定时器。它不适合替代所有状态计算；能在渲染时直接得到的值，通常不需要再放进 effect。 依赖项说明同步关系 依赖数组列出 effect 使用到、并且可能变化的响应式值。依赖变化后，React 会先运行上一次的清理函数，再运行新的 effect。 清理外部资源 useEffect(() => { const timer = window.setInterval(refresh, 5000) return () => window.clearInterval(timer) }, [refresh]) 组件卸载或依赖变化时，清理函数会释放定时器。订阅事件、WebSocket 连接也应遵循同样的清理思路。 排查依赖问题 遇到重复请求或旧数据时，先检查 effect 真正依赖哪些值，再考虑是否能把逻辑移到事件处理函数或使用更合适的数据请求方式。",{"id":119,"path":120,"title":121,"description":122,"categorySlug":123,"categoryLabel":124,"publishedAt":125,"readingMinutes":38,"featured":59,"tags":126,"searchText":128},"blog\u002Fblog\u002Fvite-react-project.md","\u002Fblog\u002Fvite-react-project","用 Vite 创建一个 React + TypeScript 小项目","记录从初始化模板到启动开发服务器的基础步骤，以及几个值得先检查的文件。","tools","开发工具","2026-06-12",[127,24,10],"Vite","Vite 提供了 React 和 TypeScript 起步模板，适合快速开始一个前端练习项目。 创建项目 npm create vite@latest my-react-app -- --template react-ts cd my-react-app npm install npm run dev 命令会创建项目目录、安装依赖并启动本地开发服务。实际命令可能随 Vite 版本调整，创建时以终端提示为准。 先看哪些文件 src\u002F：页面与组件代码。 package.json：依赖和常用命令。 tsconfig.json：TypeScript 编译选项。 vite.config.ts：Vite 插件和开发服务器设置。 一个小练习开始时不必急着增加很多工具。先熟悉开发、构建和类型检查，再按项目需要添加能力。",{"id":74,"title":76,"body":130,"categoryLabel":24,"categorySlug":23,"description":77,"extension":263,"featured":59,"meta":264,"navigation":13,"path":75,"publishedAt":78,"readingMinutes":38,"seo":265,"stem":266,"tags":267,"__hash__":268},{"type":131,"value":132,"toc":259},"minimark",[133,146,150,236,243,246,249,255],[134,135,136,137,141,142,145],"p",{},"用 ",[138,139,140],"code",{},"map"," 渲染数组时，React 要知道每一项在更新前后是否还是同一个项目。",[138,143,144],{},"key"," 就是列表项的稳定标识。",[147,148,149],"h2",{"id":149},"使用数据本身的标识",[151,152,157],"pre",{"className":153,"code":154,"language":155,"meta":156,"style":156},"language-tsx shiki shiki-themes github-light-high-contrast","{tasks.map((task) => (\n  \u003Cli key={task.id}>{task.title}\u003C\u002Fli>\n))}\n","tsx","",[138,158,159,188,230],{"__ignoreMap":156},[160,161,164,168,171,174,178,181,185],"span",{"class":162,"line":163},"line",1,[160,165,167],{"class":166},"sigWx","{tasks.",[160,169,140],{"class":170},"s3dhs",[160,172,173],{"class":166},"((",[160,175,177],{"class":176},"sV4o_","task",[160,179,180],{"class":166},") ",[160,182,184],{"class":183},"sTJeM","=>",[160,186,187],{"class":166}," (\n",[160,189,191,194,198,202,205,208,211,214,217,220,222,225,227],{"class":162,"line":190},2,[160,192,193],{"class":166},"  \u003C",[160,195,197],{"class":196},"s742_","li",[160,199,201],{"class":200},"sacAq"," key",[160,203,204],{"class":183},"={",[160,206,207],{"class":166},"task.id",[160,209,210],{"class":183},"}",[160,212,213],{"class":166},">",[160,215,216],{"class":183},"{",[160,218,219],{"class":166},"task.title",[160,221,210],{"class":183},[160,223,224],{"class":166},"\u003C\u002F",[160,226,197],{"class":196},[160,228,229],{"class":166},">\n",[160,231,233],{"class":162,"line":232},3,[160,234,235],{"class":166},"))}\n",[134,237,238,239,242],{},"如果数据里有稳定且唯一的 ",[138,240,241],{},"id","，优先使用它。插入、删除或重新排序后，React 就能更准确地对应列表项。",[147,244,245],{"id":245},"为什么通常不选数组下标",[134,247,248],{},"数组下标表达的是当前位置。当列表顺序变化时，同一个下标可能对应另一条数据，输入框等带状态的内容就可能跟着错位。",[134,250,251,252,254],{},"静态、永不改变顺序的列表用下标通常不会出问题；可变列表应尽量使用稳定标识。",[138,253,144],{}," 也只供 React 使用，不会作为普通 Props 传到组件里。",[256,257,258],"style",{},"html pre.shiki code .sigWx, html code.shiki .sigWx{--shiki-default:#0E1116}html pre.shiki code .s3dhs, html code.shiki .s3dhs{--shiki-default:#622CBC}html pre.shiki code .sV4o_, html code.shiki .sV4o_{--shiki-default:#702C00}html pre.shiki code .sTJeM, html code.shiki .sTJeM{--shiki-default:#A0111F}html pre.shiki code .s742_, html code.shiki .s742_{--shiki-default:#024C1A}html pre.shiki code .sacAq, html code.shiki .sacAq{--shiki-default:#023B95}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":156,"searchDepth":190,"depth":190,"links":260},[261,262],{"id":149,"depth":190,"text":149},{"id":245,"depth":190,"text":245},"md",{},{"title":76,"description":77},"blog\u002Freact-list-keys",[24,80,16],"eLZbCgsAOwfr8qMV0eR2tU8cAwf5mhd0Epvjm7-9-nw",1792696400069]