[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fxxNy15iIB8_-cU8OxpaDUVVFmRcGUpVhSO_rv2iOhpw":3,"donations-sidebar":10,"posts-{\"page\":1,\"pageSize\":10,\"categoryId\":\"5ed5cc62-43ea-49a2-b0b2-38bc7aae52a0\"}":11,"sidebar-data":133},{"id":4,"name":5,"slug":6,"description":5,"sortOrder":7,"createdAt":8,"updatedAt":9},"5ed5cc62-43ea-49a2-b0b2-38bc7aae52a0","Vue3","vue3",4,"2022-05-24T01:55:05.000Z","2023-02-08T02:49:14.000Z",[],{"list":12,"total":130,"page":131,"pageSize":132},[13,33,47,61,73,89,103,117],{"id":14,"title":15,"slug":15,"content":16,"excerpt":15,"coverImage":17,"status":18,"isPinned":19,"pinnedAt":20,"viewCount":21,"publishedAt":22,"createdAt":22,"updatedAt":23,"category":24,"author":25,"tags":28},"8c77a787-1db3-424c-89c2-7c7db65a7bca","阿里图标动态化导入","## 动态请求接口，获取CDN地址\n```language\n    \u002F\u002F 从CDN链接提取font ID的函数\n    const extractFontId = (url: string): string => {\n      const match = url.match(\u002Ffont_(\\d+)_\u002F);\n      return match ? match[1] : ''; \u002F\u002F 默认值\n    };\n\n    \u002F\u002F 初始化iconfont\n    async function initIconfont(): Promise\u003Cvoid> {\n      try {\n        \u002F\u002F 从API获取CDN链接\n        const res: any = await getAttrConfigDetail('iconfont_url');\n\n        const url = res.data?.value;\n\n        console.log('url', url);\n\n        if (!url) {\n          return;\n        }\n\n        \u002F\u002F 根据CDN链接动态更新iconfontGlobalName\n        const fontId = extractFontId(url);\n        iconfontGlobalName.value = `_iconfont_svg_string_${fontId}`;\n\n        console.log('fontId', fontId);\n\n        \u002F\u002F 动态创建script标签加载iconfont\n        const script = document.createElement('script');\n        script.src = url;\n        script.async = true;\n\n        \u002F\u002F 设置script标签id\n        script.id = 'iconfont';\n\n        \u002F\u002F 监听加载完成事件\n        script.onload = () => {\n          \u002F\u002F 获取图标名称列表\n          iconfontIcons.value = getIconfontNames();\n          console.log('iconfontIcons', iconfontIcons.value);\n        };\n\n        script.onerror = () => {\n          console.error('Failed to load iconfont:', url);\n        };\n\n        \u002F\u002F 添加到head标签;\n        document.head.appendChild(script);\n      } catch (error) {\n        console.error('Error initializing iconfont:', error);\n      }\n    }\n\n    \u002F\u002F 获取iconfont图标名称列表\n    const getIconfontNames = (): string[] => {\n      const globalName = iconfontGlobalName.value as keyof Window;\n      const text = window[globalName] || '';\n      return text.match(\u002F(?\u003C=id=\\\")[^\\\"]+(?=\\\")\u002Fg) || [];\n    };\n```\n\n\n\n## 渲染组件\n```language\nimport SvgIcon from '@\u002Fcomponents\u002FSvgIcon\u002Findex.vue';\nimport { ElIcon } from 'element-plus';\nimport { h, type Component, type PropType } from 'vue';\n\nexport default defineComponent({\n  name: 'BaseIcon',\n  props: {\n    name: {\n      type: [String, Object] as PropType\u003Cstring | Component | unknown>\n    },\n    size: {\n      type: [String, Number]\n    },\n    color: {\n      type: String\n    }\n  },\n  setup(props) {\n    return () => {\n      return h(\n        ElIcon,\n        {\n          size: props.size,\n          color: props.color\n        },\n        () => {\n          if (!props.name) {\n            return;\n          }\n          if (typeof props.name === 'string') {\n            if (props.name.startsWith('svg-')) {\n              return h(SvgIcon, {\n                name: props.name.slice(4)\n              });\n            }\n            if (props.name.startsWith('icon-')) {\n              return h(\n                'svg',\n                {\n                  ariaHidden: true\n                },\n                h('use', {\n                  'xlink:href': `#${props.name}`\n                })\n              );\n            }\n            return h(resolveComponent(props.name));\n          }\n          return h(props.name);\n        }\n      );\n    };\n  }\n});\n\n```\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002Ff375b202fdd16c85c3361027f467b095_1754476720058.png","PUBLISHED",false,null,433,"2025-08-06T10:40:42.000Z","2026-06-27T11:07:21.870Z",{"id":4,"name":5,"slug":6},{"id":26,"name":27,"avatar":20},"f9d0f2de-c700-4f90-b535-afd3dbe78128","Admin",[29],{"id":30,"name":31,"slug":32},"20bff9cd-7848-4c16-8775-42cf12b44b30","Vue3生态","vue3生态",{"id":34,"title":35,"slug":36,"content":37,"excerpt":38,"coverImage":39,"status":18,"isPinned":19,"pinnedAt":20,"viewCount":40,"publishedAt":41,"createdAt":41,"updatedAt":42,"category":43,"author":44,"tags":45},"0c17ce84-1fb0-46d1-a6e7-35c8d6b90228","App中内嵌H5页面实现通信","app中内嵌h5页面实现通信","> 在App中内嵌H5页面并实现通信是常见的混合开发场景，主要通过JavaScript与原生代码交互实现。以下是主流平台的核心实现方案：\n\n\n## 一、通信原理\n### H5 → App\n> H5调用原生能力（如相机、存储等）\n\n### App → H5\n> 原生触发H5页面更新（如传递用户信息）\n\n### 双向通信\n> 通过约定协议实现数据交换\n\n\n## 二、Android实现方案\n### 方法1：JavaScriptInterface（官方推荐）\n\n```language\n\u002F\u002F 1. 定义接口类\npublic class JsBridge {\n    @JavascriptInterface\n    public void showToast(String msg) {\n        Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();\n    }\n}\n\n\u002F\u002F 2. 注册到WebView\nwebView.getSettings().setJavaScriptEnabled(true);\nwebView.addJavascriptInterface(new JsBridge(), \\\"AndroidBridge\\\");\n\n\u002F\u002F 3. H5调用\nwindow.AndroidBridge.showToast(\\\"Hello from H5!\\\");\n```\n\n### 方法2：URL Scheme拦截\n\n```language\n\u002F\u002F H5触发自定义协议\nlocation.href = \\\"myapp:\u002F\u002Faction?param1=value1\\\";\n```\n\n```language\n\u002F\u002F Android拦截请求\nwebView.setWebViewClient(new WebViewClient() {\n    @Override\n    public boolean shouldOverrideUrlLoading(WebView view, String url) {\n        if (url.startsWith(\\\"myapp:\u002F\u002F\\\")) {\n            \u002F\u002F 解析并执行原生操作\n            return true; \u002F\u002F 拦截请求\n        }\n        return false;\n    }\n});\n```\n\n## 三、iOS实现方案\n### 方法1：WKScriptMessageHandler（推荐）\n\n```language\n\u002F\u002F 1. 注册消息处理器\nlet contentController = WKUserContentController()\ncontentController.add(self, name: \\\"iosBridge\\\")\n\n\u002F\u002F 2. H5发送消息\nwindow.webkit.messageHandlers.iosBridge.postMessage({data: \\\"Hello\\\"})\n\n\u002F\u002F 3. 原生处理消息\nfunc userContentController(_ controller: WKUserContentController, \n                          didReceive message: WKScriptMessage) {\n    if message.name == \\\"iosBridge\\\", \n       let data = message.body as? [String: Any] {\n        print(\\\"Received:\\\", data[\\\"data\\\"] ?? \\\"\\\")\n    }\n}\n```\n\n### 方法2：JavaScriptCore（UIWebView旧方案）\n\n```language\n\u002F\u002F 1. 获取JSContext\nJSContext *context = [webView valueForKeyPath:@\\\"documentView.webView.mainFrame.javaScriptContext\\\"];\n\n\u002F\u002F 2. 注入对象\ncontext[@\\\"nativeBridge\\\"] = ^(NSString *msg) {\n    NSLog(@\\\"H5消息: %@\\\", msg);\n};\n\n\u002F\u002F H5调用\nnativeBridge(\\\"Hello iOS!\\\");\n```\n\n## 四、通用增强方案\n### 1. 封装JS Bridge库\n\n```language\n\u002F\u002F 统一调用入口\nfunction callNative(method, params, callback) {\n    \u002F\u002F Android\n    if (window.AndroidBridge) {\n        window.AndroidBridge[method](JSON.stringify(params));\n    } \n    \u002F\u002F iOS\n    else if (window.webkit?.messageHandlers?.iosBridge) {\n        window.webkit.messageHandlers.iosBridge.postMessage({\n            func: method,\n            data: params\n        });\n    }\n}\n```\n### 2. 安全加固\n\n```language\n通信双方验证来源（防止恶意页面调用）\n\niOS启用requiresUserActionForMediaPlayback\n\nAndroid禁用setAllowUniversalAccessFromFileURLs\n```\n\n## 五、完整通信流程示例\n![](https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F7dea58c61e36f6cc5a088c261e46f8bd_1749700455493.png)\n\n## 六、调试技巧\n### Android\n> 使用Chrome chrome:\u002F\u002Finspect 调试WebView\n\n### iOS\n> Safari开发者工具调试WebView\n\n### 抓包工具\n> 使用Charles分析通信数据\n\n\n### 注意事项\n> 异步处理：所有原生操作需异步返回结果\n\n> 版本兼容：Android 4.2+ 必须使用@JavascriptInterface\n\n> 参数类型：复杂数据使用JSON序列化\n\n> 内存泄漏：iOS的WKScriptMessageHandler需及时移除\n\n### 建议使用成熟开源库简化开发：\n\n> Android：JockeyJS\n\n> iOS：WebViewJavascriptBridge\n\n> 通过标准化通信协议，可实现H5与App的高效、安全交互，同时保持H5的动态更新能力。\n\n\n## H5 端通用实现（JockeyJS + WebViewJavascriptBridge）\n### 1. 初始化 Bridge\n\n```language\n\u002F\u002F 兼容 Android 和 iOS 的初始化函数\nfunction initBridge(callback) {\n    if (window.WebViewJavascriptBridge) {\n        return callback(window.WebViewJavascriptBridge);\n    }\n    if (window.Jockey) { \u002F\u002F Android 的 JockeyJS\n        callback({ \n            callHandler: Jockey.trigger,\n            registerHandler: Jockey.on\n        });\n    } else { \u002F\u002F iOS 的 WebViewJavascriptBridge\n        document.addEventListener('WebViewJavascriptBridgeReady', () => {\n            callback(window.WebViewJavascriptBridge);\n        }, false);\n    }\n}\n\ninitBridge(function(bridge) {\n    \u002F\u002F 注册 JS 处理器供原生调用\n    bridge.registerHandler(\\\"changeTheme\\\", function(data, responseCallback) {\n        document.body.className = data.theme;\n        responseCallback(\\\"Theme applied!\\\"); \u002F\u002F 通知原生完成\n    });\n\n    \u002F\u002F 调用原生方法（如获取用户信息）\n    bridge.callHandler(\\\"getUserInfo\\\", { userId: \\\"1001\\\" }, function(response) {\n        console.log(\\\"用户数据:\\\", response.name, response.email);\n    });\n});\n```\n\n## 封装方法参考\n\n```language\n\u002F\u002F ios终端\nconst u = navigator.userAgent;\nexport const isWeiXin = !!\u002FMicroMessenger\u002Fi.test(u);\nexport const isAndroid = u.indexOf('Android') > -1 || u.indexOf('Adr') > -1; \u002F\u002F android终端\nexport const isiOS = !!u.match(\u002F\\(i[^;]+;( U;)? CPU.+Mac OS X\u002F);\n\u002F\u002F import VConsole from 'vconsole'\n\u002F\u002F new VConsole()\n\nconst printException = (func: () => void) => {\n  try {\n    func();\n    return true;\n  } catch (e) {\n    console.error('非客户端环境，无法调用');\n    return false;\n  }\n};\n\n\u002F**\n * @function 客户端回调传参\n * @androidFuncName 安卓方法\n * @iosFuncName 苹果方法\n * @iosCallbackName 苹果回调\n * *\u002F\nconst waitAppCallback = (androidFuncName: string, iosFuncName: string, iosCallbackName: string) => {\n  return new Promise((resolve, reject) => {\n    try {\n      if (isAndroid) {\n        (window as any).Android[androidFuncName] &&\n          resolve((window as any).Android[androidFuncName]());\n      } else if (isiOS) {\n        (window as any)[iosCallbackName] = (appArgs: string) => {\n          resolve(appArgs);\n        };\n        (window as any).webkit &&\n          (window as any).webkit.messageHandlers[iosFuncName].postMessage('');\n      } else {\n        reject();\n      }\n    } catch (e) {\n      console.error('非客户端环境，无法调用', `这是${androidFuncName}方法`);\n      reject(e);\n    }\n  });\n};\n```\n\n\n## 2. 主动发送消息给原生\n\n```language\n\u002F\u002F H5 触发扫码功能\ndocument.getElementById(\\\"scan-btn\\\").addEventListener(\\\"click\\\", () => {\n    bridge.callHandler(\\\"scanQRCode\\\", \\\"qrcode\\\", (result) => {\n        alert(`扫码结果: ${result.code}`);\n    });\n});\n```\n\n## 移动端调试神器eruda使用详解(推荐)\n\n```language\npnpm add eruda\n```\n\n\n## 移动端调试神器vConsole使用详解\n\n```language\npnpm add vconsole\n```\n\n\n\n\n## H5在ios浏览器全屏显示\n> viewport-fit=cover 可以全屏显示\n\n```language\n\u003Cmeta name=\\\"viewport\\\" content=\\\"width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover\\\">\n```\n\n\n## 禁用ios 禁止网页回弹效果\n\n```language\nhtml {\n  overscroll-behavior: none;\n}\n```\n\n\n","在App中内嵌H5页面并实现通信是常见的混合开发场景，主要通过JavaScript与原生代码交互实现。以下是主流平台的核心实现方案：","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002Ff35e50644ba29b4972e502faa6a72254_1749699171524.png",759,"2025-06-12T04:13:30.000Z","2026-06-27T11:08:34.303Z",{"id":4,"name":5,"slug":6},{"id":26,"name":27,"avatar":20},[46],{"id":30,"name":31,"slug":32},{"id":48,"title":49,"slug":50,"content":51,"excerpt":49,"coverImage":52,"status":18,"isPinned":19,"pinnedAt":20,"viewCount":53,"publishedAt":54,"createdAt":54,"updatedAt":55,"category":56,"author":57,"tags":58},"f2274999-5594-4e55-ba6e-e73e6e363903","Vue3技术栈成长指南","vue3技术栈成长指南","## Vue3\n> 一个用于构建 Web 用户界面的平易近人、高性能且多功能的框架。\n\n[Vue3入门到精通教程](https:\u002F\u002Fxiaolong0418.com\u002Fblogs\u002F5)\n\n\n\n## Pina\n> Pinia 是 Vue 的存储库，允许您跨组件\u002F页面共享状态。\n\n[Pinia存储库入门到精通教程](https:\u002F\u002Fxiaolong0418.com\u002Fblogs\u002F1)\n\n\n## Vite\n> 下一代前端工具\n\n[Vite快速入门教程](https:\u002F\u002Fxiaolong0418.com\u002Fblogs\u002F18)\n\n\n## Nuxt3\n> Vue的SSR框架，服务端渲染，有利于SEO优化和提高浏览器渲染速度！\n\n[Nuxt3入门到精通教程](https:\u002F\u002Fxiaolong0418.com\u002Fblogs\u002F15)\n\n## uni-app\n> 多端框架，小程序，h5，App开发\n\n[uni-app入门到精通教程](https:\u002F\u002Fxiaolong0418.com\u002Fblogs\u002F70)","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F6a5565966a2a8d210bc753c52afa7665.png",604,"2023-06-29T07:20:29.000Z","2026-06-27T11:09:02.467Z",{"id":4,"name":5,"slug":6},{"id":26,"name":27,"avatar":20},[59],{"id":60,"name":5,"slug":6},"62b94c93-724f-488d-8fc5-0449971d9204",{"id":62,"title":63,"slug":64,"content":65,"excerpt":63,"coverImage":66,"status":18,"isPinned":19,"pinnedAt":20,"viewCount":67,"publishedAt":68,"createdAt":68,"updatedAt":42,"category":69,"author":70,"tags":71},"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",{"id":4,"name":5,"slug":6},{"id":26,"name":27,"avatar":20},[72],{"id":60,"name":5,"slug":6},{"id":74,"title":75,"slug":76,"content":77,"excerpt":78,"coverImage":79,"status":18,"isPinned":19,"pinnedAt":20,"viewCount":80,"publishedAt":81,"createdAt":81,"updatedAt":82,"category":83,"author":84,"tags":85},"2c8ce67f-c88d-4c66-b2eb-db6f60256191","Video.js 视频播放器","video-js-视频播放器","## 官网\n```js\nhttps:\u002F\u002Fvideojs.com\u002Fgetting-started\u002F#videojs-cdn\n```\n\n## npm下载\n```js\npnpm add -S video.js\n```\n\n## 使用教程\n```vue\n\u003Ctemplate>\n  \u003Cvideo ref=\\\"videoRef\\\" class=\\\"video-js vjs-default-skin\\\" controls>\n    \u003Csource :src=\\\"dataSrc\\\" \u002F>\n  \u003C\u002Fvideo>\n\u003C\u002Ftemplate>\n\u003Cscript setup lang=\\\"ts\\\">\n  import videojs from 'video.js';\n  import 'video.js\u002Fdist\u002Fvideo-js.css';\n\n  defineProps({\n    dataSrc: {\n      type: String,\n      default: () => '',\n    },\n  });\n\n  let player = ref('');\n  const videoRef = ref('');\n  onMounted(() => {\n    nextTick(() => {\n      player.value = videojs(videoRef.value);\n    });\n  });\n\u003C\u002Fscript>\n\n\u003Cstyle scoped lang=\\\"scss\\\">\n  .video-js {\n    width: 100%;\n    height: 100%;\n  }\n\u003C\u002Fstyle>\n\n\n```\n\n## 常见问题\n### 谷歌浏览器上传视频mp4格式，有的能播放，有的不能播放\n> 浏览器兼容性问题，旧版本涉及专利问题，支持mp4的H264编码格式，所以把视频编码转一下就可以了，转成H264编码格式就可以了（代码转码或者用软件转码，比如格式工厂软件）\n\n","一个视频播放器插件","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F62d95bff60e226df390d2bbfbf495a8e.png",812,"2022-10-21T00:39:04.000Z","2026-06-27T11:08:50.823Z",{"id":4,"name":5,"slug":6},{"id":26,"name":27,"avatar":20},[86],{"id":87,"name":88,"slug":88},"4c6be544-8a00-4445-92a3-e3dcbaf6142e","动画",{"id":90,"title":91,"slug":92,"content":93,"excerpt":94,"coverImage":95,"status":18,"isPinned":19,"pinnedAt":20,"viewCount":96,"publishedAt":97,"createdAt":97,"updatedAt":98,"category":99,"author":100,"tags":101},"7c83dcc8-adf1-4bb9-9958-cd671040e37f","Vue 视图不更新问题","vue-视图不更新问题","## 1. Vue 无法检测实例被创建时不存在于 data 中的 property\n> 原因：由于 Vue 会在初始化实例时对 property 执行 getter\u002Fsetter 转化，所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。\n\n> 解决：提前在data定义好数据\n\n## 2. Vue 无法检测对象 property 的添加或移除\n> 原因：官方 - 由于 JavaScript（ES5） 的限制，Vue.js 不能检测到对象属性的添加或删除。因为 Vue.js 在初始化实例时将属性转为 getter\u002Fsetter，所以属性必须在 data 对象上才能让 Vue.js 转换它，才能让它是响应的。\n场景：\n\n\n解决办法：\n```js\n\u002F\u002F 动态添加 \nthis.$set(vm.obj, propertyName, newValue)\n\n\u002F\u002F 动态添加多个\n\u002F\u002F 代替 Object.assign(this.obj, { a: 1, b: 2 })\nthis.obj = Object.assign({}, this.obj, { a: 1, b: 2 })\n\n\u002F\u002F 动态移除 - vm.$delete\nthis.$delete(vm.obj, propertyName)\n```\n\n### 3. Vue 不能检测通过数组索引直接修改一个数组项\n> 原因：官方 - 由于 JavaScript 的限制，Vue 不能检测数组和对象的变化；尤雨溪 - 性能代价和获得用户体验不成正比。\n场景：\n\n解决办法：\n\n```js\nthis.$set(this.items, 1, newValue)\n```\n\n## 4. Vue 不能监测直接修改数组长度的变化\n> 原因：官方 - 由于 JavaScript 的限制，Vue 不能检测数组和对象的变化；尤雨溪 - 性能代价和获得用户体验不成正比。\n\n> 解决: .items.splice(newLength)\n\n## 5. 在异步更新执行之前操作 DOM 数据不会变化\n> 原因：Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化，Vue 将开启一个队列，并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发，只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后，在下一个的事件循环“tick”中，Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate，如果执行环境不支持，则会采用 setTimeout(fn, 0) 代替。\n> 解决：\n```js\nthis.nextTick( ()=> {\n this.textContent === 'new message' \n})\n```\n\n## 6. 循环嵌套层级太深，视图不更新？\n> 强制更新\n```js\nthis.$forceUpdate()\n```\n\n## 7. 拓展：路由参数变化时，页面不更新（数据不更新）\n> 原因：路由视图组件引用了相同组件时，当路由参会变化时，会导致该组件无法更新，也就是我们常说中的页面无法更新的问题。\n\n> 解决：\n```language\n通过 watch 监听 $route 的变化。\n给 \u003Crouter-view> 绑定 key 属性，这样 Vue 就会认为这是不同的 \u003Crouter-view>。\n```\n\n## 异步更新带来的数据响应式误解\n> 因为vue的dom更新是异步的，即当setter操作发生后，指令并不会立马更新，指令的更新操作会有一个延迟，当指令更新真正执行的时候，此时.text属性已经赋值，所以指令更新模板时得到的是新值\n","Vue 数据更新了但页面没有更新的 7 种情况汇总及延伸总结","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F1656911697456.png",1610,"2022-07-04T05:30:02.000Z","2026-06-27T11:09:10.359Z",{"id":4,"name":5,"slug":6},{"id":26,"name":27,"avatar":20},[102],{"id":60,"name":5,"slug":6},{"id":104,"title":105,"slug":105,"content":106,"excerpt":105,"coverImage":107,"status":18,"isPinned":19,"pinnedAt":20,"viewCount":108,"publishedAt":109,"createdAt":109,"updatedAt":110,"category":111,"author":112,"tags":113},"0a6dc39b-6004-45f5-94f3-f8c3e775395c","常见面试题","> 主要纪录面试过程中的面试题，对自己知识的查漏补缺\n\n# 说一下深拷贝与浅拷贝以及他们的区别\n## 浅拷贝\n### 概念\n> 概念: 对于字符串类型，浅拷贝是对值的复制，对于对象来说，浅拷贝是对对象地址的复制, 也就是拷贝的结果是两个对象指向同一个地址\n\n### 方法\n> Object.assign或者(...)展开运算符\n\n## 深拷贝\n### 概念\n> 概念: 深拷贝开辟一个新的栈，两个对象对应两个不同的地址，修改一个对象的属性，不会改变另一个对象的属性\n\n> JSON.parse(JSON.stringify(object))或者递归\n```js\nlet a = {\n    age: 1,\n    jobs: {\n        first: 'FE'\n    }\n}\nlet b = JSON.parse(JSON.stringify(a))\na.jobs.first = 'native'\nconsole.log(b.jobs.first) \u002F\u002F FE\n```\n> 该方法也是有局限性:(1)会忽略 undefined(2)不能序列化函数(3)不能解决循环引用的对象\n\n## 闭包\n\n- 闭包就是能够读取其他函数内部变量的函数\n\n- 闭包是指有权访问另一个函数作用域中变量的函数，创建闭包的最常见的方式就是在一个函数内创建另一个函数，通过另一个函数访问这个函数的局部变量,利用闭包可以突破作用链域\n\n### 闭包的特性：\n\n- 函数内再嵌套函数\n- 内部函数可以引用外层的参数和变量\n- 参数和变量不会被垃圾回收机制回收\n\n### 说说你对闭包的理解\n\n> 使用闭包主要是为了设计私有的方法和变量。闭包的优点是可以避免全局变量的污染，缺点是闭包会常驻内存，会增大内存使用量，使用不当很容易造成内存泄露。在js中，函数即闭包，只有函数才会产生作用域的概念\n\n- 闭包 的最大用处有两个，一个是可以读取函数内部的变量，另一个就是让这些变量始终保持在内存中\n\n- 闭包的另一个用处，是封装对象的私有属性和私有方法\n\n- 好处：能够实现封装和缓存等；\n\n- 坏处：就是消耗内存、不正当使用会造成内存溢出的问题\n\n使用闭包的注意点\n\n- 由于闭包会使得函数中的变量都被保存在内存中，内存消耗很大，所以不能滥用闭包，否则会造成网页的性能问题，在IE中可能导致内存泄露\n- 解决方法是，在退出函数之前，将不使用的局部变量全部删除\n\n\n## 请描述一下 cookies，sessionStorage 和 localStorage 的区别？\n- cookie是网站为了标示用户身份而储存在用户本地终端（Client Side）上的数据（通常经过加密）\n\n- cookie数据始终在同源的http请求中携带（即使不需要），记会在浏览器和服务器间来回传递\n\n- sessionStorage和localStorage不会自动把数据发给服务器，仅在本地保存\n\n### 存储大小：\n\n- cookie数据大小不能超过4k\n- sessionStorage和localStorage虽然也有存储大小的限制，但比cookie大得多，可以达到5M或更大\n\n### 有期时间：\n\n- localStorage 存储持久数据，浏览器关闭后数据不丢失除非主动删除数据\n- sessionStorage 数据在当前浏览器窗口关闭后自动删除\n- cookie 设置的cookie过期时间之前一直有效，即使窗口或浏览器关闭\n\n## JS的基本数据类型和引用数据类型\n- 基本数据类型（6种）：undefined、null、boolean、number、string、symbol\n- 引用数据类型（3种）：object、array、function\n\n## 说一下浏览器的缓存机制\n> 浏览器缓存机制有两种，一种为强缓存，一种为协商缓存\n\n- 对于强缓存，浏览器在第一次请求的时候，会直接下载资源，然后缓存在本地，第二次请求的时候，直接使用缓存。\n\n- 对于协商缓存，第一次请求缓存且保存缓存标识与时间，重复请求向服务器发送缓存标识和最后缓存时间，服务端进行校验，如果失效则使用缓存\n协商缓存相关设置\n\n> Exprires：服务端的响应头，第一次请求的时候，告诉客户端，该资源什么时候会过期。Exprires的缺陷是必须保证服务端时间和客户端时间严格同步。\nCache-control：max-age：表示该资源多少时间后过期，解决了客户端和服务端时间必须同步的问题，\nIf-None-Match\u002FETag：缓存标识，对比缓存时使用它来标识一个缓存，第一次请求的时候，服务端会返回该标识给客户端，客户端在第二次请求的时候会带上该标识与服务端进行对比并返回If-None-Match标识是否表示匹配。\nLast-modified\u002FIf-Modified-Since：第一次请求的时候服务端返回Last-modified表明请求的资源上次的修改时间，第二次请求的时候客户端带上请求头If-Modified-Since，表示资源上次的修改时间，服务端拿到这两个字段进行对比\n\n\n#  Vue面试题\n\n## 请详细说下你对vue生命周期的理解\n> 总共分为8个阶段创建前\u002F后，载入前\u002F后，更新前\u002F后，销毁前\u002F后,Vue 实例有一个完整的生命周期，也就是从开始创建、初始化数据、编译模版、挂载Dom -> 渲染、更新 -> 渲染、卸载等一系列过程，我们称这是Vue的生命周期\n\n## 各个生命周期的作用\n| 生命周期 | 描述 |\n| - | - |\n| beforeCreate | 组件实例被创建之初，组件的属性生效之前 |\n| created | 组件实例已经完全创建，属性也绑定，但真实dom还没有生成，$el还不可用 |\n| beforeMount | 在挂载开始之前被调用：相关的 render 函数首次被调用 |\n| mounted | el 被新创建的 vm.$el 替换，并挂载到实例上去之后调用该钩子 |\n| beforeUpdate | 组件数据更新之前调用，发生在虚拟 DOM 打补丁之前 |\n| update | 组件数据更新之后 |\n| activated | keep-alive专属，组件被激活时调用 |\n| deactivated | keep-alive专属，组件被销毁时调用 |\n| beforeDestroy | 组件销毁前调用 |\n| destroyed | 组件销毁后调用 |\n\n## Vue实现数据双向绑定的原理：Object.defineProperty()\n- vue实现数据双向绑定主要是：采用数据劫持结合发布者-订阅者模式的方式，通过 Object.defineProperty() 来劫持各个属性的setter，getter，在数据变动时发布消息给订阅者，触发相应监听回调。当把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项时，Vue 将遍历它的属性，用 Object.defineProperty() 将它们转为 getter\u002Fsetter。用户看不到 getter\u002Fsetter，但是在内部它们让 Vue追踪依赖，在属性被访问和修改时通知变化。\n\n- vue的数据双向绑定 将MVVM作为数据绑定的入口，整合Observer，Compile和Watcher三者，通过Observer来监听自己的model的数据变化，通过Compile来解析编译模板指令（vue中是用来解析 {{}}），最终利用watcher搭起observer和Compile之间的通信桥梁，达到数据变化 —>视图更新；视图交互变化（input）—>数据model变更双向绑定效果。\n\n\n## Proxy 相比于 defineProperty 的优势\n\n### Object.defineProperty() 的问题主要有三个：\n\n- 不能监听数组的变化\n- 必须遍历对象的每个属性\n- 必须深层遍历嵌套的对象\n### Proxy 在 ES2015 规范中被正式加入，它有以下几个特点\n\n>针对对象：针对整个对象，而不是对象的某个属性，所以也就不需要对 keys 进行遍历。这解决了上述 Object.defineProperty() 第二个问题\n支持数组：Proxy 不需要对数组的方法进行重载，省去了众多 hack，减少代码量等于减少了维护成本，而且标准的就是最好的。\n除了上述两点之外，Proxy 还拥有以下优势：\n\n> Proxy 的第二个参数可以有 13 种拦截方法，这比起 Object.defineProperty() 要更加丰富\nProxy 作为新标准受到浏览器厂商的重点关注和性能优化，相比之下 Object.defineProperty() 是一个已有的老方法。\n\n\n## v-for和v-if为什么不建议同时用\n> vue2中会优先执行 v-for, 当 v-for 把所有内容全部遍历之后 , v-if 再对已经遍历的元素进行删除 , 造成了加载的浪费 , 所以应该尽量在执行 v-for 之前优先执行 v-if , 可以减少加载的压力。(在vue3中v-if的优先级高于v-for)\n> 解决方案：\n(1)、外部条件放到遍历的父级元素上，没有父级可以使用\u003Ctemplate>\u003C\u002Ftemplate>。注意 key 不能放 template 标签上。\n(2)、在计算属性中先用内\u002F外部条件处理数据，再遍历处理后的数据\n\n\n\n\n##  说说Vue2.0和Vue3.0有什么区别\n\n- 重构响应式系统，使用Proxy替换Object.defineProperty，使用Proxy优势：\n> 可直接监听数组类型的数据变化\n 监听的目标为对象本身，不需要像Object.defineProperty一样遍历每个属性，有一定的性能提升\n可拦截apply、ownKeys、has等13种方法，而Object.defineProperty不行\n直接实现对象属性的新增\u002F删除\n\n- 新增Composition API，更好的逻辑复用和代码组织\n- 重构 Virtual DOM\n> 模板编译时的优化，将一些静态节点编译成常量\nslot优化，将slot编译为lazy函数，将slot的渲染的决定权交给子组件\n模板中内联事件的提取并重用（原本每次渲染都重新生成内联函数）\n\n- 代码结构调整，更便于Tree shaking，使得体积更小\n- 使用Typescript替换Flow\n\n## 介绍一下Vue中的Diff算法\n> 在新老虚拟DOM对比时\n\n- 首先，对比节点本身，判断是否为同一节点，如果不为相同节点，则删除该节点重新创建节点进行替换\n- 如果为相同节点，进行patchVnode，判断如何对该节点的子节点进行处理，先判断一方有子节点一方没有子节点的情况(如果新的children没有子节点，将旧的子节点移除)\n- 比较如果都有子节点，则进行updateChildren，判断如何对这些新老节点的子节点进行操作（diff核心）。 匹配时，找到相同的子节点，递归比较子节点\n在diff中，只对同层的子节点进行比较，放弃跨级的节点比较，使得时间复杂从O(n^3)降低值O(n)，也就是说，只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。\n\n15 说一说keep-alive实现原理\n> keep-alive组件接受三个属性参数：include、exclude、max\n\n- include 指定需要缓存的组件name集合，参数格式支持String, RegExp, Array。当为字符串的时候，多个组件名称以逗号隔开。\n- exclude 指定不需要缓存的组件name集合，参数格式和include一样。\n- max 指定最多可缓存组件的数量,超过数量删除第一个。参数格式支持String、Number。\n原理\n\n- keep-alive实例会缓存对应组件的VNode,如果命中缓存，直接从缓存对象返回对应VNode\n\nLRU（Least recently used） 算法根据数据的历史访问记录来进行淘汰数据，其核心思想是“如果数据最近被访问过，那么将来被访问的几率也更高”。(墨菲定律：越担心的事情越会发生)\n\n\n\n## 关于宏任务\u002F微任务，同步\u002F异步的执行顺序的面试题\n\n```language\nasync function promise1() {\n    console.log(\\\"promise1  start\\\")\n    await promise2()\n    console.log(\\\"promise1  end\\\")\n}\nfunction promise2() {\n    console.log(\\\"promise2\\\")\n}\nsetTimeout(function () {\n    console.log(\\\"setTimeout\\\")\n}, 0)\nconsole.log(\\\"script start\\\")\npromise1()\nnew Promise((resolve, reject) => {\n    console.log(\\\"Promise\\\")\n    resolve()\n}).then(function () {\n    console.log(\\\"Promise then\\\")\n})\nconsole.log(\\\"script end\\\")\n\n```\n\n### 宏任务\n```language\nscript(整体代码)\nsetTimeout\nsetInterval\nI\u002FO\nUI交互事件\npostMessage\nMessageChannel\nsetImmediate(Node.js 环境)\n```\n\n### 微任务\n```language\nPromise.then\nObject.observe（将要废弃）\nMutaionObserver（新特性）\nprocess.nextTick(Node.js 环境)\n```\n\n### 事件循环\n- js是单线程，一个线程拥有唯一一个时间循环，但任务队列可以有多个。\n- 任务队列又分为宏任务和微任务。\n- 来自不同任务源的任务会进入到不同的任务队列。（setTimeout与setInterval是同源的）\n- 事件循环的顺序，决定了JavaScript代码的执行顺序。它从script(整体代码)开始进入第一次循环，代码一行一行执行，执行过程中遇到宏任务，把宏任务加到宏任务队列中， 遇到微任务放到微任务队列中，当宏任务的函数调用栈执全部执行后，去看有没有微任务， 如果有，去执行微任务， 微任务全部执行完成后，循环再次从宏任务开始，这样循环。\n- 浏览器为了能够使得JS内部(macro)task与DOM任务能够有序的执行，会在一个(macro)task执行结束后，在下一个(macro)task 执行开始前，对页面进行重新渲染，流程：宏任务->微任务->渲染->宏任务->微任务->渲染->...\n\n\n### promise, async\u002Fawait\n\n- promise是同步的，它里面的代码会同步执行。\n- promise.then是微任务，promise.then里面的代码放到微任务队列中，等宏任务执行完成之后执行。\n- async\u002Fawait 是同步语法，解决异步回调问题，promise.then.catch 链式调用，但也是基于回调函数的。\n- await会等待一个函数的执行结果，这个函数式同步的\n- await下面的代码相当于promise.then也会放到微任务队列中。\n\n### 揭晓答案\n```language\nasync function promise1() {\n    console.log(\\\"promise1  start\\\")\n    await promise2()\n    console.log(\\\"promise1  end\\\")\n}\nfunction promise2() {\n    console.log(\\\"promise2\\\")\n}\nsetTimeout(function () {\n    console.log(\\\"setTimeout\\\")\n}, 0)\nconsole.log(\\\"script start\\\")\npromise1()\nnew Promise((resolve, reject) => {\n    console.log(\\\"Promise\\\")\n    resolve()\n}).then(function () {\n    console.log(\\\"Promise then\\\")\n})\nconsole.log(\\\"script end\\\")\n\n------------------------------------\nscript start\npromise1  start\npromise2\nPromise\nscript end\npromise1  end\nPromise then\nsetTimeout\n\n```\n\n\n# h5性能优化\n### 1、APP内嵌页面\n> APP预加载网页，点击的时候直接调用页面(缺点，造成app资源浪费，占用内存)\n> css 使用原子css\n\n\n### 1、文件优化： 通过压缩CSS、JavaScript和HTML文件大小，减少网络传输时间。同时，将多个CSS和JavaScript文件合并成一个可以减少请求次数，加快页面加载速度。\n\n###  2、图片优化： \n> 使用图片压缩工具（如TinyPNG）来减小图像文件的大小，采用适当的压缩格式（如JPEG、WebP），并合理使用CSS Sprite或者Base64编码来减少对图片的请求次数，查看图片是否可以服用\n\n###  3、资源缓存优化： \n> 设置合适的缓存策略，利用浏览器缓存机制，尽量减少重复请求，提高页面加载速度。可以通过设置HTTP响应头中的Cache-Control和Expires来控制静态资源的缓存时间。\n\n### 4、延迟加载： \n> 对于非关键内容，如图片、广告等，可以使用懒加载技术，延迟加载这些资源，当用户滚动到它们所在的位置时再进行加载，减少首次加载的时间。\n\n### 5、预加载：\n> 对于可能会在后续页面中使用到的资源（如下一页的CSS、JavaScript等），可以通过预加载机制提前加载这些资源，以减少后续页面的加载时间。\n\n### 6、DOM操作优化： \n> 减少不必要的DOM操作，尽量使用批处理和缓存DOM查询结果来提高性能。避免频繁的重排和重绘操作，可以使用CSS3动画代替JavaScript动画。\n\n### 7、使用Web Workers和Service Worker： \n> 将一些耗时的计算或网络请求任务放到Web Workers中进行并行处理，利用Service Worker实现离线缓存、消息推送等功能，提高应用的响应速度和离线体验。\n\n### 8、减少重定向和请求次数： \n> 避免不必要的重定向和请求，合理使用缓存、本地存储等技术来减少服务器请求次数。\n\n### 9、清理无用资源： \n>定期清理不再使用的资源，如未使用的JavaScript库、样式表和图片等，以减少应用的体积。\n\n### 10、性能监测和优化： \n> 使用工具对H5应用进行性能监测和分析，发现性能瓶颈，并针对性地进行优化，保持应用的高性能状态。\n\n### 11、使用SSR服务端渲染\n\n \n\n\n\n\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F2dd5c54711c1b1586ecc879243bf46ee.jpg",962,"2022-05-25T09:31:20.000Z","2026-06-27T11:06:41.729Z",{"id":4,"name":5,"slug":6},{"id":26,"name":27,"avatar":20},[114],{"id":115,"name":116,"slug":116},"9a74300d-06f7-46d0-80d9-8fe67ec0539b","数组",{"id":118,"title":119,"slug":120,"content":121,"excerpt":119,"coverImage":122,"status":18,"isPinned":19,"pinnedAt":20,"viewCount":123,"publishedAt":124,"createdAt":124,"updatedAt":125,"category":126,"author":127,"tags":128},"f0bda1fc-9873-4bf9-b7d3-be0b7752a3b6","Vue3入门到精通教程","vue3入门到精通教程","\n\n> 起初 Vue3.0 暴露变量必须 return 出来，template中才能使用；Vue3.2 中 只需要在 script 标签上加上setup 属性，组件在编译的过程中代码运行的上下文是在setup()函数中，无需return，template可直接使用。都上了Vue3就直接学setup语法糖吧\n\n## 一、文件结构\n\n```vue\n\u003Ctemplate>\n  \u002F\u002F Vue2中，template标签中只能有一个根元素，在Vue3中没有此限制 \n  \u002F\u002F ...\n  \u003Cdiv class=\\\"box\\\">hello linge\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup>\nimport { ref } from \\\"vue\\\";\nconst color = ref(\\\"red\\\");\n\u002F\u002F ...\n\u003C\u002Fscript>\n\n\u003Cstyle lang=\\\"scss\\\" scoped>\n\u002F\u002F 支持CSS变量注入v-bind(color)\n.box {\n  width: 100px;\n  height: 100px;\n  color: v-bind(color);\n}\n\u003C\u002Fstyle>\n```\n\n## 二、data\n\n```vue\n\u003Cscript setup>\nimport { reactive, ref, toRefs } from \\\"vue\\\";\n\n\u002F\u002F ref声明响应式数据，用于声明基本数据类型\nconst name = ref(\\\"小白龙\\\");\n\u002F\u002F 修改\nname.value = \\\"小龙龙\\\";\n\n\u002F\u002F reactive声明响应式数据，用于声明引用数据类型\nconst state = reactive({\n  name: \\\"小白龙\\\",\n  sex: \\\"男\\\",\n});\n\u002F\u002F 修改\nstate.name = \\\"小龙龙\\\";\n\n\u002F\u002F 使用toRefs解构\nconst { name, sex } = toRefs(state);\n\u002F\u002F template可直接使用{{name}}、{{sex}}\n\n\u002F\u002F toRef 是对定义的响应对象的某个属性进行引用,使用一个函数返回一个响应式对象,依旧保持响应式\nconst nameRef = toRef(state, 'name')\n\n\u003C\u002Fscript>\n```\n\n## 获取dom元素\n```language\n \u003Cdiv ref=\\\"myRef\\\">获取单个DOM元素\u003C\u002Fdiv>\n\n \u003Cdiv>获取多个DOM元素\u003C\u002Fdiv>\n   \u003Cul>\n     \u003Cli v-for=\\\"(item, index) in 3\\\" :key=\\\"index\\\" :ref=\\\"setRef\\\">\n       {{ item }}\n     \u003C\u002Fli>\n \u003C\u002Ful>\n\n\u002F\u002F获取单个DOM元素\nconst myRef = ref(null);\n\n\u002F\u002F获取多个DOM元素\n\u002F\u002F 存储dom数组\nconst myRefs = ref([]);\n\nconst setRef = (el) => {\n   myRefs.value.push(el);\n};\n\n```\n\n\n\n\n\n## 三、method\n\n```vue\n\u003Ctemplate>\n  \u002F\u002F 调用方法\n  \u003Cbutton @click=\\\"changeName\\\">按钮\u003C\u002Fbutton>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup>\nimport { reactive } from 'vue'\n\nconst state = reactive({\n  name: '小白龙'\n})\n\n\u002F\u002F 声明method方法\nconst changeName = () => {\n  state.name = '小龙龙'\n}  \n\u003C\u002Fscript>\n```\n\n## 四、computed\n\n```vue\n\u003Cscript setup>\nimport { computed, ref } from 'vue'\n\nconst count = ref(1)\n\n\u002F\u002F 通过computed获得doubleCount\nconst doubleCount = computed(() => {\n  return count.value * 2\n})\n\u003C\u002Fscript>\n```\n\n## 五、watch\n\n### 属性\n- watch 具备一定的惰性\n- 可以拿到原始值和当前值\n- 可以侦听多个数据的变换，用一个侦听器承载\n- 默认开启深度监听\n\n### 传参\n- 第一个参数，需要传递一个需要监听的 function 、ref 、reactive object\n- 第二个参数，用来接受数据原始值和当前值\n- 第三个参数，传递 watch 的高级配置项 例如 immediate: true\n\n```vue\n\u003Cscript setup>\nimport { watch, reactive } from 'vue'\n\n### 监听基础类型\nconst count = ref(0)\nwatch(count, (newValue, oldValue) => {\n  console.log('watch' + newValue, oldValue)\n})\n\n\n\n### 监听复杂类型\nconst boy = reactive({\n  name: '小白龙',\n  friend: {\n    friend1 : '小小龙',\n    friend2 : '小小小龙'\n  }\n})\n\n#### 监听整个对象\nwatch(boy, (newValue, oldValue) => {\n console.log('boy发生了变化')\n console.log(newValue);\n console.log(newValue.friend);\n})\n\nboy.name = 'Little children'\nboy.friend.friend3 = '小王'\n> 第一个参数传入我们要监听的对象，当监听的对象里面的任意一个属性发生变化，watch 方法便会触发。\n\n#### 监听对象中的某个属性\n\n\u002F\u002F 如果我们直接写 boy.name\nwatch(boy.name, (newValue, oldValue) => {\n console.log('boy发生了变化')\n console.log(newValue)\n})\n\n\u002F\u002F vue会提示我们，监听的对象需要是一个 function 或者 ref 或者是一个 reactive object\n\n\u002F\u002F 正确的写法是:\nwatch(() => boy.name, (newValue, oldValue) => {\n console.log('boy发生了变化')\n console.log(newValue)\n})\n\nboy.name = 'Little children'\n\n\n\n#### 监听对象的所有属性\nwatch(() => boy, (newValue, oldValue) => {\n console.log('boy发生了变化')\n console.log(newValue)\n}, {\n immediate: true\u002F\u002F立即执行\n})\n\nboy.name = 'Little children'\n\n#### 监听多个数据\n\n- watch 里面可以接收一个数组\n- 无论数组里面的哪一个数据发生变化，都会执行侦听器\n\n\nwatch([()=> boy.name, count], ([newName, newCount], [oldName, oldCount]) => {\n console.log(newName + '---' + oldName)\n console.log(newCount + '---' + oldCount)\n})\n\nboy.name = 'Little children'\n\n\n\u003C\u002Fscript>\n```\n\n### watchEffect\n#### 属性\n- 立即执行的，没有惰性\n- 不需要传递要侦听的内容，会自动感知代码依赖，不需要传递很多参数，只需要一个回调函数\n- 不能获取之前数据的值\n#### 监听基础类型\n\n```language\nconst count = ref(0)\n\nwatchEffect(() => {\n  console.log(count.value)\n})\n\ncount.value ++\nconst count = ref(0)\n\nwatchEffect(() => {\n  console.log(count.value)\n})\n\ncount.value ++\n```\n\n\n\n\n\n## 六、props父传子\n\n### 子组件\n\n```vue\n\u003Ctemplate>\n  \u003Cspan>{{ props.name }}\u003C\u002Fspan>\n  \u002F\u002F 可省略【props.】\n  \u003Cspan>{{ name }}\u003C\u002Fspan>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup>\n\u002F\u002F import { defineProps } from 'vue'\n\u002F\u002F defineProps在\u003Cscript setup>中自动可用，无需导入\n\u002F\u002F 需在.eslintrc.js文件中【globals】下配置【defineProps: true】\n\n\u002F\u002F 声明props\nconst props = defineProps({\n  name: {\n    type: String,\n    default: ''\n  }\n})  \n\u003C\u002Fscript>\n```\n\n### 父组件\n\n```vue\n\u003Ctemplate>\n  \u003Cchild name=\\\"Jerry\\\" \u002F>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup>\n\u002F\u002F 引入子组件(组件自动注册)\nimport child from '.\u002Fchild.vue'\n\u003C\u002Fscript>\n```\n\n## 七、emit子传父\n\n### 子组件\n\n```vue\n\u003Ctemplate>\n  \u003Cspan>{{ props.name }}\u003C\u002Fspan>\n  \u002F\u002F 可省略【props.】\n  \u003Cspan>{{ name }}\u003C\u002Fspan>\n  \u003Cbutton @click=\\\"changeName\\\">更名\u003C\u002Fbutton>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup>\n\u002F\u002F import { defineEmits, defineProps } from 'vue'\n\u002F\u002F defineEmits和defineProps在\u003Cscript setup>中自动可用，无需导入\n\u002F\u002F 需在.eslintrc.js文件中【globals】下配置【defineEmits: true】、【defineProps: true】\n\n\u002F\u002F 声明props\nconst props = defineProps({\n  name: {\n    type: String,\n    default: ''\n  }\n})\n\u002F\u002F 声明事件\nconst emit = defineEmits(['updateName'])\n\u002F\u002Fts写法\n\u002F\u002Fconst emit = defineEmits\u003C{\n\u002F\u002F    (e: 'updateName', name: string): void\n\u002F\u002F  }>()\n\nconst changeName = () => {\n  \u002F\u002F 执行\n  emit('updateName', '小龙龙')\n}\n\u003C\u002Fscript>\n```\n\n### 父组件\n\n```vue\n\u003Ctemplate>\n  \u003Cchild :name=\\\"state.name\\\" @updateName=\\\"updateName\\\" \u002F>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup>\nimport { reactive } from 'vue'\n\u002F\u002F 引入子组件\nimport child from '.\u002Fchild.vue'\n\nconst state = reactive({\n  name: '小白龙'\n})\n\n\u002F\u002F 接收子组件触发的方法\nconst updateName = (name) => {\n  state.name = name\n}\n\u003C\u002Fscript>\n```\n\n## 八、v-model\n\n### 子组件\n\n```vue\n\u003Ctemplate>\n  \u003Cspan @click=\\\"changeInfo\\\">我叫{{ modelValue }}，今年{{ age }}岁\u003C\u002Fspan>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup>\n\u002F\u002F import { defineEmits, defineProps } from 'vue'\n\u002F\u002F defineEmits和defineProps在\u003Cscript setup>中自动可用，无需导入\n\u002F\u002F 需在.eslintrc.js文件中【globals】下配置【defineEmits: true】、【defineProps: true】\n\ndefineProps({\n  modelValue: String,\n  age: Number\n})\n\nconst emit = defineEmits(['update:modelValue', 'update:age'])\nconst changeInfo = () => {\n  \u002F\u002F 触发父组件值更新\n  emit('update:modelValue', '小龙龙')\n  emit('update:age', 30)\n}\n\u003C\u002Fscript>\n```\n\n### 父组件\n\n```vue\n\u003Ctemplate>\n  \u002F\u002F v-model:modelValue简写为v-model\n  \u002F\u002F 可绑定多个v-model\n  \u003Cchild v-model=\\\"state.name\\\" v-model:age=\\\"state.age\\\" \u002F>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup>\nimport { reactive } from 'vue'\n\u002F\u002F 引入子组件\nimport child from '.\u002Fchild.vue'\n\nconst state = reactive({\n  name: '小白龙',\n  age: 20\n})\n\u003C\u002Fscript>\n```\n\n## 九、nextTick\n\n```vue\n\u003Cscript setup>\nimport { nextTick } from 'vue'\n\nnextTick(() => {\n  \u002F\u002F ...\n})\n\u003C\u002Fscript>\n```\n\n## 十、子组件ref变量和defineExpose\n\n- 在标准组件写法里，子组件的数据都是默认隐式暴露给父组件的，但在 script-setup 模式下，所有数据只是默认 return 给 template 使用，不会暴露到组件外，所以父组件是无法直接通过挂载 ref 变量获取子组件的数据。\n- 如果要调用子组件的数据，需要先在子组件显示的暴露出来，才能够正确的拿到，这个操作，就是由 defineExpose 来完成。\n\n### 子组件\n\n```vue\n\u003Ctemplate>\n  \u003Cspan>{{ state.name }}\u003C\u002Fspan>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup>\nimport { reactive, toRefs } from 'vue'\n\u002F\u002F defineExpose无需引入\n\u002F\u002F import { defineExpose, reactive, toRefs } from 'vue'\n\n\u002F\u002F 声明state\nconst state = reactive({\n  name: 'Jerry'\n})\n\n\u002F\u002F 将方法、变量暴露给父组件使用，父组件才可通过ref API拿到子组件暴露的数据\ndefineExpose({\n  \u002F\u002F 解构state\n  ...toRefs(state),\n  \u002F\u002F 声明方法\n  changeName() {\n    state.name = '小白龙'\n  }\n})\n\u003C\u002Fscript>\n```\n\n### 父组件\n\n```vue\n\u003Ctemplate>\n  \u003Cchild ref=\\\"childRef\\\" \u002F>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup>\nimport { ref, nextTick } from 'vue'\n\u002F\u002F 引入子组件\nimport child from '.\u002Fchild.vue'\n\n\u002F\u002F 子组件ref\nconst childRef = ref('childRef')\n\n\u002F\u002F nextTick\nnextTick(() => {\n  \u002F\u002F 获取子组件name\n  console.log(childRef.value.name)\n  \u002F\u002F 执行子组件方法\n  childRef.value.changeName()\n})\n\u003C\u002Fscript>\n```\n\n## 十、插槽slot\n\n### 子组件\n\n```vue\n\u003Ctemplate>\n  \u002F\u002F 匿名插槽\n  \u003Cslot\u002F>\n  \u002F\u002F 具名插槽\n  \u003Cslot name='title'\u002F>\n  \u002F\u002F 作用域插槽\n  \u003Cslot name=\\\"footer\\\" :scope=\\\"state\\\" \u002F>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup>\n  import { useSlots, reactive } from 'vue'\n  const state = reactive({\n    name: '小白龙',\n    age: '18岁'\n  })\n  \n  const slots = useSlots()\n  \u002F\u002F 匿名插槽使用情况\n  const defaultSlot = reactive(slots.default && slots.default().length)\n  console.log(defaultSlot) \u002F\u002F 1\n  \u002F\u002F 具名插槽使用情况\n  const titleSlot = reactive(slots.title && slots.title().length)\n  console.log(titleSlot) \u002F\u002F 3\n\u003C\u002Fscript>\n```\n\n### 父组件\n\n```vue\n\u003Ctemplate>\n  \u003Cchild>\n    \u002F\u002F 匿名插槽\n    \u003Cspan>我是默认插槽\u003C\u002Fspan>\n    \u002F\u002F 具名插槽\n    \u003Ctemplate #title>\n      \u003Ch1>我是具名插槽\u003C\u002Fh1>\n      \u003Ch1>我是具名插槽\u003C\u002Fh1>\n      \u003Ch1>我是具名插槽\u003C\u002Fh1>\n    \u003C\u002Ftemplate>\n    \u002F\u002F 作用域插槽\n    \u003Ctemplate #footer=\\\"{ scope }\\\">\n      \u003Cfooter>作用域插槽——姓名：{{ scope.name }}，年龄{{ scope.age }}\u003C\u002Ffooter>\n    \u003C\u002Ftemplate>\n  \u003C\u002Fchild> \n\u003C\u002Ftemplate>\n\n\u003Cscript setup>\n  \u002F\u002F 引入子组件\n  import child from '.\u002Fchild.vue'\n\u003C\u002Fscript>\n```\n\n## 将某个.vue组件挂载到根节点\n```vue\n  \u003Cteleport to=\\\"#app\\\">\n    \u003CChannelProgress :show=\\\"isShow\\\" @updateShow=\\\"updateShow\\\" \u002F>\n  \u003C\u002Fteleport>\n```\n\n## KeepAlive\n> \u003CKeepAlive>是一个内置组件，允许我们在多个组件之间动态切换时，有条件地缓存组件实例。\n```js\n\u003CKeepAlive>\n  \u003Ccomponent :is=\\\"activeComponent\\\" \u002F>\n\u003C\u002FKeepAlive>\n```\n### 属性\n- include 排查\n> include=\\\"a,b\\\"\n:include=\\\"\u002Fa|b\u002F\\\"\n:include=\\\"['a', 'b']\\\"\n\n- max 最大缓存实例数\n> :max=\\\"10\\\"\n\n- 缓存实例的生命周期\n\n```js\n\u002F\u002F创建\n activated() {\n\n  },\n\u002F\u002F销毁\n  deactivated() {\n    \u002F\u002F called when removed from the DOM into the cache\n    \u002F\u002F and also when unmounted\n  }\n```\n\n## vue全局方法\u002F变量（不推荐这样子使用）\n> 只要创建一个createApp(App)，然后用app变量接受就好\n> main.ts\n```ts\nconst app = createApp(App);\n\n\u002F\u002F全局方法\napp.config.globalProperties.$name1 = '小白龙';\n```\n\n> 使用\n```ts\n  const { proxy } = getCurrentInstance() as any;\n  console.log(111, proxy.$name1);\u002F\u002F小白龙\n```\n\n\n\n\n\n## 十二、路由useRoute和useRouter\n\n```vue\n\u003Cscript setup>\n  import { useRoute, useRouter } from 'vue-router'\n\t\n  \u002F\u002F 必须先声明调用\n  const route = useRoute()\n  const router = useRouter()\n\t\n  \u002F\u002F 路由信息\n  console.log(route.query)\n\n  \u002F\u002F 路由跳转\n  router.push('\u002FnewPage')\n\u003C\u002Fscript>\n```\n\n## 十三、路由导航守卫\n\n```vue\n\u003Cscript setup>\n  import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'\n\t\n  \u002F\u002F 添加一个导航守卫，在当前组件将要离开时触发。\n  onBeforeRouteLeave((to, from, next) => {\n    next()\n  })\n\n  \u002F\u002F 添加一个导航守卫，在当前组件更新时触发。\n  \u002F\u002F 在当前路由改变，但是该组件被复用时调用。\n  onBeforeRouteUpdate((to, from, next) => {\n    next()\n  })\n\u003C\u002Fscript>\n```\n\n## 十四、store\n\n*Vue3 中的Vuex不再提供辅助函数写法\n\n```vue\n\u003Cscript setup>\n  import { useStore } from 'vuex'\n  import { key } from '..\u002Fstore\u002Findex'\n\n  \u002F\u002F 必须先声明调用\n  const store = useStore(key)\n\t\n  \u002F\u002F 获取Vuex的state\n  store.state.xxx\n\n  \u002F\u002F 触发mutations的方法\n  store.commit('fnName')\n\n  \u002F\u002F 触发actions的方法\n  store.dispatch('fnName')\n\n  \u002F\u002F 获取Getters\n  store.getters.xxx\n\u003C\u002Fscript>\n```\n\n## 十五、生命周期\n\n通过在生命周期钩子前面加上 “on” 来访问组件的生命周期钩子。\n\n下表包含如何在 Option API 和 setup() 内部调用生命周期钩子\n\n| **Option API**  | **setup中**       |\n| --------------- | ----------------- |\n| beforeCreate    | 不需要            |\n| created         | 不需要            |\n| beforeMount     | onBeforeMount     |\n| mounted         | onMounted         |\n| beforeUpdate    | onBeforeUpdate    |\n| updated         | onUpdated         |\n| beforeUnmount   | onBeforeUnmount   |\n| unmounted       | onUnmounted       |\n| errorCaptured   | onErrorCaptured   |\n| renderTracked   | onRenderTracked   |\n| renderTriggered | onRenderTriggered |\n| activated       | onActivated       |\n| deactivated     | onDeactivated     |\n\n## 十六、CSS变量注入\n\n```vue\n\u003Ctemplate>\n  \u003Cspan>Jerry\u003C\u002Fspan>  \n\u003C\u002Ftemplate>\n\n\u003Cscript setup>\n  import { reactive } from 'vue'\n\n  const state = reactive({\n    color: 'red'\n  })\n\u003C\u002Fscript>\n  \n\u003Cstyle scoped>\n  span {\n    \u002F\u002F 使用v-bind绑定state中的变量\n    color: v-bind('state.color');\n  }  \n\u003C\u002Fstyle>\n```\n\n## 十七、原型绑定与组件内使用\n\n### main.js\n\n```js\nimport { createApp } from 'vue'\nimport App from '.\u002FApp.vue'\nconst app = createApp(App)\n\n\u002F\u002F 获取原型\nconst prototype = app.config.globalProperties\n\n\u002F\u002F 绑定参数\nprototype.name = 'Jerry'\n```\n\n### 组件内使用\n\n```vue\n\u003Cscript setup>\n  import { getCurrentInstance } from 'vue'\n\n  \u002F\u002F 获取原型\n  const { proxy } = getCurrentInstance()\n  \n  \u002F\u002F 输出\n  console.log(proxy.name)\n\u003C\u002Fscript>\n```\n\n## 十八、对 await 的支持\n\n不必再配合 async 就可以直接使用 await 了，这种情况下，组件的 setup 会自动变成 async setup 。\n\n```vue\n\u003Cscript setup>\n  const post = await fetch('\u002Fapi').then(() => {})\n\u003C\u002Fscript>\n```\n\n## 十九、定义组件的name\n\n用单独的`\u003Cscript>`块来定义\n\n```vue\n\u003Cscript>\n  export default {\n    name: 'ComponentName',\n  }\n\u003C\u002Fscript>\n```\n\n## 二十、provide和inject\n\n### 父组件\n\n```vue\n\u003Ctemplate>\n  \u003Cchild\u002F>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup>\n  import { provide } from 'vue'\n  import { ref, watch } from 'vue'\n  \u002F\u002F 引入子组件\n  import child from '.\u002Fchild.vue'\n\n  let name = ref('Jerry')\n  \u002F\u002F 声明provide\n  provide('provideState', {\n    name,\n    changeName: () => {\n      name.value = 'Tom'\n    }\n  })\n\n  \u002F\u002F 监听name改变\n  watch(name, () => {\n    console.log(`name变成了${name}`)\n    setTimeout(() => {\n      console.log(name.value) \u002F\u002F Tom\n    }, 1000)\n  })\n\u003C\u002Fscript>\n```\n\n### 子组件\n\n```vue\n\u003Cscript setup>\n  import { inject } from 'vue'\n\t\u002F\u002F 注入\n  const provideState = inject('provideState')\n  \n  \u002F\u002F 子组件触发name改变\n  provideState.changeName()\n\u003C\u002Fscript>\n```\n\n## 二十一、Vue3中使用echarts\n\n```js\n\u002F\u002F 安装\nnpm i echarts --save\n\n\u002F\u002F 组件内引入\nimport * as echarts from 'echarts'\n```\n\n\n## 自定义指令实现按钮防抖\n> 监听按钮的点击事件,给点击事件设置防抖，如果特定时间段内多次提交，则每以最后一次重新计算时间。\n\n### 代码实现\n> src\u002Fdirectives\u002FpreReClick.ts\n```js\nexport default (app) => {\n  app.directive('preReClick', {\n    mounted(el, binding) {\n      el.addEventListener('click', () => {\n        if (!el.disabled) {\n          el.disabled = true;\n          setTimeout(() => {\n            el.disabled = false;\n          }, binding.value || 2000)\n        }\n      })\n    }\n  })\n}\n```\n### 在main.js中引入指令文件\n```language\nimport preReClick from '@\u002Fdirectives\u002FpreReClick';\n\nconst app = createApp(App);\napp.use(ElementPlus).use(preReClick).mount('#app');\n```\n### 组件中使用\n\n```language\n  \u003Cbutton @click=\\\"confirm\\\" v-preReClick>点我点我\u003C\u002Fbutton>\n```\n\n\n## 全局消息弹框封装\n> Tips.vue\n```language\n\u003Ctemplate>\n    \u003Cdiv class=\\\"tips\\\">\n        \u003Cdiv class=\\\"box\\\" :class=\\\"type\\\">\n            \u003Csvg class=\\\"iconpark-icon\\\">\u003Cuse :href=\\\"icontype\\\">\u003C\u002Fuse>\u003C\u002Fsvg>\n            \u003Cspan>{{ title }}\u003C\u002Fspan>\n        \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript lang=\\\"ts\\\" setup>\nimport { defineProps, computed } from 'vue';\n\ninterface Props {\n    type: string;\n    title: string;\n}\n\nconst props: Props = defineProps({\n    type: {\n        type: String,\n        default: 'success'\n    },\n    title: {\n        type: String,\n        default: ''\n    }\n});\n\nconst icontype = computed(() => {\n    const type = {\n        success: '#gou',\n        warning: '#zu2141',\n        error: '#4',\n        info: '#gou',\n        hint: 'icon-tishi'\n    };\n    return type[props.type];\n});\n\u003C\u002Fscript>\n\n\u003Cstyle lang=\\\"scss\\\" scoped>\n.tips {\n    z-index: 9999;\n    position: fixed;\n    top: 100px;\n    left: 50%;\n    transform: translateX(-50%);\n    animation: tipsTop 1s;\n    overflow: hidden;\n    box-shadow: 0 1px 20px 0 rgba(153, 153, 153, 0.5);\n    .box {\n        width: fit-content;\n        height: 80px;\n        display: flex;\n        align-items: center;\n        background-color: #ffffff;\n        box-shadow: 0 1px 20px 0 rgba(153, 153, 153, 0.5);\n        border-radius: 4px;\n        padding: 0 42px;\n        .iconpark-icon {\n            width: 30px;\n            height: 30px;\n            margin-right: 10px;\n        }\n        span {\n            max-width: 1000px;\n            font-size: 18px;\n            font-weight: normal;\n            font-stretch: normal;\n            letter-spacing: 2px;\n            overflow: hidden;\n            white-space: nowrap;\n            text-overflow: ellipsis;\n        }\n    }\n    .success {\n        color: #0173cd;\n    }\n    .warning {\n        color: #ff9800;\n    }\n    .error {\n        color: #c73741;\n    }\n    .info {\n        color: #777777;\n    }\n    .hint {\n        color: #fd4d4d;\n    }\n}\n\n\u002F\u002F 动画\n@keyframes tipsTop {\n    0% {\n        top: -10px;\n    }\n\n    100% {\n        top: 100px;\n    }\n}\n\u003C\u002Fstyle>\n\n\n```\n> Tips.ts\n```ts\nimport { createVNode, render } from 'vue';\nimport Tips from '.\u002FTips.vue';\n\nconst div = document.createElement('div');\ndiv.setAttribute('class', 'tips');\ndocument.body.appendChild(div);\n\nlet timer: number;\n\nexport default ({ type, title }: { type: string; title: string }) => {\n    const vnode = createVNode(Tips, { type, title });\n    render(vnode, div);\n    timer && clearTimeout(timer);\n    timer = setTimeout(() => {\n        render(null, div);\n    }, 2000);\n};\n\n\n```\n> 使用\n```language\nimport Tips from '@\u002Fcomponents\u002FTips\u002FTips';\nTips({ type: 'success', title: '新建素材成功' });\n```\n\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F7060ae3f0a286faf6059839f27fcbb38.png",1634,"2022-05-24T01:54:49.000Z","2026-06-27T11:04:04.795Z",{"id":4,"name":5,"slug":6},{"id":26,"name":27,"avatar":20},[129],{"id":60,"name":5,"slug":6},8,1,10,{"categories":134,"tags":214,"postCount":390,"tagCount":391,"hotPosts":392},[135,142,148,155,161,162,169,173,180,184,188,193,199,204,208],{"id":136,"name":137,"slug":138,"description":139,"sortOrder":140,"createdAt":141,"updatedAt":141,"postCount":131},"e8d0bd45-d10c-46d3-8afb-0c072df7f8a7","技术","tech","技术文章",0,"2026-06-27T04:18:37.371Z",{"id":143,"name":144,"slug":145,"description":144,"sortOrder":131,"createdAt":146,"updatedAt":9,"postCount":147},"15ac46ad-edf7-4435-9a64-ff78117d58c5","Vue3 生态","vue3-生态","2022-05-21T08:05:39.000Z",6,{"id":149,"name":150,"slug":151,"description":150,"sortOrder":152,"createdAt":153,"updatedAt":9,"postCount":154},"11d4d397-685c-4180-a7b3-9b0e3a1e411e","Css","css",2,"2022-05-23T07:19:37.000Z",9,{"id":156,"name":157,"slug":157,"description":157,"sortOrder":158,"createdAt":159,"updatedAt":9,"postCount":160},"d10456a5-e649-4741-a38f-f07f266ce5f2","开发环境",3,"2022-05-24T01:52:41.000Z",13,{"id":4,"name":5,"slug":6,"description":5,"sortOrder":7,"createdAt":8,"updatedAt":9,"postCount":130},{"id":163,"name":164,"slug":165,"description":164,"sortOrder":166,"createdAt":167,"updatedAt":9,"postCount":168},"da130ba9-d4f4-49f3-aa0f-149078097ef0","JavaScript","javascript",5,"2022-05-24T02:22:57.000Z",18,{"id":170,"name":171,"slug":171,"description":171,"sortOrder":147,"createdAt":172,"updatedAt":9,"postCount":131},"d8cbe380-54b3-4a61-a12d-5438c2918574","限时优惠","2022-05-25T07:18:03.000Z",{"id":174,"name":175,"slug":176,"description":175,"sortOrder":177,"createdAt":178,"updatedAt":9,"postCount":179},"e0f3b8d8-cfe7-41fb-802b-a79699d95968","JavaScript插件","javascript插件",7,"2022-06-01T14:08:31.000Z",16,{"id":181,"name":182,"slug":182,"description":182,"sortOrder":130,"createdAt":183,"updatedAt":9,"postCount":147},"4ea3d8af-9cc3-49bb-a9cd-34dbcdc3bd85","构建工具","2022-06-02T07:28:13.000Z",{"id":185,"name":186,"slug":186,"description":186,"sortOrder":154,"createdAt":187,"updatedAt":9,"postCount":130},"9ed9827c-9cbb-42da-80e4-d04c7fdba886","开发工具","2022-06-21T03:35:05.000Z",{"id":189,"name":190,"slug":191,"description":190,"sortOrder":132,"createdAt":192,"updatedAt":9,"postCount":166},"6b9179c3-17b2-43ff-a431-a03d6eb32d89","Vue2 生态","vue2-生态","2022-07-16T13:14:29.000Z",{"id":194,"name":195,"slug":196,"description":195,"sortOrder":197,"createdAt":198,"updatedAt":9,"postCount":166},"73a5f62c-3c47-45b9-9ae2-f29953ae8dc0","Node","node",11,"2022-07-16T13:15:39.000Z",{"id":200,"name":201,"slug":201,"description":201,"sortOrder":202,"createdAt":203,"updatedAt":9,"postCount":152},"2b696c16-48ef-403b-a88b-6e57cfc79596","开发问题",12,"2022-07-16T14:06:54.000Z",{"id":205,"name":206,"slug":206,"description":206,"sortOrder":160,"createdAt":207,"updatedAt":9,"postCount":131},"c0f0561e-a47a-4ecd-8caa-cc1df2315d57","算法","2022-07-16T14:22:34.000Z",{"id":209,"name":210,"slug":211,"description":210,"sortOrder":212,"createdAt":213,"updatedAt":9,"postCount":158},"a629c1f7-29f1-439e-be3c-29670b17ba20","Vue2","vue2",15,"2022-07-16T14:41:51.000Z",[215,221,226,229,234,239,243,247,252,257,262,267,270,275,279,283,288,293,298,303,307,311,315,316,317,322,327,330,333,336,339,340,344,347,349,351,354,358,359,363,367,370,373,375,378,380,384,387],{"id":216,"name":217,"slug":218,"createdAt":219,"updatedAt":220},"076bd8b9-293e-45cb-9dc3-e162007ca474","Axios","axios","2022-06-05T07:41:56.000Z","2025-12-30T07:26:21.000Z",{"id":222,"name":223,"slug":224,"createdAt":225,"updatedAt":9},"2aa7f6d0-1fac-4ed1-b9bb-f3afc813f42c","Axure","axure","2022-06-21T03:35:15.000Z",{"id":227,"name":150,"slug":151,"createdAt":228,"updatedAt":9},"b084ddd8-09be-4e57-98f0-cf4e376aecd7","2022-05-21T09:59:55.000Z",{"id":230,"name":231,"slug":232,"createdAt":233,"updatedAt":9},"78a62bff-ff77-4878-8c25-3e6aae18c668","Docker","docker","2022-07-16T14:34:37.000Z",{"id":235,"name":236,"slug":237,"createdAt":238,"updatedAt":9},"2de16806-ef3f-4e54-a259-d1e1e182468c","Git","git","2022-07-16T14:25:15.000Z",{"id":240,"name":241,"slug":242,"createdAt":228,"updatedAt":9},"994cc226-578b-4a72-a57e-a47a63d2793e","JavaScript生态","javascript生态",{"id":244,"name":245,"slug":246,"createdAt":228,"updatedAt":9},"5086e93c-23b9-43d3-9643-cc87f0e9ee94","JenKins","jenkins",{"id":248,"name":249,"slug":250,"createdAt":251,"updatedAt":9},"b73007a8-bb5c-42a8-9fd9-163033a5b45d","Linux","linux","2022-07-16T14:40:17.000Z",{"id":253,"name":254,"slug":255,"createdAt":256,"updatedAt":9},"0b658b92-dd6b-4db3-a398-9f6d69950a02","Markdown","markdown","2022-07-16T14:39:25.000Z",{"id":258,"name":259,"slug":260,"createdAt":261,"updatedAt":9},"ab034d3a-6e5b-4db5-a2dc-faf4ccbb63f5","Nest","nest","2022-07-16T13:15:49.000Z",{"id":263,"name":264,"slug":265,"createdAt":266,"updatedAt":9},"52c41978-da06-4962-9636-45bbaeedda80","Nginx","nginx","2022-05-21T09:59:56.000Z",{"id":268,"name":269,"slug":269,"createdAt":266,"updatedAt":9},"0f1cc678-40e4-44b1-b2cf-a6fd8a1c867a","npm",{"id":271,"name":272,"slug":273,"createdAt":274,"updatedAt":9},"a4370d78-70e1-4073-a8f6-3dc5d81fd8fd","Nuxt","nuxt","2022-06-01T13:07:07.000Z",{"id":276,"name":277,"slug":278,"createdAt":228,"updatedAt":9},"d232e01f-048e-4151-8a0a-fff9561f946f","Pinia","pinia",{"id":280,"name":281,"slug":282,"createdAt":266,"updatedAt":9},"14e9ab02-b0bb-4c85-8604-fe6f1f0f33cd","Pnpm","pnpm",{"id":284,"name":285,"slug":286,"createdAt":287,"updatedAt":287},"399d1d38-cc0d-43ce-8baf-c769447a2ebd","React生态","react生态","2023-02-21T02:03:09.000Z",{"id":289,"name":290,"slug":291,"createdAt":292,"updatedAt":9},"c95bbe84-bdd0-410a-86a9-e87958c55f4f","Redis","redis","2022-10-05T05:14:14.000Z",{"id":294,"name":295,"slug":296,"createdAt":297,"updatedAt":9},"6d05f9df-e116-450f-af57-85ed710c4870","Swiper","swiper","2022-06-01T14:08:46.000Z",{"id":299,"name":300,"slug":301,"createdAt":302,"updatedAt":9},"66f3aeb0-84ef-45f6-a43a-944eefc9895a","Vite","vite","2022-06-02T07:28:24.000Z",{"id":304,"name":305,"slug":306,"createdAt":228,"updatedAt":9},"bf5b94d3-090b-4098-a03c-4bc69781fb2d","Vue","vue",{"id":308,"name":309,"slug":310,"createdAt":266,"updatedAt":9},"2f7fb1be-b9c5-4606-b54f-e9f66f2653b2","Vue-Router","vue-router",{"id":312,"name":313,"slug":314,"createdAt":228,"updatedAt":9},"2fef3b91-1c1c-4ae8-b2c1-0e04b4f9b3a2","Vue2生态","vue2生态",{"id":60,"name":5,"slug":6,"createdAt":228,"updatedAt":9},{"id":30,"name":31,"slug":32,"createdAt":228,"updatedAt":9},{"id":318,"name":319,"slug":320,"createdAt":321,"updatedAt":9},"c807b2c6-cb12-4409-a1f1-6bea9f330a6b","Vuex","vuex","2022-07-16T13:14:59.000Z",{"id":323,"name":324,"slug":325,"createdAt":326,"updatedAt":9},"5782dff5-2ea2-4427-9696-d4363a7fd5bc","Webpack","webpack","2022-07-16T14:33:41.000Z",{"id":328,"name":329,"slug":329,"createdAt":228,"updatedAt":9},"d0aa41f4-68f8-48d4-a4ed-3a503ea90451","下载",{"id":331,"name":332,"slug":332,"createdAt":228,"updatedAt":9},"a046060c-39ef-474a-8c85-2546aca0e2e5","代码片段",{"id":334,"name":335,"slug":335,"createdAt":228,"updatedAt":9},"fee73435-b2be-4b55-85b1-d133ea96aea4","伪元素",{"id":337,"name":338,"slug":338,"createdAt":228,"updatedAt":9},"436bd369-8c57-4869-8827-e88e50e5e0ab","伪类",{"id":87,"name":88,"slug":88,"createdAt":228,"updatedAt":9},{"id":341,"name":342,"slug":342,"createdAt":343,"updatedAt":9},"9321a12e-ea72-49a9-a32d-5566149f812f","图片压缩","2022-08-02T00:37:47.000Z",{"id":345,"name":346,"slug":346,"createdAt":266,"updatedAt":9},"512b16fb-576a-4397-a7c5-dd20e6a8f9ca","布局",{"id":348,"name":186,"slug":186,"createdAt":228,"updatedAt":9},"f32faa96-f2ec-45c6-9a17-2c76062edcb0",{"id":350,"name":157,"slug":157,"createdAt":228,"updatedAt":9},"3c46ed3f-6d6b-4f91-bcb3-af5112860bf5",{"id":352,"name":353,"slug":353,"createdAt":228,"updatedAt":9},"dbfc086a-73a6-4560-814d-593acb61cf98","性能优化",{"id":355,"name":356,"slug":356,"createdAt":357,"updatedAt":9},"1831cd06-0d6b-48f7-94fa-324782fe23cb","拖拽","2022-07-28T12:39:13.000Z",{"id":115,"name":116,"slug":116,"createdAt":266,"updatedAt":9},{"id":360,"name":361,"slug":361,"createdAt":362,"updatedAt":9},"19ac8998-7e0a-459b-9702-bb1adca70e8c","文本复制","2022-07-17T01:54:45.000Z",{"id":364,"name":365,"slug":365,"createdAt":366,"updatedAt":9},"5ff33473-71a4-4e02-8c82-f9ea369a768f","时间","2022-07-17T01:51:12.000Z",{"id":368,"name":369,"slug":369,"createdAt":228,"updatedAt":9},"aa47ca4d-d3f6-4cac-b495-2c67c9592c36","最新优惠",{"id":371,"name":372,"slug":372,"createdAt":266,"updatedAt":9},"f6766d54-54fc-405e-932d-b7d550559125","服务器",{"id":374,"name":182,"slug":182,"createdAt":228,"updatedAt":9},"d856559a-03ff-40b4-980d-3f272b998c3c",{"id":376,"name":377,"slug":377,"createdAt":228,"updatedAt":9},"692d5d68-b188-4e5c-aca8-65d0229399a1","渐变",{"id":379,"name":206,"slug":206,"createdAt":228,"updatedAt":9},"38e1fd6b-d7c6-4d62-bf70-7bacc175bea9",{"id":381,"name":382,"slug":382,"createdAt":383,"updatedAt":9},"be7b10bc-49eb-4a03-bea7-ceb915d500fe","规范","2022-07-16T14:41:06.000Z",{"id":385,"name":386,"slug":386,"createdAt":266,"updatedAt":9},"b42e2916-ad62-4b8a-a863-cd8c19a829de","面试",{"id":388,"name":389,"slug":389,"createdAt":266,"updatedAt":9},"7069add9-b636-44f1-9cd4-ea3a6d2b85d3","面试题",104,48,[393,405,417,422,435],{"id":394,"title":395,"slug":395,"content":396,"excerpt":395,"coverImage":397,"status":18,"isPinned":19,"pinnedAt":20,"viewCount":398,"publishedAt":399,"createdAt":399,"updatedAt":400,"category":401,"author":402,"tags":403},"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":163,"name":164,"slug":165},{"id":26,"name":27,"avatar":20},[404],{"id":240,"name":241,"slug":242},{"id":406,"title":407,"slug":407,"content":408,"excerpt":407,"coverImage":409,"status":18,"isPinned":19,"pinnedAt":20,"viewCount":410,"publishedAt":411,"createdAt":411,"updatedAt":412,"category":413,"author":414,"tags":415},"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":189,"name":190,"slug":191},{"id":26,"name":27,"avatar":20},[416],{"id":341,"name":342,"slug":342},{"id":62,"title":63,"slug":64,"content":65,"excerpt":63,"coverImage":66,"status":18,"isPinned":19,"pinnedAt":20,"viewCount":67,"publishedAt":68,"createdAt":68,"updatedAt":42,"category":418,"author":419,"tags":420},{"id":4,"name":5,"slug":6},{"id":26,"name":27,"avatar":20},[421],{"id":60,"name":5,"slug":6},{"id":423,"title":424,"slug":425,"content":426,"excerpt":424,"coverImage":427,"status":18,"isPinned":19,"pinnedAt":20,"viewCount":428,"publishedAt":429,"createdAt":429,"updatedAt":430,"category":431,"author":432,"tags":433},"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":163,"name":164,"slug":165},{"id":26,"name":27,"avatar":20},[434],{"id":240,"name":241,"slug":242},{"id":436,"title":437,"slug":438,"content":439,"excerpt":437,"coverImage":440,"status":18,"isPinned":19,"pinnedAt":20,"viewCount":441,"publishedAt":442,"createdAt":442,"updatedAt":443,"category":444,"author":448,"tags":449},"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":445,"name":446,"slug":447},"f1701085-b8c1-413a-8750-58e7a0a33832","React","react",{"id":26,"name":27,"avatar":20},[450],{"id":284,"name":285,"slug":286}]