## 1. Project Overview & Quickstart (wangrongding/LICENSE)

# LICENSE

Open-source repository wangrongding/LICENSE

### Repository Details
- **Repository:** [wangrongding/LICENSE](https://github.com/wangrongding/LICENSE)
- **Primary Language:** Code

*Note: High-volume repository documentation is actively indexed and synchronized by YakaAI.*

## 2. Official Technical Reference & Guides (wangrongding/wangrongding.github.io)

## File: README.md

# FedTop

Hi，guys！欢迎来到我的个人博客，我是荣顶，一名前端开发工程师。  
本项目，主要用于总结、分享本人在前端开发中的一些经验总结和开发心得。

博客地址：[FedTop](https://fedtop.com)，博客内容包括但不限于以下主题：

- Web 音视频，WebRTC
- 前端性能优化
- 前端工程化，开发规范
- 前端安全
- 前端框架,前端工具
- 前端面试
- Tensorflow.js
- WebAssembly
- 跨端开发（小程序，Electron，Taro，NW.js 等）
- 前端基础知识（HTML，CSS，JavaScript，Typescript，浏览器原理等）

## F&Q

### 为什么取名为 FedTop？

FedTop 是 Front End Developer Top 的缩写，意为前端之巅。（为什么不是 Fetop？因为域名被占了，哭...）

### 可以转载到 xxx 吗？

可以，本项目所有内容均为原创，转载请联系本人授权。

---

## File: index.md

---
layout: home
title: 荣顶
---

---

## File: package.json

{
  "name": "fedtop",
  "private": "true",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "vitepress dev",
    "build": "vitepress build",
    "preview": "vitepress preview",
    "format": "prettier --write ."
  },
  "lint-staged": {
    "*.{js,ts,tsx,css,scss,vue,md}": [
      "prettier --write"
    ]
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/fs-extra": "^11.0.1",
    "@types/markdown-it": "^13.0.0",
    "autoprefixer": "^10.4.14",
    "fast-glob": "^3.3.1",
    "feed": "^4.2.2",
    "fs-extra": "^11.1.1",
    "globby": "11.0.4",
    "gray-matter": "^4.0.3",
    "husky": "^8.0.3",
    "lint-staged": "^13.2.3",
    "markdown-it": "^13.0.1",
    "markdown-it-mathjax3": "^4.3.2",
    "postcss": "^8.4.27",
    "prettier": "^2.8.6",
    "prettier-plugin-tailwindcss": "^0.4.1",
    "sass": "^1.64.1",
    "tailwindcss": "^3.3.3",
    "vite": "^4.4.7",
    "vitepress": "1.0.0-beta.7"
  },
  "dependencies": {
    "@giscus/vue": "^2.3.0",
    "dayjs": "^1.11.9",
    "element-plus": "^2.3.8",
    "tiny-player": "^1.0.19",
    "vue": "3.3.2"
  },
  "postcss": {
    "plugins": {
      "tailwindcss": {},
      "autoprefixer": {}
    }
  }
}

---

## File: pnpm-lock.yaml

lockfileVersion: '6.0'

settings:
  autoInstallPeers: true
  excludeLinksFromLockfile: false

dependencies:
  '@giscus/vue':
    specifier: ^2.3.0
    version: 2.3.0(vue@3.3.2)
  dayjs:
    specifier: ^1.11.9
    version: 1.11.9
  element-plus:
    specifier: ^2.3.8
    version: 2.3.8(vue@3.3.2)
  tiny-player:
    specifier: ^1.0.19
    version: 1.0.19(hls.js@1.4.10)
  vue:
    specifier: 3.3.2
    version: 3.3.2

devDependencies:
  '@types/fs-extra':
    specifier: ^11.0.1
    version: 11.0.1
  '@types/markdown-it':
    specifier: ^13.0.0
    version: 13.0.0
  autoprefixer:
    specifier: ^10.4.14
    version: 10.4.14(postcss@8.4.27)
  fast-glob:
    specifier: ^3.3.1
    version: 3.3.1
  feed:
    specifier: ^4.2.2
    version: 4.2.2
  fs-extra:
    specifier: ^11.1.1
    version: 11.1.1
  globby:
    specifier: 11.0.4
    version: 11.0.4
  gray-matter:
    specifier: ^4.0.3
    version: 4.0.3
  husky:
    specifier: ^8.0.3
    version: 8.0.3
  lint-staged:
    specifier: ^13.2.3
    version: 13.2.3
  markdown-it:
    specifier: ^13.0.1
    version: 13.0.1
  markdown-it-mathjax3:
    specifier: ^4.3.2
    version: 4.3.2
  postcss:
    specifier: ^8.4.27
    version: 8.4.27
  prettier:
    specifier: ^2.8.6
    version: 2.8.6
  prettier-plugin-tailwindcss:
    specifier: ^0.4.1
    version: 0.4.1(prettier@2.8.6)
  sass:
    specifier: ^1.64.1
    version: 1.64.1
  tailwindcss:
    specifier: ^3.3.3
    version: 3.3.3
  vite:
    specifier: ^4.4.7
    version: 4.4.7(sass@1.64.1)
  vitepress:
    specifier: 1.0.0-beta.7
    version: 1.0.0-beta.7(@algolia/client-search@4.19.1)(sass@1.64.1)(search-insights@2.7.0)

packages:

  /@algolia/autocomplete-core@1.9.3(@algolia/client-search@4.19.1)(algoliasearch@4.19.1)(search-insights@2.7.0):
    resolution: {integrity: sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw==}
    dependencies:
      '@algolia/autocomplete-plugin-algolia-insights': 1.9.3(@algolia/client-search@4.19.1)(algoliasearch@4.19.1)(search-insights@2.7.0)
      '@algolia/autocomplete-shared': 1.9.3(@algolia/client-search@4.19.1)(algoliasearch@4.19.1)
    transitivePeerDependencies:
      - '@algolia/client-search'
      - algoliasearch
      - search-insights
    dev: true

  /@algolia/autocomplete-plugin-algolia-insights@1.9.3(@algolia/client-search@4.19.1)(algoliasearch@4.19.1)(search-insights@2.7.0):
    resolution: {integrity: sha512-a/yTUkcO/Vyy+JffmAnTWbr4/90cLzw+CC3bRbhnULr/EM0fGNvM13oQQ14f2moLMcVDyAx/leczLlAOovhSZg==}
    peerDependencies:
      search-insights: '>= 1 < 3'
    dependencies:
      '@algolia/autocomplete-shared': 1.9.3(@algolia/client-search@4.19.1)(algoliasearch@4.19.1)
      search-insights: 2.7.0
    transitivePeerDependencies:
      - '@algolia/client-search'
      - algoliasearch
    dev: true

  /@algolia/autocomplete-preset-algolia@1.9.3(@algolia/client-search@4.19.1)(algoliasearch@4.19.1):
    resolution: {integrity: sha512-d4qlt6YmrLMYy95n5TB52wtNDr6EgAIPH81dvvvW8UmuWRgxEtY0NJiPwl/h95JtG2vmRM804M0DSwMCNZlzRA==}
    peerDependencies:
      '@algolia/client-search': '>= 4.9.1 < 6'
      algoliasearch: '>= 4.9.1 < 6'
    dependencies:
      '@algolia/autocomplete-shared': 1.9.3(@algolia/client-search@4.19.1)(algoliasearch@4.19.1)
      '@algolia/client-search': 4.19.1
      algoliasearch: 4.19.1
    dev: true

  /@algolia/autocomplete-shared@1.9.3(@algolia/client-search@4.19.1)(algoliasearch@4.19.1):
    resolution: {integrity: sha512-Wnm9E4Ye6Rl6sTTqjoymD+l8DjSTHsHboVRYrKgEt8Q7UHm9nYbqhN/i0fhUYA3OAEH7WA8x3jfpnmJm3rKvaQ==}
    peerDependencies:
      '@algolia/client-search': '>= 4.9.1 < 6'
      algoliasearch: '>= 4.9.1 < 6'
    dependencies:
      '@algolia/client-search': 4.19.1
      algoliasearch: 4.19.1
    dev: true

  /@algolia/cache-browser-local-storage@4.19.1:
    resolution: {integrity: sha512-FYAZWcGsFTTaSAwj9Std8UML3Bu8dyWDncM7Ls8g+58UOe4XYdlgzXWbrIgjaguP63pCCbMoExKr61B+ztK3tw==}
    dependencies:
      '@algolia/cache-common': 4.19.1
    dev: true

  /@algolia/cache-common@4.19.1:
    resolution: {integrity: sha512-XGghi3l0qA38HiqdoUY+wvGyBsGvKZ6U3vTiMBT4hArhP3fOGLXpIINgMiiGjTe4FVlTa5a/7Zf2bwlIHfRqqg==}
    dev: true

  /@algolia/cache-in-memory@4.19.1:
    resolution: {integrity: sha512-+PDWL+XALGvIginigzu8oU6eWw+o76Z8zHbBovWYcrtWOEtinbl7a7UTt3x3lthv+wNuFr/YD1Gf+B+A9V8n5w==}
    dependencies:
      '@algolia/cache-common': 4.19.1
    dev: true

  /@algolia/client-account@4.19.1:
    resolution: {integrity: sha512-Oy0ritA2k7AMxQ2JwNpfaEcgXEDgeyKu0V7E7xt/ZJRdXfEpZcwp9TOg4TJHC7Ia62gIeT2Y/ynzsxccPw92GA==}
    dependencies:
      '@algolia/client-common': 4.19.1
      '@algolia/client-search': 4.19.1
      '@algolia/transporter': 4.19.1
    dev: true

  /@algolia/client-analytics@4.19.1:
    resolution: {integrity: sha512-5QCq2zmgdZLIQhHqwl55ZvKVpLM3DNWjFI4T+bHr3rGu23ew2bLO4YtyxaZeChmDb85jUdPDouDlCumGfk6wOg==}
    dependencies:
      '@algolia/client-common': 4.19.1
      '@algolia/client-search': 4.19.1
      '@algolia/requester-common': 4.19.1
      '@algolia/transporter': 4.19.1
    dev: true

  /@algolia/client-common@4.19.1:
    resolution: {integrity: sha512-3kAIVqTcPrjfS389KQvKzliC559x+BDRxtWamVJt8IVp7LGnjq+aVAXg4Xogkur1MUrScTZ59/AaUd5EdpyXgA==}
    dependencies:
      '@algolia/requester-common': 4.19.1
      '@algolia/transporter': 4.19.1
    dev: true

  /@algolia/client-personalization@4.19.1:
    resolution: {integrity: sha512-8CWz4/H5FA+krm9HMw2HUQenizC/DxUtsI5oYC0Jxxyce1vsr8cb1aEiSJArQT6IzMynrERif1RVWLac1m36xw==}
    dependencies:
      '@algolia/client-common': 4.19.1
      '@algolia/requester-common': 4.19.1
      '@algolia/transporter': 4.19.1
    dev: true

  /@algolia/client-search@4.19.1:
    resolution: {integrity: sha512-mBecfMFS4N+yK/p0ZbK53vrZbL6OtWMk8YmnOv1i0LXx4pelY8TFhqKoTit3NPVPwoSNN0vdSN9dTu1xr1XOVw==}
    dependencies:
      '@algolia/client-common': 4.19.1
      '@algolia/requester-common': 4.19.1
      '@algolia/transporter': 4.19.1
    dev: true

  /@algolia/logger-common@4.19.1:
    resolution: {integrity: sha512-i6pLPZW/+/YXKis8gpmSiNk1lOmYCmRI6+x6d2Qk1OdfvX051nRVdalRbEcVTpSQX6FQAoyeaui0cUfLYW5Elw==}
    dev: true

  /@algolia/logger-console@4.19.1:
    resolution: {integrity: sha512-jj72k9GKb9W0c7TyC3cuZtTr0CngLBLmc8trzZlXdfvQiigpUdvTi1KoWIb2ZMcRBG7Tl8hSb81zEY3zI2RlXg==}
    dependencies:
      '@algolia/logger-common': 4.19.1
    dev: true

  /@algolia/requester-browser-xhr@4.19.1:
    resolution: {integrity: sha512-09K/+t7lptsweRTueHnSnmPqIxbHMowejAkn9XIcJMLdseS3zl8ObnS5GWea86mu3vy4+8H+ZBKkUN82Zsq/zg==}
    dependencies:
      '@algolia/requester-common': 4.19.1
    dev: true

  /@algolia/requester-common@4.19.1:
    resolution: {integrity: sha512-BisRkcWVxrDzF1YPhAckmi2CFYK+jdMT60q10d7z3PX+w6fPPukxHRnZwooiTUrzFe50UBmLItGizWHP5bDzVQ==}
    dev: true

  /@algolia/requester-node-http@4.19.1:
    resolution: {integrity: sha512-6DK52DHviBHTG2BK/Vv2GIlEw7i+vxm7ypZW0Z7vybGCNDeWzADx+/TmxjkES2h15+FZOqVf/Ja677gePsVItA==}
    dependencies:
      '@algolia/requester-common': 4.19.1
    dev: true

  /@algolia/transporter@4.19.1:
    resolution: {integrity: sha512-nkpvPWbpuzxo1flEYqNIbGz7xhfhGOKGAZS7tzC+TELgEmi7z99qRyTfNSUlW7LZmB3ACdnqAo+9A9KFBENviQ==}
    dependencies:
      '@algolia/cache-common': 4.19.1
      '@algolia/logger-common': 4.19.1
      '@algolia/requester-common': 4.19.1
    dev: true

  /@alloc/quick-lru@5.2.0:
    resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
    engines: {node: '>=10'}
    dev: true

  /@babel/helper-string-parser@7.22.5:
    resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==}
    engines: {node: '>=6.9.0'}

  /@babel/helper-validator-identifier@7.22.5:
    resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==}
    engines: {node: '>=6.9.0'}

  /@babel/parser@7.22.7:
    resolution: {integrity: sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==}
    engines: {node: '>=6.0.0'}
    hasBin: true
    dependencies:
      '@babel/types': 7.22.5

  /@babel/types@7.22.5:
    resolution: {integrity: sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==}
    engines: {node: '>=6.9.0'}
    dependencies:
      '@babel/helper-string-parser': 7.22.5
      '@babel/helper-validator-identifier': 7.22.5
      to-fast-properties: 2.0.0

  /@ctrl/tinycolor@3.6.0:
    resolution: {integrity: sha512-/Z3l6pXthq0JvMYdUFyX9j0MaCltlIn6mfh9jLyQwg5aPKxkyNa0PTHtU1AlFXLNk55ZuAeJRcpvq+tmLfKmaQ==}
    engines: {node: '>=10'}
    dev: false

  /@docsearch/css@3.5.1:
    resolution: {integrity: sha512-2Pu9HDg/uP/IT10rbQ+4OrTQuxIWdKVUEdcw9/w7kZJv9NeHS6skJx1xuRiFyoGKwAzcHXnLp7csE99sj+O1YA==}
    dev: true

  /@docsearch/js@3.5.1(@algolia/client-search@4.19.1)(search-insights@2.7.0):
    resolution: {integrity: sha512-EXi8de5njxgP6TV3N9ytnGRLG9zmBNTEZjR4VzwPcpPLbZxxTLG2gaFyJyKiFVQxHW/DPlMrDJA3qoRRGEkgZw==}
    dependencies:
      '@docsearch/react': 3.5.1(@algolia/client-search@4.19.1)(search-insights@2.7.0)
      preact: 10.16.0
    transitivePeerDependencies:
      - '@algolia/client-search'
      - '@types/react'
      - react
      - react-dom
      - search-insights
    dev: true

  /@docsearch/react@3.5.1(@algolia/client-search@4.19.1)(search-insights@2.7.0):
    resolution: {integrity: sha512-t5mEODdLzZq4PTFAm/dvqcvZFdPDMdfPE5rJS5SC8OUq9mPzxEy6b+9THIqNM9P0ocCb4UC5jqBrxKclnuIbzQ==}
    peerDependencies:
      '@types/react': '>= 16.8.0 < 19.0.0'
      react: '>= 16.8.0 < 19.0.0'
      react-dom: '>= 16.8.0 < 19.0.0'
    peerDependenciesMeta:
      '@types/react':
        optional: true
      react:
        optional: true
      react-dom:
        optional: true
    dependencies:
      '@algolia/autocomplete-core': 1.9.3(@algolia/client-search@4.19.1)(algoliasearch@4.19.1)(search-insights@2.7.0)
      '@algolia/autocomplete-preset-algolia': 1.9.3(@algolia/client-search@4.19.1)(algoliasearch@4.19.1)
      '@docsearch/css': 3.5.1
      algoliasearch: 4.19.1
    transitivePeerDependencies:
      - '@algolia/client-search'
      - search-insights
    dev: true

  /@element-plus/icons-vue@2.1.0(vue@3.3.2):
    resolution: {integrity: sha512-PSBn3elNoanENc1vnCfh+3WA9fimRC7n+fWkf3rE5jvv+aBohNHABC/KAR5KWPecxWxDTVT1ERpRbOMRcOV/vA==}
    peerDependencies:
      vue: ^3.2.0
    dependencies:
      vue: 3.3.2
    dev: false

  /@esbuild/android-arm64@0.18.17:
    resolution: {integrity: sha512-9np+YYdNDed5+Jgr1TdWBsozZ85U1Oa3xW0c7TWqH0y2aGghXtZsuT8nYRbzOMcl0bXZXjOGbksoTtVOlWrRZg==}
    engines: {node: '>=12'}
    cpu: [arm64]
    os: [android]
    requiresBuild: true
    dev: true
    optional: true

  /@esbuild/android-arm@0.18.17:
    resolution: {integrity: sha512-wHsmJG/dnL3OkpAcwbgoBTTMHVi4Uyou3F5mf58ZtmUyIKfcdA7TROav/6tCzET4A3QW2Q2FC+eFneMU+iyOxg==}
    engines: {node: '>=12'}
    cpu: [arm]
    os: [android]
    requiresBuild: true
    dev: true
    optional: true

  /@esbuild/android-x64@0.18.17:
    resolution: {integrity: sha512-O+FeWB/+xya0aLg23hHEM2E3hbfwZzjqumKMSIqcHbNvDa+dza2D0yLuymRBQQnC34CWrsJUXyH2MG5VnLd6uw==}
    engines: {node: '>=12'}
    cpu: [x64]
    os: [android]
    requiresBuild: true
    dev: true
    optional: true

  /@esbuild/darwin-arm64@0.18.17:
    resolution: {integrity: sha512-M9uJ9VSB1oli2BE/dJs3zVr9kcCBBsE883prage1NWz6pBS++1oNn/7soPNS3+1DGj0FrkSvnED4Bmlu1VAE9g==}
    engines: {node: '>=12'}
    cpu: [arm64]
    os: [darwin]
    requiresBuild: true
    dev: true
    optional: true

  /@esbuild/darwin-x64@0.18.17:
    resolution: {integrity: sha512-XDre+J5YeIJDMfp3n0279DFNrGCXlxOuGsWIkRb1NThMZ0BsrWXoTg23Jer7fEXQ9Ye5QjrvXpxnhzl3bHtk0g==}
    engines: {node: '>=12'}
    cpu: [x64]
    os: [darwin]
    requiresBuild: true
    dev: true
    optional: true

  /@esbuild/freebsd-arm64@0.18.17:
    resolution: {integrity: sha512-cjTzGa3QlNfERa0+ptykyxs5A6FEUQQF0MuilYXYBGdBxD3vxJcKnzDlhDCa1VAJCmAxed6mYhA2KaJIbtiNuQ==}
    engines: {node: '>=12'}
    cpu: [arm64]
    os: [freebsd]
    requiresBuild: true
    dev: true
    optional: true

  /@esbuild/freebsd-x64@0.18.17:
    resolution: {integrity: sha512-sOxEvR8d7V7Kw8QqzxWc7bFfnWnGdaFBut1dRUYtu+EIRXefBc/eIsiUiShnW0hM3FmQ5Zf27suDuHsKgZ5QrA==}
    engines: {node: '>=12'}
    cpu: [x64]
    os: [freebsd]
    requiresBuild: true
    dev: true
    optional: true

  /@esbuild/linux-arm64@0.18.17:
    resolution: {integrity: sha512-c9w3tE7qA3CYWjT+M3BMbwMt+0JYOp3vCMKgVBrCl1nwjAlOMYzEo+gG7QaZ9AtqZFj5MbUc885wuBBmu6aADQ==}
    engines: {node: '>=12'}
    cpu: [arm64]
    os: [linux]
    requiresBuild: true
    dev: true
    optional: true

  /@esbuild/linux-arm@0.18.17:
    resolution: {integrity: sha512-2d3Lw6wkwgSLC2fIvXKoMNGVaeY8qdN0IC3rfuVxJp89CRfA3e3VqWifGDfuakPmp90+ZirmTfye1n4ncjv2lg==}
    engines: {node: '>=12'}
    cpu: [arm]
    os: [linux]
    requiresBuild: true
    dev: true
    optional: true

  /@esbuild/linux-ia32@0.18.17:
    resolution: {integrity: sha512-1DS9F966pn5pPnqXYz16dQqWIB0dmDfAQZd6jSSpiT9eX1NzKh07J6VKR3AoXXXEk6CqZMojiVDSZi1SlmKVdg==}
    engines: {node: '>=12'}
    cpu: [ia32]
    os: [linux]
    requiresBuild: true
    dev: true
    optional: true

  /@esbuild/linux-loong64@0.18.17:
    resolution: {integrity: sha512-EvLsxCk6ZF0fpCB6w6eOI2Fc8KW5N6sHlIovNe8uOFObL2O+Mr0bflPHyHwLT6rwMg9r77WOAWb2FqCQrVnwFg==}
    engines: {node: '>=12'}
    cpu: [loong64]
    os: [linux]
    requiresBuild: true
    dev: true
    optional: true

  /@esbuild/linux-mips64el@0.18.17:
    resolution: {integrity: sha512-e0bIdHA5p6l+lwqTE36NAW5hHtw2tNRmHlGBygZC14QObsA3bD4C6sXLJjvnDIjSKhW1/0S3eDy+QmX/uZWEYQ==}
    engines: {node: '>=12'}
    cpu: [mips64el]
    os: [linux]
    requiresBuild: true
    dev: true
    optional: true

  /@esbuild/linux-ppc64@0.18.17:
    resolution: {integrity: sha512-BAAilJ0M5O2uMxHYGjFKn4nJKF6fNCdP1E0o5t5fvMYYzeIqy2JdAP88Az5LHt9qBoUa4tDaRpfWt21ep5/WqQ==}
    engines: {node: '>=12'}
    cpu: [ppc64]
    os: [linux]
    requiresBuild: true
    dev: true
    optional: true

  /@esbuild/linux-riscv64@0.18.17:
    resolution: {integrity: sha512-Wh/HW2MPnC3b8BqRSIme/9Zhab36PPH+3zam5pqGRH4pE+4xTrVLx2+XdGp6fVS3L2x+DrsIcsbMleex8fbE6g==}
    engines: {node: '>=12'}
    cpu: [riscv64]
    os: [linux]
    requiresBuild: true
    dev: true
    optional: true

  /@esbuild/linux-s390x@0.18.17:
    resolution: {integrity: sha512-j/34jAl3ul3PNcK3pfI0NSlBANduT2UO5kZ7FCaK33XFv3chDhICLY8wJJWIhiQ+YNdQ9dxqQctRg2bvrMlYgg==}
    engines: {node: '>=12'}
    cpu: [s390x]
    os: [linux]
    requiresBuild: true
    dev: true
    optional: true

  /@esbuild/linux-x64@0.18.17:
    resolution: {integrity: sha512-QM50vJ/y+8I60qEmFxMoxIx4de03pGo2HwxdBeFd4nMh364X6TIBZ6VQ5UQmPbQWUVWHWws5MmJXlHAXvJEmpQ==}
    engines: {node: '>=12'}
    cpu: [x64]
    os: [linux]
    requiresBuild: true
    dev: true
    optional: true

  /@esbuild/netbsd-x64@0.18.17:
    resolution: {integrity: sha512-/jGlhWR7Sj9JPZHzXyyMZ1RFMkNPjC6QIAan0sDOtIo2TYk3tZn5UDrkE0XgsTQCxWTTOcMPf9p6Rh2hXtl5TQ==}
    engines: {node: '>=12'}
    cpu: [x64]
    os: [netbsd]
    requiresBuild: true
    dev: true
    optional: true

  /@esbuild/openbsd-x64@0.18.17:
    resolution: {integrity: sha512-rSEeYaGgyGGf4qZM2NonMhMOP/5EHp4u9ehFiBrg7stH6BYEEjlkVREuDEcQ0LfIl53OXLxNbfuIj7mr5m29TA==}
    engines: {node: '>=12'}
    cpu: [x64]
    os: [openbsd]
    requiresBuild: true
    dev: true
    optional: true

  /@esbuild/sunos-x64@0.18.17:
    resolution: {integrity: sha512-Y7ZBbkLqlSgn4+zot4KUNYst0bFoO68tRgI6mY2FIM+b7ZbyNVtNbDP5y8qlu4/knZZ73fgJDlXID+ohY5zt5g==}
    engines: {node: '>=12'}
    cpu: [x64]
    os: [sunos]
    requiresBuild: true
    dev: true
    optional: true

  /@esbuild/win32-arm64@0.18.17:
    resolution: {integrity: sha512-bwPmTJsEQcbZk26oYpc4c/8PvTY3J5/QK8jM19DVlEsAB41M39aWovWoHtNm78sd6ip6prilxeHosPADXtEJFw==}
    engines: {node: '>=12'}
    cpu: [arm64]
    os: [win32]
    requiresBuild: true
    dev: true
    optional: true

  /@esbuild/win32-ia32@0.18.17:
    resolution: {integrity: sha512-H/XaPtPKli2MhW+3CQueo6Ni3Avggi6hP/YvgkEe1aSaxw+AeO8MFjq8DlgfTd9Iz4Yih3QCZI6YLMoyccnPRg==}
    engines: {node: '>=12'}
    cpu: [ia32]
    os: [win32]
    requiresBuild: true
    dev: true
    optional: true

  /@esbuild/win32-x64@0.18.17:
    resolution: {integrity: sha512-fGEb8f2BSA3CW7riJVurug65ACLuQAzKq0SSqkY2b2yHHH0MzDfbLyKIGzHwOI/gkHcxM/leuSW6D5w/LMNitA==}
    engines: {node: '>=12'}
    cpu: [x64]
    os: [win32]
    requiresBuild: true
    dev: true
    optional: true

  /@floating-ui/core@1.4.1:
    resolution: {integrity: sha512-jk3WqquEJRlcyu7997NtR5PibI+y5bi+LS3hPmguVClypenMsCY3CBa3LAQnozRCtCrYWSEtAdiskpamuJRFOQ==}
    dependencies:
      '@floating-ui/utils': 0.1.1
    dev: false

  /@floating-ui/dom@1.5.1:
    resolution: {integrity: sha512-KwvVcPSXg6mQygvA1TjbN/gh///36kKtllIF8SUm0qpFj8+rvYrpvlYdL1JoA71SHpDqgSSdGOSoQ0Mp3uY5aw==}
    dependencies:
      '@floating-ui/core': 1.4.1
      '@floating-ui/utils': 0.1.1
    dev: false

  /@floating-ui/utils@0.1.1:
    resolution: {integrity: sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw==}
    dev: false

  /@giscus/vue@2.3.0(vue@3.3.2):
    resolution: {integrity: sha512-ZZ1ZRqRwoHEfYpepSozU4HKjvCQ/4PjVt6P391/c0F4ly1i57OUup2h6Pd+l7dUnh9j7fbqUp6CfFk5+99S3Jw==}
    peerDependencies:
      vue: '>=3.2.0'
    dependencies:
      giscus: 1.3.0
      vue: 3.3.2
    dev: false

  /@jridgewell/gen-mapping@0.3.3:
    resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==}
    engines: {node: '>=6.0.0'}
    dependencies:
      '@jridgewell/set-array': 1.1.2
      '@jridgewell/sourcemap-codec': 1.4.15
      '@jridgewell/trace-mapping': 0.3.18
    dev: true

  /@jridgewell/resolve-uri@3.1.0:
    resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==}
    engines: {node: '>=6.0.0'}
    dev: true

  /@jridgewell/set-array@1.1.2:
    resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==}
    engines: {node: '>=6.0.0'}
    dev: true

  /@jridgewell/sourcemap-codec@1.4.14:
    resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==}
    dev: true

  /@jridgewell/sourcemap-codec@1.4.15:
    resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}

  /@jridgewell/trace-mapping@0.3.18:
    resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==}
    dependencies:
      '@jridgewell/resolve-uri': 3.1.0
      '@jridgewell/sourcemap-codec': 1.4.14
    dev: true

  /@lit-labs/ssr-dom-shim@1.1.1:
    resolution: {integrity: sha512-kXOeFbfCm4fFf2A3WwVEeQj55tMZa8c8/f9AKHMobQMkzNUfUj+antR3fRPaZJawsa1aZiP/Da3ndpZrwEe4rQ==}
    dev: false

  /@lit/reactive-element@1.6.3:
    resolution: {integrity: sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==}
    dependencies:
      '@lit-labs/ssr-dom-shim': 1.1.1
    dev: false

  /@nodelib/fs.scandir@2.1.5:
    resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
    engines: {node: '>= 8'}
    dependencies:
      '@nodelib/fs.stat': 2.0.5
      run-parallel: 1.2.0
    dev: true

  /@nodelib/fs.stat@2.0.5:
    resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
    engines: {node: '>= 8'}
    dev: true

  /@nodelib/fs.walk@1.2.8:
    resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
    engines: {node: '>= 8'}
    dependencies:
      '@nodelib/fs.scandir': 2.1.5
      fastq: 1.15.0
    dev: true

  /@sxzz/popperjs-es@2.11.7:
    resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==}
    dev: false

  /@types/fs-extra@11.0.1:
    resolution: {integrity: sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA==}
    dependencies:
      '@types/jsonfile': 6.1.1
      '@types/node': 20.4.5
    dev: true

  /@types/jsonfile@6.1.1:
    resolution: {integrity: sha512-GSgiRCVeapDN+3pqA35IkQwasaCh/0YFH5dEF6S88iDvEn901DjOeH3/QPY+XYP1DFzDZPvIvfeEgk+7br5png==}
    dependencies:
      '@types/node': 20.4.5
    dev: true

  /@types/linkify-it@3.0.2:
    resolution: {integrity: sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==}
    dev: true

  /@types/lodash-es@4.17.8:
    resolution: {integrity: sha512-euY3XQcZmIzSy7YH5+Unb3b2X12Wtk54YWINBvvGQ5SmMvwb11JQskGsfkH/5HXK77Kr8GF0wkVDIxzAisWtog==}
    dependencies:
      '@types/lodash': 4.14.196
    dev: false

  /@types/lodash@4.14.196:
    resolution: {integrity: sha512-22y3o88f4a94mKljsZcanlNWPzO0uBsBdzLAngf2tp533LzZcQzb6+eZPJ+vCTt+bqF2XnvT9gejTLsAcJAJyQ==}
    dev: false

  /@types/markdown-it@13.0.0:
    resolution: {integrity: sha512-mPTaUl5glYfzdJFeCsvhXQwZKdyszNAZcMm5ZTP5SfpTu+vIbog7J3z8Fa4x/Fzv5TB4R6OA/pHBYIYmkYOWGQ==}
    dependencies:
      '@types/linkify-it': 3.0.2
      '@types/mdurl': 1.0.2
    dev: true

  /@types/mdurl@1.0.2:
    resolution: {integrity: sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==}
    dev: true

  /@types/node@20.4.5:
    resolution: {integrity: sha512-rt40Nk13II9JwQBdeYqmbn2Q6IVTA5uPhvSO+JVqdXw/6/4glI6oR9ezty/A9Hg5u7JH4OmYmuQ+XvjKm0Datg==}
    dev: true

  /@types/trusted-types@2.0.3:
    resolution: {integrity: sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==}
    dev: false

  /@types/web-bluetooth@0.0.16:
    resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==}
    dev: false

  /@types/web-bluetooth@0.0.17:
    resolution: {integrity: sha512-4p9vcSmxAayx72yn70joFoL44c9MO/0+iVEBIQXe3v2h2SiAsEIo/G5v6ObFWvNKRFjbrVadNf9LqEEZeQPzdA==}
    dev: true

  /@vitejs/plugin-vue@4.2.3(vite@4.4.7)(vue@3.3.4):
    resolution: {integrity: sha512-R6JDUfiZbJA9cMiguQ7jxALsgiprjBeHL5ikpXfJCH62pPHtI+JdJ5xWj6Ev73yXSlYl86+blXn1kZHQ7uElxw==}
    engines: {node: ^14.18.0 || >=16.0.0}
    peerDependencies:
      vite: ^4.0.0
      vue: ^3.2.25
    dependencies:
      vite: 4.4.7(sass@1.64.1)
      vue: 3.3.4
    dev: true

  /@vue/compiler-core@3.3.2:
    resolution: {integrity: sha512-CKZWo1dzsQYTNTft7whzjL0HsrEpMfiK7pjZ2WFE3bC1NA7caUjWioHSK+49y/LK7Bsm4poJZzAMnvZMQ7OTeg==}
    dependencies:
      '@babel/parser': 7.22.7
      '@vue/shared': 3.3.2
      estree-walker: 2.0.2
      source-map-js: 1.0.2
    dev: false

  /@vue/compiler-core@3.3.4:
    resolution: {integrity: sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==}
    dependencies:
      '@babel/parser': 7.22.7
      '@vue/shared': 3.3.4
      estree-walker: 2.0.2
      source-map-js: 1.0.2
    dev: true

  /@vue/compiler-dom@3.3.2:
    resolution: {integrity: sha512-6gS3auANuKXLw0XH6QxkWqyPYPunziS2xb6VRenM3JY7gVfZcJvkCBHkb5RuNY1FCbBO3lkIi0CdXUCW1c7SXw==}
    dependencies:
      '@vue/compiler-core': 3.3.2
      '@vue/shared': 3.3.2
    dev: false

  /@vue/compiler-dom@3.3.4:
    resolution: {integrity: sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==}
    dependencies:
      '@vue/compiler-core': 3.3.4
      '@vue/shared': 3.3.4
    dev: true

  /@vue/compiler-sfc@3.3.2:
    resolution: {integrity: sha512-jG4jQy28H4BqzEKsQqqW65BZgmo3vzdLHTBjF+35RwtDdlFE+Fk1VWJYUnDMMqkFBo6Ye1ltSKVOMPgkzYj7SQ==}
    dependencies:
      '@babel/parser': 7.22.7
      '@vue/compiler-core': 3.3.2
      '@vue/compiler-dom': 3.3.2
      '@vue/compiler-ssr': 3.3.2
      '@vue/reactivity-transform': 3.3.2
      '@vue/shared': 3.3.2
      estree-walker: 2.0.2
      magic-string: 0.30.2
      postcss: 8.4.27
      source-map-js: 1.0.2
    dev: false

  /@vue/compiler-sfc@3.3.4:
    resolution: {integrity: sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==}
    dependencies:
      '@babel/parser': 7.22.7
      '@vue/compiler-core': 3.3.4
      '@vue/compiler-dom': 3.3.4
      '@vue/compiler-ssr': 3.3.4
      '@vue/reactivity-transform': 3.3.4
      '@vue/shared': 3.3.4
      estree-walker: 2.0.2
      magic-string: 0.30.2
      postcss: 8.4.27
      source-map-js: 1.0.2
    dev: true

  /@vue/compiler-ssr@3.3.2:
    resolution: {integrity: sha512-K8OfY5FQtZaSOJHHe8xhEfIfLrefL/Y9frv4k4NsyQL3+0lRKxr9QuJhfdBDjkl7Fhz8CzKh63mULvmOfx3l2w==}
    dependencies:
      '@vue/compiler-dom': 3.3.2
      '@vue/shared': 3.3.2
    dev: false

  /@vue/compiler-ssr@3.3.4:
    resolution: {integrity: sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==}
    dependencies:
      '@vue/compiler-dom': 3.3.4
      '@vue/shared': 3.3.4
    dev: true

  /@vue/devtools-api@6.5.0:
    resolution: {integrity: sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==}
    dev: true

  /@vue/reactivity-transform@3.3.2:
    resolution: {integrity: sha512-iu2WaQvlJHdnONrsyv4ibIEnSsuKF+aHFngGj/y1lwpHQtalpVhKg9wsKMoiKXS9zPNjG9mNKzJS9vudvjzvyg==}
    dependencies:
      '@babel/parser': 7.22.7
      '@vue/compiler-core': 3.3.2
      '@vue/shared': 3.3.2
      estree-walker: 2.0.2
      magic-string: 0.30.2
    dev: false

  /@vue/reactivity-transform@3.3.4:
    resolution: {integrity: sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==}
    dependencies:
      '@babel/parser': 7.22.7
      '@vue/compiler-core': 3.3.4
      '@vue/shared': 3.3.4
      estree-walker: 2.0.2
      magic-string: 0.30.2
    dev: true

  /@vue/reactivity@3.3.2:
    resolution: {integrity: sha512-yX8C4uTgg2Tdj+512EEMnMKbLveoITl7YdQX35AYgx8vBvQGszKiiCN46g4RY6/deeo/5DLbeUUGxCq1qWMf5g==}
    dependencies:
      '@vue/shared': 3.3.2
    dev: false

  /@vue/reactivity@3.3.4:
    resolution: {integrity: sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==}
    dependencies:
      '@vue/shared': 3.3.4
    dev: true

  /@vue/runtime-core@3.3.2:
    resolution: {integrity: sha512-qSl95qj0BvKfcsO+hICqFEoLhJn6++HtsPxmTkkadFbuhe3uQfJ8HmQwvEr7xbxBd2rcJB6XOJg7nWAn/ymC5A==}
    dependencies:
      '@vue/reactivity': 3.3.2
      '@vue/shared': 3.3.2
    dev: false

  /@vue/runtime-core@3.3.4:
    resolution: {integrity: sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==}
    dependencies:
      '@vue/reactivity': 3.3.4
      '@vue/shared': 3.3.4
    dev: true

  /@vue/runtime-dom@3.3.2:
    resolution: {integrity: sha512-+drStsJT+0mtgHdarT7cXZReCcTFfm6ptxMrz0kAW5hms6UNBd8Q1pi4JKlncAhu+Ld/TevsSp7pqAZxBBoGng==}
    dependencies:
      '@vue/runtime-core': 3.3.2
      '@vue/shared': 3.3.2
      csstype: 3.1.2
    dev: false

  /@vue/runtime-dom@3.3.4:
    resolution: {integrity: sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==}
    dependencies:
      '@vue/runtime-core': 3.3.4
      '@vue/shared': 3.3.4
      csstype: 3.1.2
    dev: true

  /@vue/server-renderer@3.3.2(vue@3.3.2):
    resolution: {integrity: sha512-QCwh6OGwJg6GDLE0fbQhRTR6tnU+XDJ1iCsTYHXBiezCXAhqMygFRij7BiLF4ytvvHcg5kX9joX5R5vP85++wg==}
    peerDependencies:
      vue: 3.3.2
    dependencies:
      '@vue/compiler-ssr': 3.3.2
      '@vue/shared': 3.3.2
      vue: 3.3.2
    dev: false

  /@vue/server-renderer@3.3.4(vue@3.3.4):
    resolution: {integrity: sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==}
    peerDependencies:
      vue: 3.3.4
    dependencies:
      '@vue/compiler-ssr': 3.3.4
      '@vue/shared': 3.3.4
      vue: 3.3.4
    dev: true

  /@vue/shared@3.3.2:
    resolution: {integrity: sha512-0rFu3h8JbclbnvvKrs7Fe5FNGV9/5X2rPD7KmOzhLSUAiQH5//Hq437Gv0fR5Mev3u/nbtvmLl8XgwCU20/ZfQ==}
    dev: false

  /@vue/shared@3.3.4:
    resolution: {integrity: sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==}
    dev: true

  /@vueuse/core@10.3.0(vue@3.3.4):
    resolution: {integrity: sha512-BEM5yxcFKb5btFjTSAFjTu5jmwoW66fyV9uJIP4wUXXU8aR5Hl44gndaaXp7dC5HSObmgbnR2RN+Un1p68Mf5Q==}
    dependencies:
      '@types/web-bluetooth': 0.0.17
      '@vueuse/metadata': 10.3.0
      '@vueuse/shared': 10.3.0(vue@3.3.4)
      vue-demi: 0.14.5(vue@3.3.4)
    transitivePeerDependencies:
      - '@vue/composition-api'
      - vue
    dev: true

  /@vueuse/core@9.13.0(vue@3.3.2):
    resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==}
    dependencies:
      '@types/web-bluetooth': 0.0.16
      '@vueuse/metadata': 9.13.0
      '@vueuse/shared': 9.13.0(vue@3.3.2)
      vue-demi: 0.14.5(vue@3.3.2)
    transitivePeerDependencies:
      - '@vue/composition-api'
      - vue
    dev: false

  /@vueuse/integrations@10.3.0(focus-trap@7.5.2)(vue@3.3.4):
    resolution: {integrity: sha512-Jgiv7oFyIgC6BxmDtiyG/fxyGysIds00YaY7sefwbhCZ2/tjEx1W/1WcsISSJPNI30in28+HC2J4uuU8184ekg==}
    peerDependencies:
      async-validator: '*'
      axios: '*'
      change-case: '*'
      drauu: '*'
      focus-trap: '*'
      fuse.js: '*'
      idb-keyval: '*'
      jwt-decode: '*'
      nprogress: '*'
      qrcode: '*'
      sortablejs: '*'
      universal-cookie: '*'
    peerDependenciesMeta:
      async-validator:
        optional: true
      axios:
        optional: true
      change-case:
        optional: true
      drauu:
        optional: true
      focus-trap:
        optional: true
      fuse.js:
        optional: true
      idb-keyval:
        optional: true
      jwt-decode:
        optional: true
      nprogress:
        optional: true
      qrcode:
        optional: true
      sortablejs:
        optional: true
      universal-cookie:
        optional: true
    dependencies:
      '@vueuse/core': 10.3.0(vue@3.3.4)
      '@vueuse/shared': 10.3.0(vue@3.3.4)
      focus-trap: 7.5.2
      vue-demi: 0.14.5(vue@3.3.4)
    transitivePeerDependencies:
      - '@vue/composition-api'
      - vue
    dev: true

  /@vueuse/metadata@10.3.0:
    resolution: {integrity: sha512-Ema3YhNOa4swDsV0V7CEY5JXvK19JI/o1szFO1iWxdFg3vhdFtCtSTP26PCvbUpnUtNHBY2wx5y3WDXND5Pvnw==}
    dev: true

  /@vueuse/metadata@9.13.0:
    resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==}
    dev: false

  /@vueuse/shared@10.3.0(vue@3.3.4):
    resolution: {integrity: sha512-kGqCTEuFPMK4+fNWy6dUOiYmxGcUbtznMwBZLC1PubidF4VZY05B+Oht7Jh7/6x4VOWGpvu3R37WHi81cKpiqg==}
    dependencies:
      vue-demi: 0.14.5(vue@3.3.4)
    transitivePeerDependencies:
      - '@vue/composition-api'
      - vue
    dev: true

  /@vueuse/shared@9.13.0(vue@3.3.2):
    resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==}
    dependencies:
      vue-demi: 0.14.5(vue@3.3.2)
    transitivePeerDependencies:
      - '@vue/composition-api'
      - vue
    dev: false

  /aggregate-error@3.1.0:
    resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
    engines: {node: '>=8'}
    dependencies:
      clean-stack: 2.2.0
      indent-string: 4.0.0
    dev: true

  /algoliasearch@4.19.1:
    resolution: {integrity: sha512-IJF5b93b2MgAzcE/tuzW0yOPnuUyRgGAtaPv5UUywXM8kzqfdwZTO4sPJBzoGz1eOy6H9uEchsJsBFTELZSu+g==}
    dependencies:
      '@algolia/cache-browser-local-storage': 4.19.1
      '@algolia/cache-common': 4.19.1
      '@algolia/cache-in-memory': 4.19.1
      '@algolia/client-account': 4.19.1
      '@algolia/client-analytics': 4.19.1
      '@algolia/client-common': 4.19.1
      '@algolia/client-personalization': 4.19.1
      '@algolia/client-search': 4.19.1
      '@algolia/logger-common': 4.19.1
      '@algolia/logger-console': 4.19.1
      '@algolia/requester-browser-xhr': 4.19.1
      '@algolia/requester-common': 4.19.1
      '@algolia/requester-node-http': 4.19.1
      '@algolia/transporter': 4.19.1
    dev: true

  /ansi-colors@4.1.3:
    resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
    engines: {node: '>=6'}
    dev: true

  /ansi-escapes@4.3.2:
    resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==}
    engines: {node: '>=8'}
    dependencies:
      type-fest: 0.21.3
    dev: true

  /ansi-regex@5.0.1:
    resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
    engines: {node: '>=8'}
    dev: true

  /ansi-regex@6.0.1:
    resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==}
    engines: {node: '>=12'}
    dev: true

  /ansi-sequence-parser@1.1.1:
    resolution: {integrity: sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==}
    dev: true

  /ansi-styles@4.3.0:
    resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
    engines: {node: '>=8'}
    dependencies:
      color-convert: 2.0.1
    dev: true

  /ansi-styles@6.2.1:
    resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
    engines: {node: '>=12'}
    dev: true

  /any-promise@1.3.0:
    resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
    dev: true

  /anymatch@3.1.3:
    resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
    engines: {node: '>= 8'}
    dependencies:
      normalize-path: 3.0.0
      picomatch: 2.3.1
    dev: true

  /arg@5.0.2:
    resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
    dev: true

  /argparse@1.0.10:
    resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
    dependencies:
      sprintf-js: 1.0.3
    dev: true

  /argparse@2.0.1:
    resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
    dev: true

  /array-union@2.1.0:
    resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
    engines: {node: '>=8'}
    dev: true

  /astral-regex@2.0.0:
    resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==}
    engines: {node: '>=8'}
    dev: true

  /async-validator@4.2.5:
    resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==}
    dev: false

  /autoprefixer@10.4.14(postcss@8.4.27):
    resolution: {integrity: sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==}
    engines: {node: ^10 || ^12 || >=14}
    hasBin: true
    peerDependencies:
      postcss: ^8.1.0
    dependencies:
      browserslist: 4.21.9
      caniuse-lite: 1.0.30001517
      fraction.js: 4.2.0
      normalize-range: 0.1.2
      picocolors: 1.0.0
      postcss: 8.4.27
      postcss-value-parser: 4.2.0
    dev: true

  /balanced-match@1.0.2:
    resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
    dev: true

  /binary-extensions@2.2.0:
    resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
    engines: {node: '>=8'}
    dev: true

  /body-scroll-lock@4.0.0-beta.0:
    resolution: {integrity: sha512-a7tP5+0Mw3YlUJcGAKUqIBkYYGlYxk2fnCasq/FUph1hadxlTRjF+gAcZksxANnaMnALjxEddmSi/H3OR8ugcQ==}
    dev: true

  /boolbase@1.0.0:
    resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
    dev: true

  /brace-expansion@1.1.11:
    resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
    dependencies:
      balanced-match: 1.0.2
      concat-map: 0.0.1
    dev: true

  /braces@3.0.2:
    resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
    engines: {node: '>=8'}
    dependencies:
      fill-range: 7.0.1
    dev: true

  /browserslist@4.21.9:
    resolution: {integrity: sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==}
    engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
    hasBin: true
    dependencies:
      caniuse-lite: 1.0.30001517
      electron-to-chromium: 1.4.477
      node-releases: 2.0.13
      update-browserslist-db: 1.0.11(browserslist@4.21.9)
    dev: true

  /camelcase-css@2.0.1:
    resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
    engines: {node: '>= 6'}
    dev: true

  /caniuse-lite@1.0.30001517:
    resolution: {integrity: sha512-Vdhm5S11DaFVLlyiKu4hiUTkpZu+y1KA/rZZqVQfOD5YdDT/eQKlkt7NaE0WGOFgX32diqt9MiP9CAiFeRklaA==}
    dev: true

  /chalk@5.2.0:
    resolution: {integrity: sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==}
    engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
    dev: true

  /cheerio-select@1.6.0:
    resolution: {integrity: sha512-eq0GdBvxVFbqWgmCm7M3XGs1I8oLy/nExUnh6oLqmBditPO9AqQJrkslDpMun/hZ0yyTs8L0m85OHp4ho6Qm9g==}
    dependencies:
      css-select: 4.3.0
      css-what: 6.1.0
      domelementtype: 2.3.0
      domhandler: 4.3.1
      domutils: 2.8.0
    dev: true

  /cheerio@1.0.0-rc.10:
    resolution: {integrity: sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw==}
    engines: {node: '>= 6'}
    dependencies:
      cheerio-select: 1.6.0
      dom-serializer: 1.4.1
      domhandler: 4.3.1
      htmlparser2: 6.1.0
      parse5: 6.0.1
      parse5-htmlparser2-tree-adapter: 6.0.1
      tslib: 2.6.1
    dev: true

  /chokidar@3.5.3:
    resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
    engines: {node: '>= 8.10.0'}
    dependencies:
      anymatch: 3.1.3
      braces: 3.0.2
      glob-parent: 5.1.2
      is-binary-path: 2.1.0
      is-glob: 4.0.3
      normalize-path: 3.0.0
      readdirp: 3.6.0
    optionalDependencies:
      fsevents: 2.3.2
    dev: true

  /clean-stack@2.2.0:
    resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
    engines: {node: '>=6'}
    dev: true

  /cli-cursor@3.1.0:
    resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
    engines: {node: '>=8'}
    dependencies:
      restore-cursor: 3.1.0
    dev: true

  /cli-truncate@2.1.0:
    resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==}
    engines: {node: '>=8'}
    dependencies:
      slice-ansi: 3.0.0
      string-width: 4.2.3
    dev: true

  /cli-truncate@3.1.0:
    resolution: {integrity: sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==}
    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
    dependencies:
      slice-ansi: 5.0.0
      string-width: 5.1.2
    dev: true

  /color-convert@2.0.1:
    resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
    engines: {node: '>=7.0.0'}
    dependencies:
      color-name: 1.1.4
    dev: true

  /color-name@1.1.4:
    resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
    dev: true

  /colorette@2.0.20:
    resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
    dev: true

  /commander@10.0.1:
    resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
    engines: {node: '>=14'}
    dev: true

  /commander@4.1.1:
    resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
    engines: {node: '>= 6'}
    dev: true

  /commander@6.2.1:
    resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==}
    engines: {node: '>= 6'}
    dev: true

  /commander@9.2.0:
    resolution: {integrity: sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w==}
    engines: {node: ^12.20.0 || >=14}
    dev: true

  /concat-map@0.0.1:
    resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
    dev: true

  /cross-spawn@7.0.3:
    resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
    engines: {node: '>= 8'}
    dependencies:
      path-key: 3.1.1
      shebang-command: 2.0.0
      which: 2.0.2
    dev: true

  /css-select@4.3.0:
    resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==}
    dependencies:
      boolbase: 1.0.0
      css-what: 6.1.0
      domhandler: 4.3.1
      domutils: 2.8.0
      nth-check: 2.1.1
    dev: true

  /css-what@6.1.0:
    resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
    engines: {node: '>= 6'}
    dev: true

  /cssesc@3.0.0:
    resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
    engines: {node: '>=4'}
    hasBin: true
    dev: true

  /csstype@3.1.2:
    resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}

  /dayjs@1.11.9:
    resolution: {integrity: sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==}
    dev: false

  /debug@4.3.4:
    resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
    engines: {node: '>=6.0'}
    peerDependencies:
      supports-color: '*'
    peerDependenciesMeta:
      supports-color:
        optional: true
    dependencies:
      ms: 2.1.2
    dev: true

  /didyoumean@1.2.2:
    resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
    dev: true

  /dir-glob@3.0.1:
    resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
    engines: {node: '>=8'}
    dependencies:
      path-type: 4.0.0
    dev: true

  /dlv@1.1.3:
    resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
    dev: true

  /dom-serializer@1.4.1:
    resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==}
    dependencies:
      domelementtype: 2.3.0
      domhandler: 4.3.1
      entities: 2.2.0
    dev: true

  /domelementtype@2.3.0:
    resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
    dev: true

  /domhandler@3.3.0:
    resolution: {integrity: sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==}
    engines: {node: '>= 4'}
    dependencies:
      domelementtype: 2.3.0
    dev: true

  /domhandler@4.3.1:
    resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==}
    engines: {node: '>= 4'}
    dependencies:
      domelementtype: 2.3.0
    dev: true

  /domutils@2.8.0:
    resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==}
    dependencies:
      dom-serializer: 1.4.1
      domelementtype: 2.3.0
      domhandler: 4.3.1
    dev: true

  /eastasianwidth@0.2.0:
    resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
    dev: true

  /electron-to-chromium@1.4.477:
    resolution: {integrity: sha512-shUVy6Eawp33dFBFIoYbIwLHrX0IZ857AlH9ug2o4rvbWmpaCUdBpQ5Zw39HRrfzAFm4APJE9V+E2A/WB0YqJw==}
    dev: true

  /element-plus@2.3.8(vue@3.3.2):
    resolution: {integrity: sha512-yHQR0/tG2LvPkpGUt7Te/hPmP2XW/BytBNUbx+EFO54VnGCOE3upmQcVffNp1PLgwg9sthYDXontUWpnpmLPJw==}
    peerDependencies:
      vue: ^3.2.0
    dependencies:
      '@ctrl/tinycolor': 3.6.0
      '@element-plus/icons-vue': 2.1.0(vue@3.3.2)
      '@floating-ui/dom': 1.5.1
      '@popperjs/core': /@sxzz/popperjs-es@2.11.7
      '@types/lodash': 4.14.196
      '@types/lodash-es': 4.17.8
      '@vueuse/core': 9.13.0(vue@3.3.2)
      async-validator: 4.2.5
      dayjs: 1.11.9
      escape-html: 1.0.3
      lodash: 4.17.21
      lodash-es: 4.17.21
      lodash-unified: 1.0.3(@types/lodash-es@4.17.8)(lodash-es@4.17.21)(lodash@4.17.21)
      memoize-one: 6.0.0
      normalize-wheel-es: 1.2.0
      vue: 3.3.2
    transitivePeerDependencies:
      - '@vue/composition-api'
    dev: false

  /emoji-regex@8.0.0:
    resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
    dev: true

  /emoji-regex@9.2.2:
    resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
    dev: true

  /entities@2.2.0:
    resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==}
    dev: true

  /entities@3.0.1:
    resolution: {integrity: sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==}
    engines: {node: '>=0.12'}
    dev: true

  /esbuild@0.18.17:
    resolution: {integrity: sha512-1GJtYnUxsJreHYA0Y+iQz2UEykonY66HNWOb0yXYZi9/kNrORUEHVg87eQsCtqh59PEJ5YVZJO98JHznMJSWjg==}
    engines: {node: '>=12'}
    hasBin: true
    requiresBuild: true
    optionalDependencies:
      '@esbuild/android-arm': 0.18.17
      '@esbuild/android-arm64': 0.18.17
      '@esbuild/android-x64': 0.18.17
      '@esbuild/darwin-arm64': 0.18.17
      '@esbuild/darwin-x64': 0.18.17
      '@esbuild/freebsd-arm64': 0.18.17
      '@esbuild/freebsd-x64': 0.18.17
      '@esbuild/linux-arm': 0.18.17
      '@esbuild/linux-arm64': 0.18.17
      '@esbuild/linux-ia32': 0.18.17
      '@esbuild/linux-loong64': 0.18.17
      '@esbuild/linux-mips64el': 0.18.17
      '@esbuild/linux-ppc64': 0.18.17
      '@esbuild/linux-riscv64': 0.18.17
      '@esbuild/linux-s390x': 0.18.17
      '@esbuild/linux-x64': 0.18.17
      '@esbuild/netbsd-x64': 0.18.17
      '@esbuild/openbsd-x64': 0.18.17
      '@esbuild/sunos-x64': 0.18.17
      '@esbuild/win32-arm64': 0.18.17
      '@esbuild/win32-ia32': 0.18.17
      '@esbuild/win32-x64': 0.18.17
    dev: true

  /escalade@3.1.1:
    resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
    engines: {node: '>=6'}
    dev: true

  /escape-goat@3.0.0:
    resolution: {integrity: sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==}
    engines: {node: '>=10'}
    dev: true

  /escape-html@1.0.3:
    resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
    dev: false

  /esm@3.2.25:
    resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==}
    engines: {node: '>=6'}
    dev: true

  /esprima@4.0.1:
    resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
    engines: {node: '>=4'}
    hasBin: true
    dev: true

  /estree-walker@2.0.2:
    resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}

  /execa@7.2.0:
    resolution: {integrity: sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==}
    engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0}
    dependencies:
      cross-spawn: 7.0.3
      get-stream: 6.0.1
      human-signals: 4.3.1
      is-stream: 3.0.0
      merge-stream: 2.0.0
      npm-run-path: 5.1.0
      onetime: 6.0.0
      signal-exit: 3.0.7
      strip-final-newline: 3.0.0
    dev: true

  /extend-shallow@2.0.1:
    resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==}
    engines: {node: '>=0.10.0'}
    dependencies:
      is-extendable: 0.1.1
    dev: true

  /fast-glob@3.3.1:
    resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==}
    engines: {node: '>=8.6.0'}
    dependencies:
      '@nodelib/fs.stat': 2.0.5
      '@nodelib/fs.walk': 1.2.8
      glob-parent: 5.1.2
      merge2: 1.4.1
      micromatch: 4.0.5
    dev: true

  /fastq@1.15.0:
    resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==}
    dependencies:
      reusify: 1.0.4
    dev: true

  /feed@4.2.2:
    resolution: {integrity: sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==}
    engines: {node: '>=0.4.0'}
    dependencies:
      xml-js: 1.6.11
    dev: true

  /fill-range@7.0.1:
    resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
    engines: {node: '>=8'}
    dependencies:
      to-regex-range: 5.0.1
    dev: true

  /focus-trap@7.5.2:
    resolution: {integrity: sha512-p6vGNNWLDGwJCiEjkSK6oERj/hEyI9ITsSwIUICBoKLlWiTWXJRfQibCwcoi50rTZdbi87qDtUlMCmQwsGSgPw==}
    dependencies:
      tabbable: 6.2.0
    dev: true

  /fraction.js@4.2.0:
    resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==}
    dev: true

  /fs-extra@11.1.1:
    resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==}
    engines: {node: '>=14.14'}
    dependencies:
      graceful-fs: 4.2.11
      jsonfile: 6.1.0
      universalify: 2.0.0
    dev: true

  /fs.realpath@1.0.0:
    resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
    dev: true

  /fsevents@2.3.2:
    resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
    engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
    os: [darwin]
    requiresBuild: true
    dev: true
    optional: true

  /function-bind@1.1.1:
    resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
    dev: true

  /get-stream@6.0.1:
    resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
    engines: {node: '>=10'}
    dev: true

  /giscus@1.3.0:
    resolution: {integrity: sha512-A3tVLgSmpnh2sX9uGjo9MbzmTTEJirSyFUPRvkipvy37y9rhxUYDoh9kO37QVrP7Sc7QuJ+gihB6apkO0yDyTw==}
    dependencies:
      lit: 2.8.0
    dev: false

  /glob-parent@5.1.2:
    resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
    engines: {node: '>= 6'}
    dependencies:
      is-glob: 4.0.3
    dev: true

  /glob-parent@6.0.2:
    resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
    engines: {node: '>=10.13.0'}
    dependencies:
      is-glob: 4.0.3
    dev: true

  /glob@7.1.6:
    resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==}
    dependencies:
      fs.realpath: 1.0.0
      inflight: 1.0.6
      inherits: 2.0.4
      minimatch: 3.1.2
      once: 1.4.0
      path-is-absolute: 1.0.1
    dev: true

  /globby@11.0.4:
    resolution: {integrity: sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==}
    engines: {node: '>=10'}
    dependencies:
      array-union: 2.1.0
      dir-glob: 3.0.1
      fast-glob: 3.3.1
      ignore: 5.2.4
      merge2: 1.4.1
      slash: 3.0.0
    dev: true

  /graceful-fs@4.2.11:
    resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
    dev: true

  /gray-matter@4.0.3:
    resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==}
    engines: {node: '>=6.0'}
    dependencies:
      js-yaml: 3.14.1
      kind-of: 6.0.3
      section-matter: 1.0.0
      strip-bom-string: 1.0.0
    dev: true

  /has@1.0.3:
    resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==}
    engines: {node: '>= 0.4.0'}
    dependencies:
      function-bind: 1.1.1
    dev: true

  /hls.js@1.4.10:
    resolution: {integrity: sha512-wAVSj4Fm2MqOHy5+BlYnlKxXvJlv5IuZHjlzHu18QmjRzSDFQiUDWdHs5+NsFMQrgKEBwuWDcyvaMC9dUzJ5Uw==}
    dev: false

  /htmlparser2@5.0.1:
    resolution: {integrity: sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==}
    dependencies:
      domelementtype: 2.3.0
      domhandler: 3.3.0
      domutils: 2.8.0
      entities: 2.2.0
    dev: true

  /htmlparser2@6.1.0:
    resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==}
    dependencies:
      domelementtype: 2.3.0
      domhandler: 4.3.1
      domutils: 2.8.0
      entities: 2.2.0
    dev: true

  /human-signals@4.3.1:
    resolution: {integrity: sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==}
    engines: {node: '>=14.18.0'}
    dev: true

  /husky@8.0.3:
    resolution: {integrity: sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==}
    engines: {node: '>=14'}
    hasBin: true
    dev: true

  /ignore@5.2.4:
    resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
    engines: {node: '>= 4'}
    dev: true

  /immutable@4.3.1:
    resolution: {integrity: sha512-lj9cnmB/kVS0QHsJnYKD1uo3o39nrbKxszjnqS9Fr6NB7bZzW45U6WSGBPKXDL/CvDKqDNPA4r3DoDQ8GTxo2A==}
    dev: true

  /indent-string@4.0.0:
    resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
    engines: {node: '>=8'}
    dev: true

  /inflight@1.0.6:
    resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
    dependencies:
      once: 1.4.0
      wrappy: 1.0.2
    dev: true

  /inherits@2.0.4:
    resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
    dev: true

  /is-binary-path@2.1.0:
    resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
    engines: {node: '>=8'}
    dependencies:
      binary-extensions: 2.2.0
    dev: true

  /is-core-module@2.12.1:
    resolution: {integrity: sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==}
    dependencies:
      has: 1.0.3
    dev: true

  /is-extendable@0.1.1:
    resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==}
    engines: {node: '>=0.10.0'}
    dev: true

  /is-extglob@2.1.1:
    resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
    engines: {node: '>=0.10.0'}
    dev: true

  /is-fullwidth-code-point@3.0.0:
    resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
    engines: {node: '>=8'}
    dev: true

  /is-fullwidth-code-point@4.0.0:
    resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==}
    engines: {node: '>=12'}
    dev: true

  /is-glob@4.0.3:
    resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
    engines: {node: '>=0.10.0'}
    dependencies:
      is-extglob: 2.1.1
    dev: true

  /is-number@7.0.0:
    resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
    engines: {node: '>=0.12.0'}
    dev: true

  /is-stream@3.0.0:
    resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
    dev: true

  /isexe@2.0.0:
    resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
    dev: true

  /jiti@1.19.1:
    resolution: {integrity: sha512-oVhqoRDaBXf7sjkll95LHVS6Myyyb1zaunVwk4Z0+WPSW4gjS0pl01zYKHScTuyEhQsFxV5L4DR5r+YqSyqyyg==}
    hasBin: true
    dev: true

  /js-yaml@3.14.1:
    resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==}
    hasBin: true
    dependencies:
      argparse: 1.0.10
      esprima: 4.0.1
    dev: true

  /jsonc-parser@3.2.0:
    resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
    dev: true

  /jsonfile@6.1.0:
    resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
    dependencies:
      universalify: 2.0.0
    optionalDependencies:
      graceful-fs: 4.2.11
    dev: true

  /juice@8.1.0:
    resolution: {integrity: sha512-FLzurJrx5Iv1e7CfBSZH68dC04EEvXvvVvPYB7Vx1WAuhCp1ZPIMtqxc+WTWxVkpTIC2Ach/GAv0rQbtGf6YMA==}
    engines: {node: '>=10.0.0'}
    hasBin: true
    dependencies:
      cheerio: 1.0.0-rc.10
      commander: 6.2.1
      mensch: 0.3.4
      slick: 1.12.2
      web-resource-inliner: 6.0.1
    transitivePeerDependencies:
      - encoding
    dev: true

  /kind-of@6.0.3:
    resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
    engines: {node: '>=0.10.0'}
    dev: true

  /lilconfig@2.1.0:
    resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
    engines: {node: '>=10'}
    dev: true

  /lines-and-columns@1.2.4:
    resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
    dev: true

  /linkify-it@4.0.1:
    resolution: {integrity: sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==}
    dependencies:
      uc.micro: 1.0.6
    dev: true

  /lint-staged@13.2.3:
    resolution: {integrity: sha512-zVVEXLuQIhr1Y7R7YAWx4TZLdvuzk7DnmrsTNL0fax6Z3jrpFcas+vKbzxhhvp6TA55m1SQuWkpzI1qbfDZbAg==}
    engines: {node: ^14.13.1 || >=16.0.0}
    hasBin: true
    dependencies:
      chalk: 5.2.0
      cli-truncate: 3.1.0
      commander: 10.0.1
      debug: 4.3.4
      execa: 7.2.0
      lilconfig: 2.1.0
      listr2: 5.0.8
      micromatch: 4.0.5
      normalize-path: 3.0.0
      object-inspect: 1.12.3
      pidtree: 0.6.0
      string-argv: 0.3.2
      yaml: 2.3.1
    transitivePeerDependencies:
      - enquirer
      - supports-color
    dev: true

  /listr2@5.0.8:
    resolution: {integrity: sha512-mC73LitKHj9w6v30nLNGPetZIlfpUniNSsxxrbaPcWOjDb92SHPzJPi/t+v1YC/lxKz/AJ9egOjww0qUuFxBpA==}
    engines: {node: ^14.13.1 || >=16.0.0}
    peerDependencies:
      enquirer: '>= 2.3.0 < 3'
    peerDependenciesMeta:
      enquirer:
        optional: true
    dependencies:
      cli-truncate: 2.1.0
      colorette: 2.0.20
      log-update: 4.0.0
      p-map: 4.0.0
      rfdc: 1.3.0
      rxjs: 7.8.1
      through: 2.3.8
      wrap-ansi: 7.0.0
    dev: true

  /lit-element@3.3.3:
    resolution: {integrity: sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==}
    dependencies:
      '@lit-labs/ssr-dom-shim': 1.1.1
      '@lit/reactive-element': 1.6.3
      lit-html: 2.8.0
    dev: false

  /lit-html@2.8.0:
    resolution: {integrity: sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==}
    dependencies:
      '@types/trusted-types': 2.0.3
    dev: false

  /lit@2.8.0:
    resolution: {integrity: sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==}
    dependencies:
      '@lit/reactive-element': 1.6.3
      lit-element: 3.3.3
      lit-html: 2.8.0
    dev: false

  /lodash-es@4.17.21:
    resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
    dev: false

  /lodash-unified@1.0.3(@types/lodash-es@4.17.8)(lodash-es@4.17.21)(lodash@4.17.21):
    resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==}
    peerDependencies:
      '@types/lodash-es': '*'
      lodash: '*'
      lodash-es: '*'
    dependencies:
      '@types/lodash-es': 4.17.8
      lodash: 4.17.21
      lodash-es: 4.17.21
    dev: false

  /lodash@4.17.21:
    resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
    dev: false

  /log-update@4.0.0:
    resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==}
    engines: {node: '>=10'}
    dependencies:
      ansi-escapes: 4.3.2
      cli-cursor: 3.1.0
      slice-ansi: 4.0.0
      wrap-ansi: 6.2.0
    dev: true

  /magic-string@0.30.2:
    resolution: {integrity: sha512-lNZdu7pewtq/ZvWUp9Wpf/x7WzMTsR26TWV03BRZrXFsv+BI6dy8RAiKgm1uM/kyR0rCfUcqvOlXKG66KhIGug==}
    engines: {node: '>=12'}
    dependencies:
      '@jridgewell/sourcemap-codec': 1.4.15

  /mark.js@8.11.1:
    resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==}
    dev: true

  /markdown-it-mathjax3@4.3.2:
    resolution: {integrity: sha512-TX3GW5NjmupgFtMJGRauioMbbkGsOXAAt1DZ/rzzYmTHqzkO1rNAdiMD4NiruurToPApn2kYy76x02QN26qr2w==}
    dependencies:
      juice: 8.1.0
      mathjax-full: 3.2.2
    transitivePeerDependencies:
      - encoding
    dev: true

  /markdown-it@13.0.1:
    resolution: {integrity: sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==}
    hasBin: true
    dependencies:
      argparse: 2.0.1
      entities: 3.0.1
      linkify-it: 4.0.1
      mdurl: 1.0.1
      uc.micro: 1.0.6
    dev: true

  /mathjax-full@3.2.2:
    resolution: {integrity: sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w==}
    dependencies:
      esm: 3.2.25
      mhchemparser: 4.2.1
      mj-context-menu: 0.6.1
      speech-rule-engine: 4.0.7
    dev: true

  /mdurl@1.0.1:
    resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==}
    dev: true

  /memoize-one@6.0.0:
    resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
    dev: false

  /mensch@0.3.4:
    resolution: {integrity: sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==}
    dev: true

  /merge-stream@2.0.0:
    resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
    dev: true

  /merge2@1.4.1:
    resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
    engines: {node: '>= 8'}
    dev: true

  /mhchemparser@4.2.1:
    resolution: {integrity: sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==}
    dev: true

  /micromatch@4.0.5:
    resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==}
    engines: {node: '>=8.6'}
    dependencies:
      braces: 3.0.2
      picomatch: 2.3.1
    dev: true

  /mime@2.6.0:
    resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==}
    engines: {node: '>=4.0.0'}
    hasBin: true
    dev: true

  /mimic-fn@2.1.0:
    resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
    engines: {node: '>=6'}
    dev: true

  /mimic-fn@4.0.0:
    resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
    engines: {node: '>=12'}
    dev: true

  /minimatch@3.1.2:
    resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
    dependencies:
      brace-expansion: 1.1.11
    dev: true

  /minisearch@6.1.0:
    resolution: {integrity: sha512-PNxA/X8pWk+TiqPbsoIYH0GQ5Di7m6326/lwU/S4mlo4wGQddIcf/V//1f9TB0V4j59b57b+HZxt8h3iMROGvg==}
    dev: true

  /mj-context-menu@0.6.1:
    resolution: {integrity: sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==}
    dev: true

  /ms@2.1.2:
    resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
    dev: true

  /mz@2.7.0:
    resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
    dependencies:
      any-promise: 1.3.0
      object-assign: 4.1.1
      thenify-all: 1.6.0
    dev: true

  /nanoid@3.3.6:
    resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==}
    engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
    hasBin: true

  /node-fetch@2.6.12:
    resolution: {integrity: sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==}
    engines: {node: 4.x || >=6.0.0}
    peerDependencies:
      encoding: ^0.1.0
    peerDependenciesMeta:
      encoding:
        optional: true
    dependencies:
      whatwg-url: 5.0.0
    dev: true

  /node-releases@2.0.13:
    resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==}
    dev: true

  /normalize-path@3.0.0:
    resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
    engines: {node: '>=0.10.0'}
    dev: true

  /normalize-range@0.1.2:
    resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
    engines: {node: '>=0.10.0'}
    dev: true

  /normalize-wheel-es@1.2.0:
    resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==}
    dev: false

  /npm-run-path@5.1.0:
    resolution: {integrity: sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==}
    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
    dependencies:
      path-key: 4.0.0
    dev: true

  /nth-check@2.1.1:
    resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
    dependencies:
      boolbase: 1.0.0
    dev: true

  /object-assign@4.1.1:
    resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
    engines: {node: '>=0.10.0'}
    dev: true

  /object-hash@3.0.0:
    resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
    engines: {node: '>= 6'}
    dev: true

  /object-inspect@1.12.3:
    resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==}
    dev: true

  /once@1.4.0:
    resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
    dependencies:
      wrappy: 1.0.2
    dev: true

  /onetime@5.1.2:
    resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
    engines: {node: '>=6'}
    dependencies:
      mimic-fn: 2.1.0
    dev: true

  /onetime@6.0.0:
    resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
    engines: {node: '>=12'}
    dependencies:
      mimic-fn: 4.0.0
    dev: true

  /p-map@4.0.0:
    resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==}
    engines: {node: '>=10'}
    dependencies:
      aggregate-error: 3.1.0
    dev: true

  /parse5-htmlparser2-tree-adapter@6.0.1:
    resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==}
    dependencies:
      parse5: 6.0.1
    dev: true

  /parse5@6.0.1:
    resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==}
    dev: true

  /path-is-absolute@1.0.1:
    resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
    engines: {node: '>=0.10.0'}
    dev: true

  /path-key@3.1.1:
    resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
    engines: {node: '>=8'}
    dev: true

  /path-key@4.0.0:
    resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==}
    engines: {node: '>=12'}
    dev: true

  /path-parse@1.0.7:
    resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
    dev: true

  /path-type@4.0.0:
    resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
    engines: {node: '>=8'}
    dev: true

  /picocolors@1.0.0:
    resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}

  /picomatch@2.3.1:
    resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
    engines: {node: '>=8.6'}
    dev: true

  /pidtree@0.6.0:
    resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==}
    engines: {node: '>=0.10'}
    hasBin: true
    dev: true

  /pify@2.3.0:
    resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
    engines: {node: '>=0.10.0'}
    dev: true

  /pirates@4.0.6:
    resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
    engines: {node: '>= 6'}
    dev: true

  /postcss-import@15.1.0(postcss@8.4.27):
    resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
    engines: {node: '>=14.0.0'}
    peerDependencies:
      postcss: ^8.0.0
    dependencies:
      postcss: 8.4.27
      postcss-value-parser: 4.2.0
      read-cache: 1.0.0
      resolve: 1.22.2
    dev: true

  /postcss-js@4.0.1(postcss@8.4.27):
    resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==}
    engines: {node: ^12 || ^14 || >= 16}
    peerDependencies:
      postcss: ^8.4.21
    dependencies:
      camelcase-css: 2.0.1
      postcss: 8.4.27
    dev: true

  /postcss-load-config@4.0.1(postcss@8.4.27):
    resolution: {integrity: sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==}
    engines: {node: '>= 14'}
    peerDependencies:
      postcss: '>=8.0.9'
      ts-node: '>=9.0.0'
    peerDependenciesMeta:
      postcss:
        optional: true
      ts-node:
        optional: true
    dependencies:
      lilconfig: 2.1.0
      postcss: 8.4.27
      yaml: 2.3.1
    dev: true

  /postcss-nested@6.0.1(postcss@8.4.27):
    resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==}
    engines: {node: '>=12.0'}
    peerDependencies:
      postcss: ^8.2.14
    dependencies:
      postcss: 8.4.27
      postcss-selector-parser: 6.0.13
    dev: true

  /postcss-selector-parser@6.0.13:
    resolution: {integrity: sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==}
    engines: {node: '>=4'}
    dependencies:
      cssesc: 3.0.0
      util-deprecate: 1.0.2
    dev: true

  /postcss-value-parser@4.2.0:
    resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
    dev: true

  /postcss@8.4.27:
    resolution: {integrity: sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==}
    engines: {node: ^10 || ^12 || >=14}
    dependencies:
      nanoid: 3.3.6
      picocolors: 1.0.0
      source-map-js: 1.0.2

  /preact@10.16.0:
    resolution: {integrity: sha512-XTSj3dJ4roKIC93pald6rWuB2qQJO9gO2iLLyTe87MrjQN+HklueLsmskbywEWqCHlclgz3/M4YLL2iBr9UmMA==}
    dev: true

  /prettier-plugin-tailwindcss@0.4.1(prettier@2.8.6):
    resolution: {integrity: sha512-hwn2EiJmv8M+AW4YDkbjJ6HlZCTzLyz1QlySn9sMuKV/Px0fjwldlB7tol8GzdgqtkdPtzT3iJ4UzdnYXP25Ag==}
    engines: {node: '>=12.17.0'}
    peerDependencies:
      '@ianvs/prettier-plugin-sort-imports': '*'
      '@prettier/plugin-pug': '*'
      '@shopify/prettier-plugin-liquid': '*'
      '@shufo/prettier-plugin-blade': '*'
      '@trivago/prettier-plugin-sort-imports': '*'
      prettier: ^2.2 || ^3.0
      prettier-plugin-astro: '*'
      prettier-plugin-css-order: '*'
      prettier-plugin-import-sort: '*'
      prettier-plugin-jsdoc: '*'
      prettier-plugin-marko: '*'
      prettier-plugin-organize-attributes: '*'
      prettier-plugin-organize-imports: '*'
      prettier-plugin-style-order: '*'
      prettier-plugin-svelte: '*'
      prettier-plugin-twig-melody: '*'
    peerDependenciesMeta:
      '@ianvs/prettier-plugin-sort-imports':
        optional: true
      '@prettier/plugin-pug':
        optional: true
      '@shopify/prettier-plugin-liquid':
        optional: true
      '@shufo/prettier-plugin-blade':
        optional: true
      '@trivago/prettier-plugin-sort-imports':
        optional: true
      prettier-plugin-astro:
        optional: true
      prettier-plugin-css-order:
        optional: true
      prettier-plugin-import-sort:
        optional: true
      prettier-plugin-jsdoc:
        optional: true
      prettier-plugin-marko:
        optional: true
      prettier-plugin-organize-attributes:
        optional: true
      prettier-plugin-organize-imports:
        optional: true
      prettier-plugin-style-order:
        optional: true
      prettier-plugin-svelte:
        optional: true
      prettier-plugin-twig-melody:
        optional: true
    dependencies:
      prettier: 2.8.6
    dev: true

  /prettier@2.8.6:
    resolution: {integrity: sha512-mtuzdiBbHwPEgl7NxWlqOkithPyp4VN93V7VeHVWBF+ad3I5avc0RVDT4oImXQy9H/AqxA2NSQH8pSxHW6FYbQ==}
    engines: {node: '>=10.13.0'}
    hasBin: true
    dev: true

  /queue-microtask@1.2.3:
    resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
    dev: true

  /read-cache@1.0.0:
    resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
    dependencies:
      pify: 2.3.0
    dev: true

  /readdirp@3.6.0:
    resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
    engines: {node: '>=8.10.0'}
    dependencies:
      picomatch: 2.3.1
    dev: true

  /resolve@1.22.2:
    resolution: {integrity: sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==}
    hasBin: true
    dependencies:
      is-core-module: 2.12.1
      path-parse: 1.0.7
      supports-preserve-symlinks-flag: 1.0.0
    dev: true

  /restore-cursor@3.1.0:
    resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==}
    engines: {node: '>=8'}
    dependencies:
      onetime: 5.1.2
      signal-exit: 3.0.7
    dev: true

  /reusify@1.0.4:
    resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
    engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
    dev: true

  /rfdc@1.3.0:
    resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==}
    dev: true

  /rollup@3.27.0:
    resolution: {integrity: sha512-aOltLCrYZ0FhJDm7fCqwTjIUEVjWjcydKBV/Zeid6Mn8BWgDCUBBWT5beM5ieForYNo/1ZHuGJdka26kvQ3Gzg==}
    engines: {node: '>=14.18.0', npm: '>=8.0.0'}
    hasBin: true
    optionalDependencies:
      fsevents: 2.3.2
    dev: true

  /run-parallel@1.2.0:
    resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
    dependencies:
      queue-microtask: 1.2.3
    dev: true

  /rxjs@7.8.1:
    resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==}
    dependencies:
      tslib: 2.6.1
    dev: true

  /sass@1.64.1:
    resolution: {integrity: sha512-16rRACSOFEE8VN7SCgBu1MpYCyN7urj9At898tyzdXFhC+a+yOX5dXwAR7L8/IdPJ1NB8OYoXmD55DM30B2kEQ==}
    engines: {node: '>=14.0.0'}
    hasBin: true
    dependencies:
      chokidar: 3.5.3
      immutable: 4.3.1
      source-map-js: 1.0.2
    dev: true

  /sax@1.2.4:
    resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==}
    dev: true

  /search-insights@2.7.0:
    resolution: {integrity: sha512-GLbVaGgzYEKMvuJbHRhLi1qoBFnjXZGZ6l4LxOYPCp4lI2jDRB3jPU9/XNhMwv6kvnA9slTreq6pvK+b3o3aqg==}
    engines: {node: '>=8.16.0'}
    dev: true

  /section-matter@1.0.0:
    resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==}
    engines: {node: '>=4'}
    dependencies:
      extend-shallow: 2.0.1
      kind-of: 6.0.3
    dev: true

  /shebang-command@2.0.0:
    resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
    engines: {node: '>=8'}
    dependencies:
      shebang-regex: 3.0.0
    dev: true

  /shebang-regex@3.0.0:
    resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
    engines: {node: '>=8'}
    dev: true

  /shiki@0.14.3:
    resolution: {integrity: sha512-U3S/a+b0KS+UkTyMjoNojvTgrBHjgp7L6ovhFVZsXmBGnVdQ4K4U9oK0z63w538S91ATngv1vXigHCSWOwnr+g==}
    dependencies:
      ansi-sequence-parser: 1.1.1
      jsonc-parser: 3.2.0
      vscode-oniguruma: 1.7.0
      vscode-textmate: 8.0.0
    dev: true

  /signal-exit@3.0.7:
    resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
    dev: true

  /slash@3.0.0:
    resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
    engines: {node: '>=8'}
    dev: true

  /slice-ansi@3.0.0:
    resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==}
    engines: {node: '>=8'}
    dependencies:
      ansi-styles: 4.3.0
      astral-regex: 2.0.0
      is-fullwidth-code-point: 3.0.0
    dev: true

  /slice-ansi@4.0.0:
    resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==}
    engines: {node: '>=10'}
    dependencies:
      ansi-styles: 4.3.0
      astral-regex: 2.0.0
      is-fullwidth-code-point: 3.0.0
    dev: true

  /slice-ansi@5.0.0:
    resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==}
    engines: {node: '>=12'}
    dependencies:
      ansi-styles: 6.2.1
      is-fullwidth-code-point: 4.0.0
    dev: true

  /slick@1.12.2:
    resolution: {integrity: sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==}
    dev: true

  /source-map-js@1.0.2:
    resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
    engines: {node: '>=0.10.0'}

  /speech-rule-engine@4.0.7:
    resolution: {integrity: sha512-sJrL3/wHzNwJRLBdf6CjJWIlxC04iYKkyXvYSVsWVOiC2DSkHmxsqOhEeMsBA9XK+CHuNcsdkbFDnoUfAsmp9g==}
    hasBin: true
    dependencies:
      commander: 9.2.0
      wicked-good-xpath: 1.3.0
      xmldom-sre: 0.1.31
    dev: true

  /sprintf-js@1.0.3:
    resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
    dev: true

  /string-argv@0.3.2:
    resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
    engines: {node: '>=0.6.19'}
    dev: true

  /string-width@4.2.3:
    resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
    engines: {node: '>=8'}
    dependencies:
      emoji-regex: 8.0.0
      is-fullwidth-code-point: 3.0.0
      strip-ansi: 6.0.1
    dev: true

  /string-width@5.1.2:
    resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
    engines: {node: '>=12'}
    dependencies:
      eastasianwidth: 0.2.0
      emoji-regex: 9.2.2
      strip-ansi: 7.1.0
    dev: true

  /strip-ansi@6.0.1:
    resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
    engines: {node: '>=8'}
    dependencies:
      ansi-regex: 5.0.1
    dev: true

  /strip-ansi@7.1.0:
    resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
    engines: {node: '>=12'}
    dependencies:
      ansi-regex: 6.0.1
    dev: true

  /strip-bom-string@1.0.0:
    resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==}
    engines: {node: '>=0.10.0'}
    dev: true

  /strip-final-newline@3.0.0:
    resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==}
    engines: {node: '>=12'}
    dev: true

  /sucrase@3.34.0:
    resolution: {integrity: sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==}
    engines: {node: '>=8'}
    hasBin: true
    dependencies:
      '@jridgewell/gen-mapping': 0.3.3
      commander: 4.1.1
      glob: 7.1.6
      lines-and-columns: 1.2.4
      mz: 2.7.0
      pirates: 4.0.6
      ts-interface-checker: 0.1.13
    dev: true

  /supports-preserve-symlinks-flag@1.0.0:
    resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
    engines: {node: '>= 0.4'}
    dev: true

  /tabbable@6.2.0:
    resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
    dev: true

  /tailwindcss@3.3.3:
    resolution: {integrity: sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==}
    engines: {node: '>=14.0.0'}
    hasBin: true
    dependencies:
      '@alloc/quick-lru': 5.2.0
      arg: 5.0.2
      chokidar: 3.5.3
      didyoumean: 1.2.2
      dlv: 1.1.3
      fast-glob: 3.3.1
      glob-parent: 6.0.2
      is-glob: 4.0.3
      jiti: 1.19.1
      lilconfig: 2.1.0
      micromatch: 4.0.5
      normalize-path: 3.0.0
      object-hash: 3.0.0
      picocolors: 1.0.0
      postcss: 8.4.27
      postcss-import: 15.1.0(postcss@8.4.27)
      postcss-js: 4.0.1(postcss@8.4.27)
      postcss-load-config: 4.0.1(postcss@8.4.27)
      postcss-nested: 6.0.1(postcss@8.4.27)
      postcss-selector-parser: 6.0.13
      resolve: 1.22.2
      sucrase: 3.34.0
    transitivePeerDependencies:
      - ts-node
    dev: true

  /thenify-all@1.6.0:
    resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
    engines: {node: '>=0.8'}
    dependencies:
      thenify: 3.3.1
    dev: true

  /thenify@3.3.1:
    resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
    dependencies:
      any-promise: 1.3.0
    dev: true

  /through@2.3.8:
    resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
    dev: true

  /tiny-player@1.0.19(hls.js@1.4.10):
    resolution: {integrity: sha512-AM4B+zSFJ5mOEPa5l8Xsb2CqMuLNBetlxf2xymok5h6zxXubzh/21JQPN/6yRAF/XN5vpZkK78RiFG/Xnqz/Fg==}
    peerDependencies:
      hls.js: ^1.4.5
    dependencies:
      hls.js: 1.4.10
    dev: false

  /to-fast-properties@2.0.0:
    resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
    engines: {node: '>=4'}

  /to-regex-range@5.0.1:
    resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
    engines: {node: '>=8.0'}
    dependencies:
      is-number: 7.0.0
    dev: true

  /tr46@0.0.3:
    resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
    dev: true

  /ts-interface-checker@0.1.13:
    resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
    dev: true

  /tslib@2.6.1:
    resolution: {integrity: sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==}
    dev: true

  /type-fest@0.21.3:
    resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
    engines: {node: '>=10'}
    dev: true

  /uc.micro@1.0.6:
    resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==}
    dev: true

  /universalify@2.0.0:
    resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
    engines: {node: '>= 10.0.0'}
    dev: true

  /update-browserslist-db@1.0.11(browserslist@4.21.9):
    resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==}
    hasBin: true
    peerDependencies:
      browserslist: '>= 4.21.0'
    dependencies:
      browserslist: 4.21.9
      escalade: 3.1.1
      picocolors: 1.0.0
    dev: true

  /util-deprecate@1.0.2:
    resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
    dev: true

  /valid-data-url@3.0.1:
    resolution: {integrity: sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==}
    engines: {node: '>=10'}
    dev: true

  /vite@4.4.7(sass@1.64.1):
    resolution: {integrity: sha512-6pYf9QJ1mHylfVh39HpuSfMPojPSKVxZvnclX1K1FyZ1PXDOcLBibdq5t1qxJSnL63ca8Wf4zts6mD8u8oc9Fw==}
    engines: {node: ^14.18.0 || >=16.0.0}
    hasBin: true
    peerDependencies:
      '@types/node': '>= 14'
      less: '*'
      lightningcss: ^1.21.0
      sass: '*'
      stylus: '*'
      sugarss: '*'
      terser: ^5.4.0
    peerDependenciesMeta:
      '@types/node':
        optional: true
      less:
        optional: true
      lightningcss:
        optional: true
      sass:
        optional: true
      stylus:
        optional: true
      sugarss:
        optional: true
      terser:
        optional: true
    dependencies:
      esbuild: 0.18.17
      postcss: 8.4.27
      rollup: 3.27.0
      sass: 1.64.1
    optionalDependencies:
      fsevents: 2.3.2
    dev: true

  /vitepress@1.0.0-beta.7(@algolia/client-search@4.19.1)(sass@1.64.1)(search-insights@2.7.0):
    resolution: {integrity: sha512-P9Rw+FXatKIU4fVdtKxqwHl6fby8E/8zE3FIfep6meNgN4BxbWqoKJ6yfuuQQR9IrpQqwnyaBh4LSabyll6tWg==}
    hasBin: true
    dependencies:
      '@docsearch/css': 3.5.1
      '@docsearch/js': 3.5.1(@algolia/client-search@4.19.1)(search-insights@2.7.0)
      '@vitejs/plugin-vue': 4.2.3(vite@4.4.7)(vue@3.3.4)
      '@vue/devtools-api': 6.5.0
      '@vueuse/core': 10.3.0(vue@3.3.4)
      '@vueuse/integrations': 10.3.0(focus-trap@7.5.2)(vue@3.3.4)
      body-scroll-lock: 4.0.0-beta.0
      focus-trap: 7.5.2
      mark.js: 8.11.1
      minisearch: 6.1.0
      shiki: 0.14.3
      vite: 4.4.7(sass@1.64.1)
      vue: 3.3.4
    transitivePeerDependencies:
      - '@algolia/client-search'
      - '@types/node'
      - '@types/react'
      - '@vue/composition-api'
      - async-validator
      - axios
      - change-case
      - drauu
      - fuse.js
      - idb-keyval
      - jwt-decode
      - less
      - lightningcss
      - nprogress
      - qrcode
      - react
      - react-dom
      - sass
      - search-insights
      - sortablejs
      - stylus
      - sugarss
      - terser
      - universal-cookie
    dev: true

  /vscode-oniguruma@1.7.0:
    resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==}
    dev: true

  /vscode-textmate@8.0.0:
    resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==}
    dev: true

  /vue-demi@0.14.5(vue@3.3.2):
    resolution: {integrity: sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==}
    engines: {node: '>=12'}
    hasBin: true
    requiresBuild: true
    peerDependencies:
      '@vue/composition-api': ^1.0.0-rc.1
      vue: ^3.0.0-0 || ^2.6.0
    peerDependenciesMeta:
      '@vue/composition-api':
        optional: true
    dependencies:
      vue: 3.3.2
    dev: false

  /vue-demi@0.14.5(vue@3.3.4):
    resolution: {integrity: sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==}
    engines: {node: '>=12'}
    hasBin: true
    requiresBuild: true
    peerDependencies:
      '@vue/composition-api': ^1.0.0-rc.1
      vue: ^3.0.0-0 || ^2.6.0
    peerDependenciesMeta:
      '@vue/composition-api':
        optional: true
    dependencies:
      vue: 3.3.4
    dev: true

  /vue@3.3.2:
    resolution: {integrity: sha512-98hJcAhyDwZoOo2flAQBSPVYG/o0HA9ivIy2ktHshjE+6/q8IMQ+kvDKQzOZTFPxvnNMcGM+zS2A00xeZMA7tA==}
    dependencies:
      '@vue/compiler-dom': 3.3.2
      '@vue/compiler-sfc': 3.3.2
      '@vue/runtime-dom': 3.3.2
      '@vue/server-renderer': 3.3.2(vue@3.3.2)
      '@vue/shared': 3.3.2
    dev: false

  /vue@3.3.4:
    resolution: {integrity: sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==}
    dependencies:
      '@vue/compiler-dom': 3.3.4
      '@vue/compiler-sfc': 3.3.4
      '@vue/runtime-dom': 3.3.4
      '@vue/server-renderer': 3.3.4(vue@3.3.4)
      '@vue/shared': 3.3.4
    dev: true

  /web-resource-inliner@6.0.1:
    resolution: {integrity: sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A==}
    engines: {node: '>=10.0.0'}
    dependencies:
      ansi-colors: 4.1.3
      escape-goat: 3.0.0
      htmlparser2: 5.0.1
      mime: 2.6.0
      node-fetch: 2.6.12
      valid-data-url: 3.0.1
    transitivePeerDependencies:
      - encoding
    dev: true

  /webidl-conversions@3.0.1:
    resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
    dev: true

  /whatwg-url@5.0.0:
    resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
    dependencies:
      tr46: 0.0.3
      webidl-conversions: 3.0.1
    dev: true

  /which@2.0.2:
    resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
    engines: {node: '>= 8'}
    hasBin: true
    dependencies:
      isexe: 2.0.0
    dev: true

  /wicked-good-xpath@1.3.0:
    resolution: {integrity: sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==}
    dev: true

  /wrap-ansi@6.2.0:
    resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
    engines: {node: '>=8'}
    dependencies:
      ansi-styles: 4.3.0
      string-width: 4.2.3
      strip-ansi: 6.0.1
    dev: true

  /wrap-ansi@7.0.0:
    resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
    engines: {node: '>=10'}
    dependencies:
      ansi-styles: 4.3.0
      string-width: 4.2.3
      strip-ansi: 6.0.1
    dev: true

  /wrappy@1.0.2:
    resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
    dev: true

  /xml-js@1.6.11:
    resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==}
    hasBin: true
    dependencies:
      sax: 1.2.4
    dev: true

  /xmldom-sre@0.1.31:
    resolution: {integrity: sha512-f9s+fUkX04BxQf+7mMWAp5zk61pciie+fFLC9hX9UVvCeJQfNHRHXpeo5MPcR0EUf57PYLdt+ZO4f3Ipk2oZUw==}
    engines: {node: '>=0.1'}
    dev: true

  /yaml@2.3.1:
    resolution: {integrity: sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==}
    engines: {node: '>= 14'}
    dev: true

---

## File: tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "moduleResolution": "Node",
    "allowJs": true,
    "strict": true, // 开启所有严格类型检查选项
    "noImplicitAny": true, // 在表达式和声明上有隐含的 any类型时报错
    "strictNullChecks": true, // 启用严格的 null 检查
    "esModuleInterop": true, // 为ESM提供对CommonJS模块的默认导入导出语义的支持
    "skipLibCheck": true, // 跳过声明文件的类型检查
    "forceConsistentCasingInFileNames": true, // 不允许对同一个文件使用不一致的文件名
    "allowSyntheticDefaultImports": true // 允许从没有设置默认导出的模块中默认导入
  },
  // 包含的文件
  "include": [
    ".vitepress/**/*.ts",
    ".vitepress/**/*.d.ts",
    ".vitepress/**/*.tsx",
    ".vitepress/**/*.vue",
    "types/**/*.d.ts"
  ],
  "types": ["element-plus/global"],
  "exclude": ["node_modules", "dist", "**/*.js"] // 排除的文件
}

---

## File: vercel.json

{
  "git": {
    "deploymentEnabled": false
  }
}

---

## File: page/forum.md

---
layout: doc
---

# FedTop 讨论区

> 大家可以在这里自由交流，也可以加入我的技术交流群，一起变强！  
> ( ChicSparrow ，添加请备注来意。 )

---

## File: page/projects.md

---
layout: page
---

---

## File: page/tags.md

---
layout: page
---

<Tags/>

---

## File: page/timeline.md

---
layout: page
---

<Timeline />

---

## File: page/use.md

# Use

Things I am using

## Hardware

- MacBook Pro 16' M1 32G
- 触控板：Magic Trackpad 2
- 键盘： keychron k8

## Editor

- Editor: [VS Code](https://code.visualstudio.com/)
- Theme: [One dark pro](https://github.com/antfu/vscode-theme-vitesse)
- File Icons: [vscode-icons](https://marketplace.visualstudio.com/items?itemName=vscode-icons-team.vscode-icons)
- Extensions
  - [Error Lens](https://marketplace.visualstudio.com/items?itemName=usernamehw.errorlens) - Highlight errors in code
  - [GitLens](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens) - Git superpowers
  - [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) - Vue 3 IDE support
  - [indent-rainbow](https://marketplace.visualstudio.com/items?itemName=oderwat.indent-rainbow) - Rainbow indent
  - [npm dependency links](https://marketplace.visualstudio.com/items?itemName=herrmannplatz.npm-dependency-links) - Link to dependencies in package.json

## Chrome Extensions

- [Refined GitHub](https://chrome.google.com/webstore/detail/refined-github/hlepfoohegkhhmjieoechaddaejaokhf) - No-brainer must-have
- [Tampermonkey](https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo) - User scripts for any website
- [File Icons for GitHub and GitLab](https://chrome.google.com/webstore/detail/file-icons-for-github-and/ficfmibkjjnpogdcfhfokmihanoldbfe) - It looks very nice!
- [Grammarly](https://chrome.google.com/webstore/detail/grammarly-grammar-checker/kbfnbcaeplbcioakkpcpgfkobkghlhen) - Spell check
- [uBlock Origin](https://chrome.google.com/webstore/detail/ublock-origin/cjpalhdlnbpafiamejdnhcphjbkeiagm) - Ad blocker
- [I don't care about cookies](https://chrome.google.com/webstore/detail/i-dont-care-about-cookies/fihnjjcciajhdojfnbdddfaoknhalnja) - Disable cookie popups, probably more relevant in EU

## MacOS Productivity

- [Kap](https://getkap.co/) - GIF Recorder
- [Raycast](https://raycast.com/) - Spotlight replacement
- [MonitorControl](https://github.com/MonitorControl/MonitorControl) - Control the monitor in macOS
- [Picture View](https://wl879.github.io/apps/picview/) - Minimalist picture viewer
- [Picsee](https://picsee.chitaner.com/) - File-based photo management
- [Command X](https://apps.apple.com/us/app/command-x/id6448461551?mt=12) - The missing Cut feature for finder
- [Rectangle](https://rectangleapp.com/) - Window Manager
- [Klack](https://tryklack.com/) - Mechanical Keyboard Sound for built-in keyboard

## Design / Social Media

- [broz](https://github.com/antfu/broz) - Frameless browser for screenshots
- [Figma](https://www.figma.com/) - Illustrations, Logos, Social Media Banners
- [OBS](https://obsproject.com/) - Streaming & Recording
- [DaVinci Resolve](https://www.blackmagicdesign.com/products/davinciresolve) - Video Editing
- [Lightroom Classic](https://www.adobe.com/products/photoshop-lightroom-classic.html) - Photo Editing and Management

## CLI

- [zsh](https://zsh.org/) - Shell
  - Config: [antfu/dotfiles](https://github.com/antfu/dotfiles)
- [pnpm](https://pnpm.io/) - Preferred package managers, also for monorepo
- [ni](https://github.com/antfu/ni) - Aliasing package managers
- Build Tools for Libraries
  - [unbuild](https://github.com/unjs/unbuild) - Most of the time
  - [tsup](https://github.com/egoist/tsup) - Easy to use
  - [rollup](https://rollupjs.org/) - For very fine-grained control

---

## File: posts/01-frontend/13 个 颜色转换 的高级方法.md

---
title: 🌈 13 个 颜色转换 的高级方法
date: 2022-08-01
tags:
  - javascript
categories:
  - 前端
---

# 🌈 13 个 颜色转换 的高级方法

> 哈喽大家好 ~ 好久不见，我是荣顶，由于经常需要在 JavaScript 中使用颜色，而想高效的处理这些又需要对颜色格式和转换有一定的了解。  
> 所以我总结这个 13 个代码片段，供大家满足大部分日常的需求，从随机生成十六进制的颜色，到在十六进制和 RGB 值之间转换，甚至一些更高级的技巧，比如使用 HSL 格式来改变颜色的亮度等等。。。

**好了废话不多说 ~ 上代码！**

## 随机生成十六进制颜色

```javascript
//随机生成十六进制颜色
const randomHexColorCode = () => {
  let n = (Math.random() * 0xfffff * 1000000).toString(16)
  return '#' + n.slice(0, 6)
}

//使用
randomHexColorCode() // '#e34155'
```

## RGB 转十六进制

```javascript
//RGB转十六进制
const RGBToHex = (r, g, b) => ((r << 16) + (g << 8) + b).toString(16).padStart(6, '0')

//使用
RGBToHex(255, 165, 1) // '#ffa501'
```

## 十六进制转 RGB

```javascript
//十六进制转RGB
const hexToRGB = (hex) => {
  let alpha = false,
    h = hex.slice(hex.startsWith('#') ? 1 : 0)
  if (h.length === 3) h = [...h].map((x) => x + x).join('')
  else if (h.length === 8) alpha = true
  h = parseInt(h, 16)
  return (
    'rgb' +
    (alpha ? 'a' : '') +
    '(' +
    (h >>> (alpha ? 24 : 16)) +
    ', ' +
    ((h & (alpha ? 0x00ff0000 : 0x00ff00)) >>> (alpha ? 16 : 8)) +
    ', ' +
    ((h & (alpha ? 0x0000ff00 : 0x0000ff)) >>> (alpha ? 8 : 0)) +
    (alpha ? `, ${h & 0x000000ff}` : '') +
    ')'
  )
}

//使用
hexToRGB('#27ae60ff') // 'rgba(39, 174, 96, 255)'
hexToRGB('27ae60') // 'rgb(39, 174, 96)'
hexToRGB('#fff') // 'rgb(255, 255, 255)'
```

## 将 3 位颜色扩展为 6 位颜色

```javascript
//将 十六进制 3 位颜色扩展为 6 位颜色
const extendHex = (shortHex) =>
  '#' +
  shortHex
    .slice(shortHex.startsWith('#') ? 1 : 0)
    .split('')
    .map((x) => x + x)
    .join('')

//使用
extendHex('#03f') // '#0033ff'
extendHex('05a') // '#0055aa'
```

## 将 rgb()颜色字符串转换为具有每种颜色值的对象。

```javascript
// 将rgb()颜色字符串转换为具有每种颜色值的对象。
const toRGBObject = (rgbStr) => {
  const [red, green, blue] = rgbStr.match(/\d+/g).map(Number)
  return { red, green, blue }
}

//使用
toRGBObject('rgb(255, 12, 0)') // {red: 255, green: 12, blue: 0}
```

## 将 rgb()颜色字符串转换为数组

```javascript
//将 rgb()颜色字符串转换为数组
const toRGBArray = (rgbStr) => rgbStr.match(/\d+/g).map(Number)

//使用
toRGBArray('rgb(255, 12, 0)') // [255, 12, 0]
```

## 将 RGB 颜色元组转换为 HSB 格式。

```javascript
// 将 RGB 颜色元组转换为 HSB 格式。
const RGBToHSB = (r, g, b) => {
  r /= 255
  g /= 255
  b /= 255
  const v = Math.max(r, g, b),
    n = v - Math.min(r, g, b)
  const h = n === 0 ? 0 : n && v === r ? (g - b) / n : v === g ? 2 + (b - r) / n : 4 + (r - g) / n
  return [60 * (h < 0 ? h + 6 : h), v && (n / v) * 100, v * 100]
}

//使用
RGBToHSB(252, 111, 48)
// [18.529411764705856, 80.95238095238095, 98.82352941176471]
```

## 将 HSB 颜色元组转换为 RGB 格式。

```javascript
// 将 HSB 颜色元组转换为 RGB 格式。
const HSBToRGB = (h, s, b) => {
  s /= 100
  b /= 100
  const k = (n) => (n + h / 60) % 6
  const f = (n) => b * (1 - s * Math.max(0, Math.min(k(n), 4 - k(n), 1)))
  return [255 * f(5), 255 * f(3), 255 * f(1)]
}

//使用
HSBToRGB(18, 81, 99) // [252.45, 109.31084999999996, 47.965499999999984]
```

## 将 RGB 颜色元组转换为 HSL 格式。

```javascript
//将 RGB 颜色元组转换为 HSL 格式。
const RGBToHSL = (r, g, b) => {
  r /= 255
  g /= 255
  b /= 255
  const l = Math.max(r, g, b)
  const s = l - Math.min(r, g, b)
  const h = s ? (l === r ? (g - b) / s : l === g ? 2 + (b - r) / s : 4 + (r - g) / s) : 0
  return [60 * h < 0 ? 60 * h + 360 : 60 * h, 100 * (s ? (l <= 0.5 ? s / (2 * l - s) : s / (2 - (2 * l - s))) : 0), (100 * (2 * l - s)) / 2]
}

//使用
RGBToHSL(45, 23, 11) // [21.17647, 60.71428, 10.98039]
```

## 将 HSL 颜色元组转换为 RGB 格式。

```javascript
//将 HSL 颜色元组转换为 RGB 格式。
const HSLToRGB = (h, s, l) => {
  s /= 100
  l /= 100
  const k = (n) => (n + h / 30) % 12
  const a = s * Math.min(l, 1 - l)
  const f = (n) => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)))
  return [255 * f(0), 255 * f(8), 255 * f(4)]
}

//使用
HSLToRGB(13, 100, 11) // [56.1, 12.155, 0]
```

## 更改 hsl()颜色字符串的亮度值。

```javascript
//更改hsl()颜色字符串的亮度值。
const changeLightness = (delta, hslStr) => {
  const [hue, saturation, lightness] = hslStr.match(/\d+/g).map(Number)
  const newLightness = Math.max(0, Math.min(100, lightness + parseFloat(delta)))
  return `hsl(${hue}, ${saturation}%, ${newLightness}%)`
}

//使用
changeLightness(10, 'hsl(330, 50%, 50%)') // 'hsl(330, 50%, 60%)'
changeLightness(-10, 'hsl(330, 50%, 50%)') // 'hsl(330, 50%, 40%)'
```

## 将 hsl()颜色字符串转换为值数组。

```javascript
// 将hsl()颜色字符串转换为值数组。
const toHSLArray = (hslStr) => hslStr.match(/\d+/g).map(Number)

//使用
toHSLArray('hsl(50, 10%, 10%)') // [50, 10, 10]
```

## 将 hsl()颜色字符串转换为具有每种颜色值的对象。

```javascript
//将hsl()颜色字符串转换为具有每种颜色值的对象。
const toHSLObject = (hslStr) => {
  const [hue, saturation, lightness] = hslStr.match(/\d+/g).map(Number)
  return { hue, saturation, lightness }
}

//使用
toHSLObject('hsl(50, 10%, 10%)') // { hue: 50, saturation: 10, lightness: 10 }
```

## 最后

> 我是荣顶，很高兴能在这里和你一起变强！一起**面向快乐编程！** 😉
>
> 如果你也非常热爱前端相关技术！欢迎进入我的前端圈~ 🦄 点击 → [这里](https://juejin.cn/pin/7004843607072964621)

---

## File: posts/01-frontend/20+个前端必备的站点.md

---
title: 毕业到现在,浓缩这 20+ Niubility 的站点助你一臂之力
tags:
  - 其他
categories:
  - 前端
date: 2022-02-05
---

## 前言

看到很多小伙伴都把自己好用好玩的站点分享出来了,我也是一个收集工具站点的爱好者。  
由于有一些比较常见的网站,很多人已经分享过了,下面都是我从收藏夹中几百个网站里筛选出的,我个人认为这些对前端非常有帮助的 20+站点,希望可以在你前行的道路上,对你有所帮助!

原创不易!点赞鼓励~🧡

## [freeCodeCamp](https://chinese.freecodecamp.org/learn)

一直霸占着 gitHub 排名 No.1 的项目,足足有 329k 个 star!!! 我天~啥时候我开源项目能有它的零头 star~

有足足 500 多种教程,码住,自学妥妥的!  
详情请戳 →:[freeCodeCamp](https://chinese.freecodecamp.org/learn)







## [JavaScript 周刊](https://javascriptweekly.com/issues/554)

每个礼拜都会向你发送最新的 JavaScript 文章、新闻和酷项目的时事通讯,在网页中通过填写邮箱订阅,从此再也不怕掉队了~  
有了它你就能实时了解到 JavaScript 最新的资讯以及最新的基本动向!  
目前已经有 177401 订阅者 出到了 [554 期](https://javascriptweekly.com/issues)  
详情请戳 →:[JavaScript 周刊](https://javascriptweekly.com/issues/554)



## [JavaScript Tips](https://www.jstips.co/zh_CN/)

超多的 JavaScript 小技巧,我天~吃饭睡觉乘车上厕所的时间随便看两个,你的技术就会噌噌噌的上涨!  
当然也可以提交你的小技巧到站点,非常有意思~  
详情请戳 →:[JavaScript Tips](https://www.jstips.co/zh_CN/)



## [JavaScript 一行代码](https://1loc.dev/)

你的代码够简洁嘛?想知道更多的代码简洁技巧嘛?快看这里!

目前已经收集了 286 个 JavaScript 一行代码,助你写出更劲道的 JavaScript 代码!

详情请戳 →:[JavaScript 一行代码](https://1loc.dev/)  


## [30SecondsOfCode](https://www.30secondsofcode.org/js/p/1)

目前已经汇总了大量的简短代码,我们平时闲暇的时候,可以学学它里面的一些例子,这也是一个非常好的站点!

30 秒 JavaScript 代码满足你所有开发需求的简短代码片段 !

详情请戳 →:[30SecondsOfCode](https://www.30secondsofcode.org/js/p/1)



## [GRID GARDEN](https://cssgridgarden.com/#zh-cn)

总共 28 关,以游戏的方式,带你学习 grid 布局,非常的轻松,再也不用枯燥的死记硬背了~  
非常好的是它是支持中文的,游戏的关卡也非常的简单,通俗易懂,通过娱乐的方式学习,是我最喜欢的了!

详情请戳 →:[GRID GARDEN](https://cssgridgarden.com/#zh-cn)



## [mojs](https://mojs.github.io/)

用**简单的几行 js 代码**就能写出非常**酷炫的动画**效果!  
也是开发中的一个非常有意思的**利器!**  
详情请戳 →:[mojs](https://mojs.github.io/)



## [CodePen](https://codepen.io/)

海量的好玩的作品,可以下载非常多的小工具来辅助开发~支持在线编辑,赶快也上传你的作品吧~

详情请戳 →:[CodePen](https://codepen.io/)



## [color Hunt](https://colorhunt.co/)

作为前端程序员,不知道如何配色?

不要怕,这里有最好的颜色搭配,供你选择!  
color Hunt 作为设计师和艺术家的调色板,快去探索最新精选的调色方案吧~

详情请戳 →:[color Hunt](https://colorhunt.co/)



## [无损放大图片](http://waifu2x.udp.jp/index.zh-CN.html)

我们都知道,对图片的放大是一定会出现失真的问题,那么怎么才能无损的对图片进行放大呢?  
这位日本小哥,基于深度学习,完成了这项功能

详情请戳 →:[无损放大图片](http://waifu2x.udp.jp/index.zh-CN.html)



## [w3c 规范检测](http://jigsaw.w3.org/css-validator/#validate_by_uri)

检查层叠样式表(CSS)文档和 HTML 或者 XHTML 文档中的 CSS 内容

可以通过输入你的站点,或者上传你的相关文件,来检测你的代码是否严格符合 w3c 的标准规范.

详情请戳 →:[w3c 规范检测](http://jigsaw.w3.org/css-validator/#validate_by_uri)



## [互联网档案馆](http://wzlc0577.com/web/)

比如你想看五六年前的 Google,百度,看去年的掘金,前年的 bilibili 等等站点的页面构成和样式...

完全可以在这里找到

详情请戳 →:[互联网档案馆](http://wzlc0577.com/web/)



## [sketchToy](https://sketchtoy.com/)

一款**超级超级超级超级好用**的**免费的在线绘图应用程序**，可让您与朋友分享您的工作的**逐步回放**。无需注册。  
它最大的亮点就是它能分享给你的好友,并逐步的回放你的绘画过程!是一个非常有意思的工具.  
详情请戳 →:[sketchToy](https://sketchtoy.com/)



## [AirDroid](https://web.airdroid.com)

AirDroid 是一个快速、免费的应用，以无线方式通过网页浏览器管理您的设备。  
就是可以通过 web 端的网页来链接并管理你的或者他人的手机.

详情请戳 →:[AirDroid](https://web.airdroid.com)



## [img2css](https://javier.xyz/img2css/)

这是一个可以将任何图像转换为纯 css 图像的工具。  
是 CSS 哦,不是 Base64,底层的具体实现应该比较复杂,是一个挺有意思的站点  
详情请戳 →:[img2css](https://javier.xyz/img2css/)



## [WallHaven](https://wallhaven.cc/)

**目前我认为最好的一个图片素材网站**,不管是用来做壁纸还是做站点资源图片都是非常不错的!

详情请戳 →:[WallHaven](https://wallhaven.cc/)





## [ih5](http://www.ih5.cn/)

目前很火的低代码,这个平台是零代码[ih5](http://www.ih5.cn/),通过拖拉拽,生成你想要的 h5 页面,还能制作游戏和全景 3D 海报,简直不要太帅~

详情请戳 →:[ih5](http://www.ih5.cn/)





## [模板王](http://www.mobanwang.com/)

海量的成品商用站点模板,做博客,做官网...都可以用~免费下载它们的源码!  
非常良心的一个网站!

详情请戳 →:[模板王](http://www.mobanwang.com/)



## [Figma](https://www.figma.com/)

web 端的 UI 设计工具,主要是因为它免费

详情请戳 →:[Figma](https://www.figma.com/)



## [flagCounter](https://s11.flagcounter.com/index.html)

简单的将它生成的代码嵌入到你的站点,就能记录下访问你站点的所有访客都来自哪些国家~  
详情请戳 →:[flagCounter](https://s11.flagcounter.com/index.html)



## 最后

> 我是荣顶，很高兴能在这里和你一起变强！和我一起**面向快乐编程！** 😉  
> 关注我的公众号 **前端超人**，获取更多好玩与有趣的前端知识。**如果你也一样对技术热爱并且为之着迷,欢迎加我个人微信（ChicSparrow），我将会拉你入群一起交流学习~** 🦄  
>

---

## File: posts/01-frontend/TypeScript学习笔记.md

---
title: TypeScript学习笔记
date: 2021-09-03
tags:
  - typescript
---

## tips

传递数组或者对象作为函数参数时，可以用 Readonly<> 包裹原来的类型声明，譬如 `Readonly<Array<number>>`，TypeScript 编译器会通过拒绝编译来保护这个参数不被修改。如果的确需要一个可以被修改的数组，可以通过 spread 操作 [...array] 来复制这个数组

```ts
function sortNumbers(array: Readonly<Array<number>>) {
  return [...array].sort((a, b) => a - b)
}
```

使用 unkown 而不是 any 来标注类型尚未明确的变量。Any 告诉 TypeScript 编译器，不需要检查类型；而 unkown 则是把检查留到使用变量的时候。使用时可以通过 typeof 运算符获取到变量的实际类型，通过 as 添加上类型后就可以正常使用了

使用 Records 来代替 Objects，因为可以限定键的范围。如

```ts
type AllowedKeys = 'name' | 'age'

// use a type here instead of interface
type Person = Record<AllowedKeys, unknown>

const Human: Person = {
  name: 'Steve',
  age: 42,
}
```

## tsconfig

### 隐式推断

ts 有隐式类型推断。开启后隐式 any 的推断会被检查并报错。

```typescript
// x被隐式推断成any,抛出错误
function getNum(x) {
  return x
}
// problems: Parameter 'x' implicitly has an 'any' type.
```

```typescript
// 主动声明x是any避免错误
function getNum(x: any) {
  return x
}
```

```typescript
// 这种情况不会报错，因为隐式推断成undefined
let y
console.log(y)
```

```typescript
// 这种情况也不会报错，因为赋值的时候类型推断成number
let z
z = 123
console.log(z)
```

### 严格检查

默认情况 undefined 和 null 是其他类型的子类型，也就是其他类型变量可以被赋值 null 或 undefined。开启后会检查这种情况并报错。

```typescript
let num: number = undefined
// problems:Type 'undefined' is not assignable to type 'number'.
```

### 错误不输出

ts 默认即使编译报错也会生成 Js 文件，这一项设置 true,可以在报错的时候，不生成 js

## 基础

### 标记类型

- boolean,number,string,null,undefined
- void(一般用于函数空返回值，void 类型的变量可以赋值 undefined,不能赋值 null)
- any(允许被赋值给任意类型，any 类型不会被类型检查）
- unkonwn(允许被赋值给任意类型)

### 类型推论

定义时未指定类型且未赋值会被推断成 any，若赋值则会推断成赋值的类型

```ts
let myFavoriteNumber = 'seven' //推断成string
```

### 联合类型

‘或’

```ts
let myFavoriteNumber: string | number
```

### 交叉类型

‘与’

```typescript
interface A {
  a: string
}
interface B {
  b: string
}
let test: A & B = {
  a: 'test a',
  b: 'test b',
}
```

### 对象的类型

#### 描述对象的形状（shape）

```typescript
interface Person {
  name: string
  age: number
}
let tom: Person = {
  name: 'Tom',
  age: 25,
}
```

#### 定义的变量比接口少一些属性或多一些属性都是不允许的

可选属性

```typescript
interface Person {
  name: string
  age?: number
}

let tom: Person = {
  name: 'Tom',
}
```

任意属性

```typescript
interface Person {
  name: string
  age?: number
  [propName: string]: any
}

let tom: Person = {
  name: 'Tom',
  gender: 'male',
}
```

需要注意的是，一旦定义了任意属性，那么确定属性和可选属性的类型都必须是它的类型的子集：

```typescript
interface Person {
  name: string
  age?: number
  [propName: string]: string
}

let tom: Person = {
  name: 'Tom',
  age: 25,
  gender: 'male',
}

// index.ts(3,5): error TS2411: Property 'age' of type 'number' is not assignable to string index type 'string'.
// index.ts(7,5): error TS2322: Type '{ [x: string]: string | number; name: string; age: number; gender: string; }' is not assignable to type 'Person'.
//   Index signatures are incompatible.
//     Type 'string | number' is not assignable to type 'string'.
//       Type 'number' is not assignable to type 'string'.

//上例中，任意属性的值允许是 string，但是可选属性 age 的值却是 number，number 不是 string 的子属性，所以报错了。
```

一个接口中只能定义一个任意属性。如果接口中有多个类型的属性，则可以在任意属性中使用联合类型：

```typescript
interface Person {
  name: string
  age?: number
  [propName: string]: string | number | undefined // 因为age不一定有，可能是undefined
}

let tom: Person = {
  name: 'Tom',
  age: 25,
  gender: 'male',
}
```

只读属性

> 有时候我们希望对象中的一些字段只能在创建的时候被赋值，那么可以用 readonly 定义只读属性  
> 简单例子

```typescript
interface Person {
  readonly id: number
  name: string
  age?: number
  [propName: string]: any
}

let tom: Person = {
  id: 89757,
  name: 'Tom',
  gender: 'male',
}

tom.id = 9527

// index.ts(14,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.
// 上例中，使用 readonly 定义的属性 id 初始化后，又被赋值了，所以报错了。
```

注意，只读的约束存在于第一次给对象赋值的时候，而不是第一次给只读属性赋值的时候：

```typescript
interface Person {
  readonly id: number
  name: string
  age?: number
  [propName: string]: any
}

let tom: Person = {
  name: 'Tom',
  gender: 'male',
}

tom.id = 89757

// index.ts(8,5): error TS2322: Type '{ name: string; gender: string; }' is not assignable to type 'Person'.
//   Property 'id' is missing in type '{ name: string; gender: string; }'.
// index.ts(13,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.
//例中，报错信息有两处，第一处是在对 tom 进行赋值的时候，没有给 id 赋值。第二处是在给 tom.id 赋值的时候，由于它是只读属性，所以报错了。
```

### 数组的类型

#### 类型+方括号表示法

```ts
let fibonacci: number[] = [1, 2, 3, 4]
```

#### 泛型表示法（常用）

```ts
let fibonacci: Array<number> = [1, 2, 3, 4]
```

#### 接口表示法（不常见）

```ts
interface NumberArray {
  [index: number]: number
}
let fibonacci: NumberArray = [1, 2, 3, 4]
```

#### 类数组的表示

类数组不是数组，不能用数组表示，但是可以参照接口表示法来写  
比如函数参数 arguments。但事实上常用的类数组都有自己的接口定义，如 IArguments, NodeList, HTMLCollection 等

```ts
function sum() {
  let args: {
    [index: number]: number
    length: number
    callee: Function
  } = arguments
}
//上述等价于
function sum() {
  let args: IArguments = arguments
}
//IArguments 是 TypeScript 中定义好了的类型，它实际上就是：
interface IArguments {
  [index: number]: any
  length: number
  callee: Function
}
```

### 函数的类型

#### 函数声明式（简单，推荐）

```ts
function sum(x: number, y: number): number {
  return x + y
}
```

#### 函数表达式

```ts
let mySum: (x: number, y: number) => number = (x: number, y: number): number => {
  return x + y
}
// 第一个冒号到第一个等号之间的可以省略，相当于不写type
//ts会类型推断的，不然这么写
//就像写了两遍

//或者这么写，注意是Function不是function，其实也相当于少写了type
let newSum: Function = (x: number, y: number): number => {
  return x + y
}
```

#### 用接口定义函数,注意参数的括号

```ts
interface SearchFunc {
  (source: string, subString: string): boolean
}
let mySearch: SearchFunc = (source: string, subString: string): boolean => {
  return source.search(subString) !== -1
}
```

#### 可选参数（可选参数要放在最后），参数默认值

```ts
function buildName(firstName?: string, lastName: string = 'Clark'): string {
  if (firstName) {
    return firstName + lastName
  }
  return lastName
}
```

#### 剩余参数，...rest 访问剩余参数

```ts
function myPush(array: Array<unknown>, ...rest: Array<unknown>) {
  rest.forEach((item) => {
    array.push(item)
  })
}
myPush([], 1, 2, 3)
```

#### 重载

重载允许一个函数接受不同数量或类型的参数时，作出不同的处理  
比如，我们需要实现一个函数 reverse，输入数字 123 的时候，输出反转的数字 321，输入字符串 'hello' 的时候，输出反转的字符串 'olleh'

```ts
function reverse(x: number | string): number | string | void {
  if (typeof x === 'number') {
    return Number(x.toString().split('').reverse().join(''))
  } else if (typeof x === 'string') {
    return x.split('').reverse().join('')
  }
}
```

然而这样有一个缺点，就是不能够精确的表达，输入为数字的时候，输出也应该为数字，输入为字符串的时候，输出也应该为字符串  
这时，我们可以使用重载定义多个 reverse 的函数类型：

```ts
function reverse(x: number): number
function reverse(x: string): string
function reverse(x: number | string): number | string | void {
  if (typeof x === 'number') {
    return Number(x.toString().split('').reverse().join(''))
  } else if (typeof x === 'string') {
    return x.split('').reverse().join('')
  }
}
//上例中，我们重复定义了多次函数 reverse，前几次都是函数定义，最后一次是函数实现。在编辑器的代码提示中，可以正确的看到前两个提示。

//注意，TypeScript 会优先从最前面的函数定义开始匹配，所以多个函数定义如果有包含关系，需要优先把精确的定义写在前面。
```

### 类型断言

#### 语法

- 值 as 类型（推荐）
- `<类型>`值 （不推荐，tsx 语法中`<Foo>`表示 ReactNode；而在 ts 中泛型也是这种语法，容易混淆）
- 作用：给一个类型断言，让 ts 编译器听你的，若使用不当会造成运行时错误。常用于 ts 的一些不好解决的报错
- 示例

```ts
interface Cat {
  name: string
  run(): void
}
interface Fihs {
  name: string
  swim(): void
}
function isFish(animal: Cat | Fish) {
  if (typeof (animal as Fish).swim) {
    return true
  }
  return false
}
```

```ts
;(window as any).foo = 1
```

```ts
function getCacheData(key: string): any {
  return (window as any).cache[key]
}

interface Cat {
  name: string
  run(): void
}

const tom = getCacheData('tom') as Cat
tom.run()
```

```ts
//用泛型可以更好地实现上述代码
function getCacheData<T>(key: string): T {
  return (window as any).cache[key]
}

interface Cat {
  name: string
  run(): void
}

const tom = getCacheData<Cat>('tom')
tom.run()
```

### 声明文件

---

## File: posts/01-frontend/《chrome腾飞之路》 之 chrome 95 新特性.md

---
title: 《chrome腾飞之路》 之 chrome 95 新特性
date: 2021-10-10
tags:
  - Chrome
categories:
  - 前端
---

# 即将推出的 chrome 95 新特性

## 前言

### 为什么会想到开始写这类的文章呢?

我想先从大的方向来谈:

Google 坐拥了全球第一大操作系统 Android 以及全球第一大浏览器 Chrome，67.14%市场占有率的 Chrome 简直可以为所欲为．

人们总是对 Google 亦步亦趋，毕竟这十年里逐渐崛起的 Google Chrome 在众多的浏览器中已经无敌手了．



对于前端开发者而言，虽然是乐意看到 Chrome 一统天下的，毕竟谁也忘不了曾经被 IE 支配的恐惧。但垄断并不是一件好事！因为它大概率会带来技术的停滞，IE 就是一个鲜活的例子。IE 当初脱离标准，形成自己的 API 规范的初衷，就是为了垄断浏览器市场！（大家应该对垄断心怀警惕！）

> 微软 Edge 浏览器换用 Chromium 内核已经不是新闻了，Chromium 内核的 Edge 也已经正式发布，虽然没有通过 Windows Update 推送，但大家可以手动到 Edge 官网下载安装。

开源是一个非常有效的达到合谋的方式，可以迅速的成就一个「虚拟的寡头」乃至于造成事实上的垄断。高举规范、开源(chromium)旗帜的 Chrome，正在悄悄的成为巨龙．．．

### 为什么选择从这个时候开始写呢？

因为从 chrome95 开始，chrome 会每隔 4 周就会更新一个版本。  
版本更迭之快，从中我们能嗅探到很多前沿的技术趋势，以及未来哪些东西会面临淘汰！

我想在之后的每一个大版本更新后,我都会第一时间把其中的重要资讯总结出来给与大家一起分享学习!

> Chrome 95 测试版于 2021 年 9 月 23 日发布，预计将于 2021 年 10 月下旬成为稳定版本。

## 新版本特性分享

### 新增特性

- **EyeDropper API** EyeDropper API 提供浏览器提供的吸管，用于构建自定义颜色选择器。为 Web 构建的创意应用程序可以受益于从屏幕上的像素中采样颜色的能力。  
  **这个可以在屏幕的任何地方取颜色哦~ 包括浏览器外面的区域,它有什么好处呢?不知道大家用过 figma，或者蓝湖没有,诸如此类的 web 端 ui 设计应用,取色是一个很常用的功能项（包括我们平时开发页面时，不需要借用其他工具去别的窗口提取ＵＩ给出的设计图某块区域的颜色值，现在通过这个特性就能简单的实现！）** 

- **self.reportError API**  
  该全局方法可用于报告错误控制台或全球事件处理程序，模拟一个未捕获的 JavaScript 异常。 [详情](https://developer.mozilla.org/en-US/docs/Web/API/reportError)

  ```js
  var newError = new Error('Some error message', 'someFile.js', 11)
  self.reportError(newError)

  window.onerror = function (message, source, lineno, colno, error) {
    console.log('message:' + error.message + ', lineno: ' + lineno)
    return true
  }

  self.addEventListener('error', (error) => {
    console.log(error.filename)
  })

  // Output
  // > "message:Some error message, lineno: 11"
  // > "someFile.js"
  ```

  > 此功能主要用于自定义事件调度或回调操作库。库可以使用此功能来捕获回调代码中的错误并将它们重新抛出到顶级处理程序。这确保了一个回调中的异常不会阻止其他回调的处理，同时确保堆栈跟踪信息仍可用于顶层调试。

- **URLPattern** 作为一个新的 API，提供操作系统支持，用于将 URL 与提供的模式相匹配。 [详情](https://web.dev/urlpattern/)和[MDN](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern)

- **调试面板中对属性值的便捷修改**

  DevTools 添加了一种更简单而灵活的方式来更新 CSS 中的长度！在“样式”窗格中，查找任何具有长度的 CSS 属性（例如 height, padding）。将鼠标悬停在单位类型上，注意单位类型带有下划线。单击它以从下拉列表中选择一个单位类型。

  

  _配合 chrome 94 支持中文的特性,控制台变的越来越方便_ 😀

- **改进了 DevTools 命令菜单的 UI**

  对于经常在 chrome 中查找页面资源的开发者还是挺有用(ctrl+P 调出) 改进前 

  改进后 

- **在 Console、Sources 面板和 Properties 窗格中对自定义的属性进行加粗和排序**

  

  

  除了这些更改之外，“属性”窗格中的属性现在也被扁平化，以获得更好的 DOM 属性调试体验，尤其是对于[Web components]

  > 对于 `web component` 以后有机会我单独拎一篇文章来讲，大家可以查看[这里](https://www.webcomponents.org/introduction)

  

- **Lighthouse 现在更新到了 8.4**

  我们都知道,网站使用体验的三大核心指标 - LCP, FID, CLS 

  Lighthouse 现在将检测 LCP 元素是否是惰性加载的图像，并建议删除它的 loading 属性。

  > 影响 LCP 的四个因素如下:
  >
  > - 较慢的服务器响应时间
  > - 渲染阻塞的 js 和 css
  > - 较慢的资源加载时间
  > - 客户端渲染

  因为最近一项 WordPress 中延迟加载的最新研究发现，对于某些站点来说，如果初始视图中的图像没有延迟加载，LCP 可以提高 15%。

  详情可以看[这里](https://web.dev/lcp-lazy-loading/)

  

- **最新的 lighthouse 支持通过 npm 下载**

    
  通过命令`lighthouse https://www.example.com --view`  
  

  这里我通过百度为例子,大家替换为自己想测试的站点即可

  

## 删除与弃用

- **删除了 FTP 支持**

  Chrome 正在[取消对 FTP URL 的支持](https://www.chromestatus.com/feature/6246151319715840)。浏览器中 FTP 的使用率足够低，投资改进现有 FTP 客户端已不再可行。此外，所有受影响的平台上都提供了功能更强大的 FTP 客户端。

  Google Chrome 72 及更高版本取消了对通过 FTP 获取文档子资源和呈现顶级 FTP 资源的支持。当前导航到 FTP URL 会根据资源类型显示目录列表或下载。Google Chrome 74 及更高版本中的一个错误导致不再支持通过 HTTP 代理访问 FTP URL。在 Google Chrome 76 中完全删除了对 FTP 的代理支持。在 Chrome 86 中，预发布渠道（Canary 和 Beta）的 FTP 支持被关闭，并且实验性地关闭了百分之一的稳定用户，尽管它可以通过命令行重新启用. 在 Chrome 87 中，它对 50% 的用户关闭，但也可以通过命令行启用。从 Chrome 88 开始，它只能通过弃用试用版获得，现在已被禁用。

- **FTP 支持现在被完全删除** 自 Chrome 88 以来，它只通过废弃试验提供，但现在已经完全取消了。

- **支持以数字结尾的非 IPv4 主机名的 URL**

  大多数不是有效 IPv4 地址但以数字结尾的主机名都被视为有效，并通过 DNS 查找（例如，`http://foo.127.1/`）。根据公共后缀列表规范，该 URL 中主机名的 eTLD+1 应为`127.1`.  如果将其反馈到 URL 中，则由 URL 规范`http://127.1/`映射到`http://127.0.0.1/`，这似乎具有潜在危险。`127.0.0.0.1`也可能被用来迷惑用户。[现在拒绝带有这些主机名的 URL](https://www.chromestatus.com/feature/5679790780579840)。

- **WebAssembly 跨源模块共享**

  [Chrome 现在不赞成](https://www.chromestatus.com/feature/5650158039597056)在跨域但同站点的环境之间[共享 WebAssembly 模块](https://www.chromestatus.com/feature/5650158039597056)，以[允许代理集群长期作用于源](https://developer.chrome.com/blog/wasm-module-sharing-restricted-to-same-origin/)。

- **弃用 U2F API（加密令牌）**

  Chrome 用于与安全密钥交互的旧版 U2F API 已弃用，并在 Chrome 95 中开始弃用试用，其中该 API 默认保持启用状态，但试用令牌将禁用参与站点的密钥。U2F 安全密钥本身并未被弃用，并将继续有效。

  受影响的站点应迁移到[Web 身份验证 API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API)。最初通过 U2F API 注册的凭据可以通过 Web 身份验证进行质询。Web Authentication API 也支持 U2F API 支持的 USB 安全密钥。

  U2F 是 Chrome 的原始安全密钥 API。它允许站点在 USB 安全密钥上注册公钥凭据，并挑战它们以构建防网络钓鱼的两因素身份验证系统。U2F 从未成为开放的 Web 标准，而是被 Web 身份验证 API（在 Chrome 67 中启动）所包含。Chrome 从未直接支持 FIDO U2F JavaScript API，而是提供了一个名为 cryptotoken 的组件扩展，它公开了一个等效的`chrome.runtime.sendMessage()`方法。U2F 和 Cryptotoken 一直处于维护模式，并在过去两年鼓励站点迁移到 Web 身份验证 API。

### chrome 删除与弃用特性的历史列表

> 访问 ChromeStatus.com 以获取[当前弃用](https://www.chromestatus.com/features#browsers.chrome.status%3A%22Deprecated%22)和[以前删除的](https://www.chromestatus.com/features#browsers.chrome.status:%22Removed%22)列表。

## 最后

chrome95 开始，chrome 会每隔 4 周就会更新一个版本，对于喜欢尝鲜的同学们，我推荐下载 chrome 测试版或者金丝雀版本来体验最新的特性．对于一些正在试验中的 API 我们也可以通过下面这个网站提交登记，或者体验资格  


通过填写一些信息后等待审核通过即可，非常的方便！

 

以上就是我对即将推出的 chrome95 的主要新增特性的分享~希望对你有所帮助 ~ 😉

> 我是荣顶，很高兴能在这里和你一起变强！一起**面向快乐编程！** 😉
>
> 如果你也非常热爱前端相关技术！欢迎进入我的小密圈 ~ 🦄 **扫描下方图片~**
>
>

---

## File: posts/01-frontend/一些有用的工具库.md

---
title: 收集的一些好玩的工具
tags:
  - Node
categories:
  - 前端
date: 2021-07-23
---

#### nodejs 交互工具库系列


    点击查看详情:

- colors 处理终端文字样式
- say 终端阅读文字
- fx 终端折叠查看 json
- yargs node.js 中使用 yargs 来处理命令行参数
- chalk-pipe 使用更简单的样式字符串创建粉笔样式方案
- chalk 正确处理终端字符串样式
- Commander.js 完整的 node.js 命令行解决方案
- Inquirer.js 一组通用的交互式命令行用户界面。
- slash 系统路径符处理
- minimist 解析参数选项
- dotenv 将环境变量从 .env 文件加载到 process.env 中
- dotenv-expand 扩展计算机上已经存在的环境变量
- hash-sum 非常快的唯一哈希生成器
- deepmerge 深度合并两个或多个对象的可枚举属性。
- yaml-front-matter 解析 yaml 或 json
- resolve 实现 node 的 require.resolve()算法，这样就可以异步和同步地使用 require.resolve()代表文件
- semver npm 的语义版本器
- leven 测量两字符串之间的差异 ,最快的 JS 实现之一
- lru cache 删除最近最少使用的项的缓存对象
- portfinder 自动寻找 8000 至 65535 内可用端口号
- ora 优雅的终端转轮
- envinfo 生成故障排除软件问题(如操作系统、二进制版本、浏览器、已安装语言等)时所需的通用详细信息的报告
- memfs 内存文件系统与 Node's fs API 相同实现
- execa 针对人类的流程执行
- webpack-merge 用于连接数组和合并对象，从而创建一个新对象
- webpack-chain 使用链式 API 去生成简化 webpack 版本配置的修改
- strip-ansi 从字符串中去掉 ANSI 转义码
- address 获取当前机器的 IP, MAC 和 DNS 服务器。
- default-gateway 通过对 OS 路由接口的 exec 调用获得机器的默认网关
- joi JavaScript 最强大的模式描述语言和数据验证器。
- fs-extra 添加了未包含在原生 fs 模块中的文件系统方法，并向 fs 方法添加了 promise 支持
- Acorn 一个小而快速的 JavaScript 解析器，完全用 JavaScript 编写。
- zlib.js ZLIB.js 是 ZLIB(RFC1950)， DEFLATE(RFC1951)， GZIP(RFC1952)和 PKZIP 在 JavaScript 实现。
- node-jq 重量轻且灵活的命令行处理器 JSON



#### npm 自用工具库收集


    点击查看详情:

- get-pixels 给定一个 URL/路径，获取图像中的所有像素并将结果作为 ndarray 返回



#### Jenkins 工具


    点击查看详情:

- webhooks 远程构建触发插件 Generic Webhook Trigger



#### 其他的工具


    点击查看详情:

- OCR:
  - Tesseract.js 和 ocrad.js 两个 js 库 http://www.manongjc.com/article/66483.html
- 整篇中文文档翻译成全英文 http://www.360doc.com/content/18/0612/23/45959908_761846083.shtml
- 天气 gif 图 https://weather-icon.journeyad.repl.co/@beijing
- 二次元访客量 https://count.getloli.com/get/@:wangrongding
- 访客量统计 https://visitor-badge.glitch.me/badge?page_id=wangrongding.wangronding
- psr.exe windows 键+r 键>输入 psr.exe  记录的步骤就会出现在图示中的对话框中，每一个步骤都有详细的记录。 
- Terminalizer 记录您的终端并生成动画 gif 图像或共享网络播放器链接 terminalizer.com 



##### 不定期更新中...

---

## File: posts/01-frontend/代理 https 热更新失效的解决方案.md

---
title: 代理 https 热更新失效的解决方案
date: 2019-08-01
tags:
  - webpack
categories:
  - 前端
---

我们一般的开发步骤都是，使用 vue-cli3 开启本地服务，然后使用 whistle 代理到正式的网址。然后就会出现热更新失效。报错`/sockjs-node/info?t=`

## 一、首次尝试

查看 webpack 文档，发现有一个 https 的选项



配置为`true` 后发现开始还有用，后面就没用了。报证书非法的问题。



## 二、最终方案

经 google，发现了一个新方案。使用 devServer.public 属性。



经测试有效。

相关链接如下：

vue-cli3 热更新失效问题

项目运行时一直发 `http://localhost:8080/sockjs-node/info?t=1462183700002` 请求

---

## File: posts/01-frontend/前端JS获取内网IP.md

---
title: 前端JS获取内网IP
date: 2019-01-21
tags:
  - WebRTC
categories:
  - 前端
---

```javascript
//方法一:
function getUserIP() {
  var RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection
  if (RTCPeerConnection)
    (() => {
      var rtc = new RTCPeerConnection()
      rtc.createDataChannel('') //创建一个可以发送任意数据的数据通道
      rtc.createOffer(
        (offerDesc) => {
          //创建并存储一个sdp数据
          rtc.setLocalDescription(offerDesc)
        },
        (e) => {
          console.log(e)
        },
      )
      rtc.onicecandidate = (evt) => {
        //监听candidate事件
        if (evt.candidate) {
          var ip_addr = evt.candidate.address
          localStorage.setItem('ip_addr', ip_addr)
        }
      }
    })()
  else {
    console.log('目前仅测试了chrome浏览器OK')
  }
}

//方法二:

function getUserIP(onNewIP) {
  let MyPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection
  let pc = new MyPeerConnection({
    iceServers: [],
  })
  let noop = () => {}
  let localIPs = {}
  let ipRegex = /(\[0-9\]{1,3}(\\.\[0-9\]{1,3}){3}|\[a-f0-9\]{1,4}(:\[a-f0-9\]{1,4}){7})/g
  let iterateIP = (ip) => {
    if (!localIPs[ip]) onNewIP(ip)
    localIPs[ip] = true
  }
  pc.createDataChannel('')
  pc.createOffer()
    .then((sdp) => {
      sdp.sdp.split('\\n').forEach(function (line) {
        if (line.indexOf('candidate') < 0) return
        line.match(ipRegex).forEach(iterateIP)
      })
      pc.setLocalDescription(sdp, noop, noop)
    })
    .catch((reason) => {})
  pc.onicecandidate = (ice) => {
    if (!ice || !ice.candidate || !ice.candidate.candidate || !ice.candidate.candidate.match(ipRegex)) {
      return ice.candidate.candidate.match(ipRegex).forEach(iterateIP)
    }
  }
}

function getUserIP(onNewIP) {
  let MyPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection
  let pc = new MyPeerConnection({
    iceServers: [],
  })
  let noop = () => {}
  let localIPs = {}
  let ipRegex = /([0-9]{1,3}(\.[0-9]{1,3}){3}|[a-f0-9]{1,4}(:[a-f0-9]{1,4}){7})/g
  let iterateIP = (ip) => {
    if (!localIPs[ip]) onNewIP(ip)
    localIPs[ip] = true
  }
  pc.createDataChannel('')
  pc.createOffer()
    .then((sdp) => {
      sdp.sdp.split('\n').forEach(function (line) {
        if (line.indexOf('candidate') < 0) return
        line.match(ipRegex).forEach(iterateIP)
      })
      pc.setLocalDescription(sdp, noop, noop)
    })
    .catch((reason) => {})
  pc.onicecandidate = (ice) => {
    if (!ice || !ice.candidate || !ice.candidate.candidate || !ice.candidate.candidate.match(ipRegex)) return
    ice.candidate.candidate.match(ipRegex).forEach(iterateIP)
  }
}
```

---

## File: posts/01-frontend/我用 10000 张图片合成我们美好的瞬间.md

[Binary file]

---

## File: posts/01-frontend/打造舒适的前端调试体验.md

---
title: 打造舒适的前端调试体验（持续更新）
date: 2023-07-25
tags:
  - Chrome
categories:
  - 前端
---

# 打造舒适的前端调试体验

经常使用 Chrome 调试，但是有些功能一直没有用到，这里记录一下。

## Chrome 多版本兼容性测试

当我们需要通过旧版 Chrome 用以兼容性测试时，往往需要运行多个版本的 Chrome，但是实际不管在 Mac 还是 Windows 上，我们正常只能安装一个 Chrome，而且只能是最新版的，所以可能需要通过虚拟机或者覆盖本机的 Chrome 才可以使用旧版的，这里分享一种更实用且方便的方法。

### 下载历史版本的 Chrome

Chrome 浏览器主要包括 Stable 正式版、Beta 测试版、DEV 开发版、Canary 金丝雀版以及鼻祖 Chromium 版 。

从稳定性方面: Stable > Beta > DEV > Canary > Chromium  
更新的速度: Chromium > Canary > DEV > Beta > Stable。

https://www.chromium.org/getting-involved/dev-channel/

官方的：

- [Chromium History Versions Download](https://vikyd.github.io/download-chromium-history-version/#/)
- [Chrome 离线包 - 最新版（官方）](https://www.google.com/intl/en/chrome/browser/desktop/index.html?standalone=1)
- [历史版本（官方）](https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html)
- [非官方](https://crportable.sourceforge.net/releases.html)

第三方：

- [Chrome Win](https://www.chromedownloads.net/chrome64win/)
- [Chrome Mac](https://www.chromedownloads.net/chrome64osx-stable/)
- https://google-chrome.cn.uptodown.com/mac/versions

我一般用这个：

- [非官方](https://www.slimjet.com/chrome/google-chrome-old-version.php)

历史版本 Firefox:

- https://liulanmi.com/firefox

### 在 Mac 上运行不同版本的 Chrome

一般直接打开下载后的低版本 chrome，查看版本信息，会发现还是最新的。  
为了不影响现有的 chrome，避免了来回删除、可以使用命令行的方式运行不同版本的 chrome。

```bash
# 以 81 版本为例
# windows
open -n /Applications/Google\ Chrome\ 81.app/ --args --disable-web-security --user-data-dir=/Users/xxx/Downloads/chrome81
# mac
/Applications/Chrome81.app/Contents/MacOS/Google\ Chrome --user-data-dir=$HOME/chrome-profile --chrome-version=81.0.4044.92
/Applications/Chrome81.app/Contents/MacOS/Google\ Chrome --chrome-version=81.0.4044.92

/Applications/Google\ Chrome\ 2.app/Contents/MacOS/Google\ Chrome --chrome-version=68.0.3440.106
# /Applications/Google\ Chrome\ 2.app/Contents/MacOS/Google\ Chrome --user-data-dir=$HOME/chrome-profile  --chrome-version=68.0.3440.106
# /Applications/Google\ Chrome\ 2.app/Contents/MacOS/Google\ Chrome —-user-data-dir="/Users/wangrongding/Library/Application Support/Google/Chrome2"  --chrome-version=68.0.3440.106
/Applications/Google\ Chrome\ 2.app/Contents/MacOS/Google\ Chrome —-user-data-dir="$HOME/Library/Application Support/Google/Chrome2"  --chrome-version=68.0.3440.106
/Applications/Google\ Chrome\ 3.app/Contents/MacOS/Google\ Chrome —-user-data-dir="$HOME/Library/Application Support/Google/Chrome3"  --chrome-version=65.0.3325.181
# 如果遇到开发者权限的问题，可以使用
sudo xattr -rd com.apple.quarantine 你的应用程序路径（把应用拖进终端就可以）
```

- `/Applications/Chrome81.app/Contents/MacOS/Google\ Chrome` 指定要打开的应用程序在系统中的位置，可以把 Chrome 拖到终端里查看路径
- `--user-data-dir=%HOME/chrome-profile` 指定用户数据存放的路径
- `--chrome-version=81.0.4044.92` 指定 Chrome 版本

效果：



使用后的副作用：(需要重新验证，一些网站的登录态会被全部清空)



### 限制 Chrome 的使用内存

```sh
sudo open /Applications/Google\ Chrome.app --args --js-flags="--max_old_space_size=512"
```

chrome args : https://peter.sh/experiments/chromium-command-line-switches/

## Chrome DevTools

### Snippets

- [Chrome DevTools Snippets](https://bgrins.github.io/devtools-snippets/)

添加 Snippets：

运行 Snippets

在 Snippets 面板中运行或者直接用 `cmd + p` 通过 ！ 快速运行

### 浏览器中的自动化测试

Recorder 是一个 Chrome 新出的 devtool，可以录制用户在浏览器中的操作，然后生成代码，可以用于自动化测试。

### 控制台

1. 自定义 console 格式化输出：

2. 好用的 console 方法：

- console.dir：打印对象的属性
- console.table：打印对象的属性
- console.trace 方法：打印当前调用栈

3. 控制台中的 source map

- 生成 sourcemap
- ignore script files

4. 实时表达式

5. network initiator

查看整个请求的调用栈

source

## 代理

- [使用 Charles 创建本地代理服务器](https://blog.csdn.net/huikaichedeyeren/article/details/124761008)
- 使用 whistle 代理工具
- 安装 SwitchyOmega 插件，配置代理

---

## File: posts/01-frontend/每一个用到canvas的小伙伴都应该了解的fabric.js.md

---
title: 每一个用到canvas的小伙伴都应该了解的fabric.js
date: 2021-08-07
tags:
  - canvas
categories:
  - 前端
---

## 导语

我们想在画布上画个条基本的简单形状的时候，使用 Canvas 不会觉得有什么繁琐。但当画布上需要任何形式的互动，绘制复杂的图形和在特定情况需要改变图片的时候,使用原生 canvas API 将会变得很困难。  
而 Fabric 旨在解决这个问题。

> Fabric.js 是一个强大而简单的 Javascript HTML5 画布库 Fabric 在画布元素之上提供交互式对象模型 Fabric 还具有 SVG-to-canvas（和 canvas-to-SVG）解析器





    为了方便,下面我将通过 vue项目 为大家讲解如何使用 Fabric

## 一、 安装

```sh
yarn add fabric -S
#or
npm i fabric -S
```

也可以在 [官网](https://cdnjs.cloudflare.com/ajax/libs/fabric.js/4.5.0/fabric.min.js) 下载最新 js 文件,通过 script 标签引入

## 二、 使用

```html
<!-- html -->
<canvas id="canvas" width="500" height="500"></canvas>
```

### 2.1 绘制一个简单的图形

> Fabric 提供了 7 种基础形状：
>
> - fabric.Circle (圆)
> - fabric.Ellipse (椭圆)
> - fabric.Line (线)
> - fabric.Polyline (多条线绘制成图形)
> - fabric.triangle (三角形)
> - fabric.Rect (矩形)
> - fabric.Polygon (多边形)

- 矩形

```javascript
// js

//引入fabric
import { fabric } from 'fabric'

// 创建一个fabric实例
let canvas = new fabric.Canvas('canvas') //可以通过鼠标方法缩小,旋转
// or
// let canvas = new fabric.StaticCanvas("canvas");//没有鼠标交互的fabric对象

// 创建一个矩形对象
let rect = new fabric.Rect({
  left: 200, //距离左边的距离
  top: 200, //距离上边的距离
  fill: 'green', //填充的颜色
  width: 200, //矩形宽度
  height: 200, //矩形高度
})

// 将矩形添加到canvas画布上
canvas.add(rect)
```

可以看到界面中填充了一个可以通过鼠标放大缩小且可以旋转的绿色矩形  
通过对象的形式配置元素样式,非常的方便!



- 圆形和三角形

```javascript
// 创建一个圆形对象
let circle = new fabric.Circle({
  left: 0, //距离左边的距离
  top: 0, //距离上边的距离
  fill: 'red', //填充的颜色
  radius: 50, //圆的半径
})
// 创建一个三角形对象
let triangle = new fabric.Triangle({
  left: 200, //距离左边的距离
  top: 0, //距离上边的距离
  fill: 'blue', //填充的颜色
  width: 100, //宽度
  height: 100, //高度
})
// 将图形形添加到canvas画布上
canvas.add(circle, triangle)
```



我们可以通过以下属性设置,决定是否可以对相关元素进行交互

```javascript
canvas.selection = false // 禁止所有选中
rect.set('selectable', false) // 只是禁止这个矩形选中
```

### 2.2 绘制图片

主要有通过 url 和 img 标签绘制两种方式

```javascript
//通过url绘制图片
fabric.Image.fromURL(
  //本地图片需要通过require来引入,require("./xxx.jpeg")
  'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg.thaihot.com.cn%2Fuploadimg%2Fico%2F2021%2F0711%2F1625982535739193.jpg&refer=http%3A%2F%2Fimg.thaihot.com.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1630940858&t=e1d24ff0a7eaeea2ff89cedf656a9374',
  (img) => {
    img.scale(0.5)
    canvas.add(img)
  },
)
//也可以通过标签绘制
let img = document.getElementById('img')
let image = new fabric.Image(img, {
  left: 100,
  top: 100,
  opacity: 0.8,
})
canvas.add(image)
```



### 2.3 通过自定义的路径绘制

在此之前我们需要了解几个参数的含义

- M : “move”移动到某点
- L : “line”画线 x,y
- C : “curve”曲线
- A : “arc”弧
- z : 闭合路径(类似 PS 中的创建选区)

```javascript
let customPath = new fabric.Path('M 0 0 L 300 100 L 170 100  z')
customPath.set({
  left: 100,
  top: 100,
  fill: 'green',
})
canvas.add(customPath)
```



```javascript
let customPath = new fabric.Path('M 0 0 L 300 100 L 170 100 L 70 300 L 20 200 C136.19,2.98,128.98,0,121.32,0 z')
```



可以看到通过路径绘制,我们可以制作非常复杂的图形(但是一般用不到,我们一般用它来解析 SVG 后拿到 path 复原图形)

### 2.4 动画

第一个参数是动画的属性，第二个参数是动画的最终位置，第三个参数是一个可选的对象，指定动画的细节：持续时间，回调，动效等。

> 第三个参数主要有
>
> - duration 默认为 500ms。可以用来改变动画的持续时间。
> - from 允许指定动画属性的起始值（如果我们不希望使用当前值）。
> - onComplete 动画结束之后的回调。
> - easing 动效函数。

#### 2.41 绝对动画

```javascript
let canvas = new fabric.Canvas('canvas')
let rect = new fabric.Rect({
  left: 400, //距离左边的距离
  top: 200, //距离上边的距离
  fill: 'green', //填充的颜色
  width: 200, //宽度
  height: 200, //高度
})
rect.animate('left', 100, {
  onChange: canvas.renderAll.bind(canvas),
  duration: 1000,
})
canvas.add(rect)
```



#### 2.42 相对动画(第二个参数通过+=,-=等来决定动画的最终效果)

```javascript
rect.animate('left', '+=100', {
  onChange: canvas.renderAll.bind(canvas),
  duration: 1000,
})
```



```javaScript
rect.set({ angle: 45 });
rect.animate("angle", "-=90", {
onChange: canvas.renderAll.bind(canvas),
duration: 2000,
});
```



#### 2.43 定义动画的动效函数

默认情况下，动画使用“easeInSine”动效执行。如果这不是你需要的，fabric 为我们提供了很多内置动画效果, fabric.util.ease 下有一大堆动效的选项。  
常用的有`easeOutBounce`,`easeInCubic`，`easeOutCubic`，`easeInElastic`，`easeOutElastic`，`easeInBounce` 和 `easeOutExpo`等

```javascript
rect.animate('left', 100, {
  onChange: canvas.renderAll.bind(canvas),
  duration: 1000,
  easing: fabric.util.ease.easeOutBounce,
})
```



### 2.5 图像滤镜

目前 Fabric 为我们提供了以下内置滤镜

- BaseFilter 基本过滤器
- Blur 模糊
- Brightness 亮度
- ColorMatrix 颜色矩阵
- Contrast 对比
- Convolute 卷积
- Gamma 伽玛
- Grayscale 灰度
- HueRotation 色调旋转
- Invert 倒置
- Noise 噪音
- Pixelate 像素化
- RemoveColor 移除颜色
- Resize 调整大小
- Saturation 饱和

#### 2.51 单个滤镜

```javascript
fabric.Image.fromURL(require('./aaa.jpeg'), (img) => {
  img.scale(0.5)
  canvas.add(img)
})
fabric.Image.fromURL(require('./aaa.jpeg'), (img) => {
  img.scale(0.5)
  // 添加滤镜
  img.filters.push(new fabric.Image.filters.Grayscale())
  // 图片加载完成之后，应用滤镜效果
  img.applyFilters()
  img.set({
    left: 300,
    top: 250,
  })
  canvas.add(img)
})
```



#### 2.52 叠加滤镜

> “filters”属性是一个数组，我们可以用数组方法执行任何所需的操作：移除滤镜（pop，splice，shift），添加滤镜（push，unshift，splice），甚至可以组合多个滤镜。当我们调用 applyFilters 时，“filters”数组中存在的任何滤镜将逐个应用，所以让我们尝试创建一个既色偏又明亮（Brightness）的图像。

```javascript
fabric.Image.fromURL(require('./aaa.jpeg'), (img) => {
  img.scale(0.5)
  // 添加滤镜
  img.filters.push(
    new fabric.Image.filters.Grayscale(),
    new fabric.Image.filters.Sepia(), //色偏
    new fabric.Image.filters.Brightness({ brightness: 0.2 }), //亮度
  )
  // 图片加载完成之后，应用滤镜效果
  img.applyFilters()
  img.set({
    left: 300,
    top: 250,
  })
  canvas.add(img)
})
```



可以看到多个滤镜的效果叠加显示了,当然 Fabric 还支持自定义滤镜,在本篇文章点赞过 500 后我将更新 fabric 高级篇,感谢大家的支持~

### 2.6 颜色

> 无论你是使用十六进制，RGB 或 RGBA 颜色，Fabric 都能处理的很好

#### 2.61 定义颜色

```javascript
new fabric.Color('#f55')
new fabric.Color('#aa3123')
new fabric.Color('356333')
new fabric.Color('rgb(100,50,100)')
new fabric.Color('rgba(100, 200, 30, 0.5)')
```

#### 2.62 颜色转换

```javaScript
new fabric.Color('#f55').toRgb(); // "rgb(255,85,85)"
new fabric.Color('rgb(100,100,100)').toHex(); // "646464"
new fabric.Color('fff').toHex(); // "FFFFFF"
```

我们还可以用另一种颜色叠加，或将其转换为灰度版本。

```javascript
let redish = new fabric.Color('#f55')
let greenish = new fabric.Color('#5f5')
redish.overlayWith(greenish).toHex() // "AAAA55"
redish.toGrayscale().toHex() // "A1A1A1"
```

### 2.7 渐变

Fabric 通过 setGradient 方法支持渐变，在所有对象上定义。调用 setGradient('fill', { ... })就像设置一个对象的“fill”值一样。

```javaScript
let circle = new fabric.Circle({
  left: 100,
  top: 100,
  radius: 50
});

circle.setGradient("fill", {
    // 渐变开始的位置
    x1: 0,
    y1: 0,
    // 渐变结束的位置
    x2: circle.width,
    y2: 0,
    //渐变的颜色
    colorStops: {
        // 渐变的范围(0,0.1,0.3,0.5,0.75,1)0-1之间都可以
        0: "red",
        0.2: "orange",
        0.4: "yellow",
        0.6: "green",
        0.8: "blue",
        1: "purple"
    },
});
```



### 2.8 文本

fabric.Text 对象对于文本,提供了比 canvas 更丰富的功能，包括：

> - 支持多行 Multiline support 不幸的是，原生文本方法忽略了新建一行。
> - 文本对齐 Text alignment 左，中，右。使用多行文本时很有用。
> - 文本背景 Text background 背景也支持文本对齐。
> - 文字装饰 Text decoration 下划线，上划线，贯穿线。
> - 行高 Line Height 在使用多行文本时有用。
> - 字符间距 Char spacing 使文本更紧凑或更间隔。
> - 子范围 Subranges 将颜色和属性应用到文本对象的子对象中。
> - 多字节 Multibyte 支持表情符号。
> - 交互式画布编辑 On canvas editing 可以直接在画布上键入文本。

```javascript
let text = new fabric.Text('大家好~这里是前埔寨\n我是荣顶~\n一个要成为开发王的男人!', {
  left: 0,
  top: 200,
  fontFamily: 'Comic Sans', //字体
  fontSize: 50, //字号
  fontWeight: 800, //字体粗细,可以使用关键字（“normal”，“bold”）或数字（100,200,400,600,800）
  shadow: 'green 3px 3px 2px', //文字阴影,颜色，水平偏移，垂直偏移和模糊大小。
  underline: true, //下划线
  linethrough: true, //删除线
  overline: true, //上划线
  fontStyle: 'italic', //字体风格,normal（正常）或italic（斜体）
  stroke: '#c3bfbf', //描边的颜色
  strokeWidth: 1, //描边的宽度
  textAlign: 'center', //文本对齐方式
  lineHeight: 1.5, //行高
  textBackgroundColor: '#91A8D0', //文本背景颜色
})
canvas.add(text)
```



### 2.9 事件

fabric 中通过 on 方法来初始化事件，off 方法用来删除事件。

常用的事件有以下

- “mouse:down” 鼠标被按下
- “object:add” 对象被添加
- “after:render” 渲染完成

> 还有一大堆:  
> 鼠标事件:“mouse:down” ,“mouse:move”和“mouse:up...” 选择相关的事件：“before:selection:cleared”, “selection:created”, 详细的可以查看 [官方文档](http://fabricjs.com/events)

```javascript
canvas.on('mouse:down', function (options) {
  canvas.clear()
  let text = new fabric.Text('你点我啦~', {
    left: 200,
    top: 200,
  })
  canvas.add(text)
  console.log(options.e.clientX, options.e.clientY)
})
canvas.on('mouse:up', function (options) {
  this.text = '你没点我0.0'
  canvas.clear()
  let text = new fabric.Text('你没点我0.0', {
    left: 200,
    top: 200,
  })
  canvas.add(text)
  console.log(options.e.clientX, options.e.clientY)
})
```



Fabric 允许将侦听器直接附加到 canvas 画布中的对象上。

```javascript
let rect = new fabric.Rect({ width: 100, height: 50, fill: 'green' })
rect.on('selected', function () {
  console.log('哦吼~你选择了我')
})

let circle = new fabric.Circle({ radius: 75, fill: 'blue' })
circle.on('selected', function () {
  console.log('哈哈哈~你选择了我')
})
```

### 3.0 自由绘画

Fabric canvas 的 isDrawingMode 属性设置为 true 即可实现自由绘制模式.  
这样画布上的点击和移动就会被立刻解释为铅笔或刷子。

```javascript
let canvas = new fabric.Canvas('canvas')
canvas.isDrawingMode = true
canvas.freeDrawingBrush.color = 'blue'
canvas.freeDrawingBrush.width = 5
```



### 三、最后

很开心写下这篇文章,它是我用来总结归纳 fabric 的知识点并且非常用心的一篇文章,希望这篇文章对你有所帮助,目前 fabric 在国内还不是很火,但是 github 上已经有 19.2k 的 star 了,也算是一个明星项目.我们日常开发经常会用到 canvas,但是它的 api 对于处理复杂的业务逻辑会令人感到非常的劳累,所以我分享这篇文章,希望对大家有所帮助,点赞超过 500 我会更新 fabric.js 高级篇,感谢你的支持!

> 很高兴可以和大家一起变强！可以关注我的公众号，前埔寨。我是荣顶，和我一起在键帽和字符上横跳，在代码与程序中穿梭。

---

## File: posts/01-frontend/简单的防止你的程序被他人恶意调试.md

---
title: 简单的防止你的程序被他人恶意调试
date: 2021-09-01
tags:
  - JavaScript
categories:
  - 前端
---



## 前言

看到社区很多都在讨论**如何调试,如何高级的调试,以及一些调试的奇技淫巧**,今天我想和大家聊聊,**怎么禁止调试,禁止他人调试我们的程序**

为什么会有这篇文章呢,源自一次我寻找盗版电影的遭遇,一次好奇心的驱使下,由于很多这种平台都是只做搬运,不做存储,因为存储盗版电影向他人提供是违法的,特别是那种刚出的新电影! 当时好奇想通过看某站的控制台,想了解一下他们是怎么是通过啥接口,怎么请求,请求来的格式啥样的,抱着这样的好奇心,开始了我的奇妙之旅...



**看完本篇文章你将学会**  
我无法断定你能学到什么,但是以下是我希望你能从本篇文章中学到的:

1. 如何简单的防止你的程序被他人恶意调试
2. 逆向思维学会如何更好的调试

## 具体实现

防止调试的方法,这里我们主要是通过不断`debugger`的方法来疯狂输出断点,让控制台打开后程序就无法正常执行  
我们都知道`debugger`只有在控制台被打开的时候才会执行,所以后面的所有方法都是围绕着这一特性来进行,废话不多说,我将通过以下几个案例向你们展示**道高一尺魔高一丈**的道理,先上代码:

### 方法一:

```js
;(() => {
  function block() {
    setInterval(() => {
      debugger
    }, 50)
  }
  try {
    block()
  } catch (err) {}
})()
```

通过上方的代码我们可以看到,在页面中打开控制台后,会有以下结果:



需要在这里说明以下几点:

1. 程序被`debugger`阻泄了,我们无法像以往一样在 Source Tab 中的对应 js 代码处添加断点调试,无法调试程序的执行逻辑.在程序异常复杂且被混淆后的代码是异常难读的!通常我们会在 source 的左边加上 `breakpoint` 来让程序每次走到加点的地方停下来,以便让我们查看一些变量的值或是步骤的流程逻辑(如下图所示)



2. 我们都知道,第一次打开控制台是看不到 network tab 中的任何请求的,所以我们想通过 network tab 来查看网页都做了哪些请求,也是看不到的,当我们打开控制台就会出`debugger`阻挡我们,我们可以通过下面的解决方法来处理,或者是用抓包工具来查看具体的请求

**大家可以先不看解决方法,想想如果是你,这个时候怎么突破这个屏障呢?** 第一次遇到这种情况我也是很懵,不知道咋处理,后面发现问题简直不要太简单,我们可以带着疑问来看:

**对于第一个示例,我们如何解决?(绕过它)**



**答案是: 禁止断点**

可以看到很简单,在 Chrome 控制台的 Source Tab 页点击 Deactivate breakpoints 按钮或者按下 Ctrl + f8(如下图所示)。但是对于控制台不熟悉的小伙伴,很难会想到这里去.



但是,难道这篇文章就这样结束了?那我可顶不住小伙伴们的 **"就这?"** 😂

其实,上面的解决方法并没有帮我们解决根本问题,**我们需要做的是调试,上面虽然把`debugger`都去掉了,但是我们也无法在通过点击每一行代码左边的行号添加 `breakpoint` 了**,所以根本性的问题,并没有解决,只是去除了那碍眼的疯狂 debugger,我们还是得另辟蹊径

### 方法二:

对对应的代码行,通过添加`logpoint`为 false，然后按回车后刷新网页，发现成功跳过无限 debugger,于是我们就可以愉快的自由调试了~



**对应的还有一种方法**

即通过`add script ignore list`来添加需要忽略执行代码行或文件

  
**可以看到,我们也可以通过删除 script ignore list 里已添加的忽略代码,恢复初始状态**



**但是,你这么聪明,那人家不得想想对策?**

**对于上面的第一个方法** 🎈

将`setInterval(() => {debugger;}, 50);`写在一行中,你即使通过添加`logpoint`为 false,也没用,仍然是疯狂 debugger,即使你可能想到,通过左下角的代码格式化,来格式一下`setInterval(() => {debugger;}, 50);`将它变成多行的,也是没用的,仍然会在刷新后重新弹 debugger

```js
;(() => {
  function block() {
    setInterval(() => {
      debugger
    }, 50)
  }
  try {
    block()
  } catch (err) {}
})()
```



**对于第二个方法,我们对代码进行如下改造**😬

```js
;(() => {
  function block() {
    setInterval(() => {
      Function('debugger')()
    }, 50)
  }
  try {
    block()
  } catch (err) {}
})()
```

我们可以通过将`debugger`改写成`Function("debugger")();`的形式,来应对;Function 构造器生成的 debugger 会在每一次执行时开启一个临时 js 文件,哈哈~对方表示好无奈 😅

**于是会有以下结果**



这无限套娃,真够狠的,我们要坚信**正义最后总会胜利**,不能给想非法调试我们程序的人机会,所以我们要把各种情况都考虑周全,**可以说这种方法是最恨的,但是这还不算完~** (好家伙~ 😀 想非法调试我程序,那你就得战胜我)

### 强化以上方法

上面的代码由于没有加密混淆,多少可能还是会被别人读一些,那么我们加密混淆看看是啥样的  
好家伙,你这咋读?

```
eval(function(c,g,a,b,d,e){d=String;if(!"".replace(/^/,String)){for(;a--;)e[a]=b[a]||a;b=[function(f){return e[f]}];d=function(){return"\\w+"};a=1}for(;a--;)b[a]&&(c=c.replace(new RegExp("\\b"+d(a)+"\\b","g"),b[a]));return c}('(()=>{1 0(){2(()=>{3("4")()},5)}6{0()}7(8){}})();',9,9,"block function setInterval Function debugger 50 try catch err".split(" "),0,{}));
```

格式化后的样子



我们继续对代码进行改造,让对方尽量的难以识别我们的代码  
将`Function("debugger").call()`改成`(function(){return false;})["constructor"]("debugger")["call"]();` 并且,添加条件,当窗口外部宽高,和内部宽高的差值大于一定的值,我把 body 里的内容全部清空掉,看你还能不能操作我的按钮啊啥的~哈哈哈 😀





**需要特别说明的是**: 像 toG 的项目或者是一些为了保护自己的版权又或者是一些比较敏感的项目,出于安全的考虑在部署到生产环境后最好是不让别人调试的,当然,前端所做的也就那么一些,需要前后端一起配合,便可以很好的对项目或者数据进行私密的保护 🧡

**最后: 附上这份未混淆的来之不易的的代码(记得混淆后使用哦~)** 😢 一定要记得点赞加关注~原创太不容易了.

你可以把它当作你的工具函数,在需要不让别人轻易调试的项目中引用 **(当然它仍然有很多漏洞,但是我们可以用类似的逻辑方法,变化出很多更复杂的限制,最暴力的甚至可以当控制台打开后就立马通过 window.close();来关闭调试窗口)**,欢迎大家在评论区留下更好玩的方法~共同学习 ✨

```js
;(() => {
  function block() {
    if (window.outerHeight - window.innerHeight > 200 || window.outerWidth - window.innerWidth > 200) {
      document.body.innerHTML = '检测到非法调试,请关闭后刷新重试!'
    }
    setInterval(() => {
      ;(function () {
        return false
      })
        ['constructor']('debugger')
        ['call']()
    }, 50)
  }
  try {
    block()
  } catch (err) {}
})()
```

## 推荐一个调试页面的小技巧

说了那么多的防止被人调试,那么最后也说一个本人觉得眼前一亮的调试样式的方法  
通过给`style`标签添加`style="display: block"`,`contenteditable`两个属性实现在页面中便捷的调试样式



复制下方代码到你的 html 文件中,玩一下~

```html
<!DOCTYPE html>
<body>
  <div>来调试我吧~</div>
  <style style="display: block" contenteditable>
    body {
      background-color: rgb(140, 209, 230);
      color: white;
    }
    div {
      background-color: green;
      width: 300px;
      height: 300px;
      line-height: 300px;
      text-align: center;
    }
  </style>
</body>
```

## 最后

我所知道的禁止调试的方法就只有如上所述,但是肯定还有很多好玩的,小伙伴们可以在评论区留言,一起共同学习~

**最后抛出一个问题,如何监测控制台是否被打开(我上面提到的通过),网上找了一些方法,貌似都没用,感兴趣且有头绪,或者已经有方法的小伙伴可以小伙伴可以在评论下方说说自己的想法,也可以加我们交流群一起玩耍~**

> 关注我的公众号 **前埔寨**，获取更多好玩与有趣的前端知识。**如果你也一样对技术热爱并且为之着迷,欢迎加我个人微信（ChicSparrow），将会邀请你加入我们的前端交流学习群一起面向快乐编程~** 🦄  
> 我是荣顶，在这个互联网技术疯狂快速迭代的时代中,很高兴能和你一起变强！😉

---

## File: posts/01-frontend/讲讲端智能.md

---
title: 端智能的实现与应用
date: 2024-02-05
tags:
  - AI
categories:
  - 前端
---

端智能是指在终端设备上运行的智能应用程序，这些应用程序利用终端设备的计算能力、存储能力等资源，而不依赖外部的服务端来实现一些智能化的功能。  
目前我所了解到的，主要以前端技术栈为主，比如 TensorFlow.js、onnxruntime-web 等。这两个都能够实现在 Web 端或本地 Node 服务上进行机器学习模型的训练和部署。

当然端智能只是一个概念，其它技术栈的 GUI 程序也可以实现端智能的功能，比如 Python 的 PyQt、C++ 的 Qt 等。这块由于专业知识有限，我还没有涉及过。

## 先说端智能的挑战

我认为目前端智能的挑战包括但不限于以下几点：

- 不好存储较大的模型（加载时间长，占用内存大 😅）
- 端智能设备的计算能力有限, 运行复杂的模型不友好，不适用于 ToC ,因为用户的机器参差不齐，不好保证较好的用户体验, (不过这块主要是非常的节约成本，因为不需要再去购买昂贵的服务器 👍)

## 实现

这里我主要通过 TensorFlow.js、onnxruntime-web 的两个方向来讲一下。

### 实现 1：TensorFlow.js

TensorFlow.js 是一个用于使用 JavaScript 进行机器学习开发的库。

它可以使用现成的 JavaScript 模型，也可以转换我们 AI 团队的 Python TensorFlow 模型用来在浏览器中或 Node.js 下运行。

由于我主攻前端方向，缺乏机器学习的相关经验，我在这块的造诣只停留在使用 KNN 分类做识别手写数字或者调用二次封装的模型类库实现诸如上面偏应用层的程度。



觉得这块比较好玩，主要是用 TensorFlow.js 官方已经训练好的模型，或者二次封装的类库来做一些图像处理的功能，比如实时处理摄像头的画面，图片，视频，gif 等

比如我前年做的一个 [demo-姿态识别](https://juejin.cn/post/7171711316422295560#heading-10)



这个姿态识别的 demo 就是是基于 TensorFlow.js 实现的，里面用的是 PoseNet, 当然这方面的模型有很多，你完全可以选择适用于自己场景的，MoveNet, PoseNet, BlazePose...

这个具体实现可以看这篇文章：[WebRTC + Tensorflow.js 在运动健康类项目中的前端应用](https://juejin.cn/post/7171711316422295560)

或者眼动识别示例：

- [眼动识别-图像](https://git.fedtop.com/tensorflow/eye-tracker-image)
- [眼动识别-摄像头](https://git.fedtop.com/tensorflow/eye-tracker-video)

仓库地址： https://github.com/wangrongding/frontend-park



感兴趣的同学也可以去下载一个 MNIST 数据集，对于想要学习和测试相关算法，这是一个很好的数据库。

MNIST 数据集：http://yann.lecun.com/exdb/mnist/ （地址要复制链接到浏览器打开，直接点击此处进入需要账号和密码）

### 实现 2：onnxruntime-web

onnxruntime-web 是一个用于在浏览器中运行 ONNX 模型的库。  
onnx 是一个用于表示机器学习模型的开放标准，可以用于在不同的机器学习框架之间共享机器学习模型。

比如我最近参与的一个项目，就是在浏览器中通过 onnxruntime-web 来加载 onnx 模型，配合 opencv-ts 来实现涂抹消除的功能



体验地址：[inpaint-web](https://inpaintweb.lxfater.com/)  
仓库地址：[github: inpaint-web](https://github.com/lxfater/inpaint-web)



这里面的 onnx 模型是通过 PyTorch 写完转成 onnx 格式的模型，这里具体实现可以完全交给我们负责 AI 方向的同学  
拿到 onnx 模型后，通过 onnxruntime-web 就可以加载到浏览器中  
剩下的，就是业务代码：这里面是使用 opencv-ts 来处理图像，模型接收一个输入，然后会输出一个结果，通过处理这个数组我们就能拿到 ImageData，然后巴拉巴拉你可以拿这个 ImageData 做你想做的事情就行。

## 哪个更好？

个人认为采用 onnxruntime-web 来加载模型的方式更好，也更符合区分职能的团队协作方式。只需要协商好模型的输入输出，然后就可以分工合作了。

在工程部署中，基本不会采用 ONNX 自带的 API 去搭建网络，通常都是采用其他深度网络学习框架训练模型，然后将训练好的模型直接导出成 ONNX 模型现在很多深度学习框架都支持了 ONNX，方便了模型的部署和在各个框架之间转移，未来可期 😃  
这样也更方便专门负责做这块的同学可以用自己的技术栈来实现(TensorFlow, Keras, PyTorch...)， 我认为这本身为 AI 框架的采用提供了更大的灵活性。

然后 onnxruntime-web 可以选择在 CPU 和 GPU 上运行。  
在 CPU 上运行的时候，采用 WebAssembly 以接近原生速度执行模型。  
在 GPU 上运行的时候，由于 WebGPU 目前还是实验性的，所以目前还是用 WebGL 来运行的。

## 模型的获取

这些地方都有很多的模型可以直接拿来用，如果自己有相关经验，也可以自己训练模型。

- TensorFlow Hub (https://tfhub.dev), 今天看到已经改名了，会重定向到（https://www.kaggle.com/models）
- TensorFlow.js (https://www.tensorflow.org/js/models)
- PyTorch Hub (https://pytorch.org/hub)
- huggingface (https://huggingface.co/models)
- ONNX models (https://onnx.ai/models)

## 端智能的应用场景

最早有这个概念的认识，是 20 年刚毕业的时候，我的第一家公司（医疗行业），当时就在探索端智能的应用场景，那时候感觉前端在 AI 方向的生态都还很不完善，也可能是我不知道，但是现在看来，这块的生态已经很完善了，而且发展的特别快，特别是这两年。  
背景是，专家或者医生一天要看很多片子，看到后面很容易把明明有问题的给忽略了，所以我们想通过一些算法来帮助医生更快更准确的找到问题所在。  
当时我们的产品主要是互联网医院的生态软件，核心业务就是将 CT、MR 等影像进行 3 维重建，方便专家看片子，比 2D 的片子更容易发现问题所在。



这个过程中，我们就需要用到一些图像处理的技术，比如图像去噪、增强等然后输入给模型等待结果，种种原因，最后我们实现的是一个假的端智能，本地起个服务，前端调本地的接口 😅😅

## 结论

- 端智能未来肯定有前景（前提是网络越来越好，例如 5G 普及「按需加载影响体验？」，硬件越来越强算力越来越猛, 例如人手一块 4090🫢）
- 小型模型友好，中型吃力，大型顶不住
- 简单任务能给公司节约成本

---

## File: posts/01-frontend/跨域解决方案之proxyTable.md

---
title: 跨域解决方案之proxyTable
date: 2018-12-28
tags:
  - vue
  - webpack
categories:
  - 前端
---

> 在开发阶段，前端调用后端接口可能会出现跨域问题，在 vue-cli 中已经为我们集成了[**http-proxy-middleware**](https://github.com/chimurai/http-proxy-middleware)，我们只需要在`config/index.js`中的 proxyTable 配置即可。



如果接口是`www.aaa.com/get/getList`，那么有两种配置方案：

- 方法一

```
proxyTable: {
  '/api': {
    target: 'www.aaa.com',
    pathRewrite: {
      '^/api': '/get'
    }
  }
}
```

这个时候我们请求`/api/getList`就是请求`www.aaa.com/get/getList`

- 方法二

```
proxyTable: {
  '/api': {
    target: 'www.aaa.com',
    pathRewrite: {
      '^/api': ''
    }
  }
}
```

这个时候我们请求`/api/get/getList`就是请求`www.aaa.com/get/getList`

---

## File: posts/01-frontend/通过文本与图片的隐写来隐藏信息（非常有意思）.md

---
title: 通过文本与图片的隐写来隐藏信息(非常有意思!)
date: 2021-09-18
tags:
  - JavaScript
categories:
  - 前端
---





## 前言

哈喽大家好~我是荣顶,马上中秋节啦 ~ 先祝大家中秋节快乐,阖家团圆,幸福安康~

这次之所以会出一期版权保护的内容,是因为前段时间有群友说他的文章被盗了,于是冲着对版权保护方法的好奇,就有了这篇文章 ~

版权保护方法有很多:  
这次文章主要和大家讲讲如何在文本与图片中隐藏自己的版权信息(下一篇文章会给大家详细聊聊抠不掉的水印元素)

**本文参与了中秋活动,所以文中的例子主要围绕着中秋的主题进行讲解,大家点赞支持一下 ~ 谢谢 ~** 🥰

**看完本文,你将学会 👇**

- **文本隐写**：通过某种方法竟然可以在字符串中读取和修改隐藏信息"我是荣顶"⇄"我是[前埔寨]荣顶",点[这里](https://wangrongding.github.io/interesting-example/#/text/index)体验

  

- **图片隐写**：在图片中添加隐藏的图文(还能往里面写文件!!!)点[这里](https://wangrongding.github.io/interesting-example/#/enImg/index)体验

  

  

## 文本隐写

**首先我们需要了解一下,什么是隐写术(Steganography)**

> 隐写术：将信息隐藏在多种载体中，如：视频、硬盘和图像，将需要隐藏的信息通过特殊的方式嵌入到载体中，而又不损害载体原来信息的表达。旨在保护需要隐藏的信息不被他人识别。



当然信息隐蔽技术肯定不止隐写术,大概有：1）隐写术、2）数字水印、3）隐蔽信道、4）阀下信道、5）匿名信道 ...

### 实现原理

通过将字符串中每个`字符`转换为`只有 1 和 0 的表示`,然后通过`零宽字符`表示 0 和 1,就能实现通过零宽字符来表示一串`看不见的字符串`

**首先我们了解一下什么是零宽字符?**

顾名思义: 就是字节宽度为 0 的特殊字符。🙄 这解释...那我走?

😀 零宽字符: 是一种不可打印的 Unicode 字符, 在浏览器等环境不可见, 但是真是存在, 获取字符串长度时也会占位置, 表示某一种控制功能的字符.

> **零宽字符主要有以下六种:**
>
> - 零宽度空格符 (zero-width space) U+200B : 用于较长单词的换行分隔
> - 零宽度非断空格符 (zero width no-break space) U+FEFF : 用于阻止特定位置的换行分隔
> - 零宽度连字符 (zero-width joiner) U+200D : 用于阿拉伯文与印度语系等文字中，使不会发生连字的字符间产生连字效果
> - 零宽度断字符 (zero-width non-joiner) U+200C : 用于阿拉伯文，德文，印度语系等文字中，阻止会发生连字的字符间的连字效果
> - 左至右符 (left-to-right mark) U+200E : 用于在混合文字方向的多种语言文本中，规定排版文字书写方向为左至右
> - 右至左符 (right-to-left mark) U+200F : 用于在混合文字方向的多种语言文本中，规定排版文字书写方向为右至左

考虑到 Unicode 中有所谓的`Surrogate Pair`的情况,所以这里我们处理字符串的核心方法是`codePointAt`和`fromCodePoint`

> `Surrogate Pair`：是 UTF-16 中用于扩展字符而使用的编码方式，是一种采用四个字节(两个 UTF-16 编码)来表示一个字符。

> Unicode 编码单元（code points）的范围从 0 到 1,114,111。开头的 128 个 Unicode 编码单元和 ASCII 字符编码一样。如果指定的 index 小于 0 或大于字符串的长度，则 charCodeAt 返回 NaN

例如：汉字“𠮷”(非"吉")的码点是 0x20BB7，而 UTF-16 编码为 0xD842 0xDFB7（十进制为 55362 57271），需要 4 个字节储存。对于这种 4 个字节的字符，JavaScript 不能正确处理，字符串长度会误判为 2，`"𠮷".length // 2` 而且 charAt()方法无法读取整个字符，charCodeAt()方法只能分别返回前两个字节和后两个字节的值。 ES6 提供了 codePointAt()方法，能够正确处理 4 个字节储存的字符，返回一个字符的码点。

```js
//一个字符由两个字节还是由四个字节组成的最简单方法。
'我'.codePointAt(0) > 0xffff //false
'A'.codePointAt(0) > 0xffff //false
'3'.codePointAt(0) > 0xffff //false

'𠮷'.codePointAt(0) > 0xffff //true
```

```js
console.log(String.fromCodePoint(0x20bb7)) // "𠮷"
//或者十进制
console.log(String.fromCodePoint(134071)) // "𠮷"
//也可以多个参数
console.log(String.fromCodePoint(25105, 29233, 0x4f60)) // "我爱你"
```

`String.fromCodePoint` 方法是 ES6 新增加的特性，ES6 提供了 String.fromCodePoint()方法，可以识别大于 0xFFFF 的字符，弥补了 String.fromCharCode()方法的不足。一些老的浏览器可能还不支持。可以通过使用这段 [polyfill](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/fromCodePoint#polyfill) 代码来保证浏览器的支持

**当然,如果不需要处理复杂的字符,你也可以用`charCodeAt`和`fromCharCode`两个方法来对字符进行处理**

### 好了,铺垫完了,开干~

**先说加密**

我们将文本通过`codePointAt()` 方法得出每个字符的`Unicode编码点`值,然后将它们转为`2进制`,每个字符间通过`空格`分隔开,然后我们用`零宽字符&#8203`来表示`1`,用`零宽字符&#8204`来表示`0`,用`零宽字符&#8205`来表示`空格`,这样就可以得到一段完全用零宽字符表示的`看不见的字符串`

**多了不说,少了不唠 ~ 先看代码!**

```js
// 字符串转零宽字符串
function encodeStr(val = '隐藏的文字') {
  return val
    .split('')
    .map((char) => char.codePointAt(0).toString(2))
    .join(' ')
    .split('')
    .map((binaryNum) => {
      if (binaryNum === '1') {
        return '​' // 零宽空格符&#8203;
      } else if (binaryNum === '0') {
        return '‌' // 零宽不连字符&#8204;
      } else {
        return '‍' //空格 -> 零宽连字符&#8205;
      }
    })
    .join('‎')
}
console.log('str:', encodeStr(), 'length:', encodeStr().length) //大家可以把这段copy到控制台执行以下看看
```



这一块一开始`return`出去的都是直接写在双引号中的零宽字符,但是你们复制到浏览器中估计也不方便看,反正它是看不到的,平台要是给我过滤了,那更尴尬

**想在 `vscode`或者`atom` 中看到代码里是否有`零宽字符`,可以[下载](https://marketplace.visualstudio.com/items?itemName=wengerk.highlight-bad-chars)`Highlight Bad Chars`插件来实现零宽字符的高亮(如上图所示)**

为了方便大家容易区分空字符串与零宽字符,这里我把代码改了一下(这里是用 vue 做的,以后我的所有例子都会在[这个仓库](https://github.com/wangrongding/interesting-example)下当然它也有线上的[体验地址](https://wangrongding.github.io/interesting-example/#/home),欢迎大家来指点~)

```js

// 字符串转零宽字符串
encodeStr() {
    this.cipherText = this.text.split("");
    //在字符串中的随机一个位置插入加密文本
    this.cipherText.splice(
        parseInt(Math.random() * (this.text.length + 1)),
        0,
        //加密的文本
        this.hiddenText
            .split("")
            //['荣', '顶' ]
            .map((char) => char.codePointAt(0).toString(2))
            // ['1000001101100011','1001100001110110']
            .join(" ")
            //"1000001101100011 1001100001110110"
            .split("")
            /* [ '1', '0', '0', '0',  '0', '0', '1', '1', '0', '1', '1', '0', '0',  '0', '1', '1', ' ',
                '1', '0', '0', '1', '1',  '0', '0', '0', '0', '1', '1', '1', '0', '1',  '1', '0'] */
            .map((binaryNum) => {
                if (binaryNum === "1") {
                    return String.fromCharCode(8203); // 零宽空格符&#8203;
                } else if (binaryNum === "0") {
                    return String.fromCharCode(8204); // 零宽不连字符&#8204;
                } else {
                    return String.fromCharCode(8205); //空格 -> 零宽连字符&#8205;
                }
            })
            //对上面所有的数组元素进行处理,生成一个新的数组['​', '​', '‌'......]其中每一个元素都是零宽字符,分别代表0和1以及
            .join(String.fromCharCode(8206))
        // 用左至右符&#8206;来把上面的数组相连成一个零宽字符串=>"‎​‎‌‎‌"
    );
    this.cipherText = this.cipherText.join("");
    console.log(this.cipherText, "cipherText");
}
```

其中随机混入零宽字符串主要是这段伪代码

```js
let str = 'qwe12345789'.split('')
//在字符串中的随机一个位置插入加密文本
str.splice(parseInt(Math.random() * (str.length + 1)), 0, '加密文本').join('')
```

加密后的文本通过 trim 或者通过网络发送都是没问题的

```js
'中秋节快乐​‎​‎‌‎​‎​‎‌‎‌‎‌‎‌‎​‎​‎​‎‌‎‌‎‌‎‍‎​‎​‎‌‎‌‎​‎‌‎​‎​‎​‎​‎‌‎‌‎‌‎‌‎‌‎‍‎​‎‌‎‌‎‌‎‌‎​‎‌‎‍‎​‎​‎​‎‌‎​‎‌‎​‎‍‎​‎​‎‌‎‌‎​‎​‎​'.trim().length //114
```



**大家都了解如何加密后,我们再看以下如何对字符串进行解密**

解密首先需要了解如何提取零宽度字符

```JavaScript
"点赞鼓励~😀​‎​‎​‎‌‎‌‎​‎‌‎‌‎​‎‌‎‌‎​‎‌‎‌‎‌‎‍‎​‎​‎‌‎‌‎​‎​‎​‎‌‎​‎‌‎‌‎‌‎‌‎​‎​‎‍‎​‎‌‎‌‎​‎​‎​‎​‎​‎​‎​‎‌‎‌‎‌‎‌‎​‎‍‎​‎​‎‌‎‌‎‌‎‌‎‌‎‌‎​‎​‎‌‎​‎​‎​‎​‎‍‎​‎​‎‌‎​‎​‎‌‎‌‎‌‎‌‎‌‎​‎​‎​‎​‎‌‎​‎‍‎​‎​‎‌‎​‎​‎​‎​‎‌‎‌‎‌‎‌‎‌‎‌‎‌‎‌‎‌".replace(/[^\u200b-\u200f\uFEFF\u202a-\u202e]/g, "");
```

解密的过程就是加密的反向操作,我们先提取文本中的零宽字符串,用 `1`来表示`零宽字符&#8203`,用`0`来表示`零宽字符&#8204`,用`空格`来表示`零宽字符&#8205`,这样就可以得到一段由 1 和 0 组成空格分隔的字符串,我们将每段 1/0 表示的字符串转成十进制后,通过`String.fromCharCode`方法将其转回为可以看得见的文字即可~

```js
// 零宽字符转字符串
decodeStr() {
    if (!this.tempText) {
        this.decodeText = "";
        return;
    }
    let text = this.tempText.replace(/[\u200b-\u200f\uFEFF\u202a-\u202e]/g, "");
    let hiddenText = this.tempText.replace(/[^\u200b-\u200f\uFEFF\u202a-\u202e]/g, "");
    console.log(text, "text");
    console.log(hiddenText, "hiddenText");
    this.decodeText = hiddenText
        .split("‎") //不是空字符串,是 &#8206;
        .map((char) => {
            if (char === "​" /* 不是空字符串,是&#8203; */) {
                return "1";
            } else if (char === "‌" /*  不是空字符串,是&#8204; */) {
                return "0";
            } else {
                /* 是&#8205;时,用空格替换 */
                return " ";
            }
        })
        .join("")
        //转数组
        .split(" ")
        //根据指定的 Unicode 编码中的序号值来返回一个字符串。
        .map((binaryNum) => String.fromCharCode(parseInt(binaryNum, 2)))
        .join("");
    console.log(text + hiddenText);
},
```

### 演示一波

**核心实现方法就是以上,现在我们来演示一下,大家看图前,首先可以把这小段文字复制到任何地方打印下**(就 F12 控制台看最方便)`console.log("中秋快乐123456​‎​‎​‎‌‎‌‎​‎‌‎‌‎​‎‌‎‌‎​‎‌‎‌‎‌‎‍‎​‎​‎‌‎‌‎​‎​‎​‎‌‎​‎‌‎‌‎‌‎‌‎​‎​‎‍‎​‎‌‎‌‎​‎​‎​‎​‎​‎​‎​‎‌‎‌‎‌‎‌‎​‎‍‎​‎​‎‌‎‌‎‌‎‌‎‌‎‌‎​‎​‎‌‎​‎​‎​‎​‎‍‎​‎​‎‌‎​‎​‎‌‎‌‎‌‎‌‎‌‎​‎​‎​‎​‎‌‎​‎‍‎​‎​‎‌‎​‎​‎​‎​‎‌‎‌‎‌‎‌‎‌‎‌‎‌‎‌‎‌".length)`长度一定不是 10😀



这里的 demo 我已经上传至我的 github,你可以点击[这里](https://wangrongding.github.io/interesting-example/#/text/index)体验一下~

### 应用场景

- 数据防爬  
  将零宽度字符插入文本中,干扰关键字匹配。爬虫得到的带有零宽度字符的数据会影响他们的分析，但不会影响用户的阅读数据。
- 隐藏文本信息  
  将自定义组合的零宽度字符插入文本中，用户复制后会携带不可见信息，达到隐藏文本信息的作用。
- 逃脱敏感词过滤  
  你在游戏里骂人,`你妈*`肯定是发不出去的,但是你发`你xxxxx妈yyyyy*`就不一样了 😀,这个东西还是研究起来很有意思的。
- 嵌入隐藏的代码还有很多等等等...

**试想当在电子书籍 PDF 或者一些正版影视音乐作品中嵌入购买人的个人信息,那还有多少人敢直接传给别人看呢?**

**像下面这种明水印,加了和没加没啥区别,很容易就被别人扣掉了**



**而且还被别人更换了水印,打上了广告,啧啧啧...如果一本电子书里嵌入了大量的隐写内容,对于盗版的传播行为,其实是可以起到很好的追责作用的(各大出版商,电子书可以搞起来 😀 买电子书都有购买者的联系方式和个人信息的)**



诸如像 toG 的项目中,有很多敏感内容都是有个人信息的明水印,和隐写的个人信息,传出去了就知道是谁传的 (至少**对于不懂技术的人来说,隐写是真的很难被发现的**)

### 预防零宽字符的植入

说了这么多如何对字符进行零宽字符的植入和提取,那么像掘金哪天不允许你们加这玩意在里面了,他们对所有的文本用正则这么搞一遍就没了

**预防用户上传的文本中含有零宽字符,一般我们先做一次替换即可**

```JavaScript
str.replace(/[\u200b-\u200f\uFEFF\u202a-\u202e]/g, "");
```

## 图片隐写

什么是图片隐写?  
lsb 隐写很实用，算法简单，能存储的信息量也大，更何况是 CTF 比赛中的常客(起源于 1996 年 DEFCON 全球黑客大会)

### 实现原理

图片的隐写有很多方式,我这里只用最简单的方式 **(LSB：二进制最低位,不是"老色 X"😅)** 做演示,当然也是因为我菜 😥 数学不好,要不然我也可以用傅里叶变换通过处理图片的色波 **(图像本质上就是各种色彩波的叠加** 这个概念的具体解释可以看阮一峰老师的[这篇文章](https://www.ruanyifeng.com/blog/2017/12/image-and-wave-filters.html) ) 来做,那样又帅又飘~害!

### 二进制最低位

什么是二进制最低位?就是**二进制最后面一位**

好了,多了不说少了不唠 ~ 我们一起看图说话 ~







每个二进制的值的最低位都可以表示一个 1bit 的数据,一个像素需要 RGBA,4 个值共 4 \* 8=32 个 bit 所以至少需要八个像素 **(32 个值)** 的最低位才能表示一个像素的 RGBA 值 **(这里的 2560000 / 4 = 640000 个像素可以用来储存 2560000 / 32 = 80000 个像素的 RGBA 值)**

即:每 32 个值的二进制最低位可以用来表示一个标准像素的 RGBA 值(为方便理解,如下图所示 👇)



### 拿到需要隐藏的数据

这一步,我们可以隐藏图片,隐藏文字,也可以自己手绘一些东西在画布上  
将我们已经在主画布上绘制或加载的图片转换为 URL ,通过创建一个临时小画布, 将主画布生成的 URL 通过`drawImage`的方式,缩小的绘制到临时小画布上

**(这里为什么要这么做?还是因为上面提到的 主画布的 640000 个像素值的最低位只能储存 80000 个像素的 RGBA 值)**

```js
//将画布上的信息绘制到小画布上保存起来
saveHiddenImageData() {
    const tempCanvas = document.createElement("canvas");
    const tempCtx = tempCanvas.getContext("2d");
    //小画布的长宽=大画布的像素/8后再开平方
    //因为需要八个像素的最低位才可以表示一个小画布的像素的RGBA值
    tempCanvas.width = Math.floor(Math.sqrt((this.canvas.width * this.canvas.height) / 8));
    tempCanvas.height = Math.floor(Math.sqrt((this.canvas.width * this.canvas.height) / 8));
    var image = new Image();
    image.src = this.canvas.toDataURL("image/png");
    image.onload = () => {
        //绘制图像到临时的小画布
        tempCtx.drawImage(image, 0, 0, tempCanvas.width, tempCanvas.height);
        this.hiddenData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
        this.hiddenData.binaryList = Array.from(this.hiddenData.data, (color) => {
            color = color.toString(2).padStart(8, "0").split("");
            return color;
        });
        console.log(this.hiddenData, "hiddenData");
        this.$message({
            type: "success",
            message: "保存成功!请选择目标图片~",
        });
        this.canvas.clear();
    };
},
```

### 再拿目标图的数据

在拿目标图(你要把隐藏的数据隐写进的目标图)的数据时候,我们需要**预先处理**一下所有的颜色值

这一步很关键,可以看到,我上面的图,画布加载完目标图片时,我们可以通过`getImageData`方法读取到页面的所有像素值,我这里将所有像素的二进制最低位全部归零,即把所有的颜色值都处理成**偶数/非奇非偶数(0)**

我们通过操作每个值的最低位,用来储藏要保存的数据, **肉眼是无法看出你对最低位的更改的**

```js
//获取画布像素数据
getCanvasData() {
    this.targetData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
    //将数字化为非奇数
    function evenNum(num) {
        num = num > 254 ? num - 1 : num;
        num = num % 2 == 1 ? num - 1 : num;
        return num;
    }
    //存一个二进制的数值表示
    this.targetData.binaryList = Array.from(this.targetData.data, (color, index) => {
        this.targetData.data[index] = evenNum(this.targetData.data[index]);
        color = evenNum(color).toString(2).padStart(8, "0").split("");
        return color;
    });
    console.log(this.targetData);
    this.loading = false;
},
```

### 写入隐藏的数据

到这里,我们已经拿到了隐藏的数据,和目标图像的数据,接下来我们需要做的就是**将这 318,096 个颜色值写入到目标图片的 2560000 个颜色值的二进制最低位中**,即可实现图片的隐写

**再次需要注意的是**:通过最低位来做的话,我们隐写到图片中的数据是有限的,即 `图片的整体像素 / 8 = 图片中可以隐藏的像素(个)`  
这里我们用的是`800*800`的画布,有`640000`个像素,其中可以隐藏 `640000 / 8 = 80000个`像素  
所以我们隐藏的数据只能绘画到 `Math.floor(Math.sqrt((this.canvas.width * this.canvas.height) / 8))`这里是 282 的宽高的 canvas 中(79,524 个像素,318,096 个颜色值),这里求出的数只能向下取整,要不然会溢出,导致丢失隐藏的数据!





这里的操作就像是我们藏头诗一样,我们把数据藏在尾部



**RGB 分量值的小量变动，是肉眼无法分辨的，不影响对图片的识别。你看不出来这个+1 的区别**

---

多了不说少了不唠~先看代码

```js
//将隐写的资源图片数据存到目标图片的二进制最低位中
drawHiddenData() {
    //将隐藏的数据的二进制全部放到一个数组里面
    let bigHiddenList = [];
    for (let i = 0; i < this.hiddenData.binaryList.length; i++) {
        bigHiddenList.push(...this.hiddenData.binaryList[i]);
    }
    console.log(bigHiddenList, "bigHiddenList");
    this.targetData.binaryList.forEach((item, index) => {
        bigHiddenList[index] && (item[7] = bigHiddenList[index]);
    });
    this.canvas.clear();
    this.targetData.data.forEach((item, index) => {
        this.targetData.data[index] = parseInt(
            this.targetData.binaryList[index].join(""),
            2
        );
    });

    const tempCanvas = document.createElement("canvas");
    tempCanvas.width = 800;
    tempCanvas.height = 800;
    let ctx = tempCanvas.getContext("2d");
    ctx.putImageData(this.targetData, 0, 0);
    fabric.Image.fromURL(tempCanvas.toDataURL(), (i) => {
        this.canvas.clear();
        this.canvas.add(i);
        this.canvas.renderAll();
    });
    this.$message({
        type: "success",
        message: "加密成功!",
    });
},
```

可以看到,这里已经将一张图片隐藏到了另一张图片中,(我们这里将一个月饼图隐写进了月亮图中)



### 解析加密后的图片

完成了图片加密后我们接下来需要做的是解析加密后的图片

首先我们选取本地图片后,将其渲染到画布上,在通过`getImageData`获得画布的像素数据,建立一个二进制的存储表示,后面将通过它的最低位取出隐藏在目标图中的图片

```
//获取画布像素数据
    getCanvasData() {
        this.targetData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
        //存一个二进制的数值表示
        this.targetData.binaryList = Array.from(this.targetData.data, (color, index) => {
            color = color.toString(2).padStart(8, "0").split("");
            return color;
        });
        console.log(this.targetData);
        this.loading = false;
    },
```

### 冲图片中抽取隐藏的颜色值

这里我们主要将主画布的二进制颜色值的最低位抽离出来,组装成隐藏图片的像素值数组,最后通过`putImageData`来绘制出隐藏其中的图片

**这里非常需要注意的是: `putImageData`的第一个参数`data`的长度`必须为两个边的乘积的4的倍数`** 否则就会报错  
所以,这里我们取像素的时候,需要这么取`Math.pow(Math.floor(Math.sqrt(2560000 / 32)), 2) * 4`,由于我这里是 800 _ 800 的所以是 2560000 个值,你直接写成变量`canvas.width _ canvas.height \*4`即可

```js
//解析图片
decryptImage() {
    const c = document.getElementById("decryptCanvas");
    const ctx = c.getContext("2d");

    let decryptImageData = [];

    for (let i = 0; i < this.targetData.binaryList.length; i += 8) {
        let tempColorData = [];
        for (let j = 0; j < 8; j++) {
            tempColorData.push(this.targetData.binaryList[i + j][7]);
        }
        decryptImageData.length < Math.pow(Math.floor(Math.sqrt(2560000 / 32)), 2) * 4 &&
            decryptImageData.push([...tempColorData]);
    }
    decryptImageData = Uint8ClampedArray.from(decryptImageData, (z) => {
        z = parseInt(z.join(""), 2);
        return z;
    });
    console.log(decryptImageData, "decryptImageData");
    //需要注意的是putImageData的data的长度必须为两个边的乘积的4的倍数
    ctx.putImageData(
        new ImageData(
            decryptImageData,
            Math.floor(Math.sqrt(2560000 / 8 / 4)),
            Math.floor(Math.sqrt(2560000 / 8 / 4))
        ),
        0,
        0
    );
},
```

经过这一步后,我们可以看到,就能够从主画布中提取出隐藏在其中的图片!!!

**哇 ~ 是不是非常的有意思?**



**还需要注意的点** ：LSB 方式的隐写图片只能存储为 PNG 或者 BMP 图片格式，并且不可以再用有损压缩（比如 JPEG），否则会丢失隐写的数据！

**不要用隐写做违法犯罪的坏事!!!**因为它是可以防（某墙）监控的,比如你把坏孩子图藏进了好孩子图里边,我去太坏了 😢

但是你身为程序员,你可以用它去给你心爱的宝贝表白撒,不失为一种浪漫的方式~

感兴趣的小伙伴还可以看一下这篇论文[StegaStamp: Invisible Hyperlinks in Physical Photographs(隐写邮票：自然照片中嵌入不可见超链接)](https://arxiv.org/abs/1904.05343),作者来自加州大学伯克利分校。他们的项目主页[在这](https://www.matthewtancik.com/stegastamp)

以上是文本和图片的隐写相关内容,当然隐写的媒介不可能只局限于此,还有很多中,只要是计算机能用数字表达一切东西,按道理来说都是可以用来做隐写的,例如[音频的隐写](https://kns.cnki.net/KXReader/Detail?TIMESTAMP=637661997320734302&DBCODE=CJFD&TABLEName=CJFD2012&FileName=DZGS201204006&RESULT=1&SIGN=Nzw4NdjstxDgBHTRogyMRXOqnpA%3d)，以及[视频的隐写](https://kns.cnki.net/KXReader/Detail?TIMESTAMP=637661958755239977&DBCODE=CJFD&TABLEName=CJFD2011&FileName=JSJC201120044&RESULT=1&SIGN=DDmp0hfxaqRrgvnQ05e87a2VZo8%3d)

## 最后

隐写术是一门很深、应用很广泛的学问，这里讲的很泛，权当做抛砖引玉。图片和文字的隐写只是其中最简单的一部分，有兴趣的同学可以看一本叫《数据隐藏技术揭秘》的书。需要的小伙伴也可以加我,我发给你!  
文中的例子已经放在了我的[github](https://github.com/wangrongding)中,当然你也可以通过[这里](https://wangrongding.github.io/interesting-example/#/home)体验一下

> 我是荣顶，很高兴能在这里和你一起变强！一起**面向快乐编程！** 😉
>
> 如果你也非常热爱前端相关技术！欢迎进入我的小密圈~ 里面都是大佬，**带你飞!** 🦄 点击 → [这里](https://juejin.cn/pin/7004843607072964621)

---

## File: posts/01-frontend/防止网页被嵌入框架的代码.md

[Binary file]

---

## File: posts/01-frontend/JavaScript/Javascript将html转成pdf,下载（html2canvas 和 jsPDF）.md

---
title: Javascript将html转成pdf,下载（html2canvas 和 jsPDF）
date: 2019-02-01
categories:
  - 前端
author: 汪荣顶
---

# Javascript 将 html 转成 pdf,下载（html2canvas 和 jsPDF）

## html2canvas

简介

我们可以直接在浏览器端使用 html2canvas,对整个或局部页面进行‘截图’。但这并不是真的截图，而是通过遍历页面 DOM 结构，收集所有元素信息及相应样式，渲染出 canvas  
image。

由于 html2canvas 只能将它能处理的生成 canvas  
image，因此渲染出来的结果并不是 100%与原来一致。但它不需要服务器参与，整个图片都由客户端浏览器生成，使用很方便。

使用

使用的 API 也很简洁，下面代码可以将某个元素渲染成 canvas：

```
html2canvas(element, {
    onrendered: function(canvas) {
        // canvas is the final rendered <canvas> element
    }
});
```

通过 onrendered 方法，可以将生成的 canvas 进行回调，比如插入到页面中：

```
html2canvas(element, {
    onrendered: function(canvas) {
       document.body.appendChild(canvas);
    }
});
```

做个小例子代码如下，在线展示链接[demo1](https://linwalker.github.io/render-html-to-pdf/demo1.html)

```
<html>
  <head>
    <title>html2canvas example</title>
    <style type=&quot;text/css&quot;>...</style>
  </head>
  <body>
    <header>
      <nav>
        <ul>
          <li>one</li>
          ...
        </ul>
      </nav>
    </header>
    <section>
      <aside>
        <h3>it is a title</h3>
        <a href=&quot;&quot;>Stone Giant</a>
        ...
     </aside>
      <article>
        !\[\](./Stone.png)
        <h2>Stone Giant</h2>
        <p>Coming ... </p>
        <p>以一团石头...</p>
      </article>
    </section>
    <footer>write by linwalker @2017</footer>
    <script type=&quot;text/javascript&quot; src=&quot;./html2canvas.js&quot;></script>
    <script type=&quot;text/javascript&quot;>
        html2canvas(document.body, {
          onrendered:function(canvas) {
            document.body.appendChild(canvas)
          }
        })
    </script>
  </body>
</html>
```

这个例子将页面 body 中的元素渲染成 canvas，并插入到 body 中

## jsPDF

jsPDF 库可以用于浏览器端生成 PDF。

文字生成 PDF

使用方法如下：

```
// 默认a4大小，竖直方向，mm单位的PDF
var doc = new jsPDF();

// 添加文本‘Download PDF’
doc.text('Download PDF!', 10, 10);
doc.save('a4.pdf');
```

在线演示[demo2](https://linwalker.github.io/render-html-to-pdf/demo2.html)

图片生成 PDF

使用方法如下：

```
// 三个参数，第一个方向，第二个单位，第三个尺寸格式
var doc = new jsPDF('landscape','pt',\[205, 115\])

// 将图片转化为dataUrl
var imageData = ‘data:image/png;base64,iVBORw0KGgo...’;

doc.addImage(imageData, 'PNG', 0, 0, 205, 115);
doc.save('a4.pdf');
```

在线演示[demo3](https://linwalker.github.io/render-html-to-pdf/demo3.html)

文字与图片生成 PDF

```
// 三个参数，第一个方向，第二个尺寸，第三个尺寸格式
var doc = new jsPDF('landscape','pt',\[205, 155\])

// 将图片转化为dataUrl
var imageData = ‘data:image/png;base64,iVBORw0KGgo...’;

//设置字体大小
doc.setFontSize(20);

//10,20这两参数控制文字距离左边，与上边的距离
doc.text('Stone', 10, 20);

// 0, 40, 控制文字距离左边，与上边的距离
doc.addImage(imageData, 'PNG', 0, 40, 205, 115);
doc.save('a4.pdf')
```

在线演示[demo4](https://linwalker.github.io/render-html-to-pdf/demo4.html)

生成 pdf 需要把转化的元素添加到 jsPDF 实例中，也有添加 html 的功能，但某些元素无法生成在 pdf 中，因此可以使用 html2canvas +  
jsPDF 的方式将页面转成 pdf。通过 html2canvas 将遍历页面元素，并渲染生成 canvas，然后将 canvas 图片格式添加到 jsPDF 实例，生成 pdf。

## html2canvas + jsPDF

单页

将 demo1 的例子修改下：

```
<script type=&quot;text/javascript&quot; src=&quot;./js/jsPdf.debug.js&quot;></script>
<script type=&quot;text/javascript&quot;>
      var downPdf = document.getElementById(&quot;renderPdf&quot;);
      downPdf.onclick = function() {
          html2canvas(document.body, {
              onrendered:function(canvas) {

                  //返回图片dataURL，参数：图片格式和清晰度(0-1)
                  var pageData = canvas.toDataURL('image/jpeg', 1.0);

                  //方向默认竖直，尺寸ponits，格式a4\[595.28,841.89\]
                  var pdf = new jsPDF('', 'pt', 'a4');

                  //addImage后两个参数控制添加图片的尺寸，此处将页面高度按照a4纸宽高比列进行压缩
                  pdf.addImage(pageData, 'JPEG', 0, 0, 595.28, 592.28/canvas.width * canvas.height );

                  pdf.save('stone.pdf');

              }
          })
      }
</script>
```

在线演示[demo5](https://linwalker.github.io/render-html-to-pdf/demo5.html)

如果页面内容根据 a4 比例转化后高度超过 a4 纸高度呢，生成的 pdf 会怎么样？会分页吗？

你可以试试，验证一下自己的想法:  
[demo6](https://linwalker.github.io/render-html-to-pdf/demo6.html)

jsPDF 提供了一个很有用的 API，`addPage()`，我们可以通过`pdf.addPage()`，来添加一页 pdf，然后通过`pdf.addImage(...)`，将图片赋予这页 pdf 来显示。

那么我们如何确定哪里分页？

这个问题好回答，我们可以设置一个`pageHeight`，超过这个高度的内容放入下一页 pdf。

来捋一下思路，将 html 页面内容生成 canvas 图片，通过`addImage`将第一页图片添加到 pdf 中，超过一页内容，通过`addPage()`添加 pdf 页数,然后再通过`addImage`将下一页图片添加到 pdf 中。

嗯～，很好！巴特，难道没有发现问题吗？

这个方法实现的前提是 — —  
我们能根据`pageHeight`先将整页内容生成的 canvas 图片分割成对应的小图片，然后一个萝卜一个坑，一页一页`addImage`进去。

What? 想一想我们的 canvas 是肿么来的，不用拉上去，直接看下面：

```
html2canvas(document.body, {
    onrendered:function(canvas) {
     //it is here we handle the canvas
    }
})
```

这里的`body`就是要生成 canvas 的元素对象，一个元素生成一个 canvas；那么我们需要一页一页的 canvas，也就是说。。。

你觉得可能吗？  
我觉得不太现实，按这思路要获取页面上不同位置的 DOM 元素，然后通过`htnl2canvas(element,option)`来处理，先不说能不能刚好在每个`pageHeight`的位置刚好找到一个 DOM 元素，就算找到了，这样做累不累。

累的话  
：）可以看看下面这种方法

多页

我提供的思路是我们只生成一个 canvas，对就一个，转化元素就是你要转成 pdf 内容的母元素，在这篇 demo 里就是`body`了；其他不变，也是超过一页内容就`addPage`，然后`addImage`,只不过这里添加的是同一个 canvas。

当然这样做只会出现多页重复的 pdf，那到底怎么实现正确分页显示。其实主要利用了 jsPDF 的两点：

```
- 超过jsPDF实例格式尺寸的内容不显示
（var pdf = new jsPDF('', 'pt', 'a4'); demo中就是a4纸的尺寸）
- addImage有两个参数可以控制图片在pdf中的位置
```

虽然每一页 pdf 上显示的图片是相同的，但我们通过调整图片的位置，产生了分页的错觉。以第二页为例，将竖直方向上的偏移设置为`-841.89`即一张 a4 纸的高度，又因为超过 a4 纸高度范围的图片不显示，所以第二页显示了图片竖直方向上\[841.89,1682.78\]范围内的内容，这就得到了分页的效果，以此类推。

还是看代码吧：

```
html2canvas(document.body, {
  onrendered:function(canvas) {

      var contentWidth = canvas.width;
      var contentHeight = canvas.height;

      //一页pdf显示html页面生成的canvas高度;
      var pageHeight = contentWidth / 592.28 * 841.89;
      //未生成pdf的html页面高度
      var leftHeight = contentHeight;
      //页面偏移
      var position = 0;
      //a4纸的尺寸\[595.28,841.89\]，html页面生成的canvas在pdf中图片的宽高
      var imgWidth = 595.28;
      var imgHeight = 592.28/contentWidth * contentHeight;

      var pageData = canvas.toDataURL('image/jpeg', 1.0);

      var pdf = new jsPDF('', 'pt', 'a4');

      //有两个高度需要区分，一个是html页面的实际高度，和生成pdf的页面高度(841.89)
      //当内容未超过pdf一页显示的范围，无需分页
      if (leftHeight < pageHeight) {
      pdf.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight );
      } else {
          while(leftHeight > 0) {
              pdf.addImage(pageData, 'JPEG', 0, position, imgWidth, imgHeight)
              leftHeight -= pageHeight;
              position -= 841.89;
              //避免添加空白页
              if(leftHeight > 0) {
                pdf.addPage();
              }
          }
      }

      pdf.save('content.pdf');
  }
})
```

在线演示[demo7](https://linwalker.github.io/render-html-to-pdf/demo7.html)

两边留边距

修改 imgWidth，并且在 addImage 时 x 方向参数设置你要的边距，具体代码如下

```
var imgWidth = 555.28;
var imgHeight = 555.28/contentWidth * contentHeight;
...
pdf.addImage(pageData, 'JPEG', 20, 0, imgWidth, imgHeight );
...
pdf.addImage(pageData, 'JPEG', 20, position, imgWidth, imgHeight);
```

在线演示[demo8](https://linwalker.github.io/render-html-to-pdf/demo8.html)

---

## File: posts/01-frontend/JavaScript/js中文字合成语音,文字转语音的实现方法.md

---
title: js中文字合成语音,文字转语音的实现方法
date: 2020-05-11
tags:
  - JavaScript
categories:
  - 前端
---

SpeechSynthesisUtterance 是 HTML5 中新增的 API,用于将指定文字合成为对应的语音.也包含一些配置项,指定如何去阅读(语言,音量,音调)等,该 api 对于盲人或弱视用户是非常有用的!

```javascript
var msg = new SpeechSynthesisUtterance('这里是转语音的测试文本,')

window.speechSynthesis.speak(msg)
```

\> \*\*SpeechSynthesisUtterance 基本属性\*\*

SpeechSynthesisUtterance.lang 获取并设置话语的语言

SpeechSynthesisUtterance.pitch 获取并设置话语的音调(值越大越尖锐,越低越低沉)

SpeechSynthesisUtterance.rate 获取并设置说话的速度(值越大语速越快,越小语速越慢)

SpeechSynthesisUtterance.text 获取并设置说话时的文本

SpeechSynthesisUtterance.voice 获取并设置说话的声音

SpeechSynthesisUtterance.volume 获取并设置说话的音量

>

> \*\*speechSynthesis 对象，主要作用是触发行为，例如读，停，还原等\*\*

> speak() – 只能接收 SpeechSynthesisUtterance 作为唯一的参数，作用是读合成的话语。

> stop() – 立即终止合成过程。

> pause() – 暂停合成过程。

> resume() – 重新开始合成过程。

> getVoices – 此方法不接受任何参数，用来返回浏览器支持的语音包列表，是个数组。

>

```javascript
//因此也可以这么写

var msg = new window.SpeechSynthesisUtterance()

msg.text = '你好，世界！'
```

> **该 Api 的一些回调方法**

> onstart – 语音合成开始时候的回调。

onpause – 语音合成暂停时候的回调。

onresume – 语音合成重新开始时候的回调。

onend – 语音合成结束时候的回调。

!\[在这里插入图片描述\](https://img-blog.csdnimg.cn/20201105175635861.png?x-oss-process=image/watermark,type\_ZmFuZ3poZW5naGVpdGk,shadow\_10,text\_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MjAzODI0NQ==,size\_16,color\_FFFFFF,t\_70#pic_center)

---

## File: posts/01-frontend/JavaScript/js动态添加dom，如何绑定事件.md

---
title: js动态添加dom，如何绑定事件
date: 2020-05-24
tags:
  - JavaScript
categories:
  - 前端
---

## 原理

首先要明白浏览器在加载页面的时候是按顺序来加载的，这样以来就很清楚了，js 动态添加 dom 以后，这些 dom 并没有绑定事件，这个时候最简单的一个办法就是：将绑定事件的方法封装到一个函数 A 中，在动态添加完 dom 以后立即执行一次函数 A 即可。



## 注意

其次需要注意的是，在你可能同时需要添加许多的 dom，不要添加一个就执行一次函数 A，这样会增加浏览器的负载，你需要在所有 dom 添加完以后在执行函数 A，例如你用一个 for 循环遍历 dom 组合并拼接成一个字符串，然后添加到某个父级 dom 里面，这个时候你需要在循环外添加一次就可以了。

## 代码

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>添加div并绑定点击事件</title>
    <style>
      div.btn {
        width: 200px;
        height: 50px;
        line-height: 50px;
        text-align: center;
        border: solid 1px #000;
        cursor: pointer;
      }
      div.innerDiv {
        width: 50px;
        height: 50px;
        background-color: black;
        margin: 10px;
        cursor: pointer;
      }
    </style>
  </head>
  <body>
    <div class="aa">
      <div class="btn">添加div并绑定点击事件</div>
      <div class="outerDiv"></div>
    </div>
    <script>
      function getClass(classname) {
        return document.getElementsByClassName(classname)
      }
      getClass('btn')[0].onclick = function () {
        addDom()
      }
      //    将点击事件封装为函数
      function funA() {
        for (var i = 0; i < getClass('innerDiv').length; i++) {
          getClass('innerDiv')[i].onclick = function () {
            alert(this.innerText)
          }
        }
      }
      //    将添加dom封装为函数
      function addDom() {
        var oldHtml = ''
        for (var j = 0; j < 6; j++) {
          oldHtml += '<div class="innerDiv">' + j + '</div>'
        }
        getClass('outerDiv')[0].innerHTML = oldHtml
        funA()
      }
      //    如果将函数A放在这里就不会起作用的
      //    funA();
    </script>
  </body>
</html>
```

---

## File: posts/01-frontend/JavaScript/js变量提升以及var对变量提升的影响.md

---
title: js变量提升以及var对变量提升的影响
date: 2019-02-04
tags:
  - JavaScript
categories:
  - 前端
---

> JavaScript 声明过的变量提升往往会影响到我们对变量的正常获取，所以特写此文，以便以后翻阅。



## 什么是变量提升

```javascript
//变量声明提升
function test() {
  var a = '1'
  var f = function () {}
  var b = '2'
  var c = '3'
}

//上述代码等价于
function test() {
  var a, f, b, c
  a = '1'
  f = function () {}
  b = '2'
  c = '3'
}
```

> js 中定义变量有两种情况：（注意在方法外不加 var 是不能定义变量的，出现 xx is not defined）
>
> 1. 都加 var，在方法内则是局部变量，在方法外则是全局变量。
> 2. 在方法内,加 var 为局部变量，不加 var 则是全局变量（在执行当前方法之后）

## 变量提升案例

### 案例 1

由于 test1 函数里面定义了变量 a，由于

```javascript
var a = "I'm a in all"

function test1() {
  console.log(a)
  console.log(window.a)

  var a = "I'm a in test1"
  console.log(a)
}

test1()
```

上述代码相当于

```javascript
var a = "I'm a in all"

function test1() {
  var a
  console.log(a) // undefined
  console.log(window.a) // I'm a in all（因为window指的是全局环境）

  a = "I'm a in test1"
  console.log(a) // I'm a in test1
}

test1()
```

### 案例 2

```javascript
var a = "I'm a in all"

function test2() {
  console.log(a) // I'm a in all

  a = "I'm a in test2" // 这里本来就是赋值，所以上边的a会输出全局变量
  console.log(a) // I'm a in test2
}

test2()
```

### 案例 3

```javascript
function test3_1() {
  console.log(a) // 报错（Uncaught ReferenceError: a is not defined），阻断以下代码的运行

  a = "I'm a in test3"
  console.log(a) // 不输出
}
console.log(a)
test3_1()

console.log(a) // 不输出
```

```javascript
function test3_2() {
  a = "I'm a in test3" // 全局变量（但是在方法执行后生效）
  console.log(a) // I'm a in test3
}

// console.log(a) // 如果在方法执行前打印，还是会报错（Uncaught ReferenceError: a is not defined），阻断以下代码的运行

test3_2()

console.log(a) // I'm a in test3（本来没有全局变量a，当test3运行时，定义了一个全局变量a，所以这里会输出）
```

---

## File: posts/01-frontend/JavaScript/requestAnimationFrame.md

---
title: 修改 requestAnimationFrame 的执行频率
date: 2019-10-10
tags:
  - Javascript
categories:
  - 前端
---

## 修改 requestAnimationFrame 的执行频率

```javascript
//当前执行时间
let nowTime = 0
//记录每次动画执行结束的时间
let lastTime = Date.now()
//我们自己定义的动画时间差值
let diffTime = 40

function animloop() {
  //记录当前时间
  nowTime = Date.now()
  // 当前时间-上次执行时间如果大于diffTime，那么执行动画，并更新上次执行时间
  if (nowTime - lastTime > diffTime) {
    lastTime = nowTime
    // doSomeThing()
  }
  requestAnimationFrame(animloop)
}
```

## 相关链接

- https://www.jianshu.com/p/1242d912bb27

---

## File: posts/01-frontend/JavaScript/setTimeout,setIntervsl.md

---
title: 从 setTimeout、setInterval 中得到的一些思考 🤔
date: 2021-10-10
tags:
  - Javascript
categories:
  - 前端
---

## 关于第二个参数的一些思考

```javascript
setTimeout(fn,[delay, arg1, arg2, ...]);
setTimeout(code,[delay, arg1, arg2, ...]);
```

delay 取默认值 0

```typescript
function test() {
  let start = Date.now()
  let times = []
  setTimeout(function run() {
    times.push(Date.now() - start) // 保存前一个调用的延时

    if (start + 100 < Date.now()) {
      // 100 毫秒之后，显示延时信息
      console.log(times)
    } else {
      // 否则重新调度
      setTimeout(run)
    }
  })
}
test()

// Chrome 中的输出示例：
// 1,1,1,1,9,15,20,24,30,35,40,45,50,55,59,64,70,75,80,85,90,95,100

// Node 中的输出示例：
// 2,3,3,4,6,7,8,9,9,10,12,13,14,15,16,18,19,20,21,22,23,24,26,27,
```





## setTimeout

> setTimeout() 方法用于在指定的毫秒数后调用函数或计算表达式。

第一个参数可以是函数，也可以是字符串，如果是字符串，会被当作函数来执行。 第二个参数是毫秒数，表示多少毫秒后执行第一个参数。 后续的参数是可选的，表示传递给第一个参数的参数。

## setInterval

> setInterval() 方法会不停地调用函数，直到 clearInterval() 被调用或窗口被关闭。

第一个参数可以是函数，也可以是字符串，如果是字符串，会被当作函数来执行。

## 从 setTimeout、setInterval 上看 Event Loop

### 什么是 Event Loop

### 看一道经典题，输出顺序是什么？

```typescript
async function async1() {
  console.log('1')
  await async2()
  console.log('2')
  setTimeout(function () {
    console.log('3')
  }, 0)
}

async function async2() {
  console.log('4')
  setTimeout(function () {
    console.log('5')
  }, 0)
}

console.log('6')

setTimeout(function () {
  console.log('7')
}, 0)

async1()

new Promise(function (resolve) {
  console.log('8')
  resolve()
}).then(function () {
  console.log('9')
})

// 输出结果顺序
// 6 1 4 8 2 9 7 5 3
```

## 好文

- https://zh.javascript.info/settimeout-setinterval

- https://www.jeffjade.com/2016/01/10/2016-01-10-javacript-setTimeout/
- https://www.jeffjade.com/2016/01/10/2016-01-10-javaScript-setInterval/

---

## File: posts/01-frontend/JavaScript/宏任务，微任务.md

---
title: 说说 Event Loop 事件循环、微任务、宏任务
date: 2019-10-10
tags:
  - Javascript
categories:
  - 前端
---

1. js 的执行顺序，先同步后异步
2. 异步中任务队列的执行顺序： 先微任务 microtask 队列，再宏任务 macrotask 队列
3. 调用 Promise 中的 resolve，reject 属于微任务队列，setTimeout 属于宏任务队列注意以上都是 队列，先进先出。

## 事件循环执行机制

- 1.进入到 script 标签,就进入到了第一次事件循环.
- 2.遇到同步代码，立即执行
- 3.遇到宏任务,放入到宏任务队列里.
- 4.遇到微任务,放入到微任务队列里.
- 5.执行完所有同步代码
- 6.执行微任务代码
- 7.微任务代码执行完毕，本次队列清空
- 8.寻找下一个宏任务，重复步骤 1
  - 以此反复直到清空所以宏任务，这种不断重复的执行机制，就叫做事件循环



## 相关链接

https://blog.51cto.com/u_15812740/5728035

---

## File: posts/01-frontend/JavaScript/精度问题.md

---
title: 关于 JavaScript 精度问题的一些总结
date: 2019-10-10
tags:
  - Javascript
categories:
  - 前端
---

# 关于 JavaScript 精度问题的一些总结

## 1. 为什么会有精度问题

JavaScript 中的数字类型是双精度浮点数，也就是说，它的精度是有限的，而且是有限的小数位数。这个小数位数是 17 位，也就是说，如果一个数字的小数位数超过了 17 位，那么这个数字就会被四舍五入。

---

## File: posts/01-frontend/React/react优化.md

---
title: React 简单优化
date: 2022-09-03
tags:
  - React
---

### 优化的原因

react 里的一个基本常识是 re-render：当一个组件里的某个状态改变的时候，他会重新渲染，也就是重新执行所有组件代码，包括它的子组件。

所以会带来很多不必要的重新渲染。

### useMemo

useMemo 的基本理念是，它允许我们在渲染之间 "记住 "一个计算值

#### 使用的场景举例

```tsx
function App() {
  const [selectedNum, setSelectedNum] = React.useState(100);
  const time = useTime();

  const allPrimes = React.useMemo(() => {
    const result = [];

    for (let counter = 2; counter < selectedNum; counter++) {
      if (isPrime(counter)) {
        result.push(counter);
      }
    }

    return result;
  }, [selectedNum]);

   return (
    <-省略dom->
   )
```

这里 App 组件里有一个状态 selectedNum,还有一个自定义 hook 导出的状态 time。假如不使用 useMemo 包裹 allPrimes，那么每当 time 变化的时候，就会重新执行 allPrimses 里面的代码，造成浪费；然而我们的 allPrimes 其实只跟 selectedNum 相关，所以我们这里用 useMemo 缓存函数包裹后，里面的代码就只会在 selectedNum 变化时执行，而不受 App 组件的 re-render 影响。

当然，这里也可以有其他的写法，我们可以把 allPrimes 和 selectedNum 包括它的 dom 合并成一个子组件，time 和它的 dom 也合并成一个子组件

```tsx
function App(){

  return(
  	<Time />
    <AllPrimes />
  )
}
```

这样，他们的状态隔离开，也可以实现当 time 里面的状态改变的时候，不会影响别的代码去 re-render。

但是，有时候，我们反而是需要把状态放到外部组件的，因为可能有别的组件也同样依赖这个状态。

```tsx
function App(){
  const time = useTime();
  return(
  	<Time time={time} />
    <AllPrimes />
  )
}
```

这样的话，当 App 组件的 time 状态变化的时候，其实同样会使 Time 组件和 AllPrimes 组件 re-render。这个时候，就可以使用 React.memo 来优化

```tsx
const PureAllPrimes = React.memo(AllPrimes);
function App(){
  const time = useTime();
  return(
  	<Time time={time} />
    <PureAllPrimes />
  )
}
```

React.memo 可以把一个组件变成 pure component，pure component 只有在 props 变化的时候，才会 re-render；

当然，也可以在 AllPrimes 组件里，导出的时候去使用 React.memo

```tsx
export default React.memo(AllPrimes)
```

我们来举另外一个例子

Boxes 是一个 pure component,只有 boxes 变化的时候，才会 re-render;

```tsx
function Boxes({ boxes }) {
  return (
    <div className='boxes-wrapper'>
      {boxes.map((boxStyles, index) => (
        <div key={index} className='box' style={boxStyles} />
      ))}
    </div>
  )
}

export default React.memo(Boxes)
```

App 组件里引用了 Boxes,并声明常量 boxes 传入 Boxes 组件里

```tsx
import Boxes from './Boxes';

function App() {
  const [name, setName] = React.useState('');
  const [boxWidth, setBoxWidth] = React.useState(1);
  const boxes = [
    { flex: boxWidth, background: 'hsl(345deg 100% 50%)' },
    { flex: 3, background: 'hsl(260deg 100% 40%)' },
    { flex: 1, background: 'hsl(50deg 100% 60%)' },
  ];

  return (
    <>
      <Boxes boxes={boxes} />
      <-省略dom->
    </>
  );
}
```

当状态 name 变化的时候，我们期望只是 re-render App 组件，Boxes 组件不 re-render，然而 Boxes 组件仍然 re-render 了。这是因为，App 组件 re-render 的时候，重新生成了 boxes 变量，尽管是同样的值，但不是一个引用，所以导致了 Boxes 组件的 re-render

要解决这个问题，只需要使用 useMemo 包裹一下即可

```tsx
const boxes = React.useMemo(() => {
  return [
    { flex: boxWidth, background: 'hsl(345deg 100% 50%)' },
    { flex: 3, background: 'hsl(260deg 100% 40%)' },
    { flex: 1, background: 'hsl(50deg 100% 60%)' },
  ]
}, [boxWidth])
```

### UseCallback

它跟 useMemo 是一样的，都是缓存函数，但是他缓存的不是值，而是函数；

#### 使用场景举例

MegaBoost 是一个 pure component,接受一个 callback 回调函数，只有当 callback 变化的时候才会 re-render

```tsx
function MegaBoost({ handleClick }) {
  console.log('Render MegaBoost')

  return (
    <button className='mega-boost-button' onClick={handleClick}>
      MEGA BOOST!
    </button>
  )
}

export default React.memo(MegaBoost)
```

App 组件里引用了 MegaBoost

```tsx
function App() {
  const [count, setCount] = React.useState(0)
  function handleMegaBoost() {
    setCount((currentValue) => currentValue + 1234)
  }

  return (
    <>
      Count: {count}
      <button
        onClick={() => {
          setCount(count + 1)
        }}
      >
        Click me!
      </button>
      <MegaBoost handleClick={handleMegaBoost} />
    </>
  )
}

export default App
```

App 里声明了一个状态 count，一个函数 handleMegaBoost 并传入 MegaBoost 组件。

当状态 count 变化的时候，会触发 App 组件的 re-render，并且重新生成一个新的 handleMegaBoost 传入 MegaBoost 组件，导致 MegaBoost 组件的 re-render。

怎么解决这个问题呢？很简单，使用缓存函数即可

```tsx
const handleMegaBoost = React.useMemo(() => {
  return function () {
    setCount((currentValue) => currentValue + 1234)
  }
}, [])
```

但是，一般我们更倾向于使用 useCallback

```tsx
const handleMegaBoost = React.useCallback(() => {
  setCount((currentValue) => currentValue + 1234)
}, [])
```

### 总结

1. 组件里一些”经过复杂的逻辑计算而得到某个值“需要使用 Usememo 包裹，保证组件在 re-render 的时候不去重复计算
2. 父子组件的场景，子组件尽量用 React.memo 包裹成 pure component，避免父组件的 re-render 导致子组件跟着一起 re-render
3. 父子组件的场景，父组件声明变量传入子组件的时候，尽量用 useMemo 包裹，避免父组件 re-render 导致重新生成变量，而进一步导致子组件的 re-render
4. 父子组件的场景，副组件声明 callback 传入子组件的时候，尽量使用 useCallback 包裹，避免父组件 re-render 导致重新生成 callback，而进一步导致子组件的 re-render

### 引用链接

[引用链接](https://www.joshwcomeau.com/react/usememo-and-usecallback/#use-case-2-preserved-references)

---

## File: posts/01-frontend/Vue/Vuex精简文档.md

[Binary file]

---

## File: posts/01-frontend/Vue/Vue中key值的作用.md

[Binary file]

---

## File: posts/01-frontend/Vue/vue路由history模式下刷新页面404.md

[Binary file]

---

## File: posts/01-frontend/WebRTC/01-stream-capture.md

---
title: WebRTC 从实战到未来
date: 2022-12-10
tags:
  - WebRTC
categories:
  - 前端
---

# WebRTC 从实战到未来



## 这篇文章可以学到什么

- 学会如何使用 WebRTC 的一些 API
- 学会如何`分享屏幕`，`录制屏幕`
- 学会如何通过 WebRTC 实现`拍照`
- 学会如何`实现视频虚拟背景`
- 搭建一个 `1v1` 的 WebRTC `实时音视频通话`

所有示例在线体验地址：[frontend-park](https://fedtop.com/webrtc) 源码地址：[frontend-park](https://github.com/wangrongding/frontend-park)

## 前言

很开心在九月签约了掘金，首先感谢平台的肯定，后面几个月我会认真的给大家带来一些有价值的文章。 所以我开了一个专栏 《WebRTC 从实战到未来》，我将通过这个专栏，分享一下自己在 WebRTC 相关技术栈上的一些经验和思考，希望能够帮助到大家。🌸

虽然这篇文章作为专栏的第一篇，但我也不会从 WebRTC 的基础知识和相关概念开始讲起，因为那样会非常的枯燥乏味，我会从实际项目中的一些能快速上手的应用出发，讲一下我在实际项目中遇到的一些问题，以及我是如何解决和实现的。这样的好处是，你可以快速的上手，更加专注于实际项目中的应用，而不是上来就是大量的概念和协议相关知识直接给整劝退了。🥲🥲🥲

## 什么是 WebRTC

`WebRTC` (Web Real-Time Communications) 是一项`实时通讯技术`，它允许网络应用或者站点，在不借助中间媒介的情况下，建立浏览器之间`点对点（Peer-to-Peer）的连接`，实现视频流和（或）音频流或者其他`任意数据`的传输。WebRTC 包含的这些标准使用户在无需安装任何插件或者第三方的软件的情况下，创建点对点（Peer-to-Peer）的数据分享和电话会议成为可能。

## WebRTC 的应用场景

- 直播
- 游戏
- 视频会议/在线教育
- 屏幕共享/远程控制
- 等等等

可玩性很强，可以做很多有趣的事情。（哪怕是被玩烂了概念“元宇宙”😂） 在网速与硬件越来越好的趋势下，WebRTC 它有无限可能。

## 媒体流

要想了解 WebRTC，首先要了解媒体流，媒体流可以是来自本地设备的，也可以是来自远程设备的。媒体流可以是实时的，也可以是非实时的。上述的应用场景中，我们都需要使用到`媒体流`，我们可以通过摄像头，麦克风，屏幕共享等方式获取到媒体流，然后通过 WebRTC 技术将媒体流传输到远端实现实时通讯。



## 摄像头获取媒体流及一些其他操作

要实现 音视频通话，我们肯定要先获取到摄像头的媒体流，然后通过 WebRTC 技术将媒体流传输到远端实现实时通讯。下面我们先通过一个简单的拍照小应用来看一下如何通过摄像头获取媒体流。



先设置好用于播放媒体流的 video 标签，添加一个 autoplay 属性，这样就可以在摄像头获取到媒体流后自动播放了。

```html
<video id="localVideo" autoplay playsinline muted></video>
```

**需要注意的是，WebRTC 只能在 HTTPS 协议或者 localhost 下使用，如果是 HTTP 协议，会报错。**



这里我们暂时使用 localhost 做简单的演示，后面通过`信令服务器`实现实时音视频的章节我会讲到如何在本地用 `mkcert` 做自签名证书。

ok, continue ~

我们主要通过`navigator.mediaDevices.getUserMedia(constraints)`这个 api 来获取媒体流，这个方法接收一个配置对象作为参数，配置对象中包含了媒体流的类型，以及媒体流的分辨率等信息。

```typescript
// 获取本地音视频流
async function getLocalStream(constraints: MediaStreamConstraints) {
  // 获取媒体流
  const stream = await navigator.mediaDevices.getUserMedia(constraints)
}
```

其中`constraints`指定了请求的媒体类型和相对应的参数，用于配置视频流和音频流的信息。

我可以看下`constraints`参数中具体支持哪些配置项，可以通过`navigator.mediaDevices.getSupportedConstraints()`这个方法来获取。

```typescript
console.log('🚀🚀🚀 / SupportedConstraints', navigator.mediaDevices.getSupportedConstraints())
```

我们把它打印出来，可以看到它支持的配置项有：  


通常我们不设置`constraints`参数，那么默认就是获取摄像头和麦克风的媒体流，如果我们只想要获取摄像头的媒体流，那么我们可以这样设置：

```typescript
navigator.mediaDevices.getUserMedia({
  audio: false,
  video: true,
})
```

如果我们想要获取视频的高度宽度，那么我们可以这样设置：

```typescript
navigator.mediaDevices.getUserMedia({
  audio: false,
  video: {
    width: 1280,
    height: 720,
  },
})
```

诸如此类，video 中也可以设置设备 id 或者前后置摄像头...， 大家可以在支持的情况下根据自己的需求来设置即可。具体可以查看[MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/MediaDevices/getUserMedia)。这里我不做过多的介绍了，我们继续。

获取通过摄像头获取媒体流后，将媒体流赋值给 video 标签的 srcObject 属性，让其播放。

```typescript
// 获取本地音视频流
async function getLocalStream(constraints: MediaStreamConstraints) {
  // 获取媒体流
  const stream = await navigator.mediaDevices.getUserMedia(constraints)
  // 将媒体流设置到 video 标签上播放
  playLocalStream(stream)
}

// 播放本地视频流
function playLocalStream(stream: MediaStream) {
  const videoEl = document.getElementById('localVideo') as HTMLVideoElement
  videoEl.srcObject = stream
}

getLocalStream({
  audio: false,
  video: true,
})
```

实现拍照功能，canvas 标签可以将媒体流绘制到 canvas 上，也可以通过 toDataURL 方法将 canvas 转换为 base64 图片后做一些其他操作。

附上一个 👉 [体验地址](https://frontend-park.vercel.app/audio-and-video/webRTC/take-photos)



```html
<video id="localVideo" autoplay playsinline muted></video>
<div v-for="(item,index) in imgList.length" :key="index" class="item">
  <img :src="item" alt="" />
</div>
```

我们通过获取已经在播放媒体流的 video 标签，然后将其绘制到 canvas 上，再通过 toDataURL 方法将 canvas 转换为 base64 图片。

这里你可以加一些滤镜或者加一些美颜功能或是其他的操作，最终生成的 imgUrl 给到 img 标签让其展示就行了。

```typescript
// 拍照
function takePhoto() {
  const videoEl = document.getElementById('localVideo') as HTMLVideoElement
  const canvas = document.createElement('canvas')
  canvas.width = videoEl.videoWidth
  canvas.height = videoEl.videoHeight
  const ctx = canvas.getContext('2d')!
  ctx.drawImage(videoEl, 0, 0, canvas.width, canvas.height)
  imgList.value.push(canvas.toDataURL('image/png'))
  console.log('🚀🚀🚀 / imgList', imgList)

  // 添加滤镜
  const filterList = [
    'blur(5px)', // 模糊
    'brightness(0.5)', // 亮度
    'contrast(200%)', // 对比度
    'grayscale(100%)', // 灰度
    'hue-rotate(90deg)', // 色相旋转
    'invert(100%)', // 反色
    'opacity(90%)', // 透明度
    'saturate(200%)', // 饱和度
    'saturate(20%)', // 饱和度
    'sepia(100%)', // 褐色
    'drop-shadow(4px 4px 8px blue)', // 阴影
  ]

  for (let i = 0; i < filterList.length; i++) {
    ctx.filter = filterList[i]
    ctx.drawImage(videoEl, 0, 0, canvas.width, canvas.height)
    imgList.value.push(canvas.toDataURL('image/png'))
  }
}
```

拍摄的时候我们也可以切换摄像头，这里我们通过调用 `navigator.mediaDevices.enumerateDevices` 方法，获取到所有的设备，然后筛选出 videoinput 类型的设备，最后通过不同的设备 id 来实现切换摄像头。

```typescript
// 获取所有视频输入设备
async function getDevices() {
  const devices = await navigator.mediaDevices.enumerateDevices()
  console.log('🚀🚀🚀 / devices', devices)
  let videoDevices = devices.filter((device) => device.kind === 'videoinput')
}
```

```typescript
// 切换设备
function handleDeviceChange(deviceId: string) {
  getLocalStream()
  const stream = await navigator.mediaDevices.getUserMedia({
    audio: false,
    video: {
      deviceId: { exact: deviceId },
    },
  })
}
```

这里我们把获取到的设备列表信息打印看看，我们可以看到每个设备都有一个 deviceId，我们就是通过这个 id 来切换设备的。



可以看到，获得了多个摄像头设备，我这里是一个笔记本自带的摄像头和一个 OBS 虚拟摄像头，包括最近 MacOs 更新到 Ventura 13 ,IOS 更新到 16 后的`连续互通摄像头`，都可以获取到。这样我们就可以在视频的时候，就可以通过拍摄更清晰的手机后置来拍摄了。

虚拟摄像头更有意思，在 OBS 中开启虚拟摄像头后，可以播放一个视频，然后进行视频会议，这样你甚至可以提前录制好一个端坐的视频（简直是上网课必备！😅），我之前试过播放特朗普的视频，然后微信视频，对面看到的确实是特朗普在演讲，所以说这方面很有安全隐患，所以大家在网上和别人视频的时候，还是需要注意下，对方可能不是真的。

  
  


跑题了，我们继续。🦄🦄🦄

说完了切换摄像头，我们再来看看如何在支持切换前后置摄像头的设备上如何切换前后摄像头。我们可以通过指定 `facingMode` 来实现，facingMode 有 4 个值，分别是 user、environment 和 left、right，分别对应前后摄像头和左右摄像头。

当需要强制使用前置摄像头时，可以使用 exact 关键字，例如 facingMode: { exact: 'user' }，强制切换前后摄像头时，当摄像头不支持时，会报一个 OverconstrainedError［无法满足要求的错误］  


```typescript
// 切换前后摄像头
function switchCamera(val: number) {
  let constraints = {
    video: true, // 开启默认摄像头
    audio: true,
  }
  constraints.video = {
    // 强制切换前后摄像头时，当摄像头不支持时，会报一个OverconstrainedError［无法满足要求的错误］
    facingMode: { exact: val === 1 ? 'user' : 'environment' },
    // 也可以这样当前后摄像头不支持切换时，会继续使用当前摄像头，好处是不会报错
    // facingMode: val === 1 ? 'user' : 'environment',
  }

  navigator.mediaDevices
    .getUserMedia(constraints)
    .then((stream) => {
      ElMessage.success('切换成功')
      playLocalStream(stream)
    })
    .catch((err) => {
      ElMessage.error('你的设备不支持切换前后摄像头')
    })
}
switchCamera(1) // 切换前置摄像头
```



通过这个简单的拍照小应用，相信我们已经知道了通过摄像头获取媒体流的大概流程以及一些常用的 API 了。

ok, continue ~

## 通过屏幕共享获取获取媒体流，实现录制等操作

在视频会议中，我们经常会使用到屏幕共享，已经我们经常会有录制屏幕等需求，市面上还有那么多需要付费的软件，我们通过 WebRTC 配合一些相关 api 自己实现一个不是更好吗？🤔🤔🤔 既省钱又学到了知识。

那么我们如何通过屏幕共享获取媒体流并实现录制呢？下面通过一个小 demo 来简单实现一下。

在 WebRTC 中，我们可以通过 `getDisplayMedia` 来获取屏幕共享的媒体流，这个 API 与 `getUserMedia` 类似，但是它只能获取屏幕共享的媒体流。

```typescript
// 获取屏幕共享的媒体流
async function shareScreen() {
  let localStream = await navigator.mediaDevices.getDisplayMedia({
    audio: true,
    video: true,
  })
  // 播放本地视频流
  playStream(localStream)
}

// 在视频标签中播放视频流
function playStream(stream: MediaStream) {
  const video = document.querySelector('#localVideo') as HTMLVideoElement
  video.srcObject = stream
}
```

执行 `shareScreen` 函数后，会弹出一个权限询问框，询问是否允许获取屏幕共享的媒体流。

然后你就可以分享你的整个屏幕，如果你又多个屏幕的话，你可以选择其中一个进行分享  


然后你也可以选择只分享你屏幕上的某个应用的窗口，不用担心你一边干嘛干嘛一边录制屏幕，它只会捕捉你选择的应用窗口的内容。非常 nice。



你甚至可以在你浏览器打开的各种页面中，选择一个你想要分享的网页，当你页面各种切换时候，你的屏幕共享也只会显示你选择的网页的内容。

共享前你可以随便选一个进行预览，然后可以选择是否分享的时候包含页面中的音频，这样你获取的媒体流就会包含音频轨道了。



这里我打开自己 github 的网页，然后点击屏幕共享，可以看到共享的只有自己的 github 页面了。不用担心会有什么奇怪的东西乱入进来，非常适合视频会议或者在线教育等场景。



说完获取屏幕媒体流，接下来我们来看看如何通过媒体流进行录制。

[完整的 MIME 类型列表](https://www.iana.org/assignments/media-types/media-types.xhtml)

这里我们使用 `MediaRecorder` 来进行录制，它是一个用于录制媒体流的 API，它可以将媒体流中的数据进行录制，然后将录制的数据保存成一个文件。

由于 `MediaRecorder` api 的对 mimeType 参数的支持是有限的。所以我们需要通过 `MediaRecorder.isTypeSupported` 来判断当前浏览器是否支持我们需要的 mimeType。

chrome 中 `MediaRecorder` 支持的 `mimeType` 如下：

```sh
"video/webm"
"video/webm;codecs=vp8"
"video/webm;codecs=vp9"
"video/webm;codecs=h264"
"video/x-matroska;codecs=avc1"
```



为了验证上述的内容，这里我把一些常用的 mimeType 列出来，拼装后通过 `MediaRecorder.isTypeSupported` 来判断是否支持，最后放到下拉框中供用户根据自己的需求选择合适的 mimeType。

```typescript
// 获取支持的媒体类型
function getSupportedMimeTypes() {
  const media = 'video'
  // 常用的视频格式
  const types = ['webm', 'mp4', 'ogg', 'mov', 'avi', 'wmv', 'flv', 'mkv', 'ts', 'x-matroska']
  // 常用的视频编码
  const codecs = ['vp9', 'vp9.0', 'vp8', 'vp8.0', 'avc1', 'av1', 'h265', 'h264']
  // 支持的媒体类型
  const supported: string[] = []
  const isSupported = MediaRecorder.isTypeSupported
  // 遍历判断所有的媒体类型
  types.forEach((type: string) => {
    const mimeType = `${media}/${type}`
    codecs.forEach((codec: string) =>
      [`${mimeType};codecs=${codec}`, `${mimeType};codecs=${codec.toUpperCase()}`].forEach((variation) => {
        if (isSupported(variation)) supported.push(variation)
      }),
    )
    if (isSupported(mimeType)) supported.push(mimeType)
  })
  return supported
}

console.log(getSupportedMimeTypes())
```

可以看到这么多排列组合后，筛选出的支持的 mimeType 也就只有`"video/webm"`和 ` "video/x-matroska"` 两种。



也可以通过这个网址[👉🏻media-mime-support](https://cconcolato.github.io/media-mime-support/) 来查看当前浏览器所支持的 mimeType 的情况。

目前最常用的一般都是 `video/mp4`。 截止到目前为止，最佳的 8 种视频格式为：`mp4` ,`webm` ,`mov` ,`avi` ,`mkv` ,`wmv` ,`avchd` ,`flv`。而 webm 是 Google 专门为 web 端推出的一种视频格式。100% 开源，且 100%兼容 Google Chrome 浏览器和 Android 设备。如果你没有强烈的需求，也可以使用 webm 格式。

说了这么多，不支持就不能录制成 mp4 了？🥲  
肯定不是啊 😂

都拿到 blob 了，可以通过 ffmpeg.js 来将 webm 格式转换成 mp4 格式，但是 ffmpeg.js 的体积比较大，太重了。这里也可以通过一种 hack 的方式来实现，但是不靠谱，这种方式导出的 mp4 文件部分软件可能会识别不了，求稳的话最好还是推荐使用我们浏览器环境列出的支持的 mimeType，或者用工具转一下。

```typescript
// 录制媒体流
function startRecord() {
  const kbps = 1024
  const Mbps = kbps * kbps
  const options = {
    audioBitsPerSecond: 128000,
    videoBitsPerSecond: 2500000,
    mimeType: 'video/webm; codecs="vp8,opus"',
  }
  const mediaRecorder = new MediaRecorder(localStream, options)
  mediaRecorder.start()

  mediaRecorder.ondataavailable = (e) => {
    // 将录制的数据合并成一个 Blob 对象
    // const blob = new Blob([e.data], { type: e.data.type })

    // 🌸重点是这个地方，我们不要把获取到的 e.data.type设置成 blob 的 type，而是直接改成 mp4
    const blob = new Blob([e.data], { type: 'video/mp4' })
    downloadBlob(blob)
  }
  mediaRecorder.onstop = (e: Event) => {
    // 停止录制
  }
}

// 下载 Blob
function downloadBlob(blob: Blob) {
  // 将 Blob 对象转换成一个 URL 地址
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  // 设置 a 标签的 href 属性为刚刚生成的 URL 地址
  a.href = url
  // 设置 a 标签的 download 属性为文件名
  a.download = `${new Date().getTime()}.${blob.type.split('/')[1]}`
  // 模拟点击 a 标签
  a.click()
  // 释放 URL 地址
  URL.revokeObjectURL(url)
}
```

然后我们就可以愉快的录制视频了。当然这里只是用分享屏幕的方式来录制视频，如果你想要录制摄像头的视频，也是一样，拿到媒体流后，就可以直接录制了。

[👉 线上体验地址：](https://frontend-park.vercel.app/audio-and-video/webRTC/audio-and-video/webRTC/record)



当然，既然都拿到了媒体流，那么我们也可以将媒体流中的视频轨道录制成 gif 图片，这样在一些场景下分享起来也会更加方便。

由于 `MediaRecorder` api 不支持将 mimeType 设置成 image/gif ，所以我们需要借助一个第三方库`MediaStreamRecorder`来实现。它的用法和 `MediaRecorder` 基本一致。我就不再赘述了。

最后有一个需要注意的地方，也是我在实际项目中遇到的问题。截止到目前为止，在使用 `MediaRecorder` 录制视频的时候，如果你的系统是 Windows 或者 Chrome OS，那么录制的视频没什么问题，但是在 Mac 和 Linux 上，录制摄像头和分享屏幕时，选择网页的分享方式，所拿获得的媒体流是可以拿到视频轨道和音频轨道的，但是录制整个屏幕时，由于系统的限制，只能拿到视频的轨道。好在一般录屏都不会有带音频的需求，期待后面能够支持。



## 实现视频的虚拟背景

在视频会议中，我们经常会看到一些人在视频中的背景是虚拟的，这样可以让我们更专注于对方的表情和语言，而不会被背景中的一些干扰因素所分散注意力。那么我们如何实现这样的效果呢？

下面我们先通过一个简单的 demo 来看看效果。

[👉 线上体验地址](https://frontend-park.vercel.app/audio-and-video/webRTC/background-process)



主要原理是通过 `canvas` 将视频中的每一帧画到画布上，然后将画布中的像素逐个与设定的背景色（默认是绿色，你可以更换为任意符合你背景的颜色）进行计算，比较后的差值达到设定的阈值时，对其进行处理，将其更换为预先准备好的背景图的图像数据，最后将处理后的图像数据再画到虚拟背景画布上，通过虚拟背景画布拿到媒体流后给到 video 标签播放， 这样就实现了视频的虚拟背景效果。

下面我们来看看具体的实现。

为保持大小一致，这里我们统一设置画布和视频的宽高为 480\*300。

```html
<canvas id="backgroundImg" width="480" height="300"></canvas>
<video id="real-video" width="480" height="300" autoplay muted></video>
<video id="virtual-video" width="480" height="300" autoplay muted></video>
```

首先我们需要准备好背景图，这里我使用了一张图片，你也可以使用视频作为背景。

通过 canvas 将背景图画到画布上，然后通过 `getImageData` 方法拿到图像数据。

```typescript
// 虚拟背景的 canvas中的图片数据
let backgroundImageData: ImageData
// 获取背景图像数据
function getBackgroundImageData() {
  const backgroundCanvas = document.querySelector('#backgroundImg') as HTMLCanvasElement
  const backgroundCtx = backgroundCanvas.getContext('2d')!
  const img = new Image()
  img.src = 'https://xxxx.png'
  img.onload = () => {
    backgroundCtx.drawImage(img, 0, 0, backgroundCanvas.width, backgroundCanvas.height)

    backgroundImageData = backgroundCtx.getImageData(0, 0, backgroundCanvas.width, backgroundCanvas.height)
  }
}
```

然后我们需要通过摄像头获取到视频流，还是用之前几个 demo 中的方法。

```typescript
// 获取本地音视频流
async function getLocalStream(options: MediaStreamConstraints) {
  const stream = await navigator.mediaDevices.getUserMedia(options)
  return stream
}

// 播放原始视频流
function playRealVideo(stream: MediaStream) {
  realVideo = document.querySelector('#real-video') as HTMLVideoElement
  realVideo.srcObject = stream
}
```

上述步骤完成后，我们就可以通过创建 `canvas` 标签 先将真实的视频每隔 40ms 一次 画到画布上。

这样的话，画布就会不断的更新，能达到 25 帧的效果，这样能保证我们的视频流是非常流畅的。

一般来说，人眼舒适放松时可视帧数是每秒 24 帧，高度集中精神时不超过 30 帧。电影院里的 2D 电影一般是 24 帧的，3D 电影一般是 60 帧以上。

画到画布后，我们也相应的要通过 `getImageData` 方法拿到真实视频的图像数据。

然后每一帧都要与设置好的背景色进行比较，比较后的差值达到设定的阈值的像素，就要扣除（替换为之前拿到的背景图的像素。

  


看到这里,如果以前看过我的文章，大家一定很眼熟，这个计算颜色差的逻辑与我之前写的[《我用 10000 张图片合成我们美好的瞬间》](https://juejin.cn/post/6996431901623844894)用来做合成图的逻辑是一样的。

首先需要明白的一点就是，rgb 的色域是一个 3 维的空间，我们可以通过计算两个点之间的距离来判断两个颜色的差异。距离越小，颜色差异越小。

而这只需要我们中学时期学过的 欧式距离 公式就可以了。



我们把它转化为颜色差的计算公式如下：

```typescript
// 计算颜色差
function colorDiff(color1: number[], color2: number[]) {
  const r = color1[0] - color2[0]
  const g = color1[1] - color2[1]
  const b = color1[2] - color2[2]
  return Math.sqrt(r * r + g * g + b * b)
}
```

然后再将处理后的图像数据画到虚拟视频的画布上，再通过`captureStream`api 将画布转换为视频流，最后将视频流赋值给虚拟视频的 `srcObject` 属性。

代码如下：

```typescript
const WIDTH = 480 // 视频宽度
const HEIGHT = 300 // 视频高度

// 将视频写到 canvas 中
function drawVideoToCanvas(realVideo: HTMLVideoElement) {
  realVideoCanvas = document.createElement('canvas') as HTMLCanvasElement
  realVideoCtx = realVideoCanvas.getContext('2d')!
  virtualVideoCanvas = document.createElement('canvas') as HTMLCanvasElement
  virtualVideoCtx = virtualVideoCanvas.getContext('2d')!
  realVideoCanvas.width = virtualVideoCanvas.width = WIDTH
  realVideoCanvas.height = virtualVideoCanvas.height = HEIGHT

  // 每隔 100ms 将真实的视频写到 canvas 中，并获取视频的图像数据
  setInterval(() => {
    realVideoCtx.drawImage(realVideo, 0, 0, realVideoCanvas.width, realVideoCanvas.height)
    // 获取 realVideoCanvas 中的图像数据
    realVideoImageData = realVideoCtx.getImageData(0, 0, realVideoCanvas.width, realVideoCanvas.height)
    // 处理真实视频的图像数据，将其写到虚拟视频的 canvas 中
    processFrameDrawToVirtualVideo()
  }, 40)
  // 从 VirtualVideoCanvas 中获取视频流并在 virtualVideo 中播放
  getStreamFromVirtualVideoCanvas()
}

// 从 VirtualVideoCanvas 中获取视频流并在 virtualVideo 中播放
function getStreamFromVirtualVideoCanvas() {
  virtualVideo = document.querySelector('#virtual-video') as HTMLVideoElement
  const stream = virtualVideoCanvas.captureStream(30)
  virtualVideo.srcObject = stream
}
```

逐像素计算与要处理的目标颜色的差值，如果差值小于容差，则将该像素设置为背景图片中的对应像素

```typescript
// 处理真实视频的图像数据，将其写到虚拟视频的 canvas 中
function processFrameDrawToVirtualVideo() {
  // 逐像素计算与要处理的目标颜色的差值，如果差值小于阈值，则将该像素设置为背景图片中的对应像素
  for (let i = 0; i < realVideoImageData.data.length; i += 4) {
    const r = realVideoImageData.data[i]
    const g = realVideoImageData.data[i + 1]
    const b = realVideoImageData.data[i + 2]
    const a = realVideoImageData.data[i + 3]
    const bgR = backgroundImageData.data[i]
    const bgG = backgroundImageData.data[i + 1]
    const bgB = backgroundImageData.data[i + 2]
    const bgA = backgroundImageData.data[i + 3]

    // 计算与背景色的差值
    const diff = colorDiff([r, g, b], backgroundColor)
    // 当差值小于设定的阈值，则将该像素设置为背景图片中的对应像素
    if (diff < allowance.value) {
      realVideoImageData.data[i] = bgR
      realVideoImageData.data[i + 1] = bgG
      realVideoImageData.data[i + 2] = bgB
      realVideoImageData.data[i + 3] = bgA
    }
  }
  // 将处理后的图像数据写到虚拟视频的 canvas 中
  virtualVideoCtx.putImageData(realVideoImageData, 0, 0)
}

// 计算颜色差异
function colorDiff(rgba1: number[], rgba2: number[]) {
  let d = 0
  for (let i = 0; i < rgba1.length; i++) {
    d += (rgba1[i] - rgba2[i]) ** 2
  }
  return Math.sqrt(d)
}
```

可以看到，其中`backgroundColor`（需要扣除的背景色）和`allowance`（容差值）两个变量是由外部控制的，这样我们就可以在页面上通过滑动条或是其他的组件来动态改变容差，通过取色器来动态改变需要扣除的背景色。



## 最后

至此，我们就可以实现一个简单的背景替换的功能了。当然，这里只是简单的实现了一个背景替换的功能，实际上，我们还可以通过更多的技术手段来实现更加复杂的功能，比如：

目前只是针对纯色的背景进行了替换，如果复杂的背景，我们可以通过图像分割的方式来实现背景替换，比如：TensorFlow.js 中的 身体分割（BodyPix）。



或者是说，对于视频中的人脸，我们可以通过`face-api.js`来检测人脸，并将人脸替换为其他的图片，从而实现一个简单的换脸功能。对于视频中的人体，我们可以通过`posenet`来检测人体，并将人体替换为其他的图片，从而实现一个简单的换装功能。等等...（后续我都在这个专栏中安排~）

可以见得，用 WebRTC 相关的知识来结合一些其他相关技术，可以实现非常多的有趣的项目，可玩性非常强。

这作为我专栏的第一篇，主要是想通过这篇文章来介绍一下 WebRTC 相关的知识，以及 WebRTC 相关的一些应用场景，希望能够帮助到大家。

本来还想写下 1v1 视频聊天的实现，但是由于时间关系，我把它放到第二篇来写吧，demo 我已经放到了 我的[前端公园合集仓库](https://github.com/wangrongding/frontend-park)中，这两天抽空写完~



## 最后的最后 ~

大家喜欢这个专栏的话，😁 可以点点赞 ~ ，你的支持就是我的动力 🌸

---

## File: posts/01-frontend/WebRTC/02-p2p.md

---
title: WebRTC 从实战到未来！第二篇，音视频通话实战与原理 🔥
date: 2022-12-15
tags:
  - WebRTC
categories:
  - 前端
---

## WebRTC 从实战到未来！第二篇，音视频通话实战与原理 🔥

[👉🏻 本文示例源代码地址](https://github.com/wangrongding/frontend-park)  
[👉🏻 本文示例在线体验地址](https://frontend-park.vercel.app/audio-and-video/webRTC/p2p)

你可以开两个浏览器 tab 或者用不同的设备，根据说明即可体验音视频通话的实现，本文将会从实战出发，讲解 WebRTC 的音视频通话原理。



看到上一篇文章很受大家的欢迎 👏🏻，短短的几天就有好几百的点赞，🥳 非常开心，你们的点赞支持就是我继续好好写下去的动力，谢谢大家。🌸



上一篇文章主要讲解了如何通过 `WebRTC` 的一些 api 对 摄像头，话筒，屏幕的媒体流进行采集并加以`处理`，实现了几个小 demo，秉持着实战为王的原则，这一篇文章我们仍然是以这种方式为主,不过这一篇需要了解的概念会比上一篇多一些，但是我并不会为它大书特书，所以当有一些概念性的东西，你在看的时候觉得自己不是特别理解的话，聪明的你，一定要记得多借助搜索引擎来帮助自己 ~ ），我主要会讲一些在示例中必须要了解概念，以便让大家不会看到大量的协议或者名词概念直接劝退，目的是快速上手。



第一篇文章中，我并没有很好的介绍`WebRTC`，因为上一篇我们只是用它的一点获取媒体流等 api 来做了一些小 demo，但那都不是它的重点，`WebRTC` (Web Real-Time Communications)，是一个由 Google 发起的实时通讯解决方案，其中包含视频音频采集，编解码，数据传输，音视频展示等功能，通过它，我们可以非常方便且快速地构建出一个音视频通讯应用。

它允许网络应用或者站点，在不借助中间媒介的情况下，建立浏览器之间`点对点（Peer-to-Peer）的连接`，实现视频流和（或）音频流或者其他`任意数据`的传输。

所以它的重点在于通信~ 上一篇主要是讲的采集和处理媒体流相关的内容，这次我们主要讲解如何使用 `WebRTC`一些 api 配合 `信令服务` 来 `建立连接`，以及如何将获取到的媒体流进行`传输`。



## 一对一实时音视频通话实战（不通过信令服务）

首先，我们从最简单的 `1v1` 的`"手动"` `点对点连接`开始，这样可以更好帮助我们理解 `WebRTC`是怎么建立连接的，第二节中再来讲解如何通过`信令服务`来来帮助我们`"自动"` 建立 `点对点连接`实现 `1v1` 或者`多人`的音视频通话。

这样，我们就只使用了两篇文章就能让大家简单的了解到 如何从媒体流的`采集`，`处理`，建立 p2p（Peer-to-Peer） 对等`连接`，再到数据`传输`这一个整个过程的实现方法。🥳🥳🥳

[👉🏻 在线体验地址](https://frontend-park.vercel.app/audio-and-video/webRTC/p2p)



### 开始之前，一起来思考一个问题

两个设备，在互相不知道对方的情况下，如何建立连接？

我把这个问题换成更接地气一点：有一天，你在一个地方旅行，在旅行的途中，遇到了一个很喜欢的女孩子，一起拍了照片，假期结束后，你回家朝思暮想，想要再次见到她，但是你们忘记了互相留联系方式，我擦~好后悔，怎么办？后来你发的朋友圈被你同事评论了，说她认识这个女生，哇塞，你欣喜若狂，这个时候，你们是不是就可以通过你的同事来联系到对方呢？（有点土味的例子 😅， 相信大家应该已经能理解了~）



这样你们就可以相互建立连接了。你开心的起飞了，然后开始学起了 WebRTC。😅

### 实现简单模型，必须要知道的几个概念

#### 1. SDP

`SDP`：`Session Description Protocol`，它是一种用于描述多媒体会话的协议，它可以帮助我们描述媒体流的信息，比如媒体流的类型，编码格式，分辨率等等。WebRTC 通过`SDP`来交换端与端之间的`网络`和`媒体`信息。

下图中就是一个`SDP`信息的示例：从中你能大概的看到一些你的内网 IP 信息，外网 IP 信息，以及一些媒体流的信息。

```sh
v=0 # SDP版本号
o=- 0 0 IN IP4 120.24.99.xx # 会话标识信息
s=- # 会话名称
t=0 0 # 会话的有效时间
a=group:BUNDLE audio video # 媒体流类型
a=msid-semantic: WMS * # 媒体流标识符
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 126 # 音频媒体流
c=IN IP4 120.24.99.xx # 连接信息
a=rtcp:9 IN IP4 0.0.0.0 # RTCP 的 IP 地址
a=candidate:0 1 UDP 2122252543 120.24.99.xx 9 typ host # 候选 IP 地址
# ...等等等
```



一开始我们只需要知道，`SDP`是用来干什么的就行了，不需要过多的关注这些信息，后面用到了再深入了解。（我认为学习大多数技能的时候，如果上来就把各种概念性的东西先全部搞透再开始去动手，是效率很低的一种学习方式，应当了解一个大概后，在实战中去边做边学习，不懂什么概念再去获取，遇到问题再去解决，这样才能更快的上手一门技术，然后深入或者进阶的时候，在系统的去学习概念。）

#### 2. NAT

`NAT`：`Network Address Translation`，网络地址转换，它可以将私有 IP 地址转换为公共 IP 地址，从而实现私有网络与公共网络之间的通信。

因为 IPv4 的地址空间比较有限，所以我们大多数设备都部署在 `NAT` 网络内部。



（比较贴切的例子就是当你连接 wifi 的时候，我们就处于 `NAT` 网络内，或者网吧，公司的电脑大多都处在 `NAT` 网络内，它们共用一个路由器，然后在这些机子的上层，或上 n 层才会有一个有效的全球 IP 公网地址）





IPv6 正在逐步普及，等我们彻底用上了 IPv6，`NAT` 存在的意义就不大了。

总而言之，NAT 的存在就是因为 IPv4 的地址数量有限，我太多的设备因此只能部署在 NAT 网络内部，所以就引出了我们的内网 IP 地址是不可被外网访问的问题，

关于 NAT 的详细说明和原理可以看 [这篇](https://sspai.com/post/68037?ivk_sa=1024320u)，时间为王，我们暂时就不深入了解了，只需要把它理解为一个可以帮助我们实现内网与外网通信的工具就行。我常说的 `NAT 打洞`，`内网穿透`，就是利用 `NAT` 的这些特性和一些相关技术来实现的。

#### 3. ICE

`ICE`：`Interactive Connectivity Establishment`，交互式连接建立协议，用于在两个主机之间建立连接，它可以在两个主机之间建立连接，即使它们之间的防火墙阻止了直接连接。(可以不借助一个公网 server 完成端到端（Peer to peer，P2P）的通信)。

更多关于 WebRTC 相关协议的介绍可以看 [MDN 的详细介绍](https://developer.mozilla.org/zh-CN/docs/Web/API/WebRTC_API/Protocols)，本篇文章不做重点介绍，将会在后续文章中跟着`信令服务器`等相关内容一起进行详细的介绍。

### 建立连接的几个关键步骤

一般来说，一个 WebRTC 应用架构是这样的：



而这篇文章，我们主要为了入门`P2P`的连接，传输过程，所以我把上面的架构简化了一下，只保留了一些必要的部分：

其中`蓝色`部分是`用户和信令服务器`的交互，`红色`部分是`用户和用户`之间的交互。

而蓝色的部分我也进行了简化，我们将通过人工拷贝黏贴的形式，来模拟信令服务器的帮助用户传递关键信息的过程。



这样，我们通过一个`最简单的模型`就能实现并理解 `WebRTC` 从建立连接到传输数据的整个过程了。🥳🥳🥳

下面我把 WebRTC 建立连接的几个关键步骤总结了一下，方便大家理解。主要为：

- 创建一个用于连接控制的 `RTCPeerConnection` 对象
- 采集媒体流并添加到`RTCPeerConnection` 实例中
- 建立连接，传输媒体流。

ok，我们来逐一分解：

#### 1. 创建本地的 `RTCPeerConnection` 对象

在 WebRTC 中 ，由`RTCPeerConnection` api 负责创建，保持，监控，关闭连接。我认为它是 WebRTC 的核心 api。

为了让 WebRTC 的相关 api 在各个浏览器中都能够正常的运行，强烈推荐使用 [adapter.js](https://www.npmjs.com/package/webrtc-adapter),adapter.js 是一个垫片，用于将应用程序与 WebRTC 中的规范更改和前缀差异隔离开来。如今，前缀差异大多消失了，但浏览器之间的行为差异仍然存在。 而且，WebRTC 仍在快速发展，因此 adapter.js 是非常有用的。

```sh
# 安装它
npm install webrtc-adapter
```

```typescript
// 你只需要引入它即可，不需要做任何配置和多余的操作。
import 'webrtc-adapter'
```

完事之后，首先我们需要创建一个 `RTCPeerConnection`对象，用于后面实现本地计算机到远端的 WebRTC 连接。

如果你的应用需要在公网中使用，那么你需要在创建 `RTCPeerConnection` 对象的时候，传入一个配置对象，配置对象中包含了 STUN 和 TURN 服务器的地址。

这两个服务器的作用是什么呢？我们先来看看 STUN 服务器。  
`STUN`：`Session Traversal Utilities for NAT`，用来帮助我们获取本地计算机的公网 IP 地址，以及端口号。  
`TURN`：`Traversal Using Relays around NAT`，用来帮助我们穿越 NAT 网关，实现公网中的 WebRTC 连接。一般来说它用来做兜底的，当所有方法都无法穿越 NAT 网关或者无法直接简历 P2P 连接的时候，我们才会使用到它。这时，TURN 服务器会作为一个中继服务器，然后用户的媒体流会通过它来中转传输。

```typescript
// 公网中使用
const pc = new RTCPeerConnection({
  iceServers: [
    // 目前我在用的，免费STUN 服务器
    {
      urls: 'stun:stun.voipbuster.com ',
    },
    // 谷歌的好像都失效了，不过你们可以试试
    {
      urls: 'stun:stun.l.google.com:19301',
      // urls: 'stun:stun.l.google.com:19302',
      // urls: 'stun:stun.l.google.com:19303',
      // ...
    },
    // TURN 服务器,这个对服务器压力太大了，目前没找到免费的，后续我在自己的服务器上弄一个
    {
      urls: 'turn:turn.xxxx.org',
      username: 'webrtc',
      credential: 'turnserver',
    },
    {
      urls: 'turn:turn.ap-southeast-1.aliyuncs.com:443?transport=tcp',
      username: 'test',
      credential: 'test',
    },
  ],
})
```

搭建 STUN 服务器和 TURN 服务器的教程，我会在下一篇文章中和信令服务一起分享。

这篇文章中，我们暂时只实现一个能在内网使用的音视频通话，所以我们暂时不需要配置 STUN，TURN 服务器。如果你想在公网中使用，那么你完全可以暂时使用上面的免费 STUN 服务器，但是 TURN 服务器就需要你自己搭建了。

我的这个 👉🏻[DEMO](https://frontend-park.vercel.app/audio-and-video/webRTC/signaling-p2p)



就是用的上面的 免费 STUN 服务器 + 信令服务来实现 内外网的 P2P 连接。可以提前体验一下，这会在第三篇文章中详细介绍。

ok,不扯远了，我们继续，先创建一个 `RTCPeerConnection` 对象。

```typescript
// 内网中使用
const pc = new RTCPeerConnection()
```

然后我们创建好 `RTCPeerConnection` 对象之后，通过 `RTCPeerConnection` 对象的 `addTrack` 方法来媒体流添加到 `RTCPeerConnection` 对象中。

#### 2. 采集媒体流

采集和处理的方法在第一篇文章中已经做了很详细的介绍，下面我们直接使用。

设置好媒体流需要挂载的音视频元素。

```html
<!-- 给自己本地的视频播放设置静音，防止产生回音 -->
<video id="local" autoplay playsinline muted></video>
<video id="remote" autoplay playsinline></video>
```

然后通过 `navigator.mediaDevices.getUserMedia` 方法来获取媒体流。

一般这里的逻辑都是在我们项目加载完成的时候，或者让用户自己手动点击按钮来采集媒体流。

采集完后，我们就可以通过 `RTCPeerConnection` 对象的 `addTrack` 方法来添加媒体流。

将媒体流添加到 `RTCPeerConnection` 对象的方法还有一个，就是 `addStream` 方法，但是这个方法已经被废弃了，虽然现在很多教程仍然是使用的它，这里我们不推荐使用。

添加完后我们也需要监听远程的媒体流是否也添加进来，当远程的媒体流也在这样我们才能够将远程的媒体流播放出来。 这里 WebRTC 为我们提供了一个 `ontrack` 事件，当远程的媒体流添加进来的时候，就会触发这个事件。

第二部的初始化完整代码如下：

```typescript
// 初始化
async function init(params: type) {
  // 获取本地端视频标签
  const localVideo = document.getElementById('local') as HTMLVideoElement
  // 获取远程端视频标签
  const remoteVideo = document.getElementById('remote') as HTMLVideoElement

  // 采集本地媒体流
  const localStream = await navigator.mediaDevices.getUserMedia({
    video: true,
    audio: true,
  })
  // 设置本地视频流
  localVideo.srcObject = localStream

  // 不推荐使用：已经过时的方法 [addStream API](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addStream)
  // pc.addStream(localStream);

  // 添加本地媒体流的轨道都添加到 RTCPeerConnection 中
  localStream.getTracks().forEach((track) => {
    pc.addTrack(track, localStream)
  })

  // 监听远程流，方法一：
  pc.ontrack = (event) => {
    remoteVideo.srcObject = event.streams[0]
  }

  // 方法二：你也可以使用 addStream API，来更加详细的控制流的添加
  // const remoteStream: MediaStream = new MediaStream()
  // pc.ontrack = (event) => {
  //   event.streams[0].getTracks().forEach((track) => {
  //     remoteStream.addTrack(track)
  //   })
  //   // 设置远程视频流
  //   remoteVideo.srcObject = remoteStream
  // }
}
```

ok，下一步就到了我们的第 3 步，也就是建立连接

#### 3. 建立连接

建立连接的主要过程，就是通过 `RTCPeerConnection` 对象的 `createOffer` 方法来创建本地的 `SDP` 描述，然后通过 `RTCPeerConnection` 对象的 `setLocalDescription` 方法来设置本地的 `SDP` 描述，最后通过 `RTCPeerConnection` 对象的 `setRemoteDescription` 方法来设置远程的 `SDP` 描述。

- 创建 offer（提案）

```typescript
// 创建本地/远程 SDP 描述, 用于描述本地/远程的媒体流
let offerSdp = ''
let answerSdp = ''

const createOffer = async () => {
  // 创建 offer
  const offer = await pc.createOffer()
  // 设置本地描述
  await pc.setLocalDescription(offer)
  // await pc.setLocalDescription()
  // 到这里，我们本地的 offer 就创建好了，一般在这里通过信令服务器发送 offerSdp 给远端

  // 监听 RTCPeerConnection 的 onicecandidate 事件，当 ICE 服务器返回一个新的候选地址时，就会触发该事件
  pc.onicecandidate = async (event) => {
    if (event.candidate) {
      offerSdp.value = JSON.stringify(pc.localDescription)
    }
  }
}
```

其中的 `onicecandidate` 事件，是用来监听 ICE 服务器返回的候选地址，当 ICE 服务器返回一个新的候选地址时，就会触发该事件，这里我们需要将这个候选地址发送给远端，这样远端才能够和我们建立连接。

当然，你也可以在`peerConnection.setLocalDescription()`时，不传参数，这样 WebRTC 会默认调用`peerConnection.createOffer()`来创建 offer。

```typescript
const offer = await peerConnection.createOffer()
await peerConnection.setLocalDescription(offer)
console.log(peerConnection)
```

```typescript
// 不传参数，默认调用 peerConnection.createOffer()
await peerConnection.setLocalDescription()
console.log(peerConnection)
```

我们可以把它打印一下，可以看到一样是能够创建出 offer 并设置到本地描述中的。







但是不推荐这么做，WebRTC 还在不断的更新中，本身就存在很多实验性的 API，所以我们还是要遵循规范来。该传就传。😅

ok，下一步就到了我们的第 3 步，也就是创建 offer 并发送给远程端。并监听远程端的 answer。拿到 answer 后，我们就可以设置到远程端的描述中。

- 创建 answer

作为接收方，在拿到 offer 后，我们就可以创建 answer 并设置到本地描述中，然后通过信令服务器发送 answer 给对端。

```typescript
const createAnswer = async () => {
  // 解析字符串
  const offer = JSON.parse(offerSdp)
  pc.onicecandidate = async (event) => {
    // Event that fires off when a new answer ICE candidate is created
    if (event.candidate) {
      answerSdp = JSON.stringify(pc.localDescription)
    }
  }
  await pc.setRemoteDescription(offer)
  const answer = await pc.createAnswer()
  await pc.setLocalDescription(answer)
}
```

- 添加 answer

接收方拿到 answer 后，就可以设置到远程端的描述中。

```typescript
// 添加 answer(应答)
const addAnswer = async () => {
  const answer = JSON.parse(answerSdp)
  if (!pc.currentRemoteDescription) {
    pc.setRemoteDescription(answer)
  }
}
```

一个最简单的 WebRTC 通信流程就完成了。

[👉🏻 本文示例在线体验地址](https://frontend-park.vercel.app/audio-and-video/webRTC/p2p)

你可以开两个浏览器 tab 或者用不同的设备，根据说明即可体验音视频通话的实现。



是不是感觉形似 TCP 的三次握手？ 这样，我们就完成了从采集媒体流，建立连接，传输的全过程。只是这里的建立连接，需要我们手动将 offer 和 answer 传输给对端，如果我们想要实现自动建立连接，那么就需要使用到信令服务器了。这将会在下一篇文章中详细介绍。下一篇文章，我们会从实战出发，讲解 WebRTC 的音视频通话中如何通过信令服务器进行通信，自动建立连接，以及如何搭建一个 STUN，TURN 服务器等。

## 最后

[👉🏻 本文示例源代码地址](https://github.com/wangrongding/frontend-park)  
[👉🏻 本文示例在线体验地址](https://frontend-park.vercel.app/audio-and-video/webRTC/p2p)

你也可以提前体验一下[👉🏻 下一篇文章的示例](https://frontend-park.vercel.app/audio-and-video/webRTC/signaling-p2p) ,通过信令服务器自动建立连接的 1v1 音视频通话，支持内外网。



这篇文章的概念比较多，有很多地方我并没有花大量的篇幅去讲解它是什么以及原理是什么样的，比如 SDP，ICE 等等，为什么呢？我认为作为一个入门篇讲这些概念，上来就灌输大量的这类概念，会让文章非常乏味，就像是直接搬文档，因为聪明的读者肯定也会在看这篇文章遇到不理解的概念性的东西，或者看完对感兴趣的地方进行查阅。不过大家如果有遇到哪一块非常不理解又没找到什么好的方案的时候，可以在评论区留言，我也会尽快回复。🫶

本文是 WebRTC 系列的第二篇，后续还会有很多篇，如果你觉得本文对你有帮助，你的点赞支持就是我的动力 🌸 ~

---

## File: posts/01-frontend/WebRTC/03-signaling-p2p.md

[Binary file]

---

## File: posts/01-frontend/WebRTC/04-pose-detection.md

[Binary file]

---

## File: posts/01-frontend/WebRTC/05-file-transfer.md

[Binary file]

---

## File: posts/01-frontend/前端工程化/webpack 基础知识整理.md

[Binary file]

---

## File: posts/01-frontend/前端工程化/webpack多版本控制方案.md

---
title: webpack多版本控制方案
date: 2019-11-03
tags:
  - webpack
  - node
categories:
  - 前端
---

:::tip 项目中有这么一个需求，就是按需启动 mock 功能。考虑到 mock 只是在特定情况下，所以考虑通过 `cross-env` 来处理。 :::



## cross-env 修改生产环境变量

1. 我想要的最终效果是 npm run dev:mock 来启动 mock，所以先安装 cross-env

```bash
npm i --save cross-env
```

2. 找到 `/config/dev.evn.js` ，添加 MOCK 变量

```javascript
module.exports = {
  MOCK: JSON.stringify(process.env.MOCK) || 'false',
}
```

3. 在 package.json 中配置变量

```json
{
  "scripts": {
    "dev:mock": "cross-env MOCK=true webpack --config build/webpack.config.js"
  }
}
```

4. 使用变量

```javascript
if (process.env.MOCK === 'true') {
  fetchMock.post('https://xxx/user/login', loginAPI.login)
}
```

5. 启动 MOCK

```bash
npm run dev:mock
```

---

## File: posts/01-frontend/前端工程化/webpack配置多页面应用的一次尝试.md

---
title: webpack配置多页面应用的一次尝试
date: 2019-06-15
tags:
  - webpack
categories:
  - 前端
---

::: tip

1. 最近有一个项目，考虑到要进行 SEO，所以要做成多页面应用。为了保证开发速度和开发效率，所以决定使用 webpack 做一套模块化配置方案。

2. 下面主要针对一些重要的点提供思路，并不作详解。完整的代码，我会放在 github（[项目地址](https://github.com/recoluan/webpack-multipage-demo)）上供大家参考，如果有优化的地方，请在评论区指点出来。 :::



## 目录

```
|-- build                           webpack 配置
|   |-- utils.js                    处理 webpack 配置的公共方法
|   |-- webpack.base.conf.js        公共配置
|   |-- webpack.dev.conf.js         开发环境配置
|   |-- webapck.prod.conf.js        生产环境配置
|   |-- webpack.rules.conf.js       文件处理规则
|-- dist                            存放变异后文件
|-- |
|-- src                             源文件
|   |-- assets
|   |-- pages
|   |   |-- index                   首页
|   |   |   |-- index.html          首页模板
|   |   |   |-- index.js            首页入口文件
|   htmlarrary.js                   页面配置文件
```

## 多页面

多页面，首先最重要的就是处理多个 html 模板和对应的多个入口文件。

### html 模板

在项目根目录创建一个 `htmlarrary.js`，用来存储页面配置：

```js
// htmlarrary.js

module.exports = [
  {
    _html: 'index',
    title: '首页',
    chunks: ['index', 'manifest', 'vendors'], // 页面用到的vendor模块
  },
  {
    _html: 'login',
    title: '登录',
    chunks: ['login'],
  },
]
```

然后在 `/build/utils.js` 创建 `getHtmlArray` 方法，用来自动生成多个模板的配置：

```js
// /build/utils.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
const htmlArray = require('../htmlarray.js')

exports.getHtmlArray = function (moduleExportsPlugins) {
  // 根据模板配置生成 HtmlWebpackPlugin 需要的配置
  const getHtmlConfig = function (name, chunks, title) {
    return {
      template: `./src/pages/${name}/index.html`,
      filename: `./${name}.html`,
      favicon: './src/assets/images/public/favicon.ico',
      title,
      inject: true,
      hash: true, // 开启hash
      chunks, // 页面要引入的包
      minify:
        process.env.NODE_ENV === 'development'
          ? false
          : {
              removeComments: true, // 移除HTML中的注释
              collapseWhitespace: true, // 折叠空白区域 也就是压缩代码
              removeAttributeQuotes: true, // 去除属性引用
            },
    }
  }

  // 循环创建模板配置
  htmlArray.forEach((element) => {
    const { _html, chunks, title } = element
    moduleExportsPlugins.push(new HtmlWebpackPlugin(getHtmlConfig(_html, chunks, title)))
  })
}
```

在 `webpack.base.conf.js` 中通过 `getHtmlArray` 添加多页面引擎配置：

```js
const { getHtmlArray } = require('./utils.js')

module.exports = {
  // ... 相关配置
}

getHtmlArray(module.exports.plugins)
```

### 入口文件

在 `/build/utils.js` 创建 `getEntry` 方法，用来自动生成入口文件的配置：

```js
// /build/utils.js
const glob = require('glob')

exports.getEntry = function () {
  const entry = {}
  // 读取src目录所有page入口
  glob.sync('./src/pages/*/*.js').forEach((name) => {
    const start = name.indexOf('src/') + 4
    const end = name.length - 3
    const eArr = []
    const n = name.slice(start, end).split('/')[1]
    eArr.push(name)
    eArr.push('@babel/polyfill') // 引入这个，是为了用async await，一些IE不支持的属性能够受支持，兼容IE浏览器用的
    entry[n] = eArr
  })
  return entry
}
```

在 `webpack.base.conf.js` 中通过 `getEntry` 添加多入口配置：

```js
// webpack.base.conf.js

const { getEntry } = require('./utils.js')

module.exports = {
  entry: getEntry(),
}
```

## JS

JS 方面，我们一般有以下需求：

1. eslint 错误提醒；
2. ts-loader 解析 typescript 语法；
3. babel-loader 解析 ES6 语法。

针对以上需求，我们来配置一下子 rules，并且做一下延伸：

```js
// webpack.rules.conf.js

module.exports = [
  {
    test: /\.(js|ts)$/,
    exclude: /node_modules/,
    use: [
      {
        loader: 'babel-loader',
        options: {
          presets: [
            [
              '@babel/preset-env',
              {
                useBuiltIns: 'usage',
                targets: {
                  chrome: '58',
                  ie: '8',
                },
                corejs: 2,
              },
            ],
          ],
        },
      },
      {
        loader: 'ts-loader',
      },
      {
        loader: 'eslint-loader',
        options: {
          cache: true, // 优化打包速度
        },
      },
    ],
  },
]
```

在生产环境，我们需要对 js 文件进行压缩，公共代码抽离，所以还需要在 `webpack.prod.conf.js` 中这样去优化一下：

```js
// webpack.prod.conf.js

cconst merge = require('webpack-merge')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const baseConfig = require('./webpack.base.conf.js')

const prodConfig = {
  optimization: {
    minimizer: [
      // 会导致 sourcemap 消失
      new UglifyJsPlugin({
        uglifyOptions: ({
          compress: false
        })
      }),
      new OptimizeCSSAssetsPlugin({})
    ],
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendors: { // 抽离第三方插件
          test: /[\\/]node_modules[\\/]/, // 指定是node_modules下的第三方包
          name: 'vendors',
          priority: -10 // 抽取优先级
        },
        utilCommon: { // 抽离自定义
          name: 'common',
          minSize: 0, // 将引用模块分离成新代码文件的最小体积
          minChunks: 2, // 表示将引用模块如不同文件引用了多少次，才能分离生成新chunk
          priority: -20
        }
      }
    },
    // optimization.runtimeChunk 就是告诉 webpack 是否要把这部分单独打包出来，来优化缓存问题
    runtimeChunk: {
      name: 'manifest'
    }
  }
}

module.exports = merge(baseConfig, prodConfig)
```

## CSS

CSS 方面，我们一般有以下需求：

1. postcss-loader 安装 autoprefixer 插件，自动进行兼容性处理；
2. sass-loader 解析 sass 语法；
3. MiniCssExtractPlugin 进行 css 压缩。

针对以上需求，我们来配置一下子 rules，并且做一下延伸：

```js
// webpack.rules.conf.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = [
  {
    test: /\.scss$/i,
    use: [
      Object.assign(
        // 生产环境压缩 css 需要使用 MiniCssExtractPlugin.loader 代替 style-loader
        {
          loader: process.env.NODE_ENV === 'production' ? MiniCssExtractPlugin.loader : 'style-loader',
        },
        // 解决编译后 css 图片不能正常显示的问题
        process.env.NODE_ENV === 'production' ? { options: { publicPath: '../' } } : {},
      ),
      'css-loader',
      'sass-loader',
      'postcss-loader',
    ],
  },
]
```

在生产环境，我们需要对 css 文件进行压缩，所以还需要在 `webpack.prod.conf.js` 中这样去优化一下：

```js
// webpack.prod.conf.js

cconst merge = require('webpack-merge')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const baseConfig = require('./webpack.base.conf.js')

const prodConfig = {
  optimization: {
    minimizer: [
      new OptimizeCSSAssetsPlugin({})
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:8].css',
      chunkFileName: '[id].[contenthash:8].css'
    }),
  ]
}

module.exports = merge(baseConfig, prodConfig)
```

## images

images 方面，我们一般有以下需求：

1. css 和 js 中的图片可以被解析；
2. html 中 img 标签的图片可以被解析。

针对以上需求，我们来配置一下子 rules，并且做一下延伸：

```js
// webpack.rules.conf.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = [
  {
    test: /\.html$/,
    use: [
      // 如果 img 标签的 src 为空的话，就报错 xxxHTMLLINKxxx0.
      {
        loader: 'html-loader',
      },
    ],
  },
  {
    test: /\.(png|jpg|gif|ico)$/,
    use: [
      {
        loader: 'url-loader',
        options: {
          name: '[name].[hash:8].[ext]',
          limit: 30000,
          outputPath: './images',
        },
      },
    ],
  },
]
```

## 其他

### devserver 和 热更新

```js
// webpack.dev.conf.js

const devConfig = {
  devServer: {
    open: true,
    host: '0.0.0.0',
    port: 2000,
    useLocalIp: true,
    hot: true,
  },
  plugins: [new webpack.HotModuleReplacementPlugin()],
}
```

> 这样智能启动 css 热更新，如果需要 js 热更新，需要添加一段代码，请自行查找 [官网文档](https://webpack.docschina.org/)。

### 报错

1. 如果 img 标签的 src 为空的话，就报错 xxxHTMLLINKxxx0.
2. 如果报错：`TS2688: Cannot find type definition file for 'unist'.` 说明需要安装依赖 `@types/unist`，其他类似报错一样，这是 `typescript@2.0` 更换 types 支持方式导致的报错。
3. 编译后 css 图片路径错误，根据是否是生产环境来动态添加 `publicPath`，[点击这里](#CSS)。

---

## File: posts/01-frontend/前端工程化/前端团队基于VUE开发的格式化统一方案.md

---
title: 前端团队基于VUE开发的格式化统一方案
date: 2020-01-11
tags:
  - 前端工程化
categories:
  - 前端
---

### 1. 安装 Vetur 插件



### 2. 在 vscode 中摁住 ctrl+shift+p 搜索 settings.json 并打开



### 3.在 settings.json 文件中的最后面加上以下代码以完成格式化配置

```javascript
//==================vetur格式化配置=====================
    "vetur.format.defaultFormatter.stylus": "stylus-supremacy",
    "vetur.format.defaultFormatter.css": "prettier",
    "vetur.format.defaultFormatter.scss": "prettier",
    "vetur.format.defaultFormatter.less": "prettier",
    "vetur.format.defaultFormatter.html": "js-beautify-html",
    // "vetur.format.defaultFormatter.html": "prettyhtml",
    "vetur.format.defaultFormatter.js": "prettier",
    // "vetur.format.defaultFormatter.js": "vscode-typescript",//希望js不换行显示
    "vetur.format.defaultFormatterOptions": {
        "prettier": {
            "semi": true, // 句尾是否加;
            "singleQuote": true, // 使用单引号而不是双引号
        },
        "js-beautify-html": {
            "wrap_line_length": 300, //多少字符换行
            "singleQuote": true,
            "wrap_attributes": "force-expand-multiline",
            // "wrap_attributes": "force-aligned"// 属性强制折行对齐
        },
        "prettyhtml": {
            "printWidth": 100,
            "singleQuote": true,
            "wrapAttributes": false,
            "sortAttributes": false
        }
    },
    // "vetur.format.options.useTabs": true,
    "vetur.format.options.tabSize": 4, //缩进宽度
    "editor.formatOnSave": true, //保存时自动格式化
    "editor.formatOnPaste": true, // 在粘贴时自动格式化
    //==================vetur格式化配置End===================
```

### 4.在 vue 文件中右击选择文档格式设置方式

---

## File: posts/01-frontend/前端工程化/前端工程化之谁动了我的代码.md

---
title: 谁动了我的代码!
date: 2022-02-13
tags:
  - JavaScript
categories:
  - 前端
---



当你某一天 fetch 代码时，发现很多文件是这样的 👆 你是不是很崩溃？



## 前言

公司一个老项目，没有做代码提交前的规范校验，我拿到后，想着老项目嘛也没时间帮它弄这些，反正就是改一点点小东西；尽量跟着它的代码风格写，写完提交就行；

直到某一天，又有一个人加入了进来。好家伙，直接干出事了。  
很多个文件一起提交的，然后 commit-msg 不规范，代码缩进也不规范，换行符也不一致，里面还有很多没用的代码，可读性极差；

但问题终归是得去解决的；治理污水的最好方法就是从能污水产生的地方开始整治。我来帮它加上吧。🥰

下面我将带大家一起，先通过`husky`+`eslint`完成一个最简单的代码规范校验，然后逐步优化，最后通过`husky`+`eslint`+`lint-stage`+`commitlint`+`pritter`实现一个强有力的限制。最后再配合`commitizen`,通过`commitlint-config-cz`+`cz-customizable`实现自定义的提交模板和限制规则来实现团队最终的`项目提交限制规约`。

## eslit+pritter

这里我为该项目适配了一套 eslit 规则，按照这套规则提交的代码不会有冲突。  
eslint 和 pritter 大家平时项目应该都用到过，应该都很熟悉了，这里将不占用篇幅。(想看的可以私聊我，我可以单独出一篇 eslint 和 pritter 主题的文章)

🦄 在这篇文章中，主要讲解如何在团队协同工作时，在 git 提交代码更改前，对不规范的代码和提交信息进行校验，修复，并限制不规范的提交。

## husky

首先要介绍的是`husky`，搞工程化，我们肯定都少不了 `husky`，它能很方便的帮我们阻挡小可爱们的进攻，不，是为我们的项目添加`git hooks`

### 具体方法

首先我们将 `husky` 安装到开发依赖中

```sh
npm i husky -D
# or
yarn add husky -D
```

> ### 注意
>
> 目前我所安装的版本是`husky@7.0.4`,由于`husky@6.0.0` 后做了`breaking change`，所以在`6.0.0`版本之前的那种设置钩子的方法已经不适用了,**这里我们只介绍最新的方式**。

安装完后，我们需要在当前项目中创建一个`.husky`目录并指定该目录为 `git hooks` 所在的目录。

使用以下命令快速创建 👇

```sh
#--no-install 参数表示强制npx使用项目中node_modules目录下的husky依赖包
npx --no-install husky install
```

为了让其他人在此项目中安装依赖后也能自动创建`.husky`目录并指定该目录为 `git hooks` 所在的目录，我们需要在 `package.json` 里面添加一条脚本`"prepare": "husky install"`

使用以下命令快速添加 👇

```sh
npm set-script prepare "husky install"
```

> `prepare` 脚本会在 `npm i`或者其他`yarn or yarn add` 之后自动执行。也就是说当我们安装依赖后会自动执行 `husky install` 命令，从而自动创建`.husky`目录并指定该目录为 `git hooks` 所在的目录。

使用以下命令快速创建 👇

```sh
npx --no-instal husky add .husky/pre-commit "npm run [你要执行的命令]"
```

完成后可以看到`.husky`目录下新增了一个`pre-commit`文件,其中的内容为 👇 

这里我用的是`npm run lint`，这样我们就可以配合 Eslint 的代码校验,来限制不规范代码的提交了

 可以看到，不符合 Eslint 校验规则的代码是没法提交的；

当然，这里的报错问题只是由于缩进不规范引起的，类似这种的问题还有引号，句尾分号，换行符等等...都可以通过 `eslint` 的参数 `--fix`来自动修复，这样就可以在提交前，先将能实现自动修复的简单代码风格问题后 commit。复杂的情况还是要自己去手动处理的。

> 说到换行符，这里我们需要了解的是：在 Windows 上默认的是回车换行（Carriage Return Line Feed, CRLF），然而，在 Linux/MacOS 上则是换行（Line Feed, LF）。

我们可以试一下将原先换行符为`crlf`的文件格式化为换行符为`lf`后，执行`git add .`的情况。



可以看到最终 LF 换行符还是被 CRLF 转化了；

如果你们不会跨平台协作（都在 Mac/Linux，或者都在 Windows 上协同），只需要在当前项目中通过`git config core.autocrlf false`来阻止这种情况的发生。

为了保险起见，你需要新建一个`.gitattributes`文件（主要用于定义每种文件的属性，以方便 git 帮我们统一管理。），设置 eol(end of line)为指定的换行符（lf/crlf），这里我把所有文件`*.*`的换行符都设置为了 LF，并且将一些非文本文件进行了标记（排除它们），你也可以对每一种文件类型分别设置对应的属性 => `*.js eol=lf`，`*.ts eol=lf`...

```sh
*.* eol=lf
*.jpg -text
*.png -text
  ...
# 或者👇

*.js eol=lf
*.ts eol=lf
*.json eol=lf
  ...
```

文件内容如下 👇



这样，我们不管在什么平台上开发，文件换行符都统一为 LF。

可以看到使用`.gitattributes`配置文件后执行`git add`,所有不是指定换行符的文件都会被自动更换为你指定的换行符，例如我这里指定了`lf`,那么 `git add .` 后，不是以 `lf` 换行的文件都会被转换为 `lf` ，并在终端输出`warning: CRLF will be replaced by LF in xxxx/filename`，如图 👇



`.gitattributes`有很多用处，具体可以查看 👉[gitattributes](https://git-scm.com/docs/gitattributes)，（但是在这里不是最佳实践！）

🥰 到这里，一个最简单的代码风格限制方法就已经实现了。

既然做了，就肯定要做一套完整的，且好用的。下面我们来继续完善其他功能 ~

---

## lint-staged

什么是`lint-staged`?顾名思义，借助这个工具只是用来检查 git 暂存区文件的，就是在你`git add file1，2，3...` 后的暂存区文件中运行 `lint` 的一个工具。

每次提交一两个文件，却都要 `lint` 所有文件话，是很没有必要的，我们只对需要提交的代码进行 `lint`,这样可以减少很多没必要的时间开销。（如果你每次修改一个文件，都要去 `lint` 所有文件，这个工具对你来说就没有什么意义了，`husky` 就管够 🤐）

### 安装

```sh
yarn add lint-staged -D
```

### 使用方法

我们将`.husky/pre-commit`中之前写代码改为 👇

```diff
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
- npm run lint
+ npx --no-install lint-staged
```

然后在 package.json 中添加以下代码,`lint-staged`对象中采用键值对的方式进行配置，键名是你想处理的单个文件或一个文件类型，多个类型可以写在`{}`中，用逗号分隔；键值是一个数组，数组中为 lint 时需要执行的命令组。

```json
{
  "lint-staged": {
    "*.{ts,js,vue}": ["eslint", "echo '没问题！'"]
  }
}
```

添加玩上述代码后，我们通过测试，将两个文件的缩进改为不符合规范的情况，然后将其中一个文件暂存后，我们运行`git commit`后发现终端的报错中，只有一个文件的 `lint` 报错信息,另一个文件的报错并没有出现。



当所有暂存区代码都符合规范时 👇，才会通过校验执行提交。

```sh
✔ Preparing lint-staged...
✔ Hiding unstaged changes to partially staged files...
✔ Running tasks for staged files...
✔ Restoring unstaged changes to partially staged files...
✔ Cleaning up temporary files...
```

## commitizen

Commitizen 是一个撰写符合上面 Commit Message 标准的一款工具。通过它可以实现交互式撰写规范的 Commit Message。



如果只在本仓库使用 👇

```sh
npm install commitizen -D
# or
yarn add commitizen -D
```

如果你想全局都用 commitizen 来帮你做 commit

```sh
npm install commitizen -g
# or
yarn global add commitizen
```

安装完成后，一般我们都采用符合 Angular 的 Commit message 格式的提交规范（当然也可以自定义，后面会讲到~），运行以下命令生成符合 Angular 提交规范格式的 Commit message。

如果你项目用的是 npm 👇

```sh
# 如果你项目用的是npm
npx --no-install commitizen init cz-conventional-changelog --save-dev --save-exact
```

如果你项目用的是 yarn 👇

```sh
# 如果你项目用的是yarn
npx --no-install commitizen init cz-conventional-changelog --save-dev --save-exact
```

运行了上述命令后，它将为你项目安装 cz-conventional-changelog 适配器模块，把 config.commitizen 的密钥添加到文件的根目录添加到 `package.json` 中

可以在`package.json` 中看到，自动的新增了以下内容 👇

```json
{
  ...
  "config": {
    "commitizen": {
      "path": "./node_modules/cz-conventional-changelog"
    }
  }
  ...
}
```

如果不是全局安装的`commitizen`,则需要在 package.json 的 scripts 中添加`cz:"cz"`命令, 使用以下命令快速添加 👇

```sh
npm set-script cz "cz"
```

完成后，通过命令`yarn cz`，你如果是全局安装的 `commitizen`，那你直接 `git cz`，都可以通过以下交互式的撰写 commit messag 然后提交



## 限制 commitlint

由于 commitizen 并不是强制使用的，仍然可以通过`git commit`来提交，所以我们必须在不管是通过`cz`还是`git commit`提交前，都要对 commit messag 进行一次校验，不符合规范的情况下是不允许进行 commit 的

首先我们需要安装`commitlint`,`commitlint/config-conventional`

```sh
yarn add @commitlint/cli @commitlint/config-conventional -D
```

使用以下命令快速创建 git hooks 的 commit-msg 钩子 👇 这样每次 commit 的时候都会由 commitlint 对 commit message 进行一次检验

```sh
npx --no-instal husky add .husky/commit-msg 'npx --no-install commitlint --edit "$1"'
```

然后我们创建一个 commitlint 配置文件到项目根目录 👇

```sh
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js
```

以上将会在项目目录中生成 `commitlint.config.js`，代码如下，他将继承`@commitlint/config-conventional`中的 Commit message 规范。（"feat", "fix", "perf", "refactor"...）

```javascript
module.exports = {
  extends: ['@commitlint/config-conventional'],
}
```

然后我们在终端进行测试

```sh
echo 'feat: bar' | npx --no-install commitlint
```

如果你执行上面这个行命令后出现了以上这种报错。



将文件更改问 UTF-8 的格式即可解决；这个问题目前已经在 👉 [Issues](https://github.com/conventional-changelog/commitlint/issues/270)中，有不少人遇到了（我也是 😂）。  
最简单的方法就是，把文件用记事本打开，选择另存为，然后在弹窗的右下角更改字符编码为 UTF-8 （Windows 用户运行`echo "xxx" > xx.js`时，文件编码可能为 UTF-16 LE），更改好后，把原来的`commitlint.config.js`文件替换掉即可。

解决以上问题后，我们再测试一下，可以看到，不符合规范的 commit-msg 是会导致报错的，也就 commit 不了了，说明我们的 commitlint 已经生效了~ 👏👏👏



到此，commit-msg 的校验也已经完成 ✔

如果，你想自定义 commitlint 的交互文本（不用 feat，fix...，很多人都喜欢在 commit message 前面加一个 emoji 表情符号），当然也可以。

我们需要安装 `cz-customizable`来实现自定义 commit message 规则，以及安装对应的`commitlint-config-cz`来配套校验（直接从自定义的文件里读取规则）

运行以下命令 👇

```sh
yarn add commitlint-config-cz  cz-customizable -D
```

在项目根目录，创建一个`.cz-config.js`文件，并复制[cz-config-EXAMPLE.js ](https://github.com/leoforfree/cz-customizable/blob/master/cz-config-EXAMPLE.js)中的内容到其中。然后改成你自己想要的规则即可。

当然，你也可以用我写好的：

```javascript
module.exports = {
  types: [
    { value: 'feat', name: 'feat: 一个新的特性' },
    { value: 'fix', name: 'fix: 修复一个Bug' },
    { value: 'docs', name: 'docs: 变更的只有文档' },
    { value: 'style', name: 'style: 代码风格,格式修复' },
    { value: 'refactor', name: 'refactor: 代码重构，注意和feat、fix区分开' },
    { value: 'perf', name: 'perf: 码优化,改善性能' },
    { value: 'test', name: 'test: 测试' },
    { alue: 'chore', name: 'chore: 变更构建流程或辅助工具' },
    { value: 'revert', name: 'revert: 代码回退' },
    { value: 'init', name: 'init: 项目初始化' },
    { value: 'build', name: 'build: 变更项目构建或外部依赖' },
    { value: 'WIP', name: 'WIP: 进行中的工作' },
  ],
  scopes: [],
  allowTicketNumber: false,
  isTicketNumberRequired: false,
  ticketNumberPrefix: 'TICKET-',
  ticketNumberRegExp: '\\d{1,5}',
  // it needs to match the value for field type. Eg.: 'fix'
  /*
  scopeOverrides: {
    fix: [
      {name: 'merge'},
      {name: 'style'},
      {name: 'e2eTest'},
      {name: 'unitTest'}
    ]
  },
  */
  // override the messages, defaults are as follows
  messages: {
    type: '选择一种你的提交类型:',
    scope: '选择一个scope (可选):',
    // used if allowCustomScopes is true
    customScope: 'Denote the SCOPE of this change:',
    subject: '简短说明(最多40个字):',
    body: '长说明，使用"|"换行(可选)：\n',
    breaking: '非兼容性说明 (可选):\n',
    footer: '关联关闭的issue，例如：#12, #34(可选):\n',
    confirmCommit: '确定提交?',
  },
  allowCustomScopes: true,
  allowBreakingChanges: ['feat', 'fix'],
  // skip any questions you want
  skipQuestions: ['scope', 'body', 'breaking'],
  // limit subject length
  subjectLimit: 100,
  // breaklineChar: '|', // It is supported for fields body and footer.
  // footerPrefix : 'ISSUES CLOSED:'
  // askForBreakingChangeFirst : true, // default is false
}
```

创建完`.cz-config.js`文件后，我们需要回到 package.json 文件中，将 config.commitizen.path 更改为`"node_modules/cz-customizable"`,如果你的`.cz-config.js`文件在项目根目录下，那么可以不配置下面这条，commitlint-config-cz 会自动在项目根目录下寻找: `.cz-config.js`或 `.config/cz-config.js`

```json
...
{
  "config": {
    "commitizen": {
      "path": "node_modules/cz-customizable"
    },
    // 如果你的.cz-config.js文件在项目根目录下，那么可以不配置下面这条，commitlint-config-cz会自动在项目根目录下寻找: .cz-config.js 或 .config/cz-config.js
    "cz-customizable": {
      "config": "你的文件路径/xxxconfig.js"
    }
  }
}
...
```

关于`commitlint-config-cz`更高级的用法可以查看 👉[commitlint-config-cz](https://github.com/whizark/commitlint-config-cz)

最后我们将之前创建过的`commitlint.config.js`中的代码进行更改 👇

```diff
module.exports = {
- extends: ["@commitlint/config-conventional"],
+ extends: ["cz"],
};
```

或者你也可以在`commitlint.config.js`中手动添加自定义的规则 👇,他将覆盖 extends 中的规则

```javascript
module.exports = {
  extends: ["@commitlint/config-conventional"，"cz"],
  rules: {
    "type-enum": [
      2,
      "always",
      [
        "init",
        "build",
        "ci",
        "chore",
        "docs",
        "feat",
        "fix",
        "perf",
        "refactor",
        "revert",
        "style",
        "test",
      ],
    ],
  },
};
```

到这里，自定义的 commit message 的校验也 ok 了 ✅



## 最后

提醒：项目的代码风格和规则要和团队一起制定哦 ~  
至此，在团队协同的项目中，不符合规范的提交就被扼杀在摇篮里面了。我们大家不管是从书写代码还是提交代码最好都要规范哦~不给自己惹麻烦的同时，也不会给他人或公司带来麻烦。这就是本篇的全部内容啦~如果对你有帮助，记得点赞鼓励 ~  
我是荣顶，一个面向快乐编程的前端开发 🥰 我们下次再见 ~

---

## File: posts/01-frontend/前端工程化/前端应该知道的npm知识点.md

---
title: 2022 年了,你总不能还只会 npm i 吧?
date: 2022-11-11
tags:
  - 前端工程化
categories:
  - 前端
---

> 在键帽与字符上横跳，于代码和程序中穿梭。一起面向快乐编程！

大家好，我是荣顶，马上金三银四啦 ~ 这次给大家带来一篇 npm 命令相关的文章。目的在于查缺补漏，提升效率。

npm 作为 node 包管理器，内置了非常多的命令供我们日常开发使用，记住以下列出的命令将会给日常开发带来非常大的便利性。

好了，话不多说，看代码 👇

## 给喜欢的包加星(类似 github 的 star)

其实我把加星就当作是收藏的操作，其实在 npm 中它就是收藏的作用，因为在官网中，我们并不能像 github 一样清楚的看到这个仓库有多少颗 star。

```sh
npm star [package-name]
```

取消收藏

```sh
npm unstar [package-name]
```

 查看收藏列表

```sh
npm stars
```



这些操作都会报错在你的账号下，所以很方便。不用担心本机操作后其他地方看不到。你只需要有一个 npm 账户就可以了，还没有的话，可以看本文第二节 👇 ，注册一个并登录吧。

## 登录 npm

首先你要有一个 npm 的账号，没有的在[👉npm 官网](https://www.npmjs.com/signup)注册一个就行了。

> **注意:在官网注册的账号，登陆时使用淘宝源会报错，要改回默认源**

推荐使用 nrm 或者 yrm 来快速切换镜像源,个人推荐使用 yrm,为什么呢?看下图



yrm 会同时将你的 npm 和 yarn 一起切换,并且 ls 后会有\*号标注当前正在使用的源,记得 nrm 以前也有的,现在不知道怎么了...



当然你可以手动 npm 切换为默认源

```sh
npm config set registry https://registry.npmjs.org
```

然后执行命令`npm login`,依次填写用户名、密码、邮箱即可。如果你在 npmjs 站点还设置了其他保护，就还需要输入一些验证码之类的等等...



```sh
npm adduser
# or
npm login
# login是adduser的一个别名
```



你可以通过以下命令查看当前 npm 的登陆人

```sh
npm whoami
```

## 查看某个包的文档

每次我们想在浏览器中打开某个包的文档，总习惯去搜索引擎搜索，其实一行命令就能解决的事情,不要再去百度搜索啦~

```sh
# 此命令会尝试猜测包文档 URL 的可能位置，一般没有自定义的话，就会打开包的github地址。
npm docs [package-name]
# or
npm home [package-name]
```



尝试以下命令，可以快速打开 lodash 的官方文档 👇

```sh
npm docs lodash
# or
npm home lodash
```

`npm docs`或者`npm home`命令在不接参数时，会在当前项目中，通过 `package.json` 文件中的`homepage`配置，来打开对应的地址。



如上图所示，其原理就是：当你要查看的项目中的 `package.json` 文件中，设置了`homepage`属性，通过`npm docs/home`就能打开对应的主页，没有设置`homepage`属性时，npm 会继续寻找其中的`repository`属性，这时候打开的就是项目在 github 中的托管地址 url 拼接“#readme” (例如： https://github.com/用户名/仓库名#readme)，如果你`repository`属性也没设置，那么就会打开 npm 官网中包的所在地址，（例如：https://www.npmjs.com/package/npm-limit）

当然，你也可以在`npm docs/home`后不接参数，这样就会直接打开当前项目的主页。

## 查看某个包的代码仓库

想看某个包的源码?还要跑到 github 去搜索?nonono,也是一行代码。

```sh
# 此命令尝试猜测包的存储库 URL 的可能位置
npm repo [package-name]
```

👆 它是根据项目中的 `package.json` 文件中，设置的`repository`属性，来打开对应的 url。

## 快速给一个包提 issues

```sh
npm bugs [package-name]
```

👆 它是根据项目中的 `package.json` 文件中，设置的`bugs`属性，来打开对应的 url。



## 查看某个包的详细信息

这个命令有什么用呢？可以很方便的看到指定包的详细信息，比如我们想找到包的作者以及 ta 的联系方式（交流学习）。

```sh
npm v [package-name]
# or
npm view [package-name]
npm info [package-name]
npm show [package-name]
```



## 查看某个包的所有历史版本

```sh
npm v [package-name] versions
```



## 本地开发的 npm 包如何调试

我们可以像往常一样将本地开发的 npm 包安装到全局或指定目录。

```sh
npm install . -g
```

```sh
# 在某个项目中安装本地包
npm install ../Path/xxPackageName
```

也可以做一个软链指向当前需要调试的项目(全局)

```sh
npm link
```

将调试包链接到当前项目中（先做一个软链指向当前需要调试的项目）

```sh
# 先在本地开发的 npm 包中执行👇
npm link
# 然后切换到你要安装本地调试包的项目中，执行👇，即可将本地包安装到项目依赖中
npm link <package-name>

# 项目中取消安装本地的调试包👇
npm unlink <package-name>
```

## npm 发布包

首先，你得在本地登录（不会的看文章最开头的登录方法 👆）

登录完成后，发布自己开发的工具包，只需简单的三步！

> **注意:使用淘宝源会报错，要改回默认源**

记得每次发布前，修改下版本号！

```sh
npm version [版本号]
```

然后当前目录执行`npm publish`就好了

```sh
npm publish
```

## 弃用包的相关操作

> 注意：强烈建议弃用包或包版本而不是取消发布它们，因为取消发布会从注册表中完全删除一个包，这意味着任何依赖它的人都将无法再使用它，而不会发出警告。

弃用整个包

```sh
npm deprecate package-name "弃用信息"
```

弃用包的单个版本

```sh
npm deprecate package-name@version "弃用信息"
```

取消弃用操作

```sh
# 将弃用消息改为空字符串即可
npm deprecate package-name ""

```

取消发布（危险操作）\*

取消发布整个包

```sh
npm unpublish [package-name] -f
```

取消发布包的指定版本

```sh
npm unpublish [package-name]@<version>
```

> 取消发布包后，以相同名称重新发布将被阻止 24 小时。如果您错误地取消发布了一个包，我们建议您以不同的名称再次发布，或者对于未发布的版本，增加版本号并再次发布。

## 一些好用的其他操作

包的重命名

```sh
# （重命名包的唯一方法是以新名称重新发布它）
```

查看当前项目中有哪些包过时了

```sh
npm outdated
```

查看本地全局环境的包有哪些过时了

```sh
npm outdated -g --depth=0
```



列出 node_modules 中的所有包

```sh
ls node_modules
# or
dir node_modules
```

审计项目中所有包的安全漏洞

```sh
npm audit
# 这个命令依赖 package-lock.json 文件,所以如果你用的是yarn需要使用下面的命令
yarn audit
```

执行后会列出有问题的包



| 风险等级 | 说明                    |
| -------- | ----------------------- |
| Critical | 需要立即解决的!         |
| High     | 需要尽快解决!           |
| Moderate | 在时间允许的情况下解决  |
| Low      | 随便你,不慌不燥不急不忙 |

报告中会给出它问题的严重性,你就可以根据具体情况来进行版本更新或是调整。

```sh
npm token list
```



检测一下当前镜像源的延迟

```sh
npm ping
```



检测当前 node 和 npm 存在的问题 👇

```sh
npm doctor
```

---

## File: posts/01-frontend/前端工程化/如何引用外部cdn资源.md

---
title: 如何引用外部cdn资源
date: 2019-01-01
tags:
  - vue
  - cdn
categories:
  - 前端
---

> 在使用 vue-cli 进行项目构建时,有时是需要引用外部 cdn 资源的,生产环境中可以直接在 index.html 的头部引用,但是编译时会报错,因为脚手架无法识别这个外部资源.下面以引用腾讯地图 api 为例,介绍如何正确引用外部 cdn 资源.



## 引用

```html
<script src="http://map.qq.com/api/js?v=2.exp&key=yourkey"></script>
```

## 修改

```
// 因为腾讯地图的api都放在qq变量下输出的,所以配置的value应该是'qq'
module.exports = {
  externals: {
    'qq': 'qq'
  }
}
```

## 使用

```
import qq from 'qq'
```

说明:其实第一步的引入并不是唯一的办法,还可以通过 js 创建 script 的方式,我认为上面的方法是最简洁的.

---

## File: posts/01-frontend/前端工程化/解决 webpack 打包慢.md

---
title: 解决 webpack 打包慢
date: 2020-07-26
tags:
  - webpack
categories:
  - 前端
---

# 解决 webpack 打包慢

pacs 系统原先打包速度在 20s 左右，但是加上影像之后，打包速度就到了 8min 以上。

## 一、查找问题

开始的时候，认为是 vue-cli 配置问题导致的，使用`speed-measure-webpack-plugin` 插件，发现并没有哪个 plugin 和 loader 耗时太长，但是总耗时就是很长。

使用控制变量法，每次删除部分影像文件的引入，最终发现是 npm 引入的`cornerstoneWADOImageLoader` 为打包后的 min.js 文件，体积超过 2M，造成的打包速度太慢。

## 二、解决方案

### 1.使用 cdn 引入

尝试使用华为云 obs 当 cdn，但是不能在请求返回头中带上 gzip 标记。所以放弃了。

### 2\. 放在 pubilc 里，不走 webpack 编译

文件放在 public 中，然后在 index.html 中引入



修改 eslint,设置为全局变量



## 三、进一步加快速度

由于影像还有一些数百 k 的大文件，所以打包速度还是有些慢，所以我们使用了`HardSourceWebpackPlugin` 插件增加缓存，大幅增加了二次打包速度。

---

## File: posts/01-frontend/客户端/制作兼容 xp 的 exe.md

---
title: 制作兼容 xp 的 exe
date: 2021-03-02
tags:
  - PC客户端
categories:
  - 前端
---

# 制作兼容 xp 的 exe

由于医院部分设备限制，我们需要兼容 xp 系统。

那么我们有两个方案：

## 方案一：使用 chrome 浏览器

chrome 能兼容 xp 的版本在 50 以下，但是经过我虚拟机安装测试发现 46 能正常运行。

然后在属性中设置--kiosk 就能全屏显示

如果是 https 的网址，由于 xp 没有最新的证书，所以会提示不安全，可以加上

--ignore-certificate-errors



"C:\\Documents and Settings\\cf\\Local Settings\\Application Data\\Chromium\\Application\\chrome.exe" --ignore-certificate-errors --kiosk https://my-network.com

## 方案二：使用 nwjs 打包

如果需要兼容到 xp，则只能使用 0.14.7 版本,官方下载地址在这里，不过淘宝镜像下载更快些。

> 注：nwjs-sdk-v0.14.7-win-ia32.zip 才能打开控制台调试

所以打包使用`nwjs-v0.14.7-win-ia32` ，开发使用`nwjs-sdk-v0.14.7-win-ia32.zip`

如果只是配置网址，那么只需要解压后 package.json，修改 main 为网址，然后打包即可。

打包软件有用`Enigma Virtual Box` 和 `FilePacker` 的，FilePacker 我试了下不是很好用。我推荐 Inno setup, xp 兼容的最高版本为 5.5.9, 下载地址在这里。

然后在 xp 内打包即可，详细教程见 这里

这个打包有一个问题，就是如果打包目录里有文件夹，则需要修改打包配置文件，如下



否则会这个错误



### 1\. Inno setup 配置中文

Inno setup 默认是使用的英文，官网上也有非正式的中文文件下载，地址在这里

把它复制到 txt 中，然后修改文件名为`ChineseSimplified.isl`

存到 Inno setup 目录的`languages` 中

然后完成设置脚本引导 👇 疯狂下一步就行



完成后就出现了 👇



在安装配置文件 iss 修改 languages ，就可以了



好了之后保存-->随便取个名字



**注：这样配置后，在选择语言那一块还是会乱码，修改前面下载的中文文件**`ChineseSimplified.isl`**就好了**



### 2\. 配置开机自启

有两种方式，一种是不修改注册表，避免了杀毒软件提示，其实就是将快捷方式添加到“启动”文件夹。

```
\[Tasks\]Name: "startupicon"; Description: "开机启动"; GroupDescription: "{cm:AdditionalIcons}"; OnlyBelowVersion: 0,6.1\[Icons\]Name: "{commonstartup}\\{#MyAppName}"; Filename: "{app}\\{#MyAppExeName}"; Tasks: startupicon
```

另一种是修改注册表

```
\[Registry\] Root: HKLM; Subkey: "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run"; ValueType: string; ValueName: "testrun"; ValueData: "{app}\\{#MyAppExeName}"
```

### 3.修改图标

修改安装文件的图标，只需要在 Inno setup 向导里导入就可以了

修改底部任务栏显示的图标，在`package.json` 中修改即可



但是这个不能修改到桌面图标，这个时候我们需要使用`Resource Hacker` 来修改，下载地址在这里

我们需要先将图片转为 ico 文件，这个可以使用在线制作 favicon 工具

然后使用`Resource Hacker`打开 nw.exe,







替换后保存即可

## xp 内的局限性

1.  xp 内语音播报只有英文，无法进行中文的播报

2.  es6 语法支持比较差，需要使用 webpack 打包后使用

\*\* 另外需要在 xp 环境测试需要虚拟机,我后面会再出一个虚拟机相关的文章

---

## File: posts/01-frontend/小程序/小程序中如何用canvas画出圆角的矩形.md

---
title: 小程序中如何用canvas画出圆角的矩形
date: 2020-05-11
tags:
  - canvas
  - 小程序
categories:
  - 前端
---

\> \*\*\*`"曲线救国"`

\> 画出一个封闭的图形后调用 stroke 或 fill 方法，默认 stroke 或 fill 的颜色是 black

\> 如果这个矩形选区只是 canvas 画布的一部分，为了避免对后续的影响，最好在 beginPath 之前，将之前的动作 save ，然后画完后再 restore

```javascript

   /**

     \* 绘制圆角矩形

     \* @param {*} x 起始点x坐标

     \* @param {*} y 起始点y坐标

     \* @param {*} w 矩形宽

     \* @param {*} h 矩形高

     \* @param {*} r 圆角半径

     \* @param {*} ctx 画板上下文

     */

    darwRoundRect(x, y, w, h, r, ctx) {

        ctx.save();

        ctx.beginPath();



        // 左上弧线

        ctx.arc(x + r, y + r, r, 1 * Math.PI, 1.5 * Math.PI);

        // 左直线

        ctx.moveTo(x, y + r);

        ctx.lineTo(x, y + h - r);

        // 左下弧线

        ctx.arc(x + r, y + h - r, r, 0.5 * Math.PI, 1 * Math.PI);

        // 下直线

        ctx.lineTo(x + r, y + h);

        ctx.lineTo(x + w - r, y + h);

        // 右下弧线

        ctx.arc(x + w - r, y + h - r, r, 0 * Math.PI, 0.5 * Math.PI);

        // 右直线

        ctx.lineTo(x + w, y + h - r);

        ctx.lineTo(x + w, y + r);

        // 右上弧线

        ctx.arc(x + w - r, y + r, r, 1.5 * Math.PI, 2 * Math.PI);

        // 上直线

        ctx.lineTo(x + w - r, y);

        ctx.lineTo(x + r, y);



        ctx.setShadow(0, 0, 10, '#c1c1c1', '#c3c3c3');

        ctx.setFillStyle('white');

        ctx.fill();

        ctx.restore();

    }

```

**最后:希望大家补充其它相关方法,分享是每一位程序员都要有的美德~**

转载请注明出处!谢谢~ by:汪荣顶

---

## File: posts/01-frontend/小程序/小程序中当有异步操作或者耗时同步操作时，不能保证app.js中的onLaunch与page.js中onLoad先后执行顺序的解决方案.md

---
title: 微信小程序中当有异步操作或者耗时同步操作时，不能保证app.js中的onLaunch与page.js中onLoad先后执行顺序的解决方案
date: 2020-07-23
tags:
  - 小程序
categories:
  - 前端
---

# 微信小程序中当有异步操作或者耗时同步操作时，不能保证 app.js 中的 onLaunch 与 page.js 中 onLoad 先后执行顺序的解决方案

> 我在此为大家汇总了三种解决方案,大家普遍都会用这里的前两种方法,个人认为我的第三种方法更加巧妙且方便

## 方法一:

> **解决方法：定义回调函数** 小程序网络请求默认为异步请求，在 appjs 的 onLaunch 运行后进行异步请求时，程序不会停止，Page 页已执行 onload, 我们希望 onLaunch 执行完后再执行 onload。 目标: onLaunch 请求完 －> 再执行 Page 的 onLoad 方法缺点:每个有可能的页面都要添加相关回调逻辑

```javascript
//app.js

App({
  onLaunch: function () {
    wx.request({
      url: 'http://test.cn/login', //仅为示例，并非真实的接口地址
      data: {},
      success: function (res) {
        this.globalData.checkLogin = true
        //由于这里是网络请求，可能会在 Page.onLoad 之后才返回
        // 所以此处加入 callback 以防止这种情况
        if (this.checkLoginReadyCallback) {
          this.checkLoginReadyCallback(res)
        }
      },
    })
  },
  globalData: {
    checkLogin: false,
  },
})

//index.js
//获取应用实例
const app = getApp()

Page({
  data: {
    test: false,
  },
  onLoad: function () {
    let that = this
    //判断onLaunch是否执行完毕
    if (app.globalData.checkLogin) {
      that.setData({
        test: true,
      })
    } else {
      app.checkLoginReadyCallback = (res) => {
        that.setData({
          test: true,
        })
      }
    }
  },
})
```

执行顺序：[App] onLaunch -> [Page] onLoad -> [App] onLaunch sucess callback-> [Page] onLoad

## 方法二:

> **解决方法：将 onLaunch 中的代码片段拿出来 用 promise 重构一下** > [promise 的 NPM 地址](https://www.npmjs.com/package/es6-promise) > [promiseCDN](https://cdn.jsdelivr.net/npm/es6-promise/dist/es6-promise.min.js) 将压缩版本的源码直接复制出来 在小程序中创建一个 promise.js 的文件 直接粘贴进去 然后在 app.js 中引用 `const Promise = require('utils/promise.js');` 缺点:每个有可能的页面都要添加相关逻辑

```javascript

//app.js:
//将你本来写在 onLaunch 中的代码片段拿出来 重新写一个方法*
onLaunch:function(){
	login:function(){
    let that = this;
    return new Promise(function (resolve, reject) {
      // 登录
      wx.login({
        success: res => {
          // 发送 res.code 到后台换取 openId, sessionKey, unionId
          if (res.code) {
            // console.log('获取用户登录凭证：' + code);
            // ------ 发送凭证 ------
            /*
            * 通过code获取登录session
            */
            wx.request({
              url: 'xxx/login?code=' + res.code,
              method: 'GET',
              header: {
                'content-type': 'application/json'
              },
              success: function (res) {
                console.log(res)
                if (res.statusCode == 200) {
                  // 注意这里
                  resolve(res);
                } else {
                  console.log(res.errMsg)
                }
              },
            })
          } else {
            console.log('获取用户登录失败：' + res.errMsg);
          }
        }
      })
    })
  }
}


//page.js

onLoad: function (options) {
    let _this = this;
    app.login().then(function (res) {
      let newDate = new Date();
      // 修改到当前的时间
      _this.changeNowTime();
    })
},

```

## 方法三:

> **解决方法：通过 getCurrentPages()[0].onLoad()重新执行一次 page.js 中的 onLoad 方法** 不用每个页面都写相关逻辑,只需要在 app.js 中加入以下几行相关代码即可 **缺点:page.js 中的 onload 方法会执行两次!**

```javascript
//app.js
 onLaunch: function (info) {
        //当进入小程序的场景为通过分享进入时,预防onLoad钩子请求不到数据
        if (info.path !== 'xxxxx/xxx/xxx') {
            console.log('info', info);
            setTimeout(() => {
                console.log('getCurrentPages', getCurrentPages());
                getCurrentPages()[0].onLoad(info.query);
            }, 1000);
        }
        // 登录
        wx.login({
            success: (xxx) => {
               	//xxxxxx
            },
        });
    }
```

## 最后:希望大家补充其它相关方法,分享是每一位程序员都要有的美德~

转载请注明出处!谢谢~ by:汪荣顶

---

## File: posts/01-frontend/小程序/小程序中非常好用的富文本插件wxParse.md

---
title: 小程序非常好用的富文本插件wxParse
date: 2020-08-15
tags:
  - 小程序
categories:
  - 前端
---

> 最近在小程序的开发过程中遇到这样的需求，一个搜索联想关键词高亮，另一个是将后台传过来的富文本解析成 html 展示在页面中，这里我们引入非常牛 X 的 `wxParse` ，之所以牛，是因为可以解决很多种问题，正因为如此所以它对于小程序来说也非常大，虽然只有 100 多 kb。



## 地址

[https://github.com/icindy/wxParse](https://github.com/icindy/wxParse)

##  使用

### 单行文本解析

#### wxml

```html
<import src="../../../utils/wxParse/wxParse.wxml" />

<template is="wxParse" data="{{wxParseData:article.nodes}}" />
```

#### wxss

```css
@import '../../../utils/wxParse/wxParse.wxss';
```

#### javascript

```javascript
let WxParse = require('../../../utils/wxParse/wxParse.js')

Page({
  data: {
    wxParseData: '',
  },
  toHtml() {
    let article = '<div>123 <span>456</span></div>'
    WxParse.wxParse('article', 'html', article, this, 5)
  },
})
```

### 数组文本解析

#### wxml

```html
<import src="../../../utils/wxParse/wxParse.wxml" />

<view class="list_for list_item" wx:key="{{index}}" wx:for="{{replyTemArray}}">
  <view class="list-item" data-index="{{index}}" bindtap="clickListItem">
    <template class="list_item" is="wxParse" data="{{wxParseData:item}}" />
  </view>
</view>
```

```css
@import '../../../utils/wxParse/wxParse.wxss';
```

#### javascript

```javascript
let WxParse = require('../../../utils/wxParse/wxParse.js')

Page({
  data: {
    replyTemArray: [],
  },
  // 搜索联想，关键词高亮
  keywordAssociation(keyword) {
    let _this = this

    getSearchList(keyword).then((res) => {
      _this.setData({
        searchList: res,
      })
      let dataArr = res
      for (let i = 0; i < dataArr.length; i++) {
        WxParse.wxParse('reply' + i, 'html', _this.highLight(dataArr[i].name, keyword), _this)
        if (i === dataArr.length - 1) {
          WxParse.wxParseTemArray('replyTemArray', 'reply', dataArr.length, _this)
        }
      }
    })
  },
})
```

---

## File: posts/01-frontend/小程序/小程序使用canvas绘制多行文本的方案.md

[Binary file]

---

## File: posts/01-frontend/小程序/小程序在“页面B”更改title，返回“页面A”仍会显示“页面B”的title.md

---
title: 小程序在“页面B”更改title，返回“页面A”仍会显示“页面B”的title
date: 2020-08-11
tags:
  - 小程序
categories:
  - 前端
---

> 最近在做小程序的时候遇到这么一个有趣的问题，就是从 A 页面跳转到 B 页面，如果 B 页面需要在接口加载之后更改 title，并且如果接口请求过慢，还灭有请求完毕时就点击返回按钮回到 A 页面，这时 B 页面的请求会继续执行，并在执行完毕后修改了 A 页面的 title。



## 原因

个人猜想，小程序本身属于单页面应用，代码的运行权限没有处理好，就像上面这种情况，B 页面的代码穿越到了 A 页面的环境里面，并且执行。

## 解决

1. 既然 B 页面的请求方法可以在 A 页面执行，那么在 B 页面的方法中获取的当前的页面路由时就有可能可以获取到 A 页面的路由，经过测试果然不出所料。
2. 所以，可以这样来解决这个问题：在 B 页面的 onLoad 中获取当前页面路径，应该是`/b`，然后在方法中再次获取当前路径，可能是`/b`，也可能是`/a`，若是`/a`，则是上面提到问题情况，这个时候就不用取改变 title 了，若是`/b`，则可以更改。

## 代码

```javascript
// utils.js
const getCurrentRouter = function () {
  let arr = getCurrentPages(),
      len = arr.length;
  return arr[len - 1].route;
}

export {getCurrentRouter};


// b.js
import {getCurrentRouter};
import { getDataForB } from '../../../servies/services.js';

Page({
  onLoad (options) {
    this.currentRouter = getCurrentRouter();
  },
  // 获取数据
  getData () {
    getDataForB().then(() => {
      // 获取数据后判断router是否一致
      if (this.currentRouter == getCurrentRouter()) {
        wx.setNavigationBarTitle({
            title: '要修改的title'
        })
      }
    })
  }
})
```