[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"donations-sidebar":3,"$fnWV-xUUgIKPZ7hb8UPvaNQXNuaEP5JYdPvch2eIT9Co":4,"posts-{\"page\":1,\"pageSize\":10,\"categoryId\":\"15ac46ad-edf7-4435-9a64-ff78117d58c5\"}":11,"sidebar-data":111},[],{"id":5,"name":6,"slug":7,"description":6,"sortOrder":8,"createdAt":9,"updatedAt":10},"15ac46ad-edf7-4435-9a64-ff78117d58c5","Vue3 生态","vue3-生态",1,"2022-05-21T08:05:39.000Z","2023-02-08T02:49:14.000Z",{"list":12,"total":109,"page":8,"pageSize":110},[13,34,47,60,76,92],{"id":14,"title":15,"slug":16,"content":17,"excerpt":15,"coverImage":18,"status":19,"isPinned":20,"pinnedAt":21,"viewCount":22,"publishedAt":23,"createdAt":23,"updatedAt":24,"category":25,"author":26,"tags":29},"28222724-83c2-4a36-ac5d-12aabbfa83b0","Nuxt3最强SEO攻略","nuxt3最强seo攻略","## 使用useFetch服务端渲染请求方式\n> 方便搜索引擎可以解析网站的html，vue构建的项目是客户端渲染，无法解析\n### 官方文档\n```html\nhttps:\u002F\u002Fnuxt.com\u002Fdocs\u002Fapi\u002Fcomposables\u002Fuse-fetch\n```\n\n\n## 正确设置网站的TDK信息\n```language\nuseServerSeoMeta({\n  title: \\\"首页\\\",\n  description: \\\"欢迎来到小白龙博客\\\",\n  keywords: `${data.rows.map((item: any) => item.title)}`,\n});\n```\n\n\n## 各大搜索引擎平台\n### 百度搜索\n> 添加网站地图\n```language\nhttps:\u002F\u002Fziyuan.baidu.com\u002Flinksubmit\u002Findex?site=https:\u002F\u002Fxiaolong0418.com\u002F\n```\n![](https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002Fa73564c2d3da46f24e7f0f6614f35590.png)\n\n### 谷歌搜索\n> 添加网站地图\n\n```language\nhttps:\u002F\u002Fsearch.google.com\u002Fsearch-console\u002Fsitemaps?resource_id=sc-domain%3Axiaolong0418.com&hl=zh-CN\n```\n![](https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F96332c34d3f91760626963acce85dd69.png)\n\n\n\n### 搜狗搜索\n> 添加网站地图\n```language\nhttps:\u002F\u002Fzhanzhang.sogou.com\u002Findex.php\u002Fsitelink\u002Findex\n```\n![](https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F7d00eb3aa12ea772ca9db31bb5973b1d.png)\n\n\n\n### 360搜索\n> 添加网站地图\n```language\nhttps:\u002F\u002Fzhanzhang.so.com\u002Fsitetool\u002Fsitemap\n```\n![](https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002Fc46cfde75c0dd98e8e5bb1288f235eca.png)\n\n\n## 提高网站整体评分\n> 根据检测报告，对应修改网站问题就可以了\n```language\nhttps:\u002F\u002Fpagespeed.web.dev\u002Fanalysis\u002Fhttps-xiaolong0418-com\u002Fwpb7rnuigu?form_factor=mobile\n```\n\n![](https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002Fda0a72ead6c14ce0ff453de792c4f9c1.png)\n\n\n\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F15085311d6b955a035fe83c168a6bd1a.png","PUBLISHED",false,null,431,"2025-01-24T16:00:19.000Z","2026-06-27T11:06:08.004Z",{"id":5,"name":6,"slug":7},{"id":27,"name":28,"avatar":21},"f9d0f2de-c700-4f90-b535-afd3dbe78128","Admin",[30],{"id":31,"name":32,"slug":33},"62b94c93-724f-488d-8fc5-0449971d9204","Vue3","vue3",{"id":35,"title":36,"slug":37,"content":38,"excerpt":36,"coverImage":39,"status":19,"isPinned":20,"pinnedAt":21,"viewCount":40,"publishedAt":41,"createdAt":41,"updatedAt":42,"category":43,"author":44,"tags":45},"ff06d6ce-1563-4cd7-bb0b-dced85030a59","Vue3不同构建版本的区别和使用","vue3不同构建版本的区别和使用","## 介绍\n> 当我们通过npm i vue@next安装了vue3之后，可以看到node_modules\u002Fvue\u002Fdist目录下有12个构建版本，如果不考虑开发版本和生产版本的区别的话（时候包含.prod），也有7个版本。\n\n## 版本\n| 版本 | 完整版（包括编译器和运行时版本） | 运行时版（runtime）（由于不包括编译器，如果导入的vue是运行时版本，则要求在构建期间就要编译好） | 说明 |\n| - | - | - |- |\n| cjs | .cjs.js |  | CommonJs，有常用在nodejs服务端的一种模块导入标准 |\n| esm-browser | .esm-browser.js | .runtime.esm-browser.js | 用于浏览器通过原生 ES 模块导入使用 |\n| esm-bundler | .esm-bundler.js | .runtime.esm-bundler.js | 用于构建工具（webpack，rollup等）使用原生 ES 模块导入 | \n| global | \t.global.js | .runtime.global.js | 全局变量版本 |","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F787d127b2ba74905b10965cf40c1b147.png",901,"2023-02-24T02:18:45.000Z","2026-06-27T11:07:17.398Z",{"id":5,"name":6,"slug":7},{"id":27,"name":28,"avatar":21},[46],{"id":31,"name":32,"slug":33},{"id":48,"title":49,"slug":50,"content":51,"excerpt":49,"coverImage":52,"status":19,"isPinned":20,"pinnedAt":21,"viewCount":53,"publishedAt":54,"createdAt":54,"updatedAt":55,"category":56,"author":57,"tags":58},"85665df6-cde0-4bba-83ed-76afafb7b3c2","Qrcode二维码生成","qrcode二维码生成","\n## 官网\n```language\nhttps:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fqrcode.vue\n```\n\n\n\n## 引用方法\n\n### \n\n\n\n### Vue\n\n### 安装命令\n\n#### Vue2\n```shell \nnpm i -S qrcode.vue@1.7.0\n```\n\n#### Vue3\n```shell \nnpm i -S qrcode.vue\n```\n\n```shell\nimport QrcodeVue from \\\"qrcode.vue\\\";\n```\n\n## 使用方法\n\n```vue\n  \u003Cqrcode-vue\n        value=\\\"WIFI:T:SAE;P::Adm\\;\\;\\;in1\\;\\;\\;234\\;;S::TCL\\;-G的uest\\;\\;\\;\\;-19:4A\\;;H:false;\\\"\n        :size=\\\"200\\\"\n        level=\\\"H\\\"\n  \u002F>\n```\n\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F1657183263272.jpeg",504,"2022-07-07T08:41:42.000Z","2026-06-27T11:07:43.647Z",{"id":5,"name":6,"slug":7},{"id":27,"name":28,"avatar":21},[59],{"id":31,"name":32,"slug":33},{"id":61,"title":62,"slug":63,"content":64,"excerpt":62,"coverImage":65,"status":19,"isPinned":20,"pinnedAt":21,"viewCount":66,"publishedAt":67,"createdAt":67,"updatedAt":68,"category":69,"author":70,"tags":71},"f59aa1c6-ba4c-47c8-850f-1c7b6bce23cb","Nuxt3入门到精通教程","nuxt3入门到精通教程","## 什么是 Nuxt？\n> Vue的SSR框架，服务端渲染，有利于SEO优化和提高浏览器渲染速度！\n\n## 使用脚手架创建nuxt-app项目\n```language\npnpm dlx nuxi init nuxt-app\n```\n\n## 配置.npmrc文件\n```language\nshamefully-hoist=true\n```\n\n\n##  进入目录，安装依赖，启动项目\n```language\ncode nuxt-app\npnpm i\npnpm run dev \n```\n\n## 目录结构\n\n- .nuxt               \u002F\u002F 自动生成的目录，用于展示结果\n- node_modules        \u002F\u002F 项目依赖包存放目录\n- .gitignore          \u002F\u002F Git的配置目录，比如一些文件不用Git管理就可以在这个文件中配置\n- pages               \u002F\u002F 开发的页面目录\n- components          \u002F\u002F 组件目录\n- assets              \u002F\u002F 静态资源目录\n- layouts             \u002F\u002F 项目布局目录\n- app.vue             \u002F\u002F 项目入口文件，你可以在这里配置路由的出口\n- nuxt.config.ts      \u002F\u002F nuxt项目的配置文件 ，这个里边可以配置Nuxt项目的方法面面\n- package-lock.json   \u002F\u002F 锁定安装时包的版本，以保证其他人在 npm install时和你保持一致\n- package.json        \u002F\u002F 包的配置文件和项目的启动调式命令配置\n- README.md           \u002F\u002F 项目的说明文件\n- tsconfig.json       \u002F\u002F TypeScript的配置文件\n\n## 增加pwa功能\n### 官方教程\n```language\nhttps:\u002F\u002Fgithub.com\u002Fkevinmarrec\u002Fnuxt-pwa-module\n```\n\n### 安装\n```shell\nnpm install -D @kevinmarrec\u002Fnuxt-pwa\n```\n\n### nuxt.config.ts配置\n\n```js\nexport default defineNuxtConfig({\n  modules: [\n    '@kevinmarrec\u002Fnuxt-pwa'\n  ],\n\u002F\u002F开发环境也生成pwa\n  pwa: {\n    workbox: {\n      enabled: true\n    }\n  }\n})\n```\n\n### 根目录public目录中添加网站图标文件（不添加，无法生成）\n```language\n\u002Fpublic\u002Ficon.png\n```\n\n\n## 常见问题\n\n### 使用https，ws链接拦截，不断刷新页面\n> 通过反向代理解决\n```language\n    location \u002F_nuxt\u002Fhmr\u002F {\n        proxy_pass http:\u002F\u002Flocalhost:24678;\n        proxy_http_version 1.1;\n        proxy_set_header Upgrade $http_upgrade;\n        proxy_set_header Connection \\\"upgrade\\\";\n        proxy_set_header Host $host;\n        proxy_cache_bypass $http_upgrade;\n    }\n```\n\n### pnpm安装依赖，会缺依赖\n> 加参数\n```language\npnpm i --shamefully-hoist\n```\n\n### 出现错误“write EPIPE”\n```language\nnpx nuxi upgrade --force\n```\n\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002Fccdb6de98b94ce28e183d859aee0786d.png",942,"2022-06-01T13:06:40.000Z","2026-06-27T11:02:35.872Z",{"id":5,"name":6,"slug":7},{"id":27,"name":28,"avatar":21},[72],{"id":73,"name":74,"slug":75},"a4370d78-70e1-4073-a8f6-3dc5d81fd8fd","Nuxt","nuxt",{"id":77,"title":78,"slug":79,"content":80,"excerpt":78,"coverImage":81,"status":19,"isPinned":20,"pinnedAt":21,"viewCount":82,"publishedAt":83,"createdAt":83,"updatedAt":84,"category":85,"author":86,"tags":87},"9011961f-040f-4a51-83d3-8f5e008d279b","Vue-Router4入门到精通教程","vue-router4入门到精通教程","## 为什么要使用 Vue-Router？\n\n> Vue Router 是 Vue.js 的官方路由。它与 Vue.js 核心深度集成，让用 Vue.js 构建单页应用变得轻而易举。\n\n## 安装\n\n```shell\nnpm i -S vue-router@4\n```\n\n### 新建 src\u002Frouter\u002Findex.ts\n\n```ts\nimport { createRouter, createWebHashHistory, RouteRecordRaw } from \\\"vue-router\\\";\nexport const asyncRoutes: Array\u003CRouteRecordRaw> = [\n  {\n    path: \\\"\u002F\\\",\n    \u002F\u002F重定向到活动界面\n    redirect: \\\"\u002Factivity\\\",\n    meta:{\n          title:'首页',\u002F\u002F配置title\n          keepAlive:false,\u002F\u002F是否缓存\n          requireAuth:false\u002F\u002F是否需要身份验证\n        }\n\n  },\n  \u002F\u002F活动中心\n  {\n    path: \\\"\u002Factivity\\\",\n    name: \\\"activity\\\",\n    \u002F\u002F路由懒加载\n    component: () => import(\\\"@\u002Fviews\u002Factivity\u002FActivity.vue\\\"),\n  },\n  \u002F\u002F通过ID进入藏品详细\n  {\n    path: \\\"\u002FcollectionDetails\u002F:id\\\",\n    name: \\\"collectionDetails\\\",\n    component: () => import(\\\"@\u002Fviews\u002Fcollection-details\u002FCollectionDetails.vue\\\"),\n  },\n];\n\nconst router = createRouter({\n  \u002F\u002F 始终滚动到顶部\n  scrollBehavior() {\n    return { top: 0 };\n  },\n  history: createWebHashHistory(),\n\u002F\u002F设置路由前缀，默认为空\n\u002F\u002F history: createWebHashHistory(\\\"\u002Fcs\\\"),\n  routes: asyncRoutes,\n});\nexport default router;\n```\n\n### 在 main.ts 全局中使用\n\n```ts\nimport { createApp } from 'vue'\nimport router from \\\".\u002Frouter\u002Findex\\\";\n\nconst app = createApp(App)\napp.use(router)\napp.mount('#app')\n```\n\n### 在 App.vue 全局中使用\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv class=\\\"main\\\">\n    \u003Crouter-view>\u003C\u002Frouter-view>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n```\n\n\n## 核心概念与基本使用\n\n### history\n\n> 在创建路由器实例时，`history` 配置允许我们在不同的历史模式中进行选择。\n\n| 模式  |          说明          |         区别          |                    Nginx 配置                    |\n| :---: | :--------------------: | :-------------------: | :----------------------------------------------: |\n| Hash  | createWebHashHistory() | 有#，不需要服务端配合 |                        无                        |\n| HTML5 |   createWebHistory()   |  无#，需要服务端配合  | location \u002F { try_files \\$uri \\$uri\u002F \u002Findex.html; } |\n\n## 路由跳转\n\n```ts\nimport { useRouter } from \\\"vue-router\\\";\nconst router = useRouter();\n\u002F\u002F不带参数\nrouter.push(\\\"\u002F\\\");\n\u002F\u002F路由query传参,参数拼接到url上，刷新不会丢失\nrouter.push({\n    path: \\\"\u002F\\\",\n    query: {\n      name:\\\"小龙\\\"\n    },\n});\n\u002F\u002F路由params传参,注意params不能和path同时使用，要用params与name使用，刷新参数会丢失\nrouter.push({\n    name: \\\"\u002F\\\",\n    params: {\n      name:\\\"小龙\\\"\n    },\n});\n\n\u002F\u002F路由params传参,注意params不能和path同时使用，要用params与name使用，刷新参数会丢失\nrouter.push({\n    path: \\\"\u002F:name\\\"\n});\n```\n\n### 路由接受参数\n\n```ts\nimport { useRoute } from \\\"vue-router\\\";\nconst route = useRoute();\nconsole.log(route.query.name);\u002F\u002F小龙\nconsole.log(route.params.name);\u002F\u002F小龙\nconsole.log(route.params.name);\u002F\u002F小龙\n```\n\n- query 更加类似于 get 传参,在浏览器地址栏中显示参数,刷新不会丢失数据\n- params 则类似于 post 传参，在浏览器地址栏中不显示参数,刷新会丢失数据\n\n### 获取当前页面路由\n> route相当于当前正在跳转的路由对象 可以从里面获取name,path,params,query等\n```ts\nimport { useRoute } from \\\"vue-router\\\";\nconst route = useRoute();\nconsole.log(window.location.href);\u002F\u002F完整url\nconsole.log(route.path);\u002F\u002F路径\n```\n\n## 动态修改url,不刷新页面\n\n### vue-router修改\n```language\nroute.replace({path:'\u002Fname',query:{id:1}});\n```\n\n\n### 原生修改\n| 参数 | 作用 |\n| - | - |\n| state | 状态对象是一个JavaScript对象，它与传递给 replaceState 方法的历史记录实体相关联. |\n| title | 大部分浏览器忽略这个参数, 将来可能有用. 在此处传递空字符串应该可以防止将来对方法的更改。或者，您可以为该状态传递简短标题 |\n| url | 历史记录实体的URL. 新的URL跟当前的URL必须是同源; 否则 replaceState 抛出一个异常. |\n\n### pushState\n```language\nwindow.history.pushState(state, title[, url])   \u002F\u002F添加并激活一个历史记录条目 \n```\n```js\n\u002F\u002F 如果当前的域名是 www.baidu.com\nconst state = { name: 'dx', age: 18 }\nconst title = ''\nconst url = '\u002Fabout'\n\nconst result = history.pushState(state, title, url)\n\u002F\u002F 路由将会改变为www.baidu.com\u002Fabout\n\n```\n\n### replaceState\n```js\nwindow.history.replaceState(state, title[, url]);\n```\n\n```js\nwindow.addEventListener('popstate', function (e) {\n    console.log(e.state)\n},false)\n\nconst state1 = { name: 'dx', age: 18 }\nconst title = ''\nconst url = '\u002Fabout'\n\nhistory.pushState(state1, title, url)\n\nhistory.back()\n\u002F\u002F 先后退到之前的路由，会打印一次state,但此时的路由没有通过pushState或者replaceState设置state,所以\n\u002F\u002F null\nhistory.forward()\n\u002F\u002F 再次回到about路由时，又会打印一次state\n\u002F\u002F { name: 'dx', age: 18 }\n\n```\n> 虽然pushState和 replaceState改变路由时不能被popState监听到，但其它方式只要改变了浏览器路由，都会触发popState的监听，如果监听的路由恰好被pushState和replaceState设置过state，我们都能通过上面的方式拿到state。\n\n\n\n\n\n\n## 重定向\n\n```ts\n  {\n    path: \\\"\u002F\\\",\n    \u002F\u002F访问'\u002F'，重定向到'\u002Factivity'\n    redirect: \\\"\u002Factivity\\\",\n  },\n```\n\n## 当没有匹配到正确路由的时候，重定向\u002F404\n\n```ts\n  {\n    path: \\\"\u002F:pathMatch(.*)\\\",\n    redirect: \\\"\u002F404\\\",\n  },\n```\n\n## 导航守卫\n\n> 通常用于检查用户是否有权限访问某个页面，验证动态路由参数，或者销毁监听器 自定义逻辑运行后，`next `回调用于确认路由、声明错误或重定向 `vue-router 4.x`中可以从钩子中返回一个值或`Promise`来代替\n\n```ts\n\u002F\u002F to 将要访问的路径\n\u002F\u002F from 代表从哪个路径跳转而来\n\u002F\u002F next 是个函数，表示放行 next() 放行  next('\u002Flogin') 跳转登录页面\nrouter.beforeEach((to, from, next) => {\n  \u002F\u002F设置浏览器标签页的标题\n  if (typeof to.meta.title === \\\"string\\\") {\n    document.title = to.meta.title;\n  }\n\n  \u002F\u002F 如果用户访问的登录页，直接放行\n  if (to.path === \\\"\u002Flogin\\\") return next();\n\n  \u002F\u002F访问用户页面,兑换藏品\n  if (to.path === \\\"\u002Fuser\\\") {\n    \u002F\u002F 从 localStorage 中获取到保存的 token 值\n    const { token = \\\"\\\", idCardStatus } = storage.getItem(\\\"loginInfo\\\") || {};\n    \u002F\u002F 没有 token，强制跳转到登录页\n    if (!token) return next(\\\"\u002Flogin\\\");\n    \u002F\u002F没有实名认证\n    if (idCardStatus === 0) return next(\\\"\u002FrealName\\\");\n    next();\n  }\n  next();\n});\n```\n\n## 动态路由\n\n> 通常用于检查用户是否有权限访问某个页面，验证动态路由参数，或者销毁监听器 自定义逻辑运行后，`next `回调用于确认路由、声明错误或重定向 `vue-router 4.x`中可以从钩子中返回一个值或`Promise`来代替\n\n\n```language\nRouters.beforeEach((to,from,next) => {\n    if(to.path != '\u002F' && !store.state.isLogin) {\n        \u002F\u002F跳转的不是首页 同时 用户还未登陆\n        \u002F\u002F这个判断，我们就可以基本判定用户是在做登陆时候的跳转\n        \u002F\u002F↓拿到登陆时，接口返回的路由数组（vuex内）,大致是这样的\n        let resRouterArr = [\n            {\n                routeName: '人员管理',\n                children: [\n                    {\n                        routeName: '销售管理',\n                        ...\n                    },\n                    {\n                        routeName: '内勤管理',\n                        ...\n                    }\n                ]\n            }\n        ]\n        let routerArr = []\n        resRouterArr.forEach(item => {\n            allRouters.forEach(all => {\n                if(item.routeName == all.routeName) {\n                    \u002F\u002F拿到本地路由对象\n                    let obj = JSON.parse(JSON.stringify(all))\n                    let childrenRouter = []\n                    if(item.children && item.children.length > 0) {\n                        item.children.forEach(childItem => {\n                            all.children.forEach(allItem => {\n                                if(childItem.routeName == allItem.routerName) {\n                                    childrenRouter.push(allItem)\n                                }\n                            })\n                        })\n                        obj.children = childrenRouter\n                    }\n                    routerArr.push(obj)\n                }\n            })\n        })\n        Routers.addroutes(routerArr)\u002F\u002Faddroutes为添加路由数组的方法\n        store.commit('domRouteTree',rousterArr)\u002F\u002F存储进vuex，之后页面左右路由列表渲染使用\n        next({...to,replace:true})\u002F\u002F进行路由跳转\n    } else {\n        next()\n    }\n})\n\n```\n\n## Vue Router 4: 路由参数在 created\u002Fsetup 时不可用\n> Vue Router 4的破坏性变化是，现在所有的导航都是异步的,等待路由 ready 好后再挂载程序\n```language\nimport { createApp } from 'vue'\nimport App from '.\u002FApp.vue'\nimport router from '.\u002Frouter'\n\nconst app = createApp(App)\napp.use(router)\n\n\u002F\u002F Replace -> app.mount('#app')\nrouter.isReady().then(() => {\n    app.mount('#app')\n})\n```\n\n```language\n    \u002F\u002F 当参数更改时获取用户信息\n    watch(\n      () => route.params.id,\n      async newId => {\n        userData.value = await fetchUser(newId)\n      }\n    )\n```\n\n\n\n\n\n## 常见问题","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F2b5fd03a8f544149ec1fc75a773d5682.webp",1258,"2022-05-25T06:41:08.000Z","2026-06-27T11:06:41.449Z",{"id":5,"name":6,"slug":7},{"id":27,"name":28,"avatar":21},[88],{"id":89,"name":90,"slug":91},"2f7fb1be-b9c5-4606-b54f-e9f66f2653b2","Vue-Router","vue-router",{"id":93,"title":94,"slug":95,"content":96,"excerpt":97,"coverImage":98,"status":19,"isPinned":20,"pinnedAt":21,"viewCount":99,"publishedAt":100,"createdAt":100,"updatedAt":101,"category":102,"author":103,"tags":104},"de6b148b-2e05-4cfe-8181-908d2650e224","Pinia存储库入门到精通教程","pinia存储库入门到精通教程","\n\n## 为什么要使用Pina？\n\n> Pinia 是 Vue 的存储库，允许您跨组件\u002F页面共享状态。\n\n\n\n## 安装\n\n```shell\nnpm i -S pinia\n```\n\n\n\n### 新建 src\u002Fstore\u002Findex.ts \n\n```js\nimport { createPinia } from 'pinia'\nconst store = createPinia()\nexport default store\n```\n\n\n### 在main.ts 全局中使用 \n\n```js\nimport { createApp } from 'vue'\nimport App from '.\u002FApp.vue'\nimport store from '.\u002Fstore\u002Findex';\n \nconst app = createApp(App);\napp.use(store).mount('#app')\n```\n\n\n\n## 核心概念与基本使用\n\n### Store\n\n> Store 是一个保存状态和业务逻辑的实体，可以自由读取和写入，并通过导入后在 setup 中使用创建一个 store\n\n```javascript\n\u002F\u002F新建src\u002Fstore\u002Fuser.ts\nimport { defineStore } from 'pinia';\n\nexport const useUserStore = defineStore({\n  id: 'user',\n  state: () => {\n    return {\n      name: '小白龙',\n    };\n  },\n});\n```\n\n#### 使用 Store\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv>{{ userStore.name }}\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript lang=\\\"ts\\\" setup>\nimport { useUserStore } from '@\u002Fstore\u002Fuser'\n\nconst userStore = useUserStore()\n\u003C\u002Fscript>\n```\n\n\n\n###  storeToRefs\n\n> state 也可以使用解构，但使用解构会使其失去响应式，使用 storeToRefs可以不失去响应式\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv>{{ name }}\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript lang=\\\"ts\\\" setup>\nimport { storeToRefs } from 'pinia'\nimport { useUserStore } from '@\u002Fstore\u002Fuser'\n\nconst userStore = useUserStore()\nconst { name } = storeToRefs(userStore)\n\u003C\u002Fscript>\n```\n\n\n\n#### computed\n\n> state 直接赋值给变量，会使其失去响应式，使用vue3的computed()可以不失去响应式\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv>{{ name }}\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript lang=\\\"ts\\\" setup>\nimport { storeToRefs } from 'pinia'\nimport { useUserStore } from '@\u002Fstore\u002Fuser'\n\nconst userStore = useUserStore()\nconst name = computed(() => useUserStore.name)\n\u003C\u002Fscript>\n```\n\n\n\n## 修改 state\n\n#### 1、直接修改（不推荐）\n\n```js\nuserStore.name = '小龙龙'\n```\n\n\n\n#### 2、通过 actions去修改 state，actions 里可以直接通过 this 访问。\n\n```js\nexport const useUserStore = defineStore({\n  id: 'user',\n  state: () => {\n    return {\n      name: '小白龙',\n      age:18,\n      friendList: [\n         { name: '浪里小白龙', age: 18 },\n         { name: '浪里个浪小白龙', age: 20 },\n      ]\n    }\n  },\n  actions() {\n    updateName(name: string) {\n      this.name = name\n    }\n  }\n})\n```\n\n\n\n```vue\n\u003Cscript lang=\\\"ts\\\" setup>\nimport { useUserStore } from '@\u002Fstore\u002Fuser'\nconst userStore = useUserStore()\nuserStore.updateName('小龙龙')\n\u003C\u002Fscript>\n```\n\n\n\n#### 3、使用 $patch 修改多条数据\n\n> $patch的方式是经过优化的，多条数据同时更新状态数据会加快修改速度，性能有很大的好处。\n\n```ts\n\u003Cscript lang=\\\"ts\\\" setup>\nimport { useUserStore } from '@\u002Fstore\u002Fuser'\nconst userStore = useUserStore()\n\u002F\u002F方式一(浅层修改)\nuserStore.$patch({\n    name: '浪里' + userStore.name,\n    age: userStore.age + 2\n  });\n\u002F\u002F方式二(深层修改)\nuserStore.$patch(state => {\n   state.name = '浪里' + userStore.name;\n   state.age = userStore.age + 2;\n   state.friendList[0].name = '小萝莉' \n});\n\u003C\u002Fscript>\n```\n\n\n\n#### 4、重置状态\n\n> 可以通过调用store 上的方法将状态重置为初始状态\n\n```vue\n\u003Cscript lang=\\\"ts\\\" setup>\nimport { useUserStore } from '@\u002Fstore\u002Fuser'\nconst userStore = useUserStore()\nuserStore.$reset()\n\u003C\u002Fscript>\n```\n\n\n\n#### 5、监听订阅state\n\n>  通过 store.$subscribe() 的方法，该方法的第一个参数接受一个回调函数，该函数可以在 state 变化时触发\n\n```js\nconst subscribe = mainStore.$subscribe((mutation, state) => {\n    console.log(mutation)\n    console.log(state)\n})\n```\n\n其中 state 是 mainStore 实例，而 mutation 打印如下\n\n可以发现，打印结果的mutation对象主要包含三个属性\n\n- events : 是这次state改变的具体数据，包括改变前的值和改变后的值等等数据\n\n- storeId ：是当前store的id\n\n- type：type表示这次变化是通过什么产生的，主要有三个分别是\n\n- - “direct” ：通过 action 变化的\n  - ”patch object“ ：通过 $patch 传递对象的方式改变的\n  - “patch function” ：通过 $patch 传递函数的方式改变的\n\n\n\n#### 6、停止监听\n\nstore.$subscribe() 的方法的第二个参数options对象，是各种配置参数，包括\n\ndetached属性，其值是一个布尔值，默认是 false， 正常情况下，当 订阅所在的组件被卸载时，订阅将被停止删除，如果设置detached值为 true 时，即使所在组件被卸载，订阅依然可以生效。\n\n其他属性主要还有 immediate、deep、flush 等等，和 vue3 watch的对应参数效果一样。\n\n\n\n\n\n### Getters\n\n```javascript\nexport const useUserStore = defineStore({\n  id: 'user',\n  state: () => {\n    return {\n      name: '小白龙',\n    }\n  },\n  getters: {\n    formatName: (state) => {\n      return state.name + '0418';\n    },\n  },\n})\n```\n\n```js\nuserStore.formatName    \u002F\u002F小白龙0418\n```\n\n\n## pinia模块化\n> 新建store\u002Fuser.js文件\n```language\nimport { defineStore } from 'pinia'\n\nconst useUserStore = defineStore('user', {\n  state: () => {\n    return {\n      count: 0\n    }\n  },\n  getters: {\n    double() {\n      return this.count * 2\n    },\n  }\n})\n\nexport default useUserStore\n\n```\n\n> 新建store\u002Fcounter.js文件\n```language\nimport { defineStore } from 'pinia'\n\nconst useCounterStore = defineStore('counter', {\n  state: () => {\n    return {\n      name: 'ls',\n      age: 120,\n    }\n  },\n})\n\nexport default useCounterStore\n```\n\n> 新建store\u002Findex.js\n```language\nimport useUserStore from '.\u002Fuser'\nimport useCounterStore from '.\u002Fcounter'\n\n\u002F\u002F 统一导出useStore方法\nexport default function useStore() {\n  return {\n    user: useUserStore(),\n    counter: useCounterStore(),\n  }\n}\n```\n\n> 在组件中使用\n```language\n\u003Cscript setup>\nimport { storeToRefs } from 'pinia'\nimport useStore from '.\u002Fstore'\nconst { counter } = useStore()\n\n\u002F\u002F 使用storeToRefs可以保证解构出来的数据也是响应式的\nconst { user, counter } = storeToRefs(counter)\n\u003C\u002Fscript>\n```\n\n\n\n### Setup 语法\n>ref 与 state 对应、computed 与 getters 对应、function 与 actions 对应。\n```js\nexport const useCounterStore = defineStore('counter', () => {\n  const count = ref(0)\n  const doubleCount = computed(() => count.value * 2)\n  function increment() {\n    count.value++\n  }\n\n  return { count, doubleCount, increment }\n})\n\n\u002F** 在 setup 外使用 *\u002F\nexport function useCounterStoreHook() {\n  return useCounterStore(store)\n}\n```\n\n\n\n\n\n\n\n\n## 数据持久化\n\n> 解决页面刷新，Pinia数据丢失\n\n\n\n### 安装  \n\n```shell\nnpm i -S pinia-plugin-persistedstate\n```\n\n\n\n\n### 使用\n\n```javascript\n\u002F\u002F src\u002Fstore\u002Findex.ts\n\nimport { createPinia } from 'pinia'\nimport piniaPluginPersistedstate from 'pinia-plugin-persistedstate'\n\nconst store = createPinia()\nstore.use(piniaPluginPersistedstate)\n\nexport default store\n```\n\n\n\n```js\n\u002F\u002Fsrc\u002Fstore\u002Fuser.ts\nimport { defineStore } from 'pinia';\n\nexport const useUserStore = defineStore({\n  id: 'user',\n  state: () => {\n    return {\n      name: '小白龙',\n    };\n  },\n\n  actions: {\n    updateName(name: string) {\n      this.name = name;\n    },\n  },\n\n  getters: {\n    formatName: (state) => {\n      return state.name + '0418';\n    },\n  },\n\n  \u002F\u002F 开启数据缓存\n persist: true,\n});\n```\n\n\n\n## 自定义 key\n\n> 数据默认存在 sessionStorage 里，并且会以 store 的 id 作为 key你也可以在 strategies 里自定义 key 值，并将存放位置由 sessionStorage 改为 localStorage。\n\n```js\n\u002F\u002F 开启数据缓存\n  persist: {\n    storage: sessionStorage,\n    paths: ['someState'],\n  },\n```\n\n\n\n## 持久化部分 state\n\n> 默认所有 state 都会进行缓存，你可以通过 paths 指定要持久化的字段，其他的则不会进行持久化。\n\n```yaml\nstate: () => {\n  return {\n    name: '小白龙',\n    age: 18,\n    gender: '男'\n  }  \n},\n\n  persist: {\n    storage: sessionStorage,\n    paths: ['someState'],\n  },\n```\n\n\n\n## 常见问题\n","Pinia 是 Vue 的存储库，它允许您跨组件\u002F页面共享状态。","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F98b68019b402f3b1dbb1fd59fee9ca33.png",2211,"2022-05-21T10:21:17.000Z","2026-05-25T02:21:33.000Z",{"id":5,"name":6,"slug":7},{"id":27,"name":28,"avatar":21},[105],{"id":106,"name":107,"slug":108},"d232e01f-048e-4151-8a0a-fff9561f946f","Pinia","pinia",6,10,{"categories":112,"tags":191,"postCount":365,"tagCount":366,"hotPosts":367},[113,120,121,128,134,139,146,150,157,161,165,170,176,181,185],{"id":114,"name":115,"slug":116,"description":117,"sortOrder":118,"createdAt":119,"updatedAt":119,"postCount":8},"e8d0bd45-d10c-46d3-8afb-0c072df7f8a7","技术","tech","技术文章",0,"2026-06-27T04:18:37.371Z",{"id":5,"name":6,"slug":7,"description":6,"sortOrder":8,"createdAt":9,"updatedAt":10,"postCount":109},{"id":122,"name":123,"slug":124,"description":123,"sortOrder":125,"createdAt":126,"updatedAt":10,"postCount":127},"11d4d397-685c-4180-a7b3-9b0e3a1e411e","Css","css",2,"2022-05-23T07:19:37.000Z",9,{"id":129,"name":130,"slug":130,"description":130,"sortOrder":131,"createdAt":132,"updatedAt":10,"postCount":133},"d10456a5-e649-4741-a38f-f07f266ce5f2","开发环境",3,"2022-05-24T01:52:41.000Z",13,{"id":135,"name":32,"slug":33,"description":32,"sortOrder":136,"createdAt":137,"updatedAt":10,"postCount":138},"5ed5cc62-43ea-49a2-b0b2-38bc7aae52a0",4,"2022-05-24T01:55:05.000Z",8,{"id":140,"name":141,"slug":142,"description":141,"sortOrder":143,"createdAt":144,"updatedAt":10,"postCount":145},"da130ba9-d4f4-49f3-aa0f-149078097ef0","JavaScript","javascript",5,"2022-05-24T02:22:57.000Z",18,{"id":147,"name":148,"slug":148,"description":148,"sortOrder":109,"createdAt":149,"updatedAt":10,"postCount":8},"d8cbe380-54b3-4a61-a12d-5438c2918574","限时优惠","2022-05-25T07:18:03.000Z",{"id":151,"name":152,"slug":153,"description":152,"sortOrder":154,"createdAt":155,"updatedAt":10,"postCount":156},"e0f3b8d8-cfe7-41fb-802b-a79699d95968","JavaScript插件","javascript插件",7,"2022-06-01T14:08:31.000Z",16,{"id":158,"name":159,"slug":159,"description":159,"sortOrder":138,"createdAt":160,"updatedAt":10,"postCount":109},"4ea3d8af-9cc3-49bb-a9cd-34dbcdc3bd85","构建工具","2022-06-02T07:28:13.000Z",{"id":162,"name":163,"slug":163,"description":163,"sortOrder":127,"createdAt":164,"updatedAt":10,"postCount":138},"9ed9827c-9cbb-42da-80e4-d04c7fdba886","开发工具","2022-06-21T03:35:05.000Z",{"id":166,"name":167,"slug":168,"description":167,"sortOrder":110,"createdAt":169,"updatedAt":10,"postCount":143},"6b9179c3-17b2-43ff-a431-a03d6eb32d89","Vue2 生态","vue2-生态","2022-07-16T13:14:29.000Z",{"id":171,"name":172,"slug":173,"description":172,"sortOrder":174,"createdAt":175,"updatedAt":10,"postCount":143},"73a5f62c-3c47-45b9-9ae2-f29953ae8dc0","Node","node",11,"2022-07-16T13:15:39.000Z",{"id":177,"name":178,"slug":178,"description":178,"sortOrder":179,"createdAt":180,"updatedAt":10,"postCount":125},"2b696c16-48ef-403b-a88b-6e57cfc79596","开发问题",12,"2022-07-16T14:06:54.000Z",{"id":182,"name":183,"slug":183,"description":183,"sortOrder":133,"createdAt":184,"updatedAt":10,"postCount":8},"c0f0561e-a47a-4ecd-8caa-cc1df2315d57","算法","2022-07-16T14:22:34.000Z",{"id":186,"name":187,"slug":188,"description":187,"sortOrder":189,"createdAt":190,"updatedAt":10,"postCount":131},"a629c1f7-29f1-439e-be3c-29670b17ba20","Vue2","vue2",15,"2022-07-16T14:41:51.000Z",[192,198,203,206,211,216,220,224,229,234,239,244,247,249,250,254,259,264,269,274,278,279,283,284,288,293,298,301,304,307,310,313,317,320,322,324,327,331,334,338,342,345,348,350,353,355,359,362],{"id":193,"name":194,"slug":195,"createdAt":196,"updatedAt":197},"076bd8b9-293e-45cb-9dc3-e162007ca474","Axios","axios","2022-06-05T07:41:56.000Z","2025-12-30T07:26:21.000Z",{"id":199,"name":200,"slug":201,"createdAt":202,"updatedAt":10},"2aa7f6d0-1fac-4ed1-b9bb-f3afc813f42c","Axure","axure","2022-06-21T03:35:15.000Z",{"id":204,"name":123,"slug":124,"createdAt":205,"updatedAt":10},"b084ddd8-09be-4e57-98f0-cf4e376aecd7","2022-05-21T09:59:55.000Z",{"id":207,"name":208,"slug":209,"createdAt":210,"updatedAt":10},"78a62bff-ff77-4878-8c25-3e6aae18c668","Docker","docker","2022-07-16T14:34:37.000Z",{"id":212,"name":213,"slug":214,"createdAt":215,"updatedAt":10},"2de16806-ef3f-4e54-a259-d1e1e182468c","Git","git","2022-07-16T14:25:15.000Z",{"id":217,"name":218,"slug":219,"createdAt":205,"updatedAt":10},"994cc226-578b-4a72-a57e-a47a63d2793e","JavaScript生态","javascript生态",{"id":221,"name":222,"slug":223,"createdAt":205,"updatedAt":10},"5086e93c-23b9-43d3-9643-cc87f0e9ee94","JenKins","jenkins",{"id":225,"name":226,"slug":227,"createdAt":228,"updatedAt":10},"b73007a8-bb5c-42a8-9fd9-163033a5b45d","Linux","linux","2022-07-16T14:40:17.000Z",{"id":230,"name":231,"slug":232,"createdAt":233,"updatedAt":10},"0b658b92-dd6b-4db3-a398-9f6d69950a02","Markdown","markdown","2022-07-16T14:39:25.000Z",{"id":235,"name":236,"slug":237,"createdAt":238,"updatedAt":10},"ab034d3a-6e5b-4db5-a2dc-faf4ccbb63f5","Nest","nest","2022-07-16T13:15:49.000Z",{"id":240,"name":241,"slug":242,"createdAt":243,"updatedAt":10},"52c41978-da06-4962-9636-45bbaeedda80","Nginx","nginx","2022-05-21T09:59:56.000Z",{"id":245,"name":246,"slug":246,"createdAt":243,"updatedAt":10},"0f1cc678-40e4-44b1-b2cf-a6fd8a1c867a","npm",{"id":73,"name":74,"slug":75,"createdAt":248,"updatedAt":10},"2022-06-01T13:07:07.000Z",{"id":106,"name":107,"slug":108,"createdAt":205,"updatedAt":10},{"id":251,"name":252,"slug":253,"createdAt":243,"updatedAt":10},"14e9ab02-b0bb-4c85-8604-fe6f1f0f33cd","Pnpm","pnpm",{"id":255,"name":256,"slug":257,"createdAt":258,"updatedAt":258},"399d1d38-cc0d-43ce-8baf-c769447a2ebd","React生态","react生态","2023-02-21T02:03:09.000Z",{"id":260,"name":261,"slug":262,"createdAt":263,"updatedAt":10},"c95bbe84-bdd0-410a-86a9-e87958c55f4f","Redis","redis","2022-10-05T05:14:14.000Z",{"id":265,"name":266,"slug":267,"createdAt":268,"updatedAt":10},"6d05f9df-e116-450f-af57-85ed710c4870","Swiper","swiper","2022-06-01T14:08:46.000Z",{"id":270,"name":271,"slug":272,"createdAt":273,"updatedAt":10},"66f3aeb0-84ef-45f6-a43a-944eefc9895a","Vite","vite","2022-06-02T07:28:24.000Z",{"id":275,"name":276,"slug":277,"createdAt":205,"updatedAt":10},"bf5b94d3-090b-4098-a03c-4bc69781fb2d","Vue","vue",{"id":89,"name":90,"slug":91,"createdAt":243,"updatedAt":10},{"id":280,"name":281,"slug":282,"createdAt":205,"updatedAt":10},"2fef3b91-1c1c-4ae8-b2c1-0e04b4f9b3a2","Vue2生态","vue2生态",{"id":31,"name":32,"slug":33,"createdAt":205,"updatedAt":10},{"id":285,"name":286,"slug":287,"createdAt":205,"updatedAt":10},"20bff9cd-7848-4c16-8775-42cf12b44b30","Vue3生态","vue3生态",{"id":289,"name":290,"slug":291,"createdAt":292,"updatedAt":10},"c807b2c6-cb12-4409-a1f1-6bea9f330a6b","Vuex","vuex","2022-07-16T13:14:59.000Z",{"id":294,"name":295,"slug":296,"createdAt":297,"updatedAt":10},"5782dff5-2ea2-4427-9696-d4363a7fd5bc","Webpack","webpack","2022-07-16T14:33:41.000Z",{"id":299,"name":300,"slug":300,"createdAt":205,"updatedAt":10},"d0aa41f4-68f8-48d4-a4ed-3a503ea90451","下载",{"id":302,"name":303,"slug":303,"createdAt":205,"updatedAt":10},"a046060c-39ef-474a-8c85-2546aca0e2e5","代码片段",{"id":305,"name":306,"slug":306,"createdAt":205,"updatedAt":10},"fee73435-b2be-4b55-85b1-d133ea96aea4","伪元素",{"id":308,"name":309,"slug":309,"createdAt":205,"updatedAt":10},"436bd369-8c57-4869-8827-e88e50e5e0ab","伪类",{"id":311,"name":312,"slug":312,"createdAt":205,"updatedAt":10},"4c6be544-8a00-4445-92a3-e3dcbaf6142e","动画",{"id":314,"name":315,"slug":315,"createdAt":316,"updatedAt":10},"9321a12e-ea72-49a9-a32d-5566149f812f","图片压缩","2022-08-02T00:37:47.000Z",{"id":318,"name":319,"slug":319,"createdAt":243,"updatedAt":10},"512b16fb-576a-4397-a7c5-dd20e6a8f9ca","布局",{"id":321,"name":163,"slug":163,"createdAt":205,"updatedAt":10},"f32faa96-f2ec-45c6-9a17-2c76062edcb0",{"id":323,"name":130,"slug":130,"createdAt":205,"updatedAt":10},"3c46ed3f-6d6b-4f91-bcb3-af5112860bf5",{"id":325,"name":326,"slug":326,"createdAt":205,"updatedAt":10},"dbfc086a-73a6-4560-814d-593acb61cf98","性能优化",{"id":328,"name":329,"slug":329,"createdAt":330,"updatedAt":10},"1831cd06-0d6b-48f7-94fa-324782fe23cb","拖拽","2022-07-28T12:39:13.000Z",{"id":332,"name":333,"slug":333,"createdAt":243,"updatedAt":10},"9a74300d-06f7-46d0-80d9-8fe67ec0539b","数组",{"id":335,"name":336,"slug":336,"createdAt":337,"updatedAt":10},"19ac8998-7e0a-459b-9702-bb1adca70e8c","文本复制","2022-07-17T01:54:45.000Z",{"id":339,"name":340,"slug":340,"createdAt":341,"updatedAt":10},"5ff33473-71a4-4e02-8c82-f9ea369a768f","时间","2022-07-17T01:51:12.000Z",{"id":343,"name":344,"slug":344,"createdAt":205,"updatedAt":10},"aa47ca4d-d3f6-4cac-b495-2c67c9592c36","最新优惠",{"id":346,"name":347,"slug":347,"createdAt":243,"updatedAt":10},"f6766d54-54fc-405e-932d-b7d550559125","服务器",{"id":349,"name":159,"slug":159,"createdAt":205,"updatedAt":10},"d856559a-03ff-40b4-980d-3f272b998c3c",{"id":351,"name":352,"slug":352,"createdAt":205,"updatedAt":10},"692d5d68-b188-4e5c-aca8-65d0229399a1","渐变",{"id":354,"name":183,"slug":183,"createdAt":205,"updatedAt":10},"38e1fd6b-d7c6-4d62-bf70-7bacc175bea9",{"id":356,"name":357,"slug":357,"createdAt":358,"updatedAt":10},"be7b10bc-49eb-4a03-bea7-ceb915d500fe","规范","2022-07-16T14:41:06.000Z",{"id":360,"name":361,"slug":361,"createdAt":243,"updatedAt":10},"b42e2916-ad62-4b8a-a863-cd8c19a829de","面试",{"id":363,"name":364,"slug":364,"createdAt":243,"updatedAt":10},"7069add9-b636-44f1-9cd4-ea3a6d2b85d3","面试题",104,48,[368,380,392,405,418],{"id":369,"title":370,"slug":370,"content":371,"excerpt":370,"coverImage":372,"status":19,"isPinned":20,"pinnedAt":21,"viewCount":373,"publishedAt":374,"createdAt":374,"updatedAt":375,"category":376,"author":377,"tags":378},"a1bd1f49-6f6d-4fea-9789-b5636e19a6b3","uni-app瀑布流","## 实现思路\n> 获取父组件的列表数组，watch监听数组长度变化，截取后面新的数据，创建两个左右数组，比较左右dom的长度，哪个短，就push一条数据进去，源数组删除一条数据。利用img的load（加载成功）和error方法（加载失败），触发数组的push，实现瀑布流\n\n## 代码实现\n\n```language\n\u003Ctemplate>\n    \u003Cview class=\\\"waterfall\\\">\n        \u003Cview class=\\\"waterfall_left\\\">\n            \u003Cview class=\\\"waterfall_list\\\" v-for=\\\"(item, index) in leftList\\\" :key=\\\"index\\\">\n                \u003CSearch\n                    :name=\\\"item.name\\\"\n                    :image=\\\"item.src\\\"\n                    :label=\\\"item.label\\\"\n                    :item=\\\"item\\\"\n                    @considerPush=\\\"considerPush\\\"\n                >\n                \u003C\u002FSearch>\n            \u003C\u002Fview>\n        \u003C\u002Fview>\n        \u003Cview class=\\\"waterfall_right\\\">\n            \u003Cview class=\\\"waterfall_list\\\" v-for=\\\"(item, index) in rightList\\\" :key=\\\"index\\\">\n                \u003CSearch\n                    :name=\\\"item.name\\\"\n                    :image=\\\"item.src\\\"\n                    :label=\\\"item.label\\\"\n                    :item=\\\"item\\\"\n                    @considerPush=\\\"considerPush\\\"\n                >\n                \u003C\u002FSearch>\n            \u003C\u002Fview>\n        \u003C\u002Fview>\n    \u003C\u002Fview>\n\u003C\u002Ftemplate>\n\n\u003Cscript>\nimport Search from '@\u002Fcomponents\u002Fsearch\u002Fsearch.vue';\nexport default {\n    components: { Search },\n    props: {\n        list: {\n            type: Array,\n            default: () => []\n        }\n    },\n    data() {\n        return {\n            \u002F\u002F 左侧列表\n            leftList: [],\n            \u002F\u002F 右侧列表\n            rightList: [],\n            \u002F\u002F 组件数据备份\n            newList: [],\n            \u002F\u002F默认请求数,主要为了正常排序\n            interceptNumber: 10\n        };\n    },\n    created() {\n        this.touchOff(); \u002F\u002F 触发排列\n    },\n    mounted() {},\n    watch: {\n        list(newValue, oldValue) {\n            this.interceptNumber = newValue.length - oldValue.length;\n            this.touchOff();\n        }\n    },\n    computed: {},\n    methods: {\n        \u002F\u002F 触发重新排列\n        touchOff() {\n            this.newList = [...this.list.slice(-this.interceptNumber)];\n            if (this.newList.length !== 0) {\n                this.leftList.push(this.newList.shift()); \u002F\u002F触发排列\n            }\n        },\n        \u002F\u002F 计算排列\n        considerPush() {\n            this.$nextTick(() => {\n                if (this.newList.length == 0) return; \u002F\u002F没有数据了\n                let leftH = 0;\n                let rightH = 0; \u002F\u002F左右高度\n                let query = uni.createSelectorQuery().in(this);\n                query.selectAll('.waterfall_left').boundingClientRect();\n                query.selectAll('.waterfall_right').boundingClientRect();\n                query.exec(res => {\n                    leftH = res[0].length != 0 ? res[0][0].height : 0; \u002F\u002F防止查询不到做个处理\n                    rightH = res[1].length != 0 ? res[1][0].height : 0;\n                    if (leftH == rightH || leftH \u003C rightH) {\n                        \u002F\u002F 相等 || 左边小\n                        this.leftList.push(this.newList.shift());\n                    } else {\n                        \u002F\u002F 右边小\n                        this.rightList.push(this.newList.shift());\n                    }\n\n                    \u002F\u002F console.log('左右高度：', leftH, rightH, leftH == rightH || leftH \u003C rightH);\n                });\n            });\n        }\n    }\n};\n\u003C\u002Fscript>\n\n\u003Cstyle scoped lang=\\\"scss\\\">\n.waterfall {\n    display: flex;\n    align-items: flex-start;\n    justify-content: flex-start;\n    .waterfall_left {\n        flex: 1;\n    }\n\n    .waterfall_right {\n        flex: 1;\n    }\n}\n\u003C\u002Fstyle>\n\n```\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F7d4fa8d1d2775177b882a4656e3a5ed5.png",1818,"2022-11-22T07:54:41.000Z","2026-06-27T11:04:59.676Z",{"id":140,"name":141,"slug":142},{"id":27,"name":28,"avatar":21},[379],{"id":217,"name":218,"slug":219},{"id":381,"title":382,"slug":382,"content":383,"excerpt":382,"coverImage":384,"status":19,"isPinned":20,"pinnedAt":21,"viewCount":385,"publishedAt":386,"createdAt":386,"updatedAt":387,"category":388,"author":389,"tags":390},"b2c46bf6-d971-4cce-b21b-052dbea8e8a2","v-html使用img点击实现放大效果","## 代码实现\n```js\n\u002F**\n * JS获取html代码中所有的图片地址\n * @param htmlstr\n * @returns arr 数组\n *\u002F\n\nexport function getimgsrc(htmlstr) {\n    let reg = \u002F\u003Cimg.+?src=('|\\\")?([^'\\\"]+)('|\\\")?(?:\\s+|>)\u002Fg;\n    let arr = [];\n    let tem = 0;\n    \u002F\u002Feslint-disable-next-line\n    while ((tem = reg.exec(htmlstr))) {\n        arr.push(tem[2]); \u002F\u002F eslint-disable-line\n    }\n\n    return arr;\n}\n\n```\n\n\n\n```vue\n\u003Ctemplate>\n    \u003Cdiv class=\\\"image-expansion\\\" :class=\\\"classArr\\\">\n        \u003Cdiv @click.stop=\\\"hanldeImage($event)\\\" v-html=\\\"formatHtmlData\\\">\u003C\u002Fdiv>\n\n        \u003Cel-image-viewer\n            v-if=\\\"imgPreviewUrl\\\"\n            :initial-index=\\\"subscript\\\"\n            :src=\\\"imgPreviewUrl\\\"\n            :on-close=\\\"closeViewer\\\"\n            :url-list=\\\"imgList\\\"\n        >\u003C\u002Fel-image-viewer>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript>\nimport { getimgsrc } from '..\u002F..\u002Futils\u002Fgetimgsrc';\nimport ElImageViewer from 'element-ui\u002Fpackages\u002Fimage\u002Fsrc\u002Fimage-viewer';\nexport default {\n    components: {\n        ElImageViewer\n    },\n    props: {\n        htmlData: {\n            type: String,\n            default: () => {\n                return '';\n            }\n        },\n        classArr: {\n            type: Array,\n            default: () => {\n                return ['min'];\n            }\n        },\n\n        isArticle: {\n            type: Boolean,\n            default: () => {\n                return false;\n            }\n        }\n    },\n    data() {\n        return {\n            imgList: [],\n            formatHtmlData: '',\n            imgPreviewUrl: '',\n            subscript: 0\n        };\n    },\n    computed: {},\n\n    watch: {\n        \u002F\u002F监听数据，防止数据不更新\n        htmlData: {\n            handler(newName, oldName) {\n                \u002F\u002F判断是否为文章\n                if (this.isArticle) {\n                    newName ? (this.formatHtmlData = newName.replace(\u002F(\u003C([^>]+)>)\u002Fgi, '').replace(\u002F[\\\r\\\n]\u002Fg, '')) : '';\n                } else {\n                    \u002F\u002F剔除strong和p标签\n                    newName ? (this.formatHtmlData = newName.replace(\u002F(\u003C\\\u002F?strong.*?>)|(\u003C\\\u002F?p.*?>)\u002Fg, '')) : '';\n\n                    \u002F\u002F获取html全部图片，push成图片数组\n                    this.imgList = Object.values(getimgsrc(this.formatHtmlData));\n                    \u002F\u002F获取图片下标\n                    let subscript = this.imgList.indexOf(this.imgPreviewUrl);\n                    this.subscript = subscript > -1 ? subscript : 0;\n                }\n            },\n\n            immediate: true\n        }\n    },\n\n    mounted() {},\n\n    methods: {\n        \u002F\u002F监听点击事件\n        hanldeImage(event) {\n            if (event.target.nodeName === 'IMG' || event.target.nodeName === 'img') {\n                \u002F\u002F获取点击的图片url,decodeURIComponent转码一下，防禁url转码\n                this.imgPreviewUrl = decodeURIComponent(event.target.currentSrc);\n\n                \u002F\u002F获取图片下标\n                let subscript = this.imgList.indexOf(this.imgPreviewUrl);\n                this.subscript = subscript > -1 ? subscript : 0;\n\n                \u002F\u002F禁止遮罩层后面的内容滚动\n                document.documentElement.style.overflowY = 'hidden';\n            } else {\n                this.$emit('goDetail');\n            }\n        },\n\n        \u002F\u002F关闭弹框\n        closeViewer() {\n            this.imgPreviewUrl = '';\n            \u002F\u002F恢复遮罩层后面的内容滚动\n            document.documentElement.style.overflowY = 'auto';\n        }\n    }\n};\n\u003C\u002Fscript>\n\n\u003Cstyle lang=\\\"scss\\\" scoped>\n.image-expansion {\n}\n\n.min {\n    \u002Fdeep\u002F img {\n        cursor: pointer;\n        height: 28px;\n        padding: 0 10px 3px;\n    }\n}\n\n.max {\n    \u002Fdeep\u002F img {\n        cursor: pointer;\n    }\n}\n\n.class1 {\n    \u002Fdeep\u002F div {\n        font-size: 15px;\n        font-family: Microsoft YaHei;\n        font-weight: 400;\n        color: #888888;\n        line-height: 30px;\n    }\n}\n\u003C\u002Fstyle>\n\n\n```\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002Fc77c3fb113d1ab2f67e7afba1ca33b95.png",1572,"2023-01-10T07:22:29.000Z","2026-06-27T11:07:40.499Z",{"id":166,"name":167,"slug":168},{"id":27,"name":28,"avatar":21},[391],{"id":314,"name":315,"slug":315},{"id":393,"title":394,"slug":395,"content":396,"excerpt":394,"coverImage":397,"status":19,"isPinned":20,"pinnedAt":21,"viewCount":398,"publishedAt":399,"createdAt":399,"updatedAt":400,"category":401,"author":402,"tags":403},"9f33b49b-0785-4404-9a4e-d85937e7fc92","在 vue3 中优雅的使用 jsx\u002Ftsx","在-vue3-中优雅的使用-jsx-tsx","## 安装插件（@vitejs\u002Fplugin-vue-jsx）\n```shell\npnpm add @vitejs\u002Fplugin-vue-jsx -D\n```\n\n## 配置vite.config.ts\n```ts\nimport vueJsx from \\\"@vitejs\u002Fplugin-vue-jsx\\\";\n\nexport default defineConfig({\n  plugins: [\n    vueJsx(),\n  ]\n})\n```\n\n## 插值\n```language\n\u002F\u002F vue3模板语法\n\u003Cspan>{{ a + b }}\u003C\u002Fspan>\n\n\u002F\u002F jsx\u002Ftsx\n\u003Cspan>{ a + b }\u003C\u002Fspan>\n```\n\n\n## class与style 绑定\n\n```ts\n\u002F\u002F 模板字符串\n\u003Cdiv className={`header ${ isBg ? 'headerBg' : '' }`}>header\u003C\u002Fdiv>\n\u002F\u002F数组\n\u003Cdiv class={ [ 'header', isBg && 'headerBg' ] } >header\u003C\u002Fdiv>\n```\n\n```ts\nconst color = 'red'\nconst element = \u003Csapn style={{ color, fontSize: '16px' }}>style\u003C\u002Fsapn>\n```\n\n## 条件渲染\n\n```ts\n   setup() {\n       const isShow = false\n       const element = () => {\n           if (isShow) {\n               return \u003Cspan>我是if\u003C\u002Fspan>\n           } else {\n               return \u003Cspan>我是else\u003C\u002Fspan>\n           }\n       }\n       return () => (\n           \u003Cdiv>\n               \u003Cspan v-show={isShow}>我是v-show\u003C\u002Fspan>\n               {\n                   element()\n               }\n               {\n                   isShow ? \u003Cp>我是三目1\u003C\u002Fp> : \u003Cp>我是三目2\u003C\u002Fp>\n               }\n           \u003Cdiv>\n       )\n   }\n```\n\n## 列表渲染\n```language\nsetup() {\n   const listData = [\n       {name: 'Tom', age: 18},\n       {name: 'Jim', age: 20},\n       {name: 'Lucy', age: 16}\n   ]\n   return () => (\n       \u003Cdiv>\n           \u003Cdiv class={'box'}>\n               \u003Cspan>姓名\u003C\u002Fspan>\n               \u003Cspan>年龄\u003C\u002Fspan>\n           \u003C\u002Fdiv>\n           {\n               prop.listData.map(item => {\n                   return \u003Cdiv class={'box'}>\n                       \u003Cspan>{item.name}\u003C\u002Fspan>\n                       \u003Cspan>{item.age}\u003C\u002Fspan>\n                   \u003C\u002Fdiv>\n               })\n           }\n       \u003C\u002Fdiv>\n   )\n}\n\n```\n\n## 事件处理\n\n```language\nsetup() {\n    const clickBox = val => {\n        console.log(val)\n    }\n    return () => (\n        \u003Cdiv class={'box1'} onClick={() => clickBox('box1')}>\n            \u003Cspan>我是box1\u003C\u002Fspan>\n            \u003Cdiv class={'box2'} onClick={() => clickBox('box2')}>\n                \u003Cspan>我是box2\u003C\u002Fspan>\n                \u003Cdiv class={'box3'} onClick={withModifiers(() => clickBox('box3'), ['stop'])}>我是box3\u003C\u002Fdiv>\n            \u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n    )\n}\n```\n\n## v-model\n\n```ts\n\u002F\u002F 正常写法\n\u003Cinput v-model=\\\"value\\\" \u002F> \u002F\u002F vue\n\u003Cinput v-model={value} \u002F> \u002F\u002F jsx\n\n\u002F\u002F 指定绑定值写法\n\u003Cinput v-model:modelValue=\\\"value\\\" \u002F> \u002F\u002F vue\n\u003Cinput v-model={[value,'modelValue']} \u002F> \u002F\u002F jsx\n\n\u002F\u002F 修饰符写法\n\u003Cinput v-model:modelValue.trim=\\\"value\\\" \u002F> \u002F\u002F vue\n\u003Cinput v-model={[value,'modelValue',['trim']]} \u002F> \u002F\u002F jsx\n```\n\n## slot插槽\n\n### 定义插槽\n```ts\nimport { renderSlot } from \\\"vue\\\"\nexport default defineComponent({\n    \u002F\u002F 从ctx中解构出来 slots\n    setup(props, { slots }) {\n        return () => (\n            \u003Cdiv>\n                { renderSlot(slots, 'default') }\n                { slots.title?.() }\n            \u003C\u002Fdiv>\n        )\n    }\n})\n\n```\n\n### 使用插槽\n\n```ts\nimport Vslot from '.\u002FslotTem'\nexport default defineComponent({\n    setup() {\n        return () => (\n            \u003Cdiv class={'box'}>\n                \u003CVslot v-slots={{\n                    title: () => {\n                        return \u003Cp>我是title插槽\u003C\u002Fp>\n                    },\n                    default: () => {\n                        return \u003Cp>我是default插槽\u003C\u002Fp>\n                    }\n                }} \u002F>\n            \u003C\u002Fdiv>\n        )\n    }\n})\n\n```\n\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F3e4cb0d3df611ee3f57b8ed503e1015e.png",1392,"2023-04-19T09:23:27.000Z","2026-06-27T11:08:34.303Z",{"id":135,"name":32,"slug":33},{"id":27,"name":28,"avatar":21},[404],{"id":31,"name":32,"slug":33},{"id":406,"title":407,"slug":408,"content":409,"excerpt":407,"coverImage":410,"status":19,"isPinned":20,"pinnedAt":21,"viewCount":411,"publishedAt":412,"createdAt":412,"updatedAt":413,"category":414,"author":415,"tags":416},"1dcdd755-0e46-45a3-8998-9a213fd3fcd5","Vue3 导入导出Excel","vue3-导入导出excel","## 安装\n```shell\npnpm add -S XLSX\n```\n\n\n## 方法封装\n```js\nimport * as XLSX from 'xlsx';\n\n\u002F\u002F参数说明\n\u002F\u002Fconfiguration: {\n\u002F\u002F  data: [], \u002F\u002F 导出的数据\n\u002F\u002F  head: {}, \u002F\u002F 导出的数据对应的表头\n\u002F\u002F  name: '', \u002F\u002F 导出的文件名\n\u002F\u002F  label: '', \u002F\u002F 导出的表单名\n\u002F\u002F  widthArr: [], \u002F\u002F 导出的表单列宽\n\u002F\u002F}\n\n\u002F\u002F 导出excel\nexport const ExportXlsx = (configuration) => {\n  const { data, head, name, label, widthArr } = configuration;\n\n  const list = data.map((item) => {\n    const obj = {};\n    for (const k in item) {\n      if (head[k]) {\n        obj[head[k]] = item[k];\n      }\n    }\n    return obj;\n  });\n\n  \u002F\u002F 创建工作表\n  const xLSXData = XLSX.utils.json_to_sheet(list);\n  \u002F\u002F 创建工作簿\n  const wb = XLSX.utils.book_new();\n  \u002F\u002F 将工作表放入工作簿中\n  XLSX.utils.book_append_sheet(wb, xLSXData, label);\n  xLSXData['!cols'] = [];\n  \u002F\u002F 设置列宽\n  widthArr.forEach((item) => {\n    xLSXData['!cols'].push({ wpx: item });\n  });\n\n  \u002F\u002F 生成文件并下载\n  XLSX.writeFile(wb, `${name}.xlsx`);\n};\n\n\u002F\u002F 导入excel\nexport const ImportXlsx = (e) => {\n  const file = e.target.files[0];\n  const reader = new FileReader();\n  reader.readAsArrayBuffer(file);\n  reader.onload = (e) => {\n    const data = e.target.result;\n    const workbook = XLSX.read(data, { type: 'binary', cellDates: true });\n    const wsname = workbook.SheetNames[0];\n    const outdata = XLSX.utils.sheet_to_json(workbook.Sheets[wsname]);\n    console.log(outdata);\n  };\n};\n\n```\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F80231a326d3846b895a7d46c99e738ce.png",1335,"2023-03-14T06:18:59.000Z","2026-06-27T11:06:09.162Z",{"id":140,"name":141,"slug":142},{"id":27,"name":28,"avatar":21},[417],{"id":217,"name":218,"slug":219},{"id":419,"title":420,"slug":421,"content":422,"excerpt":420,"coverImage":423,"status":19,"isPinned":20,"pinnedAt":21,"viewCount":424,"publishedAt":425,"createdAt":425,"updatedAt":426,"category":427,"author":431,"tags":432},"80920598-a452-4357-bf53-842b200560e8","React18入门到精通教程","react18入门到精通教程","\n## JSX实现列表渲染\n> 使用`map()`方法遍历数组，必须添加`key`属性提高性能\n```jsx\nconst songs = [\n  { id: 1, name: \\\"helo1\\\" },\n  { id: 2, name: \\\"helo2\\\" },\n  { id: 3, name: \\\"helo3\\\" },\n];\n\nfunction App() {\n  return (\n    \u003Cdiv>\n      \u003Cul>\n        {songs.map((song) => (\n          \u002F\u002F 关键：添加唯一key标识符（避免使用索引）\n          \u003Cli key={song.id}>  \n            {song.id}-{song.name}\n          \u003C\u002Fli>\n        ))}\n      \u003C\u002Ful>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n**最佳实践：**\n1. 使用`\u003Cul>`包裹列表项\n2. `key`应使用稳定唯一标识（如ID），避免数组索引\n3. 空列表处理：`{songs.length > 0 && ...}` 或 `{songs.map(...) || \u003CEmptyView\u002F>}`\n\n## JSX实现条件渲染\n### 简单逻辑\n```jsx\n\u002F\u002F 三元表达式\n{isLoggedIn ? \u003CDashboard \u002F> : \u003CLoginForm \u002F>}\n\n\u002F\u002F 逻辑短路\n{hasNotification && \u003CNotificationBadge count={5} \u002F>}\n\n\u002F\u002F 空值处理\n{userProfile?.avatar || \u003CDefaultAvatar \u002F>}\n```\n\n### 复杂逻辑\n```jsx\nconst renderContent = (type) => {\n  switch(type) {\n    case 'success': \n      return \u003CSuccessAlert \u002F>;\n    case 'error':\n      return \u003CErrorAlert \u002F>;\n    default:\n      return \u003CInfoAlert \u002F>;\n  }\n}\n\nfunction App() {\n  return (\n    \u003Cdiv className=\\\"container\\\">\n      {renderContent(status)}\n      \n      {\u002F* 另一种模式：立即执行函数 *\u002F}\n      {(() => {\n        if (isLoading) return \u003CSpinner \u002F>;\n        if (isEmpty) return \u003CEmptyState \u002F>;\n        return \u003CDataTable \u002F>;\n      })()}\n    \u003C\u002Fdiv>\n  )\n}\n```\n\n## JSX样式处理\n### 行内样式\n```jsx\n\u002F\u002F 直接对象\n\u003Cdiv style={{ \n  color: 'white', \n  backgroundColor: 'teal',\n  padding: '1rem'\n}}>\n\n\u002F\u002F 样式对象复用\nconst alertStyle = {\n  padding: '15px',\n  borderRadius: '4px',\n  margin: '10px 0'\n};\n\nfunction Alert({ type }) {\n  return (\n    \u003Cdiv style={{\n      ...alertStyle,  \u002F\u002F 扩展运算符合并样式\n      background: type === 'error' ? '#f8d7da' : '#d4edda'\n    }}>\n      {message}\n    \u003C\u002Fdiv>\n  )\n}\n```\n\n### 类名控制（推荐）\n```css\n\u002F* styles.module.css *\u002F\n.card {\n  border: 1px solid #ddd;\n  border-radius: 8px;\n  padding: 20px;\n  box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n}\n\n.highlight {\n  background-color: #ffffe0;\n}\n```\n\n```jsx\nimport styles from '.\u002Fstyles.module.css';\n\nfunction ProductCard({ featured }) {\n  return (\n    \u003Cdiv className={`${styles.card} ${featured ? styles.highlight : ''}`}>\n      {\u002F* 内容 *\u002F}\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n## React 18新特性\n### 并发模式（Concurrent Mode）\n```jsx\nimport { startTransition } from 'react';\n\n\u002F\u002F 非紧急状态更新\nfunction handleSearch(query) {\n  startTransition(() => {\n    setSearchQuery(query); \u002F\u002F 可中断的渲染\n  });\n}\n```\n\n### 自动批处理（Automatic Batching）\n```jsx\n\u002F\u002F React 17及之前：两次渲染\n\u002F\u002F React 18：自动批处理，一次渲染\nfunction handleClick() {\n  setCount(c => c + 1);\n  setFlag(f => !f);\n}\n```\n\n## Redux状态管理（现代写法）\n### 安装依赖\n```bash\nnpm install @reduxjs\u002Ftoolkit react-redux\n```\n\n### 创建Store\n```js\n\u002F\u002F store.js\nimport { configureStore, createSlice } from '@reduxjs\u002Ftoolkit';\n\nconst counterSlice = createSlice({\n  name: 'counter',\n  initialState: { value: 0 },\n  reducers: {\n    increment: state => { state.value += 1 },\n    decrement: state => { state.value -= 1 },\n    incrementByAmount: (state, action) => {\n      state.value += action.payload\n    }\n  }\n});\n\n\u002F\u002F 异步操作示例\nexport const fetchUserData = () => async (dispatch) => {\n  const response = await fetch('\u002Fapi\u002Fuser');\n  dispatch(setUser(await response.json()));\n};\n\nexport const store = configureStore({\n  reducer: {\n    counter: counterSlice.reducer,\n    \u002F\u002F 其他reducer...\n  }\n});\n\nexport const { increment, decrement } = counterSlice.actions;\n```\n\n### 组件集成\n```jsx\n\u002F\u002F index.js\nimport { Provider } from 'react-redux';\nimport { store } from '.\u002Fstore';\n\nroot.render(\n  \u003CProvider store={store}>\n    \u003CApp \u002F>\n  \u003C\u002FProvider>\n);\n\n\u002F\u002F Counter.js\nimport { useSelector, useDispatch } from 'react-redux';\nimport { increment, decrement } from '.\u002Fstore';\n\nfunction Counter() {\n  const count = useSelector(state => state.counter.value);\n  const dispatch = useDispatch();\n\n  return (\n    \u003Cdiv>\n      \u003Cbutton onClick={() => dispatch(decrement())}>-\u003C\u002Fbutton>\n      \u003Cspan>{count}\u003C\u002Fspan>\n      \u003Cbutton onClick={() => dispatch(increment())}>+\u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n## 最佳实践总结\n1. **组件设计**：遵循单一职责原则，拆分智能组件（容器组件）和展示组件\n2. **状态管理**：\n   - 局部状态用`useState`\u002F`useReducer`\n   - 全局共享状态用Redux\n   - 避免过度使用状态提升\n3. **性能优化**：\n   - 使用`React.memo`记忆组件\n   - 使用`useCallback`\u002F`useMemo`避免不必要的重渲染\n   - 虚拟化长列表（react-window）\n4. **Hooks规范**：\n   - 避免在循环\u002F条件中使用Hook\n   - 自定义Hook以`use`前缀命名\n5. **TypeScript集成**：\n   ```tsx\n   interface UserCardProps {\n     name: string;\n     age: number;\n     onSelect: (id: string) => void;\n   }\n   \n   const UserCard: React.FC\u003CUserCardProps> = ({ name, age }) => (\n     \u003Cdiv>{name} ({age})\u003C\u002Fdiv>\n   )\n   ```\n\n下面为您完善教程，增加React路由和生命周期相关内容：\n\n## React路由管理（React Router v6）\n\n### 安装与基础配置\n```bash\nnpm install react-router-dom@6\n```\n\n### 路由基础结构\n```jsx\n\u002F\u002F index.js\nimport { BrowserRouter } from 'react-router-dom';\n\nconst root = ReactDOM.createRoot(document.getElementById('root'));\nroot.render(\n  \u003CBrowserRouter>\n    \u003CApp \u002F>\n  \u003C\u002FBrowserRouter>\n);\n\n\u002F\u002F App.js\nimport { Routes, Route, Link } from 'react-router-dom';\n\nfunction App() {\n  return (\n    \u003Cdiv>\n      \u003Cnav>\n        \u003CLink to=\\\"\u002F\\\">首页\u003C\u002FLink>\n        \u003CLink to=\\\"\u002Fabout\\\">关于\u003C\u002FLink>\n        \u003CLink to=\\\"\u002Fusers\\\">用户列表\u003C\u002FLink>\n      \u003C\u002Fnav>\n      \n      \u003CRoutes>\n        \u003CRoute path=\\\"\u002F\\\" element={\u003CHome \u002F>} \u002F>\n        \u003CRoute path=\\\"\u002Fabout\\\" element={\u003CAbout \u002F>} \u002F>\n        \u003CRoute path=\\\"\u002Fusers\\\" element={\u003CUserList \u002F>} \u002F>\n        \u003CRoute path=\\\"\u002Fusers\u002F:id\\\" element={\u003CUserDetail \u002F>} \u002F>\n        \u003CRoute path=\\\"*\\\" element={\u003CNotFound \u002F>} \u002F>\n      \u003C\u002FRoutes>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### 嵌套路由\n```jsx\n\u002F\u002F Dashboard.js\nimport { Outlet } from 'react-router-dom';\n\nfunction Dashboard() {\n  return (\n    \u003Cdiv>\n      \u003Ch2>仪表盘\u003C\u002Fh2>\n      \u003COutlet \u002F> {\u002F* 子路由渲染位置 *\u002F}\n    \u003C\u002Fdiv>\n  );\n}\n\n\u002F\u002F 路由配置\n\u003CRoutes>\n  \u003CRoute path=\\\"\u002Fdashboard\\\" element={\u003CDashboard \u002F>}>\n    \u003CRoute index element={\u003CDashboardHome \u002F>} \u002F>\n    \u003CRoute path=\\\"settings\\\" element={\u003CDashboardSettings \u002F>} \u002F>\n    \u003CRoute path=\\\"analytics\\\" element={\u003CDashboardAnalytics \u002F>} \u002F>\n  \u003C\u002FRoute>\n\u003C\u002FRoutes>\n```\n\n### 编程式导航\n```jsx\nimport { useNavigate, useParams, useLocation } from 'react-router-dom';\n\nfunction UserCard({ user }) {\n  const navigate = useNavigate();\n  \n  return (\n    \u003Cdiv onClick={() => navigate(`\u002Fusers\u002F${user.id}`)}>\n      {user.name}\n    \u003C\u002Fdiv>\n  );\n}\n\nfunction UserDetail() {\n  const { id } = useParams(); \u002F\u002F 获取URL参数\n  const location = useLocation(); \u002F\u002F 获取位置对象\n  \n  return (\n    \u003Cdiv>\n      \u003Ch2>用户ID: {id}\u003C\u002Fh2>\n      \u003Cp>当前路径: {location.pathname}\u003C\u002Fp>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### 路由守卫（认证保护）\n```jsx\nimport { Navigate } from 'react-router-dom';\n\nfunction ProtectedRoute({ children }) {\n  const { isAuthenticated } = useAuth();\n  \n  if (!isAuthenticated) {\n    return \u003CNavigate to=\\\"\u002Flogin\\\" replace \u002F>;\n  }\n  \n  return children;\n}\n\n\u002F\u002F 使用\n\u003CRoute \n  path=\\\"\u002Fdashboard\\\" \n  element={\n    \u003CProtectedRoute>\n      \u003CDashboard \u002F>\n    \u003C\u002FProtectedRoute>\n  } \n\u002F>\n```\n\n## React生命周期\n\n### 类组件生命周期方法\n\n```jsx\nclass LifecycleDemo extends React.Component {\n  \u002F\u002F 1. 初始化阶段\n  constructor(props) {\n    super(props);\n    this.state = { count: 0 };\n    console.log('Constructor');\n  }\n\n  \u002F\u002F 2. 挂载阶段\n  componentDidMount() {\n    console.log('Component did mount');\n    \u002F\u002F 适合进行API调用、事件订阅\n    this.timer = setInterval(() => {\n      this.setState(prev => ({ count: prev.count + 1 }));\n    }, 1000);\n  }\n\n  \u002F\u002F 3. 更新阶段\n  shouldComponentUpdate(nextProps, nextState) {\n    console.log('Should component update?');\n    return nextState.count !== this.state.count;\n  }\n\n  componentDidUpdate(prevProps, prevState) {\n    console.log('Component did update');\n  }\n\n  \u002F\u002F 4. 卸载阶段\n  componentWillUnmount() {\n    console.log('Component will unmount');\n    \u002F\u002F 清理操作\n    clearInterval(this.timer);\n  }\n\n  render() {\n    console.log('Render');\n    return \u003Cdiv>Count: {this.state.count}\u003C\u002Fdiv>;\n  }\n}\n```\n\n### 函数组件生命周期（Hooks实现）\n\n```jsx\nimport { useState, useEffect } from 'react';\n\nfunction FunctionLifecycle() {\n  const [count, setCount] = useState(0);\n  const [data, setData] = useState(null);\n\n  \u002F\u002F 相当于componentDidMount + componentDidUpdate\n  useEffect(() => {\n    console.log('每次渲染后执行');\n  });\n\n  \u002F\u002F 相当于componentDidMount\n  useEffect(() => {\n    console.log('组件挂载后执行');\n    \n    \u002F\u002F 数据获取\n    fetch('\u002Fapi\u002Fdata')\n      .then(res => res.json())\n      .then(setData);\n    \n    \u002F\u002F 相当于componentWillUnmount\n    return () => {\n      console.log('组件卸载前执行');\n    };\n  }, []); \u002F\u002F 空依赖数组\n\n  \u002F\u002F 依赖变化时执行\n  useEffect(() => {\n    console.log('count变化时执行:', count);\n    \n    document.title = `Count: ${count}`;\n    \n    return () => {\n      console.log('清理count效果');\n    };\n  }, [count]); \u002F\u002F count依赖\n\n  return (\n    \u003Cdiv>\n      \u003Cp>Count: {count}\u003C\u002Fp>\n      \u003Cbutton onClick={() => setCount(c => c + 1)}>增加\u003C\u002Fbutton>\n      {data && \u003Cpre>{JSON.stringify(data, null, 2)}\u003C\u002Fpre>}\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### 生命周期阶段对比\n\n| 阶段 | 类组件方法 | 函数组件Hook |\n|------|------------|--------------|\n| **挂载** | constructor | useState初始化 |\n|       | render      | 函数体执行    |\n|       | componentDidMount | useEffect(() => {}, []) |\n| **更新** | shouldComponentUpdate | React.memo, useMemo |\n|       | render      | 函数体执行    |\n|       | componentDidUpdate | useEffect(() => {}) |\n| **卸载** | componentWillUnmount | useEffect返回函数 |\n| **错误处理** | componentDidCatch | 暂无直接等效，需错误边界组件 |\n\n### 现代React开发建议\n\n1. **优先使用函数组件+Hooks**：\n   - 90%的场景可替代类组件\n   - 更简洁的代码结构\n   - 更好的逻辑复用\n\n2. **关键生命周期替代**：\n   - `componentDidMount` → `useEffect(() => {}, [])`\n   - `componentDidUpdate` → `useEffect(() => {})` 或带依赖的 `useEffect`\n   - `componentWillUnmount` → `useEffect(() => { return cleanup }, [])`\n   - `shouldComponentUpdate` → `React.memo` 或 `useMemo`\n\n3. **数据获取最佳实践**：\n```jsx\nuseEffect(() => {\n  let isMounted = true;\n  \n  const fetchData = async () => {\n    try {\n      const result = await api.getData();\n      if (isMounted) setData(result);\n    } catch (error) {\n      if (isMounted) setError(error);\n    }\n  };\n  \n  fetchData();\n  \n  return () => {\n    isMounted = false; \u002F\u002F 避免组件卸载后设置状态\n  };\n}, []);\n```\n\n## 路由与生命周期整合示例\n\n```jsx\nfunction UserProfile() {\n  const { id } = useParams();\n  const [user, setUser] = useState(null);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    let isActive = true;\n    \n    const fetchUser = async () => {\n      try {\n        setLoading(true);\n        const data = await fetchUserById(id);\n        if (isActive) {\n          setUser(data);\n          setLoading(false);\n        }\n      } catch (error) {\n        if (isActive) {\n          setError(error.message);\n          setLoading(false);\n        }\n      }\n    };\n    \n    fetchUser();\n    \n    return () => {\n      isActive = false; \u002F\u002F 清理效果\n    };\n  }, [id]); \u002F\u002F id变化时重新获取\n\n  if (loading) return \u003CSpinner \u002F>;\n  \n  return (\n    \u003Cdiv>\n      \u003Ch2>{user.name}\u003C\u002Fh2>\n      \u003Cp>Email: {user.email}\u003C\u002Fp>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n## 完整项目结构建议\n\n```\nsrc\u002F\n├── components\u002F      # 通用UI组件\n├── pages\u002F           # 页面组件\n├── layouts\u002F         # 布局组件\n├── hooks\u002F           # 自定义Hooks\n├── store\u002F           # Redux状态\n│   ├── slices\u002F\n│   └── store.js\n├── services\u002F        # API服务\n├── routers\u002F         # 路由配置\n├── utils\u002F           # 工具函数\n├── assets\u002F          # 静态资源\n└── App.js           # 主应用组件\n```\n\n这些新增内容涵盖了React路由的现代用法（v6版本）以及React生命周期的详细解释，包括类组件和函数组件的实现方式对比。同时还提供了路由与生命周期整合的实际示例，帮助开发者理解如何在真实项目中应用这些概念。","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F46e205aa1bd33d1bb7201019fc2fdf43.png",1148,"2023-02-21T02:01:52.000Z","2026-06-27T11:00:41.557Z",{"id":428,"name":429,"slug":430},"f1701085-b8c1-413a-8750-58e7a0a33832","React","react",{"id":27,"name":28,"avatar":21},[433],{"id":255,"name":256,"slug":257}]